char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
6534c3e
main
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/tsccargo 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 <()> { 8avril_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.