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.
- 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
- 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 almostnodeimport { 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 worldimport { 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 --prodGenerated 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 | ~50KB | ~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 instance
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 --prodThe 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 sandboxThen 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 contentFor 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 |
git clone https://github.com/macaly/almostnode.git
cd almostnode
npm install# Unit tests
npm test
# E2E tests (requires Playwright)
npm run test:e2enpm run devOpen http://localhost:5173/examples/next-demo.html to see the Next.js demo.
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