Skip to content

feat(worker-bundler): add typescript support for worker-bundler #1277

Open
zebp wants to merge 7 commits intomainfrom
zeb/typescript
Open

feat(worker-bundler): add typescript support for worker-bundler #1277
zebp wants to merge 7 commits intomainfrom
zeb/typescript

Conversation

@zebp
Copy link
Copy Markdown
Member

@zebp zebp commented Apr 8, 2026

Adds support for the TypeScript language service to @cloudflare/worker-bundler as well a new virtual file-system abstraction for interfacing with the code for bundled workers. This is primarily to allow efficient storage of Worker code in Durable Objects.

Example

const inputFileSystem = new InMemoryFileSystem({
  "package.json": JSON.stringify({
    dependencies: {
      "@cloudflare/workers-types": "^4.20260405.1"
    }
  }),
  "tsconfig.json": JSON.stringify({
    compilerOptions: {
      lib: ["es2024"],
      target: "ES2024",
      module: "ES2022",
      moduleResolution: "bundler",
      allowSyntheticDefaultImports: true,
      strict: true,
      skipLibCheck: true,
      types: ["@cloudflare/workers-types/index.d.ts"]
    }
  }),
  "src/index.ts": `
    const worker: ExportedHandler = {
    
    async fetch() {
        return new Response("Hello, world!");
      }
    };
    
    export default worker;
  `
});

const installResult = await installDependencies(inputFileSystem);
const { fileSystem, languageService } = await createTypescriptLanguageService({ fileSystem: inputFileSystem });

Separate ./typescript export

The TypeScript bundle is large (~5 MB minified). Importing it unconditionally from the main entry point would penalise every caller that only needs bundling. The language service is therefore exported under its own subpath:

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

Open with Devin

zebp added 7 commits April 8, 2026 10:16
Add DurableObjectKVFileSystem write overlay that buffers writes in memory
and only persists to KV on flush(), avoiding expensive per-write I/O.
Reads fall back through the overlay to KV so callers see their own writes
immediately. Switch read() to return string|null instead of empty string
so callers can distinguish missing files from empty ones without a separate
exists() check. Add vitest tests for both InMemoryFileSystem and
DurableObjectKVFileSystem using runInDurableObject for real KV storage.
…tion

Replace direct Record<string,string> access throughout bundler.ts, resolver.ts,
transformer.ts, config.ts, utils.ts, and app.ts with FileSystem.read() /
FileSystem.write() calls. Both createWorker and createApp now accept a plain
Files object or any FileSystem implementation for their 'files' option — plain
objects are automatically wrapped in InMemoryFileSystem.

Removes the stale 'files = installResult.files' pattern from app.ts (the
installer already mutates the provided FileSystem in-place via write()).

Add e2e tests covering the full bundle+install pipeline driven by both
InMemoryFileSystem and DurableObjectKVFileSystem, asserting that installed
node_modules are readable from the filesystem after createWorker returns and
that flushing a DurableObjectKVFileSystem persists them to KV.
…talled packages

Export installDependencies, hasDependencies, and InstallResult so callers can
pre-warm a FileSystem with npm packages independently of createWorker/createApp.

Add skip-if-already-exists guard in installPackage: if node_modules/<name>/
package.json is already present in the FileSystem, the package is skipped
without hitting the network. This makes the internal installDependencies call
inside createWorker a no-op for packages that were pre-installed.

Add tests covering standalone install, the skip behavior using a pre-seeded
filesystem, and the full pre-install-then-createWorker pipeline — including a
vi.spyOn(globalThis, 'fetch') assertion that proves no network request is made
during the second pass.
…ctRawFileSystem

Extract a generic OverlayFileSystem (internal, not exported) that buffers writes
in memory over any FileSystem inner. flush() drains the buffer via inner.write()
then inner.flush(), keeping the overlay path-agnostic with no prefix awareness.

Add DurableObjectRawFileSystem (exported) — a thin FileSystem backed directly by
Durable Object KV with no buffering. Every write is committed synchronously and
flush() is a no-op. Useful for read-heavy access or when writing a small number
of files where per-write durability is preferred over batching.

Retool DurableObjectKVFileSystem to compose OverlayFileSystem(DurableObjectRawFileSystem)
rather than reimplementing the overlay inline. Public API is unchanged.

Add DurableObjectRawFileSystem to the package exports and add a test suite
covering direct write persistence, read-back, no-op flush, and custom prefix.
…ementations

Add list(prefix?: string): string[] to the FileSystem interface. All
implementations return logical paths (without any storage prefix):

- InMemoryFileSystem: filters the backing Map's keys by prefix
- DurableObjectRawFileSystem: iterates kv.list() (Iterable<[key, value]>)
  and strips the storage prefix from each key for consistency
- OverlayFileSystem: unions inner.list() with matching overlay keys via a
  Set, so paths present in both sources appear exactly once
- DurableObjectKVFileSystem: delegates to its inner OverlayFileSystem
`createTypescriptLanguageService` wraps a `FileSystem` in a
`TypescriptFileSystem` that mirrors every write and delete into an
underlying virtual TypeScript environment. Diagnostics returned by the
language service always reflect the current state of the filesystem —
an edit that fixes a type error immediately clears `getSemanticDiagnostics`.

TypeScript is pre-bundled as a browser-safe artifact so it runs inside
the Workers runtime without Node.js APIs. Lib declarations are fetched
from the TypeScript npm tarball at runtime.

Exposed under a separate `./typescript` subpath to keep the TypeScript
bundle out of the main import path.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: 9c53d77

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/worker-bundler Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1277

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1277

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1277

hono-agents

npm i https://pkg.pr.new/hono-agents@1277

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1277

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1277

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1277

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1277

commit: 9c53d77

@zebp zebp marked this pull request as ready for review April 8, 2026 22:38
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +75 to +78
write(path: string, content: string): void {
this.innerFs.write(path, content);
this.typescriptEnv.updateFile(path, content);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 TypescriptFileSystem.write() throws for files not in the original root set

The write() method unconditionally calls this.typescriptEnv.updateFile(path, content), but @typescript/vfs's updateFile throws "Did not find a source file for <fileName>" when the file wasn't in the original root files passed to createVirtualTypeScriptEnvironment. This means any caller who writes a new file through the returned TypescriptFileSystem wrapper will get a runtime crash.

@typescript/vfs updateFile implementation (confirms the throw)
updateFile: function updateFile(fileName, content, optPrevTextSpan) {
  var prevSourceFile = languageService.getProgram().getSourceFile(fileName);
  if (!prevSourceFile) {
    throw new Error("Did not find a source file for " + fileName);
  }
  // ...
}

The VirtualTypeScriptEnvironment exposes both createFile(fileName, content) (for new files) and updateFile(fileName, content) (for existing files). The fix is to check getSourceFile() first and dispatch to the correct method.

Suggested change
write(path: string, content: string): void {
this.innerFs.write(path, content);
this.typescriptEnv.updateFile(path, content);
}
write(path: string, content: string): void {
this.innerFs.write(path, content);
if (this.typescriptEnv.getSourceFile(path)) {
this.typescriptEnv.updateFile(path, content);
} else {
this.typescriptEnv.createFile(path, content);
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant