# 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`: ```rust 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](../lsp/BACKEND.md). ```sh 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.