Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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/runtime-types-dev-cache-marker.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions packages/runtime-types/package.json
Original file line number Diff line number Diff line change
@@ -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",
Comment thread
NuroDev marked this conversation as resolved.
"typescript": "catalog:default",
"vitest": "catalog:default"
}
}
22 changes: 22 additions & 0 deletions packages/runtime-types/src/__tests__/header.test.ts
Original file line number Diff line number Diff line change
@@ -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 `
);
});
});
162 changes: 162 additions & 0 deletions packages/runtime-types/src/__tests__/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
25 changes: 25 additions & 0 deletions packages/runtime-types/src/header.ts
Original file line number Diff line number Diff line change
@@ -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(",")}`;
}
6 changes: 6 additions & 0 deletions packages/runtime-types/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
getRuntimeHeader,
RUNTIME_HEADER_COMMENT_PREFIX,
RUNTIME_TYPES_MARKER,
} from "./header";
export { generateRuntimeTypes } from "./runtime";
Loading
Loading