11import { readFileSync , writeFileSync } from "node:fs" ;
2+ import { ParseError , UserError } from "@cloudflare/workers-utils" ;
23import { version } from "workerd" ;
4+ import yargs from "yargs" ;
5+ import { getEntry } from "../deployment-bundle/entry" ;
36import { logger } from "../logger" ;
47import { generateRuntimeTypes } from "./runtime" ;
58import { generateEnvTypes } from "." ;
@@ -8,15 +11,181 @@ import type { Config } from "@cloudflare/workers-utils";
811
912export const DEFAULT_WORKERS_TYPES_FILE_NAME = "worker-configuration.d.ts" ;
1013export 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+ / \/ \/ G e n e r a t e d b y W r a n g l e r b y r u n n i n g ` (?< command > .* ) ` \( h a s h : (?< hash > [ a - z A - Z 0 - 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 ( / h a s h : (?< 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+ */
16184export 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 ( / h a s h : (?< 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