Node.js in your browser. Just like that.
A lightweight, browser-native Node.js runtime environment. Run Node.js code, install npm packages, and develop with Vite or Next.js - all without a server.
MIT License TypeScript Node.js
Built by the creators of Macaly.com — a tool that lets anyone build websites and web apps, even without coding experience. Think Claude Code for non-developers.
Warning: This project is experimental and may contain bugs. Use with caution in production environments.
- Virtual File System - Full in-memory filesystem with Node.js-compatible API
- Node.js API Shims - 40+ shimmed modules (
fs,path,http,events, and more) - npm Package Installation - Install and run real npm packages in the browser with automatic bin stub creation
- Run Any CLI Tool - npm packages with
binentries (vitest, eslint, tsc, etc.) work automatically - Dev Servers - Built-in Vite and Next.js development servers
- Hot Module Replacement - React Refresh support for instant updates
- TypeScript Support - First-class TypeScript/TSX transformation via esbuild-wasm
- Service Worker Architecture - Intercepts requests for seamless dev experience
- Optional Web Worker Support - Offload code execution to a Web Worker for improved UI responsiveness
- Secure by Default - Cross-origin sandbox support for running untrusted code safely
- Node.js 20+ - Required for development and building
- Modern browser - Chrome, Firefox, Safari, or Edge with ES2020+ support
Note: almostnode runs in the browser and emulates Node.js 20 APIs. The Node.js requirement is only for development tooling (Vite, Vitest, TypeScript).
npm install almostnode
import { createContainer } from 'almostnode'; // Create a Node.js container in the browser const container = createContainer(); // Execute JavaScript code directly const result = container.execute(` const path = require('path'); const fs = require('fs'); // Use Node.js APIs in the browser! fs.writeFileSync('/hello.txt', 'Hello from the browser!'); module.exports = fs.readFileSync('/hello.txt', 'utf8'); `); console.log(result.exports); // "Hello from the browser!"
⚠️ Security Warning: The example above runs code on the main thread with full access to your page. Do not usecreateContainer()orcontainer.execute()with untrusted code. For untrusted code, usecreateRuntime()with a cross-origin sandbox - see Sandbox Setup.
import { createRuntime, VirtualFS } from 'almostnode'; const vfs = new VirtualFS(); // Create a secure runtime with cross-origin isolation const runtime = await createRuntime(vfs, { sandbox: 'https://your-sandbox.vercel.app', // Deploy with generateSandboxFiles() }); // Now it's safe to run untrusted code const result = await runtime.execute(untrustedCode);
See Sandbox Setup for deployment instructions.
import { createContainer } from 'almostnode'; const container = createContainer(); const { vfs } = container; // Pre-populate the virtual filesystem vfs.writeFileSync('/src/index.js', ` const data = require('./data.json'); console.log('Users:', data.users.length); module.exports = data; `); vfs.writeFileSync('/src/data.json', JSON.stringify({ users: [{ name: 'Alice' }, { name: 'Bob' }] })); // Run from the virtual filesystem const result = container.runFile('/src/index.js');
import { createContainer } from 'almostnode'; const container = createContainer(); // Install a package await container.npm.install('lodash'); // Use it in your code container.execute(` const _ = require('lodash'); console.log(_.capitalize('hello world')); `); // Output: Hello world
import { createContainer } from 'almostnode'; const container = createContainer(); // Write a package.json with scripts container.vfs.writeFileSync('/package.json', JSON.stringify({ name: 'my-app', scripts: { build: 'echo Building...', test: 'vitest run' } })); // Run shell commands directly const result = await container.run('npm run build'); console.log(result.stdout); // "Building..." await container.run('npm test'); await container.run('echo hello && echo world'); await container.run('ls /');
Supported npm commands: npm run <script>, npm start, npm test, npm install, npm ls.
Pre/post lifecycle scripts (prebuild, postbuild, etc.) run automatically.
Any npm package with a bin field works automatically after install — no configuration needed.
// Install a package that includes a CLI tool await container.npm.install('vitest'); // Run it directly — bin stubs are created in /node_modules/.bin/ const result = await container.run('vitest run'); console.log(result.stdout); // Test results
This works because npm install reads each package's bin field and creates executable scripts in /node_modules/.bin/. The shell's PATH includes /node_modules/.bin, so tools like vitest, eslint, tsc, etc. resolve automatically.
For commands that run continuously (like watch mode), use streaming callbacks and abort signals:
const controller = new AbortController(); await container.run('vitest --watch', { onStdout: (data) => console.log(data), onStderr: (data) => console.error(data), signal: controller.signal, }); // Send input to the running process container.sendInput('a'); // Press 'a' to re-run all tests // Stop the command controller.abort();
import { VirtualFS, NextDevServer, getServerBridge } from 'almostnode'; const vfs = new VirtualFS(); // Create a Next.js page vfs.mkdirSync('/pages', { recursive: true }); vfs.writeFileSync('/pages/index.jsx', ` import { useState } from 'react'; export default function Home() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> <button onClick={() => setCount(c => c + 1)}>+</button> </div> ); } `); // Start the dev server const server = new NextDevServer(vfs, { port: 3000 }); const bridge = getServerBridge(); await bridge.initServiceWorker(); bridge.registerServer(server, 3000); // Access at: /__virtual__/3000/
almostnode uses a Service Worker to intercept HTTP requests and route them to virtual dev servers (e.g., ViteDevServer, NextDevServer).
Note: The service worker is only needed if you're using dev servers with URL access (e.g.,
/__virtual__/3000/). If you're only executing code withruntime.execute(), you don't need the service worker.
| Use Case | Setup Required |
|---|---|
| Cross-origin sandbox (recommended for untrusted code) | generateSandboxFiles() - includes everything |
| Same-origin with Vite | almostnodePlugin from almostnode/vite |
| Same-origin with Next.js | getServiceWorkerContent from almostnode/next |
| Same-origin with other frameworks | Manual copy to public directory |
When using createRuntime() with a cross-origin sandbox URL, the service worker must be deployed with the sandbox, not your main app.
The generateSandboxFiles() helper generates all required files:
import { generateSandboxFiles } from 'almostnode'; import fs from 'fs'; const files = generateSandboxFiles(); // Creates: index.html, vercel.json, __sw__.js fs.mkdirSync('sandbox', { recursive: true }); for (const [filename, content] of Object.entries(files)) { fs.writeFileSync(`sandbox/${filename}`, content); } // Deploy to a different origin: // cd sandbox && vercel --prod
Generated files:
| File | Purpose |
|---|---|
index.html |
Sandbox page that loads almostnode and registers the service worker |
vercel.json |
CORS headers for cross-origin iframe embedding |
__sw__.js |
Service worker for intercepting dev server requests |
See Sandbox Setup for full deployment instructions.
For trusted code using dangerouslyAllowSameOrigin: true:
// vite.config.ts import { defineConfig } from 'vite'; import { almostnodePlugin } from 'almostnode/vite'; export default defineConfig({ plugins: [almostnodePlugin()] });
The plugin serves /__sw__.js automatically during development.
Custom path:
// vite.config.ts almostnodePlugin({ swPath: '/custom/__sw__.js' }) // Then in your app: await bridge.initServiceWorker({ swUrl: '/custom/__sw__.js' });
For trusted code using dangerouslyAllowSameOrigin: true:
App Router:
// app/__sw__.js/route.ts import { getServiceWorkerContent } from 'almostnode/next'; export async function GET() { return new Response(getServiceWorkerContent(), { headers: { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache', }, }); }
Pages Router:
// pages/api/__sw__.ts import { getServiceWorkerContent } from 'almostnode/next'; import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { res.setHeader('Content-Type', 'application/javascript'); res.setHeader('Cache-Control', 'no-cache'); res.send(getServiceWorkerContent()); }
Initialize with the correct path:
// App Router (file-based route) await bridge.initServiceWorker({ swUrl: '/__sw__.js' }); // Pages Router (API route) await bridge.initServiceWorker({ swUrl: '/api/__sw__' });
Available exports from almostnode/next:
| Export | Description |
|---|---|
getServiceWorkerContent() |
Returns the service worker file content as a string |
getServiceWorkerPath() |
Returns the absolute path to the service worker file |
Copy the service worker to your public directory:
cp node_modules/almostnode/dist/__sw__.js ./public/
Or programmatically:
import { getServiceWorkerPath } from 'almostnode/next'; import fs from 'fs'; fs.copyFileSync(getServiceWorkerPath(), './public/__sw__.js');
| Feature | almostnode | WebContainers |
|---|---|---|
| Bundle Size | ~250KB gzipped | ~2MB |
| Startup Time | Instant | 2-5 seconds |
| Execution Model | Main thread or Web Worker (configurable) | Web Worker isolates |
| Shell | just-bash (POSIX subset) |
Full Linux kernel |
| Native Modules | Stubs only | Full support |
| Networking | Virtual ports | Real TCP/IP |
| Use Case | Lightweight playgrounds, demos | Full development environments |
- Building code playgrounds or tutorials
- Creating interactive documentation
- Prototyping without server setup
- Educational tools
- Lightweight sandboxed execution
import { createContainer } from 'almostnode'; function createPlayground() { const container = createContainer(); return { run: (code: string) => { try { const result = container.execute(code); return { success: true, result: result.exports }; } catch (error) { return { success: false, error: error.message }; } }, reset: () => container.runtime.clearCache(), }; } // Usage const playground = createPlayground(); const output = playground.run(` const crypto = require('crypto'); module.exports = crypto.randomUUID(); `); console.log(output); // { success: true, result: "550e8400-e29b-..." }
- Full-fidelity Node.js development
- Running native modules
- Complex build pipelines
- Production-like environments
Creates a new container with all components initialized.
interface ContainerOptions { cwd?: string; // Working directory (default: '/') env?: Record<string, string>; // Environment variables onConsole?: (method: string, args: any[]) => void; // Console hook } const container = createContainer({ cwd: '/app', env: { NODE_ENV: 'development' }, onConsole: (method, args) => console.log(`[${method}]`, ...args), });
Returns:
container.vfs- VirtualFS instancecontainer.runtime- Runtime instancecontainer.npm- PackageManager instancecontainer.serverBridge- ServerBridge instancecontainer.run(command, options?)- Run a shell command (returnsPromise<RunResult>)container.sendInput(data)- Send stdin data to the currently running processcontainer.execute(code)- Execute JavaScript codecontainer.runFile(filename)- Run a file from VirtualFS
interface RunResult { stdout: string; stderr: string; exitCode: number; } interface RunOptions { onStdout?: (data: string) => void; // Stream stdout in real-time onStderr?: (data: string) => void; // Stream stderr in real-time signal?: AbortSignal; // Cancel the command }
Sends data to the stdin of the currently running process. Emits both data and keypress events for compatibility with readline-based tools (e.g., vitest watch mode).
Node.js-compatible filesystem API.
// Synchronous operations vfs.writeFileSync(path, content); vfs.readFileSync(path, encoding?); vfs.mkdirSync(path, { recursive: true }); vfs.readdirSync(path); vfs.statSync(path); vfs.unlinkSync(path); vfs.rmdirSync(path); vfs.existsSync(path); vfs.renameSync(oldPath, newPath); // Async operations await vfs.readFile(path, encoding?); await vfs.stat(path); // File watching vfs.watch(path, { recursive: true }, (event, filename) => { console.log(`${event}: ${filename}`); });
Execute JavaScript/TypeScript code.
// Execute code string runtime.execute('console.log("Hello")'); // Run a file from VirtualFS runtime.runFile('/path/to/file.js'); // Require a module const module = runtime.require('/path/to/module.js');
For advanced use cases, use createRuntime to create a runtime with security options:
import { createRuntime, VirtualFS } from 'almostnode'; const vfs = new VirtualFS(); // RECOMMENDED: Cross-origin sandbox (fully isolated) const secureRuntime = await createRuntime(vfs, { sandbox: 'https://your-sandbox.vercel.app', }); // For demos/trusted code: Same-origin with explicit opt-in const demoRuntime = await createRuntime(vfs, { dangerouslyAllowSameOrigin: true, useWorker: true, // Optional: run in Web Worker cwd: '/project', env: { NODE_ENV: 'development' }, }); // Both modes use the same async API const result = await secureRuntime.execute('module.exports = 1 + 1;'); console.log(result.exports); // 2
| Mode | Option | Security Level | Use Case |
|---|---|---|---|
| Cross-origin sandbox | sandbox: 'https://...' |
Highest | Production, untrusted code |
| Same-origin Worker | dangerouslyAllowSameOrigin: true, useWorker: true |
Medium | Demos with trusted code |
| Same-origin main thread | dangerouslyAllowSameOrigin: true |
Lowest | Trusted code only |
Security by default: createRuntime() throws an error if neither sandbox nor dangerouslyAllowSameOrigin is provided.
For running untrusted code securely, deploy a cross-origin sandbox. The key requirement is that the sandbox must be served from a different origin (different domain, subdomain, or port).
import { generateSandboxFiles } from 'almostnode'; import fs from 'fs'; const files = generateSandboxFiles(); // Generates: index.html, vercel.json, __sw__.js fs.mkdirSync('sandbox', { recursive: true }); for (const [filename, content] of Object.entries(files)) { fs.writeFileSync(`sandbox/${filename}`, content); } // Deploy: cd sandbox && vercel --prod
The generated files include:
index.html- Sandbox page with service worker registrationvercel.json- CORS headers for cross-origin iframe embedding__sw__.js- Service worker for dev server URL access
The sandbox requires two things:
Create an index.html that loads almostnode and handles postMessage:
<!DOCTYPE html> <html> <head><meta charset="UTF-8"></head> <body> <script type="module"> import { VirtualFS, Runtime } from 'https://unpkg.com/almostnode/dist/index.js'; let vfs = null; let runtime = null; window.addEventListener('message', async (event) => { const { type, id, code, filename, vfsSnapshot, options, path, content } = event.data; try { switch (type) { case 'init': vfs = VirtualFS.fromSnapshot(vfsSnapshot); runtime = new Runtime(vfs, { cwd: options?.cwd, env: options?.env, onConsole: (method, args) => { parent.postMessage({ type: 'console', consoleMethod: method, consoleArgs: args }, '*'); }, }); break; case 'execute': const result = runtime.execute(code, filename); parent.postMessage({ type: 'result', id, result }, '*'); break; case 'runFile': const runResult = runtime.runFile(filename); parent.postMessage({ type: 'result', id, result: runResult }, '*'); break; case 'syncFile': if (content === null) { try { vfs.unlinkSync(path); } catch {} } else { vfs.writeFileSync(path, content); } break; case 'clearCache': runtime?.clearCache(); break; } } catch (error) { if (id) parent.postMessage({ type: 'error', id, error: error.message }, '*'); } }); parent.postMessage({ type: 'ready' }, '*'); </script> </body> </html>
The sandbox server must include these headers:
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
Example configurations:
Nginx
server { listen 3002; root /path/to/sandbox; location / { add_header Access-Control-Allow-Origin *; add_header Cross-Origin-Resource-Policy cross-origin; } }
Apache (.htaccess)
Header set Access-Control-Allow-Origin "*" Header set Cross-Origin-Resource-Policy "cross-origin"
Express.js
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); next(); }); app.use(express.static('sandbox')); app.listen(3002);
Python (http.server)
from http.server import HTTPServer, SimpleHTTPRequestHandler class CORSHandler(SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Cross-Origin-Resource-Policy', 'cross-origin') super().end_headers() HTTPServer(('', 3002), CORSHandler).serve_forever()
const runtime = await createRuntime(vfs, { sandbox: 'https://sandbox.yourdomain.com', // Must be different origin! }); // Code runs in isolated cross-origin iframe const result = await runtime.execute(untrustedCode);
For local testing, run the sandbox on a different port:
# Terminal 1: Main app on port 5173 npm run dev # Terminal 2: Sandbox on port 3002 npm run sandbox
Then use sandbox: 'http://localhost:3002/sandbox/' in your app.
| Threat | Status |
|---|---|
| Cookies | Blocked (different origin) |
| localStorage | Blocked (different origin) |
| IndexedDB | Blocked (different origin) |
| DOM access | Blocked (cross-origin iframe) |
Note: Network requests from the sandbox are still possible. Add CSP headers for additional protection.
Install npm packages.
// Install a package await npm.install('react'); await npm.install('lodash@4.17.21'); // Install multiple packages await npm.install(['react', 'react-dom']);
967 compatibility tests verify our Node.js API coverage.
| Module | Tests | Coverage | Notes |
|---|---|---|---|
path |
219 | High | POSIX paths (no Windows) |
buffer |
95 | High | All common operations |
fs |
76 | High | Sync + promises API |
url |
67 | High | WHATWG URL + legacy parser |
util |
77 | High | format, inspect, promisify |
process |
60 | High | env, cwd, hrtime, EventEmitter |
events |
50 | High | Full EventEmitter API |
os |
58 | High | Platform info (simulated) |
crypto |
57 | High | Hash, HMAC, random, sign/verify |
querystring |
52 | High | parse, stringify, escape |
stream |
44 | Medium | Readable, Writable, Transform |
zlib |
39 | High | gzip, deflate, brotli |
tty |
40 | High | ReadStream, WriteStream |
perf_hooks |
33 | High | Performance API |
These modules export empty objects or no-op functions:
net,tls,dns,dgramcluster,worker_threadsvm,v8,inspectorasync_hooks
import { VirtualFS, ViteDevServer, getServerBridge } from 'almostnode'; const vfs = new VirtualFS(); // Create a React app vfs.writeFileSync('/index.html', ` <!DOCTYPE html> <html> <body> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html> `); vfs.mkdirSync('/src', { recursive: true }); vfs.writeFileSync('/src/main.jsx', ` import React from 'react'; import ReactDOM from 'react-dom/client'; function App() { return <h1>Hello Vite!</h1>; } ReactDOM.createRoot(document.getElementById('root')).render(<App />); `); // Start Vite dev server const server = new ViteDevServer(vfs, { port: 5173 });
Supports both Pages Router and App Router:
/pages
/index.jsx → /
/about.jsx → /about
/users/[id].jsx → /users/:id
/api/hello.js → /api/hello
/app
/layout.jsx → Root layout
/page.jsx → /
/about/page.jsx → /about
/users/[id]/page.jsx → /users/:id
almostnode includes built-in Hot Module Replacement support for instant updates during development. When you edit files, changes appear immediately in the preview without a full page reload.
HMR is automatically enabled when using NextDevServer or ViteDevServer. The system uses:
- VirtualFS file watching - Detects file changes via
vfs.watch() - postMessage API - Communicates updates between the main page and preview iframe
- React Refresh - Preserves React component state during updates
// HMR works automatically - just edit files and save vfs.writeFileSync('/app/page.tsx', updatedContent); // The preview iframe will automatically refresh with the new content
For security, the preview iframe should be sandboxed. HMR uses postMessage for communication, which works correctly with sandboxed iframes:
// Create sandboxed iframe for security const iframe = document.createElement('iframe'); iframe.src = '/__virtual__/3000/'; // Sandbox restricts the iframe's capabilities - add only what you need iframe.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups'; container.appendChild(iframe); // Register the iframe as HMR target after it loads iframe.onload = () => { if (iframe.contentWindow) { devServer.setHMRTarget(iframe.contentWindow); } };
Recommended sandbox permissions:
allow-scripts- Required for JavaScript executionallow-same-origin- Allows the iframe to access cookies, localStorage, and IndexedDB (only add if your app needs these; omit for better isolation)allow-forms- If your app uses formsallow-popups- If your app opens new windows/tabs
Note: The service worker intercepts
/__virtual__/requests at the origin level, not the iframe level. Theallow-same-originattribute does NOT affect service worker functionality. For maximum security isolation, consider using cross-origin sandbox mode (see below) which doesn't useallow-same-origin.
If you need to manually trigger HMR updates (e.g., after programmatic file changes):
function triggerHMR(path: string, iframe: HTMLIFrameElement): void { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'update', path, timestamp: Date.now(), channel: 'next-hmr', // Use 'vite-hmr' for Vite }, '*'); } } // After writing a file vfs.writeFileSync('/app/page.tsx', newContent); triggerHMR('/app/page.tsx', iframe);
| File Type | HMR Behavior |
|---|---|
.jsx, .tsx |
React Refresh (preserves state) |
.js, .ts |
Full module reload |
.css |
Style injection (no reload) |
.json |
Full page reload |
Start the dev server with npm run dev and open any demo at http://localhost:5173:
| Demo | Path | Description |
|---|---|---|
| Next.js | /examples/next-demo.html |
Pages & App Router, CSS modules, route groups, API routes, HMR |
| Vite | /examples/vite-demo.html |
Vite dev server with React and HMR |
| Vitest | /examples/vitest-demo.html |
Real vitest execution with xterm.js terminal and watch mode |
| Express | /examples/express-demo.html |
Express.js HTTP server running in the browser |
| Convex | /examples/demo-convex-app.html |
Real-time todo app with Convex cloud deployment |
| Vercel AI SDK | /examples/demo-vercel-ai-sdk.html |
Streaming AI chatbot with Next.js and OpenAI |
| Bash | /examples/bash-demo.html |
Interactive POSIX shell emulator |
git clone https://github.com/macaly/almostnode.git
cd almostnode
npm install# Unit tests npm test # E2E tests (requires Playwright) npm run test:e2e
npm run dev
See the Demos section for all available examples.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE for details.
- esbuild-wasm - Lightning-fast JavaScript/TypeScript transformation
- just-bash - POSIX shell in WebAssembly
- React Refresh - Hot module replacement for React
- Comlink - Web Worker communication made simple
Built by the creators of Macaly.com