7.3 KiB
TS→WASM Compiler (Javy-backed, WASI-stdio variant)
Package: @suaveplan/wasm-compiler-javy
Status: PROPOSED (rewritten 2026-05-27 after architectural blocker — see §Why)
Type: new-package (compilation tooling)
Depends on: api-sandbox-wasm-stdio-abi (defines the wire protocol — SHIPPED at cddccbdb7c) AND api-plugin-example-dual-shape (provides the /wasm subpath the keystone test compiles — must ship first)
Why
The original proposal sketched a direct-export ABI emitted by Javy (__suaveplan_alloc, __suaveplan_handle_request, etc.). The build agent surfaced an architectural blocker against Javy v8.1.1: Javy's -C wit=... flag restricts WIT-exposed exports to "function exports with no arguments and no return values", and Javy's host-interaction model is fundamentally WASI fd 0 / fd 1 (stdin / stdout). The direct-export ABI cannot be produced from a Javy-compiled JavaScript module without authoring a hand-encoded outer-WASM wrapper — a route the orchestrator rejected as too brittle.
The chosen path (orchestrator decision, 2026-05-27) is to extend @suaveplan/api-sandbox-wasm-host-bridge to accept a WASI-stdio ABI variant (sibling proposal api-sandbox-wasm-stdio-abi) and let this compiler be a thin Javy wrapper: bundle TS → inject a WASI-stdio ABI shim → invoke javy build → emit manifest. No outer-WASM authorship, no Component Model migration, no novel encoding work.
Concrete consumer
@suaveplan/api-plugin-example— TypeScript reference plugin. The keystone end-to-end test compiles it via this package and deploys the resulting WASM through the WASI-stdio bridge variant.e2e/foundation-stack-fullStage-8 closure smoke (gate 2.8) — callscompilePluginToWasm({ pluginEntry: "<api-plugin-example>/src/wasm/index.ts" })and asserts the produced artifact responds to a real HTTP request viahost.installPlugin+ the WASI-stdio bridge.- Plugin operators authoring third-party TS plugins for the wasm tier.
End-to-end test
tests/integration/api-plugin-example.test.ts— compile the example plugin, instantiate viaapi-sandbox-wasm-host-bridgewithabiVariant: "wasi-stdio", deploy viahost.installPlugin, exercise every declared route via a real HTTP request throughcreateTestHost.
What changes
Compilation pipeline
pluginEntry ──[1]──> bundled ESM JS ──[2]──> ABI-shimmed JS ──[3]──> .wasm ──[4]──> validate ──[5]──> manifest sidecar
Bun.build inject WASI-stdio loop javy build ABI check + Zod <basename>.wasm.json
Bun.build({ entrypoints, target: "browser", format: "esm" })— tree-shake + inline. Rejectnode:*specifiers withWASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY+context.specifier.- Inject the WASI-stdio ABI shim — prepend a generated header that:
- Reads from
process.stdin(Javy maps this to fd 0) in length-prefix-framed loop. - Validates each frame as
PluginEnvelopeIn(Zod-validated runtime check, using the same Zod schemas exported by@suaveplan/api-sandbox-wasm-host-bridge). - Dispatches by
kind:routes→ write the plugin'sroutesarray;request→ call the plugin'shandleRequest(request)and write the response;start→ call optionalstart()then{kind:"ok"};dispose→ call optionaldispose()then{kind:"ok"}and exit. - Writes length-prefixed
PluginEnvelopeOutframes toprocess.stdout(Javy maps to fd 1). - Surfaces thrown
SuaveplanError-shaped values as{kind:"error", code, message}envelopes.
- Reads from
javy build <bundled.js> -o <output.wasm>— invokes the platform-appropriatejavy-cli-*binary viaoptionalDependenciesresolution. Non-zero exit →WASM_COMPILER.JAVY_INVOCATION_FAILEDwith{exitCode, stderr (8 KiB trunc), command}.WebAssembly.compile(bytes)+ reflection → assertmemory+_start+ the requiredwasi_snapshot_preview1.*imports. Missing →WASM_COMPILER.ABI_VALIDATION_FAILED+context.missing. This matches the WASI-stdio bridge variant's import allowlist exactly.- Emit
<basename>.wasm.jsonsidecar —{ routes, abiVersion, abiVariant: "wasi-stdio", javyVersion, compilerVersion, compiledAt (RFC 3339 UTC), sourceDigest (SHA-256 of pre-Javy bundle) }. Emptyroutes→WASM_COMPILER.MANIFEST_MISSING_ROUTES. Routes are extracted at compile time by booting the WASM in a one-shot host-side WASI runner (@bjorn3/browser_wasi_shimor equivalent) and capturing the{kind:"routes"}reply.
Package surface
import { compilePluginToWasm } from "@suaveplan/wasm-compiler-javy";
const result = await compilePluginToWasm({
pluginEntry: "/path/to/plugin/src/index.ts",
outDir: "/tmp/plugin-wasm",
optimizeFor: "size", // default "size" — passes -O to javy build
sourcemaps: false, // default false
javyVersion: "v8.1.1", // default catalog-pinned version
});
// result.wasmPath: string
// result.metadataPath: string
// result.sizeBytes: number
// result.compilationMs: number
Platform-wrapper packages
5 thin-wrapper packages under packages/devtools/tooling/:
javy-cli-linux-x64,javy-cli-linux-arm64,javy-cli-darwin-x64,javy-cli-darwin-arm64,javy-cli-win32-x64.
Each declares matching os + cpu in package.json so npm/bun skips installation on mismatched platforms. Binaries are fetched via a postinstall script from Bytecode Alliance's Javy GitHub releases at the catalog-pinned tag, with SHA-256 checksum verification of every download. Checksums are committed; binaries are not.
CLI binary
suaveplan-wasm-compile <entry> --out-dir <dir> [--optimize-for size|speed] [--sourcemaps] — delegates to compilePluginToWasm. JSON result on stdout, structured error JSON on stderr, non-zero exit on any failure.
OpenTelemetry
Meter @suaveplan/wasm-compiler-javy:
- counter
wasm_compiler.compiles_total{result}whereresult ∈ {"ok","bundle_failed","javy_failed","abi_failed","manifest_failed"}. - histogram
wasm_compiler.compile_duration_ms{result}. - counter
wasm_compiler.abi_validation_failures_total{missing_export}.
Conformance harness
runWasmCompilerConformance({ compileFn }) from ./testing with four invariants:
- ABI completeness — every required WASI import +
_start+memorypresent. - Deterministic output — same input + pinned Javy version → identical SHA-256.
- Manifest fidelity —
routesarray matches the plugin's declared routes. - Cross-bridge deployability — produced WASM passes
runWasmHostBridgeConformance({ buildBridge, abiVariant: "wasi-stdio" }).
Error codes
WASM_COMPILER.{BUNDLE_FAILED, BROWSER_INCOMPAT_DEPENDENCY, JAVY_PLATFORM_MISMATCH, JAVY_INVOCATION_FAILED, ABI_VALIDATION_FAILED, MANIFEST_MISSING_ROUTES} — 6 codes via defineCodes, error class with public constructor (rule §19).
Out of scope
- Direct-i32-i64 ABI compilation. That variant remains supported by
api-sandbox-wasm-host-bridgebut is not what this compiler emits — and is not required byapi-plugin-exampleor any planned consumer. - Component Model output. A future
wasm-compiler-componentpackage could target that surface; not in this scope. - Compilation of non-suaveplan plugin shapes. The ABI shim is hard-coded to the suaveplan WASI-stdio protocol.