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

@browsernode/sandbox

v0.0.22

Published

Browser sandbox running Next.js and Vite apps entirely client-side

Readme

@browsernode/sandbox

A virtual filesystem bundler and browser runtime for framework sandboxes. Build and run Next.js applications entirely in the browser from virtual files.

Packages

| Package | Description | |---------|-------------| | @browsernode/sandbox | Core sandbox runtime — virtual filesystem, bundler, build pipeline. Built-in 'nextjs' and 'vite' framework adapters; bash subpath for shell. | | @browsernode/react | React hooks and components (useSandbox, <SandboxBrowser />) |

The bundler is built-in: esbuild-wasm runs in the browser by default, and bundler: 'native' swaps in the native esbuild binary on a Node server. Install whichever you use as a peer dep — yarn add esbuild-wasm (browser) or yarn add esbuild (native).

Quick Start

import { createSandbox } from '@browsernode/sandbox';

const sandbox = await createSandbox({
  framework: 'nextjs',  // or 'vite'; bundler defaults to esbuild-wasm
});

await sandbox.fs.writeFiles({
  'app/layout.tsx': `
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return <html><body>{children}</body></html>;
    }
  `,
  'app/page.tsx': `
    export default function Home() {
      return <h1>Hello from the sandbox</h1>;
    }
  `,
});

const result = await sandbox.build();

if (result.ok) {
  console.log(result.html);       // self-contained HTML document
  console.log(result.stats);      // { duration: number, bundleSize: number }
} else {
  console.log(result.html);       // styled error page ready to serve
  console.error(result.errors);   // Diagnostic[]
}

API

createSandbox(options)

Creates a new sandbox instance.

const sandbox = await createSandbox({
  framework: 'nextjs',
  bundler: 'native',                          // optional — defaults to 'wasm'
  source: { type: 'git', url: 'https://github.com/user/repo.git' },
  dependencies: { 'lodash': '4.17.21' },
  scripts: ['https://cdn.jsdelivr.net/npm/[email protected]/fonts/remixicon.css'],
  sandboxId: 'project-123',
  env: { NEXT_PUBLIC_API_URL: 'https://api.example.com', STRIPE_KEY: 'sk_live_...' },
  network: { allow: ['*.example.com'], proxyUrl: '/api/proxy' },
  signal: abortController.signal,
});

| Option | Type | Description | |--------|------|-------------| | framework | 'nextjs' \| 'vite' \| FrameworkAdapter | Required. Pass 'nextjs' or 'vite' for the default adapter, or call the adapter directly (e.g. nextjs({ reactVersion: '18.3.1' })) when you need to customize options. | | bundler | 'wasm' \| 'native' \| BundlerAdapter | Optional. Defaults to 'wasm' (browser + node). Pass 'native' to use the faster esbuild binary on Node, or a custom adapter object for full control. | | source | SandboxSource | Initialize from files or a git repo | | dependencies | Record<string, string> | Override or augment the deps from package.json at build time. Not persisted. | | scripts | string[] | URLs for external scripts or stylesheets injected into the HTML | | sandboxId | string | Identifier for proxied requests AND the persistence key. Presence enables auto-persistence to OPFS. | | env | Record<string, string> | Key/value pairs exposed as process.env in the sandbox | | network | NetworkPolicy | Control outbound network access ('allow-all', 'deny-all', or { allow, proxyUrl }) | | signal | AbortSignal | Abort signal to auto-dispose the sandbox | | persistence | PersistenceAdapter \| false | Override the default OPFS persistence with a custom backend, or false to disable. | | persistenceDebounceMs | number | Debounce window for auto-save (default 500ms). |

buildSandbox(options)

One-shot convenience — creates a sandbox, writes files, builds, and disposes in one call. Returns a BuildResult.

import { buildSandbox } from '@browsernode/sandbox';

const result = await buildSandbox({
  framework: 'nextjs',
  files: {
    'app/layout.tsx': '...',
    'app/page.tsx': '...',
  },
});

