Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fix-c3-pnpm-11-auto-update-loop.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 7 additions & 17 deletions packages/create-cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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() &&
Expand All @@ -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<C3Args>) => {
printBanner(args);
Expand Down
24 changes: 22 additions & 2 deletions packages/create-cloudflare/src/helpers/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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" });
});
});
21 changes: 21 additions & 0 deletions packages/create-cloudflare/src/helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/create-cloudflare/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"build": {
"env": [
"npm_config_user_agent",
"CREATE_CLOUDFLARE_RELAUNCHED",
"CREATE_CLOUDFLARE_TELEMETRY_DISABLED",
"CREATE_CLOUDFLARE_TELEMETRY_DEBUG",
"SPARROW_SOURCE_KEY"
Expand Down
Loading