diff --git a/.changeset/runtime-types-dev-cache-marker.md b/.changeset/runtime-types-dev-cache-marker.md new file mode 100644 index 0000000000..161a0f9653 --- /dev/null +++ b/.changeset/runtime-types-dev-cache-marker.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fix runtime type caching when `wrangler dev` auto-regenerates types + +When `dev.generate_types` (or `wrangler dev --types`) regenerated an out-of-date `worker-configuration.d.ts`, the written file omitted the `// Begin runtime types` marker (and the `/* eslint-disable */` header) that `wrangler types` writes. As a result, later runs could not detect the cached runtime types and always regenerated them. The auto-regenerated file now matches `wrangler types` output, restoring the cache. diff --git a/packages/runtime-types/package.json b/packages/runtime-types/package.json new file mode 100644 index 0000000000..35b199fc36 --- /dev/null +++ b/packages/runtime-types/package.json @@ -0,0 +1,45 @@ +{ + "name": "@cloudflare/runtime-types", + "version": "0.0.0", + "private": true, + "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/runtime-types#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/runtime-types" + }, + "files": [ + "dist" + ], + "type": "module", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "scripts": { + "build": "tsdown", + "check:type": "tsc", + "dev": "tsdown --watch", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "miniflare": "workspace:*", + "workerd": "catalog:default" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-utils": "workspace:*", + "@types/node": "catalog:default", + "tsdown": "0.16.3", + "typescript": "catalog:default", + "vitest": "catalog:default" + } +} diff --git a/packages/runtime-types/src/__tests__/header.test.ts b/packages/runtime-types/src/__tests__/header.test.ts new file mode 100644 index 0000000000..f6715a51a1 --- /dev/null +++ b/packages/runtime-types/src/__tests__/header.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "vitest"; +import { getRuntimeHeader, RUNTIME_HEADER_COMMENT_PREFIX } from "../header"; + +describe("getRuntimeHeader", () => { + it("includes the workerd version and compatibility date", ({ expect }) => { + expect(getRuntimeHeader("1.0.0-test", "2024-11-06")).toBe( + `${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 ` + ); + }); + + it("sorts compatibility flags alphabetically", ({ expect }) => { + expect(getRuntimeHeader("1.0.0-test", "2024-11-06", ["b", "a", "c"])).toBe( + `${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 a,b,c` + ); + }); + + it("defaults to no flags when none are provided", ({ expect }) => { + expect(getRuntimeHeader("1.0.0-test", "2024-11-06", [])).toBe( + `${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 ` + ); + }); +}); diff --git a/packages/runtime-types/src/__tests__/runtime.test.ts b/packages/runtime-types/src/__tests__/runtime.test.ts new file mode 100644 index 0000000000..04130b904a --- /dev/null +++ b/packages/runtime-types/src/__tests__/runtime.test.ts @@ -0,0 +1,162 @@ +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { getRuntimeHeader, RUNTIME_TYPES_MARKER } from "../header"; +import { generateRuntimeTypes } from "../runtime"; + +const OUT_FILE = "worker-configuration.d.ts"; + +const WORKERD_VERSION = "1.0.0-test"; + +const { MiniflareMock, dispatchFetchMock, disposeMock } = vi.hoisted(() => { + const dispatch = vi.fn(); + const dispose = vi.fn(); + const constructor = vi.fn(function () { + return { + dispatchFetch: dispatch, + dispose: dispose, + }; + }); + return { + MiniflareMock: constructor, + dispatchFetchMock: dispatch, + disposeMock: dispose, + }; +}); + +vi.mock("workerd", () => ({ + // Must be a literal: vi.mock factories are hoisted above top-level variables. + version: "1.0.0-test", + default: "/fake/workerd", +})); +vi.mock("miniflare", () => ({ Miniflare: MiniflareMock })); + +describe("generateRuntimeTypes", () => { + runInTempDir(); + + beforeEach(() => { + vi.clearAllMocks(); + dispatchFetchMock.mockResolvedValue({ + ok: true, + text: async () => "GENERATED", + }); + }); + + it("generates types when the out file does not exist (cache miss)", async ({ + expect, + }) => { + const result = await generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + outFile: OUT_FILE, + }); + + expect(result).toEqual({ + runtimeHeader: getRuntimeHeader(WORKERD_VERSION, "2024-11-06", []), + runtimeTypes: "GENERATED", + isCached: false, + }); + expect(MiniflareMock).toHaveBeenCalledTimes(1); + }); + + it("strips nodejs_compat flags from the dispatch URL but keeps them in the header", async ({ + expect, + }) => { + const result = await generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + compatibilityFlags: ["nodejs_compat", "flag_b", "flag_a"], + outFile: OUT_FILE, + }); + + // nodejs_compat flags are stripped from the dispatch URL; the remaining + // flags keep their original caller-provided order. + expect(dispatchFetchMock).toHaveBeenCalledWith( + "http://dummy.com/2024-11-06+flag_b+flag_a" + ); + expect(result.runtimeHeader).toBe( + getRuntimeHeader(WORKERD_VERSION, "2024-11-06", [ + "nodejs_compat", + "flag_b", + "flag_a", + ]) + ); + }); + + it("returns cached types when the header and marker match", async ({ + expect, + }) => { + const header = getRuntimeHeader(WORKERD_VERSION, "2024-11-06", ["flag_a"]); + await seed({ + [OUT_FILE]: `${header}\nsome preamble\n${RUNTIME_TYPES_MARKER}\nCACHED TYPES\nmore`, + }); + + const result = await generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + compatibilityFlags: ["flag_a"], + outFile: OUT_FILE, + }); + + expect(result).toEqual({ + runtimeHeader: header, + runtimeTypes: "CACHED TYPES\nmore", + isCached: true, + }); + expect(MiniflareMock).not.toHaveBeenCalled(); + }); + + it("regenerates when the cached header is stale", async ({ expect }) => { + const staleHeader = getRuntimeHeader(WORKERD_VERSION, "2020-01-01", []); + await seed({ + [OUT_FILE]: `${staleHeader}\n${RUNTIME_TYPES_MARKER}\nOLD`, + }); + + const result = await generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + outFile: OUT_FILE, + }); + + expect(result.isCached).toBe(false); + expect(result.runtimeTypes).toBe("GENERATED"); + expect(MiniflareMock).toHaveBeenCalledTimes(1); + }); + + it("regenerates when the marker is missing even if the header matches", async ({ + expect, + }) => { + const header = getRuntimeHeader(WORKERD_VERSION, "2024-11-06", []); + await seed({ [OUT_FILE]: `${header}\nNO MARKER HERE` }); + + const result = await generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + outFile: OUT_FILE, + }); + + expect(result.isCached).toBe(false); + expect(MiniflareMock).toHaveBeenCalledTimes(1); + }); + + it("rejects and still disposes Miniflare when the response is not ok", async ({ + expect, + }) => { + dispatchFetchMock.mockResolvedValue({ + ok: false, + text: async () => "boom", + }); + + await expect( + generateRuntimeTypes({ + compatibilityDate: "2024-11-06", + outFile: OUT_FILE, + }) + ).rejects.toThrow("boom"); + expect(disposeMock).toHaveBeenCalledTimes(1); + }); + + it("rethrows non-ENOENT read errors without generating", async ({ + expect, + }) => { + // Passing a directory as the out file triggers EISDIR on read. + await expect( + generateRuntimeTypes({ compatibilityDate: "2024-11-06", outFile: "." }) + ).rejects.toThrow(); + expect(MiniflareMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/runtime-types/src/header.ts b/packages/runtime-types/src/header.ts new file mode 100644 index 0000000000..9ec9cc7bd9 --- /dev/null +++ b/packages/runtime-types/src/header.ts @@ -0,0 +1,25 @@ +/** + * Prefix of the comment written above the generated runtime types. Used to + * detect when runtime types need to be regenerated (the full header encodes the + * workerd version, compatibility date and flags). + */ +export const RUNTIME_HEADER_COMMENT_PREFIX = + "// Runtime types generated with workerd@"; + +/** + * Marker line written immediately before the generated runtime types. Used to + * locate the start of the runtime section within a combined `.d.ts` file. + */ +export const RUNTIME_TYPES_MARKER = "// Begin runtime types"; + +/** + * Generates the runtime header string used in the generated types file. + * This header is used to detect when runtime types need to be regenerated. + */ +export function getRuntimeHeader( + workerdVersion: string, + compatibilityDate: string, + compatibilityFlags: string[] = [] +): string { + return `${RUNTIME_HEADER_COMMENT_PREFIX}${workerdVersion} ${compatibilityDate} ${[...compatibilityFlags].sort().join(",")}`; +} diff --git a/packages/runtime-types/src/index.ts b/packages/runtime-types/src/index.ts new file mode 100644 index 0000000000..cf7604f21f --- /dev/null +++ b/packages/runtime-types/src/index.ts @@ -0,0 +1,6 @@ +export { + getRuntimeHeader, + RUNTIME_HEADER_COMMENT_PREFIX, + RUNTIME_TYPES_MARKER, +} from "./header"; +export { generateRuntimeTypes } from "./runtime"; diff --git a/packages/runtime-types/src/runtime.ts b/packages/runtime-types/src/runtime.ts new file mode 100644 index 0000000000..77a71146e0 --- /dev/null +++ b/packages/runtime-types/src/runtime.ts @@ -0,0 +1,132 @@ +import * as fsp from "node:fs/promises"; +import { createRequire } from "node:module"; +import { Miniflare } from "miniflare"; +import { version } from "workerd"; +import { + getRuntimeHeader, + RUNTIME_HEADER_COMMENT_PREFIX, + RUNTIME_TYPES_MARKER, +} from "./header"; + +// `require.resolve` is not available in native ESM, so reconstruct it from the +// current module URL. This resolves correctly both when this package is run +// directly (ESM) and when it is bundled into a consumer. +const require = createRequire(import.meta.url); + +/** + * Generates runtime types for a Workers project based on the provided compatibility settings. + * + * This function is designed to be isolated and portable, making it easy to integrate into various + * build processes or development workflows. It handles the whole process of generating runtime + * types, from spawning the workerd process (via Miniflare) to returning the generated types. + * + * If an `outFile` already contains runtime types generated for the same workerd version, + * compatibility date and flags, the cached types are returned instead of regenerating them. + * + * @example + * import { generateRuntimeTypes } from "@cloudflare/runtime-types"; + * + * const { runtimeHeader, runtimeTypes, isCached } = await generateRuntimeTypes({ + * compatibilityDate: "2024-11-06", + * compatibilityFlags: ["nodejs_compat"], + * outFile: "worker-configuration.d.ts", + * }); + * + * @remarks + * `nodejs_compat` flags are ignored as there is currently no mechanism to generate these + * dynamically; consumers should instead prompt users to install `@types/node`. + */ +export async function generateRuntimeTypes({ + compatibilityDate, + compatibilityFlags = [], + outFile, +}: { + compatibilityDate: string; + compatibilityFlags?: string[]; + outFile: string; +}): Promise<{ + runtimeHeader: string; + runtimeTypes: string; + isCached: boolean; +}> { + const header = getRuntimeHeader( + version, + compatibilityDate, + compatibilityFlags + ); + + try { + const lines = (await fsp.readFile(outFile, "utf8")).split("\n"); + const existingHeader = lines.find((line) => + line.startsWith(RUNTIME_HEADER_COMMENT_PREFIX) + ); + const existingTypesStart = lines.findIndex( + (line) => line === RUNTIME_TYPES_MARKER + ); + if (existingHeader === header && existingTypesStart !== -1) { + return { + runtimeHeader: header, + runtimeTypes: lines.slice(existingTypesStart + 1).join("\n"), + isCached: true, + }; + } + } catch (e) { + if ((e as { code: string }).code !== "ENOENT") { + throw e; + } + } + + const types = await generate({ + compatibilityDate, + // Ignore nodejs compat flags as there is currently no mechanism to generate these dynamically. + compatibilityFlags: compatibilityFlags.filter( + (flag) => !flag.includes("nodejs_compat") + ), + }); + + return { runtimeHeader: header, runtimeTypes: types, isCached: false }; +} + +/** + * Generates runtime types for Cloudflare Workers by spawning a workerd process with the type-generation + * worker, and then making a request to that worker to fetch types. + */ +async function generate({ + compatibilityDate, + compatibilityFlags = [], +}: { + compatibilityDate: string; + compatibilityFlags?: string[]; +}) { + const worker = ( + await fsp.readFile(require.resolve("workerd/worker.mjs")) + ).toString(); + const mf = new Miniflare({ + // Must stay before the 2024-09-23 nodejs_compat v1->v2 switchover: the + // workerd RTTI worker only runs under nodejs_compat v1. This date is + // internal to type generation and never appears in the output/header. + compatibilityDate: "2024-01-01", + compatibilityFlags: ["nodejs_compat", "rtti_api"], + modules: true, + script: worker, + }); + + const flagsString = compatibilityFlags.length + ? `+${compatibilityFlags.join("+")}` + : ""; + + const path = `http://dummy.com/${compatibilityDate}${flagsString}`; + + try { + const res = await mf.dispatchFetch(path); + const text = await res.text(); + + if (!res.ok) { + throw new Error(text); + } + + return text; + } finally { + await mf.dispose(); + } +} diff --git a/packages/runtime-types/src/workerd.d.ts b/packages/runtime-types/src/workerd.d.ts new file mode 100644 index 0000000000..1b6e6e04e3 --- /dev/null +++ b/packages/runtime-types/src/workerd.d.ts @@ -0,0 +1,6 @@ +declare module "workerd" { + const path: string; + export default path; + export const compatibilityDate: string; + export const version: string; +} diff --git a/packages/runtime-types/tsconfig.json b/packages/runtime-types/tsconfig.json new file mode 100644 index 0000000000..99e994d3b1 --- /dev/null +++ b/packages/runtime-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/base.json", + "compilerOptions": { + "types": ["@types/node"] + }, + "include": ["src"] +} diff --git a/packages/runtime-types/tsdown.config.ts b/packages/runtime-types/tsdown.config.ts new file mode 100644 index 0000000000..0c9545f089 --- /dev/null +++ b/packages/runtime-types/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + }, + platform: "node", + outDir: "dist", + dts: true, + tsconfig: "tsconfig.json", +}); diff --git a/packages/runtime-types/turbo.json b/packages/runtime-types/turbo.json new file mode 100644 index 0000000000..6556dcf3e5 --- /dev/null +++ b/packages/runtime-types/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/runtime-types/vitest.config.ts b/packages/runtime-types/vitest.config.ts new file mode 100644 index 0000000000..0c9f1f8e04 --- /dev/null +++ b/packages/runtime-types/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/__tests__/**/*.test.ts"], + reporters: ["default"], + }, +}); diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 1739722606..dcb900f12d 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -96,6 +96,7 @@ "@cloudflare/containers-shared": "workspace:*", "@cloudflare/deploy-helpers": "workspace:*", "@cloudflare/pages-shared": "workspace:^", + "@cloudflare/runtime-types": "workspace:*", "@cloudflare/types": "6.18.4", "@cloudflare/workers-auth": "workspace:*", "@cloudflare/workers-shared": "workspace:*", diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index 0b7b20dd9f..efc5449065 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -2976,6 +2976,8 @@ describe.sequential("wrangler dev", () => { ); expect(typesContent).not.toContain("old-hash-value"); expect(typesContent).toContain("NEW_VAR"); + expect(typesContent).toContain("// Begin runtime types"); + expect(typesContent).toContain("/* eslint-disable */"); }); it("should not warn about types if the types file does not exist", async ({ diff --git a/packages/wrangler/src/type-generation/helpers.ts b/packages/wrangler/src/type-generation/helpers.ts index abf0b7dc0b..df74dfadb6 100644 --- a/packages/wrangler/src/type-generation/helpers.ts +++ b/packages/wrangler/src/type-generation/helpers.ts @@ -1,4 +1,9 @@ import { readFileSync, writeFileSync } from "node:fs"; +import { + getRuntimeHeader, + RUNTIME_HEADER_COMMENT_PREFIX, + RUNTIME_TYPES_MARKER, +} from "@cloudflare/runtime-types"; import { configFileName, ParseError, @@ -15,8 +20,6 @@ import type { Config, Entry } from "@cloudflare/workers-utils"; export const DEFAULT_WORKERS_TYPES_FILE_NAME = "worker-configuration.d.ts"; export const DEFAULT_WORKERS_TYPES_FILE_PATH = `./${DEFAULT_WORKERS_TYPES_FILE_NAME}`; export const ENV_HEADER_COMMENT_PREFIX = "// Generated by Wrangler by running"; -export const RUNTIME_HEADER_COMMENT_PREFIX = - "// Runtime types generated with workerd@"; /** * Sentinel value used to identify top-level (non-environment) bindings when collecting bindings. @@ -90,24 +93,6 @@ export const getEnvHeader = (hash: string, command?: string): string => { return `${ENV_HEADER_COMMENT_PREFIX} \`${wranglerCommand}\` (hash: ${hash})`; }; -/** - * Generates the runtime header string used in the generated types file. - * This header is used to detect when runtime types need to be regenerated. - * - * @param workerdVersion - The version of workerd to use. - * @param compatibilityDate - The compatibility date of the runtime. Expected `YYYY-MM-DD` format. - * @param compatibilityFlags - Any additional compatibility flags to use. - * - * @returns A string containing the comment outlining the generated runtime types. - */ -export const getRuntimeHeader = ( - workerdVersion: string, - compatibilityDate: string, - compatibilityFlags: Array = [] -): string => { - return `${RUNTIME_HEADER_COMMENT_PREFIX}${workerdVersion} ${compatibilityDate} ${compatibilityFlags.sort().join(",")}`; -}; - /** * Attempts to convert a boolean serialized as a string. * @@ -385,10 +370,11 @@ export const checkTypesDiff = async (config: Config, entry: Entry) => { outFile: DEFAULT_WORKERS_TYPES_FILE_PATH, }); const newTypesFile = [ + "/* eslint-disable */", newEnvHeader, runtimeHeader, newEnvTypes, - runtimeTypes, + `${RUNTIME_TYPES_MARKER}\n${runtimeTypes}`, ].join("\n"); try { writeFileSync(DEFAULT_WORKERS_TYPES_FILE_PATH, newTypesFile); diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 603c30fc57..e3cd8cb139 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import * as fs from "node:fs"; import { basename, dirname, extname, join, relative, resolve } from "node:path"; +import { RUNTIME_TYPES_MARKER } from "@cloudflare/runtime-types"; import { CommandLineArgsError, configFileName, @@ -529,7 +530,7 @@ async function generateTypesFromResolvedOptions( }); runtime = runtimeTypes; header.push(runtimeHeader); - content.push(`// Begin runtime types\n${runtimeTypes}`); + content.push(`${RUNTIME_TYPES_MARKER}\n${runtimeTypes}`); if (log) { logger.log(chalk.dim("Runtime types generated.\n")); } diff --git a/packages/wrangler/src/type-generation/runtime/index.ts b/packages/wrangler/src/type-generation/runtime/index.ts index 05fa453d57..31546c6b6a 100644 --- a/packages/wrangler/src/type-generation/runtime/index.ts +++ b/packages/wrangler/src/type-generation/runtime/index.ts @@ -1,9 +1,5 @@ -import { readFileSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { Miniflare } from "miniflare"; -import { version } from "workerd"; +import { generateRuntimeTypes as generateRuntimeTypesImpl } from "@cloudflare/runtime-types"; import { logger } from "../../logger"; -import { getRuntimeHeader, RUNTIME_HEADER_COMMENT_PREFIX } from "../helpers"; import type { Config } from "@cloudflare/workers-utils"; const DEFAULT_OUTFILE_RELATIVE_PATH = "worker-configuration.d.ts"; @@ -11,10 +7,9 @@ const DEFAULT_OUTFILE_RELATIVE_PATH = "worker-configuration.d.ts"; /** * Generates runtime types for a Workers project based on the provided project configuration. * - * This function is designed to be isolated and portable, making it easy to integrate into various - * build processes or development workflows. It handles the whole process of generating runtime - * types, from ensuring the output directory exists to spawning the workerd process (via Miniflare) - * and writing the generated types to a file. + * Thin adapter around `@cloudflare/runtime-types`' `generateRuntimeTypes` that maps wrangler's + * snake_case `Config` shape onto the shared generator. Kept as a local module so that existing + * tests can spy on `generateRuntimeTypes` via `import * as generateRuntime from "./runtime"`. * * @throws {Error} If the config file does not have a compatibility date. * @@ -22,21 +17,9 @@ const DEFAULT_OUTFILE_RELATIVE_PATH = "worker-configuration.d.ts"; * import { generateRuntimeTypes } from './path/to/this/file'; * import { readConfig } from './path/to/config'; * - * const configPath = './wrangler.toml'; - * const config = readConfig(configPath); - * const outFile = '/Users/me/my-project/dist/runtime.d.ts' - * - * // This will generate runtime types and write them to ./.wrangler/types/runtime.d.ts + * const config = readConfig('./wrangler.toml'); * await generateRuntimeTypes({ config }); - * - * * // This will generate runtime types and write them to /Users/me/my-project/dist/runtime.d.ts - * await generateRuntimeTypes({ config, outFile }); - * - * @remarks - * - The generated types are written to a file specified by DEFAULT_OUTFILE_RELATIVE_PATH. - * - This could be improved by hashing the compat date and flags to avoid unnecessary regeneration. */ - export async function generateRuntimeTypes({ config: { compatibility_date, compatibility_flags = [] }, outFile = DEFAULT_OUTFILE_RELATIVE_PATH, @@ -48,80 +31,16 @@ export async function generateRuntimeTypes({ throw new Error("Config must have a compatibility date."); } - const header = getRuntimeHeader( - version, - compatibility_date, - compatibility_flags - ); - - try { - const lines = (await readFile(outFile, "utf8")).split("\n"); - const existingHeader = lines.find((line) => - line.startsWith(RUNTIME_HEADER_COMMENT_PREFIX) - ); - const existingTypesStart = lines.findIndex( - (line) => line === "// Begin runtime types" - ); - if (existingHeader === header && existingTypesStart !== -1) { - logger.debug("Using cached runtime types: ", header); + const { runtimeHeader, runtimeTypes, isCached } = + await generateRuntimeTypesImpl({ + compatibilityDate: compatibility_date, + compatibilityFlags: compatibility_flags, + outFile, + }); - return { - runtimeHeader: header, - runtimeTypes: lines.slice(existingTypesStart + 1).join("\n"), - }; - } - } catch (e) { - if ((e as { code: string }).code !== "ENOENT") { - throw e; - } + if (isCached) { + logger.debug("Using cached runtime types: ", runtimeHeader); } - const types = await generate({ - compatibilityDate: compatibility_date, - // Ignore nodejs compat flags as there is currently no mechanism to generate these dynamically. - compatibilityFlags: compatibility_flags.filter( - (flag) => !flag.includes("nodejs_compat") - ), - }); - - return { runtimeHeader: header, runtimeTypes: types }; -} - -/** - * Generates runtime types for Cloudflare Workers by spawning a workerd process with the type-generation - * worker, and then making a request to that worker to fetch types. - */ -async function generate({ - compatibilityDate, - compatibilityFlags = [], -}: { - compatibilityDate: string; - compatibilityFlags?: string[]; -}) { - const worker = readFileSync(require.resolve("workerd/worker.mjs")).toString(); - const mf = new Miniflare({ - compatibilityDate: "2024-01-01", - compatibilityFlags: ["nodejs_compat", "rtti_api"], - modules: true, - script: worker, - }); - - const flagsString = compatibilityFlags.length - ? `+${compatibilityFlags.join("+")}` - : ""; - - const path = `http://dummy.com/${compatibilityDate}${flagsString}`; - - try { - const res = await mf.dispatchFetch(path); - const text = await res.text(); - - if (!res.ok) { - throw new Error(text); - } - - return text; - } finally { - await mf.dispose(); - } + return { runtimeHeader, runtimeTypes }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee0e24039b..4658ab1598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2559,6 +2559,34 @@ importers: specifier: ^3.5.0 version: 3.5.0(esbuild@0.28.1) + packages/runtime-types: + dependencies: + miniflare: + specifier: workspace:* + version: link:../miniflare + workerd: + specifier: catalog:default + version: 1.20260708.1 + devDependencies: + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../workers-tsconfig + '@cloudflare/workers-utils': + specifier: workspace:* + version: link:../workers-utils + '@types/node': + specifier: 22.15.17 + version: 22.15.17 + tsdown: + specifier: 0.16.3 + version: 0.16.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(ms@2.1.3)(synckit@0.11.12)(typescript@5.8.3) + typescript: + specifier: catalog:default + version: 5.8.3 + vitest: + specifier: catalog:default + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + packages/solarflare-theme: {} packages/turbo-r2-archive: @@ -4322,6 +4350,9 @@ importers: '@cloudflare/pages-shared': specifier: workspace:^ version: link:../pages-shared + '@cloudflare/runtime-types': + specifier: workspace:* + version: link:../runtime-types '@cloudflare/types': specifier: 6.18.4 version: 6.18.4(react@19.2.4)