@cloudflare/worker-bundler

中文文档
0.2.6发布于 2026/10/07MITTypeScriptES Module构建来源已验证近 30 天下载 40.8万

Build and bundle Workers at runtime for Cloudflare Worker Loader binding

原始使用文档
文档目录44 节
  1. Runtime requirement
  2. Installation
  3. Quick Start
  4. Bundle a Worker
  5. Build a Full-Stack App
  6. API
  7. createWorker(options)
  8. createApp(options)
  9. handleAssetRequest(request, manifest, storage, config?)
  10. Storage Hooks
  11. AssetStorage Interface
  12. Built-in: In-Memory Storage
  13. Custom: KV Storage
  14. Custom: R2 Storage
  15. Custom: Workspace Storage
  16. Asset Configuration
  17. HTML Handling
  18. Not-Found Handling
  19. Redirects
  20. Custom Headers
  21. Caching
  22. Entry Point Detection
  23. Mounting as a Durable Object
  24. More Examples
  25. Multiple Dependencies
  26. With Wrangler Config
  27. Transform-only Mode
  28. Advanced bundler options
  29. jsx , jsxImportSource
  30. define
  31. loader
  32. conditions
  33. virtualModules
  34. __dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFired
  35. Worker Loader Setup
  36. File System
  37. Implementations
  38. Snapshotting from async sources
  39. Pre-installing Dependencies
  40. TypeScript Language Service
  41. With installDependencies
  42. Integrating with Workspace
  43. Known Limitations
  44. License

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.

Runtime requirement

This package only runs inside the Cloudflare Workers runtime (workerd) — both in production and in wrangler dev. It bundles via an esbuild-wasm WebAssembly.Module import that only the Workers module loader can resolve.

It will not work under plain Node.js. In particular, importing it from a Vitest/Jest test that uses the default Node pool will surface an error pointing you here. To test code that depends on @cloudflare/worker-bundler, run your tests with @cloudflare/vitest-pool-workers so the test body executes inside workerd. See packages/worker-bundler/src/tests/vitest.config.ts in this repo for an example setup.

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.

OptionTypeDefaultDescription
filesRecord<string, string> | FileSystemrequiredInput files — plain object or any FileSystem implementation
entryPointstringauto-detectedEntry point file path
bundlebooleantrueBundle all dependencies into one file
externalsstring[][]Modules to exclude from bundling (cloudflare:* always external)
targetstring'es2022'Target environment
minifybooleanfalseMinify output
sourcemapbooleanfalseGenerate inline source maps
registrystring'https://registry.npmjs.org'npm registry URL
jsx"transform" | "preserve" | "automatic"esbuild defaultJSX transform mode
jsxImportSourcestringesbuild defaultJSX runtime import source (e.g. "react", "preact")
defineRecord<string, string>{}Constant replacements applied at bundle time
loaderRecord<string, BundlerLoader>{}Per-extension loader overrides (e.g. { ".svg": "text", ".wasm": "binary" }). See Advanced bundler options for the supported set.
conditionsstring[]esbuild defaultPackage export conditions to honour during resolution
virtualModulesRecord<string, string>{}Exact import-specifier aliases backed by generated JavaScript module source. See Advanced bundler options.
__dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFiredunknown[][]Escape hatch: extra esbuild plugins, run before the internal virtual-fs plugin. Not covered by semver — see Advanced bundler options.

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.

OptionTypeDefaultDescription
filesRecord<string, string> | FileSystemrequiredAll source files — plain object or any FileSystem
serverstringauto-detectedServer entry point (Worker fetch handler)
clientstring | string[]—Client entry point(s) to bundle for the browser
assetsRecord<string, string | ArrayBuffer>—Static assets (pathname → content)
assetConfigAssetConfig—Asset serving configuration
bundlebooleantrueBundle server dependencies
externalsstring[][]Modules to exclude from bundling
targetstring'es2022'Server target environment
minifybooleanfalseMinify output
sourcemapbooleanfalseGenerate source maps
registrystringnpm defaultnpm registry URL
jsx"transform" | "preserve" | "automatic"esbuild defaultJSX transform mode (applied to both server and client bundles)
jsxImportSourcestringesbuild defaultJSX runtime import source (e.g. "react", "preact")
defineRecord<string, string>{}Constant replacements applied at bundle time (both server and client)
loaderRecord<string, BundlerLoader>{}Per-extension loader overrides (e.g. { ".svg": "text" }). See Advanced bundler options for the supported set.
conditionsstring[]esbuild defaultPackage export conditions to honour during resolution
virtualModulesRecord<string, string>{}Exact import-specifier aliases backed by generated JavaScript module source
__dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFiredunknown[][]Escape hatch: extra esbuild plugins. See Advanced bundler options.

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:

