npm package: @ricsam/quickjs-runtime
Description: Complete QuickJS runtime with fetch, fs, and core bindings
Author: Richard Samuelsson
Latest version: 0.2.25

# @ricsam/quickjs-runtime

The recommended way to create QuickJS sandboxed runtimes with web-standard APIs.

## Installation

```bash
bun add @ricsam/quickjs-runtime quickjs-emscripten
```

## Quick Start

```typescript
import { createRuntime } from "@ricsam/quickjs-runtime";

const runtime = await createRuntime({
  console: {
    onEntry: (entry) => {
      if (entry.type === "output") {
        console.log(`[sandbox:${entry.level}]`, ...entry.args);
      }
    },
  },
  fetch: async (request) => fetch(request),
});

await runtime.eval(`
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  console.log("Fetched:", data);
`);

await runtime.dispose();
```

## API

### createRuntime(options?)

Creates a fully configured QuickJS runtime with all WHATWG APIs.

```typescript
const runtime = await createRuntime({
  // Memory limit in megabytes
  memoryLimitMB: 10,

  // Console output handler
  console: {
    onEntry: (entry) => { /* handle console output */ },
  },

  // Fetch handler for outbound requests
  fetch: async (request) => fetch(request),

  // File system access
  fs: {
    getDirectory: async (path) => createNodeDirectoryHandle(`./sandbox${path}`),
  },

  // ES module loader
  moduleLoader: async (moduleName) => {
    if (moduleName === "@/utils") {
      return `export const add = (a, b) => a + b;`;
    }
    throw new Error(`Unknown module: ${moduleName}`);
  },

  // Custom host functions
  customFunctions: {
    hashPassword: {
      fn: async (password) => Bun.password.hash(password),
      type: 'async',
    },
    getConfig: {
      fn: () => ({ environment: "production" }),
      type: 'sync',
    },
  },

  // Enable test environment (describe, it, expect)
  testEnvironment: {
    onEvent: (event) => { /* handle test events */ },
  },

  // Playwright browser automation
  playwright: {
    page: playwrightPage,
    baseUrl: "https://example.com",
  },
});
```

### RuntimeHandle

The returned handle provides:

```typescript
interface RuntimeHandle {
  // Unique runtime identifier
  readonly id: string;

  // Execute code as ES module (supports top-level await)
  eval(code: string, filenameOrOptions?: string | EvalOptions): Promise<void>;

  // Dispose all resources
  dispose(): Promise<void>;

  // Sub-handles for specific features
  readonly fetch: RuntimeFetchHandle;
  readonly timers: RuntimeTimersHandle;
  readonly console: RuntimeConsoleHandle;
  readonly testEnvironment: RuntimeTestEnvironmentHandle;
  readonly playwright: RuntimePlaywrightHandle;
}

interface EvalOptions {
  // Filename for the evaluated code (for stack traces)
  filename?: string;
  // Maximum execution time in milliseconds
  maxExecutionMs?: number;
}
```

### Execution Timeout

Use `maxExecutionMs` to prevent infinite loops and long-running code:

```typescript
const runtime = await createRuntime();

// Set a 5 second timeout
await runtime.eval(`
  // Code that completes quickly
  const result = compute();
`, { maxExecutionMs: 5000 });

// Infinite loops will be interrupted
try {
  await runtime.eval(`
    while (true) { /* infinite loop */ }
  `, { maxExecutionMs: 100 });
} catch (error) {
  console.log("Execution timed out");
}

await runtime.dispose();
```

## Examples

### HTTP Server

```typescript
const runtime = await createRuntime({
  console: { onEntry: (e) => e.type === "output" && console.log(...e.args) },
});

await runtime.eval(`
  serve({
    fetch(request) {
      const url = new URL(request.url);
      return Response.json({ path: url.pathname });
    },
  });
`);

// Dispatch requests to the sandboxed server
const response = await runtime.fetch.dispatchRequest(
  new Request("http://localhost/api/users")
);
console.log(await response.json()); // { path: "/api/users" }

await runtime.dispose();
```

### Running Tests

```typescript
const runtime = await createRuntime({
  testEnvironment: {
    onEvent: (event) => {
      if (event.type === "testEnd") {
        const icon = event.test.status === "pass" ? "✓" : "✗";
        console.log(`${icon} ${event.test.fullName}`);
      }
    },
  },
});

await runtime.eval(`
  describe("Math", () => {
    it("adds numbers", () => {
      expect(1 + 1).toBe(2);
    });
  });
`);

const results = await runtime.testEnvironment.runTests();
console.log(`${results.passed}/${results.total} passed`);

await runtime.dispose();
```

### Browser Automation with Playwright

```typescript
import { chromium } from "playwright";

const browser = await chromium.launch();
const page = await browser.newPage();

const runtime = await createRuntime({
  testEnvironment: true,
  playwright: {
    page,
    baseUrl: "https://example.com",
  },
});

await runtime.eval(`
  describe("Homepage", () => {
    it("displays welcome message", async () => {
      await page.goto("/");
      await expect(page.getByRole("heading")).toContainText("Welcome");
    });
  });
