├── config.js ├── test ├── .env.missing ├── .env.defaults.example ├── .env.schema.regex-invalid ├── .env ├── .env.schema.example ├── .env.schema.regex ├── .env.override ├── .env.extra ├── .env.schema.regex-optional └── test.spec.js ├── .babelrc ├── .travis.yml ├── .npmignore ├── .editorconfig ├── src ├── utils │ ├── load-environment-file.js │ ├── config-from-env.js │ └── parse-command.js ├── config.js ├── bin │ └── index.js └── index.js ├── .eslintrc ├── CONTRIBUTING.md ├── .gitignore ├── gulpfile.esm.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── dotenv-extended.d.ts └── README.md /config.js: -------------------------------------------------------------------------------- 1 | require('./lib/config'); 2 | -------------------------------------------------------------------------------- /test/.env.missing: -------------------------------------------------------------------------------- 1 | TEST_TWO=two 2 | -------------------------------------------------------------------------------- /test/.env.defaults.example: -------------------------------------------------------------------------------- 1 | TEST_ONE=one 2 | -------------------------------------------------------------------------------- /test/.env.schema.regex-invalid: -------------------------------------------------------------------------------- 1 | TEST_ONE=( 2 | -------------------------------------------------------------------------------- /test/.env: -------------------------------------------------------------------------------- 1 | TEST_VAR=my test var 2 | TEST_ONE=overridden 3 | -------------------------------------------------------------------------------- /test/.env.schema.example: -------------------------------------------------------------------------------- 1 | TEST_ONE= 2 | TEST_TWO= 3 | TEST_THREE= 4 | -------------------------------------------------------------------------------- /test/.env.schema.regex: -------------------------------------------------------------------------------- 1 | TEST_ONE=e\sover 2 | TEST_TWO=^\S+$ 3 | TEST_THREE=.+ 4 | -------------------------------------------------------------------------------- /test/.env.override: -------------------------------------------------------------------------------- 1 | TEST_ONE=one overridden 2 | TEST_TWO=two 3 | TEST_THREE=three 4 | -------------------------------------------------------------------------------- /test/.env.extra: -------------------------------------------------------------------------------- 1 | TEST_ONE=one 2 | TEST_TWO=two 3 | TEST_THREE=three 4 | TEST_FOUR=four 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [] 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "12" 5 | - "10" 6 | - "8" 7 | - "6" 8 | after_success: 'npm run coveralls' 9 | -------------------------------------------------------------------------------- /test/.env.schema.regex-optional: -------------------------------------------------------------------------------- 1 | TEST_VAR=^my test var$ 2 | TEST_ONE=^overridden$ 3 | 4 | TEST_MISSING_OPTIONAL=^(optional)?$ 5 | TEST_MISSING_REQUIRED=^optional$ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage 3 | .babelrc 4 | node_modules 5 | src 6 | test 7 | .editorconfig 8 | .eslintrc 9 | .gitignore 10 | .travis.yml 11 | gulpfile.esm.js 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.eslintrc}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/utils/load-environment-file.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import dotenv from 'dotenv'; 3 | 4 | export const loadEnvironmentFile = (path, encoding, silent) => { 5 | try { 6 | const data = fs.readFileSync(path, encoding); 7 | return dotenv.parse(data); 8 | } catch (err) { 9 | if (!silent) { 10 | console.error(err.message); 11 | } 12 | return {}; 13 | } 14 | }; 15 | export default loadEnvironmentFile; 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "indent": [ 5 | 2, 6 | 4 7 | ], 8 | "quotes": [ 9 | 2, 10 | "single" 11 | ], 12 | "no-console": [0], 13 | "linebreak-style": [ 14 | 2, 15 | "unix" 16 | ], 17 | "semi": [ 18 | 2, 19 | "always" 20 | ] 21 | }, 22 | "env": { 23 | "es6": true, 24 | "node": true, 25 | "mocha": true 26 | }, 27 | "extends": "eslint:recommended" 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/config-from-env.js: -------------------------------------------------------------------------------- 1 | import autoParse from 'auto-parse'; 2 | import camelCase from 'camelcase'; 3 | 4 | export const getConfigFromEnv = env => { 5 | let config = {}; 6 | Object.keys(env).forEach((key) => { 7 | const curr = key.split('DOTENV_CONFIG_'); 8 | if (curr.length === 2 && curr[0] === '' && curr[1].length) { 9 | config[camelCase(curr[1])] = autoParse(env[key]); 10 | } 11 | }); 12 | return config; 13 | }; 14 | export default getConfigFromEnv; 15 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import {config} from './index'; 2 | 3 | const reduceArguments = (prev, curr) => { 4 | const matches = curr.match(/^dotenv_config_(.+)=(.+)/); 5 | return hasMatches(matches) 6 | ? expandKeyValFromMatches(matches, prev) 7 | : prev; 8 | }; 9 | 10 | const expandKeyValFromMatches = ([, key, value], prev) => ({ 11 | ...prev, 12 | [key]: value 13 | }); 14 | 15 | const hasMatches = matches => matches && matches.length >= 3; 16 | 17 | const options = process.argv.reduce(reduceArguments, {}); 18 | 19 | config(options); 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it and clone it 4 | 1. Checkout the `develop` branch 5 | 1. `npm install` 6 | 1. Create your feature branch (`git checkout -b my-new-feature`) 7 | 1. Add your feature code and supporting unit tests 8 | 1. `npm test` 9 | 1. Commit your changes 10 | 1. Push to the branch (`git push origin my-new-feature`) 11 | 1. Create new Pull Request from your feature branch to the main `develop` branch 12 | 13 | ## Testing 14 | 15 | ``` 16 | npm test 17 | ``` 18 | or 19 | 20 | ``` 21 | gulp unittest 22 | ``` 23 | 24 | ## Code Style 25 | 26 | ``` 27 | npm run lint 28 | ``` 29 | or 30 | 31 | ``` 32 | gulp lint 33 | ``` 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | 3 | ### Node template 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 32 | node_modules 33 | 34 | # Created by .ignore support plugin (hsz.mobi) 35 | -------------------------------------------------------------------------------- /gulpfile.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Keith Morris on 2/9/16. 3 | */ 4 | import babel from 'gulp-babel'; 5 | import del from 'del'; 6 | import eslint from 'gulp-eslint'; 7 | import { src, dest, series, watch } from 'gulp'; 8 | 9 | const options = { 10 | buildDir: 'lib' 11 | }; 12 | 13 | export const clean = () => del([ 14 | options.buildDir, 15 | 'coverage', 16 | '.nyc_output' 17 | ]); 18 | 19 | const watchFiles = () => { 20 | watch(['src/**/*.js'], series([build])); 21 | }; 22 | export { watchFiles as watch }; 23 | 24 | export const lint = () => src(['src/**/*.js']) 25 | .pipe(eslint()) 26 | .pipe(eslint.format()) 27 | .pipe(eslint.failAfterError()); 28 | 29 | export const scripts = () => src(['src/**/*.js']) 30 | .pipe(babel()) 31 | .pipe(dest(options.buildDir)); 32 | 33 | export const build = series([clean, lint, scripts]); 34 | 35 | export default build; 36 | -------------------------------------------------------------------------------- /src/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Created by Keith Morris on 4/26/17. 4 | * 5 | * This bin script is inspired by and borrows heavily from CrossEnv 6 | * https://github.com/kentcdodds/cross-env 7 | */ 8 | 9 | import {config} from '..'; 10 | import parseCommand from '../utils/parse-command'; 11 | import {spawn} from 'cross-spawn'; 12 | 13 | function loadAndExecute(args) { 14 | const [dotEnvConfig, command, commandArgs] = parseCommand(args); 15 | if (command) { 16 | config(dotEnvConfig); // mutates process.env 17 | const proc = spawn(command, commandArgs, { 18 | stdio: 'inherit', 19 | shell: true, 20 | env: process.env, 21 | }); 22 | 23 | process.on('SIGTERM', () => proc.kill('SIGTERM')); 24 | process.on('SIGINT', () => proc.kill('SIGINT')); 25 | process.on('SIGBREAK', () => proc.kill('SIGBREAK')); 26 | process.on('SIGHUP', () => proc.kill('SIGHUP')); 27 | proc.on('exit', process.exit); 28 | 29 | return proc; 30 | } 31 | } 32 | 33 | loadAndExecute(process.argv.slice(2)); 34 | -------------------------------------------------------------------------------- /src/utils/parse-command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Keith Morris on 4/26/17. 3 | */ 4 | import autoParse from 'auto-parse'; 5 | import camelcase from 'camelcase'; 6 | 7 | const dotEnvFlagRegex = /^--(.+)=(.+)/; 8 | 9 | /** 10 | * First parses config variables for dotenv-extended then selects the next item as the command and everything after that 11 | * are considered arguments for the command 12 | * 13 | * @param args 14 | * @returns {[Object,String,Array]} 15 | */ 16 | export const parseCommand = (args) => { 17 | const config = {}; 18 | let command = null; 19 | let commandArgs = []; 20 | for (let i = 0; i < args.length; i++) { 21 | const match = dotEnvFlagRegex.exec(args[i]); 22 | if (match) { 23 | config[camelcase(match[1])] = autoParse(match[2]); 24 | } else { 25 | // No more env setters, the rest of the line must be the command and args 26 | command = args[i]; 27 | commandArgs = args.slice(i + 1); 28 | break; 29 | } 30 | } 31 | return [config, command, commandArgs]; 32 | }; 33 | export default parseCommand; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Keith Morris 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotenv-extended", 3 | "version": "2.9.0", 4 | "description": "A module for loading .env files and optionally loading defaults and a schema for validating all values are present.", 5 | "repository": "git@github.com:keithmorris/node-dotenv-extended.git", 6 | "main": "lib/index.js", 7 | "types": "dotenv-extended.d.ts", 8 | "bin": "lib/bin/index.js", 9 | "engines": { 10 | "node": ">=6" 11 | }, 12 | "scripts": { 13 | "test": "npm run build && cd test && nyc --reporter=lcovonly mocha --require esm *.spec.js", 14 | "report": "nyc report", 15 | "coveralls": "npm run report -- --reporter=text-lcov | coveralls", 16 | "lint": "gulp lint", 17 | "prepare": "npm run build", 18 | "build": "gulp build" 19 | }, 20 | "keywords": [ 21 | "dotenv", 22 | "environment" 23 | ], 24 | "author": "Keith Morris (http://standupbass.wordpress.com/)", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@babel/core": "^7.9.0", 28 | "@babel/preset-env": "^7.9.0", 29 | "@types/chai": "^4.2.11", 30 | "@types/mocha": "^7.0.2", 31 | "@types/sinon": "^9.0.0", 32 | "@types/sinon-chai": "^3.2.4", 33 | "babel-eslint": "8.2.6", 34 | "chai": "^4.2.0", 35 | "coveralls": "^3.0.7", 36 | "del": "^4.1.1", 37 | "eslint": "^5.3.0", 38 | "eslint-config-standard": "11.0.0", 39 | "eslint-plugin-import": "^2.13.0", 40 | "eslint-plugin-node": "^7.0.1", 41 | "eslint-plugin-promise": "^3.8.0", 42 | "eslint-plugin-standard": "^3.1.0", 43 | "esm": "^3.2.25", 44 | "gulp": "^4.0.2", 45 | "gulp-babel": "^8.0.0", 46 | "gulp-eslint": "^5.0.0", 47 | "mocha": "^6.2.2", 48 | "mockery": "^2.1.0", 49 | "nyc": "^14.1.1", 50 | "sinon": "^9.0.1", 51 | "sinon-chai": "^3.5.0" 52 | }, 53 | "dependencies": { 54 | "auto-parse": "^1.3.0", 55 | "camelcase": "^5.3.1", 56 | "cross-spawn": "^7.0.1", 57 | "dotenv": "^8.2.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.9.0 - 2020.09.30 4 | - Missing values in schema now default to empty strings for `errorOnRegex` (thanks @FokkeZB) 5 | - Minor modernization and refactoring of unit tests 6 | 7 | ## 2.8.0 - 2020.03.25 8 | - Update dependencies while retaining compatibility with Node 6 9 | - Add ability to configure through environment variables (thanks @Levino) 10 | - Remove `@types/dotenv` as a dependency 11 | - Add Node 12 to travis-ci tests 12 | - General code cleanup/modernizing 13 | 14 | ## 2.7.1 - 2019.12.30 15 | - Update README to include `import` syntax 16 | 17 | ## 2.7.0 - 2019.12.13 18 | - fix: check for extra keys needs to be specific to schema 19 | 20 | ## 2.5.0 - 2019.10.27 21 | - Update dependencies and remove useless dependencies (thanks @webdevium) 22 | - Modernize and streamline build process 23 | 24 | ## 2.4.0 - 2019.03.01 25 | - Add ability to put regexs in the `.env.schema` file to validate and limit the values that can be added to the `.evn` and `.env.defaults` files (thanks @epiphone) 26 | 27 | ## 2.3.0 - 2018.09.13 28 | - Add error checking flag to include process.env when it checks for required variables (thanks @Vija02) 29 | 30 | ## 2.2.0 - 2018.08.07 31 | - Remove support for end-of-life versions of node (4, 5, 7, 9) 32 | - Require node >=6.0.0 33 | - Update package dependencies 34 | 35 | ## 2.1.0 - 2018.08.07 36 | - Expose entire process.env to command called with CLI 37 | 38 | ## 2.0.2 - 2018.04.16 39 | - Fix markdown header (thanks @vvo) 40 | - Update package dependencies (thanks @gregswindle) 41 | - Add node versions 8 and 9 to travis versions to test 42 | 43 | ## 2.0.1 - 2017.05.30 44 | - Add TypeScript definitions (thanks @toverux) 45 | 46 | ## 2.0.0 - 2017.04.26 47 | - Add binary for injecting .env variables into non-node scripts 48 | 49 | ## 1.0.4 - 2016.10.23 50 | - Replace `winston` library with generic `console` (Thanks @bostrom) 51 | 52 | ## 1.0.3 - 2016.08.06 53 | - Fix comma-space typo in thrown error (Thanks @niftylettuce) 54 | 55 | ## 1.0.2 - 2016.08.04 56 | - Add `default` export to simplify ES6 imports 57 | 58 | ## 1.0.1 - 2016.05.10 59 | - Add ability to load .env files from command line 60 | - Update dependencies 61 | 62 | ## 1.0.0 - 2016.02.15 63 | 64 | - Correct documentation error 65 | - Update dependencies 66 | 67 | ## 0.1.1 - 2016.02.10 68 | - Remove errant ide files from package 69 | - Add prepublish npm script 70 | 71 | ## 0.1.0 - 2016.02.09 72 | - Initial release 73 | -------------------------------------------------------------------------------- /dotenv-extended.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * The result of a call to load() or parse() 5 | */ 6 | export interface IEnvironmentMap { 7 | [name: string]: string; 8 | } 9 | 10 | /** 11 | * DotenvExtended options for load(). 12 | */ 13 | export interface IDotenvExtendedOptions { 14 | /** 15 | * Sets the encoding of the .env files. 16 | * 17 | * @default 'utf-8' 18 | */ 19 | encoding?: string; 20 | 21 | /** 22 | * Sets whether a log message is shown when missing the .env or .env.defaults files. 23 | * 24 | * @default true 25 | */ 26 | silent?: boolean; 27 | 28 | /** 29 | * Path to the main .env file that contains your variables. 30 | * 31 | * @default '.env' 32 | */ 33 | path?: string; 34 | 35 | /** 36 | * The path to the file that default values are loaded from. 37 | * 38 | * @default '.env.defaults' 39 | */ 40 | defaults?: string; 41 | 42 | /** 43 | * The path to the file that contains the schema of what values should be available 44 | * from combining .env and .env.defaults. 45 | * 46 | * @default '.env.schema' 47 | */ 48 | schema?: string; 49 | 50 | /** 51 | * Causes the library to throw a MISSING CONFIG VALUES error listing all of the variables 52 | * missing the combined .env and .env.defaults files. 53 | * 54 | * @default false 55 | */ 56 | errorOnMissing?: boolean; 57 | 58 | /** 59 | * Causes the library to throw a EXTRA CONFIG VALUES error listing all of the extra variables 60 | * from the combined .env and .env.defaults files. 61 | * 62 | * @default false 63 | */ 64 | errorOnExtra?: boolean; 65 | 66 | /** 67 | * Causes the library to throw a REGEX MISMATCH error listing all of the invalid variables from the combined .env 68 | * and .env.defaults files. Also a SyntaxError is thrown in case .env.schema contains a syntactically invalid regex. 69 | * 70 | * @default false 71 | */ 72 | errorOnRegex?: boolean; 73 | 74 | /** 75 | * Causes the library add process.env variables to error checking. The variables in process.env overrides the 76 | * variables in .env and .env.defaults while checking 77 | * 78 | * @default false 79 | */ 80 | includeProcessEnv?: boolean; 81 | 82 | /** 83 | * Sets whether the loaded values are assigned to the process.env object. 84 | * If this is set, you must capture the return value of the call to .load() or you will not be 85 | * able to use your variables. 86 | * 87 | * @default true 88 | */ 89 | assignToProcessEnv?: boolean; 90 | 91 | /** 92 | * By defaut, dotenv-entended will not overwrite any varibles that are already set in the process.env object. 93 | * If you would like to enable overwriting any already existing values, set this value to true. 94 | * 95 | * @default false 96 | */ 97 | overrideProcessEnv?: boolean; 98 | } 99 | 100 | export { parse } from 'dotenv'; 101 | 102 | /** 103 | * Loads the dotenv files, .env, .env.defaults and .env.schema. 104 | * 105 | * @param options 106 | */ 107 | export function load(options?: IDotenvExtendedOptions): IEnvironmentMap; 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Keith Morris on 2/9/16. 3 | */ 4 | import dotenv from 'dotenv'; 5 | import getConfigFromEnv from './utils/config-from-env'; 6 | import loadEnvironmentFile from './utils/load-environment-file'; 7 | 8 | export const parse = dotenv.parse.bind(dotenv); 9 | export const config = options => { 10 | 11 | let defaultsData, environmentData, 12 | defaultOptions = { 13 | encoding: 'utf8', 14 | silent: true, 15 | path: '.env', 16 | defaults: '.env.defaults', 17 | schema: '.env.schema', 18 | errorOnMissing: false, 19 | errorOnExtra: false, 20 | errorOnRegex: false, 21 | includeProcessEnv: false, 22 | assignToProcessEnv: true, 23 | overrideProcessEnv: false 24 | }, 25 | processEnvOptions = getConfigFromEnv(process.env); 26 | 27 | options = Object.assign({}, defaultOptions, processEnvOptions, options); 28 | 29 | defaultsData = loadEnvironmentFile(options.defaults, options.encoding, options.silent); 30 | environmentData = loadEnvironmentFile(options.path, options.encoding, options.silent); 31 | 32 | let configData = Object.assign({}, defaultsData, environmentData); 33 | const config = options.includeProcessEnv ? Object.assign({}, configData, process.env) : configData; 34 | const configOnlyKeys = Object.keys(configData); 35 | const configKeys = Object.keys(config); 36 | 37 | if (options.errorOnMissing || options.errorOnExtra || options.errorOnRegex) { 38 | const schema = loadEnvironmentFile(options.schema, options.encoding, options.silent); 39 | const schemaKeys = Object.keys(schema); 40 | 41 | let missingKeys = schemaKeys.filter(function (key) { 42 | return configKeys.indexOf(key) < 0; 43 | }); 44 | let extraKeys = configOnlyKeys.filter(function (key) { 45 | return schemaKeys.indexOf(key) < 0; 46 | }); 47 | if (options.errorOnMissing && missingKeys.length) { 48 | throw new Error('MISSING CONFIG VALUES: ' + missingKeys.join(', ')); 49 | } 50 | 51 | if (options.errorOnExtra && extraKeys.length) { 52 | throw new Error('EXTRA CONFIG VALUES: ' + extraKeys.join(', ')); 53 | } 54 | 55 | if (options.errorOnRegex) { 56 | const regexMismatchKeys = schemaKeys.filter(function (key) { 57 | if (schema[key]) { 58 | return !new RegExp(schema[key]).test(typeof config[key] === 'string' ? config[key] : ''); 59 | } 60 | }); 61 | 62 | if (regexMismatchKeys.length) { 63 | throw new Error('REGEX MISMATCH: ' + regexMismatchKeys.join(', ')); 64 | } 65 | } 66 | } 67 | 68 | // the returned configData object should include process.env that override 69 | if (options.includeProcessEnv && !options.overrideProcessEnv) { 70 | for (let i = 0; i < configKeys.length; i++) { 71 | if (typeof process.env[configKeys[i]] !== 'undefined') 72 | configData[configKeys[i]] = process.env[configKeys[i]]; 73 | } 74 | } 75 | 76 | if (options.assignToProcessEnv) { 77 | if (options.overrideProcessEnv) { 78 | Object.assign(process.env, configData); 79 | } else { 80 | const tmp = Object.assign({}, configData, process.env); 81 | Object.assign(process.env, tmp); 82 | } 83 | } 84 | return configData; 85 | }; 86 | 87 | export const load = config; 88 | 89 | export default {parse, config, load}; 90 | -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import chai, { expect } from 'chai'; 3 | import mockery from 'mockery'; 4 | import sinon from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | 7 | import dotenvex from '../lib/index'; 8 | import parseCommand from '../lib/utils/parse-command'; 9 | import getConfigFromEnv from '../lib/utils/config-from-env'; 10 | 11 | chai.use(sinonChai); 12 | 13 | describe('dotenv-extended tests', () => { 14 | 15 | before(() => { 16 | mockery.enable({ 17 | warnOnReplace: false, 18 | warnOnUnregistered: false, 19 | useCleanCache: true 20 | }); 21 | sinon.stub(console, 'error'); 22 | }); 23 | 24 | after(() => { 25 | mockery.disable(); 26 | }); 27 | 28 | beforeEach((done) => { 29 | delete process.env.TEST_ONE; 30 | delete process.env.TEST_TWO; 31 | delete process.env.TEST_THREE; 32 | delete process.env.DOTENV_CONFIG_PATH; 33 | delete process.env.DOTENV_CONFIG_SCHEMA; 34 | delete process.env.DOTENV_CONFIG_OVERRIDE_PROCESS_ENV; 35 | done(); 36 | }); 37 | 38 | it('Should load .env file into process.env and not override process.env properties by default', () => { 39 | process.env.TEST_ONE = 'original'; 40 | dotenvex.load(); 41 | expect(process.env.TEST_ONE).to.equal('original'); 42 | }); 43 | 44 | it('Should load .env file into process.env and override process.env properties with overrideProcessEnv set to true', () => { 45 | process.env.TEST_ONE = 'original'; 46 | dotenvex.load({overrideProcessEnv: true}); 47 | expect(process.env.TEST_ONE).to.equal('overridden'); 48 | }); 49 | 50 | it('Should load take configuration values from environment variables', () => { 51 | process.env.TEST_ONE = 'original'; 52 | process.env.DOTENV_CONFIG_PATH = '.env.override'; 53 | process.env.DOTENV_CONFIG_SCHEMA = '.env.schema.example'; 54 | process.env.DOTENV_CONFIG_OVERRIDE_PROCESS_ENV = 'true'; 55 | dotenvex.load(); 56 | expect(process.env.TEST_ONE).to.equal('one overridden'); 57 | }); 58 | 59 | it('Should throw an error when items from schema are missing and errorOnMissing is true', () => { 60 | const runTest = () => { 61 | dotenvex.load({ 62 | schema: '.env.schema.example', 63 | defaults: '.env.defaults.example', 64 | path: '.env.missing', 65 | errorOnMissing: true 66 | }); 67 | }; 68 | expect(runTest).to.throw(Error); 69 | }); 70 | 71 | it('Should throw an error when there are extra items that are not in schema and errorOnExtra is true', () => { 72 | const runTest = function () { 73 | dotenvex.load({ 74 | schema: '.env.schema.example', 75 | defaults: '.env.defaults.example', 76 | path: '.env.extra', 77 | errorOnExtra: true 78 | }); 79 | }; 80 | expect(runTest).to.throw(Error); 81 | }); 82 | 83 | it('Should process process.env variables before checking errors when includeProcessEnv is true', () => { 84 | process.env.TEST_TWO = 'two'; 85 | process.env.TEST_THREE = 'three'; 86 | dotenvex.load({schema: '.env.schema.example', includeProcessEnv: true}); 87 | expect(process.env.TEST_TWO).to.equal('two'); 88 | }); 89 | 90 | it('Should load schema, defaults and env into correct values in process.env and returned object', () => { 91 | const config = dotenvex.load({ 92 | schema: '.env.schema.example', 93 | defaults: '.env.defaults.example', 94 | path: '.env.override', 95 | errorOnExtra: true, 96 | errorOnMissing: true 97 | }); 98 | expect(config.TEST_ONE).to.equal('one overridden'); 99 | expect(config.TEST_TWO).to.equal('two'); 100 | expect(config.TEST_THREE).to.equal('three'); 101 | expect(process.env.TEST_ONE).to.equal('one overridden'); 102 | expect(process.env.TEST_TWO).to.equal('two'); 103 | expect(process.env.TEST_THREE).to.equal('three'); 104 | }); 105 | 106 | it('Should not load .env files into process.env if assignToProcessEnv is false', () => { 107 | const config = dotenvex.load({ 108 | schema: '.env.schema.example', 109 | defaults: '.env.defaults.example', 110 | path: '.env.override', 111 | errorOnExtra: true, 112 | errorOnMissing: true, 113 | assignToProcessEnv: false 114 | }); 115 | expect(config.TEST_ONE).to.equal('one overridden'); 116 | expect(config.TEST_TWO).to.equal('two'); 117 | expect(config.TEST_THREE).to.equal('three'); 118 | expect(process.env.TEST_ONE).to.equal(undefined); 119 | expect(process.env.TEST_TWO).to.equal(undefined); 120 | expect(process.env.TEST_THREE).to.equal(undefined); 121 | }); 122 | 123 | it('Should pass regex validation when errorOnRegex is true and values match patterns', () => { 124 | const runTest = () => { 125 | dotenvex.load({ 126 | schema: '.env.schema.regex', 127 | path: '.env.override', 128 | errorOnRegex: true 129 | }); 130 | }; 131 | expect(runTest).not.to.throw(Error); 132 | }); 133 | 134 | it('Should throw a SyntaxError when a schema regex is invalid and errorOnRegex is true', () => { 135 | const runTest = () => { 136 | dotenvex.load({ 137 | schema: '.env.schema.regex-invalid', 138 | errorOnRegex: true 139 | }); 140 | }; 141 | expect(runTest).to.throw(SyntaxError); 142 | }); 143 | 144 | it('Should not throw a SyntaxError when a schema regex is invalid but errorOnRegex is false', () => { 145 | const runTest = () => { 146 | dotenvex.load({ 147 | schema: '.env.schema.regex-invalid', 148 | errorOnRegex: false 149 | }); 150 | }; 151 | expect(runTest).not.to.throw(SyntaxError); 152 | }); 153 | 154 | it('Should throw an error when an item does not match schema regex and errorOnRegex is true', () => { 155 | process.env.TEST_TWO = 'string with whitespace'; 156 | process.env.TEST_THREE = ''; 157 | const runTest = () => { 158 | dotenvex.load({ 159 | schema: '.env.schema.regex', 160 | path: '.env.override', 161 | includeProcessEnv: true, 162 | errorOnRegex: true 163 | }); 164 | }; 165 | expect(runTest).to.throw('REGEX MISMATCH: TEST_TWO, TEST_THREE'); 166 | }); 167 | 168 | it('Should default missing values to empty string when errorOnRegex is true', () => { 169 | const runTest = () => { 170 | dotenvex.load({ 171 | schema: '.env.schema.regex-optional', 172 | errorOnRegex: true, 173 | }); 174 | }; 175 | expect(runTest).to.throw('REGEX MISMATCH: TEST_MISSING_REQUIRED'); 176 | }); 177 | 178 | it('Should log an error when silent is set to false and .env.defaults is missing', function () { 179 | dotenvex.load({silent: false}); 180 | expect(console.error).to.have.been.calledOnce; 181 | }); 182 | }); 183 | 184 | describe('Supporting libraries tests', () => { 185 | beforeEach(() => { 186 | delete process.env.DOTENV_CONFIG_ENCODING; 187 | delete process.env.DOTENV_CONFIG_SILENT; 188 | delete process.env.DOTENV_CONFIG_PATH; 189 | delete process.env.DOTENV_CONFIG_DEFAULTS; 190 | delete process.env.DOTENV_CONFIG_SCHEMA; 191 | delete process.env.DOTENV_CONFIG_ERROR_ON_MISSING; 192 | delete process.env.DOTENV_CONFIG_ERROR_ON_EXTRA; 193 | delete process.env.DOTENV_CONFIG_ERROR_ON_REGEX; 194 | delete process.env.DOTENV_CONFIG_INCLUDED_PROCESS_ENV; 195 | delete process.env.DOTENV_CONFIG_ASSIGN_TO_PROCESS_ENV; 196 | delete process.env.DOTENV_CONFIG_OVERRIDE_PROCESS_ENV; 197 | }); 198 | const cliArgs = [ 199 | '--encoding=utf8', 200 | '--silent=true', 201 | '--path=test/.env.override', 202 | '--defaults=test/.env.defaults.example', 203 | '--schema=test/.env.schema.example', 204 | '--error-on-missing=false', 205 | '--error-on-extra=false', 206 | '--error-on-regex=false', 207 | '--assignToProcessEnv=true', 208 | '--overrideProcessEnv=false', 209 | 'testing.sh', 210 | '--jump', 211 | '--dive=true', 212 | 'I was here' 213 | ]; 214 | 215 | it('parseCommand should parse command line arguments correctly', () => { 216 | const parsed = parseCommand(cliArgs); 217 | const expected = { 218 | encoding: 'utf8', 219 | silent: true, 220 | path: 'test/.env.override', 221 | defaults: 'test/.env.defaults.example', 222 | schema: 'test/.env.schema.example', 223 | errorOnMissing: false, 224 | errorOnExtra: false, 225 | errorOnRegex: false, 226 | assignToProcessEnv: true, 227 | overrideProcessEnv: false 228 | }; 229 | expect(parsed[0]).to.eql(expected); 230 | expect(parsed[1]).to.eql('testing.sh'); 231 | expect(parsed[2]).to.eql(['--jump', '--dive=true', 'I was here']); 232 | }); 233 | 234 | it('getConfigFromEnv should parse environment variable config values correctly', () => { 235 | const expected = { 236 | encoding: 'utf8', 237 | silent: true, 238 | path: '.env', 239 | defaults: '.env.defaults', 240 | schema: '.env.schema', 241 | errorOnMissing: false, 242 | errorOnExtra: false, 243 | errorOnRegex: false, 244 | includedProcessEnv: false, 245 | assignToProcessEnv: true, 246 | overrideProcessEnv: false 247 | }; 248 | process.env.DOTENV_CONFIG_ENCODING = 'utf8'; 249 | process.env.DOTENV_CONFIG_SILENT = 'true'; 250 | process.env.DOTENV_CONFIG_PATH = '.env'; 251 | process.env.DOTENV_CONFIG_DEFAULTS = '.env.defaults'; 252 | process.env.DOTENV_CONFIG_SCHEMA = '.env.schema'; 253 | process.env.DOTENV_CONFIG_ERROR_ON_MISSING = 'false'; 254 | process.env.DOTENV_CONFIG_ERROR_ON_EXTRA = 'false'; 255 | process.env.DOTENV_CONFIG_ERROR_ON_REGEX = 'false'; 256 | process.env.DOTENV_CONFIG_INCLUDED_PROCESS_ENV = 'false'; 257 | process.env.DOTENV_CONFIG_ASSIGN_TO_PROCESS_ENV = 'true'; 258 | process.env.DOTENV_CONFIG_OVERRIDE_PROCESS_ENV = 'false'; 259 | const parsed = getConfigFromEnv(process.env); 260 | expect(parsed).to.eql(expected); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotenv-extended 2 | 3 | [![Build Status](https://travis-ci.org/keithmorris/node-dotenv-extended.svg?branch=develop)](https://travis-ci.org/keithmorris/node-dotenv-extended) 4 | [![Coverage Status](https://coveralls.io/repos/github/keithmorris/node-dotenv-extended/badge.svg?branch=develop)](https://coveralls.io/github/keithmorris/node-dotenv-extended?branch=develop) 5 | [![Dependency Status](https://david-dm.org/keithmorris/node-dotenv-extended.svg)](https://david-dm.org/keithmorris/node-dotenv-extended) 6 | 7 | 8 | I've been a big fan of the [dotenv] for a quite some time (in fact, this library uses [dotenv] under the hood for the `.env` file parsing). However, while working on some bigger projects, we realized that the managing of the `.env` files became a bit of a chore. As the files changed in the development environments, it became a tedious manual process to compare and figure out what needed to be added or removed in the other environments. 9 | 10 | This library solves some of these issues by introducing the concept of 3 files which are used together to provide environment-specific variables, default values and a validation schema: 11 | 12 | ### `.env` 13 | 14 | The environment specific file (not committed to source control). This file will have sensitive information such as usernames, passwords, api keys, etc. These would be specific to each environment and should not be committed to source control. The format is a series of key-value pairs. Any line starting with `#` or `;` are commented out and ignored. 15 | 16 | ``` 17 | # .env file 18 | MONGO_HOST=localhost 19 | MONGO_DATABASE=TestDB 20 | MONGO_USER=dbusername 21 | MONGO_PASS=dbpassword! 22 | ``` 23 | 24 | ### `.env.defaults` 25 | 26 | Common configuration defaults across all environments (commited to source control). This contains overall app configuration values that would be common across environments. The `.env.defaults` file is loaded first and then the `.env` file is loaded and will overwrite any values from the `.env.defaults` file. Format is identical to the `.env` file. 27 | 28 | ### `.env.schema` 29 | 30 | Defines a schema of what variables _should_ be defined in the combination of `.env` and `.env.defaults`. Optionally, you can have the library throw an error if all values are not configured or if there are extra values that shouldn't be there. 31 | 32 | 33 | The `.env.schema` file should only have the name of the variable and the `=` without any value: 34 | 35 | ``` 36 | MONGO_HOST= 37 | MONGO_DATABASE= 38 | MONGO_USER= 39 | MONGO_PASS= 40 | ``` 41 | 42 | Additionally `.env.schema` can include regular expressions; see [below](#load-files-with-erroronregex) for how to configure the library to throw an error upon failed regex validation. 43 | 44 | I have tried to stay as compatible as possible with the [dotenv] library but there are some differences. 45 | 46 | ## Installation 47 | 48 | ```bash 49 | npm i --save dotenv-extended 50 | ``` 51 | 52 | ## Usage 53 | 54 | As early as possible in your main script: 55 | 56 | ```javascript 57 | require('dotenv-extended').load(); 58 | ``` 59 | 60 | Or if you prefer import syntax: 61 | 62 | ```javascript 63 | import dotEnvExtended from 'dotenv-extended'; 64 | dotEnvExtended.load(); 65 | ``` 66 | 67 | Create a `.env` file in the root directory of your project. Add environment-specific variables on new lines in the form of `NAME=VALUE`. 68 | 69 | For example: 70 | 71 | ``` 72 | MONGO_HOST=localhost 73 | MONGO_DATABASE=TestDB 74 | MONGO_USER=dbusername 75 | MONGO_PASS=dbpassword! 76 | ``` 77 | 78 | `process.env` now has the keys and values you defined in your `.env` file. 79 | 80 | ```javascript 81 | mongoose.connect('mongodb://' + process.env.MONGO_HOST + '/' + process.env.MONGO_DATABASE, { 82 | user: process.env.MONGO_USER, 83 | pass: process.env.MONGO_PASS 84 | }); 85 | ``` 86 | 87 | ### Load Configs from command line 88 | 89 | You may also load the `.env` files from the command line. Add in the require `dotenv-extended/config` along with any of the options that the `load` method takes prefixed with `dotenv_config_`. e.g.: 90 | 91 | ```bash 92 | node -r dotenv-extended/config your_script.js 93 | ``` 94 | 95 | Or to specify load options: 96 | 97 | ```bash 98 | node -r dotenv-extended/config your_script.js dotenv_config_path=./env/.env dotenv_config_defaults=./env/.env.defaults 99 | ``` 100 | 101 | ### Load Environment Variables and pass to non-NodeJS script 102 | 103 | New in 2.0.0, is a feature inspired by [cross-env](https://www.npmjs.com/package/cross-env) to allow you to load environment variables from your `.env` files and then pass them into a non-NodeJS script such as a shell script. This can simplify the process of maintaining variables used in both your Node app and other scripts. To use this command line executable, you will either need to install globally with the `-g` flag, or install `dotenv-extended` in your project and reference it from your npm scripts. 104 | 105 | Install Globally: 106 | 107 | ```bash 108 | npm install -g dotenv-extended 109 | ``` 110 | 111 | Now call your shell scripts through `dotenv-extended` (this uses the defaults): 112 | 113 | ```bash 114 | dotenv-extended ./myshellscript.sh --whatever-flags-my-script-takes 115 | ``` 116 | 117 | Configure `dotenv-extended` by passing any of the dotenv-extended options before your command. Preceed each option with two dashes `--`: 118 | 119 | ```bash 120 | dotenv-extended --path=/path/to/.env --defaults=/path/to/.env.defaults --errorOnMissing=true ./myshellscript.sh --whatever-flags-my-script-takes 121 | ``` 122 | 123 | The following are the flags you can pass to the `dotenv-extended` cli with their default values. these options detailed later in this document: 124 | 125 | ```bash 126 | --encoding=utf8 127 | --silent=true 128 | --path=.env 129 | --defaults=.env.defaults 130 | --schema=.env.schema 131 | --errorOnMissing=false # or --error-on-missing=false 132 | --errorOnExtra=false # or --error-on-extra=false 133 | --errorOnRegex=false # or --error-on-regex=false 134 | --includeProcessEnv=false # or --include-process-env=false 135 | --assignToProcessEnv=true # or --assign-to-process-env=true 136 | --overrideProcessEnv=false # or --override-process-env=true 137 | ``` 138 | 139 | ## Options 140 | 141 | Defaults are shown below: 142 | 143 | ```javascript 144 | require('dotenv-extended').load({ 145 | encoding: 'utf8', 146 | silent: true, 147 | path: '.env', 148 | defaults: '.env.defaults', 149 | schema: '.env.schema', 150 | errorOnMissing: false, 151 | errorOnExtra: false, 152 | errorOnRegex: false, 153 | includeProcessEnv: false, 154 | assignToProcessEnv: true, 155 | overrideProcessEnv: false 156 | }); 157 | ``` 158 | ### Configure via Environment Variables (New in 2.8.0) 159 | 160 | You may also set the configuration values via environment variables loaded from `process.env` shown below with defaults: 161 | 162 | ``` 163 | DOTENV_CONFIG_ENCODING=utf8 164 | DOTENV_CONFIG_SILENT=true 165 | DOTENV_CONFIG_PATH=.env 166 | DOTENV_CONFIG_DEFAULTS=.env.defaults 167 | DOTENV_CONFIG_SCHEMA=.env.schema 168 | DOTENV_CONFIG_ERROR_ON_MISSING=false 169 | DOTENV_CONFIG_ERROR_ON_EXTRA=false 170 | DOTENV_CONFIG_ERROR_ON_REGEX=false 171 | DOTENV_CONFIG_INCLUDE_PROCESS_ENV=false 172 | DOTENV_CONFIG_ASSIGN_TO_PROCESS_ENV=true 173 | DOTENV_CONFIG_OVERRIDE_PROCESS_ENV=false 174 | ``` 175 | 176 | The `load()` function always returns an object containing the variables loaded from the `.env` and `.env.defaults` files. By default the returned object does not contain the properties held in `process.env` but rather only the ones that are loaded from the `.env` and `.env.defaults` files. 177 | 178 | ```javascript 179 | const myConfig = require('dotenv-extended').load(); 180 | ``` 181 | 182 | ### encoding (_default: utf8_) 183 | 184 | Sets the encoding of the `.env` files 185 | 186 | ### silent (_default: true_) 187 | 188 | Sets whether a log message is shown when missing the `.env` or `.env.defaults` files. 189 | 190 | ### path (_default: .env_) 191 | 192 | The main `.env` file that contains your variables. 193 | 194 | ### defaults (_default: .env.defaults_) 195 | 196 | The file that default values are loaded from. 197 | 198 | ### schema (_default: .env.schema_) 199 | 200 | The file that contains the schema of what values should be available from combining `.env` and `.env.defaults` 201 | 202 | ### errorOnMissing (_default: false_) 203 | 204 | Causes the library to throw a `MISSING CONFIG VALUES` error listing all of the variables missing the combined `.env` and `.env.defaults` files. 205 | 206 | ### errorOnExtra (_default: false_) 207 | 208 | Causes the library to throw a `EXTRA CONFIG VALUES` error listing all of the extra variables from the combined `.env` and `.env.defaults` files. 209 | 210 | ### errorOnRegex (_default: false_) 211 | 212 | Causes the library to throw a `REGEX MISMATCH` error listing all of the invalid variables from the combined `.env` and `.env.defaults` files. Also a `SyntaxError` is thrown in case `.env.schema` contains a syntactically invalid regex. 213 | 214 | ### includeProcessEnv (_default: false_) 215 | 216 | Causes the library add process.env variables to error checking. The variables in process.env overrides the variables in .env and .env.defaults while checking 217 | 218 | ### assignToProcessEnv (_default: true_) 219 | 220 | Sets whether the loaded values are assigned to the `process.env` object. If this is set, you must capture the return value of the call to `.load()` or you will not be able to use your variables. 221 | 222 | ### overrideProcessEnv (_default: false_) 223 | 224 | By defaut, `dotenv-entended` will not overwrite any varibles that are already set in the `process.env` object. If you would like to enable overwriting any already existing values, set this value to `true`. 225 | 226 | ## Examples 227 | 228 | Consider the following three files: 229 | 230 | ``` 231 | # .env file 232 | DB_HOST=localhost 233 | DB_USER=databaseuser-local 234 | DB_PASS=databasepw! 235 | SHARE_URL=http://www.example.com 236 | ``` 237 | 238 | ``` 239 | # .env.defaults 240 | DB_USER=databaseuser 241 | DB_DATABASE=MyAppDB 242 | ``` 243 | 244 | ``` 245 | # .env.schema 246 | DB_HOST=[a-z]+ 247 | DB_USER=[a-z]+ 248 | DB_PASS= 249 | DB_DATABASE= 250 | API_KEY= 251 | ``` 252 | 253 | ### Load files with default options 254 | 255 | ```javascript 256 | const myConfig = require('dotenv-extended').load(); 257 | 258 | myConfig.DB_HOST === process.env.DB_HOST === "localhost" 259 | myConfig.DB_USER === process.env.DB_USER === "databaseuser-local" 260 | myConfig.DB_PASS === process.env.DB_PASS === "localhost" 261 | myConfig.DB_DATABASE === process.env.DB_DATABASE === "MyAppDB" 262 | myConfig.SHARE_URL === process.env.SHARE_URL === "http://www.example.com" 263 | ``` 264 | 265 | ### Load files with `errorOnMissing` 266 | 267 | ```javascript 268 | const myConfig = require('dotenv-extended').load({ 269 | errorOnMissing: true 270 | }); 271 | 272 | // Throws ERROR `MISSING CONFIG VALUES: API_KEY` 273 | ``` 274 | 275 | ### Load files with `errorOnExtra` 276 | 277 | ```javascript 278 | const myConfig = require('dotenv-extended').load({ 279 | errorOnExtra: true 280 | }); 281 | 282 | // Throws ERROR `EXTRA CONFIG VALUES: SHARE_URL` 283 | ``` 284 | 285 | ### Load files with `errorOnRegex` 286 | 287 | ```javascript 288 | const myConfig = require('dotenv-extended').load({ 289 | errorOnRegex: true 290 | }); 291 | 292 | // Throws ERROR `REGEX MISMATCH: DB_USER` 293 | ``` 294 | 295 | ## Contributing 296 | 297 | See [CONTRIBUTING.md](CONTRIBUTING.md) 298 | 299 | ## Change Log 300 | 301 | See [CHANGELOG.md](CHANGELOG.md) 302 | 303 | ## License 304 | 305 | See [LICENSE](LICENSE) 306 | 307 | [dotenv]: https://www.npmjs.com/package/dotenv 308 | --------------------------------------------------------------------------------