char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somjsr: read cache before taking artifact lockbce30ee

main
8.9 KiB176 linesraw

JSR packages

avril supports native jsr: imports, without npm compatibility tarballs or node_modules:

import { assertEquals } from "jsr:@std/assert@^1";

Packages belong in the deps of avril.conf:

deps: [
  'jsr:@std/assert@^1.0.19',
  {from: 'jsr:@std/path@^1.1.6', name: 'path'},
],

A dependency binds its package name, or the name given, together with its subpaths: here @std/assert and @std/assert/equals select the package's . and ./equals exports, and path/posix selects @std/path's ./posix. The same package can be listed twice with different ranges if one entry is renamed. imports can still map an alias to a single export, or a slash-terminated alias to the exports beneath a prefix:

imports: {
  posix: 'jsr:@std/path@^1/posix',
  'std-path/': 'jsr:@std/path@^1/',
},

Only declared public exports can be selected through jsr:. Published modules resolve their relative imports within their own package and version and their external dependencies through explicit jsr: references. They never inherit the application's aliases. Arbitrary HTTP imports, CommonJS, JSX, declaration-file execution, and runtime JSON imports are not supported. Npm packages use npm: specifiers instead; see Npm packages. TypeScript is transpiled, not type-checked; erased type-only dependencies do not need to exist at runtime. JSR does not guarantee runtime portability: packages that require Deno.*, Node APIs, or JSON imports will not work. Pure ECMAScript and supported Web API packages, such as @std/assert, are suitable candidates.

Preparation and identity

Before execution, the static graph is acquired and TypeScript is transformed. V8 linking reads prepared source and never performs network I/O. Computed dynamic imports asynchronously acquire the requested static closure through the host event loop, then link and evaluate it. Concurrent acquisition shares downloads, and modules share the same V8 identity regardless of their import spelling.

Local identity is a canonical file path. Registry identity is the canonical, versioned registry source URL, not the cache path. import.meta.url and mapped TypeScript stacks expose that URL. Non-canonical percent-encoded aliases are rejected. Relative registry imports cannot cross package or version boundaries. Registry graph metadata is an optional, bounded prefetch hint; source parsing remains authoritative.

Lockfile and cache

The root package's avril.lock records version selections for the execution graph and SHA-256 integrity for immutable version metadata. Manifests in that metadata cover each source's SHA-256 and size. Requests with different constraints may use different versions; a locked constraint keeps its selection until explicitly updated. New selections skip yanked versions. Existing pins can keep yanked versions, and immutable integrity pins are never silently replaced by an update.

Selections and integrity are saved after successful preparation, including for computed dynamic imports. The lock is fJSON, the same format as avril.conf, and merges concurrent changes: an update is recomputed if another process replaced the file while it was being prepared, though a write landing in the instant before the rename can still be lost. A conflicting selection used by this preparation fails rather than overwriting another process's selection. Atomic writes preserve existing modes. avril.conf edits follow the same scheme. Commit avril.lock. Unchanged locks are not rewritten, so locked runs work in read-only project directories without requiring --frozen. Root selection uses trusted host-side discovery, independently of guest execution grants.

Cached artifacts are immutable and verified on use. Writes are atomic, and per-artifact file locks coordinate processes when an artifact must be acquired or refreshed; verified cached hits are read without taking those locks. The default cache is $AVRIL_CACHE_DIR, $XDG_CACHE_HOME/avril, or $HOME/.cache/avril. Use --cache-dir=PATH to override it. A corrupt locked artifact fails with an integrity error; remove that artifact or use an empty cache to reacquire it online.

--offline never contacts the registry and fails on cache misses. --frozen requires an existing lockfile, selections, and metadata integrity; it permits acquiring missing locked artifacts unless combined with --offline. --lock=PATH overrides the default root lock. update --offline reselects from cached indexes only and does not promise the registry's latest versions.

Permissions

Package acquisition is host tooling authority, separate from guest net, read, and write. It accesses only the configured registry, cache, and lockfile; it never follows HTTP redirects. Acquiring a package does not grant guest network access or access to the cache filesystem.

