Bundled Node.js

OpenWork bundles a complete Node.js runtime to ensure MCP servers and CLI tools function reliably across all user environments. This eliminates dependency on system Node.js installations and provides consistent performance across different platforms.

Overview

Why Bundle Node.js?

  • Consistency: Ensures all users have the same Node.js version (v20.18.1)
  • Reliability: Works on systems without Node.js installed
  • Security: Known-good version with security patches
  • Performance: Optimized binaries for each platform/architecture

Version Information

const NODE_VERSION = '20.18.1';

Implementation

Source: /Users/nateb/openwork-repo/apps/desktop/src/main/utils/bundled-node.ts

Path Resolution

export function getBundledNodePaths(): BundledNodePaths | null {
  if (!app.isPackaged) {
    // In development, use system Node
    return null;
  }

  const platform = process.platform; // 'darwin', 'win32', 'linux'
  const arch = process.arch; // 'x64', 'arm64'

  const isWindows = platform === 'win32';
  const ext = isWindows ? '.exe' : '';
  const scriptExt = isWindows ? '.cmd' : '';

  // Node.js directory is architecture-specific
  const nodeDir = path.join(
    process.resourcesPath,
    'nodejs',
    arch // 'x64' or 'arm64' subdirectory
  );

  const binDir = isWindows ? nodeDir : path.join(nodeDir, 'bin');

  return {
    nodePath: path.join(binDir, `node${ext}`),
    npmPath: path.join(binDir, `npm${scriptExt}`),
    npxPath: path.join(binDir, `npx${scriptExt}`),
    binDir,
    nodeDir,
  };
}

Path Structure

resources/
└── nodejs/
    ├── x64/
    │   ├── bin/                    # Unix-like systems
    │   │   ├── node
    │   │   ├── npm
    │   │   └── npx
    │   └── node.exe               # Windows
    │   ├── npm.cmd
    │   └── npx.cmd
    └── arm64/
        └── ... (same structure)

Usage Patterns

1. Path Resolution

// Get bundled paths (production only)
const paths = getBundledNodePaths();
if (paths) {
  console.log('Node path:', paths.nodePath);
  console.log('NPM path:', paths.npmPath);
  console.log('Bin directory:', paths.binDir);
}

2. Availability Check

// Check if bundled Node.js is accessible
export function isBundledNodeAvailable(): boolean {
  const paths = getBundledNodePaths();
  if (!paths) {
    return false;
  }
  return fs.existsSync(paths.nodePath);
}

3. Smart Path Selection

// Get appropriate node binary (bundled or system)
export function getNodePath(): string {
  const bundled = getBundledNodePaths();
  if (bundled && fs.existsSync(bundled.nodePath)) {
    return bundled.nodePath;
  }
  // Fallback to system node
  return 'node';
}

Critical: Environment Configuration

PATH Injection for Spawned Processes

When spawning Node.js processes, the bundled bin directory must be added to PATH:

import { spawn } from 'child_process';
import { getNpxPath, getBundledNodePaths } from '../utils/bundled-node';

// Get bundled paths
const npxPath = getNpxPath();
const bundledPaths = getBundledNodePaths();

// Build environment with bundled node in PATH
let spawnEnv: NodeJS.ProcessEnv = { ...process.env };
if (bundledPaths) {
  const delimiter = process.platform === 'win32' ? ';' : ':';
  spawnEnv.PATH = `${bundledPaths.binDir}${delimiter}${process.env.PATH || ''}`;
}

// Spawn with modified environment
spawn(npxPath, ['-y', 'some-package@latest'], {
  stdio: ['pipe', 'pipe', 'pipe'],
  env: spawnEnv,
});

Why PATH Injection is Critical

