char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
Packages and imports
Module resolution is owned by ModuleGraph in runtime/src/modules.rs;
package configuration is defined by runtime/src/package_config.rs.
Each local avril.conf establishes a package boundary. Its imports and scopes
apply to modules in its directory and descendants, until another config is
encountered. An imported sibling library is discovered automatically; workspace
registration is not required. Maps do not inherit across package boundaries.
Targets and scope URLs are relative to the config file, not the working directory.
Imports use exact matches or longest matching slash-terminated prefixes. Prefix
targets must end with a slash. A null target blocks an import. Matching scopes
are searched from most specific to least specific, followed by the package's
default imports. Mapping is a single step, not recursive alias substitution.
URL-like keys are normalized relative to the config, and URL-like specifiers are
normalized relative to the importing module.
Local module identity follows the canonical file path, including when reached through an alias or symlink. A module never changes ownership based on which consumer imported it. Explicit relative file imports remain available.
Discovery walks canonical ancestors only while they are executable under the current capability policy. A directory execution grant permits config discovery inside that directory; an entry-file-only grant does not. Discovery stops at the first unauthorized ancestor. On Unix, discovery also stops before directories owned by neither the current user nor root, or writable by everyone, including shared temporary directories. Config files must also be owned by the current user or root, must not be world-writable, must be executable under the capability policy, must be regular files, and must not be symlinks. Successfully discovered maps and absent configurations are snapshotted for the lifetime of the loader; failed discovery can be retried. No map grants permission to execute its target; normal module access checks still apply, including to cached imports.
On a terminal, avril run and avril cache write append-only acquisition progress
to stderr: registry/source downloads, npm resolution, and package extraction and
verification. Cached artifacts do not produce download messages. Progress is
disabled for redirected stderr and TERM=dumb. The REPL reports only during
foreground evaluation, not while waiting for input. Progress action words,
including those printed by avril add, use color unless NO_COLOR is nonempty;
package names and URLs in progress messages remain plain.
The supported package schema is described in configuration. Unknown options, malformed maps, normalized duplicate keys, and blocked imports are reported as errors.
Inline modules
data: URLs can name JavaScript or TypeScript ES modules, including entrypoints
and module workers. Use text/javascript or application/javascript for JS,
and text/typescript or application/typescript for TS. Payloads may be
percent-encoded or marked ;base64; module text is decoded as UTF-8 regardless
of the MIME charset parameter. Missing MIME types default to text/plain, which
is not executable. JSON and other non-script MIME types are rejected.
import { answer } from "data:text/typescript,export%20const%20answer%3Anumber=42" ;
A data module has no directory or package bindings: relative imports and bare
package names are unsupported. Absolute file:, HTTP(S), jsr:, npm:, data:,
and public avril: imports use normal resolution and authority checks. Inline
source itself needs no filesystem or network grant and grants none to its code.
The serialized URL is the module identity and import.meta.url. Repeated imports
of the same URL share an instance; different percent/base64 encodings or fragments
remain distinct identities. Fragments are not source text. Inline payloads are
not stored as cache artifacts or lockfile entries; acquired dependencies retain
their usual caching and locking. Checking and bundling use the same MIME-selected
language; editors show decoded sources as read-only avril-builtin:/data/ documents.
This does not add fetch(data:) or synchronous require(data:) support.
Named local packages and interfaces
A consumer imports a dependency by the name it declares, or by a name the consumer chooses:
name: '@app/demo',
deps: [
'jsr:@std/assert@^1.0.19',
'../math',
{from: '../math-v2', name: 'math-next'},
{from: '../format', name: '@app/format'},
],
Each dependency directory contains its own config defining its interface:
name: '@acme/math',
version: '0.1.0',
description: 'Shared math utilities',
exports: {
'.': './mod.ts',
'./vector': './src/vector.ts',
},
deps: [{from: '../numeric', name: 'numeric'}],
The application imports @acme/math or @acme/math/vector; with
{from: '../math', name: 'math'} it would import math and math/vector
instead. Inside that library, @acme/math and @acme/math/vector are
self-imports using the same exports, whatever its consumers call it. A consumer
does not get the library's numeric dependency. Every imported file uses its nearest config's maps, even
when a parent package exports a file beneath another config boundary.
exports: './mod.ts' is shorthand for {'.': './mod.ts'}. Exports are exact:
no wildcard patterns, conditions, implicit index files, or private-subpath file
fallback. Export targets begin with ./ and must stay inside the canonical
package directory, including through symlinks. Config-relative paths use file-URL
path semantics; queries, fragments, encoded separators, and NUL bytes are
rejected. Relative file imports remain independent of the interface: exports are
not a filesystem security sandbox.
A dependency's name is optional unless it needs self-imports or is listed by
path alone. Its exports must
be nonempty, but a root '.' export is optional. Application configs need not
have exports. Version and description are optional metadata; local versions do
not select directories or constrain aliases. Two bindings may point to different
versions/directories with identical declared names, or to the same directory.
Canonical file identity determines whether they share a module instance.
When a package's config loads, the configs of dependencies listed by path alone
are read for their names; the root package's load happens at startup, before
any guest code runs. These name reads are host configuration discovery: they
ignore execution grants but keep the config trust checks above, and they grant
no access to the dependency, whose modules and full config are only loaded,
under normal checks, when a binding is selected. A missing or unnamed
dependency config, or a name colliding with another binding, the package's own
name, or a mapping, fails that package's load. Only declared dependency
directories are inspected. A dependency must contain its own config; it cannot inherit
an ancestor's interface. Header loading does not recursively build dependency
maps, so cyclic package dependencies and cyclic source graphs are supported.
Failed required-header reads or parses can be retried, provided ordinary
ancestor discovery has not already snapshotted that directory's config as absent.
Restart the loader to discover a new config in an already-snapshotted directory.
No sibling scan, global workspace registry, or transitive flattening is performed.
jsr: packages in deps bind and stay private in the same way; see
JSR packages. npm: packages do too; see Npm packages.
Bare local bindings do not
implicitly override jsr: requests, which remain registry-backed.
avril add inserts jsr: or npm: packages into deps, or subpath aliases into
imports, preserving metadata, comments, dependencies, scopes, and unrelated
entries. A dependency entry already holding the same binding, local, jsr:, or npm:, is
replaced. It rejects other collisions, such as with the package's own name or
mappings, without replacing the config; it does not read dependency configs, so
collisions with names declared by path-only dependencies are reported when the
package loads. Declare
local directory bindings in deps by hand. info and editor graph.json annotate local modules
with their owning config, name, version, description, exports, and direct
binding-to-directory or binding-to-jsr:-package metadata.
1# Packages and imports 2 3Module resolution is owned by `ModuleGraph` in `runtime/src/modules.rs`; 4package configuration is defined by `runtime/src/package_config.rs`. 5 6Each local `avril.conf` establishes a package boundary. Its `imports` and `scopes` 7apply to modules in its directory and descendants, until another config is 8encountered. An imported sibling library is discovered automatically; workspace 9registration is not required. Maps do not inherit across package boundaries. 10 11Targets and scope URLs are relative to the config file, not the working directory. 12Imports use exact matches or longest matching slash-terminated prefixes. Prefix 13targets must end with a slash. A `null` target blocks an import. Matching scopes 14are searched from most specific to least specific, followed by the package's 15default `imports`. Mapping is a single step, not recursive alias substitution. 16URL-like keys are normalized relative to the config, and URL-like specifiers are 17normalized relative to the importing module. 18 19Local module identity follows the canonical file path, including when reached 20through an alias or symlink. A module never changes ownership based on which 21consumer imported it. Explicit relative file imports remain available. 22 23Discovery walks canonical ancestors only while they are executable under the 24current capability policy. A directory execution grant permits config discovery 25inside that directory; an entry-file-only grant does not. Discovery stops at the 26first unauthorized ancestor. On Unix, discovery also stops before directories 27owned by neither the current user nor root, or writable by everyone, including 28shared temporary directories. Config files must also be owned by the current user 29or root, must not be world-writable, must be executable under the capability 30policy, must be regular files, and must not be symlinks. Successfully discovered 31maps and absent configurations are snapshotted for the lifetime of the loader; 32failed discovery can be retried. No map grants permission to execute 33its target; normal module access checks still apply, including to cached imports. 34 35On a terminal, `avril run` and `avril cache` write append-only acquisition progress 36to stderr: registry/source downloads, npm resolution, and package extraction and 37verification. Cached artifacts do not produce download messages. Progress is 38disabled for redirected stderr and `TERM=dumb`. The REPL reports only during 39foreground evaluation, not while waiting for input. Progress action words, 40including those printed by `avril add`, use color unless `NO_COLOR` is nonempty; 41package names and URLs in progress messages remain plain. 42 43The supported package schema is described in [configuration](config.md). 44Unknown options, malformed maps, normalized duplicate keys, and blocked imports 45are reported as errors. 46 47## Inline modules 48 49`data:` URLs can name JavaScript or TypeScript ES modules, including entrypoints 50and module workers. Use `text/javascript` or `application/javascript` for JS, 51and `text/typescript` or `application/typescript` for TS. Payloads may be 52percent-encoded or marked `;base64`; module text is decoded as UTF-8 regardless 53of the MIME charset parameter. Missing MIME types default to `text/plain`, which 54is not executable. JSON and other non-script MIME types are rejected. 55 56``` ts 57import { answer } from "data:text/typescript,export%20const%20answer%3Anumber=42"; 58``` 59 60A data module has no directory or package bindings: relative imports and bare 61package names are unsupported. Absolute `file:`, HTTP(S), `jsr:`, `npm:`, `data:`, 62and public `avril:` imports use normal resolution and authority checks. Inline 63source itself needs no filesystem or network grant and grants none to its code. 64 65The serialized URL is the module identity and `import.meta.url`. Repeated imports 66of the same URL share an instance; different percent/base64 encodings or fragments 67remain distinct identities. Fragments are not source text. Inline payloads are 68not stored as cache artifacts or lockfile entries; acquired dependencies retain 69their usual caching and locking. Checking and bundling use the same MIME-selected 70language; editors show decoded sources as read-only `avril-builtin:/data/` documents. 71This does not add `fetch(data:)` or synchronous `require(data:)` support. 72 73## Named local packages and interfaces 74 75A consumer imports a dependency by the name it declares, or by a name the 76consumer chooses: 77 78``` text 79name: '@app/demo', 80deps: [ 81'jsr:@std/assert@^1.0.19', 82'../math', 83{from: '../math-v2', name: 'math-next'}, 84{from: '../format', name: '@app/format'}, 85], 86``` 87 88Each dependency directory contains its own config defining its interface: 89 90``` text 91name: '@acme/math', 92version: '0.1.0', 93description: 'Shared math utilities', 94exports: { 95'.': './mod.ts', 96'./vector': './src/vector.ts', 97}, 98deps: [{from: '../numeric', name: 'numeric'}], 99``` 100 101The application imports `@acme/math` or `@acme/math/vector`; with 102`{from: '../math', name: 'math'}` it would import `math` and `math/vector` 103instead. Inside that library, `@acme/math` and `@acme/math/vector` are 104self-imports using the same exports, whatever its consumers call it. A consumer 105does **not** get the library's `numeric` dependency. Every imported file uses its nearest config's maps, even 106when a parent package exports a file beneath another config boundary. 107 108`exports: './mod.ts'` is shorthand for `{'.': './mod.ts'}`. Exports are exact: 109no wildcard patterns, conditions, implicit index files, or private-subpath file 110fallback. Export targets begin with `./` and must stay inside the canonical 111package directory, including through symlinks. Config-relative paths use file-URL 112path semantics; queries, fragments, encoded separators, and NUL bytes are 113rejected. Relative file imports remain independent of the interface: exports are 114not a filesystem security sandbox. 115 116A dependency's `name` is optional unless it needs self-imports or is listed by 117path alone. Its exports must 118be nonempty, but a root `'.'` export is optional. Application configs need not 119have exports. Version and description are optional metadata; local versions do 120not select directories or constrain aliases. Two bindings may point to different 121versions/directories with identical declared names, or to the same directory. 122Canonical file identity determines whether they share a module instance. 123 124When a package's config loads, the configs of dependencies listed by path alone 125are read for their names; the root package's load happens at startup, before 126any guest code runs. These name reads are host configuration discovery: they 127ignore execution grants but keep the config trust checks above, and they grant 128no access to the dependency, whose modules and full config are only loaded, 129under normal checks, when a binding is selected. A missing or unnamed 130dependency config, or a name colliding with another binding, the package's own 131name, or a mapping, fails that package's load. Only declared dependency 132directories are inspected. A dependency must contain its own config; it cannot inherit 133an ancestor's interface. Header loading does not recursively build dependency 134maps, so cyclic package dependencies and cyclic source graphs are supported. 135Failed required-header reads or parses can be retried, provided ordinary 136ancestor discovery has not already snapshotted that directory's config as absent. 137Restart the loader to discover a new config in an already-snapshotted directory. 138No sibling scan, global workspace registry, or transitive flattening is performed. 139`jsr:` packages in `deps` bind and stay private in the same way; see 140[JSR packages](jsr.md). `npm:` packages do too; see [Npm packages](npm.md). 141Bare local bindings do not 142implicitly override `jsr:` requests, which remain registry-backed. 143 144`avril add` inserts `jsr:` or `npm:` packages into `deps`, or subpath aliases into 145`imports`, preserving metadata, comments, dependencies, scopes, and unrelated 146entries. A dependency entry already holding the same binding, local, `jsr:`, or `npm:`, is 147replaced. It rejects other collisions, such as with the package's own name or 148mappings, without replacing the config; it does not read dependency configs, so 149collisions with names declared by path-only dependencies are reported when the 150package loads. Declare 151local directory bindings in `deps` by hand. `info` and editor `graph.json` annotate local modules 152with their owning config, name, version, description, exports, and direct 153binding-to-directory or binding-to-`jsr:`-package metadata.