ModeBehavior
auto-trailing-slash/about serves about.html; /blog/ serves blog/index.html
force-trailing-slashRedirects /about → /about/, serves from index.html
drop-trailing-slashRedirects /about/ → /about, serves from .html
noneExact path matching only

Default: auto-trailing-slash

Not-Found Handling

ModeBehavior
single-page-applicationServes /index.html for all unmatched routes
404-pageServes nearest 404.html walking up the directory
noneReturns 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.

getDurableObjectClass(name) resolves a named class export — so export the class with export class App, not export default class App. The default entrypoint slot cannot hold an actor class, and a default-exported DurableObject will surface as an opaque internal error when the facet is invoked.

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

      export 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-06-11"
}));

// 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
});

Advanced bundler options

createWorker and createApp cover the common cases (target, minify, sourcemap, externals). The options below are escape hatches for advanced consumers — RSC-style transforms, custom asset pipelines, library bundling.

All options in this section require bundle: true (the default). Transform-only mode (bundle: false) skips esbuild entirely and uses sucrase + naïve module resolution instead, so define, loader, conditions, plugins, and the JSX options have no effect there. Setting any of them with bundle: false adds a warning to result.warnings rather than failing silently.

jsx, jsxImportSource

Pick a JSX runtime explicitly. With jsx: "automatic" you don't need to import React (or whatever runtime) in every file.

await createApp({
  files,
  client: "src/client.tsx",
  jsx: "automatic",
  jsxImportSource: "react"
});

define

Compile-time constant replacement. Each value must be a valid JavaScript expression as a string (so wrap string values in extra quotes).

await createWorker({
  files,
  define: {
    "process.env.NODE_ENV": '"production"',
    __DEV__: "false",
    __VERSION__: JSON.stringify(pkg.version)
  }
});

loader

Tell the bundler how to interpret arbitrary file extensions. The built-in handling for .ts / .tsx / .js / .jsx / .json / .css is preserved unless you override it. Longer extensions match first, so ".d.ts" wins over ".ts".

await createWorker({
  files,
  loader: {
    ".svg": "text",
    ".wasm": "binary",
    ".sql": "text"
  }
});

The supported BundlerLoader values are deliberately narrowed to the portable subset that every modern bundler can express:

LoaderMeaning
jsPlain JavaScript
jsxJSX (transformed per the jsx option)
tsTypeScript
tsxTypeScript + JSX
jsonParse as JSON, expose as the default export
cssParse as CSS
textEmbed file contents as a UTF-8 string default export
binaryEmbed file contents as a Uint8Array default export
base64Embed file contents as a base64 string default export
dataurlEmbed file contents as a data: URL string default export

esbuild's file / copy loaders are intentionally not exposed: they emit secondary output files that this bundler currently discards (only the first output is read). If you need that — or anything else esbuild-specific — reach for __dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFired instead.

conditions

Package export conditions, in priority order. Useful when bundling libraries that ship multiple builds via package.json#exports.

await createWorker({
  files,
  conditions: ["workerd", "worker", "browser"]
});

virtualModules

Alias exact import specifiers to generated JavaScript modules. This is useful when a host framework wants to provide virtual runtime modules without exposing esbuild plugin details to callers.

const { mainModule, modules } = await createWorker({
  files: {
    "src/index.ts": `
      import { readFileSync } from "node:fs";

      export default {
        fetch() {
          return new Response(readFileSync("/message.txt", "utf8"));
        }
      };
    `
  },
  virtualModules: {
    "node:fs": `
      const files = { "/message.txt": "hello from a virtual module" };
      export function readFileSync(path) {
        return files[path];
      }
      export default { readFileSync };
    `,
    fs: `export * from "node:fs"; export { default } from "node:fs";`
  }
});

virtualModules keys are exact specifier matches. If you want both "fs" and "node:fs" to work, provide both aliases. The generated source is bundled with the rest of the Worker, so it can be imported by the entry point or by transitive dependencies.

__dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFired

