From 0706fbf950548aaa8177a062a7c5d41822dfba0d Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Mon, 8 Jun 2026 13:31:11 +0100 Subject: [PATCH 1/4] feat(wrangler): createTestHarness API (#14169) --- .changeset/gentle-cougars-build.md | 30 + .../wrangler/e2e/createTestHarness.test.ts | 964 ++++++++++++++++++ packages/wrangler/src/api/index.ts | 8 + .../src/api/startDevWorker/BaseController.ts | 6 + .../api/startDevWorker/BundlerController.ts | 10 +- .../api/startDevWorker/ConfigController.ts | 67 +- .../wrangler/src/api/startDevWorker/DevEnv.ts | 2 + .../startDevWorker/LocalRuntimeController.ts | 9 + .../MultiworkerRuntimeController.ts | 23 +- .../startDevWorker/RemoteRuntimeController.ts | 6 +- .../wrangler/src/api/startDevWorker/types.ts | 10 +- packages/wrangler/src/api/test-harness.ts | 626 ++++++++++++ packages/wrangler/src/cli.ts | 8 + packages/wrangler/src/dev.ts | 12 +- packages/wrangler/src/dev/miniflare/index.ts | 5 + packages/wrangler/src/dev/use-esbuild.ts | 10 +- 16 files changed, 1752 insertions(+), 44 deletions(-) create mode 100644 .changeset/gentle-cougars-build.md create mode 100644 packages/wrangler/e2e/createTestHarness.test.ts create mode 100644 packages/wrangler/src/api/test-harness.ts diff --git a/.changeset/gentle-cougars-build.md b/.changeset/gentle-cougars-build.md new file mode 100644 index 0000000000..230af2d0b7 --- /dev/null +++ b/.changeset/gentle-cougars-build.md @@ -0,0 +1,30 @@ +--- +"wrangler": minor +--- + +Introduce `createTestHarness()` for integration testing Workers + +It runs Workers in a local preview environment using production build output and works with both Wrangler projects and Workers built by the Cloudflare Vite plugin. + +Use it from any Node.js test runner to send requests to individual Workers, trigger scheduled events, reset the server between tests, and mock outbound requests with libraries that intercept `globalThis.fetch()`, such as MSW: + +```ts +import { createTestHarness } from "wrangler"; + +const server = createTestHarness({ + workers: [ + { configPath: "./dist/web_worker/wrangler.json" }, + { configPath: "./dist/api_worker/wrangler.json" }, + ], +}); + +await server.listen(); +await server.fetch("http://example.com"); + +const apiWorker = server.getWorker("api-worker"); +await apiWorker.fetch("http://example.com/users/123"); +await apiWorker.scheduled({ cron: "0 0 * * *" }); + +await server.reset(); +await server.close(); +``` diff --git a/packages/wrangler/e2e/createTestHarness.test.ts b/packages/wrangler/e2e/createTestHarness.test.ts new file mode 100644 index 0000000000..1ae8f9d93e --- /dev/null +++ b/packages/wrangler/e2e/createTestHarness.test.ts @@ -0,0 +1,964 @@ +import path from "node:path"; +import { setTimeout } from "node:timers/promises"; +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import dedent from "ts-dedent"; +import { beforeEach, describe, it, onTestFinished } from "vitest"; +import { + importWrangler, + WranglerE2ETestHelper, +} from "./helpers/e2e-wrangler-test"; + +const { createTestHarness } = await importWrangler(); + +describe("createTestHarness", { sequential: true }, () => { + let helper: WranglerE2ETestHelper; + + beforeEach(() => { + helper = new WranglerE2ETestHelper(); + }); + + it("starts with default server options", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "hello-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/index.ts": dedent` + export default { + fetch(request) { + if (new URL(request.url).pathname === "/url") { + return new Response(request.url); + } + return new Response("Hello World"); + } + }; + `, + }); + + const server = createTestHarness({ + workers: [ + { configPath: path.resolve(helper.tmpPath, "./wrangler.jsonc") }, + ], + }); + onTestFinished(server.close); + + const { url } = await server.listen(); + + expect(url.protocol).toBe("http:"); + expect(url.hostname).toBe("127.0.0.1"); + expect(Number(url.port)).toBeGreaterThan(0); + + const response = await fetch(url); + await expect(response.text()).resolves.toBe("Hello World"); + + const relativeServerResponse = await server.fetch("/url"); + await expect(relativeServerResponse.text()).resolves.toBe( + new URL("/url", url).href + ); + + const relativeWorkerResponse = await server.getWorker().fetch("/url"); + await expect(relativeWorkerResponse.text()).resolves.toBe( + new URL("/url", url).href + ); + }); + + it("support fetching different workers from the same session", async ({ + expect, + }) => { + await helper.seed({ + "wrangler.primary.jsonc": dedent` + { + "name": "primary-worker", + "main": "src/primary.ts", + "compatibility_date": "2026-05-20" + } + `, + "wrangler.auxiliary.jsonc": dedent` + { + "name": "auxiliary-worker", + "main": "src/auxiliary.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/primary.ts": dedent` + export default { + fetch() { + return new Response("Hello from Primary Worker"); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + fetch() { + return new Response("Hello from Auxiliary Worker"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { configPath: "./wrangler.primary.jsonc" }, + { configPath: "./wrangler.auxiliary.jsonc" }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + const defaultServerResponse = await server.fetch("/"); + await expect(defaultServerResponse.text()).resolves.toBe( + "Hello from Primary Worker" + ); + + const defaultWorkerResponse = await server.getWorker().fetch("/"); + await expect(defaultWorkerResponse.text()).resolves.toBe( + "Hello from Primary Worker" + ); + + const primaryResponse = await server.getWorker("primary-worker").fetch("/"); + await expect(primaryResponse.text()).resolves.toBe( + "Hello from Primary Worker" + ); + + const auxiliaryResponse = await server + .getWorker("auxiliary-worker") + .fetch("/"); + await expect(auxiliaryResponse.text()).resolves.toBe( + "Hello from Auxiliary Worker" + ); + }); + + it("supports service bindings between workers", async ({ expect }) => { + await helper.seed({ + "wrangler.primary.jsonc": dedent` + { + "name": "primary-worker", + "main": "src/primary.ts", + "compatibility_date": "2026-05-20", + "services": [ + { "binding": "AUXILIARY", "service": "auxiliary-worker" } + ] + } + `, + "wrangler.auxiliary.jsonc": dedent` + { + "name": "auxiliary-worker", + "main": "src/auxiliary.ts", + "compatibility_date": "2026-05-20", + "services": [ + { "binding": "PRIMARY", "service": "primary-worker" } + ] + } + `, + "src/primary.ts": dedent` + export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.pathname === "/call-auxiliary") { + return env.AUXILIARY.fetch("http://auxiliary.example.com/from-primary"); + } + + return new Response("primary:" + url.pathname); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.pathname === "/call-primary") { + return env.PRIMARY.fetch("http://primary.example.com/from-auxiliary"); + } + + return new Response("auxiliary:" + url.pathname); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { configPath: "./wrangler.primary.jsonc" }, + { configPath: "./wrangler.auxiliary.jsonc" }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + const auxiliaryResponse = await server + .getWorker("primary-worker") + .fetch("/call-auxiliary"); + await expect(auxiliaryResponse.text()).resolves.toBe( + "auxiliary:/from-primary" + ); + + const primaryResponse = await server + .getWorker("auxiliary-worker") + .fetch("/call-primary"); + await expect(primaryResponse.text()).resolves.toBe( + "primary:/from-auxiliary" + ); + }); + + it("routes fetches based on worker routes", async ({ expect }) => { + await helper.seed({ + "wrangler.primary.jsonc": dedent` + { + "name": "primary-worker", + "main": "src/primary.ts", + "compatibility_date": "2026-05-20", + "routes": ["primary.example.com/*"] + } + `, + "src/primary.ts": dedent` + export default { + fetch(request) { + return Response.json({ name: "primary", url: request.url }); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + fetch(request) { + return Response.json({ name: "auxiliary", url: request.url }); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { configPath: "./wrangler.primary.jsonc" }, + { + config: { + name: "auxiliary-worker", + main: "src/auxiliary.ts", + compatibility_date: "2026-05-20", + routes: ["auxiliary.example.com/*"], + }, + }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + const primaryResponse = await server.fetch( + "http://primary.example.com/path?value=1" + ); + await expect(primaryResponse.json()).resolves.toEqual({ + name: "primary", + url: "http://primary.example.com/path?value=1", + }); + + const auxiliaryResponse = await server.fetch( + "http://auxiliary.example.com/path?value=2" + ); + await expect(auxiliaryResponse.json()).resolves.toEqual({ + name: "auxiliary", + url: "http://auxiliary.example.com/path?value=2", + }); + + await server.update({ + root: helper.tmpPath, + workers: [ + { configPath: "./wrangler.primary.jsonc" }, + { + config: { + name: "auxiliary-worker", + main: "src/auxiliary.ts", + compatibility_date: "2026-05-20", + routes: ["updated-auxiliary.example.com/*"], + }, + }, + ], + }); + + const updatedAuxiliaryResponse = await server.fetch( + "http://updated-auxiliary.example.com/path?value=3" + ); + await expect(updatedAuxiliaryResponse.json()).resolves.toEqual({ + name: "auxiliary", + url: "http://updated-auxiliary.example.com/path?value=3", + }); + + await server.reset(); + + const resetAuxiliaryResponse = await server.fetch( + "http://auxiliary.example.com/path?value=4" + ); + await expect(resetAuxiliaryResponse.json()).resolves.toEqual({ + name: "auxiliary", + url: "http://auxiliary.example.com/path?value=4", + }); + }); + + it("rejects updates that change the number of workers without committing them", async ({ + expect, + }) => { + await helper.seed({ + "src/primary.ts": dedent` + export default { + fetch() { + return new Response("primary"); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + fetch() { + return new Response("auxiliary"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { + config: { + name: "primary-worker", + main: "src/primary.ts", + compatibility_date: "2026-05-20", + }, + }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + await expect( + server.update((options) => ({ + ...options, + workers: [ + ...options.workers, + { + config: { + name: "auxiliary-worker", + main: "src/auxiliary.ts", + compatibility_date: "2026-05-20", + }, + }, + ], + })) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Updating the number of workers running in the server is not supported.]` + ); + + let workerCountAfterRejectedUpdate = 0; + await server.update((options) => { + workerCountAfterRejectedUpdate = options.workers.length; + return options; + }); + + expect(workerCountAfterRejectedUpdate).toBe(1); + }); + + it("rejects update when a worker fails to build", async ({ expect }) => { + await helper.seed({ + "wrangler.primary.jsonc": dedent` + { + "name": "primary-worker", + "main": "src/primary.ts", + "compatibility_date": "2026-05-20" + } + `, + "wrangler.auxiliary.jsonc": dedent` + { + "name": "auxiliary-worker", + "main": "src/auxiliary.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/primary.ts": dedent` + export default { + fetch() { + return new Response("Hello from Primary Worker"); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + fetch() { + return new Response("Hello from Auxiliary Worker"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { configPath: "./wrangler.primary.jsonc" }, + { configPath: "./wrangler.auxiliary.jsonc" }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + // Either of these changes should cause a build failure + await helper.seed({ + "src/primary.ts": dedent` + export default { + fetch() { + return new Response("broken); + } + }; + `, + "src/auxiliary.ts": dedent` + export default { + fetch() { + return new Response("broken too"; + } + }; + `, + }); + + await expect( + Promise.race([ + server.update((options) => options), + setTimeout(5_000).then(() => { + throw new Error("server.update() timed out"); + }), + ]) + ).rejects.toThrow("Build failed"); + }); + + it("rejects calls on unknown worker handles", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "primary-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/index.ts": dedent` + export default { + fetch() { + return new Response("primary"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + await expect( + server.getWorker("unknown-worker").fetch("/") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: Worker "unknown-worker" does not exist in this server.]` + ); + + await expect( + server.getWorker("unknown-worker").scheduled({ + cron: "0 0 * * *", + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: Worker "unknown-worker" does not exist in this server.]` + ); + }); + + it("uses the current Node process fetch for outbound requests by default", async ({ + expect, + }) => { + const mockServer = setupServer( + http.get("http://example.com/", () => { + return HttpResponse.text("Mocked by MSW"); + }) + ); + mockServer.listen({ onUnhandledRequest: "error" }); + onTestFinished(() => mockServer.close()); + + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "default-outbound-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/index.ts": dedent` + export default { + fetch() { + return fetch("http://example.com"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.text()).resolves.toBe("Mocked by MSW"); + }); + + it("starts workers from inline config", async ({ expect }) => { + await helper.seed({ + "src/index.ts": dedent` + export default { + fetch() { + return new Response("Hello from inline config"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { + config: { + main: "src/index.ts", + compatibility_date: "2026-05-20", + }, + }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.text()).resolves.toBe("Hello from inline config"); + }); + + it("loads default .env files for config path workers", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "env-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "vars": { "CONFIG_VAR": "from-config" } + } + `, + ".env": dedent` + ENV_SECRET=from-env + `, + "src/index.ts": dedent` + export default { + fetch(request, env) { + return Response.json({ + CONFIG_VAR: env.CONFIG_VAR, + ENV_SECRET: env.ENV_SECRET, + }); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.json()).resolves.toEqual({ + CONFIG_VAR: "from-config", + ENV_SECRET: "from-env", + }); + }); + + it("loads default .dev.vars files for config path workers", async ({ + expect, + }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "dev-vars-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "vars": { "CONFIG_VAR": "from-config" } + } + `, + ".env": dedent` + SECRET=from-env + `, + ".dev.vars": dedent` + SECRET=from-dev-vars + `, + "src/index.ts": dedent` + export default { + fetch(request, env) { + return Response.json({ + CONFIG_VAR: env.CONFIG_VAR, + SECRET: env.SECRET, + }); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.json()).resolves.toEqual({ + CONFIG_VAR: "from-config", + SECRET: "from-dev-vars", + }); + }); + + it("overrides vars and secrets for config path workers", async ({ + expect, + }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "var-overrides-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "vars": { "CONFIG_VAR": "from-config" }, + "secrets": { "required": ["API_TOKEN", "SECRET_FROM_FILE"] } + } + `, + ".dev.vars": dedent` + API_TOKEN=from-dev-vars + SECRET_FROM_FILE=from-dev-vars + `, + "src/index.ts": dedent` + export default { + fetch(request, env) { + return Response.json({ + CONFIG_VAR: env.CONFIG_VAR, + API_TOKEN: env.API_TOKEN, + SECRET_FROM_FILE: env.SECRET_FROM_FILE, + ADDED_VAR: env.ADDED_VAR, + NULL_VAR: env.NULL_VAR, + }); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [ + { + configPath: "./wrangler.jsonc", + vars: { + CONFIG_VAR: "from-override", + ADDED_VAR: "from-override", + NULL_VAR: null, + }, + secrets: { + API_TOKEN: "from-override", + }, + }, + ], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.json()).resolves.toEqual({ + CONFIG_VAR: "from-override", + API_TOKEN: "from-override", + SECRET_FROM_FILE: "from-dev-vars", + ADDED_VAR: "from-override", + NULL_VAR: null, + }); + + await server.update({ + root: helper.tmpPath, + workers: [ + { + configPath: "./wrangler.jsonc", + vars: { + CONFIG_VAR: "from-updated-override", + ADDED_VAR: "from-updated-override", + NULL_VAR: null, + }, + secrets: { + API_TOKEN: "from-updated-override", + }, + }, + ], + }); + + const updatedResponse = await server.fetch("/"); + await expect(updatedResponse.json()).resolves.toEqual({ + CONFIG_VAR: "from-updated-override", + API_TOKEN: "from-updated-override", + SECRET_FROM_FILE: "from-dev-vars", + ADDED_VAR: "from-updated-override", + NULL_VAR: null, + }); + }); + + it(`supports "nodejs_compat" flag`, async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "nodejs-compat-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "compatibility_flags": ["nodejs_compat"] + } + `, + "src/index.ts": dedent` + import { Stream } from "node:stream"; + + export default { + fetch() { + return new Response(String(typeof Stream)); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const response = await server.fetch("/"); + await expect(response.text()).resolves.toBe("function"); + }); + + it("uses ephemeral storage by default", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "ephemeral-storage-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "kv_namespaces": [ + { "binding": "STORE", "id": "test-store" } + ] + } + `, + "src/index.ts": dedent` + export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.pathname === "/set") { + await env.STORE.put("key", "value"); + return new Response("stored"); + } + return new Response((await env.STORE.get("key")) ?? "missing"); + } + }; + `, + }); + + const firstServer = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(firstServer.close); + + await firstServer.listen(); + + const setResponse = await firstServer.fetch("/set"); + await expect(setResponse.text()).resolves.toBe("stored"); + + const storedResponse = await firstServer.fetch("/"); + await expect(storedResponse.text()).resolves.toBe("value"); + + await firstServer.close(); + + const secondServer = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(secondServer.close); + + await secondServer.listen(); + + const resetResponse = await secondServer.fetch("/"); + await expect(resetResponse.text()).resolves.toBe("missing"); + }); + + it("resets server options and restarts the session", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "storage-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20", + "kv_namespaces": [ + { "binding": "STORE", "id": "test-store" } + ] + } + `, + "src/index.ts": dedent` + export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.pathname === "/set") { + await env.STORE.put("key", "value"); + return new Response("stored"); + } + return new Response((await env.STORE.get("key")) ?? "missing"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await expect(server.reset()).rejects.toThrow( + "Server has not been started. Start it with server.listen() before calling this method." + ); + + await server.listen(); + + const setResponse = await server.fetch("/set"); + await expect(setResponse.text()).resolves.toBe("stored"); + const storedResponse = await server.fetch("/"); + await expect(storedResponse.text()).resolves.toBe("value"); + + await server.reset(); + + const resetResponse = await server.fetch("/"); + await expect(resetResponse.text()).resolves.toBe("missing"); + }); + + it("triggers scheduled handlers", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "scheduled-worker", + "main": "src/index.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/index.ts": dedent` + let lastCron = "missing"; + + export default { + fetch() { + return new Response(lastCron); + }, + scheduled(event) { + lastCron = event.cron; + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const beforeScheduled = await server.fetch("/"); + await expect(beforeScheduled.text()).resolves.toBe("missing"); + + await expect( + server.getWorker().scheduled({ + cron: "* * * * *", + scheduledTime: new Date(1_700_000_100_000), + }) + ).resolves.toEqual({ outcome: "ok", noRetry: false }); + + const afterScheduled = await server.fetch("/"); + await expect(afterScheduled.text()).resolves.toBe("* * * * *"); + }); + + it("does not reload on source changes by default", async ({ expect }) => { + await helper.seed({ + "wrangler.jsonc": dedent` + { + "name": "create-server-test", + "main": "src/index.ts", + "compatibility_date": "2026-05-20" + } + `, + "src/index.ts": dedent` + export default { + fetch() { + return new Response("Hello World"); + } + }; + `, + }); + + const server = createTestHarness({ + root: helper.tmpPath, + workers: [{ configPath: "./wrangler.jsonc" }], + }); + onTestFinished(server.close); + + await server.listen(); + + const response1 = await server.fetch("/"); + await expect(response1.text()).resolves.toBe("Hello World"); + + await helper.seed({ + "src/index.ts": dedent` + export default { + fetch() { + return new Response("Greeting"); + } + }; + `, + }); + + // Wait a moment to ensure that if the server were going to reload, it would have done so by now + await setTimeout(1000); + + const response2 = await server.fetch("/"); + await expect(response2.text()).resolves.toBe("Hello World"); + + await helper.seed({ + "src/index.ts": dedent` + export default { + fetch() { + return new Response("Bonjour"); + } + }; + `, + }); + + await setTimeout(1000); + + const response3 = await server.fetch("/"); + await expect(response3.text()).resolves.toBe("Hello World"); + }); +}); diff --git a/packages/wrangler/src/api/index.ts b/packages/wrangler/src/api/index.ts index 20e0f1de71..7a306af033 100644 --- a/packages/wrangler/src/api/index.ts +++ b/packages/wrangler/src/api/index.ts @@ -58,6 +58,14 @@ export type { } from "./startDevWorker/events"; export type { DevToolsEvent } from "./startDevWorker/devtools"; +// Exports from ./server +export { createTestHarness } from "./test-harness"; +export type { + TestHarnessOptions, + WorkerHandle, + TestHarness, +} from "./test-harness"; + // Exports from ./integrations export { unstable_getVarsForDev, diff --git a/packages/wrangler/src/api/startDevWorker/BaseController.ts b/packages/wrangler/src/api/startDevWorker/BaseController.ts index 027f5e483d..a0fbae6b63 100644 --- a/packages/wrangler/src/api/startDevWorker/BaseController.ts +++ b/packages/wrangler/src/api/startDevWorker/BaseController.ts @@ -9,6 +9,7 @@ import type { ReloadCompleteEvent, ReloadStartEvent, } from "./events"; +import type { Miniflare } from "miniflare"; export type ControllerEvent = | ErrorEvent @@ -57,6 +58,11 @@ export abstract class RuntimeController extends Controller { abstract onBundleComplete(_: BundleCompleteEvent): void; abstract onPreviewTokenExpired(_: PreviewTokenExpiredEvent): void; + // ********************* + // Runtime Accessors + // ********************* + abstract get mf(): Miniflare | undefined; + // ********************* // Event Dispatchers // ********************* diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index 1e1cec8647..9256d8a872 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -184,6 +184,7 @@ export class BundlerController extends Controller { async #startCustomBuild(config: StartDevWorkerOptions) { await this.#customBuildWatcher?.close(); + this.#customBuildWatcher = undefined; this.#customBuildAborter?.abort(); if (!config.build?.custom?.command) { @@ -195,6 +196,11 @@ export class BundlerController extends Controller { // This is always present if a custom command is provided, defaulting to `./src` assert(pathsToWatch, "config.build.custom.watch"); + if (config.dev.watch === false) { + await this.#runCustomBuild(config, String(pathsToWatch)); + return; + } + this.#customBuildWatcher = watch(pathsToWatch, { persistent: true, // The initial custom build is always done in getEntry() @@ -277,6 +283,7 @@ export class BundlerController extends Controller { config.compatibilityDate, config.compatibilityFlags ), + watch: config.dev.watch ?? true, defineNavigatorUserAgent: isNavigatorDefined( config.compatibilityDate, config.compatibilityFlags @@ -307,6 +314,7 @@ export class BundlerController extends Controller { #assetsWatcher?: ReturnType; async #ensureWatchingAssets(config: StartDevWorkerOptions) { await this.#assetsWatcher?.close(); + this.#assetsWatcher = undefined; const debouncedRefreshBundle = debounce(() => { if (this.#currentBundle) { @@ -314,7 +322,7 @@ export class BundlerController extends Controller { } }); - if (config.assets?.directory) { + if (config.dev.watch !== false && config.assets?.directory) { const assetsDir = config.assets.directory; const watcher = watch(assetsDir, { persistent: true, diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 4383bfcb16..28371ac7c3 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -149,14 +149,21 @@ async function resolveDevConfig( origin: { secure: input.dev?.origin?.secure ?? config.dev.upstream_protocol === "https", - hostname: host ?? getInferredHost(routes, config.configPath), + hostname: + host ?? + ((input.dev?.inferOriginFromRoutes ?? true) + ? getInferredHost(routes, config.configPath) + : undefined), }, + watch: input.dev?.watch, liveReload: input.dev?.liveReload || false, testScheduled: input.dev?.testScheduled, + outboundService: input.dev?.outboundService, // absolute resolved path persist: localPersistencePath, registry: input.dev?.registry, multiworkerPrimary: input.dev?.multiworkerPrimary, + inferOriginFromRoutes: input.dev?.inferOriginFromRoutes ?? true, enableContainers: input.dev?.enableContainers ?? config.dev.enable_containers, dockerPath: input.dev?.dockerPath ?? getDockerPath(), @@ -565,33 +572,39 @@ export class ConfigController extends Controller { const signal = this.#abortController.signal; this.latestInput = input; try { - const fileConfig = readConfig( - { - script: input.entrypoint, - config: input.config, - env: input.env, - "dispatch-namespace": undefined, - "legacy-env": !input.legacy?.useServiceEnvironments, - remote: !!input.dev?.remote, - upstreamProtocol: - input.dev?.origin?.secure === undefined - ? undefined - : input.dev?.origin?.secure - ? "https" - : "http", - localProtocol: - input.dev?.server?.secure === undefined - ? undefined - : input.dev?.server?.secure - ? "https" - : "http", - generateTypes: input.dev?.generateTypes, - }, - { useRedirectIfAvailable: true } - ); - - if (!getDisableConfigWatching()) { + const fileConfig = + typeof input.config === "object" + ? input.config + : readConfig( + { + script: input.entrypoint, + config: input.config, + env: input.env, + "dispatch-namespace": undefined, + "legacy-env": !input.legacy?.useServiceEnvironments, + remote: !!input.dev?.remote, + upstreamProtocol: + input.dev?.origin?.secure === undefined + ? undefined + : input.dev?.origin?.secure + ? "https" + : "http", + localProtocol: + input.dev?.server?.secure === undefined + ? undefined + : input.dev?.server?.secure + ? "https" + : "http", + generateTypes: input.dev?.generateTypes, + }, + { useRedirectIfAvailable: true } + ); + + if (!getDisableConfigWatching() && input.dev?.watch !== false) { await this.#ensureWatchingConfig(fileConfig.configPath); + } else { + await this.#configWatcher?.close(); + this.#configWatcher = undefined; } const { config: resolvedConfig, printCurrentBindings } = diff --git a/packages/wrangler/src/api/startDevWorker/DevEnv.ts b/packages/wrangler/src/api/startDevWorker/DevEnv.ts index 8f5bd60aeb..2757c816e9 100644 --- a/packages/wrangler/src/api/startDevWorker/DevEnv.ts +++ b/packages/wrangler/src/api/startDevWorker/DevEnv.ts @@ -169,6 +169,7 @@ export class DevEnv extends EventEmitter implements ControllerBus { event.cause instanceof ParseError ) { logger.error(event.cause); + this.emit("buildFailed", event); } // Build errors are recoverable by fixing the code and saving else if (event.source === "BundlerController") { @@ -179,6 +180,7 @@ export class DevEnv extends EventEmitter implements ControllerBus { } else { logger.error(event.cause.message); } + this.emit("buildFailed", event); } // if other knowable + recoverable errors occur, handle them here else { diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 1420341e9b..f1d4bcb0d6 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -112,10 +112,13 @@ export async function convertToConfigBundle( const bindings: Record = { ...event.config.bindings }; const crons = []; + const routes = []; const queueConsumers = []; for (const trigger of event.config.triggers ?? []) { if (trigger.type === "cron") { crons.push(trigger.cron); + } else if (trigger.type === "route") { + routes.push(trigger.pattern); } else if (trigger.type === "queue-consumer") { const { type: _, ...consumer } = trigger; queueConsumers.push(consumer); @@ -191,7 +194,9 @@ export async function convertToConfigBundle( localPersistencePath: event.config.dev.persist, liveReload: event.config.dev?.liveReload ?? false, crons, + routes, queueConsumers, + outboundService: event.config.dev.outboundService, localProtocol: event.config.dev?.server?.secure ? "https" : "http", httpsCertPath: event.config.dev?.server?.httpsCertPath, httpsKeyPath: event.config.dev?.server?.httpsKeyPath, @@ -237,6 +242,10 @@ export class LocalRuntimeController extends RuntimeController { #mutex = new Mutex(); #mf?: Miniflare; + override get mf(): Miniflare | undefined { + return this.#mf; + } + #remoteProxySessionData: { session: RemoteProxySession; remoteBindings: Record; diff --git a/packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts index b42fa348a4..f7a9fdf354 100644 --- a/packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts @@ -79,6 +79,10 @@ export class MultiworkerRuntimeController extends LocalRuntimeController { #mutex = new Mutex(); #mf?: Miniflare; + override get mf(): Miniflare | undefined { + return this.#mf; + } + #options = new Map(); #remoteProxySessionsData = new Map< @@ -271,7 +275,10 @@ export class MultiworkerRuntimeController extends LocalRuntimeController { // `inspectorUrl` for this set of `options`, we protect `#mf` with a mutex, // so only one update can happen at a time. const userWorkerUrl = await this.#mf.ready; - const userWorkerInspectorUrl = await this.#mf.getInspectorURL(); + const userWorkerInspectorUrl = + data.config.dev.inspector !== false + ? await this.#mf.getInspectorURL() + : null; // If we received a new `bundleComplete` event for *any* worker // before we were able to dispatch a `reloadComplete`, ignore // this bundle — the later handler will apply the full config. @@ -289,12 +296,14 @@ export class MultiworkerRuntimeController extends LocalRuntimeController { hostname: userWorkerUrl.hostname, port: userWorkerUrl.port, }, - userWorkerInspectorUrl: { - protocol: userWorkerInspectorUrl.protocol, - hostname: userWorkerInspectorUrl.hostname, - port: userWorkerInspectorUrl.port, - pathname: `/core:user:${data.config.name}`, - }, + userWorkerInspectorUrl: userWorkerInspectorUrl + ? { + protocol: userWorkerInspectorUrl.protocol, + hostname: userWorkerInspectorUrl.hostname, + port: userWorkerInspectorUrl.port, + pathname: `/core:user:${data.config.name}`, + } + : undefined, userWorkerInnerUrlOverrides: getUserWorkerInnerUrlOverrides( data.config ), diff --git a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts index cf4fca50df..cdfc1dfd3d 100644 --- a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { MissingConfigError } from "@cloudflare/workers-utils"; import chalk from "chalk"; -import { Mutex } from "miniflare"; +import { Mutex, type Miniflare } from "miniflare"; import { WebSocket } from "ws"; import { version as packageVersion } from "../../../package.json"; import { @@ -488,6 +488,10 @@ export class RemoteRuntimeController extends RuntimeController { void this.#mutex.runWith(() => this.#refreshPreviewToken()); } + override get mf(): Miniflare | undefined { + return undefined; + } + override async teardown() { await super.teardown(); if (this.#session) { diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index ae17bcdf03..d3a4725b28 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -46,8 +46,8 @@ export interface StartDevWorkerInput { * This is the `main` property of a Wrangler configuration file. */ entrypoint?: string; - /** The configuration path of the worker. */ - config?: string; + /** The configuration path of the worker, or a normalized configuration object. */ + config?: string | Config; /** The compatibility date for the workerd runtime. */ compatibilityDate?: string; @@ -173,6 +173,8 @@ export interface StartDevWorkerInput { /** Treat this as the primary worker in a multiworker setup (i.e. the first Worker in Miniflare's options) */ multiworkerPrimary?: boolean; + /** Whether to infer the local request origin from configured routes. */ + inferOriginFromRoutes?: boolean; containerBuildId?: string; /** Whether to build and connect to containers during local dev. Requires Docker daemon to be running. Defaults to true. */ @@ -208,8 +210,10 @@ export interface StartDevWorkerInput { export type StartDevWorkerOptions = Omit< StartDevWorkerInput, - "assets" | "containers" | "dev" + "assets" | "config" | "containers" | "dev" > & { + /** The configuration path of the worker */ + config?: string; /** A worker's directory. Usually where the Wrangler configuration file is located */ projectRoot: string; build: StartDevWorkerInput["build"] & { diff --git a/packages/wrangler/src/api/test-harness.ts b/packages/wrangler/src/api/test-harness.ts new file mode 100644 index 0000000000..4604b22dab --- /dev/null +++ b/packages/wrangler/src/api/test-harness.ts @@ -0,0 +1,626 @@ +import assert from "node:assert"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + normalizeAndValidateConfig, + UserError, +} from "@cloudflare/workers-utils"; +import { Headers, Request } from "miniflare"; +import { validateNodeCompatMode } from "../deployment-bundle/node-compat"; +import { logger } from "../logger"; +import { requireApiToken, requireAuth } from "../user"; +import { DevEnv } from "./startDevWorker/DevEnv"; +import { MultiworkerRuntimeController } from "./startDevWorker/MultiworkerRuntimeController"; +import { NoOpProxyController } from "./startDevWorker/NoOpProxyController"; +import { convertConfigToBindings } from "./startDevWorker/utils"; +import type { CfAccount } from "../dev/create-worker-preview"; +import type { ErrorEvent } from "./startDevWorker/events"; +import type { StartDevWorkerInput } from "./startDevWorker/types"; +import type { + FetcherScheduledOptions, + FetcherScheduledResult, +} from "@cloudflare/workers-types/experimental"; +import type { Config, RawConfig } from "@cloudflare/workers-utils"; +import type { DispatchFetch, Json, Miniflare, RequestInfo } from "miniflare"; + +export type TestHarnessOptions = { + /** + * Base directory used to resolve relative worker config paths. + * Defaults to `process.cwd()`. + */ + root?: string | undefined; + /** + * Workers to run in this server. The first worker is the primary worker. + */ + workers: WorkerInput[]; +}; + +export type WorkerHandle = { + /** + * Dispatches a fetch event directly to this worker. + * Relative URL inputs are resolved against the URL returned by `listen()`. + * + * @example + * ```ts + * const response = await worker.fetch("/", { + * method: "POST", + * body: "Hello, world!" + * }); + * ``` + */ + fetch: DispatchFetch; + /** + * Dispatches a scheduled event directly to this Worker. + * + * @example + * ```ts + * const result = await worker.scheduled({ + * cron: "0 * * * *", + * scheduledTime: new Date(), + * }); + * ``` + */ + scheduled(options: FetcherScheduledOptions): Promise; +}; + +export type TestHarness = { + /** + * Starts the server and returns its current URL. + * Calling this more than once returns the same running server session until + * the server is closed or reset. + */ + listen(): Promise<{ + url: URL; + }>; + /** + * Dispatches a fetch request through the server. + * + * - Relative URLs are resolved against the current server URL. Absolute URLs + * are also accepted, and can be used to control the hostname seen by the Worker. + * - Requests are matched against each Worker's configured routes and dispatched to + * the first matching Worker, or to the primary Worker if no routes match. + * - To dispatch directly to a specific Worker, use `server.getWorker(name).fetch()`. + * + * @example + * ```ts + * const server = createTestHarness({ + * workers: [ + * { configPath: "./wrangler.dashboard.jsonc" }, // No route pattern + * { configPath: "./wrangler.api.jsonc" }, // Route pattern: "example.com/api/*" + * { configPath: "./wrangler.admin.jsonc" }, // Route pattern: "admin.example.com/*" + * ] + * }); + * + * await server.fetch("/users"); + * // Dispatches a request to the dashboard Worker (the first Worker) with URL "http://localhost:{port}/users" + * + * await server.fetch("http://admin.example.com/accounts"); + * // Dispatches a request to the admin Worker with URL "http://admin.example.com/accounts" + * + * await server.fetch("http://example.com/api/data"); + * // Dispatches a request to the API Worker with URL "http://example.com/api/data" + * ``` + */ + fetch: DispatchFetch; + /** + * Returns a handle for dispatching events directly to a Worker. + * When no name is provided, this returns the primary Worker, which is the first + * Worker in the server's `workers` options. + */ + getWorker(name?: string): WorkerHandle; + /** + * Updates the server configuration and reloads the running Workers. + */ + update( + options: + | TestHarnessOptions + | ((currentOptions: TestHarnessOptions) => TestHarnessOptions) + ): Promise; + /** + * Restores the server to its initial `createTestHarness()` options and restarts the + * active server session. Storage is recreated, and the server URL may change + * after reset. + */ + reset(): Promise; + /** + * Stops the server and releases all runtime resources. + */ + close(): Promise; +}; + +type InlineConfig = Omit; + +type WorkerInput = + | { + /** + * Path to a Wrangler config file for this Worker. + * Relative paths resolve from server `root`. + */ + configPath: string | URL; + /** + * Wrangler environment to load from the config file. + */ + env?: string; + /** + * Test-only vars that override vars from the Wrangler config. + */ + vars?: Record; + /** + * Test-only secrets that override values loaded from `.dev.vars` and `.env` files. + */ + secrets?: Record; + } + | { + /** + * Inline Wrangler config for this Worker. + */ + config: InlineConfig; + }; + +type ServerSession = { + primaryDevEnv: DevEnv; + devEnvs: DevEnv[]; +}; + +/** + * Creates a local test server for running Workers. + * + * The server can run one or more Workers from Wrangler config files, including + * generated configs from Vite, or from inline configuration objects. + * + * @example + * ```ts + * const server = createTestHarness({ + * workers: [{ configPath: "./wrangler.jsonc" }], + * }); + * await server.listen(); + * const response = await server.fetch("/api/users"); + * await server.close(); + * ``` + */ +export function createTestHarness(options: TestHarnessOptions): TestHarness { + const initialOptions = options; + let currentOptions = options; + let serverSession: ServerSession | undefined; + let startPromise: Promise | undefined; + + function resolvePath(basePath: string, maybePath: string | URL): string { + if (maybePath instanceof URL) { + return fileURLToPath(maybePath); + } + + return path.isAbsolute(maybePath) + ? maybePath + : path.resolve(basePath, maybePath); + } + + function normalizeInlineWorkerConfig( + config: InlineConfig, + root: string + ): Config { + const configPath = path.join(root, "wrangler.jsonc"); + const { config: normalizedConfig, diagnostics } = + normalizeAndValidateConfig(config, configPath, configPath, {}); + + if (diagnostics.hasWarnings()) { + logger.warn(diagnostics.renderWarnings()); + } + + if (diagnostics.hasErrors()) { + throw new UserError(diagnostics.renderErrors(), { + telemetryMessage: "create server inline config validation failed", + }); + } + + return normalizedConfig; + } + + function resolveWorkerInputs( + serverOptions: TestHarnessOptions + ): StartDevWorkerInput[] { + if (serverOptions.workers.length === 0) { + throw new Error("Test harness requires at least one worker."); + } + + const root = serverOptions.root ?? process.cwd(); + + return serverOptions.workers.map((input, index, list) => { + const isPrimaryWorker = index === 0; + const isMultiworker = list.length > 1; + const bindings = convertConfigToBindings( + { vars: "vars" in input ? input.vars : undefined }, + { usePreviewIds: true } + ); + const secrets = "secrets" in input ? input.secrets : undefined; + for (const [key, value] of Object.entries(secrets ?? {})) { + bindings[key] = { type: "secret_text", value }; + } + + return { + config: + "config" in input + ? normalizeInlineWorkerConfig(input.config, root) + : resolvePath(root, input.configPath), + env: "env" in input ? input.env : undefined, + bindings, + dev: { + auth: serverAuthHook, + server: { hostname: "127.0.0.1", port: 0 }, + logLevel: "error", + watch: false, + persist: false, + inspector: false, + registry: undefined, + outboundService: (request) => { + /** + * Miniflare passes its own undici-based Request here. Pass the URL as + * RequestInfo and the request as RequestInit so method, headers, body, + * and duplex are preserved by global fetch. + */ + return globalThis.fetch(request.url, request); + }, + multiworkerPrimary: isMultiworker ? isPrimaryWorker : undefined, + inferOriginFromRoutes: false, + }, + build: { + nodejsCompatMode: (config) => { + return validateNodeCompatMode( + config.compatibility_date, + config.compatibility_flags ?? [], + { noBundle: config.no_bundle } + ); + }, + }, + }; + }); + } + + async function createSession( + serverOptions: TestHarnessOptions + ): Promise { + const inputs = resolveWorkerInputs(serverOptions); + const [, ...auxiliaryWorkers] = inputs; + const isMultiworker = auxiliaryWorkers.length > 0; + const primaryDevEnv = isMultiworker + ? new DevEnv({ + runtimeFactories: [ + (devEnv) => new MultiworkerRuntimeController(devEnv, inputs.length), + ], + }) + : new DevEnv(); + const auxiliaryDevEnvs = auxiliaryWorkers.map( + () => + new DevEnv({ + runtimeFactories: [() => primaryDevEnv.runtimes[0]], + proxyFactory: (devEnv) => new NoOpProxyController(devEnv), + }) + ); + const session: ServerSession = { + primaryDevEnv, + devEnvs: [primaryDevEnv, ...auxiliaryDevEnvs], + }; + + try { + await updateConfig(session, inputs); + await waitForProxyReady(session); + return session; + } catch (error) { + await teardownSession(session); + throw error; + } + } + + async function updateConfig( + session: ServerSession, + inputs: StartDevWorkerInput[] + ) { + for (const [index, workerInput] of inputs.entries()) { + const devEnv = session.devEnvs[index]; + await devEnv.config.set(workerInput, true); + } + } + + async function resolveSession(workerName?: string) { + if (startPromise) { + const session = await startPromise; + assertWorkerExists(session, workerName); + return session; + } + + assert( + serverSession, + "Server has not been started. Start it with server.listen() before calling this method." + ); + + assertWorkerExists(serverSession, workerName); + + return serverSession; + } + + function assertWorkerExists( + session: ServerSession, + workerName: string | undefined + ) { + if (workerName === undefined) { + return; + } + + const workerExists = session.devEnvs.some((devEnv) => { + return devEnv.config.latestConfig?.name === workerName; + }); + + if (!workerExists) { + throw new TypeError( + `Worker ${JSON.stringify(workerName)} does not exist in this server.` + ); + } + } + + async function serverAuthHook( + config: Pick + ): Promise { + return { + accountId: await requireAuth(config), + apiToken: requireApiToken(), + }; + } + + async function teardownSession(session: ServerSession) { + try { + await Promise.all(session.devEnvs.map((devEnv) => devEnv.teardown())); + } finally { + if (session === serverSession) { + serverSession = undefined; + } + } + } + + async function startServerSession() { + if (!startPromise) { + startPromise = createSession(currentOptions) + .then((session) => { + serverSession = session; + return session; + }) + .finally(() => { + startPromise = undefined; + }); + } + + return await startPromise; + } + + async function waitForProxyReady(session: ServerSession) { + return new Promise< + Awaited + >((resolve, reject) => { + const cleanup = () => { + for (const devEnv of session.devEnvs) { + devEnv.off("error", onError); + devEnv.off("buildFailed", onBuildFailed); + } + }; + const onError = (error: unknown) => { + cleanup(); + reject(resolveErrorCause(error)); + }; + const onBuildFailed = (error: ErrorEvent) => { + cleanup(); + reject(resolveErrorCause(error)); + }; + + for (const devEnv of session.devEnvs) { + devEnv.once("error", onError); + devEnv.once("buildFailed", onBuildFailed); + } + + void session.primaryDevEnv.proxy.ready.promise.then( + (ready) => { + cleanup(); + resolve(ready); + }, + (error: unknown) => { + cleanup(); + reject(error); + } + ); + }); + } + + async function waitForReloadComplete(session: ServerSession) { + return new Promise((resolve, reject) => { + const cleanup = () => { + for (const devEnv of session.devEnvs) { + devEnv.off("error", onError); + devEnv.off("buildFailed", onBuildFailed); + } + + session.primaryDevEnv.off("reloadComplete", onReloadComplete); + }; + const onError = (error: unknown) => { + cleanup(); + reject(resolveErrorCause(error)); + }; + const onBuildFailed = (error: ErrorEvent) => { + cleanup(); + reject(resolveErrorCause(error)); + }; + const onReloadComplete = () => { + cleanup(); + resolve(); + }; + + for (const devEnv of session.devEnvs) { + devEnv.once("error", onError); + devEnv.once("buildFailed", onBuildFailed); + } + + session.primaryDevEnv.once("reloadComplete", onReloadComplete); + }); + } + + function resolveErrorCause(error: unknown) { + if (isErrorEvent(error)) { + return error.cause; + } + + return error; + } + + function isErrorEvent(error: unknown): error is ErrorEvent { + return ( + typeof error === "object" && + error !== null && + "type" in error && + error.type === "error" && + "cause" in error + ); + } + + async function getRuntimeMiniflare(session: ServerSession) { + await session.primaryDevEnv.proxy.runtimeMessageMutex.drained(); + const miniflare = session.primaryDevEnv.runtimes[0].mf; + assert(miniflare, "Worker runtime is not available."); + return miniflare; + } + + async function dispatchFetch( + miniflare: Miniflare, + input: RequestInfo, + init?: RequestInit, + worker?: string + ) { + if (typeof input === "string") { + const session = await resolveSession(); + const { url } = await waitForProxyReady(session); + const baseUrl = new URL(url); + + if ( + baseUrl.hostname === "0.0.0.0" || + baseUrl.hostname === "::" || + baseUrl.hostname === "[::]" || + baseUrl.hostname === "*" + ) { + baseUrl.hostname = "localhost"; + } + + input = new URL(input, baseUrl); + } + + if (worker === undefined) { + return miniflare.dispatchFetch(input, init); + } + + const request = new Request(input, init); + const headers = new Headers(request.headers); + + headers.set("MF-Route-Override", worker); + + return miniflare.dispatchFetch(request, { headers }); + } + + return { + async listen() { + const session = serverSession ?? (await startServerSession()); + const ready = await waitForProxyReady(session); + + return { + url: ready.url, + }; + }, + async fetch(input, init) { + const session = await resolveSession(); + const miniflare = session.primaryDevEnv.proxy.proxyWorker; + assert( + miniflare, + "The proxy worker is not available yet. Did you call server.listen()?" + ); + + return dispatchFetch(miniflare, input, init); + }, + getWorker(name?: string) { + return { + async fetch(input, init) { + const session = await resolveSession(name); + const miniflare = await getRuntimeMiniflare(session); + + return dispatchFetch(miniflare, input, init, name); + }, + async scheduled(scheduledOptions) { + const session = await resolveSession(name); + const miniflare = await getRuntimeMiniflare(session); + const url = new URL( + "http://localhost/cdn-cgi/handler/scheduled?format=json" + ); + + if (scheduledOptions?.cron !== undefined) { + url.searchParams.set("cron", scheduledOptions.cron); + } + + if (scheduledOptions?.scheduledTime !== undefined) { + url.searchParams.set( + "time", + String(scheduledOptions.scheduledTime.getTime()) + ); + } + + const response = await dispatchFetch(miniflare, url, undefined, name); + const result = await response.json(); + + return result as FetcherScheduledResult; + }, + }; + }, + async update(updateInput) { + const nextOptions = + typeof updateInput === "function" + ? updateInput(currentOptions) + : updateInput; + + // If listen() is still starting, wait until serverSession is available so the update is applied to the running Workers. + if (startPromise) { + await startPromise; + } + + if (serverSession) { + const nextInputs = resolveWorkerInputs(nextOptions); + + if (nextInputs.length !== serverSession.devEnvs.length) { + throw new Error( + `Updating the number of workers running in the server is not supported.` + ); + } + + try { + await Promise.all([ + waitForReloadComplete(serverSession), + updateConfig(serverSession, nextInputs), + ]); + } catch (error) { + await teardownSession(serverSession); + throw error; + } + } + + currentOptions = nextOptions; + }, + async reset() { + const session = await resolveSession(); + + await teardownSession(session); + currentOptions = initialOptions; + + await startServerSession(); + }, + async close() { + if (startPromise) { + // Wait for it to start before tearing down + // ignoring any errors since we're closing the server anyway + await startPromise.catch(() => undefined); + } + if (serverSession) { + await teardownSession(serverSession); + } + }, + }; +} diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index d7e98f1722..0dc8de91d7 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -15,6 +15,7 @@ import { startRemoteProxySession, startWorker, unstable_dev, + createTestHarness, experimental_generateTypes, unstable_getDevCompatibilityDate, unstable_getDurableObjectClassNameToUseSQLiteMap, @@ -40,6 +41,9 @@ import type { Unstable_MiniflareWorkerOptions, Unstable_RawConfig, Unstable_RawEnvironment, + TestHarnessOptions, + WorkerHandle, + TestHarness, } from "./api"; import type { Logger } from "./logger"; import type { Request, Response } from "miniflare"; @@ -67,6 +71,7 @@ export { unstable_pages, DevEnv as unstable_DevEnv, startWorker as unstable_startWorker, + createTestHarness, unstable_getVarsForDev, unstable_readConfig, experimental_generateTypes, @@ -89,6 +94,9 @@ export type { Unstable_MiniflareWorkerOptions, Experimental_GenerateTypesOptions, Experimental_GenerateTypesResult, + TestHarnessOptions, + WorkerHandle, + TestHarness, }; export { printBindings as unstable_printBindings } from "./utils/print-bindings"; diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index a76a53c03f..86c6f82ac1 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -521,6 +521,16 @@ export function getBindings( usePreviewIds: true, }); + // createTestHarness() can override secrets through inputBindings. + // This filters out those required secrets so the logic doesn't consider them missing + const secrets = configParam.secrets + ? { + ...configParam.secrets, + required: configParam.secrets?.required?.filter( + (secret) => inputBindings?.[secret]?.type !== "secret_text" + ), + } + : undefined; // Override vars with .dev.vars (dev-specific) // getVarsForDev returns typed bindings: config vars are plain_text/json, // while .dev.vars/.env vars are secret_text. @@ -531,7 +541,7 @@ export function getBindings( configParam.vars, env, false, - configParam.secrets + secrets ); for (const [name, binding] of Object.entries(vars)) { // Only override plain_text/json/secret_text vars, not other binding types like kv_namespace diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 5b00d3ca4a..53f845e887 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -37,6 +37,7 @@ import type { Config, ContainerEngine, LegacyAssetPaths, + ServiceFetch, } from "@cloudflare/workers-utils"; import type { DOContainerOptions, @@ -85,6 +86,7 @@ export interface ConfigBundle { localPersistencePath: string | false; liveReload: boolean; crons: Config["triggers"]["crons"]; + routes: string[] | undefined; queueConsumers: Config["queues"]["consumers"]; localProtocol: "http" | "https"; httpsKeyPath: string | undefined; @@ -92,6 +94,7 @@ export interface ConfigBundle { localUpstream: string | undefined; upstreamProtocol: "http" | "https"; inspect: boolean; + outboundService: ServiceFetch | undefined; tails: Config["tail_consumers"] | undefined; streamingTails: Config["streaming_tail_consumers"] | undefined; testScheduled: boolean; @@ -1139,6 +1142,8 @@ export async function buildMiniflareOptions( ...bindingOptions, ...sitesOptions, ...assetOptions, + routes: config.routes, + outboundService: config.outboundService, containerEngine: config.containerEngine, zone: config.zone, }, diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 5b40945ef3..cf78fcd177 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { readFileSync, realpathSync } from "node:fs"; import path from "node:path"; -import { watch } from "chokidar"; +import { watch as watchPaths } from "chokidar"; import { bundleWorker } from "../deployment-bundle/bundle"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { dedupeModulesByName } from "../deployment-bundle/dedupe-modules"; @@ -58,6 +58,7 @@ export function runBuild( local, targetConsumer, testScheduled, + watch, projectRoot, onStart, defineNavigatorUserAgent, @@ -86,6 +87,7 @@ export function runBuild( local: boolean; targetConsumer: "dev" | "deploy"; testScheduled: boolean; + watch: boolean; projectRoot: string | undefined; onStart: () => void; defineNavigatorUserAgent: boolean; @@ -161,7 +163,7 @@ export function runBuild( additionalModules: newAdditionalModules, jsxFactory, jsxFragment, - watch: true, + watch, tsconfig, minify, keepNames, @@ -198,9 +200,9 @@ export function runBuild( // if "noBundle" is true, then we need to manually watch all modules and // trigger "builds" when any change - if (noBundle) { + if (noBundle && watch) { const watching = [path.resolve(entry.moduleRoot)]; - const watcher = watch(watching, { + const watcher = watchPaths(watching, { persistent: true, // Ignore VCS dirs, dependencies, and the .wrangler dir (which // contains miniflare state/cache files written by workerd at From 24497d0f5fb327d7c86f5f3022510b53cfec931d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:48:33 +0100 Subject: [PATCH 2/4] Bump the workerd-and-workers-types group with 2 updates (#14217) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wrangler automated PR updater --- .changeset/dependabot-update-14217.md | 12 + packages/miniflare/package.json | 2 +- packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 378 +++++++++++++------------- pnpm-workspace.yaml | 4 +- 5 files changed, 205 insertions(+), 193 deletions(-) create mode 100644 .changeset/dependabot-update-14217.md diff --git a/.changeset/dependabot-update-14217.md b/.changeset/dependabot-update-14217.md new file mode 100644 index 0000000000..7afb9cfa6b --- /dev/null +++ b/.changeset/dependabot-update-14217.md @@ -0,0 +1,12 @@ +--- +"miniflare": patch +"wrangler": patch +--- + +Update dependencies of "miniflare", "wrangler" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ---------- | ------------ | ------------ | +| workerd | 1.20260605.1 | 1.20260608.1 | diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index b06feb7d4b..156558f62e 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -52,7 +52,7 @@ "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "catalog:default", - "workerd": "1.20260605.1", + "workerd": "1.20260608.1", "ws": "catalog:default", "youch": "4.1.0-beta.10" }, diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index c12ac93bca..0902ab1170 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -72,7 +72,7 @@ "miniflare": "workspace:*", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260605.1" + "workerd": "1.20260608.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c5cc0a487..06c1e437ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: 0.13.3 version: 0.13.3 '@cloudflare/workers-types': - specifier: ^4.20260605.1 - version: 4.20260605.1 + specifier: ^4.20260608.1 + version: 4.20260608.1 '@hey-api/openapi-ts': specifier: 0.94.0 version: 0.94.0 @@ -173,7 +173,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -221,7 +221,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -239,7 +239,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -260,7 +260,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -284,7 +284,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -320,7 +320,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 undici: specifier: catalog:default version: 7.24.8 @@ -335,7 +335,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -374,7 +374,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -401,7 +401,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -471,7 +471,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -489,11 +489,11 @@ importers: dependencies: '@sentry/cloudflare': specifier: ^10 - version: 10.50.0(@cloudflare/workers-types@4.20260605.1) + version: 10.50.0(@cloudflare/workers-types@4.20260608.1) devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -524,7 +524,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -552,7 +552,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -582,7 +582,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 undici: specifier: catalog:default version: 7.24.8 @@ -600,7 +600,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/debug': specifier: 4.1.12 version: 4.1.12 @@ -633,7 +633,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -658,7 +658,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -682,7 +682,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -721,7 +721,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -742,7 +742,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -763,7 +763,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -781,7 +781,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 is-odd: specifier: ^3.0.1 version: 3.0.1 @@ -800,7 +800,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -860,7 +860,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -881,7 +881,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1037,19 +1037,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 fixtures/rules-app: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 fixtures/secrets-store: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1076,7 +1076,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -1100,7 +1100,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -1115,7 +1115,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -1133,7 +1133,7 @@ importers: devDependencies: '@better-auth/stripe': specifier: ^1.4.6 - version: 1.5.4(e96ec3aaa0c29c2ed31fd3cb36e652f1) + version: 1.5.4(aad7a1b4634f2ffd2b96be614c855837) '@cloudflare/containers': specifier: ^0.2.2 version: 0.2.2 @@ -1142,7 +1142,7 @@ importers: version: link:../../packages/vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@microlabs/otel-cf-workers': specifier: 1.0.0-rc.45 version: 1.0.0-rc.45(@opentelemetry/api@1.9.1) @@ -1157,7 +1157,7 @@ importers: version: 3.2.6 better-auth: specifier: ^1.4.6 - version: 1.5.4(@cloudflare/workers-types@4.20260605.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + version: 1.5.4(@cloudflare/workers-types@4.20260608.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) discord-api-types: specifier: 0.37.98 version: 0.37.98 @@ -1228,7 +1228,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -1283,7 +1283,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1295,7 +1295,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -1343,7 +1343,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 run-script-os: specifier: ^1.1.6 version: 1.1.6 @@ -1367,7 +1367,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1388,7 +1388,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1409,7 +1409,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1430,7 +1430,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1451,7 +1451,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -1484,7 +1484,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -1511,7 +1511,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1529,7 +1529,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1620,7 +1620,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1686,7 +1686,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1855,7 +1855,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@octokit/types': specifier: ^13.8.0 version: 13.8.0 @@ -1876,7 +1876,7 @@ importers: version: link:../vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1900,7 +1900,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1927,10 +1927,10 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260608.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -2080,8 +2080,8 @@ importers: specifier: catalog:default version: 7.24.8 workerd: - specifier: 1.20260605.1 - version: 1.20260605.1 + specifier: 1.20260608.1 + version: 1.20260608.1 ws: specifier: catalog:default version: 8.20.1 @@ -2106,7 +2106,7 @@ importers: version: link:../workers-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2272,7 +2272,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260608.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-shared': specifier: workspace:* version: link:../workers-shared @@ -2281,7 +2281,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -2309,7 +2309,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2343,7 +2343,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -2367,7 +2367,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -2450,7 +2450,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2546,7 +2546,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2567,7 +2567,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2588,7 +2588,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2609,7 +2609,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2630,7 +2630,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2651,7 +2651,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2672,7 +2672,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2693,7 +2693,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2714,7 +2714,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2735,7 +2735,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2756,7 +2756,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2777,7 +2777,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2798,7 +2798,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2819,7 +2819,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2840,7 +2840,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2861,7 +2861,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2882,7 +2882,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -2915,7 +2915,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2936,7 +2936,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2957,7 +2957,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2978,7 +2978,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2999,7 +2999,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3020,7 +3020,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3041,7 +3041,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3062,7 +3062,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@playground/main-resolution-package': specifier: file:./package version: file:packages/vite-plugin-cloudflare/playground/main-resolution/package @@ -3086,7 +3086,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -3113,7 +3113,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@playground/module-resolution-excludes': specifier: file:./packages/excludes version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes @@ -3125,7 +3125,7 @@ importers: version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires '@remix-run/cloudflare': specifier: 2.12.0 - version: 2.12.0(@cloudflare/workers-types@4.20260605.1)(typescript@5.8.3) + version: 2.12.0(@cloudflare/workers-types@4.20260608.1)(typescript@5.8.3) '@types/react': specifier: ^18.3.11 version: 18.3.18 @@ -3158,7 +3158,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3179,7 +3179,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@fixture/shared': specifier: workspace:* version: link:../../../../fixtures/shared @@ -3231,7 +3231,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3252,7 +3252,7 @@ importers: dependencies: partyserver: specifier: ^0.3.3 - version: 0.3.3(@cloudflare/workers-types@4.20260605.1) + version: 0.3.3(@cloudflare/workers-types@4.20260608.1) partysocket: specifier: ^1.1.16 version: 1.1.16(react@19.2.1) @@ -3271,7 +3271,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@tailwindcss/vite': specifier: ^4.2.1 version: 4.2.2(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3307,7 +3307,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3328,7 +3328,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../../../workers-utils @@ -3368,7 +3368,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3398,7 +3398,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3419,7 +3419,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3447,7 +3447,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3480,7 +3480,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3501,7 +3501,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3522,7 +3522,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3543,7 +3543,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@vitejs/plugin-basic-ssl': specifier: ^2.2.0 version: 2.2.0(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3567,7 +3567,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3588,7 +3588,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3609,7 +3609,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3630,7 +3630,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3667,7 +3667,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -3922,13 +3922,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260608.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@sentry/cli': specifier: ^2.37.0 version: 2.41.1(encoding@0.1.13) @@ -4046,13 +4046,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260608.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -4090,8 +4090,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: 1.20260605.1 - version: 1.20260605.1 + specifier: 1.20260608.1 + version: 1.20260608.1 devDependencies: '@aws-sdk/client-s3': specifier: ^3.721.0 @@ -4128,7 +4128,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -4387,7 +4387,7 @@ importers: dependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260605.1 + version: 4.20260608.1 wrangler: specifier: workspace:* version: link:../wrangler @@ -5325,8 +5325,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260605.1': - resolution: {integrity: sha512-eXV5VZSVAW7wlarJOOz0vx+2skInZF9VgeeCUvz0y+UTOvbb4yRXsdylJbYQCLh6ImlpjOCB8KromQU9Ugh8gw==} + '@cloudflare/workerd-darwin-64@1.20260608.1': + resolution: {integrity: sha512-0AgTweTtel2KEaKPBdEB4lDoi9vzRIms42ZSL/oY//hEl4ruhhSczoPxkI5GVWgQ3qfADr5oOm/IeNdcaHKarw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5343,8 +5343,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260605.1': - resolution: {integrity: sha512-/rO98QTCCGak1Zj8A5UzmIPoPXr6G2j/r/DmwuI/Q5cUcJwgIotSgzyerz58iOflSGF90FKZQuqx1bR3VdiLKA==} + '@cloudflare/workerd-darwin-arm64@1.20260608.1': + resolution: {integrity: sha512-qzMsWKSIzk9lon7IU5loaymsNOlPBMzU/wmakr1eFtQ0zcbOvOhvV1kg4kcDdnjXX598bNIRnpPppKnwA3m3ow==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5361,8 +5361,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260605.1': - resolution: {integrity: sha512-X+VvJnTHZJ7DKgt3iXxeltDgvSD9+sSvoNCDf94dU0Ih0y5zKIK6CZvvOgi/S+3QiOn4Op8edj69vxidrxmnJA==} + '@cloudflare/workerd-linux-64@1.20260608.1': + resolution: {integrity: sha512-8TV5vCuZWmBDrxDX6mZvJCUgxxFgdAKuG7d8LFwB0m+olFeJPpi+iAe3ytw5BsIqpO5pznmnqZ1It/Ete4N+eA==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5379,8 +5379,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260605.1': - resolution: {integrity: sha512-WllbOIEW9QBh5oX/NjQ3wNMzMtI2+cvxqBRfvV6JdPlRiU7Oky0VKeu39M4eOGb5RmNPMQRt4WYpyQwRBU5xYw==} + '@cloudflare/workerd-linux-arm64@1.20260608.1': + resolution: {integrity: sha512-/q4f1+ijKq3zVXfYn5aaOohMAItkKfZxn2geYdzPCCU09OK/WbyHG5wkRl7VpKp5Aq7rLjD44F0fwGiD6O4Clw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5397,8 +5397,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260605.1': - resolution: {integrity: sha512-GyjYp73VY2Vd/pVHPSTz47vJSppPThT3u80aerznhAfao0nZ9gV/anTIy9Fz0FN07oJ7G4eyBabHOgwTDvB6Ng==} + '@cloudflare/workerd-windows-64@1.20260608.1': + resolution: {integrity: sha512-gfSVY/F+vb03BT9KZWRwvvLcMszPOPEjnBDvZqeud66Lfl7tYiZ2DQyjf7ikXkqb3oaZXJdSC2hYVApI7mPAmQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -5411,8 +5411,8 @@ packages: react: ^17.0.2 || ^18.2.21 react-dom: ^17.0.2 || ^18.2.21 - '@cloudflare/workers-types@4.20260605.1': - resolution: {integrity: sha512-9YJaGPQwuQYFHoElVhJP40ZmUO/Z2OiBon58sKpHjgwq5btC/B2BZLC45AQ14k+1I+hjcY2z2t2R6uUxU9AqwQ==} + '@cloudflare/workers-types@4.20260608.1': + resolution: {integrity: sha512-Yz90KXPZBB9K/AhsnLaA321s5+i5ZpWZRFbcGx9dWjyknQJXPAS1pK5CJSFEedwY6zoEr1cf2SkpBATL1idsWA==} '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -15086,8 +15086,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260605.1: - resolution: {integrity: sha512-gOpBUoOficcRaq9ZRL7QkC92WbGigebgxNMHB8JeYopY47+Gh92XtHxxFP6hLWBnwsAxIkKiexCtMwj3LNn0Pg==} + workerd@1.20260608.1: + resolution: {integrity: sha512-QWMdAT2duF68dJ4M+tmwL4D4uG/84XpgYM8+iOj0CexkiJNyVC5dpEb7NQNsJPpXq8sIzMsZN4MbuxjR4YU1SA==} engines: {node: '>=16'} hasBin: true @@ -16114,7 +16114,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16125,9 +16125,9 @@ snapshots: nanostores: 1.1.1 zod: 4.4.3 optionalDependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16138,50 +16138,50 @@ snapshots: nanostores: 1.1.1 zod: 4.4.3 optionalDependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 - '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': + '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': + '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 kysely: 0.28.11 - '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': + '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) prisma: 7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3) - '@better-auth/stripe@1.5.4(e96ec3aaa0c29c2ed31fd3cb36e652f1)': + '@better-auth/stripe@1.5.4(aad7a1b4634f2ffd2b96be614c855837)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) - better-auth: 1.5.4(@cloudflare/workers-types@4.20260605.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + better-auth: 1.5.4(@cloudflare/workers-types@4.20260608.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) better-call: 1.3.2(zod@4.3.6) defu: 6.1.4 stripe: 20.4.1(@types/node@22.15.17) zod: 4.3.6 - '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': + '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16746,7 +16746,7 @@ snapshots: lodash.memoize: 4.1.2 marked: 0.3.19 - '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': + '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@4.20260608.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': dependencies: '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -16754,7 +16754,7 @@ snapshots: esbuild: 0.27.3 miniflare: 4.20260317.1 vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) - wrangler: 4.76.0(@cloudflare/workers-types@4.20260605.1) + wrangler: 4.76.0(@cloudflare/workers-types@4.20260608.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -16767,7 +16767,7 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-64@1.20260605.1': + '@cloudflare/workerd-darwin-64@1.20260608.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20260317.1': @@ -16776,7 +16776,7 @@ snapshots: '@cloudflare/workerd-darwin-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260605.1': + '@cloudflare/workerd-darwin-arm64@1.20260608.1': optional: true '@cloudflare/workerd-linux-64@1.20260317.1': @@ -16785,7 +16785,7 @@ snapshots: '@cloudflare/workerd-linux-64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-64@1.20260605.1': + '@cloudflare/workerd-linux-64@1.20260608.1': optional: true '@cloudflare/workerd-linux-arm64@1.20260317.1': @@ -16794,7 +16794,7 @@ snapshots: '@cloudflare/workerd-linux-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260605.1': + '@cloudflare/workerd-linux-arm64@1.20260608.1': optional: true '@cloudflare/workerd-windows-64@1.20260317.1': @@ -16803,7 +16803,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260423.1': optional: true - '@cloudflare/workerd-windows-64@1.20260605.1': + '@cloudflare/workerd-windows-64@1.20260608.1': optional: true '@cloudflare/workers-editor-shared@0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -16814,7 +16814,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-split-pane: 0.1.92(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@cloudflare/workers-types@4.20260605.1': {} + '@cloudflare/workers-types@4.20260608.1': {} '@codemirror/autocomplete@6.20.0': dependencies: @@ -18204,7 +18204,7 @@ snapshots: '@prisma/adapter-d1@7.0.1': dependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 '@prisma/driver-adapter-utils': 7.0.1 ky: 1.7.5 @@ -18431,10 +18431,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260605.1)(typescript@5.8.3)': + '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260608.1)(typescript@5.8.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 '@remix-run/server-runtime': 2.12.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -18967,12 +18967,12 @@ snapshots: - encoding - supports-color - '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@4.20260605.1)': + '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@4.20260608.1)': dependencies: '@opentelemetry/api': 1.9.1 '@sentry/core': 10.50.0 optionalDependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 '@sentry/core@10.50.0': {} @@ -20610,15 +20610,15 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.5.4(@cloudflare/workers-types@4.20260605.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): + better-auth@1.5.4(@cloudflare/workers-types@4.20260608.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) - '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) - '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260605.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260608.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -20631,7 +20631,7 @@ snapshots: zod: 4.3.6 optionalDependencies: '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) mongodb: 7.1.0 mysql2: 3.15.3 pg: 8.16.3 @@ -21442,9 +21442,9 @@ snapshots: dependencies: wordwrap: 1.0.0 - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260605.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260608.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): optionalDependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 '@electric-sql/pglite': 0.3.2 '@opentelemetry/api': 1.9.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) @@ -23894,9 +23894,9 @@ snapshots: parseurl@1.3.3: {} - partyserver@0.3.3(@cloudflare/workers-types@4.20260605.1): + partyserver@0.3.3(@cloudflare/workers-types@4.20260608.1): dependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 nanoid: 5.1.7 partysocket@1.1.16(react@19.2.1): @@ -26699,15 +26699,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260423.1 '@cloudflare/workerd-windows-64': 1.20260423.1 - workerd@1.20260605.1: + workerd@1.20260608.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260605.1 - '@cloudflare/workerd-darwin-arm64': 1.20260605.1 - '@cloudflare/workerd-linux-64': 1.20260605.1 - '@cloudflare/workerd-linux-arm64': 1.20260605.1 - '@cloudflare/workerd-windows-64': 1.20260605.1 + '@cloudflare/workerd-darwin-64': 1.20260608.1 + '@cloudflare/workerd-darwin-arm64': 1.20260608.1 + '@cloudflare/workerd-linux-64': 1.20260608.1 + '@cloudflare/workerd-linux-arm64': 1.20260608.1 + '@cloudflare/workerd-windows-64': 1.20260608.1 - wrangler@4.76.0(@cloudflare/workers-types@4.20260605.1): + wrangler@4.76.0(@cloudflare/workers-types@4.20260608.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) @@ -26718,7 +26718,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260317.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260605.1 + '@cloudflare/workers-types': 4.20260608.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e643a60080..3acfce3233 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -115,8 +115,8 @@ catalog: "ws": "8.20.1" esbuild: "0.27.3" playwright-chromium: "1.60.0" - "@cloudflare/workers-types": "^4.20260605.1" - workerd: "1.20260605.1" + "@cloudflare/workers-types": "^4.20260608.1" + workerd: "1.20260608.1" jsonc-parser: "3.2.0" smol-toml: "1.5.2" msw: 2.12.4 From a61ac2936ae6b35146d637c18beb94567bb40bfa Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 8 Jun 2026 14:29:34 +0100 Subject: [PATCH 3/4] [wrangler] Add `--version-tag` support to `versions deploy` (#14211) --- .changeset/versions-deploy-by-tag.md | 11 + .../versions/versions.deploy.test.ts | 291 ++++++++++++++++++ packages/wrangler/src/versions/deploy.ts | 244 +++++++++++++-- 3 files changed, 522 insertions(+), 24 deletions(-) create mode 100644 .changeset/versions-deploy-by-tag.md diff --git a/.changeset/versions-deploy-by-tag.md b/.changeset/versions-deploy-by-tag.md new file mode 100644 index 0000000000..5d7bfac7ac --- /dev/null +++ b/.changeset/versions-deploy-by-tag.md @@ -0,0 +1,11 @@ +--- +"wrangler": minor +--- + +Add `--version-tag` support to `wrangler versions deploy` to deploy a version by its tag + +You can now roll out or roll back a version by the tag it was uploaded with (e.g. a commit SHA passed to `--tag` at upload time) instead of first looking up its Version ID: + +`wrangler versions deploy --version-tag @100%` + +The tag is resolved to a Version ID against the worker's deployable versions, and the `@` shorthand works just like the existing `@` notation, including splitting traffic across multiple `--version-tag` values. If a tag matches no deployable version, or matches more than one, the command errors and asks you to deploy by Version ID directly. Note that tags can only be resolved against recent (deployable) versions — older versions that have aged out of that window must still be deployed by Version ID. diff --git a/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts b/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts index ee347107e7..64d2e6124d 100644 --- a/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, it, test, vi } from "vitest"; import { normalizeOutput } from "../../../e2e/helpers/normalize"; import { assignAndDistributePercentages, + parseTagSpecs, parseVersionSpecs, summariseVersionTraffic, validateTrafficSubtotal, @@ -75,6 +76,93 @@ const mswGetVersion30000000 = http.get( ) ); +// MSW handler for the deployable-versions endpoint (GET /versions?deployable=true) +// returning versions that carry `workers/tag` annotations, used to test +// `versions deploy --version-tag `. +const mswListVersionsWithTags = http.get( + "*/accounts/:accountId/workers/scripts/:workerName/versions", + () => + HttpResponse.json( + createFetchResult({ + items: [ + { + id: "10000000-0000-0000-0000-000000000000", + number: "1", + annotations: { + "workers/triggered_by": "upload", + "workers/tag": "abc1234", + }, + metadata: { + author_id: "Picard-Gamma-6-0-7-3", + author_email: "Jean-Luc-Picard@federation.org", + source: "wrangler", + created_on: "2021-01-01T00:00:00.000000Z", + modified_on: "2021-01-01T00:00:00.000000Z", + }, + }, + { + id: "20000000-0000-0000-0000-000000000000", + number: "2", + annotations: { + "workers/triggered_by": "upload", + "workers/tag": "def5678", + }, + metadata: { + author_id: "Picard-Gamma-6-0-7-3", + author_email: "Jean-Luc-Picard@federation.org", + source: "wrangler", + created_on: "2021-01-02T00:00:00.000000Z", + modified_on: "2021-01-02T00:00:00.000000Z", + }, + }, + ], + }) + ) +); + +// MSW handler where two deployable versions share the same `workers/tag`, +// used to test the ambiguity error. +const mswListVersionsWithDuplicateTags = http.get( + "*/accounts/:accountId/workers/scripts/:workerName/versions", + () => + HttpResponse.json( + createFetchResult({ + items: [ + { + id: "10000000-0000-0000-0000-000000000000", + number: "1", + annotations: { + "workers/triggered_by": "upload", + "workers/tag": "dupe", + }, + metadata: { + author_id: "Picard-Gamma-6-0-7-3", + author_email: "Jean-Luc-Picard@federation.org", + source: "wrangler", + created_on: "2021-01-01T00:00:00.000000Z", + modified_on: "2021-01-01T00:00:00.000000Z", + }, + }, + { + id: "20000000-0000-0000-0000-000000000000", + number: "2", + annotations: { + "workers/triggered_by": "upload", + "workers/tag": "dupe", + }, + metadata: { + author_id: "Picard-Gamma-6-0-7-3", + author_email: "Jean-Luc-Picard@federation.org", + source: "wrangler", + created_on: "2021-01-02T00:00:00.000000Z", + modified_on: "2021-01-02T00:00:00.000000Z", + }, + }, + ], + }) + ) +); + describe("versions deploy", () => { mockAccountId(); mockApiToken(); @@ -1180,6 +1268,132 @@ describe("versions deploy", () => { expect(consoleStd.warn).toMatchInlineSnapshot(`""`); }); }); + + describe("deploy by tag", () => { + type DeployedVersion = { version_id: string; percentage: number }; + + // Captures the versions sent to the create-deployment endpoint so we can + // assert that the tag was resolved to the correct Version ID. + function captureDeployment() { + const captured: { versions?: DeployedVersion[] } = {}; + msw.use( + http.post( + "*/accounts/:accountId/workers/scripts/:workerName/deployments", + async ({ request }) => { + const body = (await request.json()) as { + versions: DeployedVersion[]; + }; + captured.versions = body.versions; + return HttpResponse.json( + createFetchResult({ id: "mock-new-deployment-id" }) + ); + } + ) + ); + return captured; + } + + test("resolves a single tag to its Version ID and deploys it", async ({ + expect, + }) => { + writeWranglerConfig(); + msw.use(mswListVersionsWithTags); + const captured = captureDeployment(); + + await expect( + runWrangler("versions deploy --version-tag def5678@100% --yes") + ).resolves.toBeUndefined(); + + expect(captured.versions).toEqual([ + { + version_id: "20000000-0000-0000-0000-000000000000", + percentage: 100, + }, + ]); + + const output = normalizeOutput(cliStd.out); + expect(output).toContain("Resolving tags to versions"); + expect(output).toContain("SUCCESS Deployed test-name version"); + }); + + test("splits traffic between multiple tags using shorthand percentages", async ({ + expect, + }) => { + writeWranglerConfig(); + msw.use(mswListVersionsWithTags); + const captured = captureDeployment(); + + await expect( + runWrangler( + "versions deploy --version-tag abc1234@40% --version-tag def5678@60% --yes" + ) + ).resolves.toBeUndefined(); + + expect(captured.versions).toEqual([ + { + version_id: "10000000-0000-0000-0000-000000000000", + percentage: 40, + }, + { + version_id: "20000000-0000-0000-0000-000000000000", + percentage: 60, + }, + ]); + }); + + test("can be combined with a Version ID", async ({ expect }) => { + writeWranglerConfig(); + msw.use(mswListVersionsWithTags); + const captured = captureDeployment(); + + await expect( + runWrangler( + "versions deploy 10000000-0000-0000-0000-000000000000@30% --version-tag def5678@70% --yes" + ) + ).resolves.toBeUndefined(); + + expect(captured.versions).toEqual([ + { + version_id: "10000000-0000-0000-0000-000000000000", + percentage: 30, + }, + { + version_id: "20000000-0000-0000-0000-000000000000", + percentage: 70, + }, + ]); + }); + + test("errors when no deployable version matches the tag", async ({ + expect, + }) => { + writeWranglerConfig(); + msw.use(mswListVersionsWithTags); + + await expect( + runWrangler("versions deploy --version-tag nope@100% --yes") + ).rejects.toMatchInlineSnapshot(` + [Error: No deployable version found with tag "nope". + Tags can only be resolved against recent (deployable) versions. Run \`wrangler versions list\` to see available versions, or deploy by Version ID directly.] + `); + }); + + test("errors when a tag matches multiple versions", async ({ + expect, + }) => { + writeWranglerConfig(); + msw.use(mswListVersionsWithDuplicateTags); + + await expect( + runWrangler("versions deploy --version-tag dupe@100% --yes") + ).rejects.toMatchInlineSnapshot(` + [Error: Tag "dupe" matches multiple versions: + - 20000000-0000-0000-0000-000000000000 + - 10000000-0000-0000-0000-000000000000 + Deploy by Version ID directly to disambiguate.] + `); + }); + }); }); }); @@ -1254,6 +1468,83 @@ describe("units", () => { }); }); + describe("parseTagSpecs", () => { + test("no args", ({ expect }) => { + const result = parseTagSpecs({}); + + expect(result).toMatchObject(new Map()); + }); + + test("tag without percentage", ({ expect }) => { + const result = parseTagSpecs({ versionTag: ["abc1234"] }); + + expect(Object.fromEntries(result)).toMatchObject({ abc1234: null }); + }); + + test("tag with percentage shorthand", ({ expect }) => { + const result = parseTagSpecs({ versionTag: ["abc1234@100%"] }); + + expect(Object.fromEntries(result)).toMatchObject({ abc1234: 100 }); + }); + + test("multiple tags with percentages", ({ expect }) => { + const result = parseTagSpecs({ + versionTag: ["abc1234@40%", "def5678@60%"], + }); + + expect(Object.fromEntries(result)).toMatchObject({ + abc1234: 40, + def5678: 60, + }); + }); + + test("tag containing @ with a percentage splits on the last @", ({ + expect, + }) => { + const result = parseTagSpecs({ versionTag: ["v1.0@beta@100%"] }); + + expect(Object.fromEntries(result)).toMatchObject({ "v1.0@beta": 100 }); + }); + + test("tag containing @ without a percentage is kept whole", ({ + expect, + }) => { + const result = parseTagSpecs({ versionTag: ["v1.0@beta"] }); + + expect(Object.fromEntries(result)).toMatchObject({ "v1.0@beta": null }); + }); + + test("trailing @ keeps a percentage-like tag whole with no percentage", ({ + expect, + }) => { + const result = parseTagSpecs({ versionTag: ["build@2@"] }); + + expect(Object.fromEntries(result)).toMatchObject({ "build@2": null }); + }); + + test("throws on empty tag", ({ expect }) => { + expect(() => parseTagSpecs({ versionTag: ["@100%"] })).toThrowError( + `Could not parse a tag from --version-tag arg "@100%".` + ); + }); + + test("throws on out-of-range percentage", ({ expect }) => { + expect(() => + parseTagSpecs({ versionTag: ["abc1234@101%"] }) + ).toThrowError( + `Percentage value (101%) parsed from --version-tag arg "abc1234@101%" must be between 0 and 100.` + ); + }); + + test("treats a non-numeric @ suffix as part of the tag", ({ expect }) => { + const result = parseTagSpecs({ versionTag: ["abc1234@oops"] }); + + expect(Object.fromEntries(result)).toMatchObject({ + "abc1234@oops": null, + }); + }); + }); + describe("assignAndDistributePercentages distributes remaining share of 100%", () => { test.for([ { diff --git a/packages/wrangler/src/versions/deploy.ts b/packages/wrangler/src/versions/deploy.ts index f3701a835b..e11ccc27b0 100644 --- a/packages/wrangler/src/versions/deploy.ts +++ b/packages/wrangler/src/versions/deploy.ts @@ -76,6 +76,13 @@ export const versionsDeployCommand = createCommand({ type: "string", array: true, }, + "version-tag": { + describe: + "Worker Version tag(s) to deploy, resolved to a Version ID against the deployable versions. Supports the shorthand notation [@..].", + type: "string", + array: true, + requiresArg: true, + }, message: { describe: "Description of this deployment (optional)", type: "string", @@ -117,7 +124,9 @@ export const versionsDeployCommand = createCommand({ const versionCache: VersionCache = new Map(); const optionalVersionTraffic = parseVersionSpecs(args); - const acceptPromptDefaults = args.yes || optionalVersionTraffic.size > 0; + const tagTraffic = parseTagSpecs(args); + const acceptPromptDefaults = + args.yes || optionalVersionTraffic.size + tagTraffic.size > 0; cli.startSection( "Deploy Worker Versions", @@ -127,6 +136,21 @@ export const versionsDeployCommand = createCommand({ await printLatestDeployment(config, accountId, workerName, versionCache); + // Resolve any --version-tag args to their Version IDs against the + // deployable versions, then merge them into the version traffic to deploy. + if (tagTraffic.size > 0) { + const resolvedTagTraffic = await resolveTagsToVersions( + config, + accountId, + workerName, + tagTraffic, + versionCache + ); + for (const [versionId, percentage] of resolvedTagTraffic) { + optionalVersionTraffic.set(versionId, percentage); + } + } + // prompt to confirm or change the versionIds from the args const confirmedVersionsToDeploy = await promptVersionsToDeploy( config, @@ -635,6 +659,46 @@ async function maybePatchSettings( // *********** // UNITS // *********** +/** + * Parses an optional `@` suffix from a shorthand spec (e.g. + * `@` or `@`), validating that the + * percentage (if present) is a number between 0 and 100. + * + * @param percentageString The raw percentage portion (after the `@`), if any. + * @param spec The full spec string, used for error messages. + * @param specLabel A label describing the arg the spec came from, used for error messages. + * @returns The parsed percentage, or `null` if none was specified. + */ +function parseOptionalPercentage( + percentageString: string | undefined, + spec: string, + specLabel: string +): OptionalPercentage { + if (percentageString === undefined || percentageString === "") { + return null; + } + + const percentage = parseFloat(percentageString); + + if (isNaN(percentage)) { + throw new UserError( + `Could not parse percentage value from ${specLabel} "${spec}"`, + { telemetryMessage: "versions deploy percentage parse failed" } + ); + } + + if (percentage < 0 || percentage > 100) { + throw new UserError( + `Percentage value (${percentage}%) parsed from ${specLabel} "${spec}" must be between 0 and 100.`, + { + telemetryMessage: "versions deploy positional percentage out of range", + } + ); + } + + return percentage; +} + export type ParseVersionSpecsArgs = { percentage?: number[]; versionId?: string[]; @@ -649,29 +713,11 @@ export function parseVersionSpecs( for (const spec of args.versionSpecs ?? []) { const [versionId, percentageString] = spec.split("@"); - const percentage = - percentageString === undefined || percentageString === "" - ? null - : parseFloat(percentageString); - - if (percentage !== null) { - if (isNaN(percentage)) { - throw new UserError( - `Could not parse percentage value from version-spec positional arg "${spec}"`, - { telemetryMessage: "versions deploy percentage parse failed" } - ); - } - - if (percentage < 0 || percentage > 100) { - throw new UserError( - `Percentage value (${percentage}%) parsed from version-spec positional arg "${spec}" must be between 0 and 100.`, - { - telemetryMessage: - "versions deploy positional percentage out of range", - } - ); - } - } + const percentage = parseOptionalPercentage( + percentageString, + spec, + "version-spec positional arg" + ); versionIds.push(versionId); percentages.push(percentage); @@ -710,6 +756,156 @@ export function parseVersionSpecs( return optionalVersionTraffic; } +type ParseTagSpecsArgs = { + versionTag?: string[]; +}; + +// A percentage suffix in a `@` spec, e.g. "100", "100%" +// or "50.5%". An out-of-range value like "101%" still matches (and is reported +// later by parseOptionalPercentage); a non-numeric suffix does not. +const PERCENTAGE_SUFFIX_REGEX = /^-?\d+(\.\d+)?%?$/; + +/** + * Splits a `@` spec into its tag and percentage parts. + * + * Tags can contain `@` (the upload `--tag` flag accepts arbitrary strings), so + * we split on the *last* `@` and only treat the suffix as a percentage when it + * actually looks like one. That way `v1.0@beta@100%` parses as tag `v1.0@beta` + * at 100%, while `v1.0@beta` (no percentage) is kept whole as the tag rather + * than misreading `beta` as a percentage. + */ +function splitTagSpec(spec: string): { + tag: string; + percentageString: string | undefined; +} { + const atIndex = spec.lastIndexOf("@"); + if (atIndex === -1) { + return { tag: spec, percentageString: undefined }; + } + + const suffix = spec.substring(atIndex + 1); + + // A trailing `@` (empty suffix) is an explicit "no percentage" separator, + // letting you keep a tag that ends in something percentage-like whole. + if (suffix === "") { + return { tag: spec.substring(0, atIndex), percentageString: undefined }; + } + + if (PERCENTAGE_SUFFIX_REGEX.test(suffix)) { + return { tag: spec.substring(0, atIndex), percentageString: suffix }; + } + + return { tag: spec, percentageString: undefined }; +} + +/** + * Parses the `--version-tag` args into a map of tag to optional percentage, + * supporting the `@` shorthand. Tags are resolved to + * Version IDs later by {@link resolveTagsToVersions}. + */ +export function parseTagSpecs( + args: ParseTagSpecsArgs +): Map { + const tagTraffic = new Map(); + + for (const spec of args.versionTag ?? []) { + const { tag, percentageString } = splitTagSpec(spec); + + if (!tag) { + throw new UserError( + `Could not parse a tag from --version-tag arg "${spec}".`, + { telemetryMessage: "versions deploy tag parse failed" } + ); + } + + const percentage = parseOptionalPercentage( + percentageString, + spec, + "--version-tag arg" + ); + + tagTraffic.set(tag, percentage); + } + + return tagTraffic; +} + +/** + * Resolves tags (e.g. commit SHAs supplied via `--version-tag`) to Version IDs by + * matching against the `workers/tag` annotation on the worker's deployable + * versions. + * + * Tags are not guaranteed to be unique, so this errors if a tag matches more + * than one version, prompting the user to disambiguate with a Version ID. It + * also errors if a tag matches no deployable version (e.g. the version has aged + * out of the deployable window). + * + * The fetched versions populate `versionCache`, so a subsequent + * `promptVersionsToDeploy` call can reuse them without re-fetching. + */ +async function resolveTagsToVersions( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + tagTraffic: Map, + versionCache: VersionCache +): Promise> { + const versions = await spinnerWhile({ + startMessage: "Resolving tags to versions", + promise() { + return fetchDeployableVersions( + complianceConfig, + accountId, + workerName, + versionCache + ); + }, + }); + + // A tag may be present on more than one version (e.g. a re-deploy of the same + // commit, or a secret change inheriting the tag), so collect all matches. + const versionsByTag = new Map(); + for (const version of versions) { + const tag = version.annotations?.["workers/tag"]; + if (tag === undefined) { + continue; + } + + const matches = versionsByTag.get(tag) ?? []; + matches.push(version); + versionsByTag.set(tag, matches); + } + + const resolvedVersionTraffic = new Map(); + for (const [tag, percentage] of tagTraffic) { + const matches = versionsByTag.get(tag) ?? []; + + if (matches.length === 0) { + throw new UserError( + `No deployable version found with tag "${tag}".\nTags can only be resolved against recent (deployable) versions. Run \`wrangler versions list\` to see available versions, or deploy by Version ID directly.`, + { telemetryMessage: "versions deploy tag not found" } + ); + } + + if (matches.length > 1) { + const ids = matches + .sort((a, b) => + b.metadata.created_on.localeCompare(a.metadata.created_on) + ) + .map((version) => ` - ${version.id}`) + .join("\n"); + throw new UserError( + `Tag "${tag}" matches multiple versions:\n${ids}\nDeploy by Version ID directly to disambiguate.`, + { telemetryMessage: "versions deploy tag ambiguous" } + ); + } + + resolvedVersionTraffic.set(matches[0].id, percentage); + } + + return resolvedVersionTraffic; +} + export function assignAndDistributePercentages( versionIds: VersionId[], optionalVersionTraffic: Map From e13b8c02db3b65861cf84faaf276ffb79025079f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:13:25 +0000 Subject: [PATCH 4/4] [C3] Bump @angular/create from 21.2.13 to 22.0.0 in /packages/create-cloudflare/src/frameworks (#14209) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wrangler automated PR updater Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: pbacondarwin --- .changeset/c3-frameworks-update-14209.md | 11 +++ .../src/frameworks/package.json | 2 +- .../templates/angular/pages/c3.ts | 2 +- .../templates/angular/workers/c3.ts | 2 +- .../autoconfig/frameworks/angular.test.ts | 71 ++++++++++++++++++- .../autoconfig/frameworks/all-frameworks.ts | 4 +- .../src/autoconfig/frameworks/angular.ts | 12 +++- 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 .changeset/c3-frameworks-update-14209.md diff --git a/.changeset/c3-frameworks-update-14209.md b/.changeset/c3-frameworks-update-14209.md new file mode 100644 index 0000000000..58300fc864 --- /dev/null +++ b/.changeset/c3-frameworks-update-14209.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| --------------- | ------- | ------ | +| @angular/create | 21.2.13 | 22.0.0 | diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index faa1076d09..e35d9265b5 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -1,7 +1,7 @@ { "name": "frameworks_clis_info", "dependencies": { - "@angular/create": "21.2.13", + "@angular/create": "22.0.0", "@tanstack/cli": "0.69.1", "create-analog": "2.6.0", "create-astro": "5.0.6", diff --git a/packages/create-cloudflare/templates/angular/pages/c3.ts b/packages/create-cloudflare/templates/angular/pages/c3.ts index acfbc438a0..88da266f75 100644 --- a/packages/create-cloudflare/templates/angular/pages/c3.ts +++ b/packages/create-cloudflare/templates/angular/pages/c3.ts @@ -77,7 +77,7 @@ function updateAngularJson(ctx: C3Context) { const architectSection = angularJson.projects[ctx.project.name].architect; architectSection.build.options.outputPath = "dist"; architectSection.build.options.outputMode = "server"; - architectSection.build.options.ssr.experimentalPlatform = "neutral"; + architectSection.build.options.ssr.platform = "neutral"; architectSection.build.options.assets.push("src/_routes.json"); writeFile(resolve("angular.json"), JSON.stringify(angularJson, null, 2)); diff --git a/packages/create-cloudflare/templates/angular/workers/c3.ts b/packages/create-cloudflare/templates/angular/workers/c3.ts index 89e14c2669..e82ca85963 100644 --- a/packages/create-cloudflare/templates/angular/workers/c3.ts +++ b/packages/create-cloudflare/templates/angular/workers/c3.ts @@ -76,7 +76,7 @@ function updateAngularJson(ctx: C3Context) { const architectSection = angularJson.projects[ctx.project.name].architect; architectSection.build.options.outputPath = "dist"; architectSection.build.options.outputMode = "server"; - architectSection.build.options.ssr.experimentalPlatform = "neutral"; + architectSection.build.options.ssr.platform = "neutral"; writeFile(resolve("angular.json"), JSON.stringify(angularJson, null, 2)); s.stop(`${brandColor(`updated`)} ${dim(`\`angular.json\``)}`); diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts index e02b977681..1fd6ee4497 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts @@ -5,6 +5,7 @@ import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; import { Angular } from "../../../autoconfig/frameworks/angular"; import { NpmPackageManager } from "../../../package-manager"; +import type { AutoConfigFrameworkPackageInfo } from "../../../autoconfig/frameworks"; const BASE_OPTIONS = { projectPath: process.cwd(), workerName: "my-angular-app", @@ -14,6 +15,21 @@ const BASE_OPTIONS = { isWorkspaceRoot: false, }; +const ANGULAR_PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { + name: "@angular/core", + minimumVersion: "19.0.0", + maximumKnownMajorVersion: "22", +}; + +async function mockAngularCoreVersion(version: string) { + const pkgDir = resolve("node_modules/@angular/core"); + await mkdir(pkgDir, { recursive: true }); + await writeFile( + resolve(pkgDir, "package.json"), + JSON.stringify({ name: "@angular/core", version }) + ); +} + describe("Angular framework configure()", () => { runInTempDir(); @@ -202,7 +218,9 @@ describe("Angular framework configure()", () => { }); it("returns SSR wranglerConfig without crashing", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); const result = await framework.configure(BASE_OPTIONS); expect(result.wranglerConfig).toEqual({ @@ -214,11 +232,13 @@ describe("Angular framework configure()", () => { }); }); - it("sets experimentalPlatform in angular.json when ssr was true", async ({ + it("sets experimentalPlatform in angular.json when ssr was true (Angular <22)", async ({ expect, }) => { const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -230,6 +250,24 @@ describe("Angular framework configure()", () => { expect(typeof options.ssr).toBe("object"); expect(options.ssr.experimentalPlatform).toBe("neutral"); }); + + it("sets platform in angular.json when ssr was true (Angular >=22)", async ({ + expect, + }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("22.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + expect(typeof options.ssr).toBe("object"); + expect(options.ssr.platform).toBe("neutral"); + }); }); describe("SSR project", () => { @@ -265,7 +303,9 @@ describe("Angular framework configure()", () => { it("returns SSR wranglerConfig with main and assets", async ({ expect, }) => { + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); const result = await framework.configure(BASE_OPTIONS); expect(result.wranglerConfig).toEqual({ @@ -278,7 +318,9 @@ describe("Angular framework configure()", () => { }); it("sets SSR configurationDescription", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); await framework.configure(BASE_OPTIONS); expect(framework.configurationDescription).toBe( @@ -286,9 +328,13 @@ describe("Angular framework configure()", () => { ); }); - it("sets experimentalPlatform in angular.json", async ({ expect }) => { + it("sets experimentalPlatform in angular.json (Angular <22)", async ({ + expect, + }) => { const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -301,16 +347,37 @@ describe("Angular framework configure()", () => { expect(options.ssr.experimentalPlatform).toBe("neutral"); }); + it("sets platform in angular.json (Angular >=22)", async ({ expect }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("22.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + expect(options.outputMode).toBe("server"); + expect(options.outputPath).toBe("dist"); + expect(options.ssr.platform).toBe("neutral"); + }); + it("creates src/server.ts", async ({ expect }) => { const { existsSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); await framework.configure(BASE_OPTIONS); expect(existsSync(resolve("src/server.ts"))).toBe(true); }); it("installs additional dependencies", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); await framework.configure(BASE_OPTIONS); expect(installSpy).toHaveBeenCalledWith( diff --git a/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts b/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts index 33b943f4a0..753b68fe43 100644 --- a/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts +++ b/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts @@ -43,8 +43,10 @@ export const allKnownFrameworks = [ // Angular 19 introduced ssr.experimentalPlatform and AngularAppEngine // which are required for Cloudflare Workers support // See: https://github.com/angular/angular-cli/releases/tag/19.0.0 + // Angular 22 renamed experimentalPlatform to platform + // See: https://github.com/angular/angular-cli/commit/af2c7e9444fba81d3b1fd2d37dc4412f8305b5ed minimumVersion: "19.0.0", - maximumKnownMajorVersion: "21", + maximumKnownMajorVersion: "22", }, supported: true, }, diff --git a/packages/wrangler/src/autoconfig/frameworks/angular.ts b/packages/wrangler/src/autoconfig/frameworks/angular.ts index d7bc553132..149abd756f 100644 --- a/packages/wrangler/src/autoconfig/frameworks/angular.ts +++ b/packages/wrangler/src/autoconfig/frameworks/angular.ts @@ -5,6 +5,7 @@ import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; import { parseJSONC } from "@cloudflare/workers-utils"; +import semiver from "semiver"; import { dedent } from "../../utils/dedent"; import { Framework } from "./framework-class"; import type { PackageManager } from "../../package-manager"; @@ -28,7 +29,7 @@ export class Angular extends Framework { if (hasSsr(angularJson, workerName)) { this.configurationDescription = "Configuring project for Angular"; if (!dryRun) { - await updateAngularJson(workerName, angularJson); + await updateAngularJson(workerName, angularJson, this.frameworkVersion); await overrideServerFile(); await installAdditionalDependencies(packageManager, isWorkspaceRoot); } @@ -66,7 +67,8 @@ function hasSsr(angularJson: AngularJson, projectName: string): boolean { async function updateAngularJson( projectName: string, - angularJson: AngularJson + angularJson: AngularJson, + frameworkVersion: string ) { const s = spinner(); s.start(`Updating angular.json config`); @@ -82,7 +84,11 @@ async function updateAngularJson( if (typeof architectSection.build.options.ssr === "boolean") { architectSection.build.options.ssr = {}; } - architectSection.build.options.ssr["experimentalPlatform"] = "neutral"; + const platformKey = + semiver(frameworkVersion, "22.0.0") >= 0 + ? "platform" + : "experimentalPlatform"; + architectSection.build.options.ssr[platformKey] = "neutral"; await writeFile( resolve("angular.json"),