diff --git a/.changeset/cloudflare-env-dot-env-files.md b/.changeset/cloudflare-env-dot-env-files.md new file mode 100644 index 00000000000..2fb580366e3 --- /dev/null +++ b/.changeset/cloudflare-env-dot-env-files.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +Respect `CLOUDFLARE_ENV` when selecting `.env.` and `.dev.vars.` files + +`CLOUDFLARE_ENV` is documented as an alternative to `--env`, and the config loader already falls back to it when `--env` is not passed. But the `.env`/`.dev.vars` file lookup only looked at `--env`, so running `CLOUDFLARE_ENV=staging wrangler dev` activated the `staging` config environment while still loading the top-level `.env`/`.dev.vars` files instead of `.env.staging`/`.dev.vars.staging`. + +Both lookups now fall back to `CLOUDFLARE_ENV` in the same way the config loader does. `--env` still takes precedence when both are set. diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index b256a4a4d82..ce35d8a11ad 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -1985,6 +1985,70 @@ describe.sequential("wrangler dev", () => { " `); }); + + it("should prefer `.dev.vars.` if `CLOUDFLARE_ENV` is set", async ({ + expect, + }) => { + fs.writeFileSync("index.js", `export default {};`); + fs.writeFileSync(".dev.vars", "DEFAULT_VAR=default"); + fs.writeFileSync(".dev.vars.custom", "CUSTOM_VAR=custom"); + + writeWranglerConfig({ main: "index.js", env: { custom: {} } }); + const config = await runWranglerUntilConfig("dev", { + CLOUDFLARE_ENV: "custom", + }); + const varBindings: Record = Object.fromEntries( + Object.entries(config.bindings ?? {}) + .filter( + ( + binding + ): binding is [ + string, + Extract, + ] => + binding[1].type === "plain_text" || + binding[1].type === "secret_text" + ) + .map(([b, v]) => [b, v.value]) + ); + + expect(varBindings).toEqual({ CUSTOM_VAR: "custom" }); + expect(std.out).toContain("Using secrets defined in .dev.vars.custom"); + }); + + it("should prefer `--env` over `CLOUDFLARE_ENV` when selecting `.dev.vars.`", async ({ + expect, + }) => { + fs.writeFileSync("index.js", `export default {};`); + fs.writeFileSync(".dev.vars", "DEFAULT_VAR=default"); + fs.writeFileSync(".dev.vars.custom", "CUSTOM_VAR=custom"); + fs.writeFileSync(".dev.vars.other", "OTHER_VAR=other"); + + writeWranglerConfig({ + main: "index.js", + env: { custom: {}, other: {} }, + }); + const config = await runWranglerUntilConfig("dev --env custom", { + CLOUDFLARE_ENV: "other", + }); + const varBindings: Record = Object.fromEntries( + Object.entries(config.bindings ?? {}) + .filter( + ( + binding + ): binding is [ + string, + Extract, + ] => + binding[1].type === "plain_text" || + binding[1].type === "secret_text" + ) + .map(([b, v]) => [b, v.value]) + ); + + expect(varBindings).toEqual({ CUSTOM_VAR: "custom" }); + expect(std.out).toContain("Using secrets defined in .dev.vars.custom"); + }); }); describe("secrets config", () => { @@ -2280,6 +2344,42 @@ describe.sequential("wrangler dev", () => { `); }); + it("should populate `process.env` from appropriate `.env.` files when CLOUDFLARE_ENV is set", async ({ + expect, + }) => { + await runWranglerUntilConfig("dev", { CLOUDFLARE_ENV: "custom" }); + const dotEnvVars = Object.fromEntries( + Object.entries(process.env).filter(([key]) => + key.startsWith("__DOT_ENV_LOCAL_DEV_VAR_") + ) + ); + expect(dotEnvVars).toEqual({ + __DOT_ENV_LOCAL_DEV_VAR_1: "custom-local-1", + __DOT_ENV_LOCAL_DEV_VAR_2: "custom-2", + __DOT_ENV_LOCAL_DEV_VAR_3: "custom-local-3", + __DOT_ENV_LOCAL_DEV_VAR_LOCAL: "custom-local", + }); + }); + + it("should get local dev `vars` from appropriate `.env.` files when CLOUDFLARE_ENV is set", async ({ + expect, + }) => { + await runWranglerUntilConfig("dev", { CLOUDFLARE_ENV: "custom" }); + const out = std.out; + expect(extractUsingVars(out)).toMatchInlineSnapshot(` + "Using secrets defined in .env + Using secrets defined in .env.custom + Using secrets defined in .env.custom.local + Using secrets defined in .env.local" + `); + expect(extractBindings(out)).toMatchInlineSnapshot(` + "env.__DOT_ENV_LOCAL_DEV_VAR_1 ("(hidden)") Environment Variable local + env.__DOT_ENV_LOCAL_DEV_VAR_2 ("(hidden)") Environment Variable local + env.__DOT_ENV_LOCAL_DEV_VAR_3 ("(hidden)") Environment Variable local + env.__DOT_ENV_LOCAL_DEV_VAR_LOCAL ("(hidden)") Environment Variable local" + `); + }); + it("should get local dev vars from appropriate `.env` files when --env= is set but no .env. file exists", async ({ expect, }) => { diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index a482c5e409b..144a5a66ba2 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -6,6 +6,7 @@ import { configFileName, formatConfigSnippet, getTodaysCompatDate, + getCloudflareEnv, getDisableConfigWatching, getDockerPath, UserError, @@ -248,7 +249,10 @@ async function resolveBindings( }> { const bindings = getBindings( config, - input.env, + // The config loader resolves the environment as `--env` then + // `CLOUDFLARE_ENV`; match that here so the `.env.`/`.dev.vars.` + // files line up with the config environment that was actually applied. + input.env ?? getCloudflareEnv(), input.envFiles, !input.dev?.remote, input.bindings, diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 2501578b7e7..61b94f8bc4c 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -4,6 +4,7 @@ import { checkMacOSVersion, setLogLevel } from "@cloudflare/cli-shared-helpers"; import { CommandLineArgsError, experimental_readRawConfig, + getCloudflareEnv, } from "@cloudflare/workers-utils"; import chalk from "chalk"; import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; @@ -662,8 +663,11 @@ export function createCLIParser(argv: string[]) { .check(demandSingleValue("env")) .check((args) => { // Set process environment params from `.env` files if available. + // The environment is resolved the same way as in the config loader, so + // that `.env.` files line up with the active config environment when + // it comes from `CLOUDFLARE_ENV` rather than `--env`. const resolvedEnvFilePaths = ( - args["env-file"] ?? getDefaultEnvFiles(args.env) + args["env-file"] ?? getDefaultEnvFiles(args.env ?? getCloudflareEnv()) ).map((p) => resolve(p)); process.env = loadDotEnv(resolvedEnvFilePaths, { includeProcessEnv: true,