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

runtime-artifacts

v0.1.0

Published

Compress code into artifact blobs, download them at runtime, and materialize runnable Node.js apps on the fly.

Readme

runtime-artifacts

Compress code into artifact blobs, ship them to blob storage, then import them transparently at runtime via rta:// URLs — with capability-scoped sandboxing, atomic hot-swap, differential manifests, and on-the-fly TypeScript transpilation.

Why this library exists

This is a fair question — the previous version of this library was essentially "npm pack + S3 SDK + zlib + require()", which doesn't justify existing on its own. The current version is built around six capabilities that no single existing Node library provides together:

| Capability | runtime-artifacts | ncc / esbuild | import-http | isolated-vm | Docker / Lambda Layers | @node-loader/http | | --------------------------------- | ----------------- | ------------- | ----------- | ----------- | ---------------------- | ----------------- | | Transparent rta:// ESM imports | ✅ | ❌ (build-time) | ✅ (URL only) | ❌ | ❌ | ⚠️ (low-level) | | Compression (brotli/gzip/deflate) | ✅ | ❌ | ❌ | ❌ | ✅ (image layers) | ❌ | | SHA-256 integrity verification | ✅ | ❌ | ❌ | ❌ | ✅ (digests) | ❌ | | On-disk content-addressed cache | ✅ | N/A | ❌ | N/A | ✅ (layer cache) | ❌ | | Capability-scoped sandboxing | ✅ | ❌ | ❌ | ✅ (alone) | ✅ (cgroups) | ❌ | | Atomic hot-swap (in-flight safe) | ✅ | ❌ | ❌ | ❌ | ✅ (k8s rolling) | ❌ | | Differential manifests | ✅ | ❌ | ❌ | ❌ | ✅ (layer diff) | ❌ | | On-the-fly TS transpilation | ✅ | ❌ (build-time) | ❌ | ❌ | ❌ | ❌ | | Per-function unit splitting | ✅ | ❌ | ❌ | ❌ | ❌ (layer-grained) | ❌ |

If you only need one of these capabilities, use a specialized tool:

  • Bundle an app into one file at build time → @vercel/ncc, esbuild, rollup
  • Run untrusted JS safely in a separate V8 isolate → isolated-vm
  • Import ESM modules from HTTP URLs → @node-loader/http (then add your own integrity + caching)
  • Package a whole app with OS deps → Docker, OCI images
  • Ship runtime code layers on AWS → Lambda Layers

If you need two or more of these together — for example, shipping plugins to a long-running server that need to be sandboxed, hot-swappable, integrity-verified, and bandwidth-efficient — runtime-artifacts is the only Node library that does the whole job.

The six unique capabilities

1. Transparent rta:// ESM imports

// app.mjs
import "runtime-artifacts/loader-register";  // or: --import runtime-artifacts/loader-register

const handler = (await import("rta://[email protected]/index.js")).default;
console.log(handler("world"));

The loader hook intercepts rta:// URLs, fetches the manifest from the configured blob provider, downloads the requested entry's blob, decompresses it, verifies its SHA-256, and returns the source to Node's module system — all in-process, all cached. No materialize-to-disk-then-require round trip.

2. Capability-scoped sandboxing via isolated-vm

import { SandboxRunner } from "runtime-artifacts";

const runner = new SandboxRunner();
const { result } = await runner.run(materializedApp, manifest, {
  capabilities: {
    allowedModules: ["crypto"],       // only crypto; no fs, no child_process
    fs: { allowRead: ["/data/in/"] }, // reads allowed only from /data/in/
    http: { allowHosts: ["api.example.com", "*.cdn.com"] },
    timeoutMs: 5000,
    memoryMB: 128,
  },
  args: ["input"],
});

isolated-vm runs code in a separate V8 isolate with its own heap. Unlike Node's vm module (which the Node docs explicitly say is not a security boundary), isolated-vm doesn't share process, require, or any host global unless you explicitly bridge them. The SandboxRunner layer adds capability-based access control on top — a guest can't require("child_process") unless you whitelist it, and require("fs") returns a wrapper that rejects reads outside the allowed paths.

3. Atomic hot-swap with reference counting

import { AppRegistry } from "runtime-artifacts";

const registry = new AppRegistry(provider);
await registry.install(v1Manifest);

const handle = registry.acquire("my-app");          // refcount on v1 = 1
await registry.swap(v2Manifest);                    // new acquires → v2

// In-flight handle still uses v1:
await handle.invoke("x");                           // runs v1

