char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ab5df08
main
files
| src/ | |
| Cargo.toml | 277 B |
| README.md | 6.3 KiB |
| tsconfig.harness.json | 126 B |
| tsconfig.json | 2.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/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.