char/avril

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

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

Charlotte Somunify public module registration and declarations6534c3e

main
6.3 KiB111 linesraw

Extension build support

A build-only Oxc adapter for TypeScript-authored extensions. Add avril-ext-build and anyhow as build dependencies and delegate from build.rs:

fn main() -> anyhow::Result<()> {
    avril_ext_build::build("src/main.ts", Some("avril:fjson"))
}

build tracks the source and build script for Cargo, erases TypeScript while preserving native JavaScript syntax and value imports, and writes to OUT_DIR. Outputs mirror the source path relative to src/: src/main.ts produces main.js and main.d.ts, while src/public/posix.ts produces public/posix.js and public/posix.d.ts. A crate can therefore own several public modules without colliding. The public module name is explicit and independent of those filenames. Each owning crate lists its public modules in a PUBLIC_MODULES: &[avril_core::PublicModule] table. For example, avril_core::public_module!("avril:fs/sync", "ext:file", "fs_sync") embeds both outputs from that stem and names the JS ext:file/fs_sync.js. Use each row's module in the extension's const module array. runtime::stdlib assembles these tables once for public registration and declarations(); only the ambient globals are added by hand.

Pass None for an internal module that needs only JavaScript. Such modules are not subject to the public declaration restrictions. A stale declaration from a previous public build is removed.

This is deliberately not a module-graph builder or declaration bundler. Each call processes one source; it does not discover, resolve, rewrite, or track imports. The emitted ambient module carries the surface only: non-exported values and classes are dropped, and so are private interfaces and type aliases that no exported declaration reaches. Surviving private types are kept unimportable by an appended empty export {}. Type-only imports and re-exports naming a public avril: module are preserved verbatim, and any other surviving top-level shape — a value re-export, an import from a private module, an exported class or enum, a default export — fails explicitly rather than being dropped.

A public entry implements its API directly or binds typed exports to their private owners, as the std barrels do. It is both the registered guest module and the declaration source; there is no separate declaration facade. Internal entry points such as the test runner stay in private modules, not in the guest namespace. A captured intrinsic that a public type must also name gets a value alias at the import (Uint8Array as Uint8ArrayCtor), so the type positions still resolve to the global and no ext: import survives into the declaration. Richer public surfaces and global declarations need deliberate support here, not extension-specific compiler forks. The avril:test entry demonstrates callable intersections with properties and overloads. An exported const with an explicit type annotation can bind directly to a private implementation value. Direct class declarations are unsupported: a class-valued export is an interface describing the instances plus a const whose type exposes only what guests may use (a prototype member, or a new signature when guests may construct it). A const typed only with prototype is not a valid instanceof target: every class-valued export without a new signature must also declare [Symbol.hasInstance](value: unknown): value is X, which both permits instanceof and narrows its target. Exports with a new signature already satisfy instanceof through the constructor type and need no such member. The implementation side of that rule is a type-only declare static readonly [Symbol.hasInstance] on the class, describing ordinary prototype-walk instanceof — never a runtime hook, which would change what instanceof matches.

Oxc does not type-check or read tsconfig.json. Author erasable TypeScript and validate it separately with TypeScript, including generated declarations without private ambient modules. Normal Cargo builds require neither Node nor a TypeScript executable.

Implementation checking

Extend lib/ext_build/tsconfig.json from each extension's tsconfig and list its own sources; the shared config owns everything else. Its include globs the ambient contracts that every extension may see — core/types.d.ts, each src/internal.d.ts, each src/globals.d.ts, each crate-level types.d.ts, and http/src/file_ops.d.ts — and its paths table maps every internal ext: specifier to its source once, so a crate neither enumerates other crates' declarations nor restates their mappings. Private contracts stay out of the globs and are listed by the one crate that owns them (webstreams/src/bindings.d.ts, blob/src/platform.d.ts). Public entries must satisfy isolated declarations; internal dependencies need not.

The fJSON, test, kv, ffi, path, fs, and std (including std/sync) harnesses additionally check public consumers against generated declarations without private ambient modules. Such a harness extends lib/ext_build/tsconfig.harness.json instead: same compiler options, but paths cleared so a public specifier resolves to the generated ambient module rather than the implementation source, include empty so no implementation source enters the program, and dom in lib because guests see TypeScript's default libs.

Shared private binding contracts live beside their owners: core/types.d.ts and exts/web/types.d.ts. exts/base/types.d.ts supplies the public entry's web type names from the runtime implementations during source checking; consumer harnesses use TypeScript's default web libraries instead. Extension-specific ops stay with the extension. These are handwritten contracts, not generated Rust-op typing or a replacement for runtime tests. Do not publish them in the guest declaration catalog.

A Rust integration test can call avril_ext_build::testing::check_types(concat!(env!("CARGO_MANIFEST_DIR"), "/tsconfig.json")). Add the helper as a dev dependency. It returns early unless AVRIL_TEST_TYPESCRIPT is set; see backend setup.

AVRIL_TEST_TYPESCRIPT=/absolute/path/to/tsc cargo nextest run --workspace

Type fixtures should exercise inference and failure boundaries, not repeat every op signature. The root .oxfmtrc.json uses 120 columns for private declarations and type fixtures; vendor sources are excluded.

