Skip to content

Commit

Permalink
C3: Use latest version of @cloudflare/workers-types in workers projec…
Browse files Browse the repository at this point in the history
…ts (#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
  • Loading branch information
jculvey authored Nov 30, 2023
1 parent 841041d commit 294ca54
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 43 deletions.
7 changes: 7 additions & 0 deletions .changeset/tidy-planes-peel.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions packages/create-cloudflare/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { C3_DEFAULTS } from "helpers/cli";
import type { C3Args, PagesGeneratorContext as Context } from "types";

export const createTestArgs = (args?: Partial<C3Args>) => {
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,
};
};
116 changes: 116 additions & 0 deletions packages/create-cloudflare/src/__tests__/workers.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
2 changes: 1 addition & 1 deletion packages/create-cloudflare/src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 1 addition & 5 deletions packages/create-cloudflare/src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};

Expand Down
120 changes: 89 additions & 31 deletions packages/create-cloudflare/src/workers.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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 === "<TBD>") {
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 === "<TBD>") {
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*"<TBD>"/m, `name = "${ctx.project.name}"`)
.replace(
/^compatibility_date\s*=\s*"<TBD>"/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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"@cloudflare/itty-router-openapi": "^1.0.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230404.0",
"wrangler": "^3.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"start": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"start": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"start": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
}
Expand Down

0 comments on commit 294ca54

Please sign in to comment.