The npx executable uses a shebang (#!/usr/bin/env node) that looks for node in the PATH. Without injecting the bundled bin directory:

# Without PATH injection
$ npx -y some-package@latest
node: command not found

With PATH injection:

# With PATH injection (bundled bin directory first)
$ npx -y some-package@latest
# Successfully uses bundled node binary

Platform Support

Supported Platforms

| Platform | Architectures | Notes | |———-|—————|——-| | macOS | x64, arm64 | Native Apple Silicon support | | Windows | x64, arm64 | EXE and CMD files | | Linux | x64, arm64 | Standard ELF binaries |

Platform-Specific Considerations

macOS

  • Uses standard Unix binaries
  • No file extensions for executables
  • Supports both Intel and Apple Silicon

Windows

  • .exe extensions for node, npm, npx
  • .cmd extensions for npm and npx
  • Case-insensitive file system

Linux

  • Standard ELF binaries
  • No file extensions
  • Standard executable permissions

Build Integration

Download Script

Node.js binaries are downloaded using a dedicated script:

# Download Node.js for all platforms and architectures
node scripts/download-nodejs.cjs

After-Pack Hook

During packaging, the correct binary is copied into the app bundle:

// In scripts/after-pack.cjs
const arch = process.arch;
const platform = process.platform;

// Copy correct binary to resources
const sourceDir = path.join(__dirname, `nodejs-${platform}-${arch}`);
const targetDir = path.join(
  process.resourcesPath,
  'nodejs',
  arch
);

Resource Path Structure

OpenWork.app/Contents/Resources/
├── nodejs/
│   ├── x64/
│   │   ├── bin/
│   │   │   ├── node
│   │   │   ├── npm
│   │   │   └── npx
│   │   └── node.exe
│   │   ├── npm.cmd
│   │   └── npx.cmd
│   └── arm64/
│       └── ... (same structure)

MCP Server Configuration

When generating MCP server configurations, pass the appropriate environment:

const bundledPaths = getBundledNodePaths();

const serverConfig = {
  command: bundledPaths?.nodePath || 'node',
  args: ['--loader', 'ts-node/esm'],
  env: {
    ...process.env,
    // Inject bundled node bin directory
    ...(bundledPaths && {
      NODE_BIN_PATH: bundledPaths.binDir,
      PATH: `${bundledPaths.binDir}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH || ''}`
    })
  }
};

Debugging and Monitoring

Log Node Information

// Useful for debugging configuration
export function logBundledNodeInfo(): void {
  const paths = getBundledNodePaths();

  if (!paths) {
    console.log('[Bundled Node] Development mode - using system Node.js');
    return;
  }

  console.log('[Bundled Node] Configuration:');
  console.log(`  Platform: ${process.platform}`);
  console.log(`  Architecture: ${process.arch}`);
  console.log(`  Node directory: ${paths.nodeDir}`);
  console.log(`  Node path: ${paths.nodePath}`);
  console.log(`  Available: ${fs.existsSync(paths.nodePath)}`);
}

Manual Verification

# Check bundled node version
./resources/nodejs/x64/bin/node --version

# Test npm functionality
./resources/nodejs/x64/bin/npm --version

# Test npx functionality
./resources/nodejs/x64/bin/npx create-react-app@latest test-app

Performance Considerations

Memory Usage

  • Bundled binaries increase app size (~100MB per architecture)
  • Memory usage consistent with system Node.js
  • No additional overhead from bundling

Startup Time

  • First launch slightly slower due to binary extraction
  • Subsequent launches use cached binaries
  • No difference from system Node.js performance

Storage Requirements

  • ~200MB total for all architectures
  • Only one architecture used per installation
  • Automatic cleanup during app updates

Error Handling

Fallback Mechanisms

// Graceful fallback to system node
export function getNodePath(): string {
  const bundled = getBundledNodePaths();
  if (bundled && fs.existsSync(bundled.nodePath)) {
    return bundled.nodePath;
  }
  // Warn if falling back to system node in packaged app
  if (app.isPackaged) {
    console.warn('[Bundled Node] WARNING: Bundled Node.js not found, falling back to system node');
  }
  return 'node'; // Fallback to system node
}

Common Issues

  1. Missing binaries: Check build process and packaging
  2. Permission issues: Ensure executable permissions on Unix systems
  3. Path conflicts: Verify PATH injection in spawn calls
  4. Architecture mismatch: Match bundled binary with system architecture

Security Considerations

Binary Integrity

  • Node.js binaries downloaded from official sources
  • No modification of original binaries
  • Consistent hashing for verification

Attack Surface

  • Standard Node.js security model
  • No additional attack vectors from bundling
  • Same security considerations as system Node.js

The bundled Node.js system ensures reliable operation across all user environments while providing the flexibility to use system Node.js during development. Proper PATH injection is critical for MCP server and CLI tool functionality.


Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork