npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

runspace-sandbox

v0.1.0

Published

A platform-agnostic code execution framework for building secure, isolated runtime environments in the browser

Downloads

131

Readme

Sandbox

A modular, platform-agnostic code execution framework for building secure, isolated runtime environments in the browser. Execute user code safely while maintaining clean separation between UI, runtime, and communication layers.

Overview

Sandbox provides a complete architecture for running user code in isolated environments with real-time communication between parent applications and sandboxed contexts. Built with TypeScript and designed for extensibility, it enables developers to create interactive coding environments, educational tools, and code playgrounds.

Features

  • Platform-Agnostic Runtime: Abstract runtime interface supports multiple execution environments (Pyodide, Node.js, etc.)
  • Secure Isolation: iframe-based sandboxing with postMessage communication
  • Real-time Streaming: Capture and stream stdout/stderr from code execution
  • Output Formatting: Transform runtime output for better user experience (e.g., simplify error tracebacks)
  • Modular Architecture: Clean separation of concerns across Bridge, Controller, Runtime, and Application layers
  • Type-Safe: Full TypeScript support with comprehensive type definitions
  • Customizable UI: Build multiple sandbox applications with different interfaces using the same core infrastructure

Architecture

The sandbox architecture consists of five core components:

1. Sandbox

The orchestrator that initializes and coordinates all components:

  • Creates and configures the runtime instance
  • Initializes the SandboxController
  • Sets up the SandboxBridge for communication
  • Injects the controller into the Sandbox Application

2. SandboxBridge

Handles all communication between the parent window and the sandbox iframe:

  • Manages postMessage-based messaging
  • Forwards commands and events between contexts
  • Provides type-safe message handling

3. SandboxController

Manages the runtime lifecycle and provides the application API:

  • Initializes and monitors runtime readiness
  • Loads user code into the runtime
  • Executes functions with type-safe argument handling
  • Emits events for stdout, stderr, and errors
  • Provides status checks for runtime and code state

4. Runtime (Interface)

Platform-agnostic interface for code execution:

  • initialize(): Set up the execution environment
  • loadCode(workspace): Load workspace with files and configuration
  • runFunction(name, args): Execute functions by name
  • getStdout() / getStderr(): Retrieve output streams
  • clearOutput(): Reset output buffers
  • isReady(): Check runtime status

Implementations:

  • PyodideRuntime: Python execution using Pyodide WebAssembly runtime

5. SandboxApplication

Customizable UI layer that interacts with the controller:

  • Receives injected controller instance
  • Implements custom rendering logic
  • Calls controller methods to execute code
  • Updates UI based on execution results

Installation

npm install

Usage

Basic Setup

import { Sandbox, PyodideRuntime, SandboxApplication } from 'runspace-sandbox';

// Create your custom application
const myApp: SandboxApplication = {
  render() {
    // Implement your UI logic
    const button = document.createElement('button');
    button.textContent = 'Run Code';
    button.onclick = async () => {
      const result = await this.controller?.runFunction('myFunction', [arg1, arg2]);
      console.log('Result:', result);
    };
    document.body.appendChild(button);
  }
};

// Initialize the sandbox
const sandbox = new Sandbox({
  runtime: PyodideRuntime,
  app: myApp,
  targetWindow: window.parent,
  targetOrigin: '*'
});

await sandbox.start();

Parent Application (WorkspaceHost)

For a detailed guide on implementing a Workspace Host, please refer to docs/workspace_host_guide.md.

// Create iframe for sandbox
const iframe = document.createElement('iframe');
iframe.src = '/sandbox.html';
document.body.appendChild(iframe);

// Listen for messages
window.addEventListener('message', (event) => {
  const message = event.data;

  switch (message.type) {
    case 'READY_FOR_INIT':
      // Sandbox is loaded, send initialization command
      iframe.contentWindow?.postMessage({ type: 'INIT' }, '*');
      break;

    case 'SETUP_STREAM':
      // Receive MessagePort for streaming output
      const port = message.port;
      port.onmessage = (e) => {
        console.log('Stream output:', e.data);
      };
      break;

    case 'INITIALIZED':
      console.log('Sandbox initialized');
      // Load user code
      iframe.contentWindow?.postMessage({
        type: 'LOAD_CODE',
        payload: {
          workspace: {
            files: [
              { path: 'main.py', content: 'def add(a, b):\n    return a + b' }
            ],
            config: {
              entryPoints: ['main.py']
            }
          }
        }
      }, '*');
      break;

    case 'CODE_LOADED':
      console.log('Code loaded successfully');
      break;
  }
});

Output Formatting

