A WebAssembly Component Model host for JavaScript engines — async-native (Component Model 0.3 concurrency), built on wasmtime's translation frontend compiled to wasm, running on Deno and in browsers.
Instead of ahead-of-time transpilation, polyengine is a runtime linker: it
takes a .wasm component binary, translates it in-process (wasmtime-environ +
FACT fused adapters, running as a wasm32 module), and executes the
instantiation plan on the stock WebAssembly JS API. Cross-component calls
stay pure wasm; the 0.3 task model (tasks, streams, futures, backpressure,
cancellation) is the runtime's core structure, mapped onto the JS event loop —
the callback ABI needs no JSPI at all, and the stackful/blocking forms light
up via JSPI where the engine provides it.
Pre-1.0, but densely gated:
- Official Component Model test suite: 1250 passing / 0 failing commands across all directories (remaining: named xfail classes — wasmparser pin drift, 🧵-deferred, small gaps), identical on Deno, Chromium, and Firefox (behind its JSPI pref); WebKit reaches the same totals on trunk builds (the pinned build lacks JSC multi-memory, since implemented upstream).
- Real-workload proof points (the polymorph component family): the iroh endpoint component — detached pump tasks, multi-export concurrency, cross-task wakeups; the workload that deadlocks jco's scheduler — runs its relay-echo and WebRTC-upgrade paths end-to-end; polymorph-websocket's conformance suite passes 55/55 under this host; an 8 MB componentize-go engine instantiates and runs.
- Guest toolchains exercised: wit-bindgen (Rust) and componentize-go, sync and async.
| Path | What |
|---|---|
crates/translator-shim |
wasmtime-environ + FACT → versioned plan format (wasm32, runs everywhere) |
runtime/ |
TS core: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, embedder API (runtime/src/embedder) |
crates/bindgen |
WIT → TypeScript types for the embedder conventions |
examples/ |
start here to embed: hello-world + kitchen-sink (WIT + Rust guest + TS host, self-checking), plus the guest fixture corpus |
translator/ |
@polyengine/translator: the packaged translator asset + defaultTranslator() per-platform loader (build-time alternative: tools/translate) |
wasi/ |
minimal WASI providers (p2 baseline + p3 clocks), one per semver track |
ct-runner/ |
conformance-suite runner for the polymorph-test L1 contract |
harness/ + tools/browser |
official-suite harness; Deno lane + Chromium/Firefox/WebKit lanes |
contracts/ |
the versioned interface contracts (plan format, embedder API, intrinsics, digest) |
git clone --recursive https://github.com/polymorph-components/polyengine
cd polyengine
just test-runtime # runtime suite (builds the shim + fixtures + corpus first)
just conformance # official CM suite on Deno
just browsers-install && just browser-lane chromium # same corpus, real browserDeno workspace (TS) + cargo workspace (Rust); just
is the command surface (just --list; recipe bodies are the exact commands).
Running a component takes a translation (an execution plan + FACT adapter modules). Three ways to get one:
| method | production ships | choose when |
|---|---|---|
build-time — tools/translate emits a single-file envelope; the host reconstitutes it with artifactsFromEnvelope(envelope, componentBytes) |
component + envelope + runtime — no translator | you know your components at build time (most apps; the browser default — saves ~0.5 MB gzip and a compile per visitor). The envelope embeds the component's sha-256, so a stale pair fails loudly at instantiation |
runtime, packaged — defaultTranslator() from @polyengine/translator, passed to instantiate({ componentBytes, translator }) |
your host + the translator asset (~1.85 MB raw, 520 KB gzip) | components arrive dynamically (plugin systems), or dev/server contexts where the asset size is irrelevant. Pair with the artifact cache (@polyengine/runtime/cache) so each component translates once per client, not once per load |
runtime, explicit — Translator.create(bytes) / Translator.fromExports(ns) from @polyengine/runtime/shim |
same, minus the packaged loader | you source the translator wasm yourself: custom delivery, one shared instance across many components, or cache keying via buildHash |
Translation itself is sub-millisecond warm in all three; the methods differ
only in when it runs and what you deploy. Worked code: the
examples use the packaged form, tools/translate's
README shows the build-time deploy recipe, and
the full decision record is the design note on
#16.
Everything here is unstable (0.x, #16): no compatibility promise across minor lines. But releases are caret-honest: within a minor line they stay backward-compatible, and anything breaking bumps the minor — so caret constraints are the intended way to consume:
import { instantiate } from "jsr:@polyengine/runtime@^0.4.0/embedder";
import { defaultTranslator } from "jsr:@polyengine/translator@^0.4.0";The same five packages ship to npm under the same names, built from the same sources at the same version by the same release:
npm install @polyengine/runtime @polyengine/translatorimport { instantiate } from "@polyengine/runtime/embedder";
import { defaultTranslator } from "@polyengine/translator";The npm distribution is ESM-only (Node >= 22.14) and carries .d.ts; entry
points match the JSR subpaths one for one, so the import specifier is the only
line that differs between registries. Two things are JSR-only, both by
necessity rather than policy: dirCache() (the Deno.* filesystem cache
backend — use webCache() or your own ArtifactCache), and the translator's
permission-free Deno wasm-module load, which the npm build replaces with a
node:fs read of the same packaged asset.
@polyengine/{runtime,translator,wasi,ct-runner} release in lockstep — one
version, cut from one green commit, matching the vX.Y.Z
GitHub release that carries the
same commit's artifacts. (@polyengine/protocol versions independently; the
others depend on it by caret.)
This project was previously named deltic and published under the
@deltic JSR scope, which stops at 0.2.1. The rename is a clean break,
not an alias: @polyengine/* starts a fresh 0.1.0 line, the
Symbol.for("polyengine.*/1") cross-copy brands do not match the old
deltic.* ones, and the POLYENGINE_* environment variables replace their
DELTIC_* spellings. Nothing bridges the two — port in one step.
Between releases, every green main commit still gets a
pre-<shorthash> GitHub release
carrying the same artifacts a cut carries — the translator shim wasm, the
embedder bundle, SHA256SUMS — but nothing is published to JSR or npm
between cuts: the registries carry vX.Y.Z releases only. To track
main, use those release assets or a git reference and upgrade
deliberately. (The <next>-pre.g<shorthash> versions on JSR and the npm
pre dist-tag are frozen leftovers of the earlier flow, which published
every green commit; nothing new lands there.)
Deno's minimum-dependency-age gate (24 h by default) applies to every publish, so a freshly cut release won't resolve on day zero. To consume same-day releases while keeping the gate for the rest of your graph, exempt the scope (wildcard excludes work as of Deno 2.9):
(or --minimum-dependency-age=0 for a one-off run).
| Where | What |
|---|---|
examples/ |
runnable embedder examples: hello-world (smallest complete embedding) and kitchen-sink (imports incl. suspending, resources both directions, value-shape tour) |
docs/architecture.md |
the system design and decisions, with rationale (§-numbered; cited from code comments) |
docs/security.md |
what the WASI filesystem/network confinement does and does not guarantee — read before granting a guest host access |
docs/milestones.md |
the verified milestone record (S0 → C3) |
docs/consumers.md |
the polymorph adoption track: jco blocker mapping, cutover evidence, in-repo ports |
docs/references.md |
canonical upstream links (spec, JSPI, wasmtime internals, toolchain pins) |
contracts/ |
versioned interface contracts — plan format, descriptor IR, intrinsics, digest, embedder API |
AGENTS.md |
development protocol and the full gate list |
| issue tracker | open and deferred work |
125% more engine!