// New acquire uses v2:
const h2 = registry.acquire("my-app");
await h2.invoke("x");                               // runs v2

handle.release();                                   // v1 refcount → 0 → GC after grace

The registry keeps multiple versions of the same app alive simultaneously. swap() is atomic for new acquires; existing handles keep their pinned version. Old versions are GC'd after a configurable grace period once their refcount hits zero. This is the primitive you need for hot-reloadable plugin systems and zero-downtime deploys within a single long-running process.

4. Differential manifests

import { computeDelta, resolveDelta } from "runtime-artifacts";

// Ship only the changed blobs between v1 and v2:
const delta = computeDelta(v1Manifest, v2Manifest);
//   { base: "[email protected]", delta: { add: [...], remove: [...] } }

// Client resolves the delta against the cached base:
const resolved = await resolveDelta(deltaManifest, provider);
//   resolved.entries = v1.entries + delta.add − delta.remove

A delta manifest declares a base and a list of add/remove operations. Clients that already have the base cached only download the changed blobs. For a 10 MB app with a 200 KB hotfix, that's a 50× bandwidth reduction on the wire vs shipping a full new manifest.

5. On-the-fly TypeScript / JSX transpilation

// Ship raw TypeScript — no build step:
const { manifest } = await packAndPush({
  provider, name: "ts-plugin", version: "1.0.0",
  entry: "src/index.ts",
  srcDir: "./src",
});

When the loader encounters a .ts / .tsx / .mts blob, it transpiles to JavaScript via esbuild (lazy-loaded) before handing the source to Node. .jsx is transpiled to CJS. Transpiled output is cached by source SHA-256, so the cost is paid once per artifact version. If esbuild isn't installed, a regex-based type-strip fallback handles the common cases (with a loud warning).

6. Fine-grained module splitting (per-function updates)

// Pack with split: true — each function/class/const becomes its own blob.
const { manifest: v1 } = await packAndPush({
  provider, name: "api", version: "1.0.0",
  entry: "index.js", srcDir: "./src",
  split: true,  // ← cut JS/TS into per-export units
});

// ...later, change one function and pack v2:
const { manifest: v2 } = await packAndPush({
  provider, name: "api", version: "1.0.1",
  entry: "index.js", srcDir: "./src",
  split: true,
});

// The unit-level diff shows exactly what changed:
import { computeUnitDiff, summarizeUnitDiff, fetchDeltaBytes } from "runtime-artifacts";

const diffs = computeUnitDiff(v1, v2);
const summary = summarizeUnitDiff(v1, v2, diffs);
// summary.changedUnits === 1     (only the function you edited)
// summary.unchangedUnits === 49  (every other function, cached)
// summary.savingsRatio === 0.97  (97% bandwidth saved on this update)

// Only the changed unit's blob is fetched from the provider:
const delta = await fetchDeltaBytes(v1, v2, provider, cache);
// delta.unitsFetched === 1
// delta.unitsCached === 49

When split: true is passed to packAndPush, every JS/TS source file is parsed with acorn and split into independently-addressable units:

  • __head__ — imports + side effects (always required)
  • <ExportName> — one unit per named export (export function, export class, export const)
  • __default__ — the default export

Each unit becomes its own compressed blob in storage, with its own SHA-256 hash. When a single function changes, only that function's blob is re-fetched by clients — typically a few hundred bytes versus the file's full compressed size.

For a file with 50 exports where you tweak one function, that's a ~50× bandwidth reduction on the wire compared to file-level delta (which would re-download the entire file). For large generated files (OpenAPI clients, Prisma outputs, protobuf stubs) this is the difference between a 2-second cold-start and a 50-millisecond incremental update.

CLI:

# Pack with --split
rta pack --name api --version 1.0.0 --src ./src --provider file:///tmp/blobs --split

# See what changed between two versions (unit-level)
rta diff --old manifest-v1.json --new manifest-v2.json
# Output:
#   Entries:        2 total, 0 added, 0 removed, 1 unchanged
#   Units:          6 total, 1 changed (need fetch), 5 unchanged (cache hit)
#   Bandwidth:      86 bytes to fetch / 491 total (82.5% saved)
#   ~ index.js  [~greet | 4 cached]
#   = package.json  (whole-file, unchanged)

Reassembly: the downloader fetches each unit (cache-first by SHA-256), verifies each unit's integrity, decompresses, and concatenates them in unit order to reconstruct the original source. A final SHA-256 check on the assembled source catches any reassembly bug or unit tampering.

