char/avril

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

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

Charlotte Somnpm: assume "module" package.json entries are es modules61e3521

main
10.3 KiB197 linesraw

npm packages

avril loads ESM and CommonJS packages directly from npm:

import { z } from 'npm:zod@^4';
console.log(z.number().parse(42));

Declare bare-name bindings in avril.conf:

deps: [
  'npm:zod@^4',
  {from: 'npm:preact@^10', name: 'ui'},
],

Requests accept scoped and unscoped names, npm version ranges, dist-tags, and export subpaths (npm:preact@^10/hooks). Published packages use their own manifest dependencies, never the application's import map. avril does not read node_modules or .npmrc, and never runs npm installation scripts. Local directory require reads a local package.json main only for directory lookup (no versioning or node_modules).

Type declarations

avril.conf can redirect package imports for checking without changing runtime imports, and can add explicit global declaration modules:

types: {
  imports: {
    'npm:legacy-sdk': 'npm:@types/legacy-sdk@^3',
  },
  globals: ['npm:@types/node@^22'],
},

types.imports uses the same exact-key, trailing-slash prefix, and null blocking semantics as the top-level imports map. Relative import specifiers are resolved from the importing module; relative mapping keys, targets, and types.globals entries are resolved from avril.conf. Globals must be URLs or relative file paths, not bare package names. A versionless npm: key can match the resolved versioned package import. These mappings affect type resolution only: they do not redirect execution or enable Node runtime APIs.

Explicit overrides take precedence over bundled declarations. Otherwise the resolver uses types export conditions, types/typings, declaration siblings, and legacy declaration subpaths. Import and require conditions stay distinct. typesVersions uses the pinned TypeScript backend version, the first matching version range, and exact or wildcard path mappings; an exports map takes precedence.

During avril add, avril cache, avril update, and avril vendor, Avril discovers @types packages for npm imports without bundled declarations and records the discovery result, including negative results, in the lock. Automatic selection stays within the library's major version, including major zero; it never falls back to an unrelated latest. Scoped libraries use the DefinitelyTyped @types/scope__name convention. avril check and the language server replay those locked results. Type dependencies are traversed only when needed to resolve types; a discovered package is not automatically included as a module or as a global declaration. types.globals explicitly adds declaration roots to every compiler-options group. TypeScript /// <reference path="..." /> and /// <reference types="..." /> directives are resolved through the checked module graph. A types="name" reference must resolve to an explicitly configured global type package or a declared npm dependency/peer; Avril does not fetch an arbitrary @types/name package or guess its version from a directive. Reference edges remain scoped to their referring program rather than becoming project-wide roots.

Run avril cache main.ts before offline/frozen checking to prepare the type graph. --no-types-packages disables automatic DefinitelyTyped discovery and replay, while retaining bundled declarations, explicit overrides, and configured globals.

Resolution

exports and imports support exact keys, wildcard patterns, arrays, and nested conditions. For import, active conditions are avril, import, module, and default, evaluated in the manifest's order; node, require, and browser are not active. For synchronous require, conditions are avril, require, and default (never import/module/node), so conditional exports choose the require branch independently from import. An exports map blocks access to unexported subpaths and stays exact in both modes (no extension inference through export targets). Without one, import uses module, main, then index.js; require uses main before index (never module), with index.json/index.cjs also accepted. Exact file subpaths are available; relative imports must name files explicitly, while relative requires use Node-style lookup (exact, then .js/.json/.node rejecting .node, then directory package.json main and index files) against verified manifests.

Missing required peers are installed automatically. Available providers are shared with consumers; different resolved dependency/peer environments get separate package instances. Absent optional peers are not installed solely to satisfy a peer declaration.

Recorded roots replay their locked dependency and peer bindings directly. Those bindings are authoritative: startup does not rederive them from package metadata or check that an edited edge still matches a declared version range or peer context. Structural checks still reject invalid identities, unsafe paths, and dangling references. New root contexts and --update use the resolver to construct bindings. Recorded optional-dependency choices are also replayed; use --update to resolve again for a different platform. Packages that do not support the current platform cannot be loaded.

Node builtins, native addons, and runtime JSON imports are unsupported. See commonjs.md for CommonJS/JSON require support: .cjs/.cts and .js outside a "type": "module" scope execute as CommonJS (with ESM wrappers for import), except for the exact entry file advertised by module, which is ESM regardless of how it is imported. This hint does not change sibling files or override explicit .cjs/.cts extensions. main selects an entry, not its format. .json loads via require (parsing, not ESM), and .mjs stays ESM regardless of package type. TypeScript uses avril's existing transpiler (explicit .cts preserves CJS); type-only imports are erased before runtime acquisition. A package being ESM does not guarantee that it avoids Node APIs.

