├── .all-contributorsrc ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── assets ├── node-env-run-logo.png └── node-env-run-screenshot.png ├── bin └── node-env-run.ts ├── lib ├── __mocks__ │ └── utils.ts ├── __tests__ │ ├── run-invalid-script.ts │ ├── run-missing-env-file.ts │ ├── run-with-force.ts │ ├── run-without-force.ts │ ├── run-without-script-parameter.ts │ └── utils.ts ├── cli.ts ├── definitions.d.ts └── utils.ts ├── package.json └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "node-env-run", 3 | "projectOwner": "dkundel", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": false, 9 | "contributors": [ { 10 | "login": "dkundel", 11 | "name": "Dominik Kundel", 12 | "avatar_url": "https://avatars3.githubusercontent.com/u/1505101?v=4", 13 | "profile": "https://moin.world", 14 | "contributions": [ 15 | "code" 16 | ] 17 | }] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __mocks__/ 2 | dist/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "@typescript-eslint/no-var-requires": "off", 7 | "@typescript-eslint/no-empty-function": "off" 8 | }, 9 | "extends": [ 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [macos-latest, windows-latest, ubuntu-latest] 11 | node-version: [10, 12, 14] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | npm install 22 | npm run build --if-present 23 | npm run test 24 | env: 25 | CI: true 26 | NODE_ENV: test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist/ 40 | package-lock.json 41 | yarn.lock -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | __mocks__/ 3 | bin/ 4 | coverage/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message=":bookmark: Release v%s" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch via NPM", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": [ 13 | "run-script", 14 | "test" 15 | ], 16 | "port": 5858 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dominik Kundel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/node-env-run.svg?style=flat-square)](https://npmjs.com/packages/node-env-run) [![npm](https://img.shields.io/npm/dt/node-env-run.svg?style=flat-square)](https://npmjs.com/packages/node-env-run) [![npm](https://img.shields.io/npm/l/node-env-run.svg?style=flat-square)](/LICENSE) [!![Node CI](https://github.com/dkundel/node-env-run/workflows/Node%20CI/badge.svg) 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors) 3 | 4 |

5 | node-env-run logo 6 |

7 | 8 | # node-env-run 9 | 10 | > Command-line tool to read `.env` files and execute scripts/commands after loading those environment variables 11 | 12 | - Uses [`dotenv`](https://npm.im/dotenv) under the hood 13 | - Easy to configure 14 | - Flexible command to execute 15 | - Let's you override existing environment variables 16 | 17 |

node-env-run example screenshot. Code below in Documentation section

