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

@scelar/nodepod

v1.8.2

Published

Browser-native Node.js runtime environment

Downloads

2,364

Readme

nodepod

npm license

Run Node.js in the browser. Filesystem, shell, npm packages, HTTP servers, no backend required.

npm install @scelar/nodepod

Getting started

Nodepod uses a service worker to route preview iframes and virtual HTTP servers. It has to be served from your own origin at /__sw__.js, browsers won't register a SW from node_modules.

Pick the one-liner for your framework:

Vite

// vite.config.ts
import { defineConfig } from 'vite';
import nodepod from '@scelar/nodepod/vite';

export default defineConfig({
  plugins: [nodepod()],
});

Next.js (App Router, works Next 13 through 16)

// app/__sw__.js/route.ts
export { GET } from '@scelar/nodepod/next';

If you already have a proxy.ts (Next 16+) or middleware.ts (Next <=15), compose nodepodProxy / nodepodMiddleware alongside your own handler instead. See docs/sw-setup.md.

Any framework with a Fetch-style handler (Hono, Bun, Cloudflare, Elysia, etc.)

import { serveSW } from '@scelar/nodepod/server';

app.get('/__sw__.js', () => serveSW());

Express / Fastify / bare http

import { serveSWNode } from '@scelar/nodepod/server';

app.get('/__sw__.js', async (_req, res) => {
  const { body, headers } = await serveSWNode();
  for (const [k, v] of Object.entries(headers)) res.setHeader(k, v);
  res.status(200).send(body);
});

Static host (copy the file)

No server code? Copy the file once into your public directory:

cp node_modules/@scelar/nodepod/dist/__sw__.js public/__sw__.js

See docs/sw-setup.md for the full story and how to customise the URL.

Usage

import { Nodepod } from '@scelar/nodepod';

const nodepod = await Nodepod.boot({
  files: {
    '/index.js': 'console.log("Hello from the browser!")',
  },
});

const proc = await nodepod.spawn('node', ['index.js']);
proc.on('output', (text) => console.log(text));
await proc.completion;

If the service worker isn't reachable at /__sw__.js, boot() throws a NodepodSWSetupError with the one-liner for your framework. Pass { serviceWorker: false } to skip SW setup entirely (SSR, Node tests).

Terminal

Plug in xterm.js for an interactive shell:

import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';

const terminal = nodepod.createTerminal({ Terminal, FitAddon });
terminal.attach('#terminal-container');

npm packages

await nodepod.install(['express']);
const proc = await nodepod.spawn('node', ['server.js']);

HTTP servers

Works with Express, Hono, Vite, and anything that calls listen():

const nodepod = await Nodepod.boot({
  files: {
    '/server.js': `
      const express = require('express');
      const app = express();
      app.get('/', (req, res) => res.json({ ok: true }));
      app.listen(3000);
    `,
  },
});

await nodepod.install(['express']);
await nodepod.spawn('node', ['server.js']);

const response = await nodepod.request(3000, 'GET', '/');
console.log(response.body); // { ok: true }

Snapshots

Save and restore the filesystem:

const snapshot = await nodepod.snapshot();
// ... later
await nodepod.restore(snapshot);

API

Nodepod.boot(options?)

| Option | Type | Description | |--------|------|-------------| | files | Record<string, string \| Uint8Array> | Initial files | | workdir | string | Working directory (default "/") | | env | Record<string, string> | Environment variables | | swUrl | string | Service Worker URL for preview iframes | | watermark | boolean | Show nodepod badge in previews (default true) | | onServerReady | (port, url) => void | Called when a virtual server starts | | allowedFetchDomains | string[] \| null | Extra CORS proxy domains. null = allow all |

Instance methods

| Method | Description | |--------|-------------| | spawn(cmd, args?, opts?) | Run a command | | install(packages) | Install npm packages | | createTerminal(opts) | Create an xterm.js terminal | | fs.readFile(path, enc?) | Read a file | | fs.writeFile(path, data) | Write a file | | fs.readdir(path) | List directory | | fs.stat(path) | File stats | | fs.mkdir(path, opts?) | Create directory | | fs.rm(path, opts?) | Remove file/directory | | snapshot() | Capture filesystem state | | restore(snapshot) | Restore from snapshot | | request(port, method, path) | Send request to virtual server | | port(num) | Get preview URL for a port | | setPreviewScript(js) | Inject JS into preview iframes | | clearPreviewScript() | Remove injected script |

Process events

spawn() returns a NodepodProcess:

proc.on('output', (text) => { }); // stdout
proc.on('error', (text) => { });  // stderr
proc.on('exit', (code) => { });   // exit code
await proc.completion;             // wait for exit

Polyfills

Full: fs, path, events, stream, buffer, process, http, https, net, crypto, zlib, url, querystring, util, os, tty, child_process, assert, readline, module, timers, string_decoder, perf_hooks, constants, punycode

Stubs: dns, worker_threads, vm, v8, tls, dgram, cluster, http2, inspector, domain, diagnostics_channel, async_hooks

In development: Native WASI/WASM loading for napi-rs based packages (rolldown, lightningcss, etc.)

Why nodepod?

We built nodepod for Scelar, an AI app builder that takes you from idea to production in minutes. Scelar needed a way to run real Node.js code directly in the browser so users could build, preview, and interact with their apps instantly without waiting for remote servers to spin up. No containers, no cold starts, no infrastructure to manage.

We open-sourced it because we think running Node in the browser shouldn't be a proprietary black box. If you're building a web IDE, coding playground, AI dev tool, or anything that needs server-side JS in the browser, nodepod is for you.

Development

git clone https://github.com/ScelarOrg/Nodepod.git
cd Nodepod
npm install
npm run build:publish   # build library + types
npm test                # run tests

Publishing a new version

npm version patch       # or minor / major
npm publish             # auto-builds before publishing
git push && git push --tags

Contributing

Contributions are really appreciated. This is a big project and it's hard to maintain and push updates on my own. If you want to help out, feel free to open a PR.

Note that nodepod is not my main focus, Scelar is. I work on nodepod when I have time for it, so responses to issues and PRs might take a bit.

Before opening a PR, make sure these pass:

npm run type-check      # 0 TypeScript errors
npm run build:publish   # builds cleanly
npm test                # tests pass

Code style:

  • Files are kebab-case (shell-parser.ts, memory-volume.ts). Polyfills match their Node.js module name (fs.ts, crypto.ts).
  • Classes and types are PascalCase (MemoryVolume, ShellResult). Functions and variables are camelCase.
  • Private properties and internal helpers use a leading underscore (_registry, _ensureSlot()).
  • Use named exports. Default exports only for polyfills that need to match Node's module.exports shape.

Commit messages follow conventional commits:

feat: add readline support
fix: resolve path edge case in fs.watch
chore: bump dependencies

If you're writing a polyfill:

  • Polyfill files live in src/polyfills/ and must be named after the Node.js module they replace.
  • EventEmitter methods must use _reg() for lazy init, never access this._registry directly.
  • Polyfills registered in CORE_MODULES must not use async functions.
  • ESM-to-CJS replacement strings must include trailing semicolons.

License

MIT + Commons Clause. Use it in anything, just don't resell nodepod itself.

Built by @R1ck404, part of Scelar.