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

@cloudflare/worker-bundler

v0.0.4

Published

Build and bundle Workers at runtime for Cloudflare Worker Loader binding

Readme

worker-bundler

Experimental: This package is under active development and its API may change without notice. Not recommended for production use.

Bundle and serve full-stack applications on Cloudflare's Worker Loader binding (closed beta). Dynamically generate Workers with real npm dependencies, or build complete apps with client-side bundles and static asset serving.

Installation

npm install @cloudflare/worker-bundler

Quick Start

Bundle a Worker

Provide source code and dependencies — the bundler handles the rest:

import { createWorker } from "@cloudflare/worker-bundler";

const worker = env.LOADER.get("my-worker", async () => {
  const { mainModule, modules } = await createWorker({
    files: {
      "src/index.ts": `
        import { Hono } from 'hono';
        import { cors } from 'hono/cors';

        const app = new Hono();
        app.use('*', cors());
        app.get('/', (c) => c.text('Hello from Hono!'));
        app.get('/json', (c) => c.json({ message: 'It works!' }));

        export default app;
      `,
      "package.json": JSON.stringify({
        dependencies: { hono: "^4.0.0" }
      })
    }
  });

  return { mainModule, modules, compatibilityDate: "2026-01-01" };
});

await worker.getEntrypoint().fetch(request);

Build a Full-Stack App

Use createApp to bundle a server Worker, client-side JavaScript, and static assets together. Assets are returned separately for host-side serving — they are not embedded in the isolate:

import {
  createApp,
  handleAssetRequest,
  createMemoryStorage
} from "@cloudflare/worker-bundler";

const result = await createApp({
  files: {
    "src/server.ts": `
      export default {
        fetch(request) {
          return new Response("API response");
        }
      }
    `,
    "src/client.ts": `
      document.getElementById("app").textContent = "Hello from the client!";
    `
  },
  server: "src/server.ts",
  client: "src/client.ts",
  assets: {
    "/index.html":
      "<!DOCTYPE html><div id='app'></div><script src='/client.js'></script>",
    "/favicon.ico": favicon
  },
  assetConfig: {
    not_found_handling: "single-page-application"
  }
});

const worker = env.LOADER.get("my-app", () => ({
  mainModule: result.mainModule,
  modules: result.modules,
  compatibilityDate: "2026-01-01"
}));

// Serve assets on the host, forward the rest to the isolate
const storage = createMemoryStorage(result.assets);
const assetResponse = await handleAssetRequest(
  request,
  result.assetManifest,
  storage,
  result.assetConfig
);
if (assetResponse) return assetResponse;
return worker.getEntrypoint().fetch(request);

Static assets are served with proper content types, ETags, and caching. Requests that don't match an asset fall through to your server code.

API

createWorker(options)

Bundles source files into a Worker.

| Option | Type | Default | Description | | ------------ | ------------------------ | ------------------------------ | ----------------------------------------------------------------- | | files | Record<string, string> | required | Input files (path → content) | | entryPoint | string | auto-detected | Entry point file path | | bundle | boolean | true | Bundle all dependencies into one file | | externals | string[] | [] | Modules to exclude from bundling (cloudflare:* always external) | | target | string | 'es2022' | Target environment | | minify | boolean | false | Minify output | | sourcemap | boolean | false | Generate inline source maps | | registry | string | 'https://registry.npmjs.org' | npm registry URL |

Returns:

{
  mainModule: string;
  modules: Record<string, string | Module>;
  wranglerConfig?: WranglerConfig;
  warnings?: string[];
}

createApp(options)

Builds a full-stack app: server Worker + client bundle + static assets.

| Option | Type | Default | Description | | ------------- | --------------------------------------- | ------------- | ----------------------------------------------- | | files | Record<string, string> | required | All source files (server + client) | | server | string | auto-detected | Server entry point (Worker fetch handler) | | client | string \| string[] | — | Client entry point(s) to bundle for the browser | | assets | Record<string, string \| ArrayBuffer> | — | Static assets (pathname → content) | | assetConfig | AssetConfig | — | Asset serving configuration | | bundle | boolean | true | Bundle server dependencies | | externals | string[] | [] | Modules to exclude from bundling | | target | string | 'es2022' | Server target environment | | minify | boolean | false | Minify output | | sourcemap | boolean | false | Generate source maps | | registry | string | npm default | npm registry URL |

