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.
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.
Features
- 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
Requirements
- 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).
Quick Start
Installation
Basic Usage
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.
Running Untrusted Code Securely
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.
Working with Virtual File System
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');
With npm Packages
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
Running Shell Commands
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.
Running CLI Tools
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.
Streaming Output & Long-Running Commands
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();
With Next.js Dev Server
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/
Service Worker Setup
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.
Which Setup Do I Need?
| 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 |
Option 1: Cross-Origin Sandbox (Recommended)
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.
Option 2: Same-Origin with Vite
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' });
Option 3: Same-Origin with Next.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 |
Option 4: Manual Setup (Other Frameworks)
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');
Comparison with WebContainers
| 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 |
When to use almostnode
- Building code playgrounds or tutorials
- Creating interactive documentation
- Prototyping without server setup
- Educational tools
- Lightweight sandboxed execution
Example: Code Playground
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-..." }
When to use WebContainers
- Full-fidelity Node.js development
- Running native modules
- Complex build pipelines
- Production-like environments
API Reference
createContainer(options?)
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
container.run(command, options?)
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 }
container.sendInput(data)
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).
VirtualFS
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}`); });
Runtime
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');
createRuntime (Async Runtime Factory)
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
Security Modes
| 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.
Sandbox Setup
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).
Quick Setup (Vercel)
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
Manual Setup (Any Platform)
The sandbox requires two things:
1. The sandbox HTML page
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>
2. Required HTTP headers
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()
Use in your app
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);
Local Development
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.
What cross-origin sandbox protects
| 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.
PackageManager
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']);
Supported Node.js APIs
967 compatibility tests verify our Node.js API coverage.
Fully Shimmed Modules
| 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 |
Stubbed Modules
These modules export empty objects or no-op functions:
net,tls,dns,dgramcluster,worker_threadsvm,v8,inspectorasync_hooks
Framework Support
Vite
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 });
Next.js
Supports both Pages Router and App Router:
Pages Router
/pages
/index.jsx → /
/about.jsx → /about
/users/[id].jsx → /users/:id
/api/hello.js → /api/hello
App Router
/app
/layout.jsx → Root layout
/page.jsx → /
/about/page.jsx → /about
/users/[id]/page.jsx → /users/:id
Hot Module Replacement (HMR)
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.
How It Works
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
Setup Requirements
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.
Manual HMR Triggering
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);
Supported File Types
| File Type | HMR Behavior |
|---|---|
.jsx, .tsx |
React Refresh (preserves state) |
.js, .ts |
Full module reload |
.css |
Style injection (no reload) |
.json |
Full page reload |
Demos
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 |
Development
Setup
git clone https://github.com/macaly/almostnode.git
cd almostnode
npm installRun Tests
# Unit tests npm test # E2E tests (requires Playwright) npm run test:e2e
Development Server
See the Demos section for all available examples.
Contributing
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
License
MIT License - see LICENSE for details.
Acknowledgments
- 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