# 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/) (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: ```ts 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: ```ts import { createApp } from "@cloudflare/worker-bundler"; const worker = env.LOADER.get("my-app", async () => { const { mainModule, modules } = 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!"; `, "package.json": JSON.stringify({ dependencies: { /* ... */ } }) }, server: "src/server.ts", client: "src/client.ts", assets: { "/index.html": "
", "/favicon.ico": favicon }, assetConfig: { not_found_handling: "single-page-application" } }); return { mainModule, modules, compatibilityDate: "2026-01-01" }; }); ``` The generated Worker automatically serves static assets (with proper content types, ETags, and caching) and falls through to your server code for API routes. ## API ### `createWorker(options)` Bundles source files into a Worker. | Option | Type | Default | Description | | ------------ | ------------------------ | ------------------------------ | ----------------------------------------------------------------- | | `files` | `Record` | _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: ```ts { mainModule: string; modules: Record; wranglerConfig?: WranglerConfig; warnings?: string[]; } ``` ### `createApp(options)` Builds a full-stack app: server Worker + client bundle + static assets. | Option | Type | Default | Description | | --------------- | --------------------------------------- | ------------- | ----------------------------------------------- | | `files` | `Record` | _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` | — | 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 | | `durableObject` | `boolean \| { className?: string }` | — | Generate a Durable Object class wrapper | Returns everything from `createWorker` plus: ```ts { assetManifest: AssetManifest; // Metadata (content types, ETags) per asset assetConfig?: AssetConfig; // The asset config used clientBundles?: string[]; // Output paths of client bundles durableObjectClassName?: string; // DO class name (when durableObject option used) } ``` ### `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: ```ts import { handleAssetRequest, buildAssets } from "@cloudflare/worker-bundler"; const { manifest, storage } = await buildAssets({ "/index.html": "

Hello

", "/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` — 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 ```ts interface AssetStorage { get(pathname: string): Promise; } ``` ### Built-in: In-Memory Storage The simplest option — stores everything in memory: ```ts import { buildAssetManifest, createMemoryStorage } from "@cloudflare/worker-bundler"; const assets = { "/index.html": "

Hi

" }; const manifest = await buildAssetManifest(assets); const storage = createMemoryStorage(assets); ``` Or use the convenience function that returns both: ```ts import { buildAssets } from "@cloudflare/worker-bundler"; const { manifest, storage } = await buildAssets({ "/index.html": "

Hi

" }); ``` ### Custom: KV Storage ```ts import type { AssetStorage } from "@cloudflare/worker-bundler"; function createKVStorage(kv: KVNamespace): AssetStorage { return { async get(pathname) { return kv.get(pathname, "arrayBuffer"); } }; } ``` ### Custom: R2 Storage ```ts 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 ```ts 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 ```ts { 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 ```ts { 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` ## Durable Object Mode Pass `durableObject: true` to generate a Durable Object class wrapper instead of a module worker. This gives generated apps persistent storage via `this.ctx.storage` — state survives across requests, code rebuilds, and isolate restarts. ```ts 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": "

Counter

" }, durableObject: true }); ``` The wrapper exports a named class (default `"App"`) that: - Serves static assets first (same behavior as module worker mode) - Falls through to the user's `fetch()` via `super.fetch(request)` - If the user exports a class, the wrapper extends it (so `this.ctx.storage` works) - If the user exports a plain `{ fetch }` handler, the wrapper wraps it in a DO ### Using with `ctx.facets` The host Durable Object loads the worker via LOADER and creates a facet: ```ts const worker = env.LOADER.get(loaderId, () => ({ mainModule: result.mainModule, modules: result.modules, compatibilityDate: "2026-01-28" })); const className = result.durableObjectClassName; // "App" const facet = this.ctx.facets.get("app", () => ({ class: worker.getDurableObjectClass(className), id: "app" })); 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: ```ts this.ctx.facets.abort("app", new Error("Rebuilding")); ``` ## More Examples ### Multiple Dependencies ```ts 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 ```ts 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: ```ts const { mainModule, modules } = await createWorker({ files: { /* ... */ }, bundle: false }); ``` ## Worker Loader Setup ```jsonc // wrangler.jsonc (host worker) { "worker_loaders": [{ "binding": "LOADER" }] } ``` ```ts 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