From 294ca542c6ad57685b97fd787bfc3fe47c3cae74 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Wed, 29 Nov 2023 23:35:55 -0600 Subject: [PATCH] C3: Use latest version of @cloudflare/workers-types in workers projects (#4525) * C3: Stop using stale wrangler and workers-types versions Installing these as part of the generator instead of relying on a stale version in the template we copied ensures we have the most up-to-date version. * C3: Default stringification spacing with writeJSON * changeset * Fix unused import * C3: Restoring wrangler in worker template devDependencies * Fix unit test on windows --- .changeset/tidy-planes-peel.md | 7 + .../src/__tests__/helpers.ts | 19 +++ .../src/__tests__/workers.test.ts | 116 +++++++++++++++++ .../src/frameworks/next/index.ts | 2 +- .../create-cloudflare/src/helpers/files.ts | 6 +- packages/create-cloudflare/src/workers.ts | 120 +++++++++++++----- .../templates/chatgptPlugin/ts/package.json | 1 - .../templates/common/ts/package.json | 1 - .../templates/hello-world/ts/package.json | 1 - .../templates/openapi/ts/package.json | 1 - .../templates/queues/ts/package.json | 1 - .../templates/scheduled/ts/package.json | 1 - 12 files changed, 233 insertions(+), 43 deletions(-) create mode 100644 .changeset/tidy-planes-peel.md create mode 100644 packages/create-cloudflare/src/__tests__/helpers.ts create mode 100644 packages/create-cloudflare/src/__tests__/workers.test.ts diff --git a/.changeset/tidy-planes-peel.md b/.changeset/tidy-planes-peel.md new file mode 100644 index 000000000000..86d06774a636 --- /dev/null +++ b/.changeset/tidy-planes-peel.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": minor +--- + +C3: Use latest version of `wrangler` and `@cloudflare/workers-types`. + +Also updates the `types` entry of the project's `tsconfig.json` to use type definitions corresponding to the latest compatibility date. diff --git a/packages/create-cloudflare/src/__tests__/helpers.ts b/packages/create-cloudflare/src/__tests__/helpers.ts new file mode 100644 index 000000000000..93c96eac2fb3 --- /dev/null +++ b/packages/create-cloudflare/src/__tests__/helpers.ts @@ -0,0 +1,19 @@ +import { C3_DEFAULTS } from "helpers/cli"; +import type { C3Args, PagesGeneratorContext as Context } from "types"; + +export const createTestArgs = (args?: Partial) => { + return { + ...C3_DEFAULTS, + ...args, + }; +}; + +export const createTestContext = (name = "test", args?: C3Args): Context => { + const path = `./${name}`; + return { + project: { name, path }, + args: args ?? createTestArgs(), + originalCWD: path, + gitRepoAlreadyExisted: false, + }; +}; diff --git a/packages/create-cloudflare/src/__tests__/workers.test.ts b/packages/create-cloudflare/src/__tests__/workers.test.ts new file mode 100644 index 000000000000..8bfb91860ec5 --- /dev/null +++ b/packages/create-cloudflare/src/__tests__/workers.test.ts @@ -0,0 +1,116 @@ +import { existsSync, readdirSync } from "fs"; +import { readFile, writeFile } from "helpers/files"; +import { describe, expect, test, vi, afterEach, beforeEach } from "vitest"; +import * as workers from "../workers"; +import { createTestContext } from "./helpers"; +import type { Dirent } from "fs"; +import type { PagesGeneratorContext } from "types"; + +const mockWorkersTypesDirListing = [ + "2021-11-03", + "2022-03-21", + "2022-11-30", + "2023-03-01", + "2023-07-01", + "experimental", + "index.d.ts", + "index.ts", + "oldest", + "package.json", +]; + +vi.mock("fs"); +vi.mock("helpers/files"); + +describe("getLatestTypesEntrypoint", () => { + const ctx = createTestContext(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("happy path", async () => { + vi.mocked(readdirSync).mockImplementation( + // vitest won't resolve the type for the correct overload thus the trickery + () => [...mockWorkersTypesDirListing] as unknown as Dirent[] + ); + + const entrypoint = workers.getLatestTypesEntrypoint(ctx); + expect(entrypoint).toBe("2023-07-01"); + }); + + test("read error", async () => { + vi.mocked(readdirSync).mockImplementation(() => { + throw new Error("ENOENT: no such file or directory"); + }); + + const entrypoint = workers.getLatestTypesEntrypoint(ctx); + expect(entrypoint).toBe(null); + }); + + test("empty directory", async () => { + vi.mocked(readdirSync).mockImplementation(() => []); + + const entrypoint = workers.getLatestTypesEntrypoint(ctx); + expect(entrypoint).toBe(null); + }); + + test("no compat dates found", async () => { + vi.mocked(readdirSync).mockImplementation( + () => ["foo", "bar"] as unknown as Dirent[] + ); + + const entrypoint = workers.getLatestTypesEntrypoint(ctx); + expect(entrypoint).toBe(null); + }); +}); + +describe("updateTsConfig", () => { + let ctx: PagesGeneratorContext; + + beforeEach(() => { + ctx = createTestContext(); + + ctx.args.ts = true; + vi.mocked(existsSync).mockImplementation(() => true); + // mock getLatestTypesEntrypoint + vi.mocked(readdirSync).mockImplementation( + () => ["2023-07-01"] as unknown as Dirent[] + ); + + // Mock the read of tsconfig.json + vi.mocked(readFile).mockImplementation( + () => `{types: ["@cloudflare/workers-types"]}` + ); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("happy path", async () => { + await workers.updateTsConfig(ctx); + + expect(vi.mocked(writeFile).mock.calls[0][1]).toEqual( + `{types: ["@cloudflare/workers-types/2023-07-01"]}` + ); + }); + + test("not using ts", async () => { + ctx.args.ts = false; + expect(writeFile).not.toHaveBeenCalled(); + }); + + test("tsconfig.json not found", async () => { + vi.mocked(existsSync).mockImplementation(() => false); + expect(writeFile).not.toHaveBeenCalled(); + }); + + test("latest entrypoint not found", async () => { + vi.mocked(readdirSync).mockImplementation( + () => ["README.md"] as unknown as Dirent[] + ); + + expect(writeFile).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/create-cloudflare/src/frameworks/next/index.ts b/packages/create-cloudflare/src/frameworks/next/index.ts index 8a44ec750e5c..bd20775a36c1 100644 --- a/packages/create-cloudflare/src/frameworks/next/index.ts +++ b/packages/create-cloudflare/src/frameworks/next/index.ts @@ -132,7 +132,7 @@ export const writeEslintrc = async ( eslintConfig.extends ??= []; eslintConfig.extends.push("plugin:eslint-plugin-next-on-pages/recommended"); - writeJSON(`${ctx.project.name}/.eslintrc.json`, eslintConfig, 2); + writeJSON(`${ctx.project.name}/.eslintrc.json`, eslintConfig); }; const config: FrameworkConfig = { diff --git a/packages/create-cloudflare/src/helpers/files.ts b/packages/create-cloudflare/src/helpers/files.ts index c6f9fa16cb4e..b519639f599a 100644 --- a/packages/create-cloudflare/src/helpers/files.ts +++ b/packages/create-cloudflare/src/helpers/files.ts @@ -24,11 +24,7 @@ export const readJSON = (path: string) => { return contents ? JSON.parse(contents) : contents; }; -export const writeJSON = ( - path: string, - object: object, - stringifySpace?: number | string -) => { +export const writeJSON = (path: string, object: object, stringifySpace = 2) => { writeFile(path, JSON.stringify(object, null, stringifySpace)); }; diff --git a/packages/create-cloudflare/src/workers.ts b/packages/create-cloudflare/src/workers.ts index d9ed472cefac..2879d15d8975 100644 --- a/packages/create-cloudflare/src/workers.ts +++ b/packages/create-cloudflare/src/workers.ts @@ -1,24 +1,20 @@ -import { - cp, - mkdtemp, - readFile, - readdir, - rename, - rm, - writeFile, -} from "fs/promises"; +import { existsSync, readdirSync } from "fs"; +import { cp, mkdtemp, readdir, rename, rm } from "fs/promises"; import { tmpdir } from "os"; import { dirname, join, resolve } from "path"; import { chdir } from "process"; import { endSection, startSection, updateStatus } from "@cloudflare/cli"; import { brandColor, dim } from "@cloudflare/cli/colors"; +import { spinner } from "@cloudflare/cli/interactive"; import { processArgument } from "helpers/args"; import { C3_DEFAULTS } from "helpers/cli"; import { getWorkerdCompatibilityDate, + installPackages, npmInstall, runCommand, } from "helpers/command"; +import { readFile, readJSON, writeFile, writeJSON } from "helpers/files"; import { detectPackageManager } from "helpers/packages"; import { chooseAccount, @@ -32,7 +28,7 @@ import { } from "./common"; import type { C3Args, PagesGeneratorContext as Context } from "types"; -const { dlx } = detectPackageManager(); +const { dlx, npm } = detectPackageManager(); export const runWorkersGenerator = async (args: C3Args) => { const originalCWD = process.cwd(); @@ -61,6 +57,7 @@ export const runWorkersGenerator = async (args: C3Args) => { startSection("Installing dependencies", "Step 2 of 3"); chdir(ctx.project.path); await npmInstall(); + await installWorkersTypes(ctx); await gitCommit(ctx); endSection("Dependencies Installed"); @@ -164,33 +161,94 @@ async function copyExistingWorkerFiles(ctx: Context) { } async function updateFiles(ctx: Context) { - // build file paths - const paths = { - packagejson: resolve(ctx.project.path, "package.json"), - wranglertoml: resolve(ctx.project.path, "wrangler.toml"), - }; - - // read files - const contents = { - packagejson: JSON.parse(await readFile(paths.packagejson, "utf-8")), - wranglertoml: await readFile(paths.wranglertoml, "utf-8"), - }; - - // update files - if (contents.packagejson.name === "") { - contents.packagejson.name = ctx.project.name; + // Update package.json with project name + const pkgJsonPath = resolve(ctx.project.path, "package.json"); + const pkgJson = readJSON(pkgJsonPath); + if (pkgJson.name === "") { + pkgJson.name = ctx.project.name; } - contents.wranglertoml = contents.wranglertoml + writeJSON(pkgJsonPath, pkgJson); + + // Update wrangler.toml with name and compat date + const wranglerTomlPath = resolve(ctx.project.path, "wrangler.toml"); + let wranglerToml = readFile(wranglerTomlPath); + wranglerToml = wranglerToml .replace(/^name\s*=\s*""/m, `name = "${ctx.project.name}"`) .replace( /^compatibility_date\s*=\s*""/m, `compatibility_date = "${await getWorkerdCompatibilityDate()}"` ); + writeFile(wranglerTomlPath, wranglerToml); +} + +async function installWorkersTypes(ctx: Context) { + if (!ctx.args.ts) { + return; + } + + await installPackages(["@cloudflare/workers-types"], { + dev: true, + startText: `Installing @cloudflare/workers-types`, + doneText: `${brandColor("installed")} ${dim(`via ${npm}`)}`, + }); + await updateTsConfig(ctx); +} + +export async function updateTsConfig(ctx: Context) { + const tsconfigPath = join(ctx.project.path, "tsconfig.json"); + if (!existsSync(tsconfigPath)) { + return; + } + + const s = spinner(); + s.start(`Adding latest types to \`tsconfig.json\``); + + const tsconfig = readFile(tsconfigPath); + const entrypointVersion = getLatestTypesEntrypoint(ctx); + if (entrypointVersion === null) { + s.stop( + `${brandColor( + "skipped" + )} couldn't find latest compatible version of @cloudflare/workers-types` + ); + return; + } + + const typesEntrypoint = `@cloudflare/workers-types/${entrypointVersion}`; + const updated = tsconfig.replace( + "@cloudflare/workers-types", + typesEntrypoint + ); + + writeFile(tsconfigPath, updated); + s.stop(`${brandColor("added")} ${dim(typesEntrypoint)}`); +} - // write files - await writeFile( - paths.packagejson, - JSON.stringify(contents.packagejson, null, 2) +// @cloudflare/workers-types are versioned by compatibility dates, so we must look +// up the latest entrypiont from the installed dependency on disk. +// See also https://github.com/cloudflare/workerd/tree/main/npm/workers-types#compatibility-dates +export function getLatestTypesEntrypoint(ctx: Context) { + const workersTypesPath = resolve( + ctx.project.path, + "node_modules", + "@cloudflare", + "workers-types" ); - await writeFile(paths.wranglertoml, contents.wranglertoml); + + try { + const entrypoints = readdirSync(workersTypesPath); + + const sorted = entrypoints + .filter((filename) => filename.match(/(\d{4})-(\d{2})-(\d{2})/)) + .sort() + .reverse(); + + if (sorted.length === 0) { + return null; + } + + return sorted[0]; + } catch (error) { + return null; + } } diff --git a/packages/create-cloudflare/templates/chatgptPlugin/ts/package.json b/packages/create-cloudflare/templates/chatgptPlugin/ts/package.json index 1d2f7e3b9b9f..6dae40f7dde8 100644 --- a/packages/create-cloudflare/templates/chatgptPlugin/ts/package.json +++ b/packages/create-cloudflare/templates/chatgptPlugin/ts/package.json @@ -11,7 +11,6 @@ "@cloudflare/itty-router-openapi": "^1.0.1" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230404.0", "wrangler": "^3.0.0" } } diff --git a/packages/create-cloudflare/templates/common/ts/package.json b/packages/create-cloudflare/templates/common/ts/package.json index 51f366e45eb4..e94791b6c70c 100644 --- a/packages/create-cloudflare/templates/common/ts/package.json +++ b/packages/create-cloudflare/templates/common/ts/package.json @@ -8,7 +8,6 @@ "start": "wrangler dev" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230419.0", "itty-router": "^3.0.12", "typescript": "^5.0.4", "wrangler": "^3.0.0" diff --git a/packages/create-cloudflare/templates/hello-world/ts/package.json b/packages/create-cloudflare/templates/hello-world/ts/package.json index c918a935e37c..bc8d464b3139 100644 --- a/packages/create-cloudflare/templates/hello-world/ts/package.json +++ b/packages/create-cloudflare/templates/hello-world/ts/package.json @@ -8,7 +8,6 @@ "start": "wrangler dev" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230419.0", "typescript": "^5.0.4", "wrangler": "^3.0.0" } diff --git a/packages/create-cloudflare/templates/openapi/ts/package.json b/packages/create-cloudflare/templates/openapi/ts/package.json index 2c2ff704844b..c2958a92d56b 100644 --- a/packages/create-cloudflare/templates/openapi/ts/package.json +++ b/packages/create-cloudflare/templates/openapi/ts/package.json @@ -11,7 +11,6 @@ "@cloudflare/itty-router-openapi": "^1.0.1" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230821.0", "@types/node": "^20.5.7", "@types/service-worker-mock": "^2.0.1", "wrangler": "^3.0.0" diff --git a/packages/create-cloudflare/templates/queues/ts/package.json b/packages/create-cloudflare/templates/queues/ts/package.json index c918a935e37c..bc8d464b3139 100644 --- a/packages/create-cloudflare/templates/queues/ts/package.json +++ b/packages/create-cloudflare/templates/queues/ts/package.json @@ -8,7 +8,6 @@ "start": "wrangler dev" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230419.0", "typescript": "^5.0.4", "wrangler": "^3.0.0" } diff --git a/packages/create-cloudflare/templates/scheduled/ts/package.json b/packages/create-cloudflare/templates/scheduled/ts/package.json index c918a935e37c..bc8d464b3139 100644 --- a/packages/create-cloudflare/templates/scheduled/ts/package.json +++ b/packages/create-cloudflare/templates/scheduled/ts/package.json @@ -8,7 +8,6 @@ "start": "wrangler dev" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230419.0", "typescript": "^5.0.4", "wrangler": "^3.0.0" }