`);

await runtime.testEnvironment.runTests();
await runtime.dispose();
await browser.close();
```

### Custom Functions

Custom functions expose host capabilities to the sandbox. Arguments and return values are automatically marshalled between host and QuickJS.

**Function types:**
- `type: 'sync'` - Synchronous function
- `type: 'async'` - Returns a Promise
- `type: 'asyncIterator'` - Returns an async iterable (for streaming)

```typescript
const runtime = await createRuntime({
  customFunctions: {
    // Sync function
    generateId: {
      fn: () => crypto.randomUUID(),
      type: 'sync',
    },
    // Async function
    hashPassword: {
      fn: async (password) => Bun.password.hash(password),
      type: 'async',
    },
    // Async iterator for streaming
    streamData: {
      fn: async function* (count) {
        for (let i = 0; i < count; i++) {
          await new Promise(r => setTimeout(r, 100));
          yield { index: i, timestamp: Date.now() };
        }
      },
      type: 'asyncIterator',
    },
  },
});

await runtime.eval(`
  const id = generateId();
  const hash = await hashPassword("secret123");

  for await (const item of streamData(3)) {
    console.log(item); // { index: 0, timestamp: ... }, etc.
  }
`);
```

**Supported return types** (auto-marshalled):

| Type | Notes |
|------|-------|
| `string`, `number`, `boolean`, `null`, `undefined`, `bigint` | Primitives |
| `{ key: value }` | Plain objects (nested supported, max depth 10) |
| `[1, 2, 3]` | Arrays |
| `Date` | Becomes QuickJS Date |
| `Uint8Array`, `ArrayBuffer` | Binary data |
| `Promise` | Becomes QuickJS Promise |
| Functions | Become callable from QuickJS (see below) |

**Returning callable functions:**

```typescript
customFunctions: {
  // Return a factory function
  createMultiplier: {
    fn: (factor) => (x) => x * factor,
    type: 'sync',
  },
  // Return an object with methods
  getDatabase: {
    fn: async () => {
      const db = await connectToDb();
      return {
        query: async (sql) => db.query(sql),
        close: () => db.close(),
      };
    },
    type: 'async',
  },
}
```

```javascript
// In sandbox:
const double = createMultiplier(2);
console.log(double(5)); // 10

const db = await getDatabase();
const users = await db.query("SELECT * FROM users");
db.close();
```

### ES Modules

```typescript
const runtime = await createRuntime({
  moduleLoader: async (moduleName) => {
    const modules = {
      "@/utils": `export const double = (n) => n * 2;`,
      "@/config": `export default { apiUrl: "https://api.example.com" };`,
    };
    if (moduleName in modules) {
      return modules[moduleName];
    }
    throw new Error(`Module not found: ${moduleName}`);
  },
});

await runtime.eval(`
  import { double } from "@/utils";
  import config from "@/config";

  console.log(double(21)); // 42
  console.log(config.apiUrl);
`);
```

## Included APIs

When you use `createRuntime()`, the following globals are automatically available in the sandbox:

- **Console**: `console.log`, `console.warn`, `console.error`, etc.
- **Fetch**: `fetch`, `Request`, `Response`, `Headers`, `FormData`, `AbortController`
- **Server**: `serve()` with WebSocket support
- **Crypto**: `crypto.getRandomValues()`, `crypto.randomUUID()`, `crypto.subtle`
- **Encoding**: `atob()`, `btoa()`
- **Timers**: `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
- **Streams**: `ReadableStream`, `WritableStream`, `TransformStream`
- **Blob/File**: `Blob`, `File`
- **Path**: `path.join()`, `path.resolve()`, etc.

Optional (when configured):
- **File System**: `getDirectory()` (requires `fs` option)
- **Test Environment**: `describe`, `it`, `expect` (requires `testEnvironment` option)
- **Playwright**: `page` object (requires `playwright` option)

## Security

- **No automatic network access** - `fetch` option must be explicitly provided
- **File system isolation** - `getDirectory` controls all path access
- **Memory limits** - Use `memoryLimitMB` option to prevent memory exhaustion
- **Execution timeouts** - Use `maxExecutionMs` in `eval()` to prevent infinite loops
- **No access to host** - Code runs in isolated QuickJS VM

## Advanced: Low-level API

For advanced use cases requiring direct context manipulation, you can use the low-level `setupRuntime()` function or individual package setup functions. See the individual package READMEs for details:

- [@ricsam/quickjs-core](../core)
- [@ricsam/quickjs-console](../console)
- [@ricsam/quickjs-fetch](../fetch)
- [@ricsam/quickjs-fs](../fs)
- [@ricsam/quickjs-crypto](../crypto)
- [@ricsam/quickjs-encoding](../encoding)
- [@ricsam/quickjs-timers](../timers)
- [@ricsam/quickjs-path](../path)
- [@ricsam/quickjs-test-environment](../test-environment)
- [@ricsam/quickjs-playwright](../playwright)