Transform runtime output to provide a better user experience. Output formatters are applied to both loadCode and runFunction operations:

const sandbox = new Sandbox({
  runtime: PyodideRuntime,
  app: myApp,
  formatters: {
    // Format stdout output and function results
    stdout: (output: unknown) => {
      // Transform output as needed
      if (typeof output === 'string') {
        return output.trim();
      }
      return output;
    },

    // Format stderr output and error messages
    stderr: (error: string) => {
      // Simplify Python tracebacks by removing internal Pyodide frames
      const lines = error.split('\n');
      const filteredLines: string[] = [];
      let i = 0;

      while (i < lines.length) {
        const line = lines[i];

        // Skip internal Pyodide file references and their code context
        if (line.includes('/lib/python') || line.includes('_pyodide')) {
          i++;
          // Skip indented code context lines (start with 4+ spaces)
          while (i < lines.length && lines[i].startsWith('    ')) {
            i++;
          }
          continue;
        }

        // Keep this line and replace <exec> with main.py
        filteredLines.push(line.replace(/File "<exec>"/g, 'File "main.py"'));
        i++;
      }

      return filteredLines.join('\n').trim();
    }
  }
});

Example: Simplifying Python Error Messages

Before formatting:

Traceback (most recent call last):
  File "/lib/python313.zip/_pyodide/_base.py", line 597, in eval_code_async
    await CodeRunner(...)
  File "/lib/python313.zip/_pyodide/_base.py", line 285, in __init__
    self.ast = next(self._gen)
  File "<exec>", line 16
    return [0]''
              ^^
SyntaxError: invalid syntax

After formatting:

Traceback (most recent call last):
  File "main.py", line 16
    return [0]''
              ^^
SyntaxError: invalid syntax

OutputFormatter Interface:

  • stdout?(output: unknown): unknown - Formats both stdout output and function return values
  • stderr?(output: string): string - Formats both stderr output and error messages

Note: Formatters apply to both loadCode() and runFunction() operations, but not to initialize().

Creating Sandbox Applications

For a detailed guide on implementing a Sandbox Application, please refer to docs/sandbox_application_guide.md.

import { SandboxApplication, SandboxController } from 'runspace-sandbox';

class CalculatorApp implements SandboxApplication {
  controller?: SandboxController;

  render() {
    // Create calculator UI
    const display = document.createElement('div');
    display.id = 'display';

    const addButton = document.createElement('button');
    addButton.textContent = '+';
    addButton.onclick = () => this.calculate('add');

    document.body.append(display, addButton);

    // Listen to runtime events
    this.controller?.on('stdout', (data) => {
      console.log('Output:', data);
    });
  }

  async calculate(operation: string) {
    if (!this.controller) return;

    const a = 5;
    const b = 3;
    const result = await this.controller.runFunction(operation, [a, b]);

    const display = document.getElementById('display');
    if (display) {
      display.textContent = `Result: ${result}`;
    }
  }
}

Project Structure

sandbox/
├── packages/              # Core library source code
│   ├── core/             # Core components
│   │   ├── Sandbox.ts        # Main orchestrator
│   │   ├── SandboxBridge.ts  # Communication layer
│   │   ├── SandboxController.ts  # Runtime controller
│   │   ├── PyodideRuntime.ts # Pyodide runtime implementation
│   │   ├── types.ts          # TypeScript type definitions
│   │   └── __tests__/        # Unit tests
│   ├── builtin/          # Built-in applications
│   │   └── apps/
│   │       └── console/  # ConsoleApp (headless, auto-runs main)
│   └── index.ts          # Public API exports
├── examples/             # Example applications
│   ├── calculator/       # Calculator demo (interactive UI)
│   └── console/          # Console demo (auto-run main)
├── docs/                 # Documentation
│   ├── architecture.md   # Architecture overview
│   ├── sandbox_application_guide.md # Guide for Sandbox Application creation
│   ├── console_app_guide.md # Guide for ConsoleApp usage
│   ├── workspace_host_guide.md # Guide for Workspace Host creation
│   └── diagrams/         # PlantUML diagrams
├── dist/                 # Compiled output
└── package.json          # Package configuration

Development

Build

Compile TypeScript to JavaScript with type declarations:

npm run build

Test

Run the test suite:

npm test

Type Check

Verify TypeScript types without compilation:

npm run typecheck

Development Server

Run the calculator example with hot reload:

npm run dev

Run the console example:

npm run dev:console

Clean

Remove build artifacts:

npm run clean

Message Protocol

The sandbox uses a typed message protocol for communication:

