Skip to content

Commit

Permalink
[C3] fix: use a valid compatibility date for worker templates
Browse files Browse the repository at this point in the history
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 cloudflare#2385
  • Loading branch information
petebacondarwin committed May 27, 2023
1 parent 23ed5f5 commit b49de4c
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 40 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 @@ -37,6 +37,7 @@
"weakset",
"webassemblymemory",
"websockets",
"workerd",
"xxhash"
],
"cSpell.ignoreWords": [
Expand Down
8 changes: 6 additions & 2 deletions packages/create-cloudflare/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,16 @@ export async function initializeGit(cwd: string) {
);

// Try to create the repository with the HEAD branch of defaultBranchName ?? `main`.
await runCommand(
return runCommand(
`git init --initial-branch ${defaultBranchName.trim() ?? "main"}`, // branch names can't contain spaces, so this is safe
{ useSpinner: false, silent: true, cwd }
);
} catch {
// Unable to create the repo with a HEAD branch name, so just fall back to the default.
await runCommand(`git init`, { useSpinner: false, silent: true, cwd });
return runCommand(`git init`, {
useSpinner: false,
silent: true,
cwd,
});
}
}
59 changes: 51 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 { beforeEach, afterEach, describe, expect, test, vi } from "vitest";
import whichPMRuns from "which-pm-runs";
import {
detectPackageManager,
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: "dev" });

Expand Down Expand Up @@ -105,4 +125,27 @@ describe("Command Helpers", () => {
expect(pm.npm).toBe("yarn");
expect(pm.npx).toBe("npx");
});

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("command failed", async () => {
spawnResultCode = 1;
const date = await getWorkerdCompatibilityDate();
expectSpawnWith("npm info workerd dist-tags.latest");
expect(date).toBe("2023-05-18");
});
});
});
88 changes: 69 additions & 19 deletions packages/create-cloudflare/src/helpers/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ type Command = string | string[];

type RunOptions = {
startText?: string;
doneText?: string;
doneText?: string | ((result: string) => string);
silent?: boolean;
captureOutput?: boolean;
useSpinner?: boolean;
env?: NodeJS.ProcessEnv;
cwd?: string;
transform?: (output: string) => string;
fallback?: (error: unknown) => string;
};

type MultiRunOptions = RunOptions & {
Expand All @@ -35,35 +37,35 @@ type PrintOptions<T> = {
promise: Promise<T> | (() => Promise<T>);
useSpinner?: boolean;
startText: string;
doneText?: string;
doneText?: string | ((result: 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,
promise() {
useSpinner: opts.useSpinner ?? opts.silent,
startText: opts.startText || command.join(" "),
doneText: opts.doneText,
async 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 @@ -77,29 +79,48 @@ export const runCommand = async (
});
}

return new Promise<string>((resolve, reject) => {
return await 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 transform = opts.transform ?? ((result: string) => result);
const processedOutput = transform(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.fallback) {
resolve(opts.fallback(e));
} else {
reject(new Error(output, { cause: code }));
reject(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 +142,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 @@ -289,3 +314,28 @@ 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() {
return runCommand("npm info workerd dist-tags.latest", {
silent: true,
captureOutput: true,
startText: "Retrieving current workerd compatibility date",
transform: (result) => {
const date = result.split(".")[1];
return `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`;
},
fallback: () => "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 @@ -147,12 +151,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>"

# # KV Namespace binding - For more information: https://developers.cloudflare.com/workers/runtime-apis/kv
# [[kv_namespaces]]
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>"

# # KV Namespace binding - For more information: https://developers.cloudflare.com/workers/runtime-apis/kv
# [[kv_namespaces]]
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>"

# # KV Namespace binding - For more information: https://developers.cloudflare.com/workers/runtime-apis/kv
# [[kv_namespaces]]
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>"

# # KV Namespace binding - For more information: https://developers.cloudflare.com/workers/runtime-apis/kv
# [[kv_namespaces]]
Expand Down

0 comments on commit b49de4c

Please sign in to comment.