Remote code still needs execution authority. Default execution is unrestricted; with -0, explicitly grant URLs, for example:

avril run -0 --allow-js-exec=. --allow-js-exec=https://jsr.io/@std/ main.ts

A slash-terminated URL grant covers descendants; otherwise it grants one exact file. Denials cover the named path and descendants. URL paths are normalized before checking, so percent-encoding cannot bypass a denial. Network permission alone never grants remote execution. Prefer grants for individual packages and their transitive dependencies; scope and whole-registry grants let guest code request arbitrary package names within that grant, including names that can convey data to registry request logs even when guest net is denied. pledge({jsExec: false}) revokes both local and remote execution, and mixed URL/path allow lists narrow both domains.

Metadata acquisition checks whether some code in the requested package/version could be authorized; selecting an export and acquiring its source also check the exact module. Access is rechecked after asynchronous work and before cached modules are linked or evaluated. Revoking an already-evaluated dependency does not reexecute it when its consumer's existing namespace is reimported.

Commands

avril add jsr:@std/assert@^1 adds the package to deps in the nearest trusted avril.conf, or replaces the entry already binding @std/assert. A subpath target such as jsr:@std/path@^1/posix adds its whole package. avril add assert jsr:@std/assert@^1 binds the package as assert instead, while an alias for a subpath or slash prefix, such as avril add posix jsr:@std/path@^1/posix, goes in imports; slash-prefix aliases require slash-terminated targets. --config=PATH chooses a config explicitly. It edits source spans, preserving existing comments and other entries. It prints the resulting binding and config on stdout, preceded on stderr by whether it was added, updated, or unchanged.

A target without a version, such as jsr:@std/assert or jsr:@std/path/posix, gets a caret range of the package's newest non-yanked stable release, as in jsr:@std/assert@^1.0.13; --pre lets that release be a prerelease. After editing, add caches what the new entry can import, like avril cache, and records it in the config's avril.lock: every export of a dependency or slash prefix, or an alias's single export. --offline, --jsr-registry, and --cache-dir apply as for other commands.

avril lsp provides live type-resolution support; see editor integration.

avril run main.ts
avril cache main.ts
avril info main.ts
avril update main.ts
avril vendor --include=jsr:@my/lazy@^1 main.ts
avril run --offline --frozen --vendor=vendor main.ts

cache, info, update, and vendor prepare without executing application code. info emits the acquired graph as JSON. update explicitly reselects encountered constraints. update --prune additionally drops unused specifier and metadata pins; include all known dynamic entry points when pruning, and avoid concurrent lock-changing commands during this explicit cleanup. Ordinary runs never prune. vendor writes verified metadata and source into readable vendor/@scope/package/version/... paths. --out=PATH changes its output directory; --vendor=PATH supplies an existing vendor tree for reads.

These commands prepare static imports only. Computed dependencies cannot be inferred ahead of time; repeat --include=SPECIFIER to prepare known dynamic entry points too. For a private computed file, use for example --include-from=jsr:@my/pkg@^1 --include=./lazy.ts; the referrer must be a source module, and normal package confinement applies. --include-from=. resets the referrer to the entry. A vendored run still verifies metadata against the root lock and sources against their manifests. Previously unused dynamic files require additional cached/vendored artifacts.

The registry defaults to https://jsr.io/. --jsr-registry=URL selects another base; it must use HTTPS, except that HTTP is allowed for loopback fixture registries. Registry identity is part of the lock and cache keys, and changing registries requires a separate lockfile.

1# JSR packages
2
3avril supports native `jsr:` imports, without npm compatibility tarballs or
4`node_modules`:
5
6```ts
7import { assertEquals } from "jsr:@std/assert@^1";
8```
9
10Packages belong in the `deps` of `avril.conf`:
11
12```text
13deps: [
14  'jsr:@std/assert@^1.0.19',
15  {from: 'jsr:@std/path@^1.1.6', name: 'path'},
16],
17```
18
19A dependency binds its package name, or the `name` given, together with its
20subpaths: here `@std/assert` and `@std/assert/equals` select the package's `.` and
21`./equals` exports, and `path/posix` selects `@std/path`'s `./posix`. The same
22package can be listed twice with different ranges if one entry is renamed.
23`imports` can still map an alias to a single export, or a slash-terminated
24alias to the exports beneath a prefix:
25
26```text
27imports: {
28  posix: 'jsr:@std/path@^1/posix',
29  'std-path/': 'jsr:@std/path@^1/',
30},
31```
32Only declared public exports can be selected through `jsr:`. Published modules
33resolve their relative imports within their own package and version and their
34external dependencies through explicit `jsr:` references. They never inherit the
35application's aliases. Arbitrary HTTP imports, CommonJS, JSX, declaration-file
36execution, and runtime JSON imports are not supported. Npm packages use `npm:`
37specifiers instead; see [Npm packages](npm.md).
38TypeScript is transpiled, not type-checked; erased type-only dependencies do not
39need to exist at runtime. JSR does not guarantee runtime portability: packages
40that require `Deno.*`, Node APIs, or JSON imports will not work. Pure ECMAScript
41and supported Web API packages, such as `@std/assert`, are suitable candidates.
42
43## Preparation and identity
44
45Before execution, the static graph is acquired and TypeScript is transformed.
46V8 linking reads prepared source and never performs network I/O. Computed dynamic
47imports asynchronously acquire the requested static closure through the host
48event loop, then link and evaluate it. Concurrent acquisition shares downloads,
49and modules share the same V8 identity regardless of their import spelling.
50
51Local identity is a canonical file path. Registry identity is the canonical,
52versioned registry source URL, not the cache path. `import.meta.url` and mapped
53TypeScript stacks expose that URL. Non-canonical percent-encoded aliases are
54rejected. Relative registry imports cannot cross package or version boundaries.
55Registry graph metadata is an optional, bounded prefetch hint; source parsing
56remains authoritative.
57
58## Lockfile and cache
59
60The root package's `avril.lock` records version selections for the execution graph
61and SHA-256 integrity for immutable version metadata. Manifests in that metadata
62cover each source's SHA-256 and size. Requests with different constraints may use
63different versions; a locked constraint keeps its selection until explicitly
64updated. New selections skip yanked versions. Existing pins can keep yanked
65versions, and immutable integrity pins are never silently replaced by an update.
66
67Selections and integrity are saved after successful preparation, including for
68computed dynamic imports. The lock is [fJSON](fjson.md), the same format as
69`avril.conf`, and merges concurrent changes: an
70update is recomputed if another process replaced the file while it was being
71prepared, though a write landing in the instant before the rename can still be
72lost. A conflicting selection used by this preparation fails rather than
73overwriting another process's selection. Atomic writes preserve existing modes.
74`avril.conf` edits follow the same scheme. Commit `avril.lock`. Unchanged locks
75are not rewritten, so locked runs work in read-only project directories without
76requiring `--frozen`. Root selection uses
77trusted host-side discovery, independently of guest execution grants.
78
79Cached artifacts are immutable and verified on use. Writes are atomic, and
80per-artifact file locks coordinate processes when an artifact must be acquired
81or refreshed; verified cached hits are read without taking those locks. The
82default cache is `$AVRIL_CACHE_DIR`, `$XDG_CACHE_HOME/avril`, or
83`$HOME/.cache/avril`. Use
84`--cache-dir=PATH` to override it. A corrupt locked artifact fails with an integrity
85error; remove that artifact or use an empty cache to reacquire it online.
86
87`--offline` never contacts the registry and fails on cache misses. `--frozen`
88requires an existing lockfile, selections, and metadata integrity; it permits
89acquiring missing locked artifacts unless combined with `--offline`.
90`--lock=PATH` overrides the default root lock. `update --offline` reselects from
91cached indexes only and does not promise the registry's latest versions.
92
93## Permissions
94
95Package acquisition is host tooling authority, separate from guest `net`, `read`,
96and `write`. It accesses only the configured registry, cache, and lockfile; it
97never follows HTTP redirects. Acquiring a package does not grant guest network
98access or access to the cache filesystem.
99
100Remote code still needs execution authority. Default execution is unrestricted;
101with `-0`, explicitly grant URLs, for example:
102
103```sh
104avril run -0 --allow-js-exec=. --allow-js-exec=https://jsr.io/@std/ main.ts
105```
106
107A slash-terminated URL grant covers descendants; otherwise it grants one exact
108file. Denials cover the named path and descendants. URL paths are normalized
109before checking, so percent-encoding cannot bypass a denial. Network permission
110alone never grants remote execution. Prefer grants for individual packages and
111their transitive dependencies; scope and whole-registry grants let guest code
112request arbitrary package names within that grant, including names that can
113convey data to registry request logs even when guest `net` is denied.
114`pledge({jsExec: false})` revokes both local and remote execution, and mixed
115URL/path allow lists narrow both domains.
116
117Metadata acquisition checks whether some code in the requested package/version
118could be authorized; selecting an export and acquiring its source also check the
119exact module. Access is rechecked after asynchronous work and before cached
120modules are linked or evaluated. Revoking an already-evaluated dependency does
121not reexecute it when its consumer's existing namespace is reimported.
122
123## Commands
124
125`avril add jsr:@std/assert@^1` adds the package to `deps` in the nearest trusted
126`avril.conf`, or replaces the entry already binding `@std/assert`. A subpath target
127such as `jsr:@std/path@^1/posix` adds its whole package. `avril add assert
128jsr:@std/assert@^1` binds the package as `assert` instead, while an alias for a
129subpath or slash prefix, such as `avril add posix jsr:@std/path@^1/posix`, goes in
130`imports`; slash-prefix aliases require slash-terminated targets. `--config=PATH`
131chooses a config explicitly. It edits source spans, preserving existing comments
132and other entries. It prints the resulting binding and config on stdout, preceded
133on stderr by whether it was added, updated, or unchanged.
134
135A target without a version, such as `jsr:@std/assert` or `jsr:@std/path/posix`,
136gets a caret range of the package's newest non-yanked stable release, as in
137`jsr:@std/assert@^1.0.13`; `--pre` lets that release be a prerelease. After editing,
138`add` caches what the new entry can import, like `avril cache`, and records it in
139the config's `avril.lock`: every export of a dependency or slash prefix, or an
140alias's single export. `--offline`, `--jsr-registry`, and `--cache-dir` apply as for other commands.
141
142`avril lsp` provides live type-resolution support; see
143[editor integration](editor.md).
144
145```sh
146avril run main.ts
147avril cache main.ts
148avril info main.ts
149avril update main.ts
150avril vendor --include=jsr:@my/lazy@^1 main.ts
151avril run --offline --frozen --vendor=vendor main.ts
152```
153
154`cache`, `info`, `update`, and `vendor` prepare without executing application code.
155`info` emits the acquired graph as JSON. `update` explicitly reselects encountered
156constraints. `update --prune` additionally drops unused specifier and metadata
157pins; include all known dynamic entry points when pruning, and avoid concurrent
158lock-changing commands during this explicit cleanup. Ordinary runs never prune.
159`vendor` writes verified metadata and source into readable
160`vendor/@scope/package/version/...` paths. `--out=PATH` changes its output directory;
161`--vendor=PATH` supplies an existing vendor tree for reads.
162
163These commands prepare static imports only. Computed dependencies cannot be
164inferred ahead of time; repeat `--include=SPECIFIER` to prepare known dynamic
165entry points too. For a private computed file, use for example
166`--include-from=jsr:@my/pkg@^1 --include=./lazy.ts`; the referrer must be a source
167module, and normal package confinement applies. `--include-from=.` resets the
168referrer to the entry. A vendored run still verifies metadata against the root
169lock
170and sources against their manifests. Previously unused dynamic files require
171additional cached/vendored artifacts.
172
173The registry defaults to `https://jsr.io/`. `--jsr-registry=URL` selects another base;
174it must use HTTPS, except that HTTP is allowed for loopback fixture registries.
175Registry identity is part of the lock and cache keys, and changing registries
176requires a separate lockfile.