Non-JS files (JSON, assets, etc.) are never split — they go through the normal whole-file code path.

Quick start

Library

import {
  packAndPush, AppRegistry, MemoryBlobProvider,
} from "runtime-artifacts";

const provider = new MemoryBlobProvider();
const { manifest } = await packAndPush({
  provider, name: "hello", version: "1.0.0",
  entry: "index.js", srcDir: "./app",
});

const registry = new AppRegistry(provider);
await registry.install(manifest);
const handle = registry.acquire<{ default: (s: string) => string }>("hello");
const result = await handle.invoke("world");
handle.release();

Transparent loader

# Terminal 1: pack and publish
rta pack --name hello --version 1.0.0 --entry index.js \
         --src ./app --provider file:///tmp/blobs \
         --out ./manifest.json

# Terminal 2: import as if it were local
RTA_PROVIDER=file:///tmp/blobs RTA_CACHE=./.cache \
  node --import runtime-artifacts/loader-register ./app.mjs
// app.mjs
const mod = await import("rta://[email protected]/index.js");
console.log(mod.default("world"));

CLI

rta pack    --name X --version Y --src ./dir --provider file:///tmp/blobs --out manifest.json
rta pull    --manifest manifest.json --provider file:///tmp/blobs --out ./materialized --cache ./.cache
rta run     --manifest manifest.json --provider file:///tmp/blobs --cache ./.cache -- args...
rta info    --manifest manifest.json
rta cache   --root ./.cache stats|clear

