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` 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__/process-env-populated.test.ts b/packages/wrangler/src/__tests__/process-env-populated.test.ts new file mode 100644 index 000000000000..867dea11884b --- /dev/null +++ b/packages/wrangler/src/__tests__/process-env-populated.test.ts @@ -0,0 +1,82 @@ +import assert from "node:assert"; +import { isProcessEnvPopulated } from "../process-env"; + +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 c0de7993ab21..01c79c7cdf02 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,38 +404,135 @@ 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; + } + 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 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; @@ -465,14 +571,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 +673,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 +771,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 +810,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 +854,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 +892,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 +934,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 +973,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 +1035,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 +1059,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 +1095,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 +1195,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 +1252,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..e88357634ec7 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 }> { + 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. @@ -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}`, }; }