Project-local storage and Nix

Packages are materialized beneath the discovered project root's .avril/npm/. avril creates .avril/.gitignore containing * if it does not already exist; it never overwrites an existing ignore file. There is no required global npm cache or node_modules tree. --lock=PATH changes the lockfile location, not the materialization root.

import.meta.url is the package file's file: URL. Relative asset URLs work with ordinary new URL('./asset', import.meta.url); reading those assets still requires the relevant guest permissions. Identical resolved instances share a module regardless of the spelling used to import them.

Commit avril.lock. It pins selections, instance edges, archive integrity, and one manifest digest per package without embedding absolute project paths. The full file-hash list lives in a content-addressed sidecar at .avril/npm/.manifests/<sha256>.json, so locks stay small no matter how many files a package holds, and no retained tarball is needed for read-only or frozen runs. Warm execution loads and authenticates file manifests and root package metadata only for instances it uses, and verifies each module's bytes when it is loaded. Preparation restores missing backing and instance trees from their pins before synchronous require can run; require itself never acquires packages. Nested package.json files are authenticated and parsed only when module-type or directory-entry resolution needs them; explicit extensions such as .mjs and .cjs do not need type metadata. Consumed metadata is shared within a prepared instance, so later resolution uses the same verified snapshot. Corruption in an unused file does not block execution. avril cache <entry> is the explicit full-tree audit of reachable packages and reports corruption rather than repairing it. Peer-specific instances retain distinct module identities and real package paths, so import.meta.url and relative asset reads work normally. New instances are published atomically from verified package content using anchored hard links where supported, with a copy fallback. Existing trees receive a no-follow readiness check; their metadata and loaded files are verified at use. avril cache audits both logical instance trees and their canonical backing trees. Locks written by older avril versions carry the inline file list instead: normal runs migrate visited entries to the compact form (publishing any missing sidecars first), while unvisited entries keep their old form until used. Frozen runs never rewrite the lock, so an old lock stays byte-identical.

avril cache main.ts
avril run --offline --frozen main.ts

For Nix, acquire dependencies during a network-enabled preparation step, then include .avril/npm and avril.lock in the build input. A complete tree works read-only with --offline --frozen, including after relocation. --frozen alone permits downloading missing pinned artifacts; --offline prohibits network acquisition. Previously unknown computed imports need preparation with --include/--include-from.

Commands and permissions

add, bundle, cache, info, update, and vendor support npm packages alongside JSR and HTTP modules. bundle selects packages' browser builds (see bundle.md); running never consults the browser field. avril add npm:zod writes a caret range to avril.conf. --npm-registry=URL overrides https://registry.npmjs.org/ independently of JSR's --jsr-registry. The lockfile records these bases as npmRegistry and jsrRegistry, respectively. Registry downloads are restricted to that origin; private registry authentication is not supported yet.

Vendoring mirrors verified npm data and manifest sidecars into vendor/npm/. An offline frozen run with --vendor=vendor can reconstruct an absent .avril/npm tree without retained tarballs. That reconstruction needs a writable project directory.

Acquisition is host tooling authority, not guest network/filesystem authority. Execution grants use virtual provenance URLs, not the local cache paths:

avril run -0 --allow-js-exec=https://registry.npmjs.org/zod/ main.ts

A version/file grant can be narrower, such as https://registry.npmjs.org/zod/4.1.5/index.js. Transitive imports need their own execution authority. Local project execution grants do not implicitly authorize materialized npm code, and acquiring packages grants no guest network or cache-filesystem access.