Architecture

       ┌─────────────────┐                       ┌──────────────────┐
       │   Source tree   │  packAndPush()        │  Blob provider   │
       │  ./app/*.ts     │ ───────────────────▶  │  S3 / Azure /    │
       │                 │  compress + upload    │  HTTP / local /  │
       └─────────────────┘                       │  memory          │
                       ▲                          └──────────────────┘
                       │                                  ▲
       ┌─────────────────┐  ESM loader hook                │
       │   import        │ ─────────────────────────────────┘
       │   "rta://..."   │  fetch + decompress + verify
       └─────────────────┘  + transpile (TS/JSX)
                 │
                 ▼
       ┌─────────────────────────────────────────────────┐
       │  In-process registry                            │
       │  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
       │  │  v1.0.0 │  │  v1.0.1 │  │  v1.0.2 │          │
       │  │ refcnt 1│  │ refcnt 0│  │ refcnt 3│  ←swap() │
       │  └─────────┘  └─────────┘  └─────────┘          │
       │  ▲                       ▲                       │
       │  └── acquire() ──────────┘                       │
       └─────────────────────────────────────────────────┘
                 │
                 ▼
       ┌─────────────────┐
       │  SandboxRunner  │  (isolated-vm when available)
       │  cap-scoped     │  capability: { fs, http, modules, ... }
       │  require        │
       └─────────────────┘

Module layout

| File | Responsibility | | --------------------------- | ------------------------------------------------------------ | | src/loader.ts | Node ESM loader hook — rta:// URL resolution + source loading | | src/loader-register.ts | Side-effect import that registers the loader via module.register() | | src/sandbox.ts | isolated-vm runner with capability-scoped require, fs, http | | src/registry.ts | In-process registry with atomic hot-swap + refcount GC | | src/diff.ts | Differential manifests: compute + resolve deltas against base, unit-level diff + summary | | src/splitter.ts | AST-based module splitter — cuts JS/TS into per-export units | | src/transpile.ts | On-the-fly TS/JSX → JS via esbuild (with regex fallback) | | src/compress.ts | gzip / deflate / brotli codecs, buffers + streams | | src/manifest.ts | Manifest schema, validation, self-signing, SplitUnit type | | src/cache.ts | On-disk SHA-256-addressed cache with shard subdirs | | src/blob/index.ts | BlobProvider interface + createBlobProvider() factory | | src/blob/{memory,local,http,s3,azure}.ts | Provider implementations | | src/downloader.ts | Concurrent fetcher + cache + integrity verification + per-unit materialization | | src/factory.ts | AppFactory — materialize + invoke (basic, for non-sandboxed use) | | src/pack.ts | packAndPush() — compress + upload + emit manifest + optional split | | bin/cli.ts | rta CLI (pack/push/pull/run/info/diff/cache) |

Manifest schema

{
  "schema": 1,
  "id": "[email protected]",
  "name": "hello",
  "version": "1.0.0",
  "createdAt": "2025-01-01T00:00:00.000Z",
  "entries": [
    {
      "path": "index.js",
      "blobName": "[email protected]/index.js",
      "codec": "brotli",
      "sha256": "96bdd857d2a0ec3f095075a6f3373df894fd571548f10e321fc4dee21781f9b6",
      "size": 110,
      "compressedSize": 94
    }
  ],
  "entry": "index.js",
  "entryExport": "default",
  "env": { "NODE_ENV": "production" },
  "manifestSha256": "e2ff194f2b7180198dc1124251ce938c0a57e213cbfcc65a9a21ba2ba8799983"
}
  • entries[].sha256 is the SHA-256 of the compressed blob bytes — verified on every download.
  • manifestSha256 is the SHA-256 of the manifest's canonical JSON (sorted keys, no whitespace, excluding the field itself). The downloader rejects tampered manifests before fetching any blobs.

Delta manifest extension

{
  "schema": 1,
  "id": "[email protected]",
  "name": "app",
  "version": "1.0.1",
  "base": "[email protected]",
  "delta": {
    "add":    [ { "path": "index.js", ... } ],
    "remove": [ "old/deleted.js" ]
  },
  "entry": "index.js",
  ...
}

Split-entry extension (units)

When an entry is packed with split: true, it carries a units array describing the per-export blobs. The entry's own sha256 becomes the SHA-256 of the assembled (decompressed + concatenated) source, used as a final integrity check after reassembly:

{
  "path": "index.js",
  "blobName": "[email protected]/index.js",
  "codec": "brotli",
  "sha256": "<sha256 of the reassembled source>",
  "size": 1100,
  "compressedSize": 480,
  "units": [
    { "name": "__head__",   "codec": "brotli", "sha256": "...", "size": 200, "compressedSize": 110,
      "blobName": "[email protected]/index.js::__head__" },
    { "name": "greet",      "codec": "brotli", "sha256": "...", "size": 80,  "compressedSize": 72,
      "blobName": "[email protected]/index.js::greet" },
    { "name": "multiply",   "codec": "brotli", "sha256": "...", "size": 60,  "compressedSize": 58,
      "blobName": "[email protected]/index.js::multiply" },
    { "name": "__default__","codec": "brotli", "sha256": "...", "size": 120, "compressedSize": 90,
      "blobName": "[email protected]/index.js::__default__" }
  ]
}

When units is present, the downloader does NOT fetch [email protected]/index.js as a single blob. Instead, it fetches each unit individually (cache-first by SHA-256), decompresses, and concatenates them in array order to reconstruct the source.

Blob providers

| Spec | Backend | Notes | | ----------------------------- | ----------------------------------- | --------------------------------------- | | memory:// | In-process Map | Tests / ephemeral; not shared | | file:///abs/path | Local filesystem | Path-traversal protected | | https://host/prefix | Generic HTTP(S) blob server | GET/HEAD; PUT/DELETE if allowWrite | | s3://bucket/prefix | AWS S3 | Requires @aws-sdk/client-s3 | | azure://acct/cont/prefix | Azure Blob Storage | Requires @azure/storage-blob |

import { createBlobProvider } from "runtime-artifacts";
const p = await createBlobProvider("s3://my-bucket/apps/", {
  region: "us-east-1",
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});

When to use this (and when not to)

Use it when:

  • You run a long-lived Node process and want to ship code updates to it without restart (plugins, hot-fixes, A/B tests).
  • You need to run semi-trusted third-party code and want actual sandboxing (not vm).
  • You're bandwidth-constrained (edge / IoT) and want differential updates between versions.
  • You want to ship raw TypeScript source without a build step.
  • You need any two of: integrity verification, content-addressed caching, hot-swap, sandboxing, compression.

Don't use it when:

  • You just need to bundle a Node app at build time → use @vercel/ncc or esbuild.
  • You just need to run untrusted JS safely → use isolated-vm directly.
  • You're on AWS Lambda and just need code layers → use Lambda Layers.
  • You need to package OS-level dependencies → use Docker.
  • You're building a single deployable artifact for a CI pipeline → use a tarball + npm install.

Install

npm install runtime-artifacts
# Pick the optional peer deps you need:
npm install esbuild              # for TS/JSX transpilation
npm install isolated-vm          # for real sandboxing (native module)
npm install @aws-sdk/client-s3   # for S3BlobProvider
npm install @azure/storage-blob  # for AzureBlobProvider

Requires Node.js ≥ 18.17. Loader hooks require Node ≥ 20.6 (for module.register()).

License

MIT