diff --git a/.changeset/small-lies-thank.md b/.changeset/small-lies-thank.md new file mode 100644 index 000000000000..aa8bd2638975 --- /dev/null +++ b/.changeset/small-lies-thank.md @@ -0,0 +1,14 @@ +--- +"create-cloudflare": patch +--- + +fix: use a valid compatibility date for worker templates + +Previously, we changed wrangler.toml to use the current date for the +compatibility_date setting in wrangler.toml when generating workers. +But this is almost always going to be too recent and results in a warning. + +Now we look up the most recent compatibility date via npm on the workerd +package and use that instead. + +Fixes https://github.com/cloudflare/workers-sdk/issues/2385 diff --git a/.vscode/settings.json b/.vscode/settings.json index c1caa5d8d903..7005636f937f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,6 +42,7 @@ "webassemblymemory", "websockets", "xxhash", + "workerd", "zjcompt" ], "cSpell.ignoreWords": [ diff --git a/packages/create-cloudflare/src/helpers/__tests__/command.test.ts b/packages/create-cloudflare/src/helpers/__tests__/command.test.ts index ec44d65f2620..625b32288621 100644 --- a/packages/create-cloudflare/src/helpers/__tests__/command.test.ts +++ b/packages/create-cloudflare/src/helpers/__tests__/command.test.ts @@ -3,31 +3,51 @@ import { detectPackageManager } from "helpers/packages"; import { beforeEach, afterEach, describe, expect, test, vi } from "vitest"; import whichPMRuns from "which-pm-runs"; import { + getWorkerdCompatibilityDate, installPackages, installWrangler, npmInstall, runCommand, } from "../command"; +// We can change how the mock spawn works by setting these variables +let spawnResultCode = 0; +let spawnStdout: string | undefined = undefined; +let spawnStderr: string | undefined = undefined; + describe("Command Helpers", () => { afterEach(() => { vi.clearAllMocks(); + spawnResultCode = 0; + spawnStdout = undefined; + spawnStderr = undefined; }); beforeEach(() => { // Mock out the child_process.spawn function vi.mock("cross-spawn", () => { - const mockedSpawn = vi.fn().mockImplementation(() => ({ - on: vi.fn().mockImplementation((event, cb) => { - if (event === "close") { - cb(0); - } - }), - })); + const mockedSpawn = vi.fn().mockImplementation(() => { + return { + on: vi.fn().mockImplementation((event, cb) => { + if (event === "close") { + cb(spawnResultCode); + } + }), + stdout: { + on(event: "data", cb: (data: string) => void) { + spawnStdout !== undefined && cb(spawnStdout); + }, + }, + stderr: { + on(event: "data", cb: (data: string) => void) { + spawnStderr !== undefined && cb(spawnStderr); + }, + }, + }; + }); return { spawn: mockedSpawn }; }); - vi.mock("which-pm-runs"); vi.mocked(whichPMRuns).mockReturnValue({ name: "npm", version: "8.3.1" }); @@ -142,4 +162,35 @@ describe("Command Helpers", () => { expect(pm.dlx).toBe("yarn"); }); }); + + describe("getWorkerdCompatibilityDate()", () => { + test("normal flow", async () => { + spawnStdout = "2.20250110.5"; + const date = await getWorkerdCompatibilityDate(); + expectSpawnWith("npm info workerd dist-tags.latest"); + expect(date).toBe("2025-01-10"); + }); + + test("empty result", async () => { + spawnStdout = ""; + const date = await getWorkerdCompatibilityDate(); + expectSpawnWith("npm info workerd dist-tags.latest"); + expect(date).toBe("2023-05-18"); + }); + + test("verbose output (e.g. yarn or debug mode)", async () => { + spawnStdout = + "Debugger attached.\nyarn info v1.22.19\n2.20250110.5\n✨ Done in 0.83s."; + const date = await getWorkerdCompatibilityDate(); + expectSpawnWith("npm info workerd dist-tags.latest"); + expect(date).toBe("2025-01-10"); + }); + + test("command failed", async () => { + spawnResultCode = 1; + const date = await getWorkerdCompatibilityDate(); + expectSpawnWith("npm info workerd dist-tags.latest"); + expect(date).toBe("2023-05-18"); + }); + }); }); diff --git a/packages/create-cloudflare/src/helpers/command.ts b/packages/create-cloudflare/src/helpers/command.ts index 0afd04a99031..f92531621e51 100644 --- a/packages/create-cloudflare/src/helpers/command.ts +++ b/packages/create-cloudflare/src/helpers/command.ts @@ -18,12 +18,16 @@ type Command = string | string[]; type RunOptions = { startText?: string; - doneText?: string; + doneText?: string | ((output: string) => string); silent?: boolean; captureOutput?: boolean; useSpinner?: boolean; env?: NodeJS.ProcessEnv; cwd?: string; + /** If defined this function is called to all you to transform the output from the command into a new string. */ + transformOutput?: (output: string) => string; + /** If defined, this function is called to return a string that is used if the `transformOutput()` fails. */ + fallbackOutput?: (error: unknown) => string; }; type MultiRunOptions = RunOptions & { @@ -35,25 +39,25 @@ type PrintOptions = { promise: Promise | (() => Promise); useSpinner?: boolean; startText: string; - doneText?: string; + doneText?: string | ((output: T) => string); }; export const runCommand = async ( command: Command, - opts?: RunOptions + opts: RunOptions = {} ): Promise => { if (typeof command === "string") { command = command.trim().replace(/\s+/g, ` `).split(" "); } return printAsyncStatus({ - useSpinner: opts?.useSpinner ?? opts?.silent, - startText: opts?.startText || command.join(" "), - doneText: opts?.doneText, + useSpinner: opts.useSpinner ?? opts.silent, + startText: opts.startText || command.join(" "), + doneText: opts.doneText, promise() { const [executable, ...args] = command; - const squelch = opts?.silent || process.env.VITEST; + const squelch = opts.silent || process.env.VITEST; const cmd = spawn(executable, [...args], { // TODO: ideally inherit stderr, but npm install uses this for warnings @@ -61,9 +65,9 @@ export const runCommand = async ( stdio: squelch ? "pipe" : "inherit", env: { ...process.env, - ...opts?.env, + ...opts.env, }, - cwd: opts?.cwd, + cwd: opts.cwd, }); let output = ``; @@ -79,10 +83,27 @@ export const runCommand = async ( return new Promise((resolve, reject) => { cmd.on("close", (code) => { - if (code === 0) { - resolve(stripAnsi(output)); - } else { - reject(new Error(output, { cause: code })); + try { + if (code !== 0) { + throw new Error(output, { cause: code }); + } + + // Process any captured output + const transformOutput = + opts.transformOutput ?? ((result: string) => result); + const processedOutput = transformOutput(stripAnsi(output)); + + // Send the captured (and processed) output back to the caller + resolve(processedOutput); + } catch (e) { + // Something went wrong. + // Perhaps the command or the transform failed. + // If there is a fallback use the result of calling that + if (opts.fallbackOutput) { + resolve(opts.fallbackOutput(e)); + } else { + reject(new Error(output, { cause: e })); + } } }); }); @@ -90,16 +111,18 @@ export const runCommand = async ( }); }; -// run mutliple commands in sequence (not parallel) +// run multiple commands in sequence (not parallel) export async function runCommands({ commands, ...opts }: MultiRunOptions) { return printAsyncStatus({ useSpinner: opts.useSpinner ?? opts.silent, startText: opts.startText, doneText: opts.doneText, async promise() { + const results = []; for (const command of commands) { - await runCommand(command, { ...opts, useSpinner: false }); + results.push(await runCommand(command, { ...opts, useSpinner: false })); } + return results.join("\n"); }, }); } @@ -121,9 +144,13 @@ export const printAsyncStatus = async ({ } try { - await promise; + const output = await promise; - s?.stop(opts.doneText); + const doneText = + typeof opts.doneText === "function" + ? opts.doneText(output) + : opts.doneText; + s?.stop(doneText); } catch (err) { s?.stop((err as Error).message); } finally { @@ -276,3 +303,34 @@ export const listAccounts = async () => { return accounts; }; + +/** + * Look up the latest release of workerd and use its date as the compatibility_date + * configuration value for wrangler.toml. + * + * If the look up fails then we fall back to a well known date. + * + * The date is extracted from the version number of the workerd package tagged as `latest`. + * The format of the version is `major.yyyymmdd.patch`. + * + * @returns The latest compatibility date for workerd in the form "YYYY-MM-DD" + */ +export async function getWorkerdCompatibilityDate() { + const { npm } = detectPackageManager(); + return runCommand(`${npm} info workerd dist-tags.latest`, { + silent: true, + captureOutput: true, + startText: "Retrieving current workerd compatibility date", + transformOutput: (result) => { + // The format of the workerd version is `major.yyyymmdd.patch`. + const match = result.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/); + if (!match) { + throw new Error("Could not find workerd date"); + } + const [, year, month, date] = match; + return `${year}-${month}-${date}`; + }, + fallbackOutput: () => "2023-05-18", + doneText: (output) => `${brandColor("compatibility date")} ${dim(output)}`, + }); +} diff --git a/packages/create-cloudflare/src/workers.ts b/packages/create-cloudflare/src/workers.ts index d5fa6a05e0c2..38030e6efa65 100644 --- a/packages/create-cloudflare/src/workers.ts +++ b/packages/create-cloudflare/src/workers.ts @@ -4,7 +4,11 @@ import { resolve, join } from "path"; import { chdir } from "process"; import { endSection, updateStatus, startSection } from "helpers/cli"; import { brandColor, dim } from "helpers/colors"; -import { npmInstall, runCommand } from "helpers/command"; +import { + getWorkerdCompatibilityDate, + npmInstall, + runCommand, +} from "helpers/command"; import { confirmInput, textInput } from "helpers/interactive"; import { chooseAccount, @@ -149,12 +153,14 @@ async function updateFiles(ctx: Context) { }; // update files - contents.packagejson.name = ctx.project.name; + if (contents.packagejson.name === "") { + contents.packagejson.name = ctx.project.name; + } contents.wranglertoml = contents.wranglertoml - .replace(/^name = .+$/m, `name = "${ctx.project.name}"`) + .replace(/^name\s*=\s*""/m, `name = "${ctx.project.name}"`) .replace( - /^compatibility_date = .+$/m, - `compatibility_date = "${new Date().toISOString().substring(0, 10)}"` + /^compatibility_date\s*=\s*""/m, + `compatibility_date = "${await getWorkerdCompatibilityDate()}"` ); // write files diff --git a/packages/create-cloudflare/templates/chatgptPlugin/ts/wrangler.toml b/packages/create-cloudflare/templates/chatgptPlugin/ts/wrangler.toml index d65c181a43b7..96a248cbe314 100644 --- a/packages/create-cloudflare/templates/chatgptPlugin/ts/wrangler.toml +++ b/packages/create-cloudflare/templates/chatgptPlugin/ts/wrangler.toml @@ -1,3 +1,3 @@ -name = "cloudflare-workers-chatgpt-plugin-example" +name = "" main = "src/index.ts" -compatibility_date = "2023-04-07" +compatibility_date = "" diff --git a/packages/create-cloudflare/templates/common/js/wrangler.toml b/packages/create-cloudflare/templates/common/js/wrangler.toml index ef105f59e3d7..b08d00f97c8f 100644 --- a/packages/create-cloudflare/templates/common/js/wrangler.toml +++ b/packages/create-cloudflare/templates/common/js/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.js" -compatibility_date = "2023-04-21" +compatibility_date = "" # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Note: Use secrets to store sensitive data. diff --git a/packages/create-cloudflare/templates/common/ts/wrangler.toml b/packages/create-cloudflare/templates/common/ts/wrangler.toml index 219f8df86029..a8fe19656eb9 100644 --- a/packages/create-cloudflare/templates/common/ts/wrangler.toml +++ b/packages/create-cloudflare/templates/common/ts/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.ts" -compatibility_date = "2023-04-21" +compatibility_date = "" # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Note: Use secrets to store sensitive data. diff --git a/packages/create-cloudflare/templates/hello-world/js/wrangler.toml b/packages/create-cloudflare/templates/hello-world/js/wrangler.toml index ef105f59e3d7..b08d00f97c8f 100644 --- a/packages/create-cloudflare/templates/hello-world/js/wrangler.toml +++ b/packages/create-cloudflare/templates/hello-world/js/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.js" -compatibility_date = "2023-04-21" +compatibility_date = "" # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Note: Use secrets to store sensitive data. diff --git a/packages/create-cloudflare/templates/hello-world/ts/wrangler.toml b/packages/create-cloudflare/templates/hello-world/ts/wrangler.toml index 219f8df86029..a8fe19656eb9 100644 --- a/packages/create-cloudflare/templates/hello-world/ts/wrangler.toml +++ b/packages/create-cloudflare/templates/hello-world/ts/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.ts" -compatibility_date = "2023-04-21" +compatibility_date = "" # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Note: Use secrets to store sensitive data. diff --git a/packages/create-cloudflare/templates/queues/js/wrangler.toml b/packages/create-cloudflare/templates/queues/js/wrangler.toml index 145a24b13b20..cf30a30bdffb 100644 --- a/packages/create-cloudflare/templates/queues/js/wrangler.toml +++ b/packages/create-cloudflare/templates/queues/js/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.js" -compatibility_date = "2023-05-15" +compatibility_date = "" # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. # Docs: https://developers.cloudflare.com/queues/get-started diff --git a/packages/create-cloudflare/templates/queues/ts/wrangler.toml b/packages/create-cloudflare/templates/queues/ts/wrangler.toml index 21b87d0bf2b6..9ea03c606b79 100644 --- a/packages/create-cloudflare/templates/queues/ts/wrangler.toml +++ b/packages/create-cloudflare/templates/queues/ts/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.ts" -compatibility_date = "2023-05-15" +compatibility_date = "" # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. # Docs: https://developers.cloudflare.com/queues/get-started diff --git a/packages/create-cloudflare/templates/scheduled/js/wrangler.toml b/packages/create-cloudflare/templates/scheduled/js/wrangler.toml index 3c0e134ce227..8947cda73f49 100644 --- a/packages/create-cloudflare/templates/scheduled/js/wrangler.toml +++ b/packages/create-cloudflare/templates/scheduled/js/wrangler.toml @@ -1,6 +1,6 @@ name = "" main = "src/worker.ts" -compatibility_date = "2023-05-15" +compatibility_date = "" # Cron Triggers # Docs: https://developers.cloudflare.com/workers/platform/triggers/cron-triggers/