Skip to content

Commit 2ef9aa4

Browse files
committed
check and prompt update
1 parent 9744261 commit 2ef9aa4

File tree

6 files changed

+195
-99
lines changed

6 files changed

+195
-99
lines changed

packages/wrangler/e2e/types.test.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe("types", () => {
7171
.split("\n");
7272

7373
expect(file[0]).toMatchInlineSnapshot(
74-
`"// Generated by Wrangler by running \`wrangler types ./types.d.ts\`"`
74+
`"// Generated by Wrangler by running \`wrangler types ./types.d.ts\` (hash: e82ba4d7b995dd9ca6fb0332d81f889b)"`
7575
);
7676
expect(file[1]).match(
7777
/\/\/ Runtime types generated with workerd@1\.\d+\.\d \d\d\d\d-\d\d-\d\d ([a-z_]+,?)*/
@@ -96,29 +96,43 @@ describe("types", () => {
9696
"FAKE RUNTIME",
9797
].join("\n")
9898
);
99-
console.log(
100-
[
101-
file[0],
102-
file[1],
103-
"FAKE ENV",
104-
"// Begin runtime types",
105-
"FAKE RUNTIME",
106-
].join("\n")
107-
);
10899

109100
await helper.run(`wrangler types`);
110101

111102
const file2 = (await readFile(typesPath)).toString();
112103

113-
expect(file2).toMatchInlineSnapshot(`
114-
"// Generated by Wrangler by running \`wrangler types\`
115-
// Runtime types generated with [email protected] 2023-01-01 nodejs_compat,no_global_navigator
116-
// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type
117-
interface Env {
118-
}
104+
// regenerates env types
105+
expect(file2).toContain("interface Env {");
106+
// uses cached runtime types
107+
expect(file2).toContain("// Begin runtime types");
108+
expect(file2).toContain("FAKE RUNTIME");
109+
});
119110

120-
// Begin runtime types
121-
FAKE RUNTIME"
122-
`);
111+
it("should prompt you to update types if they've been changed", async () => {
112+
const helper = new WranglerE2ETestHelper();
113+
await helper.seed(seed);
114+
await helper.run(`wrangler types`);
115+
seed["wrangler.toml"] = dedent`
116+
name = "test-worker"
117+
main = "src/index.ts"
118+
compatibility_date = "2023-01-01"
119+
compatibility_flags = ["nodejs_compat", "no_global_navigator"]
120+
[vars]
121+
BEEP = "BOOP"
122+
`;
123+
await helper.seed(seed);
124+
const worker = helper.runLongLived("wrangler dev");
125+
await worker.readUntil(/ It looks like your types might be out of date./);
126+
seed["wrangler.toml"] = dedent`
127+
name = "test-worker"
128+
main = "src/index.ts"
129+
compatibility_date = "2023-01-01"
130+
compatibility_flags = ["nodejs_compat"]
131+
[vars]
132+
BEEP = "BOOP"
133+
ASDf = "ADSfadsf"
134+
`;
135+
await helper.seed(seed);
136+
await worker.readUntil(/ It looks like your types might be out of date./);
123137
});
124138
});

packages/wrangler/src/__tests__/type-generation.test.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,18 @@ describe("generate types", () => {
451451
expect(fs.existsSync("./worker-configuration.d.ts")).toBe(true);
452452
expect(fs.readFileSync("./worker-configuration.d.ts", "utf-8"))
453453
.toMatchInlineSnapshot(`
454-
"// Generated by Wrangler by running \`wrangler\`
455-
// Runtime types generated with workerd@
456-
interface Env {
457-
SOMETHING: \\"asdasdfasdf\\";
458-
ANOTHER: \\"thing\\";
459-
\\"some-other-var\\": \\"some-other-value\\";
460-
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
461-
}
454+
"// Generated by Wrangler by running \`wrangler\` (hash: 1baaf305bc7d12c97f589487a5ecfafb)
455+
// Runtime types generated with workerd@
456+
interface Env {
457+
SOMETHING: \\"asdasdfasdf\\";
458+
ANOTHER: \\"thing\\";
459+
\\"some-other-var\\": \\"some-other-value\\";
460+
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
461+
}
462462
463-
// Begin runtime types
464-
<runtime types go here>"
465-
`);
463+
// Begin runtime types
464+
<runtime types go here>"
465+
`);
466466
});
467467

468468
describe("when nothing was found", () => {
@@ -1028,18 +1028,18 @@ describe("generate types", () => {
10281028

10291029
expect(fs.readFileSync("./cloudflare-env.d.ts", "utf-8"))
10301030
.toMatchInlineSnapshot(`
1031-
"// Generated by Wrangler by running \`wrangler\`
1032-
// Runtime types generated with workerd@
1033-
interface Env {
1034-
SOMETHING: \\"asdasdfasdf\\";
1035-
ANOTHER: \\"thing\\";
1036-
\\"some-other-var\\": \\"some-other-value\\";
1037-
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
1038-
}
1031+
"// Generated by Wrangler by running \`wrangler\` (hash: 1baaf305bc7d12c97f589487a5ecfafb)
1032+
// Runtime types generated with workerd@
1033+
interface Env {
1034+
SOMETHING: \\"asdasdfasdf\\";
1035+
ANOTHER: \\"thing\\";
1036+
\\"some-other-var\\": \\"some-other-value\\";
1037+
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
1038+
}
10391039
1040-
// Begin runtime types
1041-
<runtime types go here>"
1042-
`);
1040+
// Begin runtime types
1041+
<runtime types go here>"
1042+
`);
10431043
});
10441044

