userland.rundocs70Try the demo
Getting started

Quick start

Install @userland-run/nano-sdk, serve the page cross-origin isolated, boot a VM and run your first command. The samples below are the same calls, running in this page.

1. Install the SDK

bash
npm install @userland-run/nano-sdk

The package is ESM-only, fully typed and has no runtime dependencies. The NanoVM runtime is vendored inside it. Licensed MPL-2.0 OR LicenseRef-UEL.

2. Serve the WebAssembly images

The SDK does not bundle the VM images; you serve them as static files. Two builds matter:

File
Size
Contains
Use for
console.wasm (built as nano.busybox.wasm)
3,229,271 bytes
emulator + BusyBox 1.36.1
terminals and shell work; boots instantly
nano.wasm (slim)
2,458,155 bytes
emulator only
installing BusyBox, Node.js and tools from the catalog at runtime
boa.wasm
2,813,775 bytes
the Boa JavaScript engine
scripting only; loaded on first use

Build them from the nano repository (make build-busybox, make build, make build-boa) or copy them from this site's /nano/ directory. Also copy the SDK's service worker (@userland-run/nano-sdk/service-worker, i.e. static/nano-sw.js) to your public root, and its dist/vendor/ directory to /vendor/ (the host-engine Node and wasm runners load their modules from there when a bundler has moved the SDK).

3. Make the page cross-origin isolated

NanoVM needs a shared WebAssembly.Memory, which browsers only allow on cross-origin isolated pages. Send these two headers, or let the shipped service worker add them. The details are on Requirements.

text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

4. Boot and run

javascript
import { createNano, ensureCrossOriginIsolated } from "@userland-run/nano-sdk";

if (!(await ensureCrossOriginIsolated({ swUrl: "/nano-sw.js" }))) {
  // the page is reloading once to pick up the headers
} else {
  const nano = await createNano({
    image: { wasm: "/nano/console.wasm" },
    ramMB: 512,
    engines: { node: "host" },
  });
  const { stdout } = await nano.shExec("echo hello from RISC-V");
  console.log(stdout);
}

This page boots exactly that configuration when you press Run. In every sample, nano is the booted instance, print() writes to the output pane and sdk is the imported module.

SDK · JavaScript

Run a shell pipeline

fs.writeFile seeds a file synchronously; shExec runs a full sh line with pipes on the RISC-V CPU and resolves with { exitCode, stdout }.

javascript
nano.fs.writeFile("/work/scores.txt", "ada 92\nlinus 87\ngrace 99\n");
const r = await nano.shExec("sort -k2 -n -r /work/scores.txt | head -1");
print(r.stdout.trim(), "(exit " + r.exitCode + ")");
loading…
Expected output
grace 99 (exit 0)
Node.js

Run a Node.js program

A plain Node.js script. On this page it runs on the host JS engine (engines: { node: "host" }), so it starts in milliseconds.

javascript
const os = require("os");
const path = require("path");
console.log("node", process.version, "on", os.platform());
console.log(path.join("/work", "src", "..", "index.js"));
loading…
Expected output
node v25.4.0 on linux
/work/index.js
shell

Keep state between commands

The SDK Shell tracks the working directory and exported variables across lines, like a terminal session.

bash
mkdir -p project && cd project
export GREETING=hi
echo "$GREETING from $(pwd)"
loading…
Expected output
hi from /work/project

5. Next steps

Building NanoVM from source

You only need this to change the emulator itself. The nano repository builds with a nightly Rust toolchain (rust-toolchain.toml pins nightly and the wasm32-unknown-unknown target).

bash
git clone https://github.com/userland-run/nano
cd nano
make build          # slim wasm/nano.wasm
make build-busybox  # wasm/nano.busybox.wasm (= console.wasm)
node test/run.mjs runners/riscv/images/busybox --cmd echo "Hello from RISC-V"

See the build guide for every make target and the command-line runner for test/run.mjs.