Example · react-studio
A Vite-built React 19 app, served from NanoVM
node · host JS engine
A small interactive React 19 app — a counter, a Fibonacci memo, a list — built with Vite. npm run demos:build installs the pinned toolchain with npm ci and runs vite build; the output is index.html plus one JS file with every asset inlined.
The build happens ahead of time, on the developer machine or in CI. In the browser, a dependency-free Node.js static server (server.js) serves dist/ from NanoVM's filesystem into the preview iframe, where React mounts and runs as usual.
Run it
Serve the React build
Seeds the built dist/ into /work, then runs this static server. The app in the preview is live — click its buttons.
javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside // NanoVM; serves the React app to the preview iframe via the service-worker bridge. const http = require("http"); const fs = require("fs"); const path = require("path"); // dist/ relative to the working directory (`cd <demo> && node server.js`). const ROOT = path.resolve("dist"); const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".map": "application/json", }; const server = http.createServer(function (req, res) { let url = req.url.split("?")[0]; if (url === "/" || url === "") url = "/index.html"; const file = path.join(ROOT, url); fs.readFile(file, function (err, data) { if (err) { // SPA fallback → index.html fs.readFile(path.join(ROOT, "index.html"), function (e2, html) { if (e2) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "Content-Type": TYPES[".html"] }); res.end(html); }); return; } res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" }); res.end(data); }); }); server.listen(3000, function () { console.log("react-studio listening on 3000"); });
Expected output
react-studio
How it runs
- The files of
dist/are fetched from/demo-files/react-studio/dist/and written to/work/dist/. server.jsstarts withnano.node()on the host JS engine and servesdist/on port 3000.- The service-worker bridge routes the iframe to it; Vite's
base: "./"keeps asset URLs relative to the preview path.
Source
From demos/react-studio/.
src/App.jsx
jsx
import { useEffect, useMemo, useState } from "react";
const C = {
bg: "#0e0e10",
panel: "#16161c",
border: "#2a2a31",
text: "#cfcfda",
dim: "#8c8c97",
cyan: "#5fd6d1",
violet: "#a984f5",
amber: "#e6b45a",
};
function Stat({ label, value, color }) {
return (
<div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: "12px 14px" }}>
<div style={{ fontSize: 10, letterSpacing: ".12em", textTransform: "uppercase", color: C.dim }}>{label}</div>
<div style={{ fontSize: 20, marginTop: 4, color: color || C.text, fontVariantNumeric: "tabular-nums" }}>{value}</div>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0);
const [tick, setTick] = useState(0);
const [items, setItems] = useState(["render", "hydrate", "reconcile"]);
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
return () => clearInterval(id);
}, []);
const fib = useMemo(() => {
let a = 0, b = 1;
for (let i = 0; i < count; i++) [a, b] = [b, a + b];
return a;
}, [count]);
return (
<div
style={{
minHeight: "100vh",
margin: 0,
background: C.bg,
color: C.text,
fontFamily: "ui-monospace, monospace",
padding: "28px 22px",
}}
>
<div style={{ maxWidth: 560, margin: "0 auto" }}>
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
<h1 style={{ margin: 0, fontSize: 19, color: C.violet }}>react-studio</h1>
<span style={{ fontSize: 11, color: C.dim }}>vite build · served from NanoVM</span>
</div>
<p style={{ fontSize: 12, color: C.dim, marginTop: 6 }}>
A React 19 app, pre-built with Vite, served by a Node.js http server running inside NanoVM in this tab.
</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10, margin: "16px 0" }}>
<Stat label="uptime" value={`${tick}s`} color={C.cyan} />
<Stat label="count" value={count} color={C.violet} />
<Stat label={`fib(${count})`} value={String(fib).slice(0, 9)} color={C.amber} />
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => setCount((c) => c + 1)} style={btn(C.violet)}>+ increment</button>
<button onClick={() => setCount(0)} style={btn(C.dim)}>reset</button>
<button onClick={() => setItems((x) => [...x, `effect ${x.length}`])} style={btn(C.cyan)}>+ row</button>
</div>
<ul style={{ listStyle: "none", padding: 0, marginTop: 16 }}>
{items.map((it, i) => (
<li
key={i}
style={{
display: "flex",
justifyContent: "space-between",
padding: "9px 12px",
background: C.panel,
border: `1px solid ${C.border}`,
borderRadius: 8,
marginBottom: 6,
fontSize: 13,
}}
>
<span>{it}</span>
<span style={{ color: C.dim }}>#{i}</span>
</li>
))}
</ul>
</div>
</div>
);
}
function btn(color) {
return {
border: `1px solid ${color}`,
background: "transparent",
color,
fontFamily: "inherit",
fontSize: 12,
padding: "7px 14px",
borderRadius: 7,
cursor: "pointer",
};
}src/main.jsx
jsx
import React from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
// Render the first frame synchronously, so the page is complete by the time
// its load event fires (the live sample checks the preview's text on load).
const root = createRoot(document.getElementById("root"));
flushSync(() =>
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
),
);index.html
html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>nano · react-studio</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>server.js
javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside // NanoVM; serves the React app to the preview iframe via the service-worker bridge. const http = require("http"); const fs = require("fs"); const path = require("path"); // dist/ relative to the working directory (`cd <demo> && node server.js`). const ROOT = path.resolve("dist"); const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".map": "application/json", }; const server = http.createServer(function (req, res) { let url = req.url.split("?")[0]; if (url === "/" || url === "") url = "/index.html"; const file = path.join(ROOT, url); fs.readFile(file, function (err, data) { if (err) { // SPA fallback → index.html fs.readFile(path.join(ROOT, "index.html"), function (e2, html) { if (e2) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "Content-Type": TYPES[".html"] }); res.end(html); }); return; } res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" }); res.end(data); }); }); server.listen(3000, function () { console.log("react-studio listening on 3000"); });
vite.config.js
javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// base: "./" → relative asset URLs, so the build works when served under the
// preview path (/sw/<port>/) instead of the origin root.
export default defineConfig({
base: "./",
plugins: [react()],
build: { assetsInlineLimit: 100000000 }, // inline everything → single JS, no binary assets
});package.json
json
{
"name": "nano-react-studio",
"private": true,
"type": "module",
"scripts": { "build": "vite build" },
"dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
"devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }
}