char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
61e3521
main
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.tsavril 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: { 33imports: { 34'npm:legacy-sdk': 'npm:@types/legacy-sdk@^3', 35}, 36globals: ['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.