Skip to content

Commit

Permalink
Merge pull request #805 from thanosd/bug/fix-merge-multiple-files
Browse files Browse the repository at this point in the history
Process multiple files in options.path, if provided.
  • Loading branch information
motdotla authored Feb 12, 2024
2 parents f6e87eb + 5ae3ed2 commit fe5ac4d
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 27 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ 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 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'] })
Expand Down Expand Up @@ -391,7 +395,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 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 })
Expand Down
59 changes: 35 additions & 24 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,53 +203,64 @@ 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 (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 {
// Specifying an encoding returns a string instead of a buffer
const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))
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, options)
}

let processEnv = process.env
if (options && options.processEnv != null) {
processEnv = options.processEnv
}

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
Expand Down
19 changes: 18 additions & 1 deletion tests/test-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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}`)
Expand All @@ -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)

Expand Down

0 comments on commit fe5ac4d

Please sign in to comment.