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

@ricsam/isolate

v0.1.26

Published

Unified runtime host for app servers, script runtimes, browser runtimes, module resolution, file bindings, and typechecking

Downloads

1,494

Readme

@ricsam/isolate

@ricsam/isolate is a runtime host for running JavaScript and TypeScript inside isolated V8 sandboxes. It gives you one host API for short-lived scripts, long-lived app servers, browser-backed tests, persistent sessions, module loading, files, fetch, and nested sandboxes.

Use it when you want a higher-level sandbox than raw isolated-vm: the host stays in control of capabilities, while sandboxed code gets a web-style runtime surface.

Getting Started

Installation

npm add @ricsam/isolate @ricsam/isolated-vm

@ricsam/isolate expects the async-context-enabled @ricsam/isolated-vm peer. Upstream isolated-vm does not provide the required createContext({ asyncContext: true }) support and will fail fast during runtime boot.

Install Playwright when you want browser-enabled runtimes or test runtimes:

npm add playwright

createIsolateHost() will auto-start a daemon when needed, so the default setup is usually enough to get going.

Quick Start

import {
  createFileBindings,
  createIsolateHost,
  createModuleResolver,
} from "@ricsam/isolate";

const host = await createIsolateHost({
  daemon: {
    socketPath: "/tmp/isolate.sock",
  },
});

const runtime = await host.createRuntime({
  bindings: {
    console: {
      onEntry(entry) {
        if (entry.type === "output") {
          console.log(entry.stdout);
        }
      },
    },
    fetch: async (request) => await fetch(request),
    files: createFileBindings({
      root: process.cwd(),
      allowWrite: true,
    }),
    modules: createModuleResolver()
      .virtual(
        "@/env",
        `export const mode = "sandbox";`,
        { filename: "env.ts", resolveDir: "/app" },
      )
      .virtual(
        "/app/main.ts",
        `
          import { mode } from "@/env";

          const response = await fetch("https://example.com");
          console.log("mode:", mode);
          console.log("status:", response.status);
          console.log(await greet("isolate"));
        `,
        { filename: "main.ts", resolveDir: "/app" },
      ),
    tools: {
      greet: async (name: string) => `hello ${name}`,
    },
  },
});

try {
  await runtime.eval(`import "/app/main.ts";`, { filename: "/app/entry.ts" });
} finally {
  await runtime.dispose();
  await host.close();
}

That example wires together the most common capabilities:

  • console to forward sandbox output
  • fetch for outbound HTTP requests
  • files for root-scoped filesystem access
  • modules for virtual modules and source trees
  • tools for async host functions

Event-style callbacks such as console.onEntry(...), runtime.test.onEvent(...), and Playwright onEvent(...) are sync-only, best-effort notifications. Returned promises are ignored after rejection logging, so schedule any async follow-up work from inside the synchronous handler.

Guides

Pick A Runtime

The host can create four runtime styles:

  • host.createRuntime() for scripts, agents, and ad hoc execution
  • host.createAppServer() for long-lived serve() request handlers
  • host.createTestRuntime() for test suites with describe, test, hooks, and expect
  • host.getNamespacedRuntime() for persistent sessions that survive soft dispose and can be reacquired later

Cancel Running Work

eval(), run(), runTests(), and app-server handle() accept web-style AbortSignal options.

If the signal is already aborted before the call starts, the call rejects with AbortError and does not start or dispose the runtime. If a signal aborts while eval(), run(), or runTests() is in flight, the runtime is hard-disposed and the handle becomes unusable. This is intentionally a hard cancellation path so CPU-bound code such as while (true) {} cannot keep running.

const runtime = await host.createRuntime({});
const controller = new AbortController();

const running = runtime.eval(
  `while (true) {}`,
  { signal: controller.signal },
);

controller.abort();

await running.catch((error) => {
  if (error.name !== "AbortError") {
    throw error;
  }
});

// The runtime was terminated by the abort.
await runtime.eval(`1 + 1`).catch((error) => {
  console.log(error.message);
});

Hard runtime termination cascades through nested hosts. If a parent runtime is aborted, times out, is hard-disposed, or is disposed during execution, child runtimes, nested app servers, test runtimes, and namespaced runtimes created through the sandbox @ricsam/isolate module are also disposed.

Signals created inside sandbox code can cancel nested work live:

await runtime.eval(`
  import { createIsolateHost } from "@ricsam/isolate";

  const nestedHost = createIsolateHost();
  const child = await nestedHost.createRuntime();
  const controller = new AbortController();

  const running = child.eval("await new Promise(() => {})", {
    signal: controller.signal,
  });

  controller.abort();

  try {
    await running;
  } catch (error) {
    console.log(error.name); // AbortError
  }
`);