1# npm packages
2
3avril loads ESM and CommonJS packages directly from npm:
4
5```ts
6import { z } from 'npm:zod@^4';
7console.log(z.number().parse(42));
8```
9
10Declare bare-name bindings in `avril.conf`:
11
12```text
13deps: [
14  'npm:zod@^4',
15  {from: 'npm:preact@^10', name: 'ui'},
16],
17```
18
19Requests accept scoped and unscoped names, npm version ranges, dist-tags, and
20export subpaths (`npm:preact@^10/hooks`). Published packages use their own
21manifest dependencies, never the application's import map. avril does not
22read `node_modules` or `.npmrc`, and never runs npm
23installation scripts. Local directory `require` reads a local `package.json`
24`main` only for directory lookup (no versioning or `node_modules`).
25
26## Type declarations
27
28`avril.conf` can redirect package imports for checking without changing runtime
29imports, and can add explicit global declaration modules:
30
31```text
32types: {
33  imports: {
34    'npm:legacy-sdk': 'npm:@types/legacy-sdk@^3',
35  },
36  globals: ['npm:@types/node@^22'],
37},
38```
39
40`types.imports` uses the same exact-key, trailing-slash prefix, and `null`
41blocking semantics as the top-level `imports` map. Relative import
42specifiers are resolved from the importing module; relative mapping keys,
43targets, and `types.globals` entries are resolved from `avril.conf`. Globals must
44be URLs or relative file paths, not bare package names. A versionless `npm:` key
45can match the resolved versioned package import. These mappings affect type resolution only: they do not redirect
46execution or enable Node runtime APIs.
47
48Explicit overrides take precedence over bundled declarations. Otherwise the
49resolver uses `types` export conditions, `types`/`typings`, declaration siblings,
50and legacy declaration subpaths. Import and require conditions stay distinct.
51`typesVersions` uses the pinned TypeScript backend version, the first matching
52version range, and exact or wildcard path mappings; an exports map takes
53precedence.
54
55During `avril add`, `avril cache`, `avril update`, and `avril vendor`, Avril
56discovers `@types` packages for npm imports without bundled declarations and
57records the discovery result, including negative results, in the lock.
58Automatic selection stays within the library's major version, including major
59zero; it never falls back to an unrelated `latest`. Scoped libraries use the
60DefinitelyTyped `@types/scope__name` convention. `avril check` and the language
61server replay those locked results. Type dependencies are traversed only when needed
62to resolve types; a discovered package is not automatically included as a
63module or as a global declaration. `types.globals` explicitly adds declaration
64roots to every compiler-options group. TypeScript `/// <reference path="..." />`
65and `/// <reference types="..." />` directives are resolved through the checked
66module graph. A `types="name"` reference must resolve to an explicitly
67configured global type package or a declared npm dependency/peer; Avril does not
68fetch an arbitrary `@types/name` package or guess its version from a directive.
69Reference edges remain scoped to their referring program rather than becoming
70project-wide roots.
71
72Run `avril cache main.ts` before offline/frozen checking to prepare the type graph.
73`--no-types-packages` disables automatic DefinitelyTyped discovery and replay,
74while retaining bundled declarations, explicit overrides, and configured globals.
75
76## Resolution
77
78`exports` and `imports` support exact keys, wildcard patterns, arrays, and
79nested conditions. For `import`, active conditions are `avril`, `import`, `module`, and
80`default`, evaluated in the manifest's order; `node`, `require`, and `browser`
81are not active. For synchronous `require`, conditions are `avril`, `require`, and
82`default` (never `import`/`module`/`node`), so conditional exports choose the
83`require` branch independently from `import`. An exports map blocks access to
84unexported subpaths and stays exact in both modes (no extension inference
85through export targets). Without one, `import` uses `module`, `main`, then
86`index.js`; `require` uses `main` before `index` (never `module`), with
87`index.json`/`index.cjs` also accepted. Exact file subpaths are available;
88relative `import`s must name files explicitly, while relative `require`s use
89Node-style lookup (exact, then `.js`/`.json`/`.node` rejecting `.node`, then
90directory `package.json` `main` and `index` files) against verified manifests.
91
92Missing required peers are installed automatically. Available providers are
93shared with consumers; different resolved dependency/peer environments get
94separate package instances. Absent optional peers are not installed solely to
95satisfy a peer declaration.
96
97Recorded roots replay their locked dependency and peer bindings directly.
98Those bindings are authoritative: startup does not rederive them from package
99metadata or check that an edited edge still matches a declared version range
100or peer context. Structural checks still reject invalid identities, unsafe
101paths, and dangling references. New root contexts and `--update` use the
102resolver to construct bindings. Recorded optional-dependency choices are also
103replayed; use `--update` to resolve again for a different platform. Packages
104that do not support the current platform cannot be loaded.
105
106Node builtins, native addons, and runtime JSON `import`s are unsupported.
107See `commonjs.md` for CommonJS/JSON `require` support: `.cjs`/`.cts` and
108`.js` outside a `"type": "module"` scope execute as CommonJS (with ESM
109wrappers for `import`), except for the exact entry file advertised by `module`,
110which is ESM regardless of how it is imported. This hint does not change sibling
111files or override explicit `.cjs`/`.cts` extensions. `main` selects an entry, not
112its format. `.json` loads via `require` (parsing, not ESM), and `.mjs` stays ESM
113regardless of package type. TypeScript uses avril's existing
114transpiler (explicit `.cts` preserves CJS); type-only imports are erased
115before runtime acquisition. A package being ESM does not guarantee that it
116avoids Node APIs.
117
118## Project-local storage and Nix
119
120Packages are materialized beneath the discovered project root's `.avril/npm/`.
121avril creates `.avril/.gitignore` containing `*` if it does not already exist;
122it never overwrites an existing ignore file. There is no required global npm
123cache or `node_modules` tree. `--lock=PATH` changes the lockfile location, not
124the materialization root.
125
126`import.meta.url` is the package file's **`file:` URL**. Relative asset URLs
127work with ordinary `new URL('./asset', import.meta.url)`; reading those assets
128still requires the relevant guest permissions. Identical resolved instances
129share a module regardless of the spelling used to import them.
130
131Commit `avril.lock`. It pins selections, instance edges, archive integrity,
132and one manifest digest per package without embedding absolute project
133paths. The full file-hash list lives in a content-addressed sidecar at
134`.avril/npm/.manifests/<sha256>.json`, so locks stay small no matter how
135many files a package holds, and no retained tarball is needed for
136read-only or frozen runs. Warm execution loads and authenticates file manifests
137and root package metadata only for instances it uses, and verifies each module's
138bytes when it is loaded. Preparation restores missing backing and instance trees
139from their pins before synchronous `require` can run; `require` itself never
140acquires packages. Nested `package.json` files are authenticated and parsed only
141when module-type or directory-entry resolution needs them; explicit extensions
142such as `.mjs` and `.cjs` do not need type metadata. Consumed metadata is shared
143within a prepared instance, so later resolution uses the same verified snapshot.
144Corruption in an unused file does not block execution. `avril cache <entry>`
145is the explicit full-tree audit of reachable packages and reports corruption
146rather than repairing it.
147Peer-specific instances retain distinct module identities and real package
148paths, so `import.meta.url` and relative asset reads work normally. New
149instances are published atomically from verified package content using anchored
150hard links where supported, with a copy fallback. Existing trees receive a
151no-follow readiness check; their metadata and loaded files are verified at use.
152`avril cache` audits both logical instance trees and their canonical backing trees.
153Locks written by older avril versions carry the inline file list instead: normal
154runs migrate visited entries to the compact form (publishing any missing sidecars
155first), while unvisited
156entries keep their old form until used. Frozen runs never rewrite the
157lock, so an old lock stays byte-identical.
158
159```sh
160avril cache main.ts
161avril run --offline --frozen main.ts
162```
163
164For Nix, acquire dependencies during a network-enabled preparation step, then
165include `.avril/npm` and `avril.lock` in the build input. A complete tree works
166read-only with `--offline --frozen`, including after relocation. `--frozen`
167alone permits downloading missing pinned artifacts; `--offline` prohibits
168network acquisition. Previously unknown computed imports need preparation
169with `--include`/`--include-from`.
170
171## Commands and permissions
172
173`add`, `bundle`, `cache`, `info`, `update`, and `vendor` support npm packages
174alongside JSR and HTTP modules. `bundle` selects packages' browser builds (see
175[bundle.md](bundle.md)); running never consults the `browser` field. `avril add npm:zod` writes a caret range to `avril.conf`.
176`--npm-registry=URL` overrides `https://registry.npmjs.org/` independently of
177JSR's `--jsr-registry`. The lockfile records these bases as `npmRegistry` and
178`jsrRegistry`, respectively. Registry downloads are restricted to that origin;
179private registry authentication is not supported yet.
180
181Vendoring mirrors verified npm data and manifest sidecars into `vendor/npm/`.
182An offline frozen run with `--vendor=vendor` can reconstruct an absent
183`.avril/npm` tree without retained tarballs. That reconstruction needs a
184writable project directory.
185
186Acquisition is host tooling authority, not guest network/filesystem authority.
187Execution grants use virtual provenance URLs, not the local cache paths:
188
189```sh
190avril run -0 --allow-js-exec=https://registry.npmjs.org/zod/ main.ts
191```
192
193A version/file grant can be narrower, such as
194`https://registry.npmjs.org/zod/4.1.5/index.js`. Transitive imports need their
195own execution authority. Local project execution grants do not implicitly
196authorize materialized npm code, and acquiring packages grants no guest
197network or cache-filesystem access.