Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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