Node.js WebNN-flavor API backed by rustnn and ONNX Runtime via a Rust napi-rs addon.
This is a Node polyfill (not a browser implementation). The public API follows the W3C WebNN IDL, with Node-only helpers for Hub model loading.
packages/webnn-node/— TypeScript WebNN API (MLContext,MLGraphBuilder, …)packages/webnn-node/native/— Rust napi-rs addonpackages/webnn-node/idl/webnn.idl— vendored IDL fallback for codegen (see WPT cache below)demo/— Runnable examples.cache/wpt/— local clone of web-platform-tests/wpt (gitignored; created bynpm run test:wpt:fetch)
- Node.js
>= 20 - Rust toolchain (
cargo,rustc) - Native build toolchain (C/C++ compiler, linker)
- ONNX Runtime shared library (see below)
- Local
rustnncheckout at../rustnnrelative to this repo (see rootCargo.toml[workspace.dependencies])
The Node addon is a Cargo workspace member. It always links the path dependency in the root Cargo.toml, not a crates.io release:
rustnn = { path = "../rustnn", features = ["onnx-runtime", "dynamic-inputs"] }Release builds land in target/release/ at the repo root (webnnjs/target/release/), not under packages/webnn-node/native/target/. After changing rustnn or the native crate, you must rebuild and re-stage index.node; otherwise Node may keep running an old binary.
npm install
npm run buildThat runs cargo build --release, then scripts/install-addon.mjs, which copies the workspace artifact into packages/webnn-node/native/index.node and prints the source path:
Using native artifact: .../webnnjs/target/release/webnn_node_native.dll
Installed webnn_node_native.dll -> index.node
If you only changed Rust/rustnn:
cargo build --release -p webnn-node-native
npm run install-addon -w @webnnjs/webnn-node-nativeDependency tree (should show your local path, not crates.io):
cargo tree -p webnn-node-native -i rustnnExample:
rustnn v0.5.11 (C:\git\rustnn-workspace\rustnn)
└── webnn-node-native v0.1.0 (...\webnnjs\packages\webnn-node\native)
Install script — confirm it points at webnnjs/target/release/, not an old native/target/ copy. If the path or timestamp looks wrong after a rebuild, Node is likely loading a stale .node file (see Native addon staging).
rustnn loads ONNX Runtime dynamically. Point it at your ORT shared library:
| Platform | Example |
|---|---|
| Windows | ORT_DYLIB_PATH=C:\path\to\onnxruntime.dll |
| Linux | ORT_DYLIB_PATH=/path/to/libonnxruntime.so |
| macOS | ORT_DYLIB_PATH=/path/to/libonnxruntime.dylib |
Alternatively, set ORT_LIB_DIR to a directory that contains the library.
Both demos load demo/.env when present. You need either ORT_DYLIB_PATH in your environment or a demo/.env file (copy or edit for your machine):
ORT_DYLIB_PATH=C:\git\rustnn-workspace\onnxruntime.dllThe demo code also searches common install locations if the variable is unset, but an explicit path is most reliable.
From the repo root:
npm install
npm run buildThis runs, in order:
- Native addon —
cargo build --release(workspace →target/release/), theninstall-addon.mjscopies the cdylib topackages/webnn-node/native/index.node - webnn-node — regenerates
MLGraphBuildermethods fromwebnn.idl, compiles TypeScript - demo — compiles demo TypeScript
Build individual packages:
# Native only (build + stage index.node)
npm run build -w @webnnjs/webnn-node-native
# Or explicitly from the workspace root:
cargo build --release -p webnn-node-native
npm run install-addon -w @webnnjs/webnn-node-native
# TypeScript package (includes native)
npm --prefix packages/webnn-node run build
# Demo only (requires webnn-node built first)
npm --prefix demo run buildThe copy to index.node happens in packages/webnn-node/native/scripts/install-addon.mjs after cargo build, not in build.rs (build.rs only runs napi_build::setup() before linking).
The install script prefers the workspace artifact (webnnjs/target/release/) over packages/webnn-node/native/target/release/. Always read its Using native artifact: line after a build.
index.js loads the newest *.node file in the native package directory (by modification time). That means:
index.staged.nodecan win overindex.nodeif it is newer — close Node, runinstall-addon, or delete stray*.staged.nodefiles after copying.- A forgotten old
index.nodecauses confusing behaviour (e.g. quantize tests failing with stale shape inference). Re-runnpm run install-addon -w @webnnjs/webnn-node-nativeafter every native rebuild.
If index.node is locked on Windows (a Node process still running), the install script writes index.staged.node instead. Close Node processes, then:
npm run install-addon -w @webnnjs/webnn-node-nativeOptional: npm run build:napi -w @webnnjs/webnn-node-native uses @napi-rs/cli instead (also regenerates index.d.ts from #[napi] exports).
Same pattern as rustnnpt: shallow-clone the WPT repo into .cache/wpt (never committed).
npm run test:wpt:fetchThis runs scripts/fetch-wpt.mjs, which:
- Creates
.cache/if needed - On first run: shallow sparse clone (
--depth 1,--filter=blob:none) of onlyinterfaces/andwebnn/→.cache/wpt - On later runs:
git fetch+reset --hard origin/master(same sparse paths; no full ~160k-file checkout)
Override the location with WPT_DIR (same as rustnnpt).
After fetch, useful paths include:
| Path | Contents |
|---|---|
.cache/wpt/interfaces/webnn.idl |
WebNN IDL |
.cache/wpt/webnn/conformance_tests/ |
WPT WebNN conformance tests |
IDL for codegen: npm run generate prefers .cache/wpt/interfaces/webnn.idl when the cache exists; otherwise it uses the vendored packages/webnn-node/idl/webnn.idl.
To refresh the committed vendor copy from the cache:
npm run test:wpt:fetch
npm run sync:idlRequires git on PATH for fetch (no Python needed for the cache step).
The runner executes only tests under .cache/wpt/webnn/conformance_tests/ (not the full WPT tree). Each subtest builds a graph with MLGraphBuilder, runs dispatch, and compares outputs with WPT tolerances.
npm run test:wpt:fetch
npm run test:wpt:run -- --op add --limit-tests 5Common options (pass after --):
| Flag | Description |
|---|---|
--op NAME |
Only files like add.https.any.js |
--file FILE |
Single file (basename) |
--limit-tests N |
Cap subtests per file |
--limit-files N |
Cap files |
--stop-on-fail |
Stop at first failure |
--report-json PATH |
Write JSON report |
Float16 WPT tests require Float16Array (Node 24+ by default, or Node 22 with --js-float16array).
Set ORT_DYLIB_PATH (or demo/.env) before running; the harness searches common ORT locations if unset.
Demos require a successful build and a valid ORT_DYLIB_PATH in demo/.env (or auto-discovered ORT).
Builds a 2×2 float32 add graph in TypeScript, executes it, and prints element-wise sums.
npm run demo:builderSkip the rebuild if already built:
npm --prefix demo run demo:builderExpected output:
Input A: [ 1, 2, 3, 4 ]
Input B: [ 5, 6, 7, 8 ]
A + B = [ 6, 8, 10, 12 ]
Source: demo/src/builder-add.ts
Downloads tarekziade/SmolLM-135M-webnn, loads the WebNN graph via MLContext, and runs autoregressive generation.
npm run demoSkip the rebuild:
npm run demo:runOptional overrides:
DEMO_PROMPT="The future of AI is" DEMO_MAX_NEW_TOKENS=32 npm run demo:runSource: demo/src/index.ts
On Unix, make can download ONNX Runtime and run the SmolLM demo with env vars set:
make install
make build
make demo # download ORT + build + run SmolLM demo
make demo-only # run SmolLM demo (already built)
make onnxruntime-download
make cleanThere is no make target for demo:builder yet; use npm run demo:builder.
Polyfill entry:
installWebNNPolyfill()— attachesnavigator.mlml.createContext(options)ml.loadModelFromHub(repoId, options)— Node-only Hub download helper
WebNN IDL types:
MLContext—createTensor,writeTensor,readTensor,dispatchMLGraphBuilder— full IDL operator set (codegen fromwebnn.idl), plusinput,constant,buildMLGraph,MLTensor,MLOperand
Rustnn extensions on MLContext:
rustnnResizeTensor,rustnnSetTensorCapacity— dynamic shapes for dispatch
Some IDL ops may still throw at runtime if rustnn or the native bridge does not implement them yet.
Regenerate builder bindings after IDL changes:
npm run test:wpt:fetch # optional: refresh WPT + use cached IDL
npm run generate -w @webnnjs/webnn-node