Skip to content

Commit df55b66

Browse files
fix: widen multi-env vars types in wrangler types
1 parent cab7e1c commit df55b66

File tree

4 files changed

+166
-28
lines changed

4 files changed

+166
-28
lines changed

.changeset/olive-kiwis-arrive.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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,48 @@ describe("generateTypes()", () => {
391391
`);
392392
});
393393

394+
it("should produce unions where appropriate for vars present in multiple environments", async () => {
395+
fs.writeFileSync(
396+
"./wrangler.toml",
397+
TOML.stringify({
398+
vars: {
399+
MY_VAR: "a var",
400+
MY_VAR_A: "A (dev)",
401+
MY_VAR_B: { value: "B (dev)" },
402+
MY_VAR_C: ["a", "b", "c"],
403+
},
404+
env: {
405+
production: {
406+
vars: {
407+
MY_VAR: "a var",
408+
MY_VAR_A: "A (prod)",
409+
MY_VAR_B: { value: "B (prod)" },
410+
MY_VAR_C: [1, 2, 3],
411+
},
412+
},
413+
staging: {
414+
vars: {
415+
MY_VAR_A: "A (stag)",
416+
},
417+
},
418+
},
419+
} as TOML.JsonMap),
420+
"utf-8"
421+
);
422+
423+
await runWrangler("types");
424+
425+
expect(std.out).toMatchInlineSnapshot(`
426+
"interface Env {
427+
MY_VAR: \\"a var\\";
428+
MY_VAR_A: \\"A (dev)\\" | \\"A (prod)\\" | \\"A (stag)\\";
429+
MY_VAR_C: [\\"a\\",\\"b\\",\\"c\\"] | [1,2,3];
430+
MY_VAR_B: {\\"value\\":\\"B (dev)\\"} | {\\"value\\":\\"B (prod)\\"};
431+
}
432+
"
433+
`);
434+
});
435+
394436
describe("customization", () => {
395437
describe("env", () => {
396438
it("should allow the user to customize the interface name", async () => {

packages/wrangler/src/config/index.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,11 @@ export function readConfig<CommandArgs>(
3131
args: CommandArgs &
3232
Pick<OnlyCamelCase<CommonYargsOptions>, "experimentalJsonConfig">
3333
): Config {
34-
let rawConfig: RawConfig = {};
35-
if (!configPath) {
36-
configPath = findWranglerToml(process.cwd(), args.experimentalJsonConfig);
37-
}
38-
// Load the configuration from disk if available
39-
if (configPath?.endsWith("toml")) {
40-
rawConfig = parseTOML(readFileSync(configPath), configPath);
41-
} else if (configPath?.endsWith("json")) {
42-
rawConfig = parseJSONC(readFileSync(configPath), configPath);
43-
}
34+
const { rawConfig, configPath: updateConfigPath } = getRawConfig<CommandArgs>(
35+
configPath,
36+
args
37+
);
38+
configPath = updateConfigPath;
4439

4540
// Process the top-level configuration.
4641
const { config, diagnostics } = normalizeAndValidateConfig(
@@ -70,6 +65,30 @@ export function readConfig<CommandArgs>(
7065
return config;
7166
}
7267

68+
/**
69+
* Get the raw (pre-processed) Wrangler configuration; read it from the give `configPath` if available.
70+
* Returns the raw configuration alongside the `configPath` for it (which is equal to the provided one, if one was)
71+
*/
72+
export function getRawConfig<CommandArgs>(
73+
configPath: string | undefined,
74+
args: CommandArgs &
75+
Pick<OnlyCamelCase<CommonYargsOptions>, "experimentalJsonConfig">
76+
) {
77+
let rawConfig: RawConfig = {};
78+
if (!configPath) {
79+
configPath = findWranglerToml(process.cwd(), args.experimentalJsonConfig);
80+
}
81+
82+
// Load the configuration from disk if available
83+
if (configPath?.endsWith("toml")) {
84+
rawConfig = parseTOML(readFileSync(configPath), configPath);
85+
} else if (configPath?.endsWith("json")) {
86+
rawConfig = parseJSONC(readFileSync(configPath), configPath);
87+
}
88+
89+
return { rawConfig, configPath };
90+
}
91+
7392
/**
7493
* Find the wrangler.toml file by searching up the file-system
7594
* from the current working directory.

packages/wrangler/src/type-generation.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as fs from "node:fs";
22
import { findUpSync } from "find-up";
3-
import { findWranglerToml, readConfig } from "./config";
3+
import { findWranglerToml, getRawConfig, readConfig } from "./config";
44
import { getEntry } from "./deployment-bundle/entry";
55
import { getVarsForDev } from "./dev/dev-vars";
66
import { UserError } from "./errors";
77
import { logger } from "./logger";
88
import { printWranglerBanner } from "./update-check";
99
import { CommandLineArgsError } from "./index";
10-
import type { Config } from "./config";
10+
import type { Config, RawEnvironment } from "./config";
1111
import type { CfScriptFormat } from "./deployment-bundle/worker";
1212
import type {
1313
CommonYargsArgv,
@@ -74,11 +74,9 @@ export async function typesHandler(
7474
true
7575
) as Record<string, string>;
7676

77-
const configBindingsWithSecrets: Partial<Config> & {
78-
secrets: Record<string, string>;
79-
} = {
77+
const configBindingsWithSecrets: ConfigToDTS = {
8078
kv_namespaces: config.kv_namespaces ?? [],
81-
vars: { ...config.vars },
79+
vars: getVarsInfo(configPath, args),
8280
wasm_modules: config.wasm_modules,
8381
text_blobs: {
8482
...config.text_blobs,
@@ -108,8 +106,12 @@ export async function typesHandler(
108106

109107
type Secrets = Record<string, string>;
110108

109+
type ConfigToDTS = Partial<Omit<Config, "vars">> & { vars: VarsInfo } & {
110+
secrets: Secrets;
111+
};
112+
111113
async function generateTypes(
112-
configToDTS: Partial<Config> & { secrets: Secrets },
114+
configToDTS: ConfigToDTS,
113115
config: Config,
114116
envInterface: string,
115117
outputPath: string
@@ -148,17 +150,25 @@ async function generateTypes(
148150
const vars = Object.entries(configToDTS.vars).filter(
149151
([key]) => !(key in configToDTS.secrets)
150152
);
151-
for (const [varName, varValue] of vars) {
152-
if (
153-
typeof varValue === "string" ||
154-
typeof varValue === "number" ||
155-
typeof varValue === "boolean"
156-
) {
157-
envTypeStructure.push(`${varName}: "${varValue}";`);
158-
}
159-
if (typeof varValue === "object" && varValue !== null) {
160-
envTypeStructure.push(`${varName}: ${JSON.stringify(varValue)};`);
161-
}
153+
for (const [varName, varInfo] of vars) {
154+
const varValueTypes = new Set(
155+
varInfo
156+
.map(({ value }) => value)
157+
.map((varValue) => {
158+
if (
159+
typeof varValue === "string" ||
160+
typeof varValue === "number" ||
161+
typeof varValue === "boolean"
162+
) {
163+
return `"${varValue}"`;
164+
}
165+
if (typeof varValue === "object" && varValue !== null) {
166+
return `${JSON.stringify(varValue)}`;
167+
}
168+
})
169+
.filter(Boolean)
170+
);
171+
envTypeStructure.push(`${varName}: ${[...varValueTypes].join(" | ")};`);
162172
}
163173
}
164174

@@ -329,3 +339,31 @@ function writeDTSFile({
329339
logger.log(combinedTypeStrings);
330340
}
331341
}
342+
343+
type VarValue = Config["vars"][string];
344+
345+
type VarInfoValue = { value: VarValue; env?: string };
346+
347+
type VarsInfo = Record<string, VarInfoValue[]>;
348+
349+
function getVarsInfo(
350+
configPath: string,
351+
args: StrictYargsOptionsToInterface<typeof typesOptions>
352+
): VarsInfo {
353+
const varsInfo: VarsInfo = {};
354+
const { rawConfig } = getRawConfig(configPath, args);
355+
356+
function collectVars(vars: RawEnvironment["vars"], envName?: string) {
357+
Object.entries(vars ?? {}).forEach(([key, value]) => {
358+
varsInfo[key] ??= [];
359+
varsInfo[key].push({ value, env: envName });
360+
});
361+
}
362+
363+
collectVars(rawConfig.vars);
364+
Object.entries(rawConfig.env ?? {}).forEach(([envName, env]) => {
365+
collectVars(env.vars, envName);
366+
});
367+
368+
return varsInfo;
369+
}

0 commit comments

Comments
 (0)