├── .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 | [](https://npmjs.com/packages/node-env-run) [](https://npmjs.com/packages/node-env-run) [](/LICENSE) [!
2 | [](#contributors)
3 |
4 |
5 |
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 | 
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 |
--------------------------------------------------------------------------------