Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process multiple files in options.path, if provided. #805

Merged
merged 3 commits into from Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Expand Up @@ -336,7 +336,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 @@ -366,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.
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
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
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