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

@statewalker/webrun-modules

v0.2.0

Published

Isomorphic module/dependency server: resolve + download + transform npm and authored TS/JS into browser-runnable ESM served from a local FilesApi cache, with no runtime CDN dependency

Readme

@statewalker/webrun-modules

Run authored TypeScript/JavaScript apps and the arbitrary npm modules they import in the browser (or Node) with no runtime CDN dependency — and with no install step: packages are downloaded, resolved, and transformed on request.

Given a TS/JS entry, webrun-modules resolves, downloads, and transforms the dependency graph — from npm (or a CDN) at resolve time only — and serves the transformed, browser-runnable ESM from a local FilesApi cache. Every internal import is rewritten to a same-origin local URL, so nothing is fetched from a third party at run time. The result is always a URL any module-compatible runtime can import directly — no bespoke client loader.

It is isomorphic: the same code runs in a browser ServiceWorker and in Node — the only difference is which FilesApi backend you inject.

Install

npm add @statewalker/webrun-modules

Quick start

import { newModuleServer, npmRegistrySource } from "@statewalker/webrun-modules";
import { NodeFilesApi } from "@statewalker/webrun-files-node";

const server = newModuleServer({
  cache: new NodeFilesApi({ rootDir: "./.modules-cache" }),
});

// Resolve an npm package (with optional subpath) to an importable URL:
const zod = await server.resolve({ pkg: "zod" });
// → { url: "/[email protected]/lib/index.mjs", target: "browser" }

// Serve it — `server.fetch` is a standard Web handler, mountable anywhere:
Deno.serve(server.fetch);                                       // Deno
export default { fetch: server.fetch };                         // Bun / Cloudflare
self.addEventListener("fetch", (e) => e.respondWith(server.fetch(e.request))); // ServiceWorker

There is no separate "install" call. The first time a module URL is requested — via resolve, prime, or a direct fetch — its package is downloaded and transformed on demand, then cached. Requesting a URL for a package that isn't in the cache yet just works:

// Nothing primed, nothing resolved — this single fetch downloads + transforms + serves:
const res = await server.fetch(new Request("http://host/[email protected]/index.js"));
// → 200, content-type: text/javascript

Eager priming (optional)

To download and transform a whole dependency graph up front (e.g. before going offline, or to warm a cache), use prime. It walks the entry's transitive graph, transforms every module, and writes the lockfile:

await server.prime({ pkg: "react-dom", version: "^18" });
// entry + every transitive dep are now cached and importable with the network off.

Listing what an entry needs, or what a package contains

Two questions, two methods:

// (1) Every module URL required to RUN an entry — the reachable graph (primes it).
const urls = await server.listResources({ pkg: "react" });
// → [ "/[email protected]/index.js",
//     "/[email protected]/cjs/react.production.js",
//     "/[email protected]/cjs/react.development.js" ]   ← the exact set of scripts to serve

// (2) Every FILE in a package — the full tarball contents (loads it if needed).
const files = await server.listPackageFiles({ pkg: "react" });
// → [ "LICENSE", "index.js", "jsx-runtime.js", "cjs/react.development.js", … ]  (27 files)

listResources is the minimal set (what the entry actually imports); listPackageFiles is everything the package ships (including alternative entry points like react/jsx-runtime the main entry never imports). To capture an app's full needs, call listResources for each entry point you import and take the union.

To download everything for later offline/static serving, point the cache at a real directory and prime — the transformed files land under {rootDir}/t/{target}/:

const server = newModuleServer({ cache: new NodeFilesApi({ rootDir: "./react-bundle" }) });
await server.prime({ pkg: "react" });
await server.prime({ pkg: "react", subpath: "jsx-runtime" }); // if you use JSX
// ./react-bundle/t/browser/[email protected]/… now holds the importable scripts.

Serving your own source too

Point the server at a project FilesApi and it resolves local scripts the same way — bare imports rewritten to /{name}@{version}/…, relative imports kept relative, TS/JSX transpiled:

const server = newModuleServer({ cache, project: myProjectFiles });
const app = await server.resolve({ url: "/src/app.ts" }); // → importable URL

Examples

Three runnable examples (each has a package script; all hit the live npm registry, so they need network on first run):

pnpm --filter @statewalker/webrun-modules example              # full-cycle (alias)
pnpm --filter @statewalker/webrun-modules example:full-cycle   # examples/full-cycle.ts
pnpm --filter @statewalker/webrun-modules example:server       # examples/http-server.ts (unpkg-like)
pnpm --filter @statewalker/webrun-modules example:site         # examples/site-pipeline.ts