1# Extension build support
2
3A build-only Oxc adapter for TypeScript-authored extensions. Add
4`avril-ext-build` and `anyhow` as build dependencies and delegate from `build.rs`:
5
6```rust
7fn main() -> anyhow::Result<()> {
8    avril_ext_build::build("src/main.ts", Some("avril:fjson"))
9}
10```
11
12`build` tracks the source and build script for Cargo, erases TypeScript while
13preserving native JavaScript syntax and value imports, and writes to `OUT_DIR`.
14Outputs mirror the source path relative to `src/`: `src/main.ts` produces
15`main.js` and `main.d.ts`, while `src/public/posix.ts` produces
16`public/posix.js` and `public/posix.d.ts`. A crate can therefore own several
17public modules without colliding. The public module name is explicit and
18independent of those filenames. Each owning crate lists its public modules in
19a `PUBLIC_MODULES: &[avril_core::PublicModule]` table. For example,
20`avril_core::public_module!("avril:fs/sync", "ext:file", "fs_sync")`
21embeds both outputs from that stem and names the JS `ext:file/fs_sync.js`.
22Use each row's `module` in the extension's const module array.
23`runtime::stdlib` assembles these tables once for public registration and
24`declarations()`; only the ambient globals are added by hand.
25
26Pass `None` for an internal module that needs only JavaScript. Such modules are
27not subject to the public declaration restrictions. A stale declaration from a
28previous public build is removed.
29
30This is deliberately not a module-graph builder or declaration bundler. Each
31call processes one source; it does not discover, resolve, rewrite, or track
32imports. The emitted ambient module carries the surface only: non-exported
33values and classes are dropped, and so are private interfaces and type aliases
34that no exported declaration reaches. Surviving private types are kept
35unimportable by an appended empty `export {}`. Type-only imports and re-exports
36naming a public `avril:` module are preserved verbatim, and any other surviving
37top-level shape — a value re-export, an import from a private module, an
38exported class or enum, a default export — fails explicitly rather than being
39dropped.
40
41A public entry implements its API directly or binds typed exports to their
42private owners, as the std barrels do. It is both the registered guest module
43and the declaration source; there is no separate declaration facade. Internal
44entry points such as the test runner stay in private modules, not in the guest
45namespace. A captured intrinsic that a public type must also name gets a value
46alias at the import (`Uint8Array as Uint8ArrayCtor`), so the type positions
47still resolve to the global and no `ext:` import survives into the declaration.
48Richer public surfaces and global declarations need deliberate support here,
49not extension-specific compiler forks. The `avril:test` entry demonstrates
50callable intersections with properties and overloads. An exported `const` with
51an explicit type annotation can bind directly to a private implementation value.
52Direct class declarations are unsupported: a class-valued export is an interface
53describing the instances plus a `const` whose type exposes only what guests may
54use (a `prototype` member, or a `new` signature when guests may construct it).
55A `const` typed only with `prototype` is not a valid `instanceof` target:
56every class-valued export without a `new` signature must also declare
57`[Symbol.hasInstance](value: unknown): value is X`, which both permits
58`instanceof` and narrows its target. Exports with a `new` signature already
59satisfy `instanceof` through the constructor type and need no such member.
60The implementation side of that rule is a type-only
61`declare static readonly [Symbol.hasInstance]` on the class, describing
62ordinary prototype-walk `instanceof` — never a runtime hook, which would
63change what `instanceof` matches.
64
65Oxc does not type-check or read `tsconfig.json`. Author erasable TypeScript and
66validate it separately with TypeScript, including generated declarations without
67private ambient modules. Normal Cargo builds require neither Node nor a
68TypeScript executable.
69
70## Implementation checking
71
72Extend `lib/ext_build/tsconfig.json` from each extension's tsconfig and list its
73own sources; the shared config owns everything else. Its `include` globs the
74ambient contracts that every extension may see — `core/types.d.ts`, each
75`src/internal.d.ts`, each `src/globals.d.ts`, each crate-level `types.d.ts`, and
76`http/src/file_ops.d.ts` — and its `paths` table maps every internal `ext:`
77specifier to its source once, so a crate neither enumerates other crates'
78declarations nor restates their mappings. Private contracts stay out of the
79globs and are listed by the one crate that owns them (`webstreams/src/bindings.d.ts`,
80`blob/src/platform.d.ts`). Public entries must satisfy isolated declarations;
81internal dependencies need not.
82
83The fJSON, test, kv, ffi, path, fs, and std (including std/sync) harnesses
84additionally check public consumers against generated declarations without
85private ambient modules. Such a harness extends
86`lib/ext_build/tsconfig.harness.json` instead: same compiler options, but `paths`
87cleared so a public specifier resolves to the generated
88ambient module rather than the implementation source, `include` empty so no
89implementation source enters the program, and `dom` in `lib` because guests see
90TypeScript's default libs.
91
92Shared private binding contracts live beside their owners: `core/types.d.ts`
93and `exts/web/types.d.ts`. `exts/base/types.d.ts` supplies the public entry's web
94type names from the runtime implementations during source checking; consumer
95harnesses use TypeScript's default web libraries instead. Extension-specific ops
96stay with the extension.
97These are handwritten contracts, not generated Rust-op typing or a replacement
98for runtime tests. Do not publish them in the guest declaration catalog.
99
100A Rust integration test can call
101`avril_ext_build::testing::check_types(concat!(env!("CARGO_MANIFEST_DIR"), "/tsconfig.json"))`.
102Add the helper as a dev dependency. It returns early unless
103`AVRIL_TEST_TYPESCRIPT` is set; see [backend setup](../lsp/BACKEND.md).
104
105```sh
106AVRIL_TEST_TYPESCRIPT=/absolute/path/to/tsc cargo nextest run --workspace
107```
108
109Type fixtures should exercise inference and failure boundaries, not repeat every
110op signature. The root `.oxfmtrc.json` uses 120 columns for private declarations
111and type fixtures; vendor sources are excluded.