/ Docs

Core Concepts

almostnode is built on four core primitives that work together to simulate a Node.js environment in the browser.

Architecture Overview

Every almostnode app uses these components:

┌─────────────────────────────────────────┐
│            Your Application              │
├─────────────────────────────────────────┤
│  Framework Dev Servers (Next.js / Vite)  │
├──────────┬──────────┬───────────────────┤
│  Runtime │   npm    │   ServerBridge    │
├──────────┴──────────┴───────────────────┤
│           VirtualFS (in-memory)          │
└─────────────────────────────────────────┘

VirtualFS

VirtualFS provides a POSIX-compatible filesystem API that stores files in memory. It supports directories, binary files, file watching, and all the common fs operations you'd expect from Node.js.

import { VirtualFS } from 'almostnode';

const vfs = new VirtualFS();

// Create directories
vfs.mkdirSync('/src', { recursive: true });

// Write files
vfs.writeFileSync('/src/index.js', 'console.log("hello")');

// Read files
const content = vfs.readFileSync('/src/index.js', 'utf8');

// List directory contents
const files = vfs.readdirSync('/src'); // ['index.js']

// Watch for changes
vfs.watch('/src', (event, filename) => {
  console.log(event, filename);
});

Key Features

Runtime

The Runtime executes JavaScript and TypeScript code with CommonJS module resolution. It shims 40+ Node.js built-in modules so code written for Node.js can run in the browser.

import { VirtualFS, Runtime } from 'almostnode';

const vfs = new VirtualFS();
const runtime = new Runtime(vfs);

// Execute code directly
runtime.execute(`
  const path = require('path');
  console.log(path.join('/foo', 'bar', 'baz.js'));
`);

// Or run a file from the VFS
vfs.writeFileSync('/app.js', 'module.exports = { version: 1 }');
const result = runtime.runFile('/app.js');
console.log(result.exports); // { version: 1 }

Shimmed Modules

The runtime includes shims for these Node.js built-in modules:

assert, buffer, child_process, console, crypto, dns, events, fs, http, https, module, net, os, path, process, querystring, readline, stream, string_decoder, timers, tls, tty, url, util, vm, worker_threads, zlib, and more.

Module Resolution

The runtime follows Node.js module resolution rules:

  1. Built-in modules (require('fs'), require('node:path'))
  2. Relative paths (require('./utils')) — resolves .js, .ts, .json, /index.js
  3. npm packages (require('express')) — looks in node_modules/

PackageManager

The PackageManager installs real npm packages into the virtual filesystem. It fetches tarballs from the npm registry, resolves the full dependency tree, and extracts files — all in the browser.

import { VirtualFS, PackageManager } from 'almostnode';

const vfs = new VirtualFS();
const npm = new PackageManager(vfs);

// Install a single package
await npm.install('lodash');

// Install a specific version
await npm.install('react@18.2.0');

// Install all deps from package.json
vfs.writeFileSync('/package.json', JSON.stringify({
  dependencies: { express: '^4.18.0' }
}));
await npm.installFromPackageJson();

// List installed packages
console.log(npm.list()); // { express: '4.18.2', ... }

How Package Installation Works

When you call npm.install('express'), the PackageManager performs these steps entirely in the browser:

  1. Resolve version — Fetch the package manifest from the npm registry (registry.npmjs.org) and resolve semver ranges (e.g. ^4.18.0 to 4.18.2)
  2. Build dependency tree — Recursively resolve all transitive dependencies with correct version matching, handling circular dependencies
  3. Download tarballs — Fetch .tgz files for each package from the npm CDN
  4. Extract to VFS — Decompress and extract tarball contents into /node_modules/{name}/ in the virtual filesystem
  5. Transform ESM to CJS — Use esbuild-wasm to convert ES module syntax to CommonJS so the Runtime can execute it (enabled by default, can be disabled with transform: false)
  6. Write lockfile — Create .package-lock.json with all resolved versions
Note

The PackageManager downloads packages from the npm registry. The framework dev servers (Next.js and Vite) use a different approach — they redirect bare npm imports to esm.sh for browser-side ES module loading (e.g. import React from 'react' becomes import React from 'https://esm.sh/react@18.2.0'). Both approaches can coexist in the same project.

// Track installation progress
await npm.install('express', {
  onProgress: (msg) => console.log(msg),
  save: true, // Add to package.json dependencies
});

Installing and Using a Package

Here's a complete example — install a package from npm, then use it in your code:

import { createContainer } from 'almostnode';

const { vfs, npm, runtime } = createContainer();

// Install lodash from npm
await npm.install('lodash');

// Use it in your code
vfs.writeFileSync('/app.js', `
  const _ = require('lodash');
  const data = [1, [2, [3, [4]]]];
  console.log(_.flattenDeep(data));  // [1, 2, 3, 4]
  console.log(_.chunk([1,2,3,4,5,6], 2));  // [[1,2],[3,4],[5,6]]
`);

runtime.runFile('/app.js');

// Install a specific version
await npm.install('dayjs@1.11.10');

// Scoped packages work too
await npm.install('@hono/node-server');