For app servers, handle(request, { signal }) is request-scoped. Aborting the request signal cancels that one request and is visible as request.signal.aborted inside the sandbox; it does not dispose the server runtime unless the owning runtime is otherwise terminated. Inside a sandbox app server, pass request.signal to a nested app server handle() call to forward the same request cancellation.

Configure Bindings

Bindings define how sandboxed code talks to the host:

  • console forwards runtime and browser console output
  • fetch handles outbound HTTP requests
  • files exposes a safe, root-scoped filesystem
  • modules resolves virtual modules, source trees, mounted packages, and fallbacks
  • tools exposes async host functions and async iterators
  • browser exposes a Playwright-like browser surface

Every host callback receives a HostCallContext with an AbortSignal, runtime identity, resource identity, and request metadata.

fetch is disabled unless you provide a host binding. If sandbox code calls fetch(...) without a bindings.fetch callback, the runtime rejects the request instead of falling back to the Node.js process fetch. To allow outbound network access, pass an explicit policy-enforcing callback:

const runtime = await host.createRuntime({
  bindings: {
    fetch: async (request, context) => {
      // Validate, log, rate-limit, meter, or rewrite here.
      return await fetch(request, {
        signal: context.signal,
      });
    },
  },
});

When exposing browser support, choose exactly one mode per runtime:

  • factory-first: provide createContext() and optionally createPage(), readFile(), and writeFile()
  • handler-first: provide handler, usually from createPlaywrightSessionHandler(...)

Do not mix handler with createContext() / createPage() / readFile() / writeFile() in the same binding.

Keep bindings plain-data and host-owned. Do not leak raw isolated-vm handles or other engine objects into untrusted code.

Create Nested Hosts Inside The Sandbox

Sandbox code can import @ricsam/isolate and create child runtimes against the same top-level host connection when the parent runtime opts in with nestedHost.

const runtime = await host.createRuntime({
  bindings: {
    console: {
      onEntry(entry) {
        if (entry.type === "output") {
          console.log(entry.stdout);
        }
      },
    },
    fetch: async (request) => await fetch(request),
  },
  nestedHost: {
    fetch: "inherit",
    maxTotalResources: 8,
    maxRuntimes: 6,
    maxAppServers: 2,
    maxMemoryLimitMB: 128,
    maxExecutionTimeoutMs: 30_000,
    maxAppServerLifetimeMs: 10 * 60_000,
  },
});

await runtime.eval(`
  import { createIsolateHost } from "@ricsam/isolate";

  const nestedHost = createIsolateHost();
  const child = await nestedHost.createRuntime({
    bindings: {
      tools: {
        greet: async (name) => "hello " + name,
      },
    },
  });

  await child.eval('console.log(await greet("nested"))');
  await child.dispose();
  await nestedHost.close();
`);

Nested hosts support:

  • createRuntime()
  • createAppServer()
  • createTestRuntime()
  • getNamespacedRuntime()
  • disposeNamespace()
  • diagnostics()
  • close()

The synthetic sandbox module also exports createModuleResolver() for isolate-authored module bindings. It supports pure in-sandbox virtual(), sourceTree(), and fallback() resolution. Host-backed helpers such as createFileBindings(), virtualFile(), and mountNodeModules() are intentionally not exposed inside sandbox code.

Nested resources are brokered by the parent host. The parent nestedHost policy controls whether child runtimes inherit the parent fetch binding, how many nested resources can exist across the whole descendant tree, and the maximum memory and execution timeout each child may request. Namespace keys created from inside a nested host are internally scoped so sandbox code can dispose only namespaces it created through that nested host.

The available policy fields are:

  • fetch: "inherit" | "disabled" - defaults to "disabled"
  • maxTotalResources - total nested runtimes, test runtimes, namespaced runtimes, and app servers
  • maxRuntimes - nested script, test, and namespaced runtimes
  • maxAppServers - nested app servers
  • maxMemoryLimitMB - maximum per-child memory limit
  • maxExecutionTimeoutMs - maximum per-child execution timeout
  • maxAppServerLifetimeMs - optional hard lifetime for nested app servers

If nestedHost is omitted, nested hosts use conservative defaults and do not inherit fetch. Pass nestedHost: false to disable the synthetic sandbox @ricsam/isolate module entirely for a runtime.

Build An App Server

createAppServer() is the long-lived server API. It boots a runtime around an entry module that calls serve() and lets the host dispatch requests into it.

import { createIsolateHost, createModuleResolver } from "@ricsam/isolate";

const host = await createIsolateHost();
const server = await host.createAppServer({
  key: "example/server",
  entry: "/server.ts",
  bindings: {
    modules: createModuleResolver().virtual(
      "/server.ts",
      `
        serve({
          fetch(request) {
            return Response.json({
              pathname: new URL(request.url).pathname,
            });
          },
        });
      `,
    ),
  },
});

const result = await server.handle(new Request("http://localhost/hello"));
if (result.type === "response") {
  console.log(await result.response.json());
}

await server.dispose();
await host.close();

server.handle() returns either an HTTP response or WebSocket upgrade metadata. Pass handle(request, { signal }) to forward request cancellation into the sandbox as request.signal. For upgraded connections, server.ws lets the host send open, message, close, and error events back into the runtime.

Add Browser Support To Script And Server Runtimes

If you provide bindings.browser, script and app runtimes get a global browser factory even when they are not full Playwright browser runtimes.

import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";

const browser = await chromium.launch();
const host = await createIsolateHost();

const runtime = await host.createRuntime({
  bindings: {
    browser: {
      createContext: async (options) =>
        await browser.newContext(options ?? undefined),
      createPage: async (contextInstance) =>
        await contextInstance.newPage(),
    },
  },
});

await runtime.eval(`
  const ctx = await browser.newContext({
    viewport: { width: 1280, height: 720 },
  });
  const page = await ctx.newPage();

  await page.goto("https://example.com");
  console.log(await page.title());
  console.log(typeof browser.close);

  await page.close();
  await ctx.close();
`);

await runtime.dispose();
await browser.close();
await host.close();

Inside these runtimes:

  • browser.newContext() is available
  • browser.contexts() is available
  • context.newPage() is available
  • context.pages() is available
  • context.newCDPSession(page) is available for Chromium-backed contexts
  • page.target().createCDPSession() is available as a Puppeteer-style CDP shim
  • page.close() and context.close() are available
  • browser.close() is not exposed inside the sandbox
  • page and context are never injected as implicit globals

Persist Browser Profiles

Browser profiles are isolate-owned virtual filesystem data. Enable them by providing bindings.files and bindings.browser.profiles; sandbox code then uses profile IDs instead of host paths.

import { chromium } from "playwright";
import {
  createFileBindings,
  createIsolateHost,
} from "@ricsam/isolate";

const browser = await chromium.launch();
const host = await createIsolateHost();

const runtime = await host.createRuntime({
  bindings: {
    files: createFileBindings({
      root: "./session-data",
      allowWrite: true,
    }),
    browser: {
      profiles: true,
      createContext: async (options) =>
        await browser.newContext(options ?? undefined),
      createPage: async (contextInstance) =>
        await contextInstance.newPage(),
    },
  },
});

await runtime.eval(`
  const ctx = await browser.newContext({ profile: "auth/main" });
  const page = await ctx.newPage();
  await page.goto("https://example.com");
  await ctx.storageState({ path: "/snapshots/auth.json" });
  await ctx.close();

  const restored = await browser.newContext({
    storageState: "/snapshots/auth.json",
  });
  await restored.close();
`);

await runtime.dispose();
await browser.close();
await host.close();

For full browser profile directories, provide createPersistentContext() and request a persistent profile. The host receives a temporary real userDataDir; @ricsam/isolate materializes the virtual profile into it before launch and syncs it back on context.close().

const runtime = await host.createRuntime({
  bindings: {
    files: createFileBindings({
      root: "./session-data",
      allowWrite: true,
    }),
    browser: {
      profiles: { defaultMode: "persistent" },
      createPersistentContext: async (userDataDir, options) =>
        await chromium.launchPersistentContext(userDataDir, {
          ...(options ?? {}),
          headless: true,
        }),
      createPage: async (contextInstance) =>
        await contextInstance.newPage(),
    },
  },
});

await runtime.eval(`
  const ctx = await browser.newContext({ profile: "chrome/main" });
  const page = await ctx.newPage();
  await page.goto("https://example.com");
  await ctx.close();
`);

Profile IDs are validated relative paths such as auth/main. Concurrent opens for the same profile are rejected. Nested isolates that reuse bindings: { browser } inherit the same profile store without receiving a raw host path.

Run Tests Inside The Sandbox

createTestRuntime() enables describe, test / it, hooks, and expect. If you also provide bindings.browser, the same test runtime gets Playwright-style browser access and matcher support.

import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";

const browser = await chromium.launch();
const host = await createIsolateHost();
const runtime = await host.createTestRuntime({
  key: "example/browser-test",
  bindings: {
    browser: {
      captureConsole: true,
      createContext: async (options) =>
        await browser.newContext(options ?? undefined),
      createPage: async (contextInstance) =>
        await contextInstance.newPage(),
    },
  },
});

const unsubscribe = runtime.test.onEvent((event) => {
  if (event.type === "testStart") {
    console.log("running", event.test.fullName);
  }
});

const result = await runtime.run(
  `
    let ctx;
    let page;

    beforeAll(async () => {
      ctx = await browser.newContext();
      page = await ctx.newPage();
    });

    afterAll(async () => {
      await ctx.close();
    });

    test("loads a page", async () => {
      expect((await browser.contexts()).length).toBe(1);
      expect((await ctx.pages()).length).toBe(1);
      await page.goto("https://example.com", {
        waitUntil: "domcontentloaded",
      });
      await expect(page).toHaveTitle(/Example Domain/);
    });
  `,
  {
    filename: "/browser-test.ts",
    timeoutMs: 10_000,
  },
);

console.log(result);

unsubscribe();
await runtime.dispose();
await browser.close();
await host.close();

From inside another sandbox, nestedHost.createTestRuntime() can reuse the sandbox browser handle:

import { createIsolateHost } from "@ricsam/isolate";

const nestedHost = createIsolateHost();
const child = await nestedHost.createTestRuntime({
  bindings: {
    browser,
  },
});

await child.run(`
  let ctx;
  let page;

  beforeAll(async () => {
    ctx = await browser.newContext();
    page = await ctx.newPage();
  });

  afterAll(async () => {
    await ctx.close();
  });

  test("loads a nested page", async () => {
    await page.goto("https://example.com");
    await expect(page).toHaveTitle(/Example Domain/);
  });
`);

await child.dispose();
await nestedHost.close();

Reuse A Namespaced Session

host.getNamespacedRuntime(key, options) is the persistent-session API. Use it when you want one underlying runtime to survive across multiple calls while refreshing host bindings on each acquire.

import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";
import { createPlaywrightSessionHandler } from "@ricsam/isolate/playwright";

const browser = await chromium.launch();
const host = await createIsolateHost();
const playwright = createPlaywrightSessionHandler({
  createContext: async (options) =>
    await browser.newContext(options ?? undefined),
  createPage: async (context) =>
    await context.newPage(),
});

const session = await host.getNamespacedRuntime("playwright:preview:session", {
  bindings: {
    browser: {
      handler: playwright.handler,
    },
  },
});

await session.eval(`
  globalThis.ctx = await browser.newContext();
  globalThis.page = await globalThis.ctx.newPage();
  await globalThis.page.goto("https://example.com");
`);

await session.dispose();

const reused = await host.getNamespacedRuntime("playwright:preview:session", {
  bindings: {
    browser: {
      handler: playwright.handler,
    },
  },
});

const unsubscribe = reused.test.onEvent((event) => {
  if (event.type === "testStart") {
    console.log("running", event.test.fullName);
  }
});

const results = await reused.runTests(`
  test("sees the existing browser state", async () => {
    const contexts = await browser.contexts();
    expect(contexts.length).toBe(1);
    const pages = await contexts[0].pages();
    expect(pages.length).toBe(1);
  });
`);

console.log(results.success);

unsubscribe();
await host.disposeNamespace("playwright:preview:session");
await browser.close();
await host.close();

Lifecycle notes:

  • only one live handle per namespace is allowed at a time
  • runTests(code) resets test registration before loading and running the provided suite
  • session.test.onEvent(...) exposes suite and test lifecycle events for timeout and progress reporting
  • runtime globals, module state, and Playwright resources survive soft dispose and reacquire
  • browser shutdown stays host-owned, while page and context shutdown stay sandbox-owned

Use Async Context Inside The Sandbox

Runtimes created by @ricsam/isolate enable the TC39 proposal-style AsyncContext global inside the sandbox. This experimental surface is also used to implement the node:async_hooks shim exposed to sandboxed code.

This shim is for async context propagation inside the sandbox. It is not a full reimplementation of Node's async_hooks lifecycle, resource graph, or profiling APIs.

Currently supported:

  • AsyncContext.Variable
  • AsyncContext.Snapshot
  • node:async_hooks AsyncLocalStorage
  • node:async_hooks AsyncResource
  • node:async_hooks createHook()
  • node:async_hooks executionAsyncId()
  • node:async_hooks triggerAsyncId()
  • node:async_hooks executionAsyncResource()
  • node:async_hooks asyncWrapProviders
import {
  createHook,
  executionAsyncResource,
} from "node:async_hooks";

const hook = createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    resource.requestTag = type + ":" + asyncId;
  },
  before() {
    console.log(executionAsyncResource().requestTag ?? null);
  },
}).enable();

setTimeout(() => {
  console.log(executionAsyncResource().requestTag);
  hook.disable();
}, 0);

API

Package Entry Points

  • @ricsam/isolate exports createIsolateHost(), createModuleResolver(), createFileBindings(), getTypeProfile(), typecheck(), formatTypecheckErrors(), and public types such as HostBindings, NestedHostPolicy, and runtime handles
  • @ricsam/isolate/playwright exports createPlaywrightSessionHandler() and related Playwright handler types
  • inside sandbox code, @ricsam/isolate is also available as a synthetic module that exports sandbox-only createIsolateHost(), createModuleResolver(), and nested-safe type aliases for runtime handles and bindings

createIsolateHost()

createIsolateHost() creates the top-level host connection. The returned host exposes:

  • createRuntime(options) for script execution
  • createAppServer(options) for long-lived serve() entrypoints
  • createTestRuntime(options) for tests
  • getNamespacedRuntime(key, options) for persistent sessions
  • disposeNamespace(key, options?) for hard-deleting a namespace
  • diagnostics() for host-level diagnostics
  • close() to shut everything down

CreateIsolateHostOptions currently supports engine: "auto" and daemon options such as socketPath, entrypoint, cwd, timeoutMs, and autoStart.

Runtime creation options support:

  • bindings for host-owned capabilities
  • cwd for path resolution
  • executionTimeout for eval/test execution limits
  • memoryLimitMB for isolate memory limits
  • nestedHost for controlling sandbox-created child runtimes, or false to disable nested host access

Execution methods support:

  • runtime.eval(code, { filename?, executionTimeout?, signal? })
  • testRuntime.run(code, { filename?, timeoutMs?, signal? })
  • namespacedRuntime.eval(code, { filename?, executionTimeout?, signal? })
  • namespacedRuntime.runTests(code, { filename?, timeoutMs?, signal? })
  • appServer.handle(request, { requestId?, metadata?, signal? })

When signal aborts an in-flight script or test execution, the runtime is hard-disposed and nested resources are terminated recursively. Pre-aborted signals reject with AbortError before starting execution.

createModuleResolver()

createModuleResolver() returns a fluent builder. You can mix and match:

  • virtual(specifier, source, options) for inline modules
  • virtualFile(specifier, filePath, options) for a host file mapped to a virtual specifier
  • sourceTree(prefix, loader) for lazy source loading under a virtual path
  • mountNodeModules(virtualMount, hostPath) for package resolution from a real node_modules
  • fallback(loader) for custom last-resort resolution

createFileBindings()

createFileBindings({ root, allowWrite }) creates a filesystem bridge that stays inside the configured root directory. Attempts to escape that root are rejected, and write operations are disabled unless allowWrite is true.

Typechecking

The typecheck helpers let you validate sandbox code against supported capability profiles before executing it.

import {
  formatTypecheckErrors,
  getTypeProfile,
  typecheck,
} from "@ricsam/isolate";

const profile = getTypeProfile({
  profile: "browser-test",
  capabilities: ["files"],
});

console.log(profile.include);

const result = typecheck({
  code: "page.goto('/')",
  profile: "browser-test",
});

if (!result.success) {
  console.error(formatTypecheckErrors(result.errors));
}

Built-in profiles:

  • backend
  • agent
  • browser-test

Capabilities can extend a profile with fetch, files, tests, browser, tools, console, crypto, encoding, and timers.

@ricsam/isolate/playwright

createPlaywrightSessionHandler() builds a handler-first browser binding for namespaced sessions and other Playwright-backed runtimes.

It accepts host callbacks such as:

  • createContext
  • createPersistentContext
  • createPage
  • readFile
  • writeFile
  • profiles
  • evaluatePredicate

It returns:

  • handler for bindings.browser.handler
  • getCollectedData() for collected browser artifacts
  • getTrackedResources() for active contexts and pages
  • clearCollectedData() to reset collected artifacts
  • onEvent(callback) for sync-only, best-effort Playwright event subscriptions

isolate-daemon

The package also exposes an isolate-daemon binary:

isolate-daemon --socket /tmp/isolate.sock

By default, createIsolateHost() will auto-start a daemon when needed. You can also point the host at an already-running daemon with daemon.socketPath, or disable auto-start with daemon.autoStart: false.