NanoVM runs the real, unmodified Node.js v25 binary — plus npm, TypeScript, ESLint and Prettier — entirely client-side. No server, no container, no install. An open-source alternative to WebContainers.
Other in-browser runtimes ship a reimplemented Node. NanoVM runs the actual Node.js executable — the same binary you’d install on a Linux server — instruction by instruction on an emulated RISC-V CPU. If it works on Node, it works here: the same APIs, the same versions, the same edge cases.
import { createNano, nanoImage } from "@userland-run/nano-sdk";
// boot the VM — real Node + the npm toolchain, 100% in the browser
const nano = await createNano({
image: nanoImage({ baseUrl: "/nano/", withNode: true, withDevenv: true }),
});
// write a TypeScript file into the in-memory filesystem
nano.fs.writeFile(
"/app/main.ts",
'const who: string = "RISC-V";\nconsole.log(`hello from ${who}`);',
);
// type-check + run it with the real tsc and node binaries
const { stdout } = await nano.shExec("cd /app && npx tsc main.ts && node main.js");
console.log(stdout); // hello from RISC-VNeed a tight run loop? Snapshot V8 once and restore it per run with the Node fast path.
// snapshot V8 once, then restore it per run for a fast loop
const rt = nano.nodeRuntime();
await rt.warmup();
await rt.run("console.log(process.version)"); // → v25.4.0fetch() (HTTP/HTTPS only, subject to CORS), so there are no raw TCP sockets yet. And, like any shared-memory tool, the page must be cross-origin isolated (COOP/COEP) — the SDK ships a service worker that arranges this for you.The SDK is a typed, ESM, zero-dependency package. Install it, point it at the wasm, and you have a Linux dev environment running in your users’ browsers.