From 3cf9d0e9daa043265f2d5cd5add1b448f6378474 Mon Sep 17 00:00:00 2001 From: James Opstad <13586373+jamesopstad@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:48:21 +0100 Subject: [PATCH 1/6] Add experimental new config and opt in Vite plugin support (#14013) --- .../vite-plugin-experimental-new-config.md | 51 + packages/config/package.json | 47 + packages/config/src/__tests__/convert.test.ts | 786 ++++++++++++++++ .../config/src/__tests__/generate.test.ts | 28 + packages/config/src/__tests__/load.test.ts | 226 +++++ packages/config/src/__tests__/schema.test.ts | 453 +++++++++ packages/config/src/bindings.ts | 890 ++++++++++++++++++ packages/config/src/convert.ts | 790 ++++++++++++++++ packages/config/src/exports.ts | 83 ++ packages/config/src/generate.ts | 60 ++ packages/config/src/index.ts | 7 + packages/config/src/inference.ts | 319 +++++++ packages/config/src/load.ts | 201 ++++ packages/config/src/public.ts | 75 ++ packages/config/src/schema.ts | 475 ++++++++++ packages/config/src/triggers.ts | 132 +++ packages/config/src/types.ts | 399 ++++++++ packages/config/src/utils.ts | 10 + packages/config/src/worker-definition.ts | 129 +++ packages/config/tsconfig.json | 7 + packages/config/tsdown.config.ts | 12 + packages/config/turbo.json | 9 + packages/config/vitest.config.ts | 8 + packages/vite-plugin-cloudflare/package.json | 5 + .../__tests__/worker.spec.ts | 11 + .../experimental-config/cloudflare.config.ts | 14 + .../experimental-config/package.json | 19 + .../experimental-config/src/index.ts | 7 + .../experimental-config/tsconfig.json | 7 + .../experimental-config/tsconfig.node.json | 4 + .../experimental-config/tsconfig.worker.json | 7 + .../experimental-config/vite.config.ts | 12 + .../worker-configuration.d.ts | 17 + .../__tests__/experimental-new-config.spec.ts | 320 +++++++ .../__tests__/resolve-plugin-config.spec.ts | 198 ++-- .../src/__tests__/shortcuts.spec.ts | 28 +- .../src/experimental-config.ts | 5 + packages/vite-plugin-cloudflare/src/index.ts | 4 +- .../src/plugin-config.ts | 206 +++- .../src/workers-configs.ts | 65 +- .../vite-plugin-cloudflare/tsdown.config.ts | 17 +- .../workers-utils/src/config/environment.ts | 8 + pnpm-lock.yaml | 107 ++- 43 files changed, 6109 insertions(+), 149 deletions(-) create mode 100644 .changeset/vite-plugin-experimental-new-config.md create mode 100644 packages/config/package.json create mode 100644 packages/config/src/__tests__/convert.test.ts create mode 100644 packages/config/src/__tests__/generate.test.ts create mode 100644 packages/config/src/__tests__/load.test.ts create mode 100644 packages/config/src/__tests__/schema.test.ts create mode 100644 packages/config/src/bindings.ts create mode 100644 packages/config/src/convert.ts create mode 100644 packages/config/src/exports.ts create mode 100644 packages/config/src/generate.ts create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/src/inference.ts create mode 100644 packages/config/src/load.ts create mode 100644 packages/config/src/public.ts create mode 100644 packages/config/src/schema.ts create mode 100644 packages/config/src/triggers.ts create mode 100644 packages/config/src/types.ts create mode 100644 packages/config/src/utils.ts create mode 100644 packages/config/src/worker-definition.ts create mode 100644 packages/config/tsconfig.json create mode 100644 packages/config/tsdown.config.ts create mode 100644 packages/config/turbo.json create mode 100644 packages/config/vitest.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/__tests__/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/experimental-new-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/experimental-config.ts diff --git a/.changeset/vite-plugin-experimental-new-config.md b/.changeset/vite-plugin-experimental-new-config.md new file mode 100644 index 0000000000..7292b60975 --- /dev/null +++ b/.changeset/vite-plugin-experimental-new-config.md @@ -0,0 +1,51 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Add experimental `experimental.newConfig` option to load the entry Worker's configuration from `cloudflare.config.ts` + +This is an experimental, opt-in feature. When enabled, the plugin loads the entry Worker's configuration from a `cloudflare.config.ts` file instead of the usual `wrangler.json` / `wrangler.jsonc` / `wrangler.toml`. + +Pass `true` to enable with defaults, or an object to customise behaviour. Currently the only sub-option is `types.generate` (defaults to `true`), which writes a `worker-configuration.d.ts` file next to the config. This enables typed `env` and `exports` for your Worker and currently assumes that you have `@cloudflare/workers-types` installed. + +```ts +// vite.config.ts +import { defineConfig } from "vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; + +export default defineConfig({ + plugins: [ + cloudflare({ + experimental: { + newConfig: true, + }, + }), + ], +}); +``` + +```ts +// cloudflare.config.ts +import { + defineWorker, + bindings, +} from "@cloudflare/vite-plugin/experimental-config"; +import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + +export default defineWorker((ctx) => ({ + name: "my-worker", + entrypoint, + compatibilityDate: "2026-05-18", + env: { + MY_TEXT: bindings.text(`The mode is ${ctx.mode}`), + MY_KV: bindings.kv(), + }, +})); +``` + +A few limitations apply while the feature is experimental: + +- `configPath` cannot be combined with `experimental.newConfig`. The entry Worker is always loaded from `cloudflare.config.ts` at the project root. +- `auxiliaryWorkers` are not yet supported with `experimental.newConfig`. + +Because this is experimental, the option, the `cloudflare.config.ts` schema, and the `@cloudflare/vite-plugin/experimental-config` exports may change in any release. diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000000..9c734bf1a9 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cloudflare/config", + "version": "0.0.0", + "private": true, + "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/config#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/config" + }, + "files": [ + "dist" + ], + "type": "module", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + }, + "./public": { + "types": "./dist/public.d.mts", + "import": "./dist/public.mjs" + } + }, + "scripts": { + "build": "tsdown", + "check:type": "tsc", + "dev": "tsdown --watch", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@cloudflare/workers-utils": "workspace:*", + "ts-dedent": "^2.2.0", + "tsdown": "0.16.3", + "typescript": "catalog:default", + "vitest": "catalog:default", + "zod": "^4.4.3" + } +} diff --git a/packages/config/src/__tests__/convert.test.ts b/packages/config/src/__tests__/convert.test.ts new file mode 100644 index 0000000000..ef27df3d49 --- /dev/null +++ b/packages/config/src/__tests__/convert.test.ts @@ -0,0 +1,786 @@ +import { describe, it } from "vitest"; +import { convertToWranglerConfig } from "../convert"; + +describe("convertToWranglerConfig", () => { + describe("top-level fields", () => { + it("returns an empty object for an empty config", ({ expect }) => { + expect(convertToWranglerConfig({})).toEqual({}); + }); + + it("maps primitive top-level fields", ({ expect }) => { + const result = convertToWranglerConfig({ + name: "my-worker", + entrypoint: "./src/index.ts", + accountId: "acc-123", + compatibilityDate: "2026-01-01", + compatibilityFlags: ["nodejs_compat"], + workersDev: true, + previewUrls: false, + logpush: true, + firstPartyWorker: false, + }); + expect(result).toEqual({ + name: "my-worker", + main: "./src/index.ts", + account_id: "acc-123", + compatibility_date: "2026-01-01", + compatibility_flags: ["nodejs_compat"], + workers_dev: true, + preview_urls: false, + logpush: true, + first_party_worker: false, + }); + }); + + it("maps complianceRegion: 'fedramp-high' to 'fedramp_high'", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + complianceRegion: "fedramp-high", + }); + expect(result.compliance_region).toBe("fedramp_high"); + }); + + it("passes complianceRegion: 'public' through unchanged", ({ expect }) => { + const result = convertToWranglerConfig({ complianceRegion: "public" }); + expect(result.compliance_region).toBe("public"); + }); + + it("passes placement through unchanged", ({ expect }) => { + const result = convertToWranglerConfig({ + placement: { mode: "smart", hint: "iad" }, + }); + expect(result.placement).toEqual({ mode: "smart", hint: "iad" }); + }); + + it("maps limits.cpuMs to limits.cpu_ms", ({ expect }) => { + const result = convertToWranglerConfig({ + limits: { cpuMs: 50, subrequests: 100 }, + }); + expect(result.limits).toEqual({ cpu_ms: 50, subrequests: 100 }); + }); + + it("converts observability camelCase to snake_case", ({ expect }) => { + const result = convertToWranglerConfig({ + observability: { + enabled: true, + headSamplingRate: 0.5, + logs: { + enabled: true, + headSamplingRate: 0.25, + invocationLogs: false, + persist: true, + destinations: ["d1"], + }, + traces: { + enabled: false, + headSamplingRate: 0.1, + persist: false, + destinations: ["d2"], + }, + }, + }); + expect(result.observability).toEqual({ + enabled: true, + head_sampling_rate: 0.5, + logs: { + enabled: true, + head_sampling_rate: 0.25, + invocation_logs: false, + persist: true, + destinations: ["d1"], + }, + traces: { + enabled: false, + head_sampling_rate: 0.1, + persist: false, + destinations: ["d2"], + }, + }); + }); + + it("passes cache through unchanged", ({ expect }) => { + expect( + convertToWranglerConfig({ cache: { enabled: true } }).cache + ).toEqual({ enabled: true }); + }); + + it("maps unsafe.metadata directly", ({ expect }) => { + const result = convertToWranglerConfig({ + unsafe: { metadata: { foo: "bar" } }, + }); + expect(result.unsafe).toEqual({ metadata: { foo: "bar" } }); + }); + + it("maps unsafe.capnp basePath variant to snake_case", ({ expect }) => { + const result = convertToWranglerConfig({ + unsafe: { + capnp: { + basePath: "/schemas", + sourceSchemas: ["a.capnp", "b.capnp"], + }, + }, + }); + expect(result.unsafe).toEqual({ + capnp: { + base_path: "/schemas", + source_schemas: ["a.capnp", "b.capnp"], + }, + }); + }); + + it("maps unsafe.capnp compiledSchema variant to snake_case", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + unsafe: { capnp: { compiledSchema: "compiled-blob" } }, + }); + expect(result.unsafe).toEqual({ + capnp: { compiled_schema: "compiled-blob" }, + }); + }); + }); + + describe("singleton bindings", () => { + it("maps each singleton binding to a {binding: name} entry", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { + MY_AI: { type: "ai" }, + MY_BROWSER: { type: "browser" }, + MY_IMAGES: { type: "images" }, + MY_MEDIA: { type: "media" }, + MY_STREAM: { type: "stream" }, + MY_VM: { type: "version-metadata" }, + MY_WEB_SEARCH: { type: "web-search" }, + }, + }); + expect(result.ai).toEqual({ binding: "MY_AI" }); + expect(result.browser).toEqual({ binding: "MY_BROWSER" }); + expect(result.images).toEqual({ binding: "MY_IMAGES" }); + expect(result.media).toEqual({ binding: "MY_MEDIA" }); + expect(result.stream).toEqual({ binding: "MY_STREAM" }); + expect(result.version_metadata).toEqual({ binding: "MY_VM" }); + expect(result.websearch).toEqual({ binding: "MY_WEB_SEARCH" }); + }); + + it("includes the remote flag on singletons that support it", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { MY_AI: { type: "ai", remote: true } }, + }); + expect(result.ai).toEqual({ binding: "MY_AI", remote: true }); + }); + + it("includes the remote flag on web-search", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { MY_WS: { type: "web-search", remote: true } }, + }); + expect(result.websearch).toEqual({ + binding: "MY_WS", + remote: true, + }); + }); + }); + + describe("array bindings", () => { + it("maps kv with id", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { MY_KV: { type: "kv", id: "abc", remote: true } }, + }); + expect(result.kv_namespaces).toEqual([ + { binding: "MY_KV", id: "abc", remote: true }, + ]); + }); + + it("maps multiple kv bindings", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + KV_1: { type: "kv" }, + KV_2: { type: "kv", id: "abc" }, + }, + }); + expect(result.kv_namespaces).toEqual([ + { binding: "KV_1" }, + { binding: "KV_2", id: "abc" }, + ]); + }); + + it("maps d1 with id and name", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + MY_DB: { type: "d1", id: "db-id", name: "db-name" }, + }, + }); + expect(result.d1_databases).toEqual([ + { binding: "MY_DB", database_id: "db-id", database_name: "db-name" }, + ]); + }); + + it("maps r2 with name and jurisdiction", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + MY_R2: { type: "r2", name: "my-bucket", jurisdiction: "eu" }, + }, + }); + expect(result.r2_buckets).toEqual([ + { binding: "MY_R2", bucket_name: "my-bucket", jurisdiction: "eu" }, + ]); + }); + + it("maps vectorize.name to index_name", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { MY_VEC: { type: "vectorize", name: "my-index" } }, + }); + expect(result.vectorize).toEqual([ + { binding: "MY_VEC", index_name: "my-index" }, + ]); + }); + + it("maps mtlsCertificate.id to certificate_id", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { MY_MTLS: { type: "mtls-certificate", id: "cert-1" } }, + }); + expect(result.mtls_certificates).toEqual([ + { binding: "MY_MTLS", certificate_id: "cert-1" }, + ]); + }); + + it("maps hyperdrive with localConnectionString (camelCase)", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { + HD: { + type: "hyperdrive", + id: "h-1", + localConnectionString: "postgres://...", + }, + }, + }); + expect(result.hyperdrive).toEqual([ + { + binding: "HD", + id: "h-1", + localConnectionString: "postgres://...", + }, + ]); + }); + + it("maps pipeline.name to stream", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { MY_PIPE: { type: "pipeline", name: "pipe-1" } }, + }); + expect(result.pipelines).toEqual([ + { binding: "MY_PIPE", stream: "pipe-1" }, + ]); + }); + + it("maps flagship.id to app_id", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { F: { type: "flagship", id: "app-1" } }, + }); + expect(result.flagship).toEqual([{ binding: "F", app_id: "app-1" }]); + }); + + it("maps ai-search.name to instance_name", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { S: { type: "ai-search", name: "inst-1" } }, + }); + expect(result.ai_search).toEqual([ + { binding: "S", instance_name: "inst-1" }, + ]); + }); + + it("maps ai-search-namespace.namespace to namespace", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { N: { type: "ai-search-namespace", namespace: "ns-1" } }, + }); + expect(result.ai_search_namespaces).toEqual([ + { binding: "N", namespace: "ns-1" }, + ]); + }); + + it("maps agent-memory bindings with namespace", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + MEM: { type: "agent-memory", namespace: "ns-1", remote: true }, + }, + }); + expect(result.agent_memory).toEqual([ + { binding: "MEM", namespace: "ns-1", remote: true }, + ]); + }); + + it("maps multiple agent-memory bindings", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + MEM_1: { type: "agent-memory", namespace: "ns-1" }, + MEM_2: { type: "agent-memory", namespace: "ns-2" }, + }, + }); + expect(result.agent_memory).toEqual([ + { binding: "MEM_1", namespace: "ns-1" }, + { binding: "MEM_2", namespace: "ns-2" }, + ]); + }); + + it("maps analytics-engine-dataset.name to dataset", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { AE: { type: "analytics-engine-dataset", name: "ds-1" } }, + }); + expect(result.analytics_engine_datasets).toEqual([ + { binding: "AE", dataset: "ds-1" }, + ]); + }); + + it("maps artifacts.namespace", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { A: { type: "artifacts", namespace: "ns-1" } }, + }); + expect(result.artifacts).toEqual([{ binding: "A", namespace: "ns-1" }]); + }); + + it("maps dispatch-namespace with outbound", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + DN: { + type: "dispatch-namespace", + namespace: "ns-1", + outbound: { workerName: "out-worker", parameters: ["p1", "p2"] }, + }, + }, + }); + expect(result.dispatch_namespaces).toEqual([ + { + binding: "DN", + namespace: "ns-1", + outbound: { service: "out-worker", parameters: ["p1", "p2"] }, + }, + ]); + }); + + it("maps secrets-store-secret to store_id + secret_name", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + SS: { + type: "secrets-store-secret", + storeId: "store-1", + secretName: "secret-1", + }, + }, + }); + expect(result.secrets_store_secrets).toEqual([ + { binding: "SS", store_id: "store-1", secret_name: "secret-1" }, + ]); + }); + + it("maps send-email with all address fields", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + EM: { + type: "send-email", + destinationAddress: "dest@example.com", + allowedDestinationAddresses: ["a@x.com", "b@x.com"], + allowedSenderAddresses: ["sender@x.com"], + }, + }, + }); + expect(result.send_email).toEqual([ + { + name: "EM", + destination_address: "dest@example.com", + allowed_destination_addresses: ["a@x.com", "b@x.com"], + allowed_sender_addresses: ["sender@x.com"], + }, + ]); + }); + + it("maps vpc-service.id to service_id", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { V: { type: "vpc-service", id: "svc-1" } }, + }); + expect(result.vpc_services).toEqual([ + { binding: "V", service_id: "svc-1" }, + ]); + }); + + it("maps vpc-network with tunnelId", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { V: { type: "vpc-network", tunnelId: "tun-1" } }, + }); + expect(result.vpc_networks).toEqual([ + { binding: "V", tunnel_id: "tun-1" }, + ]); + }); + + it("maps vpc-network with networkId", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { V: { type: "vpc-network", networkId: "net-1" } }, + }); + expect(result.vpc_networks).toEqual([ + { binding: "V", network_id: "net-1" }, + ]); + }); + + it("maps worker-loader to a worker_loaders entry", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { WL: { type: "worker-loader" } }, + }); + expect(result.worker_loaders).toEqual([{ binding: "WL" }]); + }); + + it("maps rate-limit to ratelimits with name + namespace_id + simple", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { + RL: { + type: "rate-limit", + namespace: "ns-1", + simple: { limit: 100, period: 60 }, + }, + }, + }); + expect(result.ratelimits).toEqual([ + { + name: "RL", + namespace_id: "ns-1", + simple: { limit: 100, period: 60 }, + }, + ]); + }); + + it("maps worker binding to a services entry", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + W: { + type: "worker", + workerName: "other-worker", + exportName: "MyEntry", + props: { foo: "bar" }, + remote: true, + }, + }, + }); + expect(result.services).toEqual([ + { + binding: "W", + service: "other-worker", + entrypoint: "MyEntry", + props: { foo: "bar" }, + remote: true, + }, + ]); + }); + + it("maps queue binding to queues.producers", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + Q: { type: "queue", name: "q-1", deliveryDelay: 5 }, + }, + }); + expect(result.queues).toEqual({ + producers: [{ binding: "Q", queue: "q-1", delivery_delay: 5 }], + }); + }); + + it("maps durable-object binding to durable_objects.bindings", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { + DO: { + type: "durable-object", + workerName: "other-worker", + exportName: "MyDO", + }, + }, + }); + expect(result.durable_objects).toEqual({ + bindings: [ + { name: "DO", class_name: "MyDO", script_name: "other-worker" }, + ], + }); + }); + + it("maps logfwdr binding to logfwdr.bindings", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { LF: { type: "logfwdr", destination: "dest-1" } }, + }); + expect(result.logfwdr).toEqual({ + bindings: [{ name: "LF", destination: "dest-1" }], + }); + }); + + it("maps unsafe binding with all fields", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + U: { + type: "unsafe:my-custom", + custom_field: "value-1", + dev: { + plugin: { package: "pkg", name: "plug" }, + }, + }, + }, + }); + expect(result.unsafe).toEqual({ + bindings: [ + { + name: "U", + type: "my-custom", + custom_field: "value-1", + dev: { plugin: { package: "pkg", name: "plug" } }, + }, + ], + }); + }); + }); + + describe("vars and secrets", () => { + it("merges multiple json and text bindings into a single vars object", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { + CFG: { type: "json", value: { debug: true } }, + GREETING: { type: "text", value: "hello" }, + NUM: { type: "json", value: 42 }, + }, + }); + expect(result.vars).toEqual({ + CFG: { debug: true }, + GREETING: "hello", + NUM: 42, + }); + }); + + it("collects secret bindings into secrets.required", ({ expect }) => { + const result = convertToWranglerConfig({ + env: { + A: { type: "secret" }, + B: { type: "secret" }, + }, + }); + expect(result.secrets).toEqual({ required: ["A", "B"] }); + }); + }); + + describe("exports", () => { + it("throws when sqlite durable-object exports are present", ({ + expect, + }) => { + expect(() => + convertToWranglerConfig({ + exports: { + MyDO: { type: "durable-object", storage: "sqlite" }, + }, + }) + ).toThrow(/Durable Object exports/); + }); + + it("throws when legacy-kv durable-object exports are present", ({ + expect, + }) => { + expect(() => + convertToWranglerConfig({ + exports: { + LegacyDO: { type: "durable-object", storage: "legacy-kv" }, + }, + }) + ).toThrow(/Durable Object exports/); + }); + }); + + describe("triggers", () => { + it("maps scheduled triggers to triggers.crons", ({ expect }) => { + const result = convertToWranglerConfig({ + triggers: [ + { type: "scheduled", schedule: "0 * * * *" }, + { type: "scheduled", schedule: "*/5 * * * *" }, + ], + }); + expect(result.triggers).toEqual({ + crons: ["0 * * * *", "*/5 * * * *"], + }); + }); + + it("maps fetch trigger with dot-zone to zone_name", ({ expect }) => { + const result = convertToWranglerConfig({ + triggers: [ + { type: "fetch", pattern: "example.com/*", zone: "example.com" }, + ], + }); + expect(result.routes).toEqual([ + { pattern: "example.com/*", zone_name: "example.com" }, + ]); + }); + + it("maps fetch trigger with non-dot zone to zone_id", ({ expect }) => { + const result = convertToWranglerConfig({ + triggers: [ + { + type: "fetch", + pattern: "example.com/*", + zone: "abc123zoneid", + }, + ], + }); + expect(result.routes).toEqual([ + { pattern: "example.com/*", zone_id: "abc123zoneid" }, + ]); + }); + + it("maps fetch trigger without zone to pattern only", ({ expect }) => { + const result = convertToWranglerConfig({ + triggers: [{ type: "fetch", pattern: "*/api/*" }], + }); + expect(result.routes).toEqual(["*/api/*"]); + }); + + it("maps queue trigger to queues.consumers with snake_case fields", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + triggers: [ + { + type: "queue", + name: "q-1", + deadLetterQueue: "dlq", + maxBatchSize: 10, + maxBatchTimeout: 30, + maxConcurrency: 5, + maxRetries: 3, + retryDelay: 60, + visibilityTimeoutMs: 1000, + }, + ], + }); + expect(result.queues).toEqual({ + consumers: [ + { + queue: "q-1", + dead_letter_queue: "dlq", + max_batch_size: 10, + max_batch_timeout: 30, + max_concurrency: 5, + max_retries: 3, + retry_delay: 60, + visibility_timeout_ms: 1000, + }, + ], + }); + }); + + it("merges queue producers (from bindings) and consumers (from triggers) under a single queues object", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { Q: { type: "queue", name: "p-queue" } }, + triggers: [{ type: "queue", name: "c-queue" }], + }); + expect(result.queues).toEqual({ + producers: [{ binding: "Q", queue: "p-queue" }], + consumers: [{ queue: "c-queue" }], + }); + }); + }); + + describe("domains", () => { + it("converts each domain to a custom_domain route", ({ expect }) => { + const result = convertToWranglerConfig({ domains: ["a.com", "b.com"] }); + expect(result.routes).toEqual([ + { pattern: "a.com", custom_domain: true }, + { pattern: "b.com", custom_domain: true }, + ]); + }); + + it("appends fetch-trigger routes after domain routes", ({ expect }) => { + const result = convertToWranglerConfig({ + triggers: [{ type: "fetch", pattern: "x.com/*", zone: "x.com" }], + domains: ["y.com"], + }); + expect(result.routes).toEqual([ + { pattern: "y.com", custom_domain: true }, + { pattern: "x.com/*", zone_name: "x.com" }, + ]); + }); + }); + + describe("assets", () => { + it("converts the top-level assets block to snake_case", ({ expect }) => { + const result = convertToWranglerConfig({ + assets: { + htmlHandling: "none", + notFoundHandling: "404-page", + runWorkerFirst: ["/api/*"], + }, + }); + expect(result.assets).toEqual({ + html_handling: "none", + not_found_handling: "404-page", + run_worker_first: ["/api/*"], + }); + }); + + it("attaches the assets binding name when bindings.assets() is present", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + env: { ASSETS: { type: "assets" } }, + }); + expect(result.assets).toEqual({ binding: "ASSETS" }); + }); + + it("merges the top-level assets block with the assets binding name", ({ + expect, + }) => { + const result = convertToWranglerConfig({ + assets: { htmlHandling: "none" }, + env: { ASSETS: { type: "assets" } }, + }); + expect(result.assets).toEqual({ + binding: "ASSETS", + html_handling: "none", + }); + }); + }); + + describe("tail consumers", () => { + it("maps non-streaming consumers to tail_consumers", ({ expect }) => { + const result = convertToWranglerConfig({ + tailConsumers: [{ workerName: "tail-worker" }], + }); + expect(result.tail_consumers).toEqual([{ service: "tail-worker" }]); + expect(result.streaming_tail_consumers).toBeUndefined(); + }); + + it("maps streaming consumers to streaming_tail_consumers", ({ expect }) => { + const result = convertToWranglerConfig({ + tailConsumers: [{ workerName: "stream-worker", streaming: true }], + }); + expect(result.streaming_tail_consumers).toEqual([ + { service: "stream-worker" }, + ]); + expect(result.tail_consumers).toBeUndefined(); + }); + + it("splits a mixed list of consumers into the two arrays", ({ expect }) => { + const result = convertToWranglerConfig({ + tailConsumers: [ + { workerName: "a" }, + { workerName: "b", streaming: true }, + { workerName: "c", streaming: false }, + ], + }); + expect(result.tail_consumers).toEqual([ + { service: "a" }, + { service: "c" }, + ]); + expect(result.streaming_tail_consumers).toEqual([{ service: "b" }]); + }); + }); +}); diff --git a/packages/config/src/__tests__/generate.test.ts b/packages/config/src/__tests__/generate.test.ts new file mode 100644 index 0000000000..8ed915bb6e --- /dev/null +++ b/packages/config/src/__tests__/generate.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "vitest"; +import { generateTypes } from "../generate"; + +describe("generateTypes", () => { + it("defaults the package import to @cloudflare/config", ({ expect }) => { + const out = generateTypes({ configPath: "./cloudflare.config.ts" }); + expect(out).toContain(`from "@cloudflare/config"`); + expect(out).toContain(`import type Config from "./cloudflare.config"`); + }); + + it("accepts a custom packageName", ({ expect }) => { + const out = generateTypes({ + configPath: "./cloudflare.config.ts", + packageName: "@cloudflare/vite-plugin/experimental-config", + }); + expect(out).toContain(`from "@cloudflare/vite-plugin/experimental-config"`); + expect(out).not.toContain(`} from "@cloudflare/config"`); + }); + + it("strips .ts/.js/.mts/.mjs extensions from the config import path", ({ + expect, + }) => { + for (const ext of ["ts", "js", "mts", "mjs"]) { + const out = generateTypes({ configPath: `./cloudflare.config.${ext}` }); + expect(out).toContain(`import type Config from "./cloudflare.config"`); + } + }); +}); diff --git a/packages/config/src/__tests__/load.test.ts b/packages/config/src/__tests__/load.test.ts new file mode 100644 index 0000000000..492e431489 --- /dev/null +++ b/packages/config/src/__tests__/load.test.ts @@ -0,0 +1,226 @@ +import { spawnSync } from "node:child_process"; +import * as path from "node:path"; +import { pathToFileURL } from "node:url"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { ConfigSchema } from "../schema"; + +// Vitest's module runner intercepts dynamic imports before Node's +// `module.registerHooks` can see them, so we cannot exercise `loadConfig` +// inside a test directly. Instead, we run a small Node program in a +// subprocess that calls `loadConfig`, serialises the result as JSON, and +// prints it to stdout for the test to consume. +function runLoadConfigInSubprocess(args: { cwd: string; configPath: string }): { + config: unknown; + dependencies: string[]; +} { + // Use a file:// URL rather than a raw filesystem path so the embedded + // `import` specifier is valid on Windows (where absolute paths like + // `C:\...` are not accepted as ESM specifiers). + const sourceEntry = pathToFileURL(path.resolve(__dirname, "../load.ts")).href; + const script = ` + import { loadConfig } from ${JSON.stringify(sourceEntry)}; + const result = await loadConfig(${JSON.stringify(args.configPath)}); + const serialisable = { + config: result.config, + dependencies: [...result.dependencies], + }; + process.stdout.write(JSON.stringify(serialisable, (_, v) => { + // Module namespace objects have a null prototype; convert to plain + // objects so JSON.stringify captures their enumerable string keys. + if (v && typeof v === "object" && Object.getPrototypeOf(v) === null) { + return { ...v }; + } + return v; + })); + `; + const result = spawnSync( + process.execPath, + ["--input-type=module", "-e", script], + { cwd: args.cwd, encoding: "utf8" } + ); + if (result.status !== 0) { + throw new Error( + `Subprocess failed (status ${result.status}):\n${result.stderr}` + ); + } + return JSON.parse(result.stdout); +} + +describe("loadConfig", () => { + runInTempDir(); + + it("returns the module's default export verbatim for a plain config", async ({ + expect, + }) => { + await seed({ + "cloudflare.config.ts": `export default { name: "my-worker" };`, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + + expect(result.config).toEqual({ name: "my-worker" }); + }); + + it("passes cf-worker specifiers through verbatim without resolving or executing them", async ({ + expect, + }) => { + await seed({ + "src/index.ts": `throw new Error("entrypoint must not be executed at config load time");`, + "cloudflare.config.ts": ` + import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + export default { name: "w", entrypoint }; + `, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + + expect( + (result.config as { entrypoint: { default: string } }).entrypoint + ).toEqual({ default: "./src/index.ts" }); + // The entrypoint is referenced for its specifier only; changes to + // its source must not trigger a config reload, so it is not tracked. + expect(result.dependencies).not.toContain(path.resolve("src/index.ts")); + // The config file itself is still tracked. + expect(result.dependencies).toContain(path.resolve("cloudflare.config.ts")); + }); + + it("passes bare and virtual cf-worker specifiers through verbatim", async ({ + expect, + }) => { + await seed({ + "cloudflare.config.ts": ` + import * as bare from "@example-package/some-module" with { type: "cf-worker" }; + import * as virtual from "virtual:some-module" with { type: "cf-worker" }; + export default { name: "w", bare, virtual }; + `, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + + expect( + result.config as { + bare: { default: string }; + virtual: { default: string }; + } + ).toMatchObject({ + bare: { default: "@example-package/some-module" }, + virtual: { default: "virtual:some-module" }, + }); + }); + + it("produces an entrypoint namespace that ConfigSchema.parse collapses to a string", async ({ + expect, + }) => { + await seed({ + "src/index.ts": `// not executed`, + "cloudflare.config.ts": ` + import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + export default { name: "w", entrypoint }; + `, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + const parsed = ConfigSchema.parse(result.config); + + expect(parsed.entrypoint).toBe("./src/index.ts"); + }); + + it("reloads the config when the file changes between calls in the same process", async ({ + expect, + }) => { + await seed({ + "cloudflare.config.ts": `export default { name: "first" };`, + }); + + const sourceEntry = pathToFileURL( + path.resolve(__dirname, "../load.ts") + ).href; + const script = ` + import { writeFileSync } from "node:fs"; + import { loadConfig } from ${JSON.stringify(sourceEntry)}; + const first = await loadConfig("./cloudflare.config.ts"); + writeFileSync("./cloudflare.config.ts", 'export default { name: "second" };'); + const second = await loadConfig("./cloudflare.config.ts"); + process.stdout.write(JSON.stringify({ + first: first.config, + second: second.config, + })); + `; + const sub = spawnSync( + process.execPath, + ["--input-type=module", "-e", script], + { cwd: process.cwd(), encoding: "utf8" } + ); + if (sub.status !== 0) { + throw new Error(`Subprocess failed: ${sub.stderr}`); + } + const parsed = JSON.parse(sub.stdout) as { + first: { name: string }; + second: { name: string }; + }; + expect(parsed.first.name).toBe("first"); + expect(parsed.second.name).toBe("second"); + }); + + it("collects file paths imported while resolving the config into dependencies", async ({ + expect, + }) => { + await seed({ + "helper.ts": `export const value = 42;`, + "cloudflare.config.ts": ` + import { value } from "./helper.ts"; + export default { name: "w", value }; + `, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + + const configPath = path.resolve("cloudflare.config.ts"); + const helperPath = path.resolve("helper.ts"); + expect(result.dependencies).toContain(configPath); + expect(result.dependencies).toContain(helperPath); + }); + + it("does not track node_modules imports as dependencies", async ({ + expect, + }) => { + await seed({ + "node_modules/fake-pkg/package.json": JSON.stringify({ + name: "fake-pkg", + type: "module", + main: "./index.mjs", + }), + "node_modules/fake-pkg/index.mjs": `export const value = "from-pkg";`, + "cloudflare.config.ts": ` + import { value } from "fake-pkg"; + export default { name: "w", value }; + `, + }); + + const result = runLoadConfigInSubprocess({ + cwd: process.cwd(), + configPath: "./cloudflare.config.ts", + }); + + const pkgPath = path.resolve("node_modules/fake-pkg/index.mjs"); + expect(result.dependencies).not.toContain(pkgPath); + // Sanity: the config file itself is still tracked. + expect(result.dependencies).toContain(path.resolve("cloudflare.config.ts")); + }); +}); diff --git a/packages/config/src/__tests__/schema.test.ts b/packages/config/src/__tests__/schema.test.ts new file mode 100644 index 0000000000..afc42b7332 --- /dev/null +++ b/packages/config/src/__tests__/schema.test.ts @@ -0,0 +1,453 @@ +import { describe, it } from "vitest"; +import { ConfigSchema } from "../schema"; + +describe("env singleton bindings", () => { + it("accepts undefined env", ({ expect }) => { + const result = ConfigSchema.safeParse({}); + + expect(result.success).toBe(true); + }); + + it("accepts empty env", ({ expect }) => { + const result = ConfigSchema.safeParse({ env: {} }); + + expect(result.success).toBe(true); + }); + + it("accepts a single singleton binding of each type", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_AI: { type: "ai" }, + MY_ASSETS: { type: "assets" }, + MY_BROWSER: { type: "browser" }, + MY_IMAGES: { type: "images" }, + MY_MEDIA: { type: "media" }, + MY_STREAM: { type: "stream" }, + MY_VERSION_METADATA: { type: "version-metadata" }, + MY_WEB_SEARCH: { type: "web-search" }, + }, + }); + + expect(result.success).toBe(true); + }); + + it("accepts multiple non-singleton bindings of the same type", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + KV_1: { type: "kv" }, + KV_2: { type: "kv" }, + KV_3: { type: "kv" }, + }, + }); + + expect(result.success).toBe(true); + }); + + it("accepts multiple agent-memory bindings", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + MEM_1: { type: "agent-memory", namespace: "ns-1" }, + MEM_2: { type: "agent-memory", namespace: "ns-2" }, + }, + }); + + expect(result.success).toBe(true); + }); + + it.for([ + ["ai"], + ["assets"], + ["browser"], + ["images"], + ["media"], + ["stream"], + ["version-metadata"], + ["web-search"], + ] as const)("rejects two %s bindings", ([type], { expect }) => { + const result = ConfigSchema.safeParse({ + env: { + BINDING_1: { type }, + BINDING_2: { type }, + }, + }); + + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + `${type} bindings can only be defined once` + ); + } + }); + + it("rejects multiple duplicate singleton types with 'and' message", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + AI_1: { type: "ai" }, + AI_2: { type: "ai" }, + ASSETS_1: { type: "assets" }, + ASSETS_2: { type: "assets" }, + }, + }); + + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "ai and assets bindings can only be defined once" + ); + } + }); + + it("rejects three duplicate singleton types with oxford comma", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + AI_1: { type: "ai" }, + AI_2: { type: "ai" }, + ASSETS_1: { type: "assets" }, + ASSETS_2: { type: "assets" }, + BROWSER_1: { type: "browser" }, + BROWSER_2: { type: "browser" }, + }, + }); + + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "ai, assets, and browser bindings can only be defined once" + ); + } + }); + + it("lists duplicates alphabetically regardless of input order", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + STREAM_1: { type: "stream" }, + STREAM_2: { type: "stream" }, + AI_1: { type: "ai" }, + AI_2: { type: "ai" }, + }, + }); + + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "ai and stream bindings can only be defined once" + ); + } + }); + + it("ignores non-singleton duplicates when reporting", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + AI_1: { type: "ai" }, + AI_2: { type: "ai" }, + KV_1: { type: "kv" }, + KV_2: { type: "kv" }, + }, + }); + + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "ai bindings can only be defined once" + ); + } + }); +}); + +describe("entrypoint", () => { + it("accepts a string entrypoint and passes it through unchanged", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ entrypoint: "./src/index.ts" }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.entrypoint).toBe("./src/index.ts"); + } + }); + + it("accepts a namespace-like object and collapses it to the default export string", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + entrypoint: { default: "./src/index.ts" }, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.entrypoint).toBe("./src/index.ts"); + } + }); + + it("rejects a namespace object whose default is not a string", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + entrypoint: { default: 123 }, + }); + + expect(result.success).toBe(false); + }); + + it("rejects a namespace object missing a default export", ({ expect }) => { + const result = ConfigSchema.safeParse({ + entrypoint: { other: "value" }, + }); + + expect(result.success).toBe(false); + }); + + it("accepts an undefined entrypoint", ({ expect }) => { + const result = ConfigSchema.safeParse({}); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.entrypoint).toBeUndefined(); + } + }); +}); + +describe("unknown property rejection", () => { + it("rejects unknown top-level keys (typo)", ({ expect }) => { + const result = ConfigSchema.safeParse({ + name: "w", + // Typo: should be `compatibilityDate` + compatibilityDates: "2025-01-01", + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect(issue?.path).toEqual([]); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain( + "compatibilityDates" + ); + } + }); + + it("rejects unknown keys inside `assets`", ({ expect }) => { + const result = ConfigSchema.safeParse({ + assets: { + // Typo: should be `htmlHandling` + htmlHnadling: "none", + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect(issue?.path).toEqual(["assets"]); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain( + "htmlHnadling" + ); + } + }); + + it("rejects unknown keys inside a binding", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_KV: { + type: "kv", + // Typo: should be `id` + idd: "abc123", + }, + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect(issue?.path).toEqual(["env", "MY_KV"]); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain("idd"); + } + }); + + it("rejects unknown keys inside `observability.logs`", ({ expect }) => { + const result = ConfigSchema.safeParse({ + observability: { + logs: { + enabled: true, + // Typo: should be `headSamplingRate` + sampleRate: 0.5, + }, + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect(issue?.path).toEqual(["observability", "logs"]); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain( + "sampleRate" + ); + } + }); + + it("rejects unknown keys inside a trigger", ({ expect }) => { + const result = ConfigSchema.safeParse({ + triggers: [ + { + type: "scheduled", + schedule: "0 0 * * *", + // Typo: not a real field + cronz: "0 0 * * *", + }, + ], + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect(issue?.path).toEqual(["triggers", 0]); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain( + "cronz" + ); + } + }); + + it("still accepts unknown keys on `unsafe:*` bindings (looseObject escape hatch)", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_UNSAFE: { + type: "unsafe:some-future-runtime-feature", + unknownField: { nested: 123 }, + anotherUnknown: "ok", + }, + }, + }); + + expect(result.success).toBe(true); + }); + + it("passes the `unsafe:*` `type` through unchanged on parse", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_UNSAFE: { + type: "unsafe:ratelimit", + namespace_id: "123", + }, + }, + }); + + expect(result.success).toBe(true); + if (result.success) { + const binding = result.data.env?.MY_UNSAFE as { + type: string; + namespace_id: string; + }; + expect(binding.type).toBe("unsafe:ratelimit"); + expect(binding.namespace_id).toBe("123"); + } + }); + + it("rejects `unsafe:` (empty suffix)", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_UNSAFE: { type: "unsafe:" }, + }, + }); + + expect(result.success).toBe(false); + }); + + it("still accepts arbitrary binding names in `env` (record, not object)", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { + MY_WEIRDLY_NAMED_BINDING_1234: { type: "kv" }, + }, + }); + + expect(result.success).toBe(true); + }); +}); + +describe("vpc-network binding", () => { + it("accepts a binding with `tunnelId`", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { V: { type: "vpc-network", tunnelId: "tun-1" } }, + }); + + expect(result.success).toBe(true); + }); + + it("accepts a binding with `networkId`", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { V: { type: "vpc-network", networkId: "net-1" } }, + }); + + expect(result.success).toBe(true); + }); + + it("rejects a binding with neither `tunnelId` nor `networkId`", ({ + expect, + }) => { + const result = ConfigSchema.safeParse({ + env: { V: { type: "vpc-network" } }, + }); + + expect(result.success).toBe(false); + }); + + it("rejects a binding with both `tunnelId` and `networkId`", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + V: { type: "vpc-network", tunnelId: "tun-1", networkId: "net-1" }, + }, + }); + + expect(result.success).toBe(false); + }); + + it("rejects unknown keys on a `vpc-network` binding", ({ expect }) => { + const result = ConfigSchema.safeParse({ + env: { + V: { type: "vpc-network", tunnelId: "tun-1", unknownField: "x" }, + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find( + (i) => i.code === "unrecognized_keys" + ); + expect(issue).toBeDefined(); + expect((issue as { keys?: string[] } | undefined)?.keys).toContain( + "unknownField" + ); + } + }); +}); diff --git a/packages/config/src/bindings.ts b/packages/config/src/bindings.ts new file mode 100644 index 0000000000..b7021e4fe6 --- /dev/null +++ b/packages/config/src/bindings.ts @@ -0,0 +1,890 @@ +import type { Json } from "./utils"; +import type { PipelineRecord } from "cloudflare:pipelines"; + +// JSDoc is derived from `packages/workers-utils/src/config/environment.ts` — keep both in sync. + +// ═══════════════════════════════════════════════════════════════════════════ +// BINDING TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +interface AgentMemoryBindingOptions { + /** The user-chosen namespace name. Must exist in Cloudflare at deploy time. */ + namespace: string; + /** Whether the Agent Memory binding should be remote in local development. */ + remote?: boolean; +} + +/** + * Agent Memory namespace binding. Each binding is scoped to a namespace and + * allows agents to persist and recall memory. + */ +export interface AgentMemoryBinding extends AgentMemoryBindingOptions { + type: "agent-memory"; +} + +interface AiBindingOptions { + /** Whether the AI binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to the Workers AI project. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai + */ +export interface AiBinding extends AiBindingOptions { + type: "ai"; +} + +/** + * Binding to the Workers AI project. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai + */ +export interface TypedAiBinding< + TAiModelList extends AiModelListType = AiModels, +> extends AiBinding { + /** @internal Carries type parameters for inference */ + __typeParams: [TAiModelList]; +} + +interface AiSearchBindingOptions { + /** The user-chosen instance name. Must exist in Cloudflare at deploy time. */ + name: string; + /** Whether the AI Search instance binding should be remote in local development. */ + remote?: boolean; +} + +/** + * AI Search instance binding. Each binding is bound directly to a single + * pre-existing instance within the "default" namespace. + */ +export interface AiSearchBinding extends AiSearchBindingOptions { + type: "ai-search"; +} + +interface AiSearchNamespaceBindingOptions { + /** The user-chosen namespace name. Must exist in Cloudflare at deploy time. */ + namespace: string; + /** Whether the AI Search namespace binding should be remote in local development. */ + remote?: boolean; +} + +/** + * AI Search namespace binding. Each binding is scoped to a namespace and + * allows dynamic instance CRUD within it. + */ +export interface AiSearchNamespaceBinding extends AiSearchNamespaceBindingOptions { + type: "ai-search-namespace"; +} + +interface AnalyticsEngineDatasetBindingOptions { + /** The name of this dataset to write to. */ + name?: string; +} + +/** + * Binding to an Analytics Engine dataset. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets + */ +export interface AnalyticsEngineDatasetBinding extends AnalyticsEngineDatasetBindingOptions { + type: "analytics-engine-dataset"; +} + +interface ArtifactsBindingOptions { + /** The namespace to use. */ + namespace: string; + /** Whether to use the remote Artifacts service in local dev. */ + remote?: boolean; +} + +/** + * Binding to an Artifacts instance. Artifacts provides git-compatible file + * storage on Cloudflare Workers. + */ +export interface ArtifactsBinding extends ArtifactsBindingOptions { + type: "artifacts"; +} + +/** + * Binding to the Worker's static assets. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#assets + */ +export interface AssetsBinding { + type: "assets"; +} + +interface BrowserBindingOptions { + /** Whether the Browser binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a headless browser usable from the Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering + */ +export interface BrowserBinding extends BrowserBindingOptions { + type: "browser"; +} + +interface D1BindingOptions { + /** The UUID of this D1 database (not required). */ + id?: string; + /** The name of this D1 database. */ + name?: string; + /** Whether the D1 database should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a D1 database. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases + */ +export interface D1Binding extends D1BindingOptions { + type: "d1"; +} + +interface DispatchNamespaceBindingOptions { + /** The namespace to bind to. */ + namespace: string; + /** Details about the outbound Worker which will handle outbound requests from your namespace. */ + outbound?: { + /** Name of the Worker handling the outbound requests. */ + workerName: string; + /** (Optional) List of parameter names, for sending context from your dispatch Worker to the outbound handler. */ + parameters?: string[]; + }; + /** Whether the Dispatch Namespace should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a Workers for Platforms dispatch namespace. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms + */ +export interface DispatchNamespaceBinding extends DispatchNamespaceBindingOptions { + type: "dispatch-namespace"; +} + +interface DurableObjectBindingOptions { + /** The name of the Worker that defines the Durable Object class. */ + workerName: string; + /** The exported class name of the Durable Object. */ + exportName: string; +} + +/** + * Binding to a Durable Object class. `workerName` is the name of the Worker + * that defines the class; `exportName` is the exported class name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ +export interface DurableObjectBinding extends DurableObjectBindingOptions { + type: "durable-object"; +} + +/** + * Binding to a Durable Object class. `workerName` is the name of the Worker + * that defines the class; `exportName` is the exported class name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ +export interface TypedDurableObjectBinding< + TConfig, + TExportName extends string, +> extends DurableObjectBinding { + workerName: string; + exportName: TExportName; + /** @internal Carries the config type for inference */ + __config: TConfig; +} + +interface FlagshipBindingOptions { + /** The Flagship app ID to bind to. */ + id: string; + /** Set to `true` to suppress the remote binding warning in local dev. Flagship bindings are always remote. */ + remote?: boolean; +} + +/** Binding to a Flagship feature-flag service. */ +export interface FlagshipBinding extends FlagshipBindingOptions { + type: "flagship"; +} + +interface HyperdriveBindingOptions { + /** The ID of the Hyperdrive configuration. */ + id: string; + /** The local database connection string used during local development. */ + localConnectionString?: string; +} + +/** + * Binding to a Hyperdrive configuration. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive + */ +export interface HyperdriveBinding extends HyperdriveBindingOptions { + type: "hyperdrive"; +} + +interface ImagesBindingOptions { + /** Whether the Images binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to Cloudflare Images. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#images + */ +export interface ImagesBinding extends ImagesBindingOptions { + type: "images"; +} + +/** + * Inline JSON value made available to the Worker on `env` under the + * binding name. + */ +export interface JsonBinding { + type: "json"; + /** The JSON value made available to the Worker. */ + value: T; +} + +interface KvBindingOptions { + /** The ID of the KV namespace. */ + id?: string; + // TODO: name support not yet implemented + // name?: string; + /** Whether the KV namespace should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a Workers KV namespace. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces + */ +export interface KvBinding extends KvBindingOptions { + type: "kv"; +} + +/** + * Binding to a Workers KV namespace. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces + */ +export interface TypedKvBinding< + TKey extends string = string, +> extends KvBinding { + /** @internal Carries type parameters for inference */ + __typeParams: [TKey]; +} + +interface LogfwdrBindingOptions { + /** The destination for this logged message. */ + destination: string; +} + +/** Binding for forwarding logs to logfwdr. */ +export interface LogfwdrBinding extends LogfwdrBindingOptions { + type: "logfwdr"; +} + +interface MediaBindingOptions { + /** Whether the Media binding should be remote or not. */ + remote?: boolean; +} + +/** Binding to Cloudflare Media Transformations. */ +export interface MediaBinding extends MediaBindingOptions { + type: "media"; +} + +interface MtlsCertificateBindingOptions { + /** The UUID of the uploaded mTLS certificate. */ + id: string; + /** Whether the mTLS fetcher should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to an uploaded mTLS certificate. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates + */ +export interface MtlsCertificateBinding extends MtlsCertificateBindingOptions { + type: "mtls-certificate"; +} + +interface PipelineBindingOptions { + /** Name of the Pipeline to bind. */ + name: string; + /** Whether the pipeline should be remote or not in local development. */ + remote?: boolean; +} + +/** Binding to a Cloudflare Pipeline. */ +export interface PipelineBinding extends PipelineBindingOptions { + type: "pipeline"; +} + +/** Binding to a Cloudflare Pipeline. */ +export interface TypedPipelineBinding< + TRecord extends PipelineRecord = PipelineRecord, +> extends PipelineBinding { + /** @internal Carries type parameters for inference */ + __typeParams: [TRecord]; +} + +interface QueueBindingOptions { + /** The name of this Queue. */ + name: string; + /** The number of seconds to wait before delivering a message. */ + deliveryDelay?: number; + /** Whether the Queue producer should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Producer binding to a Cloudflare Queue. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#queues + */ +export interface QueueBinding extends QueueBindingOptions { + type: "queue"; +} + +/** + * Producer binding to a Cloudflare Queue. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#queues + */ +export interface TypedQueueBinding extends QueueBinding { + /** @internal Carries type parameters for inference */ + __typeParams: [TBody]; +} + +interface R2BindingOptions { + /** The name of this R2 bucket at the edge. */ + name?: string; + /** The jurisdiction that the bucket exists in. Default if not present. */ + jurisdiction?: string; + /** Whether the R2 bucket should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to an R2 bucket. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets + */ +export interface R2Binding extends R2BindingOptions { + type: "r2"; +} + +interface RateLimitBindingOptions { + /** The namespace ID for this rate limiter. */ + namespace: string; + /** Simple rate limiting configuration. */ + simple: { + /** The maximum number of requests allowed in the time period. */ + limit: number; + /** The time period in seconds (10 for ten seconds, 60 for one minute). */ + period: 10 | 60; + }; +} + +/** Binding to a rate limiter. */ +export interface RateLimitBinding extends RateLimitBindingOptions { + type: "rate-limit"; +} + +/** + * Declares a secret that is required by your Worker, exposed on `env` under + * the binding name. + * + * When defined, this binding: + * - Replaces .dev.vars/.env/process.env inference for type generation + * - Enables local dev validation with warnings for missing secrets + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property + */ +export interface SecretBinding { + type: "secret"; +} + +interface SecretsStoreSecretBindingOptions { + /** ID of the secret store. */ + storeId: string; + /** Name of the secret. */ + secretName: string; +} + +/** Binding to a Secrets Store secret. */ +export interface SecretsStoreSecretBinding extends SecretsStoreSecretBindingOptions { + type: "secrets-store-secret"; +} + +interface SendEmailBindingOptions { + /** If this binding should be restricted to a specific verified address. */ + destinationAddress?: string; + /** If this binding should be restricted to a set of verified addresses. */ + allowedDestinationAddresses?: string[]; + /** If this binding should be restricted to a set of sender addresses. */ + allowedSenderAddresses?: string[]; + /** Whether the binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding for sending email from inside the Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#email-bindings + */ +export interface SendEmailBinding extends SendEmailBindingOptions { + type: "send-email"; +} + +interface StreamBindingOptions { + /** Whether the Stream binding should be remote or not in local development. */ + remote?: boolean; +} + +/** Binding to Cloudflare Stream. */ +export interface StreamBinding extends StreamBindingOptions { + type: "stream"; +} + +/** + * Inline string value made available to the Worker on `env` under the + * binding name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ +export interface TextBinding { + type: "text"; + /** The string value made available to the Worker. */ + value: T; +} + +interface UnsafeBindingOptions { + /** Local-dev plugin configuration for this unsafe binding. */ + dev?: { + /** The plugin package that provides the binding's local-dev implementation. */ + plugin: { + package: string; + name: string; + }; + /** Plugin-specific options. */ + options?: Record; + }; + [key: string]: unknown; +} + +/** + * Escape-hatch binding for runtime features that aren't directly supported + * by this configuration. Included in the Worker's upload metadata without + * changes. + */ +export interface UnsafeBinding extends UnsafeBindingOptions { + type: `unsafe:${string}`; +} + +interface VectorizeBindingOptions { + /** The name of the Vectorize index. */ + name: string; + /** Whether the Vectorize index should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a Vectorize index. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes + */ +export interface VectorizeBinding extends VectorizeBindingOptions { + type: "vectorize"; +} + +/** Binding to the Worker version's metadata. */ +export interface VersionMetadataBinding { + type: "version-metadata"; +} + +type VpcNetworkBindingOptions = + | { + /** The tunnel ID of the Cloudflare Tunnel to route traffic through. Mutually exclusive with `networkId`. */ + tunnelId: string; + /** Whether the VPC network is remote or not. */ + remote?: boolean; + } + | { + /** The network ID to route traffic through. Mutually exclusive with `tunnelId`. */ + networkId: string; + /** Whether the VPC network is remote or not. */ + remote?: boolean; + }; + +/** Binding to a VPC network. */ +export type VpcNetworkBinding = VpcNetworkBindingOptions & { + type: "vpc-network"; +}; + +interface VpcServiceBindingOptions { + /** The service ID of the VPC connectivity service. */ + id: string; + /** Whether the VPC service is remote or not. */ + remote?: boolean; +} + +/** Binding to a VPC service. */ +export interface VpcServiceBinding extends VpcServiceBindingOptions { + type: "vpc-service"; +} + +interface WebSearchBindingOptions { + /** Whether the Web Search binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Cloudflare Web Search binding. There is exactly one shared web corpus, so + * the binding is zero-config — only the variable name is required. + */ +export interface WebSearchBinding extends WebSearchBindingOptions { + type: "web-search"; +} + +interface WorkerBindingOptions { + /** The name of the bound Worker. */ + workerName: string; + /** The named export to bind to (defaults to the default export). */ + exportName?: string; + /** Optional properties that will be made available to the service via `ctx.props`. */ + props?: Record; + /** Whether the service binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Service binding (Worker-to-Worker). `workerName` is the name of the bound + * Worker; `exportName` selects a named `WorkerEntrypoint` export (defaults to + * the default export). + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ +export interface WorkerBinding extends WorkerBindingOptions { + type: "worker"; +} + +/** + * Service binding (Worker-to-Worker). `workerName` is the name of the bound + * Worker; `exportName` selects a named `WorkerEntrypoint` export (defaults to + * the default export). + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ +export interface TypedWorkerBinding< + TConfig, + TExportName extends string, +> extends WorkerBinding { + workerName: string; + exportName: TExportName; + /** @internal Carries the config type for inference */ + __config: TConfig; +} + +/** Binding to a Worker Loader. */ +export interface WorkerLoaderBinding { + type: "worker-loader"; +} + +interface WorkflowBindingOptions { + /** The name of the Worker that defines the Workflow. */ + workerName: string; + /** The exported class name of the Workflow. */ + exportName: string; + /** Whether the Workflow binding should be remote or not in local development. */ + remote?: boolean; +} + +/** + * Binding to a Workflow. `workerName` is the name of the Worker that defines + * the Workflow; `exportName` is the exported `WorkflowEntrypoint` class name. + */ +export interface WorkflowBinding extends WorkflowBindingOptions { + type: "workflow"; +} + +/** + * Binding to a Workflow. `workerName` is the name of the Worker that defines + * the Workflow; `exportName` is the exported `WorkflowEntrypoint` class name. + */ +export interface TypedWorkflowBinding< + TConfig, + TExportName extends string, +> extends WorkflowBinding { + workerName: string; + exportName: TExportName; + /** @internal Carries the config type for inference */ + __config: TConfig; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// BINDINGS API +// ═══════════════════════════════════════════════════════════════════════════ + +export interface Bindings { + /** + * Agent Memory namespace binding. Each binding is scoped to a namespace and + * allows agents to persist and recall memory. + */ + agentMemory(options: AgentMemoryBindingOptions): AgentMemoryBinding; + /** + * Binding to the Workers AI project. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai + */ + ai( + options?: AiBindingOptions + ): TypedAiBinding; + /** + * AI Search instance binding. Each binding is bound directly to a single + * pre-existing instance within the "default" namespace. + */ + aiSearch(options: AiSearchBindingOptions): AiSearchBinding; + /** + * AI Search namespace binding. Each binding is scoped to a namespace and + * allows dynamic instance CRUD within it. + */ + aiSearchNamespace( + options: AiSearchNamespaceBindingOptions + ): AiSearchNamespaceBinding; + /** + * Binding to an Analytics Engine dataset. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets + */ + analyticsEngineDataset( + options?: AnalyticsEngineDatasetBindingOptions + ): AnalyticsEngineDatasetBinding; + /** + * Binding to an Artifacts instance. Artifacts provides git-compatible file + * storage on Cloudflare Workers. + */ + artifacts(options: ArtifactsBindingOptions): ArtifactsBinding; + /** + * Binding to the Worker's static assets. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#assets + */ + assets(): AssetsBinding; + /** + * Binding to a headless browser usable from the Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering + */ + browser(options?: BrowserBindingOptions): BrowserBinding; + /** + * Binding to a D1 database. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases + */ + d1(options?: D1BindingOptions): D1Binding; + /** + * Binding to a Workers for Platforms dispatch namespace. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms + */ + dispatchNamespace( + options: DispatchNamespaceBindingOptions + ): DispatchNamespaceBinding; + /** + * Binding to a Durable Object class. `workerName` is the name of the Worker + * that defines the class; `exportName` is the exported class name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ + durableObject(options: DurableObjectBindingOptions): DurableObjectBinding; + /** Binding to a Flagship feature-flag service. */ + flagship(options: FlagshipBindingOptions): FlagshipBinding; + /** + * Binding to a Hyperdrive configuration. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive + */ + hyperdrive(options: HyperdriveBindingOptions): HyperdriveBinding; + /** + * Binding to Cloudflare Images. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#images + */ + images(options?: ImagesBindingOptions): ImagesBinding; + /** + * Inline JSON value made available to the Worker on `env` under the + * binding name. + */ + json(value: T): JsonBinding; + /** + * Binding to a Workers KV namespace. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces + */ + kv( + options?: KvBindingOptions + ): TypedKvBinding; + /** Binding for forwarding logs to logfwdr. */ + logfwdr(options: LogfwdrBindingOptions): LogfwdrBinding; + /** Binding to Cloudflare Media Transformations. */ + media(options?: MediaBindingOptions): MediaBinding; + /** + * Binding to an uploaded mTLS certificate. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates + */ + mtlsCertificate( + options: MtlsCertificateBindingOptions + ): MtlsCertificateBinding; + /** Binding to a Cloudflare Pipeline. */ + pipeline( + options: PipelineBindingOptions + ): TypedPipelineBinding; + /** + * Producer binding to a Cloudflare Queue. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#queues + */ + queue( + options: QueueBindingOptions + ): TypedQueueBinding; + /** + * Binding to an R2 bucket. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets + */ + r2(options?: R2BindingOptions): R2Binding; + /** Binding to a rate limiter. */ + rateLimit(options: RateLimitBindingOptions): RateLimitBinding; + /** + * Declares a secret that is required by your Worker, exposed on `env` under + * the binding name. + * + * When defined, this binding: + * - Replaces .dev.vars/.env/process.env inference for type generation + * - Enables local dev validation with warnings for missing secrets + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property + */ + secret(): SecretBinding; + /** Binding to a Secrets Store secret. */ + secretsStoreSecret( + options: SecretsStoreSecretBindingOptions + ): SecretsStoreSecretBinding; + /** + * Binding for sending email from inside the Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#email-bindings + */ + sendEmail(options?: SendEmailBindingOptions): SendEmailBinding; + /** Binding to Cloudflare Stream. */ + stream(options?: StreamBindingOptions): StreamBinding; + /** + * Inline string value made available to the Worker on `env` under the + * binding name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + text(value: T): TextBinding; + /** + * Binding to a Vectorize index. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes + */ + vectorize(options: VectorizeBindingOptions): VectorizeBinding; + /** Binding to the Worker version's metadata. */ + versionMetadata(): VersionMetadataBinding; + /** Binding to a VPC network. */ + vpcNetwork(options: VpcNetworkBindingOptions): VpcNetworkBinding; + /** Binding to a VPC service. */ + vpcService(options: VpcServiceBindingOptions): VpcServiceBinding; + /** + * Cloudflare Web Search binding. There is exactly one shared web corpus, so + * the binding is zero-config — only the variable name is required. + */ + webSearch(options?: WebSearchBindingOptions): WebSearchBinding; + /** + * Service binding (Worker-to-Worker). `workerName` is the name of the bound + * Worker; `exportName` selects a named `WorkerEntrypoint` export (defaults to + * the default export). + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + worker(options: WorkerBindingOptions): WorkerBinding; + /** Binding to a Worker Loader. */ + workerLoader(): WorkerLoaderBinding; + // TODO: re-enable when workflow bindings return. + // /** + // * Create a Workflow binding. + // * `workerName` must match a known config's name (or any `string` for untyped bindings). + // * `exportName` must be a valid `WorkflowEntrypoint` export for the given Worker. + // */ + // workflow(options: WorkflowBindingOptions): WorkflowBinding; +} + +export const bindings = { + agentMemory: (options) => ({ type: "agent-memory", ...options }), + ai: (options) => ({ type: "ai", ...options }), + aiSearch: (options) => ({ type: "ai-search", ...options }), + aiSearchNamespace: (options) => ({ + type: "ai-search-namespace", + ...options, + }), + analyticsEngineDataset: (options) => ({ + type: "analytics-engine-dataset", + ...options, + }), + artifacts: (options) => ({ type: "artifacts", ...options }), + assets: () => ({ type: "assets" }), + browser: (options) => ({ type: "browser", ...options }), + d1: (options) => ({ type: "d1", ...options }), + dispatchNamespace: (options) => ({ + type: "dispatch-namespace", + ...options, + }), + durableObject: (options) => ({ type: "durable-object", ...options }), + flagship: (options) => ({ type: "flagship", ...options }), + hyperdrive: (options) => ({ type: "hyperdrive", ...options }), + images: (options) => ({ type: "images", ...options }), + json: (value) => ({ type: "json", value }), + kv: (options) => ({ type: "kv", ...options }), + logfwdr: (options) => ({ type: "logfwdr", ...options }), + media: (options) => ({ type: "media", ...options }), + mtlsCertificate: (options) => ({ type: "mtls-certificate", ...options }), + pipeline: (options) => ({ type: "pipeline", ...options }), + queue: (options) => ({ type: "queue", ...options }), + rateLimit: (options) => ({ type: "rate-limit", ...options }), + r2: (options) => ({ type: "r2", ...options }), + secret: () => ({ type: "secret" }), + secretsStoreSecret: (options) => ({ + type: "secrets-store-secret", + ...options, + }), + sendEmail: (options) => ({ type: "send-email", ...options }), + stream: (options) => ({ type: "stream", ...options }), + text: (value) => ({ type: "text", value }), + vectorize: (options) => ({ type: "vectorize", ...options }), + versionMetadata: () => ({ type: "version-metadata" }), + vpcService: (options) => ({ type: "vpc-service", ...options }), + vpcNetwork: (options) => ({ type: "vpc-network", ...options }), + webSearch: (options) => ({ type: "web-search", ...options }), + worker: (options) => ({ type: "worker", ...options }), + workerLoader: () => ({ type: "worker-loader" }), + // TODO: re-enable when workflow bindings return. + // workflow: (options) => ({ type: "workflow", ...options }), +} as Bindings; diff --git a/packages/config/src/convert.ts b/packages/config/src/convert.ts new file mode 100644 index 0000000000..386a178056 --- /dev/null +++ b/packages/config/src/convert.ts @@ -0,0 +1,790 @@ +import { isParsedUnsafeBinding } from "./schema"; +import type { ParsedConfig } from "./schema"; +import type { Json } from "./utils"; +import type { RawConfig } from "@cloudflare/workers-utils"; + +/** + * Convert a parsed `@cloudflare/config` config into a Wrangler `RawConfig`. + * + * The caller is responsible for unwrapping any function/promise wrapper around + * the config and validating it against `ConfigSchema` before passing it in. + * + * @param config The parsed (post-validation) config. + * @returns The corresponding Wrangler `RawConfig`. + */ +export function convertToWranglerConfig(config: ParsedConfig): RawConfig { + const result: RawConfig = {}; + + convertTopLevel(config, result); + convertBindingsAndAssets(config, result); + convertExports(config, result); + convertDomains(config, result); + convertTriggers(config, result); + convertTailConsumers(config, result); + + return result; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TOP-LEVEL FIELDS +// ═══════════════════════════════════════════════════════════════════════════ + +function convertTopLevel(config: ParsedConfig, result: RawConfig): void { + if (config.name !== undefined) { + result.name = config.name; + } + if (typeof config.entrypoint === "string") { + result.main = config.entrypoint; + } + if (config.accountId !== undefined) { + result.account_id = config.accountId; + } + if (config.compatibilityDate !== undefined) { + result.compatibility_date = config.compatibilityDate; + } + if (config.compatibilityFlags !== undefined) { + result.compatibility_flags = config.compatibilityFlags; + } + if (config.workersDev !== undefined) { + result.workers_dev = config.workersDev; + } + if (config.previewUrls !== undefined) { + result.preview_urls = config.previewUrls; + } + if (config.logpush !== undefined) { + result.logpush = config.logpush; + } + if (config.complianceRegion !== undefined) { + result.compliance_region = + config.complianceRegion === "fedramp-high" ? "fedramp_high" : "public"; + } + if (config.firstPartyWorker !== undefined) { + result.first_party_worker = config.firstPartyWorker; + } + if (config.placement !== undefined) { + // `placement` shapes match 1:1 between the two configs. + result.placement = config.placement; + } + if (config.limits !== undefined) { + const limits: NonNullable = {}; + if (config.limits.cpuMs !== undefined) { + limits.cpu_ms = config.limits.cpuMs; + } + if (config.limits.subrequests !== undefined) { + limits.subrequests = config.limits.subrequests; + } + result.limits = limits; + } + if (config.observability !== undefined) { + result.observability = convertObservability(config.observability); + } + if (config.cache !== undefined) { + result.cache = { enabled: config.cache.enabled }; + } + if (config.unsafe !== undefined) { + result.unsafe = convertUnsafeTopLevel(config.unsafe); + } +} + +function convertObservability( + observability: NonNullable +): NonNullable { + const out: NonNullable = {}; + if (observability.enabled !== undefined) { + out.enabled = observability.enabled; + } + if (observability.headSamplingRate !== undefined) { + out.head_sampling_rate = observability.headSamplingRate; + } + if (observability.logs !== undefined) { + const logs: NonNullable["logs"]> = + {}; + const { logs: src } = observability; + if (src.enabled !== undefined) { + logs.enabled = src.enabled; + } + if (src.headSamplingRate !== undefined) { + logs.head_sampling_rate = src.headSamplingRate; + } + if (src.invocationLogs !== undefined) { + logs.invocation_logs = src.invocationLogs; + } + if (src.persist !== undefined) { + logs.persist = src.persist; + } + if (src.destinations !== undefined) { + logs.destinations = src.destinations; + } + out.logs = logs; + } + if (observability.traces !== undefined) { + const traces: NonNullable< + NonNullable["traces"] + > = {}; + const { traces: src } = observability; + if (src.enabled !== undefined) { + traces.enabled = src.enabled; + } + if (src.headSamplingRate !== undefined) { + traces.head_sampling_rate = src.headSamplingRate; + } + if (src.persist !== undefined) { + traces.persist = src.persist; + } + if (src.destinations !== undefined) { + traces.destinations = src.destinations; + } + out.traces = traces; + } + return out; +} + +function convertUnsafeTopLevel( + unsafe: NonNullable +): NonNullable { + const out: NonNullable = {}; + if (unsafe.metadata !== undefined) { + out.metadata = unsafe.metadata; + } + if (unsafe.capnp !== undefined) { + if ("compiledSchema" in unsafe.capnp && unsafe.capnp.compiledSchema) { + out.capnp = { compiled_schema: unsafe.capnp.compiledSchema }; + } else if ("basePath" in unsafe.capnp && unsafe.capnp.basePath) { + out.capnp = { + base_path: unsafe.capnp.basePath, + source_schemas: unsafe.capnp.sourceSchemas ?? [], + }; + } + } + return out; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// BINDINGS + ASSETS +// ═══════════════════════════════════════════════════════════════════════════ + +function convertBindingsAndAssets( + config: ParsedConfig, + result: RawConfig +): void { + let assetsBindingName: string | undefined; + + const env = config.env ?? {}; + + // Accumulators for array bindings. + const kvNamespaces: NonNullable = []; + const d1Databases: NonNullable = []; + const r2Buckets: NonNullable = []; + const vectorize: NonNullable = []; + const mtlsCertificates: NonNullable = []; + const hyperdrive: NonNullable = []; + const pipelines: NonNullable = []; + const flagship: NonNullable = []; + const aiSearch: NonNullable = []; + const aiSearchNamespaces: NonNullable = []; + const agentMemory: NonNullable = []; + const analyticsEngineDatasets: NonNullable< + RawConfig["analytics_engine_datasets"] + > = []; + const artifacts: NonNullable = []; + const dispatchNamespaces: NonNullable = []; + const secretsStoreSecrets: NonNullable = + []; + const sendEmail: NonNullable = []; + const vpcServices: NonNullable = []; + const vpcNetworks: NonNullable = []; + const workerLoaders: NonNullable = []; + const ratelimits: NonNullable = []; + const services: NonNullable = []; + const durableObjectBindings: NonNullable< + NonNullable["bindings"] + > = []; + const workflows: NonNullable = []; + const queueProducers: NonNullable< + NonNullable["producers"] + > = []; + const logfwdrBindings: NonNullable< + NonNullable["bindings"] + > = []; + const unsafeBindings: NonNullable< + NonNullable["bindings"] + > = []; + const vars: Record = {}; + const secretsRequired: string[] = []; + + for (const [name, binding] of Object.entries(env)) { + if (isParsedUnsafeBinding(binding)) { + unsafeBindings.push({ + ...binding, + name, + type: binding.type.slice("unsafe:".length), + }); + continue; + } + + switch (binding.type) { + case "agent-memory": { + agentMemory.push( + omitUndefined({ + binding: name, + namespace: binding.namespace, + remote: binding.remote, + }) + ); + break; + } + case "ai": { + result.ai = omitUndefined({ binding: name, remote: binding.remote }); + break; + } + case "ai-search": { + aiSearch.push( + omitUndefined({ + binding: name, + instance_name: binding.name, + remote: binding.remote, + }) + ); + break; + } + case "ai-search-namespace": { + aiSearchNamespaces.push( + omitUndefined({ + binding: name, + namespace: binding.namespace, + remote: binding.remote, + }) + ); + break; + } + case "analytics-engine-dataset": { + analyticsEngineDatasets.push( + omitUndefined({ binding: name, dataset: binding.name }) + ); + break; + } + case "artifacts": { + artifacts.push( + omitUndefined({ + binding: name, + namespace: binding.namespace, + remote: binding.remote, + }) + ); + break; + } + case "assets": { + assetsBindingName = name; + break; + } + case "browser": { + result.browser = omitUndefined({ + binding: name, + remote: binding.remote, + }); + break; + } + case "d1": { + d1Databases.push( + omitUndefined({ + binding: name, + database_id: binding.id, + database_name: binding.name, + remote: binding.remote, + }) + ); + break; + } + case "dispatch-namespace": { + const entry: (typeof dispatchNamespaces)[number] = { + binding: name, + namespace: binding.namespace, + }; + if (binding.outbound) { + entry.outbound = omitUndefined({ + service: binding.outbound.workerName, + parameters: binding.outbound.parameters, + }); + } + if (binding.remote !== undefined) { + entry.remote = binding.remote; + } + dispatchNamespaces.push(entry); + break; + } + case "durable-object": { + durableObjectBindings.push({ + name, + class_name: binding.exportName, + script_name: binding.workerName, + }); + break; + } + case "flagship": { + flagship.push( + omitUndefined({ + binding: name, + app_id: binding.id, + remote: binding.remote, + }) + ); + break; + } + case "hyperdrive": { + hyperdrive.push( + omitUndefined({ + binding: name, + id: binding.id, + localConnectionString: binding.localConnectionString, + }) + ); + break; + } + case "images": { + result.images = omitUndefined({ + binding: name, + remote: binding.remote, + }); + break; + } + case "json": { + vars[name] = binding.value as Json; + break; + } + case "kv": { + kvNamespaces.push( + omitUndefined({ + binding: name, + id: binding.id, + remote: binding.remote, + }) + ); + break; + } + case "logfwdr": { + logfwdrBindings.push({ name, destination: binding.destination }); + break; + } + case "media": { + result.media = omitUndefined({ + binding: name, + remote: binding.remote, + }); + break; + } + case "mtls-certificate": { + mtlsCertificates.push( + omitUndefined({ + binding: name, + certificate_id: binding.id, + remote: binding.remote, + }) + ); + break; + } + case "pipeline": { + pipelines.push( + omitUndefined({ + binding: name, + stream: binding.name, + remote: binding.remote, + }) + ); + break; + } + case "queue": { + queueProducers.push( + omitUndefined({ + binding: name, + queue: binding.name, + delivery_delay: binding.deliveryDelay, + remote: binding.remote, + }) + ); + break; + } + case "rate-limit": { + ratelimits.push({ + name, + namespace_id: binding.namespace, + simple: binding.simple, + }); + break; + } + case "r2": { + r2Buckets.push( + omitUndefined({ + binding: name, + bucket_name: binding.name, + jurisdiction: binding.jurisdiction, + remote: binding.remote, + }) + ); + break; + } + case "secret": { + secretsRequired.push(name); + break; + } + case "secrets-store-secret": { + secretsStoreSecrets.push({ + binding: name, + store_id: binding.storeId, + secret_name: binding.secretName, + }); + break; + } + case "send-email": { + sendEmail.push( + omitUndefined({ + name, + destination_address: binding.destinationAddress, + allowed_destination_addresses: binding.allowedDestinationAddresses, + allowed_sender_addresses: binding.allowedSenderAddresses, + remote: binding.remote, + }) + ); + break; + } + case "stream": { + result.stream = omitUndefined({ + binding: name, + remote: binding.remote, + }); + break; + } + case "text": { + vars[name] = binding.value; + break; + } + case "vectorize": { + vectorize.push( + omitUndefined({ + binding: name, + index_name: binding.name, + remote: binding.remote, + }) + ); + break; + } + case "version-metadata": { + result.version_metadata = { binding: name }; + break; + } + case "vpc-service": { + vpcServices.push( + omitUndefined({ + binding: name, + service_id: binding.id, + remote: binding.remote, + }) + ); + break; + } + case "vpc-network": { + // The schema's `superRefine` guarantees exactly one of `tunnelId` + // or `networkId` is defined, so an `else` branch on the + // `tunnelId` check is sufficient. + if (binding.tunnelId !== undefined) { + vpcNetworks.push( + omitUndefined({ + binding: name, + tunnel_id: binding.tunnelId, + remote: binding.remote, + }) + ); + } else if (binding.networkId !== undefined) { + vpcNetworks.push( + omitUndefined({ + binding: name, + network_id: binding.networkId, + remote: binding.remote, + }) + ); + } + break; + } + case "web-search": { + result.websearch = omitUndefined({ + binding: name, + remote: binding.remote, + }); + break; + } + case "worker": { + services.push( + omitUndefined({ + binding: name, + service: binding.workerName, + entrypoint: binding.exportName, + props: binding.props, + remote: binding.remote, + }) + ); + break; + } + case "worker-loader": { + workerLoaders.push({ binding: name }); + break; + } + // TODO: re-enable when workflow bindings return. + // case "workflow": { + // workflows.push( + // omitUndefined({ + // binding: name, + // class_name: binding.exportName, + // script_name: binding.workerName, + // remote: binding.remote, + // }) + // ); + // break; + // } + } + } + + // Attach accumulated array bindings if non-empty. + if (kvNamespaces.length) { + result.kv_namespaces = kvNamespaces; + } + if (d1Databases.length) { + result.d1_databases = d1Databases; + } + if (r2Buckets.length) { + result.r2_buckets = r2Buckets; + } + if (vectorize.length) { + result.vectorize = vectorize; + } + if (mtlsCertificates.length) { + result.mtls_certificates = mtlsCertificates; + } + if (hyperdrive.length) { + result.hyperdrive = hyperdrive; + } + if (pipelines.length) { + result.pipelines = pipelines; + } + if (flagship.length) { + result.flagship = flagship; + } + if (aiSearch.length) { + result.ai_search = aiSearch; + } + if (aiSearchNamespaces.length) { + result.ai_search_namespaces = aiSearchNamespaces; + } + if (agentMemory.length) { + result.agent_memory = agentMemory; + } + if (analyticsEngineDatasets.length) { + result.analytics_engine_datasets = analyticsEngineDatasets; + } + if (artifacts.length) { + result.artifacts = artifacts; + } + if (dispatchNamespaces.length) { + result.dispatch_namespaces = dispatchNamespaces; + } + if (secretsStoreSecrets.length) { + result.secrets_store_secrets = secretsStoreSecrets; + } + if (sendEmail.length) { + result.send_email = sendEmail; + } + if (vpcServices.length) { + result.vpc_services = vpcServices; + } + if (vpcNetworks.length) { + result.vpc_networks = vpcNetworks; + } + if (workerLoaders.length) { + result.worker_loaders = workerLoaders; + } + if (ratelimits.length) { + result.ratelimits = ratelimits; + } + if (services.length) { + result.services = services; + } + if (durableObjectBindings.length) { + result.durable_objects = { bindings: durableObjectBindings }; + } + if (workflows.length) { + result.workflows = workflows; + } + if (queueProducers.length) { + result.queues = { ...(result.queues ?? {}), producers: queueProducers }; + } + if (logfwdrBindings.length) { + result.logfwdr = { bindings: logfwdrBindings }; + } + if (unsafeBindings.length) { + result.unsafe = { ...(result.unsafe ?? {}), bindings: unsafeBindings }; + } + if (Object.keys(vars).length) { + result.vars = vars; + } + if (secretsRequired.length) { + result.secrets = { required: secretsRequired }; + } + + // Merge top-level `assets` config with the assets binding name. + if (config.assets !== undefined || assetsBindingName !== undefined) { + const assets: NonNullable = {}; + if (assetsBindingName !== undefined) { + assets.binding = assetsBindingName; + } + if (config.assets?.htmlHandling !== undefined) { + assets.html_handling = config.assets.htmlHandling; + } + if (config.assets?.notFoundHandling !== undefined) { + assets.not_found_handling = config.assets.notFoundHandling; + } + if (config.assets?.runWorkerFirst !== undefined) { + assets.run_worker_first = config.assets.runWorkerFirst; + } + result.assets = assets; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// EXPORTS (Durable Objects + Workflows) +// ═══════════════════════════════════════════════════════════════════════════ + +function convertExports(config: ParsedConfig, _result: RawConfig): void { + const exports = config.exports; + if (!exports) { + return; + } + + for (const value of Object.values(exports)) { + if (value.type === "durable-object") { + throw new Error("Durable Object exports are not currently supported."); + } + // TODO: support Workflows + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TRIGGERS (scheduled + fetch + queue consumer) +// ═══════════════════════════════════════════════════════════════════════════ + +function convertTriggers(config: ParsedConfig, result: RawConfig): void { + const triggers = config.triggers; + if (!triggers || triggers.length === 0) { + return; + } + + const crons: string[] = []; + const routes: NonNullable = result.routes + ? [...result.routes] + : []; + const queueConsumers: NonNullable< + NonNullable["consumers"] + > = result.queues?.consumers ? [...result.queues.consumers] : []; + + for (const trigger of triggers) { + switch (trigger.type) { + case "scheduled": { + crons.push(trigger.schedule); + break; + } + case "fetch": { + if (trigger.zone === undefined) { + routes.push(trigger.pattern); + } else if (trigger.zone.includes(".")) { + routes.push({ pattern: trigger.pattern, zone_name: trigger.zone }); + } else { + routes.push({ pattern: trigger.pattern, zone_id: trigger.zone }); + } + break; + } + case "queue": { + queueConsumers.push( + omitUndefined({ + queue: trigger.name, + dead_letter_queue: trigger.deadLetterQueue, + max_batch_size: trigger.maxBatchSize, + max_batch_timeout: trigger.maxBatchTimeout, + max_concurrency: trigger.maxConcurrency, + max_retries: trigger.maxRetries, + retry_delay: trigger.retryDelay, + visibility_timeout_ms: trigger.visibilityTimeoutMs, + }) + ); + break; + } + } + } + + if (crons.length) { + result.triggers = { crons }; + } + if (routes.length) { + result.routes = routes; + } + if (queueConsumers.length) { + result.queues = { ...(result.queues ?? {}), consumers: queueConsumers }; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// DOMAINS (top-level domains -> custom-domain routes) +// ═══════════════════════════════════════════════════════════════════════════ + +function convertDomains(config: ParsedConfig, result: RawConfig): void { + if (!config.domains || config.domains.length === 0) { + return; + } + const routes: NonNullable = result.routes + ? [...result.routes] + : []; + for (const domain of config.domains) { + routes.push({ pattern: domain, custom_domain: true }); + } + result.routes = routes; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TAIL CONSUMERS +// ═══════════════════════════════════════════════════════════════════════════ + +function convertTailConsumers(config: ParsedConfig, result: RawConfig): void { + const consumers = config.tailConsumers; + if (!consumers || consumers.length === 0) { + return; + } + const tail: NonNullable = []; + const streaming: NonNullable = []; + for (const consumer of consumers) { + if (consumer.streaming) { + streaming.push({ service: consumer.workerName }); + } else { + tail.push({ service: consumer.workerName }); + } + } + if (tail.length) { + result.tail_consumers = tail; + } + if (streaming.length) { + result.streaming_tail_consumers = streaming; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// UTILITIES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Strip keys whose value is `undefined`. Returns a new object preserving the + * input's value type. + */ +function omitUndefined>(obj: T): T { + const out: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined) { + out[key] = value; + } + } + return out as T; +} diff --git a/packages/config/src/exports.ts b/packages/config/src/exports.ts new file mode 100644 index 0000000000..0abd4398c6 --- /dev/null +++ b/packages/config/src/exports.ts @@ -0,0 +1,83 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// EXPORTS API +// Named types and helper factories for declaring class exports. +// ═══════════════════════════════════════════════════════════════════════════ + +interface DurableObjectExportOptions { + /** + * Storage backend for the Durable Object. + * + * - `"sqlite"`: selects the SQLite-backed storage engine + * (recommended for new classes). + * - `"legacy-kv"`: selects the legacy key-value storage engine. + */ + storage: "sqlite" | "legacy-kv"; +} + +/** + * Declares a Durable Object class defined by this Worker. + * + * For more information about Durable Objects, see the documentation at + * https://developers.cloudflare.com/workers/learning/using-durable-objects + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ +export interface DurableObjectExport extends DurableObjectExportOptions { + type: "durable-object"; +} + +interface WorkflowExportOptions { + /** The name of the Workflow. */ + name: string; + /** Optional limits for the Workflow. */ + limits?: { + /** Maximum number of steps a Workflow instance can execute. */ + steps?: number; + }; + /** Optional cron schedules for automatically triggering workflow instances. */ + schedules?: string[]; +} + +/** Declares a Workflow defined by this Worker. */ +export interface WorkflowExport extends WorkflowExportOptions { + type: "workflow"; +} + +/** + * Configuration for named exports declared by the Worker. Each entry's + * key is the exported class name; the value configures the export. + */ +export interface Exports { + /** + * Declares a Durable Object class defined by this Worker. + * + * For more information about Durable Objects, see the documentation at + * https://developers.cloudflare.com/workers/learning/using-durable-objects + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ + durableObject(options: DurableObjectExportOptions): DurableObjectExport; + // TODO: support Workflows + // /** Declares a Workflow defined by this Worker. */ + // workflow(options: WorkflowExportOptions): WorkflowExport; +} + +/** + * Exports builder for configuring Worker exports. + * + * @example + * ```typescript + * import { defineWorker, exports } from "@cloudflare/config"; + * + * export default defineWorker({ + * exports: { + * MyDurableObject: exports.durableObject({ storage: "sqlite" }), + * }, + * }); + * ``` + */ +export const exports: Exports = { + durableObject: (options) => ({ type: "durable-object", ...options }), + // TODO: support Workflows + // workflow: (options) => ({ type: "workflow", ...options }), +}; diff --git a/packages/config/src/generate.ts b/packages/config/src/generate.ts new file mode 100644 index 0000000000..2c1ca89e01 --- /dev/null +++ b/packages/config/src/generate.ts @@ -0,0 +1,60 @@ +import { dedent } from "ts-dedent"; + +interface GenerateTypesOptions { + /** Path to the config file (relative to the generated `.d.ts`). */ + configPath: string; + /** + * Package name used in the `import type { ... } from ""` line + * for `InferEnv`/`InferDurableNamespaces`/`InferMainModule`/`UnwrapConfig`. + * + * Defaults to `"@cloudflare/config"`. Integrations that bundle + * `@cloudflare/config` (e.g. `@cloudflare/vite-plugin`) should pass their + * own re-export path here so users don't need to depend on + * `@cloudflare/config` directly. + */ + packageName?: string; +} + +/** + * Generate the content for worker-configuration.d.ts + * + * @example + * ```typescript + * import { generateTypes } from "@cloudflare/config"; + * import { writeFileSync } from "node:fs"; + * + * const content = generateTypes({ + * configPath: "./cloudflare.config.ts", + * }); + * + * writeFileSync("worker-configuration.d.ts", content); + * ``` + */ +export function generateTypes({ + configPath, + packageName = "@cloudflare/config", +}: GenerateTypesOptions): string { + // Strip .ts/.js extension for import path + const configImportPath = configPath.replace(/\.(ts|js|mts|mjs)$/, ""); + + return dedent` + /* eslint-disable */ + // Generated by @cloudflare/config + import type { InferEnv, InferDurableNamespaces, InferMainModule, UnwrapConfig } from "${packageName}"; + import type Config from "${configImportPath}"; + + type WorkerConfig = UnwrapConfig; + + declare global { + namespace Cloudflare { + interface GlobalProps { + mainModule: InferMainModule; + durableNamespaces: InferDurableNamespaces; + } + interface Env extends InferEnv {} + } + interface Env extends Cloudflare.Env {} + } + + `; +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 0000000000..21f0479d58 --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,7 @@ +export * from "./public"; +export { ConfigSchema } from "./schema"; +export { generateTypes } from "./generate"; +export { convertToWranglerConfig } from "./convert"; +export { loadConfig, registerConfigHooks } from "./load"; +export { resolveWorkerDefinition } from "./worker-definition"; +export type { LoadConfigResult } from "./load"; diff --git a/packages/config/src/inference.ts b/packages/config/src/inference.ts new file mode 100644 index 0000000000..3949fac7ef --- /dev/null +++ b/packages/config/src/inference.ts @@ -0,0 +1,319 @@ +// oxlint-disable typescript/no-explicit-any -- needed in type utilities + +import type { + JsonBinding, + TextBinding, + TypedAiBinding, + TypedDurableObjectBinding, + TypedKvBinding, + TypedPipelineBinding, + TypedQueueBinding, + TypedWorkerBinding, + TypedWorkflowBinding, +} from "./bindings"; +import type { UserConfigExport, WorkerDefinition } from "./worker-definition"; +import type { Pipeline } from "cloudflare:pipelines"; + +// ═══════════════════════════════════════════════════════════════════════════ +// GENERIC UTILITIES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * The Worker's entry module, imported with the `{ type: "cf-worker" }` import attribute + * @example + * ```ts + * import * as entrypoint from "./src" with { type: "cf-worker" }; + * ``` + */ +export type WorkerModule = Record; + +/** + * Default module type representing an unknown Worker's exports. + * - default export can be `ExportedHandler` or a `WorkerEntrypoint` class constructor + * - named exports can be `WorkerEntrypoint`, `DurableObject`, or `WorkflowEntrypoint` class constructors + */ +interface DefaultModule { + default?: ExportedHandler | Constructor; + [key: string]: + | ExportedHandler + | Constructor + | Constructor + | Constructor + | undefined; +} + +/** + * Represents a class constructor that creates instances of TInstance. + */ +type Constructor = new (...args: any[]) => TInstance; + +/** + * Extracts the instance type from a class constructor if it extends `TInstance`. + */ +type ExtractInstance = + T extends Constructor ? InstanceType : never; + +// ═══════════════════════════════════════════════════════════════════════════ +// BINDING TYPE INFERENCE +// Maps binding definitions to their corresponding Cloudflare runtime types. +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Mapping from binding type literals to Cloudflare runtime types. + * + * Entries fall into two groups: + * - Parameterized bindings (ai, json, kv, pipeline, queue, text) refine their + * runtime type from the binding instance via nominal matches against the + * `Typed*Binding` / `JsonBinding` / `TextBinding` interfaces from + * `./bindings`. When `TBinding` does not match, the entry falls back to the + * unparameterized runtime type. + * - Non-parameterized bindings map their type literal directly to a runtime + * type and ignore `TBinding`. + * + * IMPORTANT: The right-hand-side identifiers in this map (e.g. `KVNamespace`, + * `ImagesBinding`, `Fetcher`) must resolve to the ambient runtime types from + * `@cloudflare/workers-types`, not to local config interfaces. Several local + * binding interfaces in `./bindings.ts` (`ImagesBinding`, `MediaBinding`, + * `StreamBinding`) share names with ambient globals — importing those local + * types into this file silently shadows the globals and breaks `InferEnv`. + * Only import the `Typed*Binding`, `JsonBinding`, and `TextBinding` interfaces + * from `./bindings` (their names do not collide with ambient globals); never + * widen the import to a wildcard or to the plain `*Binding` interfaces. + */ +interface BindingTypeMap { + // Parameterized bindings - refine via nominal match on the binding instance + ai: TBinding extends TypedAiBinding ? Ai : Ai; + json: TBinding extends JsonBinding ? T : never; + kv: TBinding extends TypedKvBinding ? KVNamespace : KVNamespace; + pipeline: TBinding extends TypedPipelineBinding + ? Pipeline + : Pipeline; + queue: TBinding extends TypedQueueBinding ? Queue : Queue; + text: TBinding extends TextBinding ? T : never; + + // Non-parameterized bindings + "agent-memory": AgentMemoryNamespace; + "ai-search": AiSearchInstance; + "ai-search-namespace": AiSearchNamespace; + "analytics-engine-dataset": AnalyticsEngineDataset; + artifacts: Artifacts; + assets: Fetcher; + browser: BrowserRun; + d1: D1Database; + "dispatch-namespace": DispatchNamespace; + "durable-object": DurableObjectNamespace; + flagship: Flagship; + hyperdrive: Hyperdrive; + images: ImagesBinding; + logfwdr: any; + media: MediaBinding; + "mtls-certificate": Fetcher; + "rate-limit": RateLimit; + r2: R2Bucket; + secret: string; + "secrets-store-secret": SecretsStoreSecret; + "send-email": SendEmail; + stream: StreamBinding; + vectorize: VectorizeIndex; + "version-metadata": WorkerVersionMetadata; + "vpc-service": Fetcher; + "vpc-network": Fetcher; + "web-search": WebSearch; + worker: Fetcher; + "worker-loader": WorkerLoader; + workflow: Workflow; +} + +type InferBindingType = + // Worker binding + TBinding extends TypedWorkerBinding< + infer TConfig, + infer TExportName extends string + > + ? InferMainModule extends infer TModule extends WorkerModule + ? TExportName extends keyof TModule + ? TModule[TExportName] extends Constructor + ? Fetcher< + ExtractInstance + > + : Fetcher + : never + : never + : // Durable Object binding + TBinding extends TypedDurableObjectBinding< + infer TConfig, + infer TExportName extends string + > + ? InferMainModule extends infer TModule extends WorkerModule + ? TExportName extends keyof TModule + ? DurableObjectNamespace< + ExtractInstance + > + : never + : never + : // Workflow binding + TBinding extends TypedWorkflowBinding< + infer TConfig, + infer TExportName extends string + > + ? InferMainModule extends infer TModule extends WorkerModule + ? TExportName extends keyof TModule + ? ExtractInstance< + TModule[TExportName], + Rpc.WorkflowEntrypointBranded + > extends infer TWorkflow + ? TWorkflow extends { + run(event: { payload: infer P }, step: any): any; + } + ? Workflow

+ : Workflow + : Workflow + : never + : never + : // Unsafe bindings + TBinding extends { type: `unsafe:${string}` } + ? any + : // Other bindings + TBinding extends { + type: infer K extends keyof BindingTypeMap; + } + ? BindingTypeMap[K] + : never; + +// ═══════════════════════════════════════════════════════════════════════════ +// CROSS-WORKER BINDING HELPERS (INTERNAL) +// Types used by the Bindings interface for type-safe cross-worker bindings. +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Infer the Worker name from a config. + * + * @example + * ```typescript + * import { defineWorker } from "@cloudflare/config"; + * import type { InferDurableNamespaces, UnwrapConfig } from "@cloudflare/config"; + * + * const config = defineWorker({ name: "my-worker", ... }); + * + * type WorkerConfig = UnwrapConfig; + * // Inferred as: "my-worker" + * type Name = InferWorkerName; + * ``` + */ +export type InferWorkerName = TUnwrappedConfig extends { + name: infer TName extends string; +} + ? TName + : never; + +/** + * Infer export names from a config's exports, optionally filtered by type. + * When TExportType is `string` (default), returns all export names. + * When TExportType is a specific literal like `"durable-object"` or `"workflow"`, + * returns only exports of that type. + */ +export type InferExportsByType< + TUnwrappedConfig, + TExportType extends string = string, +> = TUnwrappedConfig extends { + exports: infer TExports extends Record; +} + ? { + [K in keyof TExports]: TExports[K] extends { type: TExportType } + ? K & string + : never; + }[keyof TExports] + : never; + +/** + * Infer `WorkerEntrypoint` export names from a config. + * Returns named module exports that are not declared as type `"durable-object"` or `"workflow"` in `exports`. + * Excludes `"default"` since `exportName` should only be provided for named exports. + */ +export type InferWorkerEntrypointExports = Exclude< + keyof InferMainModule & string, + | "default" + | InferExportsByType +>; + +// ═══════════════════════════════════════════════════════════════════════════ +// CONFIG INFERENCE (PUBLIC) +// Exported types for inferring runtime types from config definitions. +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Unwrap function and promise types to get the underlying config. + * Use this to normalize a config before passing it to other inference utilities. + */ +export type UnwrapConfig = + TConfig extends WorkerDefinition + ? TUnwrappedConfig + : TConfig extends UserConfigExport + ? TUnwrappedConfig + : never; + +/** + * Infer the `Env` interface type from a Worker config. + * + * Transforms a config object's `env` bindings into their + * corresponding Cloudflare runtime types. + * + * @example + * ```typescript + * import { defineWorker, bindings } from "@cloudflare/config"; + * import type { InferEnv, UnwrapConfig } from "@cloudflare/config"; + * + * const config = defineWorker({ + * env: { + * MY_JSON: bindings.json({ id: string }), + * MY_KV: bindings.kv(), + * }, + * }); + * + * type WorkerConfig = UnwrapConfig; + * // Inferred as: { MY_JSON: { id: string }; MY_KV: KVNamespace } + * export type Env = InferEnv; + * ``` + */ +export type InferEnv = TUnwrappedConfig extends { + env: infer TEnv extends Record; +} + ? { [K in keyof TEnv]: InferBindingType } + : never; + +/** + * Infer the Durable Object namespace names from a Worker config's exports. + * Returns a union of export names that have `type: "durable-object"`. + * + * @example + * ```typescript + * import { defineWorker } from "@cloudflare/config"; + * import type { InferDurableNamespaces, UnwrapConfig } from "@cloudflare/config"; + * + * const config = defineWorker({ + * exports: { + * MyDurableObject: { type: "durable-object", storage: "sqlite" }, + * MyWorkflow: { type: "workflow", name: "my-workflow" }, + * }, + * }); + * + * type WorkerConfig = UnwrapConfig; + * // Inferred as: "MyDurableObject" + * type DurableNamespaces = InferDurableNamespaces; + * ``` + */ +export type InferDurableNamespaces = InferExportsByType< + TUnwrappedConfig, + "durable-object" +>; + +/** + * Infer the main module type from a Worker config's entrypoint. + * If entrypoint is a module namespace object, returns that type. + * If entrypoint is a `string` or not present, returns `DefaultModule` as a fallback. + */ +export type InferMainModule = TUnwrappedConfig extends { + entrypoint: infer TModule extends WorkerModule; +} + ? TModule + : DefaultModule; diff --git a/packages/config/src/load.ts b/packages/config/src/load.ts new file mode 100644 index 0000000000..3e6c1151d3 --- /dev/null +++ b/packages/config/src/load.ts @@ -0,0 +1,201 @@ +/** + * @module + * + * The no-cache import logic in this file is adapted from `import-without-cache` + * by Kevin Deng (https://github.com/sxzz/import-without-cache), published under + * the MIT License and Copyright © 2025-PRESENT Kevin Deng. + * + * See https://github.com/sxzz/import-without-cache for the original code. + */ + +import { AsyncLocalStorage } from "node:async_hooks"; +import { registerHooks } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import type { LoadHookContext } from "node:module"; + +const CF_WORKER_SCHEME = "cf-worker:"; +const CF_WORKER_TYPE = "cf-worker"; +const CF_ATTR = "cf"; +const CF_NO_CACHE_VALUE = "no-cache"; +const CF_NO_CACHE_QUERY_KEY = "cf-no-cache"; +const RE_NODE_MODULES = /[/\\]node_modules[/\\]/; + +const depsStore = new AsyncLocalStorage>(); + +/** + * Dynamically import a worker config file from disk. + * + * Returns the module's default export, plus the set of file paths + * imported during resolution. Callers are responsible for unwrapping + * function/promise wrappers around the returned value and validating it + * against `ConfigSchema`. + * + * @param configPath Filesystem path to the config file. Relative paths are + * resolved against `process.cwd()`. + */ +export async function loadConfig( + configPath: string +): Promise { + registerConfigHooks(); + const url = pathToFileURL(configPath).href; + const dependencies = new Set(); + const mod = await depsStore.run( + dependencies, + () => import(url, { with: { [CF_ATTR]: CF_NO_CACHE_VALUE } }) + ); + return { config: mod.default, dependencies }; +} + +export interface LoadConfigResult { + /** The default export of the config module */ + config: unknown; + /** + * Absolute file paths imported while resolving the config. + * `cf-worker` entrypoints are NOT included — they are + * referenced for their specifier only and changes to their source should + * not trigger a config reload. + */ + dependencies: Set; +} + +let deregister: (() => void) | undefined; + +/** + * Register Node module hooks for loading Worker configs. Idempotent — + * repeated calls return the same deregister function. + * + * - Handles `with { type: "cf-worker" }` import attributes by synthesising an + * ES module whose default export is the raw specifier as written by the + * user — relative paths, bare specifiers, and virtual-module specifiers + * all pass through unchanged. The referenced module is NOT resolved, is + * NOT executed, and is NOT added to the dependencies set. + * - Handles `with { cf: "no-cache" }` import attributes (set internally by + * `loadConfig`) by tagging the resolved URL with a unique UUID query + * string so Node treats each import as a fresh module. The UUID is + * propagated to all transitive imports via the parent URL, ensuring the + * entire subgraph is re-evaluated. Imports inside `node_modules` are + * skipped (treated as immutable for config purposes). + */ +export function registerConfigHooks(): () => void { + if (deregister) { + return deregister; + } + if (typeof process !== "undefined" && process.versions.bun) { + throw new Error( + "cloudflare.config.ts loading is not supported on Bun. " + + "Please use Node.js v22.18.0 or higher." + ); + } + if (typeof registerHooks !== "function") { + throw new Error( + "cloudflare.config.ts loading requires Node.js v22.18.0 or higher." + ); + } + + const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + // `importAttributes` may be absent for some resolution paths (e.g. + // CJS `require()` going through the hook chain after the hook has + // been registered for ESM use). + const importAttributes = context.importAttributes ?? {}; + + if (importAttributes.type === CF_WORKER_TYPE) { + // Path-only reference. The entrypoint is never resolved or + // loaded, so we don't add it to dependencies (changes to the + // entrypoint's source should not trigger a config reload). The + // raw specifier is preserved verbatim so consumers can apply + // their own resolution semantics. + return { + url: `${CF_WORKER_SCHEME}${encodeURIComponent(specifier)}`, + format: "module", + shortCircuit: true, + }; + } + + const isNoCache = importAttributes[CF_ATTR] === CF_NO_CACHE_VALUE; + + // `registerHooks` runs the chain synchronously, so the result is + // not a Promise even though the Node type allows for it. + const resolved = nextResolve(specifier, context) as { + url: string; + format?: string | null; + importAttributes?: Record; + shortCircuit?: boolean; + }; + + // Cache-busting + dependency tracking for the config's own graph. + if (RE_NODE_MODULES.test(resolved.url)) { + return resolved; + } + if (!resolved.url.startsWith("file://")) { + return resolved; + } + + const parentUuid = getParentUUID(context.parentURL); + + if (!parentUuid && !isNoCache) { + return resolved; + } + + depsStore.getStore()?.add(fileURLToPath(resolved.url)); + resolved.url = appendUUID( + resolved.url, + parentUuid || crypto.randomUUID() + ); + + return resolved; + }, + load(url, context, nextLoad) { + if (url.startsWith(CF_WORKER_SCHEME)) { + const specifier = decodeURIComponent( + url.slice(CF_WORKER_SCHEME.length) + ); + + return { + format: "module", + source: `export default ${JSON.stringify(specifier)};`, + shortCircuit: true, + }; + } + + cleanupImportAttributes(context); + + return nextLoad(url, context); + }, + }); + + deregister = () => { + hooks.deregister(); + deregister = undefined; + }; + + return deregister; +} + +function getParentUUID(parentURL: string | undefined): string | undefined { + if (!parentURL) { + return; + } + + return ( + new URL(parentURL).searchParams.get(CF_NO_CACHE_QUERY_KEY) ?? undefined + ); +} + +function appendUUID(url: string, uuid: string): string { + const parsed = new URL(url); + parsed.searchParams.set(CF_NO_CACHE_QUERY_KEY, uuid); + + return parsed.toString(); +} + +function cleanupImportAttributes(context: LoadHookContext): void { + if (!context.importAttributes?.[CF_ATTR]) { + return; + } + + const attrs = Object.assign(Object.create(null), context.importAttributes); + delete attrs[CF_ATTR]; + context.importAttributes = attrs; + Object.freeze(context.importAttributes); +} diff --git a/packages/config/src/public.ts b/packages/config/src/public.ts new file mode 100644 index 0000000000..e0b6215968 --- /dev/null +++ b/packages/config/src/public.ts @@ -0,0 +1,75 @@ +/** + * Curated public surface of `@cloudflare/config` — the types and values that a + * user authoring a `cloudflare.config.ts` should have access to. + */ + +export type { + Bindings, + AgentMemoryBinding, + AiBinding, + AiSearchBinding, + AiSearchNamespaceBinding, + AnalyticsEngineDatasetBinding, + ArtifactsBinding, + AssetsBinding, + BrowserBinding, + D1Binding, + DispatchNamespaceBinding, + DurableObjectBinding, + FlagshipBinding, + HyperdriveBinding, + ImagesBinding, + JsonBinding, + KvBinding, + LogfwdrBinding, + MediaBinding, + MtlsCertificateBinding, + PipelineBinding, + QueueBinding, + RateLimitBinding, + R2Binding, + SecretBinding, + SecretsStoreSecretBinding, + SendEmailBinding, + StreamBinding, + TextBinding, + TypedAiBinding, + TypedDurableObjectBinding, + TypedKvBinding, + TypedPipelineBinding, + TypedQueueBinding, + TypedWorkerBinding, + TypedWorkflowBinding, + UnsafeBinding, + VectorizeBinding, + VersionMetadataBinding, + VpcNetworkBinding, + VpcServiceBinding, + WebSearchBinding, + WorkerBinding, + WorkerLoaderBinding, + WorkflowBinding, +} from "./bindings"; +export { bindings } from "./bindings"; +export type { + Triggers, + FetchTrigger, + QueueConsumerTrigger, + ScheduledTrigger, +} from "./triggers"; +export { triggers } from "./triggers"; +export type { Exports, DurableObjectExport, WorkflowExport } from "./exports"; +export { exports } from "./exports"; +export type { + InferEnv, + InferDurableNamespaces, + InferMainModule, + UnwrapConfig, +} from "./inference"; +export type { UserConfig } from "./types"; +export type { + ConfigContext, + TypedWorkerDefinition, + UserConfigExport, +} from "./worker-definition"; +export { defineWorker } from "./worker-definition"; diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts new file mode 100644 index 0000000000..a25b81f464 --- /dev/null +++ b/packages/config/src/schema.ts @@ -0,0 +1,475 @@ +import * as z from "zod"; +import type { UserConfig } from "./types"; + +const AssetsSchema = z.strictObject({ + htmlHandling: z + .enum([ + "auto-trailing-slash", + "drop-trailing-slash", + "force-trailing-slash", + "none", + ]) + .optional(), + notFoundHandling: z + .enum(["single-page-application", "404-page", "none"]) + .optional(), + runWorkerFirst: z.union([z.array(z.string()), z.boolean()]).optional(), +}); + +const KnownBindingSchema = z.discriminatedUnion("type", [ + z.strictObject({ + type: z.literal("agent-memory"), + namespace: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("ai"), remote: z.boolean().optional() }), + z.strictObject({ + type: z.literal("ai-search"), + name: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("ai-search-namespace"), + namespace: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("analytics-engine-dataset"), + name: z.string().optional(), + }), + z.strictObject({ + type: z.literal("artifacts"), + namespace: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("assets") }), + z.strictObject({ + type: z.literal("browser"), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("d1"), + name: z.string().optional(), + id: z.string().optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("dispatch-namespace"), + namespace: z.string(), + outbound: z + .strictObject({ + workerName: z.string(), + parameters: z.array(z.string()).optional(), + }) + .optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("durable-object"), + workerName: z.string(), + exportName: z.string(), + }), + z.strictObject({ + type: z.literal("flagship"), + id: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("hyperdrive"), + id: z.string(), + localConnectionString: z.string().optional(), + }), + z.strictObject({ + type: z.literal("images"), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("json"), value: z.json() }), + z.strictObject({ + type: z.literal("kv"), + id: z.string().optional(), + // TODO: name support not yet implemented + // name: z.string().optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("logfwdr"), destination: z.string() }), + z.strictObject({ + type: z.literal("media"), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("mtls-certificate"), + id: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("pipeline"), + name: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("queue"), + name: z.string(), + deliveryDelay: z.number().optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("rate-limit"), + namespace: z.string(), + simple: z.strictObject({ + limit: z.number(), + period: z.union([z.literal(10), z.literal(60)]), + }), + }), + z.strictObject({ + type: z.literal("r2"), + name: z.string().optional(), + jurisdiction: z.string().optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("secret") }), + z.strictObject({ + type: z.literal("secrets-store-secret"), + storeId: z.string(), + secretName: z.string(), + }), + z.strictObject({ + type: z.literal("send-email"), + destinationAddress: z.string().optional(), + allowedDestinationAddresses: z.array(z.string()).optional(), + allowedSenderAddresses: z.array(z.string()).optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("stream"), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("text"), value: z.string() }), + z.strictObject({ + type: z.literal("vectorize"), + name: z.string(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("version-metadata") }), + z.strictObject({ + type: z.literal("vpc-service"), + id: z.string(), + remote: z.boolean().optional(), + }), + z + .strictObject({ + type: z.literal("vpc-network"), + tunnelId: z.string().optional(), + networkId: z.string().optional(), + remote: z.boolean().optional(), + }) + .superRefine((value, ctx) => { + const hasTunnel = value.tunnelId !== undefined; + const hasNetwork = value.networkId !== undefined; + if (hasTunnel === hasNetwork) { + ctx.addIssue({ + code: "custom", + message: hasTunnel + ? `"vpc-network" bindings must specify exactly one of "tunnelId" or "networkId", not both` + : `"vpc-network" bindings must specify either "tunnelId" or "networkId"`, + }); + } + }), + z.strictObject({ + type: z.literal("web-search"), + remote: z.boolean().optional(), + }), + z.strictObject({ + type: z.literal("worker"), + workerName: z.string(), + exportName: z.string().optional(), + props: z.record(z.string(), z.unknown()).optional(), + remote: z.boolean().optional(), + }), + z.strictObject({ type: z.literal("worker-loader") }), + // TODO: support Workflows + // z.strictObject({ + // type: z.literal("workflow"), + // workerName: z.string(), + // exportName: z.string(), + // remote: z.boolean().optional(), + // }), +]); + +const UnsafeBindingSchema = z.looseObject({ + type: z.templateLiteral(["unsafe:", z.string().min(1)]), + dev: z + .strictObject({ + plugin: z.strictObject({ + package: z.string(), + name: z.string(), + }), + options: z.record(z.string(), z.unknown()).optional(), + }) + .optional(), +}); + +type BindingInput = + | z.input + | z.input; +type BindingOutput = + | z.output + | z.output; + +const BindingSchema = z.unknown().transform((value, ctx) => { + const isUnsafe = + typeof value === "object" && + value !== null && + "type" in value && + typeof value.type === "string" && + value.type.startsWith("unsafe:"); + + const schema = isUnsafe ? UnsafeBindingSchema : KnownBindingSchema; + const result = schema.safeParse(value); + + if (!result.success) { + ctx.issues.push(...(result.error.issues as unknown as typeof ctx.issues)); + return z.NEVER; + } + + return result.data; +}) as z.ZodType; + +export function isParsedUnsafeBinding( + binding: BindingOutput +): binding is z.output { + return binding.type.startsWith("unsafe:"); +} + +const CacheSchema = z.strictObject({ + enabled: z.boolean(), +}); + +/** + * Binding types that can only be defined once per worker. + */ +const SINGLETON_BINDING_TYPES = new Set([ + "ai", + "assets", + "browser", + "images", + "media", + "stream", + "version-metadata", + "web-search", +]); + +const listFormatter = new Intl.ListFormat("en-US"); + +const EnvSchema = z + .record(z.string(), BindingSchema) + .superRefine((env, ctx) => { + const seen = new Set(); + const duplicates = new Set(); + + for (const binding of Object.values(env)) { + const type = binding.type; + + if (SINGLETON_BINDING_TYPES.has(type)) { + if (seen.has(type)) { + duplicates.add(type); + } + + seen.add(type); + } + } + + if (duplicates.size > 0) { + ctx.addIssue({ + code: "custom", + message: `${listFormatter.format([...duplicates].sort())} bindings can only be defined once`, + }); + } + }) + .optional(); + +const ExportSchema = z.discriminatedUnion("type", [ + z.strictObject({ + type: z.literal("durable-object"), + storage: z.enum(["sqlite", "legacy-kv"]), + }), + // TODO: support Workflows + // z.strictObject({ + // type: z.literal("workflow"), + // name: z.string(), + // limits: z.strictObject({ steps: z.number().optional() }).optional(), + // }), +]); + +const LimitsSchema = z.strictObject({ + cpuMs: z.number().optional(), + subrequests: z.number().optional(), +}); + +const ObservabilitySchema = z.strictObject({ + enabled: z.boolean().optional(), + headSamplingRate: z.number().optional(), + logs: z + .strictObject({ + enabled: z.boolean().optional(), + headSamplingRate: z.number().optional(), + invocationLogs: z.boolean().optional(), + persist: z.boolean().optional(), + destinations: z.array(z.string()).optional(), + }) + .optional(), + traces: z + .strictObject({ + enabled: z.boolean().optional(), + headSamplingRate: z.number().optional(), + persist: z.boolean().optional(), + destinations: z.array(z.string()).optional(), + }) + .optional(), +}); + +const PlacementSchema = z.union([ + z.strictObject({ + mode: z.enum(["off", "smart"]), + hint: z.string().optional(), + }), + z.strictObject({ + mode: z.literal("targeted").optional(), + region: z.string(), + }), + z.strictObject({ + mode: z.literal("targeted").optional(), + host: z.string(), + }), + z.strictObject({ + mode: z.literal("targeted").optional(), + hostname: z.string(), + }), +]); + +const TailConsumerSchema = z.strictObject({ + workerName: z.string(), + streaming: z.boolean().optional(), +}); + +const TriggerSchema = z.discriminatedUnion("type", [ + // TODO: email triggers not yet implemented + // z.strictObject({ type: z.literal("email") }), + z.strictObject({ + type: z.literal("fetch"), + pattern: z.string(), + zone: z.string().optional(), + }), + z.strictObject({ + type: z.literal("queue"), + name: z.string(), + deadLetterQueue: z.string().optional(), + maxBatchSize: z.number().optional(), + maxBatchTimeout: z.number().optional(), + maxConcurrency: z.number().nullable().optional(), + maxRetries: z.number().optional(), + retryDelay: z.number().optional(), + visibilityTimeoutMs: z.number().optional(), + }), + z.strictObject({ + type: z.literal("scheduled"), + schedule: z.string(), + }), +]); + +const UnsafeSchema = z.strictObject({ + metadata: z.record(z.string(), z.unknown()).optional(), + capnp: z + .union([ + z.strictObject({ + basePath: z.string(), + sourceSchemas: z.array(z.string()), + compiledSchema: z.never().optional(), + }), + z.strictObject({ + basePath: z.never().optional(), + sourceSchemas: z.never().optional(), + compiledSchema: z.string(), + }), + ]) + .optional(), +}); + +export const ConfigSchema = z.strictObject({ + name: z.string().optional(), + accountId: z.string().optional(), + compatibilityDate: z.string().optional(), + compatibilityFlags: z.array(z.string()).optional(), + entrypoint: z + .union([z.string(), z.strictObject({ default: z.string() })]) + .transform((value) => (typeof value === "string" ? value : value.default)) + .optional(), + assets: AssetsSchema.optional(), + domains: z.array(z.string()).optional(), + triggers: z.array(TriggerSchema).optional(), + tailConsumers: z.array(TailConsumerSchema).optional(), + cache: CacheSchema.optional(), + placement: PlacementSchema.optional(), + limits: LimitsSchema.optional(), + logpush: z.boolean().optional(), + observability: ObservabilitySchema.optional(), + workersDev: z.boolean().optional(), + previewUrls: z.boolean().optional(), + complianceRegion: z.enum(["public", "fedramp-high"]).optional(), + firstPartyWorker: z.boolean().optional(), + unsafe: UnsafeSchema.optional(), + // TODO: support previews + env: EnvSchema, + exports: z.record(z.string(), ExportSchema).optional(), +}); + +/** + * Parsed (post-validation) config returned by `ConfigSchema.parse(...)`. + */ +export type ParsedConfig = z.output; + +/** + * Bidirectional drift check between {@link ConfigSchema} and the public + * {@link UserConfig} interface. Excludes `entrypoint` and `env`, which + * deliberately differ: + * + * - `entrypoint`: the public type accepts a `WorkerModule` namespace + * (produced by `import ... with { type: "cf-worker" }`), but the schema + * only accepts the post-`load.ts` shape (`string` or `{ default: string }`). + * + * - `env`: see the separate unidirectional drift check below. + */ +type _ComparableInput = Omit< + z.input, + "entrypoint" | "env" +>; +type _ComparableUserConfig = Omit; +type _AssertSchemaMatchesUserConfig = [ + _ComparableInput extends _ComparableUserConfig ? true : false, + _ComparableUserConfig extends _ComparableInput ? true : false, +]; +const _assertSchemaMatchesUserConfig: _AssertSchemaMatchesUserConfig = [ + true, + true, +]; +void _assertSchemaMatchesUserConfig; + +/** + * Unidirectional drift check for `env`. The public binding return types + * (e.g. `AiBinding`) carry phantom `__typeParams` / `__config` fields for + * inference helpers that the schema does not (and cannot) validate at + * runtime, so a bidirectional check would always fail in that direction. + * + * We therefore only assert that `UserConfig['env']` is assignable to + * `z.input['env']` — i.e. every binding shape the + * public type accepts is something the schema is willing to parse. This + * catches drift where the public type drops a field the schema still + * requires, renames a field, changes a field's type to one the schema + * rejects, or adds a binding the schema doesn't know about. + */ +type _AssertUserConfigEnvExtendsSchema = UserConfig["env"] extends z.input< + typeof ConfigSchema +>["env"] + ? true + : false; +const _assertUserConfigEnvExtendsSchema: _AssertUserConfigEnvExtendsSchema = true; +void _assertUserConfigEnvExtendsSchema; diff --git a/packages/config/src/triggers.ts b/packages/config/src/triggers.ts new file mode 100644 index 0000000000..b3f2057709 --- /dev/null +++ b/packages/config/src/triggers.ts @@ -0,0 +1,132 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// TRIGGERS API +// Named types and helper factories for declaring event triggers. +// ═══════════════════════════════════════════════════════════════════════════ + +interface FetchTriggerOptions { + /** + * A route that your Worker should be published to. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#types-of-routes + */ + pattern: string; + /** + * The DNS zone the pattern is attached to. Required when the + * pattern is ambiguous. + */ + zone?: string; +} + +/** + * Fetch trigger — a route that your Worker should be published to. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#types-of-routes + */ +export interface FetchTrigger extends FetchTriggerOptions { + type: "fetch"; +} + +interface QueueConsumerTriggerOptions { + /** The name of the queue from which this consumer should consume. */ + name: string; + /** The queue to send messages that failed to be consumed. */ + deadLetterQueue?: string; + /** The maximum number of messages per batch. */ + maxBatchSize?: number; + /** The maximum number of seconds to wait to fill a batch with messages. */ + maxBatchTimeout?: number; + /** + * The maximum number of concurrent consumer Worker invocations. + * Leaving this unset will allow your consumer to scale to the + * maximum concurrency needed to keep up with the message backlog. + */ + maxConcurrency?: number | null; + /** The maximum number of retries for each message. */ + maxRetries?: number; + /** The number of seconds to wait before retrying a message. */ + retryDelay?: number; + /** The number of milliseconds to wait for pulled messages to become visible again. */ + visibilityTimeoutMs?: number; +} + +/** + * Queue consumer trigger — invokes this Worker when messages arrive on the + * named queue. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#queues + */ +export interface QueueConsumerTrigger extends QueueConsumerTriggerOptions { + type: "queue"; +} + +interface ScheduledTriggerOptions { + /** + * A "cron" definition to trigger a Worker's "scheduled" function. + * + * Lets you call Workers periodically, much like a cron job. + * + * More details here https://developers.cloudflare.com/workers/platform/cron-triggers + */ + schedule: string; +} + +/** + * Scheduled (cron) trigger — invokes this Worker on the given schedules. + * + * More details here https://developers.cloudflare.com/workers/platform/cron-triggers + */ +export interface ScheduledTrigger extends ScheduledTriggerOptions { + type: "scheduled"; +} + +/** + * Event triggers — fetch routes, queue consumers, and cron schedules + * — that invoke this Worker. Construct entries with `triggers.fetch(...)`, + * `triggers.queue(...)`, or `triggers.scheduled(...)`. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#triggers + */ +export interface Triggers { + /** + * Fetch trigger — a route that your Worker should be published to. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#types-of-routes + */ + fetch(options: FetchTriggerOptions): FetchTrigger; + /** + * Queue consumer trigger — invokes this Worker when messages arrive on the + * named queue. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#queues + */ + queue(options: QueueConsumerTriggerOptions): QueueConsumerTrigger; + /** + * Scheduled (cron) trigger — invokes this Worker on the given schedules. + * + * More details here https://developers.cloudflare.com/workers/platform/cron-triggers + */ + scheduled(options: ScheduledTriggerOptions): ScheduledTrigger; +} + +/** + * Triggers builder for configuring event triggers. + * + * @example + * ```typescript + * import { defineWorker, triggers } from "@cloudflare/config"; + * + * export default defineWorker({ + * triggers: [ + * triggers.fetch({ pattern: "example.com/*", zone: "example.com" }), + * triggers.queue({ name: "my-queue" }), + * triggers.scheduled({ schedule: "0 * * * *" }), + * triggers.scheduled({ schedule: "30 0 * * *" }), + * ], + * }); + * ``` + */ +export const triggers: Triggers = { + fetch: (options) => ({ type: "fetch", ...options }), + queue: (options) => ({ type: "queue", ...options }), + scheduled: (options) => ({ type: "scheduled", ...options }), +}; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts new file mode 100644 index 0000000000..3277a03472 --- /dev/null +++ b/packages/config/src/types.ts @@ -0,0 +1,399 @@ +/** + * The hand-authored public configuration type for `@cloudflare/config`. + * + * JSDoc on each field is derived from the equivalent field in the Wrangler + * config types in `packages/workers-utils/src/config/environment.ts`. When + * editing either file, keep the prose in sync. + */ + +import type { + AgentMemoryBinding, + AiBinding, + AiSearchBinding, + AiSearchNamespaceBinding, + AnalyticsEngineDatasetBinding, + ArtifactsBinding, + AssetsBinding, + BrowserBinding, + D1Binding, + DispatchNamespaceBinding, + DurableObjectBinding, + FlagshipBinding, + HyperdriveBinding, + ImagesBinding, + JsonBinding, + KvBinding, + LogfwdrBinding, + MediaBinding, + MtlsCertificateBinding, + PipelineBinding, + QueueBinding, + R2Binding, + RateLimitBinding, + SecretBinding, + SecretsStoreSecretBinding, + SendEmailBinding, + StreamBinding, + TextBinding, + UnsafeBinding, + VectorizeBinding, + VersionMetadataBinding, + VpcNetworkBinding, + VpcServiceBinding, + WebSearchBinding, + WorkerBinding, + WorkerLoaderBinding, + // TODO: re-enable when workflow bindings return. + // WorkflowBinding, +} from "./bindings"; +import type { DurableObjectExport } from "./exports"; +import type { WorkerModule } from "./inference"; +import type { + FetchTrigger, + QueueConsumerTrigger, + ScheduledTrigger, +} from "./triggers"; + +/** + * Union of all binding definitions accepted in `env`. + */ +type Binding = + | AgentMemoryBinding + | AiBinding + | AiSearchBinding + | AiSearchNamespaceBinding + | AnalyticsEngineDatasetBinding + | ArtifactsBinding + | AssetsBinding + | BrowserBinding + | D1Binding + | DispatchNamespaceBinding + | DurableObjectBinding + | FlagshipBinding + | HyperdriveBinding + | ImagesBinding + | JsonBinding + | KvBinding + | LogfwdrBinding + | MediaBinding + | MtlsCertificateBinding + | PipelineBinding + | QueueBinding + | R2Binding + | RateLimitBinding + | SecretBinding + | SecretsStoreSecretBinding + | SendEmailBinding + | StreamBinding + | TextBinding + | UnsafeBinding + | VectorizeBinding + | VersionMetadataBinding + | VpcNetworkBinding + | VpcServiceBinding + | WebSearchBinding + | WorkerBinding + | WorkerLoaderBinding; +// TODO: re-enable when workflow bindings return. +// | WorkflowBinding; + +/** + * Union of all trigger definitions accepted in `triggers`. + */ +type Trigger = FetchTrigger | QueueConsumerTrigger | ScheduledTrigger; + +/** + * Union of all export definitions accepted in `exports`. + */ +type Export = DurableObjectExport; +// TODO: support Workflows +// type Export = DurableObjectExport | WorkflowExport; + +/** + * Worker configuration. This is the input shape passed to + * [`defineWorker`](https://developers.cloudflare.com/workers/wrangler/configuration/). + * + * Fields are validated at runtime by `ConfigSchema` and normalised before + * being passed to downstream tooling. + */ +export interface UserConfig { + /** + * The name of your Worker. + */ + name?: string; + + /** + * This is the ID of the account associated with your zone. + * You might have more than one account, so make sure to use + * the ID of the account associated with the zone/route you + * provide, if you provide one. It can also be specified through + * the CLOUDFLARE_ACCOUNT_ID environment variable. + */ + accountId?: string; + + /** + * A date in the form yyyy-mm-dd, which will be used to determine + * which version of the Workers runtime is used. + * + * More details at https://developers.cloudflare.com/workers/configuration/compatibility-dates + */ + compatibilityDate?: string; + + /** + * A list of flags that enable features from upcoming features of + * the Workers runtime, usually used together with `compatibilityDate`. + * + * More details at https://developers.cloudflare.com/workers/configuration/compatibility-flags/ + * + * @default [] + */ + compatibilityFlags?: string[]; + + /** + * The entrypoint module that will be executed. + * + * May be either a path string (e.g. `"./src/index.ts"`) or a module + * namespace imported with the `cf-worker` import attribute. + * + * @example + * ```ts + * import * as entrypoint from "./src" with { type: "cf-worker" }; + * export default defineWorker({ entrypoint }); + * ``` + */ + entrypoint?: string | WorkerModule; + + /** + * Specify the directory of static assets to deploy/serve. + * + * More details at https://developers.cloudflare.com/workers/frameworks/ + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#assets + */ + assets?: { + /** How to handle HTML requests. */ + htmlHandling?: + | "auto-trailing-slash" + | "drop-trailing-slash" + | "force-trailing-slash" + | "none"; + + /** How to handle requests that do not match an asset. */ + notFoundHandling?: "single-page-application" | "404-page" | "none"; + + /** + * Matches will be routed to the User Worker, and matches to negative rules will go to the Asset Worker. + * + * Can also be `true`, indicating that every request should be routed to the User Worker. + */ + runWorkerFirst?: string[] | boolean; + }; + + /** + * Custom domains that your Worker should be published to. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#types-of-routes + */ + domains?: string[]; + + /** + * Event triggers — fetch routes, queue consumers, and cron schedules + * — that invoke this Worker. Construct entries with `triggers.fetch(...)`, + * `triggers.queue(...)`, or `triggers.scheduled(...)`. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#triggers + */ + triggers?: Trigger[]; + + /** + * A list of Tail Workers that are bound to this Worker. + * + * `@cloudflare/config` unifies regular and streaming tail consumers under + * a single field; pass `streaming: true` to forward streaming tail events. + * + * @default [] + */ + tailConsumers?: Array<{ + /** The name of the service tail events will be forwarded to. */ + workerName: string; + /** Whether to stream tail events in real time. */ + streaming?: boolean; + }>; + + /** + * Specify the cache behavior of the Worker. + */ + cache?: { + /** If cache is enabled for this Worker. */ + enabled: boolean; + }; + + /** + * Specify how the Worker should be located to minimize round-trip time. + * + * More details: https://developers.cloudflare.com/workers/platform/smart-placement/ + */ + placement?: + | { mode: "off" | "smart"; hint?: string } + | { mode?: "targeted"; region: string } + | { mode?: "targeted"; host: string } + | { mode?: "targeted"; hostname: string }; + + /** + * Specify limits for runtime behavior. + * Only supported for the "standard" Usage Model. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#limits + */ + limits?: { + /** Maximum allowed CPU time for a Worker's invocation in milliseconds. */ + cpuMs?: number; + /** Maximum allowed number of fetch requests that a Worker's invocation can execute. */ + subrequests?: number; + }; + + /** + * Send Trace Events from this Worker to Workers Logpush. + * + * This will not configure a corresponding Logpush job automatically. + * + * For more information about Workers Logpush, see: + * https://blog.cloudflare.com/logpush-for-workers/ + */ + logpush?: boolean; + + /** + * Specify the observability behavior of the Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#observability + */ + observability?: { + /** If observability is enabled for this Worker. */ + enabled?: boolean; + /** The sampling rate. */ + headSamplingRate?: number; + logs?: { + enabled?: boolean; + /** The sampling rate. */ + headSamplingRate?: number; + /** Set to false to disable invocation logs. */ + invocationLogs?: boolean; + /** + * If logs should be persisted to the Cloudflare observability platform where they can be queried in the dashboard. + * + * @default true + */ + persist?: boolean; + /** + * What destinations logs emitted from the Worker should be sent to. + * + * @default [] + */ + destinations?: string[]; + }; + traces?: { + enabled?: boolean; + /** The sampling rate. */ + headSamplingRate?: number; + /** + * If traces should be persisted to the Cloudflare observability platform where they can be queried in the dashboard. + * + * @default true + */ + persist?: boolean; + /** + * What destinations traces emitted from the Worker should be sent to. + * + * @default [] + */ + destinations?: string[]; + }; + }; + + /** + * Whether we use `..workers.dev` to + * test and deploy your Worker. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#workersdev + * + * @default true + */ + workersDev?: boolean; + + /** + * Whether we use `-..workers.dev` to + * serve Preview URLs for your Worker. + * + * @default false + */ + previewUrls?: boolean; + + /** + * Specify the compliance region mode of the Worker. + * + * Although if the user does not specify a compliance region, the default is `public`, + * it can be set to `undefined` in configuration to delegate to the CLOUDFLARE_COMPLIANCE_REGION environment variable. + */ + complianceRegion?: "public" | "fedramp-high"; + + /** + * Designates this Worker as an internal-only "first-party" Worker. + * + * @internal + */ + firstPartyWorker?: boolean; + + /** + * "Unsafe" tables for runtime features that aren't directly supported by + * this configuration. Values are forwarded verbatim in the Worker's + * upload metadata. + * + * @default {} + */ + unsafe?: { + /** + * Arbitrary key/value pairs that will be included in the uploaded metadata. Values specified + * here will always be applied to metadata last, so can add new or override existing fields. + */ + metadata?: Record; + /** + * Used for internal capnp uploads for the Workers runtime. + */ + capnp?: + | { + basePath: string; + sourceSchemas: string[]; + compiledSchema?: never; + } + | { + basePath?: never; + sourceSchemas?: never; + compiledSchema: string; + }; + }; + + /** + * Bindings exposed on the Worker's `env` object. Construct entries with + * `bindings.kv(...)`, `bindings.r2(...)`, etc. + */ + env?: Record; + + /** + * Configuration for named exports declared by the Worker. Each entry's + * key is the exported class name; the value configures the export. + * Construct entries with `exports.durableObject(...)` or + * `exports.workflow(...)`. + * + * Two export kinds are supported: + * + * - `durable-object`: declares a Durable Object class defined by this + * Worker. For more information about Durable Objects, see the + * documentation at + * https://developers.cloudflare.com/workers/learning/using-durable-objects + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + * + * - `workflow`: declares a Workflow defined by this Worker. + */ + exports?: Record; +} diff --git a/packages/config/src/utils.ts b/packages/config/src/utils.ts new file mode 100644 index 0000000000..1d964da7ab --- /dev/null +++ b/packages/config/src/utils.ts @@ -0,0 +1,10 @@ +/** + * Represents any valid JSON value. + */ +export type Json = + | string + | number + | boolean + | null + | Json[] + | { [key: string]: Json }; diff --git a/packages/config/src/worker-definition.ts b/packages/config/src/worker-definition.ts new file mode 100644 index 0000000000..23a8cc78e0 --- /dev/null +++ b/packages/config/src/worker-definition.ts @@ -0,0 +1,129 @@ +import type { + Bindings, + TypedDurableObjectBinding, + TypedWorkerBinding, +} from "./bindings"; +import type { + InferWorkerName, + InferExportsByType, + InferWorkerEntrypointExports, +} from "./inference"; +import type { UserConfig } from "./types"; + +// TODO: Use declaration merging in the consuming package once this package is published +export interface ConfigContext { + /** + * The Vite [`mode`](https://vite.dev/guide/env-and-mode.html#modes) the + * config is being evaluated in (e.g. `"development"`, `"production"`). + */ + mode: string; +} + +// We currently use Symbol.for rather than Symbol so that the symbol matches if duplicated across bundles +// This wouldn't be necessary if @cloudflare/config was published and included as a dependency +const CONFIG = Symbol.for("@cloudflare/config:worker-config"); + +/** + * Base shape of a Worker definition. Carries the resolved config and the + * untyped cross-worker binding helpers. + */ +export interface WorkerDefinition< + TConfig extends UserConfig = UserConfig, +> extends Pick { + [CONFIG]: + | TConfig + | Promise + | ((ctx: ConfigContext) => TConfig | Promise); +} + +/** + * Worker definition with typed cross-worker binding helpers. + */ +export interface TypedWorkerDefinition< + TConfig extends UserConfig, + TWorkerName extends string = InferWorkerName, +> extends WorkerDefinition { + /** + * Binding to a Durable Object class. `workerName` is the name of the Worker + * that defines the class; `exportName` is the exported class name. + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects + */ + durableObject< + TExportName extends InferExportsByType, + >(options: { + workerName: TWorkerName; + exportName: TExportName; + }): TypedDurableObjectBinding; + /** + * Service binding (Worker-to-Worker). `workerName` is the name of the bound + * Worker; `exportName` selects a named `WorkerEntrypoint` export (defaults to + * the default export). + * + * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + worker< + TExportName extends InferWorkerEntrypointExports | undefined = + undefined, + >(options: { + workerName: TWorkerName; + exportName?: TExportName; + props?: Record; + remote?: boolean; + }): TypedWorkerBinding< + TConfig, + TExportName extends string ? TExportName : "default" + >; + // TODO: re-enable when workflow bindings return. + // workflow< + // TExportName extends InferExportsByType, + // >(options: { + // workerName: TWorkerName; + // exportName: TExportName; + // remote?: boolean; + // }): TypedWorkflowBinding; +} + +export type UserConfigExport = + | T + | Promise + | ((ctx: ConfigContext) => T | Promise); + +export function defineWorker( + config: (ctx: ConfigContext) => (UserConfig & T) | Promise +): TypedWorkerDefinition; +export function defineWorker( + config: (UserConfig & T) | Promise +): TypedWorkerDefinition; +export function defineWorker(config: UserConfigExport): WorkerDefinition { + return { + [CONFIG]: config, + durableObject(options) { + return { type: "durable-object", ...options }; + }, + worker(options) { + return { type: "worker", ...options }; + }, + // TODO: re-enable when workflow bindings return. + // workflow(options) { + // return { type: "workflow", ...options }; + // }, + }; +} + +export async function resolveWorkerDefinition( + def: unknown, + ctx: ConfigContext +): Promise { + const raw = + typeof def === "object" && def !== null && CONFIG in def + ? (def as Record)[CONFIG] + : def; + + const value = + typeof raw === "function" + ? (raw as (ctx: ConfigContext) => unknown)(ctx) + : raw; + + return await value; +} diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json new file mode 100644 index 0000000000..84e3fdf220 --- /dev/null +++ b/packages/config/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/base.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "@types/node"] + }, + "include": ["src"] +} diff --git a/packages/config/tsdown.config.ts b/packages/config/tsdown.config.ts new file mode 100644 index 0000000000..8892471379 --- /dev/null +++ b/packages/config/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + public: "src/public.ts", + }, + platform: "node", + outDir: "dist", + dts: true, + tsconfig: "tsconfig.json", +}); diff --git a/packages/config/turbo.json b/packages/config/turbo.json new file mode 100644 index 0000000000..6556dcf3e5 --- /dev/null +++ b/packages/config/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/config/vitest.config.ts b/packages/config/vitest.config.ts new file mode 100644 index 0000000000..0c9f1f8e04 --- /dev/null +++ b/packages/config/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/__tests__/**/*.test.ts"], + reporters: ["default"], + }, +}); diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index 59bcad0cdf..eefbea6877 100644 --- a/packages/vite-plugin-cloudflare/package.json +++ b/packages/vite-plugin-cloudflare/package.json @@ -34,6 +34,10 @@ ".": { "types": "./dist/index.d.mts", "import": "./dist/index.mjs" + }, + "./experimental-config": { + "types": "./dist/experimental-config.d.mts", + "import": "./dist/experimental-config.mjs" } }, "publishConfig": { @@ -56,6 +60,7 @@ "ws": "catalog:default" }, "devDependencies": { + "@cloudflare/config": "workspace:*", "@cloudflare/containers-shared": "workspace:*", "@cloudflare/mock-npm-registry": "workspace:*", "@cloudflare/workers-shared": "workspace:*", diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/experimental-config/__tests__/worker.spec.ts new file mode 100644 index 0000000000..87fbfba1c7 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/__tests__/worker.spec.ts @@ -0,0 +1,11 @@ +import { test } from "vitest"; +import { getTextResponse, isBuild } from "../../__test-utils__"; + +test("serves the correct response for a worker configured via cloudflare.config.ts", async ({ + expect, +}) => { + const response = await getTextResponse("/"); + expect(response).toBe( + `The mode is ${isBuild ? "production" : "development"}` + ); +}); diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/cloudflare.config.ts b/packages/vite-plugin-cloudflare/playground/experimental-config/cloudflare.config.ts new file mode 100644 index 0000000000..12f46a1183 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/cloudflare.config.ts @@ -0,0 +1,14 @@ +import { + defineWorker, + bindings, +} from "@cloudflare/vite-plugin/experimental-config"; +import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + +export default defineWorker((ctx) => ({ + name: "worker", + entrypoint, + compatibilityDate: "2026-05-18", + env: { + MY_TEXT: bindings.text(`The mode is ${ctx.mode}`), + }, +})); diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/package.json b/packages/vite-plugin-cloudflare/playground/experimental-config/package.json new file mode 100644 index 0000000000..8482217dbd --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@playground/experimental-config", + "private": true, + "type": "module", + "scripts": { + "build:default": "vite build", + "check:type": "tsc --build", + "dev": "vite dev", + "preview": "vite preview" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/src/index.ts b/packages/vite-plugin-cloudflare/playground/experimental-config/src/index.ts new file mode 100644 index 0000000000..ee695db5c7 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/src/index.ts @@ -0,0 +1,7 @@ +import { env } from "cloudflare:workers"; + +export default { + fetch() { + return new Response(env.MY_TEXT); + }, +} satisfies ExportedHandler; diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.json b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.json new file mode 100644 index 0000000000..b52af703bd --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.worker.json" } + ] +} diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.node.json b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.node.json new file mode 100644 index 0000000000..773be9834a --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/base.json"], + "include": ["vite.config.ts", "__tests__"] +} diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.worker.json b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.worker.json new file mode 100644 index 0000000000..1491383e2f --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.worker.json @@ -0,0 +1,7 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/worker.json"], + "compilerOptions": { + "allowImportingTsExtensions": true + }, + "include": ["src", "cloudflare.config.ts", "worker-configuration.d.ts"] +} diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/vite.config.ts b/packages/vite-plugin-cloudflare/playground/experimental-config/vite.config.ts new file mode 100644 index 0000000000..bc2956a6cd --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/vite.config.ts @@ -0,0 +1,12 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + inspectorPort: false, + persistState: false, + experimental: { newConfig: true }, + }), + ], +}); diff --git a/packages/vite-plugin-cloudflare/playground/experimental-config/worker-configuration.d.ts b/packages/vite-plugin-cloudflare/playground/experimental-config/worker-configuration.d.ts new file mode 100644 index 0000000000..fed3a19f41 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/experimental-config/worker-configuration.d.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +// Generated by @cloudflare/config +import type { InferEnv, InferDurableNamespaces, InferMainModule, UnwrapConfig } from "@cloudflare/vite-plugin/experimental-config"; +import type Config from "./cloudflare.config"; + +type WorkerConfig = UnwrapConfig; + +declare global { + namespace Cloudflare { + interface GlobalProps { + mainModule: InferMainModule; + durableNamespaces: InferDurableNamespaces; + } + interface Env extends InferEnv {} + } + interface Env extends Cloudflare.Env {} +} diff --git a/packages/vite-plugin-cloudflare/src/__tests__/experimental-new-config.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/experimental-new-config.spec.ts new file mode 100644 index 0000000000..a4159992c9 --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/__tests__/experimental-new-config.spec.ts @@ -0,0 +1,320 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { removeDirSync } from "@cloudflare/workers-utils"; +import { afterEach, beforeEach, describe, test } from "vitest"; +import { resolvePluginConfig } from "../plugin-config"; +import type { PluginConfig, WorkersResolvedConfig } from "../plugin-config"; + +const viteEnv = { mode: "development", command: "serve" as const }; + +// Create the temp directory inside the package so Node can resolve the +// workspace-linked `@cloudflare/config` import from the generated +// `cloudflare.config.ts` (Node walks up the directory tree looking for +// `node_modules`). +const FIXTURES_ROOT = path.resolve( + __dirname, + "fixtures", + "experimental-newconfig" +); + +describe("resolvePluginConfig - experimental.newConfig", () => { + let tempDir: string; + + beforeEach(() => { + fs.mkdirSync(FIXTURES_ROOT, { recursive: true }); + tempDir = fs.realpathSync( + fs.mkdtempSync(path.join(FIXTURES_ROOT, "case-")) + ); + }); + + afterEach(() => { + removeDirSync(tempDir); + }); + + function seedWorkerSource() { + fs.mkdirSync(path.join(tempDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(tempDir, "src/index.ts"), + "export default { fetch() { return new Response('ok'); } }" + ); + } + + function writeWorkerConfig(body: string) { + fs.writeFileSync(path.join(tempDir, "cloudflare.config.ts"), body); + } + + test("throws when cloudflare.config.ts is missing", async ({ expect }) => { + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + await expect( + resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) + ).rejects.toThrow(/no `cloudflare\.config\.ts` was found/); + }); + + test("throws when configPath is combined with experimental.newConfig", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'w',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + configPath: "wrangler.jsonc", + experimental: { newConfig: true }, + }; + + await expect( + resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) + ).rejects.toThrow(/`configPath` cannot be used together/); + }); + + test("throws when auxiliaryWorkers are combined with experimental.newConfig", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'w',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + auxiliaryWorkers: [{ configPath: "aux.jsonc" }], + }; + + await expect( + resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) + ).rejects.toThrow(/auxiliaryWorkers/); + }); + + test("loads a cloudflare.config.ts and produces a worker resolved config", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + const result = (await resolvePluginConfig( + pluginConfig, + { root: tempDir }, + viteEnv + )) as WorkersResolvedConfig; + + expect(result.type).toBe("workers"); + expect(result.experimental.newConfig).toEqual({ + types: { generate: true }, + }); + const worker = result.environmentNameToWorkerMap.get( + "experimental_config_worker" + ); + expect(worker).toBeDefined(); + expect(worker?.config.name).toBe("experimental-config-worker"); + expect(worker?.config.compatibility_date).toBe("2024-12-30"); + expect(worker?.config.main).toBe(path.join(tempDir, "src/index.ts")); + }); + + test("evaluates a function config and passes the Vite mode", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker((ctx) => ({", + " name: `worker-${ctx.mode}`,", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "}));", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + const result = (await resolvePluginConfig( + pluginConfig, + { root: tempDir }, + viteEnv + )) as WorkersResolvedConfig; + + expect(result.type).toBe("workers"); + const worker = result.environmentNameToWorkerMap.get("worker_development"); + expect(worker).toBeDefined(); + expect(worker?.config.name).toBe("worker-development"); + }); + + test("adds cloudflare.config.ts to configPaths for watching", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + const result = (await resolvePluginConfig( + pluginConfig, + { root: tempDir }, + viteEnv + )) as WorkersResolvedConfig; + + const configPaths = Array.from(result.configPaths); + expect(configPaths).toContain(path.join(tempDir, "cloudflare.config.ts")); + // Transitive `dependencies` from @cloudflare/config's loadConfig + // (covered by its own unit tests) are also merged into configPaths. + }); + + test("writes worker-configuration.d.ts pointing at the vite-plugin subpath", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + await resolvePluginConfig( + { experimental: { newConfig: true } }, + { root: tempDir }, + viteEnv + ); + + const dtsPath = path.join(tempDir, "worker-configuration.d.ts"); + expect(fs.existsSync(dtsPath)).toBe(true); + const content = fs.readFileSync(dtsPath, "utf8"); + expect(content).toContain( + `from "@cloudflare/vite-plugin/experimental-config"` + ); + expect(content).toContain(`import type Config from "./cloudflare.config"`); + expect(content).not.toContain(`} from "@cloudflare/config"`); + }); + + test("does not write the .d.ts when types.generate is false", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + + await resolvePluginConfig( + { experimental: { newConfig: { types: { generate: false } } } }, + { root: tempDir }, + viteEnv + ); + + const dtsPath = path.join(tempDir, "worker-configuration.d.ts"); + expect(fs.existsSync(dtsPath)).toBe(false); + }); + + test.for([ + { mode: "development", command: "serve" as const }, + { mode: "production", command: "build" as const }, + ])("throws on durable-object exports ($command)", async (env, { expect }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + " exports: {", + " Counter: { type: 'durable-object', storage: 'sqlite' },", + " },", + "});", + ].join("\n") + ); + + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + await expect( + resolvePluginConfig(pluginConfig, { root: tempDir }, env) + ).rejects.toThrow(/Durable Object exports/); + }); + + test("does not rewrite worker-configuration.d.ts when content is unchanged", async ({ + expect, + }) => { + seedWorkerSource(); + writeWorkerConfig( + [ + "import { defineWorker } from '@cloudflare/config';", + "export default defineWorker({", + " name: 'experimental-config-worker',", + " entrypoint: './src/index.ts',", + " compatibilityDate: '2024-12-30',", + "});", + ].join("\n") + ); + const pluginConfig: PluginConfig = { + experimental: { newConfig: true }, + }; + + await resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv); + const dtsPath = path.join(tempDir, "worker-configuration.d.ts"); + const firstMtime = fs.statSync(dtsPath).mtimeMs; + + // Ensure mtime resolution boundary is crossed before the second run. + await new Promise((r) => setTimeout(r, 25)); + + await resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv); + const secondMtime = fs.statSync(dtsPath).mtimeMs; + + expect(secondMtime).toBe(firstMtime); + }); +}); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts index 4404c78a7e..4d9ba69b1e 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts @@ -36,7 +36,9 @@ describe("resolvePluginConfig - auxiliary workers", () => { return configPath; } - test("should resolve auxiliary worker from config file", ({ expect }) => { + test("should resolve auxiliary worker from config file", async ({ + expect, + }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); // Create auxiliary worker config @@ -58,16 +60,16 @@ describe("resolvePluginConfig - auxiliary workers", () => { auxiliaryWorkers: [{ configPath: auxConfigPath }], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); expect(result.environmentNameToWorkerMap.get("aux_worker")).toBeDefined(); }); - test("should resolve inline auxiliary worker with config object", ({ + test("should resolve inline auxiliary worker with config object", async ({ expect, }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); @@ -86,11 +88,11 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("inline_aux_worker"); @@ -100,7 +102,7 @@ describe("resolvePluginConfig - auxiliary workers", () => { expect(auxWorker.config.main).toBe(path.join(tempDir, "src/aux.ts")); }); - test("should resolve inline auxiliary worker with config function", ({ + test("should resolve inline auxiliary worker with config function", async ({ expect, }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); @@ -119,18 +121,18 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("fn_aux_worker"); assert(auxWorker); expect(auxWorker.config.name).toBe("fn-aux-worker"); }); - test("should auto-populate topLevelName from name if not set", ({ + test("should auto-populate topLevelName from name if not set", async ({ expect, }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); @@ -149,11 +151,11 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("my_aux_worker"); assert(auxWorker); @@ -161,7 +163,9 @@ describe("resolvePluginConfig - auxiliary workers", () => { expect(auxWorker.config.topLevelName).toBe("my-aux-worker"); }); - test("should apply config to file-based auxiliary worker", ({ expect }) => { + test("should apply config to file-based auxiliary worker", async ({ + expect, + }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); // Create auxiliary worker config with initial values @@ -190,11 +194,11 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("aux_worker"); assert(auxWorker); @@ -204,7 +208,7 @@ describe("resolvePluginConfig - auxiliary workers", () => { expect(auxWorker.config.name).toBe("aux-worker"); }); - test("should pass entryWorkerConfig as second parameter to auxiliary worker config function", ({ + test("should pass entryWorkerConfig as second parameter to auxiliary worker config function", async ({ expect, }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); @@ -233,11 +237,11 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("aux_worker"); assert(auxWorker); @@ -246,7 +250,7 @@ describe("resolvePluginConfig - auxiliary workers", () => { expect(auxWorker.config.compatibility_date).toBe("2024-01-01"); }); - test("should allow auxiliary worker to inherit entry worker compatibility_flags", ({ + test("should allow auxiliary worker to inherit entry worker compatibility_flags", async ({ expect, }) => { // Create entry worker with compatibility_flags @@ -279,11 +283,11 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("aux_worker"); assert(auxWorker); @@ -292,7 +296,7 @@ describe("resolvePluginConfig - auxiliary workers", () => { ); }); - test("should throw if inline auxiliary worker is missing required fields", ({ + test("should throw if inline auxiliary worker is missing required fields", async ({ expect, }) => { const entryConfigPath = createEntryWorkerConfig(tempDir); @@ -308,9 +312,9 @@ describe("resolvePluginConfig - auxiliary workers", () => { ], }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow(); + ).rejects.toThrow(); }); }); @@ -327,7 +331,7 @@ describe("resolvePluginConfig - entry worker config()", () => { const viteEnv = { mode: "development", command: "serve" as const }; - test("should convert assets-only worker to worker with server logic when config() adds main", ({ + test("should convert assets-only worker to worker with server logic when config() adds main", async ({ expect, }) => { // Create a config file without main (assets-only) @@ -352,11 +356,11 @@ describe("resolvePluginConfig - entry worker config()", () => { }, }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; // Should now be a worker with server logic, not assets-only expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( @@ -366,7 +370,9 @@ describe("resolvePluginConfig - entry worker config()", () => { expect(entryWorker.config.main).toMatch(/index\.ts$/); }); - test("should allow config() function to add main field", ({ expect }) => { + test("should allow config() function to add main field", async ({ + expect, + }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); fs.writeFileSync( configPath, @@ -386,11 +392,11 @@ describe("resolvePluginConfig - entry worker config()", () => { }), }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( result.entryWorkerEnvironmentName @@ -398,7 +404,7 @@ describe("resolvePluginConfig - entry worker config()", () => { expect(entryWorker).toBeDefined(); }); - test("should remain assets-only when config() does not add main", ({ + test("should remain assets-only when config() does not add main", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -417,7 +423,7 @@ describe("resolvePluginConfig - entry worker config()", () => { }, }; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -444,12 +450,12 @@ describe("resolvePluginConfig - zero-config mode", () => { const viteEnv = { mode: "development", command: "serve" as const }; - test("should return an assets-only config when no wrangler config exists", ({ + test("should return an assets-only config when no wrangler config exists", async ({ expect, }) => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -458,7 +464,9 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(result.type).toBe("assets-only"); }); - test("should derive worker name from package.json name", ({ expect }) => { + test("should derive worker name from package.json name", async ({ + expect, + }) => { fs.writeFileSync( path.join(tempDir, "package.json"), JSON.stringify({ name: "my-awesome-app" }) @@ -466,7 +474,7 @@ describe("resolvePluginConfig - zero-config mode", () => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -478,7 +486,7 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(assetsOnlyResult.config.topLevelName).toBe("my-awesome-app"); }); - test("should normalize invalid worker names from package.json", ({ + test("should normalize invalid worker names from package.json", async ({ expect, }) => { fs.writeFileSync( @@ -488,7 +496,7 @@ describe("resolvePluginConfig - zero-config mode", () => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -500,7 +508,7 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(assetsOnlyResult.config.name).toBe("scope-my-package-name"); }); - test("should fall back to directory name when package.json has no name", ({ + test("should fall back to directory name when package.json has no name", async ({ expect, }) => { const namedDir = path.join(tempDir, "my-test-project"); @@ -512,7 +520,7 @@ describe("resolvePluginConfig - zero-config mode", () => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: namedDir }, viteEnv @@ -523,7 +531,7 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(assetsOnlyResult.config.name).toBe("my-test-project"); }); - test("should fall back to directory name when no package.json exists", ({ + test("should fall back to directory name when no package.json exists", async ({ expect, }) => { const namedDir = path.join(tempDir, "another-project"); @@ -531,7 +539,7 @@ describe("resolvePluginConfig - zero-config mode", () => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: namedDir }, viteEnv @@ -542,10 +550,12 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(assetsOnlyResult.config.name).toBe("another-project"); }); - test("should set a compatibility date in zero-config mode", ({ expect }) => { + test("should set a compatibility date in zero-config mode", async ({ + expect, + }) => { const pluginConfig: PluginConfig = {}; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -557,7 +567,7 @@ describe("resolvePluginConfig - zero-config mode", () => { expect(assetsOnlyResult.config.compatibility_date).toBe("2024-01-01"); }); - test("should allow config() to add main in zero-config mode", ({ + test("should allow config() to add main in zero-config mode", async ({ expect, }) => { fs.writeFileSync( @@ -573,11 +583,11 @@ describe("resolvePluginConfig - zero-config mode", () => { }, }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( result.entryWorkerEnvironmentName @@ -624,7 +634,7 @@ describe("resolvePluginConfig - internal config path env fallback", () => { return configPath; } - test("should resolve entry worker config from CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH loaded from env files", ({ + test("should resolve entry worker config from CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH loaded from env files", async ({ expect, }) => { const hiddenConfigPath = createWorkerConfig( @@ -636,11 +646,11 @@ describe("resolvePluginConfig - internal config path env fallback", () => { "CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH=.sst/wrangler.jsonc\n" ); - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( {}, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( @@ -654,7 +664,7 @@ describe("resolvePluginConfig - internal config path env fallback", () => { expect([...result.configPaths]).toEqual([hiddenConfigPath]); }); - test("should prefer CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH over auto-discovered config", ({ + test("should prefer CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH over auto-discovered config", async ({ expect, }) => { createWorkerConfig(tempDir, "root-worker"); @@ -664,11 +674,11 @@ describe("resolvePluginConfig - internal config path env fallback", () => { ); vi.stubEnv("CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH", ".sst/wrangler.jsonc"); - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( {}, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( @@ -679,7 +689,7 @@ describe("resolvePluginConfig - internal config path env fallback", () => { expect([...result.configPaths]).toEqual([hiddenConfigPath]); }); - test("should prefer explicit configPath over CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH", ({ + test("should prefer explicit configPath over CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH", async ({ expect, }) => { createWorkerConfig(path.join(tempDir, ".sst"), "hidden-worker"); @@ -689,11 +699,11 @@ describe("resolvePluginConfig - internal config path env fallback", () => { ); vi.stubEnv("CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH", ".sst/wrangler.jsonc"); - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( { configPath: explicitConfigPath }, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( @@ -732,24 +742,24 @@ describe("resolvePluginConfig - force-local env override", () => { const viteEnv = { mode: "development", command: "serve" as const }; - test("remote bindings default to enabled", ({ expect }) => { - const result = resolvePluginConfig({}, { root: tempDir }, viteEnv); + test("remote bindings default to enabled", async ({ expect }) => { + const result = await resolvePluginConfig({}, { root: tempDir }, viteEnv); expect(result.remoteBindings).toBe(true); }); - test("CLOUDFLARE_VITE_FORCE_LOCAL=true forces remote bindings off", ({ + test("CLOUDFLARE_VITE_FORCE_LOCAL=true forces remote bindings off", async ({ expect, }) => { vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", "true"); - const result = resolvePluginConfig({}, { root: tempDir }, viteEnv); + const result = await resolvePluginConfig({}, { root: tempDir }, viteEnv); expect(result.remoteBindings).toBe(false); }); - test("CLOUDFLARE_VITE_FORCE_LOCAL=true overrides remoteBindings: true in config", ({ + test("CLOUDFLARE_VITE_FORCE_LOCAL=true overrides remoteBindings: true in config", async ({ expect, }) => { vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", "true"); - const result = resolvePluginConfig( + const result = await resolvePluginConfig( { remoteBindings: true }, { root: tempDir }, viteEnv @@ -757,10 +767,10 @@ describe("resolvePluginConfig - force-local env override", () => { expect(result.remoteBindings).toBe(false); }); - test("unset CLOUDFLARE_VITE_FORCE_LOCAL respects remoteBindings config", ({ + test("unset CLOUDFLARE_VITE_FORCE_LOCAL respects remoteBindings config", async ({ expect, }) => { - const result = resolvePluginConfig( + const result = await resolvePluginConfig( { remoteBindings: false }, { root: tempDir }, viteEnv @@ -782,7 +792,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { const viteEnv = { mode: "development", command: "serve" as const }; - test("should accept Wrangler config file with only name, filling in compatibility_date from defaults", ({ + test("should accept Wrangler config file with only name, filling in compatibility_date from defaults", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -798,7 +808,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { configPath, }; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -813,7 +823,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { ); }); - test("should accept Wrangler config file missing name when config() provides it", ({ + test("should accept Wrangler config file missing name when config() provides it", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -832,7 +842,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { }, }; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -843,7 +853,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { expect(assetsOnlyResult.config.name).toBe("configured-worker"); }); - test("should accept Wrangler config file missing compatibility_date when config() provides it", ({ + test("should accept Wrangler config file missing compatibility_date when config() provides it", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -862,7 +872,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { }, }; - const result = resolvePluginConfig( + const result = await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv @@ -873,7 +883,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { expect(assetsOnlyResult.config.compatibility_date).toBe("2025-06-01"); }); - test("should accept minimal Wrangler config file when all required fields come from config()", ({ + test("should accept minimal Wrangler config file when all required fields come from config()", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -897,11 +907,11 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { }, }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const entryWorker = result.environmentNameToWorkerMap.get( result.entryWorkerEnvironmentName @@ -912,7 +922,7 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { expect(entryWorker.config.compatibility_flags).toContain("nodejs_compat"); }); - test("should accept auxiliary worker Wrangler config file missing fields when config() provides them", ({ + test("should accept auxiliary worker Wrangler config file missing fields when config() provides them", async ({ expect, }) => { // Create entry worker config @@ -953,11 +963,11 @@ describe("resolvePluginConfig - defaults fill in missing fields", () => { ], }; - const result = resolvePluginConfig( + const result = (await resolvePluginConfig( pluginConfig, { root: tempDir }, viteEnv - ) as WorkersResolvedConfig; + )) as WorkersResolvedConfig; expect(result.type).toBe("workers"); const auxWorker = result.environmentNameToWorkerMap.get("aux_from_config"); assert(auxWorker); @@ -980,7 +990,7 @@ describe("resolvePluginConfig - environment name validation", () => { const viteEnv = { mode: "development", command: "serve" as const }; - test("throws when environment name is 'client'", ({ expect }) => { + test("throws when environment name is 'client'", async ({ expect }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); fs.writeFileSync( configPath, @@ -994,12 +1004,14 @@ describe("resolvePluginConfig - environment name validation", () => { viteEnvironment: { name: "client" }, }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow('"client" is a reserved Vite environment name'); + ).rejects.toThrow('"client" is a reserved Vite environment name'); }); - test("throws when child environment duplicates parent", ({ expect }) => { + test("throws when child environment duplicates parent", async ({ + expect, + }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); fs.writeFileSync( configPath, @@ -1013,12 +1025,14 @@ describe("resolvePluginConfig - environment name validation", () => { viteEnvironment: { childEnvironments: ["entry_worker"] }, }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow('Duplicate Vite environment name: "entry_worker"'); + ).rejects.toThrow('Duplicate Vite environment name: "entry_worker"'); }); - test("throws when child environments duplicate each other", ({ expect }) => { + test("throws when child environments duplicate each other", async ({ + expect, + }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); fs.writeFileSync( configPath, @@ -1032,12 +1046,14 @@ describe("resolvePluginConfig - environment name validation", () => { viteEnvironment: { childEnvironments: ["child", "child"] }, }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow('Duplicate Vite environment name: "child"'); + ).rejects.toThrow('Duplicate Vite environment name: "child"'); }); - test("throws when auxiliary Worker duplicates entry Worker", ({ expect }) => { + test("throws when auxiliary Worker duplicates entry Worker", async ({ + expect, + }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); fs.writeFileSync( configPath, @@ -1064,12 +1080,12 @@ describe("resolvePluginConfig - environment name validation", () => { ], }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow('Duplicate Vite environment name: "entry_worker"'); + ).rejects.toThrow('Duplicate Vite environment name: "entry_worker"'); }); - test("throws when auxiliary Worker child duplicates entry Worker", ({ + test("throws when auxiliary Worker child duplicates entry Worker", async ({ expect, }) => { const configPath = path.join(tempDir, "wrangler.jsonc"); @@ -1098,8 +1114,8 @@ describe("resolvePluginConfig - environment name validation", () => { ], }; - expect(() => + await expect( resolvePluginConfig(pluginConfig, { root: tempDir }, viteEnv) - ).toThrow('Duplicate Vite environment name: "entry_worker"'); + ).rejects.toThrow('Duplicate Vite environment name: "entry_worker"'); }); }); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts index 33aec9f1ea..55480575ee 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts @@ -120,7 +120,7 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { return () => removeDirSync(tempDir); }); - function createMockContext(options?: { + async function createMockContext(options?: { auxiliaryWorkers?: Array<{ configPath: string }>; }) { const mockContext = new PluginContext({ @@ -130,7 +130,7 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { }); mockContext.setResolvedPluginConfig( - resolvePluginConfig( + await resolvePluginConfig( { configPath: primaryConfigPath, auxiliaryWorkers: options?.auxiliaryWorkers, @@ -143,9 +143,9 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { return mockContext; } - test("prints shortcut hints in registration order", ({ expect }) => { + test("prints shortcut hints in registration order", async ({ expect }) => { vi.spyOn(tunnelPlugin, "isTunnelOpen").mockReturnValue(false); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); serverLogs.info = []; mockServer.bindCLIShortcuts(); @@ -163,10 +163,10 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { ); }); - test("registers custom shortcuts in order", ({ expect }) => { + test("registers custom shortcuts in order", async ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); expect(mockServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ @@ -195,9 +195,9 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { }); }); - test("prints bindings with a single Worker", ({ expect }) => { + test("prints bindings with a single Worker", async ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); @@ -220,11 +220,11 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { `); }); - test("prints bindings with multi Workers", ({ expect }) => { + test("prints bindings with multi Workers", async ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); addShortcuts( mockServer, - createMockContext({ + await createMockContext({ auxiliaryWorkers: [{ configPath: auxiliaryConfigPath }], }) ); @@ -256,7 +256,7 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { test("registers explorer shortcut with correct URL", async ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const explorerShortcut = customShortcuts?.find((s) => s.key === "e"); @@ -276,7 +276,7 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { .spyOn(tunnelPlugin, "extendTunnelExpiry") .mockImplementation(() => {}); const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const toggleShortcut = customShortcuts?.find((s) => s.key === "t"); @@ -289,12 +289,12 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { expect(extendExpirySpy).toHaveBeenCalledTimes(1); }); - test("display tunnel shortcut hint", ({ expect }) => { + test("display tunnel shortcut hint", async ({ expect }) => { vi.spyOn(tunnelPlugin, "isTunnelOpen") .mockReturnValueOnce(false) .mockReturnValueOnce(true); - addShortcuts(mockServer, createMockContext()); + addShortcuts(mockServer, await createMockContext()); serverLogs.info = []; mockServer.bindCLIShortcuts(); diff --git a/packages/vite-plugin-cloudflare/src/experimental-config.ts b/packages/vite-plugin-cloudflare/src/experimental-config.ts new file mode 100644 index 0000000000..f22bc1a70d --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/experimental-config.ts @@ -0,0 +1,5 @@ +/** + * Experimental entry point that re-exports the user-facing config-authoring + * surface from `@cloudflare/config`. + */ +export * from "@cloudflare/config/public"; diff --git a/packages/vite-plugin-cloudflare/src/index.ts b/packages/vite-plugin-cloudflare/src/index.ts index de408d7cad..4ec597814e 100644 --- a/packages/vite-plugin-cloudflare/src/index.ts +++ b/packages/vite-plugin-cloudflare/src/index.ts @@ -71,9 +71,9 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { { name: "vite-plugin-cloudflare", sharedDuringBuild: true, - config(userConfig, env) { + async config(userConfig, env) { ctx.setResolvedPluginConfig( - resolvePluginConfig(pluginConfig, userConfig, env) + await resolvePluginConfig(pluginConfig, userConfig, env) ); if (env.command === "build") { diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index 6ead0b6f40..a4ce674f9a 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -1,3 +1,4 @@ +import * as fs from "node:fs"; import * as path from "node:path"; import { parseStaticRouting } from "@cloudflare/workers-shared/utils/configuration/parseStaticRouting"; import { defu } from "defu"; @@ -9,6 +10,7 @@ import { hasNodeJsCompat, NodeJsCompat } from "./nodejs-compat"; import { getValidatedWranglerConfigPath, readWorkerConfigFromFile, + readWorkerConfigFromRaw, resolveWorkerType, } from "./workers-configs"; import type { Defined } from "./utils"; @@ -20,6 +22,7 @@ import type { WorkerWithServerLogicResolvedConfig, } from "./workers-configs"; import type { StaticRouting } from "@cloudflare/workers-shared/utils/types"; +import type { RawConfig } from "@cloudflare/workers-utils"; import type { Unstable_Config } from "wrangler"; export type PersistState = boolean | { path: string }; @@ -83,11 +86,46 @@ type PrerenderWorkerConfig = | PrerenderWorkerFileConfig | PrerenderWorkerInlineConfig; +interface ExperimentalNewConfig { + /** Options for type generation. */ + types?: { + /** + * Whether to auto-generate `worker-configuration.d.ts` at the project + * root. Defaults to `true`. + */ + generate?: boolean; + }; +} + +interface ResolvedExperimentalNewConfig { + types: { generate: boolean }; +} + interface Experimental { /** Experimental support for handling the _headers and _redirects files during Vite dev mode. */ headersAndRedirectsDevModeSupport?: boolean; /** Experimental support for a dedicated prerender Worker */ prerenderWorker?: PrerenderWorkerConfig; + /** + * Experimental support for loading the entry Worker's configuration from + * `cloudflare.config.ts` instead of `wrangler.json` / + * `wrangler.jsonc` / `wrangler.toml`. + * + * Pass `true` for defaults, or an object to customize behaviour. + */ + newConfig?: boolean | ExperimentalNewConfig; +} + +function normalizeNewConfig( + option: boolean | ExperimentalNewConfig | undefined +): ResolvedExperimentalNewConfig | undefined { + if (option === undefined || option === false) { + return undefined; + } + if (option === true) { + return { types: { generate: true } }; + } + return { types: { generate: option.types?.generate ?? true } }; } type FilteredEntryWorkerConfig = Omit< @@ -134,7 +172,9 @@ export interface Worker { interface BaseResolvedConfig { persistState: PersistState; inspectorPort: number | false | undefined; - experimental: Pick; + experimental: Pick & { + newConfig?: ResolvedExperimentalNewConfig; + }; remoteBindings: boolean; tunnel: TunnelConfig; } @@ -238,6 +278,12 @@ function resolveWorkerConfig( configPath: string | undefined; env: string | undefined; visitedConfigPaths: Set; + /** + * When provided, skip reading from `configPath` and instead normalize + * this in-memory `RawConfig` (produced e.g. by `convertToWranglerConfig` + * from `@cloudflare/config`). + */ + rawConfigOverride?: RawConfig; } & ( | { configCustomizer: WorkerConfigCustomizer | undefined; @@ -251,7 +297,13 @@ function resolveWorkerConfig( let raw: Unstable_Config; let nonApplicable: NonApplicableConfigMap; - if (options.configPath) { + if (options.rawConfigOverride) { + ({ + raw, + config: workerConfig, + nonApplicable, + } = readWorkerConfigFromRaw(options.rawConfigOverride)); + } else if (options.configPath) { // File config already has defaults applied ({ raw, @@ -302,11 +354,14 @@ function resolveWorkerConfig( }); } -export function resolvePluginConfig( +export async function resolvePluginConfig( pluginConfig: PluginConfig, userConfig: vite.UserConfig, viteEnv: vite.ConfigEnv -): ResolvedPluginConfig { +): Promise { + const resolvedNewConfig = normalizeNewConfig( + pluginConfig.experimental?.newConfig + ); const shared = { persistState: pluginConfig.persistState ?? true, inspectorPort: pluginConfig.inspectorPort, @@ -320,6 +375,7 @@ export function resolvePluginConfig( experimental: { headersAndRedirectsDevModeSupport: pluginConfig.experimental?.headersAndRedirectsDevModeSupport, + newConfig: resolvedNewConfig, }, }; const root = userConfig.root ? path.resolve(userConfig.root) : process.cwd(); @@ -353,20 +409,50 @@ export function resolvePluginConfig( const configPaths = new Set(); const cloudflareEnv = prefixedEnv.CLOUDFLARE_ENV; const validateAndAddEnvironmentName = createEnvironmentNameValidator(); - const requestedEntryWorkerConfigPath = - pluginConfig.configPath ?? prefixedEnv.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH; - const configPath = getValidatedWranglerConfigPath( - root, - requestedEntryWorkerConfigPath - ); + + let configPath: string | undefined; + let rawConfigOverride: RawConfig | undefined; + + if (resolvedNewConfig) { + if (pluginConfig.configPath) { + throw new Error( + "`configPath` cannot be used together with `experimental.newConfig`. Configure the entry Worker via `cloudflare.config.ts` instead." + ); + } + if (pluginConfig.auxiliaryWorkers?.length) { + throw new Error( + "`auxiliaryWorkers` are not yet supported when `experimental.newConfig` is enabled." + ); + } + const result = await loadNewConfig({ + root, + mode: viteEnv.mode, + generateTypes: resolvedNewConfig.types.generate, + }); + configPath = result.configPath; + rawConfigOverride = result.rawConfig; + configPaths.add(result.configPath); + for (const dep of result.dependencies) { + configPaths.add(dep); + } + } else { + const requestedEntryWorkerConfigPath = + pluginConfig.configPath ?? + prefixedEnv.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH; + configPath = getValidatedWranglerConfigPath( + root, + requestedEntryWorkerConfigPath + ); + } // Build entry worker config: defaults → file config → config() const entryWorkerResolvedConfig = resolveWorkerConfig({ root, - configPath, + configPath: resolvedNewConfig ? undefined : configPath, env: cloudflareEnv, configCustomizer: pluginConfig.config, visitedConfigPaths: configPaths, + rawConfigOverride, }); const environmentNameToWorkerMap = new Map(); @@ -593,3 +679,101 @@ function resolveWorker( devOnly, }; } + +const NEW_CONFIG_FILENAME = "cloudflare.config.ts"; +const TYPES_OUTPUT_FILENAME = "worker-configuration.d.ts"; +const EXPERIMENTAL_CONFIG_PKG = "@cloudflare/vite-plugin/experimental-config"; + +/** + * Load and convert a `cloudflare.config.ts` file via `@cloudflare/config`. Returns + * the resulting Wrangler `RawConfig`, the absolute path of the loaded file, + * and the set of files imported while resolving the config (for watch-mode). + * + * If `generateTypes` is true, also writes `worker-configuration.d.ts` next to + * the config when the generated content differs from what's already on disk. + */ +async function loadNewConfig(options: { + root: string; + mode: string; + generateTypes: boolean; +}): Promise<{ + rawConfig: RawConfig; + configPath: string; + dependencies: Set; +}> { + const configPath = path.resolve(options.root, NEW_CONFIG_FILENAME); + + if (!fs.existsSync(configPath)) { + throw new Error( + `\`experimental.newConfig\` is enabled but no \`${NEW_CONFIG_FILENAME}\` was found at ${configPath}.` + ); + } + + // Dynamic import so users who don't enable `experimental.newConfig` never + // pay the cost of loading `@cloudflare/config` (and its Node module hooks). + const { + loadConfig, + ConfigSchema, + convertToWranglerConfig, + generateTypes: generateTypesFn, + resolveWorkerDefinition, + } = await import("@cloudflare/config"); + + const { config: rawExport, dependencies } = await loadConfig(configPath); + + const resolved = await resolveWorkerDefinition(rawExport, { + mode: options.mode, + }); + + const parsed = ConfigSchema.safeParse(resolved); + if (!parsed.success) { + throw new Error( + `Invalid \`${NEW_CONFIG_FILENAME}\`:\n${parsed.error.message}` + ); + } + + const rawConfig = convertToWranglerConfig(parsed.data); + + if (options.generateTypes) { + writeWorkerConfigurationDts({ + root: options.root, + configPath, + generateTypes: generateTypesFn, + }); + } + + return { rawConfig, configPath, dependencies }; +} + +/** + * Write `worker-configuration.d.ts` to the project root using + * `@cloudflare/config`'s `generateTypes`, targeting the vite-plugin's + * `experimental-config` subpath (so users don't need a direct dependency on + * `@cloudflare/config`). + * + * Reads the existing file first and only writes if the content differs, to + * avoid touching mtimes unnecessarily. + */ +function writeWorkerConfigurationDts(options: { + root: string; + configPath: string; + generateTypes: (opts: { configPath: string; packageName?: string }) => string; +}): void { + const outputPath = path.resolve(options.root, TYPES_OUTPUT_FILENAME); + const relativeConfigPath = + "./" + path.relative(options.root, options.configPath); + const content = options.generateTypes({ + configPath: relativeConfigPath, + packageName: EXPERIMENTAL_CONFIG_PKG, + }); + + let existing: string | undefined; + try { + existing = fs.readFileSync(outputPath, "utf8"); + } catch { + // File doesn't exist yet — we'll create it below. + } + if (existing !== content) { + fs.writeFileSync(outputPath, content); + } +} diff --git a/packages/vite-plugin-cloudflare/src/workers-configs.ts b/packages/vite-plugin-cloudflare/src/workers-configs.ts index 417513c1d4..77e2c4dc45 100644 --- a/packages/vite-plugin-cloudflare/src/workers-configs.ts +++ b/packages/vite-plugin-cloudflare/src/workers-configs.ts @@ -1,11 +1,13 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { normalizeAndValidateConfig } from "@cloudflare/workers-utils"; import * as wrangler from "wrangler"; import type { ResolvedAssetsOnlyConfig, ResolvedWorkerConfig, } from "./plugin-config"; import type { Optional } from "./utils"; +import type { RawConfig } from "@cloudflare/workers-utils"; import type { Unstable_Config as RawWorkerConfig } from "wrangler"; export type WorkerResolvedConfig = @@ -117,10 +119,11 @@ const nullableNonApplicable = [ "tsconfig", ] as const; -function readWorkerConfig( - configPath: string, - env: string | undefined -): { +/** + * Apply the Vite-specific sanitization to a normalized Worker config, tracking + * any fields that are non-applicable when running under Vite. + */ +function processNormalizedWorkerConfig(normalized: RawWorkerConfig): { raw: RawWorkerConfig; config: WorkerConfig; nonApplicable: NonApplicableConfigMap; @@ -130,12 +133,7 @@ function readWorkerConfig( notRelevant: new Set(), notSupportedOnAuxiliary: new Set(), }; - const config: Optional = - wrangler.unstable_readConfig( - { config: configPath, env }, - // Preserve the original `main` value so that Vite can resolve it - { preserveOriginalMain: true } - ); + const config: Optional = normalized; const raw = structuredClone(config) as RawWorkerConfig; nullableNonApplicable.forEach((prop) => { @@ -175,6 +173,53 @@ function readWorkerConfig( }; } +function readWorkerConfig( + configPath: string, + env: string | undefined +): { + raw: RawWorkerConfig; + config: WorkerConfig; + nonApplicable: NonApplicableConfigMap; +} { + const normalized = wrangler.unstable_readConfig( + { config: configPath, env }, + // Preserve the original `main` value so that Vite can resolve it + { preserveOriginalMain: true } + ); + return processNormalizedWorkerConfig(normalized); +} + +/** + * Normalise a `RawConfig` produced in-memory (for example by + * `convertToWranglerConfig` from `@cloudflare/config`) using the same + * validation pipeline as on-disk Wrangler config files, then apply the + * Vite-specific sanitization. + */ +export function readWorkerConfigFromRaw(rawConfig: RawConfig): { + raw: RawWorkerConfig; + config: WorkerConfig; + nonApplicable: NonApplicableConfigMap; +} { + const { config, diagnostics } = normalizeAndValidateConfig( + rawConfig, + undefined, + undefined, + {}, + // Preserve the original `main` value so that Vite can resolve it + true + ); + + if (diagnostics.hasWarnings()) { + console.warn(diagnostics.renderWarnings()); + } + + if (diagnostics.hasErrors()) { + throw new Error(diagnostics.renderErrors()); + } + + return processNormalizedWorkerConfig(config as RawWorkerConfig); +} + // TODO: separate prerender Worker warnings from auxiliary Worker warnings export function getWarningForWorkersConfigs( configs: diff --git a/packages/vite-plugin-cloudflare/tsdown.config.ts b/packages/vite-plugin-cloudflare/tsdown.config.ts index 3fa1b69cc7..87a51524bd 100644 --- a/packages/vite-plugin-cloudflare/tsdown.config.ts +++ b/packages/vite-plugin-cloudflare/tsdown.config.ts @@ -5,7 +5,9 @@ const ignoreWatch = ["dist", "playground", "e2e"]; export default defineConfig([ { - entry: "src/index.ts", + entry: { + index: "src/index.ts", + }, platform: "node", outDir: "dist", tsconfig: "tsconfig.plugin.json", @@ -40,6 +42,19 @@ export default defineConfig([ dts: false, ignoreWatch, }, + // TODO: move into main build as an additional entry once tsdown has been upgraded + { + entry: { + "experimental-config": "src/experimental-config.ts", + }, + platform: "node", + outDir: "dist", + tsconfig: "tsconfig.plugin.json", + dts: { + resolve: ["@cloudflare/config"], + }, + ignoreWatch, + }, worker("asset-worker"), worker("router-worker"), worker("runner-worker", { diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 9fad6ef9a7..f0ef4e8aa2 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1,3 +1,11 @@ +/** + * Wrangler configuration types. The JSDoc on these fields is also the source + * of truth for the equivalent fields in `@cloudflare/config` + * (`packages/config/src/types.ts` — `UserConfig` — and the binding option + * interfaces in `packages/config/src/config.ts`). When editing prose here, + * mirror the changes there. + */ + import type { Json } from "../types"; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff79030ca4..c062574355 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1613,6 +1613,33 @@ importers: 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.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)) + packages/config: + devDependencies: + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../workers-tsconfig + '@cloudflare/workers-types': + specifier: catalog:default + version: 4.20260601.1 + '@cloudflare/workers-utils': + specifier: workspace:* + version: link:../workers-utils + ts-dedent: + specifier: ^2.2.0 + version: 2.2.0 + tsdown: + specifier: 0.16.3 + version: 0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) + typescript: + specifier: catalog:default + version: 5.8.3 + 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.8.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)) + zod: + specifier: ^4.4.3 + version: 4.4.3 + packages/containers-shared: devDependencies: '@cloudflare/workers-tsconfig': @@ -1936,7 +1963,7 @@ importers: version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@cloudflare/kumo': specifier: ^1.18.0 - version: 1.18.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + version: 1.18.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3) '@cloudflare/workers-editor-shared': specifier: ^0.1.1 version: 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) @@ -2406,6 +2433,9 @@ importers: specifier: catalog:default version: 8.20.1 devDependencies: + '@cloudflare/config': + specifier: workspace:* + version: link:../config '@cloudflare/containers-shared': specifier: workspace:* version: link:../containers-shared @@ -2468,7 +2498,7 @@ importers: version: 1.2.2 tsdown: specifier: 0.16.3 - version: 0.16.3(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) + version: 0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) typescript: specifier: catalog:default version: 5.8.3 @@ -2896,6 +2926,27 @@ importers: specifier: workspace:* version: link:../../../wrangler + packages/vite-plugin-cloudflare/playground/experimental-config: + devDependencies: + '@cloudflare/vite-plugin': + specifier: workspace:* + version: link:../.. + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../../workers-tsconfig + '@cloudflare/workers-types': + specifier: catalog:default + version: 4.20260601.1 + typescript: + specifier: catalog:default + version: 5.8.3 + vite: + specifier: catalog:default + version: 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: + specifier: workspace:* + version: link:../../../wrangler + packages/vite-plugin-cloudflare/playground/external-durable-objects: devDependencies: '@cloudflare/vite-plugin': @@ -3664,7 +3715,7 @@ importers: version: 2.2.0 tsdown: specifier: 0.16.3 - version: 0.16.3(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) + version: 0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) typescript: specifier: catalog:default version: 5.8.3 @@ -4349,7 +4400,7 @@ importers: version: 22.15.17 tsdown: specifier: 0.16.3 - version: 0.16.3(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) + version: 0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) typescript: specifier: catalog:default version: 5.8.3 @@ -15177,6 +15228,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zrender@6.0.0: resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} @@ -16069,7 +16123,7 @@ snapshots: jose: 5.9.3 kysely: 0.28.11 nanostores: 1.1.1 - zod: 4.3.6 + zod: 4.4.3 optionalDependencies: '@cloudflare/workers-types': 4.20260601.1 @@ -16082,7 +16136,7 @@ snapshots: jose: 6.2.1 kysely: 0.28.11 nanostores: 1.1.1 - zod: 4.3.6 + zod: 4.4.3 optionalDependencies: '@cloudflare/workers-types': 4.20260601.1 @@ -16547,7 +16601,7 @@ snapshots: dependencies: react: 19.2.4 - '@cloudflare/kumo@1.18.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': + '@cloudflare/kumo@1.18.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3)': dependencies: '@base-ui/react': 1.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@phosphor-icons/react': 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -16562,7 +16616,7 @@ snapshots: shiki: 4.0.2 tailwind-merge: 3.4.0 optionalDependencies: - zod: 4.3.6 + zod: 4.4.3 transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react' @@ -18502,9 +18556,12 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.49': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@rolldown/binding-wasm32-wasi@1.0.1': @@ -24775,7 +24832,7 @@ snapshots: - oxc-resolver - supports-color - rolldown-plugin-dts@0.17.6(ms@2.1.3)(rolldown@1.0.0-beta.49)(typescript@5.8.3): + rolldown-plugin-dts@0.17.6(ms@2.1.3)(rolldown@1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.8.3): dependencies: '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 @@ -24786,7 +24843,7 @@ snapshots: get-tsconfig: 4.13.6 magic-string: 0.30.21 obug: 0.1.3(ms@2.1.3) - rolldown: 1.0.0-beta.49 + rolldown: 1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -24813,7 +24870,7 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.44 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.44 - rolldown@1.0.0-beta.49: + rolldown@1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.96.0 '@rolldown/pluginutils': 1.0.0-beta.49 @@ -24828,10 +24885,13 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.49 '@rolldown/binding-linux-x64-musl': 1.0.0-beta.49 '@rolldown/binding-openharmony-arm64': 1.0.0-beta.49 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.49 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.49 '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.49 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.49 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' rolldown@1.0.1: dependencies: @@ -25781,7 +25841,7 @@ snapshots: - supports-color - vue-tsc - tsdown@0.16.3(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3): + tsdown@0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3): dependencies: ansis: 4.2.0 cac: 6.7.14 @@ -25790,17 +25850,19 @@ snapshots: empathic: 2.0.0 hookable: 5.5.3 obug: 0.1.3(ms@2.1.3) - rolldown: 1.0.0-beta.49 - rolldown-plugin-dts: 0.17.6(ms@2.1.3)(rolldown@1.0.0-beta.49)(typescript@5.8.3) + rolldown: 1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + rolldown-plugin-dts: 0.17.6(ms@2.1.3)(rolldown@1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.8.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.4.1 - unrun: 0.2.8(synckit@0.11.12) + unrun: 0.2.8(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(synckit@0.11.12) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@ts-macro/tsc' - '@typescript/native-preview' - ms @@ -26164,12 +26226,15 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unrun@0.2.8(synckit@0.11.12): + unrun@0.2.8(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(synckit@0.11.12): dependencies: '@oxc-project/runtime': 0.96.0 - rolldown: 1.0.0-beta.49 + rolldown: 1.0.0-beta.49(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optionalDependencies: synckit: 0.11.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' until-async@3.0.2: {} @@ -26771,6 +26836,8 @@ snapshots: zod@4.3.6: {} + zod@4.4.3: {} + zrender@6.0.0: dependencies: tslib: 2.3.0 From a3eea277aae46450aec1f0c811e3fe256022c46e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:57:11 +0000 Subject: [PATCH 2/6] Bump the workerd-and-workers-types group with 2 updates (#14175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Somhairle MacLeòid --- .changeset/dependabot-update-14175.md | 12 + packages/miniflare/package.json | 2 +- packages/miniflare/src/cf.ts | 4 + packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 374 +++++++++++++------------- pnpm-workspace.yaml | 4 +- 6 files changed, 207 insertions(+), 191 deletions(-) create mode 100644 .changeset/dependabot-update-14175.md diff --git a/.changeset/dependabot-update-14175.md b/.changeset/dependabot-update-14175.md new file mode 100644 index 0000000000..ef68dd5b99 --- /dev/null +++ b/.changeset/dependabot-update-14175.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.20260601.1 | 1.20260603.1 | diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 47547a0d61..ad996fbf43 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.20260601.1", + "workerd": "1.20260603.1", "ws": "catalog:default", "youch": "4.1.0-beta.10" }, diff --git a/packages/miniflare/src/cf.ts b/packages/miniflare/src/cf.ts index ab6d60c3c2..9bb0916c71 100644 --- a/packages/miniflare/src/cf.ts +++ b/packages/miniflare/src/cf.ts @@ -89,6 +89,10 @@ export const fallbackCf: IncomingRequestCfProperties = { certFingerprintSHA256: "", certNotBefore: "", certNotAfter: "", + certRFC9440: "", + certRFC9440TooLarge: false, + certChainRFC9440: "", + certChainRFC9440TooLarge: false, }, edgeRequestKeepAliveStatus: 0, hostMetadata: undefined, diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 06610aced4..1165e68066 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.20260601.1" + "workerd": "1.20260603.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c062574355..2a9d32b47a 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.20260601.1 - version: 4.20260601.1 + specifier: ^4.20260603.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.1 undici: specifier: catalog:default version: 7.24.8 @@ -335,7 +335,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1) + version: 10.50.0(@cloudflare/workers-types@4.20260603.1) devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.1 typescript: specifier: catalog:default version: 5.8.3 @@ -781,7 +781,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1037,19 +1037,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 fixtures/rules-app: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 fixtures/secrets-store: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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(4fb4cde8144f65bafda7c34822f693cf) + version: 1.5.4(9dcf2bb01dadf8b1170745dd21275bc0) '@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.20260601.1 + version: 4.20260603.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.20260601.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.20260601.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.20260603.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.20260603.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.20260601.1 + version: 4.20260603.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -1283,7 +1283,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1529,7 +1529,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1686,7 +1686,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1855,7 +1855,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1900,7 +1900,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260603.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 - version: 1.20260601.1 + specifier: 1.20260603.1 + version: 1.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260603.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.20260601.1 + version: 4.20260603.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -2309,7 +2309,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1)(typescript@5.8.3) + version: 2.12.0(@cloudflare/workers-types@4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1) + version: 0.3.3(@cloudflare/workers-types@4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260603.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.20260601.1 + version: 4.20260603.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.20260601.1 - version: 1.20260601.1 + specifier: 1.20260603.1 + version: 1.20260603.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.20260601.1 + version: 4.20260603.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -4387,7 +4387,7 @@ importers: dependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 wrangler: specifier: workspace:* version: link:../wrangler @@ -5325,8 +5325,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260601.1': - resolution: {integrity: sha512-iXZBVuRbvuVqQ/63wul01hHCv/3R8G5S8zbkjfoHvyPZFynmlKTV59Hk+H8whyGwFAZuB71UJGLr+G5mJKfjWA==} + '@cloudflare/workerd-darwin-64@1.20260603.1': + resolution: {integrity: sha512-cEXDWu6V3ZrpmwWkM4OJE9AeXjdAgOY5rh8EHhcBVCuP5rxnzUbPzLtrVOHx0UUUAcCrFq0Xsa6mZKL1VUZsKQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5343,8 +5343,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260601.1': - resolution: {integrity: sha512-veGpZQGBw07Twt+Y4z3oyo+/obKHt0iWUwvDV5GOiDAYjC/zW+YGstgVzg4SHq+k1sLH3ElqL2TXx20I5WBv3Q==} + '@cloudflare/workerd-darwin-arm64@1.20260603.1': + resolution: {integrity: sha512-uBPK4LaWJNbbCYwPnUAehlHbbVulhVZPZsdcAhBPfZhHb3QAuAEPAQepO/P67R3V6Cni4YGx1fLbL8A5wwoaNA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5361,8 +5361,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260601.1': - resolution: {integrity: sha512-n/9hDz7fPGpYF0J684+Xr5zgjcS2jdmY2Of5m6e+eQ/M9+RfR+UaU8Ee/tkA1dDC0LYQB13hfPafZG66Ff1CsA==} + '@cloudflare/workerd-linux-64@1.20260603.1': + resolution: {integrity: sha512-ht9l6/8Tk7Rp6kA4S9oFZ4X8u0VjnnFdmU/6B3fnABYKREYTKh2RdOqXqXxcp5eNJseireKnWik/hQOPK1CutQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5379,8 +5379,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260601.1': - resolution: {integrity: sha512-VHRZZbexATS+n+1j3x/CZaYbIJEye0J3iIHgG0Wp+l+NrZCKQ8qi8Lq1uTV0dLJQ67FuZtJtWdQ95mm9F7Fc+A==} + '@cloudflare/workerd-linux-arm64@1.20260603.1': + resolution: {integrity: sha512-LJZ6x00rAjSrobV4m0ZW0TpH5ilBbKcWBzlH+y+KOUsIE/CpTuhAzKV43TbSnFLRX5+jrWKiz2v0hO91lPXy6A==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5397,8 +5397,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260601.1': - resolution: {integrity: sha512-ye0C7MFLkeH16iTo8Tcjv2KiFmp23+sZGvUzSQa4xhP0QMe6EoJ+H/4SqqvnZ5nfN54slqKvx2VnXceENWe2CQ==} + '@cloudflare/workerd-windows-64@1.20260603.1': + resolution: {integrity: sha512-DvwqkXMAJRPoDN4PxapAwhlz/6ouD+6R1ttbAEK3cWD/QBvFF5STx7Ds/9Irf+rBly3np3uHWkeX+wZnNFEuzA==} 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.20260601.1': - resolution: {integrity: sha512-pYORr1EKlDu55HCHhln8XSXoOSvKAkrTkovJL66bX8xw6DAT2fhs39B6FLjCJD+x++hjBEE2bmKB1TcFKS+0Dw==} + '@cloudflare/workers-types@4.20260603.1': + resolution: {integrity: sha512-TLeVHoBbcYv35S5TdRWUoj3IJ56BhHtrsuci+O7ithU8yz7ttNdCk6rAl1QUSGNVEWSIp54bWOuV/xmX1zu79g==} '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -15086,8 +15086,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260601.1: - resolution: {integrity: sha512-Bg4+HF3B8TW0urAv8chiz25HSQ/aJxMBjgheUzu/nB1NQa+CaKGrUPv+Z3bf0np/WxLHYW1kcseVEtzZVPbX4g==} + workerd@1.20260603.1: + resolution: {integrity: sha512-NPcbhI1++CS+fnELyXtsIR52en+5kwr/OrKeiQeYXGy10HxmPdsQBv9N+DU7hJIOOmBHhOGAAsoGDjyiQ2YCaA==} 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.20260601.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.20260603.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.20260601.1 + '@cloudflare/workers-types': 4.20260603.1 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260601.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.20260603.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.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.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.20260601.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.20260603.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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(4fb4cde8144f65bafda7c34822f693cf)': + '@better-auth/stripe@1.5.4(9dcf2bb01dadf8b1170745dd21275bc0)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260601.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.20260601.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.20260601.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.20260603.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.20260603.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.1) + wrangler: 4.76.0(@cloudflare/workers-types@4.20260603.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.20260601.1': + '@cloudflare/workerd-darwin-64@1.20260603.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.20260601.1': + '@cloudflare/workerd-darwin-arm64@1.20260603.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.20260601.1': + '@cloudflare/workerd-linux-64@1.20260603.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.20260601.1': + '@cloudflare/workerd-linux-arm64@1.20260603.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.20260601.1': + '@cloudflare/workerd-windows-64@1.20260603.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.20260601.1': {} + '@cloudflare/workers-types@4.20260603.1': {} '@codemirror/autocomplete@6.20.0': dependencies: @@ -18204,7 +18204,7 @@ snapshots: '@prisma/adapter-d1@7.0.1': dependencies: - '@cloudflare/workers-types': 4.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.1)(typescript@5.8.3)': + '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260603.1)(typescript@5.8.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.1)': + '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@4.20260603.1)': dependencies: '@opentelemetry/api': 1.9.1 '@sentry/core': 10.50.0 optionalDependencies: - '@cloudflare/workers-types': 4.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.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.20260601.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.20260603.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.20260603.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.20260601.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.20260601.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.20260601.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.20260601.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.20260601.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.20260601.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.20260601.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.20260601.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.20260603.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.20260603.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.20260603.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.20260603.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.20260603.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.20260603.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.20260603.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.20260603.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.20260601.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.20260603.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.20260601.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.20260603.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.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.1): + partyserver@0.3.3(@cloudflare/workers-types@4.20260603.1): dependencies: - '@cloudflare/workers-types': 4.20260601.1 + '@cloudflare/workers-types': 4.20260603.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.20260601.1: + workerd@1.20260603.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260601.1 - '@cloudflare/workerd-darwin-arm64': 1.20260601.1 - '@cloudflare/workerd-linux-64': 1.20260601.1 - '@cloudflare/workerd-linux-arm64': 1.20260601.1 - '@cloudflare/workerd-windows-64': 1.20260601.1 + '@cloudflare/workerd-darwin-64': 1.20260603.1 + '@cloudflare/workerd-darwin-arm64': 1.20260603.1 + '@cloudflare/workerd-linux-64': 1.20260603.1 + '@cloudflare/workerd-linux-arm64': 1.20260603.1 + '@cloudflare/workerd-windows-64': 1.20260603.1 - wrangler@4.76.0(@cloudflare/workers-types@4.20260601.1): + wrangler@4.76.0(@cloudflare/workers-types@4.20260603.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.20260601.1 + '@cloudflare/workers-types': 4.20260603.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9f29eb59bd..20254df064 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.20260601.1" - workerd: "1.20260601.1" + "@cloudflare/workers-types": "^4.20260603.1" + workerd: "1.20260603.1" jsonc-parser: "3.2.0" smol-toml: "1.5.2" msw: 2.12.4 From 14f19aade82c293da4cf8ea72afadfaa55f54b5b Mon Sep 17 00:00:00 2001 From: James Opstad <13586373+jamesopstad@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:22:38 +0100 Subject: [PATCH 3/6] Fix lock file (#14178) --- pnpm-lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a9d32b47a..a8e01a3320 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1620,7 +1620,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2936,7 +2936,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260601.1 + version: 4.20260603.1 typescript: specifier: catalog:default version: 5.8.3 From c8c366e643636526806d2fd7d326825a1f119957 Mon Sep 17 00:00:00 2001 From: ANT Bot <116369605+workers-devprod@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:51:12 +0100 Subject: [PATCH 4/6] Version Packages (#14159) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/d1-execute-logger-level.md | 9 -- .changeset/d1-migrations-pattern.md | 28 ---- .changeset/dependabot-update-14175.md | 12 -- .changeset/deploy-path-positional.md | 12 -- .changeset/extract-workers-auth-oauth-flow.md | 20 --- .../fix-miniflare-hang-on-workerd-exit.md | 9 -- ...getplatformproxy-cross-script-workflows.md | 11 -- .changeset/good-ducks-clean.md | 11 -- .changeset/late-lemons-report.md | 7 - .../rename-web-search-binding-to-websearch.md | 18 --- .../simplify-construct-wrangler-config.md | 7 - .../skip-stale-bundles-during-reload.md | 7 - .changeset/update-secret-bulk-description.md | 7 - .../vite-plugin-experimental-new-config.md | 51 ------- .../warn-custom-domain-route-inheritance.md | 7 - .changeset/workflow-cross-worker-bindings.md | 13 -- .../wrangler-auth-config-file-mode-0600.md | 8 -- ...rangler-login-oauth-callback-error-hang.md | 17 --- packages/cli/CHANGELOG.md | 7 + packages/cli/package.json | 2 +- packages/deploy-helpers/CHANGELOG.md | 7 + packages/deploy-helpers/package.json | 2 +- packages/miniflare/CHANGELOG.md | 43 ++++++ packages/miniflare/package.json | 2 +- packages/pages-shared/CHANGELOG.md | 7 + packages/pages-shared/package.json | 2 +- packages/vite-plugin-cloudflare/CHANGELOG.md | 58 ++++++++ packages/vite-plugin-cloudflare/package.json | 2 +- packages/vitest-pool-workers/CHANGELOG.md | 8 ++ packages/vitest-pool-workers/package.json | 2 +- packages/workers-auth/CHANGELOG.md | 41 ++++++ packages/workers-auth/package.json | 2 +- packages/workers-utils/CHANGELOG.md | 45 ++++++ packages/workers-utils/package.json | 2 +- packages/wrangler-bundler/CHANGELOG.md | 7 + packages/wrangler-bundler/package.json | 2 +- packages/wrangler/CHANGELOG.md | 134 ++++++++++++++++++ packages/wrangler/package.json | 2 +- 38 files changed, 367 insertions(+), 264 deletions(-) delete mode 100644 .changeset/d1-execute-logger-level.md delete mode 100644 .changeset/d1-migrations-pattern.md delete mode 100644 .changeset/dependabot-update-14175.md delete mode 100644 .changeset/deploy-path-positional.md delete mode 100644 .changeset/extract-workers-auth-oauth-flow.md delete mode 100644 .changeset/fix-miniflare-hang-on-workerd-exit.md delete mode 100644 .changeset/getplatformproxy-cross-script-workflows.md delete mode 100644 .changeset/good-ducks-clean.md delete mode 100644 .changeset/late-lemons-report.md delete mode 100644 .changeset/rename-web-search-binding-to-websearch.md delete mode 100644 .changeset/simplify-construct-wrangler-config.md delete mode 100644 .changeset/skip-stale-bundles-during-reload.md delete mode 100644 .changeset/update-secret-bulk-description.md delete mode 100644 .changeset/vite-plugin-experimental-new-config.md delete mode 100644 .changeset/warn-custom-domain-route-inheritance.md delete mode 100644 .changeset/workflow-cross-worker-bindings.md delete mode 100644 .changeset/wrangler-auth-config-file-mode-0600.md delete mode 100644 .changeset/wrangler-login-oauth-callback-error-hang.md create mode 100644 packages/workers-auth/CHANGELOG.md diff --git a/.changeset/d1-execute-logger-level.md b/.changeset/d1-execute-logger-level.md deleted file mode 100644 index 2b9ed5a53c..0000000000 --- a/.changeset/d1-execute-logger-level.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch ---- - -Restore the D1 `executeSql` logger level via try/finally - -`wrangler d1 execute --json` and the internal `executeSql` helper temporarily lower the global logger to `"error"` to keep human-readable output out of the JSON payload. Previously the level was restored only on the happy path, so any early return or thrown error left the singleton logger muted, silencing later `logger.warn`/`logger.log` output (notably from migration helpers that wrap `executeSql` and are commonly mocked in tests). - -The level swap is now wrapped in `try`/`finally` so it is always restored. diff --git a/.changeset/d1-migrations-pattern.md b/.changeset/d1-migrations-pattern.md deleted file mode 100644 index 9095a2187b..0000000000 --- a/.changeset/d1-migrations-pattern.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"wrangler": minor -"@cloudflare/workers-utils": minor ---- - -Add `migrations_pattern` to D1 database bindings - -The D1 binding now accepts an optional `migrations_pattern` field, allowing you to point `wrangler d1 migrations apply` and `wrangler d1 migrations list` at migration files in nested layouts (e.g. ORM-generated folders like `migrations/0000_init/migration.sql`). - -`migrations_pattern` is a glob (relative to the wrangler config file) and defaults to `${migrations_dir}/*.sql`, which preserves today's behaviour. Files that do not match the pattern are not executed. - -```jsonc -{ - "d1_databases": [ - { - "binding": "DB", - "database_name": "my-db", - "database_id": "...", - "migrations_dir": "migrations", - "migrations_pattern": "migrations/*/migration.sql", - }, - ], -} -``` - -When no migrations match the configured pattern but files matching the common `migrations/*/migration.sql` (drizzle-style) layout do exist, Wrangler logs a hint suggesting `migrations_pattern` as an opt-in. - -`wrangler d1 migrations create` now returns an actionable error if the generated migration filename would not match the configured pattern. diff --git a/.changeset/dependabot-update-14175.md b/.changeset/dependabot-update-14175.md deleted file mode 100644 index ef68dd5b99..0000000000 --- a/.changeset/dependabot-update-14175.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"miniflare": patch -"wrangler": patch ---- - -Update dependencies of "miniflare", "wrangler" - -The following dependency versions have been updated: - -| Dependency | From | To | -| ---------- | ------------ | ------------ | -| workerd | 1.20260601.1 | 1.20260603.1 | diff --git a/.changeset/deploy-path-positional.md b/.changeset/deploy-path-positional.md deleted file mode 100644 index 021bd13ead..0000000000 --- a/.changeset/deploy-path-positional.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"wrangler": minor ---- - -Generalize `wrangler deploy` and `wrangler versions upload` positional argument from `[script]` to `[path]` - -Both `wrangler deploy` and `wrangler versions upload` now accept a generic `[path]` positional argument that can point to either a Worker entry-point file or a directory of static assets. The type is auto-detected. For example: - -- **File**: `wrangler deploy ./src/index.ts` deploys a Worker (same as before) -- **Directory**: `wrangler deploy ./public` deploys a static assets site (no interactive confirmation prompt) - -The `--script` named option is now hidden and deprecated for both commands. It continues to work for backwards compatibility but only accepts file paths. Passing a directory to `--script` now produces a clear error message suggesting the positional `path` argument or `--assets` flag instead. diff --git a/.changeset/extract-workers-auth-oauth-flow.md b/.changeset/extract-workers-auth-oauth-flow.md deleted file mode 100644 index c27549656e..0000000000 --- a/.changeset/extract-workers-auth-oauth-flow.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"wrangler": patch -"@cloudflare/workers-auth": patch ---- - -Extract the OAuth 2.0 + PKCE flow into a new `@cloudflare/workers-auth` package. - -The OAuth login / logout / refresh logic, the auth-config TOML file IO, the OAuth token exchange + local callback server, and the Cloudflare Access detection helpers that previously lived in `packages/wrangler/src/user/` have moved to the new internal-only `@cloudflare/workers-auth` package. Wrangler now wires the OAuth flow up via a small glue module that injects its logger, browser opener, interactivity detector, and config cache via a dependency- injection context. - -What stays in wrangler: - -- The yargs `login` / `logout` / `whoami` / `auth token` commands -- Environment-based credential resolution (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_API_KEY` / `CLOUDFLARE_EMAIL`, etc.) -- Cloudflare account selection (`requireAuth`, `getOrSelectAccountId`) -- The OAuth scope catalog (passed into the OAuth flow as a generic `string[]`) -- `whoami` / account fetching - -No behavior change for end users. The on-disk TOML format and location remain identical, and all telemetry message labels are preserved verbatim. - -`@cloudflare/workers-auth` is published with `prerelease: true` and is not intended for external use — its APIs may change without notice. diff --git a/.changeset/fix-miniflare-hang-on-workerd-exit.md b/.changeset/fix-miniflare-hang-on-workerd-exit.md deleted file mode 100644 index 91f1b7e82c..0000000000 --- a/.changeset/fix-miniflare-hang-on-workerd-exit.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"miniflare": patch ---- - -Detect early workerd exit instead of hanging indefinitely - -When `workerd` exits during startup before writing all expected listen events to the control file descriptor (e.g. due to an IPv6 bind failure, permission error, or missing library), Miniflare's `waitForPorts()` would block forever. This caused `wrangler dev` to stall at "Starting local server..." with no error and no timeout. - -The fix races `waitForPorts()` against the child process exit event so that any unexpected `workerd` termination is detected immediately. When `workerd` exits early, Miniflare now throws `ERR_RUNTIME_FAILURE` with the runtime's stderr output included in the error message, making the root cause diagnosable without external tools. diff --git a/.changeset/getplatformproxy-cross-script-workflows.md b/.changeset/getplatformproxy-cross-script-workflows.md deleted file mode 100644 index ba314e21f4..0000000000 --- a/.changeset/getplatformproxy-cross-script-workflows.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"wrangler": minor ---- - -`getPlatformProxy()` now passes through workflow bindings that have a `script_name` - -Workflows without a `script_name` are still stripped (and warned about) because the engine for an internal workflow can't run inside the empty proxy worker that backs `getPlatformProxy()`. Workflows with a `script_name` are handed to miniflare unchanged; miniflare reroutes the engine's `USER_WORKFLOW` binding through the dev-registry-proxy when the target worker is running in another Miniflare instance — the same mechanism Durable Objects already use. - -This means SvelteKit/Remix (and similar split-process setups) can call `platform.env.MY_WORKFLOW.create({ ... })` directly from their server-side request handlers in dev, as long as the workflow class is exposed by another worker registered in the dev registry. - -Closes [#7459](https://github.com/cloudflare/workers-sdk/issues/7459). diff --git a/.changeset/good-ducks-clean.md b/.changeset/good-ducks-clean.md deleted file mode 100644 index 3a27ee4520..0000000000 --- a/.changeset/good-ducks-clean.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"wrangler": patch ---- - -In non-interactive mode remove the skills installation message - -When Wrangler run in non interactive mode and it detected agents that it could install skills for, it would print a message such as: - -`Cloudflare agent skills are available for: . Run wrangler in an interactive terminal to install them, or use '--install-skills' to install without prompting.` - -This message seems to be confusing and unhelpful so it has now been removed. diff --git a/.changeset/late-lemons-report.md b/.changeset/late-lemons-report.md deleted file mode 100644 index 47f53d6415..0000000000 --- a/.changeset/late-lemons-report.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Limit `wrangler versions list` to the 10 most recent deployable versions - -The versions API ignores pagination when filtering to deployable versions, so Wrangler now caps the command output client-side. This keeps the command aligned with its help text and avoids overwhelming terminal output for Workers with many versions. diff --git a/.changeset/rename-web-search-binding-to-websearch.md b/.changeset/rename-web-search-binding-to-websearch.md deleted file mode 100644 index 1f148bc459..0000000000 --- a/.changeset/rename-web-search-binding-to-websearch.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@cloudflare/workers-utils": minor -"miniflare": minor -"wrangler": minor ---- - -Rename the `web_search` binding kind to `websearch` - -Pre-launch rename of the public binding type from `web_search` to `websearch` so the on-the-wire shape matches the product name (Web Search). The wrangler config key, the binding-type string sent to the Cloudflare API, and the miniflare option key all move from `web_search` / `webSearch` to `websearch`. - -Update your wrangler config: - -```diff -- "web_search": { "binding": "WEBSEARCH" } -+ "websearch": { "binding": "WEBSEARCH" } -``` - -The runtime `WebSearch` type exposed on `env.WEBSEARCH` is unchanged. diff --git a/.changeset/simplify-construct-wrangler-config.md b/.changeset/simplify-construct-wrangler-config.md deleted file mode 100644 index 69a85f2709..0000000000 --- a/.changeset/simplify-construct-wrangler-config.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/workers-utils": minor ---- - -Simplify `constructWranglerConfig` to accept a single worker instead of an array - -The `constructWranglerConfig` function now accepts a single `APIWorkerConfig` object instead of `APIWorkerConfig | APIWorkerConfig[]`. The multi-environment array support has been removed since the array use-case was removed and now the only call site already passes a single worker object. This is a breaking change to the function's public signature. diff --git a/.changeset/skip-stale-bundles-during-reload.md b/.changeset/skip-stale-bundles-during-reload.md deleted file mode 100644 index ce95b831a1..0000000000 --- a/.changeset/skip-stale-bundles-during-reload.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Skip stale bundles during dev server reload to avoid redundant restarts - -When rapidly saving a wrangler config file with remote bindings, each save would trigger a full reload cycle (remote connection setup, miniflare restart), causing many sequential "Reloading local server... / Establishing remote connection..." messages (while blocking the user). The runtime controllers now check whether a newer bundle has been queued at each expensive async boundary and bail out early if the current bundle is stale. This ensures that only the latest config change triggers a reload, making `wrangler dev` much more responsive during repeated config edits. diff --git a/.changeset/update-secret-bulk-description.md b/.changeset/update-secret-bulk-description.md deleted file mode 100644 index b756a154e8..0000000000 --- a/.changeset/update-secret-bulk-description.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Update `wrangler secret bulk` command description to reflect create/update/delete capabilities - -The help text for `wrangler secret bulk` now accurately describes that the command can create, update, or delete multiple secrets in a single request, with up to 100 secrets per command. The file argument description also clarifies that setting a key to `null` in JSON will delete it, and that deletion is not supported with `.env` files. diff --git a/.changeset/vite-plugin-experimental-new-config.md b/.changeset/vite-plugin-experimental-new-config.md deleted file mode 100644 index 7292b60975..0000000000 --- a/.changeset/vite-plugin-experimental-new-config.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -"@cloudflare/vite-plugin": minor ---- - -Add experimental `experimental.newConfig` option to load the entry Worker's configuration from `cloudflare.config.ts` - -This is an experimental, opt-in feature. When enabled, the plugin loads the entry Worker's configuration from a `cloudflare.config.ts` file instead of the usual `wrangler.json` / `wrangler.jsonc` / `wrangler.toml`. - -Pass `true` to enable with defaults, or an object to customise behaviour. Currently the only sub-option is `types.generate` (defaults to `true`), which writes a `worker-configuration.d.ts` file next to the config. This enables typed `env` and `exports` for your Worker and currently assumes that you have `@cloudflare/workers-types` installed. - -```ts -// vite.config.ts -import { defineConfig } from "vite"; -import { cloudflare } from "@cloudflare/vite-plugin"; - -export default defineConfig({ - plugins: [ - cloudflare({ - experimental: { - newConfig: true, - }, - }), - ], -}); -``` - -```ts -// cloudflare.config.ts -import { - defineWorker, - bindings, -} from "@cloudflare/vite-plugin/experimental-config"; -import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; - -export default defineWorker((ctx) => ({ - name: "my-worker", - entrypoint, - compatibilityDate: "2026-05-18", - env: { - MY_TEXT: bindings.text(`The mode is ${ctx.mode}`), - MY_KV: bindings.kv(), - }, -})); -``` - -A few limitations apply while the feature is experimental: - -- `configPath` cannot be combined with `experimental.newConfig`. The entry Worker is always loaded from `cloudflare.config.ts` at the project root. -- `auxiliaryWorkers` are not yet supported with `experimental.newConfig`. - -Because this is experimental, the option, the `cloudflare.config.ts` schema, and the `@cloudflare/vite-plugin/experimental-config` exports may change in any release. diff --git a/.changeset/warn-custom-domain-route-inheritance.md b/.changeset/warn-custom-domain-route-inheritance.md deleted file mode 100644 index 154e6ae06f..0000000000 --- a/.changeset/warn-custom-domain-route-inheritance.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Warn when a named environment silently inherits custom_domain routes from the top-level config - -When an `env.` block does not override `routes`, it inherits the top-level `routes` array. If that array contains entries with `custom_domain: true`, every deploy to the named environment will silently reassign the custom domain away from the top-level Worker and towards the env Worker, causing routing drift. Wrangler now emits a warning in this situation and suggests adding `"routes": []` to the env block to prevent inheritance. diff --git a/.changeset/workflow-cross-worker-bindings.md b/.changeset/workflow-cross-worker-bindings.md deleted file mode 100644 index d73760aadf..0000000000 --- a/.changeset/workflow-cross-worker-bindings.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"miniflare": minor ---- - -Support cross-worker workflow bindings via the dev registry - -When a workflow binding has a `scriptName` that refers to a worker registered in another Miniflare instance (via `unsafeDevRegistryPath`), miniflare now reroutes the engine's `USER_WORKFLOW` binding through the dev-registry-proxy worker — the same mechanism Durable Objects already use for cross-worker `scriptName` bindings. - -Previously the workflow engine was bound directly to a local service `core:user:`, so workerd refused to start when that script lived in a different process. - -This unblocks `getPlatformProxy()` (and any other split-Miniflare setup) for users whose workflow class is defined in a separate worker — for example SvelteKit/Remix on Cloudflare, where `adapter-cloudflare`'s dev integration runs the user's worker in a sidecar. - -See [#7459](https://github.com/cloudflare/workers-sdk/issues/7459). diff --git a/.changeset/wrangler-auth-config-file-mode-0600.md b/.changeset/wrangler-auth-config-file-mode-0600.md deleted file mode 100644 index e49fa028c5..0000000000 --- a/.changeset/wrangler-auth-config-file-mode-0600.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"wrangler": patch -"@cloudflare/workers-auth": patch ---- - -Tighten on-disk permissions of the OAuth credentials file to `0600` - -The user auth config file written by `wrangler login` (typically `~/.config/.wrangler/config/default.toml` on Linux/macOS, or `.toml` for non-production Cloudflare API environments) is now written with mode `0600` and re-`chmod`-ed on every save. This prevents other local users on shared hosts from reading the stored OAuth tokens. Existing files with looser permissions written by older Wrangler versions are tightened the next time Wrangler refreshes the token or the user logs in again. The change is a no-op on Windows, which does not honour POSIX mode bits. diff --git a/.changeset/wrangler-login-oauth-callback-error-hang.md b/.changeset/wrangler-login-oauth-callback-error-hang.md deleted file mode 100644 index 0ff9686424..0000000000 --- a/.changeset/wrangler-login-oauth-callback-error-hang.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"wrangler": patch -"@cloudflare/workers-auth": patch ---- - -Show the actual OAuth error instead of hanging when `wrangler login` is rejected by the OAuth provider (for example with `invalid_scope`). - -Previously, if the OAuth callback returned with an `error` other than `access_denied`, Wrangler would never respond to the browser. Because `server.close()`'s callback only fires once all open connections have ended, the login command would hang until the 120 second OAuth timeout — at which point it would print a generic timeout message rather than the actual OAuth failure. The same gap existed for the case where the OAuth provider redirected back without an authorisation code, and for failures during the auth-code-to-access-token exchange. - -The OAuth provider's `error_description` (RFC 6749 §4.1.2.1) is now also surfaced, so the message includes the specific reason for the failure rather than just the bare `error` code. For example, a misconfigured staging scope now surfaces as: - -``` -OAuth error: invalid_scope - The OAuth 2.0 Client is not allowed to request scope 'browser:write'. -``` - -instead of hanging silently. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 725304f850..0cd73a7153 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/cli +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c)]: + - @cloudflare/workers-utils@0.23.0 + ## 0.1.6 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index f31338a76f..b72e3db414 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/cli-shared-helpers", - "version": "0.1.6", + "version": "0.1.7", "description": "Internal shared CLI helpers for workers-sdk. Not intended for external use — APIs may change without notice.", "keywords": [ "cli", diff --git a/packages/deploy-helpers/CHANGELOG.md b/packages/deploy-helpers/CHANGELOG.md index d34b21d622..aa6e8ab05f 100644 --- a/packages/deploy-helpers/CHANGELOG.md +++ b/packages/deploy-helpers/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/deploy-helpers +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c)]: + - @cloudflare/workers-utils@0.23.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/deploy-helpers/package.json b/packages/deploy-helpers/package.json index 5e44ec9bce..2d5e891c7c 100644 --- a/packages/deploy-helpers/package.json +++ b/packages/deploy-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/deploy-helpers", - "version": "0.1.1", + "version": "0.1.2", "description": "Internal deploy helpers for workers-sdk. Not intended for external use — APIs may change without notice.", "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/deploy-helpers#readme", "bugs": { diff --git a/packages/miniflare/CHANGELOG.md b/packages/miniflare/CHANGELOG.md index 8a9811146c..9dd7c05d8d 100644 --- a/packages/miniflare/CHANGELOG.md +++ b/packages/miniflare/CHANGELOG.md @@ -1,5 +1,48 @@ # miniflare +## 4.20260603.0 + +### Minor Changes + +- [#14164](https://github.com/cloudflare/workers-sdk/pull/14164) [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64) Thanks [@G4brym](https://github.com/G4brym)! - Rename the `web_search` binding kind to `websearch` + + Pre-launch rename of the public binding type from `web_search` to `websearch` so the on-the-wire shape matches the product name (Web Search). The wrangler config key, the binding-type string sent to the Cloudflare API, and the miniflare option key all move from `web_search` / `webSearch` to `websearch`. + + Update your wrangler config: + + ```diff + - "web_search": { "binding": "WEBSEARCH" } + + "websearch": { "binding": "WEBSEARCH" } + ``` + + The runtime `WebSearch` type exposed on `env.WEBSEARCH` is unchanged. + +- [#13863](https://github.com/cloudflare/workers-sdk/pull/13863) [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495) Thanks [@aslakhellesoy](https://github.com/aslakhellesoy)! - Support cross-worker workflow bindings via the dev registry + + When a workflow binding has a `scriptName` that refers to a worker registered in another Miniflare instance (via `unsafeDevRegistryPath`), miniflare now reroutes the engine's `USER_WORKFLOW` binding through the dev-registry-proxy worker — the same mechanism Durable Objects already use for cross-worker `scriptName` bindings. + + Previously the workflow engine was bound directly to a local service `core:user:`, so workerd refused to start when that script lived in a different process. + + This unblocks `getPlatformProxy()` (and any other split-Miniflare setup) for users whose workflow class is defined in a separate worker — for example SvelteKit/Remix on Cloudflare, where `adapter-cloudflare`'s dev integration runs the user's worker in a sidecar. + + See [#7459](https://github.com/cloudflare/workers-sdk/issues/7459). + +### Patch Changes + +- [#14175](https://github.com/cloudflare/workers-sdk/pull/14175) [`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260601.1 | 1.20260603.1 | + +- [#14081](https://github.com/cloudflare/workers-sdk/pull/14081) [`1fdd8de`](https://github.com/cloudflare/workers-sdk/commit/1fdd8def456011c29c5879fe49be6fa90ad9858d) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Detect early workerd exit instead of hanging indefinitely + + When `workerd` exits during startup before writing all expected listen events to the control file descriptor (e.g. due to an IPv6 bind failure, permission error, or missing library), Miniflare's `waitForPorts()` would block forever. This caused `wrangler dev` to stall at "Starting local server..." with no error and no timeout. + + The fix races `waitForPorts()` against the child process exit event so that any unexpected `workerd` termination is detected immediately. When `workerd` exits early, Miniflare now throws `ERR_RUNTIME_FAILURE` with the runtime's stderr output included in the error message, making the root cause diagnosable without external tools. + ## 4.20260601.0 ### Patch Changes diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index ad996fbf43..d292b51756 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -1,6 +1,6 @@ { "name": "miniflare", - "version": "4.20260601.0", + "version": "4.20260603.0", "description": "Fun, full-featured, fully-local simulator for Cloudflare Workers", "keywords": [ "cloudflare", diff --git a/packages/pages-shared/CHANGELOG.md b/packages/pages-shared/CHANGELOG.md index b053867083..6229b120e7 100644 --- a/packages/pages-shared/CHANGELOG.md +++ b/packages/pages-shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/pages-shared +## 0.13.143 + +### Patch Changes + +- Updated dependencies [[`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e), [`1fdd8de`](https://github.com/cloudflare/workers-sdk/commit/1fdd8def456011c29c5879fe49be6fa90ad9858d), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495)]: + - miniflare@4.20260603.0 + ## 0.13.142 ### Patch Changes diff --git a/packages/pages-shared/package.json b/packages/pages-shared/package.json index dd24edf6e3..53a858f13d 100644 --- a/packages/pages-shared/package.json +++ b/packages/pages-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/pages-shared", - "version": "0.13.142", + "version": "0.13.143", "repository": { "type": "git", "url": "https://github.com/cloudflare/workers-sdk.git", diff --git a/packages/vite-plugin-cloudflare/CHANGELOG.md b/packages/vite-plugin-cloudflare/CHANGELOG.md index 04f434280d..13758418a3 100644 --- a/packages/vite-plugin-cloudflare/CHANGELOG.md +++ b/packages/vite-plugin-cloudflare/CHANGELOG.md @@ -1,5 +1,63 @@ # @cloudflare/vite-plugin +## 1.40.0 + +### Minor Changes + +- [#14013](https://github.com/cloudflare/workers-sdk/pull/14013) [`3cf9d0e`](https://github.com/cloudflare/workers-sdk/commit/3cf9d0e9daa043265f2d5cd5add1b448f6378474) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Add experimental `experimental.newConfig` option to load the entry Worker's configuration from `cloudflare.config.ts` + + This is an experimental, opt-in feature. When enabled, the plugin loads the entry Worker's configuration from a `cloudflare.config.ts` file instead of the usual `wrangler.json` / `wrangler.jsonc` / `wrangler.toml`. + + Pass `true` to enable with defaults, or an object to customise behaviour. Currently the only sub-option is `types.generate` (defaults to `true`), which writes a `worker-configuration.d.ts` file next to the config. This enables typed `env` and `exports` for your Worker and currently assumes that you have `@cloudflare/workers-types` installed. + + ```ts + // vite.config.ts + import { defineConfig } from "vite"; + import { cloudflare } from "@cloudflare/vite-plugin"; + + export default defineConfig({ + plugins: [ + cloudflare({ + experimental: { + newConfig: true, + }, + }), + ], + }); + ``` + + ```ts + // cloudflare.config.ts + import { + defineWorker, + bindings, + } from "@cloudflare/vite-plugin/experimental-config"; + import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + + export default defineWorker((ctx) => ({ + name: "my-worker", + entrypoint, + compatibilityDate: "2026-05-18", + env: { + MY_TEXT: bindings.text(`The mode is ${ctx.mode}`), + MY_KV: bindings.kv(), + }, + })); + ``` + + A few limitations apply while the feature is experimental: + + - `configPath` cannot be combined with `experimental.newConfig`. The entry Worker is always loaded from `cloudflare.config.ts` at the project root. + - `auxiliaryWorkers` are not yet supported with `experimental.newConfig`. + + Because this is experimental, the option, the `cloudflare.config.ts` schema, and the `@cloudflare/vite-plugin/experimental-config` exports may change in any release. + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e), [`7a6b1a4`](https://github.com/cloudflare/workers-sdk/commit/7a6b1a4f4e9d8d5bd88732c8e11368c3ad7f867b), [`7539a9b`](https://github.com/cloudflare/workers-sdk/commit/7539a9bfcf03a14b2c16f281d541b6bc45523a80), [`1fdd8de`](https://github.com/cloudflare/workers-sdk/commit/1fdd8def456011c29c5879fe49be6fa90ad9858d), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495), [`0bb2d55`](https://github.com/cloudflare/workers-sdk/commit/0bb2d55116ce90a147582a7b4d96e3090cddf7ee), [`8400fb9`](https://github.com/cloudflare/workers-sdk/commit/8400fb945a781e7a7a78a3614a702ace2d1fbc87), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`7949f81`](https://github.com/cloudflare/workers-sdk/commit/7949f81bd258292a4a0b9c5a339c6c035f27d7ca), [`d462013`](https://github.com/cloudflare/workers-sdk/commit/d46201384f656815bf9e90a595098edff43f1b32), [`c2280cd`](https://github.com/cloudflare/workers-sdk/commit/c2280cdb589c9289bb4082d0a068846f3dd22b37), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495), [`ea12b58`](https://github.com/cloudflare/workers-sdk/commit/ea12b584ee1c3141286f0ecf6b742bd79971407e), [`acf7817`](https://github.com/cloudflare/workers-sdk/commit/acf7817266b39be9707a09b918d670a468302ebc)]: + - wrangler@4.98.0 + - miniflare@4.20260603.0 + ## 1.39.2 ### Patch Changes diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index eefbea6877..feafc7b4e8 100644 --- a/packages/vite-plugin-cloudflare/package.json +++ b/packages/vite-plugin-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/vite-plugin", - "version": "1.39.2", + "version": "1.40.0", "description": "Cloudflare plugin for Vite", "keywords": [ "cloudflare", diff --git a/packages/vitest-pool-workers/CHANGELOG.md b/packages/vitest-pool-workers/CHANGELOG.md index f5e982f506..e188a6ac59 100644 --- a/packages/vitest-pool-workers/CHANGELOG.md +++ b/packages/vitest-pool-workers/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/vitest-pool-workers +## 0.16.13 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e), [`7a6b1a4`](https://github.com/cloudflare/workers-sdk/commit/7a6b1a4f4e9d8d5bd88732c8e11368c3ad7f867b), [`7539a9b`](https://github.com/cloudflare/workers-sdk/commit/7539a9bfcf03a14b2c16f281d541b6bc45523a80), [`1fdd8de`](https://github.com/cloudflare/workers-sdk/commit/1fdd8def456011c29c5879fe49be6fa90ad9858d), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495), [`0bb2d55`](https://github.com/cloudflare/workers-sdk/commit/0bb2d55116ce90a147582a7b4d96e3090cddf7ee), [`8400fb9`](https://github.com/cloudflare/workers-sdk/commit/8400fb945a781e7a7a78a3614a702ace2d1fbc87), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`7949f81`](https://github.com/cloudflare/workers-sdk/commit/7949f81bd258292a4a0b9c5a339c6c035f27d7ca), [`d462013`](https://github.com/cloudflare/workers-sdk/commit/d46201384f656815bf9e90a595098edff43f1b32), [`c2280cd`](https://github.com/cloudflare/workers-sdk/commit/c2280cdb589c9289bb4082d0a068846f3dd22b37), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495), [`ea12b58`](https://github.com/cloudflare/workers-sdk/commit/ea12b584ee1c3141286f0ecf6b742bd79971407e), [`acf7817`](https://github.com/cloudflare/workers-sdk/commit/acf7817266b39be9707a09b918d670a468302ebc)]: + - wrangler@4.98.0 + - miniflare@4.20260603.0 + ## 0.16.12 ### Patch Changes diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index dec10d6980..3452ad7d3d 100644 --- a/packages/vitest-pool-workers/package.json +++ b/packages/vitest-pool-workers/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/vitest-pool-workers", - "version": "0.16.12", + "version": "0.16.13", "description": "Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime", "keywords": [ "cloudflare", diff --git a/packages/workers-auth/CHANGELOG.md b/packages/workers-auth/CHANGELOG.md new file mode 100644 index 0000000000..8620a99e88 --- /dev/null +++ b/packages/workers-auth/CHANGELOG.md @@ -0,0 +1,41 @@ +# @cloudflare/workers-auth + +## 0.1.1 + +### Patch Changes + +- [#14121](https://github.com/cloudflare/workers-sdk/pull/14121) [`7539a9b`](https://github.com/cloudflare/workers-sdk/commit/7539a9bfcf03a14b2c16f281d541b6bc45523a80) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Extract the OAuth 2.0 + PKCE flow into a new `@cloudflare/workers-auth` package. + + The OAuth login / logout / refresh logic, the auth-config TOML file IO, the OAuth token exchange + local callback server, and the Cloudflare Access detection helpers that previously lived in `packages/wrangler/src/user/` have moved to the new internal-only `@cloudflare/workers-auth` package. Wrangler now wires the OAuth flow up via a small glue module that injects its logger, browser opener, interactivity detector, and config cache via a dependency- injection context. + + What stays in wrangler: + + - The yargs `login` / `logout` / `whoami` / `auth token` commands + - Environment-based credential resolution (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_API_KEY` / `CLOUDFLARE_EMAIL`, etc.) + - Cloudflare account selection (`requireAuth`, `getOrSelectAccountId`) + - The OAuth scope catalog (passed into the OAuth flow as a generic `string[]`) + - `whoami` / account fetching + + No behavior change for end users. The on-disk TOML format and location remain identical, and all telemetry message labels are preserved verbatim. + + `@cloudflare/workers-auth` is published with `prerelease: true` and is not intended for external use — its APIs may change without notice. + +- [#14170](https://github.com/cloudflare/workers-sdk/pull/14170) [`ea12b58`](https://github.com/cloudflare/workers-sdk/commit/ea12b584ee1c3141286f0ecf6b742bd79971407e) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Tighten on-disk permissions of the OAuth credentials file to `0600` + + The user auth config file written by `wrangler login` (typically `~/.config/.wrangler/config/default.toml` on Linux/macOS, or `.toml` for non-production Cloudflare API environments) is now written with mode `0600` and re-`chmod`-ed on every save. This prevents other local users on shared hosts from reading the stored OAuth tokens. Existing files with looser permissions written by older Wrangler versions are tightened the next time Wrangler refreshes the token or the user logs in again. The change is a no-op on Windows, which does not honour POSIX mode bits. + +- [#14022](https://github.com/cloudflare/workers-sdk/pull/14022) [`acf7817`](https://github.com/cloudflare/workers-sdk/commit/acf7817266b39be9707a09b918d670a468302ebc) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Show the actual OAuth error instead of hanging when `wrangler login` is rejected by the OAuth provider (for example with `invalid_scope`). + + Previously, if the OAuth callback returned with an `error` other than `access_denied`, Wrangler would never respond to the browser. Because `server.close()`'s callback only fires once all open connections have ended, the login command would hang until the 120 second OAuth timeout — at which point it would print a generic timeout message rather than the actual OAuth failure. The same gap existed for the case where the OAuth provider redirected back without an authorisation code, and for failures during the auth-code-to-access-token exchange. + + The OAuth provider's `error_description` (RFC 6749 §4.1.2.1) is now also surfaced, so the message includes the specific reason for the failure rather than just the bare `error` code. For example, a misconfigured staging scope now surfaces as: + + ``` + OAuth error: invalid_scope + The OAuth 2.0 Client is not allowed to request scope 'browser:write'. + ``` + + instead of hanging silently. + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c)]: + - @cloudflare/workers-utils@0.23.0 diff --git a/packages/workers-auth/package.json b/packages/workers-auth/package.json index 8b6b4f33c0..c92407768e 100644 --- a/packages/workers-auth/package.json +++ b/packages/workers-auth/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-auth", - "version": "0.1.0", + "version": "0.1.1", "description": "Internal OAuth 2.0 + PKCE flow for Cloudflare CLIs. Not intended for external use — APIs may change without notice.", "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/workers-auth#readme", "bugs": { diff --git a/packages/workers-utils/CHANGELOG.md b/packages/workers-utils/CHANGELOG.md index 086a5d50cf..81d0df9e0b 100644 --- a/packages/workers-utils/CHANGELOG.md +++ b/packages/workers-utils/CHANGELOG.md @@ -1,5 +1,50 @@ # @cloudflare/workers-utils +## 0.23.0 + +### Minor Changes + +- [#14089](https://github.com/cloudflare/workers-sdk/pull/14089) [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b) Thanks [@alsuren](https://github.com/alsuren)! - Add `migrations_pattern` to D1 database bindings + + The D1 binding now accepts an optional `migrations_pattern` field, allowing you to point `wrangler d1 migrations apply` and `wrangler d1 migrations list` at migration files in nested layouts (e.g. ORM-generated folders like `migrations/0000_init/migration.sql`). + + `migrations_pattern` is a glob (relative to the wrangler config file) and defaults to `${migrations_dir}/*.sql`, which preserves today's behaviour. Files that do not match the pattern are not executed. + + ```jsonc + { + "d1_databases": [ + { + "binding": "DB", + "database_name": "my-db", + "database_id": "...", + "migrations_dir": "migrations", + "migrations_pattern": "migrations/*/migration.sql" + } + ] + } + ``` + + When no migrations match the configured pattern but files matching the common `migrations/*/migration.sql` (drizzle-style) layout do exist, Wrangler logs a hint suggesting `migrations_pattern` as an opt-in. + + `wrangler d1 migrations create` now returns an actionable error if the generated migration filename would not match the configured pattern. + +- [#14164](https://github.com/cloudflare/workers-sdk/pull/14164) [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64) Thanks [@G4brym](https://github.com/G4brym)! - Rename the `web_search` binding kind to `websearch` + + Pre-launch rename of the public binding type from `web_search` to `websearch` so the on-the-wire shape matches the product name (Web Search). The wrangler config key, the binding-type string sent to the Cloudflare API, and the miniflare option key all move from `web_search` / `webSearch` to `websearch`. + + Update your wrangler config: + + ```diff + - "web_search": { "binding": "WEBSEARCH" } + + "websearch": { "binding": "WEBSEARCH" } + ``` + + The runtime `WebSearch` type exposed on `env.WEBSEARCH` is unchanged. + +- [#14146](https://github.com/cloudflare/workers-sdk/pull/14146) [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Simplify `constructWranglerConfig` to accept a single worker instead of an array + + The `constructWranglerConfig` function now accepts a single `APIWorkerConfig` object instead of `APIWorkerConfig | APIWorkerConfig[]`. The multi-environment array support has been removed since the array use-case was removed and now the only call site already passes a single worker object. This is a breaking change to the function's public signature. + ## 0.22.1 ### Patch Changes diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index 6ad7ec8aee..53f786017b 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-utils", - "version": "0.22.1", + "version": "0.23.0", "description": "Internal utility package for workers-sdk. Not intended for external use — APIs may change without notice.", "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/workers-utils#readme", "bugs": { diff --git a/packages/wrangler-bundler/CHANGELOG.md b/packages/wrangler-bundler/CHANGELOG.md index e27cc4147d..b880bf9ce1 100644 --- a/packages/wrangler-bundler/CHANGELOG.md +++ b/packages/wrangler-bundler/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/wrangler-bundler +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e), [`7a6b1a4`](https://github.com/cloudflare/workers-sdk/commit/7a6b1a4f4e9d8d5bd88732c8e11368c3ad7f867b), [`7539a9b`](https://github.com/cloudflare/workers-sdk/commit/7539a9bfcf03a14b2c16f281d541b6bc45523a80), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495), [`0bb2d55`](https://github.com/cloudflare/workers-sdk/commit/0bb2d55116ce90a147582a7b4d96e3090cddf7ee), [`8400fb9`](https://github.com/cloudflare/workers-sdk/commit/8400fb945a781e7a7a78a3614a702ace2d1fbc87), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`7949f81`](https://github.com/cloudflare/workers-sdk/commit/7949f81bd258292a4a0b9c5a339c6c035f27d7ca), [`d462013`](https://github.com/cloudflare/workers-sdk/commit/d46201384f656815bf9e90a595098edff43f1b32), [`c2280cd`](https://github.com/cloudflare/workers-sdk/commit/c2280cdb589c9289bb4082d0a068846f3dd22b37), [`ea12b58`](https://github.com/cloudflare/workers-sdk/commit/ea12b584ee1c3141286f0ecf6b742bd79971407e), [`acf7817`](https://github.com/cloudflare/workers-sdk/commit/acf7817266b39be9707a09b918d670a468302ebc)]: + - wrangler@4.98.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/wrangler-bundler/package.json b/packages/wrangler-bundler/package.json index de6a3e0f5d..7b2ad7c2d5 100644 --- a/packages/wrangler-bundler/package.json +++ b/packages/wrangler-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/wrangler-bundler", - "version": "0.1.1", + "version": "0.1.2", "description": "esbuild-based dev server for Cloudflare Workers, extracted from `wrangler dev`", "license": "MIT OR Apache-2.0", "repository": { diff --git a/packages/wrangler/CHANGELOG.md b/packages/wrangler/CHANGELOG.md index c21ee9721c..f908a33368 100644 --- a/packages/wrangler/CHANGELOG.md +++ b/packages/wrangler/CHANGELOG.md @@ -1,5 +1,139 @@ # wrangler +## 4.98.0 + +### Minor Changes + +- [#14089](https://github.com/cloudflare/workers-sdk/pull/14089) [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b) Thanks [@alsuren](https://github.com/alsuren)! - Add `migrations_pattern` to D1 database bindings + + The D1 binding now accepts an optional `migrations_pattern` field, allowing you to point `wrangler d1 migrations apply` and `wrangler d1 migrations list` at migration files in nested layouts (e.g. ORM-generated folders like `migrations/0000_init/migration.sql`). + + `migrations_pattern` is a glob (relative to the wrangler config file) and defaults to `${migrations_dir}/*.sql`, which preserves today's behaviour. Files that do not match the pattern are not executed. + + ```jsonc + { + "d1_databases": [ + { + "binding": "DB", + "database_name": "my-db", + "database_id": "...", + "migrations_dir": "migrations", + "migrations_pattern": "migrations/*/migration.sql" + } + ] + } + ``` + + When no migrations match the configured pattern but files matching the common `migrations/*/migration.sql` (drizzle-style) layout do exist, Wrangler logs a hint suggesting `migrations_pattern` as an opt-in. + + `wrangler d1 migrations create` now returns an actionable error if the generated migration filename would not match the configured pattern. + +- [#14153](https://github.com/cloudflare/workers-sdk/pull/14153) [`7a6b1a4`](https://github.com/cloudflare/workers-sdk/commit/7a6b1a4f4e9d8d5bd88732c8e11368c3ad7f867b) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Generalize `wrangler deploy` and `wrangler versions upload` positional argument from `[script]` to `[path]` + + Both `wrangler deploy` and `wrangler versions upload` now accept a generic `[path]` positional argument that can point to either a Worker entry-point file or a directory of static assets. The type is auto-detected. For example: + + - **File**: `wrangler deploy ./src/index.ts` deploys a Worker (same as before) + - **Directory**: `wrangler deploy ./public` deploys a static assets site (no interactive confirmation prompt) + + The `--script` named option is now hidden and deprecated for both commands. It continues to work for backwards compatibility but only accepts file paths. Passing a directory to `--script` now produces a clear error message suggesting the positional `path` argument or `--assets` flag instead. + +- [#13863](https://github.com/cloudflare/workers-sdk/pull/13863) [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495) Thanks [@aslakhellesoy](https://github.com/aslakhellesoy)! - `getPlatformProxy()` now passes through workflow bindings that have a `script_name` + + Workflows without a `script_name` are still stripped (and warned about) because the engine for an internal workflow can't run inside the empty proxy worker that backs `getPlatformProxy()`. Workflows with a `script_name` are handed to miniflare unchanged; miniflare reroutes the engine's `USER_WORKFLOW` binding through the dev-registry-proxy when the target worker is running in another Miniflare instance — the same mechanism Durable Objects already use. + + This means SvelteKit/Remix (and similar split-process setups) can call `platform.env.MY_WORKFLOW.create({ ... })` directly from their server-side request handlers in dev, as long as the workflow class is exposed by another worker registered in the dev registry. + + Closes [#7459](https://github.com/cloudflare/workers-sdk/issues/7459). + +- [#14164](https://github.com/cloudflare/workers-sdk/pull/14164) [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64) Thanks [@G4brym](https://github.com/G4brym)! - Rename the `web_search` binding kind to `websearch` + + Pre-launch rename of the public binding type from `web_search` to `websearch` so the on-the-wire shape matches the product name (Web Search). The wrangler config key, the binding-type string sent to the Cloudflare API, and the miniflare option key all move from `web_search` / `webSearch` to `websearch`. + + Update your wrangler config: + + ```diff + - "web_search": { "binding": "WEBSEARCH" } + + "websearch": { "binding": "WEBSEARCH" } + ``` + + The runtime `WebSearch` type exposed on `env.WEBSEARCH` is unchanged. + +### Patch Changes + +- [#14089](https://github.com/cloudflare/workers-sdk/pull/14089) [`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b) Thanks [@alsuren](https://github.com/alsuren)! - Restore the D1 `executeSql` logger level via try/finally + + `wrangler d1 execute --json` and the internal `executeSql` helper temporarily lower the global logger to `"error"` to keep human-readable output out of the JSON payload. Previously the level was restored only on the happy path, so any early return or thrown error left the singleton logger muted, silencing later `logger.warn`/`logger.log` output (notably from migration helpers that wrap `executeSql` and are commonly mocked in tests). + + The level swap is now wrapped in `try`/`finally` so it is always restored. + +- [#14175](https://github.com/cloudflare/workers-sdk/pull/14175) [`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260601.1 | 1.20260603.1 | + +- [#14121](https://github.com/cloudflare/workers-sdk/pull/14121) [`7539a9b`](https://github.com/cloudflare/workers-sdk/commit/7539a9bfcf03a14b2c16f281d541b6bc45523a80) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Extract the OAuth 2.0 + PKCE flow into a new `@cloudflare/workers-auth` package. + + The OAuth login / logout / refresh logic, the auth-config TOML file IO, the OAuth token exchange + local callback server, and the Cloudflare Access detection helpers that previously lived in `packages/wrangler/src/user/` have moved to the new internal-only `@cloudflare/workers-auth` package. Wrangler now wires the OAuth flow up via a small glue module that injects its logger, browser opener, interactivity detector, and config cache via a dependency- injection context. + + What stays in wrangler: + + - The yargs `login` / `logout` / `whoami` / `auth token` commands + - Environment-based credential resolution (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_API_KEY` / `CLOUDFLARE_EMAIL`, etc.) + - Cloudflare account selection (`requireAuth`, `getOrSelectAccountId`) + - The OAuth scope catalog (passed into the OAuth flow as a generic `string[]`) + - `whoami` / account fetching + + No behavior change for end users. The on-disk TOML format and location remain identical, and all telemetry message labels are preserved verbatim. + + `@cloudflare/workers-auth` is published with `prerelease: true` and is not intended for external use — its APIs may change without notice. + +- [#14162](https://github.com/cloudflare/workers-sdk/pull/14162) [`0bb2d55`](https://github.com/cloudflare/workers-sdk/commit/0bb2d55116ce90a147582a7b4d96e3090cddf7ee) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - In non-interactive mode remove the skills installation message + + When Wrangler run in non interactive mode and it detected agents that it could install skills for, it would print a message such as: + + `Cloudflare agent skills are available for: . Run wrangler in an interactive terminal to install them, or use '--install-skills' to install without prompting.` + + This message seems to be confusing and unhelpful so it has now been removed. + +- [#14165](https://github.com/cloudflare/workers-sdk/pull/14165) [`8400fb9`](https://github.com/cloudflare/workers-sdk/commit/8400fb945a781e7a7a78a3614a702ace2d1fbc87) Thanks [@NuroDev](https://github.com/NuroDev)! - Limit `wrangler versions list` to the 10 most recent deployable versions + + The versions API ignores pagination when filtering to deployable versions, so Wrangler now caps the command output client-side. This keeps the command aligned with its help text and avoids overwhelming terminal output for Workers with many versions. + +- [#14151](https://github.com/cloudflare/workers-sdk/pull/14151) [`7949f81`](https://github.com/cloudflare/workers-sdk/commit/7949f81bd258292a4a0b9c5a339c6c035f27d7ca) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Skip stale bundles during dev server reload to avoid redundant restarts + + When rapidly saving a wrangler config file with remote bindings, each save would trigger a full reload cycle (remote connection setup, miniflare restart), causing many sequential "Reloading local server... / Establishing remote connection..." messages (while blocking the user). The runtime controllers now check whether a newer bundle has been queued at each expensive async boundary and bail out early if the current bundle is stale. This ensures that only the latest config change triggers a reload, making `wrangler dev` much more responsive during repeated config edits. + +- [#14072](https://github.com/cloudflare/workers-sdk/pull/14072) [`d462013`](https://github.com/cloudflare/workers-sdk/commit/d46201384f656815bf9e90a595098edff43f1b32) Thanks [@himanshu-cf](https://github.com/himanshu-cf)! - Update `wrangler secret bulk` command description to reflect create/update/delete capabilities + + The help text for `wrangler secret bulk` now accurately describes that the command can create, update, or delete multiple secrets in a single request, with up to 100 secrets per command. The file argument description also clarifies that setting a key to `null` in JSON will delete it, and that deletion is not supported with `.env` files. + +- [#13979](https://github.com/cloudflare/workers-sdk/pull/13979) [`c2280cd`](https://github.com/cloudflare/workers-sdk/commit/c2280cdb589c9289bb4082d0a068846f3dd22b37) Thanks [@matingathani](https://github.com/matingathani)! - Warn when a named environment silently inherits custom_domain routes from the top-level config + + When an `env.` block does not override `routes`, it inherits the top-level `routes` array. If that array contains entries with `custom_domain: true`, every deploy to the named environment will silently reassign the custom domain away from the top-level Worker and towards the env Worker, causing routing drift. Wrangler now emits a warning in this situation and suggests adding `"routes": []` to the env block to prevent inheritance. + +- [#14170](https://github.com/cloudflare/workers-sdk/pull/14170) [`ea12b58`](https://github.com/cloudflare/workers-sdk/commit/ea12b584ee1c3141286f0ecf6b742bd79971407e) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Tighten on-disk permissions of the OAuth credentials file to `0600` + + The user auth config file written by `wrangler login` (typically `~/.config/.wrangler/config/default.toml` on Linux/macOS, or `.toml` for non-production Cloudflare API environments) is now written with mode `0600` and re-`chmod`-ed on every save. This prevents other local users on shared hosts from reading the stored OAuth tokens. Existing files with looser permissions written by older Wrangler versions are tightened the next time Wrangler refreshes the token or the user logs in again. The change is a no-op on Windows, which does not honour POSIX mode bits. + +- [#14022](https://github.com/cloudflare/workers-sdk/pull/14022) [`acf7817`](https://github.com/cloudflare/workers-sdk/commit/acf7817266b39be9707a09b918d670a468302ebc) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Show the actual OAuth error instead of hanging when `wrangler login` is rejected by the OAuth provider (for example with `invalid_scope`). + + Previously, if the OAuth callback returned with an `error` other than `access_denied`, Wrangler would never respond to the browser. Because `server.close()`'s callback only fires once all open connections have ended, the login command would hang until the 120 second OAuth timeout — at which point it would print a generic timeout message rather than the actual OAuth failure. The same gap existed for the case where the OAuth provider redirected back without an authorisation code, and for failures during the auth-code-to-access-token exchange. + + The OAuth provider's `error_description` (RFC 6749 §4.1.2.1) is now also surfaced, so the message includes the specific reason for the failure rather than just the bare `error` code. For example, a misconfigured staging scope now surfaces as: + + ``` + OAuth error: invalid_scope + The OAuth 2.0 Client is not allowed to request scope 'browser:write'. + ``` + + instead of hanging silently. + +- Updated dependencies [[`a3eea27`](https://github.com/cloudflare/workers-sdk/commit/a3eea277aae46450aec1f0c811e3fe256022c46e), [`1fdd8de`](https://github.com/cloudflare/workers-sdk/commit/1fdd8def456011c29c5879fe49be6fa90ad9858d), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`3b8b80a`](https://github.com/cloudflare/workers-sdk/commit/3b8b80ab32e3ac33b5df9f6944dca9cdf72c5495)]: + - miniflare@4.20260603.0 + ## 4.97.0 ### Minor Changes diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 1165e68066..f630292b58 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -1,6 +1,6 @@ { "name": "wrangler", - "version": "4.97.0", + "version": "4.98.0", "description": "Command-line interface for all things Cloudflare Workers", "keywords": [ "assembly", From b932e47d49e736cb59159341a92045dcc65df0c6 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 4 Jun 2026 07:23:33 -0500 Subject: [PATCH 5/6] Validate containers instance ID at API level (#14173) --- .changeset/curly-ducks-ssh.md | 7 ++++ .../src/client/core/request.ts | 15 ++++++++ .../containers-shared/tests/request.test.ts | 34 +++++++++++++++++++ .../src/__tests__/containers/ssh.test.ts | 20 +++++++++-- packages/wrangler/src/containers/ssh.ts | 7 ---- 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 .changeset/curly-ducks-ssh.md create mode 100644 packages/containers-shared/tests/request.test.ts diff --git a/.changeset/curly-ducks-ssh.md b/.changeset/curly-ducks-ssh.md new file mode 100644 index 0000000000..0bb5b81e3e --- /dev/null +++ b/.changeset/curly-ducks-ssh.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Handle API validation errors from `wrangler containers ssh` + +Wrangler now lets the Containers API validate SSH instance IDs and preserves raw API error bodies such as `INVALID_INSTANCE_ID` when reporting validation failures. diff --git a/packages/containers-shared/src/client/core/request.ts b/packages/containers-shared/src/client/core/request.ts index 1cb64f47c5..e46cc6495a 100644 --- a/packages/containers-shared/src/client/core/request.ts +++ b/packages/containers-shared/src/client/core/request.ts @@ -22,6 +22,10 @@ type FetchResult = { result_info?: ResultInfo; }; +type ErrorResponse = { + error: string; +}; + const isDefined = ( value: T | null | undefined ): value is Exclude => { @@ -49,6 +53,15 @@ const isBlob = (value: any): value is Blob => { ); }; +const isErrorResponse = (value: unknown): value is ErrorResponse => { + return ( + typeof value === "object" && + value !== null && + "error" in value && + typeof value.error === "string" + ); +}; + const base64 = (str: string): string => { try { return btoa(str); @@ -233,6 +246,8 @@ const parseResponseSchemaV4 = ( } else { result = {}; } + } else if (isErrorResponse(fetchResult)) { + result = fetchResult; } else { result = { error: fetchResult.errors?.[0]?.message }; } diff --git a/packages/containers-shared/tests/request.test.ts b/packages/containers-shared/tests/request.test.ts new file mode 100644 index 0000000000..8472bfd3f7 --- /dev/null +++ b/packages/containers-shared/tests/request.test.ts @@ -0,0 +1,34 @@ +import { afterEach, describe, it, vi } from "vitest"; +import { OpenAPI } from "../src/client/core/OpenAPI"; +import { request } from "../src/client/core/request"; + +describe("request", () => { + afterEach(() => { + vi.unstubAllGlobals(); + OpenAPI.BASE = ""; + }); + + it("preserves raw error responses parsed from text", async ({ expect }) => { + OpenAPI.BASE = + "https://api.cloudflare.com/client/v4/accounts/account-id/containers"; + vi.stubGlobal( + "fetch", + vi.fn(async () => { + return new Response(JSON.stringify({ error: "INVALID_INSTANCE_ID" }), { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + }) + ); + + await expect( + request(OpenAPI, { + method: "GET", + url: "/instances/invalid-id/ssh", + errors: { 400: "Bad Request" }, + }) + ).rejects.toMatchObject({ + body: { error: "INVALID_INSTANCE_ID" }, + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/containers/ssh.test.ts b/packages/wrangler/src/__tests__/containers/ssh.test.ts index 9def866a8f..6bd34615c6 100644 --- a/packages/wrangler/src/__tests__/containers/ssh.test.ts +++ b/packages/wrangler/src/__tests__/containers/ssh.test.ts @@ -87,13 +87,27 @@ describe("containers ssh", () => { `); }); - it("should reject invalid container ID format", async ({ expect }) => { + it("should let the API validate invalid container ID format", async ({ + expect, + }) => { setWranglerConfig({}); + const sshRequest = vi.fn(); + msw.use( + http.get(`*/instances/:instanceId/ssh`, async ({ params }) => { + sshRequest(params.instanceId); + return HttpResponse.json( + { error: "INVALID_INSTANCE_ID" }, + { status: 400 } + ); + }) + ); + await expect( runWrangler("containers ssh invalid-id") - ).rejects.toMatchInlineSnapshot( - `[Error: Expected an instance ID but got invalid-id]` + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Error verifying SSH access: INVALID_INSTANCE_ID]` ); + expect(sshRequest).toHaveBeenCalledWith("invalid-id"); }); it("should handle 500s when getting ssh jwt", async ({ expect }) => { diff --git a/packages/wrangler/src/containers/ssh.ts b/packages/wrangler/src/containers/ssh.ts index 6181e01914..3af4327e45 100644 --- a/packages/wrangler/src/containers/ssh.ts +++ b/packages/wrangler/src/containers/ssh.ts @@ -3,7 +3,6 @@ import { createServer } from "node:net"; import { showCursor } from "@cloudflare/cli-shared-helpers"; import { bold } from "@cloudflare/cli-shared-helpers/colors"; import { ApiError, DeploymentsService } from "@cloudflare/containers-shared"; -import { UserError } from "@cloudflare/workers-utils"; import { WebSocket } from "ws"; import { fillOpenAPIConfiguration, @@ -101,12 +100,6 @@ const sshArgDefs = { type SshArgs = HandlerArgs; async function sshCommand(sshArgs: SshArgs, _config: Config) { - if (sshArgs.ID.length !== 64) { - throw new UserError(`Expected an instance ID but got ${sshArgs.ID}`, { - telemetryMessage: "containers ssh invalid instance id", - }); - } - // Get arguments passed to the SSH command itself. yargs includes // "containers" and "ssh" as the first two elements of the array, which // we don't want, so we don't include those. From 79937112ff580c34b182b73ef830cdb17b5b798d Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 4 Jun 2026 15:34:15 +0200 Subject: [PATCH 6/6] [wrangler] Prevent draft worker creation for delete-only secret bulk (#14053) Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com> --- .changeset/guard-delete-only-secret-bulk.md | 7 +++ .../wrangler/src/__tests__/secret.test.ts | 48 +++++++++++++++++++ packages/wrangler/src/secret/index.ts | 6 +++ 3 files changed, 61 insertions(+) create mode 100644 .changeset/guard-delete-only-secret-bulk.md diff --git a/.changeset/guard-delete-only-secret-bulk.md b/.changeset/guard-delete-only-secret-bulk.md new file mode 100644 index 0000000000..8a6b1b44ba --- /dev/null +++ b/.changeset/guard-delete-only-secret-bulk.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Prevent delete-only `wrangler secret bulk` input from creating a new Worker + +Previously, `wrangler secret bulk` could create a draft Worker when the input only deleted secrets and the target Worker name did not exist. Delete-only bulk secret operations now leave Worker-not-found as an error instead of creating a new Worker. diff --git a/packages/wrangler/src/__tests__/secret.test.ts b/packages/wrangler/src/__tests__/secret.test.ts index 0e18885534..34164ff0f9 100644 --- a/packages/wrangler/src/__tests__/secret.test.ts +++ b/packages/wrangler/src/__tests__/secret.test.ts @@ -1620,6 +1620,54 @@ describe("wrangler secret", () => { `); }); + it("should not create a new worker for delete-only bulk input when the worker is not found", async ({ + expect, + }) => { + setIsTTY(false); + writeFileSync( + "secret.json", + JSON.stringify({ + "secret-to-delete": null, + }) + ); + + mockNoWorkerFound({ isBulk: true }); + const createDraftWorkerRequests = { count: 0 }; + msw.use( + http.put("*/accounts/:accountId/workers/scripts/:name", async () => { + createDraftWorkerRequests.count++; + return HttpResponse.json(createFetchResult(null)); + }) + ); + + await expect( + runWrangler("secret bulk ./secret.json --name non-existent-worker") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[APIError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/non-existent-worker/secrets-bulk) failed.]` + ); + + expect(createDraftWorkerRequests.count).toBe(0); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + 🌀 Processing the secrets for the Worker "non-existent-worker" + + 🚨 Secrets failed to upload + " + `); + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/non-existent-worker/secrets-bulk) failed. + + This Worker does not exist on your account. [code: 10007] + + If you think this is a bug, please open an issue at: + https://github.com/cloudflare/workers-sdk/issues/new/choose + + " + `); + }); + describe("multi-env warning", () => { it("should warn if the wrangler config contains environments but none was specified in the command", async ({ expect, diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index d1dcbafe12..2581d9cd42 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -487,6 +487,9 @@ export const secretBulkCommand = createCommand({ } const { content, secretSource, secretFormat } = result; + const hasSecretsToCreate = Object.values(content).some( + (value) => value != null + ); let created: Array = []; let deleted: Array = []; @@ -504,6 +507,9 @@ export const secretBulkCommand = createCommand({ if (!isWorkerNotFoundError(e)) { throw e; } + if (!hasSecretsToCreate) { + throw e; + } // Worker doesn't exist yet — create a draft worker, then retry const draftWorkerResult = await createDraftWorker({ config,