Shell Commands (child_process)

almostnode integrates just-bash — a simulated bash environment written in TypeScript — to provide child_process.exec() support in the browser. just-bash implements 50+ bash commands and uses an IFileSystem interface that almostnode bridges to the VirtualFS via a VirtualFSAdapter. This means all shell commands operate on the same virtual filesystem your code uses.

const { exec } = require('child_process');

// Run shell commands against the VirtualFS
exec('ls /src', (err, stdout) => {
  console.log(stdout); // Lists files in /src
});

// Pipes and redirects work too
exec('cat /data.csv | sort | head -5', (err, stdout) => {
  console.log(stdout);
});

// Custom 'node' command runs JS via the Runtime
exec('node /app.js', (err, stdout) => {
  console.log(stdout);
});

Available Commands

Note

execSync() is not supported in the browser — use the async exec() with callbacks or promises instead. The shell operates on the VirtualFS, not the real filesystem.

Convex Integration

almostnode can run the Convex CLI entirely in the browser, enabling in-browser Convex development with live deployment. After installing the Convex package, you can deploy functions directly from the browser.

import { VirtualFS, Runtime, PackageManager } from 'almostnode';

const vfs = new VirtualFS();
const npm = new PackageManager(vfs, { cwd: '/project' });

// Install Convex
await npm.install('convex');

// Set up project structure
vfs.writeFileSync('/project/convex.json', '{ "functions": "convex/" }');

// IMPORTANT: Both .ts AND .js config files are required
const configSrc = `import { defineApp } from "convex/server";
const app = defineApp();
export default app;`;
vfs.writeFileSync('/project/convex/convex.config.ts', configSrc);
vfs.writeFileSync('/project/convex/convex.config.js', configSrc);

// Write your Convex functions
vfs.writeFileSync('/project/convex/tasks.ts', `
  import { query } from './_generated/server';
  export const list = query({
    handler: async (ctx) => ctx.db.query('tasks').collect(),
  });
`);

// CRITICAL: Create a fresh Runtime for each deployment
const cliRuntime = new Runtime(vfs, { cwd: '/project' });

cliRuntime.execute(`
  process.env.CONVEX_DEPLOY_KEY = 'dev:your-deployment|token';
  process.argv = ['node', 'convex', 'dev', '--once'];
  require('./node_modules/convex/dist/cli.bundle.cjs');
`);

How It Works

  1. The Convex CLI bundle runs inside the almostnode Runtime via runtime.execute()
  2. It reads your function files from the VirtualFS
  3. Bundles them with esbuild (shimmed in browser)
  4. Pushes the bundled functions to your Convex deployment via the HTTP API
  5. Generated type files (_generated/api.js, server.js) are written back to the VFS

Important Notes

Note

Do not use child_process.exec('convex dev') for deployment — use runtime.execute() with inline code instead. See the full integration guide in docs/CONVEX_CLI_INTEGRATION.md for troubleshooting and advanced patterns.

ServerBridge

The ServerBridge connects virtual HTTP servers to real browser URLs using a service worker. When a request hits /__virtual__/{port}/path, the service worker intercepts it and forwards it to the registered virtual server.

import { getServerBridge } from 'almostnode';

const bridge = getServerBridge();

// Initialize the service worker
await bridge.initServiceWorker();

// Register a virtual server on port 3000
bridge.registerServer(myServer, 3000);

// Now accessible at /__virtual__/3000/ in an iframe
const url = bridge.getServerUrl(3000);
iframe.src = url;

How It Works

  1. initServiceWorker() registers a service worker that intercepts fetch requests
  2. registerServer() maps a port number to a virtual server object
  3. When the browser requests /__virtual__/3000/path, the service worker sends it to the registered server
  4. The server processes the request and returns a response — all without any real network traffic

Putting It All Together

The createContainer() helper creates all four components for you:

import { createContainer } from 'almostnode';

const {
  vfs,          // VirtualFS instance
  runtime,      // Runtime instance
  npm,          // PackageManager instance
  serverBridge, // ServerBridge instance
  execute,      // Shorthand for runtime.execute()
  runFile,      // Shorthand for runtime.runFile()
  createREPL,   // Create interactive REPL context
} = createContainer();

Supported Node.js APIs

967 compatibility tests verify almostnode's Node.js API coverage.

Fully Shimmed Modules

Module Tests Coverage Notes
path219HighPOSIX paths (no Windows)
buffer95HighAll common operations
fs76HighSync + promises API
url67HighWHATWG URL + legacy parser
util77Highformat, inspect, promisify
process60Highenv, cwd, hrtime, EventEmitter
events50HighFull EventEmitter API
os58HighPlatform info (simulated)
crypto57HighHash, HMAC, random, sign/verify
querystring52Highparse, stringify, escape
stream44MediumReadable, Writable, Transform
zlib39Highgzip, deflate, brotli
tty40HighReadStream, WriteStream
perf_hooks33HighPerformance API

Stubbed Modules

These modules export empty objects or no-op functions. They exist so that require() calls don't throw, but they don't provide real functionality: