Skip to content

Commit

Permalink
fix: widen multi-env vars types in wrangler types
Browse files Browse the repository at this point in the history
  • Loading branch information
dario-piotrowicz committed Jan 1, 2025
1 parent 2e78812 commit 376a46b
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 24 deletions.
39 changes: 39 additions & 0 deletions .changeset/proud-forks-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
"wrangler": patch
---

fix: widen multi-env `vars` types in `wrangler types`

Currently types for variable generate string literal, those are appropriate when
a single environment has been specified in the config file but if multiple environments
are specified this however wrongly restricts the typing, the changes here fix such
incorrect behavior.

For example, given a `wrangler.toml` containing the following:

```
[vars]
MY_VAR = "dev value"
[env.production]
[env.production.vars]
MY_VAR = "prod value"
```

running `wrangler types` would generate:

```ts
interface Env {
MY_VAR: "dev value";
}
```

making typescript incorrectly assume that `MY_VAR` is always going to be `"dev value"`

after these changes, the generated interface would instead be:

```ts
interface Env {
MY_VAR: "dev value" | "prod value";
}
```
44 changes: 44 additions & 0 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,50 @@ describe("generateTypes()", () => {
`);
});

it("should produce unions where appropriate for vars present in multiple environments", async () => {
fs.writeFileSync(
"./wrangler.toml",
TOML.stringify({
vars: {
MY_VAR: "a var",
MY_VAR_A: "A (dev)",
MY_VAR_B: { value: "B (dev)" },
MY_VAR_C: ["a", "b", "c"],
},
env: {
production: {
vars: {
MY_VAR: "a var",
MY_VAR_A: "A (prod)",
MY_VAR_B: { value: "B (prod)" },
MY_VAR_C: [1, 2, 3],
},
},
staging: {
vars: {
MY_VAR_A: "A (stag)",
},
},
},
} as TOML.JsonMap),
"utf-8"
);

await runWrangler("types");

expect(std.out).toMatchInlineSnapshot(`
"Generating project types...
interface Env {
MY_VAR: \\"a var\\";
MY_VAR_A: \\"A (dev)\\" | \\"A (prod)\\" | \\"A (stag)\\";
MY_VAR_C: [\\"a\\",\\"b\\",\\"c\\"] | [1,2,3];
MY_VAR_B: {\\"value\\":\\"B (dev)\\"} | {\\"value\\":\\"B (prod)\\"};
}
"
`);
});

describe("customization", () => {
describe("env", () => {
it("should allow the user to customize the interface name", async () => {
Expand Down
97 changes: 73 additions & 24 deletions packages/wrangler/src/type-generation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from "node:fs";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { findUpSync } from "find-up";
import { getNodeCompat } from "miniflare";
import { readConfig } from "../config";
import { experimental_readRawConfig, readConfig } from "../config";
import { resolveWranglerConfigPath } from "../config/config-helpers";
import { getEntry } from "../deployment-bundle/entry";
import { getVarsForDev } from "../dev/dev-vars";
Expand All @@ -12,7 +12,7 @@ import { parseJSONC } from "../parse";
import { printWranglerBanner } from "../wrangler-banner";
import { generateRuntimeTypes } from "./runtime";
import { logRuntimeTypesMessage } from "./runtime/log-runtime-types-message";
import type { Config } from "../config";
import type { Config, RawEnvironment } from "../config";
import type { Entry } from "../deployment-bundle/entry";
import type { CfScriptFormat } from "../deployment-bundle/worker";
import type {
Expand Down Expand Up @@ -113,11 +113,9 @@ export async function typesHandler(
true
) as Record<string, string>;

const configBindingsWithSecrets: Partial<Config> & {
secrets: Record<string, string>;
} = {
const configBindingsWithSecrets = {
kv_namespaces: config.kv_namespaces ?? [],
vars: { ...config.vars },
vars: getVarsInfo(args),
wasm_modules: config.wasm_modules,
text_blobs: {
...config.text_blobs,
Expand Down Expand Up @@ -207,15 +205,29 @@ export function generateImportSpecifier(from: string, to: string) {
*/
export function constructType(
key: string,
value: string | number | boolean,
value: string | number | boolean | string[],
useRawVal = true
) {
const typeKey = constructTypeKey(key);
if (typeof value === "string") {

const stringValue =
typeof value === "string"
? value
: Array.isArray(value) && value.length === 1
? value[0]
: null;

if (stringValue) {
if (useRawVal) {
return `${typeKey}: ${stringValue};`;
}
return `${typeKey}: ${JSON.stringify(stringValue)};`;
}
if (Array.isArray(value)) {
if (useRawVal) {
return `${typeKey}: ${value};`;
return `${typeKey}: ${value.join(" | ")};`;
}
return `${typeKey}: ${JSON.stringify(value)};`;
return `${typeKey}: ${value.map((str) => JSON.stringify(str)).join("|")};`;
}
if (typeof value === "number" || typeof value === "boolean") {
return `${typeKey}: ${value};`;
Expand All @@ -225,8 +237,12 @@ export function constructType(

type Secrets = Record<string, string>;

type ConfigToDTS = Partial<Omit<Config, "vars">> & { vars: VarsInfo } & {
secrets: Secrets;
};

async function generateTypes(
configToDTS: Partial<Config> & { secrets: Secrets },
configToDTS: ConfigToDTS,
config: Config,
envInterface: string,
outputPath: string
Expand Down Expand Up @@ -275,19 +291,25 @@ async function generateTypes(
const vars = Object.entries(configToDTS.vars).filter(
([key]) => !(key in configToDTS.secrets)
);
for (const [varName, varValue] of vars) {
if (
typeof varValue === "string" ||
typeof varValue === "number" ||
typeof varValue === "boolean"
) {
envTypeStructure.push(constructType(varName, varValue, false));
}
if (typeof varValue === "object" && varValue !== null) {
envTypeStructure.push(
constructType(varName, JSON.stringify(varValue), true)
);
}
for (const [varName, varInfo] of vars) {
const varValueTypes = new Set(
varInfo
.map(({ value }) => value)
.map((varValue) => {
if (
typeof varValue === "string" ||
typeof varValue === "number" ||
typeof varValue === "boolean"
) {
return `"${varValue}"`;
}
if (typeof varValue === "object" && varValue !== null) {
return `${JSON.stringify(varValue)}`;
}
})
.filter(Boolean)
) as Set<string>;
envTypeStructure.push(constructType(varName, [...varValueTypes], true));
}
}

Expand Down Expand Up @@ -578,3 +600,30 @@ type TSConfig = {
types: string[];
};
};

type VarValue = Config["vars"][string];

type VarInfoValue = { value: VarValue; env?: string };

type VarsInfo = Record<string, VarInfoValue[]>;

function getVarsInfo(
args: StrictYargsOptionsToInterface<typeof typesOptions>
): VarsInfo {
const varsInfo: VarsInfo = {};
const { rawConfig } = experimental_readRawConfig(args);

function collectVars(vars: RawEnvironment["vars"], envName?: string) {
Object.entries(vars ?? {}).forEach(([key, value]) => {
varsInfo[key] ??= [];
varsInfo[key].push({ value, env: envName });
});
}

collectVars(rawConfig.vars);
Object.entries(rawConfig.env ?? {}).forEach(([envName, env]) => {
collectVars(env.vars, envName);
});

return varsInfo;
}

0 comments on commit 376a46b

Please sign in to comment.