File System

The virtual filesystem is fully in-memory. Paths are normalized (no leading / or ./).

// Write
await sandbox.fs.writeFile('app/page.tsx', 'export default () => <h1>Hi</h1>');
await sandbox.fs.writeFiles({ 'a.ts': '...', 'b.ts': '...' });

// Read
const source = await sandbox.fs.readFile('app/page.tsx');
const binary = await sandbox.fs.readBinary('image.png');
const info = await sandbox.fs.stat('app/page.tsx');  // { type: 'file', size: number }

// Query (synchronous)
sandbox.fs.exists('app/page.tsx');   // true
sandbox.fs.list();                   // ['app/layout.tsx', 'app/page.tsx']
sandbox.fs.list('app/');             // ['app/layout.tsx', 'app/page.tsx']
sandbox.fs.tree();                   // { app: { 'layout.tsx': null, 'page.tsx': null } }

// Mutate
await sandbox.fs.copyFile('a.ts', 'b.ts');
await sandbox.fs.rename('old.ts', 'new.ts');
await sandbox.fs.rm('temp.ts');
await sandbox.fs.mkdir('lib');
await sandbox.fs.truncate('log.txt', 0);

// Transactions — batches change events into a single emission
await sandbox.fs.transaction(async (fs) => {
  await fs.writeFile('a.ts', '...');
  await fs.writeFile('b.ts', '...');
});

// Snapshots
const snap = sandbox.fs.snapshot();
// ... make changes ...
await sandbox.fs.restore(snap);

// Watch for changes
const unsubscribe = sandbox.fs.on('change', (events) => {
  // events: Array<{ type: 'create' | 'update' | 'delete' | 'rename', path: string }>
});

Network Policy

Control which domains the sandbox can reach and whether requests go through a proxy.

network: {
  allow: ['*'],                          // allow all (default when no network option)
  allow: [],                             // deny all
  allow: ['api.example.com'],            // exact domain
  allow: ['*.example.com'],              // wildcard subdomain
  allow: [...PROXY_DOMAINS, 'my.api'],   // preset list + custom
  proxyUrl: '/api/proxy',                // route allowed requests through proxy
}

allow controls which external domains are reachable. proxyUrl is independent — when set, allowed requests are rewritten as ${proxyUrl}/${originalUrl}. When not set, allowed requests go direct via fetch. Git operations also respect the proxy.

import { PROXY_DOMAINS } from '@browsernode/sandbox/nextjs';

const sandbox = await createSandbox({
  framework: 'nextjs',
  network: {
    allow: [...PROXY_DOMAINS, 'api.example.com'],
    proxyUrl: '/api/proxy',
  },
});

Git

Full git support powered by isomorphic-git, operating entirely on the in-memory filesystem.

// Initialize a new repo
await sandbox.git.init();

// Clone a repo into the sandbox
await sandbox.git.clone('https://github.com/user/repo.git', {
  ref: 'main',        // default: 'main'
  depth: 1,           // default: 1
  username: '...',     // optional, for private repos
  password: '...',     // optional, for private repos
});

// Or initialize from a git source at creation
const sandbox = await createSandbox({
  framework: 'nextjs',
  source: {
    type: 'git',
    url: 'https://github.com/user/repo.git',
    ref: 'main',
    depth: 1,
  },
});

Branching

await sandbox.git.branch('feature');
await sandbox.git.checkout('feature');

const current = await sandbox.git.currentBranch();   // 'feature'
const branches = await sandbox.git.listBranches();   // ['main', 'feature']

Staging & Committing

await sandbox.fs.writeFile('app/page.tsx', '...');

await sandbox.git.add('app/page.tsx');
// or add multiple files
await sandbox.git.add(['app/page.tsx', 'app/layout.tsx']);

const oid = await sandbox.git.commit('feat: add homepage', {
  author: { name: 'Alice', email: '[email protected]' },
});

