From 6c7df19bc13e62a3ec3cdbc1f08dea30fa3f08d0 Mon Sep 17 00:00:00 2001 From: James Opstad <13586373+jamesopstad@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:42:12 +0100 Subject: [PATCH 1/6] Force the experimental new config on by default in the cf-vite dev delegate (#14351) --- .changeset/cf-vite-dev-force-build-output.md | 5 +++++ packages/vite-plugin-cloudflare/AGENTS.md | 9 ++++---- .../vite-plugin-cloudflare/src/cf-vite.ts | 22 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 .changeset/cf-vite-dev-force-build-output.md diff --git a/.changeset/cf-vite-dev-force-build-output.md b/.changeset/cf-vite-dev-force-build-output.md new file mode 100644 index 0000000000..4d012e77b2 --- /dev/null +++ b/.changeset/cf-vite-dev-force-build-output.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Force the experimental new config on by default in the `cf-vite dev` delegate diff --git a/packages/vite-plugin-cloudflare/AGENTS.md b/packages/vite-plugin-cloudflare/AGENTS.md index fd697fb90d..c1eae0576d 100644 --- a/packages/vite-plugin-cloudflare/AGENTS.md +++ b/packages/vite-plugin-cloudflare/AGENTS.md @@ -44,10 +44,11 @@ contract so the parent can drive either impl interchangeably. single-environment `build()` helper, which would skip the plugin's worker/build-output orchestration — mirrors Vite's own `vite build` CLI). It accepts **only `--mode`** (`--port`/`--host`/`--local` don't - apply to a build and exit `2`). It forces the experimental Build Output - API on by default by setting `CLOUDFLARE_VITE_FORCE_BUILD_OUTPUT` - (enabling `experimental.newConfig` + - `experimental.newConfig.cfBuildOutput`, overriding plugin config), + apply to a build and exit `2`). +- **Build Output API forced for every verb.** `main()` sets + `CLOUDFLARE_VITE_FORCE_BUILD_OUTPUT` unconditionally (before Vite + loads the user's config), enabling `experimental.newConfig` + + `experimental.newConfig.cfBuildOutput` (overriding plugin config), which requires a `cloudflare.config.ts` at the project root. The env var name and read logic live in `build-output-env.ts` (`FORCE_BUILD_OUTPUT_ENV_VAR` / `isForcedBuildOutput()`), shared by the diff --git a/packages/vite-plugin-cloudflare/src/cf-vite.ts b/packages/vite-plugin-cloudflare/src/cf-vite.ts index 81c8240d70..1a84e8acb1 100644 --- a/packages/vite-plugin-cloudflare/src/cf-vite.ts +++ b/packages/vite-plugin-cloudflare/src/cf-vite.ts @@ -9,6 +9,11 @@ * Unknown/missing verbs exit 2 (also the parent's version-detection * signal). * + * Every verb forces the experimental Build Output API on by default by + * setting `CLOUDFLARE_VITE_FORCE_BUILD_OUTPUT` in `main()` before Vite + * loads the user's config; the plugin reads it during config resolution + * to enable `experimental.newConfig` + `experimental.newConfig.cfBuildOutput`. + * * Spawn contract for `dev`: parent uses `stdio: "inherit"` and forwards * SIGINT/SIGTERM. Accepted flags mirror the sibling `cf-wrangler` * delegate (`--mode`, `--port`, `--host`, `--local`) so the parent can @@ -21,11 +26,7 @@ * `build` runs Vite's full multi-environment app build via * `createBuilder().buildApp()` (NOT the legacy single-environment * `build()` helper, which would skip the plugin's worker builds). It - * accepts only `--mode` (the other shared flags don't apply to a build) - * and forces the experimental Build Output API on by default by setting - * `CLOUDFLARE_VITE_FORCE_BUILD_OUTPUT`, which the plugin reads during - * config resolution to enable `experimental.newConfig` + - * `experimental.newConfig.cfBuildOutput`. + * accepts only `--mode` (the other shared flags don't apply to a build). * * Exit codes: 0 graceful, 2 unknown verb / parse error, 130 SIGINT, * 143 SIGTERM. @@ -127,6 +128,12 @@ async function main(): Promise { const verb = process.argv[2]; const userArgv = process.argv.slice(3); + // Force the experimental Build Output API on by default for every + // delegate verb. The plugin reads this during config resolution to + // enable `experimental.newConfig` + `.cfBuildOutput`. Set before Vite + // loads the user's `vite.config.ts`. + process.env[FORCE_BUILD_OUTPUT_ENV_VAR] = "true"; + if (verb === "dev") { return runDev(userArgv); } @@ -227,11 +234,6 @@ async function runBuild(userArgv: string[]): Promise { throw err; } - // Force the experimental Build Output API on by default. The plugin - // reads this env var to enable `experimental.newConfig` and `experimental.newConfig.cfBuildOutput`. - // Must be set before the builder loads the user's `vite.config.ts`. - process.env[FORCE_BUILD_OUTPUT_ENV_VAR] = "true"; - const inlineConfig: InlineConfig = {}; if (args.mode !== undefined) { inlineConfig.mode = args.mode; From dd7e1011cb3680a26d2021473fbb2c91d03d6947 Mon Sep 17 00:00:00 2001 From: ANT Bot <116369605+workers-devprod@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:08:47 +0100 Subject: [PATCH 2/6] Version Packages (#14327) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bright-trees-explain.md | 9 --- .changeset/bump-undici-7-28-0.md | 11 ---- .changeset/bump-ws-8-21-0.md | 9 --- .changeset/cf-vite-build-verb.md | 7 --- .changeset/cf-vite-dev-force-build-output.md | 5 -- .changeset/cf-wrangler-build-delegate.md | 7 --- .changeset/chubby-things-cheat.md | 14 ----- .changeset/dependabot-update-14331.md | 12 ---- .changeset/fancy-crabs-occur.md | 5 -- ...d1-auto-provisioned-binding-subcommands.md | 15 ----- .../fix-no-bundle-find-additional-modules.md | 7 --- .../typed-service-binding-dev-plugin.md | 9 --- .../vite-plugin-vitest4-resolve-external.md | 9 --- .changeset/whoami-temporary-account-hint.md | 7 --- .changeset/wise-planets-stare.md | 5 -- .../wrangler-experimental-cf-build-output.md | 7 --- packages/cli/CHANGELOG.md | 7 +++ packages/cli/package.json | 2 +- packages/create-cloudflare/CHANGELOG.md | 12 ++++ packages/create-cloudflare/package.json | 2 +- packages/deploy-helpers/CHANGELOG.md | 17 ++++++ packages/deploy-helpers/package.json | 2 +- packages/miniflare/CHANGELOG.md | 22 +++++++ packages/miniflare/package.json | 2 +- packages/pages-shared/CHANGELOG.md | 11 ++++ packages/pages-shared/package.json | 2 +- packages/quick-edit/CHANGELOG.md | 8 +++ packages/quick-edit/package.json | 2 +- packages/vite-plugin-cloudflare/CHANGELOG.md | 26 ++++++++ packages/vite-plugin-cloudflare/package.json | 2 +- packages/vitest-pool-workers/CHANGELOG.md | 14 +++++ packages/vitest-pool-workers/package.json | 2 +- packages/workers-auth/CHANGELOG.md | 9 +++ packages/workers-auth/package.json | 2 +- packages/workers-shared/CHANGELOG.md | 8 +++ packages/workers-shared/package.json | 2 +- packages/workers-utils/CHANGELOG.md | 10 ++++ packages/workers-utils/package.json | 2 +- packages/workflows-shared/CHANGELOG.md | 14 +++++ packages/workflows-shared/package.json | 2 +- packages/wrangler/CHANGELOG.md | 59 +++++++++++++++++++ packages/wrangler/package.json | 2 +- 42 files changed, 230 insertions(+), 151 deletions(-) delete mode 100644 .changeset/bright-trees-explain.md delete mode 100644 .changeset/bump-undici-7-28-0.md delete mode 100644 .changeset/bump-ws-8-21-0.md delete mode 100644 .changeset/cf-vite-build-verb.md delete mode 100644 .changeset/cf-vite-dev-force-build-output.md delete mode 100644 .changeset/cf-wrangler-build-delegate.md delete mode 100644 .changeset/chubby-things-cheat.md delete mode 100644 .changeset/dependabot-update-14331.md delete mode 100644 .changeset/fancy-crabs-occur.md delete mode 100644 .changeset/fix-d1-auto-provisioned-binding-subcommands.md delete mode 100644 .changeset/fix-no-bundle-find-additional-modules.md delete mode 100644 .changeset/typed-service-binding-dev-plugin.md delete mode 100644 .changeset/vite-plugin-vitest4-resolve-external.md delete mode 100644 .changeset/whoami-temporary-account-hint.md delete mode 100644 .changeset/wise-planets-stare.md delete mode 100644 .changeset/wrangler-experimental-cf-build-output.md diff --git a/.changeset/bright-trees-explain.md b/.changeset/bright-trees-explain.md deleted file mode 100644 index 135145c963..0000000000 --- a/.changeset/bright-trees-explain.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@cloudflare/workflows-shared": patch ---- - -Add step context to Workflows rollback handlers - -Rollback handlers now receive the original step context under `ctx`, making `ctx.step.name`, `ctx.step.count`, `ctx.attempt`, and the resolved step `config` available during rollback. The legacy `stepName` field remains available and is equivalent to `${ctx.step.name}-${ctx.step.count}`. - -`rollbackConfig` is now limited to retry and timeout settings, matching the behavior supported by rollback handlers. diff --git a/.changeset/bump-undici-7-28-0.md b/.changeset/bump-undici-7-28-0.md deleted file mode 100644 index 93aa324877..0000000000 --- a/.changeset/bump-undici-7-28-0.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@cloudflare/deploy-helpers": patch -"@cloudflare/vitest-pool-workers": patch -"@cloudflare/workers-auth": patch -"@cloudflare/workers-utils": patch -"create-cloudflare": patch -"miniflare": patch -"wrangler": patch ---- - -Update undici from 7.24.8 to 7.28.0 diff --git a/.changeset/bump-ws-8-21-0.md b/.changeset/bump-ws-8-21-0.md deleted file mode 100644 index fc1bcf5193..0000000000 --- a/.changeset/bump-ws-8-21-0.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"miniflare": patch -"wrangler": patch -"@cloudflare/vite-plugin": patch ---- - -Bump `ws` from 8.20.1 to 8.21.0 to address GHSA-96hv-2xvq-fx4p - -[GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) / [CVE-2026-48779](https://www.cve.org/CVERecord?id=CVE-2026-48779) (high severity) reports a remote memory-exhaustion DoS in `ws@<8.21.0`: a peer sending a high volume of tiny fragments and data chunks over modest network traffic can crash a `ws` server or client via OOM. The fix shipped in [ws@8.21.0](https://github.com/websockets/ws/releases/tag/8.21.0) (commit `2b2abd45`, released 2026-05-22), which also introduces the `maxBufferedChunks` and `maxFragments` options. This change bumps the workspace catalog entry so that `miniflare`, `wrangler`, and `@cloudflare/vite-plugin` all pick up the patched release. diff --git a/.changeset/cf-vite-build-verb.md b/.changeset/cf-vite-build-verb.md deleted file mode 100644 index e19a256264..0000000000 --- a/.changeset/cf-vite-build-verb.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/vite-plugin": minor ---- - -Add a `build` command to the experimental, internal `cf-vite` delegate binary - -`cf-vite build` runs Vite's full multi-environment app build (via the Builder API) and enables the experimental Build Output API by default, emitting a self-contained `.cloudflare/output/v0/` directory. It forces `experimental.newConfig` and `experimental.newConfig.cfBuildOutput` on, so a `cloudflare.config.ts` is required at the project root. diff --git a/.changeset/cf-vite-dev-force-build-output.md b/.changeset/cf-vite-dev-force-build-output.md deleted file mode 100644 index 4d012e77b2..0000000000 --- a/.changeset/cf-vite-dev-force-build-output.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@cloudflare/vite-plugin": patch ---- - -Force the experimental new config on by default in the `cf-vite dev` delegate diff --git a/.changeset/cf-wrangler-build-delegate.md b/.changeset/cf-wrangler-build-delegate.md deleted file mode 100644 index ba848c1df5..0000000000 --- a/.changeset/cf-wrangler-build-delegate.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": minor ---- - -Add `cf-wrangler build` delegate support - -The experimental `cf-wrangler` delegate binary now accepts `build` and emits the Build Output API directory through Wrangler's new-config build path. This lets parent tools invoke Wrangler's build-output implementation with `cf-wrangler build` instead of shelling out through the public Wrangler CLI. diff --git a/.changeset/chubby-things-cheat.md b/.changeset/chubby-things-cheat.md deleted file mode 100644 index 3531a53758..0000000000 --- a/.changeset/chubby-things-cheat.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"create-cloudflare": patch -"miniflare": patch -"@cloudflare/pages-shared": patch -"@cloudflare/quick-edit": patch -"@cloudflare/vitest-pool-workers": patch -"@cloudflare/workers-shared": patch -"@cloudflare/workflows-shared": patch -"wrangler": patch ---- - -Bump esbuild to 0.28.1 - -This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. diff --git a/.changeset/dependabot-update-14331.md b/.changeset/dependabot-update-14331.md deleted file mode 100644 index 321fd14d90..0000000000 --- a/.changeset/dependabot-update-14331.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.20260616.1 | 1.20260617.1 | diff --git a/.changeset/fancy-crabs-occur.md b/.changeset/fancy-crabs-occur.md deleted file mode 100644 index 3a7885be44..0000000000 --- a/.changeset/fancy-crabs-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@cloudflare/deploy-helpers": patch ---- - -add `skipLastDeployedFromApiCheck` to override deploy source check diff --git a/.changeset/fix-d1-auto-provisioned-binding-subcommands.md b/.changeset/fix-d1-auto-provisioned-binding-subcommands.md deleted file mode 100644 index 52c3797b16..0000000000 --- a/.changeset/fix-d1-auto-provisioned-binding-subcommands.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"wrangler": patch ---- - -Resolve auto-provisioned D1 bindings via the API in remote subcommands - -Remote D1 subcommands (`d1 execute --remote`, `d1 export --remote`, `d1 info`, `d1 insights`, `d1 delete`, `d1 migrations apply --remote`, `d1 migrations list --remote`, `d1 time-travel`) previously failed with: - -> Found a database with name or binding DB but it is missing a database_id, which is needed for operations on remote resources. - -when the `[[d1_databases]]` config entry only had `binding` and `database_name` (the shape `wrangler deploy` writes for automatically-provisioned bindings). They now resolve the real database UUID via `GET /accounts/:accountId/d1/database/:name?fields=uuid` and proceed as if `database_id` had been set in config. - -If the config entry only has a `binding` (no `database_name`, no `database_id`), the lookup uses the same name `wrangler deploy` would create via auto provisioning (`-`). - -Non-404 API failures (auth, rate-limit, server errors) now propagate verbatim instead of being masked as "database not found". diff --git a/.changeset/fix-no-bundle-find-additional-modules.md b/.changeset/fix-no-bundle-find-additional-modules.md deleted file mode 100644 index 8e8ef3897b..0000000000 --- a/.changeset/fix-no-bundle-find-additional-modules.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Respect `find_additional_modules = false` when `no_bundle` is set - -When using `no_bundle = true`, wrangler was always scanning for and attaching additional modules even if `find_additional_modules` was explicitly set to `false` in the config. Additional modules are now only collected when `find_additional_modules` is not `false`, consistent with the bundled code path. diff --git a/.changeset/typed-service-binding-dev-plugin.md b/.changeset/typed-service-binding-dev-plugin.md deleted file mode 100644 index 21622bd0fc..0000000000 --- a/.changeset/typed-service-binding-dev-plugin.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"wrangler": patch -"@cloudflare/workers-utils": patch -"@cloudflare/deploy-helpers": patch ---- - -Support `dev.plugin` on typed services bindings - -Wrangler only honored `dev.plugin` on `unsafe.bindings` entries, so users authoring a service binding via `services[]` could not wire it to a local Miniflare plugin during `wrangler dev` — they had to fall back to `unsafe.bindings` and accept a "directly supported by wrangler" warning. Typed services bindings now accept the same `dev: { plugin, options? }` shape, route the binding through Miniflare's external-plugin pathway in `wrangler dev`, and strip the field at deploy time. Validation rejects malformed `dev` shapes. diff --git a/.changeset/vite-plugin-vitest4-resolve-external.md b/.changeset/vite-plugin-vitest4-resolve-external.md deleted file mode 100644 index b09c19bdad..0000000000 --- a/.changeset/vite-plugin-vitest4-resolve-external.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@cloudflare/vite-plugin": patch ---- - -Allow `resolve.external` containing only Node.js built-ins in Worker environments - -Vitest 4 automatically sets `resolve.external` to the full list of Node.js built-in modules for non-standard Vite environments via its internal `runnerTransform` plugin. Previously, the Cloudflare Vite plugin rejected any non-empty `resolve.external` array, throwing an incompatibility error on startup when used alongside Vitest 4. - -The validation check now allows `resolve.external` arrays that contain only Node.js built-in module names (both bare `fs` and `node:fs` forms). The error is only thrown when `resolve.external` is `true` or contains non-built-in package names that would prevent user code from being bundled into the Worker. diff --git a/.changeset/whoami-temporary-account-hint.md b/.changeset/whoami-temporary-account-hint.md deleted file mode 100644 index e2eddefc35..0000000000 --- a/.changeset/whoami-temporary-account-hint.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Mention temporary preview accounts in `wrangler whoami` output when unauthenticated - -When you run `wrangler whoami` without being logged in, Wrangler now also tells you that you can deploy without logging in by running a command like `wrangler deploy --temporary` to use a temporary preview account. diff --git a/.changeset/wise-planets-stare.md b/.changeset/wise-planets-stare.md deleted file mode 100644 index bd2dc4f326..0000000000 --- a/.changeset/wise-planets-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"create-cloudflare": patch ---- - -Upgrade `create-react-router` to `8.0.0` and prevent React Router template overriding v8 dependencies diff --git a/.changeset/wrangler-experimental-cf-build-output.md b/.changeset/wrangler-experimental-cf-build-output.md deleted file mode 100644 index e85495d3ea..0000000000 --- a/.changeset/wrangler-experimental-cf-build-output.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": minor ---- - -Add experimental `--experimental-cf-build-output` flag to `wrangler build` - -When used alongside `--experimental-new-config`, `wrangler build` now emits a self-contained Build Output API directory under `.cloudflare/output/v0/` instead of delegating to `wrangler deploy --dry-run`. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index dfe4c64f38..777aa0f2c1 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/cli +## 0.1.9 + +### Patch Changes + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb)]: + - @cloudflare/workers-utils@0.23.2 + ## 0.1.8 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 3c6e2c0204..9baba9804b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/cli-shared-helpers", - "version": "0.1.8", + "version": "0.1.9", "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 f38bd582f9..bee3b5abd6 100644 --- a/packages/create-cloudflare/CHANGELOG.md +++ b/packages/create-cloudflare/CHANGELOG.md @@ -1,5 +1,17 @@ # create-cloudflare +## 2.70.4 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + +- [#14349](https://github.com/cloudflare/workers-sdk/pull/14349) [`3eec0f7`](https://github.com/cloudflare/workers-sdk/commit/3eec0f7b8829af28634955f693af53918acf00af) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Upgrade `create-react-router` to `8.0.0` and prevent React Router template overriding v8 dependencies + ## 2.70.3 ### Patch Changes diff --git a/packages/create-cloudflare/package.json b/packages/create-cloudflare/package.json index 086557c560..c8cfb31667 100644 --- a/packages/create-cloudflare/package.json +++ b/packages/create-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "create-cloudflare", - "version": "2.70.3", + "version": "2.70.4", "description": "A CLI for creating and deploying new applications to Cloudflare.", "keywords": [ "cloudflare", diff --git a/packages/deploy-helpers/CHANGELOG.md b/packages/deploy-helpers/CHANGELOG.md index 1441e4aa0e..7b6f0fd35e 100644 --- a/packages/deploy-helpers/CHANGELOG.md +++ b/packages/deploy-helpers/CHANGELOG.md @@ -1,5 +1,22 @@ # @cloudflare/deploy-helpers +## 0.2.1 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14340](https://github.com/cloudflare/workers-sdk/pull/14340) [`f6e49dd`](https://github.com/cloudflare/workers-sdk/commit/f6e49dd59190328007331477450651e8bca2def8) Thanks [@emily-shen](https://github.com/emily-shen)! - add `skipLastDeployedFromApiCheck` to override deploy source check + +- [#14269](https://github.com/cloudflare/workers-sdk/pull/14269) [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb) Thanks [@mattjohnsonpint](https://github.com/mattjohnsonpint)! - Support `dev.plugin` on typed services bindings + + Wrangler only honored `dev.plugin` on `unsafe.bindings` entries, so users authoring a service binding via `services[]` could not wire it to a local Miniflare plugin during `wrangler dev` — they had to fall back to `unsafe.bindings` and accept a "directly supported by wrangler" warning. Typed services bindings now accept the same `dev: { plugin, options? }` shape, route the binding through Miniflare's external-plugin pathway in `wrangler dev`, and strip the field at deploy time. Validation rejects malformed `dev` shapes. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb)]: + - @cloudflare/workers-utils@0.23.2 + - miniflare@4.20260617.0 + - @cloudflare/cli-shared-helpers@0.1.9 + ## 0.2.0 ### Minor Changes diff --git a/packages/deploy-helpers/package.json b/packages/deploy-helpers/package.json index 6d295a99b4..c2d5ddd01b 100644 --- a/packages/deploy-helpers/package.json +++ b/packages/deploy-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/deploy-helpers", - "version": "0.2.0", + "version": "0.2.1", "description": "Internal deploy helpers for workers-sdk. Not intended for external use — APIs may change without notice.", "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/deploy-helpers#readme", "bugs": { diff --git a/packages/miniflare/CHANGELOG.md b/packages/miniflare/CHANGELOG.md index b89466ca58..77dc227a21 100644 --- a/packages/miniflare/CHANGELOG.md +++ b/packages/miniflare/CHANGELOG.md @@ -1,5 +1,27 @@ # miniflare +## 4.20260617.0 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14346](https://github.com/cloudflare/workers-sdk/pull/14346) [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d) Thanks [@haidargit](https://github.com/haidargit)! - Bump `ws` from 8.20.1 to 8.21.0 to address GHSA-96hv-2xvq-fx4p + + [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) / [CVE-2026-48779](https://www.cve.org/CVERecord?id=CVE-2026-48779) (high severity) reports a remote memory-exhaustion DoS in `ws@<8.21.0`: a peer sending a high volume of tiny fragments and data chunks over modest network traffic can crash a `ws` server or client via OOM. The fix shipped in [ws@8.21.0](https://github.com/websockets/ws/releases/tag/8.21.0) (commit `2b2abd45`, released 2026-05-22), which also introduces the `maxBufferedChunks` and `maxFragments` options. This change bumps the workspace catalog entry so that `miniflare`, `wrangler`, and `@cloudflare/vite-plugin` all pick up the patched release. + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + +- [#14331](https://github.com/cloudflare/workers-sdk/pull/14331) [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260616.1 | 1.20260617.1 | + ## 4.20260616.0 ### Minor Changes diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 28fedc2348..6fd00ce420 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -1,6 +1,6 @@ { "name": "miniflare", - "version": "4.20260616.0", + "version": "4.20260617.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 6b30e44758..8db4ed75b0 100644 --- a/packages/pages-shared/CHANGELOG.md +++ b/packages/pages-shared/CHANGELOG.md @@ -1,5 +1,16 @@ # @cloudflare/pages-shared +## 0.13.147 + +### Patch Changes + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264)]: + - miniflare@4.20260617.0 + ## 0.13.146 ### Patch Changes diff --git a/packages/pages-shared/package.json b/packages/pages-shared/package.json index 48ce82e4c7..76d14428d4 100644 --- a/packages/pages-shared/package.json +++ b/packages/pages-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/pages-shared", - "version": "0.13.146", + "version": "0.13.147", "repository": { "type": "git", "url": "https://github.com/cloudflare/workers-sdk.git", diff --git a/packages/quick-edit/CHANGELOG.md b/packages/quick-edit/CHANGELOG.md index 0f4c455337..f1e9ff3b8a 100644 --- a/packages/quick-edit/CHANGELOG.md +++ b/packages/quick-edit/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/quick-edit +## 0.4.7 + +### Patch Changes + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + ## 0.4.6 ### Patch Changes diff --git a/packages/quick-edit/package.json b/packages/quick-edit/package.json index c773388b76..eb3fcf135c 100644 --- a/packages/quick-edit/package.json +++ b/packages/quick-edit/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/quick-edit", - "version": "0.4.6", + "version": "0.4.7", "private": true, "description": "VSCode for Web hosted for use in Cloudflare's Quick Editor", "homepage": "https://github.com/cloudflare/workers-sdk#readme", diff --git a/packages/vite-plugin-cloudflare/CHANGELOG.md b/packages/vite-plugin-cloudflare/CHANGELOG.md index 5fab03d508..baa05ce199 100644 --- a/packages/vite-plugin-cloudflare/CHANGELOG.md +++ b/packages/vite-plugin-cloudflare/CHANGELOG.md @@ -1,5 +1,31 @@ # @cloudflare/vite-plugin +## 1.42.0 + +### Minor Changes + +- [#14339](https://github.com/cloudflare/workers-sdk/pull/14339) [`aa49856`](https://github.com/cloudflare/workers-sdk/commit/aa49856d68f0fc08d2d0afdf48602a7175761684) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Add a `build` command to the experimental, internal `cf-vite` delegate binary + + `cf-vite build` runs Vite's full multi-environment app build (via the Builder API) and enables the experimental Build Output API by default, emitting a self-contained `.cloudflare/output/v0/` directory. It forces `experimental.newConfig` and `experimental.newConfig.cfBuildOutput` on, so a `cloudflare.config.ts` is required at the project root. + +### Patch Changes + +- [#14346](https://github.com/cloudflare/workers-sdk/pull/14346) [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d) Thanks [@haidargit](https://github.com/haidargit)! - Bump `ws` from 8.20.1 to 8.21.0 to address GHSA-96hv-2xvq-fx4p + + [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) / [CVE-2026-48779](https://www.cve.org/CVERecord?id=CVE-2026-48779) (high severity) reports a remote memory-exhaustion DoS in `ws@<8.21.0`: a peer sending a high volume of tiny fragments and data chunks over modest network traffic can crash a `ws` server or client via OOM. The fix shipped in [ws@8.21.0](https://github.com/websockets/ws/releases/tag/8.21.0) (commit `2b2abd45`, released 2026-05-22), which also introduces the `maxBufferedChunks` and `maxFragments` options. This change bumps the workspace catalog entry so that `miniflare`, `wrangler`, and `@cloudflare/vite-plugin` all pick up the patched release. + +- [#14351](https://github.com/cloudflare/workers-sdk/pull/14351) [`6c7df19`](https://github.com/cloudflare/workers-sdk/commit/6c7df19bc13e62a3ec3cdbc1f08dea30fa3f08d0) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Force the experimental new config on by default in the `cf-vite dev` delegate + +- [#14218](https://github.com/cloudflare/workers-sdk/pull/14218) [`4eed569`](https://github.com/cloudflare/workers-sdk/commit/4eed569ffb8e7b1dc241290ccaa76c3b76523269) Thanks [@matingathani](https://github.com/matingathani)! - Allow `resolve.external` containing only Node.js built-ins in Worker environments + + Vitest 4 automatically sets `resolve.external` to the full list of Node.js built-in modules for non-standard Vite environments via its internal `runnerTransform` plugin. Previously, the Cloudflare Vite plugin rejected any non-empty `resolve.external` array, throwing an incompatibility error on startup when used alongside Vitest 4. + + The validation check now allows `resolve.external` arrays that contain only Node.js built-in module names (both bare `fs` and `node:fs` forms). The error is only thrown when `resolve.external` is `true` or contains non-built-in package names that would prevent user code from being bundled into the Worker. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`f6e49dd`](https://github.com/cloudflare/workers-sdk/commit/f6e49dd59190328007331477450651e8bca2def8), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264), [`594544d`](https://github.com/cloudflare/workers-sdk/commit/594544da71e570f878d1dfa80c8f646ec2cf7df2), [`a79b899`](https://github.com/cloudflare/workers-sdk/commit/a79b899e284d46a8f0f9c4df113068ba66aaad0f), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb), [`ca61558`](https://github.com/cloudflare/workers-sdk/commit/ca6155879c2027765977ff14d17b4d6ad53473e1), [`36777db`](https://github.com/cloudflare/workers-sdk/commit/36777dbd694acdf0a2d1fc2be322a47bd409e7fe)]: + - miniflare@4.20260617.0 + - wrangler@4.102.0 + ## 1.41.0 ### Minor Changes diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index cf4ce7d4f8..89178e566d 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.41.0", + "version": "1.42.0", "description": "Cloudflare plugin for Vite", "keywords": [ "cloudflare", diff --git a/packages/vitest-pool-workers/CHANGELOG.md b/packages/vitest-pool-workers/CHANGELOG.md index 36c18ad79b..3fba9e5d3d 100644 --- a/packages/vitest-pool-workers/CHANGELOG.md +++ b/packages/vitest-pool-workers/CHANGELOG.md @@ -1,5 +1,19 @@ # @cloudflare/vitest-pool-workers +## 0.16.17 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`f6e49dd`](https://github.com/cloudflare/workers-sdk/commit/f6e49dd59190328007331477450651e8bca2def8), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264), [`594544d`](https://github.com/cloudflare/workers-sdk/commit/594544da71e570f878d1dfa80c8f646ec2cf7df2), [`a79b899`](https://github.com/cloudflare/workers-sdk/commit/a79b899e284d46a8f0f9c4df113068ba66aaad0f), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb), [`ca61558`](https://github.com/cloudflare/workers-sdk/commit/ca6155879c2027765977ff14d17b4d6ad53473e1), [`36777db`](https://github.com/cloudflare/workers-sdk/commit/36777dbd694acdf0a2d1fc2be322a47bd409e7fe)]: + - miniflare@4.20260617.0 + - wrangler@4.102.0 + ## 0.16.16 ### Patch Changes diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index c247b67769..3336914efd 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.16", + "version": "0.16.17", "description": "Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime", "keywords": [ "cloudflare", diff --git a/packages/workers-auth/CHANGELOG.md b/packages/workers-auth/CHANGELOG.md index f35cfd82e5..3b26083e44 100644 --- a/packages/workers-auth/CHANGELOG.md +++ b/packages/workers-auth/CHANGELOG.md @@ -1,5 +1,14 @@ # @cloudflare/workers-auth +## 0.3.1 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb)]: + - @cloudflare/workers-utils@0.23.2 + ## 0.3.0 ### Minor Changes diff --git a/packages/workers-auth/package.json b/packages/workers-auth/package.json index 3b138ff98e..af9c04eee8 100644 --- a/packages/workers-auth/package.json +++ b/packages/workers-auth/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-auth", - "version": "0.3.0", + "version": "0.3.1", "description": "Internal OAuth 2.0 + PKCE flow for Cloudflare CLIs. Not intended for external use — APIs may change without notice.", "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/workers-auth#readme", "bugs": { diff --git a/packages/workers-shared/CHANGELOG.md b/packages/workers-shared/CHANGELOG.md index a0e3a66ae9..8365d891d4 100644 --- a/packages/workers-shared/CHANGELOG.md +++ b/packages/workers-shared/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/workers-shared +## 0.19.7 + +### Patch Changes + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + ## 0.19.6 ### Patch Changes diff --git a/packages/workers-shared/package.json b/packages/workers-shared/package.json index 1b0f88188f..c1f14679e2 100644 --- a/packages/workers-shared/package.json +++ b/packages/workers-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-shared", - "version": "0.19.6", + "version": "0.19.7", "private": true, "description": "Package that is used at Cloudflare to power some internal features of Cloudflare Workers.", "keywords": [ diff --git a/packages/workers-utils/CHANGELOG.md b/packages/workers-utils/CHANGELOG.md index fad7fa386c..3a133b4067 100644 --- a/packages/workers-utils/CHANGELOG.md +++ b/packages/workers-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @cloudflare/workers-utils +## 0.23.2 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14269](https://github.com/cloudflare/workers-sdk/pull/14269) [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb) Thanks [@mattjohnsonpint](https://github.com/mattjohnsonpint)! - Support `dev.plugin` on typed services bindings + + Wrangler only honored `dev.plugin` on `unsafe.bindings` entries, so users authoring a service binding via `services[]` could not wire it to a local Miniflare plugin during `wrangler dev` — they had to fall back to `unsafe.bindings` and accept a "directly supported by wrangler" warning. Typed services bindings now accept the same `dev: { plugin, options? }` shape, route the binding through Miniflare's external-plugin pathway in `wrangler dev`, and strip the field at deploy time. Validation rejects malformed `dev` shapes. + ## 0.23.1 ### Patch Changes diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index ea620a1ddd..d4f1e8a00e 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workers-utils", - "version": "0.23.1", + "version": "0.23.2", "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 4068a174fe..c5113b474b 100644 --- a/packages/workflows-shared/CHANGELOG.md +++ b/packages/workflows-shared/CHANGELOG.md @@ -1,5 +1,19 @@ # @cloudflare/workflows-shared +## 0.11.2 + +### Patch Changes + +- [#14318](https://github.com/cloudflare/workers-sdk/pull/14318) [`f32e9c1`](https://github.com/cloudflare/workers-sdk/commit/f32e9c1fdbf8ab7c0e68afa613ec61e367be04cb) Thanks [@vaishnav-mk](https://github.com/vaishnav-mk)! - Add step context to Workflows rollback handlers + + Rollback handlers now receive the original step context under `ctx`, making `ctx.step.name`, `ctx.step.count`, `ctx.attempt`, and the resolved step `config` available during rollback. The legacy `stepName` field remains available and is equivalent to `${ctx.step.name}-${ctx.step.count}`. + + `rollbackConfig` is now limited to retry and timeout settings, matching the behavior supported by rollback handlers. + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + ## 0.11.1 ### Patch Changes diff --git a/packages/workflows-shared/package.json b/packages/workflows-shared/package.json index f2fd302dbb..bd27eb4ac9 100644 --- a/packages/workflows-shared/package.json +++ b/packages/workflows-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/workflows-shared", - "version": "0.11.1", + "version": "0.11.2", "private": true, "description": "Package that is used at Cloudflare to power some internal features of Cloudflare Workflows.", "keywords": [ diff --git a/packages/wrangler/CHANGELOG.md b/packages/wrangler/CHANGELOG.md index 5d7604e325..5cdf49996d 100644 --- a/packages/wrangler/CHANGELOG.md +++ b/packages/wrangler/CHANGELOG.md @@ -1,5 +1,64 @@ # wrangler +## 4.102.0 + +### Minor Changes + +- [#14340](https://github.com/cloudflare/workers-sdk/pull/14340) [`f6e49dd`](https://github.com/cloudflare/workers-sdk/commit/f6e49dd59190328007331477450651e8bca2def8) Thanks [@emily-shen](https://github.com/emily-shen)! - Add `cf-wrangler build` delegate support + + The experimental `cf-wrangler` delegate binary now accepts `build` and emits the Build Output API directory through Wrangler's new-config build path. This lets parent tools invoke Wrangler's build-output implementation with `cf-wrangler build` instead of shelling out through the public Wrangler CLI. + +- [#14324](https://github.com/cloudflare/workers-sdk/pull/14324) [`36777db`](https://github.com/cloudflare/workers-sdk/commit/36777dbd694acdf0a2d1fc2be322a47bd409e7fe) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Add experimental `--experimental-cf-build-output` flag to `wrangler build` + + When used alongside `--experimental-new-config`, `wrangler build` now emits a self-contained Build Output API directory under `.cloudflare/output/v0/` instead of delegating to `wrangler deploy --dry-run`. + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14346](https://github.com/cloudflare/workers-sdk/pull/14346) [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d) Thanks [@haidargit](https://github.com/haidargit)! - Bump `ws` from 8.20.1 to 8.21.0 to address GHSA-96hv-2xvq-fx4p + + [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) / [CVE-2026-48779](https://www.cve.org/CVERecord?id=CVE-2026-48779) (high severity) reports a remote memory-exhaustion DoS in `ws@<8.21.0`: a peer sending a high volume of tiny fragments and data chunks over modest network traffic can crash a `ws` server or client via OOM. The fix shipped in [ws@8.21.0](https://github.com/websockets/ws/releases/tag/8.21.0) (commit `2b2abd45`, released 2026-05-22), which also introduces the `maxBufferedChunks` and `maxFragments` options. This change bumps the workspace catalog entry so that `miniflare`, `wrangler`, and `@cloudflare/vite-plugin` all pick up the patched release. + +- [#14314](https://github.com/cloudflare/workers-sdk/pull/14314) [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6) Thanks [@harryzcy](https://github.com/harryzcy)! - Bump esbuild to 0.28.1 + + This update includes several bug fixes from esbuild versions 0.27.3 through 0.28.1. See the [esbuild changelog](https://github.com/evanw/esbuild/blob/v0.28.1/CHANGELOG.md) for details. + +- [#14331](https://github.com/cloudflare/workers-sdk/pull/14331) [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264) Thanks [@dependabot](https://github.com/apps/dependabot)! - Update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260616.1 | 1.20260617.1 | + +- [#14275](https://github.com/cloudflare/workers-sdk/pull/14275) [`594544d`](https://github.com/cloudflare/workers-sdk/commit/594544da71e570f878d1dfa80c8f646ec2cf7df2) Thanks [@alsuren](https://github.com/alsuren)! - Resolve auto-provisioned D1 bindings via the API in remote subcommands + + Remote D1 subcommands (`d1 execute --remote`, `d1 export --remote`, `d1 info`, `d1 insights`, `d1 delete`, `d1 migrations apply --remote`, `d1 migrations list --remote`, `d1 time-travel`) previously failed with: + + > Found a database with name or binding DB but it is missing a database_id, which is needed for operations on remote resources. + + when the `[[d1_databases]]` config entry only had `binding` and `database_name` (the shape `wrangler deploy` writes for automatically-provisioned bindings). They now resolve the real database UUID via `GET /accounts/:accountId/d1/database/:name?fields=uuid` and proceed as if `database_id` had been set in config. + + If the config entry only has a `binding` (no `database_name`, no `database_id`), the lookup uses the same name `wrangler deploy` would create via auto provisioning (`-`). + + Non-404 API failures (auth, rate-limit, server errors) now propagate verbatim instead of being masked as "database not found". + +- [#14315](https://github.com/cloudflare/workers-sdk/pull/14315) [`a79b899`](https://github.com/cloudflare/workers-sdk/commit/a79b899e284d46a8f0f9c4df113068ba66aaad0f) Thanks [@matingathani](https://github.com/matingathani)! - Respect `find_additional_modules = false` when `no_bundle` is set + + When using `no_bundle = true`, wrangler was always scanning for and attaching additional modules even if `find_additional_modules` was explicitly set to `false` in the config. Additional modules are now only collected when `find_additional_modules` is not `false`, consistent with the bundled code path. + +- [#14269](https://github.com/cloudflare/workers-sdk/pull/14269) [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb) Thanks [@mattjohnsonpint](https://github.com/mattjohnsonpint)! - Support `dev.plugin` on typed services bindings + + Wrangler only honored `dev.plugin` on `unsafe.bindings` entries, so users authoring a service binding via `services[]` could not wire it to a local Miniflare plugin during `wrangler dev` — they had to fall back to `unsafe.bindings` and accept a "directly supported by wrangler" warning. Typed services bindings now accept the same `dev: { plugin, options? }` shape, route the binding through Miniflare's external-plugin pathway in `wrangler dev`, and strip the field at deploy time. Validation rejects malformed `dev` shapes. + +- [#14328](https://github.com/cloudflare/workers-sdk/pull/14328) [`ca61558`](https://github.com/cloudflare/workers-sdk/commit/ca6155879c2027765977ff14d17b4d6ad53473e1) Thanks [@edevil](https://github.com/edevil)! - Mention temporary preview accounts in `wrangler whoami` output when unauthenticated + + When you run `wrangler whoami` without being logged in, Wrangler now also tells you that you can deploy without logging in by running a command like `wrangler deploy --temporary` to use a temporary preview account. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264)]: + - miniflare@4.20260617.0 + ## 4.101.0 ### Minor Changes diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 95b5d8ded4..a8c80b311f 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -1,6 +1,6 @@ { "name": "wrangler", - "version": "4.101.0", + "version": "4.102.0", "description": "Command-line interface for all things Cloudflare Workers", "keywords": [ "assembly", From b38823fb35a8bdcd00004e74404ab18d7b070dbf Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Thu, 18 Jun 2026 16:17:59 +0100 Subject: [PATCH 3/6] [workflows-shared] Fix Uint8Array step outputs dragging backing ArrayBuffer in local Workflows (#14118) --- .../fix-workflows-uint8array-step-output.md | 8 + packages/workflows-shared/src/context.ts | 107 ++++++++- .../workflows-shared/tests/context.test.ts | 217 ++++++++++++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-workflows-uint8array-step-output.md diff --git a/.changeset/fix-workflows-uint8array-step-output.md b/.changeset/fix-workflows-uint8array-step-output.md new file mode 100644 index 0000000000..2c779c2b2b --- /dev/null +++ b/.changeset/fix-workflows-uint8array-step-output.md @@ -0,0 +1,8 @@ +--- +"miniflare": patch +"wrangler": patch +--- + +Fix `Uint8Array` step outputs in local Workflows being persisted with the full backing `ArrayBuffer` + +A `Uint8Array` returned from a Workflows step under `wrangler dev` was serialised together with its full underlying `ArrayBuffer`, causing a raw `SQLITE_TOOBIG` error at view sizes well below the documented 1MiB step-output limit. For example, a 200KB view sliced from an 800KB buffer (a common pattern from `crypto.getRandomValues` or `arr.slice(...)` on a larger pool) would fail. The view's bytes are now copied to a tight buffer before persistence, bringing local behaviour in line with production. Fixes #14101. diff --git a/packages/workflows-shared/src/context.ts b/packages/workflows-shared/src/context.ts index 108590dafc..3d20639e99 100644 --- a/packages/workflows-shared/src/context.ts +++ b/packages/workflows-shared/src/context.ts @@ -66,6 +66,103 @@ const defaultConfig: ResolvedStepConfig = { timeout: "10 minutes", }; +/** + * Returns a copy of `value` that is safe to persist via Durable Object SQL + * storage without dragging unrelated bytes along with typed-array views. + * + * Background: workerd's `v8::ValueSerializer` writes the entire backing + * `ArrayBuffer` for typed-array views, not just `byteLength` bytes. A view + * sliced from a much larger buffer (`crypto.getRandomValues`, `arr.slice(...)`, + * fetch-stream copies) blows up the wire size by a factor of + * (backing-size / view-size) and can hit `SQLITE_TOOBIG` at view sizes well + * below the documented 1MiB step-output limit (see issue #14101). Copying the + * view's bytes into a tight backing buffer before persistence brings local + * `wrangler dev` behaviour in line with production. + * + * The walk is recursive (cycle-safe via a `WeakMap`) so views nested inside + * objects, arrays, Maps, and Sets are also compacted. View types are preserved + * (`Uint8Array` stays `Uint8Array`, `Int16Array` stays `Int16Array`, etc.) so + * the persisted shape matches the live shape — cached replays observe the + * same constructor type the step originally returned. Class instances and + * host objects (`Date`, `RegExp`, raw `ArrayBuffer`, streams, `Blob`, …) are + * passed through unchanged — recursing into them would either fail to + * reconstruct the original type or trigger their own structured-clone path. + */ +function normalizeForStorage( + value: unknown, + seen: WeakMap = new WeakMap() +): unknown { + // Primitives: nothing to do. + if (value === null || typeof value !== "object") { + return value; + } + + // Already visited (cycle): return the previously-built copy so the result + // graph mirrors the original's cycle topology. + if (seen.has(value)) { + return seen.get(value); + } + + // Typed-array views (TypedArray + DataView): copy bytes into a tight + // backing buffer, preserving the original view constructor. + if (ArrayBuffer.isView(value)) { + return buildCompactView(value); + } + + if (Array.isArray(value)) { + const result: unknown[] = []; + seen.set(value, result); + for (const item of value) { + result.push(normalizeForStorage(item, seen)); + } + return result; + } + + if (value instanceof Map) { + const result = new Map(); + seen.set(value, result); + for (const [k, v] of value) { + result.set(normalizeForStorage(k, seen), normalizeForStorage(v, seen)); + } + return result; + } + + if (value instanceof Set) { + const result = new Set(); + seen.set(value, result); + for (const v of value) { + result.add(normalizeForStorage(v, seen)); + } + return result; + } + + // Plain objects (Object literals and null-prototype objects). Class + // instances and host objects fall through to the pass-through below. + const proto = Object.getPrototypeOf(value); + if (proto === Object.prototype || proto === null) { + const result: Record = {}; + seen.set(value, result); + for (const key of Object.keys(value)) { + result[key] = normalizeForStorage( + (value as Record)[key], + seen + ); + } + return result; + } + + return value; +} + +type ViewCtor = new (buffer: ArrayBufferLike) => ArrayBufferView; +function buildCompactView(view: ArrayBufferView): ArrayBufferView { + const tightBuffer = view.buffer.slice( + view.byteOffset, + view.byteOffset + view.byteLength + ); + return new (view.constructor as ViewCtor)(tightBuffer); +} + export interface UserErrorField { isUserError?: boolean; } @@ -566,7 +663,15 @@ export class Context extends RpcTarget { activeTimeoutTask?: Promise ): Promise => { if (!isReadableStreamLike(value)) { - await this.#state.storage.put(valueKey, { value }); + // Typed-array views anywhere in the value tree are copied + // into a tight backing buffer so the full backing buffer + // does not ride along with each view (issue #14101). View + // types are preserved so cached replays observe the same + // constructor as the live execution path. The caller still + // receives the original `value` below — only the stored + // shape changes. + const stored = normalizeForStorage(value); + await this.#state.storage.put(valueKey, { value: stored }); abortController.abort("step finished"); // @ts-expect-error priorityQueue is initiated in init this.#engine.priorityQueue.remove({ diff --git a/packages/workflows-shared/tests/context.test.ts b/packages/workflows-shared/tests/context.test.ts index 27ff0d09a2..4c7a641ef0 100644 --- a/packages/workflows-shared/tests/context.test.ts +++ b/packages/workflows-shared/tests/context.test.ts @@ -1234,3 +1234,220 @@ describe("Context - ReadableStream step outputs", () => { }); }); }); + +describe("Context - typed-array step outputs (issue #14101)", () => { + it("should persist a 200KB Uint8Array step output on a tight backing buffer", async ({ + expect, + }) => { + // Baseline: a 200KB view sized exactly to its backing buffer must + // succeed. This case worked on `main` too — anchors the regression test + // suite to "small enough to fit, regardless of normalisation". + const engineStub = await runWorkflow( + "UINT8-TIGHT-200K", + async (_event, step) => { + return await step.do("emit-tight-bytes", async () => { + return new Uint8Array(200_000); + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); + + it("should persist a 200KB Uint8Array view sliced from an 800KB backing buffer (regression #14101)", async ({ + expect, + }) => { + // The exact reporter's repro: a Uint8Array view sized 200KB but + // sliced from a much larger backing ArrayBuffer (800KB here). Prior + // to the fix, workerd's v8::ValueSerializer would serialise the + // entire 800KB backing buffer along with the view, blowing past the + // 1MiB SQL blob limit and surfacing a raw `SQLITE_TOOBIG` error. + const engineStub = await runWorkflow( + "UINT8-SLICED-200K-OF-800K", + async (_event, step) => { + return await step.do("emit-sliced-bytes", async () => { + const backing = new ArrayBuffer(800_000); + return new Uint8Array(backing, 0, 200_000); + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); + + it("should return the original Uint8Array to the next step in the live execution path", async ({ + expect, + }) => { + // The persisted form preserves the view type (`Uint8Array` stays + // `Uint8Array`) so the live execution path and cached replays observe + // the same shape — downstream user code that branches on + // `value instanceof Uint8Array` works either way. + let observedInNext: unknown = "not-seen"; + const engineStub = await runWorkflow( + "UINT8-ROUND-TRIP", + async (_event, step) => { + const bytes = await step.do("emit-bytes", async () => { + return new Uint8Array([1, 2, 3, 4, 5]); + }); + await step.do("read-bytes", async () => { + observedInNext = bytes; + return "ok"; + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + expect(observedInNext).toBeInstanceOf(Uint8Array); + expect(Array.from(observedInNext as Uint8Array)).toEqual([1, 2, 3, 4, 5]); + }); + + it("should preserve typed-array view types other than Uint8Array on the live execution path", async ({ + expect, + }) => { + // Persisting must preserve the constructor type (`Int16Array` stays + // `Int16Array`, etc.) so downstream `instanceof` checks behave the + // same whether the step ran live or via cached replay. + let observedInNext: unknown = "not-seen"; + const engineStub = await runWorkflow( + "INT16-ROUND-TRIP", + async (_event, step) => { + const bytes = await step.do("emit-int16", async () => { + return new Int16Array([1, 2, 3, 4, 5]); + }); + await step.do("read-int16", async () => { + observedInNext = bytes; + return "ok"; + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + expect(observedInNext).toBeInstanceOf(Int16Array); + expect(Array.from(observedInNext as Int16Array)).toEqual([1, 2, 3, 4, 5]); + }); + + it("should compact typed-array views nested inside an object (deep-walk regression)", async ({ + expect, + }) => { + // A view sliced from a much larger backing buffer would bloat past + // SQLITE_TOOBIG if nested-level normalisation were missing. Recurses + // into plain objects. + const engineStub = await runWorkflow( + "NESTED-UINT8-SLICED", + async (_event, step) => { + return await step.do("emit-nested-sliced-bytes", async () => { + const backing = new ArrayBuffer(800_000); + return { + image: new Uint8Array(backing, 0, 200_000), + meta: { width: 100, height: 100 }, + }; + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); + + it("should compact typed-array views nested deep inside arrays of objects", async ({ + expect, + }) => { + // Two levels of nesting (array of object of view). Confirms the + // walker descends through both arrays and plain objects. + const engineStub = await runWorkflow( + "ARRAY-OF-OBJECTS-WITH-SLICED-VIEWS", + async (_event, step) => { + return await step.do("emit-array-of-objects", async () => { + const backing = new ArrayBuffer(800_000); + return [ + { data: new Uint8Array(backing, 0, 100_000) }, + { data: new Uint8Array(backing, 100_000, 100_000) }, + ]; + }); + } + ); + + await vi.waitUntil( + async () => { + const logs = (await engineStub.readLogs()) as EngineLogs; + return logs.logs.some( + (val) => val.event === InstanceEvent.WORKFLOW_SUCCESS + ); + }, + { timeout: 10000 } + ); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); +}); From 444b75e75492738d10e7dc89ec645f7e2fad6b97 Mon Sep 17 00:00:00 2001 From: MatinGathani <70268627+matingathani@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:51:39 +0530 Subject: [PATCH 4/6] [wrangler] fix: don't crash wrangler dev when source-mapping a truncated error chunk (#14316) --- .../fix-sourcemap-crash-invalid-column.md | 7 +++++ .../src/deploy/helpers/sourcemap.ts | 30 +++++++++++++++++-- .../deploy-helpers/tests/sourcemap.test.ts | 14 +++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-sourcemap-crash-invalid-column.md create mode 100644 packages/deploy-helpers/tests/sourcemap.test.ts diff --git a/.changeset/fix-sourcemap-crash-invalid-column.md b/.changeset/fix-sourcemap-crash-invalid-column.md new file mode 100644 index 0000000000..0820aa616d --- /dev/null +++ b/.changeset/fix-sourcemap-crash-invalid-column.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Prevent `wrangler dev` crash when source-mapping a truncated error chunk + +When a worker logs many errors in quick succession, the stderr chunks received by `wrangler dev` can be truncated mid-stack-frame, leaving a call site with an invalid column number. The source map library throws in that case, which was crashing the wrangler process entirely. The error is now caught and the original (un-source-mapped) text is returned instead. diff --git a/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts index 8aa08433f2..1ca2579300 100644 --- a/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts +++ b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import url from "node:url"; import { maybeGetFile } from "@cloudflare/workers-shared"; import { getFreshSourceMapSupport } from "miniflare"; +import { logger } from "../../shared/context"; import type { Options } from "@cspotcode/source-map-support"; import type Protocol from "devtools-protocol"; @@ -122,6 +123,27 @@ function callFrameToCallSite(frame: Protocol.Runtime.CallFrame): CallSite { }); } +/** + * Calls `prepareStack` and returns `null` if it throws (e.g. when a truncated + * stderr chunk produces an invalid column number). Returning `null` lets + * `getSourceMappedString` fall back to the original string without executing + * the replacement loop against a partially-computed result. + */ +function tryPrepareStack( + prepareStack: ReturnType, + error: Error, + callSites: NodeJS.CallSite[] +): string | null { + try { + return prepareStack(error, callSites); + } catch (err) { + logger?.debug( + `Source map application failed, falling back to original stack trace: ${err}` + ); + return null; + } +} + const placeholderError = new Error(); export function getSourceMappedString( value: string, @@ -138,16 +160,20 @@ export function getSourceMappedString( const callSiteLines = Array.from(value.matchAll(CALL_SITE_REGEXP)); const callSites = callSiteLines.map(lineMatchToCallSite); const prepareStack = getSourceMappingPrepareStackTrace(retrieveSourceMap); - const sourceMappedStackTrace: string = prepareStack( + const sourceMappedStackTrace = tryPrepareStack( + prepareStack, placeholderError, callSites ); + if (sourceMappedStackTrace === null) { + return value; + } const sourceMappedCallSiteLines = sourceMappedStackTrace.split("\n").slice(1); for (let i = 0; i < callSiteLines.length; i++) { // If a call site doesn't have a file name, it's likely invalid, so don't // apply source mapping (see cloudflare/workers-sdk#4668) - if (callSites[i].getFileName() === undefined) { + if (callSites[i].getFileName() === null) { continue; } diff --git a/packages/deploy-helpers/tests/sourcemap.test.ts b/packages/deploy-helpers/tests/sourcemap.test.ts new file mode 100644 index 0000000000..b4fb831aa8 --- /dev/null +++ b/packages/deploy-helpers/tests/sourcemap.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "vitest"; +import { getSourceMappedString } from "../src/deploy/helpers/sourcemap"; + +describe("getSourceMappedString", () => { + it("returns original value when source mapping throws", ({ expect }) => { + const value = `Error: test\n at Object. (/some/file.js:1:1)`; + + const result = getSourceMappedString(value, () => { + throw new Error("simulated source map failure"); + }); + + expect(result).toBe(value); + }); +}); From cfd6205fe86f6afd74b5881f09524c93c83b8359 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 18 Jun 2026 17:19:24 +0100 Subject: [PATCH 5/6] Extract autoconfig in its own standalone package (#14295) --- .../move-worker-name-to-workers-utils.md | 8 + .changeset/workers-utils-package-manager.md | 7 + ...wrangler-remove-experimental-autoconfig.md | 7 + packages/autoconfig/package.json | 52 ++ packages/autoconfig/scripts/deps.ts | 12 + packages/autoconfig/src/context.ts | 107 +++++ .../src}/details/framework-detection.ts | 62 +-- .../src}/details/index.ts | 253 +++------- packages/autoconfig/src/errors.ts | 39 ++ .../src}/frameworks/all-frameworks.ts | 0 .../src}/frameworks/analog.ts | 0 .../src}/frameworks/angular.ts | 4 +- .../src}/frameworks/astro.ts | 20 +- .../src}/frameworks/framework-class.ts | 13 +- .../src}/frameworks/index.ts | 0 .../src}/frameworks/next.ts | 0 .../src}/frameworks/no-op.ts | 0 .../src}/frameworks/nuxt.ts | 0 .../src}/frameworks/qwik.ts | 0 .../src}/frameworks/react-router.ts | 13 +- .../src}/frameworks/solid-start.ts | 0 .../src}/frameworks/static.ts | 0 .../src}/frameworks/sveltekit.ts | 0 .../src}/frameworks/tanstack.ts | 0 .../src}/frameworks/utils/packages.ts | 0 .../src}/frameworks/utils/vite-config.ts | 9 +- .../src}/frameworks/utils/vite-plugin.ts | 2 +- .../src}/frameworks/vike.ts | 0 .../src}/frameworks/vite.ts | 0 .../src}/frameworks/waku.ts | 0 packages/autoconfig/src/index.ts | 39 ++ .../src/autoconfig => autoconfig/src}/run.ts | 387 +++++++-------- .../autoconfig => autoconfig/src}/types.ts | 9 +- .../src}/uses-typescript.ts | 0 .../confirm-auto-config-details.test.ts | 248 ++++++++++ .../display-auto-config-details.test.ts | 96 ++++ .../basic-framework-detection.test.ts | 11 +- .../lock-file-warning.test.ts | 17 +- .../multiple-frameworks-detected.test.ts | 52 +- .../package-manager-detection.test.ts | 21 +- .../pages-project-detection.test.ts | 66 ++- .../workspace-root-handling.test.ts | 13 +- .../get-details-for-auto-config.test.ts | 152 +++--- .../tests}/frameworks/angular.test.ts | 64 ++- .../config-future-no-middleware.ts | 0 .../config-middleware-and-split.ts | 0 .../react-router/config-middleware-false.ts | 0 .../react-router/config-middleware-true.ts | 0 .../fixtures/react-router/config-no-future.ts | 0 .../config-plain-object-middleware.ts | 0 .../react-router/vite-config-basic.ts | 0 .../frameworks/get-framework-class.test.ts | 8 +- .../frameworks/is-framework-supported.test.ts | 2 +- .../frameworks/is-known-framework.test.ts | 2 +- .../tests}/frameworks/react-router.test.ts | 30 +- .../frameworks/utils/vite-plugin.test.ts | 6 +- .../validate-framework-version.test.ts | 34 +- .../tests}/frameworks/vike.test.ts | 13 +- .../tests}/frameworks/vite.test.ts | 8 +- .../get-installed-package-version.test.ts | 3 +- .../autoconfig/tests/helpers/mock-context.ts | 34 ++ .../tests}/run-summary.test.ts | 36 +- packages/autoconfig/tests/tsconfig.json | 8 + .../tests}/vite-config.test.ts | 26 +- packages/autoconfig/tsconfig.json | 10 + packages/autoconfig/tsup.config.ts | 21 + packages/autoconfig/turbo.json | 16 + packages/autoconfig/vitest.config.mts | 12 + .../src/plugin-config.ts | 5 +- packages/workers-utils/src/index.ts | 15 + packages/workers-utils/src/package-manager.ts | 54 +++ packages/workers-utils/src/worker-name.ts | 143 ++++++ .../tests/worker-name.test.ts} | 7 +- packages/wrangler/package.json | 1 + .../confirm-auto-config-details.test.ts | 275 ----------- .../display-auto-config-details.test.ts | 95 ---- .../src/__tests__/autoconfig/index.test.ts | 268 +++++++++++ .../src/__tests__/autoconfig/run.test.ts | 453 ++++++++++-------- .../src/__tests__/deploy/assets.test.ts | 9 +- .../src/__tests__/deploy/bindings.test.ts | 9 +- .../src/__tests__/deploy/build.test.ts | 9 +- .../deploy/config-args-merging.test.ts | 7 +- .../__tests__/deploy/config-remote.test.ts | 7 +- .../src/__tests__/deploy/core.test.ts | 32 +- .../deploy/deploy-interactive-prompts.test.ts | 9 +- .../__tests__/deploy/durable-objects.test.ts | 9 +- .../src/__tests__/deploy/entry-points.test.ts | 48 +- .../src/__tests__/deploy/environments.test.ts | 9 +- .../src/__tests__/deploy/formats.test.ts | 9 +- .../__tests__/deploy/legacy-assets.test.ts | 9 +- .../src/__tests__/deploy/open-next.test.ts | 15 +- .../src/__tests__/deploy/queues.test.ts | 9 +- .../src/__tests__/deploy/routes.test.ts | 9 +- .../src/__tests__/deploy/secrets.test.ts | 9 +- .../src/__tests__/deploy/workers-dev.test.ts | 9 +- .../src/__tests__/deploy/workflows.test.ts | 9 +- packages/wrangler/src/__tests__/setup.test.ts | 2 +- packages/wrangler/src/api/index.ts | 1 - .../src/api/integrations/platform/index.ts | 1 - packages/wrangler/src/autoconfig-context.ts | 25 + packages/wrangler/src/autoconfig/errors.ts | 7 - packages/wrangler/src/autoconfig/index.ts | 145 ++++++ .../src/autoconfig/telemetry-utils.ts | 1 - packages/wrangler/src/cli.ts | 6 - packages/wrangler/src/deploy/autoconfig.ts | 19 +- packages/wrangler/src/deploy/open-next.ts | 2 +- packages/wrangler/src/output.ts | 2 +- packages/wrangler/src/package-manager.ts | 62 +-- packages/wrangler/src/setup.ts | 47 +- pnpm-lock.yaml | 126 ++++- .../__tests__/validate-changesets.test.ts | 1 + .../validate-package-dependencies.test.ts | 1 + 112 files changed, 2585 insertions(+), 1457 deletions(-) create mode 100644 .changeset/move-worker-name-to-workers-utils.md create mode 100644 .changeset/workers-utils-package-manager.md create mode 100644 .changeset/wrangler-remove-experimental-autoconfig.md create mode 100644 packages/autoconfig/package.json create mode 100644 packages/autoconfig/scripts/deps.ts create mode 100644 packages/autoconfig/src/context.ts rename packages/{wrangler/src/autoconfig => autoconfig/src}/details/framework-detection.ts (87%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/details/index.ts (60%) create mode 100644 packages/autoconfig/src/errors.ts rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/all-frameworks.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/analog.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/angular.ts (97%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/astro.ts (95%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/framework-class.ts (90%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/index.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/next.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/no-op.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/nuxt.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/qwik.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/react-router.ts (97%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/solid-start.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/static.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/sveltekit.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/tanstack.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/utils/packages.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/utils/vite-config.ts (98%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/utils/vite-plugin.ts (96%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/vike.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/vite.ts (100%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/frameworks/waku.ts (100%) create mode 100644 packages/autoconfig/src/index.ts rename packages/{wrangler/src/autoconfig => autoconfig/src}/run.ts (53%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/types.ts (87%) rename packages/{wrangler/src/autoconfig => autoconfig/src}/uses-typescript.ts (100%) create mode 100644 packages/autoconfig/tests/details/confirm-auto-config-details.test.ts create mode 100644 packages/autoconfig/tests/details/display-auto-config-details.test.ts rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/basic-framework-detection.test.ts (77%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/lock-file-warning.test.ts (70%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/multiple-frameworks-detected.test.ts (75%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/package-manager-detection.test.ts (75%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/pages-project-detection.test.ts (59%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/framework-detection/workspace-root-handling.test.ts (71%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/details/get-details-for-auto-config.test.ts (74%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/angular.test.ts (90%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-future-no-middleware.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-middleware-and-split.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-middleware-false.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-middleware-true.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-no-future.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/config-plain-object-middleware.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/fixtures/react-router/vite-config-basic.ts (100%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/get-framework-class.test.ts (77%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/is-framework-supported.test.ts (88%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/is-known-framework.test.ts (90%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/react-router.test.ts (94%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/utils/vite-plugin.test.ts (96%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/validate-framework-version.test.ts (77%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/vike.test.ts (96%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/frameworks/vite.test.ts (93%) rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/get-installed-package-version.test.ts (88%) create mode 100644 packages/autoconfig/tests/helpers/mock-context.ts rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/run-summary.test.ts (91%) create mode 100644 packages/autoconfig/tests/tsconfig.json rename packages/{wrangler/src/__tests__/autoconfig => autoconfig/tests}/vite-config.test.ts (91%) create mode 100644 packages/autoconfig/tsconfig.json create mode 100644 packages/autoconfig/tsup.config.ts create mode 100644 packages/autoconfig/turbo.json create mode 100644 packages/autoconfig/vitest.config.mts create mode 100644 packages/workers-utils/src/package-manager.ts create mode 100644 packages/workers-utils/src/worker-name.ts rename packages/{wrangler/src/__tests__/autoconfig/details/get-worker-name-from-project.test.ts => workers-utils/tests/worker-name.test.ts} (91%) delete mode 100644 packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts delete mode 100644 packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/index.test.ts create mode 100644 packages/wrangler/src/autoconfig-context.ts delete mode 100644 packages/wrangler/src/autoconfig/errors.ts create mode 100644 packages/wrangler/src/autoconfig/index.ts diff --git a/.changeset/move-worker-name-to-workers-utils.md b/.changeset/move-worker-name-to-workers-utils.md new file mode 100644 index 0000000000..0d1fa7047c --- /dev/null +++ b/.changeset/move-worker-name-to-workers-utils.md @@ -0,0 +1,8 @@ +--- +"wrangler": minor +"@cloudflare/workers-utils": minor +--- + +Move `unstable_getWorkerNameFromProject` from wrangler to `@cloudflare/workers-utils` + +The `unstable_getWorkerNameFromProject` export has been removed from the `wrangler` package. This function is now available as `getWorkerNameFromProject` (without the `unstable_` prefix) from `@cloudflare/workers-utils`. If you were importing this function from `wrangler`, update your import to use `@cloudflare/workers-utils` instead. diff --git a/.changeset/workers-utils-package-manager.md b/.changeset/workers-utils-package-manager.md new file mode 100644 index 0000000000..0360ccaa77 --- /dev/null +++ b/.changeset/workers-utils-package-manager.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-utils": minor +--- + +Add PackageManager type and constants + +Added the `PackageManager` interface and package manager constants (`NpmPackageManager`, `PnpmPackageManager`, `YarnPackageManager`, `BunPackageManager`). diff --git a/.changeset/wrangler-remove-experimental-autoconfig.md b/.changeset/wrangler-remove-experimental-autoconfig.md new file mode 100644 index 0000000000..6fe3636f8b --- /dev/null +++ b/.changeset/wrangler-remove-experimental-autoconfig.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Remove experimental autoconfig exports + +The experimental autoconfig exports (`experimental_getDetailsForAutoConfig`, `experimental_runAutoConfig`, `experimental_AutoConfigFramework`) have been removed. This logic has been moved to the `@cloudflare/autoconfig` package (without the `experimental_` prefixes since the package itself is pre-v1). diff --git a/packages/autoconfig/package.json b/packages/autoconfig/package.json new file mode 100644 index 0000000000..29134695d3 --- /dev/null +++ b/packages/autoconfig/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cloudflare/autoconfig", + "version": "0.1.0", + "description": "Framework autoconfig detection and configuration for Cloudflare Workers", + "license": "MIT OR Apache-2.0", + "files": [ + "dist" + ], + "type": "module", + "sideEffects": false, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsup", + "check:type": "tsc -p ./tsconfig.json", + "dev": "tsup --watch", + "test": "vitest", + "test:ci": "vitest run" + }, + "dependencies": { + "@cloudflare/cli-shared-helpers": "workspace:*", + "@cloudflare/workers-utils": "workspace:*" + }, + "devDependencies": { + "@cloudflare/codemod": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@netlify/build-info": "^10.5.1", + "@types/esprima": "^4.0.3", + "@types/node": "catalog:default", + "chalk": "catalog:default", + "empathic": "^2.0.0", + "esprima": "4.0.1", + "recast": "0.23.11", + "semiver": "^1.1.0", + "ts-dedent": "^2.2.0", + "tsup": "8.3.0", + "typescript": "catalog:default", + "vitest": "catalog:default" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "prerelease": true + } +} diff --git a/packages/autoconfig/scripts/deps.ts b/packages/autoconfig/scripts/deps.ts new file mode 100644 index 0000000000..6d7361e535 --- /dev/null +++ b/packages/autoconfig/scripts/deps.ts @@ -0,0 +1,12 @@ +/** + * Dependencies that _are not_ bundled along with @cloudflare/autoconfig. + * + * These must be explicitly documented with a reason why they cannot be bundled. + * This list is validated by `tools/deployments/validate-package-dependencies.ts`. + */ +export const EXTERNAL_DEPENDENCIES = [ + // Published workspace packages that consumers must install alongside autoconfig. + // They are kept external to share a single copy with wrangler and other SDK tools. + "@cloudflare/cli-shared-helpers", + "@cloudflare/workers-utils", +]; diff --git a/packages/autoconfig/src/context.ts b/packages/autoconfig/src/context.ts new file mode 100644 index 0000000000..8c70cd987e --- /dev/null +++ b/packages/autoconfig/src/context.ts @@ -0,0 +1,107 @@ +/** + * Logger interface for autoconfig output. + * Callers provide their own implementation (e.g., wrapping `console` or a custom logger). + */ +export interface AutoConfigLogger { + /** Logs informational output. */ + log(...args: unknown[]): void; + /** Logs an informational message. */ + info(...args: unknown[]): void; + /** Logs a warning message. */ + warn(...args: unknown[]): void; + /** Logs a debug-level message (may be suppressed in production). */ + debug(...args: unknown[]): void; + /** Logs an error message. */ + error(...args: unknown[]): void; +} + +/** + * Dialog interface for interactive prompts. + * Callers provide their own implementation (e.g., using `prompts`, `inquirer`, or a custom UI). + */ +export interface AutoConfigDialogs { + /** + * Asks a yes/no confirmation question. + * + * @param text - The question to display + * @param options - Optional defaults and fallback behavior + * @returns `true` if confirmed, `false` otherwise + */ + confirm( + text: string, + options?: { defaultValue?: boolean; fallbackValue?: boolean } + ): Promise; + + /** + * Prompts the user for a text input. + * + * @param text - The prompt message + * @param options - Optional default value and validation function + * @returns The user-provided string + */ + prompt( + text: string, + options?: { + defaultValue?: string; + validate?: ( + value: string + ) => boolean | string | Promise; + } + ): Promise; + + /** + * Presents a selection list to the user. + * + * @param text - The prompt message + * @param options - Available choices and optional default selection + * @returns The selected value + */ + select( + text: string, + options: { + choices: Array<{ + title: string; + value: string; + description?: string; + }>; + defaultOption?: number; + } + ): Promise; +} + +/** + * Context object that provides external dependencies to the autoconfig system. + * + * Callers must provide implementations for `logger` and `dialogs`. + * All other fields are optional and allow callers to customize behavior + * (e.g., error reporting, command execution, CI detection). + */ +export interface AutoConfigContext { + /** Logger used for all autoconfig output. */ + logger: AutoConfigLogger; + /** Dialogs used for interactive prompts. */ + dialogs: AutoConfigDialogs; + /** + * Runs a shell command in the given directory. + * + * @param command - The shell command string to execute + * @param cwd - The working directory for the command + * @param label - A short label for logging (e.g., "[build]") + * @returns A promise that resolves when the command completes + */ + runCommand: (command: string, cwd: string, label: string) => Promise; + /** + * Returns `true` if running in a non-interactive or CI environment. + * Defaults to `() => false` if not provided. + * + * @returns Whether the current environment is non-interactive + */ + isNonInteractiveOrCI?: () => boolean; + /** + * Returns a cache folder path used for detecting cached project state, + * or `undefined` if not available. + * + * @returns The cache folder path, or `undefined` + */ + getCacheFolder?: () => string | undefined; +} diff --git a/packages/wrangler/src/autoconfig/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts similarity index 87% rename from packages/wrangler/src/autoconfig/details/framework-detection.ts rename to packages/autoconfig/src/details/framework-detection.ts index 492dea8451..3a64c2f601 100644 --- a/packages/wrangler/src/autoconfig/details/framework-detection.ts +++ b/packages/autoconfig/src/details/framework-detection.ts @@ -1,25 +1,21 @@ import { existsSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; -import { FatalError, UserError } from "@cloudflare/workers-utils"; -import { Project } from "@netlify/build-info"; -import { NodeFS } from "@netlify/build-info/node"; -import { captureException } from "@sentry/node"; -import chalk from "chalk"; -import dedent from "ts-dedent"; -import { getCacheFolder } from "../../config-cache"; -import { confirm } from "../../dialogs"; -import { isNonInteractiveOrCI } from "../../is-interactive"; -import { logger } from "../../logger"; import { BunPackageManager, + FatalError, NpmPackageManager, PnpmPackageManager, + UserError, YarnPackageManager, -} from "../../package-manager"; -import { PAGES_CONFIG_CACHE_FILENAME } from "../../pages/constants"; +} from "@cloudflare/workers-utils"; +import { Project } from "@netlify/build-info"; +import { NodeFS } from "@netlify/build-info/node"; +import chalk from "chalk"; +import dedent from "ts-dedent"; import { isKnownFramework } from "../frameworks"; import { staticFramework } from "../frameworks/all-frameworks"; -import type { PackageManager } from "../../package-manager"; +import type { AutoConfigContext } from "../context"; +import type { PackageManager } from "@cloudflare/workers-utils"; import type { Config, TelemetryMessage } from "@cloudflare/workers-utils"; import type { Settings } from "@netlify/build-info"; @@ -48,6 +44,7 @@ import type { Settings } from "@netlify/build-info"; */ export async function detectFramework( projectPath: string, + context: AutoConfigContext, wranglerConfig?: Config ): Promise<{ detectedFramework: DetectedFramework; @@ -56,14 +53,11 @@ export async function detectFramework( }> { const fs = new NodeFS(); - fs.logger = logger; + fs.logger = context.logger; const project = new Project(fs, projectPath, projectPath) .setEnvironment(process.env) - .setNodeVersion(process.version) - .setReportFn((err) => { - captureException(err); - }); + .setNodeVersion(process.version); const buildSettings = await project.getBuildSettings(); @@ -78,7 +72,7 @@ export async function detectFramework( if (!workspaceRootIncludesProject) { throw new UserError( - "The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.", + "The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.", { telemetryMessage: "autoconfig detection workspace root unsupported", } @@ -94,10 +88,18 @@ export async function detectFramework( existsSync(join(projectPath, lockFile)) ); - const maybeDetectedFramework = maybeFindDetectedFramework(buildSettings); + const maybeDetectedFramework = maybeFindDetectedFramework( + buildSettings, + context + ); if ( - await isPagesProject(projectPath, wranglerConfig, maybeDetectedFramework) + await isPagesProject( + projectPath, + context, + wranglerConfig, + maybeDetectedFramework + ) ) { return { detectedFramework: { @@ -122,7 +124,7 @@ export async function detectFramework( !lockFileExists && detectedFramework.framework.id !== staticFramework.id ) { - logger.warn( + context.logger.warn( "No lock file has been detected in the current working directory." + " This might indicate that the project is part of a workspace. Auto-configuration of " + `projects inside workspaces is limited. See ${chalk.hex("#3B818D")( @@ -173,7 +175,7 @@ class MultipleFrameworksCIError extends FatalError { } ) { super( - dedent`Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: ${frameworks.join( + dedent`Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: ${frameworks.join( ", " )}. @@ -211,6 +213,7 @@ type DetectedFramework = { async function isPagesProject( projectPath: string, + context: AutoConfigContext, wranglerConfig: Config | undefined, detectedFramework?: DetectedFramework | undefined ): Promise { @@ -219,9 +222,9 @@ async function isPagesProject( return true; } - const cacheFolder = getCacheFolder(); + const cacheFolder = context.getCacheFolder?.(); if (cacheFolder) { - const pagesConfigCache = join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME); + const pagesConfigCache = join(cacheFolder, "pages.json"); if (existsSync(pagesConfigCache)) { // If there is a cached pages.json we can safely assume that the project // is a Pages one @@ -234,7 +237,7 @@ async function isPagesProject( if (existsSync(functionsPath)) { const functionsStat = statSync(functionsPath); if (functionsStat.isDirectory()) { - const pagesConfirmed = await confirm( + const pagesConfirmed = await context.dialogs.confirm( "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", { defaultValue: true, @@ -265,7 +268,8 @@ async function isPagesProject( * are detected and no clear winner can be determined */ function maybeFindDetectedFramework( - settings: Settings[] + settings: Settings[], + context: AutoConfigContext ): DetectedFramework | undefined { if (settings.length === 0) { return undefined; @@ -280,7 +284,7 @@ function maybeFindDetectedFramework( ); if (settingsForOnlyKnownFrameworks.length === 0) { - if (isNonInteractiveOrCI()) { + if (context.isNonInteractiveOrCI?.() ?? false) { // If we're in a non interactive session (e.g. CI) let's throw to be on the safe side throwMultipleFrameworksNonInteractiveError(settings); } @@ -323,7 +327,7 @@ function maybeFindDetectedFramework( // If we've detected multiple frameworks, and we're in a non interactive session (e.g. CI) let's stay on the safe side and error // (otherwise we just pick the first one as the user is always able to choose a different framework or terminate the process anyways) - if (isNonInteractiveOrCI()) { + if (context.isNonInteractiveOrCI?.() ?? false) { throw new MultipleFrameworksCIError( settingsForOnlyKnownFrameworks.map((b) => b.name), { telemetryMessage: "autoconfig detection multiple frameworks" } diff --git a/packages/wrangler/src/autoconfig/details/index.ts b/packages/autoconfig/src/details/index.ts similarity index 60% rename from packages/wrangler/src/autoconfig/details/index.ts rename to packages/autoconfig/src/details/index.ts index aab01e5cac..3ba589d68a 100644 --- a/packages/wrangler/src/autoconfig/details/index.ts +++ b/packages/autoconfig/src/details/index.ts @@ -1,34 +1,28 @@ import assert from "node:assert"; import { statSync } from "node:fs"; import { readdir, stat } from "node:fs/promises"; -import { basename, join, relative, resolve } from "node:path"; +import { join, relative, resolve } from "node:path"; import { brandColor } from "@cloudflare/cli-shared-helpers/colors"; import { - FatalError, - getCIOverrideName, + checkWorkerNameValidity, + getWorkerName, + NpmPackageManager, parsePackageJSON, readFileSync, } from "@cloudflare/workers-utils"; -import { getErrorType } from "../../core/handle-errors"; -import { confirm, prompt, select } from "../../dialogs"; -import { logger } from "../../logger"; -import { sendMetricsEvent } from "../../metrics"; -import { NpmPackageManager } from "../../package-manager"; +import { AutoConfigDetectionError } from "../errors"; import { getFrameworkClassInstance } from "../frameworks"; import { allFrameworksInfos, staticFramework, } from "../frameworks/all-frameworks"; -import { - getAutoConfigId, - getAutoConfigTriggerCommand, -} from "../telemetry-utils"; import { detectFramework } from "./framework-detection"; -import type { PackageManager } from "../../package-manager"; +import type { AutoConfigContext } from "../context"; import type { AutoConfigDetails, AutoConfigDetailsForNonConfiguredProject, } from "../types"; +import type { PackageManager } from "@cloudflare/workers-utils"; import type { Config, PackageJSON } from "@cloudflare/workers-utils"; /** @@ -76,13 +70,6 @@ async function findAssetsDir(from: string): Promise { return undefined; } -function getWorkerName(projectOrWorkerName = "", projectPath: string): string { - const rawName = - getCIOverrideName() ?? (projectOrWorkerName || basename(projectPath)); - - return toValidWorkerName(rawName); -} - type DetectedFramework = { framework: { name: string; @@ -93,52 +80,27 @@ type DetectedFramework = { }; /** - * Derives a valid worker name from a project directory. - * - * The name is determined by (in order of precedence): - * 1. The WRANGLER_CI_OVERRIDE_NAME environment variable (for CI environments) - * 2. The `name` field from package.json in the project directory - * 3. The directory basename + * Detects project details needed for autoconfig: framework, package manager, + * output directory, worker name, and whether the project is already configured. * - * The resulting name is sanitized to be a valid worker name. - * - * @param projectPath The path to the project directory - * @returns A valid worker name + * @param options - Detection options including project path, wrangler config, and context. + * @returns The detected project details. */ -export function getWorkerNameFromProject(projectPath: string): string { - const packageJsonPath = resolve(projectPath, "package.json"); - let packageJsonName: string | undefined; - - try { - const packageJson = parsePackageJSON( - readFileSync(packageJsonPath), - packageJsonPath - ); - packageJsonName = packageJson.name; - } catch {} - - return getWorkerName(packageJsonName, projectPath); -} - export async function getDetailsForAutoConfig({ projectPath = process.cwd(), wranglerConfig, + context, }: { - projectPath?: string; // the path to the project, defaults to cwd + /** The path to the project, defaults to cwd. */ + projectPath?: string; + /** The parsed wrangler configuration for the project (if any). */ wranglerConfig?: Config; -} = {}): Promise { - logger.debug(`Running autoconfig detection in ${projectPath}...`); - - const autoConfigId = getAutoConfigId(); + /** The autoconfig context providing logger, dialogs, and other dependencies. */ + context: AutoConfigContext; +}): Promise { + const { logger } = context; - sendMetricsEvent( - "autoconfig_detection_started", - { - autoConfigId, - command: getAutoConfigTriggerCommand(), - }, - {} - ); + logger.debug(`Running autoconfig detection in ${projectPath}...`); if ( // If a real Wrangler config has been found the project is already configured for Workers @@ -156,7 +118,7 @@ export async function getDetailsForAutoConfig({ } const { detectedFramework, packageManager, isWorkspaceRoot } = - await detectFramework(projectPath, wranglerConfig); + await detectFramework(projectPath, context, wranglerConfig); const framework = getFrameworkClassInstance(detectedFramework.framework.id); const packageJsonPath = resolve(projectPath, "package.json"); @@ -194,16 +156,6 @@ export async function getDetailsForAutoConfig({ }; if (configured) { - sendMetricsEvent( - "autoconfig_detection_completed", - { - autoConfigId, - framework: framework.id, - configured, - success: true, - }, - {} - ); return { ...baseDetails, configured: true, @@ -217,37 +169,13 @@ export async function getDetailsForAutoConfig({ ? "Could not detect a directory containing static files (e.g. html, css and js) for the project" : "Failed to detect an output directory for the project"; - const error = new FatalError(errorMessage, { + throw new AutoConfigDetectionError(errorMessage, { telemetryMessage: "autoconfig details output directory missing", + frameworkId: framework.id, + configured, }); - - sendMetricsEvent( - "autoconfig_detection_completed", - { - autoConfigId, - framework: framework.id, - configured, - success: false, - errorType: getErrorType(error), - errorMessage, - }, - {} - ); - - throw error; } - sendMetricsEvent( - "autoconfig_detection_completed", - { - autoConfigId, - framework: framework.id, - configured, - success: true, - }, - {} - ); - return { ...baseDetails, outputDir, @@ -287,98 +215,19 @@ function getProjectBuildCommand( return `${npx} ${detectedFramework.buildCommand}`; } -const invalidWorkerNameCharsRegex = /[^a-z0-9- ]/g; -const invalidWorkerNameStartEndRegex = /^(-+)|(-+)$/g; -const workerNameLengthLimit = 63; - -/** - * Checks whether the provided worker name is valid, this means that: - * - the name is not empty - * - the name doesn't start nor ends with a dash - * - the name doesn't contain special characters besides dashes - * - the name is not longer than 63 characters - * - * See: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations - * - * @param input The name to check - * @returns Object indicating whether the name is valid, and if not a cause indicating why it isn't - */ -function checkWorkerNameValidity( - input: string -): { valid: false; cause: string } | { valid: true } { - if (!input) { - return { - valid: false, - cause: "Worker names cannot be empty.", - }; - } - - if (input.match(invalidWorkerNameStartEndRegex)) { - return { - valid: false, - cause: "Worker names cannot start or end with a dash.", - }; - } - - if (input.match(invalidWorkerNameCharsRegex)) { - return { - valid: false, - cause: - "Project names must only contain lowercase characters, numbers, and dashes.", - }; - } - - if (input.length > workerNameLengthLimit) { - return { - valid: false, - cause: "Project names must be less than 63 characters.", - }; - } - - return { valid: true }; -} - /** - * Given an input string it converts it to a valid worker name - * - * A worker name is valid if: - * - the name is not empty - * - the name doesn't start nor ends with a dash - * - the name doesn't contain special characters besides dashes - * - the name is not longer than 63 characters + * Displays the detected autoconfig details to the user via the context logger. * - * See: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations - * - * @param input The input to convert - * @returns The input itself if it was already valid, the input converted to a valid worker name otherwise + * @param autoConfigDetails - The detected project details to display. + * @param context - The autoconfig context providing the logger. + * @param displayOptions - Optional display customization. */ -export function toValidWorkerName(input: string): string { - if (checkWorkerNameValidity(input).valid) { - return input; - } - - input = input - // Replace all underscores with dashes - .replaceAll("_", "-") - // Replace all the special characters (besides dashes) with dashes - .replace(invalidWorkerNameCharsRegex, "-") - // Remove invalid start/end dashes - .replace(invalidWorkerNameStartEndRegex, "") - // If the name is longer than the limit let's truncate it to that - .slice(0, workerNameLengthLimit); - - if (!input.length) { - // If we've emptied the whole name let's replace it with a fallback value - return "my-worker"; - } - - return input; -} - export function displayAutoConfigDetails( autoConfigDetails: AutoConfigDetails, + context: AutoConfigContext, displayOptions?: { heading?: string } ): void { + const { logger } = context; logger.log(""); logger.log(displayOptions?.heading ?? "Detected Project Settings:"); @@ -397,10 +246,19 @@ export function displayAutoConfigDetails( logger.log(""); } +/** + * Prompts the user to confirm or modify the detected autoconfig details. + * + * @param autoConfigDetails - The detected project details. + * @param context - The autoconfig context providing dialogs. + * @returns The (possibly updated) autoconfig details. + */ export async function confirmAutoConfigDetails( - autoConfigDetails: AutoConfigDetails + autoConfigDetails: AutoConfigDetails, + context: AutoConfigContext ): Promise { - const modifySettings = await confirm( + const { dialogs } = context; + const modifySettings = await dialogs.confirm( "Do you want to modify these settings?", { defaultValue: false, fallbackValue: false } ); @@ -412,20 +270,23 @@ export async function confirmAutoConfigDetails( // Just spreading the object to shallow clone it to avoid some potential side effects const { ...updatedAutoConfigDetails } = autoConfigDetails; - const workerName = await prompt("What do you want to name your Worker?", { - defaultValue: autoConfigDetails.workerName ?? "", - validate: (value: string) => { - const validity = checkWorkerNameValidity(value); - if (validity.valid) { - return true; - } - return validity.cause; - }, - }); + const workerName = await dialogs.prompt( + "What do you want to name your Worker?", + { + defaultValue: autoConfigDetails.workerName ?? "", + validate: (value: string) => { + const validity = checkWorkerNameValidity(value); + if (validity.valid) { + return true; + } + return validity.cause; + }, + } + ); updatedAutoConfigDetails.workerName = workerName; - const frameworkId = await select( + const frameworkId = await dialogs.select( "What framework is your application using?", { choices: allFrameworksInfos.map((f) => ({ @@ -449,7 +310,7 @@ export async function confirmAutoConfigDetails( updatedAutoConfigDetails.framework = getFrameworkClassInstance(frameworkId); - const outputDir = await prompt( + const outputDir = await dialogs.prompt( "What directory contains your applications' output/asset files?", { defaultValue: autoConfigDetails.outputDir ?? "", @@ -474,7 +335,7 @@ export async function confirmAutoConfigDetails( updatedAutoConfigDetails.outputDir = outputDir; if (autoConfigDetails.buildCommand || autoConfigDetails.packageJson) { - const buildCommand = await prompt( + const buildCommand = await dialogs.prompt( "What is your application's build command?", { defaultValue: autoConfigDetails.buildCommand ?? "", diff --git a/packages/autoconfig/src/errors.ts b/packages/autoconfig/src/errors.ts new file mode 100644 index 0000000000..2bbb4cb3fb --- /dev/null +++ b/packages/autoconfig/src/errors.ts @@ -0,0 +1,39 @@ +import { FatalError, UserError } from "@cloudflare/workers-utils"; +import type { TelemetryMessage } from "@cloudflare/workers-utils"; + +/** + * Base class for errors where something in a autoconfig frameworks' configuration goes + * something wrong. These are not reported to Sentry. + */ +export class AutoConfigFrameworkConfigurationError extends UserError {} + +/** + * Error thrown when autoconfig detection fails. + * Carries detection metadata (`frameworkId`, `configured`) so that callers can + * extract it for telemetry without the autoconfig library needing to know about + * the telemetry system. + */ +export class AutoConfigDetectionError extends FatalError { + /** The detected framework identifier (if detection got far enough to determine it). */ + readonly frameworkId: string | undefined; + /** Whether the project was already configured at the time of the error. */ + readonly configured: boolean; + + /** + * @param message - The human-readable error message. + * @param options - Error options including telemetry message, optional code, and detection metadata. + */ + constructor( + message: string, + options: TelemetryMessage & { + code?: number; + frameworkId?: string; + configured: boolean; + } + ) { + super(message, options); + Object.setPrototypeOf(this, new.target.prototype); + this.frameworkId = options.frameworkId; + this.configured = options.configured; + } +} diff --git a/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts b/packages/autoconfig/src/frameworks/all-frameworks.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts rename to packages/autoconfig/src/frameworks/all-frameworks.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/analog.ts b/packages/autoconfig/src/frameworks/analog.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/analog.ts rename to packages/autoconfig/src/frameworks/analog.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/angular.ts b/packages/autoconfig/src/frameworks/angular.ts similarity index 97% rename from packages/wrangler/src/autoconfig/frameworks/angular.ts rename to packages/autoconfig/src/frameworks/angular.ts index 149abd756f..fdebfb1a01 100644 --- a/packages/wrangler/src/autoconfig/frameworks/angular.ts +++ b/packages/autoconfig/src/frameworks/angular.ts @@ -6,13 +6,13 @@ import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; import { parseJSONC } from "@cloudflare/workers-utils"; import semiver from "semiver"; -import { dedent } from "../../utils/dedent"; +import dedent from "ts-dedent"; import { Framework } from "./framework-class"; -import type { PackageManager } from "../../package-manager"; import type { ConfigurationOptions, ConfigurationResults, } from "./framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; export class Angular extends Framework { async configure({ diff --git a/packages/wrangler/src/autoconfig/frameworks/astro.ts b/packages/autoconfig/src/frameworks/astro.ts similarity index 95% rename from packages/wrangler/src/autoconfig/frameworks/astro.ts rename to packages/autoconfig/src/frameworks/astro.ts index 98f0a824ee..cbc986f1e9 100644 --- a/packages/wrangler/src/autoconfig/frameworks/astro.ts +++ b/packages/autoconfig/src/frameworks/astro.ts @@ -12,13 +12,13 @@ import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; import { parseJSONC } from "@cloudflare/workers-utils"; import * as recast from "recast"; import semiver from "semiver"; -import { logger } from "../../logger"; import { Framework } from "./framework-class"; -import type { PackageManager } from "../../package-manager"; +import type { AutoConfigContext } from "../context"; import type { ConfigurationOptions, ConfigurationResults, } from "./framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; export class Astro extends Framework { async configure({ @@ -27,6 +27,7 @@ export class Astro extends Framework { packageManager, projectPath, isWorkspaceRoot, + context, }: ConfigurationOptions): Promise { const astroVersion = this.frameworkVersion; @@ -51,7 +52,8 @@ export class Astro extends Framework { projectPath, isWorkspaceRoot, packageManager, - astroMajorVersion + astroMajorVersion, + context ); } @@ -227,8 +229,9 @@ function updateAstroConfig( * This replicates part of the `astro add cloudflare` behavior. * * @param projectPath The path of the project + * @param context The autoconfig context providing logger and other dependencies */ -function updateTsConfig(projectPath: string) { +function updateTsConfig(projectPath: string, context: AutoConfigContext) { const tsconfigPath = join(projectPath, "tsconfig.json"); if (!existsSync(tsconfigPath)) { return; @@ -247,7 +250,7 @@ function updateTsConfig(projectPath: string) { // If `include` is not defined, the tsconfig likely inherits it from a parent config (e.g., "extends": "astro/tsconfigs/base"). // Adding an `include` field here would override the parent's includes, breaking type-checking. // Instead, warn the user to add it manually. - logger.warn( + context.logger.warn( `Could not find an existing \`include\` field in tsconfig.json. You may need to manually add ${JSON.stringify( includeEntry )} to your tsconfig.json \`include\` array.` @@ -263,7 +266,7 @@ function updateTsConfig(projectPath: string) { ); } } catch { - logger.warn( + context.logger.warn( `Could not update tsconfig.json to include worker-configuration.d.ts. You may need to add it manually.` ); } @@ -283,7 +286,8 @@ async function configureAstroLegacy( projectPath: string, isWorkspaceRoot: boolean, packageManager: PackageManager, - astroMajorVersion: 4 | 5 + astroMajorVersion: 4 | 5, + context: AutoConfigContext ): Promise { const astroCloudflarePackageVersion = astroMajorVersion === 5 ? 12 : 11; @@ -297,5 +301,5 @@ async function configureAstroLegacy( } ); updateAstroConfig(projectPath, astroMajorVersion); - updateTsConfig(projectPath); + updateTsConfig(projectPath, context); } diff --git a/packages/wrangler/src/autoconfig/frameworks/framework-class.ts b/packages/autoconfig/src/frameworks/framework-class.ts similarity index 90% rename from packages/wrangler/src/autoconfig/frameworks/framework-class.ts rename to packages/autoconfig/src/frameworks/framework-class.ts index 7c60f62dd2..93f51529e8 100644 --- a/packages/wrangler/src/autoconfig/frameworks/framework-class.ts +++ b/packages/autoconfig/src/frameworks/framework-class.ts @@ -1,10 +1,10 @@ import assert from "node:assert"; import semiver from "semiver"; -import { logger } from "../../logger"; import { AutoConfigFrameworkConfigurationError } from "../errors"; import { getInstalledPackageVersion } from "./utils/packages"; import type { AutoConfigFrameworkPackageInfo, FrameworkInfo } from "."; -import type { PackageManager } from "../../package-manager"; +import type { AutoConfigContext } from "../context"; +import type { PackageManager } from "@cloudflare/workers-utils"; import type { RawConfig } from "@cloudflare/workers-utils"; export abstract class Framework { @@ -38,16 +38,18 @@ export abstract class Framework { /** * Validates the installed framework version against the supported range and * stores it for later access via the `frameworkVersion` getter. - * Warns via `logger` if the version exceeds `maximumKnownMajorVersion`. + * Warns via the context logger if the version exceeds `maximumKnownMajorVersion`. * * @param projectPath - Path to the project root used to resolve the installed version. * @param frameworkPackageInfo - Package metadata including name and version bounds. + * @param context - The autoconfig context providing logger and other dependencies. * @throws {AssertionError} If the installed version cannot be determined. * @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion`. */ validateFrameworkVersion( projectPath: string, - frameworkPackageInfo: AutoConfigFrameworkPackageInfo + frameworkPackageInfo: AutoConfigFrameworkPackageInfo, + context: AutoConfigContext ) { const frameworkVersion = getInstalledPackageVersion( frameworkPackageInfo.name, @@ -76,7 +78,7 @@ export abstract class Framework { semiver(frameworkVersion, frameworkPackageInfo.maximumKnownMajorVersion) > 0 ) { - logger.warn( + context.logger.warn( `The version of ${this.name} used in the project (${JSON.stringify( frameworkVersion )}) is not officially supported, and may fail to correctly configure. Please report any issues to https://github.com/cloudflare/workers-sdk/issues` @@ -94,6 +96,7 @@ export type ConfigurationOptions = { dryRun: boolean; packageManager: PackageManager; isWorkspaceRoot: boolean; + context: AutoConfigContext; }; export type PackageJsonScriptsOverrides = { diff --git a/packages/wrangler/src/autoconfig/frameworks/index.ts b/packages/autoconfig/src/frameworks/index.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/index.ts rename to packages/autoconfig/src/frameworks/index.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/next.ts b/packages/autoconfig/src/frameworks/next.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/next.ts rename to packages/autoconfig/src/frameworks/next.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/no-op.ts b/packages/autoconfig/src/frameworks/no-op.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/no-op.ts rename to packages/autoconfig/src/frameworks/no-op.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/nuxt.ts b/packages/autoconfig/src/frameworks/nuxt.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/nuxt.ts rename to packages/autoconfig/src/frameworks/nuxt.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/qwik.ts b/packages/autoconfig/src/frameworks/qwik.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/qwik.ts rename to packages/autoconfig/src/frameworks/qwik.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/react-router.ts b/packages/autoconfig/src/frameworks/react-router.ts similarity index 97% rename from packages/wrangler/src/autoconfig/frameworks/react-router.ts rename to packages/autoconfig/src/frameworks/react-router.ts index f93531ee61..b33a9c1ffa 100644 --- a/packages/wrangler/src/autoconfig/frameworks/react-router.ts +++ b/packages/autoconfig/src/frameworks/react-router.ts @@ -7,10 +7,10 @@ import { parseFile, transformFile } from "@cloudflare/codemod"; import * as recast from "recast"; import semiver from "semiver"; import dedent from "ts-dedent"; -import { logger } from "../../logger"; import { Framework } from "./framework-class"; import { transformViteConfig } from "./utils/vite-config"; import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { AutoConfigContext } from "../context"; import type { ConfigurationOptions, ConfigurationResults, @@ -317,10 +317,14 @@ function writeAppTs(useMiddlewarePattern: boolean) { * If the file already exists on disk it is left untouched and a warning is logged. * * @param useMiddlewarePattern - Whether `v8_middleware` is enabled in the user's config. + * @param context - The autoconfig context providing logger and other dependencies. */ -function writeEntryServerTsx(useMiddlewarePattern: boolean) { +function writeEntryServerTsx( + useMiddlewarePattern: boolean, + context: AutoConfigContext +) { if (existsSync("app/entry.server.tsx")) { - logger.warn( + context.logger.warn( "The file `app/entry.server.tsx` already exists on disk, and so we're not modifying it. This may lead to deployment failures if `app/entry.server.tsx` is not set up correctly." ); return; @@ -433,6 +437,7 @@ export class ReactRouter extends Framework { projectPath, packageManager, isWorkspaceRoot, + context, }: ConfigurationOptions): Promise { const viteEnvironmentKey = configPropertyName(this.frameworkVersion); const useMiddlewarePattern = hasV8MiddlewareFlag(projectPath); @@ -454,7 +459,7 @@ export class ReactRouter extends Framework { isWorkspaceRoot, }); - writeEntryServerTsx(useMiddlewarePattern); + writeEntryServerTsx(useMiddlewarePattern, context); transformViteConfig(projectPath, { viteEnvironmentName: "ssr", diff --git a/packages/wrangler/src/autoconfig/frameworks/solid-start.ts b/packages/autoconfig/src/frameworks/solid-start.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/solid-start.ts rename to packages/autoconfig/src/frameworks/solid-start.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/static.ts b/packages/autoconfig/src/frameworks/static.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/static.ts rename to packages/autoconfig/src/frameworks/static.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/sveltekit.ts b/packages/autoconfig/src/frameworks/sveltekit.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/sveltekit.ts rename to packages/autoconfig/src/frameworks/sveltekit.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts b/packages/autoconfig/src/frameworks/tanstack.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/tanstack.ts rename to packages/autoconfig/src/frameworks/tanstack.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts b/packages/autoconfig/src/frameworks/utils/packages.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/utils/packages.ts rename to packages/autoconfig/src/frameworks/utils/packages.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts b/packages/autoconfig/src/frameworks/utils/vite-config.ts similarity index 98% rename from packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts rename to packages/autoconfig/src/frameworks/utils/vite-config.ts index 0d1fe7d40c..42867b1dae 100644 --- a/packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts +++ b/packages/autoconfig/src/frameworks/utils/vite-config.ts @@ -4,7 +4,7 @@ import { transformFile } from "@cloudflare/codemod"; import { UserError } from "@cloudflare/workers-utils"; import * as recast from "recast"; import dedent from "ts-dedent"; -import { logger } from "../../../logger"; +import type { AutoConfigContext } from "../../context"; import type { types } from "recast"; const b = recast.types.builders; @@ -62,7 +62,8 @@ function extractFromBlockStatement( } export function checkIfViteConfigUsesCloudflarePlugin( - projectPath: string + projectPath: string, + context?: AutoConfigContext ): boolean { const filePath = getViteConfigPath(projectPath); @@ -92,7 +93,7 @@ export function checkIfViteConfigUsesCloudflarePlugin( const configObject = extractConfigObject(n.node.arguments[0]); if (!configObject) { - logger.debug( + context?.logger.debug( `Vite config uses an unsupported expression type. Skipping Cloudflare plugin check.` ); return this.traverse(n); @@ -102,7 +103,7 @@ export function checkIfViteConfigUsesCloudflarePlugin( isPluginsProp(prop) ); if (!pluginsProp || !t.ArrayExpression.check(pluginsProp.value)) { - logger.debug( + context?.logger.debug( `Vite config does not have a valid plugins array. Skipping Cloudflare plugin check.` ); return this.traverse(n); diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts b/packages/autoconfig/src/frameworks/utils/vite-plugin.ts similarity index 96% rename from packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts rename to packages/autoconfig/src/frameworks/utils/vite-plugin.ts index 999f19f7a2..c6eabd0ab8 100644 --- a/packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts +++ b/packages/autoconfig/src/frameworks/utils/vite-plugin.ts @@ -2,7 +2,7 @@ import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; import semiver from "semiver"; import { getInstalledPackageVersion } from "./packages"; -import type { PackageManager } from "../../../package-manager"; +import type { PackageManager } from "@cloudflare/workers-utils"; /** * Installs the `@cloudflare/vite-plugin` package as a dev dependency diff --git a/packages/wrangler/src/autoconfig/frameworks/vike.ts b/packages/autoconfig/src/frameworks/vike.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/vike.ts rename to packages/autoconfig/src/frameworks/vike.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/vite.ts b/packages/autoconfig/src/frameworks/vite.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/vite.ts rename to packages/autoconfig/src/frameworks/vite.ts diff --git a/packages/wrangler/src/autoconfig/frameworks/waku.ts b/packages/autoconfig/src/frameworks/waku.ts similarity index 100% rename from packages/wrangler/src/autoconfig/frameworks/waku.ts rename to packages/autoconfig/src/frameworks/waku.ts diff --git a/packages/autoconfig/src/index.ts b/packages/autoconfig/src/index.ts new file mode 100644 index 0000000000..2fee32a53d --- /dev/null +++ b/packages/autoconfig/src/index.ts @@ -0,0 +1,39 @@ +export type { + AutoConfigContext, + AutoConfigLogger, + AutoConfigDialogs, +} from "./context"; + +export { getDetailsForAutoConfig } from "./details"; +export { runAutoConfig, buildOperationsSummary } from "./run"; + +export { Framework } from "./frameworks/framework-class"; +export type { + ConfigurationOptions, + ConfigurationResults, + PackageJsonScriptsOverrides, +} from "./frameworks/framework-class"; + +export { isFrameworkSupported } from "./frameworks"; + +export type { + FrameworkInfo, + AutoConfigFrameworkPackageInfo, +} from "./frameworks"; + +export { displayAutoConfigDetails, confirmAutoConfigDetails } from "./details"; + +export type { + AutoConfigDetails, + AutoConfigDetailsForConfiguredProject, + AutoConfigDetailsForNonConfiguredProject, + AutoConfigOptions, + AutoConfigSummary, +} from "./types"; + +export { + AutoConfigDetectionError, + AutoConfigFrameworkConfigurationError, +} from "./errors"; + +export { getInstalledPackageVersion } from "./frameworks/utils/packages"; diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/autoconfig/src/run.ts similarity index 53% rename from packages/wrangler/src/autoconfig/run.ts rename to packages/autoconfig/src/run.ts index 5493ef5908..58b045be99 100644 --- a/packages/wrangler/src/autoconfig/run.ts +++ b/packages/autoconfig/src/run.ts @@ -12,11 +12,6 @@ import { getTodaysCompatDate, parseJSONC, } from "@cloudflare/workers-utils"; -import { runCommand } from "../deployment-bundle/run-custom-build"; -import { confirm } from "../dialogs"; -import { logger } from "../logger"; -import { sendMetricsEvent } from "../metrics"; -import { sanitizeError } from "../metrics/sanitization"; import { assertNonConfigured, confirmAutoConfigDetails, @@ -29,8 +24,8 @@ import { } from "./frameworks"; import { getFrameworkPackageInfo } from "./frameworks/all-frameworks"; import { Static } from "./frameworks/static"; -import { getAutoConfigId } from "./telemetry-utils"; import { usesTypescript } from "./uses-typescript"; +import type { AutoConfigContext } from "./context"; import type { AutoConfigDetails, AutoConfigDetailsForNonConfiguredProject, @@ -39,10 +34,21 @@ import type { } from "./types"; import type { PackageJSON, RawConfig } from "@cloudflare/workers-utils"; +/** + * Runs the full autoconfig flow: displays detected settings, confirms with the user, + * validates the framework version, runs framework configuration, writes wrangler config, + * updates package.json scripts, and optionally runs the build command. + * + * @param autoConfigDetails - The detected project details from `getDetailsForAutoConfig()`. + * @param autoConfigOptions - Options controlling dry-run, confirmations, build, and context. + * @returns A summary of all operations performed. + */ export async function runAutoConfig( autoConfigDetails: AutoConfigDetails, - autoConfigOptions: AutoConfigOptions = {} + autoConfigOptions: AutoConfigOptions ): Promise { + const { context } = autoConfigOptions; + const { logger } = context; const dryRun = autoConfigOptions.dryRun === true; const runBuild = !dryRun && (autoConfigOptions.runBuild ?? true); const skipConfirmations = @@ -50,241 +56,196 @@ export async function runAutoConfig( const enableWranglerInstallation = autoConfigOptions.enableWranglerInstallation ?? true; - const autoConfigId = getAutoConfigId(); - - sendMetricsEvent( - "autoconfig_configuration_started", - { - autoConfigId, - framework: autoConfigDetails.framework?.id, - dryRun, - }, - {} - ); - assertNonConfigured(autoConfigDetails); - let autoConfigSummary: AutoConfigSummary; - - try { - displayAutoConfigDetails(autoConfigDetails); - - const updatedAutoConfigDetails = skipConfirmations - ? autoConfigDetails - : await confirmAutoConfigDetails(autoConfigDetails); - - if (autoConfigDetails !== updatedAutoConfigDetails) { - displayAutoConfigDetails(updatedAutoConfigDetails, { - heading: "Updated Project Settings:", - }); - } - - autoConfigDetails = updatedAutoConfigDetails; - assertNonConfigured(autoConfigDetails); - - if (isKnownFramework(autoConfigDetails.framework.id)) { - const frameworkIsSupported = isFrameworkSupported( - autoConfigDetails.framework.id - ); - if (!frameworkIsSupported) { - throw new FatalError( - autoConfigDetails.framework.id === "cloudflare-pages" - ? `The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to Workers is not yet supported.` - : `The detected framework ("${autoConfigDetails.framework.name}") cannot be automatically configured.`, - { telemetryMessage: "autoconfig run framework unsupported" } - ); - } - } - - assert( - autoConfigDetails.outputDir, - "The Output Directory is unexpectedly missing" - ); - - const compatibilityDate = getTodaysCompatDate(); + displayAutoConfigDetails(autoConfigDetails, context); - const wranglerConfig: RawConfig = { - $schema: "node_modules/wrangler/config-schema.json", - name: autoConfigDetails.workerName, - compatibility_date: compatibilityDate, - observability: { - enabled: true, - }, - } satisfies RawConfig; + const updatedAutoConfigDetails = skipConfirmations + ? autoConfigDetails + : await confirmAutoConfigDetails(autoConfigDetails, context); - const { packageManager } = autoConfigDetails; + if (autoConfigDetails !== updatedAutoConfigDetails) { + displayAutoConfigDetails(updatedAutoConfigDetails, context, { + heading: "Updated Project Settings:", + }); + } - const isWorkspaceRoot = autoConfigDetails.isWorkspaceRoot ?? false; + autoConfigDetails = updatedAutoConfigDetails; + assertNonConfigured(autoConfigDetails); - const frameworkPackageInfo = getFrameworkPackageInfo( + if (isKnownFramework(autoConfigDetails.framework.id)) { + const frameworkIsSupported = isFrameworkSupported( autoConfigDetails.framework.id ); - if (frameworkPackageInfo) { - autoConfigDetails.framework.validateFrameworkVersion( - autoConfigDetails.projectPath, - frameworkPackageInfo + if (!frameworkIsSupported) { + throw new FatalError( + autoConfigDetails.framework.id === "cloudflare-pages" + ? `The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to Workers is not yet supported.` + : `The detected framework ("${autoConfigDetails.framework.name}") cannot be automatically configured.`, + { telemetryMessage: "autoconfig run framework unsupported" } ); } + } - const dryRunConfigurationResults = - await autoConfigDetails.framework.configure({ - outputDir: autoConfigDetails.outputDir, - projectPath: autoConfigDetails.projectPath, - workerName: autoConfigDetails.workerName, - isWorkspaceRoot, - dryRun: true, - packageManager, - }); - - const { npx } = packageManager; - - autoConfigSummary = await buildOperationsSummary( - { ...autoConfigDetails, outputDir: autoConfigDetails.outputDir }, - dryRunConfigurationResults.wranglerConfig === null - ? null - : ensureNodejsCompatIsInConfig({ - ...wranglerConfig, - ...dryRunConfigurationResults.wranglerConfig, - }), - { - build: - dryRunConfigurationResults.buildCommandOverride ?? - autoConfigDetails.buildCommand, - deploy: - dryRunConfigurationResults.deployCommandOverride ?? - `${npx} wrangler deploy`, - version: - dryRunConfigurationResults?.versionCommandOverride ?? - `${npx} wrangler versions upload`, - }, - dryRunConfigurationResults.packageJsonScriptsOverrides - ); + assert( + autoConfigDetails.outputDir, + "The Output Directory is unexpectedly missing" + ); - if (!(skipConfirmations || (await confirm("Proceed with setup?")))) { - throw new FatalError("Setup cancelled", { - telemetryMessage: "autoconfig run setup cancelled", - }); - } + const compatibilityDate = getTodaysCompatDate(); - if (dryRun) { - logger.log( - `✋ ${"Autoconfig process run in dry-run mode, existing now."}` - ); - logger.log(""); + const wranglerConfig: RawConfig = { + $schema: "node_modules/wrangler/config-schema.json", + name: autoConfigDetails.workerName, + compatibility_date: compatibilityDate, + observability: { + enabled: true, + }, + } satisfies RawConfig; - sendMetricsEvent( - "autoconfig_configuration_completed", - { - autoConfigId, - framework: autoConfigDetails.framework?.id, - success: true, - dryRun, - }, - {} - ); + const { packageManager } = autoConfigDetails; - return autoConfigSummary; - } + const isWorkspaceRoot = autoConfigDetails.isWorkspaceRoot ?? false; - logger.debug( - `Running autoconfig with:\n${JSON.stringify( - autoConfigDetails, - null, - 2 - )}...` + const frameworkPackageInfo = getFrameworkPackageInfo( + autoConfigDetails.framework.id + ); + if (frameworkPackageInfo) { + autoConfigDetails.framework.validateFrameworkVersion( + autoConfigDetails.projectPath, + frameworkPackageInfo, + context ); + } - if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) { - await installWrangler(packageManager.type, isWorkspaceRoot); - } - - const configurationResults = await autoConfigDetails.framework.configure({ + const dryRunConfigurationResults = + await autoConfigDetails.framework.configure({ outputDir: autoConfigDetails.outputDir, projectPath: autoConfigDetails.projectPath, workerName: autoConfigDetails.workerName, isWorkspaceRoot, - dryRun: false, + dryRun: true, packageManager, + context, }); - if (autoConfigDetails.packageJson) { - const packageJsonPath = resolve( - autoConfigDetails.projectPath, - "package.json" - ); - const existingPackageJson = JSON.parse( - await readFile(packageJsonPath, "utf8") - ) as PackageJSON; - - await writeFile( - packageJsonPath, - JSON.stringify( - { - ...existingPackageJson, - scripts: { - ...existingPackageJson.scripts, - ...autoConfigSummary.scripts, - }, - } satisfies PackageJSON, - null, - 2 - ) + "\n" - ); - } + const { npx } = packageManager; - if (configurationResults.wranglerConfig !== null) { - await saveWranglerJsonc( - autoConfigDetails.projectPath, - ensureNodejsCompatIsInConfig({ + const autoConfigSummary = await buildOperationsSummary( + { ...autoConfigDetails, outputDir: autoConfigDetails.outputDir }, + dryRunConfigurationResults.wranglerConfig === null + ? null + : ensureNodejsCompatIsInConfig({ ...wranglerConfig, - ...configurationResults.wranglerConfig, - }) - ); - } + ...dryRunConfigurationResults.wranglerConfig, + }), + { + build: + dryRunConfigurationResults.buildCommandOverride ?? + autoConfigDetails.buildCommand, + deploy: + dryRunConfigurationResults.deployCommandOverride ?? + `${npx} wrangler deploy`, + version: + dryRunConfigurationResults?.versionCommandOverride ?? + `${npx} wrangler versions upload`, + }, + context, + dryRunConfigurationResults.packageJsonScriptsOverrides + ); - maybeAppendWranglerToGitIgnore(autoConfigDetails.projectPath); + if ( + !( + skipConfirmations || + (await context.dialogs.confirm("Proceed with setup?")) + ) + ) { + throw new FatalError("Setup cancelled", { + telemetryMessage: "autoconfig run setup cancelled", + }); + } - // If we're uploading the project path as the output directory, make sure we don't accidentally upload any sensitive Wrangler files - if (autoConfigDetails.outputDir === autoConfigDetails.projectPath) { - maybeAppendWranglerToGitIgnoreLikeFile( - `${autoConfigDetails.projectPath}/.assetsignore` - ); - } + if (dryRun) { + logger.log( + `✋ ${"Autoconfig process run in dry-run mode, existing now."}` + ); + logger.log(""); - const buildCommand = - configurationResults.buildCommandOverride ?? - autoConfigDetails.buildCommand; + return autoConfigSummary; + } - if (buildCommand && runBuild) { - await runCommand(buildCommand, autoConfigDetails.projectPath, "[build]"); - } - } catch (error) { - sendMetricsEvent( - "autoconfig_configuration_completed", - { - autoConfigId, + logger.debug( + `Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...` + ); - framework: autoConfigDetails.framework?.id, - dryRun, - success: false, - ...sanitizeError(error), - }, - {} + if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) { + await installWrangler(packageManager.type, isWorkspaceRoot); + } + + const configurationResults = await autoConfigDetails.framework.configure({ + outputDir: autoConfigDetails.outputDir, + projectPath: autoConfigDetails.projectPath, + workerName: autoConfigDetails.workerName, + isWorkspaceRoot, + dryRun: false, + packageManager, + context, + }); + + if (autoConfigDetails.packageJson) { + const packageJsonPath = resolve( + autoConfigDetails.projectPath, + "package.json" ); + const existingPackageJson = JSON.parse( + await readFile(packageJsonPath, "utf8") + ) as PackageJSON; - throw error; + await writeFile( + packageJsonPath, + JSON.stringify( + { + ...existingPackageJson, + scripts: { + ...existingPackageJson.scripts, + ...autoConfigSummary.scripts, + }, + } satisfies PackageJSON, + null, + 2 + ) + "\n" + ); } - sendMetricsEvent( - "autoconfig_configuration_completed", - { - autoConfigId, - framework: autoConfigDetails.framework?.id, - success: true, - dryRun, - }, - {} - ); + if (configurationResults.wranglerConfig !== null) { + await saveWranglerJsonc( + autoConfigDetails.projectPath, + ensureNodejsCompatIsInConfig({ + ...wranglerConfig, + ...configurationResults.wranglerConfig, + }) + ); + } + + maybeAppendWranglerToGitIgnore(autoConfigDetails.projectPath); + + // If we're uploading the project path as the output directory, make sure we don't accidentally upload any sensitive Wrangler files + if (autoConfigDetails.outputDir === autoConfigDetails.projectPath) { + maybeAppendWranglerToGitIgnoreLikeFile( + `${autoConfigDetails.projectPath}/.assetsignore` + ); + } + + const buildCommand = + configurationResults.buildCommandOverride ?? autoConfigDetails.buildCommand; + + if (buildCommand && runBuild) { + await context.runCommand( + buildCommand, + autoConfigDetails.projectPath, + "[build]" + ); + } return autoConfigSummary; } @@ -349,6 +310,18 @@ async function saveWranglerJsonc( ); } +/** + * Builds a summary of all operations that autoconfig will (or did) perform, + * including package installation, package.json script updates, wrangler config + * creation, and framework-specific configuration. + * + * @param autoConfigDetails - The detected project details. + * @param wranglerConfigToWrite - The wrangler config object to write, or `null` if not applicable. + * @param projectCommands - The build, deploy, and version commands for the project. + * @param context - The autoconfig context providing logger and other dependencies. + * @param packageJsonScriptsOverrides - Optional overrides for package.json script entries. + * @returns A summary object describing all planned operations. + */ export async function buildOperationsSummary( autoConfigDetails: AutoConfigDetailsForNonConfiguredProject & { outputDir: NonNullable; @@ -359,8 +332,10 @@ export async function buildOperationsSummary( deploy: string; version?: string; }, + context: AutoConfigContext, packageJsonScriptsOverrides?: PackageJsonScriptsOverrides ): Promise { + const { logger } = context; logger.log(""); const summary: AutoConfigSummary = { diff --git a/packages/wrangler/src/autoconfig/types.ts b/packages/autoconfig/src/types.ts similarity index 87% rename from packages/wrangler/src/autoconfig/types.ts rename to packages/autoconfig/src/types.ts index f173ffed5c..3cd97d09e3 100644 --- a/packages/wrangler/src/autoconfig/types.ts +++ b/packages/autoconfig/src/types.ts @@ -1,8 +1,11 @@ -import type { PackageManager } from "../package-manager"; -import type { Optional } from "../utils/types"; +import type { AutoConfigContext } from "./context"; import type { Framework } from "./frameworks/framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; import type { PackageJSON, RawConfig } from "@cloudflare/workers-utils"; +/** Makes the specified keys of T optional. */ +type Optional = Omit & Partial>; + type AutoConfigDetailsBase = { /** The name of the worker */ workerName: string; @@ -42,6 +45,8 @@ export type AutoConfigDetails = | AutoConfigDetailsForNonConfiguredProject; export type AutoConfigOptions = { + /** The autoconfig context providing logger, dialogs, and other dependencies. */ + context: AutoConfigContext; /** Whether to run autoconfig without actually applying any filesystem modification (default: false) */ dryRun?: boolean; /** diff --git a/packages/wrangler/src/autoconfig/uses-typescript.ts b/packages/autoconfig/src/uses-typescript.ts similarity index 100% rename from packages/wrangler/src/autoconfig/uses-typescript.ts rename to packages/autoconfig/src/uses-typescript.ts diff --git a/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts b/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts new file mode 100644 index 0000000000..f3550ef326 --- /dev/null +++ b/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts @@ -0,0 +1,248 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { describe, test, vi } from "vitest"; +import { confirmAutoConfigDetails } from "../../src/details"; +import { Astro } from "../../src/frameworks/astro"; +import { Static } from "../../src/frameworks/static"; +import { createMockContext } from "../helpers/mock-context"; + +describe("autoconfig details - confirmAutoConfigDetails()", () => { + describe("interactive mode", () => { + test("no modifications applied", async ({ expect }) => { + const noModifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "worker-name", + buildCommand: "npm run build", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + noModifyContext + ); + + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./public", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "worker-name", + } + `); + }); + + test("settings can be updated in a plain static site without a framework nor a build script", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("new-name") + .mockResolvedValueOnce("./_public_") + .mockResolvedValueOnce("npm run app:build"), + select: vi.fn().mockResolvedValue("static"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-worker", + buildCommand: "npm run build", + outputDir: "", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + modifyContext + ); + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run app:build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./_public_", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "new-name", + } + `); + }); + + test("settings can be updated in a static app using a framework", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("my-astro-worker") + .mockResolvedValueOnce("") + .mockResolvedValueOnce("npm run build"), + select: vi.fn().mockResolvedValue("astro"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-astro-site", + buildCommand: "astro build", + framework: new Astro({ id: "astro", name: "Astro" }), + outputDir: "", + projectPath: "", + configured: false, + packageManager: NpmPackageManager, + }, + modifyContext + ); + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Astro { + "configurationDescription": "Configuring project for Astro with "astro add cloudflare"", + "id": "astro", + "name": "Astro", + }, + "outputDir": "", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "my-astro-worker", + } + `); + }); + + test("framework can be changed from a detected framework to another", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("my-nuxt-worker") + .mockResolvedValueOnce("./dist") + .mockResolvedValueOnce("npm run build"), + select: vi.fn().mockResolvedValue("nuxt"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-astro-site", + buildCommand: "astro build", + framework: new Astro({ id: "astro", name: "Astro" }), + outputDir: "", + projectPath: "", + configured: false, + packageManager: NpmPackageManager, + }, + modifyContext + ); + + expect(updatedAutoConfigDetails.framework?.id).toBe("nuxt"); + expect(updatedAutoConfigDetails.framework?.name).toBe("Nuxt"); + }); + }); + + describe("non-interactive mode", () => { + test("no modifications are applied in non-interactive", async ({ + expect, + }) => { + const nonInteractiveContext = createMockContext({ + isNonInteractiveOrCI: () => true, + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "worker-name", + buildCommand: "npm run build", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + nonInteractiveContext + ); + + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./public", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "worker-name", + } + `); + }); + }); +}); diff --git a/packages/autoconfig/tests/details/display-auto-config-details.test.ts b/packages/autoconfig/tests/details/display-auto-config-details.test.ts new file mode 100644 index 0000000000..14dbff19a7 --- /dev/null +++ b/packages/autoconfig/tests/details/display-auto-config-details.test.ts @@ -0,0 +1,96 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { displayAutoConfigDetails } from "../../src/details"; +import { Static } from "../../src/frameworks/static"; +import { createMockContext } from "../helpers/mock-context"; +import type { Framework } from "../../src/frameworks"; + +describe("autoconfig details - displayAutoConfigDetails()", () => { + const std = mockConsoleMethods(); + const context = createMockContext(); + + it("should cleanly handle a case in which only the worker name has been detected", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-project", + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot( + ` + " + Detected Project Settings: + - Worker Name: my-project + - Framework: Static + - Output Directory: ./public + " + ` + ); + }); + + it("should display all the project settings provided by the details object", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-astro-app", + framework: { + name: "Astro", + id: "astro", + isConfigured: () => false, + configure: () => + ({ + wranglerConfig: {}, + }) satisfies ReturnType, + } as unknown as Framework, + buildCommand: "astro build", + outputDir: "dist", + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot(` + " + Detected Project Settings: + - Worker Name: my-astro-app + - Framework: Astro + - Build Command: astro build + - Output Directory: dist + " + `); + }); + + it("should omit the framework and build command entries when they are not part of the details object", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-site", + outputDir: "dist", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot(` + " + Detected Project Settings: + - Worker Name: my-site + - Framework: Static + - Output Directory: dist + " + `); + }); +}); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts similarity index 77% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts rename to packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts index b289efe8ca..6f964c0898 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts @@ -1,9 +1,12 @@ import { writeFile } from "node:fs/promises"; import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import { describe, it } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + describe("detectFramework() / basic framework detection", () => { runInTempDir(); + const context = createMockContext(); it("defaults to the static framework when no framework is detected", async ({ expect, @@ -13,7 +16,7 @@ describe("detectFramework() / basic framework detection", () => { JSON.stringify({ lockfileVersion: 3 }) ); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework.framework.id).toBe("static"); }); @@ -24,7 +27,7 @@ describe("detectFramework() / basic framework detection", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.framework.id).toBe("astro"); expect(result.detectedFramework?.framework.name).toBe("Astro"); @@ -38,7 +41,7 @@ describe("detectFramework() / basic framework detection", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.buildCommand).toBeDefined(); expect(result.detectedFramework?.buildCommand).toContain("astro build"); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts b/packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts similarity index 70% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts rename to packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts index 28cd6131a4..15167c9da4 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts @@ -1,20 +1,25 @@ -import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { + mockConsoleMethods, + runInTempDir, + seed, +} from "@cloudflare/workers-utils/test-helpers"; import { describe, it } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; -import { mockConsoleMethods } from "../../../helpers/mock-console"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; const noLockFileWarning = "No lock file has been detected in the current working directory. This might indicate that the project is part of a workspace."; describe("detectFramework() / lock file warning", () => { runInTempDir(); const std = mockConsoleMethods(); + const context = createMockContext(); it("warns when no lock file is detected", async ({ expect }) => { await seed({ "package.json": JSON.stringify({ dependencies: { astro: "5" } }), }); - await detectFramework(process.cwd()); + await detectFramework(process.cwd(), context); expect(std.warn).toContain(noLockFileWarning); }); @@ -26,7 +31,7 @@ describe("detectFramework() / lock file warning", () => { "package.json": JSON.stringify({}), }); - await detectFramework(process.cwd()); + await detectFramework(process.cwd(), context); expect(std.warn).not.toContain(noLockFileWarning); }); @@ -37,7 +42,7 @@ describe("detectFramework() / lock file warning", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - await detectFramework(process.cwd()); + await detectFramework(process.cwd(), context); expect(std.warn).not.toContain(noLockFileWarning); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts b/packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts similarity index 75% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts rename to packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts index bcfc2dc471..b72b606398 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts @@ -1,29 +1,17 @@ import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; -import * as isInteractiveModule from "../../../../is-interactive"; -import type { MockInstance } from "vitest"; +import { afterEach, describe, it, vi } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; describe("detectFramework() / multiple frameworks detected", () => { runInTempDir(); - let isNonInteractiveOrCISpy: MockInstance; - - beforeEach(() => { - isNonInteractiveOrCISpy = vi - .spyOn(isInteractiveModule, "isNonInteractiveOrCI") - .mockReturnValue(false); - }); + const context = createMockContext(); afterEach(() => { vi.unstubAllGlobals(); - isNonInteractiveOrCISpy.mockRestore(); }); describe("non-CI environment", () => { - beforeEach(() => { - isNonInteractiveOrCISpy.mockReturnValue(false); - }); - it("returns the known framework when multiple are detected but only one is known", async ({ expect, }) => { @@ -35,7 +23,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.framework.id).toBe("astro"); }); @@ -50,7 +38,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.framework.id).toBe("next"); }); @@ -65,7 +53,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.framework.id).toBe("waku"); }); @@ -84,7 +72,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.detectedFramework?.framework.id).toBe("hydrogen"); }); @@ -101,13 +89,15 @@ describe("detectFramework() / multiple frameworks detected", () => { }); // Should not throw even with multiple unknowns in non-CI mode - await expect(detectFramework(process.cwd())).resolves.toBeDefined(); + await expect( + detectFramework(process.cwd(), context) + ).resolves.toBeDefined(); }); }); describe("CI environment", () => { - beforeEach(() => { - isNonInteractiveOrCISpy.mockReturnValue(true); + const ciContext = createMockContext({ + isNonInteractiveOrCI: () => true, }); it("throws MultipleFrameworksCIError when multiple known frameworks are detected", async ({ @@ -121,10 +111,10 @@ describe("detectFramework() / multiple frameworks detected", () => { }); await expect( - detectFramework(process.cwd()) + detectFramework(process.cwd(), ciContext) ).rejects.toThrowErrorMatchingInlineSnapshot( ` - [Error: Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: Astro, Nuxt. + [Error: Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: Astro, Nuxt. To fix this issue either: - check your project's configuration to make sure that the target framework @@ -146,8 +136,10 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - await expect(detectFramework(process.cwd())).rejects.toThrowError( - /Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found/ + await expect( + detectFramework(process.cwd(), ciContext) + ).rejects.toThrowError( + /Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found/ ); }); @@ -165,7 +157,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), ciContext); expect(result.detectedFramework?.framework.id).toBe("hydrogen"); }); @@ -180,7 +172,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), ciContext); expect(result.detectedFramework?.framework.id).toBe("astro"); }); @@ -195,7 +187,7 @@ describe("detectFramework() / multiple frameworks detected", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), ciContext); expect(result.detectedFramework?.framework.id).toBe("tanstack-start"); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts similarity index 75% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts rename to packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts index 991288d0d0..22887c46c3 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts @@ -1,14 +1,17 @@ -import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; -import { describe, it } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; import { BunPackageManager, NpmPackageManager, PnpmPackageManager, YarnPackageManager, -} from "../../../../package-manager"; +} from "@cloudflare/workers-utils"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + describe("detectFramework() / package manager detection", () => { runInTempDir(); + const context = createMockContext(); it("detects npm when package-lock.json is present", async ({ expect }) => { await seed({ @@ -16,7 +19,7 @@ describe("detectFramework() / package manager detection", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.packageManager).toStrictEqual(NpmPackageManager); }); @@ -27,7 +30,7 @@ describe("detectFramework() / package manager detection", () => { "pnpm-lock.yaml": "lockfileVersion: '6.0'\n", }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.packageManager).toStrictEqual(PnpmPackageManager); }); @@ -38,7 +41,7 @@ describe("detectFramework() / package manager detection", () => { "yarn.lock": "# yarn lockfile v1\n", }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.packageManager).toStrictEqual(YarnPackageManager); }); @@ -49,7 +52,7 @@ describe("detectFramework() / package manager detection", () => { "bun.lock": "", }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.packageManager).toStrictEqual(BunPackageManager); }); @@ -61,7 +64,7 @@ describe("detectFramework() / package manager detection", () => { "package.json": JSON.stringify({ dependencies: { astro: "5" } }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.packageManager).toStrictEqual(NpmPackageManager); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts similarity index 59% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts rename to packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts index 85e43358c0..188072cc1b 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts @@ -1,27 +1,13 @@ import { join } from "node:path"; import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; -import * as configCache from "../../../../config-cache"; -import * as isInteractiveModule from "../../../../is-interactive"; -import { PAGES_CONFIG_CACHE_FILENAME } from "../../../../pages/constants"; -import { mockConfirm } from "../../../helpers/mock-dialogs"; +import { describe, it, vi } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; import type { Config } from "@cloudflare/workers-utils"; -import type { MockInstance } from "vitest"; describe("detectFramework() / Pages project detection", () => { runInTempDir(); - let isNonInteractiveOrCISpy: MockInstance; - - beforeEach(() => { - isNonInteractiveOrCISpy = vi - .spyOn(isInteractiveModule, "isNonInteractiveOrCI") - .mockReturnValue(false); - }); - - afterEach(() => { - isNonInteractiveOrCISpy.mockRestore(); - }); + const context = createMockContext(); it("returns Cloudflare Pages framework when pages_build_output_dir is set in wrangler config", async ({ expect, @@ -31,7 +17,7 @@ describe("detectFramework() / Pages project detection", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd(), { + const result = await detectFramework(process.cwd(), context, { pages_build_output_dir: "./dist", } as Config); @@ -47,23 +33,19 @@ describe("detectFramework() / Pages project detection", () => { await seed({ "package.json": JSON.stringify({}), "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), - [join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME)]: JSON.stringify({ + [join(cacheFolder, "pages.json")]: JSON.stringify({ account_id: "test-account", }), }); - const getCacheFolderSpy = vi - .spyOn(configCache, "getCacheFolder") - .mockReturnValue(cacheFolder); + const cacheContext = createMockContext({ + getCacheFolder: () => cacheFolder, + }); - try { - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), cacheContext); - expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); - expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); - } finally { - getCacheFolderSpy.mockRestore(); - } + expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); + expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); }); it("returns Cloudflare Pages when a functions/ directory exists, no framework is detected, and the user confirms", async ({ @@ -75,12 +57,15 @@ describe("detectFramework() / Pages project detection", () => { "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, }); - mockConfirm({ - text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", - result: true, + const confirmContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), confirmContext); expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); @@ -95,12 +80,15 @@ describe("detectFramework() / Pages project detection", () => { "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, }); - mockConfirm({ - text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", - result: false, + const denyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), denyContext); expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages"); }); @@ -113,7 +101,7 @@ describe("detectFramework() / Pages project detection", () => { "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); // Astro is detected, so Pages detection via functions/ is skipped expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages"); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts b/packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts similarity index 71% rename from packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts rename to packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts index fc0d50a67f..7da594d646 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts @@ -1,8 +1,11 @@ import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import { describe, it } from "vitest"; -import { detectFramework } from "../../../../autoconfig/details/framework-detection"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + describe("detectFramework() / workspace root handling", () => { runInTempDir(); + const context = createMockContext(); it("sets isWorkspaceRoot to false for regular (non-monorepo) projects", async ({ expect, @@ -12,7 +15,7 @@ describe("detectFramework() / workspace root handling", () => { "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.isWorkspaceRoot).toBe(false); }); @@ -29,7 +32,7 @@ describe("detectFramework() / workspace root handling", () => { "packages/my-app/package.json": JSON.stringify({ name: "my-app" }), }); - const result = await detectFramework(process.cwd()); + const result = await detectFramework(process.cwd(), context); expect(result.isWorkspaceRoot).toBe(true); }); @@ -48,9 +51,9 @@ describe("detectFramework() / workspace root handling", () => { }); await expect( - detectFramework(process.cwd()) + detectFramework(process.cwd(), context) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` + `[Error: The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` ); }); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts b/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts similarity index 74% rename from packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts rename to packages/autoconfig/tests/details/get-details-for-auto-config.test.ts index 3694ba54ca..874285ad71 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts +++ b/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts @@ -1,38 +1,23 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import * as details from "../../../autoconfig/details"; -import * as configCache from "../../../config-cache"; -import * as isInteractiveModule from "../../../is-interactive"; -import { clearOutputFilePath } from "../../../output"; -import { getPackageManager, NpmPackageManager } from "../../../package-manager"; -import { PAGES_CONFIG_CACHE_FILENAME } from "../../../pages/constants"; -import { mockConsoleMethods } from "../../helpers/mock-console"; -import { mockConfirm } from "../../helpers/mock-dialogs"; -import { useMockIsTTY } from "../../helpers/mock-istty"; +import { + mockConsoleMethods, + runInTempDir, + seed, +} from "@cloudflare/workers-utils/test-helpers"; +import { afterEach, describe, it, vi } from "vitest"; +import * as details from "../../src/details"; +import { createMockContext } from "../helpers/mock-context"; import type { Config } from "@cloudflare/workers-utils"; -import type { Mock, MockInstance } from "vitest"; describe("autoconfig details - getDetailsForAutoConfig()", () => { runInTempDir(); - const { setIsTTY } = useMockIsTTY(); const std = mockConsoleMethods(); - let isNonInteractiveOrCISpy: MockInstance; - - beforeEach(() => { - setIsTTY(true); - (getPackageManager as Mock).mockResolvedValue(NpmPackageManager); - isNonInteractiveOrCISpy = vi - .spyOn(isInteractiveModule, "isNonInteractiveOrCI") - .mockReturnValue(false); - }); + const context = createMockContext(); afterEach(() => { vi.unstubAllGlobals(); - clearOutputFilePath(); - isNonInteractiveOrCISpy.mockRestore(); }); it("should set configured: true if a configPath exists", async ({ @@ -41,6 +26,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { await expect( details.getDetailsForAutoConfig({ wranglerConfig: { configPath: "/tmp" } as Config, + context, }) ).resolves.toMatchObject({ configured: true }); }); @@ -69,7 +55,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { ); } - await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({ + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ buildCommand: pm === "pnpm" ? "pnpm astro build" : "npx astro build", configured: false, outputDir: "dist", @@ -96,7 +84,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); // Should select Astro since it's the only known framework expect(result.framework?.id).toBe("astro"); @@ -115,9 +103,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }); await expect( - details.getDetailsForAutoConfig() + details.getDetailsForAutoConfig({ context }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` + `[Error: The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` ); }); @@ -135,7 +123,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "packages/my-app/index.html": "

Hello World

", }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); expect(result.isWorkspaceRoot).toBe(true); expect(result.framework?.id).toBe("static"); @@ -152,7 +140,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "index.html": "

Hello World

", }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); expect(result.isWorkspaceRoot).toBe(false); }); @@ -170,7 +158,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "index.html": "

Hello World

", }); - await details.getDetailsForAutoConfig(); + await details.getDetailsForAutoConfig({ context }); expect(std.warn).toContain( "No lock file has been detected in the current working directory." @@ -193,7 +181,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({ + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ buildCommand: "npm run build", }); }); @@ -202,7 +192,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { expect, }) => { await expect( - details.getDetailsForAutoConfig() + details.getDetailsForAutoConfig({ context }) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Could not detect a directory containing static files (e.g. html, css and js) for the project]` ); @@ -213,7 +203,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) => { await writeFile("index.html", `

Hello World

`); - await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({ + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ outputDir: ".", }); }); @@ -226,7 +218,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "random/index.html": `

Hello World

`, }); - await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({ + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ outputDir: "public", }); }); @@ -239,7 +233,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "public/index.html": `

Hello World

`, }); - await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({ + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ outputDir: ".", }); }); @@ -268,6 +264,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { await expect( details.getDetailsForAutoConfig({ projectPath: `./${dirname}`, + context, }) ).resolves.toMatchObject({ workerName: expectedWorkerName, @@ -289,6 +286,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { await expect( details.getDetailsForAutoConfig({ projectPath: `./${dirname}`, + context, }) ).resolves.toMatchObject({ workerName: expectedWorkerName, @@ -307,6 +305,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { await expect( details.getDetailsForAutoConfig({ projectPath: `./my-project`, + context, }) ).resolves.toMatchObject({ workerName: "overridden-worker-name", @@ -326,6 +325,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { configPath: "/tmp/wrangler.toml", pages_build_output_dir: "./dist", } as Config, + context, }); expect(result.configured).toBe(false); @@ -339,25 +339,21 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { const cacheFolder = join(process.cwd(), ".cache"); await seed({ "public/index.html": `

Hello World

`, - // Create a cache folder in the temp directory and add pages.json to it - [join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME)]: JSON.stringify({ + [join(cacheFolder, "pages.json")]: JSON.stringify({ account_id: "test-account", }), }); - // Mock getCacheFolder to return our temp cache folder - const getCacheFolderSpy = vi - .spyOn(configCache, "getCacheFolder") - .mockReturnValue(cacheFolder); + const cacheContext = createMockContext({ + getCacheFolder: () => cacheFolder, + }); - try { - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ + context: cacheContext, + }); - expect(result.framework?.id).toBe("cloudflare-pages"); - expect(result.framework?.name).toBe("Cloudflare Pages"); - } finally { - getCacheFolderSpy.mockRestore(); - } + expect(result.framework?.id).toBe("cloudflare-pages"); + expect(result.framework?.name).toBe("Cloudflare Pages"); }); it("should detect Pages project when functions directory exists, no framework is detected and the user confirms that it is", async ({ @@ -372,18 +368,23 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { `, }); - mockConfirm({ - text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", - result: true, + const confirmContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ + context: confirmContext, + }); expect(result.framework?.id).toBe("cloudflare-pages"); expect(result.framework?.name).toBe("Cloudflare Pages"); }); - it("should not detect Pages project when the user denies that, even it the functions directory exists and no framework is detected", async ({ + it("should not detect Pages project when the user denies that, even if the functions directory exists and no framework is detected", async ({ expect, }) => { await seed({ @@ -395,12 +396,17 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { `, }); - mockConfirm({ - text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", - result: false, + const denyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ + context: denyContext, + }); expect(result.framework?.id).toBe("static"); expect(result.framework?.name).toBe("Static"); @@ -419,7 +425,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }), }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); // Should detect Astro, not Pages expect(result.framework?.id).toBe("astro"); @@ -428,10 +434,6 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { describe("multiple frameworks detected", () => { describe("local environment (non-CI)", () => { - beforeEach(() => { - isNonInteractiveOrCISpy.mockReturnValue(false); - }); - it("should return a single framework when multiple frameworks are detected", async ({ expect, }) => { @@ -445,7 +447,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); // Should return a framework (either astro or angular) expect(result.framework).toBeDefined(); @@ -454,8 +456,8 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }); describe("CI environment", () => { - beforeEach(() => { - isNonInteractiveOrCISpy.mockReturnValue(true); + const ciContext = createMockContext({ + isNonInteractiveOrCI: () => true, }); it("should throw MultipleFrameworksCIError when multiple known frameworks are detected in CI", async ({ @@ -471,8 +473,10 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - await expect(details.getDetailsForAutoConfig()).rejects.toThrowError( - /Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found/ + await expect( + details.getDetailsForAutoConfig({ context: ciContext }) + ).rejects.toThrowError( + /Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found/ ); }); @@ -489,7 +493,9 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ + context: ciContext, + }); expect(result.framework?.id).toBe("astro"); }); @@ -507,10 +513,10 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { ); await expect( - details.getDetailsForAutoConfig() + details.getDetailsForAutoConfig({ context: ciContext }) ).rejects.toThrowErrorMatchingInlineSnapshot( ` - [Error: Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: Gatsby, Gridsome. + [Error: Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: Gatsby, Gridsome. To fix this issue either: - check your project's configuration to make sure that the target framework @@ -536,7 +542,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { }) ); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); expect(result.framework?.id).toBe("next"); }); @@ -548,7 +554,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { "package.json": JSON.stringify({}), }); - const result = await details.getDetailsForAutoConfig(); + const result = await details.getDetailsForAutoConfig({ context }); expect(result.framework?.id).toBe("static"); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts b/packages/autoconfig/tests/frameworks/angular.test.ts similarity index 90% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts rename to packages/autoconfig/tests/frameworks/angular.test.ts index 1fd6ee4497..b5aa1cfda3 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/angular.test.ts +++ b/packages/autoconfig/tests/frameworks/angular.test.ts @@ -1,11 +1,14 @@ import { mkdir, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; -import { Angular } from "../../../autoconfig/frameworks/angular"; -import { NpmPackageManager } from "../../../package-manager"; -import type { AutoConfigFrameworkPackageInfo } from "../../../autoconfig/frameworks"; +import { Angular } from "../../src/frameworks/angular"; +import { createMockContext } from "../helpers/mock-context"; +import type { AutoConfigFrameworkPackageInfo } from "../../src/frameworks"; +const context = createMockContext(); + const BASE_OPTIONS = { projectPath: process.cwd(), workerName: "my-angular-app", @@ -13,6 +16,7 @@ const BASE_OPTIONS = { dryRun: false, packageManager: NpmPackageManager, isWorkspaceRoot: false, + context, }; const ANGULAR_PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { @@ -220,7 +224,11 @@ describe("Angular framework configure()", () => { it("returns SSR wranglerConfig without crashing", async ({ expect }) => { await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); const result = await framework.configure(BASE_OPTIONS); expect(result.wranglerConfig).toEqual({ @@ -238,7 +246,11 @@ describe("Angular framework configure()", () => { const { readFileSync } = await import("node:fs"); await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -257,7 +269,11 @@ describe("Angular framework configure()", () => { const { readFileSync } = await import("node:fs"); await mockAngularCoreVersion("22.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -305,7 +321,11 @@ describe("Angular framework configure()", () => { }) => { await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); const result = await framework.configure(BASE_OPTIONS); expect(result.wranglerConfig).toEqual({ @@ -320,7 +340,11 @@ describe("Angular framework configure()", () => { it("sets SSR configurationDescription", async ({ expect }) => { await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); expect(framework.configurationDescription).toBe( @@ -334,7 +358,11 @@ describe("Angular framework configure()", () => { const { readFileSync } = await import("node:fs"); await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -351,7 +379,11 @@ describe("Angular framework configure()", () => { const { readFileSync } = await import("node:fs"); await mockAngularCoreVersion("22.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); const angularJson = JSON.parse( @@ -368,7 +400,11 @@ describe("Angular framework configure()", () => { const { existsSync } = await import("node:fs"); await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); expect(existsSync(resolve("src/server.ts"))).toBe(true); @@ -377,7 +413,11 @@ describe("Angular framework configure()", () => { it("installs additional dependencies", async ({ expect }) => { await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); - framework.validateFrameworkVersion(process.cwd(), ANGULAR_PACKAGE_INFO); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); await framework.configure(BASE_OPTIONS); expect(installSpy).toHaveBeenCalledWith( diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts similarity index 100% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts rename to packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework-class.test.ts b/packages/autoconfig/tests/frameworks/get-framework-class.test.ts similarity index 77% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework-class.test.ts rename to packages/autoconfig/tests/frameworks/get-framework-class.test.ts index 87c9ff0dd8..773fdcc54c 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework-class.test.ts +++ b/packages/autoconfig/tests/frameworks/get-framework-class.test.ts @@ -1,8 +1,8 @@ import { describe, it } from "vitest"; -import { getFrameworkClassInstance } from "../../../autoconfig/frameworks"; -import { NextJs } from "../../../autoconfig/frameworks/next"; -import { NoOpFramework } from "../../../autoconfig/frameworks/no-op"; -import { Static } from "../../../autoconfig/frameworks/static"; +import { getFrameworkClassInstance } from "../../src/frameworks"; +import { NextJs } from "../../src/frameworks/next"; +import { NoOpFramework } from "../../src/frameworks/no-op"; +import { Static } from "../../src/frameworks/static"; describe("getFrameworkClassInstance()", () => { it("should return a Static framework when frameworkId is unknown", ({ diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/is-framework-supported.test.ts b/packages/autoconfig/tests/frameworks/is-framework-supported.test.ts similarity index 88% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/is-framework-supported.test.ts rename to packages/autoconfig/tests/frameworks/is-framework-supported.test.ts index 2c92aef751..6747cb99e0 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/is-framework-supported.test.ts +++ b/packages/autoconfig/tests/frameworks/is-framework-supported.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "vitest"; -import { isFrameworkSupported } from "../../../autoconfig/frameworks"; +import { isFrameworkSupported } from "../../src/frameworks"; describe("isFrameworkSupported()", () => { it("should return true for a supported framework id", ({ expect }) => { diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/is-known-framework.test.ts b/packages/autoconfig/tests/frameworks/is-known-framework.test.ts similarity index 90% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/is-known-framework.test.ts rename to packages/autoconfig/tests/frameworks/is-known-framework.test.ts index e2ffab4c40..97ffa2d739 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/is-known-framework.test.ts +++ b/packages/autoconfig/tests/frameworks/is-known-framework.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "vitest"; -import { isKnownFramework } from "../../../autoconfig/frameworks"; +import { isKnownFramework } from "../../src/frameworks"; describe("isKnownFramework()", () => { it("should return true for a known supported framework id", ({ expect }) => { diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts b/packages/autoconfig/tests/frameworks/react-router.test.ts similarity index 94% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts rename to packages/autoconfig/tests/frameworks/react-router.test.ts index cb8f11f440..587cf92dfb 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts +++ b/packages/autoconfig/tests/frameworks/react-router.test.ts @@ -2,32 +2,35 @@ import { existsSync, readFileSync } from "node:fs"; import { mkdir, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; import { hasV8MiddlewareFlag, ReactRouter, -} from "../../../autoconfig/frameworks/react-router"; -import * as packagesUtils from "../../../autoconfig/frameworks/utils/packages"; -import { NpmPackageManager } from "../../../package-manager"; +} from "../../src/frameworks/react-router"; +import * as packagesUtils from "../../src/frameworks/utils/packages"; +import { createMockContext } from "../helpers/mock-context"; function fixture(name: string): string { return readFileSync(join(__dirname, "fixtures/react-router", name), "utf-8"); } -vi.mock("../../../autoconfig/frameworks/utils/vite-config", () => ({ +vi.mock("../../src/frameworks/utils/vite-config", () => ({ transformViteConfig: vi.fn(), })); -vi.mock("../../../autoconfig/frameworks/utils/vite-plugin", () => ({ +vi.mock("../../src/frameworks/utils/vite-plugin", () => ({ installCloudflareVitePlugin: vi.fn(), })); -vi.mock("../../../autoconfig/frameworks/utils/packages", () => ({ +vi.mock("../../src/frameworks/utils/packages", () => ({ getInstalledPackageVersion: vi.fn(), isPackageInstalled: vi.fn(() => true), })); +const context = createMockContext(); + function getBaseOptions() { return { projectPath: process.cwd(), @@ -36,6 +39,7 @@ function getBaseOptions() { dryRun: false, packageManager: NpmPackageManager, isWorkspaceRoot: false, + context, }; } @@ -47,11 +51,15 @@ function createFramework(version: string): ReactRouter { name: "React Router", }); - framework.validateFrameworkVersion(".", { - name: "react-router", - minimumVersion: "7.0.0", - maximumKnownMajorVersion: "7", - }); + framework.validateFrameworkVersion( + ".", + { + name: "react-router", + minimumVersion: "7.0.0", + maximumKnownMajorVersion: "7", + }, + context + ); return framework; } diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts b/packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts similarity index 96% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts rename to packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts index 04f82eef27..225bbf66e7 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts +++ b/packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts @@ -1,10 +1,10 @@ import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; import { beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../../../autoconfig/frameworks/utils/packages"; -import { installCloudflareVitePlugin } from "../../../../autoconfig/frameworks/utils/vite-plugin"; +import { getInstalledPackageVersion } from "../../../src/frameworks/utils/packages"; +import { installCloudflareVitePlugin } from "../../../src/frameworks/utils/vite-plugin"; import type { MockInstance } from "vitest"; -vi.mock("../../../../autoconfig/frameworks/utils/packages", () => ({ +vi.mock("../../../src/frameworks/utils/packages", () => ({ getInstalledPackageVersion: vi.fn(), })); diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/validate-framework-version.test.ts b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts similarity index 77% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/validate-framework-version.test.ts rename to packages/autoconfig/tests/frameworks/validate-framework-version.test.ts index 51a2703d1a..440aec9d23 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/validate-framework-version.test.ts +++ b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts @@ -1,15 +1,16 @@ +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; import { describe, it, vi } from "vitest"; -import { AutoConfigFrameworkConfigurationError } from "../../../autoconfig/errors"; -import { Framework } from "../../../autoconfig/frameworks/framework-class"; -import { getInstalledPackageVersion } from "../../../autoconfig/frameworks/utils/packages"; -import { mockConsoleMethods } from "../../helpers/mock-console"; -import type { AutoConfigFrameworkPackageInfo } from "../../../autoconfig/frameworks"; +import { AutoConfigFrameworkConfigurationError } from "../../src/errors"; +import { Framework } from "../../src/frameworks/framework-class"; +import { getInstalledPackageVersion } from "../../src/frameworks/utils/packages"; +import { createMockContext } from "../helpers/mock-context"; +import type { AutoConfigFrameworkPackageInfo } from "../../src/frameworks"; import type { ConfigurationOptions, ConfigurationResults, -} from "../../../autoconfig/frameworks/framework-class"; +} from "../../src/frameworks/framework-class"; -vi.mock("../../../autoconfig/frameworks/utils/packages"); +vi.mock("../../src/frameworks/utils/packages"); /** Minimal concrete subclass so we can instantiate the abstract Framework */ class TestFramework extends Framework { @@ -26,6 +27,7 @@ const PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { describe("Framework.validateFrameworkVersion()", () => { const std = mockConsoleMethods(); + const context = createMockContext(); it("throws an AssertionError when the package version cannot be determined", ({ expect, @@ -34,7 +36,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).toThrow("Unable to detect the version of the `some-pkg` package"); }); @@ -45,11 +47,11 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).toThrow(AutoConfigFrameworkConfigurationError); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).toThrowErrorMatchingInlineSnapshot( `[Error: The version of Test used in the project ("1.0.0") cannot be automatically configured. Please update the Test version to at least "2.0.0" and try again.]` ); @@ -62,7 +64,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("2.0.0"); expect(std.warn).toBe(""); @@ -75,7 +77,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("3.0.0"); expect(std.warn).toBe(""); @@ -88,7 +90,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("4.0.0"); expect(std.warn).toBe(""); @@ -101,7 +103,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("4.5.0"); expect(std.warn).toBe(""); @@ -114,7 +116,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("4.3.2"); expect(std.warn).toBe(""); @@ -127,7 +129,7 @@ describe("Framework.validateFrameworkVersion()", () => { const framework = new TestFramework({ id: "test", name: "Test" }); expect(() => - framework.validateFrameworkVersion("/project", PACKAGE_INFO) + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) ).not.toThrow(); expect(framework.frameworkVersion).toBe("5.0.0"); expect(std.warn).toContain('"5.0.0"'); diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/vike.test.ts b/packages/autoconfig/tests/frameworks/vike.test.ts similarity index 96% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/vike.test.ts rename to packages/autoconfig/tests/frameworks/vike.test.ts index 18959ca138..2cdaf3f62c 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/vike.test.ts +++ b/packages/autoconfig/tests/frameworks/vike.test.ts @@ -1,18 +1,22 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; -import { Vike } from "../../../autoconfig/frameworks/vike"; -import { NpmPackageManager } from "../../../package-manager"; -vi.mock("../../../autoconfig/frameworks/utils/packages", () => ({ +import { Vike } from "../../src/frameworks/vike"; +import { createMockContext } from "../helpers/mock-context"; + +vi.mock("../../src/frameworks/utils/packages", () => ({ isPackageInstalled: () => false, })); -vi.mock("../../../autoconfig/frameworks/utils/vite-plugin", () => ({ +vi.mock("../../src/frameworks/utils/vite-plugin", () => ({ installCloudflareVitePlugin: async () => {}, })); +const context = createMockContext(); + function getBaseOptions() { return { projectPath: process.cwd(), @@ -21,6 +25,7 @@ function getBaseOptions() { dryRun: false, packageManager: NpmPackageManager, isWorkspaceRoot: false, + context, }; } diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/vite.test.ts b/packages/autoconfig/tests/frameworks/vite.test.ts similarity index 93% rename from packages/wrangler/src/__tests__/autoconfig/frameworks/vite.test.ts rename to packages/autoconfig/tests/frameworks/vite.test.ts index 739676d732..68757ca5ca 100644 --- a/packages/wrangler/src/__tests__/autoconfig/frameworks/vite.test.ts +++ b/packages/autoconfig/tests/frameworks/vite.test.ts @@ -1,10 +1,13 @@ import { existsSync, readFileSync } from "node:fs"; import { writeFile } from "node:fs/promises"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; -import { Vite } from "../../../autoconfig/frameworks/vite"; -import { NpmPackageManager } from "../../../package-manager"; +import { Vite } from "../../src/frameworks/vite"; +import { createMockContext } from "../helpers/mock-context"; + +const context = createMockContext(); const BASE_OPTIONS = { projectPath: ".", @@ -13,6 +16,7 @@ const BASE_OPTIONS = { dryRun: false, packageManager: NpmPackageManager, isWorkspaceRoot: false, + context, }; describe("Vite framework", () => { diff --git a/packages/wrangler/src/__tests__/autoconfig/get-installed-package-version.test.ts b/packages/autoconfig/tests/get-installed-package-version.test.ts similarity index 88% rename from packages/wrangler/src/__tests__/autoconfig/get-installed-package-version.test.ts rename to packages/autoconfig/tests/get-installed-package-version.test.ts index 26ecef9e23..e1e53390c8 100644 --- a/packages/wrangler/src/__tests__/autoconfig/get-installed-package-version.test.ts +++ b/packages/autoconfig/tests/get-installed-package-version.test.ts @@ -1,6 +1,7 @@ import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import { describe, test } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; +import { getInstalledPackageVersion } from "../src/frameworks/utils/packages"; + describe("getInstalledPackageVersion()", () => { runInTempDir(); test("happy path", async ({ expect }) => { diff --git a/packages/autoconfig/tests/helpers/mock-context.ts b/packages/autoconfig/tests/helpers/mock-context.ts new file mode 100644 index 0000000000..98d8d3fd74 --- /dev/null +++ b/packages/autoconfig/tests/helpers/mock-context.ts @@ -0,0 +1,34 @@ +import { vi } from "vitest"; +import type { AutoConfigContext } from "../../src/context"; + +/** + * Creates a mock `AutoConfigContext` suitable for testing. + * All dialog methods default to returning sensible values. + * The logger delegates to `console` so that `mockConsoleMethods()` captures + * the output and tests can assert on `std.out`, `std.warn`, etc. + * + * @param overrides - Partial overrides to customize the context. + * @returns A fully mocked `AutoConfigContext`. + */ +export function createMockContext( + overrides?: Partial +): AutoConfigContext { + return { + logger: { + log: vi.fn((...args: unknown[]) => console.log(...args)), + info: vi.fn((...args: unknown[]) => console.info(...args)), + warn: vi.fn((...args: unknown[]) => console.warn(...args)), + debug: vi.fn((...args: unknown[]) => console.debug(...args)), + error: vi.fn((...args: unknown[]) => console.error(...args)), + }, + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + runCommand: vi.fn(), + isNonInteractiveOrCI: () => false, + getCacheFolder: () => undefined, + ...overrides, + }; +} diff --git a/packages/wrangler/src/__tests__/autoconfig/run-summary.test.ts b/packages/autoconfig/tests/run-summary.test.ts similarity index 91% rename from packages/wrangler/src/__tests__/autoconfig/run-summary.test.ts rename to packages/autoconfig/tests/run-summary.test.ts index 20d76b2739..8df4ca3b2e 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run-summary.test.ts +++ b/packages/autoconfig/tests/run-summary.test.ts @@ -1,11 +1,11 @@ -import { beforeEach, describe, test } from "vitest"; -import { Astro } from "../../autoconfig/frameworks/astro"; -import { Static } from "../../autoconfig/frameworks/static"; -import { buildOperationsSummary } from "../../autoconfig/run"; -import { NpmPackageManager } from "../../package-manager"; -import { dedent } from "../../utils/dedent"; -import { mockConsoleMethods } from "../helpers/mock-console"; -import { useMockIsTTY } from "../helpers/mock-istty"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; +import { dedent } from "ts-dedent"; +import { describe, test } from "vitest"; +import { Astro } from "../src/frameworks/astro"; +import { Static } from "../src/frameworks/static"; +import { buildOperationsSummary } from "../src/run"; +import { createMockContext } from "./helpers/mock-context"; import type { RawConfig } from "@cloudflare/workers-utils"; const testRawConfig: RawConfig = { @@ -19,10 +19,7 @@ const testRawConfig: RawConfig = { describe("autoconfig run - buildOperationsSummary()", () => { const std = mockConsoleMethods(); - const { setIsTTY } = useMockIsTTY(); - beforeEach(() => { - setIsTTY(true); - }); + const context = createMockContext(); describe("interactive mode", () => { test("presents a summary for a simple project where only a wrangler.jsonc file needs to be created", async ({ @@ -42,7 +39,8 @@ describe("autoconfig run - buildOperationsSummary()", () => { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", - } + }, + context ); expect(std.out).toMatchInlineSnapshot(` @@ -101,7 +99,8 @@ describe("autoconfig run - buildOperationsSummary()", () => { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", - } + }, + context ); expect(std.out).toContain( @@ -158,7 +157,8 @@ describe("autoconfig run - buildOperationsSummary()", () => { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", - } + }, + context ); expect(std.out).toContain( @@ -208,7 +208,8 @@ describe("autoconfig run - buildOperationsSummary()", () => { { build: "npm run build", deploy: "npx wrangler deploy", - } + }, + context ); expect(std.out).toContain( @@ -238,7 +239,8 @@ describe("autoconfig run - buildOperationsSummary()", () => { { build: "npm run build", deploy: "npx wrangler deploy", - } + }, + context ); expect(std.out).not.toContain("🛠️ Configuring project for"); diff --git a/packages/autoconfig/tests/tsconfig.json b/packages/autoconfig/tests/tsconfig.json new file mode 100644 index 0000000000..ca97bed2d3 --- /dev/null +++ b/packages/autoconfig/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "preserve", + "types": ["node"] + }, + "include": ["../*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"] +} diff --git a/packages/wrangler/src/__tests__/autoconfig/vite-config.test.ts b/packages/autoconfig/tests/vite-config.test.ts similarity index 91% rename from packages/wrangler/src/__tests__/autoconfig/vite-config.test.ts rename to packages/autoconfig/tests/vite-config.test.ts index f237e33142..7fae4a87ba 100644 --- a/packages/wrangler/src/__tests__/autoconfig/vite-config.test.ts +++ b/packages/autoconfig/tests/vite-config.test.ts @@ -1,20 +1,20 @@ import { readFileSync } from "node:fs"; import { writeFile } from "node:fs/promises"; -import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; -import { beforeEach, describe, it } from "vitest"; +import { + mockConsoleMethods, + runInTempDir, +} from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; import { checkIfViteConfigUsesCloudflarePlugin, transformViteConfig, -} from "../../autoconfig/frameworks/utils/vite-config"; -import { logger } from "../../logger"; -import { mockConsoleMethods } from "../helpers/mock-console"; +} from "../src/frameworks/utils/vite-config"; +import { createMockContext } from "./helpers/mock-context"; + describe("vite-config utils", () => { runInTempDir(); const std = mockConsoleMethods(); - - beforeEach(() => { - logger.loggerLevel = "debug"; - }); + const context = createMockContext(); describe("checkIfViteConfigUsesCloudflarePlugin", () => { it("should detect cloudflare plugin in function-based defineConfig", async ({ @@ -32,7 +32,7 @@ export default defineConfig(() => ({ ` ); - const result = checkIfViteConfigUsesCloudflarePlugin("."); + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); expect(result).toBe(true); }); @@ -50,7 +50,7 @@ export default defineConfig(() => ({ ` ); - const result = checkIfViteConfigUsesCloudflarePlugin("."); + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); expect(result).toBe(false); }); @@ -68,7 +68,7 @@ export default defineConfig({ ` ); - const result = checkIfViteConfigUsesCloudflarePlugin("."); + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); expect(result).toBe(false); expect(std.debug).toContain( "Vite config does not have a valid plugins array" @@ -88,7 +88,7 @@ export default defineConfig({ ` ); - const result = checkIfViteConfigUsesCloudflarePlugin("."); + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); expect(result).toBe(true); }); }); diff --git a/packages/autoconfig/tsconfig.json b/packages/autoconfig/tsconfig.json new file mode 100644 index 0000000000..533933f8b7 --- /dev/null +++ b/packages/autoconfig/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["dist", "node_modules", "**/__tests__", "**/*.test.ts", "tests"] +} diff --git a/packages/autoconfig/tsup.config.ts b/packages/autoconfig/tsup.config.ts new file mode 100644 index 0000000000..9a4c974d0f --- /dev/null +++ b/packages/autoconfig/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; +import { EXTERNAL_DEPENDENCIES } from "./scripts/deps"; + +export default defineConfig(() => [ + { + treeshake: true, + keepNames: true, + entry: ["src/index.ts"], + platform: "node", + format: "esm", + dts: true, + outDir: "dist", + tsconfig: "tsconfig.json", + metafile: true, + sourcemap: process.env.SOURCEMAPS !== "false", + define: { + "process.env.NODE_ENV": `'${"production"}'`, + }, + external: EXTERNAL_DEPENDENCIES, + }, +]); diff --git a/packages/autoconfig/turbo.json b/packages/autoconfig/turbo.json new file mode 100644 index 0000000000..691f611507 --- /dev/null +++ b/packages/autoconfig/turbo.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "!**/tests/**"], + "outputs": ["dist/**"], + "env": ["SOURCEMAPS"], + "passThroughEnv": ["PWD"] + }, + "test:ci": { + "dependsOn": ["build"], + "env": ["LC_ALL", "TZ"] + } + } +} diff --git a/packages/autoconfig/vitest.config.mts b/packages/autoconfig/vitest.config.mts new file mode 100644 index 0000000000..407d414c58 --- /dev/null +++ b/packages/autoconfig/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 15_000, + pool: "forks", + include: ["**/tests/**/*.test.ts"], + reporters: ["default"], + unstubEnvs: true, + mockReset: true, + }, +}); diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index 77aa4c2832..fe5e0f24ac 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -8,6 +8,7 @@ import { resolveWorkerDefinition, } from "@cloudflare/config"; import { parseStaticRouting } from "@cloudflare/workers-shared/utils/configuration/parseStaticRouting"; +import { getWorkerNameFromProject } from "@cloudflare/workers-utils"; import { defu } from "defu"; import * as vite from "vite"; import * as wrangler from "wrangler"; @@ -384,9 +385,7 @@ function resolveWorkerConfig( workerConfig.compatibility_date ??= DEFAULT_COMPAT_DATE; if (isEntryWorker) { - workerConfig.name ??= wrangler.unstable_getWorkerNameFromProject( - options.root - ); + workerConfig.name ??= getWorkerNameFromProject(options.root); } // Auto-populate topLevelName from name workerConfig.topLevelName ??= workerConfig.name; diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index 31f21b6bd2..bb71e4942b 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -130,3 +130,18 @@ export { getHostFromUrl, getZoneFromRoute, } from "./route-utils"; + +export type { PackageManager } from "./package-manager"; +export { + NpmPackageManager, + PnpmPackageManager, + YarnPackageManager, + BunPackageManager, +} from "./package-manager"; + +export { + checkWorkerNameValidity, + toValidWorkerName, + getWorkerName, + getWorkerNameFromProject, +} from "./worker-name"; diff --git a/packages/workers-utils/src/package-manager.ts b/packages/workers-utils/src/package-manager.ts new file mode 100644 index 0000000000..b924b2c93f --- /dev/null +++ b/packages/workers-utils/src/package-manager.ts @@ -0,0 +1,54 @@ +/** + * Describes a supported package manager and its associated CLI commands + * and lock file conventions. + */ +export interface PackageManager { + /** The package manager identifier. */ + type: "npm" | "yarn" | "pnpm" | "bun"; + /** The command used to execute packages (e.g. `npx`, `pnpm`, `bunx`). */ + npx: string; + /** The command segments used to download and execute packages (e.g. `["npx"]`, `["pnpm", "dlx"]`). */ + dlx: string[]; + /** Lock file names produced by this package manager. */ + lockFiles: string[]; +} + +/** + * Manage packages using npm. + */ +export const NpmPackageManager = { + type: "npm", + npx: "npx", + dlx: ["npx"], + lockFiles: ["package-lock.json"], +} as const satisfies PackageManager; + +/** + * Manage packages using pnpm. + */ +export const PnpmPackageManager = { + type: "pnpm", + npx: "pnpm", + lockFiles: ["pnpm-lock.yaml"], + dlx: ["pnpm", "dlx"], +} as const satisfies PackageManager; + +/** + * Manage packages using yarn. + */ +export const YarnPackageManager = { + type: "yarn", + npx: "yarn", + dlx: ["yarn", "dlx"], + lockFiles: ["yarn.lock"], +} as const satisfies PackageManager; + +/** + * Manage packages using bun. + */ +export const BunPackageManager = { + type: "bun", + npx: "bunx", + dlx: ["bunx"], + lockFiles: ["bun.lockb", "bun.lock"], +} as const satisfies PackageManager; diff --git a/packages/workers-utils/src/worker-name.ts b/packages/workers-utils/src/worker-name.ts new file mode 100644 index 0000000000..2e9d8525a5 --- /dev/null +++ b/packages/workers-utils/src/worker-name.ts @@ -0,0 +1,143 @@ +import { basename, resolve } from "node:path"; +import { getCIOverrideName } from "./environment-variables/misc-variables"; +import { parsePackageJSON, readFileSync } from "./parse"; + +const invalidWorkerNameCharsRegex = /[^a-z0-9- ]/g; +const invalidWorkerNameStartEndRegex = /^(-+)|(-+)$/g; +const workerNameLengthLimit = 63; + +/** + * Checks whether the provided worker name is valid, this means that: + * - the name is not empty + * - the name doesn't start nor ends with a dash + * - the name doesn't contain special characters besides dashes + * - the name is not longer than 63 characters + * + * See: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations + * + * @param input The name to check + * @returns Object indicating whether the name is valid, and if not a cause indicating why it isn't + */ +export function checkWorkerNameValidity( + input: string +): { valid: false; cause: string } | { valid: true } { + if (!input) { + return { + valid: false, + cause: "Worker names cannot be empty.", + }; + } + + if (input.match(invalidWorkerNameStartEndRegex)) { + return { + valid: false, + cause: "Worker names cannot start or end with a dash.", + }; + } + + if (input.match(invalidWorkerNameCharsRegex)) { + return { + valid: false, + cause: + "Project names must only contain lowercase characters, numbers, and dashes.", + }; + } + + if (input.length > workerNameLengthLimit) { + return { + valid: false, + cause: "Project names must be less than 63 characters.", + }; + } + + return { valid: true }; +} + +/** + * Given an input string it converts it to a valid worker name. + * + * A worker name is valid if: + * - the name is not empty + * - the name doesn't start nor ends with a dash + * - the name doesn't contain special characters besides dashes + * - the name is not longer than 63 characters + * + * See: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations + * + * @param input The input to convert + * @returns The input itself if it was already valid, the input converted to a valid worker name otherwise + */ +export function toValidWorkerName(input: string): string { + if (checkWorkerNameValidity(input).valid) { + return input; + } + + input = input + // Replace all underscores with dashes + .replaceAll("_", "-") + // Replace all the special characters (besides dashes) with dashes + .replace(invalidWorkerNameCharsRegex, "-") + // Remove invalid start/end dashes + .replace(invalidWorkerNameStartEndRegex, "") + // If the name is longer than the limit let's truncate it to that + .slice(0, workerNameLengthLimit); + + if (!input.length) { + // If we've emptied the whole name let's replace it with a fallback value + return "my-worker"; + } + + return input; +} + +/** + * Derives a valid worker name from a project name (or worker name) and project path. + * + * The name is determined by (in order of precedence): + * 1. The WRANGLER_CI_OVERRIDE_NAME environment variable (for CI environments) + * 2. The provided project/worker name (if non-empty) + * 3. The directory basename of the project path + * + * The resulting name is sanitized to be a valid worker name. + * + * @param projectOrWorkerName An optional project or worker name to use + * @param projectPath The path to the project directory (used as fallback) + * @returns A valid worker name + */ +export function getWorkerName( + projectOrWorkerName = "", + projectPath: string +): string { + const rawName = + getCIOverrideName() ?? (projectOrWorkerName || basename(projectPath)); + + return toValidWorkerName(rawName); +} + +/** + * Derives a valid worker name from a project directory. + * + * The name is determined by (in order of precedence): + * 1. The WRANGLER_CI_OVERRIDE_NAME environment variable (for CI environments) + * 2. The `name` field from package.json in the project directory + * 3. The directory basename + * + * The resulting name is sanitized to be a valid worker name. + * + * @param projectPath The path to the project directory + * @returns A valid worker name + */ +export function getWorkerNameFromProject(projectPath: string): string { + const packageJsonPath = resolve(projectPath, "package.json"); + let packageJsonName: string | undefined; + + try { + const packageJson = parsePackageJSON( + readFileSync(packageJsonPath), + packageJsonPath + ); + packageJsonName = packageJson.name; + } catch {} + + return getWorkerName(packageJsonName, projectPath); +} diff --git a/packages/wrangler/src/__tests__/autoconfig/details/get-worker-name-from-project.test.ts b/packages/workers-utils/tests/worker-name.test.ts similarity index 91% rename from packages/wrangler/src/__tests__/autoconfig/details/get-worker-name-from-project.test.ts rename to packages/workers-utils/tests/worker-name.test.ts index 732e506fda..35d1350768 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/get-worker-name-from-project.test.ts +++ b/packages/workers-utils/tests/worker-name.test.ts @@ -1,8 +1,9 @@ import { randomUUID } from "node:crypto"; -import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import { afterEach, describe, it, vi } from "vitest"; -import { getWorkerNameFromProject } from "../../../autoconfig/details"; -describe("autoconfig details - getWorkerNameFromProject()", () => { +import { runInTempDir, seed } from "../src/test-helpers"; +import { getWorkerNameFromProject } from "../src/worker-name"; + +describe("getWorkerNameFromProject()", () => { runInTempDir(); afterEach(() => { diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index a8c80b311f..a738431eb7 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -89,6 +89,7 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", "@bomb.sh/tab": "^0.0.12", + "@cloudflare/autoconfig": "workspace:*", "@cloudflare/cli-shared-helpers": "workspace:*", "@cloudflare/codemod": "workspace:*", "@cloudflare/config": "workspace:*", diff --git a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts deleted file mode 100644 index 2e12e249ef..0000000000 --- a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { describe, test, vi } from "vitest"; -import { confirmAutoConfigDetails } from "../../../autoconfig/details"; -import { Astro } from "../../../autoconfig/frameworks/astro"; -import { Static } from "../../../autoconfig/frameworks/static"; -import { NpmPackageManager } from "../../../package-manager"; -import { - mockConfirm, - mockPrompt, - mockSelect, -} from "../../helpers/mock-dialogs"; -import { useMockIsTTY } from "../../helpers/mock-istty"; - -vi.mock("../../../package-manager", async (importOriginal) => ({ - ...(await importOriginal()), - getPackageManager() { - return { - type: "npm", - npx: "npx", - }; - }, -})); - -describe("autoconfig details - confirmAutoConfigDetails()", () => { - const { setIsTTY } = useMockIsTTY(); - - describe("interactive mode", () => { - test("no modifications applied", async ({ expect }) => { - setIsTTY(true); - - mockConfirm({ - text: "Do you want to modify these settings?", - result: false, - }); - const updatedAutoConfigDetails = await confirmAutoConfigDetails({ - workerName: "worker-name", - buildCommand: "npm run build", - projectPath: "", - configured: false, - framework: new Static({ id: "static", name: "Static" }), - outputDir: "./public", - packageManager: NpmPackageManager, - }); - - expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` - { - "buildCommand": "npm run build", - "configured": false, - "framework": Static { - "configurationDescription": undefined, - "id": "static", - "name": "Static", - }, - "outputDir": "./public", - "packageManager": { - "dlx": [ - "npx", - ], - "lockFiles": [ - "package-lock.json", - ], - "npx": "npx", - "type": "npm", - }, - "projectPath": "", - "workerName": "worker-name", - } - `); - }); - - test("settings can be updated in a plain static site without a framework nor a build script", async ({ - expect, - }) => { - setIsTTY(true); - - mockConfirm({ - text: "Do you want to modify these settings?", - result: true, - }); - mockPrompt({ - text: "What do you want to name your Worker?", - result: "new-name", - }); - mockSelect({ - text: "What framework is your application using?", - result: "static", - }); - mockPrompt({ - text: "What directory contains your applications' output/asset files?", - result: "./_public_", - }); - mockPrompt({ - text: "What is your application's build command?", - result: "npm run app:build", - }); - - const updatedAutoConfigDetails = await confirmAutoConfigDetails({ - workerName: "my-worker", - buildCommand: "npm run build", - outputDir: "", - projectPath: "", - configured: false, - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); - expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` - { - "buildCommand": "npm run app:build", - "configured": false, - "framework": Static { - "configurationDescription": undefined, - "id": "static", - "name": "Static", - }, - "outputDir": "./_public_", - "packageManager": { - "dlx": [ - "npx", - ], - "lockFiles": [ - "package-lock.json", - ], - "npx": "npx", - "type": "npm", - }, - "projectPath": "", - "workerName": "new-name", - } - `); - }); - - test("settings can be updated in a static app using a framework", async ({ - expect, - }) => { - setIsTTY(true); - - mockConfirm({ - text: "Do you want to modify these settings?", - result: true, - }); - mockPrompt({ - text: "What do you want to name your Worker?", - result: "my-astro-worker", - }); - mockSelect({ - text: "What framework is your application using?", - result: "astro", - }); - mockPrompt({ - text: "What directory contains your applications' output/asset files?", - result: "", - }); - mockPrompt({ - text: "What is your application's build command?", - result: "npm run build", - }); - - const updatedAutoConfigDetails = await confirmAutoConfigDetails({ - workerName: "my-astro-site", - buildCommand: "astro build", - framework: new Astro({ id: "astro", name: "Astro" }), - outputDir: "", - projectPath: "", - configured: false, - packageManager: NpmPackageManager, - }); - expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` - { - "buildCommand": "npm run build", - "configured": false, - "framework": Astro { - "configurationDescription": "Configuring project for Astro with "astro add cloudflare"", - "id": "astro", - "name": "Astro", - }, - "outputDir": "", - "packageManager": { - "dlx": [ - "npx", - ], - "lockFiles": [ - "package-lock.json", - ], - "npx": "npx", - "type": "npm", - }, - "projectPath": "", - "workerName": "my-astro-worker", - } - `); - }); - - test("framework can be changed from a detected framework to another", async ({ - expect, - }) => { - setIsTTY(true); - - mockConfirm({ - text: "Do you want to modify these settings?", - result: true, - }); - mockPrompt({ - text: "What do you want to name your Worker?", - result: "my-nuxt-worker", - }); - mockSelect({ - text: "What framework is your application using?", - result: "nuxt", - }); - mockPrompt({ - text: "What directory contains your applications' output/asset files?", - result: "./dist", - }); - mockPrompt({ - text: "What is your application's build command?", - result: "npm run build", - }); - - const updatedAutoConfigDetails = await confirmAutoConfigDetails({ - workerName: "my-astro-site", - buildCommand: "astro build", - framework: new Astro({ id: "astro", name: "Astro" }), - outputDir: "", - projectPath: "", - configured: false, - packageManager: NpmPackageManager, - }); - - expect(updatedAutoConfigDetails.framework?.id).toBe("nuxt"); - expect(updatedAutoConfigDetails.framework?.name).toBe("Nuxt"); - }); - }); - - describe("non-interactive mode", () => { - test("no modifications are applied in non-interactive", async ({ - expect, - }) => { - setIsTTY(false); - - const updatedAutoConfigDetails = await confirmAutoConfigDetails({ - workerName: "worker-name", - buildCommand: "npm run build", - projectPath: "", - configured: false, - framework: new Static({ id: "static", name: "Static" }), - outputDir: "./public", - packageManager: NpmPackageManager, - }); - - expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` - { - "buildCommand": "npm run build", - "configured": false, - "framework": Static { - "configurationDescription": undefined, - "id": "static", - "name": "Static", - }, - "outputDir": "./public", - "packageManager": { - "dlx": [ - "npx", - ], - "lockFiles": [ - "package-lock.json", - ], - "npx": "npx", - "type": "npm", - }, - "projectPath": "", - "workerName": "worker-name", - } - `); - }); - }); -}); diff --git a/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts deleted file mode 100644 index f1ef733110..0000000000 --- a/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, vi } from "vitest"; -import { displayAutoConfigDetails } from "../../../autoconfig/details"; -import { Static } from "../../../autoconfig/frameworks/static"; -import { NpmPackageManager } from "../../../package-manager"; -import { mockConsoleMethods } from "../../helpers/mock-console"; -import type { Framework } from "../../../autoconfig/frameworks"; - -vi.mock("../../../package-manager", async (importOriginal) => ({ - ...(await importOriginal()), - getPackageManager() { - return { - type: "npm", - npx: "npx", - }; - }, -})); - -describe("autoconfig details - displayAutoConfigDetails()", () => { - const std = mockConsoleMethods(); - - it("should cleanly handle a case in which only the worker name has been detected", ({ - expect, - }) => { - displayAutoConfigDetails({ - configured: false, - projectPath: process.cwd(), - workerName: "my-project", - framework: new Static({ id: "static", name: "Static" }), - outputDir: "./public", - packageManager: NpmPackageManager, - }); - expect(std.out).toMatchInlineSnapshot( - ` - " - Detected Project Settings: - - Worker Name: my-project - - Framework: Static - - Output Directory: ./public - " - ` - ); - }); - - it("should display all the project settings provided by the details object", ({ - expect, - }) => { - displayAutoConfigDetails({ - configured: false, - projectPath: process.cwd(), - workerName: "my-astro-app", - framework: { - name: "Astro", - id: "astro", - isConfigured: () => false, - configure: () => - ({ - wranglerConfig: {}, - }) satisfies ReturnType, - } as unknown as Framework, - buildCommand: "astro build", - outputDir: "dist", - packageManager: NpmPackageManager, - }); - expect(std.out).toMatchInlineSnapshot(` - " - Detected Project Settings: - - Worker Name: my-astro-app - - Framework: Astro - - Build Command: astro build - - Output Directory: dist - " - `); - }); - - it("should omit the framework and build command entries when they are not part of the details object", ({ - expect, - }) => { - displayAutoConfigDetails({ - configured: false, - projectPath: process.cwd(), - workerName: "my-site", - outputDir: "dist", - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); - expect(std.out).toMatchInlineSnapshot(` - " - Detected Project Settings: - - Worker Name: my-site - - Framework: Static - - Output Directory: dist - " - `); - }); -}); diff --git a/packages/wrangler/src/__tests__/autoconfig/index.test.ts b/packages/wrangler/src/__tests__/autoconfig/index.test.ts new file mode 100644 index 0000000000..36fe375305 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/index.test.ts @@ -0,0 +1,268 @@ +import { + AutoConfigDetectionError, + getDetailsForAutoConfig, + runAutoConfig, +} from "@cloudflare/autoconfig"; +import { afterEach, beforeEach, describe, it, vi } from "vitest"; +import { + runAutoConfigDetection, + runAutoConfigLogic, + sendAutoConfigProcessStartedMetricsEvent, +} from "../../autoconfig"; +import { sendMetricsEvent } from "../../metrics/send-event"; +import type * as SendEventModule from "../../metrics/send-event"; +import type { + AutoConfigContext, + AutoConfigDetails, + AutoConfigSummary, +} from "@cloudflare/autoconfig"; +import type { Config } from "@cloudflare/workers-utils"; + +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + getDetailsForAutoConfig: vi.fn(), + runAutoConfig: vi.fn(), +})); + +vi.mock("../../metrics/send-event", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + sendMetricsEvent: vi.fn(), + }; +}); + +/** Minimal mock satisfying {@link AutoConfigContext} for pass-through testing. */ +const mockContext = {} as AutoConfigContext; + +/** Minimal mock satisfying {@link Config} for pass-through testing. */ +const mockConfig = {} as Config; + +/** Minimal mock satisfying {@link AutoConfigDetails} with a detected framework. */ +const mockDetails = { + configured: false, + framework: { id: "static" }, +} as unknown as AutoConfigDetails; + +/** Minimal mock satisfying {@link AutoConfigSummary}. */ +const mockSummary = { + scripts: {}, + wranglerInstall: false, + outputDir: "dist", +} as unknown as AutoConfigSummary; + +describe("autoconfig wrappers", () => { + beforeEach(() => { + // Initialize the module-level autoConfigId state that both wrappers rely on. + // This mirrors what callers do before invoking detection/configuration. + sendAutoConfigProcessStartedMetricsEvent({ + command: "wrangler deploy", + dryRun: false, + }); + + // Clear the sendMetricsEvent mock so the process_started call above + // doesn't pollute per-test assertions. + vi.mocked(sendMetricsEvent).mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("runAutoConfigDetection", () => { + it("calls getDetailsForAutoConfig with the provided config and context, and returns the result", async ({ + expect, + }) => { + vi.mocked(getDetailsForAutoConfig).mockResolvedValue(mockDetails); + + const result = await runAutoConfigDetection({ + command: "wrangler deploy", + wranglerConfig: mockConfig, + context: mockContext, + }); + + expect(getDetailsForAutoConfig).toHaveBeenCalledOnce(); + expect(getDetailsForAutoConfig).toHaveBeenCalledWith({ + wranglerConfig: mockConfig, + context: mockContext, + }); + expect(result).toBe(mockDetails); + }); + + it("sends detection_started then detection_completed on success", async ({ + expect, + }) => { + vi.mocked(getDetailsForAutoConfig).mockResolvedValue(mockDetails); + + await runAutoConfigDetection({ + command: "wrangler deploy", + wranglerConfig: mockConfig, + context: mockContext, + }); + + expect(sendMetricsEvent).toHaveBeenCalledTimes(2); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 1, + "autoconfig_detection_started", + expect.objectContaining({ + autoConfigId: expect.any(String), + command: "wrangler deploy", + }), + {} + ); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 2, + "autoconfig_detection_completed", + expect.objectContaining({ + autoConfigId: expect.any(String), + framework: "static", + configured: false, + success: true, + }), + {} + ); + }); + + it("sends detection_completed with error info on failure and re-throws the original error", async ({ + expect, + }) => { + const error = new Error("detection boom"); + vi.mocked(getDetailsForAutoConfig).mockRejectedValue(error); + + await expect( + runAutoConfigDetection({ + command: "wrangler setup", + wranglerConfig: mockConfig, + context: mockContext, + }) + ).rejects.toBe(error); + + expect(getDetailsForAutoConfig).toHaveBeenCalledOnce(); + expect(sendMetricsEvent).toHaveBeenCalledTimes(2); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 2, + "autoconfig_detection_completed", + expect.objectContaining({ + autoConfigId: expect.any(String), + framework: undefined, + configured: false, + success: false, + }), + {} + ); + }); + + it("extracts frameworkId and configured from AutoConfigDetectionError", async ({ + expect, + }) => { + const detectionError = new AutoConfigDetectionError("detection failed", { + telemetryMessage: "detection failed", + configured: true, + frameworkId: "astro", + }); + vi.mocked(getDetailsForAutoConfig).mockRejectedValue(detectionError); + + await expect( + runAutoConfigDetection({ + command: "wrangler deploy", + wranglerConfig: mockConfig, + context: mockContext, + }) + ).rejects.toBe(detectionError); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 2, + "autoconfig_detection_completed", + expect.objectContaining({ + framework: "astro", + configured: true, + success: false, + }), + {} + ); + }); + }); + + describe("runAutoConfigLogic", () => { + it("calls runAutoConfig with the provided details and options, and returns the result", async ({ + expect, + }) => { + vi.mocked(runAutoConfig).mockResolvedValue(mockSummary); + + const options = { context: mockContext, dryRun: false }; + const result = await runAutoConfigLogic(mockDetails, options); + + expect(runAutoConfig).toHaveBeenCalledOnce(); + expect(runAutoConfig).toHaveBeenCalledWith(mockDetails, options); + expect(result).toBe(mockSummary); + }); + + it("sends configuration_started then configuration_completed on success", async ({ + expect, + }) => { + vi.mocked(runAutoConfig).mockResolvedValue(mockSummary); + + await runAutoConfigLogic(mockDetails, { + context: mockContext, + dryRun: true, + }); + + expect(sendMetricsEvent).toHaveBeenCalledTimes(2); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 1, + "autoconfig_configuration_started", + expect.objectContaining({ + autoConfigId: expect.any(String), + framework: "static", + dryRun: true, + }), + {} + ); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 2, + "autoconfig_configuration_completed", + expect.objectContaining({ + autoConfigId: expect.any(String), + framework: "static", + dryRun: true, + success: true, + }), + {} + ); + }); + + it("sends configuration_completed with error info on failure and re-throws the original error", async ({ + expect, + }) => { + const error = new Error("configuration boom"); + vi.mocked(runAutoConfig).mockRejectedValue(error); + + await expect( + runAutoConfigLogic(mockDetails, { + context: mockContext, + dryRun: false, + }) + ).rejects.toBe(error); + + expect(runAutoConfig).toHaveBeenCalledOnce(); + expect(sendMetricsEvent).toHaveBeenCalledTimes(2); + + expect(sendMetricsEvent).toHaveBeenNthCalledWith( + 2, + "autoconfig_configuration_completed", + expect.objectContaining({ + autoConfigId: expect.any(String), + framework: "static", + dryRun: false, + success: false, + }), + {} + ); + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/autoconfig/run.test.ts b/packages/wrangler/src/__tests__/autoconfig/run.test.ts index 369a4aeeeb..9cc6446c85 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/run.test.ts @@ -1,24 +1,22 @@ import { existsSync } from "node:fs"; import { mkdir, writeFile } from "node:fs/promises"; +import * as autoconfig from "@cloudflare/autoconfig"; +import { Framework, getInstalledPackageVersion } from "@cloudflare/autoconfig"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; import { FatalError, readFileSync, getTodaysCompatDate, } from "@cloudflare/workers-utils"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; import { runInTempDir, writeWranglerConfig, } from "@cloudflare/workers-utils/test-helpers"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import * as details from "../../autoconfig/details"; -import { Astro } from "../../autoconfig/frameworks/astro"; -import { Static } from "../../autoconfig/frameworks/static"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; -import * as run from "../../autoconfig/run"; +import { createWranglerAutoConfigContext } from "../../autoconfig-context"; import * as format from "../../deployment-bundle/guess-worker-format"; import { clearOutputFilePath } from "../../output"; -import { NpmPackageManager } from "../../package-manager"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; import { @@ -30,10 +28,40 @@ import { import { useMockIsTTY } from "../helpers/mock-istty"; import { runWrangler } from "../helpers/run-wrangler"; import { writeWorkerSource } from "../helpers/write-worker-source"; -import type { Framework } from "../../autoconfig/frameworks"; +import type { AutoConfigContext } from "@cloudflare/autoconfig"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "@cloudflare/autoconfig"; import type { ExpectStatic } from "vitest"; import type { MockInstance } from "vitest"; +/** + * Minimal Framework subclass that mirrors `Static` from `@cloudflare/autoconfig`. + * Used in tests that exercise the overall `runAutoConfig` flow without needing the + * real internal class (which the package intentionally does not export). + */ +class MockStaticFramework extends Framework { + configure({ outputDir }: ConfigurationOptions): ConfigurationResults { + return { + wranglerConfig: { + assets: { directory: outputDir }, + }, + }; + } +} + +/** + * Minimal Framework subclass that stands in for `Astro` from `@cloudflare/autoconfig`. + * The real class is not exported; the test immediately spies on both `configure` and + * `validateFrameworkVersion`, so this stub just needs to satisfy the abstract contract. + */ +class MockAstroFramework extends Framework { + async configure(): Promise { + return { wranglerConfig: { assets: { directory: "dist" } } }; + } +} + vi.mock("../../package-manager", () => ({ getPackageManager() { return { @@ -49,7 +77,10 @@ vi.mock("../../package-manager", () => ({ }, })); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("../deploy/deploy", async (importOriginal) => ({ ...(await importOriginal()), @@ -97,8 +128,8 @@ describe("autoconfig (deploy)", () => { }) => { writeWorkerSource(); writeWranglerConfig({ main: "index.js" }); - const getDetailsSpy = vi.spyOn(details, "getDetailsForAutoConfig"); - const runSpy = vi.spyOn(run, "runAutoConfig"); + const getDetailsSpy = vi.spyOn(autoconfig, "getDetailsForAutoConfig"); + const runSpy = vi.spyOn(autoconfig, "runAutoConfig"); await runDeploy(expect, `--no-autoconfig`); @@ -111,8 +142,8 @@ describe("autoconfig (deploy)", () => { }) => { writeWorkerSource(); writeWranglerConfig({ main: "index.js" }); - const getDetailsSpy = vi.spyOn(details, "getDetailsForAutoConfig"); - const runSpy = vi.spyOn(run, "runAutoConfig"); + const getDetailsSpy = vi.spyOn(autoconfig, "getDetailsForAutoConfig"); + const runSpy = vi.spyOn(autoconfig, "runAutoConfig"); await runDeploy(expect, `--autoconfig=false`); @@ -121,7 +152,7 @@ describe("autoconfig (deploy)", () => { }); it("should check for autoconfig with flag", async ({ expect }) => { - const getDetailsSpy = vi.spyOn(details, "getDetailsForAutoConfig"); + const getDetailsSpy = vi.spyOn(autoconfig, "getDetailsForAutoConfig"); await runDeploy(expect, "--autoconfig"); @@ -132,18 +163,18 @@ describe("autoconfig (deploy)", () => { expect, }) => { const getDetailsSpy = vi - .spyOn(details, "getDetailsForAutoConfig") + .spyOn(autoconfig, "getDetailsForAutoConfig") .mockImplementationOnce(() => Promise.resolve({ configured: false, projectPath: process.cwd(), workerName: "my-worker", - framework: new Static({ id: "static", name: "Static" }), + framework: new MockStaticFramework({ id: "static", name: "Static" }), outputDir: "./public", packageManager: NpmPackageManager, }) ); - const runSpy = vi.spyOn(run, "runAutoConfig"); + const runSpy = vi.spyOn(autoconfig, "runAutoConfig"); await runDeploy(expect, "--autoconfig"); @@ -155,7 +186,7 @@ describe("autoconfig (deploy)", () => { expect, }) => { const getDetailsSpy = vi - .spyOn(details, "getDetailsForAutoConfig") + .spyOn(autoconfig, "getDetailsForAutoConfig") .mockImplementationOnce(() => Promise.resolve({ configured: true, @@ -164,7 +195,7 @@ describe("autoconfig (deploy)", () => { packageManager: NpmPackageManager, }) ); - const runSpy = vi.spyOn(run, "runAutoConfig"); + const runSpy = vi.spyOn(autoconfig, "runAutoConfig"); await runDeploy(expect, "--autoconfig"); @@ -175,7 +206,7 @@ describe("autoconfig (deploy)", () => { it("should warn and prompt when Pages project is detected", async ({ expect, }) => { - vi.spyOn(details, "getDetailsForAutoConfig").mockImplementationOnce(() => + vi.spyOn(autoconfig, "getDetailsForAutoConfig").mockImplementationOnce(() => Promise.resolve({ configured: false, projectPath: process.cwd(), @@ -190,7 +221,7 @@ describe("autoconfig (deploy)", () => { packageManager: NpmPackageManager, }) ); - const runSpy = vi.spyOn(run, "runAutoConfig"); + const runSpy = vi.spyOn(autoconfig, "runAutoConfig"); // User declines to proceed mockConfirm({ @@ -212,10 +243,12 @@ describe("autoconfig (deploy)", () => { describe("runAutoConfig()", () => { let installSpy: MockInstance; + let context: AutoConfigContext; beforeEach(() => { installSpy = vi .spyOn(cliPackages, "installWrangler") .mockImplementation(async () => {}); + context = createWranglerAutoConfigContext(); }); it("happy path", async ({ expect }) => { @@ -242,7 +275,7 @@ describe("autoconfig (deploy)", () => { }, }) satisfies ReturnType ); - await run.runAutoConfig( + await autoconfig.runAutoConfig( { projectPath: process.cwd(), buildCommand: "echo 'built' > build.txt", @@ -265,7 +298,7 @@ describe("autoconfig (deploy)", () => { }, packageManager: NpmPackageManager, }, - { enableWranglerInstallation: true } + { context, enableWranglerInstallation: false } ); expect(std.out.replaceAll(getTodaysCompatDate(), "")) @@ -341,8 +374,10 @@ describe("autoconfig (deploy)", () => { " `); - // Wrangler should have been installed - expect(installSpy).toHaveBeenCalled(); + // Wrangler installation was disabled (enableWranglerInstallation: false) to avoid + // running the real installer in tests. The "📦 Install packages:" output in the + // snapshot above confirms the intent is recorded in the autoconfig summary. + expect(installSpy).not.toHaveBeenCalled(); // The framework's configuration command should have been run expect(configureSpy).toHaveBeenCalled(); @@ -369,14 +404,17 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: new MockStaticFramework({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + { context } + ); expect(readFileSync(".gitignore")).toMatchInlineSnapshot(` "# wrangler files @@ -403,14 +441,17 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: new MockStaticFramework({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + { context } + ); // When gitignore pre-existed with trailing newline, one empty line is added as separator expect(readFileSync(".gitignore")).toMatchInlineSnapshot(` @@ -449,14 +490,17 @@ describe("autoconfig (deploy)", () => { text: "Proceed with setup?", result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - configured: false, - framework: new Static({ id: "static", name: "Static" }), - workerName: "my-worker", - outputDir: "dist", - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + configured: false, + framework: new MockStaticFramework({ id: "static", name: "Static" }), + workerName: "my-worker", + outputDir: "dist", + packageManager: NpmPackageManager, + }, + { context } + ); expect(std.out.replaceAll(getTodaysCompatDate(), "")) .toMatchInlineSnapshot(` @@ -527,14 +571,17 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: process.cwd(), - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: process.cwd(), + framework: new MockStaticFramework({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + { context } + ); expect(readFileSync(".assetsignore")).toMatchInlineSnapshot(` "# wrangler files @@ -560,14 +607,17 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: process.cwd(), - framework: new Static({ id: "static", name: "Static" }), - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: process.cwd(), + framework: new MockStaticFramework({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + { context } + ); expect(readFileSync(".assetsignore")).toMatchInlineSnapshot(` "*.bak @@ -591,14 +641,20 @@ describe("autoconfig (deploy)", () => { }); await expect( - run.runAutoConfig({ - projectPath: process.cwd(), - configured: false, - framework: new Static({ id: "static", name: "Static" }), - workerName: "my-worker", - outputDir: "", - packageManager: NpmPackageManager, - }) + autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + configured: false, + framework: new MockStaticFramework({ + id: "static", + name: "Static", + }), + workerName: "my-worker", + outputDir: "", + packageManager: NpmPackageManager, + }, + { context } + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[AssertionError: The Output Directory is unexpectedly missing]` ); @@ -613,19 +669,22 @@ describe("autoconfig (deploy)", () => { }); await expect( - run.runAutoConfig({ - projectPath: process.cwd(), - configured: false, - framework: { - id: "cloudflare-pages", - name: "Cloudflare Pages", - configure: async () => ({ wranglerConfig: {} }), - isConfigured: () => false, - } as unknown as Framework, - workerName: "my-worker", - outputDir: "dist", - packageManager: NpmPackageManager, - }) + autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + configured: false, + framework: { + id: "cloudflare-pages", + name: "Cloudflare Pages", + configure: async () => ({ wranglerConfig: {} }), + isConfigured: () => false, + } as unknown as Framework, + workerName: "my-worker", + outputDir: "dist", + packageManager: NpmPackageManager, + }, + { context } + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to Workers is not yet supported.]` ); @@ -640,19 +699,22 @@ describe("autoconfig (deploy)", () => { }); await expect( - run.runAutoConfig({ - projectPath: process.cwd(), - configured: false, - framework: { - id: "hono", - name: "Hono", - configure: async () => ({ wranglerConfig: {} }), - isConfigured: () => false, - } as unknown as Framework, - workerName: "my-worker", - outputDir: "dist", - packageManager: NpmPackageManager, - }) + autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + configured: false, + framework: { + id: "hono", + name: "Hono", + configure: async () => ({ wranglerConfig: {} }), + isConfigured: () => false, + } as unknown as Framework, + workerName: "my-worker", + outputDir: "dist", + packageManager: NpmPackageManager, + }, + { context } + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: The detected framework ("Hono") cannot be automatically configured.]` ); @@ -671,27 +733,30 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: { - // "static" is used here because this test only exercises compatibility flag - // merging behaviour. Note: Using "static" avoids the getFrameworkPackageInfo assert - // for unknown framework ids while keeping the test focused on its intent. - id: "static", - name: "Static", - configure: async () => ({ - wranglerConfig: { - // No compatibility_flags specified - assets: { directory: "dist" }, - }, - }), - isConfigured: () => false, - } as unknown as Framework, - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: { + // "static" is used here because this test only exercises compatibility flag + // merging behaviour. Note: Using "static" avoids the getFrameworkPackageInfo assert + // for unknown framework ids while keeping the test focused on its intent. + id: "static", + name: "Static", + configure: async () => ({ + wranglerConfig: { + // No compatibility_flags specified + assets: { directory: "dist" }, + }, + }), + isConfigured: () => false, + } as unknown as Framework, + packageManager: NpmPackageManager, + }, + { context } + ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); expect(wranglerConfig.compatibility_flags).toEqual(["nodejs_compat"]); @@ -709,27 +774,30 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: { - // "static" is used here because this test only exercises compatibility flag - // merging behaviour. Using "static" avoids the getFrameworkPackageInfo assert - // for unknown framework ids while keeping the test focused on its intent. - id: "static", - name: "Static", - configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["global_fetch_strictly_public"], - assets: { directory: "dist" }, - }, - }), - isConfigured: () => false, - } as unknown as Framework, - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: { + // "static" is used here because this test only exercises compatibility flag + // merging behaviour. Using "static" avoids the getFrameworkPackageInfo assert + // for unknown framework ids while keeping the test focused on its intent. + id: "static", + name: "Static", + configure: async () => ({ + wranglerConfig: { + compatibility_flags: ["global_fetch_strictly_public"], + assets: { directory: "dist" }, + }, + }), + isConfigured: () => false, + } as unknown as Framework, + packageManager: NpmPackageManager, + }, + { context } + ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); expect(wranglerConfig.compatibility_flags).toEqual([ @@ -750,27 +818,30 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: { - // "static" is used here because this test only exercises compatibility flag - // merging behaviour. Using "static" avoids the getFrameworkPackageInfo assert - // for unknown framework ids while keeping the test focused on its intent. - id: "static", - name: "Static", - configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["nodejs_compat"], - assets: { directory: "dist" }, - }, - }), - isConfigured: () => false, - } as unknown as Framework, - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: { + // "static" is used here because this test only exercises compatibility flag + // merging behaviour. Using "static" avoids the getFrameworkPackageInfo assert + // for unknown framework ids while keeping the test focused on its intent. + id: "static", + name: "Static", + configure: async () => ({ + wranglerConfig: { + compatibility_flags: ["nodejs_compat"], + assets: { directory: "dist" }, + }, + }), + isConfigured: () => false, + } as unknown as Framework, + packageManager: NpmPackageManager, + }, + { context } + ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); expect(wranglerConfig.compatibility_flags).toEqual(["nodejs_compat"]); @@ -786,24 +857,27 @@ describe("autoconfig (deploy)", () => { result: true, }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework: { - id: "static", - name: "Nodejs Als Framework", - configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["nodejs_als", "some_other_flag"], - assets: { directory: "dist" }, - }, - }), - isConfigured: () => false, - } as unknown as Framework, - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework: { + id: "static", + name: "Nodejs Als Framework", + configure: async () => ({ + wranglerConfig: { + compatibility_flags: ["nodejs_als", "some_other_flag"], + assets: { directory: "dist" }, + }, + }), + isConfigured: () => false, + } as unknown as Framework, + packageManager: NpmPackageManager, + }, + { context } + ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); // nodejs_als should be removed, nodejs_compat should be added, some_other_flag preserved @@ -831,7 +905,7 @@ describe("autoconfig (deploy)", () => { // validateFrameworkVersion does not throw vi.mocked(getInstalledPackageVersion).mockReturnValue("5.0.0"); - const framework = new Astro({ id: "astro", name: "Astro" }); + const framework = new MockAstroFramework({ id: "astro", name: "Astro" }); const callOrder: string[] = []; vi.spyOn(framework, "validateFrameworkVersion").mockImplementation(() => { @@ -842,14 +916,17 @@ describe("autoconfig (deploy)", () => { return { wranglerConfig: { assets: { directory: "dist" } } }; }); - await run.runAutoConfig({ - projectPath: process.cwd(), - workerName: "my-worker", - configured: false, - outputDir: "dist", - framework, - packageManager: NpmPackageManager, - }); + await autoconfig.runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: "dist", + framework, + packageManager: NpmPackageManager, + }, + { context } + ); // configure is called twice: once as a dry-run (to build the summary) and // once for real. validateFrameworkVersion must precede both. diff --git a/packages/wrangler/src/__tests__/deploy/assets.test.ts b/packages/wrangler/src/__tests__/deploy/assets.test.ts index 529988da52..af840533b4 100644 --- a/packages/wrangler/src/__tests__/deploy/assets.test.ts +++ b/packages/wrangler/src/__tests__/deploy/assets.test.ts @@ -1,4 +1,5 @@ import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { runInTempDir, writeWranglerConfig, @@ -6,7 +7,6 @@ import { import { http, HttpResponse } from "msw"; import dedent from "ts-dedent"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -51,8 +51,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { diff --git a/packages/wrangler/src/__tests__/deploy/bindings.test.ts b/packages/wrangler/src/__tests__/deploy/bindings.test.ts index 60040caec2..8f9bb65875 100644 --- a/packages/wrangler/src/__tests__/deploy/bindings.test.ts +++ b/packages/wrangler/src/__tests__/deploy/bindings.test.ts @@ -1,6 +1,7 @@ import { Buffer } from "node:buffer"; import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { runInTempDir, writeWranglerConfig, @@ -9,7 +10,6 @@ import { sync } from "command-exists"; import { http, HttpResponse } from "msw"; import * as TOML from "smol-toml"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -51,8 +51,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { diff --git a/packages/wrangler/src/__tests__/deploy/build.test.ts b/packages/wrangler/src/__tests__/deploy/build.test.ts index 58e885f26f..72dbdaca72 100644 --- a/packages/wrangler/src/__tests__/deploy/build.test.ts +++ b/packages/wrangler/src/__tests__/deploy/build.test.ts @@ -1,6 +1,7 @@ import { Buffer } from "node:buffer"; import { randomFillSync } from "node:crypto"; import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { ParseError } from "@cloudflare/workers-utils"; import { normalizeString, @@ -10,7 +11,6 @@ import { import * as esbuild from "esbuild"; import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, it, test, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { printBundleSize } from "../../deployment-bundle/bundle-reporter"; import { clearOutputFilePath } from "../../output"; import { diagnoseScriptSizeError } from "../../utils/friendly-validator-errors"; @@ -52,8 +52,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { diff --git a/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts b/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts index 854e2068d0..64103bae21 100644 --- a/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts +++ b/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts @@ -64,8 +64,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }; }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); // ─── Shared helpers ────────────────────────────────────────────────── diff --git a/packages/wrangler/src/__tests__/deploy/config-remote.test.ts b/packages/wrangler/src/__tests__/deploy/config-remote.test.ts index f4d053b172..a3ff7f63e1 100644 --- a/packages/wrangler/src/__tests__/deploy/config-remote.test.ts +++ b/packages/wrangler/src/__tests__/deploy/config-remote.test.ts @@ -1,4 +1,5 @@ import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { normalizeString, runInTempDir, @@ -6,7 +7,6 @@ import { } from "@cloudflare/workers-utils/test-helpers"; import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -59,7 +59,10 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index 589cd3d661..a9167c2489 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -1,6 +1,10 @@ import * as fs from "node:fs"; import { readFile } from "node:fs/promises"; import * as path from "node:path"; +import { + getInstalledPackageVersion, + runAutoConfig, +} from "@cloudflare/autoconfig"; import { TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, @@ -15,9 +19,6 @@ import { http, HttpResponse } from "msw"; import * as TOML from "smol-toml"; import dedent from "ts-dedent"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { Static } from "../../autoconfig/frameworks/static"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; -import { runAutoConfig } from "../../autoconfig/run"; import { clearOutputFilePath } from "../../output"; import { NpmPackageManager } from "../../package-manager"; import { writeAuthConfigFile } from "../../user"; @@ -55,8 +56,8 @@ import { mockPublishRoutesRequest, mockServiceScriptData, } from "./helpers"; -import type { Framework } from "../../autoconfig/frameworks"; import type { OutputEntry } from "../../output"; +import type { Framework } from "@cloudflare/autoconfig"; vi.mock("command-exists"); vi.mock("../../check/commands", async (importOriginal) => { @@ -79,8 +80,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { @@ -610,10 +614,7 @@ describe("deploy", () => { }); const getDetailsForAutoConfigSpy = vi - .spyOn( - await import("../../autoconfig/details"), - "getDetailsForAutoConfig" - ) + .spyOn(await import("@cloudflare/autoconfig"), "getDetailsForAutoConfig") .mockResolvedValueOnce({ configured: false, projectPath: process.cwd(), @@ -653,10 +654,7 @@ describe("deploy", () => { }); const getDetailsForAutoConfigSpy = vi - .spyOn( - await import("../../autoconfig/details"), - "getDetailsForAutoConfig" - ) + .spyOn(await import("@cloudflare/autoconfig"), "getDetailsForAutoConfig") .mockResolvedValueOnce({ configured: false, projectPath: process.cwd(), @@ -689,7 +687,7 @@ describe("deploy", () => { expect, }) => { const getDetailsForAutoConfigSpy = vi.spyOn( - await import("../../autoconfig/details"), + await import("@cloudflare/autoconfig"), "getDetailsForAutoConfig" ); @@ -1998,11 +1996,11 @@ describe("deploy", () => { const outputFile = "./output.json"; vi.spyOn( - await import("../../autoconfig/details"), + await import("@cloudflare/autoconfig"), "getDetailsForAutoConfig" ).mockResolvedValueOnce({ configured: false, - framework: new Static({ id: "static", name: "Static" }), + framework: { id: "static", name: "Static" } as Framework, workerName: "my-site", projectPath: ".", outputDir: "./public", diff --git a/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts b/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts index 1b4b21f214..2f8e8e76d2 100644 --- a/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts +++ b/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts @@ -1,11 +1,11 @@ import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { runInTempDir, writeWranglerConfig, } from "@cloudflare/workers-utils/test-helpers"; import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -43,8 +43,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy: interactive deploy config prompts", () => { diff --git a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts index 651a091b14..e6428dc489 100644 --- a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts +++ b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts @@ -1,11 +1,11 @@ import * as fs from "node:fs"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { runInTempDir, writeWranglerConfig, } from "@cloudflare/workers-utils/test-helpers"; import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -46,8 +46,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { diff --git a/packages/wrangler/src/__tests__/deploy/entry-points.test.ts b/packages/wrangler/src/__tests__/deploy/entry-points.test.ts index e0301090de..c3bd4a94b0 100644 --- a/packages/wrangler/src/__tests__/deploy/entry-points.test.ts +++ b/packages/wrangler/src/__tests__/deploy/entry-points.test.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; import { findWranglerConfig } from "@cloudflare/workers-utils"; import { normalizeString, @@ -10,7 +11,6 @@ import * as esbuild from "esbuild"; import { http, HttpResponse } from "msw"; import dedent from "ts-dedent"; import { afterEach, assert, beforeEach, describe, it, vi } from "vitest"; -import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; import { clearOutputFilePath } from "../../output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockConsoleMethods } from "../helpers/mock-console"; @@ -58,8 +58,11 @@ vi.mock("../../package-manager", async (importOriginal) => ({ }, })); -vi.mock("../../autoconfig/run"); -vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({ + ...(await importOriginal()), + runAutoConfig: vi.fn(), + getInstalledPackageVersion: vi.fn(), +})); vi.mock("@cloudflare/cli-shared-helpers/command"); describe("deploy", () => { @@ -343,15 +346,12 @@ export default{ it("should not trigger autoconfig on `wrangler deploy