Returns everything from createWorker plus:

{
  assets: Record<string, string | ArrayBuffer>;  // Combined assets for host-side serving
  assetManifest: AssetManifest;        // Metadata (content types, ETags) per asset
  assetConfig?: AssetConfig;           // The asset config used
  clientBundles?: string[];            // Output paths of client bundles
}

handleAssetRequest(request, manifest, storage, config?)

Standalone async asset request handler. The manifest holds routing metadata (content types, ETags) while the storage backend provides content on demand:

import { handleAssetRequest, buildAssets } from "@cloudflare/worker-bundler";

const { manifest, storage } = await buildAssets({
  "/index.html": "<h1>Hello</h1>",
  "/app.js": "console.log('hi')"
});

export default {
  async fetch(request) {
    const assetResponse = await handleAssetRequest(request, manifest, storage, {
      not_found_handling: "single-page-application"
    });
    if (assetResponse) return assetResponse;

    // Fall through to your API logic
    return new Response("API");
  }
};

Returns a Promise<Response | null> — a Response if an asset matches, or null to fall through.

Storage Hooks

The asset handler separates manifest (routing metadata) from storage (content retrieval). This lets you plug in any backend.

AssetStorage Interface

interface AssetStorage {
  get(pathname: string): Promise<ReadableStream | ArrayBuffer | string | null>;
}

Built-in: In-Memory Storage

The simplest option — stores everything in memory:

import {
  buildAssetManifest,
  createMemoryStorage
} from "@cloudflare/worker-bundler";

const assets = { "/index.html": "<h1>Hi</h1>" };
const manifest = await buildAssetManifest(assets);
const storage = createMemoryStorage(assets);

Or use the convenience function that returns both:

import { buildAssets } from "@cloudflare/worker-bundler";

const { manifest, storage } = await buildAssets({
  "/index.html": "<h1>Hi</h1>"
});

Custom: KV Storage

import type { AssetStorage } from "@cloudflare/worker-bundler";

function createKVStorage(kv: KVNamespace): AssetStorage {
  return {
    async get(pathname) {
      return kv.get(pathname, "arrayBuffer");
    }
  };
}

Custom: R2 Storage

function createR2Storage(bucket: R2Bucket, prefix = "assets"): AssetStorage {
  return {
    async get(pathname) {
      const obj = await bucket.get(`${prefix}${pathname}`);
      return obj?.body ?? null;
    }
  };
}

Custom: Workspace Storage

import type { Workspace } from "@cloudflare/shell";

function createWorkspaceStorage(workspace: Workspace): AssetStorage {
  return {
    async get(pathname) {
      return workspace.readFile(pathname);
    }
  };
}

All storage backends work with the same handleAssetRequest — just pass a different storage argument.

Asset Configuration

HTML Handling

Controls how HTML files are resolved and how trailing slashes are handled:

| Mode | Behavior | | ---------------------- | --------------------------------------------------------------- | | auto-trailing-slash | /about serves about.html; /blog/ serves blog/index.html | | force-trailing-slash | Redirects /about/about/, serves from index.html | | drop-trailing-slash | Redirects /about//about, serves from .html | | none | Exact path matching only |

Default: auto-trailing-slash

Not-Found Handling

| Mode | Behavior | | ------------------------- | -------------------------------------------------- | | single-page-application | Serves /index.html for all unmatched routes | | 404-page | Serves nearest 404.html walking up the directory | | none | Returns null (fall through to your Worker) |

Default: none

Redirects

{
  redirects: {
    static: {
      "/old-page": { status: 301, to: "/new-page" }
    },
    dynamic: {
      "/blog/:slug": { status: 302, to: "/posts/:slug" },
      "/docs/*": { status: 301, to: "/wiki/*" }
    }
  }
}

Dynamic redirects support :placeholder tokens and * splat patterns.

Custom Headers

{
  headers: {
    "/*": {
      set: { "X-Frame-Options": "DENY" }
    },
    "/api/*": {
      set: { "Access-Control-Allow-Origin": "*" },
      unset: ["X-Frame-Options"]
    }
  }
}

Patterns use glob syntax. Rules are applied in order — later rules can override earlier ones.

Caching

