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

feat(dotenv-flow): implement options.files, closes #83 #87

Merged
merged 1 commit into from
Dec 12, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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