Status & History

const fileStatus = await sandbox.git.status('app/page.tsx');

const matrix = await sandbox.git.statusMatrix({
  filter: (f) => !f.startsWith('node_modules/'),
});
// Returns Array<[filepath, head, workdir, stage]>

const changed = await sandbox.git.diff();  // string[] of changed paths

const log = await sandbox.git.log({ depth: 5 });
// Array<{ oid, message, author: { name, email, timestamp } }>

Push, Pull & Fetch

const auth = { username: 'token', password: 'ghp_...' };

await sandbox.git.push('origin', { ref: 'main', ...auth });
await sandbox.git.pull('origin', { ref: 'main', ...auth });
await sandbox.git.fetch('origin', { ref: 'main', ...auth });

const remoteBranches = await sandbox.git.listBranches({ remote: 'origin' });

NPM

sandbox.npm is backed by the VFS-resident package.json — installs and removes mutate that file directly, exactly like real npm. Reads merge dependencies and devDependencies.

await sandbox.npm.install('lodash', '4.17.21');           // writes to package.json#dependencies
await sandbox.npm.install({ axios: '^1', 'date-fns': '^3' });
await sandbox.npm.remove('lodash');                       // removes from both deps and devDeps
sandbox.npm.list();                                       // sync; merged deps + devDeps

If package.json doesn't exist, install auto-creates a minimal one (name: 'sandbox', version: '0.0.0'). Other fields (scripts, name, version, etc.) are preserved untouched. Dependency keys are alphabetized on every write for stable diffs. Malformed JSON throws on install/remove and emits an error event during build.

The dependencies createSandbox option overrides whatever's in package.json at build time only — it does not persist or appear in npm.list(). Useful for pinning or injecting deps without touching the VFS file.


Persistence

When sandboxId is provided, the sandbox auto-persists its filesystem to OPFS, hydrates from it on boot, and debounces saves on every change.

const sandbox = await createSandbox({
  framework: 'nextjs',
  sandboxId: 'project-1',                     // enables persistence
  source: { type: 'files', files: { ... } },  // only seeded on first boot
});
// AI agent / consumer writes files; OPFS auto-saves ~500ms after the last change.

// Manage stored sandboxes (default OPFS backend)
import { listSandboxes, deleteSandbox } from '@browsernode/sandbox';
const ids = await listSandboxes();      // ['project-1', 'project-2']
await deleteSandbox('project-1');

// Download as zip
const blob = await sandbox.download();   // Blob (lazy-imports jszip)
const url = URL.createObjectURL(blob);

Persisted state wins over source when both are present — source only seeds on first boot. To force a reset, await deleteSandbox(id) then create.

Custom persistence backend

Override the default OPFS adapter for any backend (remote DB, encrypted storage, Node fs, etc.):

import type { PersistenceAdapter } from '@browsernode/sandbox';

const remoteAdapter: PersistenceAdapter = {
  load: async (id) => fetch(`/api/sandboxes/${id}`).then(r => r.ok ? r.json() : null),
  save: async (id, snap) => fetch(`/api/sandboxes/${id}`, { method: 'PUT', body: JSON.stringify(snap) }),
  delete: async (id) => fetch(`/api/sandboxes/${id}`, { method: 'DELETE' }),
  list: async () => fetch('/api/sandboxes').then(r => r.json()),
};

const sandbox = await createSandbox({
  framework: 'nextjs',
  sandboxId: 'project-1',
  persistence: remoteAdapter,
});

// Or wrap the default adapter:
import { defaultPersistence } from '@browsernode/sandbox';
const audited: PersistenceAdapter = {
  ...defaultPersistence(),
  save: async (id, snap) => {
    analytics.track('sandbox.save', { id });
    return defaultPersistence().save(id, snap);
  },
};

// Or disable persistence entirely
createSandbox({ ..., sandboxId: 'foo', persistence: false });