(From inside the package directory you can drop the --filter … prefix: pnpm example:server.)

examples/full-cycle.ts demonstrates the entire cycle against the live npm registry — lazy download-on-request, resolve, prime, executing a served module, ?raw, and the lockfile.

An unpkg-like HTTP service

Because server.fetch is a standard Web handler, exposing an unpkg-style endpoint is a thin wrapper — mount it on any host and add the one convenience of redirecting a bare/ranged spec to its pinned, versioned URL. examples/http-server.ts is a complete, dependency- free Node server that does exactly this:

pnpm --filter @statewalker/webrun-modules example:server
# then:
curl -L localhost:8787/lodash-es@4/merge   # 302 → /[email protected]/merge.js → importable ESM
curl -L localhost:8787/debug               # 302 → /[email protected]/src/browser.js
curl 'localhost:8787/react?meta'           # JSON: react's full file list
curl 'localhost:8787/react?graph'          # JSON: every module URL needed to run react
curl 'localhost:8787/[email protected]/package.json'   # non-JS files served raw (application/json)

For a given package it returns an importable JS module with every dependency already resolved to a same-origin URL — e.g. requesting lodash-es@4/merge serves merge.js whose import "./_baseMerge.js" / import "./_createAssigner.js" all point back at the same server. Use it straight from a browser:

<script type="module">
  import merge from "http://localhost:8787/lodash-es@4/merge";
  console.log(merge({ a: 1 }, { b: 2 }));
</script>

In-browser site pipeline (replacing a jspm-based resolver)

Because the server transpiles first-party TS/TSX and resolves the npm deps, one newModuleServer replaces an entire @jspm/generator-based pipeline (resolver + CDN providers + es-module-lexer rewrite + recursive prefetch + /external mount). Put your source in a project FilesApi, mount server.fetch under a site, and run server modules through the existing server-runner:

const server = newModuleServer({ cache, project: myAppFiles, target: "browser" });

new SiteBuilder()
  .setEndpoint("/", server.fetch)                                  // html + transpiled TSX + deps
  .setEndpoint("/api", newServerRunner(serverEntryUrl, () => baseUrl)) // run server modules
  .build();

examples/site-pipeline.ts runs the whole thing (JSX/TSX transpiled, import "react" rewritten to a same-origin URL, react + react/jsx-runtime resolved, listResources = the exact scripts to serve). Note: the server resolves a bare import "react" to latest unless a version is pinned — seed lock (e.g. { react: "18.3.1" }) to honor a project's package.json versions reproducibly.

Options

| Option | Default | Purpose | |-------------|--------------------------|---------| | cache | — (required) | Injected FilesApi for the module cache. | | project | — | FilesApi of local project files to serve. | | sources | [npmRegistrySource()] | Acquisition sources (npm tarball by default). | | transform | newDefaultTransform() | Per-file transform (ESM + CJS-interop). | | target | "browser" | Selects exports conditions + cache key; "node" supported. | | lock | — | A Lockfile (pins versions); prime also writes one back. | | basePath | "/" | Mount prefix, e.g. "/deps/v1/". |

ModuleServer

interface ModuleServer {
  resolve(ref: ModuleRef, importer?: string): Promise<ResolvedModule>; // single ref → URL
  prime(entry: ModuleRef): Promise<ResolvedModule>;                    // warm the whole graph
  listResources(entry: ModuleRef): Promise<string[]>;                  // every URL the entry needs
  listPackageFiles(ref: ModuleRef): Promise<string[]>;                 // a package's full file list
  fetch(request: Request): Promise<Response>;                          // standard Web handler
  readonly lock: Lockfile;                                             // resolution map
}

type ModuleRef =
  | { pkg: string; version?: string; subpath?: string } // e.g. { pkg: "lodash-es", subpath: "merge" }
  | { url: string };                                     // a local project script

Targets: browser vs node

target selects which package.json exports conditions win and how Node builtins are handled, and is part of the cache key (a browser build and a node build of the same package never collide):

// Browser (default): node:* builtins → self-hosted @jspm/core polyfill URLs.
newModuleServer({ cache, target: "browser" });

// Node: node:* builtins stay external (real Node builtins).
newModuleServer({ cache, target: "node" });

Reproducible resolution (the lockfile)

The resolution map is a Lockfile ({ [name]: version }). prime writes it to the cache and returns it via server.lock. Supply it back as lock to pin versions reproducibly — a partial lockfile pins only the names it lists:

const first = newModuleServer({ cache });
await first.prime({ pkg: "app" });
const lock = first.lock; // e.g. { app: "1.0.0", react: "18.3.1", … }

// Elsewhere / later: identical resolution, no re-solve.
const pinned = newModuleServer({ cache, lock });

Custom Source (npm / JSR / URL / your own registry)

A Source turns a reference into a package's files + manifest. The default is npmRegistrySource(); provide your own (or several — the first whose matches returns true wins):

import type { Source } from "@statewalker/webrun-modules";
import { MemFilesApi } from "@statewalker/webrun-files-mem";

const myRegistry: Source = {
  matches: (ref) => "pkg" in ref,
  async load(ref) {
    // fetch + unpack however you like; return the package tree + manifest
    const files = new MemFilesApi();
    // … write files …
    return { name: ref.pkg, version: "1.0.0", files, manifest: { name: ref.pkg, version: "1.0.0" } };
  },
};

newModuleServer({ cache, sources: [myRegistry, npmRegistrySource()] });

npmRegistrySource(options?) accepts { registryUrl, fetch, createFiles } — pass a custom fetch (e.g. to add auth or point at a private registry) or a private registry URL.

Custom Transform

The default transform (newDefaultTransform()) dispatches per file: ESM/TS/JSX go through newEsmTransform(), CommonJS through newCjsTransform(). Swap in your own Transform — it receives one file and a rewrite(specifier) => url callback and returns browser-runnable ESM:

import { newDefaultTransform, detectFormat } from "@statewalker/webrun-modules";
import type { Transform } from "@statewalker/webrun-modules";

const myTransform: Transform = {
  async transform(file, rewrite) {
    // file = { path, source, format: "esm" | "cjs" | "ts" | "tsx" }
    // call rewrite(spec) for each import specifier to get its local URL
    return /* transformed ESM */ file.source;
  },
};

newModuleServer({ cache, transform: myTransform });

detectFormat(path, source, manifest?) returns the SourceFormat the default transform would infer.

Serving surface

server.fetch(request) is a plain (Request) => Promise<Response>:

  • JS/TS module files are transformed and served as text/javascript;
  • non-module files (package.json, README.md, .css, …) are served raw, untransformed, with a content-type guessed from the extension (application/json, text/markdown, …);
  • append ?raw to get the raw bytes of any file as application/octet-stream;
  • an unresolvable path returns a 404 Response (never throws).

Mount it under any basePath (returned URLs carry the prefix; the cached bytes stay portable, because internal imports are rewritten as relative URLs):

const server = newModuleServer({ cache, basePath: "/deps/v1/" });
const r = await server.resolve({ pkg: "zod" }); // → { url: "/deps/v1/[email protected]/lib/index.mjs" }

Errors

  • ModuleResolveError { ref, reason } — a package / version / subpath can't be resolved (surfaced as a 404 from fetch).
  • ModuleTransformError { path, reason } — a file can't be transformed to runnable ESM.

Utilities

Also exported: untarTgz(bytes) (isomorphic npm-tarball unpacker), parseSpecifier(spec) (bare specifier → { pkg, subpath? }, scope-aware), and relativeUrl(fromId, toId).

How it works

  • Acquire — the default Source fetches the npm registry tarball, untars it in memory (pure-JS, isomorphic), and caches every file.
  • Resolve — versions resolve against the registry with whole-name dedupe (one version per package where semver allows; incompatible ranges are kept side by side). package.json exports/imports conditions are honored for the target; Node builtins map to @jspm/core polyfills (browser) or stay external (node). The resolution map is persisted as a lockfile.
  • Transform — each file becomes browser-runnable ESM one-to-one. ESM/TS/JSX is transpiled and its specifiers rewritten in place; CommonJS is wrapped so the ESM module graph itself provides require (synchronously, backed by the eagerly primed graph). Internal imports are rewritten as relative URLs, so cached bytes are portable across mount prefixes.

Limitations

  • Computed require(expr) across package boundaries can't be pre-resolved and throws at execution time — the boundary where an esbuild-wasm bundle fallback would take over.
  • Dedupe is greedy (first-resolved version wins per name), not a full constraint hoist.
  • Free Node globals under target: "browser". require("process") is polyfilled, but packages that reference process, Buffer, or global as bare free variables (e.g. React's process.env.NODE_ENV) need the page to define them — set globalThis.process = { env: { NODE_ENV: "development" } } (as esbuild/Vite do via a define). Under target: "node" they resolve natively.
  • Bundling/copying the resolved graph into a distributable tree, .d.ts type serving, package lifecycle scripts, and HMR are out of scope.

License

MIT