18 | 19 | ## Installation 20 | 21 | ### Install per project: 22 | 23 | I recommend installing this module as a `devDependency` for the respective project. 24 | 25 | **Install via `yarn`**: 26 | 27 | ```bash 28 | yarn add node-env-run --dev 29 | ``` 30 | 31 | **Install via `npm`**: 32 | 33 | ```bash 34 | npm install node-env-run --save-dev 35 | ``` 36 | 37 | ### Install globally: 38 | 39 | You can alternatively install the module globally if you want to: 40 | 41 | ```bash 42 | npm install node-env-run --global 43 | ``` 44 | 45 | ## Usage 46 | 47 | Add a new scripts entry to your `package.json`. Example: 48 | 49 | ```json 50 | { 51 | "scripts": { 52 | "dev": "nodenv .", 53 | "test": "nodenv -E test/.env test/test.js" 54 | } 55 | } 56 | ``` 57 | 58 | Or use it with [`npx`](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b): 59 | 60 | ```bash 61 | npx node-env-run . 62 | ``` 63 | 64 | ## Documentation 65 | 66 | This module uses under the hood the [`dotenv` module](https://www.npmjs.com/package/dotenv) to parse the `.env` file. For more information about how to structure your `.env` file, please refer to its [documentation](https://www.npmjs.com/package/dotenv#rules). 67 | 68 | ### Usage examples: 69 | 70 | **Start up the `main` file in `package.json` with the enviornment variables from `.env`:** 71 | 72 | ```bash 73 | nodenv . 74 | ``` 75 | 76 |
77 | 78 | More examples: 79 | 80 | **Start Node.js REPL with set environment variables from `.env.repl`**: 81 | 82 | ```bash 83 | nodenv -E .env.repl 84 | ``` 85 | 86 | **Run Python file with overridden environment variables**: 87 | 88 | ```bash 89 | nodenv app.py --exec python --force 90 | ``` 91 | 92 | **Run `server.js` file using [`nodemon`](https://npm.im/nodemon)**: 93 | 94 | ```bash 95 | nodenv server.js --exec nodemon 96 | ``` 97 | 98 | **Pass `--inspect` flag for debugging after `--`:** 99 | 100 | ```bash 101 | nodenv someScript -- --inspect 102 | ``` 103 | 104 |
105 | 106 | ### Arguments 107 | 108 | You can pass `node-env-run` a variety of arguments. These are the currently supported arguments: 109 | 110 | | Flag | Type | Description | 111 | | -------------------- | --------- | ------------------------------------------------------------------------------------------------------- | 112 | | `--encoding` | `string` | Lets you specify the encoding of the `.env` file. Defaults to `utf8` encoding. | 113 | | `--env` or
`-E` | `string` | Specifies the path to the `.env` file that should be read | 114 | | `--exec` or
`-e` | `string` | This lets you specify a command other than `node` to execute the script with. More in the next section. | 115 | | `--force` or `-f` | `boolean` | Flag to temporarily override existing environment variables with the ones in the `.env` file | 116 | | `--help` | `boolean` | Displays the usage/help instructions | 117 | | `--verbose` | `boolean` | Flag to enable more verbose logging | 118 | | `--version` | `boolean` | Displays the current version of the package | 119 | 120 | ### Using `node-env-run` with other executables 121 | 122 | You can use `node-env-run` with other executables. This is particularly useful if you try to combine it with things like [`babel-node`](https://www.npmjs.com/package/@babel/node) or [`ts-node`](https://npm.im/ts-node): 123 | 124 | ```bash 125 | nodenv index.ts --exec "ts-node" 126 | ``` 127 | 128 | However, you can also use it with completely unrelated executables such as python: 129 | 130 | ```bash 131 | nodenv app.py --exec python 132 | ``` 133 | 134 | ## Caveats & Limitations 135 | 136 | ### Additional Arguments 137 | 138 | If you want to pass additional flags/arguments to the script you are executing using `node-env-run`, you can use the empty `--` argument and follow it with any arguments you'd want to pass. For example: 139 | 140 | ```bash 141 | nodenv index.js --exec "ts-node" -- --log-level debug 142 | ``` 143 | 144 | `--log-level debug` will be passed to `index.js`. 145 | 146 | If you want to do the same with a REPL like node or python you'll have to specify `REPL` explictly, due to some parsing behavior of yargs. For example: 147 | 148 | ```bash 149 | nodenv REPL --exec node -- -e "console.log('hello world!')" 150 | ``` 151 | 152 | ### Using Quotes and Escaping Characters 153 | 154 | Using quotes for escaping special characters should generally work out of the box. However, there is one edge case if you are trying to use double quotes (`"`) inside and want to preserve it. In that case you'll have to double escape it due to some inner workings of Node.js. For example: 155 | 156 | ```bash 157 | nodenv REPL --exec echo -- 'A common greeting is "Hello World"' 158 | # outputs: A common greeting is Hello World 159 | 160 | nodenv REPL --exec echo -- 'A common greeting is \"Hello World\"' 161 | # outputs: A common greeting is "Hello World" 162 | ``` 163 | 164 | Similarly if you want to avoid variables to be interpolated you'll have to escape the `$` separately. For example: 165 | 166 | ```bash 167 | nodenv REPL --exec echo -- '$PATH' 168 | # outputs your actual values stored in $PATH 169 | 170 | nodenv REPL --exec echo -- '\$PATH' 171 | # outputs: $PATH 172 | ``` 173 | 174 | ## Contributors 175 | 176 | 177 | 178 | 179 | | [
Dominik Kundel](https://moin.world)
[💻](https://github.com/dkundel/node-env-run/commits?author=dkundel "Code") | 180 | | :---: | 181 | 182 | 183 | 184 | ## License 185 | 186 | MIT 187 | -------------------------------------------------------------------------------- /assets/node-env-run-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkundel/node-env-run/af3a21c0188e567595a09e405f7389dafeb1a505/assets/node-env-run-logo.png -------------------------------------------------------------------------------- /assets/node-env-run-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkundel/node-env-run/af3a21c0188e567595a09e405f7389dafeb1a505/assets/node-env-run-screenshot.png -------------------------------------------------------------------------------- /bin/node-env-run.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from 'cross-spawn'; 4 | import * as Debug from 'debug'; 5 | import { start } from 'repl'; 6 | import { init, parseArgs } from '../lib/cli'; 7 | import { escapeArguments } from '../lib/utils'; 8 | 9 | const debug = Debug('node-env-run'); 10 | 11 | /** 12 | * Spawns a new shell with the given command and inherits I/O 13 | * IMPORTANT: By default it will exit the process when child process exits 14 | * @param {string} cmd The command to execute 15 | * @param {string[]} cmdArgs An array of arguments to pass to the command 16 | * @returns {void} 17 | */ 18 | function runCommand(cmd: string, cmdArgs: string[]): void { 19 | const shell: string | boolean = process.env.SHELL || true; 20 | 21 | debug(`Execute command: "${cmd}"`); 22 | debug(`Using arguments: "%o"`, cmdArgs); 23 | debug(`Using shell: "${shell}"`); 24 | const child = spawn(cmd, cmdArgs, { 25 | shell, 26 | stdio: 'inherit', 27 | env: process.env, 28 | }); 29 | child.on('exit', (code: number) => { 30 | debug(`Child process exit with code ${code}`); 31 | process.exit(code); 32 | }); 33 | } 34 | 35 | debug(`Raw args: %o`, process.argv); 36 | const args = parseArgs(process.argv); 37 | debug(`Parsed args: %o`, args.program._); 38 | debug(`Parsed script: "${args.script}"`); 39 | const cli = init(args); 40 | if (cli.isRepl) { 41 | if (cli.node && args.program.newArguments.length === 0) { 42 | start({}); 43 | } else { 44 | const cmdArgs = escapeArguments(args.program.newArguments); 45 | runCommand(args.program.exec, cmdArgs); 46 | } 47 | } else if (cli.script !== undefined) { 48 | const cmd = args.program.exec; 49 | const cmdArgs = escapeArguments([cli.script, ...args.program.newArguments]); 50 | runCommand(cmd, cmdArgs); 51 | } else { 52 | console.error(cli.error); 53 | process.exit(1); 54 | } 55 | -------------------------------------------------------------------------------- /lib/__mocks__/utils.ts: -------------------------------------------------------------------------------- 1 | let scriptToExecute = './main.js'; 2 | 3 | export function __setScriptToExecute(script: string) { 4 | scriptToExecute = script; 5 | } 6 | 7 | export const getScriptToExecute = jest.fn(() => scriptToExecute ); 8 | 9 | export const setEnvironmentVariables = jest.fn(); -------------------------------------------------------------------------------- /lib/__tests__/run-invalid-script.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../utils'); 2 | jest.mock('./main.js', () => {}, { virtual: true }); 3 | 4 | // turn off error logs 5 | console.error = jest.fn(); 6 | 7 | import * as mockFs from 'mock-fs'; 8 | import { resolve } from 'path'; 9 | import { init, parseArgs } from '../cli'; 10 | import { getScriptToExecute, setEnvironmentVariables } from '../utils'; 11 | 12 | const __setScriptToExecute = require('../utils').__setScriptToExecute; 13 | 14 | const CMD = 'path/to/node node-env-run foo-bar-bla.js --force'.split(' '); 15 | 16 | const ENV_FILE_PATH = resolve(process.cwd(), '.env'); 17 | const FILES: { [key: string]: string } = {}; 18 | FILES[ENV_FILE_PATH] = ` 19 | TEST_STRING=hello 20 | TEST_EMPTY= 21 | TEST_NUMBER=42 22 | # TEST_COMMENT=invisible 23 | TEST_PREDEFINED=moin 24 | `; 25 | 26 | describe('test command without necessary parameters', () => { 27 | beforeAll(() => { 28 | __setScriptToExecute(null); 29 | mockFs(FILES); 30 | process.env['TEST_PREDEFINED'] = 'servus'; 31 | }); 32 | 33 | afterAll(() => { 34 | mockFs.restore(); 35 | }); 36 | 37 | test('returns null', () => { 38 | const cli = init(parseArgs(CMD)); 39 | expect(cli.isRepl).toBeFalsy(); 40 | if (cli.isRepl === false) { 41 | expect(cli?.error?.message).toBe('Failed to determine script to execute'); 42 | } 43 | }); 44 | 45 | test('has called the right functions', () => { 46 | expect(setEnvironmentVariables).toHaveBeenCalledTimes(1); 47 | expect(getScriptToExecute).toHaveBeenCalledTimes(1); 48 | expect(getScriptToExecute).toHaveBeenCalledWith( 49 | 'foo-bar-bla.js', 50 | process.cwd() 51 | ); 52 | }); 53 | 54 | afterAll(() => { 55 | (getScriptToExecute as jest.Mock).mockClear(); 56 | (setEnvironmentVariables as jest.Mock).mockClear(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /lib/__tests__/run-missing-env-file.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../utils'); 2 | jest.mock('./main.js', () => {}, { virtual: true }); 3 | 4 | // turn off error logs 5 | console.error = jest.fn(); 6 | 7 | import * as mockFs from 'mock-fs'; 8 | import { init, parseArgs } from '../cli'; 9 | import { getScriptToExecute, setEnvironmentVariables } from '../utils'; 10 | 11 | const __setScriptToExecute = require('../utils').__setScriptToExecute; 12 | 13 | const CMD = 'path/to/node node-env-run . --force'.split(' '); 14 | 15 | describe('test command with missing env file', () => { 16 | beforeAll(() => { 17 | mockFs({}); 18 | __setScriptToExecute('./main.js'); 19 | 20 | process.env['TEST_PREDEFINED'] = 'servus'; 21 | }); 22 | 23 | afterAll(() => { 24 | mockFs.restore(); 25 | }); 26 | 27 | test('returns null', () => { 28 | const cli = init(parseArgs(CMD)); 29 | expect(cli.isRepl).toBeFalsy(); 30 | if (cli.isRepl === false) { 31 | expect(cli.error).not.toBeUndefined(); 32 | expect(cli.script).toBeUndefined(); 33 | } 34 | }); 35 | 36 | test('has called the right functions', () => { 37 | expect(setEnvironmentVariables).toHaveBeenCalledTimes(0); 38 | expect(getScriptToExecute).toHaveBeenCalledTimes(0); 39 | }); 40 | 41 | afterAll(() => { 42 | (getScriptToExecute as jest.Mock).mockClear(); 43 | (setEnvironmentVariables as jest.Mock).mockClear(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/__tests__/run-with-force.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../utils'); 2 | jest.mock('./main.js', () => {}, { virtual: true }); 3 | 4 | import * as mockFs from 'mock-fs'; 5 | import { resolve } from 'path'; 6 | import { init, parseArgs } from '../cli'; 7 | import { getScriptToExecute, setEnvironmentVariables } from '../utils'; 8 | 9 | const __setScriptToExecute = require('../utils').__setScriptToExecute; 10 | 11 | const CMD = 'path/to/node node-env-run . --force'.split(' '); 12 | const ENV_FILE_PATH = resolve(process.cwd(), '.env'); 13 | const FILES: { [key: string]: string } = {}; 14 | FILES[ENV_FILE_PATH] = ` 15 | TEST_STRING=hello 16 | TEST_EMPTY= 17 | TEST_NUMBER=42 18 | # TEST_COMMENT=invisible 19 | TEST_PREDEFINED=moin 20 | `; 21 | FILES[resolve(process.cwd(), 'main.js')] = '//foo'; 22 | 23 | describe('test command in force mode', () => { 24 | beforeAll(() => { 25 | mockFs(FILES); 26 | __setScriptToExecute('./main.js'); 27 | 28 | process.env['TEST_PREDEFINED'] = 'servus'; 29 | }); 30 | 31 | afterAll(() => { 32 | mockFs.restore(); 33 | }); 34 | 35 | test('returns the right script to execute', () => { 36 | const cli = init(parseArgs(CMD)); 37 | expect(cli.isRepl).toBeFalsy(); 38 | if (cli.isRepl === false) { 39 | expect(cli.script).toBe('./main.js'); 40 | expect(cli.error).toBeUndefined(); 41 | } 42 | }); 43 | 44 | test('has called the right functions', () => { 45 | expect(setEnvironmentVariables).toHaveBeenCalledWith( 46 | { 47 | TEST_STRING: 'hello', 48 | TEST_EMPTY: '', 49 | TEST_NUMBER: '42', 50 | TEST_PREDEFINED: 'moin', 51 | }, 52 | true 53 | ); 54 | expect(getScriptToExecute).toHaveBeenCalledWith('.', process.cwd()); 55 | }); 56 | 57 | afterAll(() => { 58 | (getScriptToExecute as jest.Mock).mockClear(); 59 | (setEnvironmentVariables as jest.Mock).mockClear(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/__tests__/run-without-force.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../utils'); 2 | jest.mock('./main.js', () => {}, { virtual: true }); 3 | 4 | import * as mockFs from 'mock-fs'; 5 | import { resolve } from 'path'; 6 | import { init, parseArgs } from '../cli'; 7 | import { getScriptToExecute, setEnvironmentVariables } from '../utils'; 8 | 9 | const __setScriptToExecute = require('../utils').__setScriptToExecute; 10 | 11 | const CMD = 'path/to/node node-env-run .'.split(' '); 12 | const ENV_FILE_PATH = resolve(process.cwd(), '.env'); 13 | const FILES: { [key: string]: string } = {}; 14 | FILES[ENV_FILE_PATH] = ` 15 | TEST_STRING=hello 16 | TEST_EMPTY= 17 | TEST_NUMBER=42 18 | # TEST_COMMENT=invisible 19 | TEST_PREDEFINED=moin 20 | `; 21 | FILES['./main.js'] = '//foo'; 22 | 23 | describe('test command in force mode', () => { 24 | beforeAll(() => { 25 | mockFs(FILES); 26 | __setScriptToExecute('./main.js'); 27 | 28 | process.env['TEST_PREDEFINED'] = 'servus'; 29 | }); 30 | 31 | afterAll(() => { 32 | mockFs.restore(); 33 | }); 34 | 35 | test('returns the right script to execute', () => { 36 | const cli = init(parseArgs(CMD)); 37 | expect(cli.isRepl).toBeFalsy(); 38 | if (cli.isRepl === false) { 39 | expect(cli.script).toBe('./main.js'); 40 | expect(cli.error).toBeUndefined(); 41 | } 42 | }); 43 | 44 | test('has called the right functions', () => { 45 | expect(setEnvironmentVariables).toHaveBeenCalledWith( 46 | { 47 | TEST_STRING: 'hello', 48 | TEST_EMPTY: '', 49 | TEST_NUMBER: '42', 50 | TEST_PREDEFINED: 'moin', 51 | }, 52 | undefined 53 | ); 54 | expect(getScriptToExecute).toHaveBeenCalledWith('.', process.cwd()); 55 | }); 56 | 57 | afterAll(() => { 58 | (getScriptToExecute as jest.Mock).mockClear(); 59 | (setEnvironmentVariables as jest.Mock).mockClear(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/__tests__/run-without-script-parameter.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../utils'); 2 | jest.mock('./main.js', () => {}, { virtual: true }); 3 | 4 | // turn off error logs 5 | console.error = jest.fn(); 6 | 7 | import * as mockFs from 'mock-fs'; 8 | import { resolve } from 'path'; 9 | import { init, parseArgs } from '../cli'; 10 | import { getScriptToExecute, setEnvironmentVariables } from '../utils'; 11 | 12 | const __setScriptToExecute = require('../utils').__setScriptToExecute; 13 | 14 | const ENV_FILE_PATH = resolve(process.cwd(), '.env'); 15 | const FILES: { [key: string]: string } = {}; 16 | FILES[ENV_FILE_PATH] = '#SOMETHING'; 17 | 18 | const CMD = 'path/to/node node-env-run --force'.split(' '); 19 | 20 | describe('test command without script parameter', () => { 21 | beforeAll(() => { 22 | mockFs(FILES); 23 | __setScriptToExecute('./main.js'); 24 | 25 | process.env['TEST_PREDEFINED'] = 'servus'; 26 | }); 27 | 28 | afterAll(() => { 29 | mockFs.restore(); 30 | }); 31 | 32 | test('returns null', () => { 33 | const cli = init(parseArgs(CMD)); 34 | expect(cli.isRepl).toBeTruthy(); 35 | }); 36 | 37 | test('has called the right functions', () => { 38 | expect(setEnvironmentVariables).toHaveBeenCalledTimes(1); 39 | expect(getScriptToExecute).toHaveBeenCalledTimes(0); 40 | }); 41 | 42 | afterAll(() => { 43 | (getScriptToExecute as jest.Mock).mockClear(); 44 | (setEnvironmentVariables as jest.Mock).mockClear(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | // turn off error logs 2 | console.error = jest.fn(); 3 | 4 | import * as mockFs from 'mock-fs'; 5 | import * as upath from 'upath'; 6 | import { 7 | escapeArguments, 8 | getScriptToExecute, 9 | setEnvironmentVariables, 10 | } from '../utils'; 11 | 12 | function normalizePath(path: string | null): string | null { 13 | if (typeof path !== 'string') { 14 | return path; 15 | } 16 | 17 | let normalized = upath.toUnix(path); 18 | if (normalized.includes(':')) { 19 | normalized = normalized.substr(normalized.indexOf(':') + 1); 20 | } 21 | return normalized; 22 | } 23 | 24 | describe('test getScriptExecuted', () => { 25 | afterEach(() => { 26 | mockFs.restore(); 27 | }); 28 | 29 | test('returns null for missing package.json', () => { 30 | const returnVal = getScriptToExecute('.', '/fake/path/to/'); 31 | expect(returnVal).toBeNull(); 32 | }); 33 | 34 | test('returns null for missing main entry', () => { 35 | mockFs({ '/fake/path/to/package.json': '{}' }); 36 | const returnVal = getScriptToExecute('.', '/fake/path/to/'); 37 | expect(returnVal).toBeNull(); 38 | }); 39 | 40 | test('returns correct path derived from main', () => { 41 | mockFs({ 42 | '/fake/path/to/package.json': `{"main": "./lib/foo.js"}`, 43 | }); 44 | const returnVal = getScriptToExecute('.', '/fake/path/to/'); 45 | expect(normalizePath(returnVal)).toBe('/fake/path/to/lib/foo.js'); 46 | }); 47 | 48 | test('returns correct path manually entered', () => { 49 | mockFs({ '/fake/path/to/package.json': '{}' }); 50 | const returnVal = getScriptToExecute('../bar.js', '/fake/path/to/'); 51 | expect(normalizePath(returnVal)).toBe('/fake/path/bar.js'); 52 | }); 53 | }); 54 | 55 | describe('test setEnvironmentVariables', () => { 56 | test('overrides all values when forced', () => { 57 | const toOverride = { 58 | TEST_ONE: 'moin', 59 | TEST_TWO: 'bar', 60 | }; 61 | process.env.TEST_ONE = 'hello'; 62 | process.env.TEST_TWO = 'foo'; 63 | setEnvironmentVariables(toOverride, true); 64 | expect(process.env.TEST_ONE).toBe('moin'); 65 | expect(process.env.TEST_TWO).toBe('bar'); 66 | }); 67 | 68 | test('does not override empty values', () => { 69 | const toOverride = { 70 | TEST_ONE: 'moin', 71 | TEST_TWO: '', 72 | }; 73 | process.env.TEST_ONE = 'hello'; 74 | process.env.TEST_TWO = 'foo'; 75 | setEnvironmentVariables(toOverride, true); 76 | expect(process.env.TEST_ONE).toBe('moin'); 77 | expect(process.env.TEST_TWO).toBe('foo'); 78 | }); 79 | 80 | test('does not override without force', () => { 81 | const toOverride = { 82 | TEST_ONE: 'moin', 83 | TEST_TWO: 'bar', 84 | }; 85 | process.env.TEST_ONE = 'hello'; 86 | process.env.TEST_TWO = 'foo'; 87 | setEnvironmentVariables(toOverride, undefined); 88 | expect(process.env.TEST_ONE).toBe('hello'); 89 | expect(process.env.TEST_TWO).toBe('foo'); 90 | }); 91 | }); 92 | 93 | describe('escapeArguments', () => { 94 | test('keeps regular arguments untouched', () => { 95 | const result = escapeArguments(['--verbose', '-l', 'debug']); 96 | expect(result).toEqual(['--verbose', '-l', 'debug']); 97 | }); 98 | 99 | test('keeps regular assignments', () => { 100 | const result = escapeArguments(['--greeting=hello', '-l=debug']); 101 | expect(result).toEqual([`--greeting=hello`, `-l=debug`]); 102 | }); 103 | 104 | test('wraps arguments containing spaces', () => { 105 | const result = escapeArguments(['--greeting', 'Hey how is it going?']); 106 | expect(result).toEqual(['--greeting', `"Hey how is it going?"`]); 107 | }); 108 | 109 | test('handles single quotes', () => { 110 | const result = escapeArguments(['--greeting', `Hey what's up?`]); 111 | expect(result).toEqual(['--greeting', `"Hey what's up?"`]); 112 | }); 113 | 114 | test('does not touch existing double quotes', () => { 115 | const result = escapeArguments(['--greeting', `Hey "Dominik"!`]); 116 | expect(result).toEqual(['--greeting', `"Hey "Dominik"!"`]); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /lib/cli.ts: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags'; 2 | import * as Debug from 'debug'; 3 | import * as dotenv from 'dotenv'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import * as yargs from 'yargs'; 7 | import { 8 | EnvironmentDictionary, 9 | getScriptToExecute, 10 | setEnvironmentVariables, 11 | } from './utils'; 12 | 13 | export interface CliOptions extends yargs.Arguments { 14 | force: boolean; 15 | env: string; 16 | verbose: boolean; 17 | encoding: string; 18 | exec: string; 19 | script: string; 20 | newArguments: string[]; 21 | } 22 | 23 | export type CliArgs = { 24 | program: CliOptions; 25 | script: string | undefined; 26 | }; 27 | 28 | export type Cli = 29 | | { isRepl: true; node: boolean } 30 | | { isRepl: false; error?: Error; script?: string }; 31 | 32 | const debug = Debug('node-env-run'); 33 | const cwd = process.cwd(); 34 | 35 | const usageDescription = stripIndent` 36 | Runs the given script with set environment variables. 37 | * If no script is passed it will run the REPL instead. 38 | * If '.' is passed it will read the package.json and execute the 'main' file. 39 | 40 | Pass additional arguments to the script after the "--" 41 | `; 42 | 43 | /** 44 | * Parses a list of arguments and turns them into an object using Commander 45 | * @param {string[]} argv Array of arguments like process.argv 46 | * @returns {CliArgs} The parsed configuration 47 | */ 48 | export function parseArgs(argv: string[]): CliArgs { 49 | const result = yargs 50 | .usage('$0 [script]', usageDescription, (yargs) => { 51 | yargs.positional('script', { 52 | describe: 'the file that should be executed', 53 | type: 'string', 54 | }); 55 | yargs.example('$0 --exec "python"', 'Runs the Python REPL instead'); 56 | yargs.example( 57 | '$0 server.js --exec "nodemon"', 58 | 'Runs "nodemon server.js"' 59 | ); 60 | yargs.example( 61 | '$0 someScript.js -- --inspect', 62 | 'Run script with --inspect' 63 | ); 64 | return yargs; 65 | }) 66 | .option('force', { 67 | alias: 'f', 68 | demandOption: false, 69 | describe: 70 | 'temporarily overrides existing env variables with the ones in the .env file', 71 | }) 72 | .option('env', { 73 | alias: 'E', 74 | demandOption: false, 75 | describe: 76 | 'location of .env file relative from the current working directory', 77 | default: '.env', 78 | type: 'string', 79 | }) 80 | .option('verbose', { 81 | demandOption: false, 82 | describe: 'enable verbose logging', 83 | type: 'boolean', 84 | }) 85 | .option('encoding', { 86 | demandOption: false, 87 | describe: 'encoding of the .env file', 88 | default: 'utf8', 89 | type: 'string', 90 | }) 91 | .option('exec', { 92 | alias: 'e', 93 | demandOption: false, 94 | describe: 'the command to execute the script with', 95 | default: 'node', 96 | }) 97 | .showHelpOnFail(true) 98 | .help('help') 99 | .strict() 100 | .version() 101 | .parse(argv.slice(2)) as CliOptions; 102 | 103 | const script: string | undefined = result.script; 104 | result.newArguments = result._; 105 | 106 | debug('Yargs Result %o', result); 107 | 108 | return { program: result, script }; 109 | } 110 | 111 | /** 112 | * Reads .env file, sets the environment variables and dtermines the script to execute 113 | * @param {CliArgs} args The arguments as parsed by parseArgs 114 | * @returns {Cli} An object specifying if it should execute the REPL or execute a script 115 | */ 116 | export function init(args: CliArgs): Cli { 117 | const { program, script } = args; 118 | const envFilePath = path.resolve(cwd, program.env); 119 | if (!fs.existsSync(envFilePath)) { 120 | const error = new Error( 121 | `Could not find the .env file under: "${envFilePath}"` 122 | ); 123 | return { isRepl: false, error }; 124 | } 125 | 126 | debug('Reading .env file'); 127 | const envContent = fs.readFileSync( 128 | path.resolve(cwd, program.env), 129 | program.encoding 130 | ); 131 | const envValues: EnvironmentDictionary = dotenv.parse(envContent); 132 | 133 | setEnvironmentVariables(envValues, program.force); 134 | 135 | if (!script || script === 'REPL') { 136 | const node = args.program.exec === undefined; 137 | return { isRepl: true, node }; 138 | } 139 | 140 | const scriptToExecute = getScriptToExecute(script, cwd); 141 | if (scriptToExecute === null || !fs.existsSync(scriptToExecute)) { 142 | const error = new Error('Failed to determine script to execute'); 143 | return { isRepl: false, error }; 144 | } 145 | 146 | return { isRepl: false, script: scriptToExecute }; 147 | } 148 | -------------------------------------------------------------------------------- /lib/definitions.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module 'pkginfo' { 3 | namespace pkginfo { 4 | export interface Info { 5 | version: string; 6 | } 7 | } 8 | 9 | function pkginfo(module?: NodeModule): pkginfo.Info; 10 | function pkginfo(module: NodeModule, ...args: string[]): pkginfo.Info; 11 | 12 | export = pkginfo; 13 | } 14 | /* eslint-enable */ 15 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Debug from 'debug'; 2 | import * as fs from 'fs'; 3 | import { resolve } from 'path'; 4 | 5 | export type EnvironmentDictionary = { 6 | [key: string]: string | undefined; 7 | }; 8 | 9 | const debug = Debug('node-env-run'); 10 | 11 | /** 12 | * Determines the full path of the script to execute. 13 | * If the script path is "." it will read the package.json to determine the path 14 | * @param {string} script A relative path to the script to execute 15 | * @param {string} cwd The current working directory 16 | * @returns {string | null} A full path to the script or null if it could not be determined 17 | */ 18 | export function getScriptToExecute(script: string, cwd: string): string | null { 19 | if (script === '.') { 20 | debug('Evalute package.json to determine script to execute.'); 21 | const pathToPkg = resolve(cwd, 'package.json'); 22 | if (!fs.existsSync(pathToPkg)) { 23 | debug('could not find package.json'); 24 | return null; 25 | } 26 | 27 | const pkg = JSON.parse(fs.readFileSync(pathToPkg, 'utf8')); 28 | 29 | if (!pkg.main) { 30 | console.error('Could not find a "main" entry in the package.json'); 31 | return null; 32 | } 33 | 34 | script = resolve(cwd, pkg.main); 35 | } else { 36 | script = resolve(cwd, script); 37 | } 38 | 39 | return script; 40 | } 41 | 42 | /** 43 | * Sets the values passed as environment variables if they don't exist already 44 | * In force mode it will override existing ones 45 | * @param {EnvironmentDictionary} readValues A dictionary of values to be set 46 | * @param {boolean} force Forces the override of existing variables 47 | * @returns {void} 48 | */ 49 | export function setEnvironmentVariables( 50 | readValues: EnvironmentDictionary, 51 | force = false 52 | ): void { 53 | if (force) { 54 | debug('Force overriding enabled'); 55 | } 56 | 57 | const envKeysToSet = Object.keys(readValues).filter((key) => { 58 | if (force && typeof readValues[key] !== 'undefined') { 59 | const val = readValues[key]; 60 | if (typeof val === 'string' && val.length === 0) { 61 | debug(`Not overriding ${key}`); 62 | return false; 63 | } 64 | 65 | debug(`Overriding ${key}`); 66 | return true; 67 | } 68 | 69 | return !process.env[key]; 70 | }); 71 | 72 | envKeysToSet.forEach((key) => { 73 | process.env[key] = readValues[key]; 74 | }); 75 | 76 | debug( 77 | `Set the env variables: ${envKeysToSet.map((k) => `"${k}"`).join(',')}` 78 | ); 79 | } 80 | 81 | /** 82 | * Constructs the new argv to override process.argv to simulate the script being executed directly 83 | * @param {string[]} currentArgv The current list of argv (like process.argv) 84 | * @param {string} script The path to the script that should be executed 85 | * @param {string} newArguments The arguments that are passed to the script 86 | * @returns {string[]} The new value to be set as process.argv 87 | */ 88 | export function constructNewArgv( 89 | currentArgv: string[], 90 | script: string, 91 | newArguments: string 92 | ): string[] { 93 | const [node] = currentArgv; 94 | return [node, script, ...newArguments.split(' ')]; 95 | } 96 | 97 | const REGULAR_SHELL_CHARACTERS = ['a-z', 'A-Z', '1-9', '-', '_', '/', ':', '=']; 98 | const REGEX_NOT_REGULAR_CHARACTER = new RegExp( 99 | `[^${REGULAR_SHELL_CHARACTERS.join('')}]` 100 | ); 101 | 102 | /** 103 | * Wraps arguments that contain characters that need to be escaped into quotes 104 | * That way they will be passed correctly to the spawn command 105 | * 106 | * @param args list of arguments to pass to spawn 107 | */ 108 | export function escapeArguments(args: string[]): string[] { 109 | const escapedArguments = args.map((arg) => { 110 | if (arg.match(REGEX_NOT_REGULAR_CHARACTER)) { 111 | return `"${arg}"`; 112 | } 113 | return arg; 114 | }); 115 | return escapedArguments; 116 | } 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-env-run", 3 | "version": "4.0.1", 4 | "description": "Wrapper executable to load env variables from .env and run Node", 5 | "repository": { 6 | "url": "git@github.com:dkundel/node-env-run.git", 7 | "type": "git" 8 | }, 9 | "keywords": [ 10 | "dotenv", 11 | "environment", 12 | "variables", 13 | "env", 14 | ".env", 15 | "config", 16 | "settings" 17 | ], 18 | "author": "Dominik Kundel ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@types/common-tags": "^1.4.0", 22 | "@types/debug": "^4.1.5", 23 | "@types/dotenv": "^8.2.0", 24 | "@types/node": "^10.17.28", 25 | "@types/yargs": "^15.0.5", 26 | "common-tags": "^1.7.2", 27 | "cross-spawn": "^7.0.3", 28 | "debug": "^4.1.1", 29 | "dotenv": "^8.2.0", 30 | "pkginfo": "^0.4.1", 31 | "upath": "^1.2.0", 32 | "yargs": "^15.4.1" 33 | }, 34 | "bin": { 35 | "nodenv": "./dist/bin/node-env-run.js", 36 | "node-env-run": "./dist/bin/node-env-run.js" 37 | }, 38 | "types": "./dist/bin/node-env-run.d.ts", 39 | "scripts": { 40 | "contrib:generate": "all-contributors generate", 41 | "contrib:add": "all-contributors add", 42 | "prepublish": "npm run tsc", 43 | "tsc": "tsc", 44 | "build": "npm run tsc", 45 | "lint": "eslint \"!(node_modules)/**/*.ts\"", 46 | "pretest": "npm run lint", 47 | "test": "jest" 48 | }, 49 | "devDependencies": { 50 | "@types/cross-spawn": "^6.0.2", 51 | "@types/jest": "^26.0.7", 52 | "@types/mock-fs": "^4.10.0", 53 | "@typescript-eslint/eslint-plugin": "^3.7.1", 54 | "@typescript-eslint/parser": "^3.7.1", 55 | "all-contributors-cli": "^6.17.0", 56 | "eslint": "^7.5.0", 57 | "eslint-config-prettier": "^6.11.0", 58 | "eslint-plugin-prettier": "^3.1.4", 59 | "jest": "^26.1.0", 60 | "mock-fs": "^4.12.0", 61 | "npm-run-all": "^4.0.2", 62 | "prettier": "^2.0.5", 63 | "ts-jest": "^26.1.4", 64 | "typescript": "^3.9.7" 65 | }, 66 | "jest": { 67 | "preset": "ts-jest", 68 | "testPathIgnorePatterns": [ 69 | "/dist/", 70 | "/node_modules/" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | 5 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation: */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist" /* Redirect output structure to the directory. */, 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | "removeComments": true /* Do not emit comments to output. */, 17 | // "noEmit": true, /* Do not emit outputs. */ 18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 20 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 21 | 22 | /* Strict Type-Checking Options */ 23 | 24 | "strict": true /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | }, 56 | "include": ["lib/**/*", "bin/**/*"], 57 | "exclude": [ 58 | "node_modules", 59 | "dist", 60 | "lib/__mocks__/**/*", 61 | "lib/__tests__/**/*" 62 | ] 63 | } 64 | --------------------------------------------------------------------------------