wasm-pack and napi-rs executors and generators. Closes the
Monodon-parity surface for Node-native and browser-targeted Rust crates.
| ID | Owner | Status |
|---|---|---|
| WASM-NAPI | eddacraft | In Progress (WN-001 shipped 0.2.0; WN-002 seam generator done 2026-06-21, unreleased — Anvil #3) |
Spec §6.10 says: some Rust workspaces need WASM (browser/edge) or Node-native module support, and Monodon already exposes this shape, so compatibility expectations exist. But WASM and NAPI should not dominate the core plugin model — they are optional capability packs.
This module ships the executors and generators that bring nxrust to
parity with @monodon/rust's WASM/NAPI surface, while keeping the core
Cargo-native plugin uncluttered. It absorbs the WASM/NAPI items from the
original 02-monodon-parity module (now refactored away).
NAPI (napi-rs):
napiexecutor — wrapsnapi buildandnapi prepublishfor napi-rs crates that produce a Node-loadable.nodebinary.add-napigenerator — converts an existing Rust crate into a napi-rs crate: Cargo manifest deltas (crate-type = ["cdylib"],napi-derivedep,napi-buildbuild-script),package.jsonshape for the sibling JS package,napi.config.jsonbaseline.create-napi-npm-dirsgenerator — generates per-target npm sub-package directories that napi-rs expects for prebuilt platform-specific binaries (e.g.npm/darwin-arm64,npm/linux-x64-gnu).
WASM (wasm-pack):
wasm-packexecutor — wrapswasm-pack buildfor crates targetingwasm32-*. Supports--target(bundler,nodejs,web,no-modules),--release,--scope, output directory.add-wasmgenerator — converts an existing Rust crate into awasm-pack-buildable crate:crate-type = ["cdylib", "rlib"],wasm-bindgendep, siblingpkg.jsonshape.add-wasm-referencegenerator — wires a JS/TS project to consume a siblingwasm-packcrate: dep in the JSpackage.json, optional types entry, bundler-specific glue (Vite/webpack/Next).
Cross-language dependsOn contract (ISS-001):
add-wasm-referenceandadd-napiMUST NOT emit adependsOnshape on the JS side that inherits the workspace-default^buildtest dependency. The generator's emitted JS target config explicitly overridestest.dependsOnto a narrow form ([]by default; consumer opts in to^buildonly when their JS build actually imports the Rust artefact at TS build time — e.g. a WASM module bundled into a webpack/Vite build).- The opt-in shape is a generator flag (e.g.
--js-build-depends-on-rust) and is documented in the generator'sschema.jsondescription. - Rationale: see ISS-001 in
plans/issues.md. Anvil's PR #1729 (40m → 52s, 46× speedup) is the empirical anchor. - NAPI follows the same default — the
.nodebinary is loaded atrequiretime, not at JS build time, so a JStscnever needs the Rust artefact present.
Cache:
napiandwasm-packexecutors cache the build artefact directory (.nodefor napi,pkg/for wasm). Cache rules inherit from 04-cache-semantics — toolchain + target triple + features participate in the key.- WASM target triples (
wasm32-unknown-unknownetc.) participate explicitly so a--target=webcache hit cannot satisfy a--target=nodejsinvocation.
Borrow vs rewrite:
- Per index D-001 (and the original 02-monodon-parity's D-P2), borrowing
from
@monodon/rust(MIT) is permitted where it saves real time. - Borrowed code carries a top-of-file attribution comment naming the
source file and commit SHA, plus a
THIRD-PARTY-NOTICES.mdentry preserving the MIT licence text. - Borrow vs rewrite decision is recorded per item at promotion time; napi-rs v3 may have shipped a new CLI shape that makes a borrow stale, in which case rewriting is cheaper.
create-nx-workspacepreset (workspace bootstrap) — that's 16-adoption-and-docs. The original 02-monodon-parity grouped them; they are split because preset is packaging-different (must be reachable without the plugin already installed).- Generic
fficrate generator (cdylib for non-Node, non-WASM cases) — that's 07-generators. - Publishing the resulting npm packages — that flow uses the consumer's
existing
nx release publishfor the JS sibling; the Cargo side uses 08-release-support. wasm-bindgen-only flows that bypasswasm-pack. Possible later, not in this module's first cut.
napi-rsCLI (@napi-rs/cli) on PATH or in the consumer'sdevDependencies.wasm-packbinary on PATH (cargo install wasm-pack, or the officialwasm-packinstaller script / pre-built binary release).- v0.1's executor and generator scaffolding utilities.
- 03-target-inference — for inferring the
napi/wasm-packtargets on appropriately-shaped crates. - 04-cache-semantics — output narrowing rules for WASM/NAPI artefacts.
- 07-generators — shared generator helpers.
- Executors:
napi,wasm-pack. - Generators:
add-napi,add-wasm,add-wasm-reference,create-napi-npm-dirs. - Cache contract for WASM/NAPI artefact directories.
- Attribution mandatory for borrowed code. Top-of-file attribution
comment +
THIRD-PARTY-NOTICES.mdentry preserving the MIT licence text. Stripping attribution to make a file "look fresh" is not permitted. - Apache-2.0 stays the umbrella licence. Borrowed MIT code is compatible; do not introduce dependencies under copyleft licences.
- Borrow, but verify. Borrowed code passes nxrust's TypeScript
strict mode, ESLint config, and Vitest suite. Adapt monodon's patterns
to nxrust's existing helpers (
buildCargoArgs,inferProjectConfig) rather than importing monodon's full abstraction tree. - One executor/generator per PR. Bundled drops make review hard and rollback harder.
- napi-rs CLI version pinning. Document the supported napi-rs CLI version range; reject earlier/later with a diagnostic.
- Tool-presence detection. Missing
napiorwasm-pack⇒ diagnostic via 14-diagnostics, never raw shell error. - Cross-language edges never inherit
^build.add-wasm-referenceandadd-napiemit JS-side target config that explicitly overridestest.dependsOnso adopters with a workspace-defaulttest.dependsOn: ["^build"]do not pay a transitive cargo build per JS test invocation. Opt-in only when the JS build actually consumes the Rust artefact at TS build time. See D-WN4.
Promote individual Work Items to Ready when:
- A real consumer asks for NAPI or WASM (per D-007).
- The consumer's use case is captured (which executor/generator, against which crate shape, with which command shape).
- Monodon's current implementation is read; a borrow vs. rewrite decision is recorded.
- A Work Item is drafted scoped to the specific item being promoted.
- The other items stay Proposed.
Status: Released — implementation + 19 unit tests complete; PR
#21 squash-merged to main
(67bd4f1) on 2026-06-09. Helper shipped at src/utils/cross-language-edges.ts
and exported from src/index.ts. Released in @eddacraft/nxrust@0.2.0
on 2026-06-10 (npm latest → 0.2.0; release commit b8d7f4b, tag v0.2.0
pushed). Full gate green at release (build · 152 tests · e2e).
- Intent: Provide the reusable utility that enforces D-WN4 — given a JS
project that depends on a Rust crate, sever the workspace-default
^buildfrom that JS project'stesttarget so a JS test never pulls a transitive cargo build and serialises on the workspacetarget/lock. Trigger: Anvil PReddacraft/anvil-001#1729/ ISS-001 (46× speedup); D-WN4 is already ratified (and lifted to index D-009), so the contract is decided — only the code-authoring is outstanding. Promotes per D-007. - Scope / Non-scope: Ships the contract helper + tests only. The
add-wasm-reference,add-napi,add-wasmgenerators and thenapi/wasm-packexecutors stay Proposed — no consumer has asked for those executables, and building them would be the speculative work D-007 forbids. When the first of those generators is promoted, it consumes this helper rather than re-deriving thedependsOnshape; until then the helper is also callable directly by a consumer wiring a cross-language edge by hand. - Expected Outcome: A
src/utilshelper accepts a JS project'sProjectConfiguration(or itstestTargetConfiguration) and returns it withtest.dependsOnset to an explicit value that excludes^build(empty by default), overriding any workspace-defaulttest.dependsOn: ["^build"]. AconsumesArtifactAtBuildTimeopt-in retains^buildfor the legitimate case where the JS build imports the Rust artefact at TS build time (WASM bundled into webpack/Vite, generated.d.tsconsumed bytsc). Pre-existing non-^builddependsOnentries on the JStesttarget are preserved; only the^buildtoken is stripped. Idempotent — applying twice is a no-op. - Validation:
pnpm test— unit suite over the helper asserting: (a)^buildstripped from a JStest.dependsOnthat had only^build; (b) sibling non-^builddeps preserved; (c) opt-in flag retains^build; (d) idempotency.pnpm typecheckgreen. Ships as a minor bump with a CHANGELOG entry (new exported utility; no graph-shape change, additive).
Status: Done (2026-06-21, unreleased — Anvil's #3 upstream ask. First slice under D-011.)
The kind-agnostic generator that applies the cross-language test-seam
contract to a JS/TS project consuming a Rust crate, turning the WN-001
helper into an invokable nx g @eddacraft/nxrust:add-rust-reference --project <js> --crate <rust>. It reads the JS project config, applies
severCrossLanguageTestEdge (severing the workspace-default ^build by
default; retaining it under --consumesArtifactAtBuildTime), writes back an
explicit test.dependsOn, and warns if --crate is not an nxrust crate.
Idempotent; skipFormat supported.
- Scope / non-scope. This is only the
test.dependsOnseam — the part every cross-language edge needs and that Nx gets wrong by default (ISS-001). The binding-specific scaffolding ofadd-napi(manifestcrate-type = ["cdylib"],napi-derive, npm dirs) andadd-wasm-reference(bundler glue,pkg/wiring) stays Proposed — those promote per D-007 against a consumer's actual napi-rs / wasm-pack version with a borrow-vs-rewrite decision, and will wrap this generator rather than re-derive the seam. Thenapi/wasm-packexecutors likewise stay Proposed. - Why a kind-agnostic name. Resolves the module Open Question (merge vs
split) in favour of a shared seam core:
add-napi/add-wasm-referencebecome thin binding-specific wrappers overadd-rust-reference, so the D-009 contract has exactly one implementation. Adding it togenerators.jsonis the promotion D-WN2 anticipates (it is the seam core, not napi/wasm tooling). - Validation.
pnpm test— severs^buildby default, preserves sibling deps, retains under the opt-in, materialises a severedtestwhen the JS project has none, idempotent, warns on a non-Rust--crate, and surfaces a clear error for a missing project. New generator → minor bump (D-008).
Further items (the napi / wasm-pack executors and the add-napi /
add-wasm / add-wasm-reference scaffolding generators) stay Proposed and
promote individually on real-consumer asks per D-007 — same gate as the
original 02-monodon-parity module before refactor.
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Borrowed monodon code drifts from upstream and we miss a bug fix | medium | medium | Record source commit SHA in file header; periodically diff against upstream when an issue lands |
| napi-rs / wasm-pack ecosystem moves faster than borrowed code | medium | medium | Validate against the consumer's actual napi-rs/wasm-pack version at promotion time; do not promote against a stale monodon snapshot |
| Attribution stripped accidentally during refactor | medium | low | THIRD-PARTY-NOTICES.md is the source of truth; CI lint that fails on borrowed-file attribution loss is a v0.4 candidate |
| WASM artefact caching across target triples mixes browser and nodejs builds | high | medium | Target triple participates in cache key; tested in 04-cache-semantics fixture matrix |
add-wasm-reference glue lags behind bundler upgrades (Vite/webpack/Next) |
medium | medium | Template snapshot date in each glue file's comment; bump templates as part of v0.x line |
Cross-language edge inherits workspace ^build test default, serialising JS tests on cargo lock |
high | high | D-WN4: generator pins narrow dependsOn on JS side, opt-in for real artefact consumption; recipe in 16-adoption-and-docs. Empirical anchor: anvil-001 PR #1729 (40m → 52s). |
- D-WN1: Borrow vs rewrite is per item, decided at promotion time against the consumer's actual napi-rs/wasm-pack version. Accepted (inherits index D-001).
- D-WN2: WASM and NAPI stay optional capability packs, not part of
the core plugin model. They do not appear in
executors.json/generators.jsonuntil promotion. Accepted (inherits spec §6.10). - D-WN3:
create-nx-workspacepreset is not in this module — preset lives in 16-adoption-and-docs. Different scope (workspace-bootstrap vs in-workspace generator). Accepted 2026-05-17 (refactor decision). - D-WN4: Cross-language edges constructed by
add-wasm-referenceandadd-napidefault to emptytest.dependsOnon the JS side, explicitly overriding any workspace-default^build. The generator exposes an opt-in flag for cases where the JS build actually consumes the Rust artefact at TS build time (e.g. WASM bundled into webpack/Vite). Rationale: cargotarget/lock contention silently turns a 52-second test suite into a 40-minute one when every JS test pulls a transitive cargo build. Empirical anchor: anvil-001 PR #1729 — 46× speedup after splittingtest:js && test:rust. Tracked as ISS-001. Accepted 2026-05-20 (consumer-driven, per D-007). Lifted to index-level D-009 on 2026-05-20 so the rule binds future cross-language generators outside this module.
- napi-rs has shipped a v3 with a different build CLI shape. Borrow
from monodon's
napiexecutor or rewrite against napi-rs v3? Resolve at promotion time against the consumer's napi-rs version. - Should
add-napiandadd-wasmmerge into a singleadd-bindinggenerator with a--kindoption, or stay split? Split is more discoverable; merge is more uniform. Resolve at promotion based on which one promotes first. - Should
wasm-packexecutor exposewasm-bindgendirectly for lower-level flows, or only viawasm-pack?wasm-packcovers most cases; lower-level is a v0.4+ ask. - napi-rs prebuilt-binary publishing flow — does the plugin own the "publish per-platform package" loop, or does that stay manual? Most consumers use a GitHub Actions matrix; the plugin can stay out of the matrix and just wrap the build step.