Node.js runtime

JavaScript runtime for WASM, built with QuickJS targeting WASI

Kumar Anirudha

Table of content
  1. Status
  2. At a glance
  3. Capabilities
  4. Limitations
  5. Install
  6. Usage examples
  7. CommonJS require()
  8. Use from Rust
  9. Building from source
  10. Technical notes
  11. Roadmap

Status

Available — nodejs-20.wasm is fully working. Built with QuickJS compiled to WASM via the WASI SDK.

At a glance

Engine QuickJS 2024-01-13 (ES2020)
Node.js compat v20.x API surface
Binary size ~1.1 MB (optimized)
Target wasm32-wasi (WASI Preview 1)
License MIT
Source https://bellard.org/quickjs/

Capabilities

Limitations

Install

wasmhub get nodejs 20

Usage examples

# Print version info
wasmrun exec nodejs-20.wasm -- version

# Evaluate JavaScript
wasmrun exec nodejs-20.wasm -- eval "1 + 1"
# → 2

# Complex expressions
wasmrun exec nodejs-20.wasm -- eval "[1,2,3].map(x => x * x).join(',')"
# → 1,4,9

# Echo arguments
wasmrun exec nodejs-20.wasm -- echo hello world
# → hello world

# Print env
wasmrun exec nodejs-20.wasm -- env

# Run a JS file (requires --dir mount)
wasmrun exec --dir /path/to/scripts nodejs-20.wasm -- run /path/to/scripts/app.js

CommonJS require()

A worked example is in tests/runtimes/nodejs/fixtures/:

// app.js
const path = require("path");
const { square } = require("./math");
const config = require("./config.json");
const greet = require("greet");          // resolves via node_modules/greet/package.json

console.log(square(4), config.name, greet("world"));
console.log("entry:", path.basename(__filename));
console.log("require.main===module:", require.main === module);

Resolution rules (mirroring Node.js for the supported subset):

  1. Built-in — path, fs, os, node:path, node:fs, node:os.
  2. Relative / absolute — ./x, ../x, /abs/x. Tries x, x.js, x.json, x/package.json main field, x/index.js, x/index.json.
  3. Bare specifier — walks up from the requiring file's directory, looking for node_modules/<name> and applying the same file/dir rules.

Modules are evaluated inside new Function('exports','require','module','__filename','__dirname', src), the same wrapper Node.js uses. Cached in require.cache keyed by resolved filename.

Use from Rust

use wasmhub::{RuntimeLoader, Language};

let loader = RuntimeLoader::new()?;
let nodejs = loader.get_runtime(Language::NodeJs, "20").await?;
// Pass nodejs.path to your WASM runtime (wasmtime, wasmrun, etc.)

Building from source

just build-nodejs

Requires Docker (runs inside wasmhub-builder). The build:

  1. Downloads QuickJS 2024-01-13 source
  2. Compiles main.js to C bytecode via native qjsc
  3. Cross-compiles all sources with WASI SDK clang (wasm32-wasi target)
  4. Links with 8 MB C stack (required for QuickJS's parser depth)
  5. Optimizes with wasm-opt -O3

Technical notes

The runtime is built from QuickJS rather than full Node.js because Node.js (V8 + libuv) cannot currently compile to WASM/WASI. QuickJS is a complete ES2020 engine in ~210 KB of C, and compiles cleanly with the WASI SDK.

Three non-obvious build issues were debugged and fixed:

Roadmap