Skip to content

Commit 6a47b2c

Browse files
authored
feat(dotenv-flow): implement options.files, closes #83 (#87)
Add the `files` option which allows explicitly specifying a list (and the order) of `.env*` files to load.
1 parent 9dd5a8c commit 6a47b2c

File tree

5 files changed

+289
-14
lines changed

5 files changed

+289
-14
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,24 @@ For example, if you set the pattern to `".env/[local/]env[.node_env]"`,
460460

461461
› Please refer to [`.listFiles([options])`](#listfiles-options--string) to dive deeper.
462462

463+
##### `options.files`
464+
###### Type: `string[]`
465+
466+
Allows explicitly specifying a list (and the order) of `.env*` files to load.
467+
468+
Note that options like `node_env`, `default_node_env`, and `pattern` are ignored in this case.
469+
470+
```js
471+
require('dotenv-flow').config({
472+
files: [
473+
'.env',
474+
'.env.local',
475+
`.env.${process.env.NODE_ENV}`, // '.env.development'
476+
`.env.${process.env.NODE_ENV}.local` // '.env.development.local'
477+
]
478+
});
479+
```
480+
463481
##### `options.encoding`
464482
###### Type: `string`
465483
###### Default: `"utf8"`

lib/dotenv-flow.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export function unload(filenames: string | string[], options?: DotenvFlowParseOp
113113
export type DotenvFlowConfigOptions = DotenvFlowListFilesOptions & DotenvFlowLoadOptions & {
114114
default_node_env?: string;
115115
purge_dotenv?: boolean;
116+
files?: string[];
116117
}
117118

118119
export type DotenvFlowConfigResult<T extends DotenvFlowParseResult = DotenvFlowParseResult> = DotenvFlowLoadResult<T>;

lib/dotenv-flow.js

+37-11
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ const CONFIG_OPTION_KEYS = [
272272
'default_node_env',
273273
'path',
274274
'pattern',
275+
'files',
275276
'encoding',
276277
'purge_dotenv',
277278
'silent'
@@ -287,6 +288,7 @@ const CONFIG_OPTION_KEYS = [
287288
* @param {string} [options.default_node_env] - the default node environment
288289
* @param {string} [options.path=process.cwd()] - path to `.env*` files directory
289290
* @param {string} [options.pattern=".env[.node_env][.local]"] - `.env*` files' naming convention pattern
291+
* @param {string[]} [options.files] - an explicit list of `.env*` files to load (note that `options.[default_]node_env` and `options.pattern` are ignored in this case)
290292
* @param {string} [options.encoding="utf8"] - encoding of `.env*` files
291293
* @param {boolean} [options.purge_dotenv=false] - perform the `.env` file {@link unload}
292294
* @param {boolean} [options.debug=false] - turn on detailed logging to help debug why certain variables are not being set as you expect
@@ -302,8 +304,6 @@ function config(options = {}) {
302304
.forEach(key => debug(`| options.${key} =`, options[key]));
303305
}
304306

305-
const node_env = getEffectiveNodeEnv(options);
306-
307307
const {
308308
path = process.cwd(),
309309
pattern = DEFAULT_PATTERN
@@ -324,17 +324,43 @@ function config(options = {}) {
324324
}
325325

326326
try {
327-
const filenames = listFiles({ node_env, path, pattern, debug: options.debug });
328-
329-
if (filenames.length === 0) {
330-
const _pattern = node_env
331-
? pattern.replace(NODE_ENV_PLACEHOLDER_REGEX, `[$1${node_env}$2]`)
332-
: pattern;
327+
let filenames;
333328

334-
return failure(
335-
new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`),
336-
options
329+
if (options.files) {
330+
options.debug && debug(
331+
'using explicit list of `.env*` files: %s…',
332+
options.files.join(', ')
337333
);
334+
335+
filenames = options.files
336+
.reduce((list, basename) => {
337+
const filename = p.resolve(path, basename);
338+
339+
if (fs.existsSync(filename)) {
340+
list.push(filename);
341+
}
342+
else if (options.debug) {
343+
debug('>> %s does not exist, skipping…', filename);
344+
}
345+
346+
return list;
347+
}, []);
348+
}
349+
else {
350+
const node_env = getEffectiveNodeEnv(options);
351+
352+
filenames = listFiles({ node_env, path, pattern, debug: options.debug });
353+
354+
if (filenames.length === 0) {
355+
const _pattern = node_env
356+
? pattern.replace(NODE_ENV_PLACEHOLDER_REGEX, `[$1${node_env}$2]`)
357+
: pattern;
358+
359+
return failure(
360+
new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`),
361+
options
362+
);
363+
}
338364
}
339365

340366
const result = load(filenames, {

test/types/dotenv-flow.ts

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ dotenvFlow.config({ node_env: 'production' });
8484
dotenvFlow.config({ default_node_env: 'development' });
8585
dotenvFlow.config({ path: '/path/to/project' });
8686
dotenvFlow.config({ pattern: '.env[.node_env][.local]' });
87+
dotenvFlow.config({ files: ['.env', '.env.local'] });
8788
dotenvFlow.config({ encoding: 'utf8' });
8889
dotenvFlow.config({ purge_dotenv: true });
8990
dotenvFlow.config({ debug: true });
@@ -93,6 +94,7 @@ dotenvFlow.config({
9394
default_node_env: 'development',
9495
path: '/path/to/project',
9596
pattern: '.env[.node_env][.local]',
97+
files: ['.env', '.env.local'],
9698
encoding: 'utf8',
9799
purge_dotenv: true,
98100
debug: true,

test/unit/dotenv-flow-api.spec.js

+231-3
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,211 @@ describe('dotenv-flow (API)', () => {
14651465
});
14661466
});
14671467

1468+
describe('when `options.files` is given', () => {
1469+
let options;
1470+
1471+
beforeEach('setup `options.files`', () => {
1472+
options = {
1473+
files: [
1474+
'.env',
1475+
'.env.production',
1476+
'.env.local'
1477+
]
1478+
};
1479+
});
1480+
1481+
it('loads the given list of files', () => {
1482+
mockFS({
1483+
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok',
1484+
'/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok',
1485+
'/path/to/project/.env.production': 'PRODUCTION_ENV_VAR=ok',
1486+
'/path/to/project/.env.production.local': 'LOCAL_PRODUCTION_ENV_VAR=ok'
1487+
});
1488+
1489+
expect(process.env)
1490+
.to.not.have.keys([
1491+
'DEFAULT_ENV_VAR',
1492+
'PRODUCTION_ENV_VAR',
1493+
'LOCAL_ENV_VAR',
1494+
'LOCAL_PRODUCTION_ENV_VAR'
1495+
]);
1496+
1497+
const result = dotenvFlow.config(options);
1498+
1499+
expect(result)
1500+
.to.be.an('object')
1501+
.with.property('parsed')
1502+
.that.deep.equals({
1503+
DEFAULT_ENV_VAR: 'ok',
1504+
PRODUCTION_ENV_VAR: 'ok',
1505+
LOCAL_ENV_VAR: 'ok'
1506+
});
1507+
1508+
expect(process.env)
1509+
.to.include({
1510+
DEFAULT_ENV_VAR: 'ok',
1511+
PRODUCTION_ENV_VAR: 'ok',
1512+
LOCAL_ENV_VAR: 'ok'
1513+
});
1514+
1515+
expect(process.env)
1516+
.to.not.have.key('LOCAL_PRODUCTION_ENV_VAR');
1517+
});
1518+
1519+
it('ignores `options.node_env`', () => {
1520+
options.node_env = 'development';
1521+
1522+
mockFS({
1523+
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok',
1524+
'/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok',
1525+
'/path/to/project/.env.development': 'DEVELOPMENT_ENV_VAR=ok',
1526+
'/path/to/project/.env.production': 'PRODUCTION_ENV_VAR=ok'
1527+
});
1528+
1529+
expect(process.env)
1530+
.to.not.have.keys([
1531+
'DEFAULT_ENV_VAR',
1532+
'DEVELOPMENT_ENV_VAR',
1533+
'PRODUCTION_ENV_VAR',
1534+
'LOCAL_ENV_VAR'
1535+
]);
1536+
1537+
const result = dotenvFlow.config(options);
1538+
1539+
expect(result)
1540+
.to.be.an('object')
1541+
.with.property('parsed')
1542+
.that.deep.equals({
1543+
DEFAULT_ENV_VAR: 'ok',
1544+
PRODUCTION_ENV_VAR: 'ok',
1545+
LOCAL_ENV_VAR: 'ok'
1546+
});
1547+
1548+
expect(process.env)
1549+
.to.include({
1550+
DEFAULT_ENV_VAR: 'ok',
1551+
PRODUCTION_ENV_VAR: 'ok',
1552+
LOCAL_ENV_VAR: 'ok'
1553+
});
1554+
1555+
expect(process.env)
1556+
.to.not.have.key('DEVELOPMENT_ENV_VAR');
1557+
});
1558+
1559+
it('loads the list of files in the given order', () => {
1560+
mockFS({
1561+
'/path/to/project/.env': (
1562+
'DEFAULT_ENV_VAR=ok\n' +
1563+
'PRODUCTION_ENV_VAR="should be overwritten by `.env.production"`'
1564+
),
1565+
'/path/to/project/.env.local': (
1566+
'LOCAL_ENV_VAR=ok'
1567+
),
1568+
'/path/to/project/.env.production': (
1569+
'LOCAL_ENV_VAR="should be overwritten by `.env.local"`\n' +
1570+
'PRODUCTION_ENV_VAR=ok'
1571+
)
1572+
});
1573+
1574+
expect(process.env)
1575+
.to.not.have.keys([
1576+
'DEFAULT_ENV_VAR',
1577+
'PRODUCTION_ENV_VAR',
1578+
'LOCAL_ENV_VAR'
1579+
]);
1580+
1581+
const result = dotenvFlow.config(options);
1582+
1583+
expect(result)
1584+
.to.be.an('object')
1585+
.with.property('parsed')
1586+
.that.deep.equals({
1587+
DEFAULT_ENV_VAR: 'ok',
1588+
PRODUCTION_ENV_VAR: 'ok',
1589+
LOCAL_ENV_VAR: 'ok'
1590+
});
1591+
1592+
expect(process.env)
1593+
.to.include({
1594+
DEFAULT_ENV_VAR: 'ok',
1595+
PRODUCTION_ENV_VAR: 'ok',
1596+
LOCAL_ENV_VAR: 'ok'
1597+
});
1598+
});
1599+
1600+
it('ignores missing files', () => {
1601+
mockFS({
1602+
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok'
1603+
});
1604+
1605+
expect(process.env)
1606+
.to.not.have.keys([
1607+
'DEFAULT_ENV_VAR',
1608+
'PRODUCTION_ENV_VAR',
1609+
'LOCAL_ENV_VAR'
1610+
]);
1611+
1612+
const result = dotenvFlow.config(options);
1613+
1614+
expect(result)
1615+
.to.be.an('object')
1616+
.with.property('parsed')
1617+
.that.deep.equals({
1618+
DEFAULT_ENV_VAR: 'ok'
1619+
});
1620+
1621+
expect(process.env)
1622+
.to.include({
1623+
DEFAULT_ENV_VAR: 'ok'
1624+
});
1625+
1626+
expect(process.env)
1627+
.to.not.have.keys([
1628+
'PRODUCTION_ENV_VAR',
1629+
'LOCAL_ENV_VAR'
1630+
]);
1631+
});
1632+
1633+
describe('… and `options.path` is given', () => {
1634+
beforeEach('setup `options.path`', () => {
1635+
options.path = '/path/to/another/project';
1636+
});
1637+
1638+
it('uses the given `options.path` as a working directory', () => {
1639+
mockFS({
1640+
'/path/to/another/project/.env': 'DEFAULT_ENV_VAR=ok',
1641+
'/path/to/another/project/.env.production': 'PRODUCTION_ENV_VAR=ok',
1642+
'/path/to/another/project/.env.local': 'LOCAL_ENV_VAR=ok'
1643+
});
1644+
1645+
expect(process.env)
1646+
.to.not.have.keys([
1647+
'DEFAULT_ENV_VAR',
1648+
'PRODUCTION_ENV_VAR',
1649+
'LOCAL_ENV_VAR'
1650+
]);
1651+
1652+
const result = dotenvFlow.config(options);
1653+
1654+
expect(result)
1655+
.to.be.an('object')
1656+
.with.property('parsed')
1657+
.that.deep.equals({
1658+
DEFAULT_ENV_VAR: 'ok',
1659+
PRODUCTION_ENV_VAR: 'ok',
1660+
LOCAL_ENV_VAR: 'ok'
1661+
});
1662+
1663+
expect(process.env)
1664+
.to.include({
1665+
DEFAULT_ENV_VAR: 'ok',
1666+
PRODUCTION_ENV_VAR: 'ok',
1667+
LOCAL_ENV_VAR: 'ok'
1668+
});
1669+
});
1670+
});
1671+
});
1672+
14681673
describe('when `options.encoding` is given', () => {
14691674
let options;
14701675

@@ -1560,9 +1765,7 @@ describe('dotenv-flow (API)', () => {
15601765
let options;
15611766

15621767
beforeEach('setup `options.debug`', () => {
1563-
options = {
1564-
debug: true
1565-
};
1768+
options = { debug: true };
15661769
});
15671770

15681771
beforeEach('stub `console.debug`', () => {
@@ -1658,6 +1861,31 @@ describe('dotenv-flow (API)', () => {
16581861
.to.have.been.calledWithMatch('options.silent', false);
16591862
});
16601863

1864+
it('prints out initialization options [4]', () => {
1865+
dotenvFlow.config({
1866+
...options,
1867+
path: '/path/to/another/project',
1868+
files: [
1869+
'.env',
1870+
'.env.production',
1871+
'.env.local'
1872+
],
1873+
});
1874+
1875+
expect(console.debug)
1876+
.to.have.been.calledWithMatch(/dotenv-flow\b.*init/);
1877+
1878+
expect(console.debug)
1879+
.to.have.been.calledWithMatch('options.path', '/path/to/another/project');
1880+
1881+
expect(console.debug)
1882+
.to.have.been.calledWithMatch('options.files', [
1883+
'.env',
1884+
'.env.production',
1885+
'.env.local'
1886+
]);
1887+
});
1888+
16611889
it('prints out effective node_env set by `options.node_env`', () => {
16621890
dotenvFlow.config({
16631891
...options,

0 commit comments

Comments
 (0)