almostnode

repository·main·Indexed 22 days ago

https://github.com/macaly/almostnode

A lightweight, browser-native Node.js runtime environment (v0.2.14) that enables running Node.js code, installing npm packages, and executing dev servers like Vite or Next.js entirely within the browser. It features a Virtual File System (VirtualFS), a sandboxed execution environment via createRuntime(), and a container system for managing shell commands and npm scripts without a backend server.

Tokens
29K
Snippets
83
Records
116
Agent score
78%

What's inside almostnode

  1. Work with the Virtual File System (VFS)

    main

    The VirtualFS provides a Node.js-compatible in-memory filesystem. You can pre-populate it with files using vfs.writeFileSync() and then run files from that filesystem using container.runFile(path).

    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');
  2. How the Browser-based Convex Runtime works

    main

    The demo architecture relies on a virtualized environment within the browser to simulate a Node.js-like development experience:

    • Virtual File System (VirtualFS): Files are stored in-memory using VirtualFS.
    • Package Manager: NPM packages are fetched dynamically from esm.sh or unpkg CDNs.
    • Runtime (JS): CommonJS modules are executed within a sandboxed environment.
    • Convex CLI Bundle: A bundled version of the Convex CLI runs inside the browser, specifically executing convex dev --once to deploy functions to the Convex Cloud API.
    • React App: The frontend uses the standard Convex React client to connect to the backend deployed via the virtualized CLI.
  3. How Hot Module Replacement (HMR) works in almostnode

    main

    HMR is automatically enabled when using NextDevServer or ViteDevServer. It allows for instant updates in the preview iframe without a full page reload.

    Mechanism

    1. VirtualFS file watching: Detects changes via vfs.watch().
    2. postMessage API: Communicates updates between the main page and the preview iframe.
    3. React Refresh: Preserves React component state during updates.

    Supported File Types

    File TypeHMR Behavior
    .jsx, .tsxReact Refresh (preserves state)
    .js, .tsFull module reload
    .cssStyle injection (no reload)
    .jsonFull page reload

    Manual HMR Triggering

    If you need to manually trigger updates after programmatic file changes, send a postMessage to the iframe's contentWindow with the following shape:

    • type: 'update'
    • path: The file path that changed.
    • timestamp: Date.now()
    • channel: 'vite-hmr' for Vite or 'next-hmr' for Next.js.
    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
        }, '*');
      }
    }
    
    // Usage after writing a file
    vfs.writeFileSync('/app/page.tsx', newContent);
    triggerHMR('/app/page.tsx', iframe);
  4. How almostnode's core components work together

    main

    almostnode provides a browser-based Node.js environment through several interacting abstractions:

    • VirtualFS: An in-memory filesystem that mimics the Node.js fs module. It serves as the storage layer for all project files and installed npm packages.
    • Runtime: The execution engine that runs JavaScript/TypeScript code using Node.js-compatible APIs and globals.
    • PackageManager: A tool that installs npm packages directly into the VirtualFS.
    • NextDevServer: A development server that serves Next.js applications, handles JSX/TS transforms, and supports Hot Module Replacement (HMR).
    • Server Bridge: A mechanism using Service Workers to route browser requests to your virtual servers (like NextDevServer), making them accessible via a URL.
  5. Compare almostnode with WebContainers

    main

    Use almostnode when you need a lightweight (~250KB gzipped), instant-startup solution for code playgrounds, tutorials, or educational tools. It uses a just-bash (POSIX subset) shell and virtual ports for networking.

    Use WebContainers when you require a full Linux kernel, native module support, or complex build pipelines for production-like development environments.

  6. How streaming works in the almostnode browser runtime

    main

    The almostnode runtime enables a streaming architecture that allows a browser-based Next.js application to simulate a server-side environment.

    The Streaming Flow

    1. User Interaction: The useChat hook (from Vercel AI SDK) sends a POST request to an API route (e.g., /api/chat).
    2. Interception: A Service Worker intercepts the request.
    3. Bridging: The Server Bridge (using MessageChannel) receives the request and creates a streaming response with callbacks.
    4. Execution: The API Handler executes in the virtual environment, calling external APIs (like OpenAI) via a CORS proxy.
    5. Chunking: As the API responds, res.write() is called to send stream-chunk messages.
    6. UI Update: The useChat hook parses these chunks and updates the React state in real-time.
  7. Implement the AI SDK Data Stream Format

    main

    To ensure compatibility with the Vercel AI SDK, your API route must output data in the specific AI SDK stream format. The stream uses prefix markers for different types of data:

    • 0:"text content"\n — Represents a text chunk.
    • d:{"finishReason":"stop"}\n — Represents the end of the stream.
    0:"text content"\n     # Text chunk
    d:{"finishReason":"stop"}\n  # End of stream
  8. Set up the almostnode development environment

    main

    To contribute to or develop the almostnode project locally, follow these steps:

    Installation

    git clone https://github.com/macaly/almostnode.git
    cd almostnode
    npm install

    Running the Project

    • Development Server: npm run dev
    • Unit Tests: npm test
    • E2E Tests (requires Playwright): npm run test:e2e
  9. Configure Service Worker for Dev Servers

    main

    almostnode uses a Service Worker to intercept HTTP requests and route them to virtual dev servers. The required setup depends on your use case:

    1. Cross-Origin Sandbox: Use generateSandboxFiles() to create files (index.html, vercel.json, __sw__.js) and deploy them to a separate origin.
    2. Same-Origin with Vite: Use the almostnodePlugin() from almostnode/vite in your vite.config.ts.
    3. Same-Origin with Next.js: Use getServiceWorkerContent() from almostnode/next in an API route (Pages Router) or a Route Handler (App Router).
    4. Manual Setup: Copy node_modules/almostnode/dist/__sw__.js to your public/ directory.
  10. Deploy a Convex project using almostnode

    main

    To deploy a Convex backend within an almostnode environment, you must manage a VirtualFS, use a PackageManager to install the convex dependency, and execute the Convex CLI via a Runtime instance.

    Critical Deployment Steps:

    1. Fresh Runtime: Always create a new Runtime instance for each deployment to ensure the CLI sees the latest file changes and avoids stale closures.
    2. Clear Generated Files: Manually remove existing _generated directories (e.g., /project/convex/_generated) before running the CLI. If the CLI finds existing generated files, it may skip the push.
    3. CLI Execution: Execute the Convex CLI by setting process.env.CONVEX_DEPLOY_KEY and configuring process.argv to run convex dev --once using the bundled CLI file.
    4. Post-Deployment: Poll for the creation of .env.local to confirm success, then extract the CONVEX_URL. You must also copy the generated files from the project's _generated directory to your application's expected location (e.g., /convex/_generated).
    async function deployToConvex(deployKey: string): Promise<string> {
      // CRITICAL: Create a fresh Runtime for each deployment
      cliRuntime = new Runtime(vfs, { cwd: '/project' });
    
      // IMPORTANT: Remove existing _generated directories
      const genPaths = ['/project/convex/_generated', '/convex/_generated'];
      for (const path of genPaths) {
        if (vfs.existsSync(path)) {
          for (const file of vfs.readdirSync(path)) {
            vfs.unlinkSync(`${path}/${file}`);
          }
          vfs.rmdirSync(path);
        }
      }
    
      const cliCode = `
        process.env.CONVEX_DEPLOY_KEY = '${deployKey}';
        process.argv = ['node', 'convex', 'dev', '--once'];
        require('./node_modules/convex/dist/cli.bundle.cjs');
      `;
    
      try {
        cliRuntime.execute(cliCode, '/project/cli-runner.js');
      } catch (error) {
        console.log('CLI completed:', error.message);
      }
    
      await waitForDeployment(vfs);
      await waitForGenerated(vfs);
    
      const envContent = vfs.readFileSync('/project/.env.local', 'utf8');
      const match = envContent.match(/CONVEX_URL=(.+)/);
      if (!match) throw new Error('Deployment failed');
    
      return match[1].trim();
    }
  11. Quick Start: Minimal Next.js Example

    main

    To run a minimal Next.js application in the browser, you need to initialize a VirtualFS, write your project files, start a NextDevServer, and register it with the getServerBridge to enable Service Worker routing.

    import { VirtualFS, Runtime, NextDevServer, PackageManager } from 'almostnode';
    import { getServerBridge } from 'almostnode/server-bridge';
    
    // 1. Create virtual filesystem
    const vfs = new VirtualFS();
    
    // 2. Write project files
    vfs.writeFileSync('/package.json', JSON.stringify({ name: 'my-app' }));
    vfs.mkdirSync('/app', { recursive: true });
    vfs.writeFileSync('/app/page.tsx', `
      export default function Home() {
        return <h1>Hello from almostnode!</h1>;
      }
    `);
    
    // 3. Create and start the dev server
    const server = new NextDevServer(vfs, {
      port: 3000,
      preferAppRouter: true,
    });
    
    // 4. Register with the server bridge (enables Service Worker routing)
    const bridge = getServerBridge();
    await bridge.initServiceWorker();
    bridge.registerServer(server, 3000);
    server.start();
    
    // 5. Navigate to the app
    const url = bridge.getServerUrl(3000);
    console.log(`App running at: ${url}`);