10451045
it("should error if the user points to a non-d.ts file", async () => {
@@ -1082,18 +1082,18 @@ describe("generate types", () => {
10821082

10831083
expect(fs.readFileSync("./my-cloudflare-env-interface.d.ts", "utf-8"))
10841084
.toMatchInlineSnapshot(`
1085-
"// Generated by Wrangler by running \`wrangler\`
1086-
// Runtime types generated with workerd@
1087-
interface MyCloudflareEnvInterface {
1088-
SOMETHING: \\"asdasdfasdf\\";
1089-
ANOTHER: \\"thing\\";
1090-
\\"some-other-var\\": \\"some-other-value\\";
1091-
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
1092-
}
1085+
"// Generated by Wrangler by running \`wrangler\` (hash: 6b0c21549436b64a143346d5df73ba4d)
1086+
// Runtime types generated with workerd@
1087+
interface MyCloudflareEnvInterface {
1088+
SOMETHING: \\"asdasdfasdf\\";
1089+
ANOTHER: \\"thing\\";
1090+
\\"some-other-var\\": \\"some-other-value\\";
1091+
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
1092+
}
10931093
1094-
// Begin runtime types
1095-
<runtime types go here>"
1096-
`);
1094+
// Begin runtime types
1095+
<runtime types go here>"
1096+
`);
10971097
});
10981098
});
10991099

packages/wrangler/src/api/startDevWorker/ConfigController.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getClassNamesWhichUseSQLite } from "../../dev/class-names-sqlite";
1414
import { getLocalPersistencePath } from "../../dev/get-local-persistence-path";
1515
import { UserError } from "../../errors";
1616
import { logger } from "../../logger";
17+
import { checkTypesDiff } from "../../type-generation/helpers";
1718
import { requireApiToken, requireAuth } from "../../user";
1819
import {
1920
DEFAULT_INSPECTOR_PORT,
@@ -369,6 +370,9 @@ async function resolveConfig(
369370
logger.warn("SQLite in Durable Objects is only supported in local mode.");
370371
}
371372

373+
// prompt user to update their types if we detect that it is out of date
374+
await checkTypesDiff(config, entry);
375+
372376
return resolved;
373377
}
374378
export class ConfigController extends Controller<ConfigControllerEventMap> {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { readFileSync } from "fs";
2+
import { version } from "workerd";
3+
import { logger } from "../logger";
4+
import { generateEnvTypes } from ".";
5+
import type { Config } from "../config";
6+
import type { Entry } from "../deployment-bundle/entry";
7+
8+
export const checkTypesDiff = async (config: Config, entry: Entry) => {
9+
if (!entry.file.endsWith(".ts")) {
10+
return;
11+
}
12+
let maybeExistingTypesFile: string[];
13+
try {
14+
// Note: this checks the default location only
15+
maybeExistingTypesFile = readFileSync(
16+
"./worker-configuration.d.ts",
17+
"utf-8"
18+
).split("\n");
19+
} catch {
20+
return;
21+
}
22+
const existingEnvHeader = maybeExistingTypesFile.find((line) =>
23+
line.startsWith("// Generated by Wrangler by running")
24+
);
25+
const maybeExistingHash =
26+
existingEnvHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;
27+
const previousStrictVars = existingEnvHeader?.match(
28+
/--strict-vars(=|\s)(?<result>true|false)/
29+
)?.groups?.result;
30+
const previousEnvInterface = existingEnvHeader?.match(
31+
/--env-interface(=|\s)(?<result>[a-zA-Z][a-zA-Z0-9_]*)/
32+
)?.groups?.result;
33+
34+
let newEnvHeader: string | undefined;
35+
try {
36+
const { envHeader } = await generateEnvTypes(
37+
config,
38+
{ strictVars: previousStrictVars === "false" ? false : true },
39+
previousEnvInterface ?? "Env",
40+
"worker-configuration.d.ts",
41+
entry,
42+
// don't log anything
43+
false
44+
);
45+
newEnvHeader = envHeader;
46+
} catch (e) {
47+
logger.error(e);
48+
}
49+
50+
const newHash = newEnvHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;
51+
52+
const existingRuntimeHeader = maybeExistingTypesFile.find((line) =>
53+
line.startsWith("// Runtime types generated with")
54+
);
55+
const newRuntimeHeader = `// Runtime types generated with workerd@${version} ${config.compatibility_date} ${config.compatibility_flags.sort().join(",")}`;
56+
57+
const envOutOfDate = existingEnvHeader && maybeExistingHash !== newHash;
58+
const runtimeOutOfDate =
59+
existingRuntimeHeader && existingRuntimeHeader !== newRuntimeHeader;
60+
61+
if (envOutOfDate || runtimeOutOfDate) {
62+
logger.log(
63+
"❓ It looks like your types might be out of date. Have you updated your config file since last running `wrangler types`?"
64+
);
65+
}
66+
};

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

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "node:fs";
22
import { basename, dirname, extname, join, relative, resolve } from "node:path";
3+
import { hash as blake3hash } from "blake3-wasm";
34
import chalk from "chalk";
45
import { findUpSync } from "find-up";
56
import { getNodeCompat } from "miniflare";
@@ -139,47 +140,10 @@ export const typesCommand = createCommand({
139140
const types = [];
140141
if (args.includeEnv) {
141142
logger.log(`Generating project types...\n`);
142-
const secrets = getVarsForDev(
143-
// We do not want `getVarsForDev()` to merge in the standard vars into the dev vars
144-
// because we want to be able to work with secrets differently to vars.
145-
// So we pass in a fake vars object here.
146-
{ ...config, vars: {} },
147-
args.env,
148-
true
149-
) as Record<string, string>;
150-
151-
const configBindingsWithSecrets = {
152-
kv_namespaces: config.kv_namespaces ?? [],
153-
vars: collectAllVars(args),
154-
wasm_modules: config.wasm_modules,
155-
text_blobs: {
156-
...config.text_blobs,
157-
},
158-
data_blobs: config.data_blobs,
159-
durable_objects: config.durable_objects,
160-
r2_buckets: config.r2_buckets,
161-
d1_databases: config.d1_databases,
162-
services: config.services,
163-
analytics_engine_datasets: config.analytics_engine_datasets,
164-
dispatch_namespaces: config.dispatch_namespaces,
165-
logfwdr: config.logfwdr,
166-
unsafe: config.unsafe,
167-
rules: config.rules,
168-
queues: config.queues,
169-
send_email: config.send_email,
170-
vectorize: config.vectorize,
171-
hyperdrive: config.hyperdrive,
172-
mtls_certificates: config.mtls_certificates,
173-
browser: config.browser,
174-
ai: config.ai,
175-
version_metadata: config.version_metadata,
176-
secrets,
177-
assets: config.assets,
178-
workflows: config.workflows,
179-
};
180143

181144
const { envHeader, envTypes } = await generateEnvTypes(
182-
configBindingsWithSecrets,
145+
config,
146+
args,
183147
envInterface,
184148
outputPath,
185149
entrypoint
@@ -286,12 +250,53 @@ type ConfigToDTS = Partial<Omit<Config, "vars">> & { vars: VarTypes } & {
286250
secrets: Secrets;
287251
};
288252

289-
async function generateEnvTypes(
290-
configToDTS: ConfigToDTS,
253+
export async function generateEnvTypes(
254+
config: Config,
255+
args: Partial<(typeof typesCommand)["args"]>,
291256
envInterface: string,
292257
outputPath: string,
293-
entrypoint?: Entry
258+
entrypoint?: Entry,
259+
log = true
294260
): Promise<{ envHeader?: string; envTypes?: string }> {
261+
const secrets = getVarsForDev(
262+
// We do not want `getVarsForDev()` to merge in the standard vars into the dev vars
263+
// because we want to be able to work with secrets differently to vars.
264+
// So we pass in a fake vars object here.
265+
{ ...config, vars: {} },
266+
args.env,
267+
true
268+
) as Record<string, string>;
269+
270+
const configToDTS: ConfigToDTS = {
271+
kv_namespaces: config.kv_namespaces ?? [],
272+
vars: collectAllVars(args),
273+
wasm_modules: config.wasm_modules,
274+
text_blobs: {
275+
...config.text_blobs,
276+
},
277+
data_blobs: config.data_blobs,
278+
durable_objects: config.durable_objects,
279+
r2_buckets: config.r2_buckets,
280+
d1_databases: config.d1_databases,
281+
services: config.services,
282+
analytics_engine_datasets: config.analytics_engine_datasets,
283+
dispatch_namespaces: config.dispatch_namespaces,
284+
logfwdr: config.logfwdr,
285+
unsafe: config.unsafe,
286+
rules: config.rules,
287+
queues: config.queues,
288+
send_email: config.send_email,
289+
vectorize: config.vectorize,
290+
hyperdrive: config.hyperdrive,
291+
mtls_certificates: config.mtls_certificates,
292+
browser: config.browser,
293+
ai: config.ai,
294+
version_metadata: config.version_metadata,
295+
secrets,
296+
assets: config.assets,
297+
workflows: config.workflows,
298+
};
299+
295300
const entrypointFormat = entrypoint?.format ?? "modules";
296301
const fullOutputPath = resolve(outputPath);
297302

@@ -530,13 +535,20 @@ async function generateEnvTypes(
530535
envTypeStructure.map(([key, value]) => `${key}: ${value};`),
531536
modulesTypeStructure
532537
);
533-
const envHeader = `// Generated by Wrangler by running \`${wranglerCommandUsed}\``;
538+
// todo replace envInterface with generic
539+
const hash = blake3hash(consoleOutput).toString("hex").slice(0, 32);
534540

535-
logger.log(chalk.dim(consoleOutput));
541+
const envHeader = `// Generated by Wrangler by running \`${wranglerCommandUsed}\` (hash: ${hash})`;
542+
543+
if (log) {
544+
logger.log(chalk.dim(consoleOutput));
545+
}
536546

537547
return { envHeader, envTypes: fileContent };
538548
} else {
539-
logger.log(chalk.dim("No project types to add.\n"));
549+
if (log) {
550+
logger.log(chalk.dim("No project types to add.\n"));
551+
}
540552
return {
541553
envHeader: undefined,
542554
envTypes: undefined,
@@ -627,7 +639,7 @@ type VarTypes = Record<string, string[]>;
627639
* @returns an object which keys are the variable names and values are arrays containing all the computed types for such variables
628640
*/
629641
function collectAllVars(
630-
args: (typeof typesCommand)["args"]
642+
args: Partial<(typeof typesCommand)["args"]>
631643
): Record<string, string[]> {
632644
const varsInfo: Record<string, Set<string>> = {};
633645

0 commit comments

Comments
 (0)