Message Types

  • READY_FOR_INIT: Sandbox is loaded and waiting for initialization
  • INIT: Command to initialize the Sandbox runtime
  • SETUP_STREAM: Transfers a MessagePort for streaming output
  • INITIALIZED: Sandbox initialization is complete
  • LOAD_CODE: Load user code files
  • CODE_LOADED: Code successfully loaded

Message Format

interface BaseMessage {
  type: MessageType;
  id?: string;
}

interface LoadCodeMessage extends BaseMessage {
  type: 'LOAD_CODE';
  payload: {
    workspace: {
      files: Array<{
        path: string;
        content: string;
      }>;
      config?: {
        build?: Array<{ command: string; args?: string[]; }>;
        run?: Array<{ command: string; args?: string[]; }>;
        entryPoints?: string[];
      };
    };
  };
}

Execution Flow

Initialization Sequence

  1. WorkspaceHost loads sandbox iframe
  2. SandboxBridge sends READY_FOR_INIT to WorkspaceHost
  3. WorkspaceHost sends INIT to SandboxBridge
  4. SandboxBridge initializes Controller and Runtime
  5. Runtime signals ready state
  6. SandboxBridge sets up streaming and sends SETUP_STREAM (with port) to WorkspaceHost
  7. SandboxBridge sends INITIALIZED to WorkspaceHost

Code Execution Flow

  1. User interacts with SandboxApplication UI
  2. Application calls controller.runFunction(name, args)
  3. Controller forwards request to runtime
  4. Runtime executes code and returns result
  5. Controller returns result to application
  6. Runtime output (stdout/stderr) streamed via MessageChannel to WorkspaceHost
  7. Controller also emits events for local subscribers

Examples

Calculator Example

A fully functional calculator demonstrating:

  • Custom SandboxApplication implementation
  • Python code execution via Pyodide
  • Real-time UI updates
  • Error handling and output streaming

Run the example:

npm run dev

Then open your browser to the displayed URL.

API Reference

Sandbox

class Sandbox {
  constructor(config: SandboxConfig);
  initialize(): Promise<void>;
}

interface SandboxConfig {
  runtime: new () => Runtime;
  app?: SandboxApplication;
  formatters?: OutputFormatter;
  targetWindow?: Window;
  targetOrigin?: string;
}

interface OutputFormatter {
  stdout?(output: unknown): unknown;
  stderr?(output: string): string;
}

SandboxController

interface SandboxController {
  runFunction(name: string, args: unknown[]): Promise<unknown>;
  on(event: 'stdout' | 'stderr' | 'error' | 'code_loaded', handler: (message: string) => void): void;
  isReady(): boolean;
  isCodeLoaded(): boolean;
}

Runtime

interface Runtime {
  initialize(): Promise<void>;
  loadCode(workspace: Workspace): Promise<void>;
  runFunction(name: string, args: unknown[]): Promise<unknown>;
  getStdout(): string;
  getStderr(): string;
  clearOutput(): void;
  isReady(): boolean;
}

interface Workspace {
  files: WorkspaceFile[];
  config?: WorkspaceConfig;
}

interface WorkspaceFile {
  path: string;
  content: string;
}

interface WorkspaceConfig {
  build?: PipelineStep[];
  run?: PipelineStep[];
  entryPoints?: string[];
}

interface PipelineStep {
  command: string;
  args?: string[];
}

SandboxApplication

interface SandboxApplication {
  controller?: SandboxController;
  render(): void;
  onCodeLoaded?(): void;  // Optional lifecycle hook
}

Testing

The project includes comprehensive test coverage:

  • Unit Tests: Test individual components in isolation
  • Integration Tests: Verify component interactions
  • Runtime Tests: Validate code execution behavior

Test files are located in packages/__tests__/.

TypeScript Support

Full TypeScript support with exported type definitions:

import type {
  Runtime,
  SandboxApplication,
  SandboxController,
  SandboxConfig,
  SandboxMessage,
  Workspace,
  WorkspaceFile,
  WorkspaceConfig,
  PipelineStep,
  OutputFormatter
} from 'sandbox';

Browser Compatibility

  • Modern browsers with ES2020+ support
  • WebAssembly support required for PyodideRuntime
  • postMessage API for iframe communication

TODO

  • [x] Add configurable entry point for PyodideRuntime.loadCode() via Workspace config (build/run pipelines, entryPoints)
  • [x] Support console-only applications without UI rendering (pure stdin/stdout/stderr streaming)
  • [x] Implement stdin streaming to sandbox runtime
  • [x] Add real-time stdout streaming during function execution (currently only emitted after completion)

Contributing

This is a private project. For questions or suggestions, contact the author.

License

UNLICENSED - Private project