11 — Modules and packages

Previous: Option and the railway

日本語版: 11_modules_packages.vibe.md

One file is one module. Nothing in it is visible to another file unless you write export, and nothing arrives in your file unless you write import. Those two words are most of what you need.

Two files

Here is a real one. support/mathx.vibe contains an exported function:

export fn triple(x: Int) -> Int {
  x * 3
}

and this chapter imports it by relative path, naming what it wants:

import ./support/mathx.vibe {
  triple
}

fn main with Console {
  println("triple(14) = \{triple(14)}")
}
triple(14) = 42

Variations on the import line:

two modules disagree about a good name.

its index.vibe.

declaration kind. Values stay bare; for a type, struct, enum, effect or trait the qualifier is exact — asking for struct Color when Color is an enum is rejected — and bare non-value imports are accepted today only for compatibility.

A relative import may climb above the entry file's own directory — import ../../../helper.vibe from a nested entry resolves. What bounds it is not the entry's directory but what the host made visible to the compiler: the preopened directory it was given. A path outside that is not found, whatever the ../ count.

Crossing into a package is a different boundary again: a directory with an index.vpkg publishes that contract, so a name it does not export is rejected at check time rather than silently imported.

Packages by name

An import starting with @ names a package rather than a path:

import @vibe/core {
  hex_encode, sha1
}

fn main with Console {
  println("length(sha1(\"vibe\")) = \{String::length(sha1("vibe"))}")
  println("hex_encode(\"hi\") = \{hex_encode("hi")}")
}
length(sha1("vibe")) = 40
hex_encode("hi") = 6869

The name is looked for in three places, in order: the project's pinned store, the workspace's own lib/, and the installed standard library. The first hit wins, so a local copy shadows the installed one while you are working on it.

The contract file

A package does not export whatever its files happen to export. It declares a public API in index.vpkg — a contract of bodyless declarations — and the compiler checks the implementation against it. If they disagree, that is a compile error, not a surprise for a consumer.

name = @you/counter
version = 1.0.0
description =
  #|A tiny counter contract
deps = {}

generated_hash =

type Counter
fn add(x: Int, y: Int) -> Int

type Counter with no definition means consumers get the name but not the representation, so you can change it later. The header above the declarations is not vibe syntax — it is package metadata, and docs/adding-modules.md is the reference for it.

The contract is the consumer-facing boundary: it says what other packages may reach. Inside the package it does not get in your way — two files with the same nearest index.vpkg can share a helper by exporting it and importing it directly, with no entry in the contract:

pkg/index.vpkg     fn package_value() -> Int          <- only this is published
pkg/impl.vibe      import ./_helper.vibe { private_value }
pkg/_helper.vibe   export fn private_value() -> Int { 41 }

That compiles. So an internal helper stays internal — putting it in the contract would be the thing that makes it public API, not the thing that lets a sibling use it.

Reproducible builds

When you depend on someone else's package, the version number is not what gets verified — the content hash is. The pin is a require line in the metadata header at the top of your package's index.vpkg, above the declarations:

require @vibe/core 0.2.0 = #pkg:sha1:<40hex>

The build re-checks that hash offline on every build, so neither the registry nor the network has to be trusted between builds. vibe hash computes the value. Set VIBE_REQUIRE_PINS=1 and an unpinned dependency becomes an error, which is what a release build should do.

(vibe.deps from chapter 1 is a different, coarser mechanism: it lists <name> <url> lines for vibe add / vibe fetch to vendor whole repositories into deps/. The require pin is the registry lane — per-package, content-addressed, checked by the compiler itself.)

Publishing

When you are ready to hand a package to someone else:

vibe pkg publish lib/@you/pkg     # version check, then append to the log
vibe pkg install @you/pkg@1.0.0   # fetch and verify against the log
vibe pkg add github:owner/repo/dir@ref
vibe pkg yank @you/pkg@1.0.0      # withdraw a version
vibe pkg update @you/pkg          # move to the newest, showing the contract diff

Publish and yank append to a transparency log, and install verifies its proof — so a version cannot be swapped underneath you after the fact. docs/registry-design.md has the design.

Next: Writing tests.