From 353bd1241eaf531251a93571057f655f02fa0954 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 26 Oct 2026 21:50:39 +0000 Subject: [PATCH] [vite-plugin] Clear v1 Worker export types after dev server closes --- .changeset/vite-plugin-reset-export-types.md | 7 ++ .../__tests__/sequential-dev-servers.spec.ts | 96 +++++++++++++++++++ .../vite-plugin-cloudflare/src/context.ts | 5 + 3 files changed, 108 insertions(+) create mode 100644 .changeset/vite-plugin-reset-export-types.md create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/sequential-dev-servers.spec.ts diff --git a/.changeset/vite-plugin-reset-export-types.md b/.changeset/vite-plugin-reset-export-types.md new file mode 100644 index 00000000000..b71f3fed659 --- /dev/null +++ b/.changeset/vite-plugin-reset-export-types.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Fix a dev server failing to start when an earlier dev server in the same process had different Worker exports + +The Vite plugin now clears the Worker export types it detected when a dev server closes. A dev server created afterwards in the same process derives them from its own config again, so it no longer fails with `Class extends value undefined` for a Durable Object the earlier server's Worker did not export. diff --git a/packages/vite-plugin-cloudflare/src/__tests__/sequential-dev-servers.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/sequential-dev-servers.spec.ts new file mode 100644 index 00000000000..383fd704736 --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/__tests__/sequential-dev-servers.spec.ts @@ -0,0 +1,96 @@ +import assert from "node:assert"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { cloudflare } from "@cloudflare/vite-plugin"; +import { removeDirSync } from "@cloudflare/workers-utils"; +import { createServer } from "vite"; +import { afterEach, describe, test } from "vitest"; + +const FIXTURES_ROOT = path.resolve( + __dirname, + "fixtures", + "sequential-dev-servers" +); + +describe("sequential dev servers", () => { + const roots: string[] = []; + + afterEach(() => { + for (const root of roots.splice(0)) { + removeDirSync(root); + } + }); + + // Separate roots keep each server's configuration and module graph independent. + function createWorker(options: { + config: Record; + entry: string[]; + }) { + fs.mkdirSync(FIXTURES_ROOT, { recursive: true }); + const root = fs.realpathSync( + fs.mkdtempSync(path.join(FIXTURES_ROOT, "case-")) + ); + roots.push(root); + fs.mkdirSync(path.join(root, "src")); + fs.writeFileSync( + path.join(root, "wrangler.json"), + JSON.stringify({ + name: "worker", + main: "./src/index.ts", + compatibility_date: "2024-12-30", + ...options.config, + }) + ); + fs.writeFileSync(path.join(root, "src/index.ts"), options.entry.join("\n")); + + return root; + } + + async function fetchFromNewServer(root: string): Promise { + const server = await createServer({ + root, + configFile: false, + logLevel: "silent", + plugins: [cloudflare({ inspectorPort: false, persistState: false })], + }); + + try { + await server.listen(0); + const url = server.resolvedUrls?.local[0]; + assert(url, "Expected the server URL to be defined"); + + return await (await fetch(url)).text(); + } finally { + await server.close(); + } + } + + // Reference: https://github.com/cloudflare/workers-sdk/issues/16324 + test("a server created after another one has closed uses its own Worker exports", async ({ + expect, + }) => { + const firstRoot = createWorker({ + config: {}, + entry: [ + "export default { fetch: () => new Response('ok') };", + "export function helper() { return 1; }", + ], + }); + expect(await fetchFromNewServer(firstRoot)).toBe("ok"); + + const secondRoot = createWorker({ + config: { + durable_objects: { + bindings: [{ name: "COUNTER", class_name: "Counter" }], + }, + migrations: [{ tag: "v1", new_sqlite_classes: ["Counter"] }], + }, + entry: [ + "import { DurableObject } from 'cloudflare:workers';", + "export class Counter extends DurableObject {}", + "export default { fetch: (_request, env) => new Response(env.COUNTER ? 'ok' : 'no binding') };", + ], + }); + expect(await fetchFromNewServer(secondRoot)).toBe("ok"); + }); +}); diff --git a/packages/vite-plugin-cloudflare/src/context.ts b/packages/vite-plugin-cloudflare/src/context.ts index e430bbc03ba..a69caef91c8 100644 --- a/packages/vite-plugin-cloudflare/src/context.ts +++ b/packages/vite-plugin-cloudflare/src/context.ts @@ -55,9 +55,14 @@ export class PluginContext { debuglog("Miniflare is ready"); } + /** + * Disposes the Miniflare instance and clears the export types it was configured with. + * A server created later in the same process starts again from its own config. + */ async disposeMiniflare(): Promise { await this.#sharedContext.miniflare?.dispose(); this.#sharedContext.miniflare = undefined; + this.#sharedContext.workerNameToExportTypesMap = undefined; } get miniflare(): Miniflare {