From d5c9622ac36865e80d2cf194d2af21ad245efa0e Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Wed, 12 Mar 2025 17:53:35 +0000 Subject: [PATCH 1/4] process.env & importable env typing support --- packages/wrangler/e2e/types.test.ts | 13 +- .../src/__tests__/type-generation.test.ts | 266 +++++++++++------- packages/wrangler/src/process-env.ts | 30 ++ .../wrangler/src/type-generation/index.ts | 28 +- 4 files changed, 220 insertions(+), 117 deletions(-) create mode 100644 packages/wrangler/src/process-env.ts diff --git a/packages/wrangler/e2e/types.test.ts b/packages/wrangler/e2e/types.test.ts index 09f34165fd3e..0e89bbd53ccc 100644 --- a/packages/wrangler/e2e/types.test.ts +++ b/packages/wrangler/e2e/types.test.ts @@ -73,10 +73,13 @@ describe("types", () => { "utf8" ); expect(file).toMatchInlineSnapshot(` - "// Generated by Wrangler by running \`wrangler types --include-runtime=false\` (hash: 7fbca0b39560512499078acfe5f450c0) - interface Env { - MY_VAR: "my-var-value"; + "// Generated by Wrangler by running \`wrangler types --include-runtime=false\` (hash: 7915eccca244b8d5c107e358ee5929e8) + declare namespace Cloudflare { + interface Env { + MY_VAR: "my-var-value"; + } } + interface Env extends Cloudflare.Env {} " `); }); @@ -92,7 +95,7 @@ describe("types", () => { ).split("\n"); expect(lines[0]).toMatchInlineSnapshot( - `"// Generated by Wrangler by running \`wrangler types ./types.d.ts\` (hash: 7fbca0b39560512499078acfe5f450c0)"` + `"// Generated by Wrangler by running \`wrangler types ./types.d.ts\` (hash: 7915eccca244b8d5c107e358ee5929e8)"` ); expect(lines[1]).match( /\/\/ Runtime types generated with workerd@1\.\d{8}\.\d \d{4}-\d{2}-\d{2} ([a-z_]+,?)*/ @@ -119,7 +122,7 @@ describe("types", () => { ).split("\n"); expect(lines[0]).toMatchInlineSnapshot( - `"// Generated by Wrangler by running \`wrangler types -c wranglerA.toml --env-interface MyCloudflareEnv ./cflare-env.d.ts\` (hash: 8fcf1ed67a52a2d34d6d34c3068e89b8)"` + `"// Generated by Wrangler by running \`wrangler types -c wranglerA.toml --env-interface MyCloudflareEnv ./cflare-env.d.ts\` (hash: 2f74a5a99f09ae4d994228b5bb959d24)"` ); expect(lines[1]).match( /\/\/ Runtime types generated with workerd@1\.\d{8}\.\d \d{4}-\d{2}-\d{2} ([a-z_]+,?)*/ diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index c0de7993ab21..ed5b14f7557f 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -317,9 +317,12 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - var: \\"from wrangler toml\\"; + declare namespace Cloudflare { + interface Env { + var: \\"from wrangler toml\\"; + } } + interface Env extends Cloudflare.Env {} Generating runtime types... @@ -334,9 +337,12 @@ describe("generate types", () => { Generating project types... - interface Env { - var: \\"from my-wrangler-config-a\\"; + declare namespace Cloudflare { + interface Env { + var: \\"from my-wrangler-config-a\\"; + } } + interface Env extends Cloudflare.Env {} Generating runtime types... @@ -351,9 +357,12 @@ describe("generate types", () => { Generating project types... - interface Env { - var: \\"from my-wrangler-config-b\\"; + declare namespace Cloudflare { + interface Env { + var: \\"from my-wrangler-config-b\\"; + } } + interface Env extends Cloudflare.Env {} Generating runtime types... @@ -395,39 +404,42 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - TEST_KV_NAMESPACE: KVNamespace; - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; - DURABLE_DIRECT_EXPORT: DurableObjectNamespace; - DURABLE_RE_EXPORT: DurableObjectNamespace; - DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */; - DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */; - R2_BUCKET_BINDING: R2Bucket; - D1_TESTING_SOMETHING: D1Database; - SERVICE_BINDING: Fetcher; - AE_DATASET_BINDING: AnalyticsEngineDataset; - NAMESPACE_BINDING: DispatchNamespace; - LOGFWDR_SCHEMA: any; - SOME_DATA_BLOB1: ArrayBuffer; - SOME_DATA_BLOB2: ArrayBuffer; - SOME_TEXT_BLOB1: string; - SOME_TEXT_BLOB2: string; - testing_unsafe: any; - UNSAFE_RATELIMIT: RateLimit; - TEST_QUEUE_BINDING: Queue; - SEND_EMAIL_BINDING: SendEmail; - VECTORIZE_BINDING: VectorizeIndex; - HYPERDRIVE_BINDING: Hyperdrive; - MTLS_BINDING: Fetcher; - BROWSER_BINDING: Fetcher; - AI_BINDING: Ai; - IMAGES_BINDING: ImagesBinding; - VERSION_METADATA_BINDING: { id: string; tag: string }; - ASSETS_BINDING: Fetcher; + declare namespace Cloudflare { + interface Env { + TEST_KV_NAMESPACE: KVNamespace; + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + DURABLE_DIRECT_EXPORT: DurableObjectNamespace; + DURABLE_RE_EXPORT: DurableObjectNamespace; + DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */; + DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */; + R2_BUCKET_BINDING: R2Bucket; + D1_TESTING_SOMETHING: D1Database; + SERVICE_BINDING: Fetcher; + AE_DATASET_BINDING: AnalyticsEngineDataset; + NAMESPACE_BINDING: DispatchNamespace; + LOGFWDR_SCHEMA: any; + SOME_DATA_BLOB1: ArrayBuffer; + SOME_DATA_BLOB2: ArrayBuffer; + SOME_TEXT_BLOB1: string; + SOME_TEXT_BLOB2: string; + testing_unsafe: any; + UNSAFE_RATELIMIT: RateLimit; + TEST_QUEUE_BINDING: Queue; + SEND_EMAIL_BINDING: SendEmail; + VECTORIZE_BINDING: VectorizeIndex; + HYPERDRIVE_BINDING: Hyperdrive; + MTLS_BINDING: Fetcher; + BROWSER_BINDING: Fetcher; + AI_BINDING: Ai; + IMAGES_BINDING: ImagesBinding; + VERSION_METADATA_BINDING: { id: string; tag: string }; + ASSETS_BINDING: Fetcher; + } } + interface Env extends Cloudflare.Env {} declare module \\"*.txt\\" { const value: string; export default value; @@ -465,14 +477,17 @@ describe("generate types", () => { expect(fs.existsSync("./worker-configuration.d.ts")).toBe(true); expect(fs.readFileSync("./worker-configuration.d.ts", "utf-8")) .toMatchInlineSnapshot(` - "// Generated by Wrangler by running \`wrangler\` (hash: e0442e27e492fd2b5e8bb36627f0213c) + "// Generated by Wrangler by running \`wrangler\` (hash: a123396658ac84465faf6f0f82c0337b) // Runtime types generated with workerd@ - interface Env { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface Env extends Cloudflare.Env {} // Begin runtime types " @@ -564,13 +579,17 @@ describe("generate types", () => { await runWrangler("types --include-runtime=false"); expect(fs.readFileSync("./worker-configuration.d.ts", "utf-8")).toContain( - `// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type\ninterface Env {\n}` + `\t// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type\n\tinterface Env {\n\t}` ); expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { + declare namespace Cloudflare { + // eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type + interface Env { + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -658,12 +677,15 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface Env extends Cloudflare.Env {} Generating runtime types... @@ -694,12 +716,15 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -735,13 +760,16 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - myTomlVarA: \\"A from wrangler toml\\"; - myTomlVarB: \\"B from wrangler toml\\"; - SECRET_A: string; - MULTI_LINE_SECRET: string; - UNQUOTED_SECRET: string; + declare namespace Cloudflare { + interface Env { + myTomlVarA: \\"A from wrangler toml\\"; + myTomlVarB: \\"B from wrangler toml\\"; + SECRET_A: string; + MULTI_LINE_SECRET: string; + UNQUOTED_SECRET: string; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -770,12 +798,15 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - varStr: string; - varArrNum: number[]; - varArrMix: (boolean|number|string)[]; - varObj: object; + declare namespace Cloudflare { + interface Env { + varStr: string; + varArrNum: number[]; + varArrMix: (boolean|number|string)[]; + varObj: object; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -809,10 +840,13 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - MY_VARIABLE_A: string; - MY_VARIABLE_B: string; + declare namespace Cloudflare { + interface Env { + MY_VARIABLE_A: string; + MY_VARIABLE_B: string; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -845,19 +879,22 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - \\"1\\": 1; - \\"12345\\": 12345; - \\"var-a\\": \\"/\\"a///\\"/\\"\\"; - \\"var-a-1\\": \\"/\\"a/////\\"\\"; - \\"var-a-b\\": \\"/\\"a////b/\\"\\"; - \\"var-a-b-\\": \\"/\\"a////b///\\"/\\"\\"; - true: true; - false: false; - \\"multi + declare namespace Cloudflare { + interface Env { + \\"1\\": 1; + \\"12345\\": 12345; + \\"var-a\\": \\"/\\"a///\\"/\\"\\"; + \\"var-a-1\\": \\"/\\"a/////\\"\\"; + \\"var-a-b\\": \\"/\\"a////b/\\"\\"; + \\"var-a-b-\\": \\"/\\"a////b///\\"/\\"\\"; + true: true; + false: false; + \\"multi line var\\": \\"this/nis/na/nmulti/nline/nvariable!\\"; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -904,12 +941,15 @@ describe("generate 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)\\"}; + declare namespace Cloudflare { + 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)\\"}; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -925,12 +965,15 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface Env { - MY_VAR: string; - MY_VAR_A: string; - MY_VAR_C: string[] | number[]; - MY_VAR_B: object; + declare namespace Cloudflare { + interface Env { + MY_VAR: string; + MY_VAR_A: string; + MY_VAR_C: string[] | number[]; + MY_VAR_B: object; + } } + interface Env extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -958,12 +1001,15 @@ describe("generate types", () => { expect(std.out).toMatchInlineSnapshot(` "Generating project types... - interface CloudflareEnv { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface CloudflareEnv extends Cloudflare.Env {} ──────────────────────────────────────────────────────────── ✨ Types written to worker-configuration.d.ts @@ -1055,14 +1101,17 @@ describe("generate types", () => { expect(fs.readFileSync("./cloudflare-env.d.ts", "utf-8")) .toMatchInlineSnapshot(` - "// Generated by Wrangler by running \`wrangler\` (hash: e0442e27e492fd2b5e8bb36627f0213c) + "// Generated by Wrangler by running \`wrangler\` (hash: a123396658ac84465faf6f0f82c0337b) // Runtime types generated with workerd@ - interface Env { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface Env extends Cloudflare.Env {} // Begin runtime types " @@ -1109,14 +1158,17 @@ describe("generate types", () => { expect(fs.readFileSync("./my-cloudflare-env-interface.d.ts", "utf-8")) .toMatchInlineSnapshot(` - "// Generated by Wrangler by running \`wrangler\` (hash: 15fe0821fea3c43df1b7e2b020b0fb7b) + "// Generated by Wrangler by running \`wrangler\` (hash: 7e48a0a15b531f54ca31c564fe6cb101) // Runtime types generated with workerd@ - interface MyCloudflareEnvInterface { - SOMETHING: \\"asdasdfasdf\\"; - ANOTHER: \\"thing\\"; - \\"some-other-var\\": \\"some-other-value\\"; - OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + declare namespace Cloudflare { + interface Env { + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + } } + interface MyCloudflareEnvInterface extends Cloudflare.Env {} // Begin runtime types " diff --git a/packages/wrangler/src/process-env.ts b/packages/wrangler/src/process-env.ts new file mode 100644 index 000000000000..b198ac019b2f --- /dev/null +++ b/packages/wrangler/src/process-env.ts @@ -0,0 +1,30 @@ +import { UserError } from "./errors"; + +export function isProcessEnvPopulated( + compatibility_date: string | undefined, + compatibility_flags: string[] = [] +) { + if ( + compatibility_flags.includes("nodejs_compat_populate_process_env") && + compatibility_flags.includes("nodejs_compat_do_not_populate_process_env") + ) { + throw new UserError("Can't both enable and disable a flag"); + } + + if ( + compatibility_flags.includes("nodejs_compat_populate_process_env") && + compatibility_flags.includes("nodejs_compat") + ) { + return true; + } + if ( + compatibility_flags.includes("nodejs_compat_do_not_populate_process_env") + ) { + return false; + } + return ( + compatibility_flags.includes("nodejs_compat") && + !!compatibility_date && + compatibility_date >= "2025-04-01" + ); +} diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 08fe07fd78fe..5ff6d4ca0cba 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -11,6 +11,7 @@ import { getVarsForDev } from "../dev/dev-vars"; import { CommandLineArgsError, UserError } from "../errors"; import { logger } from "../logger"; import { parseJSONC } from "../parse"; +import { isProcessEnvPopulated } from "../process-env"; import { generateRuntimeTypes } from "./runtime"; import { logRuntimeTypesMessage } from "./runtime/log-runtime-types-message"; import type { Config, RawEnvironment } from "../config"; @@ -254,6 +255,7 @@ export async function generateEnvTypes( entrypoint?: Entry, log = true ): Promise<{ envHeader?: string; envTypes?: string }> { + let stringKeys: string[] = []; const secrets = getVarsForDev( // We do not want `getVarsForDev()` to merge in the standard vars into the dev vars // because we want to be able to work with secrets differently to vars. @@ -332,11 +334,13 @@ export async function generateEnvTypes( constructTypeKey(varName), varValues.length === 1 ? varValues[0] : varValues.join(" | "), ]); + stringKeys.push(varName); } } for (const secretName in configToDTS.secrets) { envTypeStructure.push([constructTypeKey(secretName), "string"]); + stringKeys.push(secretName); } if (configToDTS.durable_objects?.bindings) { @@ -537,7 +541,10 @@ export async function generateEnvTypes( entrypointFormat, envInterface, envTypeStructure.map(([key, value]) => `${key}: ${value};`), - modulesTypeStructure + modulesTypeStructure, + stringKeys, + config.compatibility_date, + config.compatibility_flags ); const hash = createHash("sha256") .update(consoleOutput) @@ -589,17 +596,28 @@ function generateTypeStrings( formatType: string, envInterface: string, envTypeStructure: string[], - modulesTypeStructure: string[] + modulesTypeStructure: string[], + stringKeys: string[], + compatibilityDate: string | undefined, + compatibilityFlags: string[] | undefined ): { fileContent: string; consoleOutput: string } { let baseContent = ""; let eslintDisable = ""; + let processEnv = ""; if (formatType === "modules") { if (envTypeStructure.length === 0) { eslintDisable = - "// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type\n"; + "\t// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type\n"; + } + if ( + isProcessEnvPopulated(compatibilityDate, compatibilityFlags) && + stringKeys.length > 0 + ) { + // StringifyValues ensures that json vars are correctly types as strings, not objects on process.env + processEnv = `\ntype StringifyValues> = {\n\t[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;\n};\ndeclare namespace NodeJS {\n\tinterface ProcessEnv extends StringifyValues `"${k}"`).join(" | ")}>> {}\n}`; } - baseContent = `interface ${envInterface} {${envTypeStructure.map((value) => `\n\t${value}`).join("")}\n}`; + baseContent = `declare namespace Cloudflare {\n${eslintDisable}\tinterface Env {${envTypeStructure.map((value) => `\n\t\t${value}`).join("")}\n\t}\n}\ninterface ${envInterface} extends Cloudflare.Env {}${processEnv}`; } else { baseContent = `export {};\ndeclare global {\n${envTypeStructure.map((value) => `\tconst ${value}`).join("\n")}\n}`; } @@ -607,7 +625,7 @@ function generateTypeStrings( const modulesContent = modulesTypeStructure.join("\n"); return { - fileContent: `${eslintDisable}${baseContent}\n${modulesContent}`, + fileContent: `${baseContent}\n${modulesContent}`, consoleOutput: `${baseContent}\n${modulesContent}`, }; } From a2cdf6df6dddc684836f1e224767e3f7582191d8 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Thu, 13 Mar 2025 16:20:58 +0000 Subject: [PATCH 2/4] add more tests --- .../__tests__/process-env-populated.test.ts | 103 ++++++++++++++++++ .../src/__tests__/type-generation.test.ts | 94 ++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 packages/wrangler/src/__tests__/process-env-populated.test.ts diff --git a/packages/wrangler/src/__tests__/process-env-populated.test.ts b/packages/wrangler/src/__tests__/process-env-populated.test.ts new file mode 100644 index 000000000000..0cb9349320ed --- /dev/null +++ b/packages/wrangler/src/__tests__/process-env-populated.test.ts @@ -0,0 +1,103 @@ +import assert from "node:assert"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import dedent from "ts-dedent"; +import { bundleWorker } from "../deployment-bundle/bundle"; +import { noopModuleCollector } from "../deployment-bundle/module-collection"; +import { isProcessEnvPopulated } from "../process-env"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { runInTempDir } from "./helpers/run-in-tmp"; + +/* + * This file contains inline comments with the word "javascript" + * This signals to a compatible editor extension that the template string + * contents should be syntax-highlighted as JavaScript. One such extension + * is zjcompt.es6-string-javascript, but there are others. + */ + +async function seedFs(files: Record): Promise { + for (const [location, contents] of Object.entries(files)) { + await mkdir(path.dirname(location), { recursive: true }); + await writeFile(location, contents); + } +} + +describe("isProcessEnvPopulated", () => { + test("default", () => { + expect(isProcessEnvPopulated(undefined, ["nodejs_compat"])).toBe(false); + }); + + test("future date", () => { + expect(isProcessEnvPopulated("2026-01-01", ["nodejs_compat"])).toBe(true); + }); + + test("old date", () => { + expect(isProcessEnvPopulated("2000-01-01", ["nodejs_compat"])).toBe(false); + }); + + test("switch date", () => { + expect(isProcessEnvPopulated("2025-04-01", ["nodejs_compat"])).toBe(true); + }); + + test("old date, but with flag", () => { + expect( + isProcessEnvPopulated("2000-01-01", [ + "nodejs_compat", + "nodejs_compat_populate_process_env", + ]) + ).toBe(true); + }); + + test("old date, with disable flag", () => { + expect( + isProcessEnvPopulated("2000-01-01", [ + "nodejs_compat", + "nodejs_compat_do_not_populate_process_env", + ]) + ).toBe(false); + }); + + test("future date, but with disable flag", () => { + expect( + isProcessEnvPopulated("2026-01-01", [ + "nodejs_compat", + "nodejs_compat_do_not_populate_process_env", + ]) + ).toBe(false); + }); + + test("future date, with enable flag", () => { + expect( + isProcessEnvPopulated("2026-01-01", [ + "nodejs_compat", + "nodejs_compat_populate_process_env", + ]) + ).toBe(true); + }); + + test("future date without nodejs_compat", () => { + expect(isProcessEnvPopulated("2026-01-01")).toBe(false); + }); + + test("future date, with enable flag but without nodejs_compat", () => { + expect( + isProcessEnvPopulated("2026-01-01", [ + "nodejs_compat_populate_process_env", + ]) + ).toBe(false); + }); + + test("errors with disable and enable flags specified", () => { + try { + isProcessEnvPopulated("2024-01-01", [ + "nodejs_compat_populate_process_env", + "nodejs_compat_do_not_populate_process_env", + ]); + assert(false, "Unreachable"); + } catch (e) { + expect(e).toMatchInlineSnapshot( + `[Error: Can't both enable and disable a flag]` + ); + } + }); +}); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index ed5b14f7557f..01c79c7cdf02 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -460,6 +460,100 @@ describe("generate types", () => { `); }); + it("should include stringified process.env types for vars, secrets, and json", async () => { + fs.writeFileSync( + "./index.ts", + `import { DurableObject } from 'cloudflare:workers'; + export default { async fetch () {} }; + export class DurableDirect extends DurableObject {} + export { DurableReexport } from './durable-2.js'; + // This should not be picked up, because it's external: + export class DurableExternal extends DurableObject {}` + ); + fs.writeFileSync( + "./wrangler.toml", + TOML.stringify({ + compatibility_date: "2022-01-12", + compatibility_flags: [ + "nodejs_compat", + "nodejs_compat_populate_process_env", + ], + name: "test-name", + main: "./index.ts", + ...bindingsConfigMock, + unsafe: bindingsConfigMock.unsafe ?? {}, + } as unknown as TOML.JsonMap), + "utf-8" + ); + fs.writeFileSync("./.dev.vars", "SECRET=test", "utf-8"); + + await runWrangler("types --include-runtime=false"); + expect(std.out).toMatchInlineSnapshot(` + "Generating project types... + + declare namespace Cloudflare { + interface Env { + TEST_KV_NAMESPACE: KVNamespace; + SOMETHING: \\"asdasdfasdf\\"; + ANOTHER: \\"thing\\"; + \\"some-other-var\\": \\"some-other-value\\"; + OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"}; + SECRET: string; + DURABLE_DIRECT_EXPORT: DurableObjectNamespace; + DURABLE_RE_EXPORT: DurableObjectNamespace; + DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */; + DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */; + R2_BUCKET_BINDING: R2Bucket; + D1_TESTING_SOMETHING: D1Database; + SERVICE_BINDING: Fetcher; + AE_DATASET_BINDING: AnalyticsEngineDataset; + NAMESPACE_BINDING: DispatchNamespace; + LOGFWDR_SCHEMA: any; + SOME_DATA_BLOB1: ArrayBuffer; + SOME_DATA_BLOB2: ArrayBuffer; + SOME_TEXT_BLOB1: string; + SOME_TEXT_BLOB2: string; + testing_unsafe: any; + UNSAFE_RATELIMIT: RateLimit; + TEST_QUEUE_BINDING: Queue; + SEND_EMAIL_BINDING: SendEmail; + VECTORIZE_BINDING: VectorizeIndex; + HYPERDRIVE_BINDING: Hyperdrive; + MTLS_BINDING: Fetcher; + BROWSER_BINDING: Fetcher; + AI_BINDING: Ai; + IMAGES_BINDING: ImagesBinding; + VERSION_METADATA_BINDING: { id: string; tag: string }; + ASSETS_BINDING: Fetcher; + } + } + interface Env extends Cloudflare.Env {} + type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; + }; + declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} + } + declare module \\"*.txt\\" { + const value: string; + export default value; + } + declare module \\"*.webp\\" { + const value: ArrayBuffer; + export default value; + } + declare module \\"*.wasm\\" { + const value: WebAssembly.Module; + export default value; + } + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.toml file. + " + `); + }); + it("should create a DTS file at the location that the command is executed from", async () => { fs.writeFileSync("./index.ts", "export default { async fetch () {} };"); fs.writeFileSync( From dc29d0d1d4fd295f6489f953b3a4884987e1f47a Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Thu, 13 Mar 2025 17:02:57 +0000 Subject: [PATCH 3/4] fix lint --- .../__tests__/process-env-populated.test.ts | 21 ------------------- .../wrangler/src/type-generation/index.ts | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/wrangler/src/__tests__/process-env-populated.test.ts b/packages/wrangler/src/__tests__/process-env-populated.test.ts index 0cb9349320ed..867dea11884b 100644 --- a/packages/wrangler/src/__tests__/process-env-populated.test.ts +++ b/packages/wrangler/src/__tests__/process-env-populated.test.ts @@ -1,26 +1,5 @@ import assert from "node:assert"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; -import dedent from "ts-dedent"; -import { bundleWorker } from "../deployment-bundle/bundle"; -import { noopModuleCollector } from "../deployment-bundle/module-collection"; import { isProcessEnvPopulated } from "../process-env"; -import { mockConsoleMethods } from "./helpers/mock-console"; -import { runInTempDir } from "./helpers/run-in-tmp"; - -/* - * This file contains inline comments with the word "javascript" - * This signals to a compatible editor extension that the template string - * contents should be syntax-highlighted as JavaScript. One such extension - * is zjcompt.es6-string-javascript, but there are others. - */ - -async function seedFs(files: Record): Promise { - for (const [location, contents] of Object.entries(files)) { - await mkdir(path.dirname(location), { recursive: true }); - await writeFile(location, contents); - } -} describe("isProcessEnvPopulated", () => { test("default", () => { diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 5ff6d4ca0cba..e88357634ec7 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -255,7 +255,7 @@ export async function generateEnvTypes( entrypoint?: Entry, log = true ): Promise<{ envHeader?: string; envTypes?: string }> { - let stringKeys: string[] = []; + const stringKeys: string[] = []; const secrets = getVarsForDev( // We do not want `getVarsForDev()` to merge in the standard vars into the dev vars // because we want to be able to work with secrets differently to vars. From 142f65e715efb89fdfcc9501806615625333f4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Somhairle=20MacLe=C3=B2id?= Date: Thu, 13 Mar 2025 18:41:46 +0000 Subject: [PATCH 4/4] Create green-shrimps-grab.md --- .changeset/green-shrimps-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/green-shrimps-grab.md diff --git a/.changeset/green-shrimps-grab.md b/.changeset/green-shrimps-grab.md new file mode 100644 index 000000000000..ec3e6e39af2d --- /dev/null +++ b/.changeset/green-shrimps-grab.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Add `wrangler types` support for importable env and `process.env`