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:
import ./lib.vibe { f as renamed }renames on the way in, for when
two modules disagree about a good name.
import ./subdir { helper }imports a directory, which resolves to
its index.vibe.
import @vibe/core { struct MutMap, trait Default }names the
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.