require('@jess/package'): { open, openJpk, pack, resolve, manifest, satisfies }A built-in module, typed as Module__jess_package. It is the package model Packages describes, as functions: open a package from either of its forms, pack one, and resolve layers over several into a plan. The container underneath is @jess/jpk.
Nothing here runs package code. Opening reads jess.toml and the payload's bytes, and resolving reads manifests. A package's modules run when something requires them, after a host has activated a plan, which this module never does.
package.open§
package.open(location: string) -> JessPackageOpens the package at location: a Package Source folder (the folder holding jess.toml), or a .jpk file. Either way the result is the same shape:
| Field | Is |
|---|---|
manifest | The checked manifest, as a table: id, version, jessApi, include, tests, dependencies, modules, layers. |
id, version | The manifest's. |
digest | The content digest, hex. |
kind | 'source' or 'jpk'. Ordinary code has no business branching on it. |
location | Where it was opened from. |
files | The payload, in canonical order: jess.toml first, then every other path bytewise. |
read(path) | A payload file's bytes, or nil for a path that isn't in the payload. |
Opening checks the whole package before it returns. A Package Source must not hold symlinks, files that are neither file nor folder, or two paths that differ only in case (Name.jsl and name.jsl are one file on Windows and a fresh bug everywhere else). A JPK must hold nothing outside its manifest's include. Every module the manifest exports must be in the payload, and a Package Source must hold every test its manifest names.
--!strict
local package = require '@jess/package'
local greeter = package.open 'greeter'
print(greeter.id, greeter.version, greeter.kind)
print(greeter.digest)
for _, path in greeter.files do
print(path)
end
com.example.greeter 1.0.0 source
6fdd1a56f49cf9b23f2f9765b0cfce84836ba001cf57a602aa5ea47f7768e35b
jess.toml
src/init.jsl
src/words.jsl
tests/run.jsl
package.openJpk§
package.openJpk(source: JessJpkSource, label: string) -> JessPackageOpens a JPK that isn't a file of its own: one in memory, or stored inside another archive, from source, { size, read }, the same source @jess/jpk reads. It is the same package as the file would be, with the same digest and the same checks, label standing in for where it came from in location and in every message. A packed workspace opens its packages this way, in place.
--!strict
local package, fs = require '@jess/package', require '@dream/fs'
local bytes, problem = fs.readFile 'words.jpk'
assert(bytes, problem)
local held = package.openJpk({
size = buffer.len(bytes),
read = function(offset: number, length: number): buffer
local slice = buffer.create(length)
buffer.copy(slice, 0, bytes, offset, length)
return slice
end,
}, 'words.jpk, in memory')
print(held.id, held.kind, held.location)
print(held.digest == package.open('words.jpk').digest)
org.example.words jpk words.jpk, in memory
true
package.pack§
package.pack(source: string, output: string, options: { version: string? }?) -> JessPackResultPacks the Package Source at source into the canonical JPK at output, written through a temporary file and a rename. Returns { path, version, digest, files, size }. Only the payload goes in: jess.toml and what include names, which holds the tests the manifest names. Benchmarks, .git, and an editor's or a merge tool's leftovers (*.bak*, *~, *.orig) stay home. jess pack is this function.
With options.version, the payload is packed at that version: the one version line of jess.toml's [package] table is revised, every other byte stays, and the manifest, the id, the digest, and the files all read as revised. That is how a development build is made: the same files as the source, at 1.2.1-dev.57, with a digest of its own, because its manifest differs. The source on disk is untouched.
package.resolve§
package.resolve(packages: { JessPackage }, request: JessPackageRequest) -> JessActivationPlanResolves request.layers over packages into an activation plan, deterministically, or fails naming exactly what's wrong. Every rule is checked before a plan exists:
- one provider per package id: the same package offered twice is an error, even as its source and its JPK (
com.example.greeter is offered twice (...); choose one); - one provider per layer id;
- every requested layer, and every layer they require, has a provider (
example.tools requires example.core: no provider found for example.core); - no layer cycles (
layer cycle: cycle.a -> cycle.b -> cycle.a); - every package dependency of a selected package is offered and satisfies its requirement, with the chain that asked for it;
- no module name is exported by two selected packages, or by a package and the host (
request.hostModules):@dream/fsis L3i's, and a package exporting it is an error (<id> exports '@dream/fs', which the host already provides), not a shadow; - with
request.grant, every capability the resolved layers' entrypoints require is in it; otherwise the plan fails before any code runs:
cannot activate example.tools
required capability denied:
filesystem.read
requested by:
org.example.alpha 1.2.3
layer example.tools
entrypoint @alpha/tools/register
Besides the packages handed over, a request may offer candidates, packages read only as far as their manifests and opened only when a layer or a dependency needs one, never in place of a package handed over and never chosen between. A candidate is opened with package.open by its location, or by request.open when the request names one: a resolution whose candidates aren't files of their own, such as a packed workspace's, names what opens them, and the rules are the same either way. request.pins holds packages with given ids to given digests.
The plan is { requested, layers, packages, modules, entrypoints, capabilities }:
layers, every resolved layer, each after the layers it requires, with its provider package and version, its exports, and its entrypoints;packages, the providers and their package dependencies, with each one's digest, kind, and location;modules, the module namespace: every export of every layer of the selected packages, with its package and path. Exports are inert, so a dependency's whole public API is resolvable without activating any of its layers;entrypoints, the resolved layers' entrypoints in the order activation runs them, with their package, path, and layer;capabilities, every capability those entrypoints require, with the entrypoint, layer, and package asking.
jess package graph prints this table. Activating it is the host's job: jess run --package does it, and a Rust host calls jess::lua::package::activate. Activation mounts the plan, so require reaches the namespace, then runs the entrypoints, each once, in order. An entrypoint that fails stops the activation with its error; what ran before it stays done, because activation isn't a transaction.
package.manifest§
package.manifest(text: string) -> JessPackageManifestParses and checks a jess.toml, with the same errors open gives. An unknown key is an error, not a field quietly ignored.
package.satisfies§
package.satisfies(version: string, requirement: string) -> booleanWhether version meets requirement. Requirements are Cargo's caret ranges and exact pins: ^1.2 (and 1.2) is at least 1.2.0 and below 2.0.0, ^0.2 stays below 0.3.0, ^0.0.3 is exactly 0.0.3, and =1.2.3 is exactly that. A version is major.minor.patch, optionally with a pre-release after a hyphen: dot-separated identifiers of ASCII letters, digits and hyphens, numeric ones without a leading zero, as 1.2.1-dev.57. A pre-release precedes its release, and pre-releases order identifier by identifier, numbers numerically and below words, words bytewise, a shorter list first. A pre-release meets only the exact requirement that names it, =1.2.1-dev.57: no range ever picks one, so a development build is used by whoever asks for it and nobody else.
package.requirement§
package.requirement(requirement: unknown, what: string) -> stringrequirement when it is a version requirement as satisfies reads one, and otherwise an error that starts with what: the same check a manifest's [dependencies] get, for a file that states requirements of its own, as jess-workspace.toml does.
The content digest§
A package's identity is its id and version. Its content is the digest: BLAKE3 over the files, framed so no two different packages can hash the same.
- The domain tag
jess-package-digest-v1and a NUL byte. - For every payload file in canonical order (
jess.tomlfirst, then the rest bytewise): the path's length as a little-endian u64, the path, the file's size as a little-endian u64, and the file's own BLAKE3.
The ZIP container is nowhere in it. A Package Source and the JPK packed from it have the same digest, however the JPK was compressed, which is the point: same digest, same package. Edit one byte of the source and the digest changes, though the version doesn't. A version string is a promise. The digest is evidence.