From 92695c6ef7c09c161bf57c249bc6dd5170a86525 Mon Sep 17 00:00:00 2001 From: Thanos Diacakis Date: Tue, 6 Feb 2024 19:11:11 -0800 Subject: [PATCH 1/3] Process multiple files in options.path, if provided. --- lib/main.js | 63 +++++++++++++++++++++++++++----------------- tests/test-config.js | 19 ++++++++++++- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/lib/main.js b/lib/main.js index 069f49c9..c5dab345 100644 --- a/lib/main.js +++ b/lib/main.js @@ -203,37 +203,54 @@ function _configVault (options) { } function configDotenv (options) { - let dotenvPath = path.resolve(process.cwd(), '.env') + const dotenvPath = path.resolve(process.cwd(), '.env') let encoding = 'utf8' const debug = Boolean(options && options.debug) - if (options) { - if (options.path != null) { - let envPath = options.path + if (options?.encoding) { + encoding = options.encoding + } else { + if (debug) { + _debug('No encoding is specified. UTF-8 is used by default') + } + } - if (Array.isArray(envPath)) { - for (const filepath of options.path) { - if (fs.existsSync(filepath)) { - envPath = filepath - break - } - } + let optionPathsThatExist = [] + if (options?.path) { + if (!Array.isArray(options.path)) { + if (fs.existsSync(options.path)) { + optionPathsThatExist = [_resolveHome(options.path)] } - - dotenvPath = _resolveHome(envPath) - } - if (options.encoding != null) { - encoding = options.encoding } else { - if (debug) { - _debug('No encoding is specified. UTF-8 is used by default') + for (const filepath of options.path) { + if (fs.existsSync(filepath)) { + optionPathsThatExist.push(_resolveHome(filepath)) + } } } + + if (!optionPathsThatExist.length) { + optionPathsThatExist = [dotenvPath] + } } + // If we have options.path, and it had valid paths, use them. Else fall back to .env + const pathsToProcess = optionPathsThatExist.length ? optionPathsThatExist : [dotenvPath] + + // Build the parsed data in a temporary object. As we iterate through the multiple option.path objects, + // we will not want to be overriding any variable after the first time it is discovered. Thus, the + // "options.override" will be false. Once we have the final parsed data, we will populate the process.env + // (or options.processEnv) object with the parsed data, and for that merge, we will consider the "options.override" + + const parsed = {} try { - // Specifying an encoding returns a string instead of a buffer - const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding })) + const mergeOptions = { ...options } + delete mergeOptions.override + for (const path of pathsToProcess) { + // Specifying an encoding returns a string instead of a buffer + const singleFileParsed = DotenvModule.parse(fs.readFileSync(path, { encoding })) + DotenvModule.populate(parsed, singleFileParsed, mergeOptions) + } let processEnv = process.env if (options && options.processEnv != null) { @@ -241,15 +258,13 @@ function configDotenv (options) { } DotenvModule.populate(processEnv, parsed, options) - - return { parsed } } catch (e) { if (debug) { - _debug(`Failed to load ${dotenvPath} ${e.message}`) + _debug(`Failed to load ${pathsToProcess} ${e.message}`) } - return { error: e } } + return { parsed } } // Populates process.env from .env file diff --git a/tests/test-config.js b/tests/test-config.js index fc420476..a3780a6d 100644 --- a/tests/test-config.js +++ b/tests/test-config.js @@ -41,7 +41,7 @@ t.test('takes two or more files in the array for path option', ct => { ct.end() }) -t.test('sets values from both .env.local and .env. first file key wins.', { skip: true }, ct => { +t.test('sets values from both .env.local and .env. first file key wins.', ct => { delete process.env.SINGLE_QUOTES const testPath = ['tests/.env.local', 'tests/.env'] @@ -62,6 +62,19 @@ t.test('sets values from both .env.local and .env. first file key wins.', { skip ct.end() }) +t.test('sets values from both .env.local and .env. but none is used as value existed in process.env.', ct => { + const testPath = ['tests/.env.local', 'tests/.env'] + process.env.BASIC = 'existing' + + const env = dotenv.config({ path: testPath }) + + // does not override process.env + ct.equal(env.parsed.BASIC, 'local_basic') + ct.equal(process.env.BASIC, 'existing') + + ct.end() +}) + t.test('takes URL for path option', ct => { const envPath = path.resolve(__dirname, '.env') const fileUrl = new URL(`file://${envPath}`) @@ -75,12 +88,16 @@ t.test('takes URL for path option', ct => { }) t.test('takes option for path along with home directory char ~', ct => { + const existsSyncStub = sinon.stub(fs, 'existsSync').returns(true) const readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo') const mockedHomedir = '/Users/dummy' const homedirStub = sinon.stub(os, 'homedir').returns(mockedHomedir) const testPath = '~/.env' dotenv.config({ path: testPath }) + ct.equal(existsSyncStub.args[0][0], testPath) + ct.ok(existsSyncStub.called) + ct.equal(readFileSyncStub.args[0][0], path.join(mockedHomedir, '.env')) ct.ok(homedirStub.called) From 0aa78182f69a12568537d5c3d8b557d8506d0867 Mon Sep 17 00:00:00 2001 From: Thanos Diacakis Date: Tue, 6 Feb 2024 19:18:31 -0800 Subject: [PATCH 2/3] Clarified documentation. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf3199e3..faa1f2d2 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,9 @@ Specify a custom path if your file containing environment variables is located e require('dotenv').config({ path: '/custom/path/to/.env' }) ``` -By default, `config` will look for a file called .env in the current working directory. Pass in multiple files as an array, and they will be loaded in order. The first value set for a variable will win. +By default, `config` will look for a file called .env in the current working directory. + +Pass in multiple files as an array, and they will be parsed in order and combined. The first value set for a variable will win. There is no overriding. The combined result will then be applied to `process.env` (or `options.processEnv`, if set). For this final application the `options.override` flag is respected. ```js require('dotenv').config({ path: ['.env.local', '.env'] }) @@ -366,7 +368,7 @@ require('dotenv').config({ debug: process.env.DEBUG }) Default: `false` -Override any environment variables that have already been set on your machine with values from your .env file. +Override any environment variables that have already been set on your machine with values from your .env file(s). If multiple files have been provided in `option.path` the override does not apply while the files are being combined (see `options.path`). It applies though at the merging of the combined results with `process.env` (or `optiors.processEnv`, if set). ```js require('dotenv').config({ override: true }) From 5ae3ed2d1a6189d428f6c329efc277b3170211bb Mon Sep 17 00:00:00 2001 From: Thanos Diacakis Date: Mon, 12 Feb 2024 08:22:47 -0800 Subject: [PATCH 3/3] Updating to always use `override` flag --- README.md | 6 ++++-- lib/main.js | 10 +++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index faa1f2d2..c948b2bf 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,9 @@ require('dotenv').config({ path: '/custom/path/to/.env' }) By default, `config` will look for a file called .env in the current working directory. -Pass in multiple files as an array, and they will be parsed in order and combined. The first value set for a variable will win. There is no overriding. The combined result will then be applied to `process.env` (or `options.processEnv`, if set). For this final application the `options.override` flag is respected. +Pass in multiple files as an array, and they will be parsed in order and combined with `process.env` (or `option.processEnv`, if set). The first value set for a variable will win, unless the `options.override` flag is set, in which case the last value set will win. If a value already exists in `process.env` and the `options.override` flag is NOT set, no changes will be made to that value. + +```js ```js require('dotenv').config({ path: ['.env.local', '.env'] }) @@ -368,7 +370,7 @@ require('dotenv').config({ debug: process.env.DEBUG }) Default: `false` -Override any environment variables that have already been set on your machine with values from your .env file(s). If multiple files have been provided in `option.path` the override does not apply while the files are being combined (see `options.path`). It applies though at the merging of the combined results with `process.env` (or `optiors.processEnv`, if set). +Override any environment variables that have already been set on your machine with values from your .env file(s). If multiple files have been provided in `option.path` the override will also be used as each file is combined with the next. Without `override` being set, the first value wins. With `override` set the last value wins. ```js require('dotenv').config({ override: true }) diff --git a/lib/main.js b/lib/main.js index c5dab345..22612d59 100644 --- a/lib/main.js +++ b/lib/main.js @@ -237,19 +237,15 @@ function configDotenv (options) { // If we have options.path, and it had valid paths, use them. Else fall back to .env const pathsToProcess = optionPathsThatExist.length ? optionPathsThatExist : [dotenvPath] - // Build the parsed data in a temporary object. As we iterate through the multiple option.path objects, - // we will not want to be overriding any variable after the first time it is discovered. Thus, the - // "options.override" will be false. Once we have the final parsed data, we will populate the process.env - // (or options.processEnv) object with the parsed data, and for that merge, we will consider the "options.override" + // Build the parsed data in a temporary object (because we need to return it). Once we have the final + // parsed data, we will combine it with process.env (or options.processEnv if provided). const parsed = {} try { - const mergeOptions = { ...options } - delete mergeOptions.override for (const path of pathsToProcess) { // Specifying an encoding returns a string instead of a buffer const singleFileParsed = DotenvModule.parse(fs.readFileSync(path, { encoding })) - DotenvModule.populate(parsed, singleFileParsed, mergeOptions) + DotenvModule.populate(parsed, singleFileParsed, options) } let processEnv = process.env