char/avril

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

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

Charlotte Somdeslop: factor module prep/transpilatione/etc into loader.rsab5df08

main
1 folder4 files

files

src/
Cargo.toml277 B
README.md6.3 KiB
tsconfig.harness.json126 B
tsconfig.json2.4 KiB

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.