diff --git a/README.md b/README.md index 2f223ad..5d8dba9 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Using PNPM: $ pnpm add dotenv-flow ``` + ## Usage As early as possible in your Node.js app, initialize **dotenv-flow**: @@ -297,7 +298,9 @@ Then at every place `.env` is mentioned in the docs, read it as: "`.env.defaults ## `dotenv-flow/config` options -When preloading **dotenv-flow** using the node's `-r` switch you can use the following configuration options: +The following configuration options can be used when: + a) preloading **dotenv-flow** using Node's `-r` (`[ts-]node --require`) switch, or… + b) `import`ing the `dotenv-flow/config` entry point. ### Environment variables @@ -306,8 +309,10 @@ When preloading **dotenv-flow** using the node's `-r` switch you can use the fol * `DOTENV_FLOW_PATH` => [`options.path`](#optionspath); * `DOTENV_FLOW_ENCODING` => [`options.encoding`](#optionsencoding); * `DOTENV_FLOW_PURGE_DOTENV` => [`options.purge_dotenv`](#optionspurge_dotenv); +* `DOTENV_FLOW_DEBUG` => [`options.debug`](#optionsdebug); * `DOTENV_FLOW_SILENT` => [`options.silent`](#optionssilent); +##### _for example:_ ```sh $ NODE_ENV=production DOTENV_FLOW_PATH=/path/to/env-files-dir node -r dotenv-flow/config your_script.js ``` @@ -319,12 +324,14 @@ $ NODE_ENV=production DOTENV_FLOW_PATH=/path/to/env-files-dir node -r dotenv-flo * `--dotenv-flow-path` => [`options.path`](#optionspath); * `--dotenv-flow-encoding` => [`options.encoding`](#optionsencoding); * `--dotenv-flow-purge-dotenv` => [`options.purge_dotenv`](#optionspurge_dotenv); +* `--dotenv-flow-debug` => [`options.debug`](#optionsdebug); * `--dotenv-flow-silent` => [`options.silent`](#optionssilent); -Don't forget to separate **dotenv-flow/config**-specific CLI switches with `--` because they are not recognized by **Node.js**: +> Make sure that **dotenv-flow/config**-specific CLI switches are separated from Node's by `--` (double dash) since they are not recognized by **Node.js**. +##### _for example:_ ```sh -$ node -r dotenv-flow/config your_script.js -- --dotenv-flow-encoding=latin1 --dotenv-flow-path=... +$ node --require dotenv-flow/config your_script.js -- --dotenv-flow-path=/path/to/project --dotenv-flow-encoding=base64 ``` @@ -332,9 +339,13 @@ $ node -r dotenv-flow/config your_script.js -- --dotenv-flow-encoding=latin1 --d #### `.config([options]) => object` -The main entry point function that parses the contents of your `.env*` files, merges the results and appends to `process.env.*`. +"dotenv-flow" initialization function (API entry point). + +Allows configuring dotenv-flow programmatically. -Also, like the original module ([dotenv](https://github.com/motdotla/dotenv)), it returns an `object` with the `parsed` property containing the resulting key/values or the `error` property if the initialization is failed. +Also, like the original module ([dotenv](https://github.com/motdotla/dotenv)), +it returns an `object` with `.parsed` property containing the resulting +`varname => values` pairs or `.error` property if the initialization is failed. ##### `options.node_env` ###### Type: `string` @@ -478,11 +489,17 @@ require('dotenv-flow').config({ }); ``` +##### `options.debug` +###### Type: `boolean` +###### Default: `false` + +Enables detailed logging to debug why certain variables are not being set as you expect. + ##### `options.silent` ###### Type: `boolean` ###### Default: `false` -With this option you can suppress all the console outputs except errors and deprecation warnings. +Suppresses all kinds of warnings including ".env*" files' loading errors. ```js require('dotenv-flow').config({ @@ -492,16 +509,19 @@ require('dotenv-flow').config({ --- -The following API considered as internal, but it is also exposed to give the ability to be used programmatically by your own needs. +The following API is considered as internal, although it's exposed +for programmatic use of **dotenv-flow** for your own project-specific needs. #### `.listFiles([options]) => string[]` -Returns a list of `.env*` filenames depending on the given `options`. +Returns a list of existing `.env*` filenames depending on the given `options`. The resulting list is ordered by the env files' variables overwriting priority from lowest to highest. -This is also referenced as "env files' environment cascade." + +This can also be referenced as "env files' environment cascade" +or "order of ascending priority." ⚠️ Note that the `.env.local` file is not listed for "test" environment, since normally you expect tests to produce the same results for everyone. @@ -568,6 +588,13 @@ which (as mentioned above) will exclude `.env.local` producing just a single: … since normally we expect tests to produce the same results for everyone. +##### `options.debug` +###### Type: `boolean` +###### Default: `false` + +Enables debug messages. + + ##### Returns: ###### Type: `string[]` @@ -605,6 +632,7 @@ When several filenames are given, the parsed variables are merged into a single A filename or a list of filenames to parse. + ##### `options.encoding` ###### Type: `string` ###### Default: `"utf8"` @@ -612,6 +640,13 @@ A filename or a list of filenames to parse. An optional encoding for reading files. +##### `options.debug` +###### Type: `boolean` +###### Default: `false` + +Enables debug messages. + + ##### Returns: ###### Type: `object` @@ -667,11 +702,17 @@ A filename or a list of filenames to load. An optional encoding for reading files. +##### `options.debug` +###### Type: `boolean` +###### Default: `false` + +Optionally, turn on debug messages. + ##### `options.silent` ###### Type: `boolean` ###### Default: `false` -Suppress all the console outputs except errors and deprecation warnings. +If enabled, suppresses all kinds of warnings including ".env*" files' loading errors. ##### Returns: diff --git a/lib/cli-options.js b/lib/cli-options.js index 26d2714..ad5b975 100644 --- a/lib/cli-options.js +++ b/lib/cli-options.js @@ -6,6 +6,7 @@ const CLI_OPTIONS_MAP = { '--dotenv-flow-path': 'path', '--dotenv-flow-encoding': 'encoding', '--dotenv-flow-purge-dotenv': 'purge_dotenv', + '--dotenv-flow-debug': 'debug', '--dotenv-flow-silent': 'silent' }; diff --git a/lib/dotenv-flow.js b/lib/dotenv-flow.js index cd0de88..eb7fec2 100644 --- a/lib/dotenv-flow.js +++ b/lib/dotenv-flow.js @@ -3,6 +3,7 @@ const fs = require('fs'); const p = require('path'); const dotenv = require('dotenv'); +const {version} = require('../package.json'); const DEFAULT_PATTERN = '.env[.node_env][.local]'; @@ -51,16 +52,19 @@ function composeFilename(pattern, options) { * @param {object} [options.path] - path to the working directory (default: `process.cwd()`) * @param {string} [options.pattern] - `.env*` files' naming convention pattern * (default: ".env[.node_env][.local]") + * @param {boolean} [options.debug] - turn on debug messages * @return {string[]} */ function listFiles(options = {}) { + options.debug && debug('listing effective `.env*` files…'); + const { node_env, path = process.cwd(), pattern = DEFAULT_PATTERN, } = options; - const includeLocals = LOCAL_PLACEHOLDER_REGEX.test(pattern); + const hasLocalPlaceholder = LOCAL_PLACEHOLDER_REGEX.test(pattern); const filenames = {}; @@ -70,15 +74,25 @@ function listFiles(options = {}) { filenames['.env'] = composeFilename(pattern); - if (node_env !== 'test' && includeLocals) { - filenames['.env.local'] = composeFilename(pattern, { local: true }); + if (hasLocalPlaceholder) { + const envlocal = composeFilename(pattern, { local: true }); + + if (node_env !== 'test') { + filenames['.env.local'] = envlocal; + } + else if (options.debug && fs.existsSync(p.resolve(path, envlocal))) { + debug( + '[!] note that `%s` is being skipped for "test" environment', + envlocal + ); + } } if (node_env && NODE_ENV_PLACEHOLDER_REGEX.test(pattern)) { - filenames['.env.'] = composeFilename(pattern, { node_env }); + filenames['.env.node_env'] = composeFilename(pattern, { node_env }); - if (includeLocals) { - filenames['.env..local'] = composeFilename(pattern, { node_env, local: true }); + if (hasLocalPlaceholder) { + filenames['.env.node_env.local'] = composeFilename(pattern, { node_env, local: true }); } } @@ -86,8 +100,8 @@ function listFiles(options = {}) { '.env.defaults', '.env', '.env.local', - '.env.', - '.env..local' + '.env.node_env', + '.env.node_env.local' ] .reduce((list, basename) => { if (!filenames[basename]) { @@ -96,6 +110,7 @@ function listFiles(options = {}) { const filename = p.resolve(path, filenames[basename]); if (fs.existsSync(filename)) { + options.debug && debug('>> %s', filename); list.push(filename); } @@ -109,16 +124,38 @@ function listFiles(options = {}) { * When a list of filenames is given, the files will be parsed and merged in the same order as given. * * @param {string|string[]} filenames - filename or a list of filenames to parse and merge - * @param {{ encoding?: string }} [options] - `fs.readFileSync` options + * @param {{ encoding?: string, debug:? boolean }} [options] - parse options * @return {Object} the resulting map of `{ env_var: value }` as an object */ -function parse(filenames, options) { +function parse(filenames, options = {}) { if (typeof filenames === 'string') { - return dotenv.parse(fs.readFileSync(filenames, options)); + options.debug && debug('parsing "%s"…', filenames); + + const parsed = dotenv.parse( + fs.readFileSync( + filenames, + options.encoding && { encoding: options.encoding } + ) + ); + + if (options.debug) { + Object.keys(parsed) + .forEach(varname => debug('>> %s', varname)); + } + + return parsed; } - return filenames.reduce((parsed, filename) => { - return Object.assign(parsed, parse(filename, options)); + return filenames.reduce((result, filename) => { + const parsed = parse(filename, options); + + if (options.debug) { + Object.keys(parsed) + .filter(varname => result.hasOwnProperty(varname)) + .forEach(varname => debug('`%s` is being overwritten by merge from "%s"', varname, filename)); + } + + return Object.assign(result, parsed); }, {}); } @@ -139,29 +176,33 @@ function parse(filenames, options) { * @param {string|string[]} filenames - filename or a list of filenames to parse and merge * @param {object} [options] - file loading options * @param {string} [options.encoding="utf8"] - encoding of `.env*` files - * @param {boolean} [options.silent=false] - suppress all the console outputs except errors and deprecations + * @param {boolean} [options.debug=false] - turn on debug messages + * @param {boolean} [options.silent=false] - suppress console errors and warnings * @return {{ error: Error } | { parsed: Object }} */ -function load(filenames, options) { - const _options = (options && options.encoding) ? { encoding: options.encoding } : undefined; - const _verbose = !(options && options.silent); - +function load(filenames, options = {}) { try { - const parsed = parse(filenames, _options); + const parsed = parse(filenames, { + encoding: options.encoding, + debug: options.debug + }); + + options.debug && debug('safe-merging parsed environment variables into `process.env`…'); for (const varname of Object.keys(parsed)) { if (!process.env.hasOwnProperty(varname)) { + options.debug && debug('>> process.env.%s', varname); process.env[varname] = parsed[varname]; } - else if (_verbose) { - console.warn('dotenv-flow: "%s" is already defined in `process.env` and will not be overwritten', varname); // >>> + else if (options.debug && process.env[varname] !== parsed[varname]) { + debug('environment variable `%s` is predefined and not being overwritten', varname); } } return { parsed }; } catch (error) { - return { error }; + return failure(error, options); } } @@ -190,7 +231,56 @@ function unload(filenames, options = {}) { } /** - * Main entry point into the "dotenv-flow". Allows configuration before loading `.env*` files. + * Returns effective (computed) `node_env`. + * + * @param {object} [options] + * @param {string} [options.node_env] + * @param {string} [options.default_node_env] + * @param {boolean} [options.debug] + * @return {string|undefined} node_env + */ +function getEffectiveNodeEnv(options = {}) { + if (options.node_env) { + options.debug && debug( + `operating in "${options.node_env}" environment (set by \`options.node_env\`)` + ); + return options.node_env; + } + + if (process.env.NODE_ENV) { + options.debug && debug( + `operating in "${process.env.NODE_ENV}" environment (as per \`process.env.NODE_ENV\`)` + ); + return process.env.NODE_ENV; + } + + if (options.default_node_env) { + options.debug && debug( + `operating in "${options.default_node_env}" environment (taken from \`options.default_node_env\`)` + ); + return options.default_node_env; + } + + options.debug && debug( + 'operating in "no environment" mode (no environment-related options are set)' + ); + return undefined; +} + +const CONFIG_OPTION_KEYS = [ + 'node_env', + 'default_node_env', + 'path', + 'pattern', + 'encoding', + 'purge_dotenv', + 'silent' +]; + +/** + * "dotenv-flow" initialization function (API entry point). + * + * Allows configuring dotenv-flow programmatically. * * @param {object} [options] - configuration options * @param {string} [options.node_env=process.env.NODE_ENV] - node environment (development/test/production/etc.) @@ -199,46 +289,87 @@ function unload(filenames, options = {}) { * @param {string} [options.pattern=".env[.node_env][.local]"] - `.env*` files' naming convention pattern * @param {string} [options.encoding="utf8"] - encoding of `.env*` files * @param {boolean} [options.purge_dotenv=false] - perform the `.env` file {@link unload} - * @param {boolean} [options.silent=false] - suppress all the console outputs except errors and deprecations + * @param {boolean} [options.debug=false] - turn on detailed logging to help debug why certain variables are not being set as you expect + * @param {boolean} [options.silent=false] - suppress all kinds of warnings including ".env*" files' loading errors * @return {{ parsed?: object, error?: Error }} with a `parsed` key containing the loaded content or an `error` key with an error that is occurred */ function config(options = {}) { + if (options.debug) { + debug('initializing…'); + + CONFIG_OPTION_KEYS + .filter(key => key in options) + .forEach(key => debug(`| options.${key} =`, options[key])); + } + + const node_env = getEffectiveNodeEnv(options); + const { - node_env = process.env.NODE_ENV || options.default_node_env, path = process.cwd(), - pattern = DEFAULT_PATTERN, - encoding, - silent = false + pattern = DEFAULT_PATTERN } = options; if (options.purge_dotenv) { + options.debug && debug( + '`options.purge_dotenv` is enabled, unloading potentially pre-loaded `.env`…' + ); + const dotenvFile = p.resolve(path, '.env'); try { - fs.existsSync(dotenvFile) && unload(dotenvFile, { encoding }); + fs.existsSync(dotenvFile) && unload(dotenvFile, { encoding: options.encoding }); } catch (error) { - return { error }; + !options.silent && warn('unloading failed: ', error); } } try { - const filenames = listFiles({ node_env, path, pattern }); + 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; - return { - error: new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`) - }; + return failure( + new Error(`no ".env*" files matching pattern "${_pattern}" in "${path}" dir`), + options + ); } - return load(filenames, { encoding, silent }); + const result = load(filenames, { + encoding: options.encoding, + debug: options.debug, + silent: options.silent + }); + + options.debug && debug('initialization completed.'); + + return result; } catch (error) { - return { error }; + return failure(error, options); + } +} + +function failure(error, options) { + if (!options.silent) { + warn(`".env*" files loading failed: ${error.message || error}`); } + + return { error }; +} + +function warn(message, error) { + if (error) { + message += ': %s'; + } + + console.warn(`[dotenv-flow@${version}]: ${message}`, error); +} + +function debug(message, ...values) { + console.debug(`[dotenv-flow@${version}]: ${message}`, ...values); } module.exports = { diff --git a/lib/env-options.js b/lib/env-options.js index ebc92c8..c94ed75 100644 --- a/lib/env-options.js +++ b/lib/env-options.js @@ -6,6 +6,7 @@ const ENV_OPTIONS_MAP = { DOTENV_FLOW_PATH: 'path', DOTENV_FLOW_ENCODING: 'encoding', DOTENV_FLOW_PURGE_DOTENV: 'purge_dotenv', + DOTENV_FLOW_DEBUG: 'debug', DOTENV_FLOW_SILENT: 'silent' }; diff --git a/test/integration/exports.spec.mjs b/test/integration/exports.spec.mjs index cf5bb15..b39fbe1 100644 --- a/test/integration/exports.spec.mjs +++ b/test/integration/exports.spec.mjs @@ -1,9 +1,37 @@ import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { expect } from 'chai'; +import sinon from 'sinon'; -const require = createRequire(import.meta.url) +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const require = createRequire(import.meta.url); describe('exports', () => { + let _processEnvBackup; + + before('backup the original `process.env` object', () => { + _processEnvBackup = process.env; + }); + + beforeEach('setup the `process.env` copy', () => { + process.env = { ..._processEnvBackup }; + }); + + after('restore the original `process.env` object', () => { + process.env = _processEnvBackup; + }); + + beforeEach('stub `process.cwd()`', () => { + sinon.stub(process, 'cwd') + .returns(resolve(__dirname, 'fixtures', 'env')); + }); + + afterEach('restore `process.cwd()`', () => { + process.cwd.restore(); + }); + describe('commonjs', () => { it('should load module using require', () => { const dotenv = require('../..') diff --git a/test/unit/cli-options.spec.js b/test/unit/cli-options.spec.js index 93a33a4..03261df 100644 --- a/test/unit/cli-options.spec.js +++ b/test/unit/cli-options.spec.js @@ -13,7 +13,8 @@ describe('cli_options', () => { '--dotenv-flow-path', '/path/to/project', '--dotenv-flow-encoding', 'latin1', '--dotenv-flow-purge-dotenv', 'yes', - '--dotenv-flow-silent', 'yes' + '--dotenv-flow-debug', 'enabled', + '--dotenv-flow-silent', 'true' ])) .to.deep.equal({ node_env: 'production', @@ -21,7 +22,8 @@ describe('cli_options', () => { path: '/path/to/project', encoding: 'latin1', purge_dotenv: 'yes', - silent: 'yes' + debug: 'enabled', + silent: 'true' }); }); @@ -34,7 +36,8 @@ describe('cli_options', () => { '--dotenv-flow-path=/path/to/project', '--dotenv-flow-encoding=latin1', '--dotenv-flow-purge-dotenv=yes', - '--dotenv-flow-silent=yes' + '--dotenv-flow-debug=enabled', + '--dotenv-flow-silent=true' ])) .to.deep.equal({ node_env: 'production', @@ -42,7 +45,8 @@ describe('cli_options', () => { path: '/path/to/project', encoding: 'latin1', purge_dotenv: 'yes', - silent: 'yes' + debug: 'enabled', + silent: 'true' }); }); diff --git a/test/unit/dotenv-flow-api.spec.js b/test/unit/dotenv-flow-api.spec.js index 98b980f..522e91a 100644 --- a/test/unit/dotenv-flow-api.spec.js +++ b/test/unit/dotenv-flow-api.spec.js @@ -920,18 +920,6 @@ describe('dotenv-flow (API)', () => { }); }); - it('warns about predefined variable is not being overwritten', () => { - process.env.DEFAULT_ENV_VAR = 'predefined'; - - dotenvFlow.load([ - '/path/to/project/.env', - '/path/to/project/.env.development' - ]); - - expect(console.warn) - .to.have.been.calledWithMatch(/^dotenv-flow: .*%s.+/, 'DEFAULT_ENV_VAR'); - }); - describe('when `options.encoding` is given', () => { let options; @@ -953,52 +941,34 @@ describe('dotenv-flow (API)', () => { }); }); - describe('when `options.silent` is enabled', () => { - let options; - - beforeEach('setup `options.encoding`', () => { - options = { silent: true }; - }); - - it('suppresses the "predefined environment variable" warning', () => { - process.env.DEFAULT_ENV_VAR = 'predefined'; - - dotenvFlow.load([ - '/path/to/project/.env', - '/path/to/project/.env.development', - ], options); - - expect(console.warn) - .to.have.not.been.called; - }); - }); - - describe('if an error is occurred during the parsing', () => { + describe('if parsing is failed', () => { beforeEach('stub `fs.readFileSync` error', () => { $fs_readFileSync .withArgs('/path/to/project/.env.local') .throws(new Error('`.env.local` file reading error stub')); }); - it('leaves `process.env` untouched (does not assign any variables)', () => { - const processEnvCopy = { ...process.env }; + let filenames; - dotenvFlow.load([ + beforeEach('setup `filenames` for loading', () => { + filenames = [ '/path/to/project/.env', '/path/to/project/.env.local', // << the mocked error filename '/path/to/project/.env.development' - ]); + ]; + }); + + it('leaves `process.env` untouched (does not assign any variables)', () => { + const processEnvCopy = { ...process.env }; + + dotenvFlow.load(filenames); expect(process.env) - .to.deep.equal(processEnvCopy); + .to.deep.equal(processEnvCopy); }); it('returns the occurred error within the `.error` property', () => { - const result = dotenvFlow.load([ - '/path/to/project/.env', - '/path/to/project/.env.local', - '/path/to/project/.env.development' - ]); + const result = dotenvFlow.load(filenames); expect(result) .to.be.an('object') @@ -1006,6 +976,22 @@ describe('dotenv-flow (API)', () => { .that.is.an('error') .with.property('message', '`.env.local` file reading error stub'); }); + + it('warns about the occurred error', () => { + dotenvFlow.load(filenames); + + expect(console.warn) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*`\.env\.local` file reading error stub/ + ); + }); + + it("doesn't warn when suppressed by `options.silent`", () => { + dotenvFlow.load(filenames, { silent: true }); + + expect(console.warn) + .to.have.not.been.called; + }); }); }); @@ -1570,34 +1556,437 @@ describe('dotenv-flow (API)', () => { }); }); - describe('when `options.silent` is enabled', () => { + describe('when `options.debug` is enabled', () => { let options; - beforeEach('setup `options.purge_dotenv`', () => { - options = { silent: true }; + beforeEach('setup `options.debug`', () => { + options = { + debug: true + }; }); - beforeEach("setup `.env*` files' contents", () => { + beforeEach('stub `console.debug`', () => { + sinon.stub(console, 'debug'); + }); + + afterEach('restore `console.debug`', () => { + console.debug.restore(); + }); + + beforeEach("stub `.env*` files' contents", () => { mockFS({ '/path/to/project/.env': 'DEFAULT_ENV_VAR=ok' }); }); - beforeEach('stub `console.warn`', () => { - sinon.stub(console, 'warn'); + it('prints out initialization options [0]', () => { + dotenvFlow.config(options); + + expect(console.debug) + .to.have.been.calledWithMatch(/dotenv-flow\b.*init/); + + expect(console.debug) + .to.have.not.been.calledWithMatch(/dotenv-flow\b.*options\./); }); - afterEach('restore `console.warn`', () => { - console.warn.restore(); + it('prints out initialization options [1]', () => { + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch(/dotenv-flow\b.*init/); + + expect(console.debug) + .to.have.been.calledWithMatch('options.node_env', 'development'); + }); + + it('prints out initialization options [2]', () => { + dotenvFlow.config({ + ...options, + node_env: 'production', + default_node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch(/dotenv-flow\b.*init/); + + expect(console.debug) + .to.have.been.calledWithMatch('options.node_env', 'production'); + + expect(console.debug) + .to.have.been.calledWithMatch('options.default_node_env', 'development'); + }); + + it('prints out initialization options [3]', () => { + process.env.NODE_ENV = 'test'; + + dotenvFlow.config({ + ...options, + node_env: 'production', + default_node_env: 'development', + path: '/path/to/project', + pattern: '.env[.node_env][.local]', + encoding: 'utf8', + purge_dotenv: false, + silent: false, + }); + + expect(console.debug) + .to.have.been.calledWithMatch(/dotenv-flow\b.*init/); + + expect(console.debug) + .to.have.been.calledWithMatch('options.node_env', "production"); + + expect(console.debug) + .to.have.been.calledWithMatch('options.default_node_env', "development"); + + expect(console.debug) + .to.have.been.calledWithMatch('options.path', '/path/to/project'); + + expect(console.debug) + .to.have.been.calledWithMatch('options.pattern', '.env[.node_env][.local]'); + + expect(console.debug) + .to.have.been.calledWithMatch('options.encoding', 'utf8'); + + expect(console.debug) + .to.have.been.calledWithMatch('options.purge_dotenv', false); + + expect(console.debug) + .to.have.been.calledWithMatch('options.silent', false); + }); + + it('prints out effective node_env set by `options.node_env`', () => { + dotenvFlow.config({ + ...options, + node_env: 'production' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*operating in "production" environment.*`options\.node_env`/ + ); + }); + + it('prints out effective node_env set by `process.env.NODE_ENV`', () => { + process.env.NODE_ENV = 'test'; + + dotenvFlow.config(options); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*operating in "test" environment.*`process\.env\.NODE_ENV`/ + ); + }); + + it('prints out effective node_env taken from `options.default_node_env`', () => { + dotenvFlow.config({ + ...options, + default_node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*operating in "development" environment.*`options\.default_node_env`/ + ); + }); + + it('notifies about operating in "no environment" mode when none of the related options is set', () => { + dotenvFlow.config(options); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*operating in "no environment" mode/ + ); + }); + + it('prints out the list of effective `.env*` files', () => { + 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.development.local': 'LOCAL_DEVELOPMENT_ENV_VAR=ok' + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> %s/, + /^\/path\/to\/project\/\.env$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> %s/, + /^\/path\/to\/project\/\.env\.local$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> %s/, + /^\/path\/to\/project\/\.env\.development$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> %s/, + /^\/path\/to\/project\/\.env\.development\.local$/ + ); + }); + + it('prints out parsing files', () => { + 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.development.local': 'LOCAL_DEVELOPMENT_ENV_VAR=ok' + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*parsing.*%s/, + /^\/path\/to\/project\/\.env$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*parsing.*%s/, + /^\/path\/to\/project\/\.env\.local$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*parsing.*%s/, + /^\/path\/to\/project\/\.env\.development$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*parsing.*%s/, + /^\/path\/to\/project\/\.env\.development\.local$/ + ); + }); + + it('prints out parsed environment variables', () => { + 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.development.local': 'LOCAL_DEVELOPMENT_ENV_VAR=ok' + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> %s/, + 'DEFAULT_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*> %s/, + 'LOCAL_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*> %s/, + 'DEVELOPMENT_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*> %s/, + 'LOCAL_DEVELOPMENT_ENV_VAR' + ); + }); + + it('prints out environment variables assigned to `process.env`', () => { + 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.development.local': 'LOCAL_DEVELOPMENT_ENV_VAR=ok' + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*merging.*variables.*`process.env`/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> process\.env\.%s/, + 'DEFAULT_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> process\.env\.%s/, + 'LOCAL_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> process\.env\.%s/, + 'DEVELOPMENT_ENV_VAR' + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*>> process\.env\.%s/, + 'LOCAL_DEVELOPMENT_ENV_VAR' + ); + }); + + it('informs when merging with overwrites', () => { + mockFS({ + '/path/to/project/.env': ( + 'DEFAULT_ENV_VAR=ok\n' + + 'SHARED_ENV_VAR=1' + ), + '/path/to/project/.env.local': ( + 'LOCAL_ENV_VAR=ok\n' + + 'SHARED_ENV_VAR=2' + ), + '/path/to/project/.env.development': ( + 'DEVELOPMENT_ENV_VAR=ok\n' + + 'SHARED_ENV_VAR=3' + ), + '/path/to/project/.env.development.local': ( + 'LOCAL_DEVELOPMENT_ENV_VAR=ok\n' + + 'SHARED_ENV_VAR=4' + ) + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*%s.*overwritten by.*%s/, + 'SHARED_ENV_VAR', + /^\/path\/to\/project\/\.env\.local$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*%s.*overwritten by.*%s/, + 'SHARED_ENV_VAR', + /^\/path\/to\/project\/\.env\.development$/ + ); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*%s.*overwritten by.*%s/, + 'SHARED_ENV_VAR', + /^\/path\/to\/project\/\.env\.development\.local$/ + ); }); - it('suppresses all the warnings', () => { + it('informs when predefined environment variable is not being overwritten', () => { + mockFS({ + '/path/to/project/.env': 'DEFAULT_ENV_VAR="should be predefined"' + }); + process.env.DEFAULT_ENV_VAR = 'predefined'; dotenvFlow.config(options); - expect(console.warn) - .to.have.not.been.called; + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*%s.*predefined.*not.*overwritten/, + 'DEFAULT_ENV_VAR' + ); + }); + + it('prints out the completion status', () => { + mockFS({ + '/path/to/project/.env': 'DEFAULT_ENV_VAR=ok' + }); + + dotenvFlow.config({ + ...options, + node_env: 'development' + }); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*initialization completed/ + ); + }); + + describe('… and `options.node_env` is set to "test"', () => { + beforeEach('set `.options.node_env` to "test"', () => { + options.node_env = 'test'; + }); + + it('notifies that `.env.local` is being skipped in "test" environment', () => { + mockFS({ + '/path/to/project/.env': 'DEFAULT_ENV_VAR=ok', + '/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok', + '/path/to/project/.env.test': 'TEST_ENV_VAR=ok', + '/path/to/project/.env.test.local': 'LOCAL_TEST_ENV_VAR=ok' + }); + + dotenvFlow.config(options); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*%s.*is being skipped for "test" environment/, + '.env.local' + ); + }); + + it("doesn't spam about skipping `.env.local` if it doesn't exist", () => { + mockFS({ + '/path/to/project/.env': 'DEFAULT_ENV_VAR=ok', + '/path/to/project/.env.test': 'TEST_ENV_VAR=ok', + '/path/to/project/.env.test.local': 'LOCAL_TEST_ENV_VAR=ok' + }); + + dotenvFlow.config(options); + + expect(console.debug) + .to.have.not.been.calledWithMatch(/dotenv-flow\b.*%s.*skipped/); + }); + }); + + describe('… and `options.purge_dotenv` is enabled', () => { + beforeEach('setup `options.purge_dotenv`', () => { + options.purge_dotenv = true; + }); + + it('prints out the "unloading `.env` file" message', () => { + dotenvFlow.config(options); + + expect(console.debug) + .to.have.been.calledWithMatch( + /dotenv-flow\b.*`options\.purge_dotenv`.*unloading.*`\.env`/ + ); + }); }); }); @@ -1615,6 +2004,14 @@ describe('dotenv-flow (API)', () => { .throws(new Error('`.env.local` file reading error stub')); }); + beforeEach('stub `console.warn`', () => { + sinon.stub(console, 'warn'); + }); + + afterEach('restore `console.warn`', () => { + console.warn.restore(); + }); + it("doesn't load any environment variables", () => { const processEnvCopy = { ...process.env }; @@ -1633,55 +2030,79 @@ describe('dotenv-flow (API)', () => { .that.is.an('error') .with.property('message', '`.env.local` file reading error stub'); }); + + it('warns about the occurred error', () => { + dotenvFlow.config(); + + expect(console.warn) + .to.have.been.calledWithMatch(/dotenv-flow\b.*`\.env\.local` file reading error stub/); + }); }); describe('when none of the appropriate ".env*" files is present', () => { - it('returns "no `.env*` files" error', () => { - const result = dotenvFlow.config(); + beforeEach('stub `console.warn`', () => { + sinon.stub(console, 'warn'); + }); - expect(result) - .to.be.an('object') - .with.property('error') - .that.is.an('error') - .with.property('message') - .that.matches(/no "\.env\*" files/); + afterEach('restore `console.warn`', () => { + console.warn.restore(); }); describe('… and no "node_env-related" options are set', () => { - it('returns an error with a message indicating the working directory', () => { - const defaultResult = dotenvFlow.config(); + it('returns "no `.env*` files" error', () => { + const result = dotenvFlow.config(); - expect(defaultResult.error) - .to.be.an('error') + expect(result) + .to.be.an('object') + .with.property('error') + .that.is.an('error') .with.property('message') - .that.includes('/path/to/project'); + .that.matches(/no "\.env\*" files/); + }); - const pathResult = dotenvFlow.config({ - path: '/path/to/another/project' - }); + it('warns about the "no `.env*` files" error', () => { + dotenvFlow.config(); - expect(pathResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('/path/to/another/project'); + expect(console.warn) + .to.have.been.calledWithMatch(/dotenv-flow\b.*no "\.env\*" files/); }); - it('returns an error with a message indicating the naming convention pattern', () => { - const defaultResult = dotenvFlow.config(); + describe('the returning error message', () => { + it('indicates the working directory', () => { + const defaultResult = dotenvFlow.config(); - expect(defaultResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('.env[.node_env][.local]'); + expect(defaultResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('/path/to/project'); + + const pathResult = dotenvFlow.config({ + path: '/path/to/another/project' + }); - const patternResult = dotenvFlow.config({ - pattern: 'config/[local/].env[.node_env]' + expect(pathResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('/path/to/another/project'); }); - expect(patternResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('config/[local/].env[.node_env]'); + it('indicates the naming convention pattern', () => { + const defaultResult = dotenvFlow.config(); + + expect(defaultResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('.env[.node_env][.local]'); + + const patternResult = dotenvFlow.config({ + pattern: 'config/[local/].env[.node_env]' + }); + + expect(patternResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('config/[local/].env[.node_env]'); + }); }); }); @@ -1690,42 +2111,110 @@ describe('dotenv-flow (API)', () => { process.env.NODE_ENV = 'development'; }); - it('returns an error with a message indicating the working directory', () => { - const defaultResult = dotenvFlow.config(); + it('returns "no `.env*` files" error', () => { + const result = dotenvFlow.config(); - expect(defaultResult.error) - .to.be.an('error') + expect(result) + .to.be.an('object') + .with.property('error') + .that.is.an('error') .with.property('message') - .that.includes('/path/to/project'); + .that.matches(/no "\.env\*" files/); + }); - const pathResult = dotenvFlow.config({ - path: '/path/to/another/project' - }); + it('warns about the "no `.env*` files" error', () => { + dotenvFlow.config(); - expect(pathResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('/path/to/another/project'); + expect(console.warn) + .to.have.been.calledWithMatch(/dotenv-flow\b.*no "\.env\*" files/); }); - it('returns an error with a message indicating the naming convention pattern for the specified node_env', () => { - const defaultResult = dotenvFlow.config(); + describe('the returning error message', () => { + it('indicates the working directory', () => { + const defaultResult = dotenvFlow.config(); - expect(defaultResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('.env[.development][.local]'); + expect(defaultResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('/path/to/project'); + + const pathResult = dotenvFlow.config({ + path: '/path/to/another/project' + }); - const patternResult = dotenvFlow.config({ - pattern: 'config/[local/].env[.node_env]' + expect(pathResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('/path/to/another/project'); }); - expect(patternResult.error) - .to.be.an('error') - .with.property('message') - .that.includes('config/[local/].env[.development]'); + it('indicates the naming convention pattern for the specified node_env', () => { + const defaultResult = dotenvFlow.config(); + + expect(defaultResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('.env[.development][.local]'); + + const patternResult = dotenvFlow.config({ + pattern: 'config/[local/].env[.node_env]' + }); + + expect(patternResult.error) + .to.be.an('error') + .with.property('message') + .that.includes('config/[local/].env[.development]'); + }); }); }); }); + + describe('when `options.silent` is enabled', () => { + let options; + + beforeEach('setup `options.silent`', () => { + options = { silent: true }; + }); + + beforeEach('stub `console.warn`', () => { + sinon.stub(console, 'warn'); + }); + + afterEach('restore `console.warn`', () => { + console.warn.restore(); + }); + + it("doesn't warn if parsing is failed", () => { + mockFS({ + '/path/to/project/.env': 'DEFAULT_ENV_VAR=ok', + '/path/to/project/.env.local': 'LOCAL_ENV_VAR=ok' + }); + + $fs_readFileSync + .withArgs('/path/to/project/.env.local') + .throws(new Error('`.env.local` file reading error stub')); + + const result = dotenvFlow.config(options); + + expect(console.warn) + .to.have.not.been.called; + + expect(result.error) + .to.be.an('error') + .with.property('message', '`.env.local` file reading error stub'); + }); + + it("doesn't warn about missing `.env*` files", () => { + const result = dotenvFlow.config(options); + + expect(console.warn) + .to.have.not.been.called; + + expect(result.error) + .to.be.an('error') + .with.property('message') + .that.matches(/no "\.env\*" files/); + }); + }); }); }); diff --git a/test/unit/env-options.spec.js b/test/unit/env-options.spec.js index 2e5f276..a63ab51 100644 --- a/test/unit/env-options.spec.js +++ b/test/unit/env-options.spec.js @@ -11,7 +11,8 @@ describe('env_options', () => { DOTENV_FLOW_PATH: '/path/to/project', DOTENV_FLOW_ENCODING: 'latin1', DOTENV_FLOW_PURGE_DOTENV: 'yes', - DOTENV_FLOW_SILENT: 'yes' + DOTENV_FLOW_DEBUG: 'enabled', + DOTENV_FLOW_SILENT: 'true' })) .to.deep.equal({ node_env: 'production', @@ -19,7 +20,8 @@ describe('env_options', () => { path: '/path/to/project', encoding: 'latin1', purge_dotenv: 'yes', - silent: 'yes' + debug: 'enabled', + silent: 'true' }); });