Skip to content

Commit 376a46b

Browse files
fix: widen multi-env vars types in wrangler types
1 parent 2e78812 commit 376a46b

File tree

3 files changed

+156
-24
lines changed

3 files changed

+156
-24
lines changed

.changeset/proud-forks-build.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: widen multi-env `vars` types in `wrangler types`
6+
7+
Currently types for variable generate string literal, those are appropriate when
8+
a single environment has been specified in the config file but if multiple environments
9+
are specified this however wrongly restricts the typing, the changes here fix such
10+
incorrect behavior.
11+
12+
For example, given a `wrangler.toml` containing the following:
13+
14+
```
15+
[vars]
16+
MY_VAR = "dev value"
17+
18+
[env.production]
19+
[env.production.vars]
20+
MY_VAR = "prod value"
21+
```
22+
23+
running `wrangler types` would generate:
24+
25+
```ts
26+
interface Env {
27+
MY_VAR: "dev value";
28+
}
29+
```
30+
31+
making typescript incorrectly assume that `MY_VAR` is always going to be `"dev value"`
32+
33+
after these changes, the generated interface would instead be:
34+
35+
```ts
36+
interface Env {
37+
MY_VAR: "dev value" | "prod value";
38+
}
39+
```

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,50 @@ describe("generateTypes()", () => {
635635
`);
636636
});
637637

638+
it("should produce unions where appropriate for vars present in multiple environments", async () => {
639+
fs.writeFileSync(
640+
"./wrangler.toml",
641+
TOML.stringify({
642+
vars: {
643+
MY_VAR: "a var",
644+
MY_VAR_A: "A (dev)",
645+
MY_VAR_B: { value: "B (dev)" },
646+
MY_VAR_C: ["a", "b", "c"],
647+
},
648+
env: {
649+
production: {
650+
vars: {
651+
MY_VAR: "a var",
652+
MY_VAR_A: "A (prod)",
653+
MY_VAR_B: { value: "B (prod)" },
654+
MY_VAR_C: [1, 2, 3],
655+
},
656+
},
657+
staging: {
658+
vars: {
659+
MY_VAR_A: "A (stag)",
660+
},
661+
},
662+
},
663+
} as TOML.JsonMap),
664+
"utf-8"
665+
);
666+
667+
await runWrangler("types");
668+
669+
expect(std.out).toMatchInlineSnapshot(`
670+
"Generating project types...
671+
672+
interface Env {
673+
MY_VAR: \\"a var\\";
674+
MY_VAR_A: \\"A (dev)\\" | \\"A (prod)\\" | \\"A (stag)\\";
675+
MY_VAR_C: [\\"a\\",\\"b\\",\\"c\\"] | [1,2,3];
676+
MY_VAR_B: {\\"value\\":\\"B (dev)\\"} | {\\"value\\":\\"B (prod)\\"};
677+
}
678+
"
679+
`);
680+
});
681+
638682
describe("customization", () => {
639683
describe("env", () => {
640684
it("should allow the user to customize the interface name", async () => {

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

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
22
import { basename, dirname, extname, join, relative, resolve } from "node:path";
33
import { findUpSync } from "find-up";
44
import { getNodeCompat } from "miniflare";
5-
import { readConfig } from "../config";
5+
import { experimental_readRawConfig, readConfig } from "../config";
66
import { resolveWranglerConfigPath } from "../config/config-helpers";
77
import { getEntry } from "../deployment-bundle/entry";
88
import { getVarsForDev } from "../dev/dev-vars";
@@ -12,7 +12,7 @@ import { parseJSONC } from "../parse";
1212
import { printWranglerBanner } from "../wrangler-banner";
1313
import { generateRuntimeTypes } from "./runtime";
1414
import { logRuntimeTypesMessage } from "./runtime/log-runtime-types-message";
15-
import type { Config } from "../config";
15+
import type { Config, RawEnvironment } from "../config";
1616
import type { Entry } from "../deployment-bundle/entry";
1717
import type { CfScriptFormat } from "../deployment-bundle/worker";
1818
import type {
@@ -113,11 +113,9 @@ export async function typesHandler(
113113
true
114114
) as Record<string, string>;
115115

116-
const configBindingsWithSecrets: Partial<Config> & {
117-
secrets: Record<string, string>;
118-
} = {
116+
const configBindingsWithSecrets = {
119117
kv_namespaces: config.kv_namespaces ?? [],
120-
vars: { ...config.vars },
118+
vars: getVarsInfo(args),
121119
wasm_modules: config.wasm_modules,
122120
text_blobs: {
123121
...config.text_blobs,
@@ -207,15 +205,29 @@ export function generateImportSpecifier(from: string, to: string) {
207205
*/
208206
export function constructType(
209207
key: string,
210-
value: string | number | boolean,
208+
value: string | number | boolean | string[],
211209
useRawVal = true
212210
) {
213211
const typeKey = constructTypeKey(key);
214-
if (typeof value === "string") {
212+
213+
const stringValue =
214+
typeof value === "string"
215+
? value
216+
: Array.isArray(value) && value.length === 1
217+
? value[0]
218+
: null;
219+
220+
if (stringValue) {
221+
if (useRawVal) {
222+
return `${typeKey}: ${stringValue};`;
223+
}
224+
return `${typeKey}: ${JSON.stringify(stringValue)};`;
225+
}
226+
if (Array.isArray(value)) {
215227
if (useRawVal) {
216-
return `${typeKey}: ${value};`;
228+
return `${typeKey}: ${value.join(" | ")};`;
217229
}
218-
return `${typeKey}: ${JSON.stringify(value)};`;
230+
return `${typeKey}: ${value.map((str) => JSON.stringify(str)).join("|")};`;
219231
}
220232
if (typeof value === "number" || typeof value === "boolean") {
221233
return `${typeKey}: ${value};`;
@@ -225,8 +237,12 @@ export function constructType(
225237

226238
type Secrets = Record<string, string>;
227239

240+
type ConfigToDTS = Partial<Omit<Config, "vars">> & { vars: VarsInfo } & {
241+
secrets: Secrets;
242+
};
243+
228244
async function generateTypes(
229-
configToDTS: Partial<Config> & { secrets: Secrets },
245+
configToDTS: ConfigToDTS,
230246
config: Config,
231247
envInterface: string,
232248
outputPath: string
@@ -275,19 +291,25 @@ async function generateTypes(
275291
const vars = Object.entries(configToDTS.vars).filter(
276292
([key]) => !(key in configToDTS.secrets)
277293
);
278-
for (const [varName, varValue] of vars) {
279-
if (
280-
typeof varValue === "string" ||
281-
typeof varValue === "number" ||
282-
typeof varValue === "boolean"
283-
) {
284-
envTypeStructure.push(constructType(varName, varValue, false));
285-
}
286-
if (typeof varValue === "object" && varValue !== null) {
287-
envTypeStructure.push(
288-
constructType(varName, JSON.stringify(varValue), true)
289-
);
290-
}
294+
for (const [varName, varInfo] of vars) {
295+
const varValueTypes = new Set(
296+
varInfo
297+
.map(({ value }) => value)
298+
.map((varValue) => {
299+
if (
300+
typeof varValue === "string" ||
301+
typeof varValue === "number" ||
302+
typeof varValue === "boolean"
303+
) {
304+
return `"${varValue}"`;
305+
}
306+
if (typeof varValue === "object" && varValue !== null) {
307+
return `${JSON.stringify(varValue)}`;
308+
}
309+
})
310+
.filter(Boolean)
311+
) as Set<string>;
312+
envTypeStructure.push(constructType(varName, [...varValueTypes], true));
291313
}
292314
}
293315

@@ -578,3 +600,30 @@ type TSConfig = {
578600
types: string[];
579601
};
580602
};
603+
604+
type VarValue = Config["vars"][string];
605+
606+
type VarInfoValue = { value: VarValue; env?: string };
607+
608+
type VarsInfo = Record<string, VarInfoValue[]>;
609+
610+
function getVarsInfo(
611+
args: StrictYargsOptionsToInterface<typeof typesOptions>
612+
): VarsInfo {
613+
const varsInfo: VarsInfo = {};
614+
const { rawConfig } = experimental_readRawConfig(args);
615+
616+
function collectVars(vars: RawEnvironment["vars"], envName?: string) {
617+
Object.entries(vars ?? {}).forEach(([key, value]) => {
618+
varsInfo[key] ??= [];
619+
varsInfo[key].push({ value, env: envName });
620+
});
621+
}
622+
623+
collectVars(rawConfig.vars);
624+
Object.entries(rawConfig.env ?? {}).forEach(([envName, env]) => {
625+
collectVars(env.vars, envName);
626+
});
627+
628+
return varsInfo;
629+
}

0 commit comments

Comments
 (0)