diff --git a/.changeset/fix-c3-pnpm-11-auto-update-loop.md b/.changeset/fix-c3-pnpm-11-auto-update-loop.md new file mode 100644 index 0000000000..8f6ba9e083 --- /dev/null +++ b/.changeset/fix-c3-pnpm-11-auto-update-loop.md @@ -0,0 +1,9 @@ +--- +"create-cloudflare": patch +--- + +Fix infinite loop when running C3 with pnpm 11 + +When invoked via `pnpm create cloudflare@latest`, C3 checks npm for a newer version and re-launches itself with the latest version if one is available. pnpm 11 enables the `minimumReleaseAge` supply-chain protection by default, so `pnpm create cloudflare@latest` will not resolve a version published in the last 24 hours. When the npm `latest` tag points at a version newer than what pnpm is willing to install, the update check stayed true and C3 re-launched itself forever. + +The relaunched process is now marked so it never re-runs the auto-update check, ensuring C3 starts up after at most one relaunch regardless of the package manager's version resolution. diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 56cdf1d682..9a1fc24ada 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -10,12 +10,11 @@ import { logRaw, startSection, } from "@cloudflare/cli-shared-helpers"; -import { runCommand } from "@cloudflare/cli-shared-helpers/command"; import { CancelError } from "@cloudflare/cli-shared-helpers/error"; import { maybeAppendWranglerToGitIgnore } from "@cloudflare/cli-shared-helpers/gitignore"; import { isInteractive } from "@cloudflare/cli-shared-helpers/interactive"; import { cliDefinition, parseArgs, processArgument } from "helpers/args"; -import { C3_DEFAULTS, isUpdateAvailable } from "helpers/cli"; +import { C3_DEFAULTS, isUpdateAvailable, runLatest } from "helpers/cli"; import { runWranglerCommand } from "helpers/command"; import { detectPackageManager, @@ -46,8 +45,6 @@ import { addTypes } from "./workers"; import { updateWranglerConfig } from "./wrangler/config"; import type { C3Args, C3Context } from "types"; -const { npm } = detectPackageManager(); - export const main = async (argv: string[]) => { const result = await parseArgs(argv); @@ -79,6 +76,12 @@ export const main = async (argv: string[]) => { if ( args.autoUpdate && + // If this process was already spawned by `runLatest`, don't try to update + // again. Otherwise, package managers that resolve `cloudflare@latest` to a + // version older than the npm `latest` tag (e.g. pnpm 11's `minimumReleaseAge` + // supply-chain protection) would cause `isUpdateAvailable()` to stay true and + // re-spawn C3 forever. + !process.env.CREATE_CLOUDFLARE_RELAUNCHED && !process.env.VITEST && !process.env.CI && isInteractive() && @@ -96,19 +99,6 @@ export const main = async (argv: string[]) => { } }; -// Spawn a separate process running the most recent version of c3 -export const runLatest = async () => { - const args = process.argv.slice(2); - - // the parsing logic of `npm create` requires `--` to be supplied - // before any flags intended for the target command. - if (npm === "npm") { - args.unshift("--"); - } - - await runCommand([npm, "create", "cloudflare@latest", ...args]); -}; - // Entrypoint to c3 export const runCli = async (args: Partial) => { printBanner(args); diff --git a/packages/create-cloudflare/src/helpers/__tests__/cli.test.ts b/packages/create-cloudflare/src/helpers/__tests__/cli.test.ts index f77e9fb0b7..3136675cc4 100644 --- a/packages/create-cloudflare/src/helpers/__tests__/cli.test.ts +++ b/packages/create-cloudflare/src/helpers/__tests__/cli.test.ts @@ -1,12 +1,15 @@ +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; import { SemVer } from "semver"; import { getGlobalDispatcher, MockAgent, setGlobalDispatcher } from "undici"; import { afterEach, beforeEach, describe, test, vi } from "vitest"; import { version as currentVersion } from "../../../package.json"; -import { isUpdateAvailable } from "../cli"; -import { mockSpinner } from "./mocks"; +import { isUpdateAvailable, runLatest } from "../cli"; +import { mockPackageManager, mockSpinner } from "./mocks"; vi.mock("process"); +vi.mock("@cloudflare/cli-shared-helpers/command"); vi.mock("@cloudflare/cli-shared-helpers/interactive"); +vi.mock("which-pm-runs"); beforeEach(() => { mockSpinner(); @@ -78,3 +81,20 @@ describe("isUpdateAvailable", () => { ); } }); + +describe("runLatest", () => { + beforeEach(() => { + mockPackageManager("pnpm", "11.0.0"); + vi.mocked(runCommand).mockResolvedValue(""); + }); + + test("marks the relaunched process so it does not update again", async ({ + expect, + }) => { + await runLatest(); + + expect(runCommand).toHaveBeenCalledTimes(1); + const [, opts] = vi.mocked(runCommand).mock.calls[0]; + expect(opts?.env).toEqual({ CREATE_CLOUDFLARE_RELAUNCHED: "true" }); + }); +}); diff --git a/packages/create-cloudflare/src/helpers/cli.ts b/packages/create-cloudflare/src/helpers/cli.ts index 2c2967b410..be09b9d22a 100644 --- a/packages/create-cloudflare/src/helpers/cli.ts +++ b/packages/create-cloudflare/src/helpers/cli.ts @@ -1,10 +1,12 @@ import { updateStatus, warn } from "@cloudflare/cli-shared-helpers"; import { blue } from "@cloudflare/cli-shared-helpers/colors"; +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; import { spinner, spinnerFrames, } from "@cloudflare/cli-shared-helpers/interactive"; import Haikunator from "haikunator"; +import { detectPackageManager } from "helpers/packageManagers"; import { getLatestPackageVersion } from "helpers/packages"; import open from "open"; import semver from "semver"; @@ -48,6 +50,25 @@ export const isUpdateAvailable = async () => { } }; +// Spawn a separate process running the most recent version of c3 +export const runLatest = async () => { + const { npm } = detectPackageManager(); + const args = process.argv.slice(2); + + // the parsing logic of `npm create` requires `--` to be supplied + // before any flags intended for the target command. + if (npm === "npm") { + args.unshift("--"); + } + + await runCommand([npm, "create", "cloudflare@latest", ...args], { + // Mark the spawned process so it doesn't attempt to update and re-spawn + // again, which would loop indefinitely when the package manager keeps + // resolving the same version of `cloudflare@latest`. + env: { CREATE_CLOUDFLARE_RELAUNCHED: "true" }, + }); +}; + export const C3_DEFAULTS: C3Args = { projectName: new Haikunator().haikunate({ tokenHex: true }), category: "hello-world", diff --git a/packages/create-cloudflare/turbo.json b/packages/create-cloudflare/turbo.json index 04e2b5ebf5..918fd28c77 100644 --- a/packages/create-cloudflare/turbo.json +++ b/packages/create-cloudflare/turbo.json @@ -5,6 +5,7 @@ "build": { "env": [ "npm_config_user_agent", + "CREATE_CLOUDFLARE_RELAUNCHED", "CREATE_CLOUDFLARE_TELEMETRY_DISABLED", "CREATE_CLOUDFLARE_TELEMETRY_DEBUG", "SPARROW_SOURCE_KEY"