Files
2026-05-27 23:29:53 +01:00

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-full Stage-8 closure smoke (gate 2.8) — calls compilePluginToWasm({ pluginEntry: "<api-plugin-example>/src/wasm/index.ts" }) and asserts the produced artifact responds to a real HTTP request via host.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 via api-sandbox-wasm-host-bridge with abiVariant: "wasi-stdio", deploy via host.installPlugin, exercise every declared route via a real HTTP request through createTestHost.

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
  1. Bun.build({ entrypoints, target: "browser", format: "esm" }) — tree-shake + inline. Reject node:* specifiers with WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY + context.specifier.
  2. 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's routes array; request → call the plugin's handleRequest(request) and write the response; start → call optional start() then {kind:"ok"}; dispose → call optional dispose() then {kind:"ok"} and exit.
    • Writes length-prefixed PluginEnvelopeOut frames to process.stdout (Javy maps to fd 1).
    • Surfaces thrown SuaveplanError-shaped values as {kind:"error", code, message} envelopes.
  3. javy build <bundled.js> -o <output.wasm> — invokes the platform-appropriate javy-cli-* binary via optionalDependencies resolution. Non-zero exit → WASM_COMPILER.JAVY_INVOCATION_FAILED with {exitCode, stderr (8 KiB trunc), command}.
  4. WebAssembly.compile(bytes) + reflection → assert memory + _start + the required wasi_snapshot_preview1.* imports. Missing → WASM_COMPILER.ABI_VALIDATION_FAILED + context.missing. This matches the WASI-stdio bridge variant's import allowlist exactly.
  5. Emit <basename>.wasm.json sidecar — { routes, abiVersion, abiVariant: "wasi-stdio", javyVersion, compilerVersion, compiledAt (RFC 3339 UTC), sourceDigest (SHA-256 of pre-Javy bundle) }. Empty routes → 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_shim or 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} where result ∈ {"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:

  1. ABI completeness — every required WASI import + _start + memory present.
  2. Deterministic output — same input + pinned Javy version → identical SHA-256.
  3. Manifest fidelity — routes array matches the plugin's declared routes.
  4. 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-bridge but is not what this compiler emits — and is not required by api-plugin-example or any planned consumer.
  • Component Model output. A future wasm-compiler-component package 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.