Almostnode
Node.js in your browser. Just like that.
Install / Use
/learn @macaly/AlmostnodeREADME
almostnode
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
npm install almostnode
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 use
createContainer()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.se
