From 50ef72497eda26760bd88f5d5f6281208f761479 Mon Sep 17 00:00:00 2001 From: ANT Bot <116369605+workers-devprod@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:37:53 +0100 Subject: [PATCH 01/18] Version Packages (#14082) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/add-web-search-binding.md | 29 --- .changeset/agent-memory-binding-support.md | 25 --- .changeset/agent-memory-namespace-commands.md | 14 -- .../aggregate-trigger-deploy-failures.md | 11 - .changeset/angry-carrots-switch.md | 9 - .changeset/browser-launch-timeout.md | 7 - .changeset/browser-run-type.md | 7 - .changeset/bump-rosie-skills.md | 5 - .changeset/c3-tanstack-cli-migration.md | 7 - .changeset/dependabot-update-14060.md | 12 -- .changeset/dependabot-update-14076.md | 12 -- .changeset/dependabot-update-14100.md | 12 -- .changeset/disable-sentry-by-default.md | 7 - .changeset/fix-autoconfig-vite-no-config.md | 9 - .changeset/fix-cloudflared-macos-checksum.md | 9 - .changeset/fix-miniflare-pnp-sourcemap.md | 7 - .../fix-open-in-browser-enoent-headless.md | 9 - ...x-secrets-store-value-length-validation.md | 5 - .changeset/kv-bulk-get-success-stdout.md | 5 - .changeset/nine-mangos-camp.md | 7 - .changeset/pipeline-stream-rename.md | 37 ---- .changeset/proxied-durable-object-rpc.md | 7 - .changeset/quiet-birds-fly.md | 10 - .changeset/quiet-tokens-guard.md | 7 - .../react-router-autoconfig-v8-middleware.md | 9 - .changeset/resolve-pipeline-names.md | 5 - .changeset/secret-bulk-delete-support.md | 11 - .changeset/sharp-containers-proxy.md | 7 - .changeset/smooth-nails-search.md | 7 - .changeset/stale-shoes-learn.md | 7 - .changeset/unstable-dev-remove-test-mode.md | 9 - .changeset/whoami-trailing-period.md | 10 - .../workflows-uint8array-sqlite-toobig.md | 17 -- .changeset/wrangler-bundler-initial.md | 5 - packages/cli/CHANGELOG.md | 7 + packages/cli/package.json | 2 +- packages/create-cloudflare/CHANGELOG.md | 8 + packages/create-cloudflare/package.json | 2 +- packages/miniflare/CHANGELOG.md | 119 +++++++++++ packages/miniflare/package.json | 2 +- packages/pages-shared/CHANGELOG.md | 7 + packages/pages-shared/package.json | 2 +- packages/vite-plugin-cloudflare/CHANGELOG.md | 18 ++ packages/vite-plugin-cloudflare/package.json | 2 +- packages/vitest-pool-workers/CHANGELOG.md | 12 ++ packages/vitest-pool-workers/package.json | 2 +- packages/workers-utils/CHANGELOG.md | 74 +++++++ packages/workers-utils/package.json | 2 +- packages/workflows-shared/CHANGELOG.md | 18 ++ packages/workflows-shared/package.json | 2 +- packages/wrangler-bundler/CHANGELOG.md | 12 ++ packages/wrangler-bundler/package.json | 2 +- packages/wrangler/CHANGELOG.md | 202 ++++++++++++++++++ packages/wrangler/package.json | 2 +- 54 files changed, 487 insertions(+), 366 deletions(-) delete mode 100644 .changeset/add-web-search-binding.md delete mode 100644 .changeset/agent-memory-binding-support.md delete mode 100644 .changeset/agent-memory-namespace-commands.md delete mode 100644 .changeset/aggregate-trigger-deploy-failures.md delete mode 100644 .changeset/angry-carrots-switch.md delete mode 100644 .changeset/browser-launch-timeout.md delete mode 100644 .changeset/browser-run-type.md delete mode 100644 .changeset/bump-rosie-skills.md delete mode 100644 .changeset/c3-tanstack-cli-migration.md delete mode 100644 .changeset/dependabot-update-14060.md delete mode 100644 .changeset/dependabot-update-14076.md delete mode 100644 .changeset/dependabot-update-14100.md delete mode 100644 .changeset/disable-sentry-by-default.md delete mode 100644 .changeset/fix-autoconfig-vite-no-config.md delete mode 100644 .changeset/fix-cloudflared-macos-checksum.md delete mode 100644 .changeset/fix-miniflare-pnp-sourcemap.md delete mode 100644 .changeset/fix-open-in-browser-enoent-headless.md delete mode 100644 .changeset/fix-secrets-store-value-length-validation.md delete mode 100644 .changeset/kv-bulk-get-success-stdout.md delete mode 100644 .changeset/nine-mangos-camp.md delete mode 100644 .changeset/pipeline-stream-rename.md delete mode 100644 .changeset/proxied-durable-object-rpc.md delete mode 100644 .changeset/quiet-birds-fly.md delete mode 100644 .changeset/quiet-tokens-guard.md delete mode 100644 .changeset/react-router-autoconfig-v8-middleware.md delete mode 100644 .changeset/resolve-pipeline-names.md delete mode 100644 .changeset/secret-bulk-delete-support.md delete mode 100644 .changeset/sharp-containers-proxy.md delete mode 100644 .changeset/smooth-nails-search.md delete mode 100644 .changeset/stale-shoes-learn.md delete mode 100644 .changeset/unstable-dev-remove-test-mode.md delete mode 100644 .changeset/whoami-trailing-period.md delete mode 100644 .changeset/workflows-uint8array-sqlite-toobig.md delete mode 100644 .changeset/wrangler-bundler-initial.md create mode 100644 packages/wrangler-bundler/CHANGELOG.md diff --git a/.changeset/add-web-search-binding.md b/.changeset/add-web-search-binding.md deleted file mode 100644 index 0ed5773f85..0000000000 --- a/.changeset/add-web-search-binding.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"miniflare": minor -"wrangler": minor -"@cloudflare/workers-utils": minor ---- - -Add support for the new `web_search` binding kind. - -Cloudflare Web Search is a managed, zero-setup web discovery primitive for agents and Workers. Declare the binding as a single object in `wrangler.jsonc`: - -```jsonc -{ - "web_search": { "binding": "WEBSEARCH" }, -} -``` - -There is exactly one shared web corpus, so there is no namespace, instance, or other field to specify -- only the variable name. The binding exposes a single `search()` method that returns URLs and catalog metadata for a query. Web Search is discovery-only -- to read a result's content the caller invokes the global `fetch()` API against the result's `url`. - -The binding is **always remote** in local development: Miniflare proxies to the production Web Search service via the remote-bindings transport. Adds the `websearch.run` OAuth scope to `wrangler login`. - -Also adds a `wrangler websearch search` command for running ad-hoc queries from the CLI: - -```sh -npx wrangler websearch search "cloudflare workers" -npx wrangler websearch search "cloudflare workers" --limit 5 -npx wrangler websearch search "cloudflare workers" --json -``` - -`--limit` is optional (defaults to 10, capped at 20). `--json` prints the raw response; without it the results render as a pretty table. diff --git a/.changeset/agent-memory-binding-support.md b/.changeset/agent-memory-binding-support.md deleted file mode 100644 index bf34802ced..0000000000 --- a/.changeset/agent-memory-binding-support.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"miniflare": minor -"wrangler": minor ---- - -Add support for `agent_memory` bindings - -Agent Memory bindings allow Workers to connect to Cloudflare's Agent Memory service for storing and retrieving agent conversation state. This binding is remote-only, meaning it always connects to the Cloudflare API during `wrangler dev` rather than using a local simulation. - -To configure an `agent_memory` binding, add the following to your `wrangler.json`: - -```jsonc -{ - "agent_memory": [ - { - "binding": "MY_MEMORY", - "namespace": "my-namespace", - }, - ], -} -``` - -Wrangler will automatically provision the namespace during deployment if it does not already exist. Type generation via `wrangler types` is also supported. - -This change also adds the `agent-memory:write` OAuth scope to Wrangler's default login scopes, so `wrangler login` can request the permissions needed to provision and manage Agent Memory namespaces. diff --git a/.changeset/agent-memory-namespace-commands.md b/.changeset/agent-memory-namespace-commands.md deleted file mode 100644 index 57e1b72d42..0000000000 --- a/.changeset/agent-memory-namespace-commands.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"wrangler": minor ---- - -Add `wrangler agent-memory namespace` commands - -The following commands have been added for managing Agent Memory namespaces: - -```bash -wrangler agent-memory namespace create -wrangler agent-memory namespace list [--json] -wrangler agent-memory namespace get [--json] -wrangler agent-memory namespace delete [--force] -``` diff --git a/.changeset/aggregate-trigger-deploy-failures.md b/.changeset/aggregate-trigger-deploy-failures.md deleted file mode 100644 index 14e0879664..0000000000 --- a/.changeset/aggregate-trigger-deploy-failures.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"wrangler": patch ---- - -report all failing triggers from a single deploy - -`wrangler deploy` deploys several kinds of trigger in parallel (routes, custom domains, schedules, queue producers/consumers, workflows). Previously, if one of those API calls failed, the first rejection short-circuited the rest, no other deployments were reported, and (in the case of custom-domain confirmation conflicts) some failures were silently logged to stdout without the deploy actually failing. - -`wrangler deploy` now waits for every trigger deployment to settle, prints every successfully-deployed target (so you still see what landed), and then throws a single error listing every trigger that failed. - -Note that this also turns the previously-silent "user declined to override a conflicting Custom Domain" case into a hard failure of `wrangler deploy`, which matches what was always implied by the message ("Publishing to Custom Domain ... was skipped, fix conflict and try again"). diff --git a/.changeset/angry-carrots-switch.md b/.changeset/angry-carrots-switch.md deleted file mode 100644 index a4bac445cd..0000000000 --- a/.changeset/angry-carrots-switch.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch ---- - -Bump `rosie-skills` from `0.7.6` to `0.8.1` and bundle it into the Wrangler output - -The new version of `rosie-skills` is a [pure-TypeScript rewrite](https://github.com/withastro/rosie/pull/21) that removes the previously necessary ~600kb WASM binary. The package now ships only JavaScript with one minimal dependencies (`modern-tar`). - -Additionally, `rosie-skills` is now bundled directly into Wrangler's distributable rather than kept as an external runtime dependency. This eliminates the supply chain concern raised in [#14110](https://github.com/cloudflare/workers-sdk/issues/14110): there is no separate package to resolve at install time, since all code is inlined into Wrangler's build output. diff --git a/.changeset/browser-launch-timeout.md b/.changeset/browser-launch-timeout.md deleted file mode 100644 index dd5f55d748..0000000000 --- a/.changeset/browser-launch-timeout.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"miniflare": patch ---- - -Add timeout to browser-rendering browser launch to prevent infinite hangs - -The browser-rendering plugin's `launchBrowser()` function now passes a 5-minute timeout to `waitForLineOutput()` when waiting for Chrome to print its DevTools WebSocket URL. Previously, if Chrome failed to start or crashed before printing the URL, the promise would hang forever. This could cause CI pipelines and local dev sessions to get stuck indefinitely. diff --git a/.changeset/browser-run-type.md b/.changeset/browser-run-type.md deleted file mode 100644 index 16fd8e122b..0000000000 --- a/.changeset/browser-run-type.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Update the generated type for browser bindings to `BrowserRun` - -When running `wrangler types`, browser bindings were previously typed as the generic `Fetcher`. They now generate the more specific and accurate `BrowserRun` type. diff --git a/.changeset/bump-rosie-skills.md b/.changeset/bump-rosie-skills.md deleted file mode 100644 index 0d5104b7bd..0000000000 --- a/.changeset/bump-rosie-skills.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": patch ---- - -Bump `rosie-skills` package from 0.6.3 to 0.7.6 diff --git a/.changeset/c3-tanstack-cli-migration.md b/.changeset/c3-tanstack-cli-migration.md deleted file mode 100644 index 6cf2e6e280..0000000000 --- a/.changeset/c3-tanstack-cli-migration.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"create-cloudflare": minor ---- - -Migrate TanStack Start scaffolding from `@tanstack/create-start` to `@tanstack/cli` - -TanStack has consolidated their project scaffolding into a unified CLI package (`@tanstack/cli`) with a `create` subcommand, replacing the previous `@tanstack/create-start` package. This updates C3 to use the new CLI while preserving the same Cloudflare deployment target and React framework options. diff --git a/.changeset/dependabot-update-14060.md b/.changeset/dependabot-update-14060.md deleted file mode 100644 index ee078d8d50..0000000000 --- a/.changeset/dependabot-update-14060.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.20260526.1 | 1.20260527.1 | diff --git a/.changeset/dependabot-update-14076.md b/.changeset/dependabot-update-14076.md deleted file mode 100644 index 5b5849e5ff..0000000000 --- a/.changeset/dependabot-update-14076.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.20260527.1 | 1.20260528.1 | diff --git a/.changeset/dependabot-update-14100.md b/.changeset/dependabot-update-14100.md deleted file mode 100644 index fe4b6d8c43..0000000000 --- a/.changeset/dependabot-update-14100.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.20260528.1 | 1.20260529.1 | diff --git a/.changeset/disable-sentry-by-default.md b/.changeset/disable-sentry-by-default.md deleted file mode 100644 index a1946ffcff..0000000000 --- a/.changeset/disable-sentry-by-default.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Disable Sentry error reporting by default - -`WRANGLER_SEND_ERROR_REPORTS` now defaults to `false` instead of prompting on every error. The current prompt produces too many false-positive reports. Users can still opt in explicitly by setting `WRANGLER_SEND_ERROR_REPORTS=true`. diff --git a/.changeset/fix-autoconfig-vite-no-config.md b/.changeset/fix-autoconfig-vite-no-config.md deleted file mode 100644 index 0d743e4b73..0000000000 --- a/.changeset/fix-autoconfig-vite-no-config.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch ---- - -Fix `wrangler setup` failing for Vite projects without a config file - -`wrangler setup` (and `wrangler deploy --experimental-autoconfig`) crashed with "Could not find Vite config file to modify" for Vite projects that don't have a `vite.config.js` or `vite.config.ts`. This affected 6 of the 16 `create-vite` templates: `vanilla`, `vanilla-ts`, `react-swc`, `react-swc-ts`, `lit`, and `lit-ts`. - -Autoconfig now creates a minimal Vite config with the Cloudflare plugin when no config file exists, instead of failing. The file extension (`.ts` or `.js`) is chosen based on whether the project has a `tsconfig.json`. diff --git a/.changeset/fix-cloudflared-macos-checksum.md b/.changeset/fix-cloudflared-macos-checksum.md deleted file mode 100644 index 7345d15839..0000000000 --- a/.changeset/fix-cloudflared-macos-checksum.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@cloudflare/workers-utils": patch ---- - -Fix cloudflared SHA256 checksum mismatch on macOS - -The update service (`update.argotunnel.com`) returns a checksum for the extracted binary, not the `.tgz` tarball. We were computing the SHA256 of the tarball itself, which always mismatched on macOS where cloudflared is distributed as a compressed archive. - -This aligns with cloudflared's own auto-updater (`cmd/cloudflared/updater/workers_update.go`), which decompresses the tarball first, then checksums the resulting binary. We now do the same: extract, then verify. diff --git a/.changeset/fix-miniflare-pnp-sourcemap.md b/.changeset/fix-miniflare-pnp-sourcemap.md deleted file mode 100644 index 0aff01958b..0000000000 --- a/.changeset/fix-miniflare-pnp-sourcemap.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"miniflare": patch ---- - -Fix `wrangler dev` crash under Yarn PnP when the worker emits a structured log or the inspector forwards a stack trace. - -`getFreshSourceMapSupport` was unconditionally indexing `require.cache`, but when `miniflare` is `import`ed from ESM under Yarn PnP, Node's ESM->CJS bridge (`loadCJSModule` in `node:internal/modules/esm/translators`) hands the wrapped CJS module a re-invented `require` that only carries `.resolve` and `.main`, with no `.cache`. Fall back to `createRequire(__filename)` in that case so the fresh-load cache-swap keeps working. diff --git a/.changeset/fix-open-in-browser-enoent-headless.md b/.changeset/fix-open-in-browser-enoent-headless.md deleted file mode 100644 index 8546b79128..0000000000 --- a/.changeset/fix-open-in-browser-enoent-headless.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch ---- - -Show helpful message with URL when browser cannot be opened in headless/container environments - -Previously, running `wrangler login` (or any command that opens a browser) in headless Linux environments without `xdg-open` installed would crash with a confusing "A file or directory could not be found — Missing file or directory: xdg-open" error. - -Now wrangler catches the error and prints a clear warning with the URL so users can copy-paste it into a browser manually. diff --git a/.changeset/fix-secrets-store-value-length-validation.md b/.changeset/fix-secrets-store-value-length-validation.md deleted file mode 100644 index 7fa773668f..0000000000 --- a/.changeset/fix-secrets-store-value-length-validation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": patch ---- - -`wrangler secrets-store secret create` and `secret update` now reject secret values larger than 64 KiB (65,536 bytes) with a clear error before calling the Cloudflare API. Previously the CLI accepted them, the secret appeared in `secret list`, and the failure surfaced later (and confusingly) at worker deploy time as a "secret doesn't exist" error against the binding. 64 KiB is the cap enforced by the API; the CLI now enforces it at the same boundary. diff --git a/.changeset/kv-bulk-get-success-stdout.md b/.changeset/kv-bulk-get-success-stdout.md deleted file mode 100644 index 7581cfe9b6..0000000000 --- a/.changeset/kv-bulk-get-success-stdout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": patch ---- - -Fix `wrangler kv bulk get` printing "Success!" to stdout, which corrupted JSON output when piped to tools like `jq` diff --git a/.changeset/nine-mangos-camp.md b/.changeset/nine-mangos-camp.md deleted file mode 100644 index 9a5cf618e4..0000000000 --- a/.changeset/nine-mangos-camp.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": minor ---- - -Add confirmation prompt to `wrangler containers images delete` - -Previously, running `wrangler containers images delete IMAGE:TAG` would delete the image immediately with no confirmation. The command now prompts for confirmation before deleting. Use `-y` or `--skip-confirmation` to bypass the prompt in non-interactive or scripted environments. diff --git a/.changeset/pipeline-stream-rename.md b/.changeset/pipeline-stream-rename.md deleted file mode 100644 index 8f564ed6e5..0000000000 --- a/.changeset/pipeline-stream-rename.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"wrangler": minor -"@cloudflare/workers-utils": minor -"miniflare": minor ---- - -Rename `pipeline` field to `stream` in pipeline bindings configuration - -The `pipeline` field inside `pipelines` bindings has been renamed to `stream` to align with the updated API wire format. The old `pipeline` field is still accepted but deprecated and will emit a warning. - -Before: - -```jsonc -// wrangler.json -{ - "pipelines": [ - { - "binding": "MY_PIPELINE", - "pipeline": "my-stream-name", - }, - ], -} -``` - -After: - -```jsonc -// wrangler.json -{ - "pipelines": [ - { - "binding": "MY_PIPELINE", - "stream": "my-stream-name", - }, - ], -} -``` diff --git a/.changeset/proxied-durable-object-rpc.md b/.changeset/proxied-durable-object-rpc.md deleted file mode 100644 index 17c20fcb89..0000000000 --- a/.changeset/proxied-durable-object-rpc.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/vitest-pool-workers": patch ---- - -Fix Durable Object RPC dispatch for constructors that return proxies - -Durable Object RPC methods mediated by a returned `Proxy` are now resolved through that proxy after validating prototype exposure. This allows wrappers that bind methods to the underlying instance to use private fields and methods in Vitest, while matching workerd's rejection of constructor-assigned RPC overrides. diff --git a/.changeset/quiet-birds-fly.md b/.changeset/quiet-birds-fly.md deleted file mode 100644 index cc07c816f6..0000000000 --- a/.changeset/quiet-birds-fly.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@cloudflare/vite-plugin": patch -"@cloudflare/workers-utils": patch ---- - -Filter compatibility date fallback warning when no update is available - -The compatibility date warning from workerd (e.g., "The latest compatibility date supported by the installed Cloudflare Workers Runtime is...") is now only shown when a newer version of `@cloudflare/vite-plugin` is available. This matches the behavior in Wrangler and reduces noise when the user is already on the latest version. - -The update-check logic has been extracted to `@cloudflare/workers-utils` so it can be shared across packages. diff --git a/.changeset/quiet-tokens-guard.md b/.changeset/quiet-tokens-guard.md deleted file mode 100644 index 96af7a9b54..0000000000 --- a/.changeset/quiet-tokens-guard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Show a clear error for invalid API token header characters - -Wrangler now detects API tokens containing characters that cannot be sent in the HTTP Authorization header before making an API request. This avoids a low-level ByteString conversion error and helps users recreate or recopy the token without printing the token value. diff --git a/.changeset/react-router-autoconfig-v8-middleware.md b/.changeset/react-router-autoconfig-v8-middleware.md deleted file mode 100644 index c4564b2056..0000000000 --- a/.changeset/react-router-autoconfig-v8-middleware.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch ---- - -Adapt React Router autoconfig based on `v8_middleware` future flag - -The React Router autoconfig (`wrangler setup`) now detects whether `v8_middleware: true` is set in the user's `react-router.config.ts`. When it is, the generated `workers/app.ts` uses a simplified fetch handler without `AppLoadContext` module augmentation, and the generated `app/entry.server.tsx` omits the `_loadContext` parameter. When `v8_middleware` is not set, the existing `AppLoadContext` pattern with `env`/`ctx` params is preserved. - -This avoids breaking projects that use the `v8_middleware` future flag (which changes the context API from `AppLoadContext` to `RouterContextProvider`), while keeping the traditional pattern for projects that haven't opted in. diff --git a/.changeset/resolve-pipeline-names.md b/.changeset/resolve-pipeline-names.md deleted file mode 100644 index 2539668f1c..0000000000 --- a/.changeset/resolve-pipeline-names.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": minor ---- - -Allow pipeline, stream, and sink commands to resolve resources by name with pagination-aware lookups. diff --git a/.changeset/secret-bulk-delete-support.md b/.changeset/secret-bulk-delete-support.md deleted file mode 100644 index 9fc7c63e2c..0000000000 --- a/.changeset/secret-bulk-delete-support.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"wrangler": minor ---- - -Support deleting secrets via `wrangler secret bulk` - -You can now delete secrets in bulk by setting their value to `null` in the JSON input file: - -```json -{ "SECRET_TO_DELETE": null, "SECRET_TO_UPDATE": "new-value" } -``` diff --git a/.changeset/sharp-containers-proxy.md b/.changeset/sharp-containers-proxy.md deleted file mode 100644 index 0f8e3a7171..0000000000 --- a/.changeset/sharp-containers-proxy.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": minor ---- - -Add ProxyCommand support for `wrangler containers ssh` - -`wrangler containers ssh` now automatically switches to a stdio proxy when invoked by OpenSSH's `ProxyCommand`, and `--stdio` can force this mode. This lets users connect with `ssh ` when their SSH config uses Wrangler as the proxy command. diff --git a/.changeset/smooth-nails-search.md b/.changeset/smooth-nails-search.md deleted file mode 100644 index d1eaeaee88..0000000000 --- a/.changeset/smooth-nails-search.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/vite-plugin": patch ---- - -Fix `Tunnel closed` being logged when no tunnel was opened - -Previously, the Vite plugin printed `Tunnel closed` during cleanup even when tunnel startup had never begun. This message is now only shown after tunnel startup begins, including when the tunnel is still starting or has already expired. diff --git a/.changeset/stale-shoes-learn.md b/.changeset/stale-shoes-learn.md deleted file mode 100644 index 3d3505c274..0000000000 --- a/.changeset/stale-shoes-learn.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"miniflare": minor ---- - -Add JSON output to `/cdn-cgi/handler/scheduled` - -The `/cdn-cgi/handler/scheduled` endpoint now accepts `?format=json` to return the scheduled handler result as JSON, including whether the handler called `controller.noRetry()`. Requests without `format=json` still return the existing text outcome for backward compatibility. diff --git a/.changeset/unstable-dev-remove-test-mode.md b/.changeset/unstable-dev-remove-test-mode.md deleted file mode 100644 index 92c034fecb..0000000000 --- a/.changeset/unstable-dev-remove-test-mode.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": minor ---- - -Remove the deprecated `experimental.testMode` option from `unstable_dev` - -`experimental.testMode` previously only affected the default `logLevel` (`warn` when `testMode: true`, `log` otherwise) and has been flagged for removal in its type-definition comment since it landed. It is now removed, and `unstable_dev`'s default log level matches `wrangler dev`'s (`log`). - -Callers that explicitly passed `testMode: true` to get quieter logs should now set `logLevel: "warn"` directly. diff --git a/.changeset/whoami-trailing-period.md b/.changeset/whoami-trailing-period.md deleted file mode 100644 index e2996ba1e7..0000000000 --- a/.changeset/whoami-trailing-period.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"wrangler": patch ---- - -Fix `wrangler whoami` printing a trailing period after the api-tokens URL - -The message `To see token permissions visit https://...api-tokens.` ended with -a period that became part of the URL when clicked in terminals or GitHub Actions -output, causing a 404. The period is removed and a comma added before "visit" -so the sentence reads naturally without a trailing period on the URL. diff --git a/.changeset/workflows-uint8array-sqlite-toobig.md b/.changeset/workflows-uint8array-sqlite-toobig.md deleted file mode 100644 index 21754da21b..0000000000 --- a/.changeset/workflows-uint8array-sqlite-toobig.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@cloudflare/workflows-shared": patch ---- - -Fix `wrangler dev` Workflows crashing with `SQLITE_TOOBIG` when a step returns a large `Uint8Array` - -`JSON.stringify` encodes each byte of a `Uint8Array` as a separate numeric key -(`{"0":1,"1":2,...}`), producing a string ~10× larger than the array's byte -length. A 200 KB `Uint8Array` became a ~2 MB JSON string that exceeded SQLite's -blob limit, crashing the Workflow step. The same bytes returned as an -`ArrayBuffer` succeeded because `JSON.stringify(ArrayBuffer)` → `{}`. - -The step log metadata (used by the local Workflows explorer) now stores a -human-readable description for `TypedArray` and `ArrayBuffer` outputs -(`[Uint8Array(200000 bytes)]`) instead of attempting to JSON-serialise the raw -bytes. The actual step value is unaffected — it is preserved in Durable Object -key-value storage via structured clone for replay by subsequent steps. diff --git a/.changeset/wrangler-bundler-initial.md b/.changeset/wrangler-bundler-initial.md deleted file mode 100644 index a810db2566..0000000000 --- a/.changeset/wrangler-bundler-initial.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@cloudflare/wrangler-bundler": minor ---- - -Add `@cloudflare/wrangler-bundler` — an experimental internal package. Not intended for external use. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index d9c505af80..79a613d04d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/cli +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`599b27a`](https://github.com/cloudflare/workers-sdk/commit/599b27aafb9bc432524a35eb4e5a414de21bef41), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee)]: + - @cloudflare/workers-utils@0.22.0 + ## 0.1.4 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 67a4d893b3..c6d720f51c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/cli-shared-helpers", - "version": "0.1.4", + "version": "0.1.5", "description": "Internal shared CLI helpers for workers-sdk. Not intended for external use — APIs may change without notice.", "keywords": [ "cli", diff --git a/packages/create-cloudflare/CHANGELOG.md b/packages/create-cloudflare/CHANGELOG.md index 810e5c9933..022f82eeda 100644 --- a/packages/create-cloudflare/CHANGELOG.md +++ b/packages/create-cloudflare/CHANGELOG.md @@ -1,5 +1,13 @@ # create-cloudflare +## 2.69.0 + +### Minor Changes + +- [#14096](https://github.com/cloudflare/workers-sdk/pull/14096) [`a5b7690`](https://github.com/cloudflare/workers-sdk/commit/a5b76906ec568eb6ad096dc166c5b6228040acb7) Thanks [@MattieTK](https://github.com/MattieTK)! - Migrate TanStack Start scaffolding from `@tanstack/create-start` to `@tanstack/cli` + + TanStack has consolidated their project scaffolding into a unified CLI package (`@tanstack/cli`) with a `create` subcommand, replacing the previous `@tanstack/create-start` package. This updates C3 to use the new CLI while preserving the same Cloudflare deployment target and React framework options. + ## 2.68.4 ### Patch Changes diff --git a/packages/create-cloudflare/package.json b/packages/create-cloudflare/package.json index 90c7cc5286..0050c602cb 100644 --- a/packages/create-cloudflare/package.json +++ b/packages/create-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "create-cloudflare", - "version": "2.68.4", + "version": "2.69.0", "description": "A CLI for creating and deploying new applications to Cloudflare.", "keywords": [ "cloudflare", diff --git a/packages/miniflare/CHANGELOG.md b/packages/miniflare/CHANGELOG.md index 0bdd0e00bd..81b886d2b4 100644 --- a/packages/miniflare/CHANGELOG.md +++ b/packages/miniflare/CHANGELOG.md @@ -1,5 +1,124 @@ # miniflare +## 4.20260529.0 + +### Minor Changes + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Add support for the new `web_search` binding kind. + + Cloudflare Web Search is a managed, zero-setup web discovery primitive for agents and Workers. Declare the binding as a single object in `wrangler.jsonc`: + + ```jsonc + { + "web_search": { "binding": "WEBSEARCH" } + } + ``` + + There is exactly one shared web corpus, so there is no namespace, instance, or other field to specify -- only the variable name. The binding exposes a single `search()` method that returns URLs and catalog metadata for a query. Web Search is discovery-only -- to read a result's content the caller invokes the global `fetch()` API against the result's `url`. + + The binding is **always remote** in local development: Miniflare proxies to the production Web Search service via the remote-bindings transport. Adds the `websearch.run` OAuth scope to `wrangler login`. + + Also adds a `wrangler websearch search` command for running ad-hoc queries from the CLI: + + ```sh + npx wrangler websearch search "cloudflare workers" + npx wrangler websearch search "cloudflare workers" --limit 5 + npx wrangler websearch search "cloudflare workers" --json + ``` + + `--limit` is optional (defaults to 10, capped at 20). `--json` prints the raw response; without it the results render as a pretty table. + +- [#13610](https://github.com/cloudflare/workers-sdk/pull/13610) [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Add support for `agent_memory` bindings + + Agent Memory bindings allow Workers to connect to Cloudflare's Agent Memory service for storing and retrieving agent conversation state. This binding is remote-only, meaning it always connects to the Cloudflare API during `wrangler dev` rather than using a local simulation. + + To configure an `agent_memory` binding, add the following to your `wrangler.json`: + + ```jsonc + { + "agent_memory": [ + { + "binding": "MY_MEMORY", + "namespace": "my-namespace" + } + ] + } + ``` + + Wrangler will automatically provision the namespace during deployment if it does not already exist. Type generation via `wrangler types` is also supported. + + This change also adds the `agent-memory:write` OAuth scope to Wrangler's default login scopes, so `wrangler login` can request the permissions needed to provision and manage Agent Memory namespaces. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Rename `pipeline` field to `stream` in pipeline bindings configuration + + The `pipeline` field inside `pipelines` bindings has been renamed to `stream` to align with the updated API wire format. The old `pipeline` field is still accepted but deprecated and will emit a warning. + + Before: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "pipeline": "my-stream-name" + } + ] + } + ``` + + After: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "stream": "my-stream-name" + } + ] + } + ``` + +- [#14079](https://github.com/cloudflare/workers-sdk/pull/14079) [`972d13d`](https://github.com/cloudflare/workers-sdk/commit/972d13d7054586bb9e3c11e888179d3df7753338) Thanks [@edmundhung](https://github.com/edmundhung)! - Add JSON output to `/cdn-cgi/handler/scheduled` + + The `/cdn-cgi/handler/scheduled` endpoint now accepts `?format=json` to return the scheduled handler result as JSON, including whether the handler called `controller.noRetry()`. Requests without `format=json` still return the existing text outcome for backward compatibility. + +### Patch Changes + +- [#14106](https://github.com/cloudflare/workers-sdk/pull/14106) [`7bb5c7a`](https://github.com/cloudflare/workers-sdk/commit/7bb5c7a78a22320283549a86a29a76146f7252a4) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Add timeout to browser-rendering browser launch to prevent infinite hangs + + The browser-rendering plugin's `launchBrowser()` function now passes a 5-minute timeout to `waitForLineOutput()` when waiting for Chrome to print its DevTools WebSocket URL. Previously, if Chrome failed to start or crashed before printing the URL, the promise would hang forever. This could cause CI pipelines and local dev sessions to get stuck indefinitely. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260526.1 | 1.20260527.1 | + +- [#14076](https://github.com/cloudflare/workers-sdk/pull/14076) [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260527.1 | 1.20260528.1 | + +- [#14100](https://github.com/cloudflare/workers-sdk/pull/14100) [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260528.1 | 1.20260529.1 | + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Fix `wrangler dev` crash under Yarn PnP when the worker emits a structured log or the inspector forwards a stack trace. + + `getFreshSourceMapSupport` was unconditionally indexing `require.cache`, but when `miniflare` is `import`ed from ESM under Yarn PnP, Node's ESM->CJS bridge (`loadCJSModule` in `node:internal/modules/esm/translators`) hands the wrapped CJS module a re-invented `require` that only carries `.resolve` and `.main`, with no `.cache`. Fall back to `createRequire(__filename)` in that case so the fresh-load cache-swap keeps working. + ## 4.20260526.0 ### Patch Changes diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 7fe9df2a0c..b38a45aab4 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -1,6 +1,6 @@ { "name": "miniflare", - "version": "4.20260526.0", + "version": "4.20260529.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 b56f78fe04..829278781b 100644 --- a/packages/pages-shared/CHANGELOG.md +++ b/packages/pages-shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/pages-shared +## 0.13.141 + +### Patch Changes + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`7bb5c7a`](https://github.com/cloudflare/workers-sdk/commit/7bb5c7a78a22320283549a86a29a76146f7252a4), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280), [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`972d13d`](https://github.com/cloudflare/workers-sdk/commit/972d13d7054586bb9e3c11e888179d3df7753338)]: + - miniflare@4.20260529.0 + ## 0.13.140 ### Patch Changes diff --git a/packages/pages-shared/package.json b/packages/pages-shared/package.json index 6c38872a1e..03f6c7f74f 100644 --- a/packages/pages-shared/package.json +++ b/packages/pages-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/pages-shared", - "version": "0.13.140", + "version": "0.13.141", "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 5b8c8ed3c5..91de82865e 100644 --- a/packages/vite-plugin-cloudflare/CHANGELOG.md +++ b/packages/vite-plugin-cloudflare/CHANGELOG.md @@ -1,5 +1,23 @@ # @cloudflare/vite-plugin +## 1.39.1 + +### Patch Changes + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Filter compatibility date fallback warning when no update is available + + The compatibility date warning from workerd (e.g., "The latest compatibility date supported by the installed Cloudflare Workers Runtime is...") is now only shown when a newer version of `@cloudflare/vite-plugin` is available. This matches the behavior in Wrangler and reduces noise when the user is already on the latest version. + + The update-check logic has been extracted to `@cloudflare/workers-utils` so it can be shared across packages. + +- [#14080](https://github.com/cloudflare/workers-sdk/pull/14080) [`ec70cf1`](https://github.com/cloudflare/workers-sdk/commit/ec70cf1e00db7185625d7542b9dc855b9fb28ab2) Thanks [@edmundhung](https://github.com/edmundhung)! - Fix `Tunnel closed` being logged when no tunnel was opened + + Previously, the Vite plugin printed `Tunnel closed` during cleanup even when tunnel startup had never begun. This message is now only shown after tunnel startup begins, including when the tunnel is still starting or has already expired. + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`408432a`](https://github.com/cloudflare/workers-sdk/commit/408432aed493563cb13b9a9c241806112ea606bc), [`1103c07`](https://github.com/cloudflare/workers-sdk/commit/1103c07646569208c4b0a623d123395643e022d5), [`7bb5c7a`](https://github.com/cloudflare/workers-sdk/commit/7bb5c7a78a22320283549a86a29a76146f7252a4), [`5b5cbd3`](https://github.com/cloudflare/workers-sdk/commit/5b5cbd3e98e5713ecf5ee0afa975a1f2ee38b2cc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280), [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`b64b7e4`](https://github.com/cloudflare/workers-sdk/commit/b64b7e4499b940efd74cdc09215620ee0b34a290), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e4c8fd9`](https://github.com/cloudflare/workers-sdk/commit/e4c8fd97a63230fccffe3d2c62185f5350fc5351), [`2dffeeb`](https://github.com/cloudflare/workers-sdk/commit/2dffeeb92d4f0b8a4c2c91f9cca7959d1970638a), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`4c0da7b`](https://github.com/cloudflare/workers-sdk/commit/4c0da7be0d47e6127066dc6edd8a59e536e7c24c), [`972d13d`](https://github.com/cloudflare/workers-sdk/commit/972d13d7054586bb9e3c11e888179d3df7753338), [`13cbadb`](https://github.com/cloudflare/workers-sdk/commit/13cbadbd7ecdd2b7c56b850df1209960a71f7d54), [`59e43e4`](https://github.com/cloudflare/workers-sdk/commit/59e43e4e066f9d201fc6c1e3b31cb232853e83d7)]: + - miniflare@4.20260529.0 + - wrangler@4.96.0 + ## 1.39.0 ### Minor Changes diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index 22b8af701a..8a7e49d0b5 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.0", + "version": "1.39.1", "description": "Cloudflare plugin for Vite", "keywords": [ "cloudflare", diff --git a/packages/vitest-pool-workers/CHANGELOG.md b/packages/vitest-pool-workers/CHANGELOG.md index d699d32163..d6a5307fc3 100644 --- a/packages/vitest-pool-workers/CHANGELOG.md +++ b/packages/vitest-pool-workers/CHANGELOG.md @@ -1,5 +1,17 @@ # @cloudflare/vitest-pool-workers +## 0.16.11 + +### Patch Changes + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Fix Durable Object RPC dispatch for constructors that return proxies + + Durable Object RPC methods mediated by a returned `Proxy` are now resolved through that proxy after validating prototype exposure. This allows wrappers that bind methods to the underlying instance to use private fields and methods in Vitest, while matching workerd's rejection of constructor-assigned RPC overrides. + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`408432a`](https://github.com/cloudflare/workers-sdk/commit/408432aed493563cb13b9a9c241806112ea606bc), [`1103c07`](https://github.com/cloudflare/workers-sdk/commit/1103c07646569208c4b0a623d123395643e022d5), [`7bb5c7a`](https://github.com/cloudflare/workers-sdk/commit/7bb5c7a78a22320283549a86a29a76146f7252a4), [`5b5cbd3`](https://github.com/cloudflare/workers-sdk/commit/5b5cbd3e98e5713ecf5ee0afa975a1f2ee38b2cc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280), [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`b64b7e4`](https://github.com/cloudflare/workers-sdk/commit/b64b7e4499b940efd74cdc09215620ee0b34a290), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e4c8fd9`](https://github.com/cloudflare/workers-sdk/commit/e4c8fd97a63230fccffe3d2c62185f5350fc5351), [`2dffeeb`](https://github.com/cloudflare/workers-sdk/commit/2dffeeb92d4f0b8a4c2c91f9cca7959d1970638a), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`4c0da7b`](https://github.com/cloudflare/workers-sdk/commit/4c0da7be0d47e6127066dc6edd8a59e536e7c24c), [`972d13d`](https://github.com/cloudflare/workers-sdk/commit/972d13d7054586bb9e3c11e888179d3df7753338), [`13cbadb`](https://github.com/cloudflare/workers-sdk/commit/13cbadbd7ecdd2b7c56b850df1209960a71f7d54), [`59e43e4`](https://github.com/cloudflare/workers-sdk/commit/59e43e4e066f9d201fc6c1e3b31cb232853e83d7)]: + - miniflare@4.20260529.0 + - wrangler@4.96.0 + ## 0.16.10 ### Patch Changes diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index c0c1ba2477..841d11180b 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.10", + "version": "0.16.11", "description": "Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime", "keywords": [ "cloudflare", diff --git a/packages/workers-utils/CHANGELOG.md b/packages/workers-utils/CHANGELOG.md index 6da6542d14..4952b3779b 100644 --- a/packages/workers-utils/CHANGELOG.md +++ b/packages/workers-utils/CHANGELOG.md @@ -1,5 +1,79 @@ # @cloudflare/workers-utils +## 0.22.0 + +### Minor Changes + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Add support for the new `web_search` binding kind. + + Cloudflare Web Search is a managed, zero-setup web discovery primitive for agents and Workers. Declare the binding as a single object in `wrangler.jsonc`: + + ```jsonc + { + "web_search": { "binding": "WEBSEARCH" } + } + ``` + + There is exactly one shared web corpus, so there is no namespace, instance, or other field to specify -- only the variable name. The binding exposes a single `search()` method that returns URLs and catalog metadata for a query. Web Search is discovery-only -- to read a result's content the caller invokes the global `fetch()` API against the result's `url`. + + The binding is **always remote** in local development: Miniflare proxies to the production Web Search service via the remote-bindings transport. Adds the `websearch.run` OAuth scope to `wrangler login`. + + Also adds a `wrangler websearch search` command for running ad-hoc queries from the CLI: + + ```sh + npx wrangler websearch search "cloudflare workers" + npx wrangler websearch search "cloudflare workers" --limit 5 + npx wrangler websearch search "cloudflare workers" --json + ``` + + `--limit` is optional (defaults to 10, capped at 20). `--json` prints the raw response; without it the results render as a pretty table. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Rename `pipeline` field to `stream` in pipeline bindings configuration + + The `pipeline` field inside `pipelines` bindings has been renamed to `stream` to align with the updated API wire format. The old `pipeline` field is still accepted but deprecated and will emit a warning. + + Before: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "pipeline": "my-stream-name" + } + ] + } + ``` + + After: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "stream": "my-stream-name" + } + ] + } + ``` + +### Patch Changes + +- [#14111](https://github.com/cloudflare/workers-sdk/pull/14111) [`599b27a`](https://github.com/cloudflare/workers-sdk/commit/599b27aafb9bc432524a35eb4e5a414de21bef41) Thanks [@nikitacano](https://github.com/nikitacano)! - Fix cloudflared SHA256 checksum mismatch on macOS + + The update service (`update.argotunnel.com`) returns a checksum for the extracted binary, not the `.tgz` tarball. We were computing the SHA256 of the tarball itself, which always mismatched on macOS where cloudflared is distributed as a compressed archive. + + This aligns with cloudflared's own auto-updater (`cmd/cloudflared/updater/workers_update.go`), which decompresses the tarball first, then checksums the resulting binary. We now do the same: extract, then verify. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Filter compatibility date fallback warning when no update is available + + The compatibility date warning from workerd (e.g., "The latest compatibility date supported by the installed Cloudflare Workers Runtime is...") is now only shown when a newer version of `@cloudflare/vite-plugin` is available. This matches the behavior in Wrangler and reduces noise when the user is already on the latest version. + + The update-check logic has been extracted to `@cloudflare/workers-utils` so it can be shared across packages. + ## 0.21.1 ### Patch Changes diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index 4128a1e5aa..3c83df5ca1 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-utils", - "version": "0.21.1", + "version": "0.22.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/workflows-shared/CHANGELOG.md b/packages/workflows-shared/CHANGELOG.md index bd8e619961..4068a174fe 100644 --- a/packages/workflows-shared/CHANGELOG.md +++ b/packages/workflows-shared/CHANGELOG.md @@ -1,5 +1,23 @@ # @cloudflare/workflows-shared +## 0.11.1 + +### Patch Changes + +- [#14134](https://github.com/cloudflare/workers-sdk/pull/14134) [`262dfc2`](https://github.com/cloudflare/workers-sdk/commit/262dfc2b32531165f94ba87c70ce75fcb1490b61) Thanks [@matingathani](https://github.com/matingathani)! - Fix `wrangler dev` Workflows crashing with `SQLITE_TOOBIG` when a step returns a large `Uint8Array` + + `JSON.stringify` encodes each byte of a `Uint8Array` as a separate numeric key + (`{"0":1,"1":2,...}`), producing a string ~10× larger than the array's byte + length. A 200 KB `Uint8Array` became a ~2 MB JSON string that exceeded SQLite's + blob limit, crashing the Workflow step. The same bytes returned as an + `ArrayBuffer` succeeded because `JSON.stringify(ArrayBuffer)` → `{}`. + + The step log metadata (used by the local Workflows explorer) now stores a + human-readable description for `TypedArray` and `ArrayBuffer` outputs + (`[Uint8Array(200000 bytes)]`) instead of attempting to JSON-serialise the raw + bytes. The actual step value is unaffected — it is preserved in Durable Object + key-value storage via structured clone for replay by subsequent steps. + ## 0.11.0 ### Minor Changes diff --git a/packages/workflows-shared/package.json b/packages/workflows-shared/package.json index 0df7e3c87c..f2fd302dbb 100644 --- a/packages/workflows-shared/package.json +++ b/packages/workflows-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workflows-shared", - "version": "0.11.0", + "version": "0.11.1", "private": true, "description": "Package that is used at Cloudflare to power some internal features of Cloudflare Workflows.", "keywords": [ diff --git a/packages/wrangler-bundler/CHANGELOG.md b/packages/wrangler-bundler/CHANGELOG.md new file mode 100644 index 0000000000..37d78ae72f --- /dev/null +++ b/packages/wrangler-bundler/CHANGELOG.md @@ -0,0 +1,12 @@ +# @cloudflare/wrangler-bundler + +## 0.1.0 + +### Minor Changes + +- [#13892](https://github.com/cloudflare/workers-sdk/pull/13892) [`13cbadb`](https://github.com/cloudflare/workers-sdk/commit/13cbadbd7ecdd2b7c56b850df1209960a71f7d54) Thanks [@penalosa](https://github.com/penalosa)! - Add `@cloudflare/wrangler-bundler` — an experimental internal package. Not intended for external use. + +### Patch Changes + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`408432a`](https://github.com/cloudflare/workers-sdk/commit/408432aed493563cb13b9a9c241806112ea606bc), [`1103c07`](https://github.com/cloudflare/workers-sdk/commit/1103c07646569208c4b0a623d123395643e022d5), [`5b5cbd3`](https://github.com/cloudflare/workers-sdk/commit/5b5cbd3e98e5713ecf5ee0afa975a1f2ee38b2cc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280), [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`b64b7e4`](https://github.com/cloudflare/workers-sdk/commit/b64b7e4499b940efd74cdc09215620ee0b34a290), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e4c8fd9`](https://github.com/cloudflare/workers-sdk/commit/e4c8fd97a63230fccffe3d2c62185f5350fc5351), [`2dffeeb`](https://github.com/cloudflare/workers-sdk/commit/2dffeeb92d4f0b8a4c2c91f9cca7959d1970638a), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`4c0da7b`](https://github.com/cloudflare/workers-sdk/commit/4c0da7be0d47e6127066dc6edd8a59e536e7c24c), [`13cbadb`](https://github.com/cloudflare/workers-sdk/commit/13cbadbd7ecdd2b7c56b850df1209960a71f7d54), [`59e43e4`](https://github.com/cloudflare/workers-sdk/commit/59e43e4e066f9d201fc6c1e3b31cb232853e83d7)]: + - wrangler@4.96.0 diff --git a/packages/wrangler-bundler/package.json b/packages/wrangler-bundler/package.json index b8b00bffec..7f6cc6d7e2 100644 --- a/packages/wrangler-bundler/package.json +++ b/packages/wrangler-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/wrangler-bundler", - "version": "0.0.0", + "version": "0.1.0", "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 553e3351c9..f92e0ba116 100644 --- a/packages/wrangler/CHANGELOG.md +++ b/packages/wrangler/CHANGELOG.md @@ -1,5 +1,207 @@ # wrangler +## 4.96.0 + +### Minor Changes + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Add support for the new `web_search` binding kind. + + Cloudflare Web Search is a managed, zero-setup web discovery primitive for agents and Workers. Declare the binding as a single object in `wrangler.jsonc`: + + ```jsonc + { + "web_search": { "binding": "WEBSEARCH" } + } + ``` + + There is exactly one shared web corpus, so there is no namespace, instance, or other field to specify -- only the variable name. The binding exposes a single `search()` method that returns URLs and catalog metadata for a query. Web Search is discovery-only -- to read a result's content the caller invokes the global `fetch()` API against the result's `url`. + + The binding is **always remote** in local development: Miniflare proxies to the production Web Search service via the remote-bindings transport. Adds the `websearch.run` OAuth scope to `wrangler login`. + + Also adds a `wrangler websearch search` command for running ad-hoc queries from the CLI: + + ```sh + npx wrangler websearch search "cloudflare workers" + npx wrangler websearch search "cloudflare workers" --limit 5 + npx wrangler websearch search "cloudflare workers" --json + ``` + + `--limit` is optional (defaults to 10, capped at 20). `--json` prints the raw response; without it the results render as a pretty table. + +- [#13610](https://github.com/cloudflare/workers-sdk/pull/13610) [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Add support for `agent_memory` bindings + + Agent Memory bindings allow Workers to connect to Cloudflare's Agent Memory service for storing and retrieving agent conversation state. This binding is remote-only, meaning it always connects to the Cloudflare API during `wrangler dev` rather than using a local simulation. + + To configure an `agent_memory` binding, add the following to your `wrangler.json`: + + ```jsonc + { + "agent_memory": [ + { + "binding": "MY_MEMORY", + "namespace": "my-namespace" + } + ] + } + ``` + + Wrangler will automatically provision the namespace during deployment if it does not already exist. Type generation via `wrangler types` is also supported. + + This change also adds the `agent-memory:write` OAuth scope to Wrangler's default login scopes, so `wrangler login` can request the permissions needed to provision and manage Agent Memory namespaces. + +- [#13610](https://github.com/cloudflare/workers-sdk/pull/13610) [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Add `wrangler agent-memory namespace` commands + + The following commands have been added for managing Agent Memory namespaces: + + ```bash + wrangler agent-memory namespace create + wrangler agent-memory namespace list [--json] + wrangler agent-memory namespace get [--json] + wrangler agent-memory namespace delete [--force] + ``` + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Add confirmation prompt to `wrangler containers images delete` + + Previously, running `wrangler containers images delete IMAGE:TAG` would delete the image immediately with no confirmation. The command now prompts for confirmation before deleting. Use `-y` or `--skip-confirmation` to bypass the prompt in non-interactive or scripted environments. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Rename `pipeline` field to `stream` in pipeline bindings configuration + + The `pipeline` field inside `pipelines` bindings has been renamed to `stream` to align with the updated API wire format. The old `pipeline` field is still accepted but deprecated and will emit a warning. + + Before: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "pipeline": "my-stream-name" + } + ] + } + ``` + + After: + + ```jsonc + // wrangler.json + { + "pipelines": [ + { + "binding": "MY_PIPELINE", + "stream": "my-stream-name" + } + ] + } + ``` + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Allow pipeline, stream, and sink commands to resolve resources by name with pagination-aware lookups. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Support deleting secrets via `wrangler secret bulk` + + You can now delete secrets in bulk by setting their value to `null` in the JSON input file: + + ```json + { "SECRET_TO_DELETE": null, "SECRET_TO_UPDATE": "new-value" } + ``` + +- [#14091](https://github.com/cloudflare/workers-sdk/pull/14091) [`4c0da7b`](https://github.com/cloudflare/workers-sdk/commit/4c0da7be0d47e6127066dc6edd8a59e536e7c24c) Thanks [@gpanders](https://github.com/gpanders)! - Add ProxyCommand support for `wrangler containers ssh` + + `wrangler containers ssh` now automatically switches to a stdio proxy when invoked by OpenSSH's `ProxyCommand`, and `--stdio` can force this mode. This lets users connect with `ssh ` when their SSH config uses Wrangler as the proxy command. + +- [#13892](https://github.com/cloudflare/workers-sdk/pull/13892) [`13cbadb`](https://github.com/cloudflare/workers-sdk/commit/13cbadbd7ecdd2b7c56b850df1209960a71f7d54) Thanks [@penalosa](https://github.com/penalosa)! - Remove the deprecated `experimental.testMode` option from `unstable_dev` + + `experimental.testMode` previously only affected the default `logLevel` (`warn` when `testMode: true`, `log` otherwise) and has been flagged for removal in its type-definition comment since it landed. It is now removed, and `unstable_dev`'s default log level matches `wrangler dev`'s (`log`). + + Callers that explicitly passed `testMode: true` to get quieter logs should now set `logLevel: "warn"` directly. + +### Patch Changes + +- [#14016](https://github.com/cloudflare/workers-sdk/pull/14016) [`408432a`](https://github.com/cloudflare/workers-sdk/commit/408432aed493563cb13b9a9c241806112ea606bc) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - report all failing triggers from a single deploy + + `wrangler deploy` deploys several kinds of trigger in parallel (routes, custom domains, schedules, queue producers/consumers, workflows). Previously, if one of those API calls failed, the first rejection short-circuited the rest, no other deployments were reported, and (in the case of custom-domain confirmation conflicts) some failures were silently logged to stdout without the deploy actually failing. + + `wrangler deploy` now waits for every trigger deployment to settle, prints every successfully-deployed target (so you still see what landed), and then throws a single error listing every trigger that failed. + + Note that this also turns the previously-silent "user declined to override a conflicting Custom Domain" case into a hard failure of `wrangler deploy`, which matches what was always implied by the message ("Publishing to Custom Domain ... was skipped, fix conflict and try again"). + +- [#14125](https://github.com/cloudflare/workers-sdk/pull/14125) [`1103c07`](https://github.com/cloudflare/workers-sdk/commit/1103c07646569208c4b0a623d123395643e022d5) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Bump `rosie-skills` from `0.7.6` to `0.8.1` and bundle it into the Wrangler output + + The new version of `rosie-skills` is a [pure-TypeScript rewrite](https://github.com/withastro/rosie/pull/21) that removes the previously necessary ~600kb WASM binary. The package now ships only JavaScript with one minimal dependencies (`modern-tar`). + + Additionally, `rosie-skills` is now bundled directly into Wrangler's distributable rather than kept as an external runtime dependency. This eliminates the supply chain concern raised in [#14110](https://github.com/cloudflare/workers-sdk/issues/14110): there is no separate package to resolve at install time, since all code is inlined into Wrangler's build output. + +- [#14135](https://github.com/cloudflare/workers-sdk/pull/14135) [`5b5cbd3`](https://github.com/cloudflare/workers-sdk/commit/5b5cbd3e98e5713ecf5ee0afa975a1f2ee38b2cc) Thanks [@Refaerds](https://github.com/Refaerds)! - Update the generated type for browser bindings to `BrowserRun` + + When running `wrangler types`, browser bindings were previously typed as the generic `Fetcher`. They now generate the more specific and accurate `BrowserRun` type. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Bump `rosie-skills` package from 0.6.3 to 0.7.6 + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260526.1 | 1.20260527.1 | + +- [#14076](https://github.com/cloudflare/workers-sdk/pull/14076) [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260527.1 | 1.20260528.1 | + +- [#14100](https://github.com/cloudflare/workers-sdk/pull/14100) [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260528.1 | 1.20260529.1 | + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Disable Sentry error reporting by default + + `WRANGLER_SEND_ERROR_REPORTS` now defaults to `false` instead of prompting on every error. The current prompt produces too many false-positive reports. Users can still opt in explicitly by setting `WRANGLER_SEND_ERROR_REPORTS=true`. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Fix `wrangler setup` failing for Vite projects without a config file + + `wrangler setup` (and `wrangler deploy --experimental-autoconfig`) crashed with "Could not find Vite config file to modify" for Vite projects that don't have a `vite.config.js` or `vite.config.ts`. This affected 6 of the 16 `create-vite` templates: `vanilla`, `vanilla-ts`, `react-swc`, `react-swc-ts`, `lit`, and `lit-ts`. + + Autoconfig now creates a minimal Vite config with the Cloudflare plugin when no config file exists, instead of failing. The file extension (`.ts` or `.js`) is chosen based on whether the project has a `tsconfig.json`. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - Show helpful message with URL when browser cannot be opened in headless/container environments + + Previously, running `wrangler login` (or any command that opens a browser) in headless Linux environments without `xdg-open` installed would crash with a confusing "A file or directory could not be found — Missing file or directory: xdg-open" error. + + Now wrangler catches the error and prints a clear warning with the URL so users can copy-paste it into a browser manually. + +- [#14087](https://github.com/cloudflare/workers-sdk/pull/14087) [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee) Thanks [@edmundhung](https://github.com/edmundhung)! - `wrangler secrets-store secret create` and `secret update` now reject secret values larger than 64 KiB (65,536 bytes) with a clear error before calling the Cloudflare API. Previously the CLI accepted them, the secret appeared in `secret list`, and the failure surfaced later (and confusingly) at worker deploy time as a "secret doesn't exist" error against the binding. 64 KiB is the cap enforced by the API; the CLI now enforces it at the same boundary. + +- [#14059](https://github.com/cloudflare/workers-sdk/pull/14059) [`b64b7e4`](https://github.com/cloudflare/workers-sdk/commit/b64b7e4499b940efd74cdc09215620ee0b34a290) Thanks [@matingathani](https://github.com/matingathani)! - Fix `wrangler kv bulk get` printing "Success!" to stdout, which corrupted JSON output when piped to tools like `jq` + +- [#14002](https://github.com/cloudflare/workers-sdk/pull/14002) [`e4c8fd9`](https://github.com/cloudflare/workers-sdk/commit/e4c8fd97a63230fccffe3d2c62185f5350fc5351) Thanks [@danyalahmed1995](https://github.com/danyalahmed1995)! - Show a clear error for invalid API token header characters + + Wrangler now detects API tokens containing characters that cannot be sent in the HTTP Authorization header before making an API request. This avoids a low-level ByteString conversion error and helps users recreate or recopy the token without printing the token value. + +- [#14132](https://github.com/cloudflare/workers-sdk/pull/14132) [`2dffeeb`](https://github.com/cloudflare/workers-sdk/commit/2dffeeb92d4f0b8a4c2c91f9cca7959d1970638a) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Adapt React Router autoconfig based on `v8_middleware` future flag + + The React Router autoconfig (`wrangler setup`) now detects whether `v8_middleware: true` is set in the user's `react-router.config.ts`. When it is, the generated `workers/app.ts` uses a simplified fetch handler without `AppLoadContext` module augmentation, and the generated `app/entry.server.tsx` omits the `_loadContext` parameter. When `v8_middleware` is not set, the existing `AppLoadContext` pattern with `env`/`ctx` params is preserved. + + This avoids breaking projects that use the `v8_middleware` future flag (which changes the context API from `AppLoadContext` to `RouterContextProvider`), while keeping the traditional pattern for projects that haven't opted in. + +- [#14133](https://github.com/cloudflare/workers-sdk/pull/14133) [`59e43e4`](https://github.com/cloudflare/workers-sdk/commit/59e43e4e066f9d201fc6c1e3b31cb232853e83d7) Thanks [@matingathani](https://github.com/matingathani)! - Fix `wrangler whoami` printing a trailing period after the api-tokens URL + + The message `To see token permissions visit https://...api-tokens.` ended with + a period that became part of the URL when clicked in terminals or GitHub Actions + output, causing a 404. The period is removed and a comma added before "visit" + so the sentence reads naturally without a trailing period on the URL. + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`cbb39bd`](https://github.com/cloudflare/workers-sdk/commit/cbb39bdc90d4b93f9a9b4355124570d838eb1a2d), [`7bb5c7a`](https://github.com/cloudflare/workers-sdk/commit/7bb5c7a78a22320283549a86a29a76146f7252a4), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`97d7d81`](https://github.com/cloudflare/workers-sdk/commit/97d7d81e0a757e30e7700b183133249e2136a280), [`c647ccc`](https://github.com/cloudflare/workers-sdk/commit/c647ccc7873c2cada60ba5f4ce7c8dfeb4801acc), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`972d13d`](https://github.com/cloudflare/workers-sdk/commit/972d13d7054586bb9e3c11e888179d3df7753338)]: + - miniflare@4.20260529.0 + ## 4.95.0 ### Minor Changes diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 53f496db87..11a35e8460 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -1,6 +1,6 @@ { "name": "wrangler", - "version": "4.95.0", + "version": "4.96.0", "description": "Command-line interface for all things Cloudflare Workers", "keywords": [ "assembly", From 4ef790b3ee22389db29c64f49564aac28022e40e Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 15:53:28 +0100 Subject: [PATCH 02/18] Use `127.0.0.1` instead of `localhost` for the runtime inspector address (#14086) --- .changeset/fix-inspector-localhost.md | 9 +++++++++ packages/miniflare/src/index.ts | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-inspector-localhost.md diff --git a/.changeset/fix-inspector-localhost.md b/.changeset/fix-inspector-localhost.md new file mode 100644 index 0000000000..8782f3ae61 --- /dev/null +++ b/.changeset/fix-inspector-localhost.md @@ -0,0 +1,9 @@ +--- +"miniflare": patch +--- + +Use `127.0.0.1` instead of `localhost` for the runtime inspector address + +On systems where `getaddrinfo("localhost")` returns `::1` but IPv6 is disabled at the kernel level, `workerd` fails to bind the inspector socket and silently continues without emitting the `listen-inspector` event to the control FD. This caused `wrangler dev` to hang indefinitely at "Starting local server..." with no error output. + +Using `127.0.0.1` explicitly is consistent with `DEFAULT_HOST`, `--debug-port`, and `resolveLocalhost()` already in the codebase. diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index a59dc72719..391c4f135c 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2429,7 +2429,13 @@ export class Miniflare { runtimeInspectorAddress = this.#getSocketAddress( kInspectorSocket, this.#previousRuntimeInspectorPort, - "localhost", + // Use "127.0.0.1" explicitly instead of "localhost" to avoid + // DNS resolution issues. On systems where getaddrinfo("localhost") + // returns ::1 but IPv6 is disabled, workerd fails to bind the + // inspector socket and silently continues without emitting the + // listen-inspector event, causing waitForPorts() to hang. + // See https://github.com/cloudflare/workers-sdk/issues/14077 + "127.0.0.1", runtimeInspectorPort ); this.#previousRuntimeInspectorPort = runtimeInspectorPort; From e86489a5743ff9bad7bcb5b444ad3d952d5b0164 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 15:55:17 +0100 Subject: [PATCH 03/18] Fix json serialization but in `mapWorkerMetadataBindings` and add missing unit tests (#14084) --- .changeset/fix-json-binding-mapping.md | 7 + ...ix-wrangler-json-binding-init-from-dash.md | 12 + .../src/map-worker-metadata-bindings.ts | 3 +- .../tests/construct-wrangler-config.test.ts | 517 +++++++++++++ .../map-worker-metadata-bindings.test.ts | 726 ++++++++++++++++++ 5 files changed, 1263 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-json-binding-mapping.md create mode 100644 .changeset/fix-wrangler-json-binding-init-from-dash.md create mode 100644 packages/workers-utils/tests/construct-wrangler-config.test.ts create mode 100644 packages/workers-utils/tests/map-worker-metadata-bindings.test.ts diff --git a/.changeset/fix-json-binding-mapping.md b/.changeset/fix-json-binding-mapping.md new file mode 100644 index 0000000000..2b49c4b937 --- /dev/null +++ b/.changeset/fix-json-binding-mapping.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-utils": patch +--- + +Correctly map JSON bindings in `mapWorkerMetadataBindings` + +The `json` binding case used literal keys `name` and `json` instead of a computed property key `[binding.name]: binding.json`. This caused JSON bindings to always produce `{ name: "", json: }` instead of `{ : }`, clobbering any existing vars with those keys. This is now consistent with how `plain_text` bindings are mapped. diff --git a/.changeset/fix-wrangler-json-binding-init-from-dash.md b/.changeset/fix-wrangler-json-binding-init-from-dash.md new file mode 100644 index 0000000000..5b9c8037d9 --- /dev/null +++ b/.changeset/fix-wrangler-json-binding-init-from-dash.md @@ -0,0 +1,12 @@ +--- +"wrangler": patch +--- + +Fix JSON variable bindings in `wrangler init --from-dash` and remote config diff + +When fetching a remote Worker's configuration, JSON variable bindings (e.g. `{"my_value": 5}`) were incorrectly serialized as `{ "name": "MY_JSON", "json": {"my_value": 5} }` instead of `{ "MY_JSON": {"my_value": 5} }`. This affected two areas: + +- `wrangler init --from-dash` would generate a `wrangler.json` with broken `vars` entries +- Remote config diff checks would always report JSON bindings as changed, since the malformed remote representation could never match the local config + +Both issues are now fixed and remote JSON bindings are now correctly mapped. diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index db23b0543c..8f52db82bd 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -34,8 +34,7 @@ export function mapWorkerMetadataBindings( { configObj.vars = { ...(configObj.vars ?? {}), - name: binding.name, - json: binding.json, + [binding.name]: binding.json, }; } break; diff --git a/packages/workers-utils/tests/construct-wrangler-config.test.ts b/packages/workers-utils/tests/construct-wrangler-config.test.ts new file mode 100644 index 0000000000..0c696da6bc --- /dev/null +++ b/packages/workers-utils/tests/construct-wrangler-config.test.ts @@ -0,0 +1,517 @@ +import { describe, it } from "vitest"; +import { ENVIRONMENT_TAG_PREFIX, SERVICE_TAG_PREFIX } from "../src/constants"; +import { constructWranglerConfig } from "../src/construct-wrangler-config"; + +type APIWorkerConfigOrArray = Parameters[0]; +type APIWorkerConfig = Exclude; + +/** + * Factory for a minimal valid APIWorkerConfig. + * Tests override only the fields they care about. + * + * @param overrides - Partial fields to merge into the default config + * @returns A complete APIWorkerConfig with sensible defaults + */ +function makeWorkerConfig( + overrides: Partial = {} +): APIWorkerConfig { + return { + name: "my-worker", + entrypoint: "index.js", + tags: [], + compatibility_date: "2025-01-01", + compatibility_flags: [], + logpush: undefined, + routes: [], + tail_consumers: undefined, + domains: [], + schedules: [], + bindings: [], + observability: undefined, + limits: undefined, + placement: undefined, + subdomain: { enabled: true, previews_enabled: false }, + ...overrides, + }; +} + +describe("constructWranglerConfig", () => { + describe("input normalization", () => { + it("accepts a single worker (non-array)", ({ expect }) => { + const config = makeWorkerConfig(); + const result = constructWranglerConfig(config); + expect(result.name).toBe("my-worker"); + expect(result.main).toBe("index.js"); + }); + + it("accepts an array with one worker", ({ expect }) => { + const config = makeWorkerConfig(); + const result = constructWranglerConfig([config]); + expect(result.name).toBe("my-worker"); + expect(result.main).toBe("index.js"); + }); + }); + + describe("basic field mapping", () => { + it("maps core fields correctly", ({ expect }) => { + const config = makeWorkerConfig({ + name: "test-worker", + entrypoint: "src/main.ts", + compatibility_date: "2025-03-15", + compatibility_flags: ["nodejs_compat", "url_standard"], + subdomain: { enabled: true, previews_enabled: true }, + }); + const result = constructWranglerConfig(config); + + expect(result.name).toBe("test-worker"); + expect(result.main).toBe("src/main.ts"); + expect(result.workers_dev).toBe(true); + expect(result.preview_urls).toBe(true); + expect(result.compatibility_date).toBe("2025-03-15"); + expect(result.compatibility_flags).toEqual([ + "nodejs_compat", + "url_standard", + ]); + }); + + it("maps workers_dev and preview_urls from subdomain", ({ expect }) => { + const config = makeWorkerConfig({ + subdomain: { enabled: false, previews_enabled: true }, + }); + const result = constructWranglerConfig(config); + + expect(result.workers_dev).toBe(false); + expect(result.preview_urls).toBe(true); + }); + }); + + describe("routes", () => { + it("merges API routes and custom domains into a unified routes array", ({ + expect, + }) => { + const config = makeWorkerConfig({ + routes: [ + { + id: "route-1", + pattern: "example.com/*", + zone_name: "example.com", + script: "my-worker", + }, + ], + domains: [ + { + id: "domain-1", + hostname: "api.example.com", + zone_name: "example.com", + service: "my-worker", + environment: "production", + zone_id: "zone-123", + }, + ], + }); + const result = constructWranglerConfig(config); + + expect(result.routes).toEqual([ + { pattern: "example.com/*", zone_name: "example.com" }, + { + pattern: "api.example.com", + zone_name: "example.com", + custom_domain: true, + enabled: undefined, + previews_enabled: undefined, + }, + ]); + }); + + it("omits routes key when both routes and domains are empty", ({ + expect, + }) => { + const config = makeWorkerConfig({ routes: [], domains: [] }); + const result = constructWranglerConfig(config); + expect(result.routes).toBeUndefined(); + }); + }); + + describe("placement", () => { + it("sets placement to { mode: 'smart' } when mode is smart", ({ + expect, + }) => { + const config = makeWorkerConfig({ + placement: { mode: "smart" }, + }); + const result = constructWranglerConfig(config); + expect(result.placement).toEqual({ mode: "smart" }); + }); + + it("sets placement to undefined when mode is not smart", ({ expect }) => { + const config = makeWorkerConfig({ + placement: { mode: "off" } as unknown as APIWorkerConfig["placement"], + }); + const result = constructWranglerConfig(config); + expect(result.placement).toBeUndefined(); + }); + + it("sets placement to undefined when placement is undefined", ({ + expect, + }) => { + const config = makeWorkerConfig({ placement: undefined }); + const result = constructWranglerConfig(config); + expect(result.placement).toBeUndefined(); + }); + }); + + describe("durable object migrations", () => { + it("generates migrations when DO bindings match worker name and migration_tag is set", ({ + expect, + }) => { + const config = makeWorkerConfig({ + name: "my-worker", + migration_tag: "v1", + bindings: [ + { + type: "durable_object_namespace", + name: "MY_DO", + class_name: "MyDurableObject", + script_name: "my-worker", + }, + { + type: "durable_object_namespace", + name: "OTHER_DO", + class_name: "OtherDO", + script_name: "my-worker", + }, + ], + }); + const result = constructWranglerConfig(config); + + expect(result.migrations).toEqual([ + { + tag: "v1", + new_classes: ["MyDurableObject", "OtherDO"], + }, + ]); + }); + + it("does not generate migrations when DO bindings have different script_name", ({ + expect, + }) => { + const config = makeWorkerConfig({ + name: "my-worker", + migration_tag: "v1", + bindings: [ + { + type: "durable_object_namespace", + name: "EXTERNAL_DO", + class_name: "ExternalDO", + script_name: "other-worker", + }, + ], + }); + const result = constructWranglerConfig(config); + expect(result.migrations).toBeUndefined(); + }); + + it("does not generate migrations when migration_tag is missing", ({ + expect, + }) => { + const config = makeWorkerConfig({ + name: "my-worker", + bindings: [ + { + type: "durable_object_namespace", + name: "MY_DO", + class_name: "MyDO", + script_name: "my-worker", + }, + ], + }); + const result = constructWranglerConfig(config); + expect(result.migrations).toBeUndefined(); + }); + }); + + describe("scheduled triggers", () => { + it("generates triggers.crons from schedules", ({ expect }) => { + const config = makeWorkerConfig({ + schedules: [{ cron: "*/5 * * * *" }, { cron: "0 0 * * *" }], + }); + const result = constructWranglerConfig(config); + + expect(result.triggers).toEqual({ + crons: ["*/5 * * * *", "0 0 * * *"], + }); + }); + + it("omits triggers when schedules is empty", ({ expect }) => { + const config = makeWorkerConfig({ schedules: [] }); + const result = constructWranglerConfig(config); + expect(result.triggers).toBeUndefined(); + }); + }); + + describe("tail_consumers", () => { + it("converts null tail_consumers to undefined (PR #11286 fix)", ({ + expect, + }) => { + const config = makeWorkerConfig({ tail_consumers: null }); + const result = constructWranglerConfig(config); + expect(result.tail_consumers).toBeUndefined(); + }); + + it("passes through non-null tail_consumers", ({ expect }) => { + const consumers = [ + { service: "logger" }, + { service: "metrics", environment: "production" }, + ]; + const config = makeWorkerConfig({ tail_consumers: consumers }); + const result = constructWranglerConfig(config); + expect(result.tail_consumers).toEqual(consumers); + }); + + it("passes through undefined tail_consumers as undefined", ({ expect }) => { + const config = makeWorkerConfig({ tail_consumers: undefined }); + const result = constructWranglerConfig(config); + expect(result.tail_consumers).toBeUndefined(); + }); + }); + + describe("observability", () => { + it("passes through observability config", ({ expect }) => { + const observability = { enabled: true, head_sampling_rate: 0.5 }; + const config = makeWorkerConfig({ observability }); + const result = constructWranglerConfig(config); + expect(result.observability).toEqual(observability); + }); + + it("passes through undefined observability", ({ expect }) => { + const config = makeWorkerConfig({ observability: undefined }); + const result = constructWranglerConfig(config); + expect(result.observability).toBeUndefined(); + }); + }); + + describe("limits", () => { + it("passes through limits config", ({ expect }) => { + const limits = { cpu_ms: 50, subrequests: 100 }; + const config = makeWorkerConfig({ limits }); + const result = constructWranglerConfig(config); + expect(result.limits).toEqual(limits); + }); + + it("passes through undefined limits", ({ expect }) => { + const config = makeWorkerConfig({ limits: undefined }); + const result = constructWranglerConfig(config); + expect(result.limits).toBeUndefined(); + }); + }); + + describe("bindings integration", () => { + it("maps key binding types through to the config", ({ expect }) => { + const config = makeWorkerConfig({ + bindings: [ + { type: "plain_text", name: "MY_VAR", text: "hello" }, + { + type: "kv_namespace", + name: "MY_KV", + namespace_id: "kv-123", + }, + { + type: "r2_bucket", + name: "MY_BUCKET", + bucket_name: "my-bucket", + }, + { type: "d1", name: "MY_DB", id: "db-123" }, + { + type: "service", + name: "MY_SERVICE", + service: "other-worker", + }, + { + type: "queue", + name: "MY_QUEUE", + queue_name: "my-queue", + }, + ], + }); + const result = constructWranglerConfig(config); + + expect(result.vars).toEqual({ MY_VAR: "hello" }); + expect(result.kv_namespaces).toEqual([ + { id: "kv-123", binding: "MY_KV" }, + ]); + expect(result.r2_buckets).toEqual([ + { + binding: "MY_BUCKET", + bucket_name: "my-bucket", + jurisdiction: undefined, + }, + ]); + expect(result.d1_databases).toEqual([ + { binding: "MY_DB", database_id: "db-123" }, + ]); + expect(result.services).toEqual([ + { + binding: "MY_SERVICE", + service: "other-worker", + environment: undefined, + entrypoint: undefined, + }, + ]); + expect(result.queues).toEqual({ + producers: [ + { + binding: "MY_QUEUE", + queue: "my-queue", + delivery_delay: undefined, + }, + ], + }); + }); + + it("handles assets binding without throwing (PR #11339 fix)", ({ + expect, + }) => { + const config = makeWorkerConfig({ + bindings: [{ type: "assets", name: "ASSETS" }], + }); + const result = constructWranglerConfig(config); + expect(result.assets).toEqual({ binding: "ASSETS" }); + }); + + it("filters out secret_text bindings", ({ expect }) => { + const config = makeWorkerConfig({ + bindings: [ + { type: "secret_text", name: "MY_SECRET", text: "s3cret" }, + { type: "plain_text", name: "MY_VAR", text: "hello" }, + ], + }); + const result = constructWranglerConfig(config); + expect(result.vars).toEqual({ MY_VAR: "hello" }); + // secret_text should not appear anywhere in the output + expect(JSON.stringify(result)).not.toContain("s3cret"); + }); + }); + + describe("multi-environment support", () => { + it("uses top-level worker as base config and tagged workers as environments", ({ + expect, + }) => { + const topLevel = makeWorkerConfig({ + name: "my-worker", + entrypoint: "index.js", + tags: [], + compatibility_date: "2025-01-01", + }); + const staging = makeWorkerConfig({ + name: "my-worker-staging", + entrypoint: "index.js", + tags: [ + `${SERVICE_TAG_PREFIX}my-worker`, + `${ENVIRONMENT_TAG_PREFIX}staging`, + ], + compatibility_date: "2025-02-01", + }); + const production = makeWorkerConfig({ + name: "my-worker-production", + entrypoint: "index.js", + tags: [ + `${SERVICE_TAG_PREFIX}my-worker`, + `${ENVIRONMENT_TAG_PREFIX}production`, + ], + compatibility_date: "2025-03-01", + }); + + const result = constructWranglerConfig([topLevel, staging, production]); + + expect(result.name).toBe("my-worker"); + expect(result.compatibility_date).toBe("2025-01-01"); + expect(result.env).toBeDefined(); + expect(result.env?.staging).toBeDefined(); + expect(result.env?.staging?.compatibility_date).toBe("2025-02-01"); + expect(result.env?.production).toBeDefined(); + expect(result.env?.production?.compatibility_date).toBe("2025-03-01"); + }); + + it("creates synthetic top-level when no untagged worker exists", ({ + expect, + }) => { + const staging = makeWorkerConfig({ + name: "my-worker-staging", + entrypoint: "src/index.ts", + tags: [ + `${ENVIRONMENT_TAG_PREFIX}staging`, + `${SERVICE_TAG_PREFIX}my-worker-staging`, + ], + }); + const production = makeWorkerConfig({ + name: "my-worker-prod", + entrypoint: "src/index.ts", + tags: [ + `${ENVIRONMENT_TAG_PREFIX}production`, + `${SERVICE_TAG_PREFIX}my-worker-staging`, + ], + }); + + const result = constructWranglerConfig([staging, production]); + + // Synthetic top-level should use workers[0] for name and entrypoint + expect(result.name).toBe("my-worker-staging"); + expect(result.main).toBe("src/index.ts"); + // Should not have compatibility_date etc. since it's synthetic + expect(result.compatibility_date).toBeUndefined(); + }); + + it("skips workers with mismatched service tags", ({ expect }) => { + const topLevel = makeWorkerConfig({ + name: "my-worker", + tags: [], + }); + const matchingEnv = makeWorkerConfig({ + name: "my-worker-staging", + tags: [ + `${SERVICE_TAG_PREFIX}my-worker`, + `${ENVIRONMENT_TAG_PREFIX}staging`, + ], + }); + const mismatchedEnv = makeWorkerConfig({ + name: "other-worker-prod", + tags: [ + `${SERVICE_TAG_PREFIX}other-worker`, + `${ENVIRONMENT_TAG_PREFIX}production`, + ], + }); + + const result = constructWranglerConfig([ + topLevel, + matchingEnv, + mismatchedEnv, + ]); + + expect(result.env?.staging).toBeDefined(); + expect(result.env?.production).toBeUndefined(); + }); + + it("skips workers without an environment tag", ({ expect }) => { + const topLevel = makeWorkerConfig({ + name: "my-worker", + tags: [], + }); + const noEnvTag = makeWorkerConfig({ + name: "my-worker-noenv", + tags: [`${SERVICE_TAG_PREFIX}my-worker`], + }); + + const result = constructWranglerConfig([topLevel, noEnvTag]); + expect(result.env).toBeUndefined(); + }); + + it("handles null tags by treating worker as top-level", ({ expect }) => { + const config = makeWorkerConfig({ tags: null }); + const result = constructWranglerConfig(config); + // Worker with null tags is treated as having no tags, + // so it becomes the top-level environment + expect(result.name).toBe("my-worker"); + expect(result.main).toBe("index.js"); + }); + }); +}); diff --git a/packages/workers-utils/tests/map-worker-metadata-bindings.test.ts b/packages/workers-utils/tests/map-worker-metadata-bindings.test.ts new file mode 100644 index 0000000000..2899cc8129 --- /dev/null +++ b/packages/workers-utils/tests/map-worker-metadata-bindings.test.ts @@ -0,0 +1,726 @@ +import { describe, it } from "vitest"; +import { mapWorkerMetadataBindings } from "../src/map-worker-metadata-bindings"; +import type { WorkerMetadataBinding } from "../src/types"; + +describe("mapWorkerMetadataBindings", () => { + it("returns an empty object for an empty bindings array", ({ expect }) => { + const result = mapWorkerMetadataBindings([]); + expect(result).toEqual({}); + }); + + describe("vars", () => { + it("maps plain_text binding to vars", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "plain_text", name: "MY_VAR", text: "hello world" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vars).toEqual({ MY_VAR: "hello world" }); + }); + + it("accumulates multiple plain_text bindings", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "plain_text", name: "VAR_A", text: "aaa" }, + { type: "plain_text", name: "VAR_B", text: "bbb" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vars).toEqual({ VAR_A: "aaa", VAR_B: "bbb" }); + }); + + it("maps json binding to vars", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "json", + name: "MY_JSON", + json: { key: "value" }, + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vars).toEqual({ + MY_JSON: { key: "value" }, + }); + }); + }); + + describe("kv_namespaces", () => { + it("maps kv_namespace binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "kv_namespace", + name: "MY_KV", + namespace_id: "kv-abc-123", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.kv_namespaces).toEqual([ + { id: "kv-abc-123", binding: "MY_KV" }, + ]); + }); + + it("accumulates multiple kv_namespace bindings", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "kv_namespace", + name: "KV_A", + namespace_id: "ns-1", + }, + { + type: "kv_namespace", + name: "KV_B", + namespace_id: "ns-2", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.kv_namespaces).toHaveLength(2); + }); + }); + + describe("durable_objects", () => { + it("maps durable_object_namespace binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "durable_object_namespace", + name: "MY_DO", + class_name: "MyDurableObject", + script_name: "my-worker", + environment: "production", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.durable_objects).toEqual({ + bindings: [ + { + name: "MY_DO", + class_name: "MyDurableObject", + script_name: "my-worker", + environment: "production", + }, + ], + }); + }); + + it("accumulates multiple DO bindings", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "durable_object_namespace", + name: "DO_A", + class_name: "ClassA", + }, + { + type: "durable_object_namespace", + name: "DO_B", + class_name: "ClassB", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.durable_objects?.bindings).toHaveLength(2); + }); + }); + + describe("d1_databases", () => { + it("maps d1 binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "d1", name: "MY_DB", id: "db-456" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.d1_databases).toEqual([ + { binding: "MY_DB", database_id: "db-456" }, + ]); + }); + }); + + describe("r2_buckets", () => { + it("maps r2_bucket binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "r2_bucket", + name: "MY_BUCKET", + bucket_name: "my-bucket", + jurisdiction: "eu", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.r2_buckets).toEqual([ + { + binding: "MY_BUCKET", + bucket_name: "my-bucket", + jurisdiction: "eu", + }, + ]); + }); + }); + + describe("singleton bindings", () => { + it("maps browser binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "browser", name: "BROWSER" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.browser).toEqual({ binding: "BROWSER" }); + }); + + it("maps ai binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [{ type: "ai", name: "AI" }]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.ai).toEqual({ binding: "AI" }); + }); + + it("maps images binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "images", name: "IMAGES" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.images).toEqual({ binding: "IMAGES" }); + }); + + it("maps stream binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "stream", name: "STREAM" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.stream).toEqual({ binding: "STREAM" }); + }); + + it("maps media binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "media", name: "MEDIA" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.media).toEqual({ binding: "MEDIA" }); + }); + + it("maps version_metadata binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "version_metadata", name: "VERSION" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.version_metadata).toEqual({ binding: "VERSION" }); + }); + }); + + describe("services", () => { + it("maps service binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "service", + name: "AUTH", + service: "auth-worker", + environment: "production", + entrypoint: "AuthHandler", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.services).toEqual([ + { + binding: "AUTH", + service: "auth-worker", + environment: "production", + entrypoint: "AuthHandler", + }, + ]); + }); + }); + + describe("queues", () => { + it("maps queue binding to queues.producers", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "queue", + name: "MY_QUEUE", + queue_name: "my-queue", + delivery_delay: 30, + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.queues).toEqual({ + producers: [ + { + binding: "MY_QUEUE", + queue: "my-queue", + delivery_delay: 30, + }, + ], + }); + }); + + it("accumulates multiple queue bindings", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "queue", name: "Q1", queue_name: "queue-1" }, + { type: "queue", name: "Q2", queue_name: "queue-2" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.queues?.producers).toHaveLength(2); + }); + }); + + describe("vectorize", () => { + it("maps vectorize binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "vectorize", + name: "MY_INDEX", + index_name: "my-index", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vectorize).toEqual([ + { binding: "MY_INDEX", index_name: "my-index" }, + ]); + }); + }); + + describe("hyperdrive", () => { + it("maps hyperdrive binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "hyperdrive", name: "MY_HD", id: "hd-123" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.hyperdrive).toEqual([{ binding: "MY_HD", id: "hd-123" }]); + }); + }); + + describe("analytics_engine_datasets", () => { + it("maps analytics_engine binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "analytics_engine", + name: "ANALYTICS", + dataset: "my-dataset", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.analytics_engine_datasets).toEqual([ + { binding: "ANALYTICS", dataset: "my-dataset" }, + ]); + }); + }); + + describe("dispatch_namespaces", () => { + it("maps dispatch_namespace binding without outbound", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "dispatch_namespace", + name: "DISPATCHER", + namespace: "my-ns", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.dispatch_namespaces).toEqual([ + { binding: "DISPATCHER", namespace: "my-ns" }, + ]); + }); + + it("maps dispatch_namespace binding with outbound", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "dispatch_namespace", + name: "DISPATCHER", + namespace: "my-ns", + outbound: { + worker: { + service: "outbound-worker", + environment: "production", + }, + params: [{ name: "param1" }, { name: "param2" }], + }, + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.dispatch_namespaces).toEqual([ + { + binding: "DISPATCHER", + namespace: "my-ns", + outbound: { + service: "outbound-worker", + environment: "production", + parameters: ["param1", "param2"], + }, + }, + ]); + }); + }); + + describe("logfwdr", () => { + it("maps logfwdr binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "logfwdr", + name: "LOG", + destination: "my-destination", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.logfwdr).toEqual({ + bindings: [{ name: "LOG", destination: "my-destination" }], + }); + }); + }); + + describe("blob bindings", () => { + it("maps wasm_module binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "wasm_module", name: "WASM", part: "wasm-part" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.wasm_modules).toEqual({ WASM: "wasm-part" }); + }); + + it("maps text_blob binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "text_blob", name: "TEXT", part: "text-part" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.text_blobs).toEqual({ TEXT: "text-part" }); + }); + + it("maps data_blob binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "data_blob", name: "DATA", part: "data-part" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.data_blobs).toEqual({ DATA: "data-part" }); + }); + }); + + describe("send_email", () => { + it("maps send_email binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "send_email", + name: "EMAIL", + destination_address: "test@example.com", + allowed_destination_addresses: ["a@b.com", "c@d.com"], + allowed_sender_addresses: ["sender@example.com"], + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.send_email).toEqual([ + { + name: "EMAIL", + destination_address: "test@example.com", + allowed_destination_addresses: ["a@b.com", "c@d.com"], + allowed_sender_addresses: ["sender@example.com"], + }, + ]); + }); + }); + + describe("mtls_certificates", () => { + it("maps mtls_certificate binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "mtls_certificate", + name: "CERT", + certificate_id: "cert-123", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.mtls_certificates).toEqual([ + { binding: "CERT", certificate_id: "cert-123" }, + ]); + }); + }); + + describe("secrets_store_secrets", () => { + it("maps secrets_store_secret binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "secrets_store_secret", + name: "SECRET", + store_id: "store-1", + secret_name: "my-secret", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.secrets_store_secrets).toEqual([ + { + binding: "SECRET", + store_id: "store-1", + secret_name: "my-secret", + }, + ]); + }); + }); + + describe("artifacts", () => { + it("maps artifacts binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "artifacts", + name: "ARTIFACTS", + namespace: "my-artifacts", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.artifacts).toEqual([ + { binding: "ARTIFACTS", namespace: "my-artifacts" }, + ]); + }); + }); + + describe("unsafe_hello_world", () => { + it("maps unsafe_hello_world binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "unsafe_hello_world", + name: "HELLO", + enable_timer: true, + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.unsafe_hello_world).toEqual([ + { binding: "HELLO", enable_timer: true }, + ]); + }); + }); + + describe("flagship", () => { + it("maps flagship binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "flagship", + name: "FLAGS", + app_id: "app-123", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.flagship).toEqual([ + { binding: "FLAGS", app_id: "app-123" }, + ]); + }); + }); + + describe("pipelines", () => { + it("maps pipelines binding with stream", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "pipelines", + name: "PIPE", + stream: "my-stream", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.pipelines).toEqual([ + { binding: "PIPE", stream: "my-stream" }, + ]); + }); + + it("maps pipelines binding with pipeline", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "pipelines", + name: "PIPE", + pipeline: "my-pipeline", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.pipelines).toEqual([ + { binding: "PIPE", pipeline: "my-pipeline" }, + ]); + }); + }); + + describe("assets", () => { + it("maps assets binding (PR #11339 fix)", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "assets", name: "ASSETS" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.assets).toEqual({ binding: "ASSETS" }); + }); + }); + + describe("workflows", () => { + it("maps workflow binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "workflow", + name: "MY_WORKFLOW", + workflow_name: "my-wf", + class_name: "MyWorkflow", + script_name: "wf-worker", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.workflows).toEqual([ + { + binding: "MY_WORKFLOW", + name: "my-wf", + class_name: "MyWorkflow", + script_name: "wf-worker", + }, + ]); + }); + }); + + describe("worker_loaders", () => { + it("maps worker_loader binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "worker_loader", name: "LOADER" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.worker_loaders).toEqual([{ binding: "LOADER" }]); + }); + }); + + describe("ratelimits", () => { + it("maps ratelimit binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "ratelimit", + name: "RATE_LIMIT", + namespace_id: "rl-ns-1", + simple: { limit: 100, period: 60 }, + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.ratelimits).toEqual([ + { + name: "RATE_LIMIT", + namespace_id: "rl-ns-1", + simple: { limit: 100, period: 60 }, + }, + ]); + }); + }); + + describe("vpc_services", () => { + it("maps vpc_service binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "vpc_service", + name: "VPC", + service_id: "svc-123", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vpc_services).toEqual([ + { binding: "VPC", service_id: "svc-123" }, + ]); + }); + }); + + describe("vpc_networks", () => { + it("maps vpc_network binding with tunnel_id", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "vpc_network", + name: "VPC_NET", + tunnel_id: "tun-123", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vpc_networks).toEqual([ + { binding: "VPC_NET", tunnel_id: "tun-123" }, + ]); + }); + + it("maps vpc_network binding with network_id", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "vpc_network", + name: "VPC_NET", + network_id: "net-456", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vpc_networks).toEqual([ + { binding: "VPC_NET", network_id: "net-456" }, + ]); + }); + + it("does not map vpc_network binding without tunnel_id or network_id", ({ + expect, + }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "vpc_network", name: "VPC_NET" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vpc_networks).toBeUndefined(); + }); + }); + + describe("ai_search", () => { + it("maps ai_search_namespace binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "ai_search_namespace", + name: "SEARCH_NS", + namespace: "my-search-ns", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.ai_search_namespaces).toEqual([ + { binding: "SEARCH_NS", namespace: "my-search-ns" }, + ]); + }); + + it("maps ai_search binding", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { + type: "ai_search", + name: "SEARCH", + instance_name: "my-instance", + }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.ai_search).toEqual([ + { binding: "SEARCH", instance_name: "my-instance" }, + ]); + }); + }); + + describe("secret_text filtering", () => { + it("filters out secret_text bindings", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "secret_text", name: "SECRET", text: "s3cret" }, + { type: "plain_text", name: "VAR", text: "public" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.vars).toEqual({ VAR: "public" }); + expect(JSON.stringify(result)).not.toContain("s3cret"); + }); + }); + + describe("inherit binding", () => { + it("maps inherit binding to unsafe", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "inherit", name: "INHERITED" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + expect(result.unsafe).toEqual({ + bindings: [{ type: "inherit", name: "INHERITED" }], + metadata: undefined, + }); + }); + }); + + describe("mixed bindings", () => { + it("correctly combines multiple different binding types", ({ expect }) => { + const bindings: WorkerMetadataBinding[] = [ + { type: "plain_text", name: "VAR", text: "hello" }, + { + type: "kv_namespace", + name: "KV", + namespace_id: "ns-1", + }, + { + type: "r2_bucket", + name: "BUCKET", + bucket_name: "bucket", + }, + { type: "ai", name: "AI" }, + { type: "d1", name: "DB", id: "db-1" }, + { type: "secret_text", name: "SECRET", text: "hidden" }, + ]; + const result = mapWorkerMetadataBindings(bindings); + + expect(result.vars).toEqual({ VAR: "hello" }); + expect(result.kv_namespaces).toHaveLength(1); + expect(result.r2_buckets).toHaveLength(1); + expect(result.ai).toEqual({ binding: "AI" }); + expect(result.d1_databases).toHaveLength(1); + // secret_text should be filtered out + expect(JSON.stringify(result)).not.toContain("hidden"); + }); + }); +}); From 063d98e96e39a4e08cad6d6bccf4f382bc654967 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 1 Jun 2026 16:15:40 +0100 Subject: [PATCH 04/18] [create-cloudflare] Switch react-router template to overlay Cloudflare files locally (#14113) --- .changeset/react-router-template-overlay.md | 11 +++ .../templates/react-router/c3.ts | 50 +++++++++-- .../templates/react-router/ts/.gitignore | 14 +++ .../templates/react-router/ts/README.md | 79 ++++++++++++++++ .../templates/react-router/ts/app/app.css | 15 ++++ .../react-router/ts/app/entry.server.tsx | 42 +++++++++ .../react-router/ts/app/routes/home.tsx | 19 ++++ .../react-router/ts/app/welcome/welcome.tsx | 90 +++++++++++++++++++ .../react-router/ts/react-router.config.ts | 8 -- .../react-router/ts/tsconfig.cloudflare.json | 27 ++++++ .../templates/react-router/ts/tsconfig.json | 14 +++ .../react-router/ts/tsconfig.node.json | 13 +++ .../templates/react-router/ts/vite.config.ts | 15 ++++ .../templates/react-router/ts/workers/app.ts | 12 +++ .../templates/react-router/ts/wrangler.jsonc | 9 ++ 15 files changed, 402 insertions(+), 16 deletions(-) create mode 100644 .changeset/react-router-template-overlay.md create mode 100644 packages/create-cloudflare/templates/react-router/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/react-router/ts/README.md create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/app.css create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/entry.server.tsx create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/routes/home.tsx create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/welcome/welcome.tsx delete mode 100644 packages/create-cloudflare/templates/react-router/ts/react-router.config.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/vite.config.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/workers/app.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc diff --git a/.changeset/react-router-template-overlay.md b/.changeset/react-router-template-overlay.md new file mode 100644 index 0000000000..639fc67fba --- /dev/null +++ b/.changeset/react-router-template-overlay.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Switch the `react-router` template to scaffold from the upstream `create-react-router` default template and overlay Cloudflare-specific files locally + +Previously, C3 invoked `create-react-router` with `--template ` pointing at a specific commit of `remix-run/react-router-templates/cloudflare`. This pinning was needed because the upstream Cloudflare template had been deleted before, leaving us reliant on a third-party source we don't control. + +We now invoke `create-react-router` without `--template` (using the upstream default template) and overlay all Cloudflare-specific files — `workers/app.ts`, `wrangler.jsonc`, split `tsconfig`s, a Cloudflare-flavored `vite.config.ts`, `entry.server.tsx`, etc. — from `templates/react-router/ts/`. A `configure` step deletes `Dockerfile`/`.dockerignore` and the `@react-router/node`/`@react-router/serve` dependencies and `start` script that ship with the default template. + +This brings the `react-router` template in line with how `astro`, `svelte`, and `react` already work and removes our dependency on a deleted upstream template. The scaffolded project is functionally equivalent to before. diff --git a/packages/create-cloudflare/templates/react-router/c3.ts b/packages/create-cloudflare/templates/react-router/c3.ts index d24b74f3c2..d592f52e9d 100644 --- a/packages/create-cloudflare/templates/react-router/c3.ts +++ b/packages/create-cloudflare/templates/react-router/c3.ts @@ -1,21 +1,22 @@ +import { resolve } from "node:path"; import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; import { runFrameworkGenerator } from "frameworks/index"; +import { readJSON, removeFile, writeJSON } from "helpers/files"; import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; import type { TemplateConfig } from "../../src/templates"; -import type { C3Context } from "types"; +import type { C3Context, PackageJson } from "types"; const { npm } = detectPackageManager(); const generate = async (ctx: C3Context) => { + // We use the upstream `create-react-router` default template and overlay + // our Cloudflare-specific files via `copyFiles`. This avoids depending on + // a third-party Cloudflare template that has been deleted upstream in the past. await runFrameworkGenerator(ctx, [ ctx.project.name, - ...(ctx.args.experimental - ? [] - : [ - "--template", - // React-router deleted the template here - "https://github.com/remix-run/react-router-templates/tree/29ac272b9532fe26463a2d2693fc73ff3c1e884b/cloudflare", - ]), // to prevent asking about git twice, just let c3 do it "--no-git-init", "--no-install", @@ -24,6 +25,36 @@ const generate = async (ctx: C3Context) => { logRaw(""); // newline }; +const configure = async (ctx: C3Context) => { + // `npmInstall` has already run by this point with the upstream default + // template's `package.json`, which doesn't include `@cloudflare/vite-plugin`. + // Our overlaid `vite.config.ts` imports it, so install it now (this also + // adds it to `package.json`). `wrangler` is installed separately by + // `installWrangler()` in the main configure flow before this runs. + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor("installed")} ${dim("@cloudflare/vite-plugin")}`, + }); + + // The upstream default template targets a generic Node.js/Docker deployment. + // Remove artifacts that don't apply to a Cloudflare Workers project. + const s = spinner(); + s.start("Removing non-Cloudflare artifacts from template"); + removeFile(resolve(ctx.project.path, "Dockerfile")); + removeFile(resolve(ctx.project.path, ".dockerignore")); + + // `transformPackageJson` is deep-merge only and cannot remove keys, so strip + // the Node-server deps and `start` script that the default template ships. + const pkgJsonPath = resolve(ctx.project.path, "package.json"); + const pkgJson = readJSON(pkgJsonPath) as PackageJson; + delete pkgJson.dependencies?.["@react-router/node"]; + delete pkgJson.dependencies?.["@react-router/serve"]; + delete pkgJson.scripts?.start; + writeJSON(pkgJsonPath, pkgJson); + s.stop(`${brandColor("removed")} ${dim("Node-server template artifacts")}`); +}; + const config: TemplateConfig = { configVersion: 1, id: "react-router", @@ -34,6 +65,7 @@ const config: TemplateConfig = { path: "./ts", }, generate, + configure, transformPackageJson: async () => ({ dependencies: { "react-router": "^7.10.0", @@ -45,6 +77,8 @@ const config: TemplateConfig = { deploy: `${npm} run build && wrangler deploy`, preview: `${npm} run build && vite preview`, "cf-typegen": `wrangler types`, + typecheck: `wrangler types && react-router typegen && tsc -b`, + postinstall: `wrangler types`, }, }), devScript: "dev", diff --git a/packages/create-cloudflare/templates/react-router/ts/.gitignore b/packages/create-cloudflare/templates/react-router/ts/.gitignore new file mode 100644 index 0000000000..39e01403e5 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +.env +/node_modules/ +*.tsbuildinfo + +# React Router +/.react-router/ +/build/ + +# Cloudflare +.mf +.wrangler +.dev.vars* +worker-configuration.d.ts diff --git a/packages/create-cloudflare/templates/react-router/ts/README.md b/packages/create-cloudflare/templates/react-router/ts/README.md new file mode 100644 index 0000000000..e83f9fc9de --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/README.md @@ -0,0 +1,79 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Previewing the Production Build + +Preview the production build locally: + +```bash +npm run preview +``` + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +Deployment is done using the Wrangler CLI. + +To build and deploy directly to production: + +```sh +npm run deploy +``` + +To deploy a preview URL: + +```sh +npx wrangler versions upload +``` + +You can then promote a version to production after verification or roll it out progressively. + +```sh +npx wrangler versions deploy +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/packages/create-cloudflare/templates/react-router/ts/app/app.css b/packages/create-cloudflare/templates/react-router/ts/app/app.css new file mode 100644 index 0000000000..9b4c3ef902 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/app/app.css @@ -0,0 +1,15 @@ +@import "tailwindcss" source("."); + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/app/entry.server.tsx b/packages/create-cloudflare/templates/react-router/ts/app/entry.server.tsx new file mode 100644 index 0000000000..1d4754c474 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/app/entry.server.tsx @@ -0,0 +1,42 @@ +import type { EntryContext } from "react-router"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, +) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/packages/create-cloudflare/templates/react-router/ts/app/routes/home.tsx b/packages/create-cloudflare/templates/react-router/ts/app/routes/home.tsx new file mode 100644 index 0000000000..e2f33bf8fa --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/app/routes/home.tsx @@ -0,0 +1,19 @@ +import { env } from "cloudflare:workers"; + +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export function loader() { + return { message: env.VALUE_FROM_CLOUDFLARE }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return ; +} diff --git a/packages/create-cloudflare/templates/react-router/ts/app/welcome/welcome.tsx b/packages/create-cloudflare/templates/react-router/ts/app/welcome/welcome.tsx new file mode 100644 index 0000000000..0134ed8012 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/app/welcome/welcome.tsx @@ -0,0 +1,90 @@ +import logoDark from "./logo-dark.svg"; +import logoLight from "./logo-light.svg"; + +export function Welcome({ message }: { message: string }) { + return ( +
+
+
+
+ React Router + React Router +
+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/packages/create-cloudflare/templates/react-router/ts/react-router.config.ts b/packages/create-cloudflare/templates/react-router/ts/react-router.config.ts deleted file mode 100644 index c5aecdb73e..0000000000 --- a/packages/create-cloudflare/templates/react-router/ts/react-router.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Config } from "@react-router/dev/config"; - -export default { - ssr: true, - future: { - v8_viteEnvironmentApi: true, - }, -} satisfies Config; diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json new file mode 100644 index 0000000000..c637606d07 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "include": [ + ".react-router/types/**/*", + "app/**/*", + "app/**/.server/**/*", + "app/**/.client/**/*", + "workers/**/*", + "worker-configuration.d.ts" + ], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.json new file mode 100644 index 0000000000..d7ce9e49b0 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.cloudflare.json" } + ], + "compilerOptions": { + "checkJs": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json new file mode 100644 index 0000000000..bf6283b0c1 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["vite.config.ts"], + "compilerOptions": { + "composite": true, + "strict": true, + "types": ["node"], + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/vite.config.ts b/packages/create-cloudflare/templates/react-router/ts/vite.config.ts new file mode 100644 index 0000000000..ccca16024f --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/vite.config.ts @@ -0,0 +1,15 @@ +import { reactRouter } from "@react-router/dev/vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ viteEnvironment: { name: "ssr" } }), + tailwindcss(), + reactRouter(), + ], + resolve: { + tsconfigPaths: true, + }, +}); diff --git a/packages/create-cloudflare/templates/react-router/ts/workers/app.ts b/packages/create-cloudflare/templates/react-router/ts/workers/app.ts new file mode 100644 index 0000000000..dd5805657f --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/workers/app.ts @@ -0,0 +1,12 @@ +import { createRequestHandler } from "react-router"; + +const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, +); + +export default { + async fetch(request) { + return requestHandler(request); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc b/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc new file mode 100644 index 0000000000..5f6f51c6b6 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "", + "compatibility_date": "", + "main": "./workers/app.ts", + "vars": { + "VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare" + } +} From 65b5f9e1855651c2df2c1bdfc8930141e36413d5 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:17:47 +0100 Subject: [PATCH 05/18] move fetchResult, fetchListResult and dependents into workers-utils (#14063) Co-authored-by: Samuel Macleod --- .../move-fetch-helpers-workers-utils.md | 9 + packages/deploy-helpers/src/shared/types.ts | 11 + .../src/cfetch/errors.ts | 2 +- packages/workers-utils/src/cfetch/index.ts | 477 ++++++++++++++++++ packages/workers-utils/src/index.ts | 4 + packages/workers-utils/src/logger.ts | 8 + .../workers-utils/tests/cfetch-utils.test.ts | 59 +++ .../src/__tests__/cfetch-internal.test.ts | 10 +- .../src/__tests__/cfetch-utils.test.ts | 57 --- .../wrangler/src/__tests__/pipelines.test.ts | 17 +- .../src/__tests__/r2/notification.test.ts | 2 +- packages/wrangler/src/cfetch/index.ts | 185 ++----- packages/wrangler/src/cfetch/internal.ts | 281 ++--------- packages/wrangler/src/cloudchamber/common.ts | 3 +- packages/wrangler/src/core/handle-errors.ts | 2 +- .../wrangler/src/dev/create-worker-preview.ts | 2 +- packages/wrangler/src/dev/remote.ts | 2 +- .../wrangler/src/r2/helpers/notification.ts | 3 +- packages/wrangler/src/r2/sql.ts | 8 +- packages/wrangler/src/routes.ts | 6 +- packages/wrangler/src/user/user.ts | 14 +- packages/wrangler/src/user/whoami.ts | 7 +- 22 files changed, 680 insertions(+), 489 deletions(-) create mode 100644 .changeset/move-fetch-helpers-workers-utils.md rename packages/{wrangler => workers-utils}/src/cfetch/errors.ts (92%) create mode 100644 packages/workers-utils/src/cfetch/index.ts create mode 100644 packages/workers-utils/src/logger.ts create mode 100644 packages/workers-utils/tests/cfetch-utils.test.ts diff --git a/.changeset/move-fetch-helpers-workers-utils.md b/.changeset/move-fetch-helpers-workers-utils.md new file mode 100644 index 0000000000..2a70b077a7 --- /dev/null +++ b/.changeset/move-fetch-helpers-workers-utils.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/workers-utils": patch +"@cloudflare/deploy-helpers": patch +"wrangler": patch +--- + +Move fetch helpers into `@cloudflare/workers-utils` + +Shared Cloudflare API fetch helper types and plumbing now live in `@cloudflare/workers-utils` so Wrangler and other clients can use the same implementation. diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index 6a5e3741b3..fb88d6b823 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -5,11 +5,22 @@ import type { CfPlacement, Config, EphemeralDirectory, + FetchResultFetcher, + Logger, Route, Entry, } from "@cloudflare/workers-utils"; import type { NodeJSCompatMode } from "miniflare"; +/** + * client needs to handle logger and fetch/auth implementation + * these are passed into this package to handle any API requests/logs + */ +export type DeployHelpersContext = { + fetchResult: FetchResultFetcher; + logger: Logger; +}; + /** * Shared fields produced by merging CLI args with wrangler config. * After this point, no raw config/arg merging should happen. diff --git a/packages/wrangler/src/cfetch/errors.ts b/packages/workers-utils/src/cfetch/errors.ts similarity index 92% rename from packages/wrangler/src/cfetch/errors.ts rename to packages/workers-utils/src/cfetch/errors.ts index c16c262934..da2f40c9cc 100644 --- a/packages/wrangler/src/cfetch/errors.ts +++ b/packages/workers-utils/src/cfetch/errors.ts @@ -1,4 +1,4 @@ -import { ParseError } from "@cloudflare/workers-utils"; +import { ParseError } from "../parse"; export interface FetchError { code: number; diff --git a/packages/workers-utils/src/cfetch/index.ts b/packages/workers-utils/src/cfetch/index.ts new file mode 100644 index 0000000000..758c093fbb --- /dev/null +++ b/packages/workers-utils/src/cfetch/index.ts @@ -0,0 +1,477 @@ +import assert from "node:assert"; +import { URLSearchParams } from "node:url"; +import { fetch, FormData, Headers, Response } from "undici"; +import { + getCloudflareApiBaseUrl, + getTraceHeader, +} from "../environment-variables/misc-variables"; +import { UserError } from "../errors"; +import { APIError, parseJSON } from "../parse"; +import { type FetchError, maybeThrowFriendlyError } from "./errors"; +import type { ComplianceConfig } from "../environment-variables/misc-variables"; +import type { Logger } from "../logger"; +import type { HeadersInit, RequestInit } from "undici"; + +export type ApiCredentials = + | { + apiToken: string; + } + | { + authKey: string; + authEmail: string; + }; + +export interface FetchResult { + success: boolean; + result: ResponseType; + errors: FetchError[]; + messages?: (string | { code?: number; message: string })[]; + result_info?: unknown; +} + +export type FetchResultFetcher = ( + complianceConfig: ComplianceConfig, + resource: string, + init?: RequestInit, + queryParams?: URLSearchParams, + abortSignal?: AbortSignal +) => Promise; + +function logHeaders(headers: Headers, logger: Logger): void { + const clone = cloneHeaders(headers); + clone.delete("Authorization"); + logger.debugWithSanitization( + "HEADERS:", + JSON.stringify(Object.fromEntries(clone), null, 2) + ); +} + +/** + * + * Note this requires its caller to handle credentials + * (need to call requireLoggedIn and requireApiToken) + */ +export async function performApiFetchBase( + complianceConfig: ComplianceConfig, + resource: string, + init: RequestInit = {}, + userAgent: string, + logger: Logger, + queryParams?: URLSearchParams, + abortSignal?: AbortSignal, + credentials?: ApiCredentials +): Promise { + assert(credentials, "credentials are required for performApiFetch"); + const method = init.method ?? "GET"; + assert( + resource.startsWith("/"), + `CF API fetch - resource path must start with a "/" but got "${resource}"` + ); + const headers = cloneHeaders(new Headers(init.headers)); + addAuthorizationHeader(headers, credentials); + headers.set("User-Agent", userAgent); + maybeAddTraceHeader(headers); + + const queryString = queryParams ? `?${queryParams.toString()}` : ""; + logger.debug( + `-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl(complianceConfig)}${resource}` + ); + logger.debugWithSanitization("QUERY STRING:", queryString); + logHeaders(headers, logger); + + logger.debugWithSanitization("INIT:", JSON.stringify({ ...init }, null, 2)); + if (init.body instanceof FormData) { + logger.debugWithSanitization( + "BODY:", + await new Response(init.body).text(), + null, + 2 + ); + } + logger.debug("-- END CF API REQUEST"); + return await fetch( + `${getCloudflareApiBaseUrl(complianceConfig)}${resource}${queryString}`, + { + method, + ...init, + headers, + signal: abortSignal, + } + ); +} + +export async function fetchInternalBase( + complianceConfig: ComplianceConfig, + resource: string, + init: RequestInit = {}, + userAgent: string, + logger: Logger, + queryParams?: URLSearchParams, + abortSignal?: AbortSignal, + credentials?: ApiCredentials +): Promise<{ response: ResponseType; status: number }> { + const method = init.method ?? "GET"; + const response = await performApiFetchBase( + complianceConfig, + resource, + init, + userAgent, + logger, + queryParams, + abortSignal, + credentials + ); + const jsonText = await response.text(); + logger.debug( + "-- START CF API RESPONSE:", + response.statusText, + response.status + ); + logHeaders(response.headers, logger); + logger.debugWithSanitization("RESPONSE:", jsonText); + logger.debug("-- END CF API RESPONSE"); + + if (!jsonText && (response.status === 204 || response.status === 205)) { + return { + response: { + result: {}, + success: true, + errors: [], + messages: [], + } as ResponseType, + status: response.status, + }; + } + + if (isWAFBlockResponse(response.headers)) { + throwWAFBlockError( + response.headers, + method, + resource, + response.status, + response.statusText + ); + } + + try { + const json = parseJSON(jsonText) as ResponseType; + return { response: json, status: response.status }; + } catch { + const rayId = extractWAFBlockRayId(response.headers); + + throw new APIError({ + text: "Received a malformed response from the API", + notes: [ + { + text: truncate(jsonText, 100), + }, + { + text: `${method} ${resource} -> ${response.status} ${response.statusText}`, + }, + ...(rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []), + ], + status: response.status, + telemetryMessage: false, + }); + } +} + +export async function fetchResultBase( + complianceConfig: ComplianceConfig, + resource: string, + init: RequestInit = {}, + userAgent: string, + logger: Logger, + queryParams?: URLSearchParams, + abortSignal?: AbortSignal, + credentials?: ApiCredentials +): Promise { + const { response: json, status } = await fetchInternalBase< + FetchResult + >( + complianceConfig, + resource, + init, + userAgent, + logger, + queryParams, + abortSignal, + credentials + ); + if (json.success) { + return json.result; + } else { + throwFetchError(resource, json, status); + } +} + +export async function fetchListResultBase( + complianceConfig: ComplianceConfig, + resource: string, + init: RequestInit = {}, + userAgent: string, + logger: Logger, + queryParams?: URLSearchParams, + credentials?: ApiCredentials +): Promise { + const results: ResponseType[] = []; + let getMoreResults = true; + let cursor: string | undefined; + while (getMoreResults) { + if (cursor) { + queryParams = new URLSearchParams(queryParams); + queryParams.set("cursor", cursor); + } + const { response: json, status } = await fetchInternalBase< + FetchResult + >( + complianceConfig, + resource, + init, + userAgent, + logger, + queryParams, + undefined, + credentials + ); + if (json.success) { + results.push(...json.result); + if (hasCursor(json.result_info)) { + cursor = json.result_info?.cursor; + } else { + getMoreResults = false; + } + } else { + throwFetchError(resource, json, status); + } + } + return results; +} + +export function truncate(text: string, maxLength: number): string { + const { length } = text; + if (length <= maxLength) { + return text; + } + return `${text.substring(0, maxLength)}... (length = ${length})`; +} + +export function isWAFBlockResponse(headers: Headers): boolean { + return headers.get("cf-mitigated") === "challenge"; +} + +export function extractWAFBlockRayId(headers: Headers): string | undefined { + return headers.get("cf-ray") ?? undefined; +} + +export function extractAccountTag(resource: string): string | undefined { + const re = new RegExp("/accounts/([a-zA-Z0-9]+)/?"); + const matches = re.exec(resource); + return matches?.[1]; +} + +interface PageResultInfo { + page: number; + per_page: number; + count: number; + total_count: number; +} + +export function hasMorePages( + result_info: unknown +): result_info is PageResultInfo { + const page = (result_info as PageResultInfo | undefined)?.page; + const per_page = (result_info as PageResultInfo | undefined)?.per_page; + const total = (result_info as PageResultInfo | undefined)?.total_count; + + return ( + page !== undefined && + per_page !== undefined && + total !== undefined && + page * per_page < total + ); +} + +export function renderError( + err: + | FetchError + | { code?: number; message?: string; documentation_url?: string }, + level = 0 +): string { + const indent = " ".repeat(level); + const message = err.message ?? ""; + const chainedMessages = + "error_chain" in err + ? ((err as FetchError).error_chain + ?.map( + (chainedError) => + `\n\n${indent}- ${renderError(chainedError, level + 1)}` + ) + .join("\n") ?? "") + : ""; + return ( + (err.code ? `${message} [code: ${err.code}]` : message) + + (err.documentation_url + ? `\n${indent}To learn more about this error, visit: ${err.documentation_url}` + : "") + + chainedMessages + ); +} + +export function addAuthorizationHeader( + headers: Headers, + auth: ApiCredentials, + overrideExisting = false +): void { + if (!headers.has("Authorization") || overrideExisting) { + if ("apiToken" in auth) { + const authorizationHeader = `Bearer ${auth.apiToken}`; + validateAuthorizationHeaderValue(authorizationHeader); + headers.set("Authorization", authorizationHeader); + } else { + headers.set("X-Auth-Key", auth.authKey); + headers.set("X-Auth-Email", auth.authEmail); + } + } +} + +function validateAuthorizationHeaderValue(value: string): void { + for (const character of value) { + const codePoint = character.codePointAt(0); + if (codePoint === undefined || codePoint > 255) { + throw new UserError( + `The configured Cloudflare API token contains a character that cannot be used in an HTTP Authorization header: ${formatAuthorizationHeaderCharacter(character, codePoint)}. Recreate or copy the token again, making sure it does not include characters such as ellipses.`, + { + telemetryMessage: "cfetch auth invalid authorization header", + } + ); + } + } +} + +function formatAuthorizationHeaderCharacter( + character: string, + codePoint: number | undefined +): string { + if (codePoint === undefined) { + return '"\\u{unknown}"'; + } + + const codePointLabel = `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`; + const characterLabel = isPrintableCharacter(character) + ? `"${character}"` + : `"${escapeCharacter(character)}"`; + + return `${characterLabel} (${codePointLabel})`; +} + +function isPrintableCharacter(character: string): boolean { + return !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character); +} + +function escapeCharacter(character: string): string { + return Array.from(character) + .map((c) => { + const codePoint = c.codePointAt(0); + if (codePoint === undefined) { + return ""; + } + return codePoint <= 0xffff + ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` + : `\\u{${codePoint.toString(16).toUpperCase()}}`; + }) + .join(""); +} + +export function throwFetchError( + resource: string, + response: FetchResult, + status: number +): never { + const errors = response.errors ?? []; + for (const error of errors) { + maybeThrowFriendlyError(error); + } + + // Some API endpoints return non-standard error envelopes (e.g. {code, error} + // instead of {errors: [...]}). Surface those as notes when errors is empty. + const notes = [ + ...errors.map((err) => ({ text: renderError(err) })), + ...(response.messages?.map((msg) => ({ + text: typeof msg === "string" ? msg : (msg.message ?? String(msg)), + })) ?? []), + ]; + if (notes.length === 0) { + const raw = response as unknown as Record; + const fallbackMessage = + typeof raw.error === "string" + ? `${raw.error}${raw.code ? ` [code: ${raw.code}]` : ""}` + : undefined; + if (fallbackMessage) { + notes.push({ text: fallbackMessage }); + } + } + + const error = new APIError({ + text: `A request to the Cloudflare API (${resource}) failed.`, + notes, + status, + telemetryMessage: false, + }); + // add the first error code directly to this error + // so consumers can use it for specific behaviour + const code = errors[0]?.code; + if (code) { + error.code = code; + } + // extract the account tag from the resource (if any) + error.accountTag = extractAccountTag(resource); + throw error; +} + +function throwWAFBlockError( + headers: Headers, + method: string, + resource: string, + status: number, + statusText: string +): never { + const rayId = extractWAFBlockRayId(headers); + throw new APIError({ + text: "The Cloudflare API responded with a WAF block page instead of the expected JSON response", + notes: [ + { + text: "Cloudflare's firewall (WAF) blocked this API request. This is usually a false positive.", + }, + ...(rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []), + { + text: rayId + ? "If the issue persists, please open a Cloudflare Support ticket and include the Ray ID above." + : "If the issue persists, please open a Cloudflare Support ticket. You can find the Cloudflare Ray ID on the block page in your browser.", + }, + { + text: `${method} ${resource} -> ${status} ${statusText}`, + }, + ], + status, + telemetryMessage: false, + }); +} + +export function hasCursor( + result_info: unknown +): result_info is { cursor: string } { + const cursor = (result_info as { cursor: string } | undefined)?.cursor; + return cursor !== undefined && cursor !== null && cursor !== ""; +} + +export function maybeAddTraceHeader(headers: Headers): void { + const traceHeader = getTraceHeader(); + if (traceHeader) { + headers.set("Cf-Trace-Id", traceHeader); + } +} + +function cloneHeaders(headers: HeadersInit | undefined): Headers { + return new Headers(headers); +} diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index 3a7f08fd46..1d68fdaddf 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -111,5 +111,9 @@ export type { Tunnel, TunnelOptions } from "./tunnel"; export { startTunnel } from "./tunnel"; export { spawnCloudflared } from "./cloudflared"; +export * from "./cfetch"; + export { fetchLatestNpmVersion } from "./update-check"; export type { NpmVersionCheckResult } from "./update-check"; + +export type { Logger } from "./logger"; diff --git a/packages/workers-utils/src/logger.ts b/packages/workers-utils/src/logger.ts new file mode 100644 index 0000000000..60698de494 --- /dev/null +++ b/packages/workers-utils/src/logger.ts @@ -0,0 +1,8 @@ +export type Logger = { + debug: (...args: unknown[]) => void; + debugWithSanitization: (label: string, ...args: unknown[]) => void; + log: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +}; diff --git a/packages/workers-utils/tests/cfetch-utils.test.ts b/packages/workers-utils/tests/cfetch-utils.test.ts new file mode 100644 index 0000000000..5801c3e894 --- /dev/null +++ b/packages/workers-utils/tests/cfetch-utils.test.ts @@ -0,0 +1,59 @@ +import { describe, it } from "vitest"; +import { extractAccountTag, hasMorePages } from "../src/cfetch"; + +/** + * hasMorePages is a function that returns a boolean based on the result_info + * object returned from the cloudflare v4 API - if the current page is less + * than the total number of pages, it returns true, otherwise false. + */ +describe("hasMorePages", () => { + it("should handle result_info not having enough results to paginate", ({ + expect, + }) => { + expect( + hasMorePages({ + page: 1, + per_page: 10, + count: 5, + total_count: 5, + }) + ).toBe(false); + }); + it("should return true if the current page is less than the total number of pages", ({ + expect, + }) => { + expect( + hasMorePages({ + page: 1, + per_page: 10, + count: 10, + total_count: 100, + }) + ).toBe(true); + }); + it("should return false if we are on the last page of results", ({ + expect, + }) => { + expect( + hasMorePages({ + page: 10, + per_page: 10, + count: 10, + total_count: 100, + }) + ).toBe(false); + }); +}); + +describe("extractAccountTag", () => { + it("should return undefined when resource does not have it", ({ expect }) => { + expect(extractAccountTag("/accounts")).toBeUndefined(); + expect(extractAccountTag("/accounts/")).toBeUndefined(); + expect(extractAccountTag("/accounts//more")).toBeUndefined(); + }); + it("should return tag when resource has it", ({ expect }) => { + expect(extractAccountTag("/accounts/foo")).toBe("foo"); + expect(extractAccountTag("/accounts/bar/")).toBe("bar"); + expect(extractAccountTag("/accounts/baz/more")).toBe("baz"); + }); +}); diff --git a/packages/wrangler/src/__tests__/cfetch-internal.test.ts b/packages/wrangler/src/__tests__/cfetch-internal.test.ts index 1759807af6..757ffbbc48 100644 --- a/packages/wrangler/src/__tests__/cfetch-internal.test.ts +++ b/packages/wrangler/src/__tests__/cfetch-internal.test.ts @@ -1,12 +1,12 @@ -import { COMPLIANCE_REGION_CONFIG_UNKNOWN } from "@cloudflare/workers-utils"; -import { http, HttpResponse } from "msw"; -import { describe, it } from "vitest"; -import { fetchGraphqlResult } from "../cfetch"; import { + COMPLIANCE_REGION_CONFIG_UNKNOWN, addAuthorizationHeader, extractWAFBlockRayId, isWAFBlockResponse, -} from "../cfetch/internal"; +} from "@cloudflare/workers-utils"; +import { http, HttpResponse } from "msw"; +import { describe, it } from "vitest"; +import { fetchGraphqlResult } from "../cfetch"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { msw } from "./helpers/msw"; diff --git a/packages/wrangler/src/__tests__/cfetch-utils.test.ts b/packages/wrangler/src/__tests__/cfetch-utils.test.ts index eac224a246..ff3ab95fb6 100644 --- a/packages/wrangler/src/__tests__/cfetch-utils.test.ts +++ b/packages/wrangler/src/__tests__/cfetch-utils.test.ts @@ -1,55 +1,11 @@ import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { http, HttpResponse } from "msw"; import { describe, it } from "vitest"; -import { extractAccountTag, hasMorePages } from "../cfetch"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { mockConsoleMethods } from "./helpers/mock-console"; import { createFetchResult, msw } from "./helpers/msw"; import { runWrangler } from "./helpers/run-wrangler"; -/** -hasMorePages is a function that returns a boolean based on the result_info object returned from the cloudflare v4 API - if the current page is less than the total number of pages, it returns true, otherwise false. -*/ - -describe("hasMorePages", () => { - it("should handle result_info not having enough results to paginate", ({ - expect, - }) => { - expect( - hasMorePages({ - page: 1, - per_page: 10, - count: 5, - total_count: 5, - }) - ).toBe(false); - }); - it("should return true if the current page is less than the total number of pages", ({ - expect, - }) => { - expect( - hasMorePages({ - page: 1, - per_page: 10, - count: 10, - total_count: 100, - }) - ).toBe(true); - }); - it("should return false if we are on the last page of results", ({ - expect, - }) => { - expect( - hasMorePages({ - page: 10, - per_page: 10, - count: 10, - total_count: 100, - }) - ).toBe(false); - }); -}); - describe("throwFetchError", () => { mockAccountId(); mockApiToken(); @@ -221,16 +177,3 @@ describe("throwFetchError", () => { }); }); }); - -describe("extractAccountTag", () => { - it("should return undefined when resource does not have it", ({ expect }) => { - expect(extractAccountTag("/accounts")).toBeUndefined(); - expect(extractAccountTag("/accounts/")).toBeUndefined(); - expect(extractAccountTag("/accounts//more")).toBeUndefined(); - }); - it("should return tag when resource has it", ({ expect }) => { - expect(extractAccountTag("/accounts/foo")).toBe("foo"); - expect(extractAccountTag("/accounts/bar/")).toBe("bar"); - expect(extractAccountTag("/accounts/baz/more")).toBe("baz"); - }); -}); diff --git a/packages/wrangler/src/__tests__/pipelines.test.ts b/packages/wrangler/src/__tests__/pipelines.test.ts index 16adf23b39..a66c3b5249 100644 --- a/packages/wrangler/src/__tests__/pipelines.test.ts +++ b/packages/wrangler/src/__tests__/pipelines.test.ts @@ -34,10 +34,19 @@ describe("wrangler pipelines", () => { expect(body.sql).toBe(sql); if (!isValid) { - const error = { - notes: [{ text: "Invalid SQL syntax near 'INVALID'" }], - }; - throw error; + return HttpResponse.json( + { + success: false, + errors: [ + { + message: "Invalid SQL syntax near 'INVALID'", + }, + ], + messages: [], + result: null, + }, + { status: 400 } + ); } return HttpResponse.json({ diff --git a/packages/wrangler/src/__tests__/r2/notification.test.ts b/packages/wrangler/src/__tests__/r2/notification.test.ts index f0c4e300cd..c23ef2a26f 100644 --- a/packages/wrangler/src/__tests__/r2/notification.test.ts +++ b/packages/wrangler/src/__tests__/r2/notification.test.ts @@ -7,7 +7,7 @@ import { import formatLabelledValues from "../../utils/render-labelled-values"; import { mockConsoleMethods } from "../helpers/mock-console"; import type { GetNotificationConfigResponse } from "../../r2/helpers/notification"; -import type { ApiCredentials } from "../../user"; +import type { ApiCredentials } from "@cloudflare/workers-utils"; describe("event notifications", () => { const std = mockConsoleMethods(); diff --git a/packages/wrangler/src/cfetch/index.ts b/packages/wrangler/src/cfetch/index.ts index 596f1d69ad..77846607e6 100644 --- a/packages/wrangler/src/cfetch/index.ts +++ b/packages/wrangler/src/cfetch/index.ts @@ -1,23 +1,23 @@ import { URLSearchParams } from "node:url"; -import { APIError } from "@cloudflare/workers-utils"; -import { maybeThrowFriendlyError } from "./errors"; -import { fetchInternal } from "./internal"; -import type { ApiCredentials } from "../user"; -import type { FetchError } from "./errors"; -import type { ComplianceConfig } from "@cloudflare/workers-utils"; -import type { ErrorData } from "cloudflare/resources/shared"; +import { + throwFetchError, + hasCursor, + hasMorePages, + fetchResultBase, + fetchListResultBase, +} from "@cloudflare/workers-utils"; +import { version as wranglerVersion } from "../../package.json"; +import { logger } from "../logger"; +import { fetchInternal, resolveCredentials } from "./internal"; +import type { + ComplianceConfig, + FetchResult, + ApiCredentials, +} from "@cloudflare/workers-utils"; import type { RequestInit } from "undici"; // Check out https://api.cloudflare.com/ for API docs. -export interface FetchResult { - success: boolean; - result: ResponseType; - errors: FetchError[]; - messages?: (string | { code?: number; message: string })[]; - result_info?: unknown; -} - export { fetchKVGetValue, performApiFetch } from "./internal"; /** @@ -31,14 +31,17 @@ export async function fetchResult( abortSignal?: AbortSignal, apiToken?: ApiCredentials ): Promise { - const { response: json, status } = await fetchInternal< - FetchResult - >(complianceConfig, resource, init, queryParams, abortSignal, apiToken); - if (json.success) { - return json.result; - } else { - throwFetchError(resource, json, status); - } + apiToken = await resolveCredentials(complianceConfig, apiToken); + return fetchResultBase( + complianceConfig, + resource, + init, + `wrangler/${wranglerVersion}`, + logger, + queryParams, + abortSignal, + apiToken + ); } /** @@ -74,29 +77,16 @@ export async function fetchListResult( init: RequestInit = {}, queryParams?: URLSearchParams ): Promise { - const results: ResponseType[] = []; - let getMoreResults = true; - let cursor: string | undefined; - while (getMoreResults) { - if (cursor) { - queryParams = new URLSearchParams(queryParams); - queryParams.set("cursor", cursor); - } - const { response: json, status } = await fetchInternal< - FetchResult - >(complianceConfig, resource, init, queryParams); - if (json.success) { - results.push(...json.result); - if (hasCursor(json.result_info)) { - cursor = json.result_info?.cursor; - } else { - getMoreResults = false; - } - } else { - throwFetchError(resource, json, status); - } - } - return results; + const credentials = await resolveCredentials(complianceConfig); + return fetchListResultBase( + complianceConfig, + resource, + init, + `wrangler/${wranglerVersion}`, + logger, + queryParams, + credentials + ); } /** @@ -184,106 +174,3 @@ export async function fetchCursorPage( return results; } - -interface PageResultInfo { - page: number; - per_page: number; - count: number; - total_count: number; -} - -export function hasMorePages( - result_info: unknown -): result_info is PageResultInfo { - const page = (result_info as PageResultInfo | undefined)?.page; - const per_page = (result_info as PageResultInfo | undefined)?.per_page; - const total = (result_info as PageResultInfo | undefined)?.total_count; - - return ( - page !== undefined && - per_page !== undefined && - total !== undefined && - page * per_page < total - ); -} - -function throwFetchError( - resource: string, - response: FetchResult, - status: number -): never { - // This is an error from within an MSW handler - if (typeof vitest !== "undefined" && !("errors" in response)) { - throw response; - } - const errors = response.errors ?? []; - for (const error of errors) { - maybeThrowFriendlyError(error); - } - - // Some API endpoints return non-standard error envelopes (e.g. {code, error} - // instead of {errors: [...]}). Surface those as notes when errors is empty. - const notes = [ - ...errors.map((err) => ({ text: renderError(err) })), - ...(response.messages?.map((msg) => ({ - text: typeof msg === "string" ? msg : (msg.message ?? String(msg)), - })) ?? []), - ]; - if (notes.length === 0) { - const raw = response as unknown as Record; - const fallbackMessage = - typeof raw.error === "string" - ? `${raw.error}${raw.code ? ` [code: ${raw.code}]` : ""}` - : undefined; - if (fallbackMessage) { - notes.push({ text: fallbackMessage }); - } - } - - const error = new APIError({ - text: `A request to the Cloudflare API (${resource}) failed.`, - notes, - status, - telemetryMessage: false, - }); - // add the first error code directly to this error - // so consumers can use it for specific behaviour - const code = errors[0]?.code; - if (code) { - error.code = code; - } - // extract the account tag from the resource (if any) - error.accountTag = extractAccountTag(resource); - throw error; -} - -export function extractAccountTag(resource: string) { - const re = new RegExp("/accounts/([a-zA-Z0-9]+)/?"); - const matches = re.exec(resource); - return matches?.[1]; -} - -function hasCursor(result_info: unknown): result_info is { cursor: string } { - const cursor = (result_info as { cursor: string } | undefined)?.cursor; - return cursor !== undefined && cursor !== null && cursor !== ""; -} - -export function renderError(err: FetchError | ErrorData, level = 0): string { - const indent = " ".repeat(level); - const chainedMessages = - "error_chain" in err - ? (err.error_chain - ?.map( - (chainedError) => - `\n\n${indent}- ${renderError(chainedError, level + 1)}` - ) - .join("\n") ?? "") - : ""; - return ( - (err.code ? `${err.message} [code: ${err.code}]` : err.message) + - (err.documentation_url - ? `\n${indent}To learn more about this error, visit: ${err.documentation_url}` - : "") + - chainedMessages - ); -} diff --git a/packages/wrangler/src/cfetch/internal.ts b/packages/wrangler/src/cfetch/internal.ts index 9a07d77210..1c751f8685 100644 --- a/packages/wrangler/src/cfetch/internal.ts +++ b/packages/wrangler/src/cfetch/internal.ts @@ -1,9 +1,9 @@ -import assert from "node:assert"; import { + addAuthorizationHeader, APIError, + fetchInternalBase, getCloudflareApiBaseUrl, - getTraceHeader, - parseJSON, + performApiFetchBase, UserError, } from "@cloudflare/workers-utils"; import Cloudflare from "cloudflare"; @@ -11,8 +11,11 @@ import { fetch, FormData, Headers, Request, Response } from "undici"; import { version as wranglerVersion } from "../../package.json"; import { logger } from "../logger"; import { loginOrRefreshIfRequired, requireApiToken } from "../user"; -import type { ApiCredentials } from "../user"; -import type { ComplianceConfig, Message } from "@cloudflare/workers-utils"; +import type { + ApiCredentials, + ComplianceConfig, + Message, +} from "@cloudflare/workers-utils"; import type { URLSearchParams } from "node:url"; import type { HeadersInit, RequestInfo, RequestInit } from "undici"; @@ -94,52 +97,16 @@ export async function performApiFetch( abortSignal?: AbortSignal, apiToken?: ApiCredentials ) { - const method = init.method ?? "GET"; - assert( - resource.startsWith("/"), - `CF API fetch - resource path must start with a "/" but got "${resource}"` - ); - await requireLoggedIn(complianceConfig); - apiToken ??= requireApiToken(); - const headers = cloneHeaders(new Headers(init.headers)); - addAuthorizationHeader(headers, apiToken); - addUserAgent(headers); - maybeAddTraceHeader(headers); - - const queryString = queryParams ? `?${queryParams.toString()}` : ""; - logger.debug( - `-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl(complianceConfig)}${resource}` - ); - logger.debugWithSanitization("QUERY STRING:", queryString); - logHeaders(headers); - - logger.debugWithSanitization("INIT:", JSON.stringify({ ...init }, null, 2)); - if (init.body instanceof FormData) { - logger.debugWithSanitization( - "BODY:", - await new Response(init.body).text(), - null, - 2 - ); - } - logger.debug("-- END CF API REQUEST"); - return await fetch( - `${getCloudflareApiBaseUrl(complianceConfig)}${resource}${queryString}`, - { - method, - ...init, - headers, - signal: abortSignal, - } - ); -} - -function logHeaders(headers: Headers) { - headers = cloneHeaders(headers); - headers.delete("Authorization"); - logger.debugWithSanitization( - "HEADERS:", - JSON.stringify(Object.fromEntries(headers), null, 2) + apiToken = await resolveCredentials(complianceConfig, apiToken); + return performApiFetchBase( + complianceConfig, + resource, + init, + `wrangler/${wranglerVersion}`, + logger, + queryParams, + abortSignal, + apiToken ); } @@ -160,147 +127,33 @@ export async function fetchInternal( abortSignal?: AbortSignal, apiToken?: ApiCredentials ): Promise<{ response: ResponseType; status: number }> { - const method = init.method ?? "GET"; - const response = await performApiFetch( + apiToken = await resolveCredentials(complianceConfig, apiToken); + return fetchInternalBase( complianceConfig, resource, init, + `wrangler/${wranglerVersion}`, + logger, queryParams, abortSignal, apiToken ); - const jsonText = await response.text(); - logger.debug( - "-- START CF API RESPONSE:", - response.statusText, - response.status - ); - logHeaders(response.headers); - logger.debugWithSanitization("RESPONSE:", jsonText); - logger.debug("-- END CF API RESPONSE"); - - // HTTP 204 and HTTP 205 responses do not return a body. We need to special-case this - // as otherwise parseJSON will throw an error back to the user. - if (!jsonText && (response.status === 204 || response.status === 205)) { - return { - response: { - result: {}, - success: true, - errors: [], - messages: [], - } as ResponseType, - status: response.status, - }; - } - - // Detect Cloudflare WAF block pages via the cf-mitigated response header. - // Without this check, the JSON parser throws a confusing "malformed response" error. - if (isWAFBlockResponse(response.headers)) { - throwWAFBlockError( - response.headers, - method, - resource, - response.status, - response.statusText - ); - } - - try { - const json = parseJSON(jsonText) as ResponseType; - return { response: json, status: response.status }; - } catch { - const rayId = extractWAFBlockRayId(response.headers); - - throw new APIError({ - text: "Received a malformed response from the API", - notes: [ - { - text: truncate(jsonText, 100), - }, - { - text: `${method} ${resource} -> ${response.status} ${response.statusText}`, - }, - ...(rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []), - ], - status: response.status, - telemetryMessage: false, - }); - } -} - -export function truncate(text: string, maxLength: number): string { - const { length } = text; - if (length <= maxLength) { - return text; - } - return `${text.substring(0, maxLength)}... (length = ${length})`; -} - -/** - * Checks whether the response was blocked by Cloudflare's WAF by inspecting - * the `cf-mitigated` response header. When the WAF blocks or challenges a - * request the response will include `cf-mitigated: challenge`. - * - * @see https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/ - * - * @param headers - The response headers to inspect. - * @returns `true` if the response was mitigated by the WAF. - */ -export function isWAFBlockResponse(headers: Headers): boolean { - return headers.get("cf-mitigated") === "challenge"; } -/** - * Extracts the Cloudflare Ray ID from the `cf-ray` response header. - * - * @param headers - The response headers to inspect. - * @returns The Ray ID string, or `undefined` if the header is absent. - */ -export function extractWAFBlockRayId(headers: Headers): string | undefined { - return headers.get("cf-ray") ?? undefined; +function cloneHeaders(headers: HeadersInit | undefined): Headers { + return new Headers(headers); } /** - * Throws a descriptive {@link APIError} for a WAF block response. * - * @param headers - The response headers (used to extract the Ray ID). - * @param method - The HTTP method of the blocked request. - * @param resource - The URL or path that was requested. - * @param status - The HTTP status code returned. - * @param statusText - The HTTP status text returned. - * @throws {APIError} Always — this function never returns. + * Triggers a login or token refresh if necessary */ -function throwWAFBlockError( - headers: Headers, - method: string, - resource: string, - status: number, - statusText: string -): never { - const rayId = extractWAFBlockRayId(headers); - throw new APIError({ - text: "The Cloudflare API responded with a WAF block page instead of the expected JSON response", - notes: [ - { - text: "Cloudflare's firewall (WAF) blocked this API request. This is usually a false positive.", - }, - ...(rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []), - { - text: rayId - ? "If the issue persists, please open a Cloudflare Support ticket and include the Ray ID above." - : "If the issue persists, please open a Cloudflare Support ticket. You can find the Cloudflare Ray ID on the block page in your browser.", - }, - { - text: `${method} ${resource} -> ${status} ${statusText}`, - }, - ], - status, - telemetryMessage: false, - }); -} - -function cloneHeaders(headers: HeadersInit | undefined): Headers { - return new Headers(headers); +export async function resolveCredentials( + complianceConfig: ComplianceConfig, + apiToken?: ApiCredentials +): Promise { + await requireLoggedIn(complianceConfig); + return apiToken ?? requireApiToken(); } export async function requireLoggedIn( @@ -314,82 +167,10 @@ export async function requireLoggedIn( } } -export function addAuthorizationHeader( - headers: Headers, - auth: ApiCredentials, - overrideExisting = false -): void { - if (!headers.has("Authorization") || overrideExisting) { - if ("apiToken" in auth) { - const authorizationHeader = `Bearer ${auth.apiToken}`; - validateAuthorizationHeaderValue(authorizationHeader); - headers.set("Authorization", authorizationHeader); - } else { - headers.set("X-Auth-Key", auth.authKey); - headers.set("X-Auth-Email", auth.authEmail); - } - } -} - -function validateAuthorizationHeaderValue(value: string): void { - for (const character of value) { - const codePoint = character.codePointAt(0); - if (codePoint === undefined || codePoint > 255) { - throw new UserError( - `The configured Cloudflare API token contains a character that cannot be used in an HTTP Authorization header: ${formatAuthorizationHeaderCharacter(character, codePoint)}. Recreate or copy the token again, making sure it does not include characters such as ellipses.`, - { - telemetryMessage: "cfetch auth invalid authorization header", - } - ); - } - } -} - -function formatAuthorizationHeaderCharacter( - character: string, - codePoint: number | undefined -): string { - if (codePoint === undefined) { - return '"\\u{unknown}"'; - } - - const codePointLabel = `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`; - const characterLabel = isPrintableCharacter(character) - ? `"${character}"` - : `"${escapeCharacter(character)}"`; - - return `${characterLabel} (${codePointLabel})`; -} - -function isPrintableCharacter(character: string): boolean { - return !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character); -} - -function escapeCharacter(character: string): string { - return Array.from(character) - .map((c) => { - const codePoint = c.codePointAt(0); - if (codePoint === undefined) { - return ""; - } - return codePoint <= 0xffff - ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` - : `\\u{${codePoint.toString(16).toUpperCase()}}`; - }) - .join(""); -} - export function addUserAgent(headers: Headers): void { headers.set("User-Agent", `wrangler/${wranglerVersion}`); } -export function maybeAddTraceHeader(headers: Headers): void { - const traceHeader = getTraceHeader(); - if (traceHeader) { - headers.set("Cf-Trace-Id", traceHeader); - } -} - /** * The implementation for fetching a kv value from the cloudflare API. * We special-case this one call, because it's the only API call that diff --git a/packages/wrangler/src/cloudchamber/common.ts b/packages/wrangler/src/cloudchamber/common.ts index 09cd98b713..a95c418f66 100644 --- a/packages/wrangler/src/cloudchamber/common.ts +++ b/packages/wrangler/src/cloudchamber/common.ts @@ -10,11 +10,12 @@ import { OpenAPI, } from "@cloudflare/containers-shared"; import { + addAuthorizationHeader, getCloudflareApiBaseUrl, parseByteSize, UserError, } from "@cloudflare/workers-utils"; -import { addAuthorizationHeader, addUserAgent } from "../cfetch/internal"; +import { addUserAgent } from "../cfetch/internal"; import { readConfig } from "../config"; import { constructStatusMessage } from "../core/CommandRegistry"; import { isNonInteractiveOrCI } from "../is-interactive"; diff --git a/packages/wrangler/src/core/handle-errors.ts b/packages/wrangler/src/core/handle-errors.ts index 3bc66ea204..f6ce22f8d3 100644 --- a/packages/wrangler/src/core/handle-errors.ts +++ b/packages/wrangler/src/core/handle-errors.ts @@ -6,13 +6,13 @@ import { COMPLIANCE_REGION_CONFIG_UNKNOWN, JsonFriendlyFatalError, ParseError, + renderError, UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; import { Cloudflare } from "cloudflare"; import dedent from "ts-dedent"; import { createCLIParser } from ".."; -import { renderError } from "../cfetch"; import { readConfig } from "../config"; import { isBuildFailure, diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index 97a701c43d..f4c6b00f99 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -7,9 +7,9 @@ import { createWorkerUploadForm } from "../deployment-bundle/create-worker-uploa import { logger } from "../logger"; import { getWorkersDevSubdomain } from "../routes"; import { getAccessHeaders } from "../user/access"; -import type { ApiCredentials } from "../user"; import type { CfWorkerInitWithName } from "./remote"; import type { + ApiCredentials, CfWorkerContext, ComplianceConfig, } from "@cloudflare/workers-utils"; diff --git a/packages/wrangler/src/dev/remote.ts b/packages/wrangler/src/dev/remote.ts index 0dbda2ca04..1840a4b6fa 100644 --- a/packages/wrangler/src/dev/remote.ts +++ b/packages/wrangler/src/dev/remote.ts @@ -12,9 +12,9 @@ import { requireApiToken } from "../user"; import { isAbortError } from "../utils/isAbortError"; import { getZoneIdForPreview } from "../zones"; import type { StartDevWorkerInput } from "../api"; -import type { ApiCredentials } from "../user"; import type { CfAccount } from "./create-worker-preview"; import type { EsbuildBundle } from "./use-esbuild"; +import type { ApiCredentials } from "@cloudflare/workers-utils"; import type { AssetsOptions, CfModule, diff --git a/packages/wrangler/src/r2/helpers/notification.ts b/packages/wrangler/src/r2/helpers/notification.ts index c80196b018..2a52418aca 100644 --- a/packages/wrangler/src/r2/helpers/notification.ts +++ b/packages/wrangler/src/r2/helpers/notification.ts @@ -1,8 +1,7 @@ import { fetchResult } from "../../cfetch"; import { logger } from "../../logger"; import { getQueue, getQueueById } from "../../queues/client"; -import type { ApiCredentials } from "../../user"; -import type { Config } from "@cloudflare/workers-utils"; +import type { Config, ApiCredentials } from "@cloudflare/workers-utils"; import type { HeadersInit } from "undici"; export type R2EventableOperation = diff --git a/packages/wrangler/src/r2/sql.ts b/packages/wrangler/src/r2/sql.ts index b0b9c79cbb..f50227f7e4 100644 --- a/packages/wrangler/src/r2/sql.ts +++ b/packages/wrangler/src/r2/sql.ts @@ -1,8 +1,12 @@ import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; -import { APIError, parseJSON, UserError } from "@cloudflare/workers-utils"; +import { + APIError, + parseJSON, + UserError, + truncate, +} from "@cloudflare/workers-utils"; import prettyBytes from "pretty-bytes"; import { fetch } from "undici"; -import { truncate } from "../cfetch/internal"; import { createCommand, createNamespace } from "../core/create-command"; import { logger } from "../logger"; import { diff --git a/packages/wrangler/src/routes.ts b/packages/wrangler/src/routes.ts index 93f2bf5062..d1854745e4 100644 --- a/packages/wrangler/src/routes.ts +++ b/packages/wrangler/src/routes.ts @@ -7,8 +7,10 @@ import chalk from "chalk"; import { fetchResult } from "./cfetch"; import { confirm, prompt } from "./dialogs"; import { logger } from "./logger"; -import type { ApiCredentials } from "./user/user"; -import type { ComplianceConfig } from "@cloudflare/workers-utils"; +import type { + ComplianceConfig, + ApiCredentials, +} from "@cloudflare/workers-utils"; type WorkersDevSubdomainRegistrationContext = "workers_dev" | "workflows"; diff --git a/packages/wrangler/src/user/user.ts b/packages/wrangler/src/user/user.ts index 93f4f82bfd..c7836d0d31 100644 --- a/packages/wrangler/src/user/user.ts +++ b/packages/wrangler/src/user/user.ts @@ -251,19 +251,13 @@ import { fetchAllAccounts } from "./fetch-accounts"; import { generateAuthUrl, OAUTH_CALLBACK_URL } from "./generate-auth-url"; import { generateRandomState } from "./generate-random-state"; import type { Account } from "./shared"; -import type { ComplianceConfig } from "@cloudflare/workers-utils"; +import type { + ApiCredentials, + ComplianceConfig, +} from "@cloudflare/workers-utils"; import type { ParsedUrlQuery } from "node:querystring"; import type { Response } from "undici"; -export type ApiCredentials = - | { - apiToken: string; - } - | { - authKey: string; - authEmail: string; - }; - /** * Try to read API credentials from environment variables. * diff --git a/packages/wrangler/src/user/whoami.ts b/packages/wrangler/src/user/whoami.ts index 94e39b1163..076a630954 100644 --- a/packages/wrangler/src/user/whoami.ts +++ b/packages/wrangler/src/user/whoami.ts @@ -10,8 +10,11 @@ import { formatMessage } from "../utils/format-message"; import { fetchAllAccounts } from "./fetch-accounts"; import { fetchMembershipRoles } from "./membership"; import { DefaultScopeKeys, getAPIToken, getAuthFromEnv, getScopes } from "."; -import type { ApiCredentials, Scope } from "."; -import type { ComplianceConfig } from "@cloudflare/workers-utils"; +import type { Scope } from "."; +import type { + ComplianceConfig, + ApiCredentials, +} from "@cloudflare/workers-utils"; /** * Represents the JSON output of `wrangler whoami --json`. From fe97ff8e8e5c74a03cf040c4fefb425f0cc59467 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:21:48 +0100 Subject: [PATCH 06/18] [C3] bump create-react-router from 7.15.1 to 7.16.0 in /packages/create-cloudflare/src/frameworks (#14129) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wrangler automated PR updater --- .changeset/c3-frameworks-update-14129.md | 11 +++++++++++ .../create-cloudflare/src/frameworks/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/c3-frameworks-update-14129.md diff --git a/.changeset/c3-frameworks-update-14129.md b/.changeset/c3-frameworks-update-14129.md new file mode 100644 index 0000000000..d462377e58 --- /dev/null +++ b/.changeset/c3-frameworks-update-14129.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ------------------- | ------ | ------ | +| create-react-router | 7.15.1 | 7.16.0 | diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index 16eedd8eb1..23dd508ac3 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -9,7 +9,7 @@ "create-hono": "0.19.4", "create-next-app": "16.2.6", "create-qwik": "1.20.0", - "create-react-router": "7.15.1", + "create-react-router": "7.16.0", "create-rwsdk": "3.1.3", "create-solid": "0.7.0", "create-vike": "0.0.627", From 64ef9fd46eeb590813bb8cbc61b58c407452362e Mon Sep 17 00:00:00 2001 From: Kaido Iwamoto Date: Tue, 2 Jun 2026 00:23:10 +0900 Subject: [PATCH 07/18] [wrangler] Fix secret bulk stdin env parsing (#14124) Co-authored-by: Dario Piotrowicz --- .changeset/tame-wrangler-secrets.md | 7 ++ .../src/__tests__/pages/secret.test.ts | 18 +-- .../wrangler/src/__tests__/secret.test.ts | 39 ++++-- .../__tests__/versions/secrets/bulk.test.ts | 113 +++++++++++------- packages/wrangler/src/secret/index.ts | 5 +- 5 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 .changeset/tame-wrangler-secrets.md diff --git a/.changeset/tame-wrangler-secrets.md b/.changeset/tame-wrangler-secrets.md new file mode 100644 index 0000000000..e356fa4eef --- /dev/null +++ b/.changeset/tame-wrangler-secrets.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fix `wrangler secret bulk` dropping newlines from `.env` input read from stdin + +Previously, `.env` input piped through stdin was concatenated without line breaks, so only the first secret could be parsed correctly. Stdin input now preserves line separators before parsing. diff --git a/packages/wrangler/src/__tests__/pages/secret.test.ts b/packages/wrangler/src/__tests__/pages/secret.test.ts index 6226812768..a933fc7072 100644 --- a/packages/wrangler/src/__tests__/pages/secret.test.ts +++ b/packages/wrangler/src/__tests__/pages/secret.test.ts @@ -23,6 +23,12 @@ import type { PagesConfigCache } from "../../pages/types"; import type { Interface } from "node:readline"; import type { ExpectStatic } from "vitest"; +function mockReadlineInput(input: string) { + vi.spyOn(readline, "createInterface").mockImplementation( + () => input.split(/\r?\n/) as unknown as Interface + ); +} + describe("wrangler pages secret", () => { const std = mockConsoleMethods(); const { setIsTTY } = useMockIsTTY(); @@ -561,13 +567,11 @@ describe("wrangler pages secret", () => { }); it("should use secret bulk w/ pipe input", async ({ expect }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - secret1: "secret-value", - password: "hunter2", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + secret1: "secret-value", + password: "hunter2", + }) ); mockProjectRequests(expect, [ diff --git a/packages/wrangler/src/__tests__/secret.test.ts b/packages/wrangler/src/__tests__/secret.test.ts index ae64f07c63..3915253c62 100644 --- a/packages/wrangler/src/__tests__/secret.test.ts +++ b/packages/wrangler/src/__tests__/secret.test.ts @@ -79,6 +79,12 @@ function mockNoWorkerFound({ isBulk = false } = {}) { } } +function mockReadlineInput(input: string) { + vi.spyOn(readline, "createInterface").mockImplementation( + () => input.split(/\r?\n/) as unknown as Interface + ); +} + describe("wrangler secret", () => { const std = mockConsoleMethods(); const { setIsTTY } = useMockIsTTY(); @@ -1136,13 +1142,11 @@ describe("wrangler secret", () => { }); it("should use secret bulk w/ pipe input", async ({ expect }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - secret1: "secret-value", - password: "hunter2", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + secret1: "secret-value", + password: "hunter2", + }) ); mockBulkRequest(expect); @@ -1162,6 +1166,27 @@ describe("wrangler secret", () => { expect(std.warn).toMatchInlineSnapshot(`""`); }); + it("should create secrets from env stdin", async ({ expect }) => { + mockReadlineInput("SECRET_NAME_1=secret_text\nSECRET_NAME_2=secret_text"); + mockBulkRequest(expect); + + await runWrangler("secret bulk --name script-name"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + 🌀 Processing the secrets for the Worker "script-name" + ✨ Successfully created secret for key: SECRET_NAME_1 + ✨ Successfully created secret for key: SECRET_NAME_2 + + Finished processing secrets file: + ✨ 2 secrets successfully created" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + it("should create secrets from JSON file", async ({ expect }) => { writeFileSync( "secret.json", diff --git a/packages/wrangler/src/__tests__/versions/secrets/bulk.test.ts b/packages/wrangler/src/__tests__/versions/secrets/bulk.test.ts index 8fcb8da6be..4f06054a5f 100644 --- a/packages/wrangler/src/__tests__/versions/secrets/bulk.test.ts +++ b/packages/wrangler/src/__tests__/versions/secrets/bulk.test.ts @@ -14,6 +14,12 @@ import type { VersionDetails } from "../../../versions/secrets"; import type { CfPlacement } from "@cloudflare/workers-utils"; import type { Interface } from "node:readline"; +function mockReadlineInput(input: string) { + vi.spyOn(readline, "createInterface").mockImplementation( + () => input.split(/\r?\n/) as unknown as Interface + ); +} + describe("versions secret bulk", () => { const std = mockConsoleMethods(); runInTempDir(); @@ -124,14 +130,49 @@ describe("versions secret bulk", () => { }); test("uploading secrets from stdin", async ({ expect }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - SECRET_2: "secret-2", - SECRET_3: "secret-3", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + SECRET_2: "secret-2", + SECRET_3: "secret-3", + }) + ); + + mockSetupApiCalls(expect); + mockPostVersion(expect, (metadata) => { + expect(metadata.bindings).toStrictEqual([ + { type: "inherit", name: "do-binding" }, + { type: "secret_text", name: "SECRET_1", text: "secret-1" }, + { type: "secret_text", name: "SECRET_2", text: "secret-2" }, + { type: "secret_text", name: "SECRET_3", text: "secret-3" }, + ]); + expect(metadata.keep_bindings).toStrictEqual([ + "secret_key", + "secret_text", + ]); + expect(metadata.keep_assets).toBeTruthy(); + }); + + await runWrangler(`versions secret bulk --name script-name`); + expect(std.out).toMatchInlineSnapshot( + ` + " + ⛅️ wrangler x.x.x + ────────────────── + 🌀 Creating the secrets for the Worker "script-name" + ✨ Successfully created secret for key: SECRET_1 + ✨ Successfully created secret for key: SECRET_2 + ✨ Successfully created secret for key: SECRET_3 + ✨ Success! Created version id with 3 secrets. + ➡️ To deploy this version to production traffic use the command "wrangler versions deploy"." + ` + ); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + test("uploading secrets from env stdin", async ({ expect }) => { + mockReadlineInput( + "SECRET_1=secret-1\nSECRET_2=secret-2\nSECRET_3=secret-3" ); mockSetupApiCalls(expect); @@ -177,11 +218,7 @@ describe("versions secret bulk", () => { }); test("should error on invalid json stdin", async ({ expect }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - "hello world" as unknown as Interface - ); + mockReadlineInput("hello world"); mockSetupApiCalls(expect); mockPostVersion(expect, (metadata) => { @@ -317,12 +354,10 @@ describe("versions secret bulk", () => { it("should warn if the wrangler config contains environments but none was specified in the command", async ({ expect, }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + }) ); writeWranglerConfig({ env: { test: {} } }); @@ -345,12 +380,10 @@ describe("versions secret bulk", () => { it("should not warn if the wrangler config contains environments and one was specified in the command", async ({ expect, }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + }) ); writeWranglerConfig({ env: { test: {} } }); @@ -364,12 +397,10 @@ describe("versions secret bulk", () => { it("should not warn if the wrangler config doesn't contain environments and none was specified in the command", async ({ expect, }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + }) ); writeWranglerConfig(); @@ -384,12 +415,10 @@ describe("versions secret bulk", () => { expect, }) => { vi.stubEnv("CLOUDFLARE_ENV", "test"); - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + }) ); writeWranglerConfig({ env: { test: {} } }); @@ -403,12 +432,10 @@ describe("versions secret bulk", () => { it('should not warn if --env="" is passed to explicitly target the top-level environment', async ({ expect, }) => { - vi.spyOn(readline, "createInterface").mockImplementation( - () => - // `readline.Interface` is an async iterator: `[Symbol.asyncIterator](): AsyncIterableIterator` - JSON.stringify({ - SECRET_1: "secret-1", - }) as unknown as Interface + mockReadlineInput( + JSON.stringify({ + SECRET_1: "secret-1", + }) ); writeWranglerConfig({ env: { test: {} } }); diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index b6a618c989..623b2adc42 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -657,10 +657,11 @@ export async function parseBulkInputToObject( secretSource = "stdin"; try { const rl = readline.createInterface({ input: process.stdin }); - let pipedInput = ""; + const pipedInputLines: string[] = []; for await (const line of rl) { - pipedInput += line; + pipedInputLines.push(line); } + const pipedInput = pipedInputLines.join("\n"); try { content = parseJSON(pipedInput) as Record; secretFormat = "json"; From d8a16e7ff2de6f912a8f3148d464b56cf0cb6f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Somhairle=20MacLe=C3=B2id?= Date: Mon, 1 Jun 2026 17:24:56 +0200 Subject: [PATCH 08/18] [vite-plugin] Add cf-vite delegate binary with dev subcommand (#13893) --- .changeset/cf-vite-delegate-binary.md | 7 + packages/vite-plugin-cloudflare/AGENTS.md | 37 ++++ packages/vite-plugin-cloudflare/bin/cf-vite | 2 + packages/vite-plugin-cloudflare/package.json | 4 + .../__tests__/resolve-plugin-config.spec.ts | 61 ++++++ .../vite-plugin-cloudflare/src/cf-vite.ts | 187 ++++++++++++++++++ .../src/plugin-config.ts | 14 +- .../vite-plugin-cloudflare/tsdown.config.ts | 14 ++ 8 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 .changeset/cf-vite-delegate-binary.md create mode 100755 packages/vite-plugin-cloudflare/bin/cf-vite create mode 100644 packages/vite-plugin-cloudflare/src/cf-vite.ts diff --git a/.changeset/cf-vite-delegate-binary.md b/.changeset/cf-vite-delegate-binary.md new file mode 100644 index 0000000000..fca9bd254b --- /dev/null +++ b/.changeset/cf-vite-delegate-binary.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Add an experimental, internal `cf-vite` delegate binary + +This adds an experimental `bin/cf-vite` binary that is spawned by Cloudflare's own parent tooling to drive the plugin as a long-running dev-server subprocess. It is not part of the plugin's public API surface, is not intended to be invoked directly, and its contract may change at any time without notice. diff --git a/packages/vite-plugin-cloudflare/AGENTS.md b/packages/vite-plugin-cloudflare/AGENTS.md index d5e7094666..dfb07d163b 100644 --- a/packages/vite-plugin-cloudflare/AGENTS.md +++ b/packages/vite-plugin-cloudflare/AGENTS.md @@ -7,6 +7,8 @@ Vite plugin for Cloudflare Workers development. Exports `cloudflare()` plugin fa ## STRUCTURE - `src/index.ts` — Plugin factory (uses top-level `await` for `assertWranglerVersion()`) +- `src/cf-vite.ts` — `cf-vite` delegate binary entry (see below) +- `bin/cf-vite` — shebang shim that dynamic-imports `dist/cf-vite.mjs` - `src/workers/` — 4 internal worker entries: `asset-worker`, `router-worker`, `runner-worker`, `vite-proxy-worker` - `playground/` — ~47 playground apps, each a workspace member (nested workspace under this package) - `e2e/` — E2E tests with Playwright @@ -16,8 +18,43 @@ Vite plugin for Cloudflare Workers development. Exports `cloudflare()` plugin fa - Only package using `tsdown` as build tool - Outputs ESM (`.mjs`) to `dist/index.mjs` +- `src/cf-vite.ts` is a second top-level tsdown entry, bundled to `dist/cf-vite.mjs` (no dts) - Also bundles 4 internal worker scripts from `src/workers/*/index.ts` as separate neutral-platform outputs to `dist/workers/` +## cf-vite DELEGATE BINARY (experimental / internal) + +`bin/cf-vite` is an experimental, internal delegate binary spawned by +Cloudflare's "cf-dev" parent process — NOT part of the plugin's public +API and not meant for direct end-user invocation. It is the sibling of +`@cloudflare/wrangler-bundler`'s `cf-wrangler` binary, and the two MUST +keep a shared spawn contract so the parent can drive either impl +interchangeably. + +- **Verb dispatch.** `cf-vite [flags]`. `dev` is the only verb + today; future verbs (`build`, `deploy`) follow the same shape. + Unknown/missing verbs exit `2` (this doubles as the parent's + version-detection signal — no JSON handshake). +- **Shared flag vocabulary.** Only `--config`, `--mode`, `--port`, + `--host`, `--local` are accepted, mirroring `cf-wrangler` exactly. + Parsed with `node:util.parseArgs` strict mode → unknown flags exit + `2`. Do NOT add flags here unless `cf-wrangler` grows them too. +- **Flag wiring.** `cf-vite` boots Vite via `createServer()` against the + user's own `vite.config.ts` (which must include `cloudflare()`). + Plugin-owned flags are bridged via env vars the plugin already reads + (`--config` → `CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH`, `--local` → + `CLOUDFLARE_VITE_FORCE_LOCAL`); Vite-owned flags go through inline + config (`--port`/`--host` → `server.*`, `--mode` → `mode`). +- **`--local`** forces remote bindings off. There is no plugin env knob + for `remoteBindings` other than `CLOUDFLARE_VITE_FORCE_LOCAL`, which + `resolvePluginConfig` in `plugin-config.ts` honours by overriding the + `remoteBindings` config option. Keep that override in sync if the flag + semantics change. +- **Hotkeys differ by design.** `cf-vite` uses Vite's own + `bindCLIShortcuts` (`h`/`r`/`q`/…), not wrangler's hotkey set. The + parent process should not assume identical hotkey UX across delegates. +- **Exit codes.** `0` graceful, `2` unknown verb / argv parse error, + `130` SIGINT, `143` SIGTERM. + ## CONVENTIONS - No named imports from `"wrangler"` — must use `import * as wrangler from "wrangler"` (namespace import only, enforced by eslint) diff --git a/packages/vite-plugin-cloudflare/bin/cf-vite b/packages/vite-plugin-cloudflare/bin/cf-vite new file mode 100755 index 0000000000..76b7d62164 --- /dev/null +++ b/packages/vite-plugin-cloudflare/bin/cf-vite @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import("../dist/cf-vite.mjs"); diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index 8a7e49d0b5..05c4ac1059 100644 --- a/packages/vite-plugin-cloudflare/package.json +++ b/packages/vite-plugin-cloudflare/package.json @@ -19,7 +19,11 @@ "url": "https://github.com/cloudflare/workers-sdk.git", "directory": "packages/vite-plugin-cloudflare" }, + "bin": { + "cf-vite": "./bin/cf-vite" + }, "files": [ + "bin", "dist" ], "type": "module", 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 3e4d78158b..4404c78a7e 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 @@ -708,6 +708,67 @@ describe("resolvePluginConfig - internal config path env fallback", () => { }); }); +describe("resolvePluginConfig - force-local env override", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "vite-plugin-test-")); + fs.mkdirSync(path.join(tempDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(tempDir, "wrangler.jsonc"), + JSON.stringify({ + name: "worker", + main: "./src/index.ts", + compatibility_date: "2024-01-01", + }) + ); + fs.writeFileSync(path.join(tempDir, "src/index.ts"), "export default {}"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + removeDirSync(tempDir); + }); + + const viteEnv = { mode: "development", command: "serve" as const }; + + test("remote bindings default to enabled", ({ expect }) => { + const result = resolvePluginConfig({}, { root: tempDir }, viteEnv); + expect(result.remoteBindings).toBe(true); + }); + + test("CLOUDFLARE_VITE_FORCE_LOCAL=true forces remote bindings off", ({ + expect, + }) => { + vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", "true"); + const result = resolvePluginConfig({}, { root: tempDir }, viteEnv); + expect(result.remoteBindings).toBe(false); + }); + + test("CLOUDFLARE_VITE_FORCE_LOCAL=true overrides remoteBindings: true in config", ({ + expect, + }) => { + vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", "true"); + const result = resolvePluginConfig( + { remoteBindings: true }, + { root: tempDir }, + viteEnv + ); + expect(result.remoteBindings).toBe(false); + }); + + test("unset CLOUDFLARE_VITE_FORCE_LOCAL respects remoteBindings config", ({ + expect, + }) => { + const result = resolvePluginConfig( + { remoteBindings: false }, + { root: tempDir }, + viteEnv + ); + expect(result.remoteBindings).toBe(false); + }); +}); + describe("resolvePluginConfig - defaults fill in missing fields", () => { let tempDir: string; diff --git a/packages/vite-plugin-cloudflare/src/cf-vite.ts b/packages/vite-plugin-cloudflare/src/cf-vite.ts new file mode 100644 index 0000000000..758917d046 --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/cf-vite.ts @@ -0,0 +1,187 @@ +/** + * `cf-vite` delegate binary entry point for `@cloudflare/vite-plugin`. + * + * EXPERIMENTAL / internal: spawned by Cloudflare's "cf-dev" parent + * process, not invoked directly by end users. Contract may change. + * + * Usage: `/bin/cf-vite [flags...]`. `dev` is the only + * verb today; future verbs follow the same shape. Unknown/missing verbs + * exit 2 (also the parent's version-detection signal). + * + * Spawn contract for `dev`: parent uses `stdio: "inherit"` and forwards + * SIGINT/SIGTERM. Accepted flags mirror the sibling `cf-wrangler` + * delegate (`--config`, `--mode`, `--port`, `--host`, `--local`) so the + * parent can drive either impl interchangeably; everything else lives in + * the user's `vite.config.ts` / `wrangler.jsonc`. `cf-vite` boots Vite + * via `createServer()` against the user's own config (expected to + * include `cloudflare()`); flags are bridged to it as documented inline. + * + * Exit codes: 0 graceful, 2 unknown verb / parse error, 130 SIGINT, + * 143 SIGTERM. + */ + +import { parseArgs as nodeParseArgs } from "node:util"; +import { createServer } from "vite"; +import type { InlineConfig, ServerOptions } from "vite"; + +interface DevArgs { + config?: string; + mode?: string; + port?: number; + host?: string; + local?: boolean; +} + +class ArgParseError extends Error { + constructor(message: string) { + super(message); + this.name = "ArgParseError"; + } +} + +/** Strict argv parser; mirrors `cf-wrangler`'s flags (unknown → throw). */ +function parseArgs(argv: string[]): DevArgs { + let parsed; + try { + parsed = nodeParseArgs({ + args: argv, + options: { + config: { type: "string" }, + mode: { type: "string" }, + host: { type: "string" }, + // `node:util.parseArgs` has no `number` type; coerce below. + port: { type: "string" }, + local: { type: "boolean" }, + }, + strict: true, + allowPositionals: false, + }); + } catch (err) { + throw new ArgParseError(err instanceof Error ? err.message : String(err)); + } + + const out: DevArgs = {}; + if (parsed.values.config !== undefined) { + out.config = parsed.values.config; + } + if (parsed.values.mode !== undefined) { + out.mode = parsed.values.mode; + } + if (parsed.values.host !== undefined) { + out.host = parsed.values.host; + } + if (parsed.values.port !== undefined) { + const raw = parsed.values.port; + const n = Number(raw); + // TCP port range; 0 = OS-assigned. + if (!Number.isInteger(n) || n < 0 || n > 65535) { + throw new ArgParseError( + `--port expects an integer between 0 and 65535, got "${raw}"` + ); + } + out.port = n; + } + if (parsed.values.local !== undefined) { + out.local = parsed.values.local; + } + + return out; +} + +async function main(): Promise { + // argv: [0] node [1] cf-vite.mjs [2] verb [3+] forwarded flags. + const verb = process.argv[2]; + const userArgv = process.argv.slice(3); + + if (verb !== "dev") { + // Format mirrors `cf-wrangler`. + process.stderr.write( + `Error: unknown subcommand "${verb ?? ""}".\n` + + `Usage: cf-vite dev [args]\n` + ); + return 2; + } + + let args: DevArgs; + try { + args = parseArgs(userArgv); + } catch (err) { + if (err instanceof ArgParseError) { + process.stderr.write(`Error: ${err.message}\n`); + return 2; + } + throw err; + } + + // Bridge plugin-owned flags via env vars the plugin reads during config + // resolution — must be set before `createServer()`. Precedence vs the + // user's `cloudflare()` options differs per flag, by design: + // - `--config`: an explicit `cloudflare({ configPath })` wins, since + // this maps to the existing `CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH` + // discovery fallback (`configPath ?? env`). A pinned configPath is + // a deliberate user choice, so it stays authoritative. + // - `--local`: overrides `remoteBindings` outright, mirroring + // `wrangler dev --local` (force local even if config opts into + // remote). + if (args.config !== undefined) { + process.env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = args.config; + } + if (args.local) { + process.env.CLOUDFLARE_VITE_FORCE_LOCAL = "true"; + } + + // Bridge Vite-owned flags via inline config; the rest auto-loads from + // the user's `vite.config.{ts,js,mjs}` (which supplies `cloudflare()`). + const serverOptions: ServerOptions = {}; + if (args.port !== undefined) { + serverOptions.port = args.port; + } + if (args.host !== undefined) { + serverOptions.host = args.host; + } + const inlineConfig: InlineConfig = { server: serverOptions }; + if (args.mode !== undefined) { + inlineConfig.mode = args.mode; + } + + const server = await createServer(inlineConfig); + + await server.listen(); + server.printUrls(); + server.bindCLIShortcuts({ print: true }); + + // Close cleanly on signal so the dev registry entry, workerd + // subprocess, and ports release. `closing` guards against a + // double-signal (e.g. rapid Ctrl+C, or SIGINT then SIGTERM) racing + // two `server.close()` / `process.exit()` calls. + let closing = false; + const shutdown = (signal: NodeJS.Signals) => { + if (closing) { + return; + } + closing = true; + // Signal handlers can't be async; void the IIFE to avoid unhandled + // rejections if `close()` throws. + void (async () => { + try { + await server.close(); + process.exit(0); + } catch { + // Forced-exit: terminate with the 128+N code rather than + // hang. SIGINT=130, SIGTERM=143. + process.exit(signal === "SIGINT" ? 130 : 143); + } + })(); + }; + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); + + // Keep the event loop alive until a signal handler exits; Vite's own + // handles (HTTP listener, watchers, workerd) hold the process open. + return new Promise(() => {}); +} + +// Let unhandled rejections propagate — Node prints the stack and exits +// non-zero, which the parent surfaces via inherited stdio. +const exitCode = await main(); +process.exit(exitCode); diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index aa152fa16a..6ead0b6f40 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -333,10 +333,18 @@ export function resolvePluginConfig( // wrangler when it loads the worker configuration files. Object.assign(process.env, prefixedEnv); + // The `cf-vite` delegate binary's `--local` flag sets this env var to + // force remote bindings off, overriding any `remoteBindings` value in the + // plugin config (mirrors `wrangler dev --local`). + const remoteBindings = + prefixedEnv.CLOUDFLARE_VITE_FORCE_LOCAL === "true" + ? false + : (pluginConfig.remoteBindings ?? true); + if (viteEnv.isPreview) { return { ...shared, - remoteBindings: pluginConfig.remoteBindings ?? true, + remoteBindings, type: "preview", workers: getWorkerConfigs(root, !!process.env.CLOUDFLARE_VITE_BUILD), }; @@ -425,7 +433,7 @@ export function resolvePluginConfig( environmentNameToChildEnvironmentNamesMap, prerenderWorkerEnvironmentName, configPaths, - remoteBindings: pluginConfig.remoteBindings ?? true, + remoteBindings, rawConfigs: { entryWorker: entryWorkerResolvedConfig, }, @@ -532,7 +540,7 @@ export function resolvePluginConfig( prerenderWorkerEnvironmentName, entryWorkerEnvironmentName, staticRouting, - remoteBindings: pluginConfig.remoteBindings ?? true, + remoteBindings, rawConfigs: { entryWorker: entryWorkerResolvedConfig, auxiliaryWorkers: auxiliaryWorkersResolvedConfigs, diff --git a/packages/vite-plugin-cloudflare/tsdown.config.ts b/packages/vite-plugin-cloudflare/tsdown.config.ts index c5b2ebe9ac..3fa1b69cc7 100644 --- a/packages/vite-plugin-cloudflare/tsdown.config.ts +++ b/packages/vite-plugin-cloudflare/tsdown.config.ts @@ -26,6 +26,20 @@ export default defineConfig([ }, ignoreWatch, }, + { + // `cf-vite` delegate-binary entry. Bundled separately from the + // plugin itself so the bin shim (`bin/cf-vite`) can + // dynamic-import it without dragging the plugin's type-export + // overhead. The entry exposes a small subcommand-based CLI + // (currently just `dev`) that any parent process can spawn + // (see `src/cf-vite.ts` for the protocol). + entry: "src/cf-vite.ts", + platform: "node", + outDir: "dist", + tsconfig: "tsconfig.plugin.json", + dts: false, + ignoreWatch, + }, worker("asset-worker"), worker("router-worker"), worker("runner-worker", { From 8b4e9174a496ede02b97ed81779d1e3f450b7d53 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 16:26:53 +0100 Subject: [PATCH 09/18] In C3 hide non-framework categories when `--platform=pages` is specified (#14095) --- .changeset/filter-categories-by-platform.md | 7 ++ .../e2e/tests/cli/cli.test.ts | 95 ++++++++++++++++++- .../create-cloudflare/src/helpers/args.ts | 10 ++ packages/create-cloudflare/src/templates.ts | 34 ++++++- 4 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 .changeset/filter-categories-by-platform.md diff --git a/.changeset/filter-categories-by-platform.md b/.changeset/filter-categories-by-platform.md new file mode 100644 index 0000000000..f825727373 --- /dev/null +++ b/.changeset/filter-categories-by-platform.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": minor +--- + +Hide non-framework categories when `--platform=pages` is specified + +When running C3 with `--platform=pages`, the "Hello World example" and "Application Starter" categories are now hidden since they only produce Workers projects. The framework list is also filtered to only show frameworks that support the Pages platform. This makes it clear that C3 can only create Pages projects when using a framework. diff --git a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts index 3f5aa4be10..2e8513a1bb 100644 --- a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts +++ b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts @@ -740,7 +740,7 @@ describe("Create Cloudflare CLI", () => { }); describe("frameworks related", () => { - ["solid", "next"].forEach((framework) => + ["solid", "next", "react-router", "analog"].forEach((framework) => test(`error when trying to create a ${framework} app on Pages`, async ({ expect, logStream, @@ -839,6 +839,99 @@ describe("Create Cloudflare CLI", () => { expect(output).not.toContain("Select a variant"); }); }); + + describe.skipIf(isExperimental)("platform filtering", () => { + test.skipIf(isWindows)( + "--platform=pages hides non-framework categories", + async ({ expect, logStream, project }) => { + const { output } = await runC3( + [project.path, "--platform=pages"], + [ + { + matcher: /What would you like to start with\?/, + input: { + type: "select", + target: "Framework Starter", + // "Framework Starter" should be the default (first visible) + // option since "Hello World example" is hidden + assertDefaultSelection: "Framework Starter", + }, + }, + { + matcher: /Which development framework do you want to use\?/, + input: { + type: "select", + target: "Angular", + }, + }, + { + matcher: /Do you want to use git for version control/, + input: ["n"], + }, + { + matcher: /Do you want to deploy your application/, + input: ["n"], + }, + ], + logStream + ); + + expect(output).toContain("category Framework Starter"); + expect(output).not.toContain("Hello World"); + expect(output).not.toContain("Application Starter"); + } + ); + + test.skipIf(isWindows)( + "--platform=pages filters out workers-only frameworks", + async ({ expect, logStream, project }) => { + const { output } = await runC3( + [project.path, "--platform=pages", "--category=web-framework"], + [ + { + matcher: /Which development framework do you want to use\?/, + input: { + type: "select", + target: "Angular", + }, + }, + { + matcher: /Do you want to use git for version control/, + input: ["n"], + }, + { + matcher: /Do you want to deploy your application/, + input: ["n"], + }, + ], + logStream + ); + + // Workers-only frameworks should not appear in the output + expect(output).not.toContain("Next"); + expect(output).not.toContain("SolidStart"); + expect(output).not.toContain("React Router"); + expect(output).not.toContain("Analog"); + } + ); + + ["hello-world", "demo"].forEach((category) => + test(`--platform=pages --category=${category} errors for workers-only category`, async ({ + expect, + logStream, + project, + }) => { + const { errors } = await runC3( + [project.path, "--platform=pages", `--category=${category}`], + [], + logStream + ); + expect(errors).toContain( + `The "${category}" category is not available for the "pages" platform` + ); + }) + ); + }); }); function normalizeOutput(output: string): string { diff --git a/packages/create-cloudflare/src/helpers/args.ts b/packages/create-cloudflare/src/helpers/args.ts index b5178e991a..4964b16c13 100644 --- a/packages/create-cloudflare/src/helpers/args.ts +++ b/packages/create-cloudflare/src/helpers/args.ts @@ -69,8 +69,18 @@ export const cliDefinition: ArgumentsDefinition = { description: `Specifies the kind of templates that should be created`, values(args) { const experimental = Boolean(args?.["experimental"]); + const platform = args?.["platform"] as string | undefined; if (experimental) { return [{ name: "web-framework", description: "Framework Starter" }]; + } else if (platform === "pages") { + // Only framework starters can produce Pages projects + return [ + { name: "web-framework", description: "Framework Starter" }, + { + name: "remote-template", + description: "Template from a GitHub repo", + }, + ]; } else { return [ { name: "hello-world", description: "Hello World Starter" }, diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index 4e7d12fe0e..d3ff105c74 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -478,7 +478,9 @@ export const createContext = async ( }); const categoryOptions = []; - if (Object.keys(helloWorldTemplateMap).length) { + // Hello World and Application Starter templates are all Workers-only, + // so hide them when the user has explicitly targeted the Pages platform + if (args.platform !== "pages" && Object.keys(helloWorldTemplateMap).length) { categoryOptions.push({ label: "Hello World example", value: "hello-world", @@ -492,7 +494,7 @@ export const createContext = async ( description: "Select from the most popular full-stack web frameworks", }); } - if (Object.keys(otherTemplateMap).length) { + if (args.platform !== "pages" && Object.keys(otherTemplateMap).length) { categoryOptions.push({ label: "Application Starter", value: "demo", @@ -501,6 +503,9 @@ export const createContext = async ( }); } categoryOptions.push( + // TODO: hide "Template from a GitHub repo" when a --platform arg is + // provided (whether workers or pages), since remote templates have no + // platform validation and would ignore the user's platform choice { label: "Template from a GitHub repo", value: "remote-template", @@ -524,6 +529,19 @@ export const createContext = async ( return goBack("category"); } + // Validate that the selected category is compatible with the target platform. + // This catches the case where a user passes e.g. --platform=pages --category=hello-world + // directly via CLI args, bypassing the interactive prompt filtering. + if ( + args.platform === "pages" && + category !== "web-framework" && + category !== "remote-template" + ) { + throw new Error( + `The "${category}" category is not available for the "pages" platform` + ); + } + let template: TemplateConfig; if (category === "web-framework") { @@ -532,6 +550,18 @@ export const createContext = async ( // only hide if we're going to show the options - otherwise, the // result will show up as (skipped) instead of the actual value if (!config.hidden || args.framework) { + // When a platform is specified, filter out frameworks that don't + // support that platform (e.g. workers-only frameworks when + // --platform=pages) + if (args.platform && !args.framework) { + const supportsTargetPlatform = + "platformVariants" in config + ? args.platform in config.platformVariants + : config.platform === args.platform; + if (!supportsTargetPlatform) { + return acc; + } + } acc.push({ label: config.displayName, value: key, From b210c5eefdb22d83f937728527bc0091f9308070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Mon, 1 Jun 2026 16:30:34 +0100 Subject: [PATCH 10/18] [wrangler] Add re-authentication hint to account fetch errors (#14141) --- .changeset/add-reauth-hint-to-account-errors.md | 7 +++++++ packages/wrangler/src/__tests__/deploy/core.test.ts | 1 + packages/wrangler/src/__tests__/kv/key.test.ts | 2 +- packages/wrangler/src/__tests__/pages/secret.test.ts | 3 ++- packages/wrangler/src/__tests__/secret.test.ts | 3 ++- packages/wrangler/src/user/fetch-accounts.ts | 7 ++++--- 6 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/add-reauth-hint-to-account-errors.md diff --git a/.changeset/add-reauth-hint-to-account-errors.md b/.changeset/add-reauth-hint-to-account-errors.md new file mode 100644 index 0000000000..7af4104143 --- /dev/null +++ b/.changeset/add-reauth-hint-to-account-errors.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Add re-authentication hint to account fetch error messages + +When Wrangler fails to automatically retrieve account IDs, the error messages now suggest running `wrangler login` as a troubleshooting step. This addresses confusion for users who encounter these errors after OAuth system changes or other authentication issues. diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index cc53109208..dc688b98b9 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -1033,6 +1033,7 @@ describe("deploy", () => { In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your Wrangler configuration file. + Alternatively, try running \`wrangler login\` to re-authenticate. " `); diff --git a/packages/wrangler/src/__tests__/kv/key.test.ts b/packages/wrangler/src/__tests__/kv/key.test.ts index 9b3d663024..ac73ea68c0 100644 --- a/packages/wrangler/src/__tests__/kv/key.test.ts +++ b/packages/wrangler/src/__tests__/kv/key.test.ts @@ -1258,7 +1258,7 @@ describe("kv", () => { ).rejects.toThrowErrorMatchingInlineSnapshot(` [Error: Failed to automatically retrieve account IDs for the logged in user. You may have incorrect permissions on your API token, or an environment variable such as CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY, or CLOUDFLARE_EMAIL may be set to an invalid value. - Check your environment and unset or correct any Cloudflare credential variables, or run \`wrangler logout\` followed by \`wrangler login\` to re-authenticate. + Check your environment and unset or correct any Cloudflare credential variables, or run \`wrangler login\` to re-authenticate. You can also skip this account check by adding an \`account_id\` in your Wrangler configuration file, or by setting the value of CLOUDFLARE_ACCOUNT_ID] `); }); diff --git a/packages/wrangler/src/__tests__/pages/secret.test.ts b/packages/wrangler/src/__tests__/pages/secret.test.ts index a933fc7072..ef34c72fe4 100644 --- a/packages/wrangler/src/__tests__/pages/secret.test.ts +++ b/packages/wrangler/src/__tests__/pages/secret.test.ts @@ -284,7 +284,8 @@ describe("wrangler pages secret", () => { runWrangler("pages secret put the-key --project some-project-name") ).rejects.toThrowErrorMatchingInlineSnapshot(` [Error: Failed to automatically retrieve account IDs for the logged in user. - In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your Wrangler configuration file.] + In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your Wrangler configuration file. + Alternatively, try running \`wrangler login\` to re-authenticate.] `); }); diff --git a/packages/wrangler/src/__tests__/secret.test.ts b/packages/wrangler/src/__tests__/secret.test.ts index 3915253c62..0e18885534 100644 --- a/packages/wrangler/src/__tests__/secret.test.ts +++ b/packages/wrangler/src/__tests__/secret.test.ts @@ -437,7 +437,8 @@ describe("wrangler secret", () => { await expect(runWrangler("secret put the-key --name script-name")) .rejects.toThrowErrorMatchingInlineSnapshot(` [Error: Failed to automatically retrieve account IDs for the logged in user. - In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your Wrangler configuration file.] + In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your Wrangler configuration file. + Alternatively, try running \`wrangler login\` to re-authenticate.] `); }); diff --git a/packages/wrangler/src/user/fetch-accounts.ts b/packages/wrangler/src/user/fetch-accounts.ts index d1f6e9162b..7675eb3fe2 100644 --- a/packages/wrangler/src/user/fetch-accounts.ts +++ b/packages/wrangler/src/user/fetch-accounts.ts @@ -85,14 +85,14 @@ export async function fetchAllAccounts( throw new UserError( `Failed to automatically retrieve account IDs for the logged in user. You may have incorrect permissions on your API token, or an environment variable such as CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY, or CLOUDFLARE_EMAIL may be set to an invalid value. -Check your environment and unset or correct any Cloudflare credential variables, or run \`wrangler logout\` followed by \`wrangler login\` to re-authenticate. +Check your environment and unset or correct any Cloudflare credential variables, or run \`wrangler login\` to re-authenticate. You can also skip this account check by adding an \`account_id\` in your ${configFileName(undefined)} file, or by setting the value of CLOUDFLARE_ACCOUNT_ID`, { telemetryMessage: "user account fetch permission denied" } ); } throw new UserError( `Failed to automatically retrieve account IDs for the logged in user. -You may have incorrect permissions on your API token. You can skip this account check by adding an \`account_id\` in your ${configFileName(undefined)} file, or by setting the value of CLOUDFLARE_ACCOUNT_ID`, +You may have incorrect permissions on your API token, or your authentication may have expired. Try running \`wrangler login\` to re-authenticate. You can also skip this account check by adding an \`account_id\` in your ${configFileName(undefined)} file, or by setting the value of CLOUDFLARE_ACCOUNT_ID`, { telemetryMessage: "user account fetch permission denied" } ); } else { @@ -108,7 +108,8 @@ You may have incorrect permissions on your API token. You can skip this account if (usableAccounts.length === 0 && throwOnEmpty) { throw new UserError( `Failed to automatically retrieve account IDs for the logged in user. -In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your ${configFileName(undefined)} file.`, +In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your ${configFileName(undefined)} file. +Alternatively, try running \`wrangler login\` to re-authenticate.`, { telemetryMessage: "user account fetch empty" } ); } From 337e9124cfa461a99ce7ffb800dcc341f7b2f026 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 16:38:59 +0100 Subject: [PATCH 11/18] Remove trailing periods from URLs in terminal output (#14105) --- .changeset/fix-trailing-period-urls.md | 10 ++++++++++ .../src/__tests__/utils/agent-prompt.test.ts | 2 +- packages/local-explorer-ui/src/utils/agent-prompt.ts | 2 +- packages/miniflare/src/http/fetch.ts | 2 +- packages/miniflare/src/runtime/structured-logs.ts | 2 +- packages/miniflare/src/workers/email/validate.ts | 2 +- packages/miniflare/test/http/fetch.spec.ts | 4 ++-- packages/miniflare/test/plugins/email/index.spec.ts | 2 +- packages/vitest-pool-workers/src/worker/entrypoints.ts | 2 +- packages/vitest-pool-workers/test/validation.test.ts | 2 +- packages/workers-utils/src/tunnel.ts | 8 ++++---- packages/workers-utils/tests/tunnel.test.ts | 6 +++--- packages/wrangler/src/__tests__/deploy/core.test.ts | 2 +- packages/wrangler/src/__tests__/deploy/formats.test.ts | 6 +++--- packages/wrangler/src/__tests__/dev.test.ts | 2 +- .../src/api/startDevWorker/ConfigController.ts | 2 +- .../esbuild-plugins/cloudflare-internal.ts | 2 +- .../esbuild-plugins/hybrid-nodejs-compat.ts | 2 +- .../deployment-bundle/esbuild-plugins/nodejs-compat.ts | 2 +- packages/wrangler/src/user/whoami.ts | 2 +- 20 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 .changeset/fix-trailing-period-urls.md diff --git a/.changeset/fix-trailing-period-urls.md b/.changeset/fix-trailing-period-urls.md new file mode 100644 index 0000000000..4cf927ef0e --- /dev/null +++ b/.changeset/fix-trailing-period-urls.md @@ -0,0 +1,10 @@ +--- +"wrangler": patch +"miniflare": patch +"@cloudflare/vitest-pool-workers": patch +"@cloudflare/workers-utils": patch +--- + +Remove trailing periods from URLs in terminal output + +URLs printed to the terminal with a sentence-ending period (e.g. `https://example.com/path.`) would include the period when clicked in some terminal emulators, causing 404 errors. This removes trailing periods from all URLs displayed in CLI output across wrangler, miniflare, vitest-pool-workers, and workers-utils. diff --git a/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts b/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts index 982fc0aba0..c150d49474 100644 --- a/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts +++ b/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts @@ -21,7 +21,7 @@ describe("llm-prompt utils", () => { ); expect(prompt).toContain( - "API endpoint: http://localhost:8787/cdn-cgi/explorer/api." + "API endpoint: http://localhost:8787/cdn-cgi/explorer/api" ); expect(prompt).toContain( "Fetch the OpenAPI schema from http://localhost:8787/cdn-cgi/explorer/api" diff --git a/packages/local-explorer-ui/src/utils/agent-prompt.ts b/packages/local-explorer-ui/src/utils/agent-prompt.ts index efe1b3ceb8..c9843e5e53 100644 --- a/packages/local-explorer-ui/src/utils/agent-prompt.ts +++ b/packages/local-explorer-ui/src/utils/agent-prompt.ts @@ -1,5 +1,5 @@ const AGENT_PROMPT_TEMPLATE = `You have access to local Cloudflare services (KV, R2, D1, Durable Objects, and Workflows) for this app via the Explorer API. -API endpoint: {{apiEndpoint}}. +API endpoint: {{apiEndpoint}} Fetch the OpenAPI schema from {{apiEndpoint}} to discover available operations. Use these endpoints to list, query, and manage local resources during development.`; /** diff --git a/packages/miniflare/src/http/fetch.ts b/packages/miniflare/src/http/fetch.ts index 0feccfbd7e..127e39833c 100644 --- a/packages/miniflare/src/http/fetch.ts +++ b/packages/miniflare/src/http/fetch.ts @@ -27,7 +27,7 @@ export async function fetch( const url = new URL(request.url); if (url.protocol !== "http:" && url.protocol !== "https:") { throw new TypeError( - `Fetch API cannot load: ${url.toString()}.\nMake sure you're using http(s):// URLs for WebSocket requests via fetch.` + `Fetch API cannot load: ${url.toString()}\nMake sure you're using http(s):// URLs for WebSocket requests via fetch.` ); } url.protocol = url.protocol.replace("http", "ws"); diff --git a/packages/miniflare/src/runtime/structured-logs.ts b/packages/miniflare/src/runtime/structured-logs.ts index f31210cbea..ba6c892ff8 100644 --- a/packages/miniflare/src/runtime/structured-logs.ts +++ b/packages/miniflare/src/runtime/structured-logs.ts @@ -126,7 +126,7 @@ function wrapStructuredLogsHandler( error += "\nOn Windows, this may be caused by an outdated Microsoft Visual C++ Redistributable library.\n" + "Check that you have the latest version installed.\n" + - "See https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist."; + "See https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist"; } return structuredLogsHandler({ diff --git a/packages/miniflare/src/workers/email/validate.ts b/packages/miniflare/src/workers/email/validate.ts index 61f32cff3c..b9e0883cd8 100644 --- a/packages/miniflare/src/workers/email/validate.ts +++ b/packages/miniflare/src/workers/email/validate.ts @@ -41,7 +41,7 @@ export async function isEmailReplyable( if ((email.references.match(/@/g)?.length ?? 0) >= 100) { await log( red( - 'The incoming email\'s "References" header has more than 100 entries. As such, your Worker cannot respond to this email. Refer to https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/.' + 'The incoming email\'s "References" header has more than 100 entries. As such, your Worker cannot respond to this email. Refer to https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/' ) ); return false; diff --git a/packages/miniflare/test/http/fetch.spec.ts b/packages/miniflare/test/http/fetch.spec.ts index 0498b9584e..a7fa9df9f1 100644 --- a/packages/miniflare/test/http/fetch.spec.ts +++ b/packages/miniflare/test/http/fetch.spec.ts @@ -195,7 +195,7 @@ test("fetch: throws on ws(s) protocols", async ({ expect }) => { }) ).rejects.toThrow( new TypeError( - "Fetch API cannot load: ws://localhost/.\nMake sure you're using http(s):// URLs for WebSocket requests via fetch." + "Fetch API cannot load: ws://localhost/\nMake sure you're using http(s):// URLs for WebSocket requests via fetch." ) ); await expect( @@ -204,7 +204,7 @@ test("fetch: throws on ws(s) protocols", async ({ expect }) => { }) ).rejects.toThrow( new TypeError( - "Fetch API cannot load: wss://localhost/.\nMake sure you're using http(s):// URLs for WebSocket requests via fetch." + "Fetch API cannot load: wss://localhost/\nMake sure you're using http(s):// URLs for WebSocket requests via fetch." ) ); }); diff --git a/packages/miniflare/test/plugins/email/index.spec.ts b/packages/miniflare/test/plugins/email/index.spec.ts index 1bfa97f1d3..17618484f6 100644 --- a/packages/miniflare/test/plugins/email/index.spec.ts +++ b/packages/miniflare/test/plugins/email/index.spec.ts @@ -629,7 +629,7 @@ test("reply validation: >100 References", async ({ expect }) => { expect((await res.text()).includes("Original email is not replyable")); expect(log.logs[1][0]).toBe(LogLevel.ERROR); expect(log.logs[1][1].split("\n")[0]).toBe( - 'The incoming email\'s "References" header has more than 100 entries. As such, your Worker cannot respond to this email. Refer to https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/.' + 'The incoming email\'s "References" header has more than 100 entries. As such, your Worker cannot respond to this email. Refer to https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/' ); }); diff --git a/packages/vitest-pool-workers/src/worker/entrypoints.ts b/packages/vitest-pool-workers/src/worker/entrypoints.ts index 47e18a8729..6cffde8076 100644 --- a/packages/vitest-pool-workers/src/worker/entrypoints.ts +++ b/packages/vitest-pool-workers/src/worker/entrypoints.ts @@ -291,7 +291,7 @@ async function getWorkerEntrypointExport( if (!entrypointValue) { const message = `${mainPath} does not export a ${entrypoint} entrypoint. \`@cloudflare/vitest-pool-workers\` does not support service workers or named entrypoints for \`SELF\`.\n` + - "If you're using service workers, please migrate to the modules format: https://developers.cloudflare.com/workers/reference/migrate-to-module-workers."; + "If you're using service workers, please migrate to the modules format: https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/"; throw new TypeError(message); } return { mainPath, entrypointValue }; diff --git a/packages/vitest-pool-workers/test/validation.test.ts b/packages/vitest-pool-workers/test/validation.test.ts index 6e3141dc5f..f43fdea5e7 100644 --- a/packages/vitest-pool-workers/test/validation.test.ts +++ b/packages/vitest-pool-workers/test/validation.test.ts @@ -103,7 +103,7 @@ test( expect(await result.exitCode).toBe(1); expected = dedent` ${path.join(tmpPathName, "index.ts")} does not export a default entrypoint. \`@cloudflare/vitest-pool-workers\` does not support service workers or named entrypoints for \`SELF\`. - If you're using service workers, please migrate to the modules format: https://developers.cloudflare.com/workers/reference/migrate-to-module-workers. + If you're using service workers, please migrate to the modules format: https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ `; expect(result.stderr).toMatch(expected); } diff --git a/packages/workers-utils/src/tunnel.ts b/packages/workers-utils/src/tunnel.ts index c8f4efe5e4..a8a7420f77 100644 --- a/packages/workers-utils/src/tunnel.ts +++ b/packages/workers-utils/src/tunnel.ts @@ -147,9 +147,9 @@ export function startTunnel(options: TunnelOptions): Tunnel { logger?.log( `${ publicURL - ? `The tunnel is still open at ${publicURL}.` - : "The tunnel is still open." - } It expires in ${formatTunnelDuration(remainingMs)}. ${options.extendHint ?? ""}` + ? `Tunnel still open, expires in ${formatTunnelDuration(remainingMs)}: ${publicURL}` + : `The tunnel is still open. It expires in ${formatTunnelDuration(remainingMs)}.` + }${options.extendHint ? ` ${options.extendHint}` : ""}` ); }, reminderIntervalMs); reminderInterval.unref?.(); @@ -326,7 +326,7 @@ function createTunnelStartupError( const errorMessage = `${message}\n` + `cloudflared output:\n${stderrOutput || "(no output)"}\n\n` + - `The local dev server started at ${origin.href}.\n` + + `The local dev server started at ${origin.href}\n` + (isQuickTunnelRateLimited ? "Cloudflare Quick Tunnel creation was rate limited. Try again in a few minutes, or use a named tunnel if you need more reliable access." : `Check the cloudflared output above for more details, and verify that ${origin.href} is reachable from this machine if this keeps happening.`); diff --git a/packages/workers-utils/tests/tunnel.test.ts b/packages/workers-utils/tests/tunnel.test.ts index 24f362b6b6..451edcb8eb 100644 --- a/packages/workers-utils/tests/tunnel.test.ts +++ b/packages/workers-utils/tests/tunnel.test.ts @@ -355,7 +355,7 @@ describe("startTunnel", () => { "Cloudflare Quick Tunnel creation was rate limited." ); await expect(() => tunnel.ready()).rejects.toThrow( - "The local dev server started at http://localhost:8787/." + "The local dev server started at http://localhost:8787/" ); await expect(() => tunnel.ready()).rejects.toBeInstanceOf(UserError); }); @@ -387,7 +387,7 @@ describe("startTunnel", () => { await vi.advanceTimersByTimeAsync(60_000); expect(logger.log).toHaveBeenCalledWith( - "The tunnel is still open at https://my-tunnel.trycloudflare.com. It expires in 1m. Press [t] to extend by 1 hour." + "Tunnel still open, expires in 1m: https://my-tunnel.trycloudflare.com Press [t] to extend by 1 hour." ); await vi.advanceTimersByTimeAsync(60_000); @@ -429,7 +429,7 @@ describe("startTunnel", () => { ); expect(killSpy).not.toHaveBeenCalled(); expect(logger.log).toHaveBeenCalledWith( - "The tunnel is still open at https://my-tunnel.trycloudflare.com. It expires in 1m. " + "Tunnel still open, expires in 1m: https://my-tunnel.trycloudflare.com" ); await vi.advanceTimersByTimeAsync(60_000); diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index dc688b98b9..979e6a6574 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -529,7 +529,7 @@ describe("deploy", () => { ├─┼─┤ │ Account Three │ account-3 │ └─┴─┘ - 🔓 To see token permissions, visit https://dash.cloudflare.com/profile/api-tokens", + 🔓 To see token permissions visit https://dash.cloudflare.com/profile/api-tokens", "warn": "", } `); diff --git a/packages/wrangler/src/__tests__/deploy/formats.test.ts b/packages/wrangler/src/__tests__/deploy/formats.test.ts index 5f02d7b3b4..3a731da748 100644 --- a/packages/wrangler/src/__tests__/deploy/formats.test.ts +++ b/packages/wrangler/src/__tests__/deploy/formats.test.ts @@ -484,7 +484,7 @@ describe("deploy", () => { Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. [plugin cloudflare-internal-imports]" + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ [plugin cloudflare-internal-imports]" `); }); @@ -517,7 +517,7 @@ describe("deploy", () => { Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. [plugin nodejs_compat-imports]" + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ [plugin nodejs_compat-imports]" `); }); @@ -551,7 +551,7 @@ describe("deploy", () => { Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. [plugin hybrid-nodejs_compat]" + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ [plugin hybrid-nodejs_compat]" `); }); }); diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index a5b5777aeb..83bdf82eca 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -1287,7 +1287,7 @@ describe.sequential("wrangler dev", () => { "▲ [WARNING] Setting upstream-protocol to http is not currently supported for remote mode. If this is required in your project, please add your use case to the following issue: - https://github.com/cloudflare/workers-sdk/issues/583. + https://github.com/cloudflare/workers-sdk/issues/583 " `); diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 13b3dbc74b..4383bfcb16 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -409,7 +409,7 @@ async function resolveConfig( logger.once.warn( "Setting upstream-protocol to http is not currently supported for remote mode.\n" + "If this is required in your project, please add your use case to the following issue:\n" + - "https://github.com/cloudflare/workers-sdk/issues/583." + "https://github.com/cloudflare/workers-sdk/issues/583" ); } diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts index cb52fbef17..dc93df15ca 100644 --- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts @@ -31,7 +31,7 @@ export const cloudflareInternalPlugin: Plugin = { Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ `, }, ], diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts index 39a0e9ee60..a8e23c9987 100644 --- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts @@ -90,7 +90,7 @@ function errorOnServiceWorkerFormat( Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ `, }, ], diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts index ea3d5e146a..94dfaf60e9 100644 --- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts @@ -79,7 +79,7 @@ export const nodejsCompatPlugin = (mode: NodeJSCompatMode): Plugin => ({ Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. - See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. + See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ `, }, ], diff --git a/packages/wrangler/src/user/whoami.ts b/packages/wrangler/src/user/whoami.ts index 076a630954..160e8cc96d 100644 --- a/packages/wrangler/src/user/whoami.ts +++ b/packages/wrangler/src/user/whoami.ts @@ -173,7 +173,7 @@ function printTokenPermissions(user: UserInfo) { user.tokenPermissions?.map((scope) => scope.split(":")) ?? []; if (user.authType !== "OAuth Token") { return void logger.log( - `🔓 To see token permissions, visit https://dash.cloudflare.com/${user.authType === "User API Token" ? "profile" : user.accounts[0].id}/api-tokens` + `🔓 To see token permissions visit https://dash.cloudflare.com/${user.authType === "User API Token" ? "profile" : user.accounts[0].id}/api-tokens` ); } logger.log(`🔓 Token Permissions:`); From 3a746ac56a40b805e38f26ef5328e44917b543e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Somhairle=20MacLe=C3=B2id?= Date: Mon, 1 Jun 2026 18:00:26 +0200 Subject: [PATCH 12/18] [tools] Lint that all non-bundled deps of published packages are pinned (#14112) Co-authored-by: Dario Piotrowicz Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- .changeset/pin-non-bundled-dependencies.md | 11 + CONTRIBUTING.md | 11 + package.json | 3 +- packages/cli/package.json | 8 +- packages/miniflare/package.json | 2 +- packages/vitest-pool-workers/package.json | 4 +- packages/workers-editor-shared/package.json | 2 +- packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 391 ++++++++---------- pnpm-workspace.yaml | 30 +- tools/README.md | 3 + .../validate-pinned-dependencies.test.ts | 198 +++++++++ tools/deployments/validate-catalog-usage.ts | 43 +- .../validate-package-dependencies.ts | 1 + .../validate-pinned-dependencies.ts | 174 ++++++++ 15 files changed, 629 insertions(+), 254 deletions(-) create mode 100644 .changeset/pin-non-bundled-dependencies.md create mode 100644 tools/deployments/__tests__/validate-pinned-dependencies.test.ts create mode 100644 tools/deployments/validate-pinned-dependencies.ts diff --git a/.changeset/pin-non-bundled-dependencies.md b/.changeset/pin-non-bundled-dependencies.md new file mode 100644 index 0000000000..ad16714373 --- /dev/null +++ b/.changeset/pin-non-bundled-dependencies.md @@ -0,0 +1,11 @@ +--- +"wrangler": patch +"miniflare": patch +"@cloudflare/vitest-pool-workers": patch +"@cloudflare/workers-editor-shared": patch +"@cloudflare/cli-shared-helpers": patch +--- + +Pin non-bundled runtime dependencies to exact versions + +Dependencies that are not bundled into a package's published output are installed directly into consumers' dependency trees, so they are now pinned to exact versions instead of semver ranges. This closes a supply-chain gap where an unpinned external dependency could resolve to a compromised upstream release on a fresh install. A new `pnpm check:pinned-deps` lint enforces this for all published packages (and for the shared pnpm catalog) going forward. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23f19e80ce..cf4145d354 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -457,6 +457,17 @@ export const EXTERNAL_DEPENDENCIES = [ - **Runtime resolution**: Packages like `esbuild` or `unenv` that need to be resolved when bundling user code - **Peer dependencies**: Packages the user is expected to provide (e.g., `react`, `vite`) +### Pinning External Dependencies + +Because external dependencies are installed into downstream users' dependency trees rather than bundled, their versions must be **pinned to an exact version** (e.g. `1.2.3`, not `^1.2.3`). This closes the supply-chain hole above: an unpinned external dependency could resolve to a compromised upstream release without us vetting it. + +This is enforced by `pnpm check:pinned-deps`, which requires: + +- Every `dependencies` and `optionalDependencies` entry of a published package to be an exact version, or a `workspace:`/`catalog:` reference. +- Every entry in the pnpm `catalog:` (in `pnpm-workspace.yaml`) to be an exact version, so that any `catalog:default` reference is also pinned. Deliberate exceptions live in `CATALOG_PIN_EXCEPTIONS` in `tools/deployments/validate-pinned-dependencies.ts` (currently only `@cloudflare/workers-types`, which is consumed as a peer dependency). + +`peerDependencies` are exempt — ranges there are intentional, since they describe the set of consumer-provided versions a package is compatible with. + ## Changesets Every non-trivial change to the project - those that should appear in the changelog - must be captured in a "changeset". diff --git a/package.json b/package.json index 521a540eb4..26bc18e468 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "author": "wrangler@cloudflare.com", "scripts": { "build": "dotenv -- turbo build", - "check": "pnpm check:fixtures && pnpm check:private-packages && pnpm check:package-deps && pnpm check:catalog && pnpm check:deployments && pnpm check:workflows && node lint-turbo.mjs && dotenv -- turbo check:lint check:type check:format type:tests", + "check": "pnpm check:fixtures && pnpm check:private-packages && pnpm check:package-deps && pnpm check:catalog && pnpm check:pinned-deps && pnpm check:deployments && pnpm check:workflows && node lint-turbo.mjs && dotenv -- turbo check:lint check:type check:format type:tests", "check:catalog": "node -r esbuild-register tools/deployments/validate-catalog-usage.ts", "check:deployments": "node -r esbuild-register tools/deployments/deploy-non-npm-packages.ts check", "check:fixtures": "node -r esbuild-register tools/deployments/validate-fixtures.ts", "check:format": "oxfmt --check", "check:lint": "oxlint --deny-warnings --type-aware", "check:package-deps": "pnpm build && node -r esbuild-register tools/deployments/validate-package-dependencies.ts", + "check:pinned-deps": "node -r esbuild-register tools/deployments/validate-pinned-dependencies.ts", "check:private-packages": "node -r esbuild-register tools/deployments/validate-private-packages.ts", "check:type": "dotenv -- turbo check:type type:tests", "check:workflows": "node -r esbuild-register tools/github-workflow-helpers/validate-action-pinning.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index c6d720f51c..60afad5992 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,12 +68,12 @@ "test:ci": "vitest run" }, "dependencies": { - "@clack/core": "^1.2.0", + "@clack/core": "1.2.0", "@cloudflare/workers-utils": "workspace:*", - "chalk": "^5.2.0", + "chalk": "5.3.0", "ci-info": "catalog:default", - "cross-spawn": "^7.0.3", - "log-update": "^5.0.1" + "cross-spawn": "7.0.6", + "log-update": "5.0.1" }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index b38a45aab4..c0412fe3cf 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", + "sharp": "0.34.5", "undici": "catalog:default", "workerd": "1.20260529.1", "ws": "catalog:default", diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index 841d11180b..b1f89800f1 100644 --- a/packages/vitest-pool-workers/package.json +++ b/packages/vitest-pool-workers/package.json @@ -53,11 +53,11 @@ "test:ci": "vitest run" }, "dependencies": { - "cjs-module-lexer": "^1.2.3", + "cjs-module-lexer": "1.2.3", "esbuild": "catalog:default", "miniflare": "workspace:*", "wrangler": "workspace:*", - "zod": "^3.25.76" + "zod": "3.25.76" }, "devDependencies": { "@cloudflare/mock-npm-registry": "workspace:*", diff --git a/packages/workers-editor-shared/package.json b/packages/workers-editor-shared/package.json index 30947ebdcc..2b257b32e4 100644 --- a/packages/workers-editor-shared/package.json +++ b/packages/workers-editor-shared/package.json @@ -27,7 +27,7 @@ "dev": "vite" }, "dependencies": { - "react-split-pane": "^0.1.92" + "react-split-pane": "0.1.92" }, "devDependencies": { "@cloudflare/style-const": "^5.7.2", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 11a35e8460..e4ac0a93c9 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -179,7 +179,7 @@ } }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "2.3.3" }, "engines": { "node": ">=22.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0762fa3f24..d6d74c7526 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,13 +7,13 @@ settings: catalogs: default: '@cloudflare/vitest-pool-workers': - specifier: ^0.13.0 + specifier: 0.13.3 version: 0.13.3 '@cloudflare/workers-types': specifier: ^4.20260529.1 version: 4.20260529.1 '@hey-api/openapi-ts': - specifier: ^0.94.0 + specifier: 0.94.0 version: 0.94.0 '@vitest/runner': specifier: 4.1.0 @@ -25,49 +25,49 @@ catalogs: specifier: 4.1.0 version: 4.1.0 capnp-es: - specifier: ^0.0.14 + specifier: 0.0.14 version: 0.0.14 capnweb: - specifier: ^0.5.0 + specifier: 0.5.0 version: 0.5.0 ci-info: - specifier: ^4.4.0 + specifier: 4.4.0 version: 4.4.0 esbuild: specifier: 0.27.3 version: 0.27.3 jsonc-parser: - specifier: ^3.2.0 + specifier: 3.2.0 version: 3.2.0 msw: specifier: 2.12.4 version: 2.12.4 open: - specifier: ^11.0.0 + specifier: 11.0.0 version: 11.0.0 playwright-chromium: - specifier: ^1.60.0 + specifier: 1.60.0 version: 1.60.0 signal-exit: - specifier: ^3.0.7 + specifier: 3.0.7 version: 3.0.7 smol-toml: - specifier: ^1.5.2 + specifier: 1.5.2 version: 1.5.2 tinyglobby: - specifier: ^0.2.12 + specifier: 0.2.16 version: 0.2.16 tree-kill: - specifier: ^1.2.2 + specifier: 1.2.2 version: 1.2.2 typescript: - specifier: ~5.8.3 + specifier: 5.8.3 version: 5.8.3 undici: specifier: 7.24.8 version: 7.24.8 vite: - specifier: ^8.0.12 + specifier: 8.0.13 version: 8.0.13 vitest: specifier: 4.1.0 @@ -81,7 +81,7 @@ overrides: '@types/react-tabs>@types/react': ^18 '@types/react-transition-group>@types/react': ^18 '@cloudflare/elements>@types/react': ^18 - '@types/node': ^22.10.1 + '@types/node': 22.15.17 '@types/node>undici-types': 7.24.8 patchedDependencies: @@ -121,7 +121,7 @@ importers: specifier: ^0.23.0 version: 0.23.0 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@vue/compiler-sfc': specifier: ^3.3.4 @@ -202,7 +202,7 @@ importers: specifier: workspace:* version: link:../../packages/vitest-pool-workers '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 typescript: specifier: catalog:default @@ -554,7 +554,7 @@ importers: specifier: catalog:default version: 4.20260529.1 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/react': specifier: ^18.3.3 @@ -1150,7 +1150,7 @@ importers: specifier: ^3.0.1 version: 3.0.1 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/nunjucks': specifier: ^3.2.6 @@ -1456,7 +1456,7 @@ importers: specifier: ^6.4.0 version: 6.4.0 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 jest-image-snapshot: specifier: ^6.5.1 @@ -1486,7 +1486,7 @@ importers: specifier: catalog:default version: 4.20260529.1 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 playwright-chromium: specifier: catalog:default @@ -1552,22 +1552,22 @@ importers: packages/cli: dependencies: '@clack/core': - specifier: ^1.2.0 + specifier: 1.2.0 version: 1.2.0 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils chalk: - specifier: ^5.2.0 + specifier: 5.3.0 version: 5.3.0 ci-info: specifier: catalog:default version: 4.4.0 cross-spawn: - specifier: ^7.0.3 + specifier: 7.0.6 version: 7.0.6 log-update: - specifier: ^5.0.1 + specifier: 5.0.1 version: 5.0.1 devDependencies: '@cloudflare/workers-tsconfig': @@ -1589,7 +1589,7 @@ importers: specifier: ^4.0.3 version: 4.0.3 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@vitest/ui': specifier: catalog:default @@ -1622,7 +1622,7 @@ importers: specifier: workspace:* version: link:../workers-utils '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 typescript: specifier: catalog:default @@ -1682,7 +1682,7 @@ importers: specifier: ^4.0.3 version: 4.0.3 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/semver': specifier: ^7.5.1 @@ -1799,7 +1799,7 @@ importers: specifier: workspace:* version: link:../workers-utils '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 concurrently: specifier: ^8.2.2 @@ -1901,7 +1901,7 @@ importers: specifier: ^3.0.4 version: 3.0.4 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 mime: specifier: ^3.0.0 @@ -2040,7 +2040,7 @@ importers: specifier: 0.8.1 version: 0.8.1 sharp: - specifier: ^0.34.5 + specifier: 0.34.5 version: 0.34.5 undici: specifier: catalog:default @@ -2104,7 +2104,7 @@ importers: specifier: ^3.0.4 version: 3.0.4 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/stoppable': specifier: ^1.1.1 @@ -2209,7 +2209,7 @@ importers: specifier: workspace:* version: link:../workers-utils '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@verdaccio/types': specifier: ^10.8.0 @@ -2311,7 +2311,7 @@ importers: specifier: catalog:default version: 4.20260529.1 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 concurrently: specifier: ^8.2.2 @@ -2421,7 +2421,7 @@ importers: specifier: ^0.8.0 version: 0.8.0 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/semver': specifier: ^7.5.1 @@ -3129,7 +3129,7 @@ importers: specifier: ^4.1.12 version: 4.1.12 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/pg': specifier: ^8.15.4 @@ -3586,7 +3586,7 @@ importers: packages/vitest-pool-workers: dependencies: cjs-module-lexer: - specifier: ^1.2.3 + specifier: 1.2.3 version: 1.2.3 esbuild: specifier: catalog:default @@ -3598,7 +3598,7 @@ importers: specifier: workspace:* version: link:../wrangler zod: - specifier: ^3.25.76 + specifier: 3.25.76 version: 3.25.76 devDependencies: '@cloudflare/mock-npm-registry': @@ -3620,7 +3620,7 @@ importers: specifier: ^17.3.0 version: 17.3.0 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/semver': specifier: ^7.5.1 @@ -3671,7 +3671,7 @@ importers: packages/workers-editor-shared: dependencies: react-split-pane: - specifier: ^0.1.92 + specifier: 0.1.92 version: 0.1.92(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@cloudflare/style-const': @@ -3880,7 +3880,7 @@ importers: specifier: ^1.2.0 version: 1.2.0 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/signal-exit': specifier: ^3.0.1 @@ -4071,7 +4071,7 @@ importers: specifier: ^5.1.2 version: 5.1.2 '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 '@types/node-forge': specifier: ^1.3.11 @@ -4279,7 +4279,7 @@ importers: version: 17.7.2 optionalDependencies: fsevents: - specifier: ~2.3.2 + specifier: 2.3.3 version: 2.3.3 packages/wrangler-bundler: @@ -4295,7 +4295,7 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@types/node': - specifier: ^22.10.1 + specifier: 22.15.17 version: 22.15.17 tsdown: specifier: 0.16.3 @@ -5385,21 +5385,9 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.7.0': - resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} - - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -6448,7 +6436,7 @@ packages: resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -6457,7 +6445,7 @@ packages: resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -6466,7 +6454,7 @@ packages: resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -6479,7 +6467,7 @@ packages: resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -8050,7 +8038,7 @@ packages: '@rushstack/node-core-library@5.13.1': resolution: {integrity: sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -8058,7 +8046,7 @@ packages: '@rushstack/node-core-library@5.5.1': resolution: {integrity: sha512-ZutW56qIzH8xIOlfyaLQJFx+8IBqdbVCZdnj+XT1MorQ1JqqxHse8vbCpEM+2MjsrqcbxcgDIbfggB1ZSQ2A3g==} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -8069,7 +8057,7 @@ packages: '@rushstack/terminal@0.13.3': resolution: {integrity: sha512-fc3zjXOw8E0pXS5t9vTiIPx9gHA0fIdTXsu9mT4WbH+P3mYvnrX0iAQ5a6NvyK1+CqYWBTw/wVNx7SDJkI+WYQ==} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -8077,7 +8065,7 @@ packages: '@rushstack/terminal@0.15.3': resolution: {integrity: sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -13085,10 +13073,6 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -14079,7 +14063,7 @@ packages: resolution: {integrity: sha512-axCguHItc8Sxt0HC6aSkdVRPffjYPV7EQqZRb2GkIa8FzWDycE7nHJM19C6xAIynH1Qp1/BHiopSi96jGBxT0w==} engines: {node: '>=16'} peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 peerDependenciesMeta: '@types/node': optional: true @@ -14787,7 +14771,7 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 jiti: '>=1.21.0' less: ^4.0.0 lightningcss: ^1.21.0 @@ -14827,7 +14811,7 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^22.10.1 + '@types/node': 22.15.17 '@vitejs/devtools': ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' @@ -14877,7 +14861,7 @@ packages: peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^22.10.1 + '@types/node': 22.15.17 '@vitest/browser-playwright': 4.1.0 '@vitest/browser-preview': 4.1.0 '@vitest/browser-webdriverio': 4.1.0 @@ -16842,32 +16826,11 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -17500,7 +17463,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.7.0 + '@emnapi/runtime': 1.10.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -17753,8 +17716,8 @@ snapshots: '@napi-rs/wasm-runtime@1.1.1': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -18592,7 +18555,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.57.1 @@ -20466,14 +20429,14 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.4.20(postcss@8.5.8): + autoprefixer@10.4.20(postcss@8.5.14): dependencies: browserslist: 4.24.2 caniuse-lite: 1.0.30001669 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -21062,9 +21025,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.2.0(postcss@8.5.8): + css-declaration-sorter@7.2.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 css-in-js-utils@2.0.1: dependencies: @@ -21104,49 +21067,49 @@ snapshots: clone: 2.1.2 parserlib: 1.1.1 - cssnano-preset-default@7.0.6(postcss@8.5.8): + cssnano-preset-default@7.0.6(postcss@8.5.14): dependencies: browserslist: 4.24.2 - css-declaration-sorter: 7.2.0(postcss@8.5.8) - cssnano-utils: 5.0.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-calc: 10.1.0(postcss@8.5.8) - postcss-colormin: 7.0.2(postcss@8.5.8) - postcss-convert-values: 7.0.4(postcss@8.5.8) - postcss-discard-comments: 7.0.3(postcss@8.5.8) - postcss-discard-duplicates: 7.0.1(postcss@8.5.8) - postcss-discard-empty: 7.0.0(postcss@8.5.8) - postcss-discard-overridden: 7.0.0(postcss@8.5.8) - postcss-merge-longhand: 7.0.4(postcss@8.5.8) - postcss-merge-rules: 7.0.4(postcss@8.5.8) - postcss-minify-font-values: 7.0.0(postcss@8.5.8) - postcss-minify-gradients: 7.0.0(postcss@8.5.8) - postcss-minify-params: 7.0.2(postcss@8.5.8) - postcss-minify-selectors: 7.0.4(postcss@8.5.8) - postcss-normalize-charset: 7.0.0(postcss@8.5.8) - postcss-normalize-display-values: 7.0.0(postcss@8.5.8) - postcss-normalize-positions: 7.0.0(postcss@8.5.8) - postcss-normalize-repeat-style: 7.0.0(postcss@8.5.8) - postcss-normalize-string: 7.0.0(postcss@8.5.8) - postcss-normalize-timing-functions: 7.0.0(postcss@8.5.8) - postcss-normalize-unicode: 7.0.2(postcss@8.5.8) - postcss-normalize-url: 7.0.0(postcss@8.5.8) - postcss-normalize-whitespace: 7.0.0(postcss@8.5.8) - postcss-ordered-values: 7.0.1(postcss@8.5.8) - postcss-reduce-initial: 7.0.2(postcss@8.5.8) - postcss-reduce-transforms: 7.0.0(postcss@8.5.8) - postcss-svgo: 7.0.1(postcss@8.5.8) - postcss-unique-selectors: 7.0.3(postcss@8.5.8) - - cssnano-utils@5.0.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - cssnano@7.0.6(postcss@8.5.8): - dependencies: - cssnano-preset-default: 7.0.6(postcss@8.5.8) + css-declaration-sorter: 7.2.0(postcss@8.5.14) + cssnano-utils: 5.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-calc: 10.1.0(postcss@8.5.14) + postcss-colormin: 7.0.2(postcss@8.5.14) + postcss-convert-values: 7.0.4(postcss@8.5.14) + postcss-discard-comments: 7.0.3(postcss@8.5.14) + postcss-discard-duplicates: 7.0.1(postcss@8.5.14) + postcss-discard-empty: 7.0.0(postcss@8.5.14) + postcss-discard-overridden: 7.0.0(postcss@8.5.14) + postcss-merge-longhand: 7.0.4(postcss@8.5.14) + postcss-merge-rules: 7.0.4(postcss@8.5.14) + postcss-minify-font-values: 7.0.0(postcss@8.5.14) + postcss-minify-gradients: 7.0.0(postcss@8.5.14) + postcss-minify-params: 7.0.2(postcss@8.5.14) + postcss-minify-selectors: 7.0.4(postcss@8.5.14) + postcss-normalize-charset: 7.0.0(postcss@8.5.14) + postcss-normalize-display-values: 7.0.0(postcss@8.5.14) + postcss-normalize-positions: 7.0.0(postcss@8.5.14) + postcss-normalize-repeat-style: 7.0.0(postcss@8.5.14) + postcss-normalize-string: 7.0.0(postcss@8.5.14) + postcss-normalize-timing-functions: 7.0.0(postcss@8.5.14) + postcss-normalize-unicode: 7.0.2(postcss@8.5.14) + postcss-normalize-url: 7.0.0(postcss@8.5.14) + postcss-normalize-whitespace: 7.0.0(postcss@8.5.14) + postcss-ordered-values: 7.0.1(postcss@8.5.14) + postcss-reduce-initial: 7.0.2(postcss@8.5.14) + postcss-reduce-transforms: 7.0.0(postcss@8.5.14) + postcss-svgo: 7.0.1(postcss@8.5.14) + postcss-unique-selectors: 7.0.3(postcss@8.5.14) + + cssnano-utils@5.0.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + cssnano@7.0.6(postcss@8.5.14): + dependencies: + cssnano-preset-default: 7.0.6(postcss@8.5.14) lilconfig: 3.1.3 - postcss: 8.5.8 + postcss: 8.5.14 csso@5.0.5: dependencies: @@ -23336,17 +23299,17 @@ snapshots: mkdist@2.2.0(typescript@5.8.3)(vue-tsc@2.0.29(typescript@5.8.3)): dependencies: - autoprefixer: 10.4.20(postcss@8.5.8) + autoprefixer: 10.4.20(postcss@8.5.14) citty: 0.1.6 - cssnano: 7.0.6(postcss@8.5.8) + cssnano: 7.0.6(postcss@8.5.14) defu: 6.1.4 esbuild: 0.24.2 jiti: 1.21.7 mlly: 1.7.4 pathe: 1.1.2 pkg-types: 1.3.1 - postcss: 8.5.8 - postcss-nested: 7.0.2(postcss@8.5.8) + postcss: 8.5.14 + postcss-nested: 7.0.2(postcss@8.5.14) semver: 7.7.3 tinyglobby: 0.2.16 optionalDependencies: @@ -24019,42 +23982,42 @@ snapshots: postal-mime@2.4.4(patch_hash=44bc62560d3d339b5c0836c18991f42b3b998db144f5ee7101d6758bbe74d3f2): {} - postcss-calc@10.1.0(postcss@8.5.8): + postcss-calc@10.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.2(postcss@8.5.8): + postcss-colormin@7.0.2(postcss@8.5.14): dependencies: browserslist: 4.24.2 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.4(postcss@8.5.8): + postcss-convert-values@7.0.4(postcss@8.5.14): dependencies: browserslist: 4.24.2 - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.3(postcss@8.5.8): + postcss-discard-comments@7.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-discard-duplicates@7.0.1(postcss@8.5.8): + postcss-discard-duplicates@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 - postcss-discard-empty@7.0.0(postcss@8.5.8): + postcss-discard-empty@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 - postcss-discard-overridden@7.0.0(postcss@8.5.8): + postcss-discard-overridden@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.14)(tsx@3.12.10)(yaml@2.8.1): dependencies: @@ -24074,110 +24037,110 @@ snapshots: tsx: 4.21.0 yaml: 2.8.1 - postcss-merge-longhand@7.0.4(postcss@8.5.8): + postcss-merge-longhand@7.0.4(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - stylehacks: 7.0.4(postcss@8.5.8) + stylehacks: 7.0.4(postcss@8.5.14) - postcss-merge-rules@7.0.4(postcss@8.5.8): + postcss-merge-rules@7.0.4(postcss@8.5.14): dependencies: browserslist: 4.24.2 caniuse-api: 3.0.0 - cssnano-utils: 5.0.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 5.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-minify-font-values@7.0.0(postcss@8.5.8): + postcss-minify-font-values@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.0(postcss@8.5.8): + postcss-minify-gradients@7.0.0(postcss@8.5.14): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 5.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.2(postcss@8.5.8): + postcss-minify-params@7.0.2(postcss@8.5.14): dependencies: browserslist: 4.24.2 - cssnano-utils: 5.0.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 5.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.4(postcss@8.5.8): + postcss-minify-selectors@7.0.4(postcss@8.5.14): dependencies: cssesc: 3.0.0 - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-nested@7.0.2(postcss@8.5.8): + postcss-nested@7.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 7.0.0 - postcss-normalize-charset@7.0.0(postcss@8.5.8): + postcss-normalize-charset@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 - postcss-normalize-display-values@7.0.0(postcss@8.5.8): + postcss-normalize-display-values@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.0(postcss@8.5.8): + postcss-normalize-positions@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.0(postcss@8.5.8): + postcss-normalize-repeat-style@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.0(postcss@8.5.8): + postcss-normalize-string@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.0(postcss@8.5.8): + postcss-normalize-timing-functions@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.2(postcss@8.5.8): + postcss-normalize-unicode@7.0.2(postcss@8.5.14): dependencies: browserslist: 4.24.2 - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.0(postcss@8.5.8): + postcss-normalize-url@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.0(postcss@8.5.8): + postcss-normalize-whitespace@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.1(postcss@8.5.8): + postcss-ordered-values@7.0.1(postcss@8.5.14): dependencies: - cssnano-utils: 5.0.0(postcss@8.5.8) - postcss: 8.5.8 + cssnano-utils: 5.0.0(postcss@8.5.14) + postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.2(postcss@8.5.8): + postcss-reduce-initial@7.0.2(postcss@8.5.14): dependencies: browserslist: 4.24.2 caniuse-api: 3.0.0 - postcss: 8.5.8 + postcss: 8.5.14 - postcss-reduce-transforms@7.0.0(postcss@8.5.8): + postcss-reduce-transforms@7.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 postcss-selector-parser@6.1.2: @@ -24190,15 +24153,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.0.1(postcss@8.5.8): + postcss-svgo@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 svgo: 3.3.2 - postcss-unique-selectors@7.0.3(postcss@8.5.8): + postcss-unique-selectors@7.0.3(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 postcss-value-parser@4.2.0: {} @@ -24221,12 +24184,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postgres-array@2.0.0: {} postgres-array@3.0.2: {} @@ -25494,10 +25451,10 @@ snapshots: '@styled-system/variant': 5.1.5 object-assign: 4.1.1 - stylehacks@7.0.4(postcss@8.5.8): + stylehacks@7.0.4(postcss@8.5.14): dependencies: browserslist: 4.24.2 - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 stylis@4.3.0: {} @@ -25635,8 +25592,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.16: dependencies: @@ -26154,7 +26111,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 unrun@0.2.8(synckit@0.11.12): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d3daafb72e..50b70c3bba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -99,35 +99,35 @@ patchedDependencies: # ────────────────────────────────────────────────────────────────────────────── catalog: - "@hey-api/openapi-ts": "^0.94.0" - "@types/node": "^22.10.1" + "@hey-api/openapi-ts": "0.94.0" + "@types/node": "22.15.17" "@vitest/runner": 4.1.0 "@vitest/snapshot": 4.1.0 "@vitest/ui": 4.1.0 - typescript: "~5.8.3" + typescript: "5.8.3" undici: "7.24.8" # Override undici-types from @types/node so that the Cloudflare SDK typings match our installed # version of Undici undici-types: "7.24.8" vitest: "4.1.0" - vite: "^8.0.12" + vite: "8.0.13" "ws": "8.20.1" esbuild: "0.27.3" - playwright-chromium: "^1.60.0" + playwright-chromium: "1.60.0" "@cloudflare/workers-types": "^4.20260529.1" workerd: "1.20260529.1" - jsonc-parser: "^3.2.0" - smol-toml: "^1.5.2" + jsonc-parser: "3.2.0" + smol-toml: "1.5.2" msw: 2.12.4 - tinyglobby: "^0.2.12" - "tree-kill": "^1.2.2" - "capnp-es": "^0.0.14" - "capnweb": "^0.5.0" - "ci-info": "^4.4.0" - "open": "^11.0.0" - "signal-exit": "^3.0.7" + tinyglobby: "0.2.16" + "tree-kill": "1.2.2" + "capnp-es": "0.0.14" + "capnweb": "0.5.0" + "ci-info": "4.4.0" + "open": "11.0.0" + "signal-exit": "3.0.7" # CAUTION: Most usage of @cloudflare/vitest-pool-workers in this monorepo should use workspace:* instead of this catalog version # However, some packages (pages-shared, workers-shared, etc...) need to be tested using vitest-pool-workers but are themselves # ultimately included in vitest-pool-workers (through Wrangler), causing a circular dependency. - "@cloudflare/vitest-pool-workers": "^0.13.0" + "@cloudflare/vitest-pool-workers": "0.13.3" diff --git a/tools/README.md b/tools/README.md index 2b6657d152..cdd89ad161 100644 --- a/tools/README.md +++ b/tools/README.md @@ -11,6 +11,9 @@ Tools for helping with CI - `deployments/validate-changesets.ts` - Validate that changesets are formatted correctly. Used by the changesets.yml and test-and-check.yml GitHub Action workflows. +- `deployments/validate-pinned-dependencies.ts` - Ensures all non-bundled dependencies of published packages (and all pnpm catalog entries) are pinned to exact versions. + Used by the test-and-check.yml GitHub Action workflow, as part of the `check` npm script (`pnpm check:pinned-deps`). + - `dependabot/generate-dependabot-pr-changesets.ts` - Generates and commits a changeset for a Dependabot PR. Used by the c3-dependabot-versioning-prs.yml and miniflare-dependabot-versioning-prs.yml GitHub Action workflows. diff --git a/tools/deployments/__tests__/validate-pinned-dependencies.test.ts b/tools/deployments/__tests__/validate-pinned-dependencies.test.ts new file mode 100644 index 0000000000..03fc035945 --- /dev/null +++ b/tools/deployments/__tests__/validate-pinned-dependencies.test.ts @@ -0,0 +1,198 @@ +import { describe, it } from "vitest"; +import { parseCatalog } from "../validate-catalog-usage"; +import { + isPinnedVersion, + validateCatalogPins, + validatePackagePins, +} from "../validate-pinned-dependencies"; + +describe("isPinnedVersion()", () => { + it("should accept exact versions", ({ expect }) => { + expect(isPinnedVersion("1.2.3")).toBe(true); + expect(isPinnedVersion("0.0.14")).toBe(true); + expect(isPinnedVersion("1.20260529.1")).toBe(true); + }); + + it("should accept exact prerelease and build versions", ({ expect }) => { + expect(isPinnedVersion("4.1.0-beta.10")).toBe(true); + expect(isPinnedVersion("2.0.0-rc.24")).toBe(true); + expect(isPinnedVersion("1.2.3+build.5")).toBe(true); + }); + + it("should reject caret and tilde ranges", ({ expect }) => { + expect(isPinnedVersion("^1.2.3")).toBe(false); + expect(isPinnedVersion("~5.8.3")).toBe(false); + }); + + it("should reject comparator ranges", ({ expect }) => { + expect(isPinnedVersion(">1.20260305.0 <2.0.0-0")).toBe(false); + expect(isPinnedVersion(">=1.0.0")).toBe(false); + expect(isPinnedVersion("^6.1.0 || ^7.0.0 || ^8.0.0")).toBe(false); + }); + + it("should reject wildcards and partial versions", ({ expect }) => { + expect(isPinnedVersion("*")).toBe(false); + expect(isPinnedVersion("1.x")).toBe(false); + expect(isPinnedVersion("1.2")).toBe(false); + expect(isPinnedVersion("1")).toBe(false); + }); + + it("should reject non-version specifiers", ({ expect }) => { + expect(isPinnedVersion("")).toBe(false); + expect(isPinnedVersion("latest")).toBe(false); + expect(isPinnedVersion("workspace:*")).toBe(false); + expect(isPinnedVersion("catalog:default")).toBe(false); + }); +}); + +describe("validateCatalogPins()", () => { + it("should pass when all entries are pinned", ({ expect }) => { + const errors = validateCatalogPins( + new Map([ + ["undici", "7.24.8"], + ["esbuild", "0.27.3"], + ["youch", "4.1.0-beta.10"], + ]) + ); + expect(errors).toEqual([]); + }); + + it("should flag ranged entries", ({ expect }) => { + const errors = validateCatalogPins( + new Map([ + ["undici", "7.24.8"], + ["ci-info", "^4.4.0"], + ["typescript", "~5.8.3"], + ]) + ); + expect(errors).toHaveLength(2); + expect(errors[0]).toContain('"ci-info"'); + expect(errors[0]).toContain('"^4.4.0"'); + expect(errors[1]).toContain('"typescript"'); + }); + + it("should skip entries in the exceptions allowlist", ({ expect }) => { + const errors = validateCatalogPins( + new Map([["@cloudflare/workers-types", "^4.20260529.1"]]), + new Set(["@cloudflare/workers-types"]) + ); + expect(errors).toEqual([]); + }); + + it("should still flag non-excepted ranged entries", ({ expect }) => { + const errors = validateCatalogPins( + new Map([ + ["@cloudflare/workers-types", "^4.20260529.1"], + ["vite", "^8.0.12"], + ]), + new Set(["@cloudflare/workers-types"]) + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"vite"'); + }); + + it("should use the default exceptions allowlist", ({ expect }) => { + const errors = validateCatalogPins( + new Map([["@cloudflare/workers-types", "^4.20260529.1"]]) + ); + expect(errors).toEqual([]); + }); +}); + +describe("validatePackagePins()", () => { + it("should pass when all dependencies are pinned", ({ expect }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + dependencies: { "blake3-wasm": "2.1.5", "path-to-regexp": "6.3.0" }, + }); + expect(errors).toEqual([]); + }); + + it("should flag ranged dependencies", ({ expect }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + dependencies: { sharp: "^0.34.5" }, + }); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"sharp"'); + expect(errors[0]).toContain('"^0.34.5"'); + expect(errors[0]).toContain("dependencies"); + }); + + it("should flag ranged optionalDependencies", ({ expect }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + optionalDependencies: { fsevents: "~2.3.2" }, + }); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"fsevents"'); + expect(errors[0]).toContain("optionalDependencies"); + }); + + it("should skip workspace, catalog, npm, link and file specifiers", ({ + expect, + }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + dependencies: { + miniflare: "workspace:*", + "@cloudflare/kv-asset-handler": "workspace:^", + esbuild: "catalog:default", + aliased: "npm:other@1.2.3", + linked: "link:../other", + filed: "file:../other", + }, + }); + expect(errors).toEqual([]); + }); + + it("should ignore peerDependencies and devDependencies", ({ expect }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + peerDependencies: { vitest: "^4.1.0", react: "^17.0.2 || ^18.2.21" }, + devDependencies: { typescript: "^5.0.0", esbuild: "^0.20.0" }, + }); + expect(errors).toEqual([]); + }); + + it("should report multiple violations across sections", ({ expect }) => { + const errors = validatePackagePins("test-package", "test-package", { + name: "test-package", + dependencies: { zod: "^3.25.76", "blake3-wasm": "2.1.5" }, + optionalDependencies: { fsevents: "~2.3.2" }, + }); + expect(errors).toHaveLength(2); + expect(errors[0]).toContain('"zod"'); + expect(errors[1]).toContain('"fsevents"'); + }); +}); + +describe("parseCatalog()", () => { + it("should parse quoted and unquoted entries", ({ expect }) => { + const catalog = parseCatalog( + [ + "packages:", + " - packages/*", + "", + "minimumReleaseAge: 1440", + "", + "catalog:", + " # a comment", + ' "@cloudflare/workers-types": "^4.20260529.1"', + ' undici: "7.24.8"', + " '@vitest/runner': 4.1.0", + ' typescript: "~5.8.3"', + "", + "overrides:", + ' "@types/node": "$@types/node"', + ].join("\n") + ); + + expect(catalog.get("@cloudflare/workers-types")).toBe("^4.20260529.1"); + expect(catalog.get("undici")).toBe("7.24.8"); + expect(catalog.get("@vitest/runner")).toBe("4.1.0"); + expect(catalog.get("typescript")).toBe("~5.8.3"); + // Should stop at the next top-level key. + expect(catalog.has("@types/node")).toBe(false); + }); +}); diff --git a/tools/deployments/validate-catalog-usage.ts b/tools/deployments/validate-catalog-usage.ts index 54c01d378f..11f7540353 100644 --- a/tools/deployments/validate-catalog-usage.ts +++ b/tools/deployments/validate-catalog-usage.ts @@ -14,12 +14,15 @@ const ROOT = resolve(__dirname, "../.."); // bumped in coordinated PRs with its own automation). const IGNORED_DEPS = new Set(["workerd"]); -function loadCatalogDeps(): Set { - const content = readFileSync(resolve(ROOT, "pnpm-workspace.yaml"), "utf-8"); - const deps = new Set(); +/** + * Parses the `catalog:` block of a pnpm-workspace.yaml into a map of + * dependency name -> version specifier. + */ +export function parseCatalog(workspaceYaml: string): Map { + const catalog = new Map(); let inCatalog = false; - for (const line of content.split("\n")) { + for (const line of workspaceYaml.split("\n")) { if (line.startsWith("catalog:")) { inCatalog = true; continue; @@ -36,13 +39,27 @@ function loadCatalogDeps(): Set { continue; } - const match = trimmed.match(/^["']?(@?[^"':]+)["']?\s*:/); + const match = trimmed.match( + /^["']?(@?[^"':]+)["']?\s*:\s*["']?([^"'#\s]+)["']?/ + ); if (match) { - deps.add(match[1]); + catalog.set(match[1].trim(), match[2]); } } - return deps; + return catalog; +} + +/** + * Loads the pnpm catalog as a map of dependency name -> version specifier. + */ +export function loadCatalog(): Map { + const content = readFileSync(resolve(ROOT, "pnpm-workspace.yaml"), "utf-8"); + return parseCatalog(content); +} + +function loadCatalogDeps(): Set { + return new Set(loadCatalog().keys()); } async function main(): Promise { @@ -105,8 +122,10 @@ async function main(): Promise { process.exit(errors.length > 0 ? 1 : 0); } -main().catch((error) => { - console.log("::endgroup::"); - console.error("An unexpected error occurred", error); - process.exit(1); -}); +if (require.main === module) { + main().catch((error) => { + console.log("::endgroup::"); + console.error("An unexpected error occurred", error); + process.exit(1); + }); +} diff --git a/tools/deployments/validate-package-dependencies.ts b/tools/deployments/validate-package-dependencies.ts index 47f3b4b21f..ae216a2ee3 100644 --- a/tools/deployments/validate-package-dependencies.ts +++ b/tools/deployments/validate-package-dependencies.ts @@ -37,6 +37,7 @@ export interface PackageJSON { dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; + optionalDependencies?: Record; } export interface PackageInfo { diff --git a/tools/deployments/validate-pinned-dependencies.ts b/tools/deployments/validate-pinned-dependencies.ts new file mode 100644 index 0000000000..278b7957ce --- /dev/null +++ b/tools/deployments/validate-pinned-dependencies.ts @@ -0,0 +1,174 @@ +/** + * Validates that all non-bundled dependencies of published packages are pinned + * to exact versions. + * + * Anything a published package does not bundle is installed into the consumer's + * dependency tree at install time. If those specifiers are ranges, a malicious + * or broken upstream release can be pulled into users' installs without us + * having a chance to vet it. Pinning to exact versions locks down exactly what + * ships. (devDependencies are intentionally NOT checked: npm never installs a + * published package's devDependencies into a consumer's tree, so their + * specifiers cannot affect users.) + * + * The check has two halves: + * + * 1. Catalog pinning — every entry in the `catalog:` block of + * pnpm-workspace.yaml must be an exact version. Because the catalog is + * guaranteed pinned, any `catalog:` reference elsewhere is trusted and + * doesn't need to be resolved per-package. + * + * 2. Package pinning — in every published (non-private) package, every + * `dependencies` and `optionalDependencies` entry must be an exact + * version. Specifiers using `workspace:`, `catalog:`, `npm:`, `link:` or + * `file:` are skipped (the catalog ones are covered by half 1; workspace + * ones are released atomically with the monorepo). `peerDependencies` are + * excluded because ranges there are intentional and generally necessary. + */ + +import { loadCatalog } from "./validate-catalog-usage"; +import { + getPublicPackages, + type PackageJSON, +} from "./validate-package-dependencies"; + +/** + * Matches an exact semantic version: MAJOR.MINOR.PATCH with optional + * `-prerelease` and `+build` metadata (e.g. "1.2.3", "4.1.0-beta.10", + * "2.0.0-rc.24"). Rejects ranges (^, ~, >, <, ||, hyphen ranges), wildcards + * (*, x), and partial versions. + */ +const PINNED_VERSION_RE = + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/; + +/** + * Specifier prefixes that are not literal version pins. These are validated + * elsewhere (catalog entries by `validateCatalogPins`) or are released + * atomically with the monorepo (workspace), so they are skipped here. + */ +const NON_LITERAL_PREFIXES = [ + "workspace:", + "catalog:", + "npm:", + "link:", + "file:", +]; + +/** + * Catalog entries that are deliberately left as ranges. + * + * `@cloudflare/workers-types` is consumed as an (optional) peerDependency via + * `catalog:default`. A range is intentional there so consumers aren't forced + * onto a single exact version of the types package. + */ +export const CATALOG_PIN_EXCEPTIONS = new Set(["@cloudflare/workers-types"]); + +export function isPinnedVersion(version: string): boolean { + return PINNED_VERSION_RE.test(version); +} + +function isNonLiteralSpecifier(version: string): boolean { + return NON_LITERAL_PREFIXES.some((prefix) => version.startsWith(prefix)); +} + +/** + * Validates that every catalog entry is pinned to an exact version (except + * deliberately-ranged entries in the exceptions allowlist). + */ +export function validateCatalogPins( + catalog: Map, + exceptions: Set = CATALOG_PIN_EXCEPTIONS +): string[] { + const errors: string[] = []; + + for (const [name, version] of catalog) { + if (exceptions.has(name)) { + continue; + } + if (!isPinnedVersion(version)) { + errors.push( + `Catalog entry "${name}" uses "${version}" but must be pinned to an exact ` + + `version in pnpm-workspace.yaml (e.g. "1.2.3", not a range). Catalog entries ` + + `can be consumed as non-bundled dependencies, so their versions must be locked down.` + ); + } + } + + return errors; +} + +/** + * Validates that a single package's non-bundled dependencies are pinned. + * Returns an array of error messages (empty if valid). + */ +export function validatePackagePins( + packageName: string, + relativePath: string, + packageJson: PackageJSON +): string[] { + const errors: string[] = []; + const sections = ["dependencies", "optionalDependencies"] as const; + + for (const section of sections) { + const deps = packageJson[section]; + if (!deps) { + continue; + } + for (const [name, version] of Object.entries(deps)) { + if (isNonLiteralSpecifier(version)) { + continue; + } + if (!isPinnedVersion(version)) { + errors.push( + `Package "${packageName}" has ${section} "${name}" set to "${version}" ` + + `(packages/${relativePath}/package.json), but non-bundled dependencies must be ` + + `pinned to an exact version (e.g. "1.2.3", not a range). If it should be ` + + `sourced from the pnpm catalog, use "catalog:default" instead.` + ); + } + } + } + + return errors; +} + +/** + * Validates that all catalog entries and all published packages' non-bundled + * dependencies are pinned to exact versions. + */ +export async function checkPinnedDependencies(): Promise { + const errors: string[] = []; + + console.log("- catalog (pnpm-workspace.yaml)"); + errors.push(...validateCatalogPins(loadCatalog())); + + const packages = await getPublicPackages(); + for (const { dir, packageJson } of packages) { + const relativePath = dir.split("/packages/")[1]; + console.log(`- ${packageJson.name}`); + errors.push( + ...validatePackagePins(packageJson.name, relativePath, packageJson) + ); + } + + return errors; +} + +if (require.main === module) { + console.log("::group::Checking dependency version pinning"); + checkPinnedDependencies() + .then((errors) => { + if (errors.length > 0) { + console.error( + "::error::Dependency pinning checks:" + + errors.map((e) => `\n- ${e}`).join("") + ); + } + console.log("::endgroup::"); + process.exit(errors.length > 0 ? 1 : 0); + }) + .catch((error) => { + console.log("::endgroup::"); + console.error("An unexpected error occurred", error); + process.exit(1); + }); +} From 0998725139680d803f510c3126b4c4e617b3a37b Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 17:18:02 +0100 Subject: [PATCH 13/18] Set `disallowTypeAnnotations` to `false` in `@typescript-eslint/consistent-type-imports` config (#14051) --- .oxlintrc.jsonc | 5 ++++- packages/miniflare/src/plugins/core/errors/index.ts | 2 +- packages/miniflare/src/plugins/core/errors/sourcemap.ts | 1 - packages/vitest-pool-workers/src/worker/types-ambient.d.ts | 1 - .../workers-shared/asset-worker/worker-configuration.d.ts | 1 - packages/wrangler/e2e/helpers/e2e-wrangler-test.ts | 2 -- packages/wrangler/src/__tests__/dev/remote-bindings.test.ts | 4 +--- .../src/__tests__/pages/pages-deployment-tail.test.ts | 1 - .../wrangler/src/__tests__/pages/project-validate.test.ts | 1 - packages/wrangler/src/__tests__/tail.test.ts | 1 - packages/wrangler/src/__tests__/vitest.setup.ts | 1 - packages/wrangler/src/sourcemap.ts | 1 - 12 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 30c78a3739..a004f3dc2e 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -32,7 +32,10 @@ "prefer-const": "error", "prefer-rest-params": "error", "prefer-spread": "error", - "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-imports": [ + "error", + { "disallowTypeAnnotations": false }, + ], "no-var": "error", "no-regex-spaces": "error", "@typescript-eslint/no-empty-object-type": "error", diff --git a/packages/miniflare/src/plugins/core/errors/index.ts b/packages/miniflare/src/plugins/core/errors/index.ts index 5e902234b5..8cbb5aaf88 100644 --- a/packages/miniflare/src/plugins/core/errors/index.ts +++ b/packages/miniflare/src/plugins/core/errors/index.ts @@ -319,7 +319,7 @@ export async function handlePrettyErrorRequest( } // Lazily import `youch` when required - // eslint-disable-next-line typescript/consistent-type-imports, @typescript-eslint/no-require-imports -- lazy require to avoid loading youch until an error page is needed + // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require to avoid loading youch until an error page is needed const { Youch }: typeof import("youch") = require("youch"); const youch = new Youch(); diff --git a/packages/miniflare/src/plugins/core/errors/sourcemap.ts b/packages/miniflare/src/plugins/core/errors/sourcemap.ts index 0094459369..827a7e8d60 100644 --- a/packages/miniflare/src/plugins/core/errors/sourcemap.ts +++ b/packages/miniflare/src/plugins/core/errors/sourcemap.ts @@ -13,7 +13,6 @@ import type { Options } from "@cspotcode/source-map-support"; // // ...load a fresh copy, by resetting then restoring the `require` cache, and // overriding `Symbol.for()` to return a unique symbol. -// eslint-disable-next-line typescript/consistent-type-imports -- dynamic import type used for return type annotation export function getFreshSourceMapSupport(): typeof import("@cspotcode/source-map-support") { // Under Yarn PnP, Node's ESM->CJS bridge (`loadCJSModule` in // `node:internal/modules/esm/translators`) hands this module a re-invented diff --git a/packages/vitest-pool-workers/src/worker/types-ambient.d.ts b/packages/vitest-pool-workers/src/worker/types-ambient.d.ts index 300f5a5273..5a311d9276 100644 --- a/packages/vitest-pool-workers/src/worker/types-ambient.d.ts +++ b/packages/vitest-pool-workers/src/worker/types-ambient.d.ts @@ -18,7 +18,6 @@ namespace Cloudflare { __VITEST_POOL_WORKERS_UNSAFE_EVAL: UnsafeEval; } interface GlobalProps { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof import() in ambient declarations requires inline import mainModule: typeof import("./index"); durableNamespaces: "__VITEST_POOL_WORKERS_RUNNER_DURABLE_OBJECT__"; } diff --git a/packages/workers-shared/asset-worker/worker-configuration.d.ts b/packages/workers-shared/asset-worker/worker-configuration.d.ts index 02fb110c81..a66707070f 100644 --- a/packages/workers-shared/asset-worker/worker-configuration.d.ts +++ b/packages/workers-shared/asset-worker/worker-configuration.d.ts @@ -2,7 +2,6 @@ // bindings derived from the main module's exports. declare namespace Cloudflare { interface GlobalProps { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- Cloudflare typegen requires `typeof import()` for mainModule mainModule: typeof import("./src/worker"); } } diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index 00bdf7eec4..c110466143 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -22,12 +22,10 @@ import { import type { WranglerCommandOptions } from "./wrangler"; import type { Awaitable } from "miniflare"; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import, not a type import export function importWrangler(): Promise { return import(WRANGLER_IMPORT.href); } -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import, not a type import export function importMiniflare(): Promise { return import(MINIFLARE_IMPORT.href); } diff --git a/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts b/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts index 552f1e0c0e..1b19e833d3 100644 --- a/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts +++ b/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports -- Test file uses dynamic imports where typeof requires value imports */ import assert from "node:assert"; import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import { fetch } from "undici"; @@ -12,8 +11,6 @@ import { onTestFailed, vi, } from "vitest"; -/* eslint-enable no-restricted-imports */ -import { Binding, StartRemoteProxySessionOptions } from "../../api"; import { unwrapHook } from "../../api/startDevWorker/utils"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -24,6 +21,7 @@ import { mswZoneHandlers, } from "../helpers/msw"; import { runWrangler } from "../helpers/run-wrangler"; +import type { Binding, StartRemoteProxySessionOptions } from "../../api"; import type { StartDevOptions } from "../../dev"; import type { RawConfig } from "@cloudflare/workers-utils"; import type { RemoteProxyConnectionString, WorkerOptions } from "miniflare"; diff --git a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts index 78f7cc6dff..84df26c1c0 100644 --- a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts @@ -26,7 +26,6 @@ import type { RequestInit } from "undici"; import type WebSocket from "ws"; vi.mock("ws", async (importOriginal) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import in vi.mock importOriginal callback const realModule = await importOriginal(); const module = { __esModule: true, diff --git a/packages/wrangler/src/__tests__/pages/project-validate.test.ts b/packages/wrangler/src/__tests__/pages/project-validate.test.ts index 526aef3c7e..a8f8429fb3 100644 --- a/packages/wrangler/src/__tests__/pages/project-validate.test.ts +++ b/packages/wrangler/src/__tests__/pages/project-validate.test.ts @@ -7,7 +7,6 @@ import { mockConsoleMethods } from "../helpers/mock-console"; import { runWrangler } from "../helpers/run-wrangler"; vi.mock("../../pages/constants", async (importActual) => ({ - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import in vi.mock importActual callback ...(await importActual()), MAX_ASSET_SIZE: 1 * 1024 * 1024, MAX_ASSET_COUNT_DEFAULT: 10, diff --git a/packages/wrangler/src/__tests__/tail.test.ts b/packages/wrangler/src/__tests__/tail.test.ts index ea2ce539c1..ecb91e29c3 100644 --- a/packages/wrangler/src/__tests__/tail.test.ts +++ b/packages/wrangler/src/__tests__/tail.test.ts @@ -32,7 +32,6 @@ import type { ExpectStatic } from "vitest"; import type WebSocket from "ws"; vi.mock("ws", async (importOriginal) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import in vi.mock importOriginal callback const realModule = await importOriginal(); const module = { __esModule: true, diff --git a/packages/wrangler/src/__tests__/vitest.setup.ts b/packages/wrangler/src/__tests__/vitest.setup.ts index fda2f133fd..2a78aaa665 100644 --- a/packages/wrangler/src/__tests__/vitest.setup.ts +++ b/packages/wrangler/src/__tests__/vitest.setup.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports -- Setup file uses dynamic imports where typeof requires value imports */ import { PassThrough } from "node:stream"; import chalk from "chalk"; import { passthrough } from "msw"; diff --git a/packages/wrangler/src/sourcemap.ts b/packages/wrangler/src/sourcemap.ts index ef61bf93e9..8aa08433f2 100644 --- a/packages/wrangler/src/sourcemap.ts +++ b/packages/wrangler/src/sourcemap.ts @@ -67,7 +67,6 @@ function getSourceMappingPrepareStackTrace( return sourceMappingPrepareStackTrace; } - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof requires a value import, not a type import const support: typeof import("@cspotcode/source-map-support") = getFreshSourceMapSupport(); const originalPrepareStackTrace = Error.prepareStackTrace; From 7868998b047e77b71ff58dabd448434e3612a70b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:01:09 +0100 Subject: [PATCH 14/18] [C3] Bump @angular/create from 21.2.12 to 21.2.13 in /packages/create-cloudflare/src/frameworks (#14128) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wrangler automated PR updater --- .changeset/c3-frameworks-update-14128.md | 11 +++++++++++ .../create-cloudflare/src/frameworks/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/c3-frameworks-update-14128.md diff --git a/.changeset/c3-frameworks-update-14128.md b/.changeset/c3-frameworks-update-14128.md new file mode 100644 index 0000000000..9703d6fe0c --- /dev/null +++ b/.changeset/c3-frameworks-update-14128.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| --------------- | ------- | ------- | +| @angular/create | 21.2.12 | 21.2.13 | diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index 23dd508ac3..ba22033c38 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -1,7 +1,7 @@ { "name": "frameworks_clis_info", "dependencies": { - "@angular/create": "21.2.12", + "@angular/create": "21.2.13", "@tanstack/cli": "0.68.0", "create-analog": "2.5.2", "create-astro": "5.0.6", From 890fca7d63a6efab5a58e4829cf02bf731eab197 Mon Sep 17 00:00:00 2001 From: MatinGathani <70268627+matingathani@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:40:09 +0530 Subject: [PATCH 15/18] [wrangler] fix: show clear error when --metadata is not valid JSON (#13881) --- .changeset/fix-kv-metadata-invalid-json.md | 5 +++ .../wrangler/src/__tests__/kv/key.test.ts | 36 +++++++++++++++++++ packages/wrangler/src/kv/index.ts | 21 +++++++++-- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-kv-metadata-invalid-json.md diff --git a/.changeset/fix-kv-metadata-invalid-json.md b/.changeset/fix-kv-metadata-invalid-json.md new file mode 100644 index 0000000000..089280bc9b --- /dev/null +++ b/.changeset/fix-kv-metadata-invalid-json.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Show a clear error when `--metadata` is not valid JSON instead of silently ignoring the value diff --git a/packages/wrangler/src/__tests__/kv/key.test.ts b/packages/wrangler/src/__tests__/kv/key.test.ts index ac73ea68c0..c22ae4495e 100644 --- a/packages/wrangler/src/__tests__/kv/key.test.ts +++ b/packages/wrangler/src/__tests__/kv/key.test.ts @@ -354,6 +354,42 @@ describe("kv", () => { expect(std.err).toMatchInlineSnapshot(`""`); }); + it("should error if --metadata is not valid JSON", async ({ expect }) => { + await expect( + runWrangler( + `kv key put --remote dKey dVal --namespace-id some-namespace-id --metadata not-valid-json` + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: --metadata must be valid JSON. Received: not-valid-json]` + ); + }); + + it("should error if --metadata is not a JSON object", async ({ + expect, + }) => { + await expect( + runWrangler( + `kv key put --remote dKey dVal --namespace-id some-namespace-id --metadata '"a-string"'` + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: --metadata must be a JSON object. Received: "a-string"]` + ); + await expect( + runWrangler( + `kv key put --remote dKey dVal --namespace-id some-namespace-id --metadata '[1,2,3]'` + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: --metadata must be a JSON object. Received: [1,2,3]]` + ); + await expect( + runWrangler( + `kv key put --remote dKey dVal --namespace-id some-namespace-id --metadata null` + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: --metadata must be a JSON object. Received: null]` + ); + }); + it("should error if no key is provided", async ({ expect }) => { await expect( runWrangler("kv key put") diff --git a/packages/wrangler/src/kv/index.ts b/packages/wrangler/src/kv/index.ts index 586416cb08..04be00b65f 100644 --- a/packages/wrangler/src/kv/index.ts +++ b/packages/wrangler/src/kv/index.ts @@ -427,9 +427,26 @@ const putCommonArgs = { type: "string", describe: "Arbitrary JSON that is associated with a key", coerce: (jsonStr: string): KeyValue["metadata"] => { + let parsed: unknown; try { - return JSON.parse(jsonStr); - } catch {} + parsed = JSON.parse(jsonStr); + } catch { + throw new CommandLineArgsError( + `--metadata must be valid JSON. Received: ${jsonStr}`, + { telemetryMessage: "kv key put --metadata not valid JSON" } + ); + } + if ( + parsed === null || + Array.isArray(parsed) || + typeof parsed !== "object" + ) { + throw new CommandLineArgsError( + `--metadata must be a JSON object. Received: ${jsonStr}`, + { telemetryMessage: "kv key put --metadata not a JSON object" } + ); + } + return parsed as KeyValue["metadata"]; }, }, local: { From aec1bb826aaba963bfc1ee96ba7359e284162bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Mon, 1 Jun 2026 20:30:01 +0100 Subject: [PATCH 16/18] [wrangler] Bump am-i-vibing from 0.1.1 to 0.4.0 (#14078) --- .changeset/bump-am-i-vibing.md | 7 +++++++ packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/bump-am-i-vibing.md diff --git a/.changeset/bump-am-i-vibing.md b/.changeset/bump-am-i-vibing.md new file mode 100644 index 0000000000..52e4835465 --- /dev/null +++ b/.changeset/bump-am-i-vibing.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Bump `am-i-vibing` from 0.1.1 to 0.4.0 + +This updates the agentic environment detection library to the latest version, which includes improved detection coverage for newer AI coding agents. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index e4ac0a93c9..3840c689a4 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -111,7 +111,7 @@ "@types/yargs": "^17.0.22", "@vitest/ui": "catalog:default", "@webcontainer/env": "^1.1.0", - "am-i-vibing": "^0.1.1", + "am-i-vibing": "^0.4.0", "capnweb": "catalog:default", "chalk": "^5.2.0", "chokidar": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6d74c7526..8e5ecd2b85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4104,8 +4104,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 am-i-vibing: - specifier: ^0.1.1 - version: 0.1.1 + specifier: ^0.4.0 + version: 0.4.0 capnweb: specifier: catalog:default version: 0.5.0 @@ -9278,8 +9278,8 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - am-i-vibing@0.1.1: - resolution: {integrity: sha512-6a7UWRIoCjdoGGG2rjlSJjOoJKxns78pl+x15Bic0KflMjoVnRGzzwQsrPDHE/mur8O2OuU5KV/YgF2ZevuW1g==} + am-i-vibing@0.4.0: + resolution: {integrity: sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==} hasBin: true ansi-colors@4.1.3: @@ -20331,7 +20331,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - am-i-vibing@0.1.1: + am-i-vibing@0.4.0: dependencies: process-ancestry: 0.1.0 From da8e306153843c6f42508bf7fe7737e91ac67241 Mon Sep 17 00:00:00 2001 From: Nathan Cabasso Date: Mon, 1 Jun 2026 21:51:56 +0200 Subject: [PATCH 17/18] [vitest-pool-workers] Preserve Durable Object handler order (for hibernated DOs) (#14061) --- .changeset/sharp-dogs-relax.md | 9 +++ .../durable-objects/src/index.ts | 20 +++++ .../durable-objects/test/websockets.test.ts | 79 +++++++++++++++++++ .../src/worker/entrypoints.ts | 54 +++++++------ 4 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 .changeset/sharp-dogs-relax.md create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts diff --git a/.changeset/sharp-dogs-relax.md b/.changeset/sharp-dogs-relax.md new file mode 100644 index 0000000000..af4e76b7dd --- /dev/null +++ b/.changeset/sharp-dogs-relax.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/vitest-pool-workers": patch +--- + +Preserve Durable Object WebSocket handler invocation order + +Durable Object WebSocket events could begin executing out of order in the Workers Vitest integration when several events arrived while the test wrapper was resolving user code. + +Handler invocation now preserves arrival order while still allowing asynchronous handler completion to run concurrently. diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts b/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts index 0a21f0db75..ce2dd7fbc3 100644 --- a/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts +++ b/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts @@ -2,6 +2,7 @@ import { DurableObject } from "cloudflare:workers"; export class Counter extends DurableObject { count: number = 0; + #webSocketMessages: string[] = []; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); @@ -17,6 +18,14 @@ export class Counter extends DurableObject { fetch(request: Request) { const url = new URL(request.url); + if (url.pathname === "/websocket-order") { + const { 0: client, 1: server } = new WebSocketPair(); + this.ctx.acceptWebSocket(server); + return new Response(null, { status: 101, webSocket: client }); + } + if (url.pathname === "/websocket-order-log") { + return Response.json(this.#webSocketMessages); + } if (url.pathname === "/redirect") { return Response.redirect("https://example.com/redirected", 302); } @@ -32,6 +41,17 @@ export class Counter extends DurableObject { scheduleReset(afterMillis: number) { void this.ctx.storage.setAlarm(Date.now() + afterMillis); } + + webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { + const value = + typeof message === "string" ? message : new TextDecoder().decode(message); + this.#webSocketMessages.push(value); + ws.send(value); + } + + webSocketClose() {} + + webSocketError() {} } export class SQLiteDurableObject extends DurableObject { diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts new file mode 100644 index 0000000000..b72bf3d2b3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts @@ -0,0 +1,79 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +const orderingAttempts = 5; +const orderingMessages = 100; + +function expectedOrderingMessages() { + return Array.from({ length: orderingMessages }, (_, i) => `message-${i}`); +} + +function getMessageData(event: MessageEvent) { + if (typeof event.data === "string") { + return event.data; + } + if (event.data instanceof ArrayBuffer) { + return new TextDecoder().decode(event.data); + } + throw new TypeError( + `Unexpected WebSocket message type: ${typeof event.data}` + ); +} + +function waitForMessages(socket: WebSocket, count: number) { + return new Promise((resolve, reject) => { + const messages: string[] = []; + const timeout = setTimeout(() => { + reject(new Error(`Timed out waiting for ${count} WebSocket messages`)); + }, 10_000); + + socket.addEventListener("message", (event) => { + messages.push(getMessageData(event)); + if (messages.length === count) { + clearTimeout(timeout); + resolve(messages); + } + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for messages")); + }); + }); +} + +function getResponseWebSocket(response: Response) { + const socket = response.webSocket; + if (socket === null || socket === undefined) { + throw new TypeError("Expected WebSocket response"); + } + return socket; +} + +it("preserves hibernatable WebSocket message order", async ({ expect }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + const id = env.COUNTER.idFromName( + `websocket-ordering-${crypto.randomUUID()}-${attempt}` + ); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + const expected = expectedOrderingMessages(); + const messagesPromise = waitForMessages(socket, orderingMessages); + + socket.accept(); + for (const message of expected) { + socket.send(message); + } + + expect(await messagesPromise).toEqual(expected); + + const logResponse = await stub.fetch( + "https://example.com/websocket-order-log" + ); + expect(await logResponse.json()).toEqual(expected); + + socket.close(1000, "done"); + } +}); diff --git a/packages/vitest-pool-workers/src/worker/entrypoints.ts b/packages/vitest-pool-workers/src/worker/entrypoints.ts index 6cffde8076..6fa52e4cc4 100644 --- a/packages/vitest-pool-workers/src/worker/entrypoints.ts +++ b/packages/vitest-pool-workers/src/worker/entrypoints.ts @@ -122,7 +122,7 @@ function getRPCProperty( return Reflect.get(/* target */ ctor.prototype, key, /* receiver */ instance); } -type RPCInvocationQueueOwner = +type InvocationQueueOwner = | WorkerEntrypoint | DurableObjectClass | WorkflowEntrypoint; @@ -146,10 +146,10 @@ type RPCInvocationQueueOwner = function getRPCPropertyCallableThenable( key: string, property: Promise, - queueOwner: RPCInvocationQueueOwner + queueOwner: InvocationQueueOwner ) { const fn = async function (...args: unknown[]) { - return enqueueRPCInvocation(queueOwner, async (release) => { + return enqueueInvocation(queueOwner, async (release) => { try { const maybeFn = await property; if (typeof maybeFn === "function") { @@ -168,25 +168,23 @@ function getRPCPropertyCallableThenable( return fn; } -const rpcInvocationQueues = new WeakMap< - RPCInvocationQueueOwner, - Promise ->(); +const invocationQueues = new WeakMap>(); /** - * Preserve the order in which dynamically-wrapped RPC methods begin executing. + * Preserve the order in which async wrapper invocations begin executing. * - * Resolving a property like `stub.method` may need to import user modules or - * instantiate wrapper objects. If several calls are fired synchronously, those - * async steps can otherwise complete out of order before the actual user method - * is invoked. The queue is released as soon as invocation starts, so async RPC - * completions can still run concurrently. + * Resolving a property like `stub.method`, or ensuring a Durable Object handler + * instance, may need to import user modules or instantiate wrapper objects. If + * several calls are fired synchronously, those async steps can otherwise + * complete out of order before the actual user code is invoked. The queue is + * released as soon as invocation starts, so async completions can still run + * concurrently. */ -async function enqueueRPCInvocation( - owner: RPCInvocationQueueOwner, +async function enqueueInvocation( + owner: InvocationQueueOwner, callback: (release: () => void) => Promise ): Promise { - const previous = rpcInvocationQueues.get(owner) ?? Promise.resolve(); + const previous = invocationQueues.get(owner) ?? Promise.resolve(); let releaseStarted: (() => void) | undefined; const started = new Promise((resolve) => { releaseStarted = resolve; @@ -198,7 +196,7 @@ async function enqueueRPCInvocation( } }; const result = previous.catch(() => {}).then(() => callback(release)); - rpcInvocationQueues.set(owner, started); + invocationQueues.set(owner, started); return result; } @@ -551,14 +549,20 @@ export function createDurableObjectWrapper( this: DurableObjectWrapper, ...args: unknown[] ) { - const { mainPath, instance } = await this[kEnsureInstance](); - const maybeFn = instance[key]; - if (typeof maybeFn === "function") { - return (maybeFn as (...a: unknown[]) => void).apply(instance, args); - } else { - const message = `${className} exported by ${mainPath} does not define a \`${key}()\` method`; - throw new TypeError(message); - } + return enqueueInvocation(this, async (release) => { + try { + const { mainPath, instance } = await this[kEnsureInstance](); + const maybeFn = instance[key]; + if (typeof maybeFn === "function") { + return (maybeFn as (...a: unknown[]) => void).apply(instance, args); + } else { + const message = `${className} exported by ${mainPath} does not define a \`${key}()\` method`; + throw new TypeError(message); + } + } finally { + release(); + } + }); }; } From 9a26191e1a8c4246f7999bdb3637a176b9166207 Mon Sep 17 00:00:00 2001 From: MatinGathani <70268627+matingathani@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:26:53 +0530 Subject: [PATCH 18/18] [wrangler] fix: gracefully handle EMFILE when assets watcher exceeds directory limit (#14027) --- .../fix-assets-watcher-emfile-graceful.md | 13 ++ .../BundlerController.emfile.test.ts | 138 ++++++++++++++++++ .../api/startDevWorker/BundlerController.ts | 36 ++++- 3 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-assets-watcher-emfile-graceful.md create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/BundlerController.emfile.test.ts diff --git a/.changeset/fix-assets-watcher-emfile-graceful.md b/.changeset/fix-assets-watcher-emfile-graceful.md new file mode 100644 index 0000000000..9b511775b6 --- /dev/null +++ b/.changeset/fix-assets-watcher-emfile-graceful.md @@ -0,0 +1,13 @@ +--- +"wrangler": patch +--- + +Gracefully handle EMFILE error when assets directory exceeds OS watcher limit + +Previously, when `wrangler dev` was pointed at an assets directory with more than ~4,096 subdirectories, the chokidar file watcher threw an `EMFILE: too many open files` error that was not caught, causing an infinite error loop that made the dev server unresponsive. + +Now the error is caught and wrangler: + +1. Logs a clear warning explaining the platform watcher limit was hit +2. Recommends reducing the number of subdirectories by flattening or restructuring the assets directory +3. Disables the assets watcher gracefully so the dev server continues working without hot-reload diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundlerController.emfile.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundlerController.emfile.test.ts new file mode 100644 index 0000000000..783ecf3baa --- /dev/null +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundlerController.emfile.test.ts @@ -0,0 +1,138 @@ +import { EventEmitter } from "node:events"; +import path from "node:path"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { afterEach, beforeEach, describe, test, vi } from "vitest"; +import { BundlerController } from "../../../api/startDevWorker/BundlerController"; +import { FakeBus } from "../../helpers/fake-bus"; +import { mockConsoleMethods } from "../../helpers/mock-console"; +import type { StartDevWorkerOptions } from "../../../api"; +import type { FSWatcher } from "chokidar"; + +// Mock chokidar so we can simulate watcher errors without a real filesystem. +vi.mock("chokidar"); + +function configDefaults( + overrides: Partial = {} +): StartDevWorkerOptions { + const persist = path.join(process.cwd(), ".wrangler/persist"); + return { + name: "test-worker", + complianceRegion: undefined, + entrypoint: path.resolve("src/index.ts"), + projectRoot: path.resolve("src"), + legacy: {}, + dev: { persist }, + build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, + bundle: true, + moduleRules: [], + custom: {}, + define: {}, + format: "modules", + moduleRoot: path.resolve("src"), + exports: [], + }, + ...overrides, + }; +} + +describe("BundlerController — assets watcher EMFILE handling", () => { + const std = mockConsoleMethods(); + runInTempDir(); + + let bus: FakeBus; + let controller: BundlerController; + + beforeEach(async () => { + bus = new FakeBus(); + controller = new BundlerController(bus); + + // Set up a minimal entry point so onConfigUpdate doesn't fail. + await seed({ + "src/index.ts": `export default { fetch() { return new Response("ok"); } }`, + "assets/placeholder.txt": "hello", + }); + }); + + afterEach(() => controller.teardown()); + + test( + "logs a warning and disables the watcher when chokidar emits EMFILE", + { timeout: 5_000 }, + async ({ expect }) => { + const chokidar = await import("chokidar"); + const fakeWatcher = new EventEmitter() as EventEmitter & { + close: ReturnType; + }; + fakeWatcher.close = vi.fn().mockResolvedValue(undefined); + vi.mocked(chokidar.watch).mockReturnValue( + fakeWatcher as unknown as FSWatcher + ); + + const config = configDefaults({ + assets: { + directory: path.resolve("assets"), + binding: undefined, + routerConfig: { has_user_worker: true }, + assetConfig: {}, + }, + }); + + controller.onConfigUpdate({ type: "configUpdate", config }); + + // Let the async watch setup complete. + await new Promise((r) => setTimeout(r, 50)); + + const emfileError = Object.assign( + new Error("EMFILE: too many open files, watch"), + { code: "EMFILE" } + ); + fakeWatcher.emit("error", emfileError); + + // Tick once so the warning is flushed. + await new Promise((r) => setTimeout(r, 0)); + + expect(std.warn).toContain("platform limit"); + expect(std.warn).toContain("flattening"); + expect(fakeWatcher.close).toHaveBeenCalled(); + } + ); + + test( + "logs a warning and closes the watcher for non-EMFILE watcher errors", + { timeout: 5_000 }, + async ({ expect }) => { + const chokidar = await import("chokidar"); + const fakeWatcher = new EventEmitter() as EventEmitter & { + close: ReturnType; + }; + fakeWatcher.close = vi.fn().mockResolvedValue(undefined); + vi.mocked(chokidar.watch).mockReturnValue( + fakeWatcher as unknown as FSWatcher + ); + + const config = configDefaults({ + assets: { + directory: path.resolve("assets"), + binding: undefined, + routerConfig: { has_user_worker: true }, + assetConfig: {}, + }, + }); + + controller.onConfigUpdate({ type: "configUpdate", config }); + await new Promise((r) => setTimeout(r, 50)); + + const genericError = new Error("EACCES: permission denied"); + fakeWatcher.emit("error", genericError); + await new Promise((r) => setTimeout(r, 0)); + + expect(std.warn).toContain("encountered an error and has been disabled"); + expect(std.warn).toContain("EACCES: permission denied"); + // Watcher must be closed so the error doesn't loop. + expect(fakeWatcher.close).toHaveBeenCalled(); + } + ); +}); diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index e124e57c1e..1e1cec8647 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -315,14 +315,38 @@ export class BundlerController extends Controller { }); if (config.assets?.directory) { - this.#assetsWatcher = watch(config.assets.directory, { + const assetsDir = config.assets.directory; + const watcher = watch(assetsDir, { persistent: true, ignoreInitial: true, - }).on("all", async (eventName, filePath) => { - const message = getAssetChangeMessage(eventName, filePath); - logger.debug(`🌀 ${message}...`); - debouncedRefreshBundle(); - }); + }) + .on("all", async (eventName, filePath) => { + const message = getAssetChangeMessage(eventName, filePath); + logger.debug(`🌀 ${message}...`); + debouncedRefreshBundle(); + }) + .on("error", (err) => { + const errnoError = err as NodeJS.ErrnoException; + if (errnoError.code === "EMFILE") { + logger.warn( + `Assets directory watcher hit a platform limit and has been disabled.\n` + + `Hot-reloading will not reflect changes to files in ${assetsDir}.\n` + + `This can occur when watching very large assets directory trees.\n` + + `To work around this, reduce the number of subdirectories under ${assetsDir} by flattening or restructuring the assets directory.` + ); + } else { + logger.warn( + `Assets directory watcher encountered an error and has been disabled.\n` + + `Hot-reloading will not reflect changes to files in ${assetsDir}.\n` + + `Watcher error: ${err.message}` + ); + } + void watcher.close(); + if (this.#assetsWatcher === watcher) { + this.#assetsWatcher = undefined; + } + }); + this.#assetsWatcher = watcher; } }