char/avril

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

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

Charlotte Somsupport data urls1c8ffed

main
8.3 KiB153 linesraw

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.