Skip to content

Commit ad65efa

Browse files
authored
Add --check flag to wrangler types (#11852)
* Added initial `--check` flag support to `wrangler types` * Added changeset for `--check` flag for `wrangler types` * Minor code cleanup * Switched type tests from Wrangler TOML to JSONC * Minor doc comment improvements * Removed out of date reason from `checkTypesUpToDate` * Minor doc comment tweaks * Minor changeset usage example fix * Moved typegen prefix comments to shared constants * Removed inlined constants from snapshots * Improve argument boolean-ish argument parsing * Update `getRuntimeHeader` to make compat flags optional * Improved type generation helper JSDoc comments * Removed unused import * Minor args existance bug fix * Restore type file regeneration inline snapshot * Add status code checks to `--check` type tests * Minor code review tweaks & fixes * Minor code refactoring * Added default value parameter to `unsafeParseBooleanString` * Revert "Added default value parameter to `unsafeParseBooleanString`" This reverts commit 60c9d21.
1 parent beb96af commit ad65efa

File tree

5 files changed

+341
-10
lines changed

5 files changed

+341
-10
lines changed

.changeset/tame-bobcats-notice.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add `--check` flag to `wrangler types` command
6+
7+
The new `--check` flag allows you to verify that your generated types file is up-to-date without regenerating it. This is useful for CI/CD pipelines, pre-commit hooks, or any scenario where you want to ensure types have been committed after configuration changes.
8+
9+
When types are up-to-date, the command exits with code 0:
10+
11+
```bash
12+
$ wrangler types --check
13+
✨ Types at worker-configuration.d.ts are up to date.
14+
```
15+
16+
When types are out-of-date, the command exits with code 1:
17+
18+
```bash
19+
$ wrangler types --check
20+
✘ [ERROR] Types at worker-configuration.d.ts are out of date. Run `wrangler types` to regenerate.
21+
```
22+
23+
You can also use it with a custom output path:
24+
25+
```bash
26+
$ wrangler types ./custom-types.d.ts --check
27+
```

packages/wrangler/e2e/types.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { readFileSync } from "node:fs";
22
import { writeFile } from "node:fs/promises";
33
import path from "node:path";
4-
import { describe, expect, it } from "vitest";
4+
import { beforeEach, describe, expect, it } from "vitest";
55
import { dedent } from "../src/utils/dedent";
66
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
77

@@ -194,4 +194,108 @@ describe("types", () => {
194194
"
195195
`);
196196
});
197+
198+
describe("--check", () => {
199+
let helper: WranglerE2ETestHelper;
200+
201+
beforeEach(async () => {
202+
helper = new WranglerE2ETestHelper();
203+
await helper.seed(seed);
204+
});
205+
206+
it("should not error when types are up to date", async () => {
207+
await helper.run(`wrangler types`);
208+
const output = await helper.run(`wrangler types --check`);
209+
expect(output.stderr).toBeFalsy();
210+
expect(output.stdout).toContain("up to date");
211+
expect(output.status).toBe(0);
212+
});
213+
214+
it("should error when env types are out of date", async () => {
215+
await helper.run(`wrangler types`);
216+
217+
await helper.seed({
218+
...seed,
219+
"wrangler.jsonc": JSON.stringify({
220+
name: "test-worker",
221+
main: "src/index.ts",
222+
compatibility_date: "2026-01-01",
223+
compatibility_flags: ["nodejs_compat", "no_global_navigator"],
224+
vars: {
225+
NEW_VAR: "new-value",
226+
},
227+
}),
228+
});
229+
230+
const output = await helper.run(`wrangler types --check`);
231+
expect(output.stderr).toContain("out of date");
232+
expect(output.status).toBe(1);
233+
});
234+
235+
it("should error when runtime types are out of date", async () => {
236+
await helper.run(`wrangler types`);
237+
238+
await helper.seed({
239+
...seed,
240+
"wrangler.jsonc": JSON.stringify({
241+
name: "test-worker",
242+
main: "src/index.ts",
243+
compatibility_date: "2026-01-01",
244+
compatibility_flags: ["nodejs_compat"],
245+
vars: {
246+
MY_VAR: "my-var-value",
247+
},
248+
}),
249+
});
250+
251+
const output = await helper.run(`wrangler types --check`);
252+
expect(output.stderr).toContain("out of date");
253+
expect(output.status).toBe(1);
254+
});
255+
256+
it("should work with custom output path", async () => {
257+
await helper.run(`wrangler types ./custom.d.ts`);
258+
259+
const output = await helper.run(`wrangler types ./custom.d.ts --check`);
260+
expect(output.stderr).toBeFalsy();
261+
expect(output.stdout).toContain("./custom.d.ts");
262+
expect(output.stdout).toContain("up to date");
263+
expect(output.status).toBe(0);
264+
});
265+
266+
it("should not error on `--check` if types generated with `--include-env=false`", async () => {
267+
await helper.run(`wrangler types --include-env=false`);
268+
269+
const output = await helper.run(`wrangler types --check`);
270+
expect(output.stderr).toBeFalsy();
271+
expect(output.stdout).toContain("up to date");
272+
expect(output.status).toBe(0);
273+
});
274+
275+
it("should not error on `--check` if types generated with `--include-runtime=false`", async () => {
276+
await helper.run(`wrangler types --include-runtime=false`);
277+
278+
const output = await helper.run(`wrangler types --check`);
279+
expect(output.stderr).toBeFalsy();
280+
expect(output.stdout).toContain("up to date");
281+
expect(output.status).toBe(0);
282+
});
283+
284+
it("should error if types file does not exist", async () => {
285+
const output = await helper.run(`wrangler types --check`);
286+
expect(output.stderr).toContain("not found");
287+
expect(output.status).toBe(1);
288+
});
289+
290+
it("should error if types file was not generated by wrangler", async () => {
291+
await writeFile(
292+
path.join(helper.tmpPath, "worker-configuration.d.ts"),
293+
"// Some other content\ninterface Foo {}"
294+
);
295+
296+
const output = await helper.run(`wrangler types --check`);
297+
expect(output.stderr).toContain("non-Wrangler");
298+
expect(output.status).toBe(1);
299+
});
300+
});
197301
});

packages/wrangler/src/type-generation/helpers.ts

Lines changed: 180 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { readFileSync, writeFileSync } from "node:fs";
2+
import { ParseError, UserError } from "@cloudflare/workers-utils";
23
import { version } from "workerd";
4+
import yargs from "yargs";
5+
import { getEntry } from "../deployment-bundle/entry";
36
import { logger } from "../logger";
47
import { generateRuntimeTypes } from "./runtime";
58
import { generateEnvTypes } from ".";
@@ -8,15 +11,181 @@ import type { Config } from "@cloudflare/workers-utils";
811

912
export const DEFAULT_WORKERS_TYPES_FILE_NAME = "worker-configuration.d.ts";
1013
export const DEFAULT_WORKERS_TYPES_FILE_PATH = `./${DEFAULT_WORKERS_TYPES_FILE_NAME}`;
14+
export const ENV_HEADER_COMMENT_PREFIX = "// Generated by Wrangler by running";
15+
export const RUNTIME_HEADER_COMMENT_PREFIX =
16+
"// Runtime types generated with workerd@";
1117

12-
// Checks the default location for a generated types file and compares if the
13-
// recorded Env hash, workerd version or compat date and flags have changed
14-
// compared to the current values in the config. Prompts user to re-run wrangler
15-
// types if so.
18+
/**
19+
* Generates the runtime header string used in the generated types file.
20+
* This header is used to detect when runtime types need to be regenerated.
21+
*
22+
* @param workerdVersion - The version of workerd to use.
23+
* @param compatibilityDate - The compatibility date of the runtime. Expected `YYYY-MM-DD` format.
24+
* @param compatibilityFlags - Any additional compatibility flags to use.
25+
*
26+
* @returns A string containing the comment outlining the generated runtime types.
27+
*/
28+
export const getRuntimeHeader = (
29+
workerdVersion: string,
30+
compatibilityDate: string,
31+
compatibilityFlags: Array<string> = []
32+
): string => {
33+
return `${RUNTIME_HEADER_COMMENT_PREFIX}${workerdVersion} ${compatibilityDate} ${compatibilityFlags.sort().join(",")}`;
34+
};
35+
36+
/**
37+
* Attempts to convert a boolean serialized as a string.
38+
*
39+
* @param value - The unknown or serialized value.
40+
*
41+
* @returns `true` or `false` depending on the contents of the string.
42+
*
43+
* @throws {ParseError} If the provided value cannot be parsed as a boolean.
44+
*/
45+
const unsafeParseBooleanString = (value: unknown): boolean => {
46+
if (typeof value !== "string") {
47+
throw new ParseError({
48+
text: `Invalid value: ${value}`,
49+
kind: "error",
50+
});
51+
}
52+
53+
if (value.toLowerCase() === "true") {
54+
return true;
55+
}
56+
if (value.toLowerCase() === "false") {
57+
return false;
58+
}
59+
60+
throw new ParseError({
61+
text: `Invalid value: ${value}`,
62+
kind: "error",
63+
});
64+
};
65+
66+
/**
67+
* Determines whether the generated types file is stale compared to the current config.
68+
*
69+
* Checks if the generated types file at the specified path is up-to-date
70+
* by comparing the recorded hash and runtime header with what would be
71+
* generated from the current config.
72+
*
73+
* This function parses the wrangler command from the header to extract
74+
* the original options used for generation, ensuring accurate comparison.
75+
*
76+
* @throws {UserError} If the types file doesn't exist or wasn't generated by Wrangler
77+
*/
78+
export const checkTypesUpToDate = async (
79+
primaryConfig: Config,
80+
typesPath: string = DEFAULT_WORKERS_TYPES_FILE_PATH
81+
): Promise<boolean> => {
82+
let typesFileLines = new Array<string>();
83+
try {
84+
typesFileLines = readFileSync(typesPath, "utf-8").split("\n");
85+
} catch (e) {
86+
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
87+
throw new UserError(`Types file not found at ${typesPath}.`);
88+
}
89+
90+
throw e;
91+
}
92+
93+
const existingEnvHeader = typesFileLines.find((line) =>
94+
line.startsWith(ENV_HEADER_COMMENT_PREFIX)
95+
);
96+
const existingRuntimeHeader = typesFileLines.find((line) =>
97+
line.startsWith(RUNTIME_HEADER_COMMENT_PREFIX)
98+
);
99+
if (!existingEnvHeader && !existingRuntimeHeader) {
100+
throw new UserError(`No generated types found in ${typesPath}.`);
101+
}
102+
103+
const { command: wranglerCommand = "", hash: maybeExistingHash } =
104+
existingEnvHeader?.match(
105+
/\/\/ Generated by Wrangler by running `(?<command>.*)` \(hash: (?<hash>[a-zA-Z0-9]+)\)/
106+
)?.groups ?? {};
107+
108+
// Note: `yargs` doesn't automatically handle aliases, so we check both forms
109+
const rawArgs = yargs(wranglerCommand).parseSync();
110+
111+
// Determine what was included based on what headers exist
112+
// If no env header exists, env types were not included (--include-env=false)
113+
// If no runtime header exists, runtime types were not included (--include-runtime=false)
114+
const args = {
115+
includeEnv: existingEnvHeader
116+
? unsafeParseBooleanString(rawArgs.includeEnv ?? "true")
117+
: false,
118+
includeRuntime: existingRuntimeHeader
119+
? unsafeParseBooleanString(rawArgs.includeRuntime ?? "true")
120+
: false,
121+
envInterface: (rawArgs.envInterface ?? "Env") as string,
122+
strictVars: unsafeParseBooleanString(rawArgs.strictVars ?? "true"),
123+
} satisfies Record<string, string | number | boolean>;
124+
125+
const configContainsEntrypoint =
126+
primaryConfig.main !== undefined || !!primaryConfig.site?.["entry-point"];
127+
128+
const entrypoint = configContainsEntrypoint
129+
? await getEntry({}, primaryConfig, "types").catch(() => undefined)
130+
: undefined;
131+
132+
let envOutOfDate = false;
133+
let runtimeOutOfDate = false;
134+
135+
// Check if env types are out of date
136+
if (args.includeEnv) {
137+
try {
138+
const { envHeader } = await generateEnvTypes(
139+
primaryConfig,
140+
{ strictVars: args.strictVars },
141+
args.envInterface,
142+
typesPath,
143+
entrypoint,
144+
new Map(),
145+
false // don't log anything
146+
);
147+
const newHash = envHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;
148+
envOutOfDate = maybeExistingHash !== newHash;
149+
} catch {
150+
// If we can't generate env types for comparison, consider them out of date
151+
envOutOfDate = true;
152+
}
153+
}
154+
155+
// Check if runtime types are out of date
156+
if (args.includeRuntime) {
157+
if (!primaryConfig.compatibility_date) {
158+
// If no compatibility date, we can't check runtime types
159+
runtimeOutOfDate = true;
160+
} else {
161+
const newRuntimeHeader = getRuntimeHeader(
162+
version,
163+
primaryConfig.compatibility_date,
164+
primaryConfig.compatibility_flags
165+
);
166+
runtimeOutOfDate = existingRuntimeHeader !== newRuntimeHeader;
167+
}
168+
}
169+
170+
return envOutOfDate || runtimeOutOfDate;
171+
};
172+
173+
/**
174+
* Detects stale types during `wrangler dev` and optionally regenerates them.
175+
*
176+
* Checks the default location for a generated types file and compares if the
177+
* recorded Env hash, workerd version or compat date and flags have changed
178+
* compared to the current values in the config. Prompts user to re-run wrangler
179+
* types if so, or automatically regenerates them during `wrangler dev` if
180+
* `dev.generate_types` is enabled.
181+
*
182+
* This is used during `wrangler dev` to detect out-of-date types.
183+
*/
16184
export const checkTypesDiff = async (config: Config, entry: Entry) => {
17185
if (!entry.file.endsWith(".ts")) {
18186
return;
19187
}
188+
20189
let maybeExistingTypesFileLines: string[];
21190
try {
22191
// Checking the default location only
@@ -27,8 +196,9 @@ export const checkTypesDiff = async (config: Config, entry: Entry) => {
27196
} catch {
28197
return;
29198
}
199+
30200
const existingEnvHeader = maybeExistingTypesFileLines.find((line) =>
31-
line.startsWith("// Generated by Wrangler by running")
201+
line.startsWith(ENV_HEADER_COMMENT_PREFIX)
32202
);
33203
const maybeExistingHash =
34204
existingEnvHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;
@@ -63,7 +233,11 @@ export const checkTypesDiff = async (config: Config, entry: Entry) => {
63233
const existingRuntimeHeader = maybeExistingTypesFileLines.find((line) =>
64234
line.startsWith("// Runtime types generated with")
65235
);
66-
const newRuntimeHeader = `// Runtime types generated with workerd@${version} ${config.compatibility_date} ${config.compatibility_flags.sort().join(",")}`;
236+
const newRuntimeHeader = getRuntimeHeader(
237+
version,
238+
config.compatibility_date ?? "",
239+
config.compatibility_flags ?? []
240+
);
67241

68242
const envOutOfDate = existingEnvHeader && maybeExistingHash !== newHash;
69243
const runtimeOutOfDate =

0 commit comments

Comments
 (0)