@cloudflare/worker-bundler 0.0.0 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,494 @@
1
+ # worker-bundler
2
+
3
+ > **Experimental**: This package is under active development and its API may change without notice. Not recommended for production use.
4
+
5
+ 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.
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ npm install @cloudflare/worker-bundler
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### Bundle a Worker
16
+
17
+ Provide source code and dependencies — the bundler handles the rest:
18
+
19
+ ```ts
20
+ import { createWorker } from "@cloudflare/worker-bundler";
21
+
22
+ const worker = env.LOADER.get("my-worker", async () => {
23
+ const { mainModule, modules } = await createWorker({
24
+ files: {
25
+ "src/index.ts": `
26
+ import { Hono } from 'hono';
27
+ import { cors } from 'hono/cors';
28
+
29
+ const app = new Hono();
30
+ app.use('*', cors());
31
+ app.get('/', (c) => c.text('Hello from Hono!'));
32
+ app.get('/json', (c) => c.json({ message: 'It works!' }));
33
+
34
+ export default app;
35
+ `,
36
+ "package.json": JSON.stringify({
37
+ dependencies: { hono: "^4.0.0" }
38
+ })
39
+ }
40
+ });
41
+
42
+ return { mainModule, modules, compatibilityDate: "2026-01-01" };
43
+ });
44
+
45
+ await worker.getEntrypoint().fetch(request);
46
+ ```
47
+
48
+ ### Build a Full-Stack App
49
+
50
+ Use `createApp` to bundle a server Worker, client-side JavaScript, and static assets together:
51
+
52
+ ```ts
53
+ import { createApp } from "@cloudflare/worker-bundler";
54
+
55
+ const worker = env.LOADER.get("my-app", async () => {
56
+ const { mainModule, modules } = await createApp({
57
+ files: {
58
+ "src/server.ts": `
59
+ export default {
60
+ fetch(request) {
61
+ return new Response("API response");
62
+ }
63
+ }
64
+ `,
65
+ "src/client.ts": `
66
+ document.getElementById("app").textContent = "Hello from the client!";
67
+ `,
68
+ "package.json": JSON.stringify({
69
+ dependencies: {
70
+ /* ... */
71
+ }
72
+ })
73
+ },
74
+ server: "src/server.ts",
75
+ client: "src/client.ts",
76
+ assets: {
77
+ "/index.html":
78
+ "<!DOCTYPE html><div id='app'></div><script src='/client.js'></script>",
79
+ "/favicon.ico": favicon
80
+ },
81
+ assetConfig: {
82
+ not_found_handling: "single-page-application"
83
+ }
84
+ });
85
+
86
+ return { mainModule, modules, compatibilityDate: "2026-01-01" };
87
+ });
88
+ ```
89
+
90
+ The generated Worker automatically serves static assets (with proper content types, ETags, and caching) and falls through to your server code for API routes.
91
+
92
+ ## API
93
+
94
+ ### `createWorker(options)`
95
+
96
+ Bundles source files into a Worker.
97
+
98
+ | Option | Type | Default | Description |
99
+ | ------------ | ------------------------ | ------------------------------ | ----------------------------------------------------------------- |
100
+ | `files` | `Record<string, string>` | _required_ | Input files (path → content) |
101
+ | `entryPoint` | `string` | auto-detected | Entry point file path |
102
+ | `bundle` | `boolean` | `true` | Bundle all dependencies into one file |
103
+ | `externals` | `string[]` | `[]` | Modules to exclude from bundling (`cloudflare:*` always external) |
104
+ | `target` | `string` | `'es2022'` | Target environment |
105
+ | `minify` | `boolean` | `false` | Minify output |
106
+ | `sourcemap` | `boolean` | `false` | Generate inline source maps |
107
+ | `registry` | `string` | `'https://registry.npmjs.org'` | npm registry URL |
108
+
109
+ Returns:
110
+
111
+ ```ts
112
+ {
113
+ mainModule: string;
114
+ modules: Record<string, string | Module>;
115
+ wranglerConfig?: WranglerConfig;
116
+ warnings?: string[];
117
+ }
118
+ ```
119
+
120
+ ### `createApp(options)`
121
+
122
+ Builds a full-stack app: server Worker + client bundle + static assets.
123
+
124
+ | Option | Type | Default | Description |
125
+ | --------------- | --------------------------------------- | ------------- | ----------------------------------------------- |
126
+ | `files` | `Record<string, string>` | _required_ | All source files (server + client) |
127
+ | `server` | `string` | auto-detected | Server entry point (Worker fetch handler) |
128
+ | `client` | `string \| string[]` | — | Client entry point(s) to bundle for the browser |
129
+ | `assets` | `Record<string, string \| ArrayBuffer>` | — | Static assets (pathname → content) |
130
+ | `assetConfig` | `AssetConfig` | — | Asset serving configuration |
131
+ | `bundle` | `boolean` | `true` | Bundle server dependencies |
132
+ | `externals` | `string[]` | `[]` | Modules to exclude from bundling |
133
+ | `target` | `string` | `'es2022'` | Server target environment |
134
+ | `minify` | `boolean` | `false` | Minify output |
135
+ | `sourcemap` | `boolean` | `false` | Generate source maps |
136
+ | `registry` | `string` | npm default | npm registry URL |
137
+ | `durableObject` | `boolean \| { className?: string }` | — | Generate a Durable Object class wrapper |
138
+
139
+ Returns everything from `createWorker` plus:
140
+
141
+ ```ts
142
+ {
143
+ assetManifest: AssetManifest; // Metadata (content types, ETags) per asset
144
+ assetConfig?: AssetConfig; // The asset config used
145
+ clientBundles?: string[]; // Output paths of client bundles
146
+ durableObjectClassName?: string; // DO class name (when durableObject option used)
147
+ }
148
+ ```
149
+
150
+ ### `handleAssetRequest(request, manifest, storage, config?)`
151
+
152
+ Standalone async asset request handler. The manifest holds routing metadata (content types, ETags) while the storage backend provides content on demand:
153
+
154
+ ```ts
155
+ import { handleAssetRequest, buildAssets } from "@cloudflare/worker-bundler";
156
+
157
+ const { manifest, storage } = await buildAssets({
158
+ "/index.html": "<h1>Hello</h1>",
159
+ "/app.js": "console.log('hi')"
160
+ });
161
+
162
+ export default {
163
+ async fetch(request) {
164
+ const assetResponse = await handleAssetRequest(request, manifest, storage, {
165
+ not_found_handling: "single-page-application"
166
+ });
167
+ if (assetResponse) return assetResponse;
168
+
169
+ // Fall through to your API logic
170
+ return new Response("API");
171
+ }
172
+ };
173
+ ```
174
+
175
+ Returns a `Promise<Response | null>` — a `Response` if an asset matches, or `null` to fall through.
176
+
177
+ ## Storage Hooks
178
+
179
+ The asset handler separates **manifest** (routing metadata) from **storage** (content retrieval). This lets you plug in any backend.
180
+
181
+ ### `AssetStorage` Interface
182
+
183
+ ```ts
184
+ interface AssetStorage {
185
+ get(pathname: string): Promise<ReadableStream | ArrayBuffer | string | null>;
186
+ }
187
+ ```
188
+
189
+ ### Built-in: In-Memory Storage
190
+
191
+ The simplest option — stores everything in memory:
192
+
193
+ ```ts
194
+ import {
195
+ buildAssetManifest,
196
+ createMemoryStorage
197
+ } from "@cloudflare/worker-bundler";
198
+
199
+ const assets = { "/index.html": "<h1>Hi</h1>" };
200
+ const manifest = await buildAssetManifest(assets);
201
+ const storage = createMemoryStorage(assets);
202
+ ```
203
+
204
+ Or use the convenience function that returns both:
205
+
206
+ ```ts
207
+ import { buildAssets } from "@cloudflare/worker-bundler";
208
+
209
+ const { manifest, storage } = await buildAssets({
210
+ "/index.html": "<h1>Hi</h1>"
211
+ });
212
+ ```
213
+
214
+ ### Custom: KV Storage
215
+
216
+ ```ts
217
+ import type { AssetStorage } from "@cloudflare/worker-bundler";
218
+
219
+ function createKVStorage(kv: KVNamespace): AssetStorage {
220
+ return {
221
+ async get(pathname) {
222
+ return kv.get(pathname, "arrayBuffer");
223
+ }
224
+ };
225
+ }
226
+ ```
227
+
228
+ ### Custom: R2 Storage
229
+
230
+ ```ts
231
+ function createR2Storage(bucket: R2Bucket, prefix = "assets"): AssetStorage {
232
+ return {
233
+ async get(pathname) {
234
+ const obj = await bucket.get(`${prefix}${pathname}`);
235
+ return obj?.body ?? null;
236
+ }
237
+ };
238
+ }
239
+ ```
240
+
241
+ ### Custom: Workspace Storage
242
+
243
+ ```ts
244
+ import type { Workspace } from "agents/experimental/workspace";
245
+
246
+ function createWorkspaceStorage(workspace: Workspace): AssetStorage {
247
+ return {
248
+ async get(pathname) {
249
+ return workspace.readFile(pathname);
250
+ }
251
+ };
252
+ }
253
+ ```
254
+
255
+ All storage backends work with the same `handleAssetRequest` — just pass a different `storage` argument.
256
+
257
+ ## Asset Configuration
258
+
259
+ ### HTML Handling
260
+
261
+ Controls how HTML files are resolved and how trailing slashes are handled:
262
+
263
+ | Mode | Behavior |
264
+ | ---------------------- | --------------------------------------------------------------- |
265
+ | `auto-trailing-slash` | `/about` serves `about.html`; `/blog/` serves `blog/index.html` |
266
+ | `force-trailing-slash` | Redirects `/about` → `/about/`, serves from `index.html` |
267
+ | `drop-trailing-slash` | Redirects `/about/` → `/about`, serves from `.html` |
268
+ | `none` | Exact path matching only |
269
+
270
+ Default: `auto-trailing-slash`
271
+
272
+ ### Not-Found Handling
273
+
274
+ | Mode | Behavior |
275
+ | ------------------------- | -------------------------------------------------- |
276
+ | `single-page-application` | Serves `/index.html` for all unmatched routes |
277
+ | `404-page` | Serves nearest `404.html` walking up the directory |
278
+ | `none` | Returns `null` (fall through to your Worker) |
279
+
280
+ Default: `none`
281
+
282
+ ### Redirects
283
+
284
+ ```ts
285
+ {
286
+ redirects: {
287
+ static: {
288
+ "/old-page": { status: 301, to: "/new-page" }
289
+ },
290
+ dynamic: {
291
+ "/blog/:slug": { status: 302, to: "/posts/:slug" },
292
+ "/docs/*": { status: 301, to: "/wiki/*" }
293
+ }
294
+ }
295
+ }
296
+ ```
297
+
298
+ Dynamic redirects support `:placeholder` tokens and `*` splat patterns.
299
+
300
+ ### Custom Headers
301
+
302
+ ```ts
303
+ {
304
+ headers: {
305
+ "/*": {
306
+ set: { "X-Frame-Options": "DENY" }
307
+ },
308
+ "/api/*": {
309
+ set: { "Access-Control-Allow-Origin": "*" },
310
+ unset: ["X-Frame-Options"]
311
+ }
312
+ }
313
+ }
314
+ ```
315
+
316
+ Patterns use glob syntax. Rules are applied in order — later rules can override earlier ones.
317
+
318
+ ### Caching
319
+
320
+ The asset handler sets cache headers automatically:
321
+
322
+ - **HTML and non-hashed files**: `public, max-age=0, must-revalidate`
323
+ - **Hashed files** (e.g. `app.a1b2c3d4.js`): `public, max-age=31536000, immutable`
324
+ - **ETag support**: Conditional requests with `If-None-Match` return `304 Not Modified`
325
+
326
+ ## Entry Point Detection
327
+
328
+ Priority order for `createWorker` (and `createApp` server entry):
329
+
330
+ 1. Explicit `entryPoint` / `server` option
331
+ 2. `main` field in wrangler config
332
+ 3. `exports`, `module`, or `main` field in `package.json`
333
+ 4. Default paths: `src/index.ts`, `src/index.js`, `index.ts`, `index.js`
334
+
335
+ ## Durable Object Mode
336
+
337
+ 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.
338
+
339
+ ```ts
340
+ const result = await createApp({
341
+ files: {
342
+ "src/server.ts": `
343
+ import { DurableObject } from 'cloudflare:workers';
344
+
345
+ export default class App extends DurableObject {
346
+ async fetch(request: Request) {
347
+ const url = new URL(request.url);
348
+ if (url.pathname === '/api/count') {
349
+ const count = ((await this.ctx.storage.get('count')) as number) ?? 0;
350
+ await this.ctx.storage.put('count', count + 1);
351
+ return Response.json({ count });
352
+ }
353
+ return new Response('Not found', { status: 404 });
354
+ }
355
+ }
356
+ `
357
+ },
358
+ assets: {
359
+ "/index.html": "<!DOCTYPE html><h1>Counter</h1>"
360
+ },
361
+ durableObject: true
362
+ });
363
+ ```
364
+
365
+ The wrapper exports a named class (default `"App"`) that:
366
+
367
+ - Serves static assets first (same behavior as module worker mode)
368
+ - Falls through to the user's `fetch()` via `super.fetch(request)`
369
+ - If the user exports a class, the wrapper extends it (so `this.ctx.storage` works)
370
+ - If the user exports a plain `{ fetch }` handler, the wrapper wraps it in a DO
371
+
372
+ ### Using with `ctx.facets`
373
+
374
+ The host Durable Object loads the worker via LOADER and creates a facet:
375
+
376
+ ```ts
377
+ const worker = env.LOADER.get(loaderId, () => ({
378
+ mainModule: result.mainModule,
379
+ modules: result.modules,
380
+ compatibilityDate: "2026-01-28"
381
+ }));
382
+
383
+ const className = result.durableObjectClassName; // "App"
384
+ const facet = this.ctx.facets.get("app", () => ({
385
+ class: worker.getDurableObjectClass(className),
386
+ id: "app"
387
+ }));
388
+
389
+ return facet.fetch(request);
390
+ ```
391
+
392
+ 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:
393
+
394
+ ```ts
395
+ this.ctx.facets.abort("app", new Error("Rebuilding"));
396
+ ```
397
+
398
+ ## More Examples
399
+
400
+ ### Multiple Dependencies
401
+
402
+ ```ts
403
+ const { mainModule, modules } = await createWorker({
404
+ files: {
405
+ "src/index.ts": `
406
+ import { Hono } from 'hono';
407
+ import { zValidator } from '@hono/zod-validator';
408
+ import { z } from 'zod';
409
+
410
+ const app = new Hono();
411
+ const schema = z.object({ name: z.string() });
412
+
413
+ app.post('/greet', zValidator('json', schema), (c) => {
414
+ const { name } = c.req.valid('json');
415
+ return c.json({ message: \`Hello, \${name}!\` });
416
+ });
417
+
418
+ export default app;
419
+ `,
420
+ "package.json": JSON.stringify({
421
+ dependencies: {
422
+ hono: "^4.0.0",
423
+ "@hono/zod-validator": "^0.4.0",
424
+ zod: "^3.23.0"
425
+ }
426
+ })
427
+ }
428
+ });
429
+ ```
430
+
431
+ ### With Wrangler Config
432
+
433
+ ```ts
434
+ const { mainModule, modules, wranglerConfig } = await createWorker({
435
+ files: {
436
+ "src/index.ts": `
437
+ export default {
438
+ fetch: () => new Response('Hello!')
439
+ }
440
+ `,
441
+ "wrangler.toml": `
442
+ main = "src/index.ts"
443
+ compatibility_date = "2026-01-01"
444
+ compatibility_flags = ["nodejs_compat"]
445
+ `
446
+ }
447
+ });
448
+
449
+ const worker = env.LOADER.get("my-worker", () => ({
450
+ mainModule,
451
+ modules,
452
+ compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",
453
+ compatibilityFlags: wranglerConfig?.compatibilityFlags
454
+ }));
455
+ ```
456
+
457
+ ### Transform-only Mode
458
+
459
+ Skip bundling to preserve the module structure:
460
+
461
+ ```ts
462
+ const { mainModule, modules } = await createWorker({
463
+ files: {
464
+ /* ... */
465
+ },
466
+ bundle: false
467
+ });
468
+ ```
469
+
470
+ ## Worker Loader Setup
471
+
472
+ ```jsonc
473
+ // wrangler.jsonc (host worker)
474
+ {
475
+ "worker_loaders": [{ "binding": "LOADER" }]
476
+ }
477
+ ```
478
+
479
+ ```ts
480
+ interface Env {
481
+ LOADER: WorkerLoader;
482
+ }
483
+ ```
484
+
485
+ ## Known Limitations
486
+
487
+ - **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.
488
+ - **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.
489
+ - **Text files only from npm** — Only text files (`.js`, `.json`, `.css`, etc.) are extracted from tarballs. Binary files like `.wasm` or `.node` are skipped.
490
+ - **No recursion depth limit** — Transitive dependency resolution has no depth limit. A pathological dependency tree could cause excessive network requests.
491
+
492
+ ## License
493
+
494
+ MIT
Binary file