The adapter receives a Snapshot ({ files: Map<string, string | Uint8Array> }) with normalized VFS paths. The default OPFS backend serializes to JSON-with-base64-binaries; custom adapters can use whatever wire format they want.


Events

sandbox.on('build', (result) => {
  if (result.ok) console.log('Built in', result.stats.duration, 'ms');
});

sandbox.on('console', (level, args) => {
  console.log(`[sandbox:${level}]`, ...args);
});

sandbox.on('error', (error) => {
  console.error('Sandbox error:', error);
});

sandbox.on('navigate', (url) => {
  console.log('Navigated to:', url);
});

sandbox.on('ready', () => {
  console.log('Sandbox ready');
});

// File system changes (cascaded from sandbox.fs.on('change')).
// Events arrive batched: writeFiles/transaction produce a single array.
sandbox.on('change', async (events) => {
  for (const e of events) {
    // e.type: 'create' | 'update' | 'delete' | 'rename'
    // e.path: string  (e.oldPath set for 'rename')
    if (e.type === 'delete' || e.path === '*') continue;
    const content = await sandbox.fs.readFile(e.path);  // fetch on demand
  }
});

// All listeners return an unsubscribe function
const unsub = sandbox.on('build', () => {});
unsub();

React

import { useEffect } from 'react';
import { useSandbox, SandboxBrowser } from '@browsernode/react';

function App() {
  const { sandbox, loading, building, result, build } = useSandbox({
    framework: 'nextjs',                     // or 'vite'
    // bundler defaults to esbuild-wasm
    sandboxId: 'project-1',                  // enables OPFS auto-persistence
    onChange: (events) => console.log('fs:', events),
  });

  useEffect(() => {
    if (sandbox) build();
  }, [sandbox]);

  if (loading) return <div>Initializing...</div>;

  return (
    <SandboxBrowser
      sandbox={sandbox}
      showUrlBar
      onBuild={(result) => console.log('Built:', result.ok)}
    />
  );
}

Architecture

Virtual Files (Map<string, string>)
        │
        ▼
┌──────────────────┐
│ Framework Adapter│  (e.g. framework: 'nextjs' or 'vite')
│                  │  - Generates router code
│                  │  - Collects CSS
│                  │  - Applies shims (next/image, next/navigation, etc.)
│                  │  - Detects server actions & API routes
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│      Bundler     │  esbuild-wasm by default; 'native' for Node servers
│                  │  - Bundles entry + plugins into a single JS output
│                  │  - Resolves virtual files via plugins
│                  │  - External deps resolved via import maps at runtime
└────────┬─────────┘
         │
         ▼
   HTML Document
   - Import map (react, react-dom, react-router-dom via esm.sh)
   - Bundled JS (app code + shims)
   - Collected CSS
   - <div id="root"> mount point

Next.js support

The Next.js adapter (framework: 'nextjs', or nextjs({ reactVersion }) for options) supports:

  • App Router — file-based routing with layouts, pages, error boundaries, not-found pages, route groups, dynamic segments, and catch-all routes
  • Server components — async components are wrapped for browser execution
  • Server actions"use server" files are detected and registered with a client-side dispatcher
  • API routesroute.ts files with GET/POST/PUT/DELETE handlers, intercepted via a patched fetch
  • Next.js shimsnext/image, next/link, next/navigation (useRouter, usePathname, useSearchParams, redirect, notFound), next/server (NextRequest, NextResponse), next/headers (cookies, headers), next/font/google, next/font/local, next/head
  • Node module shimspath, fs, crypto, buffer, stream, url, events, process, and more
  • CSS — global CSS files are collected and inlined into the HTML output
  • Metadata & viewport — extracted from layouts/pages and rendered as <meta> tags

Development

# Install dependencies
yarn

# Run tests
yarn test

# Run tests in watch mode
yarn test --watch

# Build all packages
yarn build

License

MIT - Frontend AI, Inc.