Skip to content

Commit

Permalink
[C3] fix: use a valid compatibility date for worker templates (#3343)
Browse files Browse the repository at this point in the history
* [C3] 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 new 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 #2385

* Improve workerd date matching and support non-npm runners.

* fixup! [C3] fix: use a valid compatibility date for worker templates
  • Loading branch information
petebacondarwin authored Jun 23, 2023
1 parent 9ae3d93 commit cc9ced8
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 39 deletions.
14 changes: 14 additions & 0 deletions .changeset/small-lies-thank.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"webassemblymemory",
"websockets",
"xxhash",
"workerd",
"zjcompt"
],
"cSpell.ignoreWords": [
Expand Down
67 changes: 59 additions & 8 deletions packages/create-cloudflare/src/helpers/__tests__/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });

Expand Down Expand Up @@ -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");
});
});
});
92 changes: 75 additions & 17 deletions packages/create-cloudflare/src/helpers/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand All @@ -35,35 +39,35 @@ type PrintOptions<T> = {
promise: Promise<T> | (() => Promise<T>);
useSpinner?: boolean;
startText: string;
doneText?: string;
doneText?: string | ((output: T) => string);
};

export const runCommand = async (
command: Command,
opts?: RunOptions
opts: RunOptions = {}
): Promise<string> => {
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
// stdio: [ioMode, ioMode, "inherit"],
stdio: squelch ? "pipe" : "inherit",
env: {
...process.env,
...opts?.env,
...opts.env,
},
cwd: opts?.cwd,
cwd: opts.cwd,
});

let output = ``;
Expand All @@ -79,27 +83,46 @@ export const runCommand = async (

return new Promise<string>((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 }));
}
}
});
});
},
});
};

// 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");
},
});
}
Expand All @@ -121,9 +144,13 @@ export const printAsyncStatus = async <T>({
}

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 {
Expand Down Expand Up @@ -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)}`,
});
}
16 changes: 11 additions & 5 deletions packages/create-cloudflare/src/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -149,12 +153,14 @@ async function updateFiles(ctx: Context) {
};

// update files
contents.packagejson.name = ctx.project.name;
if (contents.packagejson.name === "<TBD>") {
contents.packagejson.name = ctx.project.name;
}
contents.wranglertoml = contents.wranglertoml
.replace(/^name = .+$/m, `name = "${ctx.project.name}"`)
.replace(/^name\s*=\s*"<TBD>"/m, `name = "${ctx.project.name}"`)
.replace(
/^compatibility_date = .+$/m,
`compatibility_date = "${new Date().toISOString().substring(0, 10)}"`
/^compatibility_date\s*=\s*"<TBD>"/m,
`compatibility_date = "${await getWorkerdCompatibilityDate()}"`
);

// write files
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
name = "cloudflare-workers-chatgpt-plugin-example"
name = "<TBD>"
main = "src/index.ts"
compatibility_date = "2023-04-07"
compatibility_date = "<TBD>"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.js"
compatibility_date = "2023-04-21"
compatibility_date = "<TBD>"

# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.ts"
compatibility_date = "2023-04-21"
compatibility_date = "<TBD>"

# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.js"
compatibility_date = "2023-04-21"
compatibility_date = "<TBD>"

# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.ts"
compatibility_date = "2023-04-21"
compatibility_date = "<TBD>"

# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.js"
compatibility_date = "2023-05-15"
compatibility_date = "<TBD>"

# 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.ts"
compatibility_date = "2023-05-15"
compatibility_date = "<TBD>"

# 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "<TBD>"
main = "src/worker.ts"
compatibility_date = "2023-05-15"
compatibility_date = "<TBD>"

# Cron Triggers
# Docs: https://developers.cloudflare.com/workers/platform/triggers/cron-triggers/
Expand Down

0 comments on commit cc9ced8

Please sign in to comment.