Read this before using. This option is not covered by semver. The name is the API contract: it can change shape, be renamed, or be removed in any release. The runtime ties you to esbuild — if this package switches bundlers (e.g. to rolldown), plugins authored against this API will break.

Pass extra esbuild plugins. They run before the bundler's internal virtual-filesystem plugin, so a plugin's onResolve / onLoad claims fire first (e.g. an RSC plugin claiming "server-function:*" before virtual-fs tries to read it from disk).

The option is typed as unknown[] at the public boundary so the published .d.ts doesn't depend on esbuild-wasm's types. Cast your plugin array when passing it in:

import type { Plugin } from "esbuild-wasm";

const myRscPlugin: Plugin = {
  name: "rsc",
  setup(build) {
    build.onResolve({ filter: /^server-function:/ }, (args) => ({
      path: args.path,
      namespace: "rsc"
    }));
    build.onLoad({ filter: /.*/, namespace: "rsc" }, (args) => ({
      contents: generateServerFunctionStub(args.path),
      loader: "ts"
    }));
  }
};

await createApp({
  files,
  client: "src/client.tsx",
  __dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFired: [
    myRscPlugin
  ] as unknown[]
});

Worker Loader Setup

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

File System

The bundler operates on a FileSystem interface — a minimal, synchronous abstraction for reading, writing, and listing files. All bundler APIs (createWorker, createApp) accept either a plain Record<string, string> (auto-wrapped in InMemoryFileSystem) or any FileSystem implementation.

interface FileSystem {
  read(path: string): string | null;
  write(path: string, content: string): void;
  delete(path: string): void;
  list(prefix?: string): string[];
  flush(): Promise<void>;
}

Implementations

InMemoryFileSystem — Map-backed, no persistence. Accepts a seed object or Map:

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

const fs = new InMemoryFileSystem({
  "src/index.ts": "export default { fetch() { return new Response('ok'); } };"
});

DurableObjectKVFileSystem — Writes are buffered in memory and flushed to Durable Object KV in one batch. Reads see buffered writes immediately:

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

const fs = new DurableObjectKVFileSystem(this.ctx.storage);
fs.write("src/index.ts", code);

const result = await createWorker({ files: fs });

// Persist everything (source + installed node_modules) to KV
await fs.flush();

After flushing, the filesystem is durable — a subsequent createWorker call on a new DurableObjectKVFileSystem over the same storage will find node_modules already present and skip installation entirely.

DurableObjectRawFileSystem — Every write goes directly to KV with no buffering. Use when writing a small number of files where per-write durability is preferred:

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

const fs = new DurableObjectRawFileSystem(this.ctx.storage);

Snapshotting from async sources

The FileSystem interface is synchronous because esbuild and TypeScript require synchronous file access. To bridge from an async storage backend (like Workspace from @cloudflare/shell), use createFileSystemSnapshot:

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

// Accepts any sync or async iterable of [path, content] pairs
async function* workspaceFiles(workspace) {
  for (const entry of workspace.glob("**/*.{ts,tsx,json}")) {
    const content = await workspace.readFile(entry.path);
    if (content !== null) yield [entry.path, content];
  }
}

const fs = await createFileSystemSnapshot(workspaceFiles(workspace));
const result = await createWorker({ files: fs });

Pre-installing Dependencies

installDependencies is exported separately so you can pre-warm a filesystem with npm packages independently of createWorker:

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

const fs = new DurableObjectKVFileSystem(this.ctx.storage);
fs.write("package.json", JSON.stringify({ dependencies: { hono: "^4.0.0" } }));
fs.write("src/index.ts", code);

// Install once, flush to KV
await installDependencies(fs);
await fs.flush();

// On subsequent requests, createWorker finds node_modules already
// present and skips installation — no network requests
const result = await createWorker({ files: fs });

This is useful for separating the install step (slow, network-bound) from the bundle step (fast, CPU-only), and for persisting installed packages across DO restarts.

TypeScript Language Service

A full TypeScript language service runs in-process inside the Workers runtime. Import it from the ./typescript subpath to keep the TypeScript bundle (~5 MB) out of the main entry:

import { createTypescriptLanguageService } from "@cloudflare/worker-bundler/typescript";
import { InMemoryFileSystem } from "@cloudflare/worker-bundler";