The asset handler sets cache headers automatically:

  • HTML and non-hashed files: public, max-age=0, must-revalidate
  • Hashed files (e.g. app.a1b2c3d4.js): public, max-age=31536000, immutable
  • ETag support: Conditional requests with If-None-Match return 304 Not Modified

Entry Point Detection

Priority order for createWorker (and createApp server entry):

  1. Explicit entryPoint / server option
  2. main field in wrangler config
  3. exports, module, or main field in package.json
  4. Default paths: src/index.ts, src/index.js, index.ts, index.js

Mounting as a Durable Object

createApp bundles code and collects assets — how the output is mounted is the caller's concern. If the user's code exports a DurableObject subclass, load it with getDurableObjectClass and mount it as a facet for persistent storage:

const result = await createApp({
  files: {
    "src/server.ts": `
      import { DurableObject } from 'cloudflare:workers';

      export default class App extends DurableObject {
        async fetch(request: Request) {
          const url = new URL(request.url);
          if (url.pathname === '/api/count') {
            const count = ((await this.ctx.storage.get('count')) as number) ?? 0;
            await this.ctx.storage.put('count', count + 1);
            return Response.json({ count });
          }
          return new Response('Not found', { status: 404 });
        }
      }
    `
  },
  assets: {
    "/index.html": "<!DOCTYPE html><h1>Counter</h1>"
  }
});

// Load the bundle and get the DO class
const worker = env.LOADER.get(loaderId, () => ({
  mainModule: result.mainModule,
  modules: result.modules,
  compatibilityDate: "2026-01-28"
}));

// Mount as a facet for persistent storage
const facet = this.ctx.facets.get("app", () => ({
  class: worker.getDurableObjectClass("App"),
  id: "app"
}));

// Serve assets on the host, forward the rest to the facet
const storage = createMemoryStorage(result.assets);
const assetResponse = await handleAssetRequest(
  request,
  result.assetManifest,
  storage,
  result.assetConfig
);
if (assetResponse) return assetResponse;
return facet.fetch(request);

The facet gets its own isolated SQLite storage that persists independently of the host DO's storage. Abort the facet on code changes to pick up new code while preserving storage:

this.ctx.facets.abort("app", new Error("Rebuilding"));

More Examples

Multiple Dependencies

const { mainModule, modules } = await createWorker({
  files: {
    "src/index.ts": `
      import { Hono } from 'hono';
      import { zValidator } from '@hono/zod-validator';
      import { z } from 'zod';

      const app = new Hono();
      const schema = z.object({ name: z.string() });

      app.post('/greet', zValidator('json', schema), (c) => {
        const { name } = c.req.valid('json');
        return c.json({ message: \`Hello, \${name}!\` });
      });

      export default app;
    `,
    "package.json": JSON.stringify({
      dependencies: {
        hono: "^4.0.0",
        "@hono/zod-validator": "^0.4.0",
        zod: "^3.23.0"
      }
    })
  }
});

With Wrangler Config

const { mainModule, modules, wranglerConfig } = await createWorker({
  files: {
    "src/index.ts": `
      export default {
        fetch: () => new Response('Hello!')
      }
    `,
    "wrangler.toml": `
      main = "src/index.ts"
      compatibility_date = "2026-01-01"
      compatibility_flags = ["nodejs_compat"]
    `
  }
});

const worker = env.LOADER.get("my-worker", () => ({
  mainModule,
  modules,
  compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",
  compatibilityFlags: wranglerConfig?.compatibilityFlags
}));

Transform-only Mode

Skip bundling to preserve the module structure:

const { mainModule, modules } = await createWorker({
  files: {
    /* ... */
  },
  bundle: false
});

Worker Loader Setup

// wrangler.jsonc (host worker)
{
  "worker_loaders": [{ "binding": "LOADER" }]
}
interface Env {
  LOADER: WorkerLoader;
}

Known Limitations

  • Flat node_modules — All packages are installed into a single flat directory. If two packages need different incompatible versions of the same transitive dependency, only one version is installed.
  • Long file paths in tarballs — The tar parser handles classic headers but not POSIX extended (PAX) headers. Packages with paths longer than 100 characters may have those files silently truncated.
  • Text files only from npm — Only text files (.js, .json, .css, etc.) are extracted from tarballs. Binary files like .wasm or .node are skipped.
  • No recursion depth limit — Transitive dependency resolution has no depth limit. A pathological dependency tree could cause excessive network requests.

License

MIT