diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3eabb5a..c84d553 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node: [ '8', '10', '12', '14' ] + node: [ '10', '12', '14' ] steps: - name: 🛑 Cancel Previous Runs uses: styfle/cancel-workflow-action@0.6.0 diff --git a/.gitignore b/.gitignore index ee565f9..ce8ebe6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Tests +test/output + # Logs logs *.log diff --git a/README.md b/README.md index 0a7f582..4725f55 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,14 @@ Add `.env` to your `.gitignore` file Due to the fact that we use `webpack.DefinePlugin` under the hood, we cannot support destructing as that breaks how this plugin is meant to be used. Because of this, please reference your variables without destructing. For more information about this, please review the issue [here](https://github.com/mrsteele/dotenv-webpack/issues/70). +## `process.env` stubbing / replacing + +`process.env` is not polyfilled in Webpack 5+, leading to errors in environments where `process` is `null` (browsers). + +We automatically replace any remaining `process.env`s in these environments with `"MISSING_ENV_VAR"` to avoid these errors. + +If you are running into issues where you or another package you use interfaces with `process.env`, it might be best to set `ignoreStubs: true` and make sure you always reference variables that exist within your code (See [this issue](https://github.com/mrsteele/dotenv-webpack/issues/271) for more information). + ## Properties Use the following properties to configure your instance. @@ -106,6 +114,7 @@ Use the following properties to configure your instance. * **silent** (`false`) - If true, all warnings will be suppressed. * **expand** (`false`) - Allows your variables to be "expanded" for reusability within your `.env` file. * **defaults** (`false`) - Adds support for `dotenv-defaults`. If set to `true`, uses `./.env.defaults`. If a string, uses that location for a defaults file. Read more at [npm](https://www.npmjs.com/package/dotenv-defaults). +* **ignoreStub** (`false`) - Override the automatic check whether to stub `process.env`. [Read more here](#user-content-processenv-stubbing--replacing). The following example shows how to set any/all arguments. diff --git a/package-lock.json b/package-lock.json index b4d3e5e..dc983d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3865,6 +3865,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -5726,6 +5732,18 @@ "map-cache": "^0.2.2" } }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -7184,6 +7202,16 @@ "minimist": "^1.2.5" } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -9746,6 +9774,12 @@ "set-value": "^2.0.1" } }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index ed866fd..07c0efe 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ }, "jest": { "coverageDirectory": "./coverage/", - "collectCoverage": true + "collectCoverage": true, + "watchPathIgnorePatterns": [ + "output/.*?" + ] }, "keywords": [ "dotenv", @@ -41,8 +44,11 @@ "url": "https://github.com/mrsteele/dotenv-webpack/issues" }, "homepage": "https://github.com/mrsteele/dotenv-webpack#readme", + "engines": { + "node": ">=10" + }, "peerDependencies": { - "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + "webpack": "^4 || ^5" }, "dependencies": { "dotenv-defaults": "^2.0.1" @@ -52,6 +58,7 @@ "@babel/core": "^7.13.8", "@babel/preset-env": "^7.13.9", "@babel/register": "^7.13.8", + "fs-extra": "^9.1.0", "husky": "^5.1.3", "jest": "^25.5.4", "jsdoc": "^3.6.6", diff --git a/src/index.js b/src/index.js index 51d5236..f0cbac5 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,9 @@ const interpolate = (env, vars) => { return env } +const isMainThreadElectron = (target) => + target.startsWith('electron') && target.endsWith('main') + class Dotenv { /** * The dotenv-webpack plugin. @@ -30,21 +33,19 @@ class Dotenv { this.config = Object.assign({}, { path: './.env' }, config) - - this.checkDeprecation() - - return new DefinePlugin(this.formatData(this.gatherVariables())) } - checkDeprecation () { - const { sample, safe, silent } = this.config - // Catch older packages, but hold their hand (just for a bit) - if (sample) { - if (safe) { - this.config.safe = sample - } - this.warn('dotenv-webpack: "options.sample" is a deprecated property. Please update your configuration to use "options.safe" instead.', silent) - } + apply (compiler) { + const variables = this.gatherVariables() + const target = compiler.options.target ?? 'web' + const version = compiler.webpack.version + const data = this.formatData({ + variables, + target, + version + }) + + new DefinePlugin(data).apply(compiler) } gatherVariables () { @@ -121,10 +122,10 @@ class Dotenv { return '' } - formatData (vars = {}) { + formatData ({ variables = {}, target, version }) { const { expand } = this.config - const formatted = Object.keys(vars).reduce((obj, key) => { - const v = vars[key] + const formatted = Object.keys(variables).reduce((obj, key) => { + const v = variables[key] const vKey = `process.env.${key}` let vValue if (expand) { @@ -133,7 +134,7 @@ class Dotenv { } else if (v.indexOf('\\$') > 0) { vValue = v.replace(/\\\$/g, '$') } else { - vValue = interpolate(v, vars) + vValue = interpolate(v, variables) } } else { vValue = v @@ -144,12 +145,39 @@ class Dotenv { return obj }, {}) - // fix in case of missing - formatted['process.env'] = '{}' + // We have to stub any remaining `process.env`s due to Webpack 5 not polyfilling it anymore + // https://github.com/mrsteele/dotenv-webpack/issues/240#issuecomment-710231534 + // However, if someone targets Node or Electron `process.env` still exists, and should therefore be kept + // https://webpack.js.org/configuration/target + if (this.shouldStub({ target, version })) { + // Results in `"MISSING_ENV_VAR".NAME` which is valid JS + formatted['process.env'] = '"MISSING_ENV_VAR"' + } return formatted } + shouldStub ({ target: targetInput, version }) { + if (!version.startsWith('5')) { + return false + } + + const targets = Array.isArray(targetInput) ? targetInput : [targetInput] + + return targets.every( + target => + // If we're not configured to never stub + this.config.ignoreStub !== true && + // And + ( + // We are configured to always stub + this.config.ignoreStub === false || + // Or if we should according to the target + (!target.includes('node') && !isMainThreadElectron(target)) + ) + ) + } + /** * Load a file. * @param {String} config.file - The file to load. diff --git a/test/fixtures/index.js b/test/fixtures/index.js new file mode 100644 index 0000000..60730f2 --- /dev/null +++ b/test/fixtures/index.js @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-vars */ + +// Basic +const TEST = process.env.TEST +const TEST2 = process.env.TEST2 + +// System +const PATH = process.env.PATH + +// Expanded +const NODE_ENV = process.env.NODE_ENV +const BASIC = process.env.BASIC +const BASIC_EXPAND = process.env.BASIC_EXPAND +const MACHINE = process.env.MACHINE +const MACHINE_EXPAND = process.env.MACHINE_EXPAND +const UNDEFINED_EXPAND = process.env.UNDEFINED_EXPAND +const ESCAPED_EXPAND = process.env.ESCAPED_EXPAND +const MONGOLAB_DATABASE = process.env.MONGOLAB_DATABASE +const MONGOLAB_USER = process.env.MONGOLAB_USER +const MONGOLAB_PASSWORD = process.env.MONGOLAB_PASSWORD +const MONGOLAB_DOMAIN = process.env.MONGOLAB_DOMAIN +const MONGOLAB_PORT = process.env.MONGOLAB_PORT +const MONGOLAB_URI = process.env.MONGOLAB_URI +const MONGOLAB_USER_RECURSIVELY = process.env.MONGOLAB_USER_RECURSIVELY +const MONGOLAB_URI_RECURSIVELY = process.env.MONGOLAB_URI_RECURSIVELY +const WITHOUT_CURLY_BRACES_URI = process.env.WITHOUT_CURLY_BRACES_URI +const WITHOUT_CURLY_BRACES_USER_RECURSIVELY = process.env.WITHOUT_CURLY_BRACES_USER_RECURSIVELY +const WITHOUT_CURLY_BRACES_URI_RECURSIVELY = process.env.WITHOUT_CURLY_BRACES_URI_RECURSIVELY diff --git a/test/index.test.js b/test/index.test.js index eb4f7fb..8873da4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,310 +1,492 @@ -/* global jest, describe, test, expect, beforeEach */ +/* global jest, describe, test, expect, afterEach, beforeAll, beforeEach */ -// Tests suite -const path = require('path') +const { resolve } = require('path') +const { createHash } = require('crypto') +const webpack = require('webpack') +const { readFileSync } = require('fs') +const { removeSync } = require('fs-extra') -// The star of the show const Src = require('../src') const Dist = require('../dist') -const envEmpty = path.resolve(__dirname, './envs/.empty') -const envEmptyExample = path.resolve(__dirname, './envs/.empty.example') -const envSimple = path.resolve(__dirname, './envs/.simple') -const envSimpleExample = path.resolve(__dirname, './envs/.simple.example') -const envOneEmpty = path.resolve(__dirname, './envs/.oneempty') -const envOneEmptyExample = path.resolve(__dirname, './envs/.oneempty.example') -const envMissingOne = path.resolve(__dirname, './envs/.missingone') -const envMissingOneExample = path.resolve(__dirname, './envs/.missingone.example') -const envSystemvars = path.resolve(__dirname, './envs/.systemvars') -const envSystemvarsExample = path.resolve(__dirname, './envs/.systemvars.example') -const envExpanded = path.resolve(__dirname, './envs/.expanded') -const envDefaults = path.resolve(__dirname, './envs/.defaults') - -const buildExpectation = (obj) => { - const raw = Object.keys(obj).reduce((all, key) => { - all[`process.env.${key}`] = JSON.stringify(obj[key]) - return all - }, {}) - - raw['process.env'] = '{}' - - return raw +const envEmpty = resolve(__dirname, './envs/.empty') +const envEmptyExample = resolve(__dirname, './envs/.empty.example') +const envSimple = resolve(__dirname, './envs/.simple') +const envSimpleExample = resolve(__dirname, './envs/.simple.example') +const envOneEmpty = resolve(__dirname, './envs/.oneempty') +const envOneEmptyExample = resolve(__dirname, './envs/.oneempty.example') +const envMissingOne = resolve(__dirname, './envs/.missingone') +const envMissingOneExample = resolve(__dirname, './envs/.missingone.example') +const envSystemvars = resolve(__dirname, './envs/.systemvars') +const envSystemvarsExample = resolve(__dirname, './envs/.systemvars.example') +const envExpanded = resolve(__dirname, './envs/.expanded') +const envDefaults = resolve(__dirname, './envs/.defaults') + +const emptyResult = {} +const defaultEnvResult = { TEST: 'hi' } +const simpleResult = { TEST: 'testing' } +const defaultsResult = { TEST: 'hi', TEST2: 'hidefault' } +const defaultsResult2 = { TEST: 'hi', TEST2: 'youcanseethis' } +const oneEmptyResult = { TEST: '', TEST2: 'Hello' } +const missingOneResult = { TEST2: 'Hello' } + +const hash = (str) => createHash('md5').update(str).digest('hex').slice(0, 8) + +const getConfig = (target, plugin) => ({ + mode: 'development', + devtool: false, + target, + entry: resolve(__dirname, './fixtures/index'), + output: { + path: resolve(__dirname, `./output/${hash(expect.getState().currentTestName)}`) + }, + plugins: [plugin] +}) + +const compile = (config, callback) => { + webpack(config, (err, stats) => { + expect(err).toBeNull() + expect(stats.compilation.errors).toHaveLength(0) + + const result = readFileSync( + resolve(__dirname, config.output.path, 'main.js'), + { encoding: 'utf-8' } + ) + + callback(result) + }) +} + +const expectResultsToContainReplacements = (plugin, env, done) => { + const config = getConfig('web', plugin) + + compile(config, (result) => { + Object.entries(env).forEach(([key, value]) => { + expect(result).toMatch(`const ${key} = "${value}"`) + }) + + done?.() + }) } -const envDefJson = buildExpectation({ TEST: 'hi' }) -const envEmptyJson = buildExpectation({}) -const envSimpleJson = buildExpectation({ TEST: 'testing' }) -const envOneEmptyJson = buildExpectation({ TEST: '', TEST2: 'Hello' }) -const envMissingOneJson = buildExpectation({ TEST2: 'Hello' }) -const envDefaultsJson = buildExpectation({ TEST: 'hi', TEST2: 'hidefault' }) -const envDefaultsJson2 = buildExpectation({ TEST: 'hi', TEST2: 'youcanseethis' }) - -/* -NODE_ENV=test -BASIC=basic -BASIC_EXPAND=$BASIC -MACHINE=machine_env -MACHINE_EXPAND=$MACHINE -UNDEFINED_EXPAND=$UNDEFINED_ENV_KEY -ESCAPED_EXPAND=\$ESCAPED -MONGOLAB_DATABASE=heroku_db -MONGOLAB_USER=username -MONGOLAB_PASSWORD=password -MONGOLAB_DOMAIN=abcd1234.mongolab.com -MONGOLAB_PORT=12345 -MONGOLAB_URI=mongodb://${MONGOLAB_USER}:${MONGOLAB_PASSWORD}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE} - -MONGOLAB_USER_RECURSIVELY=${MONGOLAB_USER}:${MONGOLAB_PASSWORD} -MONGOLAB_URI_RECURSIVELY=mongodb://${MONGOLAB_USER_RECURSIVELY}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE} - -WITHOUT_CURLY_BRACES_URI=mongodb://$MONGOLAB_USER:$MONGOLAB_PASSWORD@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE -WITHOUT_CURLY_BRACES_USER_RECURSIVELY=$MONGOLAB_USER:$MONGOLAB_PASSWORD -WITHOUT_CURLY_BRACES_URI_RECURSIVELY=mongodb://$MONGOLAB_USER_RECURSIVELY@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE -*/ -const envExpandedNotJson = buildExpectation({ - NODE_ENV: 'test', - BASIC: 'basic', - BASIC_EXPAND: '$BASIC', - MACHINE: 'machine_env', - MACHINE_EXPAND: '$MACHINE', - UNDEFINED_EXPAND: '$UNDEFINED_ENV_KEY', - // eslint-disable-next-line - ESCAPED_EXPAND: '\\$ESCAPED', - MONGOLAB_DATABASE: 'heroku_db', - MONGOLAB_USER: 'username', - MONGOLAB_PASSWORD: 'password', - MONGOLAB_DOMAIN: 'abcd1234.mongolab.com', - MONGOLAB_PORT: '12345', - // eslint-disable-next-line - MONGOLAB_URI: 'mongodb://${MONGOLAB_USER}:${MONGOLAB_PASSWORD}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE}', - // eslint-disable-next-line - MONGOLAB_USER_RECURSIVELY: '${MONGOLAB_USER}:${MONGOLAB_PASSWORD}', - // eslint-disable-next-line - MONGOLAB_URI_RECURSIVELY: 'mongodb://${MONGOLAB_USER_RECURSIVELY}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE}', - WITHOUT_CURLY_BRACES_URI: 'mongodb://$MONGOLAB_USER:$MONGOLAB_PASSWORD@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE', - WITHOUT_CURLY_BRACES_USER_RECURSIVELY: '$MONGOLAB_USER:$MONGOLAB_PASSWORD', - WITHOUT_CURLY_BRACES_URI_RECURSIVELY: 'mongodb://$MONGOLAB_USER_RECURSIVELY@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE' +const versions = [ + ['Source', Src.default], + ['Dist', Dist.default] +] + +beforeAll(() => { + global.console.warn = jest.fn() }) -const envExpandedJson = buildExpectation({ - NODE_ENV: 'test', - BASIC: 'basic', - BASIC_EXPAND: 'basic', - MACHINE: 'machine_env', - MACHINE_EXPAND: 'machine_env', - UNDEFINED_EXPAND: '', - // eslint-disable-next-line - ESCAPED_EXPAND: '\$ESCAPED', - MONGOLAB_DATABASE: 'heroku_db', - MONGOLAB_USER: 'username', - MONGOLAB_PASSWORD: 'password', - MONGOLAB_DOMAIN: 'abcd1234.mongolab.com', - MONGOLAB_PORT: '12345', - MONGOLAB_URI: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', - MONGOLAB_USER_RECURSIVELY: 'username:password', - MONGOLAB_URI_RECURSIVELY: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', - WITHOUT_CURLY_BRACES_URI: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', - WITHOUT_CURLY_BRACES_USER_RECURSIVELY: 'username:password', - WITHOUT_CURLY_BRACES_URI_RECURSIVELY: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db' + +beforeEach(() => { + jest.resetAllMocks() + + const outputDir = resolve(__dirname, `output/${hash(expect.getState().currentTestName)}`) + try { + removeSync(outputDir) + } catch (err) { + // rmdir might error if the target doesn't exist, but we don't care about that. + if (!err.message.includes('ENOENT')) { + throw err + } + } }) -// const consoleSpy = jest.spyOn(console, 'warn') -global.console.warn = jest.fn() +describe.each(versions)('%s', (_, DotenvPlugin) => { + test('Should be an function.', () => { + expect(typeof DotenvPlugin).toEqual('function') + }) -function runTests (Obj, name) { - function envTest (config) { - return new Obj(config).definitions - } + test('Should return a instance of Dotenv.', () => { + expect((new DotenvPlugin()).constructor.name).toEqual('Dotenv') + }) - /** @test {Dotenv} **/ - describe(name, () => { - beforeEach(() => { - global.console.warn.mockClear() + describe('Defaults', () => { + test('Should include environment variables that exist in .env file.', (done) => { + expectResultsToContainReplacements(new DotenvPlugin(), defaultEnvResult, done) }) - describe('Defaults', () => { - test('Should be an function.', () => { - expect(typeof Obj).toEqual('function') - }) + test('Should not expand variables by default', (done) => { + const expected = { + NODE_ENV: 'test', + BASIC: 'basic', + BASIC_EXPAND: '$BASIC', + MACHINE: 'machine_env', + MACHINE_EXPAND: '$MACHINE', + UNDEFINED_EXPAND: '$UNDEFINED_ENV_KEY', + // eslint-disable-next-line + ESCAPED_EXPAND: '\\\\$ESCAPED', + MONGOLAB_DATABASE: 'heroku_db', + MONGOLAB_USER: 'username', + MONGOLAB_PASSWORD: 'password', + MONGOLAB_DOMAIN: 'abcd1234.mongolab.com', + MONGOLAB_PORT: '12345', + // eslint-disable-next-line + MONGOLAB_URI: 'mongodb://${MONGOLAB_USER}:${MONGOLAB_PASSWORD}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE}', + // eslint-disable-next-line + MONGOLAB_USER_RECURSIVELY: '${MONGOLAB_USER}:${MONGOLAB_PASSWORD}', + // eslint-disable-next-line + MONGOLAB_URI_RECURSIVELY: 'mongodb://${MONGOLAB_USER_RECURSIVELY}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE}', + WITHOUT_CURLY_BRACES_URI: 'mongodb://$MONGOLAB_USER:$MONGOLAB_PASSWORD@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE', + WITHOUT_CURLY_BRACES_USER_RECURSIVELY: '$MONGOLAB_USER:$MONGOLAB_PASSWORD', + WITHOUT_CURLY_BRACES_URI_RECURSIVELY: 'mongodb://$MONGOLAB_USER_RECURSIVELY@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE' + } + + expectResultsToContainReplacements( + new DotenvPlugin({ path: envExpanded }), + expected, + done + ) + }) - // @todo - This one isn't a great test, but it wasn't really working for me. - test('Should return a instance of DefinePlugin.', () => { - expect(typeof envTest()).toEqual('object') - }) + test('Should expand variables when configured', (done) => { + const expected = { + NODE_ENV: 'test', + BASIC: 'basic', + BASIC_EXPAND: 'basic', + MACHINE: 'machine_env', + MACHINE_EXPAND: 'machine_env', + UNDEFINED_EXPAND: '', + // eslint-disable-next-line + ESCAPED_EXPAND: '\$ESCAPED', + MONGOLAB_DATABASE: 'heroku_db', + MONGOLAB_USER: 'username', + MONGOLAB_PASSWORD: 'password', + MONGOLAB_DOMAIN: 'abcd1234.mongolab.com', + MONGOLAB_PORT: '12345', + MONGOLAB_URI: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', + MONGOLAB_USER_RECURSIVELY: 'username:password', + MONGOLAB_URI_RECURSIVELY: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', + WITHOUT_CURLY_BRACES_URI: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db', + WITHOUT_CURLY_BRACES_USER_RECURSIVELY: 'username:password', + WITHOUT_CURLY_BRACES_URI_RECURSIVELY: 'mongodb://username:password@abcd1234.mongolab.com:12345/heroku_db' + } + + expectResultsToContainReplacements( + new DotenvPlugin({ path: envExpanded, expand: true }), + expected, + done + ) + }) + }) - test('Should include environment variables that exist in .env file.', () => { - expect(envTest()).toEqual(envDefJson) - }) + describe('Simple configuration', () => { + test('Should load enviornment variables when they exist in the .env file.', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envSimple }), + simpleResult, + done + ) + }) - test('Should not expand variables by default', () => { - expect(envTest({ path: envExpanded })).toEqual(envExpandedNotJson) - }) + test('Should be an empty object when no environment variables exist in .env file.', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: false }), + emptyResult, + done + ) + }) - test('Should expand variables when configured', () => { - expect(envTest({ path: envExpanded, expand: true })).toEqual(envExpandedJson) - }) + test('Should recognize safe-mode', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ safe: true }), + defaultEnvResult, + done + ) }) - describe('Simple configuration', () => { - test('Should load enviornment variables when they exist in the .env file.', () => { - expect(envTest({ path: envSimple })).toEqual(envSimpleJson) - }) + test('Should fail when not passing safe-mode', (done) => { + const config = getConfig('web', new DotenvPlugin({ path: envEmpty, safe: true })) - test('Should be an empty object when no environment variables exist in .env file.', () => { - expect(envTest({ path: false })).toEqual(envEmptyJson) - }) + webpack(config, (err) => { + expect(err.message).toBe('Missing environment variable: TEST') - test('Should recognize safe-mode', () => { - expect(envTest({ safe: true })).toEqual(envDefJson) + done() }) + }) + }) - test('Should fail when not passing safe-mode', () => { - try { - envTest({ path: envEmpty, safe: true }) - throw new Error('Should not get here') - } catch (err) { - expect(err.message).toEqual('Missing environment variable: TEST') + describe('Safe configuration', () => { + test('Should load successfully if variables defined', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envEmpty, safe: envEmptyExample }), + emptyResult, + () => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envSimple, safe: envSimpleExample }), + simpleResult, + done + ) } - }) + ) }) - describe('Safe configuration', () => { - test('Should load successfully if variables defined', () => { - expect(envTest({ path: envEmpty, safe: envEmptyExample })).toEqual(envEmptyJson) - expect(envTest({ path: envSimple, safe: envSimpleExample })).toEqual(envSimpleJson) - }) + test('Should fail if env does not match sample.', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ path: envEmpty, safe: envSimpleExample }) + ) - test('Should fail if env does not match sample.', () => { - try { - envTest({ path: envEmpty, safe: envSimpleExample }) - throw new Error('Should not get here') - } catch (err) { - expect(err.message).toEqual('Missing environment variable: TEST') - } + webpack(config, (err) => { + expect(err.message).toBe('Missing environment variable: TEST') + + done() }) }) + }) - describe('Defaults configuration', () => { - test('should support default configurations', () => { - expect(envTest({ defaults: true })).toEqual(envDefaultsJson) - }) + describe('Defaults configuration', () => { + test('should support default configurations', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ defaults: true }), + defaultsResult, + done + ) + }) - test('should support string configurations', () => { - expect(envTest({ defaults: envDefaults })).toEqual(envDefaultsJson2) - }) + test('should support string configurations', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ defaults: envDefaults }), + defaultsResult2, + done + ) + }) - test('Should display warning when default cannot be loaded', () => { - const envDefaultName = '.does.not.exist' - expect(envTest({ defaults: envDefaultName })).toEqual(envDefJson) - expect(global.console.warn).toHaveBeenCalledWith(`Failed to load ${envDefaultName}.`) - }) + test('Should display warning when default cannot be loaded', (done) => { + const envDefaultName = '.does.not.exist' + expectResultsToContainReplacements( + new DotenvPlugin({ defaults: envDefaultName }), + defaultEnvResult, + done + ) + + expect(global.console.warn).toHaveBeenCalledWith(`Failed to load ${envDefaultName}.`) }) + }) - describe('System variables', () => { - test('Should allow system env variables', () => { - const test = envTest({ path: envSimple, systemvars: true }) - const key = Object.keys(envSimpleJson)[0] - const value = envSimpleJson[key] - expect(test[key]).toEqual(value) - expect(Object.keys(test).length > Object.keys(envSimpleJson).length).toEqual(true) - }) + describe('System variables', () => { + const originalPath = process.env.PATH + beforeEach(() => { + process.env.PATH = '/usr/local/bin:/usr/local/sbin:' + }) + afterEach(() => { + process.env.PATH = originalPath + }) - test('should pass if the systemvar satisfies the requirement', () => { - const PATH = envTest({ safe: envSystemvarsExample, systemvars: true })['process.env.PATH'] - expect(typeof PATH).toEqual('string') - expect(PATH.indexOf('/') !== -1 || PATH.indexOf('\\') !== -1).toEqual(true) - }) + test('Should allow system env variables', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ path: envSimple, systemvars: true }) + ) - test('should not allow local variables to override systemvars', () => { - expect(envTest({ path: envSystemvars, systemvars: true })['process.env.PATH2'] !== '""').toEqual(true) - }) + compile(config, (result) => { + expect(result).toMatch('const TEST = "testing"') + expect(result).toMatch('const PATH = "/usr/local/bin:/usr/local/sbin:') - test('Should give the highest priority for the system variables', () => { - process.env.TEST = 'production' - const test = envTest({ safe: true, systemvars: true, defaults: true }) - expect(test['process.env.TEST']).toEqual('"production"') - expect(test['process.env.TEST2']).toEqual('"hidefault"') - delete process.env.TEST + done() }) }) - describe('Empty variables', () => { - test('Should load fine (not-safe)', () => { - expect(envTest({ path: envOneEmpty })).toEqual(envOneEmptyJson) - }) + test('should pass if the systemvar satisfies the requirement', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ safe: envSystemvarsExample, systemvars: true }) + ) - test('Should fail on safe mode', () => { - try { - envTest({ path: envOneEmpty, safe: envOneEmptyExample }) - throw new Error('Should not get here') - } catch (err) { - expect(err.message).toEqual('Missing environment variable: TEST') - } + compile(config, (result) => { + expect(result).toMatch('const TEST = "hi"') + expect(result).toMatch(/const PATH = ".*[\\/].*"/) + + done() }) + }) + + test('should not allow local variables to override systemvars', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ path: envSystemvars, systemvars: true }) + ) + + compile(config, (result) => { + expect(result).toMatch('const TEST = "MISSING_ENV_VAR".TEST') + expect(result).not.toMatch('const PATH = ""') - test('Should succeed in safe mode if allowEmptyValues is true', () => { - expect(envTest({ path: envOneEmpty, safe: envOneEmptyExample, allowEmptyValues: true })).toEqual(envOneEmptyJson) + done() }) }) - describe('Missing a variable', () => { - test('Should load fine (not-safe)', () => { - expect(envTest({ path: envMissingOne })).toEqual(envMissingOneJson) + test('Should give the highest priority for the system variables', (done) => { + process.env.TEST = 'production' + + const config = getConfig( + 'web', + new DotenvPlugin({ safe: true, systemvars: true, defaults: true }) + ) + + compile(config, (result) => { + expect(result).toMatch('const TEST = "production"') + expect(result).toMatch('const TEST2 = "hidefault"') + + done() }) - test('Should fail on safe mode (if allowEmptyValues is false)', () => { - try { - envTest({ path: envMissingOne, safe: envMissingOneExample }) - throw new Error('Should not get here') - } catch (err) { - expect(err.message).toEqual('Missing environment variable: TEST') - } + delete process.env.TEST + }) + }) + + describe('Empty variables', () => { + test('Should load fine (not-safe)', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envOneEmpty }), + oneEmptyResult, + done + ) + }) + + test('Should fail on safe mode', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ path: envOneEmpty, safe: envOneEmptyExample }) + ) + + webpack(config, (err) => { + expect(err.message).toBe('Missing environment variable: TEST') + + done() }) }) - describe('Deprecated configuration', () => { - test('Should use safe when safe and sample set', () => { - expect(envTest({ path: envSimple, safe: true, sample: envSimpleExample })).toEqual(envSimpleJson) + test('Should succeed in safe mode if allowEmptyValues is true', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envOneEmpty, safe: envOneEmptyExample, allowEmptyValues: true }), + oneEmptyResult, + done + ) + }) + }) + + describe('Missing a variable', () => { + test('Should load fine (not-safe)', (done) => { + expectResultsToContainReplacements( + new DotenvPlugin({ path: envMissingOne }), + missingOneResult, + done + ) + }) + + test('Should fail on safe mode (if allowEmptyValues is false)', (done) => { + const config = getConfig( + 'web', + new DotenvPlugin({ path: envMissingOne, safe: envMissingOneExample }) + ) + + webpack(config, (err) => { + expect(err.message).toBe('Missing environment variable: TEST') + + done() }) + }) + }) - test('Should display deprecation warning by default', () => { - expect(envTest({ path: envSimple, safe: true, sample: envSimpleExample })).toEqual(envSimpleJson) + describe('Silent mode', () => { + test('Should display warning by default', (done) => { + compile(getConfig('web', new DotenvPlugin({ path: false })), () => { expect(global.console.warn).toHaveBeenCalled() - }) - test('Should not display deprecation warning when silent mode enabled', () => { - expect(envTest({ path: envSimple, safe: true, sample: envSimpleExample, silent: true })).toEqual(envSimpleJson) - expect(global.console.warn).toHaveBeenCalledTimes(0) + done() }) + }) + + test('Should not display warning when silent mode enabled', (done) => { + compile( + getConfig('web', new DotenvPlugin({ path: false, silent: true })), + () => { + expect(global.console.warn).toHaveBeenCalledTimes(0) - test('Should fail naturally when using deprecated values', () => { - try { - envTest({ path: envMissingOne, safe: true, sample: envMissingOneExample }) - throw new Error('Should not get here') - } catch (err) { - expect(err.message).toEqual('Missing environment variable: TEST') + done() } - }) + ) + }) + }) - test('Should not fail naturally when using deprecated values improperly', () => { - expect(envTest({ path: envMissingOne, sample: envMissingOneExample })).toEqual(envMissingOneJson) - }) + describe('process.env stubbing', () => { + const expectToBeStubbed = (result) => { + expect(result).toMatch('const TEST = "testing"') + expect(result).toMatch('const TEST2 = "MISSING_ENV_VAR".TEST2') + expect(result).toMatch('const NODE_ENV = "development"') + expect(result).toMatch('const MONGOLAB_USER = "MISSING_ENV_VAR".MONGOLAB_USER') + } + + const expectNotToBeStubbed = (result) => { + expect(result).toMatch('const TEST = "testing"') + expect(result).toMatch('const TEST2 = process.env.TEST2') + expect(result).toMatch('const NODE_ENV = "development"') + expect(result).toMatch('const MONGOLAB_USER = process.env.MONGOLAB_USER') + } + + const plugin = new DotenvPlugin({ path: envSimple }) + const cases = [ + ['web', true], + [['web'], true], + ['es5', true], + ['es2020', true], + [['es2020', 'web'], true], + ['electron-renderer', true], + ['electron9-renderer', true], + ['electron-preload', true], + ['node', false], + [['node'], false], + ['node14', false], + ['electron-main', false], + ['electron9-main', false], + [['es2020', 'node'], false] + ] + + test.each(cases)('%s', (target, shouldStub, done) => { + compile( + getConfig(target, plugin), + (result) => { + if (shouldStub) { + expectToBeStubbed(result) + } else { + expectNotToBeStubbed(result) + } + + done() + } + ) }) - describe('Silent mode', () => { - test('Should display warning by default', () => { - envTest({ path: false }) - expect(global.console.warn).toHaveBeenCalled() + describe('ignoreStub override', () => { + // `ignoreStub == null` case is covered by above test + + test('should never stub if set to true', (done) => { + const plugin = new DotenvPlugin({ ignoreStub: true, path: envSimple }) + + compile(getConfig('web', plugin), (result) => { + expectNotToBeStubbed(result) + + compile(getConfig('node', plugin), (result) => { + expectNotToBeStubbed(result) + + done() + }) + }) }) - test('Should not display warning when silent mode enabled', () => { - envTest({ path: false, silent: true }) - expect(global.console.warn).toHaveBeenCalledTimes(0) + test('should always stub if set to false', (done) => { + const plugin = new DotenvPlugin({ ignoreStub: false, path: envSimple }) + + compile(getConfig('web', plugin), (result) => { + expectToBeStubbed(result) + + compile(getConfig('node', plugin), (result) => { + expectToBeStubbed(result) + + done() + }) + }) }) }) }) -} - -describe('Tests', () => { - runTests(Src.default, 'Source') - runTests(Dist.default, 'Dist') })