nacelle
v1.0.6
Published
Self-contained Node.js execution engine and virtual runtime in the browser via WebAssembly, Web Workers, and Virtual Filesystem
Downloads
1,228
Maintainers
Readme
nacelle
Self-contained Node.js execution engine and virtual runtime in the browser.
Powered by WebAssembly, Web Workers, Virtual Filesystems (VFS), and in-browser networking, nacelle lets you run Node.js code, standard libraries, and npm packages directly inside client browsers with zero server execution.
Node 22 Alpha
The alpha ships one target: Node 22, referenced against native Node 22.23.2
(module ABI 127, Node-API 10). latest, lts, n22, and the nacelle/v22
subpath all select that same runtime. Other majors and the current alias are
rejected instead of silently falling back.
Installation & Release Channels
Install nacelle using your preferred package manager and release channel:
# Install the Node 22 major channel
npm install nacelle@n22
# Or install the latest default release
npm install nacelle@latestCDN / Direct Browser Import (ESM)
You can import nacelle directly in browser scripts via modern CDNs:
<script type="module">
import { Nacelle } from 'https://esm.sh/nacelle@n22';
const node = await Nacelle.create({
files: {
'/app/index.js': `console.log('Hello from Node ' + process.version + ' in your browser!');`
}
});
const proc = await node.run({ entry: '/app/index.js' });
console.log(await proc.stdoutText());
// Run shell lines directly in the virtual filesystem
const shell = await node.bash('NODE_ENV=production echo "$NODE_ENV" | sed \'s/production/ready/\'');
console.log(await shell.stdoutText());
</script>Quick Start
1. High-Level API (Nacelle)
import { Nacelle } from 'nacelle';
// Initialize an in-browser Node engine instance
const node = await Nacelle.create({
cwd: '/workspace',
env: { NODE_ENV: 'development' },
files: {
'/workspace/server.js': `
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from in-browser HTTP server!' }));
});
server.listen(3000, () => {
console.log('Server listening on http://localhost:3000');
});
`
}
});
// Run a script in an isolated Web Worker child process (browser runtimes)
const proc = await node.run({ entry: '/workspace/server.js' });
// Read stdout
console.log(await proc.stdoutText());
// File System (VFS) Operations
await node.fs.writeFile('/workspace/data.txt', 'Hello VFS');
const content = await node.fs.readFile('/workspace/data.txt', 'utf8');
// In-Browser NPM package installation
await node.npm.install('express');Features
- 🌐 60+ Node Built-in Modules:
node:fs,node:http,node:http2,node:tls,node:crypto,node:buffer,node:stream,node:events,node:path,node:process,node:child_process,node:worker_threads,node:zlib,node:sqlite,node:net,node:dgram,node:dns,node:test,node:vm,node:v8,node:os,node:assert, and more. - ⚡ WebAssembly Artifact Loader: Integrity-checked, export-validated low-level
sqlite,zlib, and standard Node-API bridge artifacts. A listed artifact is not described as a working Node binding until a subsystem parity test wires and verifies it. - 📁 Virtual POSIX Filesystem (VFS): Isolated in-memory filesystem with synchronous and asynchronous operations, streams, and file descriptors.
- 🧵 Web Worker Isolation: Optional true multi-threaded process execution via Web Workers with IPC; browser Nacelle+ requests fail closed when the boundary is unavailable.
- 📦 In-Browser NPM Installer: Direct npm package resolution, tarball downloading, and untarring right inside the browser VFS.
- 🔒 Run-Scoped Capability Policy: Immutable grants for VFS, workers, network, npm, preview, persistence, host bridges, and named secrets, with auditable grant deltas.
- 🧰 Bounded Process Output: Streaming callbacks plus byte limits, tail retention, and dropped-byte accounting for untrusted workloads.
- ⏪ Content-Addressed Checkpoints: Workspace snapshots with metadata, diffs, rollback, and deterministic content identities.
- 🔎 Structured Failure Traces: Stable error codes, bounded event history, and secret-redacted run diagnostics.
- 🔑 Named Secret Broker: Origin-bound request signatures without exposing raw secret material to guest code.
- 🔒 Security & Isolation: Strict capability boundary architecture with zero server-side dependencies.
Subpath Exports
nacelle-> Main bundle (Nacelle,createRuntime,runtime)nacelle/v22-> Explicit Node v22 runtime entrynacelle/latest/nacelle/lts-> Alpha aliases for the v22 entrynacelle/runtime-> Low-level runtime assembly and module loadernacelle/worker-> Dedicated Web Worker process scriptnacelle/sw-> Virtual network gateway Service Workernacelle/wasm/*-> Integrity-checked WebAssembly artifacts listed by the selected release manifestnacelle/support/nacelle/version-> Shipped aliases, profile hashes, and WASM artifact-set identity
WASM adapters load lazily from the selected v22 manifest. A custom CDN or
application path can be supplied with wasmBaseUrl:
const node = await Nacelle.create({
wasmBaseUrl: new URL('/nacelle/v22/wasm/', location.href).href,
});
const artifact = await node.wasm.load('node_addon_napi');
console.log(artifact.path, artifact.bytes);Examples & Demos
Try out the interactive in-browser examples included in examples/:
# Start local examples server (Express in-browser IDE)
npm run examples
# Test the CDN-origin direct iframe flow with a local esm.sh simulation
npm run examples:cdn
# Run Vite + React in-browser IDE example
npm run examples:vite-react
# Inspect the shipped WASM artifacts and export contracts
npm run check:wasm
# Build and inspect every published package export
npm run verify:package
# Run Native WASM Addons example
npm run examples:wasm
# Try the inline bash compatibility demo
npm run examples:bash
# Try the TypeScript strip-and-run demo
npm run examples:typescript
# Try the browser proxy configuration demo
npm run examples:proxy
# Run the native Node-compatible proxy example without a browser
npm run examples:proxy-nativeEach example page includes a navigation menu (☰) at the top to easily switch between examples.
Optional Nacelle+ transport
For APIs that reject ordinary browser requests because of CORS, the optional
nacelle-plus/extension companion provides a capability-gated HTTP transport
for Chrome and Firefox. Nacelle remains the only runtime: native page fetch is
attempted first only for replay-safe GET and HEAD requests. Unsafe methods
are sent to the privileged adapter before any page fetch, because a browser
failure does not prove that a non-idempotent request was not sent. The Nacelle
run must explicitly grant its proxy capability; the extension's per-origin
permission is a separate check. See
nacelle-plus/README.md for setup, streaming, and
permission handling.
The supported Node release-line audit and upgrade policy are in
docs/node-version-support.md.
Run policy and recovery
Capabilities are validated once when a runtime is created and are available as
node.capabilities. Network methods are selected before a request is issued:
GET and HEAD may try the page fetch before a privileged fallback, while
unsafe methods go directly to the privileged adapter. Process output is bounded
by the run manifest; handle.stats('stdout') reports retained and dropped
bytes. Checkpoints are exposed through node.checkpoint(), node.diff(), and
node.rollback(). Secrets are available through node.secretBroker only as
named, origin-bound signatures.
Alpha build and release commands
npm run versions
npm run build
npm run validate:versions
npm run check:wasm
npm run verify:package
npm run parity
npm run test:fullThe release gate covers the JavaScript runtime, builds, artifacts, and browser
workloads. Python harness tests run separately with npm run test:python.
npm run publish:n22 -- --dry-run exercises the release path without
uploading. The live command publishes the major-scoped n22 tag first and promotes latest and
lts only after the explicit release gate.
Inline shell execution
node.bash(command, options) runs the supported POSIX shell subset against the virtual filesystem and returns a normal ProcessHandle. It supports npm-style command lists, environment assignments, PATH lookup, pipes, redirects, globbing, and common commands including mv, cp, ls, ps, grep, cat, find, cut, tr, sort, uniq, tee, head, tail, wc, mkdir, rm, and touch:
const proc = await node.bash(`
mkdir -p dist &&
echo "built in $NODE_ENV" > dist/status.txt &&
cat dist/status.txt
`, { env: { NODE_ENV: 'production' } });
console.log(await proc.stdoutText());The TypeScript demo showcases three conversion pipelines from inline bash build scripts: Node.js 22 built-in module.stripTypeScriptTypes(), Vite's fast transform pipeline (vite build), and the official Microsoft TypeScript compiler (tsc), then executes the emitted JavaScript.
Run CITGM in Chromium or Firefox
The Playwright adapter preloads the pinned CITGM CLI and the candidate package's registry metadata, tarballs, and dependency graph into Nacelle's browser-side npm cache, then executes CITGM itself as a Nacelle child process. Preloading does not run candidate tests. Playwright only hosts the browser page and selects the browser engine; CITGM, the candidate package, npm commands, and package tests stay inside the browser runtime.
Install the adapter dependencies and browser binaries once:
npm --prefix dev/adapters/playwright install
npx --prefix dev/adapters/playwright playwright install chromium firefoxGenerate the host-side artifact once per CITGM version, candidate package, and registry. This uses host npm only to fetch package data with lifecycle scripts disabled; it does not run the candidate. The generated cache is ignored by git and is consumed as local assets by the browser page:
npm run citgm:precache -- expressRun one registry package through either engine:
npm run citgm:browser:chromium -- express
npm run citgm:browser:firefox -- expressCITGM agent workflow and gates
Use Node 22.23.2 for every native CITGM, browser CITGM, build, and test
command. Find the first unprocessed table row marked BLOCKED or
GATE-BLOCKED in CITGM-TOP-100-STATUS.md, and
work forward in rank order. The status table is the source of truth for the
next candidate; re-check it before each run because the cursor changes after
every committed CITGM result.
Prepare a Node 22 shell first. The temporary npm link matters for native CITGM:
CITGM launches package-manager scripts as node <npm-path>, so a shell-wrapper
npm can otherwise produce a false native failure before the package test runs.
export NODE22_HOME="$(mise where [email protected])"
export PATH="$NODE22_HOME/bin:$PATH"
export NATIVE_NPM_DIR="$(mktemp -d)"
ln -s "$NODE22_HOME/lib/node_modules/npm/bin/npm-cli.js" "$NATIVE_NPM_DIR/npm"
ln -s "$NODE22_HOME/lib/node_modules/npm/bin/npx-cli.js" "$NATIVE_NPM_DIR/npx"
export NATIVE_PATH="$NATIVE_NPM_DIR:$PATH"
node --version # v22.23.2
PATH="$NATIVE_PATH" node "$NODE22_HOME/lib/node_modules/npm/bin/npm-cli.js" \
exec --yes [email protected] -- sh -c \
'unset npm_config_package npm_command npm_lifecycle_event npm_lifecycle_script; exec citgm "$@"' \
sh <package> --tmpDir /tmp
# Keep the candidate checkout under the POSIX /tmp root, matching native CITGM.
# This prevents legacy tools from resolving unrelated /node ancestor packages.
npm run citgm:browser:chromium -- <package> --tmpDir /tmp
npm run citgm:browser:firefox -- <package> --tmpDir /tmpThe full repository Playwright suites are separate from the two browser CITGM runs. Follow these rules exactly:
Always commit the completed CITGM run's artifacts, logs, and truthful status update before moving on, whether the candidate passed, failed, or was classified as blocked. Never start another CITGM with a dirty worktree from the previous run.
If the candidate passes without any source, runtime, harness, or regression test changes, the native and two browser CITGM results are the gate. Do not run the full Chromium and Firefox Playwright suites for that candidate; commit the artifacts and advance.
If any source, runtime, harness, or regression-test changes are made, stop before running a different CITGM candidate. Rebuild and run the complete repository gate set first:
npm run build:v22 npm run check:wasm node --test --test-concurrency=1 \ dev/tests/runtime/runtime/patch-regressions.mjs \ dev/tests/runtime/runtime/patch-source-regressions.mjs \ dev/tests/runtime/runtime/patch-messaging-regressions.mjs \ dev/tests/runtime/runtime/patch-crypto-regressions.mjs npm test npm run test:browser:chromium npm run test:browser:firefoxOnly after every gate passes may the next CITGM candidate start. Commit the fix, permanent regression tests, gate logs, and CITGM artifacts before advancing. The artifact corpus is ignored by default, so force-add the authoritative run directory explicitly, for example:
git add -f artifacts/citgm-top-100/rank-<rank>-<package>/<run-dir> git commit -m "record <package> CITGM run"
The full repository Playwright suites are never a substitute for the candidate CITGM: when changes are present, run them only after that candidate has passed the real upstream CITGM in both Chromium and Firefox, then use their results as the final repository gate.
Treat a browser-only failure as an ours-side runtime or harness failure until the same package, git revision, and failing path have been reproduced under native Node 22. A package may be recorded as an upstream or dependency blocker only when that external oracle supports the classification. Preserve complete logs and explain the classification in the status record. Temporary tracing, diagnostic hooks, and scratch directories must be removed before the final commit; retain only general runtime fixes and permanent regression tests.
If no matching artifact exists, the runner falls back to direct browser registry
fetches. Arguments after the module are forwarded to CITGM. The runner currently
targets registry packages; local-directory and native-addon cases need an
explicit browser-safe fixture or capability before they can be meaningful.
Use NACELLE_CITGM_VERSION to select a different CITGM release and
NACELLE_CITGM_TIMEOUT_MS to change the outer browser-run timeout.
Set NACELLE_NPM_REGISTRY when generating and consuming an
alternate registry artifact.
Next.js 16 App Router
Nacelle runs full Next.js App Router applications entirely in-browser without host subprocesses. The Next.js demo (npm run examples:next) demonstrates Server-Side Rendering (SSR), file-system routing (app/page.tsx, app/about/page.tsx, app/dashboard/page.tsx), Server Actions & API endpoints (app/api/hello/route.ts), and Next.js CLI orchestration (next dev, next build, next start). Nacelle advertises the WebContainer runtime signal that makes stock Next.js select its own official SWC WebAssembly fallback (@next/swc-wasm-nodejs) instead of its platform .node package.
The demo's next start button builds the current sources when needed, then starts the production server. next build can also run separately. Production builds use one worker with worker threads to fit the browser's memory budget, and generated files remain available between commands.
Proxy configuration
createProxyConfig() provides one small, explicit configuration for HTTP(S)
environment proxy routing. It normalizes proxy URLs, mirrors upper- and
lower-case environment keys, supports NO_PROXY, and enables routing for the
virtual Node process:
import { Nacelle, createProxyConfig } from 'nacelle';
const node = await Nacelle.create({
proxy: createProxyConfig({
httpProxy: 'http://127.0.0.1:3128',
httpsProxy: 'http://127.0.0.1:3128',
noProxy: ['localhost', '127.0.0.1:3000'],
}),
});The native example uses standard node:http createServer() and get() calls
inside Nacelle. The first request is routed to the proxy, while the proxy's
upstream request temporarily disables environment routing to avoid a loop.
License
MIT © Nacelle Contributors
Virtual HTTP previews: Service Worker or direct iframe
Nacelle.create({ gateway: true }) selects a gateway from the loaded module's
import.meta.url and the host page's origin. Same-origin deployments use the
Service Worker when available. Cross-origin imports (including esm.sh), missing
Service Worker support, and failed automatic worker registration use a direct
iframe/message-channel gateway. Cross-origin automatic selection does not try
/runtime/gateway-sw.js first. Explicit worker failures reject creation.
// CDN / no worker asset:
const node = await Nacelle.create({ gateway: { mode: 'direct-iframe' } });
// Same-origin worker deployment:
// const node = await Nacelle.create({
// gateway: { mode: 'service-worker', swPath: '/runtime/gateway-sw.js', scope: '/' },
// });
node.on('gateway-diagnostic', error => console.error(error.code, error.message));
console.log(node.gateway.mode, node.gateway.reason, node.gateway.policy);
// Start a virtual HTTP server before connecting.
const disconnect = node.connectIframe(document.querySelector('iframe'), {
port: 3000,
path: '/hello?name=Developer',
onNavigate: cleanPath => { addressInput.value = cleanPath; },
onError: error => { errorOutput.textContent = `${error.code}: ${error.message}`; },
});
// Additional methods on a direct connection:
await disconnect.navigate?.('/another-page');
disconnect.back?.();
disconnect(); // idempotent; cancels requests and sockets
await node.shutdown(); // closes every connection and process
// node.reset() also closes direct connections before resetting the runtime.gateway: false disables iframe gateway setup. The read-only node.gateway
diagnostic retains up to 128 errors and exposes active sessions, clean virtual
paths, request counts, Blob ownership, and the resolved policies. The existing
getVirtualUrl() helper is for Service Worker navigation; do not assign its
result to a direct iframe's src. Use connectIframe and its direct navigation
handle instead.
Direct mode uses sandbox="allow-scripts allow-forms", an opaque origin, fresh
nonces and MessagePorts, and a deny-by-default resource CSP. The parent never
uses host fetch for virtual HTTP, assets or sockets, and a missing virtual
listener cannot fall through to a configured Nacelle+ proxy. Request credentials,
authorization and cookie headers are not forwarded. The session's preview port
must be granted by the runtime's capability manifest.
Defaults are a 30-second request/navigation timeout, 16 MiB per request or
response body and aggregate document assets, 32 combined HTTP/WebSocket
operations, 20 redirects, and 1,024 document resources. Set requestTimeoutMs,
maxBodyBytes, and maxConcurrentRequests in the gateway options to change the
first three. WebSockets have a handshake timeout; open sockets count against the
concurrency cap until closed, and each message is size-limited.
HTTP fetch bodies stream over the channel; XHR buffers its eventual result and HTML navigation is buffered within the limit for inert parsing and resource rewriting. Scripts, literal module dependencies, stylesheets, recursive CSS URLs/imports, images, media, links, forms and virtual history are supported. The parent owns the resource catalog; the opaque document creates local Blob mirrors because it cannot reliably fetch another origin's Blob URLs. Both sets are revoked on navigation/close. Classic scripts run in document order after staging the DOM; deferred/module scripts run afterward. This is not a complete emulation of parser-blocking HTML execution.
Nested frames, external resources, custom rewriters, import maps, computed or
cyclic module imports, encoded responses, integrity metadata, srcset, escaped
CSS URLs, and unsupported browser networking APIs produce structured errors.
Application CSP/framing headers that cannot survive origin/resource rewriting
reject navigation instead of being silently weakened. A restrictive host CSP
can also block the fixed inline bootstrap; direct mode does not bypass it.
Security boundary: the opaque iframe prevents access to parent DOM/storage;
it is not a general network-egress sandbox for arbitrary hostile JavaScript.
Browser sandbox/CSP do not prevent every self-navigation through location.
Intercepted APIs never fall back to host networking, but an unmanaged navigation
is detected and closed only on load, potentially after its network request.
Deploy a browser-level network restriction when complete egress isolation is
required. The existing same-origin Service Worker mode has a different trust
boundary and is not made opaque by this change. Neither transport changes the
runtime's separate inline versus worker guest execution isolation policy.
See examples/direct-iframe-cdn.html for an esm.sh/Express example. Run
npm run examples:cdn to serve that page on one origin and the built Nacelle
package behind an esm.sh-shaped static origin on another. The fixture also
proxies npm registry requests needed by the example's browser install. See
docs/direct-iframe-gateway-validation.md for the test commands, coverage and
outstanding live-environment acceptance checks. Pin a release containing this
implementation before deploying the CDN example.