const fileSystem = new InMemoryFileSystem({
  "tsconfig.json": JSON.stringify({
    compilerOptions: {
      lib: ["es2024"],
      target: "ES2024",
      module: "ES2022",
      moduleResolution: "bundler",
      strict: true,
      skipLibCheck: true
    }
  }),
  "src/index.ts": `
    const worker: ExportedHandler = {
      async fetch() {
        return new Response("Hello, world!");
      }
    };
    export default worker;
  `
});

const { fileSystem: tsFileSystem, languageService } =
  await createTypescriptLanguageService({ fileSystem });

// Get diagnostics
const errors = languageService.getSemanticDiagnostics("src/index.ts");

The returned fileSystem wraps the original and keeps the TypeScript environment in sync — write through it to see updated diagnostics immediately:

// Fix an error
tsFileSystem.write("src/index.ts", correctedCode);

// Diagnostics now reflect the fix
const errors = languageService.getSemanticDiagnostics("src/index.ts");
// => []

With installDependencies

Install type packages into the filesystem before creating the language service to get full type coverage:

import {
  installDependencies,
  InMemoryFileSystem
} from "@cloudflare/worker-bundler";
import { createTypescriptLanguageService } from "@cloudflare/worker-bundler/typescript";

const fs = new InMemoryFileSystem({
  "package.json": JSON.stringify({
    dependencies: { "@cloudflare/workers-types": "^4.20260405.1" }
  }),
  "tsconfig.json": JSON.stringify({
    compilerOptions: {
      lib: ["es2024"],
      strict: true,
      skipLibCheck: true,
      types: ["@cloudflare/workers-types/index.d.ts"]
    }
  }),
  "src/index.ts": workerSource
});

await installDependencies(fs);

const { languageService } = await createTypescriptLanguageService({
  fileSystem: fs
});
const diagnostics = languageService.getSemanticDiagnostics("src/index.ts");

Integrating with Workspace

When using Workspace from @cloudflare/shell as durable storage, snapshot source files into an InMemoryFileSystem for type checking:

import { createFileSystemSnapshot } from "@cloudflare/worker-bundler";
import { createTypescriptLanguageService } from "@cloudflare/worker-bundler/typescript";

// 1. Snapshot source files from Workspace (async reads)
async function* sourceFiles(workspace) {
  for (const entry of workspace.glob("**/*.{ts,tsx,json}")) {
    const content = await workspace.readFile(entry.path);
    if (content !== null) yield [entry.path, content];
  }
}

const fs = await createFileSystemSnapshot(sourceFiles(workspace));

// 2. Create language service (synchronous reads during init)
const { fileSystem, languageService } = await createTypescriptLanguageService({
  fileSystem: fs
});

// 3. Get diagnostics
const errors = languageService.getSemanticDiagnostics("src/index.ts");

// 4. On edits, update both the TS environment and durable storage
fileSystem.write("src/index.ts", newContent);
await workspace.writeFile("src/index.ts", newContent);

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.
  • TypeScript lib declarations fetched at runtime — createTypescriptLanguageService downloads the TypeScript npm tarball (~23 MB) on every call to extract the standard library .d.ts files (~3.7 MB). There is no caching — each cold start of a Durable Object that creates a language service incurs this fetch. A future improvement would pre-bundle these declarations at build time (following the same pattern as the TypeScript compiler bundle).

License

MIT

版本与发布记录

@cloudflare/worker-bundler 共发布 14 个版本,首次发布于 2026/03/12;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.6latest2026/10/071 个月前
0.2.52026/10/021 个月前
0.2.42026/09/181 个月前
0.2.32026/08/182 个月前
0.2.22026/07/233 个月前
0.2.12026/06/075 个月前

关于 @cloudflare/worker-bundler 的常见问题

@cloudflare/worker-bundler 是什么?

Build and bundle Workers at runtime for Cloudflare Worker Loader binding

如何安装 @cloudflare/worker-bundler?

在项目目录运行 npm install @cloudflare/worker-bundler,或使用 pnpm add / yarn add / bun add。

@cloudflare/worker-bundler 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cloudflare/worker-bundler 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cloudflare/worker-bundler 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cloudflare/worker-bundler 可以商用吗?

@cloudflare/worker-bundler 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cloudflare/worker-bundler 的下载量有多少?

根据 npm 官方下载统计,@cloudflare/worker-bundler 在 2026/10/10 – 2026/11/08 这 30 天内被下载 408,436 次,日均约 13,615 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cloudflare/worker-bundler 还在维护吗?

最新版本 0.2.6 发布于 2026/10/07(1 个月前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/03/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。