Skip to content

Commit

Permalink
feat(dotenv-flow): implement options.files, closes #83 (#87)
Browse files Browse the repository at this point in the history
Add the `files` option which allows explicitly specifying a list (and the order) of `.env*` files to load.
  • Loading branch information
kerimdzhanov authored Dec 12, 2023
1 parent 9dd5a8c commit 6a47b2c
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 14 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,24 @@ For example, if you set the pattern to `".env/[local/]env[.node_env]"`,

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

##### `options.files`
###### Type: `string[]`

Allows explicitly specifying a list (and the order) of `.env*` files to load.

Note that options like `node_env`, `default_node_env`, and `pattern` are ignored in this case.

```js
require('dotenv-flow').config({
files: [
'.env',
'.env.local',
`.env.${process.env.NODE_ENV}`, // '.env.development'
`.env.${process.env.NODE_ENV}.local` // '.env.development.local'
]
});
```

##### `options.encoding`
###### Type: `string`
###### Default: `"utf8"`
Expand Down
1 change: 1 addition & 0 deletions lib/dotenv-flow.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function unload(filenames: string | string[], options?: DotenvFlowParseOp
export type DotenvFlowConfigOptions = DotenvFlowListFilesOptions & DotenvFlowLoadOptions & {
default_node_env?: string;
purge_dotenv?: boolean;
files?: string[];
}

export type DotenvFlowConfigResult<T extends DotenvFlowParseResult = DotenvFlowParseResult> = DotenvFlowLoadResult<T>;
Expand Down
48 changes: 37 additions & 11 deletions lib/dotenv-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ const CONFIG_OPTION_KEYS = [
'default_node_env',
'path',
'pattern',
'files',
'encoding',
'purge_dotenv',
'silent'
Expand All @@ -287,6 +288,7 @@ const CONFIG_OPTION_KEYS = [
* @param {string} [options.default_node_env] - the default node environment
* @param {string} [options.path=process.cwd()] - path to `.env*` files directory
* @param {string} [options.pattern=".env[.node_env][.local]"] - `.env*` files' naming convention pattern
* @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)
* @param {string} [options.encoding="utf8"] - encoding of `.env*` files
* @param {boolean} [options.purge_dotenv=false] - perform the `.env` file {@link unload}
* @param {boolean} [options.debug=false] - turn on detailed logging to help debug why certain variables are not being set as you expect
Expand All @@ -302,8 +304,6 @@ function config(options = {}) {
.forEach(key => debug(`| options.${key} =`, options[key]));
}

const node_env = getEffectiveNodeEnv(options);

const {
path = process.cwd(),
pattern = DEFAULT_PATTERN
Expand All @@ -324,17 +324,43 @@ function config(options = {}) {
}

try {
const filenames = listFiles({ node_env, path, pattern, debug: options.debug });

if (filenames.length === 0) {
const _pattern = node_env
? pattern.replace(NODE_ENV_PLACEHOLDER_REGEX, `[$1${node_env}$2]`)
: pattern;
let filenames;

return failure(
new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`),
options
if (options.files) {
options.debug && debug(
'using explicit list of `.env*` files: %s…',
options.files.join(', ')
);

filenames = options.files
.reduce((list, basename) => {
const filename = p.resolve(path, basename);

if (fs.existsSync(filename)) {
list.push(filename);
}
else if (options.debug) {
debug('>> %s does not exist, skipping…', filename);
}

return list;
}, []);
}
else {
const node_env = getEffectiveNodeEnv(options);

filenames = listFiles({ node_env, path, pattern, debug: options.debug });

if (filenames.length === 0) {
const _pattern = node_env
? pattern.replace(NODE_ENV_PLACEHOLDER_REGEX, `[$1${node_env}$2]`)
: pattern;

return failure(
new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`),
options
);
}
}

const result = load(filenames, {
Expand Down
2 changes: 2 additions & 0 deletions test/types/dotenv-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ dotenvFlow.config({ node_env: 'production' });
dotenvFlow.config({ default_node_env: 'development' });
dotenvFlow.config({ path: '/path/to/project' });
dotenvFlow.config({ pattern: '.env[.node_env][.local]' });
dotenvFlow.config({ files: ['.env', '.env.local'] });
dotenvFlow.config({ encoding: 'utf8' });
dotenvFlow.config({ purge_dotenv: true });
dotenvFlow.config({ debug: true });
Expand All @@ -93,6 +94,7 @@ dotenvFlow.config({
default_node_env: 'development',
path: '/path/to/project',
pattern: '.env[.node_env][.local]',
files: ['.env', '.env.local'],
encoding: 'utf8',
purge_dotenv: true,
debug: true,
Expand Down
234 changes: 231 additions & 3 deletions test/unit/dotenv-flow-api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,211 @@ describe('dotenv-flow (API)', () => {
});
});

describe('when `options.files` is given', () => {
let options;

beforeEach('setup `options.files`', () => {
options = {
files: [
'.env',
'.env.production',
'.env.local'
]
};
});

it('loads the given list of files', () => {
mockFS({
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok',
'/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok',
'/path/to/project/.env.production': 'PRODUCTION_ENV_VAR=ok',
'/path/to/project/.env.production.local': 'LOCAL_PRODUCTION_ENV_VAR=ok'
});

expect(process.env)
.to.not.have.keys([
'DEFAULT_ENV_VAR',
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR',
'LOCAL_PRODUCTION_ENV_VAR'
]);

const result = dotenvFlow.config(options);

expect(result)
.to.be.an('object')
.with.property('parsed')
.that.deep.equals({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.include({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.not.have.key('LOCAL_PRODUCTION_ENV_VAR');
});

it('ignores `options.node_env`', () => {
options.node_env = 'development';

mockFS({
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok',
'/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok',
'/path/to/project/.env.development': 'DEVELOPMENT_ENV_VAR=ok',
'/path/to/project/.env.production': 'PRODUCTION_ENV_VAR=ok'
});

expect(process.env)
.to.not.have.keys([
'DEFAULT_ENV_VAR',
'DEVELOPMENT_ENV_VAR',
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR'
]);

const result = dotenvFlow.config(options);

expect(result)
.to.be.an('object')
.with.property('parsed')
.that.deep.equals({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.include({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.not.have.key('DEVELOPMENT_ENV_VAR');
});

it('loads the list of files in the given order', () => {
mockFS({
'/path/to/project/.env': (
'DEFAULT_ENV_VAR=ok\n' +
'PRODUCTION_ENV_VAR="should be overwritten by `.env.production"`'
),
'/path/to/project/.env.local': (
'LOCAL_ENV_VAR=ok'
),
'/path/to/project/.env.production': (
'LOCAL_ENV_VAR="should be overwritten by `.env.local"`\n' +
'PRODUCTION_ENV_VAR=ok'
)
});

expect(process.env)
.to.not.have.keys([
'DEFAULT_ENV_VAR',
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR'
]);

const result = dotenvFlow.config(options);

expect(result)
.to.be.an('object')
.with.property('parsed')
.that.deep.equals({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.include({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});
});

it('ignores missing files', () => {
mockFS({
'/path/to/project/.env': 'DEFAULT_ENV_VAR=ok'
});

expect(process.env)
.to.not.have.keys([
'DEFAULT_ENV_VAR',
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR'
]);

const result = dotenvFlow.config(options);

expect(result)
.to.be.an('object')
.with.property('parsed')
.that.deep.equals({
DEFAULT_ENV_VAR: 'ok'
});

expect(process.env)
.to.include({
DEFAULT_ENV_VAR: 'ok'
});

expect(process.env)
.to.not.have.keys([
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR'
]);
});

describe('… and `options.path` is given', () => {
beforeEach('setup `options.path`', () => {
options.path = '/path/to/another/project';
});

it('uses the given `options.path` as a working directory', () => {
mockFS({
'/path/to/another/project/.env': 'DEFAULT_ENV_VAR=ok',
'/path/to/another/project/.env.production': 'PRODUCTION_ENV_VAR=ok',
'/path/to/another/project/.env.local': 'LOCAL_ENV_VAR=ok'
});

expect(process.env)
.to.not.have.keys([
'DEFAULT_ENV_VAR',
'PRODUCTION_ENV_VAR',
'LOCAL_ENV_VAR'
]);

const result = dotenvFlow.config(options);

expect(result)
.to.be.an('object')
.with.property('parsed')
.that.deep.equals({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});

expect(process.env)
.to.include({
DEFAULT_ENV_VAR: 'ok',
PRODUCTION_ENV_VAR: 'ok',
LOCAL_ENV_VAR: 'ok'
});
});
});
});

describe('when `options.encoding` is given', () => {
let options;

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

beforeEach('setup `options.debug`', () => {
options = {
debug: true
};
options = { debug: true };
});

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

it('prints out initialization options [4]', () => {
dotenvFlow.config({
...options,
path: '/path/to/another/project',
files: [
'.env',
'.env.production',
'.env.local'
],
});

expect(console.debug)
.to.have.been.calledWithMatch(/dotenv-flow\b.*init/);

expect(console.debug)
.to.have.been.calledWithMatch('options.path', '/path/to/another/project');

expect(console.debug)
.to.have.been.calledWithMatch('options.files', [
'.env',
'.env.production',
'.env.local'
]);
});

it('prints out effective node_env set by `options.node_env`', () => {
dotenvFlow.config({
...options,
Expand Down

0 comments on commit 6a47b2c

Please sign in to comment.