mirror of
https://github.com/toddbluhm/env-cmd.git
synced 2025-12-08 18:23:33 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
6f0d50b83c
67
.github/dependabot.yml
vendored
Normal file
67
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "11:00"
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
versions:
|
||||
- ">= 13.a, < 14"
|
||||
- dependency-name: "@types/node"
|
||||
versions:
|
||||
- 14.14.22
|
||||
- 14.14.25
|
||||
- 14.14.27
|
||||
- 14.14.28
|
||||
- 14.14.30
|
||||
- 14.14.31
|
||||
- 14.14.32
|
||||
- 14.14.33
|
||||
- 14.14.34
|
||||
- 14.14.35
|
||||
- 14.14.36
|
||||
- 14.14.37
|
||||
- 14.14.39
|
||||
- 14.14.41
|
||||
- 15.0.0
|
||||
- dependency-name: husky
|
||||
versions:
|
||||
- 5.0.9
|
||||
- 5.1.0
|
||||
- 5.1.1
|
||||
- 5.1.2
|
||||
- 5.1.3
|
||||
- 5.2.0
|
||||
- dependency-name: mocha
|
||||
versions:
|
||||
- 8.2.1
|
||||
- 8.3.0
|
||||
- 8.3.1
|
||||
- dependency-name: typescript
|
||||
versions:
|
||||
- 4.1.3
|
||||
- 4.1.4
|
||||
- 4.1.5
|
||||
- 4.2.2
|
||||
- 4.2.3
|
||||
- dependency-name: "@commitlint/cli"
|
||||
versions:
|
||||
- 11.0.0
|
||||
- 12.0.0
|
||||
- 12.0.1
|
||||
- dependency-name: "@commitlint/config-conventional"
|
||||
versions:
|
||||
- 11.0.0
|
||||
- 12.0.0
|
||||
- 12.0.1
|
||||
- dependency-name: "@types/mocha"
|
||||
versions:
|
||||
- 8.2.0
|
||||
- 8.2.1
|
||||
- dependency-name: commander
|
||||
versions:
|
||||
- 7.0.0
|
||||
- 7.1.0
|
||||
28
.github/workflows/linux-tests.yml
vendored
28
.github/workflows/linux-tests.yml
vendored
@ -3,17 +3,19 @@ name: linux tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8.x, 10.x, 12.x]
|
||||
node-version: [18.x, 20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -23,7 +25,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
@ -33,20 +35,24 @@ jobs:
|
||||
- name: Lint Files
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Tests and Converage
|
||||
- name: Run Tests and Coverage
|
||||
env:
|
||||
CI: true
|
||||
run: npm run test-cover
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@master
|
||||
- name: Send Coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel: true
|
||||
path-to-lcov: ./coverage/lcov.info
|
||||
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
finish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close Coveralls Parallel Build
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
||||
27
.github/workflows/windows-tests.yml
vendored
27
.github/workflows/windows-tests.yml
vendored
@ -3,22 +3,25 @@ name: windows tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8.x, 10.x, 12.x]
|
||||
node-version: [18.x, 20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
@ -28,20 +31,24 @@ jobs:
|
||||
- name: Lint Files
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Tests and Converage
|
||||
- name: Run Tests and Coverage
|
||||
env:
|
||||
CI: true
|
||||
run: npm run test-cover
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@master
|
||||
- name: Send Coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel: true
|
||||
path-to-lcov: ./coverage/lcov.info
|
||||
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
finish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close Coveralls Parallel Build
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@ -0,0 +1 @@
|
||||
npx commitlint --edit
|
||||
8
.mocharc.json
Normal file
8
.mocharc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/mocharc.json",
|
||||
"require": ["tsx/esm", "esmock"],
|
||||
"extensions": ["ts"],
|
||||
"spec": [
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Todd Bluhm
|
||||
Copyright (c) Todd Bluhm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@ -4,8 +4,7 @@
|
||||
[](https://www.npmjs.com/package/env-cmd)
|
||||
[](https://www.npmjs.com/package/env-cmd)
|
||||
[](https://github.com/toddbluhm/env-cmd/blob/master/LICENSE)
|
||||
[](https://github.com/toddbluhm/ts-standard)
|
||||
[](https://dependabot.com/)
|
||||
[](https://github.com/typescript-eslint/typescript-eslint)
|
||||
|
||||
# env-cmd
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
#! /usr/bin/env node
|
||||
require('../dist').CLI(process.argv.slice(2))
|
||||
import { CLI } from '../dist/index.js'
|
||||
CLI(process.argv.slice(2))
|
||||
|
||||
8
dist/cli.d.ts
vendored
Normal file
8
dist/cli.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import type { Environment } from './types.ts';
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<Environment>}
|
||||
*/
|
||||
export declare function CLI(args: string[]): Promise<Environment>;
|
||||
21
dist/cli.js
vendored
Normal file
21
dist/cli.js
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
import * as processLib from 'node:process';
|
||||
import { EnvCmd } from './env-cmd.js';
|
||||
import { parseArgs } from './parse-args.js';
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<Environment>}
|
||||
*/
|
||||
export async function CLI(args) {
|
||||
// Parse the args from the command line
|
||||
const parsedArgs = parseArgs(args);
|
||||
// Run EnvCmd
|
||||
try {
|
||||
return await EnvCmd(parsedArgs);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
return processLib.exit(1);
|
||||
}
|
||||
}
|
||||
17
dist/env-cmd.d.ts
vendored
17
dist/env-cmd.d.ts
vendored
@ -1,21 +1,10 @@
|
||||
import { EnvCmdOptions } from './types';
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<{ [key: string]: any }>}
|
||||
*/
|
||||
export declare function CLI(args: string[]): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
import type { EnvCmdOptions, Environment } from './types.ts';
|
||||
/**
|
||||
* The main env-cmd program. This will spawn a new process and run the given command using
|
||||
* various environment file solutions.
|
||||
*
|
||||
* @export
|
||||
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
|
||||
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value
|
||||
* @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
|
||||
*/
|
||||
export declare function EnvCmd({ command, commandArgs, envFile, rc, options }: EnvCmdOptions): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
export declare function EnvCmd({ command, commandArgs, envFile, rc, options, }: EnvCmdOptions): Promise<Environment>;
|
||||
|
||||
55
dist/env-cmd.js
vendored
55
dist/env-cmd.js
vendored
@ -1,70 +1,47 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const spawn_1 = require("./spawn");
|
||||
const signal_termination_1 = require("./signal-termination");
|
||||
const parse_args_1 = require("./parse-args");
|
||||
const get_env_vars_1 = require("./get-env-vars");
|
||||
const expand_envs_1 = require("./expand-envs");
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<{ [key: string]: any }>}
|
||||
*/
|
||||
async function CLI(args) {
|
||||
// Parse the args from the command line
|
||||
const parsedArgs = parse_args_1.parseArgs(args);
|
||||
// Run EnvCmd
|
||||
try {
|
||||
return await exports.EnvCmd(parsedArgs);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
return process.exit(1);
|
||||
}
|
||||
}
|
||||
exports.CLI = CLI;
|
||||
import { default as spawn } from 'cross-spawn';
|
||||
import { TermSignals } from './signal-termination.js';
|
||||
import { getEnvVars } from './get-env-vars.js';
|
||||
import { expandEnvs } from './expand-envs.js';
|
||||
import * as processLib from 'node:process';
|
||||
/**
|
||||
* The main env-cmd program. This will spawn a new process and run the given command using
|
||||
* various environment file solutions.
|
||||
*
|
||||
* @export
|
||||
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
|
||||
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value
|
||||
* @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
|
||||
*/
|
||||
async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) {
|
||||
var _a;
|
||||
export async function EnvCmd({ command, commandArgs, envFile, rc, options = {}, }) {
|
||||
let env = {};
|
||||
try {
|
||||
env = await get_env_vars_1.getEnvVars({ envFile, rc, verbose: options.verbose });
|
||||
env = await getEnvVars({ envFile, rc, verbose: options.verbose });
|
||||
}
|
||||
catch (e) {
|
||||
if (!((_a = options.silent) !== null && _a !== void 0 ? _a : false)) {
|
||||
if (!(options.silent ?? false)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Override the merge order if --no-override flag set
|
||||
if (options.noOverride === true) {
|
||||
env = Object.assign({}, env, process.env);
|
||||
env = Object.assign({}, env, processLib.env);
|
||||
}
|
||||
else {
|
||||
// Add in the system environment variables to our environment list
|
||||
env = Object.assign({}, process.env, env);
|
||||
env = Object.assign({}, processLib.env, env);
|
||||
}
|
||||
if (options.expandEnvs === true) {
|
||||
command = expand_envs_1.expandEnvs(command, env);
|
||||
commandArgs = commandArgs.map(arg => expand_envs_1.expandEnvs(arg, env));
|
||||
command = expandEnvs(command, env);
|
||||
commandArgs = commandArgs.map(arg => expandEnvs(arg, env));
|
||||
}
|
||||
// Execute the command with the given environment variables
|
||||
const proc = spawn_1.spawn(command, commandArgs, {
|
||||
const proc = spawn(command, commandArgs, {
|
||||
stdio: 'inherit',
|
||||
shell: options.useShell,
|
||||
env
|
||||
env: env,
|
||||
});
|
||||
// Handle any termination signals for parent and child proceses
|
||||
const signals = new signal_termination_1.TermSignals({ verbose: options.verbose });
|
||||
const signals = new TermSignals({ verbose: options.verbose });
|
||||
signals.handleUncaughtExceptions();
|
||||
signals.handleTermSignals(proc);
|
||||
return env;
|
||||
}
|
||||
exports.EnvCmd = EnvCmd;
|
||||
|
||||
7
dist/expand-envs.d.ts
vendored
7
dist/expand-envs.d.ts
vendored
@ -1,7 +1,6 @@
|
||||
import type { Environment } from './types.ts';
|
||||
/**
|
||||
* expandEnvs Replaces $var in args and command with environment variables
|
||||
* the environment variable doesn't exist, it leaves it as is.
|
||||
* if the environment variable doesn't exist, it leaves it as is.
|
||||
*/
|
||||
export declare function expandEnvs(str: string, envs: {
|
||||
[key: string]: any;
|
||||
}): string;
|
||||
export declare function expandEnvs(str: string, envs: Environment): string;
|
||||
|
||||
12
dist/expand-envs.js
vendored
12
dist/expand-envs.js
vendored
@ -1,13 +1,11 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* expandEnvs Replaces $var in args and command with environment variables
|
||||
* the environment variable doesn't exist, it leaves it as is.
|
||||
* if the environment variable doesn't exist, it leaves it as is.
|
||||
*/
|
||||
function expandEnvs(str, envs) {
|
||||
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => {
|
||||
export function expandEnvs(str, envs) {
|
||||
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, (varName) => {
|
||||
const varValue = envs[varName.slice(1)];
|
||||
return varValue === undefined ? varName : varValue;
|
||||
// const test = 42;
|
||||
return varValue === undefined ? varName : varValue.toString();
|
||||
});
|
||||
}
|
||||
exports.expandEnvs = expandEnvs;
|
||||
|
||||
14
dist/get-env-vars.d.ts
vendored
14
dist/get-env-vars.d.ts
vendored
@ -1,18 +1,12 @@
|
||||
import { GetEnvVarOptions } from './types';
|
||||
export declare function getEnvVars(options?: GetEnvVarOptions): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
import type { GetEnvVarOptions, Environment } from './types.ts';
|
||||
export declare function getEnvVars(options?: GetEnvVarOptions): Promise<Environment>;
|
||||
export declare function getEnvFile({ filePath, fallback, verbose }: {
|
||||
filePath?: string;
|
||||
fallback?: boolean;
|
||||
verbose?: boolean;
|
||||
}): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}): Promise<Environment>;
|
||||
export declare function getRCFile({ environments, filePath, verbose }: {
|
||||
environments: string[];
|
||||
filePath?: string;
|
||||
verbose?: boolean;
|
||||
}): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}): Promise<Environment>;
|
||||
|
||||
46
dist/get-env-vars.js
vendored
46
dist/get-env-vars.js
vendored
@ -1,40 +1,38 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const parse_rc_file_1 = require("./parse-rc-file");
|
||||
const parse_env_file_1 = require("./parse-env-file");
|
||||
import { getRCFileVars } from './parse-rc-file.js';
|
||||
import { getEnvFileVars } from './parse-env-file.js';
|
||||
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'];
|
||||
const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'];
|
||||
async function getEnvVars(options = {}) {
|
||||
options.envFile = options.envFile !== undefined ? options.envFile : {};
|
||||
export async function getEnvVars(options = {}) {
|
||||
options.envFile = options.envFile ?? {};
|
||||
// Check for rc file usage
|
||||
if (options.rc !== undefined) {
|
||||
return await getRCFile({
|
||||
environments: options.rc.environments,
|
||||
filePath: options.rc.filePath,
|
||||
verbose: options.verbose
|
||||
verbose: options.verbose,
|
||||
});
|
||||
}
|
||||
return await getEnvFile({
|
||||
filePath: options.envFile.filePath,
|
||||
fallback: options.envFile.fallback,
|
||||
verbose: options.verbose
|
||||
verbose: options.verbose,
|
||||
});
|
||||
}
|
||||
exports.getEnvVars = getEnvVars;
|
||||
async function getEnvFile({ filePath, fallback, verbose }) {
|
||||
export async function getEnvFile({ filePath, fallback, verbose }) {
|
||||
// Use env file
|
||||
if (filePath !== undefined) {
|
||||
try {
|
||||
const env = await parse_env_file_1.getEnvFileVars(filePath);
|
||||
const env = await getEnvFileVars(filePath);
|
||||
if (verbose === true) {
|
||||
console.info(`Found .env file at path: ${filePath}`);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
if (verbose === true) {
|
||||
console.info(`Failed to find .env file at path: ${filePath}`);
|
||||
}
|
||||
// Ignore error as we are just trying this location
|
||||
}
|
||||
if (fallback !== true) {
|
||||
throw new Error(`Failed to find .env file at path: ${filePath}`);
|
||||
@ -43,13 +41,15 @@ async function getEnvFile({ filePath, fallback, verbose }) {
|
||||
// Use the default env file locations
|
||||
for (const path of ENV_FILE_DEFAULT_LOCATIONS) {
|
||||
try {
|
||||
const env = await parse_env_file_1.getEnvFileVars(path);
|
||||
const env = await getEnvFileVars(path);
|
||||
if (verbose === true) {
|
||||
console.info(`Found .env file at default path: ${path}`);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
catch (e) { }
|
||||
catch {
|
||||
// Ignore error because we are just trying this location
|
||||
}
|
||||
}
|
||||
const error = `Failed to find .env file at default paths: [${ENV_FILE_DEFAULT_LOCATIONS.join(',')}]`;
|
||||
if (verbose === true) {
|
||||
@ -57,18 +57,18 @@ async function getEnvFile({ filePath, fallback, verbose }) {
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
exports.getEnvFile = getEnvFile;
|
||||
async function getRCFile({ environments, filePath, verbose }) {
|
||||
export async function getRCFile({ environments, filePath, verbose }) {
|
||||
// User provided an .rc file path
|
||||
if (filePath !== undefined) {
|
||||
try {
|
||||
const env = await parse_rc_file_1.getRCFileVars({ environments, filePath });
|
||||
const env = await getRCFileVars({ environments, filePath });
|
||||
if (verbose === true) {
|
||||
console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.name === 'PathError') {
|
||||
if (verbose === true) {
|
||||
console.info(`Failed to find .rc file at path: ${filePath}`);
|
||||
@ -79,19 +79,21 @@ async function getRCFile({ environments, filePath, verbose }) {
|
||||
console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Use the default .rc file locations
|
||||
for (const path of RC_FILE_DEFAULT_LOCATIONS) {
|
||||
try {
|
||||
const env = await parse_rc_file_1.getRCFileVars({ environments, filePath: path });
|
||||
const env = await getRCFileVars({ environments, filePath: path });
|
||||
if (verbose === true) {
|
||||
console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.name === 'EnvironmentError') {
|
||||
const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`;
|
||||
if (verbose === true) {
|
||||
@ -99,6 +101,13 @@ async function getRCFile({ environments, filePath, verbose }) {
|
||||
}
|
||||
throw new Error(errorText);
|
||||
}
|
||||
if (e.name === 'ParseError') {
|
||||
if (verbose === true) {
|
||||
console.info(e.message);
|
||||
}
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const errorText = `Failed to find .rc file at default paths: [${RC_FILE_DEFAULT_LOCATIONS.join(',')}]`;
|
||||
@ -107,4 +116,3 @@ async function getRCFile({ environments, filePath, verbose }) {
|
||||
}
|
||||
throw new Error(errorText);
|
||||
}
|
||||
exports.getRCFile = getRCFile;
|
||||
|
||||
7
dist/index.d.ts
vendored
7
dist/index.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import { getEnvVars } from './get-env-vars';
|
||||
export * from './types';
|
||||
export * from './env-cmd';
|
||||
import { getEnvVars } from './get-env-vars.js';
|
||||
export * from './types.js';
|
||||
export * from './cli.js';
|
||||
export * from './env-cmd.js';
|
||||
export declare const GetEnvVars: typeof getEnvVars;
|
||||
|
||||
14
dist/index.js
vendored
14
dist/index.js
vendored
@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const get_env_vars_1 = require("./get-env-vars");
|
||||
__export(require("./env-cmd"));
|
||||
exports.GetEnvVars = get_env_vars_1.getEnvVars;
|
||||
import { getEnvVars } from './get-env-vars.js';
|
||||
// Export the core env-cmd API
|
||||
export * from './types.js';
|
||||
export * from './cli.js';
|
||||
export * from './env-cmd.js';
|
||||
export const GetEnvVars = getEnvVars;
|
||||
|
||||
5
dist/parse-args.d.ts
vendored
5
dist/parse-args.d.ts
vendored
@ -1,7 +1,6 @@
|
||||
import * as commander from 'commander';
|
||||
import { EnvCmdOptions } from './types';
|
||||
import type { EnvCmdOptions, CommanderOptions } from './types.ts';
|
||||
/**
|
||||
* Parses the arguments passed into the cli
|
||||
*/
|
||||
export declare function parseArgs(args: string[]): EnvCmdOptions;
|
||||
export declare function parseArgsUsingCommander(args: string[]): commander.Command;
|
||||
export declare function parseArgsUsingCommander(args: string[]): CommanderOptions;
|
||||
|
||||
29
dist/parse-args.js
vendored
29
dist/parse-args.js
vendored
@ -1,13 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const commander = require("commander");
|
||||
const utils_1 = require("./utils");
|
||||
// Use commonjs require to prevent a weird folder hierarchy in dist
|
||||
const packageJson = require('../package.json'); /* eslint-disable-line */
|
||||
import * as commander from 'commander';
|
||||
import { parseArgList } from './utils.js';
|
||||
import { default as packageJson } from '../package.json' with { type: 'json' };
|
||||
/**
|
||||
* Parses the arguments passed into the cli
|
||||
*/
|
||||
function parseArgs(args) {
|
||||
export function parseArgs(args) {
|
||||
// Run the initial arguments through commander in order to determine
|
||||
// which value in the args array is the `command` to execute
|
||||
let program = parseArgsUsingCommander(args);
|
||||
@ -39,17 +36,19 @@ function parseArgs(args) {
|
||||
silent = true;
|
||||
}
|
||||
let rc;
|
||||
if (program.environments !== undefined && program.environments.length !== 0) {
|
||||
if (program.environments !== undefined
|
||||
&& Array.isArray(program.environments)
|
||||
&& program.environments.length !== 0) {
|
||||
rc = {
|
||||
environments: program.environments,
|
||||
filePath: program.rcFile
|
||||
filePath: program.rcFile,
|
||||
};
|
||||
}
|
||||
let envFile;
|
||||
if (program.file !== undefined) {
|
||||
envFile = {
|
||||
filePath: program.file,
|
||||
fallback: program.fallback
|
||||
fallback: program.fallback,
|
||||
};
|
||||
}
|
||||
const options = {
|
||||
@ -62,21 +61,20 @@ function parseArgs(args) {
|
||||
noOverride,
|
||||
silent,
|
||||
useShell,
|
||||
verbose
|
||||
}
|
||||
verbose,
|
||||
},
|
||||
};
|
||||
if (verbose) {
|
||||
console.info(`Options: ${JSON.stringify(options, null, 0)}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
exports.parseArgs = parseArgs;
|
||||
function parseArgsUsingCommander(args) {
|
||||
export function parseArgsUsingCommander(args) {
|
||||
const program = new commander.Command();
|
||||
return program
|
||||
.version(packageJson.version, '-v, --version')
|
||||
.usage('[options] <command> [...args]')
|
||||
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', utils_1.parseArgList)
|
||||
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList)
|
||||
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
|
||||
.option('--fallback', 'Fallback to default env file path, if custom env file path not found')
|
||||
.option('--no-override', 'Do not override existing environment variables')
|
||||
@ -88,4 +86,3 @@ function parseArgsUsingCommander(args) {
|
||||
.allowUnknownOption(true)
|
||||
.parse(['_', '_', ...args]);
|
||||
}
|
||||
exports.parseArgsUsingCommander = parseArgsUsingCommander;
|
||||
|
||||
13
dist/parse-env-file.d.ts
vendored
13
dist/parse-env-file.d.ts
vendored
@ -1,21 +1,16 @@
|
||||
import type { Environment } from './types.ts';
|
||||
/**
|
||||
* Gets the environment vars from an env file
|
||||
*/
|
||||
export declare function getEnvFileVars(envFilePath: string): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
export declare function getEnvFileVars(envFilePath: string): Promise<Environment>;
|
||||
/**
|
||||
* Parse out all env vars from a given env file string and return an object
|
||||
*/
|
||||
export declare function parseEnvString(envFileString: string): {
|
||||
[key: string]: string;
|
||||
};
|
||||
export declare function parseEnvString(envFileString: string): Environment;
|
||||
/**
|
||||
* Parse out all env vars from an env file string
|
||||
*/
|
||||
export declare function parseEnvVars(envString: string): {
|
||||
[key: string]: string;
|
||||
};
|
||||
export declare function parseEnvVars(envString: string): Environment;
|
||||
/**
|
||||
* Strips out comments from env file string
|
||||
*/
|
||||
|
||||
72
dist/parse-env-file.js
vendored
72
dist/parse-env-file.js
vendored
@ -1,37 +1,48 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const utils_1 = require("./utils");
|
||||
const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs'];
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { extname } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
|
||||
/**
|
||||
* Gets the environment vars from an env file
|
||||
*/
|
||||
async function getEnvFileVars(envFilePath) {
|
||||
const absolutePath = utils_1.resolveEnvFilePath(envFilePath);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
export async function getEnvFileVars(envFilePath) {
|
||||
const absolutePath = resolveEnvFilePath(envFilePath);
|
||||
if (!existsSync(absolutePath)) {
|
||||
const pathError = new Error(`Invalid env file path (${envFilePath}).`);
|
||||
pathError.name = 'PathError';
|
||||
throw pathError;
|
||||
}
|
||||
// Get the file extension
|
||||
const ext = path.extname(absolutePath).toLowerCase();
|
||||
const ext = extname(absolutePath).toLowerCase();
|
||||
let env = {};
|
||||
if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) {
|
||||
const possiblePromise = require(absolutePath); /* eslint-disable-line */
|
||||
env = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise;
|
||||
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
||||
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
||||
let attributeTypes = {};
|
||||
if (ext === '.json') {
|
||||
attributeTypes = { with: { type: 'json' } };
|
||||
}
|
||||
const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
|
||||
if ('default' in res) {
|
||||
env = res.default;
|
||||
}
|
||||
else {
|
||||
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' });
|
||||
env = res;
|
||||
}
|
||||
// Check to see if the imported value is a promise
|
||||
if (isPromise(env)) {
|
||||
env = await env;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const file = readFileSync(absolutePath, { encoding: 'utf8' });
|
||||
env = parseEnvString(file);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
exports.getEnvFileVars = getEnvFileVars;
|
||||
/**
|
||||
* Parse out all env vars from a given env file string and return an object
|
||||
*/
|
||||
function parseEnvString(envFileString) {
|
||||
export function parseEnvString(envFileString) {
|
||||
// First thing we do is stripe out all comments
|
||||
envFileString = stripComments(envFileString.toString());
|
||||
// Next we stripe out all the empty lines
|
||||
@ -39,30 +50,41 @@ function parseEnvString(envFileString) {
|
||||
// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
|
||||
return parseEnvVars(envFileString);
|
||||
}
|
||||
exports.parseEnvString = parseEnvString;
|
||||
/**
|
||||
* Parse out all env vars from an env file string
|
||||
*/
|
||||
function parseEnvVars(envString) {
|
||||
export function parseEnvVars(envString) {
|
||||
const envParseRegex = /^((.+?)[=](.*))$/gim;
|
||||
const matches = {};
|
||||
let match;
|
||||
while ((match = envParseRegex.exec(envString)) !== null) {
|
||||
// Note: match[1] is the full env=var line
|
||||
const key = match[2].trim();
|
||||
const value = match[3].trim();
|
||||
let value = match[3].trim();
|
||||
// remove any surrounding quotes
|
||||
matches[key] = value
|
||||
value = value
|
||||
.replace(/(^['"]|['"]$)/g, '')
|
||||
.replace(/\\n/g, '\n');
|
||||
// Convert string to JS type if appropriate
|
||||
if (value !== '' && !isNaN(+value)) {
|
||||
matches[key] = +value;
|
||||
}
|
||||
return matches;
|
||||
else if (value === 'true') {
|
||||
matches[key] = true;
|
||||
}
|
||||
else if (value === 'false') {
|
||||
matches[key] = false;
|
||||
}
|
||||
else {
|
||||
matches[key] = value;
|
||||
}
|
||||
}
|
||||
return JSON.parse(JSON.stringify(matches));
|
||||
}
|
||||
exports.parseEnvVars = parseEnvVars;
|
||||
/**
|
||||
* Strips out comments from env file string
|
||||
*/
|
||||
function stripComments(envString) {
|
||||
export function stripComments(envString) {
|
||||
const commentsRegex = /(^#.*$)/gim;
|
||||
let match = commentsRegex.exec(envString);
|
||||
let newString = envString;
|
||||
@ -72,12 +94,10 @@ function stripComments(envString) {
|
||||
}
|
||||
return newString;
|
||||
}
|
||||
exports.stripComments = stripComments;
|
||||
/**
|
||||
* Strips out newlines from env file string
|
||||
*/
|
||||
function stripEmptyLines(envString) {
|
||||
export function stripEmptyLines(envString) {
|
||||
const emptyLinesRegex = /(^\n)/gim;
|
||||
return envString.replace(emptyLinesRegex, '');
|
||||
}
|
||||
exports.stripEmptyLines = stripEmptyLines;
|
||||
|
||||
5
dist/parse-rc-file.d.ts
vendored
5
dist/parse-rc-file.d.ts
vendored
@ -1,9 +1,8 @@
|
||||
import type { Environment } from './types.ts';
|
||||
/**
|
||||
* Gets the env vars from the rc file and rc environments
|
||||
*/
|
||||
export declare function getRCFileVars({ environments, filePath }: {
|
||||
environments: string[];
|
||||
filePath: string;
|
||||
}): Promise<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}): Promise<Environment>;
|
||||
|
||||
62
dist/parse-rc-file.js
vendored
62
dist/parse-rc-file.js
vendored
@ -1,31 +1,44 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fs_1 = require("fs");
|
||||
const util_1 = require("util");
|
||||
const path_1 = require("path");
|
||||
const utils_1 = require("./utils");
|
||||
const statAsync = util_1.promisify(fs_1.stat);
|
||||
const readFileAsync = util_1.promisify(fs_1.readFile);
|
||||
import { stat, readFile } from 'node:fs';
|
||||
import { promisify } from 'node:util';
|
||||
import { extname } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
|
||||
const statAsync = promisify(stat);
|
||||
const readFileAsync = promisify(readFile);
|
||||
/**
|
||||
* Gets the env vars from the rc file and rc environments
|
||||
*/
|
||||
async function getRCFileVars({ environments, filePath }) {
|
||||
const absolutePath = utils_1.resolveEnvFilePath(filePath);
|
||||
export async function getRCFileVars({ environments, filePath }) {
|
||||
const absolutePath = resolveEnvFilePath(filePath);
|
||||
try {
|
||||
await statAsync(absolutePath);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`);
|
||||
pathError.name = 'PathError';
|
||||
throw pathError;
|
||||
}
|
||||
// Get the file extension
|
||||
const ext = path_1.extname(absolutePath).toLowerCase();
|
||||
let parsedData;
|
||||
const ext = extname(absolutePath).toLowerCase();
|
||||
let parsedData = {};
|
||||
try {
|
||||
if (ext === '.json' || ext === '.js' || ext === '.cjs') {
|
||||
const possiblePromise = require(absolutePath); /* eslint-disable-line */
|
||||
parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise;
|
||||
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
||||
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
||||
let attributeTypes = {};
|
||||
if (ext === '.json') {
|
||||
attributeTypes = { with: { type: 'json' } };
|
||||
}
|
||||
const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
|
||||
if ('default' in res) {
|
||||
parsedData = res.default;
|
||||
}
|
||||
else {
|
||||
parsedData = res;
|
||||
}
|
||||
// Check to see if the imported value is a promise
|
||||
if (isPromise(parsedData)) {
|
||||
parsedData = await parsedData;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const file = await readFileAsync(absolutePath, { encoding: 'utf8' });
|
||||
@ -33,20 +46,26 @@ async function getRCFileVars({ environments, filePath }) {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`);
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`);
|
||||
parseError.name = 'ParseError';
|
||||
throw parseError;
|
||||
}
|
||||
// Parse and merge multiple rc environments together
|
||||
let result = {};
|
||||
let environmentFound = false;
|
||||
environments.forEach((name) => {
|
||||
for (const name of environments) {
|
||||
if (name in parsedData) {
|
||||
const envVars = parsedData[name];
|
||||
if (envVars !== undefined) {
|
||||
if (envVars != null && typeof envVars === 'object') {
|
||||
environmentFound = true;
|
||||
result = Object.assign(Object.assign({}, result), envVars);
|
||||
result = {
|
||||
...result,
|
||||
...envVars,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!environmentFound) {
|
||||
const environmentError = new Error(`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`);
|
||||
environmentError.name = 'EnvironmentError';
|
||||
@ -54,4 +73,3 @@ async function getRCFileVars({ environments, filePath }) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
exports.getRCFileVars = getRCFileVars;
|
||||
|
||||
4
dist/signal-termination.d.ts
vendored
4
dist/signal-termination.d.ts
vendored
@ -1,7 +1,7 @@
|
||||
/// <reference types="node" />
|
||||
import { ChildProcess } from 'child_process';
|
||||
export declare class TermSignals {
|
||||
private readonly terminateSpawnedProcessFuncHandlers;
|
||||
private terminateSpawnedProcessFuncExitHandler?;
|
||||
private readonly verbose;
|
||||
_exitCalled: boolean;
|
||||
constructor(options?: {
|
||||
@ -15,7 +15,7 @@ export declare class TermSignals {
|
||||
/**
|
||||
* Terminate parent process helper
|
||||
*/
|
||||
_terminateProcess(code?: number, signal?: NodeJS.Signals): void;
|
||||
_terminateProcess(signal?: NodeJS.Signals | number): void;
|
||||
/**
|
||||
* Exit event listener clean up helper
|
||||
*/
|
||||
|
||||
83
dist/signal-termination.js
vendored
83
dist/signal-termination.js
vendored
@ -1,73 +1,73 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const SIGNALS_TO_HANDLE = [
|
||||
'SIGINT', 'SIGTERM', 'SIGHUP'
|
||||
'SIGINT', 'SIGTERM', 'SIGHUP',
|
||||
];
|
||||
class TermSignals {
|
||||
export class TermSignals {
|
||||
terminateSpawnedProcessFuncHandlers = {};
|
||||
terminateSpawnedProcessFuncExitHandler;
|
||||
verbose = false;
|
||||
_exitCalled = false;
|
||||
constructor(options = {}) {
|
||||
this.terminateSpawnedProcessFuncHandlers = {};
|
||||
this.verbose = false;
|
||||
this._exitCalled = false;
|
||||
this.verbose = options.verbose === true;
|
||||
}
|
||||
handleTermSignals(proc) {
|
||||
// Terminate child process if parent process receives termination events
|
||||
SIGNALS_TO_HANDLE.forEach((signal) => {
|
||||
this.terminateSpawnedProcessFuncHandlers[signal] =
|
||||
(signal, code) => {
|
||||
const terminationFunc = (signal) => {
|
||||
this._removeProcessListeners();
|
||||
if (!this._exitCalled) {
|
||||
if (this.verbose) {
|
||||
console.info('Parent process exited with signal: ' +
|
||||
signal.toString() +
|
||||
'. Terminating child process...');
|
||||
console.info('Parent process exited with signal: '
|
||||
+ signal.toString()
|
||||
+ '. Terminating child process...');
|
||||
}
|
||||
// Mark shared state so we do not run into a signal/exit loop
|
||||
this._exitCalled = true;
|
||||
// Use the signal code if it is an error code
|
||||
let correctSignal;
|
||||
// let correctSignal: NodeJS.Signals | undefined
|
||||
if (typeof signal === 'number') {
|
||||
if (signal > (code !== null && code !== void 0 ? code : 0)) {
|
||||
code = signal;
|
||||
correctSignal = 'SIGINT';
|
||||
if (signal > 0) {
|
||||
// code = signal
|
||||
signal = 'SIGINT';
|
||||
}
|
||||
}
|
||||
else {
|
||||
correctSignal = signal;
|
||||
}
|
||||
// else {
|
||||
// correctSignal = signal
|
||||
// }
|
||||
// Kill the child process
|
||||
proc.kill(correctSignal !== null && correctSignal !== void 0 ? correctSignal : code);
|
||||
proc.kill(signal);
|
||||
// Terminate the parent process
|
||||
this._terminateProcess(code, correctSignal);
|
||||
this._terminateProcess(signal);
|
||||
}
|
||||
};
|
||||
for (const signal of SIGNALS_TO_HANDLE) {
|
||||
this.terminateSpawnedProcessFuncHandlers[signal] = terminationFunc;
|
||||
process.once(signal, this.terminateSpawnedProcessFuncHandlers[signal]);
|
||||
});
|
||||
process.once('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM);
|
||||
}
|
||||
this.terminateSpawnedProcessFuncExitHandler = terminationFunc;
|
||||
process.once('exit', this.terminateSpawnedProcessFuncExitHandler);
|
||||
// Terminate parent process if child process receives termination events
|
||||
proc.on('exit', (code, signal) => {
|
||||
this._removeProcessListeners();
|
||||
if (!this._exitCalled) {
|
||||
if (this.verbose) {
|
||||
console.info(`Child process exited with code: ${(code !== null && code !== void 0 ? code : '').toString()} and signal:` +
|
||||
(signal !== null && signal !== void 0 ? signal : '').toString() +
|
||||
'. Terminating parent process...');
|
||||
console.info(`Child process exited with code: ${(code ?? '').toString()} and signal:`
|
||||
+ (signal ?? '').toString()
|
||||
+ '. Terminating parent process...');
|
||||
}
|
||||
// Mark shared state so we do not run into a signal/exit loop
|
||||
this._exitCalled = true;
|
||||
// Use the signal code if it is an error code
|
||||
let correctSignal;
|
||||
if (typeof signal === 'number') {
|
||||
if (signal > (code !== null && code !== void 0 ? code : 0)) {
|
||||
if (signal > (code ?? 0)) {
|
||||
code = signal;
|
||||
correctSignal = 'SIGINT';
|
||||
}
|
||||
}
|
||||
else {
|
||||
correctSignal = signal !== null && signal !== void 0 ? signal : undefined;
|
||||
correctSignal = signal ?? undefined;
|
||||
}
|
||||
// Terminate the parent process
|
||||
this._terminateProcess(code, correctSignal);
|
||||
this._terminateProcess(correctSignal ?? code);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -75,17 +75,23 @@ class TermSignals {
|
||||
* Enables catching of unhandled exceptions
|
||||
*/
|
||||
handleUncaughtExceptions() {
|
||||
process.on('uncaughtException', (e) => this._uncaughtExceptionHandler(e));
|
||||
process.on('uncaughtException', (e) => {
|
||||
this._uncaughtExceptionHandler(e);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Terminate parent process helper
|
||||
*/
|
||||
_terminateProcess(code, signal) {
|
||||
if (signal !== undefined) {
|
||||
return process.kill(process.pid, signal);
|
||||
_terminateProcess(signal) {
|
||||
if (signal != null) {
|
||||
if (typeof signal === 'string') {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
if (typeof signal === 'number') {
|
||||
process.exit(signal);
|
||||
return;
|
||||
}
|
||||
if (code !== undefined) {
|
||||
return process.exit(code);
|
||||
}
|
||||
throw new Error('Unable to terminate parent process successfully');
|
||||
}
|
||||
@ -96,7 +102,9 @@ class TermSignals {
|
||||
SIGNALS_TO_HANDLE.forEach((signal) => {
|
||||
process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]);
|
||||
});
|
||||
process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM);
|
||||
if (this.terminateSpawnedProcessFuncExitHandler != null) {
|
||||
process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* General exception handler
|
||||
@ -106,4 +114,3 @@ class TermSignals {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
exports.TermSignals = TermSignals;
|
||||
|
||||
2
dist/spawn.d.ts
vendored
2
dist/spawn.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
import * as spawn from 'cross-spawn';
|
||||
export { spawn };
|
||||
4
dist/spawn.js
vendored
4
dist/spawn.js
vendored
@ -1,4 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const spawn = require("cross-spawn");
|
||||
exports.spawn = spawn;
|
||||
30
dist/types.d.ts
vendored
30
dist/types.d.ts
vendored
@ -1,15 +1,31 @@
|
||||
export interface GetEnvVarOptions {
|
||||
envFile?: {
|
||||
filePath?: string;
|
||||
import { Command } from 'commander';
|
||||
export type Environment = Partial<Record<string, string | number | boolean>>;
|
||||
export type RCEnvironment = Partial<Record<string, Environment>>;
|
||||
export interface CommanderOptions extends Command {
|
||||
override?: boolean;
|
||||
useShell?: boolean;
|
||||
expandEnvs?: boolean;
|
||||
verbose?: boolean;
|
||||
silent?: boolean;
|
||||
fallback?: boolean;
|
||||
};
|
||||
rc?: {
|
||||
environments?: string[];
|
||||
rcFile?: string;
|
||||
file?: string;
|
||||
}
|
||||
export interface RCFileOptions {
|
||||
environments: string[];
|
||||
filePath?: string;
|
||||
};
|
||||
}
|
||||
export interface EnvFileOptions {
|
||||
filePath?: string;
|
||||
fallback?: boolean;
|
||||
}
|
||||
export interface GetEnvVarOptions {
|
||||
envFile?: EnvFileOptions;
|
||||
rc?: RCFileOptions;
|
||||
verbose?: boolean;
|
||||
}
|
||||
export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'> {
|
||||
export interface EnvCmdOptions extends GetEnvVarOptions {
|
||||
command: string;
|
||||
commandArgs: string[];
|
||||
options?: {
|
||||
|
||||
3
dist/types.js
vendored
3
dist/types.js
vendored
@ -1,2 +1 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
export {};
|
||||
|
||||
5
dist/utils.d.ts
vendored
5
dist/utils.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
export declare const IMPORT_HOOK_EXTENSIONS: string[];
|
||||
/**
|
||||
* A simple function for resolving the path the user entered
|
||||
*/
|
||||
@ -7,6 +8,6 @@ export declare function resolveEnvFilePath(userPath: string): string;
|
||||
*/
|
||||
export declare function parseArgList(list: string): string[];
|
||||
/**
|
||||
* A simple function to test if the value is a promise
|
||||
* A simple function to test if the value is a promise/thenable
|
||||
*/
|
||||
export declare function isPromise(value: any | PromiseLike<Object>): value is Promise<any>;
|
||||
export declare function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T>;
|
||||
|
||||
31
dist/utils.js
vendored
31
dist/utils.js
vendored
@ -1,30 +1,31 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
import { resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { cwd } from 'node:process';
|
||||
// Special file extensions that node can natively import
|
||||
export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs'];
|
||||
/**
|
||||
* A simple function for resolving the path the user entered
|
||||
*/
|
||||
function resolveEnvFilePath(userPath) {
|
||||
export function resolveEnvFilePath(userPath) {
|
||||
// Make sure a home directory exist
|
||||
const home = os.homedir();
|
||||
if (home !== undefined) {
|
||||
const home = homedir();
|
||||
if (home != null) {
|
||||
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`);
|
||||
}
|
||||
return path.resolve(process.cwd(), userPath);
|
||||
return resolve(cwd(), userPath);
|
||||
}
|
||||
exports.resolveEnvFilePath = resolveEnvFilePath;
|
||||
/**
|
||||
* A simple function that parses a comma separated string into an array of strings
|
||||
*/
|
||||
function parseArgList(list) {
|
||||
export function parseArgList(list) {
|
||||
return list.split(',');
|
||||
}
|
||||
exports.parseArgList = parseArgList;
|
||||
/**
|
||||
* A simple function to test if the value is a promise
|
||||
* A simple function to test if the value is a promise/thenable
|
||||
*/
|
||||
function isPromise(value) {
|
||||
return value != null && typeof value.then === 'function';
|
||||
export function isPromise(value) {
|
||||
return value != null
|
||||
&& typeof value === 'object'
|
||||
&& 'then' in value
|
||||
&& typeof value.then === 'function';
|
||||
}
|
||||
exports.isPromise = isPromise;
|
||||
|
||||
38
eslint.config.js
Normal file
38
eslint.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { default as tseslint } from 'typescript-eslint'
|
||||
import { default as globals } from 'globals'
|
||||
import { default as eslint } from '@eslint/js'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Ignore build folder
|
||||
ignores: ['dist/*'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.strictTypeChecked,
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
// Enable Type generation
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
// For test files ignore some rules
|
||||
files: ['test/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off'
|
||||
},
|
||||
},
|
||||
// Disable Type Checking JS/CJS/MJS files
|
||||
{
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
|
||||
extends: [tseslint.configs.disableTypeChecked],
|
||||
},
|
||||
)
|
||||
89
package.json
89
package.json
@ -4,17 +4,19 @@
|
||||
"description": "Executes a command using the environment variables in an env file",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"env-cmd": "bin/env-cmd.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha -r ts-node/register ./test/**/*.ts",
|
||||
"test-cover": "nyc npm test",
|
||||
"prepare": "husky",
|
||||
"test": "mocha",
|
||||
"test-cover": "c8 npm test",
|
||||
"coveralls": "coveralls < coverage/lcov.info",
|
||||
"lint": "ts-standard --fix && tsc --noEmit",
|
||||
"lint": "npx eslint .",
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
@ -51,68 +53,35 @@
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^8.0.0",
|
||||
"@commitlint/config-conventional": "^8.0.0",
|
||||
"@types/chai": "^4.0.0",
|
||||
"@commitlint/cli": "^19.6.0",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/mocha": "^7.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/sinon": "^9.0.0",
|
||||
"chai": "^4.0.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"c8": "^10.1.2",
|
||||
"chai": "^5.1.2",
|
||||
"coveralls": "^3.0.0",
|
||||
"husky": "^4.0.0",
|
||||
"mocha": "^7.0.0",
|
||||
"nyc": "^15.0.0",
|
||||
"sinon": "^9.0.0",
|
||||
"ts-node": "^8.0.0",
|
||||
"ts-standard": "^8.0.0",
|
||||
"typescript": "^3.7.0"
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"instrument": true
|
||||
},
|
||||
"ts-standard": {
|
||||
"project": "./tsconfig.eslint.json",
|
||||
"ignore": [
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"@types/node"
|
||||
],
|
||||
"commitMessages": {
|
||||
"initialBadge": "docs: add greenkeeper badge",
|
||||
"initialDependencies": "chore: update dependencies",
|
||||
"initialBranches": "chore: whitelist greenkeeper branches",
|
||||
"dependencyUpdate": "chore: update dependency ${dependency}",
|
||||
"devDependencyUpdate": "chore: update devDependecy ${dependency}",
|
||||
"dependencyPin": "fix: pin dependency ${dependency}",
|
||||
"devDependencyPin": "fix: pin devDependecy ${dependency}"
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
"esmock": "^2.6.9",
|
||||
"globals": "^15.12.0",
|
||||
"husky": "^9.1.7",
|
||||
"mocha": "^11.0.0",
|
||||
"sinon": "^19.0.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
25
src/cli.ts
Normal file
25
src/cli.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as processLib from 'node:process'
|
||||
import type { Environment } from './types.ts'
|
||||
import { EnvCmd } from './env-cmd.js'
|
||||
import { parseArgs } from './parse-args.js'
|
||||
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<Environment>}
|
||||
*/
|
||||
export async function CLI(args: string[]): Promise<Environment> {
|
||||
|
||||
// Parse the args from the command line
|
||||
const parsedArgs = parseArgs(args)
|
||||
|
||||
// Run EnvCmd
|
||||
try {
|
||||
return await EnvCmd(parsedArgs)
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return processLib.exit(1)
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,9 @@
|
||||
import { spawn } from './spawn'
|
||||
import { EnvCmdOptions } from './types'
|
||||
import { TermSignals } from './signal-termination'
|
||||
import { parseArgs } from './parse-args'
|
||||
import { getEnvVars } from './get-env-vars'
|
||||
import { expandEnvs } from './expand-envs'
|
||||
|
||||
/**
|
||||
* Executes env - cmd using command line arguments
|
||||
* @export
|
||||
* @param {string[]} args Command line argument to pass in ['-f', './.env']
|
||||
* @returns {Promise<{ [key: string]: any }>}
|
||||
*/
|
||||
export async function CLI (args: string[]): Promise<{ [key: string]: any }> {
|
||||
// Parse the args from the command line
|
||||
const parsedArgs = parseArgs(args)
|
||||
|
||||
// Run EnvCmd
|
||||
try {
|
||||
return await (exports.EnvCmd(parsedArgs) as Promise<{ [key: string]: any }>)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return process.exit(1)
|
||||
}
|
||||
}
|
||||
import { default as spawn } from 'cross-spawn'
|
||||
import type { EnvCmdOptions, Environment } from './types.ts'
|
||||
import { TermSignals } from './signal-termination.js'
|
||||
import { getEnvVars } from './get-env-vars.js'
|
||||
import { expandEnvs } from './expand-envs.js'
|
||||
import * as processLib from 'node:process'
|
||||
|
||||
/**
|
||||
* The main env-cmd program. This will spawn a new process and run the given command using
|
||||
@ -30,31 +11,33 @@ export async function CLI (args: string[]): Promise<{ [key: string]: any }> {
|
||||
*
|
||||
* @export
|
||||
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
|
||||
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value
|
||||
* @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
|
||||
*/
|
||||
export async function EnvCmd (
|
||||
export async function EnvCmd(
|
||||
{
|
||||
command,
|
||||
commandArgs,
|
||||
envFile,
|
||||
rc,
|
||||
options = {}
|
||||
}: EnvCmdOptions
|
||||
): Promise<{ [key: string]: any }> {
|
||||
let env: { [name: string]: string } = {}
|
||||
options = {},
|
||||
}: EnvCmdOptions,
|
||||
): Promise<Environment> {
|
||||
let env: Environment = {}
|
||||
try {
|
||||
env = await getEnvVars({ envFile, rc, verbose: options.verbose })
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
if (!(options.silent ?? false)) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
// Override the merge order if --no-override flag set
|
||||
if (options.noOverride === true) {
|
||||
env = Object.assign({}, env, process.env)
|
||||
} else {
|
||||
env = Object.assign({}, env, processLib.env)
|
||||
}
|
||||
else {
|
||||
// Add in the system environment variables to our environment list
|
||||
env = Object.assign({}, process.env, env)
|
||||
env = Object.assign({}, processLib.env, env)
|
||||
}
|
||||
|
||||
if (options.expandEnvs === true) {
|
||||
@ -66,7 +49,7 @@ export async function EnvCmd (
|
||||
const proc = spawn(command, commandArgs, {
|
||||
stdio: 'inherit',
|
||||
shell: options.useShell,
|
||||
env
|
||||
env: env as Record<string, string>,
|
||||
})
|
||||
|
||||
// Handle any termination signals for parent and child proceses
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { Environment } from './types.ts'
|
||||
|
||||
/**
|
||||
* expandEnvs Replaces $var in args and command with environment variables
|
||||
* the environment variable doesn't exist, it leaves it as is.
|
||||
* if the environment variable doesn't exist, it leaves it as is.
|
||||
*/
|
||||
export function expandEnvs (str: string, envs: { [key: string]: any }): string {
|
||||
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => {
|
||||
export function expandEnvs(str: string, envs: Environment): string {
|
||||
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, (varName) => {
|
||||
const varValue = envs[varName.slice(1)]
|
||||
return varValue === undefined ? varName : varValue
|
||||
// const test = 42;
|
||||
return varValue === undefined ? varName : varValue.toString()
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,30 +1,30 @@
|
||||
import { GetEnvVarOptions } from './types'
|
||||
import { getRCFileVars } from './parse-rc-file'
|
||||
import { getEnvFileVars } from './parse-env-file'
|
||||
import type { GetEnvVarOptions, Environment } from './types.ts'
|
||||
import { getRCFileVars } from './parse-rc-file.js'
|
||||
import { getEnvFileVars } from './parse-env-file.js'
|
||||
|
||||
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json']
|
||||
const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json']
|
||||
|
||||
export async function getEnvVars (options: GetEnvVarOptions = {}): Promise<{ [key: string]: any }> {
|
||||
options.envFile = options.envFile !== undefined ? options.envFile : {}
|
||||
export async function getEnvVars(options: GetEnvVarOptions = {}): Promise<Environment> {
|
||||
options.envFile = options.envFile ?? {}
|
||||
// Check for rc file usage
|
||||
if (options.rc !== undefined) {
|
||||
return await getRCFile({
|
||||
environments: options.rc.environments,
|
||||
filePath: options.rc.filePath,
|
||||
verbose: options.verbose
|
||||
verbose: options.verbose,
|
||||
})
|
||||
}
|
||||
return await getEnvFile({
|
||||
filePath: options.envFile.filePath,
|
||||
fallback: options.envFile.fallback,
|
||||
verbose: options.verbose
|
||||
verbose: options.verbose,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getEnvFile (
|
||||
{ filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean }
|
||||
): Promise<{ [key: string]: any }> {
|
||||
export async function getEnvFile(
|
||||
{ filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean },
|
||||
): Promise<Environment> {
|
||||
// Use env file
|
||||
if (filePath !== undefined) {
|
||||
try {
|
||||
@ -33,10 +33,12 @@ export async function getEnvFile (
|
||||
console.info(`Found .env file at path: ${filePath}`)
|
||||
}
|
||||
return env
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
if (verbose === true) {
|
||||
console.info(`Failed to find .env file at path: ${filePath}`)
|
||||
}
|
||||
// Ignore error as we are just trying this location
|
||||
}
|
||||
if (fallback !== true) {
|
||||
throw new Error(`Failed to find .env file at path: ${filePath}`)
|
||||
@ -51,7 +53,10 @@ export async function getEnvFile (
|
||||
console.info(`Found .env file at default path: ${path}`)
|
||||
}
|
||||
return env
|
||||
} catch (e) { }
|
||||
}
|
||||
catch {
|
||||
// Ignore error because we are just trying this location
|
||||
}
|
||||
}
|
||||
|
||||
const error = `Failed to find .env file at default paths: [${ENV_FILE_DEFAULT_LOCATIONS.join(',')}]`
|
||||
@ -61,9 +66,9 @@ export async function getEnvFile (
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
export async function getRCFile (
|
||||
{ environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean }
|
||||
): Promise<{ [key: string]: any }> {
|
||||
export async function getRCFile(
|
||||
{ environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean },
|
||||
): Promise<Environment> {
|
||||
// User provided an .rc file path
|
||||
if (filePath !== undefined) {
|
||||
try {
|
||||
@ -72,7 +77,9 @@ export async function getRCFile (
|
||||
console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`)
|
||||
}
|
||||
return env
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.name === 'PathError') {
|
||||
if (verbose === true) {
|
||||
console.info(`Failed to find .rc file at path: ${filePath}`)
|
||||
@ -83,6 +90,7 @@ export async function getRCFile (
|
||||
console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -95,7 +103,9 @@ export async function getRCFile (
|
||||
console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`)
|
||||
}
|
||||
return env
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.name === 'EnvironmentError') {
|
||||
const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`
|
||||
if (verbose === true) {
|
||||
@ -103,6 +113,13 @@ export async function getRCFile (
|
||||
}
|
||||
throw new Error(errorText)
|
||||
}
|
||||
if (e.name === 'ParseError') {
|
||||
if (verbose === true) {
|
||||
console.info(e.message)
|
||||
}
|
||||
throw new Error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { getEnvVars } from './get-env-vars'
|
||||
import { getEnvVars } from './get-env-vars.js'
|
||||
|
||||
// Export the core env-cmd API
|
||||
export * from './types'
|
||||
export * from './env-cmd'
|
||||
export * from './types.js'
|
||||
export * from './cli.js'
|
||||
export * from './env-cmd.js'
|
||||
export const GetEnvVars = getEnvVars
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import * as commander from 'commander'
|
||||
import { EnvCmdOptions } from './types'
|
||||
import { parseArgList } from './utils'
|
||||
|
||||
// Use commonjs require to prevent a weird folder hierarchy in dist
|
||||
const packageJson = require('../package.json') /* eslint-disable-line */
|
||||
import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts'
|
||||
import { parseArgList } from './utils.js'
|
||||
import { default as packageJson } from '../package.json' with { type: 'json' };
|
||||
|
||||
/**
|
||||
* Parses the arguments passed into the cli
|
||||
*/
|
||||
export function parseArgs (args: string[]): EnvCmdOptions {
|
||||
export function parseArgs(args: string[]): EnvCmdOptions {
|
||||
// Run the initial arguments through commander in order to determine
|
||||
// which value in the args array is the `command` to execute
|
||||
let program = parseArgsUsingCommander(args)
|
||||
@ -42,23 +40,27 @@ export function parseArgs (args: string[]): EnvCmdOptions {
|
||||
silent = true
|
||||
}
|
||||
|
||||
let rc: any
|
||||
if (program.environments !== undefined && program.environments.length !== 0) {
|
||||
let rc: RCFileOptions | undefined
|
||||
if (
|
||||
program.environments !== undefined
|
||||
&& Array.isArray(program.environments)
|
||||
&& program.environments.length !== 0
|
||||
) {
|
||||
rc = {
|
||||
environments: program.environments,
|
||||
filePath: program.rcFile
|
||||
filePath: program.rcFile,
|
||||
}
|
||||
}
|
||||
|
||||
let envFile: any
|
||||
let envFile: EnvFileOptions | undefined
|
||||
if (program.file !== undefined) {
|
||||
envFile = {
|
||||
filePath: program.file,
|
||||
fallback: program.fallback
|
||||
fallback: program.fallback,
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: EnvCmdOptions = {
|
||||
command,
|
||||
commandArgs,
|
||||
envFile,
|
||||
@ -68,8 +70,8 @@ export function parseArgs (args: string[]): EnvCmdOptions {
|
||||
noOverride,
|
||||
silent,
|
||||
useShell,
|
||||
verbose
|
||||
}
|
||||
verbose,
|
||||
},
|
||||
}
|
||||
if (verbose) {
|
||||
console.info(`Options: ${JSON.stringify(options, null, 0)}`)
|
||||
@ -77,8 +79,8 @@ export function parseArgs (args: string[]): EnvCmdOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
export function parseArgsUsingCommander (args: string[]): commander.Command {
|
||||
const program = new commander.Command()
|
||||
export function parseArgsUsingCommander(args: string[]): CommanderOptions {
|
||||
const program = new commander.Command() as CommanderOptions
|
||||
return program
|
||||
.version(packageJson.version, '-v, --version')
|
||||
.usage('[options] <command> [...args]')
|
||||
|
||||
@ -1,28 +1,42 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { resolveEnvFilePath, isPromise } from './utils'
|
||||
|
||||
const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs']
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { extname } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
|
||||
import type { Environment } from './types.ts'
|
||||
|
||||
/**
|
||||
* Gets the environment vars from an env file
|
||||
*/
|
||||
export async function getEnvFileVars (envFilePath: string): Promise<{ [key: string]: any }> {
|
||||
export async function getEnvFileVars(envFilePath: string): Promise<Environment> {
|
||||
const absolutePath = resolveEnvFilePath(envFilePath)
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
if (!existsSync(absolutePath)) {
|
||||
const pathError = new Error(`Invalid env file path (${envFilePath}).`)
|
||||
pathError.name = 'PathError'
|
||||
throw pathError
|
||||
}
|
||||
|
||||
// Get the file extension
|
||||
const ext = path.extname(absolutePath).toLowerCase()
|
||||
let env = {}
|
||||
if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) {
|
||||
const possiblePromise = require(absolutePath) /* eslint-disable-line */
|
||||
env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise
|
||||
const ext = extname(absolutePath).toLowerCase()
|
||||
let env: Environment = {}
|
||||
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
||||
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
||||
let attributeTypes = {}
|
||||
if (ext === '.json') {
|
||||
attributeTypes = { with: { type: 'json' } }
|
||||
}
|
||||
const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as Environment | { default: Environment }
|
||||
if ('default' in res) {
|
||||
env = res.default as Environment
|
||||
} else {
|
||||
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' })
|
||||
env = res
|
||||
}
|
||||
// Check to see if the imported value is a promise
|
||||
if (isPromise(env)) {
|
||||
env = await env
|
||||
}
|
||||
}
|
||||
else {
|
||||
const file = readFileSync(absolutePath, { encoding: 'utf8' })
|
||||
env = parseEnvString(file)
|
||||
}
|
||||
return env
|
||||
@ -31,7 +45,7 @@ export async function getEnvFileVars (envFilePath: string): Promise<{ [key: stri
|
||||
/**
|
||||
* Parse out all env vars from a given env file string and return an object
|
||||
*/
|
||||
export function parseEnvString (envFileString: string): { [key: string]: string } {
|
||||
export function parseEnvString(envFileString: string): Environment {
|
||||
// First thing we do is stripe out all comments
|
||||
envFileString = stripComments(envFileString.toString())
|
||||
|
||||
@ -45,27 +59,41 @@ export function parseEnvString (envFileString: string): { [key: string]: string
|
||||
/**
|
||||
* Parse out all env vars from an env file string
|
||||
*/
|
||||
export function parseEnvVars (envString: string): { [key: string]: string } {
|
||||
export function parseEnvVars(envString: string): Environment {
|
||||
const envParseRegex = /^((.+?)[=](.*))$/gim
|
||||
const matches: { [key: string]: string } = {}
|
||||
const matches: Environment = {}
|
||||
let match
|
||||
while ((match = envParseRegex.exec(envString)) !== null) {
|
||||
// Note: match[1] is the full env=var line
|
||||
const key = match[2].trim()
|
||||
const value = match[3].trim()
|
||||
let value: string | number | boolean = match[3].trim()
|
||||
|
||||
// remove any surrounding quotes
|
||||
matches[key] = value
|
||||
value = value
|
||||
.replace(/(^['"]|['"]$)/g, '')
|
||||
.replace(/\\n/g, '\n')
|
||||
|
||||
// Convert string to JS type if appropriate
|
||||
if (value !== '' && !isNaN(+value)) {
|
||||
matches[key] = +value
|
||||
}
|
||||
return matches
|
||||
else if (value === 'true') {
|
||||
matches[key] = true
|
||||
}
|
||||
else if (value === 'false') {
|
||||
matches[key] = false
|
||||
}
|
||||
else {
|
||||
matches[key] = value
|
||||
}
|
||||
}
|
||||
return JSON.parse(JSON.stringify(matches)) as Environment
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips out comments from env file string
|
||||
*/
|
||||
export function stripComments (envString: string): string {
|
||||
export function stripComments(envString: string): string {
|
||||
const commentsRegex = /(^#.*$)/gim
|
||||
let match = commentsRegex.exec(envString)
|
||||
let newString = envString
|
||||
@ -79,7 +107,7 @@ export function stripComments (envString: string): string {
|
||||
/**
|
||||
* Strips out newlines from env file string
|
||||
*/
|
||||
export function stripEmptyLines (envString: string): string {
|
||||
export function stripEmptyLines(envString: string): string {
|
||||
const emptyLinesRegex = /(^\n)/gim
|
||||
return envString.replace(emptyLinesRegex, '')
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { stat, readFile } from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import { extname } from 'path'
|
||||
import { resolveEnvFilePath, isPromise } from './utils'
|
||||
import { stat, readFile } from 'node:fs'
|
||||
import { promisify } from 'node:util'
|
||||
import { extname } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
|
||||
import type { Environment, RCEnvironment } from './types.ts'
|
||||
|
||||
const statAsync = promisify(stat)
|
||||
const readFileAsync = promisify(readFile)
|
||||
@ -9,14 +11,15 @@ const readFileAsync = promisify(readFile)
|
||||
/**
|
||||
* Gets the env vars from the rc file and rc environments
|
||||
*/
|
||||
export async function getRCFileVars (
|
||||
export async function getRCFileVars(
|
||||
{ environments, filePath }:
|
||||
{ environments: string[], filePath: string }
|
||||
): Promise<{ [key: string]: any }> {
|
||||
{ environments: string[], filePath: string },
|
||||
): Promise<Environment> {
|
||||
const absolutePath = resolveEnvFilePath(filePath)
|
||||
try {
|
||||
await statAsync(absolutePath)
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`)
|
||||
pathError.name = 'PathError'
|
||||
throw pathError
|
||||
@ -24,38 +27,58 @@ export async function getRCFileVars (
|
||||
|
||||
// Get the file extension
|
||||
const ext = extname(absolutePath).toLowerCase()
|
||||
let parsedData: { [key: string]: any }
|
||||
let parsedData: Partial<RCEnvironment> = {}
|
||||
try {
|
||||
if (ext === '.json' || ext === '.js' || ext === '.cjs') {
|
||||
const possiblePromise = require(absolutePath) /* eslint-disable-line */
|
||||
parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise
|
||||
} else {
|
||||
const file = await readFileAsync(absolutePath, { encoding: 'utf8' })
|
||||
parsedData = JSON.parse(file)
|
||||
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
||||
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
||||
let attributeTypes = {}
|
||||
if (ext === '.json') {
|
||||
attributeTypes = { with: { type: 'json' } }
|
||||
}
|
||||
} catch (e) {
|
||||
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`)
|
||||
const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as RCEnvironment | { default: RCEnvironment }
|
||||
if ('default' in res) {
|
||||
parsedData = res.default as RCEnvironment
|
||||
} else {
|
||||
parsedData = res
|
||||
}
|
||||
// Check to see if the imported value is a promise
|
||||
if (isPromise(parsedData)) {
|
||||
parsedData = await parsedData
|
||||
}
|
||||
}
|
||||
else {
|
||||
const file = await readFileAsync(absolutePath, { encoding: 'utf8' })
|
||||
parsedData = JSON.parse(file) as Partial<RCEnvironment>
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error'
|
||||
const parseError = new Error(
|
||||
`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`,
|
||||
)
|
||||
parseError.name = 'ParseError'
|
||||
throw parseError
|
||||
}
|
||||
|
||||
// Parse and merge multiple rc environments together
|
||||
let result = {}
|
||||
let result: Environment = {}
|
||||
let environmentFound = false
|
||||
environments.forEach((name): void => {
|
||||
for (const name of environments) {
|
||||
if (name in parsedData) {
|
||||
const envVars = parsedData[name]
|
||||
if (envVars !== undefined) {
|
||||
if (envVars != null && typeof envVars === 'object') {
|
||||
environmentFound = true
|
||||
result = {
|
||||
...result,
|
||||
...envVars
|
||||
...envVars,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!environmentFound) {
|
||||
const environmentError = new Error(
|
||||
`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`
|
||||
`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`,
|
||||
)
|
||||
environmentError.name = 'EnvironmentError'
|
||||
throw environmentError
|
||||
|
||||
@ -1,52 +1,56 @@
|
||||
import { ChildProcess } from 'child_process'
|
||||
|
||||
const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [
|
||||
'SIGINT', 'SIGTERM', 'SIGHUP'
|
||||
'SIGINT', 'SIGTERM', 'SIGHUP',
|
||||
]
|
||||
|
||||
export class TermSignals {
|
||||
private readonly terminateSpawnedProcessFuncHandlers: { [key: string]: any } = {}
|
||||
private readonly terminateSpawnedProcessFuncHandlers: Record<string, NodeJS.SignalsListener> = {}
|
||||
private terminateSpawnedProcessFuncExitHandler?: NodeJS.ExitListener
|
||||
private readonly verbose: boolean = false
|
||||
public _exitCalled = false
|
||||
|
||||
constructor (options: { verbose?: boolean } = {}) {
|
||||
constructor(options: { verbose?: boolean } = {}) {
|
||||
this.verbose = options.verbose === true
|
||||
}
|
||||
|
||||
public handleTermSignals (proc: ChildProcess): void {
|
||||
public handleTermSignals(proc: ChildProcess): void {
|
||||
// Terminate child process if parent process receives termination events
|
||||
SIGNALS_TO_HANDLE.forEach((signal): void => {
|
||||
this.terminateSpawnedProcessFuncHandlers[signal] =
|
||||
(signal: NodeJS.Signals | number, code: number): void => {
|
||||
const terminationFunc = (signal: NodeJS.Signals | number): void => {
|
||||
this._removeProcessListeners()
|
||||
if (!this._exitCalled) {
|
||||
if (this.verbose) {
|
||||
console.info(
|
||||
'Parent process exited with signal: ' +
|
||||
signal.toString() +
|
||||
'. Terminating child process...')
|
||||
'Parent process exited with signal: '
|
||||
+ signal.toString()
|
||||
+ '. Terminating child process...')
|
||||
}
|
||||
// Mark shared state so we do not run into a signal/exit loop
|
||||
this._exitCalled = true
|
||||
// Use the signal code if it is an error code
|
||||
let correctSignal: NodeJS.Signals | undefined
|
||||
// let correctSignal: NodeJS.Signals | undefined
|
||||
if (typeof signal === 'number') {
|
||||
if (signal > (code ?? 0)) {
|
||||
code = signal
|
||||
correctSignal = 'SIGINT'
|
||||
if (signal > 0) {
|
||||
// code = signal
|
||||
signal = 'SIGINT'
|
||||
}
|
||||
} else {
|
||||
correctSignal = signal
|
||||
}
|
||||
// else {
|
||||
// correctSignal = signal
|
||||
// }
|
||||
// Kill the child process
|
||||
proc.kill(correctSignal ?? code)
|
||||
proc.kill(signal)
|
||||
// Terminate the parent process
|
||||
this._terminateProcess(code, correctSignal)
|
||||
this._terminateProcess(signal)
|
||||
}
|
||||
}
|
||||
|
||||
for (const signal of SIGNALS_TO_HANDLE) {
|
||||
this.terminateSpawnedProcessFuncHandlers[signal] = terminationFunc
|
||||
process.once(signal, this.terminateSpawnedProcessFuncHandlers[signal])
|
||||
})
|
||||
process.once('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM)
|
||||
}
|
||||
this.terminateSpawnedProcessFuncExitHandler = terminationFunc
|
||||
process.once('exit', this.terminateSpawnedProcessFuncExitHandler)
|
||||
|
||||
// Terminate parent process if child process receives termination events
|
||||
proc.on('exit', (code: number | undefined, signal: NodeJS.Signals | number | null): void => {
|
||||
@ -54,9 +58,9 @@ export class TermSignals {
|
||||
if (!this._exitCalled) {
|
||||
if (this.verbose) {
|
||||
console.info(
|
||||
`Child process exited with code: ${(code ?? '').toString()} and signal:` +
|
||||
(signal ?? '').toString() +
|
||||
'. Terminating parent process...'
|
||||
`Child process exited with code: ${(code ?? '').toString()} and signal:`
|
||||
+ (signal ?? '').toString()
|
||||
+ '. Terminating parent process...',
|
||||
)
|
||||
}
|
||||
// Mark shared state so we do not run into a signal/exit loop
|
||||
@ -68,11 +72,12 @@ export class TermSignals {
|
||||
code = signal
|
||||
correctSignal = 'SIGINT'
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
correctSignal = signal ?? undefined
|
||||
}
|
||||
// Terminate the parent process
|
||||
this._terminateProcess(code, correctSignal)
|
||||
this._terminateProcess(correctSignal ?? code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -80,19 +85,25 @@ export class TermSignals {
|
||||
/**
|
||||
* Enables catching of unhandled exceptions
|
||||
*/
|
||||
public handleUncaughtExceptions (): void {
|
||||
process.on('uncaughtException', (e): void => this._uncaughtExceptionHandler(e))
|
||||
public handleUncaughtExceptions(): void {
|
||||
process.on('uncaughtException', (e): void => {
|
||||
this._uncaughtExceptionHandler(e)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate parent process helper
|
||||
*/
|
||||
public _terminateProcess (code?: number, signal?: NodeJS.Signals): void {
|
||||
if (signal !== undefined) {
|
||||
return process.kill(process.pid, signal)
|
||||
public _terminateProcess(signal?: NodeJS.Signals | number): void {
|
||||
if (signal != null) {
|
||||
if (typeof signal === 'string') {
|
||||
process.kill(process.pid, signal)
|
||||
return
|
||||
}
|
||||
if (typeof signal === 'number') {
|
||||
process.exit(signal)
|
||||
return
|
||||
}
|
||||
if (code !== undefined) {
|
||||
return process.exit(code)
|
||||
}
|
||||
throw new Error('Unable to terminate parent process successfully')
|
||||
}
|
||||
@ -100,17 +111,19 @@ export class TermSignals {
|
||||
/**
|
||||
* Exit event listener clean up helper
|
||||
*/
|
||||
public _removeProcessListeners (): void {
|
||||
public _removeProcessListeners(): void {
|
||||
SIGNALS_TO_HANDLE.forEach((signal): void => {
|
||||
process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal])
|
||||
})
|
||||
process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM)
|
||||
if (this.terminateSpawnedProcessFuncExitHandler != null) {
|
||||
process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General exception handler
|
||||
*/
|
||||
public _uncaughtExceptionHandler (e: Error): void {
|
||||
public _uncaughtExceptionHandler(e: Error): void {
|
||||
console.error(e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import * as spawn from 'cross-spawn'
|
||||
export {
|
||||
spawn
|
||||
}
|
||||
39
src/types.ts
39
src/types.ts
@ -1,16 +1,39 @@
|
||||
export interface GetEnvVarOptions {
|
||||
envFile?: {
|
||||
filePath?: string
|
||||
fallback?: boolean
|
||||
}
|
||||
rc?: {
|
||||
import { Command } from 'commander'
|
||||
|
||||
// Define an export type
|
||||
export type Environment = Partial<Record<string, string | number | boolean>>
|
||||
|
||||
export type RCEnvironment = Partial<Record<string, Environment>>
|
||||
|
||||
export interface CommanderOptions extends Command {
|
||||
override?: boolean // Default: false
|
||||
useShell?: boolean // Default: false
|
||||
expandEnvs?: boolean // Default: false
|
||||
verbose?: boolean // Default: false
|
||||
silent?: boolean // Default: false
|
||||
fallback?: boolean // Default false
|
||||
environments?: string[]
|
||||
rcFile?: string
|
||||
file?: string
|
||||
}
|
||||
|
||||
export interface RCFileOptions {
|
||||
environments: string[]
|
||||
filePath?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvFileOptions {
|
||||
filePath?: string
|
||||
fallback?: boolean
|
||||
}
|
||||
|
||||
export interface GetEnvVarOptions {
|
||||
envFile?: EnvFileOptions
|
||||
rc?: RCFileOptions
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'> {
|
||||
export interface EnvCmdOptions extends GetEnvVarOptions {
|
||||
command: string
|
||||
commandArgs: string[]
|
||||
options?: {
|
||||
|
||||
27
src/utils.ts
27
src/utils.ts
@ -1,27 +1,34 @@
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { resolve } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { cwd } from 'node:process'
|
||||
|
||||
// Special file extensions that node can natively import
|
||||
export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']
|
||||
|
||||
/**
|
||||
* A simple function for resolving the path the user entered
|
||||
*/
|
||||
export function resolveEnvFilePath (userPath: string): string {
|
||||
export function resolveEnvFilePath(userPath: string): string {
|
||||
// Make sure a home directory exist
|
||||
const home = os.homedir()
|
||||
if (home !== undefined) {
|
||||
const home = homedir() as string | undefined
|
||||
if (home != null) {
|
||||
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`)
|
||||
}
|
||||
return path.resolve(process.cwd(), userPath)
|
||||
return resolve(cwd(), userPath)
|
||||
}
|
||||
/**
|
||||
* A simple function that parses a comma separated string into an array of strings
|
||||
*/
|
||||
export function parseArgList (list: string): string[] {
|
||||
export function parseArgList(list: string): string[] {
|
||||
return list.split(',')
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple function to test if the value is a promise
|
||||
* A simple function to test if the value is a promise/thenable
|
||||
*/
|
||||
export function isPromise (value: any | PromiseLike<Object>): value is Promise<any> {
|
||||
return value != null && typeof value.then === 'function'
|
||||
export function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T> {
|
||||
return value != null
|
||||
&& typeof value === 'object'
|
||||
&& 'then' in value
|
||||
&& typeof value.then === 'function'
|
||||
}
|
||||
|
||||
57
test/cli.spec.ts
Normal file
57
test/cli.spec.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { default as sinon } from 'sinon'
|
||||
import { assert } from 'chai'
|
||||
import { default as esmock } from 'esmock'
|
||||
import type { CLI } from '../src/cli.ts'
|
||||
|
||||
describe('CLI', (): void => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let parseArgsStub: sinon.SinonStub<any>
|
||||
let envCmdStub: sinon.SinonStub<any>
|
||||
let processExitStub: sinon.SinonStub<any>
|
||||
let cliLib: { CLI: typeof CLI }
|
||||
|
||||
before(async (): Promise<void> => {
|
||||
sandbox = sinon.createSandbox()
|
||||
envCmdStub = sandbox.stub()
|
||||
parseArgsStub = sandbox.stub()
|
||||
processExitStub = sandbox.stub()
|
||||
cliLib = await esmock('../src/cli.ts', {
|
||||
'../src/env-cmd': {
|
||||
EnvCmd: envCmdStub,
|
||||
},
|
||||
'../src/parse-args': {
|
||||
parseArgs: parseArgsStub,
|
||||
},
|
||||
'node:process': {
|
||||
exit: processExitStub,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
after((): void => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
afterEach((): void => {
|
||||
sandbox.resetHistory()
|
||||
sandbox.resetBehavior()
|
||||
})
|
||||
|
||||
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
|
||||
parseArgsStub.returns({})
|
||||
await cliLib.CLI(['node', './env-cmd', '-v'])
|
||||
assert.equal(parseArgsStub.callCount, 1)
|
||||
assert.equal(envCmdStub.callCount, 1)
|
||||
assert.equal(processExitStub.callCount, 0)
|
||||
})
|
||||
|
||||
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
|
||||
parseArgsStub.returns({})
|
||||
envCmdStub.throwsException('Error')
|
||||
await cliLib.CLI(['node', './env-cmd', '-v'])
|
||||
assert.equal(parseArgsStub.callCount, 1)
|
||||
assert.equal(envCmdStub.callCount, 1)
|
||||
assert.equal(processExitStub.callCount, 1)
|
||||
assert.equal(processExitStub.args[0][0], 1)
|
||||
})
|
||||
})
|
||||
@ -1,68 +1,44 @@
|
||||
import * as sinon from 'sinon'
|
||||
import { default as sinon } from 'sinon'
|
||||
import { assert } from 'chai'
|
||||
import * as signalTermLib from '../src/signal-termination'
|
||||
import * as parseArgsLib from '../src/parse-args'
|
||||
import * as getEnvVarsLib from '../src/get-env-vars'
|
||||
import * as expandEnvsLib from '../src/expand-envs'
|
||||
import * as spawnLib from '../src/spawn'
|
||||
import * as envCmdLib from '../src/env-cmd'
|
||||
import { default as esmock } from 'esmock'
|
||||
import { expandEnvs } from '../src/expand-envs.js'
|
||||
import type { EnvCmd } from '../src/env-cmd.ts'
|
||||
|
||||
describe('CLI', (): void => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let parseArgsStub: sinon.SinonStub<any, any>
|
||||
let envCmdStub: sinon.SinonStub<any, any>
|
||||
let processExitStub: sinon.SinonStub<any, any>
|
||||
before((): void => {
|
||||
sandbox = sinon.createSandbox()
|
||||
parseArgsStub = sandbox.stub(parseArgsLib, 'parseArgs')
|
||||
envCmdStub = sandbox.stub(envCmdLib, 'EnvCmd')
|
||||
processExitStub = sandbox.stub(process, 'exit')
|
||||
})
|
||||
|
||||
after((): void => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
afterEach((): void => {
|
||||
sandbox.resetHistory()
|
||||
sandbox.resetBehavior()
|
||||
})
|
||||
|
||||
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
|
||||
parseArgsStub.returns({})
|
||||
await envCmdLib.CLI(['node', './env-cmd', '-v'])
|
||||
assert.equal(parseArgsStub.callCount, 1)
|
||||
assert.equal(envCmdStub.callCount, 1)
|
||||
assert.equal(processExitStub.callCount, 0)
|
||||
})
|
||||
|
||||
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
|
||||
parseArgsStub.returns({})
|
||||
envCmdStub.throwsException('Error')
|
||||
await envCmdLib.CLI(['node', './env-cmd', '-v'])
|
||||
assert.equal(parseArgsStub.callCount, 1)
|
||||
assert.equal(envCmdStub.callCount, 1)
|
||||
assert.equal(processExitStub.callCount, 1)
|
||||
assert.equal(processExitStub.args[0][0], 1)
|
||||
})
|
||||
})
|
||||
let envCmdLib: { EnvCmd: typeof EnvCmd }
|
||||
|
||||
describe('EnvCmd', (): void => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let getEnvVarsStub: sinon.SinonStub<any, any>
|
||||
let spawnStub: sinon.SinonStub<any, any>
|
||||
let expandEnvsSpy: sinon.SinonSpy<any, any>
|
||||
before((): void => {
|
||||
let getEnvVarsStub: sinon.SinonStub<any>
|
||||
let spawnStub: sinon.SinonStub<any>
|
||||
let expandEnvsSpy: sinon.SinonSpy<any>
|
||||
before(async (): Promise<void> => {
|
||||
sandbox = sinon.createSandbox()
|
||||
getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars')
|
||||
spawnStub = sandbox.stub(spawnLib, 'spawn')
|
||||
getEnvVarsStub = sandbox.stub()
|
||||
spawnStub = sandbox.stub()
|
||||
spawnStub.returns({
|
||||
on: (): void => { /* Fake the on method */ },
|
||||
kill: (): void => { /* Fake the kill method */ }
|
||||
on: sinon.stub(),
|
||||
kill: sinon.stub(),
|
||||
})
|
||||
expandEnvsSpy = sandbox.spy(expandEnvs)
|
||||
|
||||
const TermSignals = sandbox.stub()
|
||||
TermSignals.prototype.handleTermSignals = sandbox.stub()
|
||||
TermSignals.prototype.handleUncaughtExceptions = sandbox.stub()
|
||||
|
||||
envCmdLib = await esmock('../src/env-cmd.ts', {
|
||||
'../src/get-env-vars': {
|
||||
getEnvVars: getEnvVarsStub,
|
||||
},
|
||||
'cross-spawn': {
|
||||
default: spawnStub,
|
||||
},
|
||||
'../src/expand-envs': {
|
||||
expandEnvs: expandEnvsSpy,
|
||||
},
|
||||
'../src/signal-termination': {
|
||||
TermSignals,
|
||||
},
|
||||
})
|
||||
expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs')
|
||||
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals')
|
||||
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleUncaughtExceptions')
|
||||
})
|
||||
|
||||
after((): void => {
|
||||
@ -80,12 +56,12 @@ describe('EnvCmd', (): void => {
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env',
|
||||
fallback: true
|
||||
fallback: true,
|
||||
},
|
||||
rc: {
|
||||
environments: ['dev'],
|
||||
filePath: './.rc'
|
||||
}
|
||||
filePath: './.rc',
|
||||
},
|
||||
})
|
||||
assert.equal(getEnvVarsStub.callCount, 1)
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
@ -100,17 +76,17 @@ describe('EnvCmd', (): void => {
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env',
|
||||
fallback: true
|
||||
fallback: true,
|
||||
},
|
||||
rc: {
|
||||
environments: ['dev'],
|
||||
filePath: './.rc'
|
||||
}
|
||||
filePath: './.rc',
|
||||
},
|
||||
})
|
||||
assert.equal(getEnvVarsStub.callCount, 1)
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
assert.equal(spawnStub.args[0][2].env.BOB, 'test')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should not override existing env vars if noOverride option is true',
|
||||
@ -122,20 +98,20 @@ describe('EnvCmd', (): void => {
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env',
|
||||
fallback: true
|
||||
fallback: true,
|
||||
},
|
||||
rc: {
|
||||
environments: ['dev'],
|
||||
filePath: './.rc'
|
||||
filePath: './.rc',
|
||||
},
|
||||
options: {
|
||||
noOverride: true
|
||||
}
|
||||
noOverride: true,
|
||||
},
|
||||
})
|
||||
assert.equal(getEnvVarsStub.callCount, 1)
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
assert.equal(spawnStub.args[0][2].env.BOB, 'cool')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should spawn process with shell option if useShell option is true',
|
||||
@ -147,20 +123,20 @@ describe('EnvCmd', (): void => {
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env',
|
||||
fallback: true
|
||||
fallback: true,
|
||||
},
|
||||
rc: {
|
||||
environments: ['dev'],
|
||||
filePath: './.rc'
|
||||
filePath: './.rc',
|
||||
},
|
||||
options: {
|
||||
useShell: true
|
||||
}
|
||||
useShell: true,
|
||||
},
|
||||
})
|
||||
assert.equal(getEnvVarsStub.callCount, 1)
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
assert.equal(spawnStub.args[0][2].shell, true)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should spawn process with command and args expanded if expandEnvs option is true',
|
||||
@ -171,15 +147,15 @@ describe('EnvCmd', (): void => {
|
||||
commandArgs: ['$PING', '\\$IP'],
|
||||
envFile: {
|
||||
filePath: './.env',
|
||||
fallback: true
|
||||
fallback: true,
|
||||
},
|
||||
rc: {
|
||||
environments: ['dev'],
|
||||
filePath: './.rc'
|
||||
filePath: './.rc',
|
||||
},
|
||||
options: {
|
||||
expandEnvs: true
|
||||
}
|
||||
expandEnvs: true,
|
||||
},
|
||||
})
|
||||
|
||||
const spawnArgs = spawnStub.args[0]
|
||||
@ -188,9 +164,9 @@ describe('EnvCmd', (): void => {
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
assert.equal(expandEnvsSpy.callCount, 3, 'command + number of args')
|
||||
assert.equal(spawnArgs[0], 'node')
|
||||
assert.sameOrderedMembers(spawnArgs[1], ['PONG', '\\$IP'])
|
||||
assert.sameOrderedMembers(spawnArgs[1] as string[], ['PONG', '\\$IP'])
|
||||
assert.equal(spawnArgs[2].env.PING, 'PONG')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should ignore errors if silent flag provided',
|
||||
@ -201,16 +177,16 @@ describe('EnvCmd', (): void => {
|
||||
command: 'node',
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env'
|
||||
filePath: './.env',
|
||||
},
|
||||
options: {
|
||||
silent: true
|
||||
}
|
||||
silent: true,
|
||||
},
|
||||
})
|
||||
assert.equal(getEnvVarsStub.callCount, 1)
|
||||
assert.equal(spawnStub.callCount, 1)
|
||||
assert.isUndefined(spawnStub.args[0][2].env.BOB)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should allow errors if silent flag not provided',
|
||||
@ -221,14 +197,16 @@ describe('EnvCmd', (): void => {
|
||||
command: 'node',
|
||||
commandArgs: ['-v'],
|
||||
envFile: {
|
||||
filePath: './.env'
|
||||
}
|
||||
filePath: './.env',
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.equal(e.name, 'MissingFile')
|
||||
return
|
||||
}
|
||||
assert.fail('Should not get here.')
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
/* eslint @typescript-eslint/no-non-null-assertion: 0 */
|
||||
import { assert } from 'chai'
|
||||
import { expandEnvs } from '../src/expand-envs'
|
||||
import { expandEnvs } from '../src/expand-envs.js'
|
||||
|
||||
describe('expandEnvs', (): void => {
|
||||
const envs = {
|
||||
notvar: 'this is not used',
|
||||
dollar: 'money',
|
||||
PING: 'PONG',
|
||||
IP1: '127.0.0.1'
|
||||
IP1: '127.0.0.1',
|
||||
THANKSFORALLTHEFISH: 42,
|
||||
BRINGATOWEL: true,
|
||||
}
|
||||
const args = ['notvar', '$dollar', '\\$notvar', '-4', '$PING', '$IP1', '\\$IP1', '$NONEXIST']
|
||||
const argsExpanded = ['notvar', 'money', '\\$notvar', '-4', 'PONG', '127.0.0.1', '\\$IP1', '$NONEXIST']
|
||||
@ -15,5 +17,8 @@ describe('expandEnvs', (): void => {
|
||||
it('should replace environment variables in args', (): void => {
|
||||
const res = args.map(arg => expandEnvs(arg, envs))
|
||||
assert.sameOrderedMembers(res, argsExpanded)
|
||||
for (const arg of args) {
|
||||
assert.typeOf(arg, 'string')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import * as sinon from 'sinon'
|
||||
import { default as sinon } from 'sinon'
|
||||
import { assert } from 'chai'
|
||||
import { getEnvVars } from '../src/get-env-vars'
|
||||
import * as rcFile from '../src/parse-rc-file'
|
||||
import * as envFile from '../src/parse-env-file'
|
||||
import { default as esmock } from 'esmock'
|
||||
import type { getEnvVars } from '../src/get-env-vars.ts'
|
||||
|
||||
let getEnvVarsLib: { getEnvVars: typeof getEnvVars }
|
||||
|
||||
describe('getEnvVars', (): void => {
|
||||
let getRCFileVarsStub: sinon.SinonStub<any, any>
|
||||
let getEnvFileVarsStub: sinon.SinonStub<any, any>
|
||||
let logInfoStub: sinon.SinonStub<any, any>
|
||||
let getRCFileVarsStub: sinon.SinonStub<any>
|
||||
let getEnvFileVarsStub: sinon.SinonStub<any>
|
||||
let logInfoStub: sinon.SinonStub<any> | undefined
|
||||
|
||||
before((): void => {
|
||||
getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars')
|
||||
getEnvFileVarsStub = sinon.stub(envFile, 'getEnvFileVars')
|
||||
before(async (): Promise<void> => {
|
||||
getRCFileVarsStub = sinon.stub()
|
||||
getEnvFileVarsStub = sinon.stub()
|
||||
getEnvVarsLib = await esmock('../src/get-env-vars.ts', {
|
||||
'../src/parse-rc-file': {
|
||||
getRCFileVars: getRCFileVarsStub
|
||||
},
|
||||
'../src/parse-env-file': {
|
||||
getEnvFileVars: getEnvFileVarsStub
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after((): void => {
|
||||
@ -21,15 +30,13 @@ describe('getEnvVars', (): void => {
|
||||
afterEach((): void => {
|
||||
sinon.resetHistory()
|
||||
sinon.resetBehavior()
|
||||
if (logInfoStub !== undefined) {
|
||||
logInfoStub.restore()
|
||||
}
|
||||
logInfoStub?.restore()
|
||||
})
|
||||
|
||||
it('should parse the json .rc file from the default path with the given environment',
|
||||
async (): Promise<void> => {
|
||||
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars({ rc: { environments: ['production'] } })
|
||||
const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
@ -37,16 +44,16 @@ describe('getEnvVars', (): void => {
|
||||
assert.lengthOf(getRCFileVarsStub.args[0][0].environments, 1)
|
||||
assert.equal(getRCFileVarsStub.args[0][0].environments[0], 'production')
|
||||
assert.equal(getRCFileVarsStub.args[0][0].filePath, './.env-cmdrc')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should print path of custom .rc file and environments to info for verbose',
|
||||
async (): Promise<void> => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
await getEnvVars({ rc: { environments: ['production'] }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should search all default .rc file paths', async (): Promise<void> => {
|
||||
@ -54,7 +61,7 @@ describe('getEnvVars', (): void => {
|
||||
pathError.name = 'PathError'
|
||||
getRCFileVarsStub.rejects(pathError)
|
||||
getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars({ rc: { environments: ['production'] } })
|
||||
const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
@ -69,9 +76,11 @@ describe('getEnvVars', (): void => {
|
||||
pathError.name = 'PathError'
|
||||
getRCFileVarsStub.rejects(pathError)
|
||||
try {
|
||||
await getEnvVars({ rc: { environments: ['production'] } })
|
||||
await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find/gi)
|
||||
assert.match(e.message, /\.rc file/gi)
|
||||
assert.match(e.message, /default paths/gi)
|
||||
@ -84,9 +93,10 @@ describe('getEnvVars', (): void => {
|
||||
pathError.name = 'PathError'
|
||||
getRCFileVarsStub.rejects(pathError)
|
||||
try {
|
||||
await getEnvVars({ rc: { environments: ['production'] }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
})
|
||||
@ -96,9 +106,11 @@ describe('getEnvVars', (): void => {
|
||||
environmentError.name = 'EnvironmentError'
|
||||
getRCFileVarsStub.rejects(environmentError)
|
||||
try {
|
||||
await getEnvVars({ rc: { environments: ['bad'] } })
|
||||
await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] } })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find environments/gi)
|
||||
assert.match(e.message, /\.rc file at path/gi)
|
||||
}
|
||||
@ -110,17 +122,18 @@ describe('getEnvVars', (): void => {
|
||||
environmentError.name = 'EnvironmentError'
|
||||
getRCFileVarsStub.rejects(environmentError)
|
||||
try {
|
||||
await getEnvVars({ rc: { environments: ['bad'] }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] }, verbose: true })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should find .rc file at custom path path', async (): Promise<void> => {
|
||||
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' }
|
||||
const envs = await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' },
|
||||
})
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
@ -134,9 +147,9 @@ describe('getEnvVars', (): void => {
|
||||
it('should print custom .rc file path to info for verbose', async (): Promise<void> => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
await getEnvVars({
|
||||
await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' },
|
||||
verbose: true
|
||||
verbose: true,
|
||||
})
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
})
|
||||
@ -146,11 +159,13 @@ describe('getEnvVars', (): void => {
|
||||
pathError.name = 'PathError'
|
||||
getRCFileVarsStub.rejects(pathError)
|
||||
try {
|
||||
await getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' }
|
||||
await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' },
|
||||
})
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find/gi)
|
||||
assert.match(e.message, /\.rc file at path/gi)
|
||||
}
|
||||
@ -162,12 +177,13 @@ describe('getEnvVars', (): void => {
|
||||
pathError.name = 'PathError'
|
||||
getRCFileVarsStub.rejects(pathError)
|
||||
try {
|
||||
await getEnvVars({
|
||||
await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['production'], filePath: '../.custom-rc' },
|
||||
verbose: true
|
||||
verbose: true,
|
||||
})
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
})
|
||||
@ -177,11 +193,13 @@ describe('getEnvVars', (): void => {
|
||||
environmentError.name = 'EnvironmentError'
|
||||
getRCFileVarsStub.rejects(environmentError)
|
||||
try {
|
||||
await getEnvVars({
|
||||
rc: { environments: ['bad'], filePath: '../.custom-rc' }
|
||||
await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['bad'], filePath: '../.custom-rc' },
|
||||
})
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find environments/gi)
|
||||
assert.match(e.message, /\.rc file at path/gi)
|
||||
}
|
||||
@ -194,20 +212,21 @@ describe('getEnvVars', (): void => {
|
||||
environmentError.name = 'EnvironmentError'
|
||||
getRCFileVarsStub.rejects(environmentError)
|
||||
try {
|
||||
await getEnvVars({
|
||||
await getEnvVarsLib.getEnvVars({
|
||||
rc: { environments: ['bad'], filePath: '../.custom-rc' },
|
||||
verbose: true
|
||||
verbose: true,
|
||||
})
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should parse the env file from a custom path', async (): Promise<void> => {
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars({ envFile: { filePath: '../.env-file' } })
|
||||
const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
@ -218,16 +237,18 @@ describe('getEnvVars', (): void => {
|
||||
it('should print path of .env file to info for verbose', async (): Promise<void> => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
})
|
||||
|
||||
it('should fail to find env file at custom path', async (): Promise<void> => {
|
||||
getEnvFileVarsStub.rejects('Not found.')
|
||||
try {
|
||||
await getEnvVars({ envFile: { filePath: '../.env-file' } })
|
||||
await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find/gi)
|
||||
assert.match(e.message, /\.env file at path/gi)
|
||||
}
|
||||
@ -237,43 +258,44 @@ describe('getEnvVars', (): void => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getEnvFileVarsStub.rejects('Not found.')
|
||||
try {
|
||||
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it(
|
||||
'should parse the env file from the default path if custom ' +
|
||||
'path not found and fallback option provided',
|
||||
'should parse the env file from the default path if custom '
|
||||
+ 'path not found and fallback option provided',
|
||||
async (): Promise<void> => {
|
||||
getEnvFileVarsStub.onFirstCall().rejects('File not found.')
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } })
|
||||
const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } })
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
assert.equal(getEnvFileVarsStub.callCount, 2)
|
||||
assert.equal(getEnvFileVarsStub.args[1][0], './.env')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it(
|
||||
'should print multiple times for failure to find .env file and ' +
|
||||
'failure to find fallback file to infor for verbose',
|
||||
'should print multiple times for failure to find .env file and '
|
||||
+ 'failure to find fallback file to infor for verbose',
|
||||
async (): Promise<void> => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getEnvFileVarsStub.onFirstCall().rejects('File not found.')
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true })
|
||||
assert.equal(logInfoStub.callCount, 2)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should parse the env file from the default path', async (): Promise<void> => {
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars()
|
||||
const envs = await getEnvVarsLib.getEnvVars()
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
@ -284,14 +306,14 @@ describe('getEnvVars', (): void => {
|
||||
it('should print path of .env file to info for verbose', async (): Promise<void> => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
await getEnvVars({ verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ verbose: true })
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
})
|
||||
|
||||
it('should search all default env file paths', async (): Promise<void> => {
|
||||
getEnvFileVarsStub.throws('Not found.')
|
||||
getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
|
||||
const envs = await getEnvVars()
|
||||
const envs = await getEnvVarsLib.getEnvVars()
|
||||
assert.isOk(envs)
|
||||
assert.lengthOf(Object.keys(envs), 1)
|
||||
assert.equal(envs.THANKS, 'FOR ALL THE FISH')
|
||||
@ -302,9 +324,11 @@ describe('getEnvVars', (): void => {
|
||||
it('should fail to find env file at default path', async (): Promise<void> => {
|
||||
getEnvFileVarsStub.rejects('Not found.')
|
||||
try {
|
||||
await getEnvVars()
|
||||
await getEnvVarsLib.getEnvVars()
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /failed to find/gi)
|
||||
assert.match(e.message, /\.env file/gi)
|
||||
assert.match(e.message, /default paths/gi)
|
||||
@ -317,11 +341,12 @@ describe('getEnvVars', (): void => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
getEnvFileVarsStub.rejects('Not found.')
|
||||
try {
|
||||
await getEnvVars({ verbose: true })
|
||||
await getEnvVarsLib.getEnvVars({ verbose: true })
|
||||
assert.fail('should not get here.')
|
||||
} catch (e) {
|
||||
}
|
||||
catch {
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint @typescript-eslint/no-non-null-assertion: 0 */
|
||||
import * as sinon from 'sinon'
|
||||
import { default as sinon } from 'sinon'
|
||||
import { assert } from 'chai'
|
||||
import { parseArgs } from '../src/parse-args'
|
||||
import { parseArgs } from '../src/parse-args.js'
|
||||
|
||||
describe('parseArgs', (): void => {
|
||||
const command = 'command'
|
||||
@ -9,7 +9,7 @@ describe('parseArgs', (): void => {
|
||||
const environments = ['development', 'production']
|
||||
const rcFilePath = './.env-cmdrc'
|
||||
const envFilePath = './.env'
|
||||
let logInfoStub: sinon.SinonStub<any, any>
|
||||
let logInfoStub: sinon.SinonStub<any>
|
||||
|
||||
before((): void => {
|
||||
logInfoStub = sinon.stub(console, 'info')
|
||||
@ -27,13 +27,13 @@ describe('parseArgs', (): void => {
|
||||
it('should parse environment value', (): void => {
|
||||
const res = parseArgs(['-e', environments[0], command])
|
||||
assert.exists(res.rc)
|
||||
assert.sameOrderedMembers(res.rc!.environments, [environments[0]])
|
||||
assert.sameOrderedMembers(res.rc.environments, [environments[0]])
|
||||
})
|
||||
|
||||
it('should parse multiple environment values', (): void => {
|
||||
const res = parseArgs(['-e', environments.join(','), command])
|
||||
assert.exists(res.rc)
|
||||
assert.sameOrderedMembers(res.rc!.environments, environments)
|
||||
assert.sameOrderedMembers(res.rc.environments, environments)
|
||||
})
|
||||
|
||||
it('should parse command value', (): void => {
|
||||
@ -53,37 +53,37 @@ describe('parseArgs', (): void => {
|
||||
assert.sameOrderedMembers(res.commandArgs, commandFlags)
|
||||
assert.notOk(res.options!.useShell)
|
||||
assert.notOk(res.envFile)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should parse override option', (): void => {
|
||||
const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs])
|
||||
assert.exists(res.options)
|
||||
assert.isTrue(res.options!.noOverride)
|
||||
assert.isTrue(res.options.noOverride)
|
||||
})
|
||||
|
||||
it('should parse use shell option', (): void => {
|
||||
const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs])
|
||||
assert.exists(res.options)
|
||||
assert.isTrue(res.options!.useShell)
|
||||
assert.isTrue(res.options.useShell)
|
||||
})
|
||||
|
||||
it('should parse rc file path', (): void => {
|
||||
const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs])
|
||||
assert.exists(res.rc)
|
||||
assert.equal(res.rc!.filePath, rcFilePath)
|
||||
assert.equal(res.rc.filePath, rcFilePath)
|
||||
})
|
||||
|
||||
it('should parse env file path', (): void => {
|
||||
const res = parseArgs(['-f', envFilePath, command, ...commandArgs])
|
||||
assert.exists(res.envFile)
|
||||
assert.equal(res.envFile!.filePath, envFilePath)
|
||||
assert.equal(res.envFile.filePath, envFilePath)
|
||||
})
|
||||
|
||||
it('should parse fallback option', (): void => {
|
||||
const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs])
|
||||
assert.exists(res.envFile)
|
||||
assert.isTrue(res.envFile!.fallback)
|
||||
assert.isTrue(res.envFile.fallback)
|
||||
})
|
||||
|
||||
it('should print to console.info if --verbose flag is passed', (): void => {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { assert } from 'chai'
|
||||
import {
|
||||
stripEmptyLines, stripComments, parseEnvVars,
|
||||
parseEnvString, getEnvFileVars
|
||||
} from '../src/parse-env-file'
|
||||
parseEnvString, getEnvFileVars,
|
||||
} from '../src/parse-env-file.js'
|
||||
|
||||
describe('stripEmptyLines', (): void => {
|
||||
it('should strip out all empty lines', (): void => {
|
||||
@ -20,10 +20,12 @@ describe('stripComments', (): void => {
|
||||
|
||||
describe('parseEnvVars', (): void => {
|
||||
it('should parse out all env vars in string when not ending with \'\\n\'', (): void => {
|
||||
const envVars = parseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING')
|
||||
const envVars = parseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\nNUMBER=42\nBOOLEAN=true')
|
||||
assert(envVars.BOB === 'COOL')
|
||||
assert(envVars.NODE_ENV === 'dev')
|
||||
assert(envVars.ANSWER === '42 AND COUNTING')
|
||||
assert(envVars.NUMBER === 42)
|
||||
assert(envVars.BOOLEAN === true)
|
||||
})
|
||||
|
||||
it('should parse out all env vars in string with format \'key=value\'', (): void => {
|
||||
@ -96,7 +98,7 @@ describe('parseEnvString', (): void => {
|
||||
const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
|
||||
assert(env.BOB === 'COOL')
|
||||
assert(env.NODE_ENV === 'dev')
|
||||
assert(env.ANSWER === '42')
|
||||
assert(env.ANSWER === 42)
|
||||
})
|
||||
})
|
||||
|
||||
@ -107,7 +109,8 @@ describe('getEnvFileVars', (): void => {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION',
|
||||
GALAXY: 'hitch\nhiking'
|
||||
GALAXY: 'hitch\nhiking',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
|
||||
@ -117,24 +120,42 @@ describe('getEnvFileVars', (): void => {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN\n PRODUCTION',
|
||||
GALAXY: 'hitch\nhiking\n\n'
|
||||
GALAXY: 'hitch\nhiking\n\n',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse a js file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test.js')
|
||||
it('should parse a js/cjs file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test.cjs')
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
GALAXY: 'hitch\nhiking'
|
||||
GALAXY: 'hitch\nhiking',
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse an async js file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test-async.js')
|
||||
it('should parse an async js/cjs file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test-async.cjs')
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0
|
||||
ANSWER: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse a mjs file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test.mjs')
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
GALAXY: 'hitch\nhiking',
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse an async mjs file', async (): Promise<void> => {
|
||||
const env = await getEnvFileVars('./test/test-files/test-async.mjs')
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
})
|
||||
})
|
||||
|
||||
@ -142,9 +163,10 @@ describe('getEnvFileVars', (): void => {
|
||||
const env = await getEnvFileVars('./test/test-files/test')
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: '42',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN=PRODUCTION',
|
||||
GALAXY: 'hitch\nhiking'
|
||||
GALAXY: 'hitch\nhiking',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
|
||||
@ -152,7 +174,9 @@ describe('getEnvFileVars', (): void => {
|
||||
try {
|
||||
await getEnvFileVars('./test/test-files/non-existent-file')
|
||||
assert.fail('Should not get here!')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /file path/gi)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { assert } from 'chai'
|
||||
import { getRCFileVars } from '../src/parse-rc-file'
|
||||
import { getRCFileVars } from '../src/parse-rc-file.js'
|
||||
|
||||
const rcFilePath = './test/test-files/.rc-test'
|
||||
const rcJSONFilePath = './test/test-files/.rc-test.json'
|
||||
@ -11,7 +11,8 @@ describe('getRCFileVars', (): void => {
|
||||
assert.deepEqual(res, {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION'
|
||||
ONLY: 'IN PRODUCTION',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,7 +21,7 @@ describe('getRCFileVars', (): void => {
|
||||
assert.exists(res)
|
||||
assert.deepEqual(res, {
|
||||
THANKS: 'FOR MORE FISHIES',
|
||||
ANSWER: 21
|
||||
ANSWER: 21,
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,7 +29,9 @@ describe('getRCFileVars', (): void => {
|
||||
try {
|
||||
await getRCFileVars({ environments: ['bad'], filePath: 'bad-path' })
|
||||
assert.fail('Should not get here!')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /\.rc file at path/gi)
|
||||
}
|
||||
})
|
||||
@ -37,7 +40,9 @@ describe('getRCFileVars', (): void => {
|
||||
try {
|
||||
await getRCFileVars({ environments: ['bad'], filePath: rcFilePath })
|
||||
assert.fail('Should not get here!')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /environments/gi)
|
||||
}
|
||||
})
|
||||
@ -46,20 +51,36 @@ describe('getRCFileVars', (): void => {
|
||||
try {
|
||||
await getRCFileVars({ environments: ['bad'], filePath: './test/test-files/.rc-test-bad-format' })
|
||||
assert.fail('Should not get here!')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /parse/gi)
|
||||
}
|
||||
})
|
||||
|
||||
it('should parse an async js .rc file', async (): Promise<void> => {
|
||||
it('should parse an async js/cjs .rc file', async (): Promise<void> => {
|
||||
const env = await getRCFileVars({
|
||||
environments: ['production'],
|
||||
filePath: './test/test-files/.rc-test-async.js'
|
||||
filePath: './test/test-files/.rc-test-async.cjs',
|
||||
})
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION'
|
||||
ONLY: 'IN PRODUCTION',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse an async mjs .rc file', async (): Promise<void> => {
|
||||
const env = await getRCFileVars({
|
||||
environments: ['production'],
|
||||
filePath: './test/test-files/.rc-test-async.mjs',
|
||||
})
|
||||
assert.deepEqual(env, {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION',
|
||||
BRINGATOWEL: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { assert } from 'chai'
|
||||
import * as sinon from 'sinon'
|
||||
import { TermSignals } from '../src/signal-termination'
|
||||
import { default as sinon } from 'sinon'
|
||||
import { TermSignals } from '../src/signal-termination.js'
|
||||
import { ChildProcess } from 'child_process'
|
||||
|
||||
type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void
|
||||
|
||||
describe('signal-termination', (): void => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
@ -15,8 +18,8 @@ describe('signal-termination', (): void => {
|
||||
describe('TermSignals', (): void => {
|
||||
describe('_uncaughtExceptionHandler', (): void => {
|
||||
const term = new TermSignals()
|
||||
let logStub: sinon.SinonStub<any, any>
|
||||
let processStub: sinon.SinonStub<any, any>
|
||||
let logStub: sinon.SinonStub<any>
|
||||
let processStub: sinon.SinonStub<any>
|
||||
|
||||
beforeEach((): void => {
|
||||
logStub = sandbox.stub(console, 'error')
|
||||
@ -39,7 +42,7 @@ describe('signal-termination', (): void => {
|
||||
|
||||
describe('_removeProcessListeners', (): void => {
|
||||
const term = new TermSignals()
|
||||
let removeListenerStub: sinon.SinonStub<any, any>
|
||||
let removeListenerStub: sinon.SinonStub<any>
|
||||
before((): void => {
|
||||
removeListenerStub = sandbox.stub(process, 'removeListener')
|
||||
})
|
||||
@ -50,15 +53,15 @@ describe('signal-termination', (): void => {
|
||||
|
||||
it('should remove all listeners from default signals and exit signal', (): void => {
|
||||
term._removeProcessListeners()
|
||||
assert.equal(removeListenerStub.callCount, 4)
|
||||
assert.equal(removeListenerStub.args[3][0], 'exit')
|
||||
assert.equal(removeListenerStub.callCount, 3)
|
||||
assert.oneOf(removeListenerStub.args[2][0], ['SIGTERM', 'SIGINT', 'SIGHUP'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('_terminateProcess', (): void => {
|
||||
const term = new TermSignals()
|
||||
let exitStub: sinon.SinonStub<any, any>
|
||||
let killStub: sinon.SinonStub<any, any>
|
||||
let exitStub: sinon.SinonStub<any>
|
||||
let killStub: sinon.SinonStub<any>
|
||||
|
||||
beforeEach((): void => {
|
||||
exitStub = sandbox.stub(process, 'exit')
|
||||
@ -71,6 +74,7 @@ describe('signal-termination', (): void => {
|
||||
|
||||
it('should call exit method on parent process if no signal provided', (): void => {
|
||||
term._terminateProcess(0)
|
||||
// We here test code that in reality is unreachable.
|
||||
assert.equal(exitStub.callCount, 1)
|
||||
})
|
||||
|
||||
@ -82,7 +86,7 @@ describe('signal-termination', (): void => {
|
||||
})
|
||||
|
||||
it('should call kill method with correct kill signal', (): void => {
|
||||
term._terminateProcess(1, 'SIGINT')
|
||||
term._terminateProcess('SIGINT')
|
||||
assert.equal(killStub.callCount, 1)
|
||||
assert.equal(exitStub.callCount, 0)
|
||||
assert.equal(killStub.args[0][1], 'SIGINT')
|
||||
@ -92,7 +96,9 @@ describe('signal-termination', (): void => {
|
||||
try {
|
||||
term._terminateProcess()
|
||||
assert.fail('should not get here')
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
assert.instanceOf(e, Error)
|
||||
assert.match(e.message, /unable to terminate parent process/gi)
|
||||
}
|
||||
})
|
||||
@ -100,8 +106,8 @@ describe('signal-termination', (): void => {
|
||||
|
||||
describe('handleUncaughtExceptions', (): void => {
|
||||
const term = new TermSignals()
|
||||
let processOnStub: sinon.SinonStub<any, any>
|
||||
let _uncaughtExceptionHandlerStub: sinon.SinonStub<any, any>
|
||||
let processOnStub: sinon.SinonStub<any>
|
||||
let _uncaughtExceptionHandlerStub: sinon.SinonStub<any>
|
||||
|
||||
before((): void => {
|
||||
processOnStub = sandbox.stub(process, 'on')
|
||||
@ -116,22 +122,22 @@ describe('signal-termination', (): void => {
|
||||
term.handleUncaughtExceptions()
|
||||
assert.equal(processOnStub.callCount, 1)
|
||||
assert.equal(_uncaughtExceptionHandlerStub.callCount, 0)
|
||||
processOnStub.args[0][1]()
|
||||
;(processOnStub.args[0][1] as () => void)()
|
||||
assert.equal(_uncaughtExceptionHandlerStub.callCount, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleTermSignals', (): void => {
|
||||
let term: TermSignals
|
||||
let procKillStub: sinon.SinonStub<any, any>
|
||||
let procOnStub: sinon.SinonStub<any, any>
|
||||
let processOnceStub: sinon.SinonStub<any, any>
|
||||
let _removeProcessListenersStub: sinon.SinonStub<any, any>
|
||||
let _terminateProcessStub: sinon.SinonStub<any, any>
|
||||
let logInfoStub: sinon.SinonStub<any, any>
|
||||
let proc: any
|
||||
let procKillStub: sinon.SinonStub<any>
|
||||
let procOnStub: sinon.SinonStub<any>
|
||||
let processOnceStub: sinon.SinonStub<any>
|
||||
let _removeProcessListenersStub: sinon.SinonStub<any>
|
||||
let _terminateProcessStub: sinon.SinonStub<any>
|
||||
let logInfoStub: sinon.SinonStub<any>
|
||||
let proc: ChildProcess
|
||||
|
||||
function setup (verbose: boolean = false): void {
|
||||
function setup(verbose = false): void {
|
||||
term = new TermSignals({ verbose })
|
||||
procKillStub = sandbox.stub()
|
||||
procOnStub = sandbox.stub()
|
||||
@ -140,8 +146,8 @@ describe('signal-termination', (): void => {
|
||||
_terminateProcessStub = sandbox.stub(term, '_terminateProcess')
|
||||
proc = {
|
||||
kill: procKillStub,
|
||||
on: procOnStub
|
||||
}
|
||||
on: procOnStub,
|
||||
} as unknown as ChildProcess
|
||||
}
|
||||
|
||||
beforeEach((): void => {
|
||||
@ -162,7 +168,7 @@ describe('signal-termination', (): void => {
|
||||
it('should terminate child process if parent process terminated', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1]('SIGTERM', 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM')
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(procKillStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
@ -175,17 +181,17 @@ describe('signal-termination', (): void => {
|
||||
logInfoStub = sandbox.stub(console, 'info')
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1]('SIGTERM', 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM')
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
})
|
||||
|
||||
it('should not terminate child process if child process termination ' +
|
||||
'has already been called by parent', (): void => {
|
||||
it('should not terminate child process if child process termination '
|
||||
+ 'has already been called by parent', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1]('SIGINT', 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGINT')
|
||||
assert.isOk(term._exitCalled)
|
||||
processOnceStub.args[0][1]('SIGTERM', 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM')
|
||||
assert.equal(_removeProcessListenersStub.callCount, 2)
|
||||
assert.equal(procKillStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
@ -195,7 +201,7 @@ describe('signal-termination', (): void => {
|
||||
it('should convert and use number signal as code', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1](4, 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.ExitListener)(1)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(procKillStub.callCount, 1)
|
||||
assert.equal(procKillStub.args[0][0], 'SIGINT')
|
||||
@ -203,13 +209,13 @@ describe('signal-termination', (): void => {
|
||||
assert.isOk(term._exitCalled)
|
||||
})
|
||||
|
||||
it('should not use signal number as code if value is 0', (): void => {
|
||||
it('should not use default signal if code is 0', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1](0, 1)
|
||||
;(processOnceStub.args[0][1] as NodeJS.ExitListener)(0)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(procKillStub.callCount, 1)
|
||||
assert.equal(procKillStub.args[0], 1)
|
||||
assert.equal(procKillStub.args[0], 0)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.isOk(term._exitCalled)
|
||||
})
|
||||
@ -217,7 +223,7 @@ describe('signal-termination', (): void => {
|
||||
it('should use signal value and default SIGINT signal if code is undefined', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
processOnceStub.args[0][1](4, undefined)
|
||||
;(processOnceStub.args[0][1] as NodeJS.ExitListener)(4)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(procKillStub.callCount, 1)
|
||||
assert.equal(procKillStub.args[0][0], 'SIGINT')
|
||||
@ -228,7 +234,7 @@ describe('signal-termination', (): void => {
|
||||
it('should terminate parent process if child process terminated', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](1, 'SIGTERM')
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM')
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.isOk(term._exitCalled)
|
||||
@ -240,31 +246,31 @@ describe('signal-termination', (): void => {
|
||||
logInfoStub = sandbox.stub(console, 'info')
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](1, 'SIGTERM')
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM')
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
})
|
||||
|
||||
it(
|
||||
'should print parent process terminated to info for verbose when ' +
|
||||
'code and signal are undefined',
|
||||
'should print parent process terminated to info for verbose when '
|
||||
+ 'code and signal are undefined',
|
||||
(): void => {
|
||||
sandbox.restore()
|
||||
setup(true)
|
||||
logInfoStub = sandbox.stub(console, 'info')
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](undefined, null)
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(null, null)
|
||||
assert.equal(logInfoStub.callCount, 1)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('should not terminate parent process if parent process already terminating', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](1, 'SIGINT')
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGINT')
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
procOnStub.args[0][1](1, 'SIGTERM')
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM')
|
||||
assert.equal(_removeProcessListenersStub.callCount, 2)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.isOk(term._exitCalled)
|
||||
@ -273,7 +279,7 @@ describe('signal-termination', (): void => {
|
||||
it('should convert null signal value to undefined', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](0, null)
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(0, null)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[1], undefined)
|
||||
@ -283,33 +289,30 @@ describe('signal-termination', (): void => {
|
||||
it('should convert and use number signal as code', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](1, 4)
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 4)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 4)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT')
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT')
|
||||
assert.isOk(term._exitCalled)
|
||||
})
|
||||
|
||||
it('should not use signal number as code if value is 0', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](1, 0)
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(1, 0)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1)
|
||||
assert.isUndefined(_terminateProcessStub.firstCall.args[1])
|
||||
assert.isOk(term._exitCalled)
|
||||
})
|
||||
|
||||
it('should use signal value and default SIGINT signal if code is undefined', (): void => {
|
||||
assert.notOk(term._exitCalled)
|
||||
term.handleTermSignals(proc)
|
||||
procOnStub.args[0][1](null, 1)
|
||||
;(procOnStub.args[0][1] as ChildExitListener)(null, 1)
|
||||
assert.equal(_removeProcessListenersStub.callCount, 1)
|
||||
assert.equal(_terminateProcessStub.callCount, 1)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1)
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT')
|
||||
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT')
|
||||
assert.isOk(term._exitCalled)
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"production": {
|
||||
"THANKS": "FOR WHAT?!",
|
||||
"ANSWER": 42,
|
||||
"ONLY": "IN PRODUCTION"
|
||||
"ONLY": "IN PRODUCTION",
|
||||
"BRINGATOWEL": true
|
||||
}
|
||||
}
|
||||
21
test/test-files/.rc-test-async.cjs
Normal file
21
test/test-files/.rc-test-async.cjs
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('resolved')
|
||||
resolve({
|
||||
development: {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
},
|
||||
test: {
|
||||
THANKS: 'FOR MORE FISHIES',
|
||||
ANSWER: 21,
|
||||
},
|
||||
production: {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION',
|
||||
BRINGATOWEL: true,
|
||||
},
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
@ -1,19 +0,0 @@
|
||||
module.exports = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
'development': {
|
||||
'THANKS': 'FOR ALL THE FISH',
|
||||
'ANSWER': 0
|
||||
},
|
||||
'test': {
|
||||
'THANKS': 'FOR MORE FISHIES',
|
||||
'ANSWER': 21
|
||||
},
|
||||
'production': {
|
||||
'THANKS': 'FOR WHAT?!',
|
||||
'ANSWER': 42,
|
||||
'ONLY': 'IN PRODUCTION'
|
||||
}
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
20
test/test-files/.rc-test-async.mjs
Normal file
20
test/test-files/.rc-test-async.mjs
Normal file
@ -0,0 +1,20 @@
|
||||
export default new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
development: {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
},
|
||||
test: {
|
||||
THANKS: 'FOR MORE FISHIES',
|
||||
ANSWER: 21,
|
||||
},
|
||||
production: {
|
||||
THANKS: 'FOR WHAT?!',
|
||||
ANSWER: 42,
|
||||
ONLY: 'IN PRODUCTION',
|
||||
BRINGATOWEL: true,
|
||||
},
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
@ -10,6 +10,7 @@
|
||||
"production": {
|
||||
"THANKS": "FOR WHAT?!",
|
||||
"ANSWER": 42,
|
||||
"ONLY": "IN PRODUCTION"
|
||||
"ONLY": "IN PRODUCTION",
|
||||
"BRINGATOWEL": true
|
||||
}
|
||||
}
|
||||
@ -2,3 +2,4 @@ THANKS = FOR WHAT?!
|
||||
ANSWER=42
|
||||
ONLY= "IN=PRODUCTION"
|
||||
GALAXY="hitch\nhiking"
|
||||
BRINGATOWEL=true
|
||||
|
||||
@ -2,7 +2,7 @@ module.exports = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0
|
||||
ANSWER: 0,
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
8
test/test-files/test-async.mjs
Normal file
8
test/test-files/test-async.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
export default new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
@ -2,5 +2,6 @@
|
||||
"THANKS": "FOR WHAT?!",
|
||||
"ANSWER": 42,
|
||||
"ONLY": "IN\n PRODUCTION",
|
||||
"GALAXY": "hitch\nhiking\n\n"
|
||||
"GALAXY": "hitch\nhiking\n\n",
|
||||
"BRINGATOWEL": true
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
GALAXY: 'hitch\nhiking'
|
||||
GALAXY: 'hitch\nhiking',
|
||||
}
|
||||
@ -2,5 +2,6 @@
|
||||
"THANKS": "FOR WHAT?!",
|
||||
"ANSWER": 42,
|
||||
"ONLY": "IN PRODUCTION",
|
||||
"GALAXY": "hitch\nhiking"
|
||||
"GALAXY": "hitch\nhiking",
|
||||
"BRINGATOWEL": true
|
||||
}
|
||||
5
test/test-files/test.mjs
Normal file
5
test/test-files/test.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
THANKS: 'FOR ALL THE FISH',
|
||||
ANSWER: 0,
|
||||
GALAXY: 'hitch\nhiking',
|
||||
}
|
||||
18
test/tsconfig.json
Normal file
18
test/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"esModuleInterop": false,
|
||||
"lib": ["es2023"],
|
||||
"module": "Node16",
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"./test-files/.rc-test-async.cjs",
|
||||
"./test-files/.rc-test-async.mjs",
|
||||
]
|
||||
}
|
||||
@ -1,14 +1,31 @@
|
||||
import * as os from 'os'
|
||||
import * as process from 'process'
|
||||
import * as path from 'path'
|
||||
import { homedir } from 'node:os'
|
||||
import { cwd } from 'node:process'
|
||||
import { normalize } from 'node:path'
|
||||
import { assert } from 'chai'
|
||||
import * as sinon from 'sinon'
|
||||
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils'
|
||||
import { default as sinon } from 'sinon'
|
||||
import { default as esmock } from 'esmock'
|
||||
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils.js'
|
||||
|
||||
let utilsLib: {
|
||||
resolveEnvFilePath: typeof resolveEnvFilePath,
|
||||
parseArgList: typeof parseArgList,
|
||||
isPromise: typeof isPromise
|
||||
}
|
||||
|
||||
describe('utils', (): void => {
|
||||
describe('resolveEnvFilePath', (): void => {
|
||||
const homePath = os.homedir()
|
||||
const currentDir = process.cwd()
|
||||
const homePath = homedir()
|
||||
const currentDir = cwd()
|
||||
let homedirStub: sinon.SinonStub<any>
|
||||
|
||||
before(async (): Promise<void> => {
|
||||
homedirStub = sinon.stub()
|
||||
utilsLib = await esmock('../src/utils.js', {
|
||||
'node:os': {
|
||||
homedir: homedirStub
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach((): void => {
|
||||
sinon.restore()
|
||||
@ -16,18 +33,17 @@ describe('utils', (): void => {
|
||||
|
||||
it('should return an absolute path, given a relative path', (): void => {
|
||||
const res = resolveEnvFilePath('./bob')
|
||||
assert.equal(res, path.normalize(`${currentDir}/bob`))
|
||||
assert.equal(res, normalize(`${currentDir}/bob`))
|
||||
})
|
||||
|
||||
it('should return an absolute path, given a path with ~ for home directory', (): void => {
|
||||
const res = resolveEnvFilePath('~/bob')
|
||||
assert.equal(res, path.normalize(`${homePath}/bob`))
|
||||
assert.equal(res, normalize(`${homePath}/bob`))
|
||||
})
|
||||
|
||||
it('should not attempt to replace ~ if home dir does not exist', (): void => {
|
||||
sinon.stub(os, 'homedir')
|
||||
const res = resolveEnvFilePath('~/bob')
|
||||
assert.equal(res, path.normalize(`${currentDir}/~/bob`))
|
||||
const res = utilsLib.resolveEnvFilePath('~/bob')
|
||||
assert.equal(res, normalize(`${currentDir}/~/bob`))
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,5 +64,13 @@ describe('utils', (): void => {
|
||||
const res = isPromise({})
|
||||
assert.isFalse(res)
|
||||
})
|
||||
it('should return false for string', (): void => {
|
||||
const res = isPromise('test')
|
||||
assert.isFalse(res)
|
||||
})
|
||||
it('should return false for undefined', (): void => {
|
||||
const res = isPromise(undefined)
|
||||
assert.isFalse(res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"test/**/*",
|
||||
"bin/**/*"
|
||||
]
|
||||
}
|
||||
@ -1,18 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"esModuleInterop": false,
|
||||
"lib": ["es2023"],
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"outDir": "./dist",
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"lib": [
|
||||
"es2018",
|
||||
"es2019",
|
||||
"es2020"
|
||||
]
|
||||
"target": "ES2022",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user