diff --git a/CHANGELOG.md b/CHANGELOG.md index 5918e641e67c..3c49188c8545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,24 +100,19 @@ DataDog/import-in-the-middle#57 * This version does not support Node.js 18.19 or later -- Add support for additional env var files (#9961) and (#10093) +- Add support for loading more env var files (#9961, #10093, and #10094) Fixes #9877. This PR adds CLI functionality to load more `.env` files via `NODE_ENV` and an `--add-env-files` flag. - - Env vars loaded via `NODE_ENV` override the values in `.env`; if there are conflicts, they win out: + Env vars loaded via either of these methods override the values in `.env`: ``` - # Loads '.env.production', which overwrites values in '.env' + # Loads '.env.production', which overrides values in '.env' NODE_ENV=production yarn rw exec myScript - ``` - - Env vars loaded via `--add-env-files` only add to `process.env`; they will not override anything that was previously there: - ```bash - # Add new env vars defined in '.env.stripe' and '.env.nakama' - yarn rw exec myScript --add-env-files stripe nakama - # Or you can specify the flag twice: + # Load '.env.stripe' and '.env.nakama', which overrides values yarn rw exec myScript --add-env-files stripe --add-env-files nakama + # Or you can specify the flag once: + yarn rw exec myScript --add-env-files stripe nakama ``` Note that this feature is mainly for local scripting. Most deploy providers don't let you upload `.env` files (unless you're using baremetal) and usually have their own way of determining environments. diff --git a/packages/cli/src/__tests__/loadEnvFiles.test.js b/packages/cli/src/__tests__/loadEnvFiles.test.js index e940ce7201c2..07c8542d93c1 100644 --- a/packages/cli/src/__tests__/loadEnvFiles.test.js +++ b/packages/cli/src/__tests__/loadEnvFiles.test.js @@ -3,9 +3,9 @@ import path from 'path' import { afterEach, beforeAll, describe, expect, it, test } from 'vitest' import { - loadBasicEnvFiles, + loadDefaultEnvFiles, loadNodeEnvDerivedEnvFile, - addUserSpecifiedEnvFiles, + loadUserSpecifiedEnvFiles, } from '../lib/loadEnvFiles' describe('loadEnvFiles', () => { @@ -19,18 +19,18 @@ describe('loadEnvFiles', () => { it("doesn't load .env files if there are none to load", () => { const cwd = __dirname - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, []) + loadUserSpecifiedEnvFiles(cwd, []) expect(process.env).toEqual(originalProcessEnv) }) it("doesn't load .env files if not instructed to", () => { const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-prod') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, []) + loadUserSpecifiedEnvFiles(cwd, []) expect(process.env).toEqual(originalProcessEnv) }) @@ -39,9 +39,9 @@ describe('loadEnvFiles', () => { expect(process.env).not.toHaveProperty('PROD_DATABASE_URL') const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-prod') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, ['prod']) + loadUserSpecifiedEnvFiles(cwd, ['prod']) expect(process.env).toHaveProperty( 'PROD_DATABASE_URL', @@ -58,9 +58,9 @@ describe('loadEnvFiles', () => { expect(process.env).not.toHaveProperty('PROD_DATABASE_URL') const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-many') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, ['dev', 'prod']) + loadUserSpecifiedEnvFiles(cwd, ['dev', 'prod']) expect(process.env).toHaveProperty( 'DEV_DATABASE_URL', @@ -78,13 +78,13 @@ describe('loadEnvFiles', () => { expect(process.env).not.toHaveProperty('TEST_COLLISION') const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-collision') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, ['base', 'collision']) + loadUserSpecifiedEnvFiles(cwd, ['base', 'collision']) expect(process.env).toHaveProperty( 'DATABASE_URL', - 'postgresql://user:password@localhost:5432/mydb' + 'postgresql://user:password@localhost:5432/mycollisiondb' ) expect(process.env).toHaveProperty('TEST_BASE', '1') expect(process.env).toHaveProperty('TEST_COLLISION', '1') @@ -96,9 +96,9 @@ describe('loadEnvFiles', () => { process.env.NODE_ENV = 'bazinga' const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-node-env') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, []) + loadUserSpecifiedEnvFiles(cwd, []) expect(process.env).toHaveProperty( 'PROD_DATABASE_URL', @@ -113,13 +113,13 @@ describe('loadEnvFiles', () => { process.env.NODE_ENV = 'bazinga' const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-node-env') - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, ['prod']) + loadUserSpecifiedEnvFiles(cwd, ['prod']) expect(process.env).toHaveProperty( 'PROD_DATABASE_URL', - 'postgresql://user:password@localhost:5432/bazinga' + 'postgresql://user:password@localhost:5432/myproddb' ) expect(process.env).toHaveProperty('BAZINGA', '1') }) @@ -128,9 +128,9 @@ describe('loadEnvFiles', () => { const cwd = path.join(__dirname, '__fixtures__/redwood-app-env-node-env') try { - loadBasicEnvFiles(cwd) + loadDefaultEnvFiles(cwd) loadNodeEnvDerivedEnvFile(cwd) - addUserSpecifiedEnvFiles(cwd, ['missing']) + loadUserSpecifiedEnvFiles(cwd, ['missing']) } catch (error) { // Just testing that the error message reports the file it tried to load. expect(error.message).toMatch(/\.env\.missing/) diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 49b39450d408..cc3dfd34dc31 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -175,12 +175,13 @@ async function runYargs() { describe: 'Working directory to use (where `redwood.toml` is located)', }) .option('add-env-files', { - describe: 'Load additional .env files. These are incremental', + describe: + 'Load additional .env files. Values defined in files specified later override earlier ones.', array: true, }) .example( - 'yarn rw exec MigrateUsers --add-env-files prod stripe-prod', - '"Run a script, and also include .env.prod and .env.stripe-prod"' + 'yarn rw exec migrateUsers --add-env-files stripe nakama', + "Run a script, also loading env vars from '.env.stripe' and '.env.nakama'" ) .option('telemetry', { describe: 'Whether to send anonymous usage telemetry to RedwoodJS', diff --git a/packages/cli/src/lib/loadEnvFiles.js b/packages/cli/src/lib/loadEnvFiles.js index 3e4dab66b0af..f6b61e1ada8f 100644 --- a/packages/cli/src/lib/loadEnvFiles.js +++ b/packages/cli/src/lib/loadEnvFiles.js @@ -16,14 +16,9 @@ export function loadEnvFiles() { const { base } = getPaths() - // These override. - loadBasicEnvFiles(base) + loadDefaultEnvFiles(base) loadNodeEnvDerivedEnvFile(base) - // These are additive. I.e., They don't override existing env vars. - // defined in .env.defaults, .env, or .env.${NODE_ENV} - // - // Users have to opt-in to loading these files via `--add-env-files`. const { addEnvFiles } = Parser(hideBin(process.argv), { array: ['add-env-files'], default: { @@ -31,7 +26,7 @@ export function loadEnvFiles() { }, }) if (addEnvFiles.length > 0) { - addUserSpecifiedEnvFiles(base, addEnvFiles) + loadUserSpecifiedEnvFiles(base, addEnvFiles) } process.env.REDWOOD_ENV_FILES_LOADED = 'true' @@ -40,7 +35,7 @@ export function loadEnvFiles() { /** * @param {string} cwd */ -export function loadBasicEnvFiles(cwd) { +export function loadDefaultEnvFiles(cwd) { dotenvDefaultsConfig({ path: path.join(cwd, '.env'), defaults: path.join(cwd, '.env.defaults'), @@ -70,7 +65,7 @@ export function loadNodeEnvDerivedEnvFile(cwd) { /** * @param {string} cwd */ -export function addUserSpecifiedEnvFiles(cwd, addEnvFiles) { +export function loadUserSpecifiedEnvFiles(cwd, addEnvFiles) { for (const suffix of addEnvFiles) { const envPath = path.join(cwd, `.env.${suffix}`) if (!fs.pathExistsSync(envPath)) { @@ -79,6 +74,6 @@ export function addUserSpecifiedEnvFiles(cwd, addEnvFiles) { ) } - dotenvConfig({ path: envPath }) + dotenvConfig({ path: envPath, override: true }) } } diff --git a/tasks/server-tests/bothServer.test.mts b/tasks/server-tests/bothServer.test.mts index a68e54c9eb51..964f81b9e230 100644 --- a/tasks/server-tests/bothServer.test.mts +++ b/tasks/server-tests/bothServer.test.mts @@ -20,8 +20,9 @@ describe('rw serve', () => { --version Show version number [boolean] --cwd Working directory to use (where \`redwood.toml\` is located) - --add-env-files Load additional .env files. These - are incremental [array] + --add-env-files Load additional .env files. Values + defined in files specified later + override earlier ones. [array] --telemetry Whether to send anonymous usage telemetry to RedwoodJS [boolean] --webPort, --web-port The port for the web server to @@ -67,8 +68,9 @@ describe('rw serve', () => { --version Show version number [boolean] --cwd Working directory to use (where \`redwood.toml\` is located) - --add-env-files Load additional .env files. These - are incremental [array] + --add-env-files Load additional .env files. Values + defined in files specified later + override earlier ones. [array] --telemetry Whether to send anonymous usage telemetry to RedwoodJS [boolean] --webPort, --web-port The port for the web server to