diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e93b98..e889481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 10.1.0 - Pending + +- **Feature**: Added a new `--verbose` flag that prints additional debugging info to `console.info` +- **Change**: Updated `commander` dependency to `v4` + ## 10.0.1 - **Fix**: Fixed bug introduced by strict equal checking for `undefined` when the value was `null`. This diff --git a/README.md b/README.md index 5c9a544..5db9714 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Options: --fallback Fallback to default env file path, if custom env file path not found --no-override Do not override existing environment variables --use-shell Execute the command in a new shell with the given environment + --verbose Print helpful debugging information -h, --help output usage information ``` @@ -186,6 +187,7 @@ A function that executes a given command in a new child process with the given e - **`options`** { `object` } - **`noOverride`** { `boolean` }: Prevent `.env` file vars from overriding existing `process.env` vars (default: `false`) - **`useShell`** { `boolean` }: Runs command inside a new shell instance (default: `false`) + - **`verbose`** { `boolean` }: Prints extra debug logs to `console.info` (default: `false`) - **Returns** { `Promise` }: key is env var name and value is the env var value ### `GetEnvVars` @@ -199,6 +201,7 @@ A function that parses environment variables from a `.env` or a `.rc` file - **`rc`** { `object` } - **`environments`** { `string[]` }: List of environment to read from the `.rc` file - **`filePath`** { `string` }: Custom path to the `.rc` file (defaults to: `./.env-cmdrc(|.js|.json)`) + - **`verbose`** { `boolean` }: Prints extra debug logs to `console.info` (default: `false`) - **Returns** { `Promise` }: key is env var name and value is the env var value ## Why diff --git a/dist/env-cmd.js b/dist/env-cmd.js index 8f24b2e..ea5162d 100644 --- a/dist/env-cmd.js +++ b/dist/env-cmd.js @@ -32,7 +32,7 @@ exports.CLI = CLI; * @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value */ async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) { - let env = await get_env_vars_1.getEnvVars({ envFile, rc }); + let env = await get_env_vars_1.getEnvVars({ envFile, rc, verbose: options.verbose }); // Override the merge order if --no-override flag set if (options.noOverride === true) { env = Object.assign({}, env, process.env); @@ -48,7 +48,7 @@ async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) { env }); // Handle any termination signals for parent and child proceses - const signals = new signal_termination_1.TermSignals(); + const signals = new signal_termination_1.TermSignals({ verbose: options.verbose }); signals.handleUncaughtExceptions(); signals.handleTermSignals(proc); return env; diff --git a/dist/get-env-vars.d.ts b/dist/get-env-vars.d.ts index d076d89..dc7ea2c 100644 --- a/dist/get-env-vars.d.ts +++ b/dist/get-env-vars.d.ts @@ -2,15 +2,17 @@ import { GetEnvVarOptions } from './types'; export declare function getEnvVars(options?: GetEnvVarOptions): Promise<{ [key: string]: any; }>; -export declare function getEnvFile({ filePath, fallback }: { +export declare function getEnvFile({ filePath, fallback, verbose }: { filePath?: string; fallback?: boolean; + verbose?: boolean; }): Promise<{ [key: string]: any; }>; -export declare function getRCFile({ environments, filePath }: { +export declare function getRCFile({ environments, filePath, verbose }: { environments: string[]; filePath?: string; + verbose?: boolean; }): Promise<{ [key: string]: any; }>; diff --git a/dist/get-env-vars.js b/dist/get-env-vars.js index 3723f59..9dd5403 100644 --- a/dist/get-env-vars.js +++ b/dist/get-env-vars.js @@ -8,54 +8,103 @@ async function getEnvVars(options = {}) { options.envFile = options.envFile !== undefined ? options.envFile : {}; // Check for rc file usage if (options.rc !== undefined) { - return getRCFile({ environments: options.rc.environments, filePath: options.rc.filePath }); + return getRCFile({ + environments: options.rc.environments, + filePath: options.rc.filePath, + verbose: options.verbose + }); } - return getEnvFile({ filePath: options.envFile.filePath, fallback: options.envFile.fallback }); + return getEnvFile({ + filePath: options.envFile.filePath, + fallback: options.envFile.fallback, + verbose: options.verbose + }); } exports.getEnvVars = getEnvVars; -async function getEnvFile({ filePath, fallback }) { +async function getEnvFile({ filePath, fallback, verbose }) { // Use env file if (filePath !== undefined) { try { - return await parse_env_file_1.getEnvFileVars(filePath); + const env = await parse_env_file_1.getEnvFileVars(filePath); + if (verbose === true) { + console.info(`Found .env file at path: ${filePath}`); + } + return env; + } + catch (e) { + if (verbose === true) { + console.info(`Failed to find .env file at path: ${filePath}`); + } } - catch (e) { } if (fallback !== true) { - throw new Error(`Unable to locate env file at location (${filePath})`); + throw new Error(`Failed to find .env file at path: ${filePath}`); } } // Use the default env file locations for (const path of ENV_FILE_DEFAULT_LOCATIONS) { try { - return await parse_env_file_1.getEnvFileVars(path); + const env = await parse_env_file_1.getEnvFileVars(path); + if (verbose === true) { + console.info(`Found .env file at default path: ${path}`); + } + return env; } catch (e) { } } - throw new Error(`Unable to locate env file at default locations (${ENV_FILE_DEFAULT_LOCATIONS})`); + const error = `Failed to find .env file at default paths: ${ENV_FILE_DEFAULT_LOCATIONS}`; + if (verbose === true) { + console.info(error); + } + throw new Error(error); } exports.getEnvFile = getEnvFile; -async function getRCFile({ environments, filePath }) { +async function getRCFile({ environments, filePath, verbose }) { // User provided an .rc file path if (filePath !== undefined) { try { - return await parse_rc_file_1.getRCFileVars({ environments, filePath }); + const env = await parse_rc_file_1.getRCFileVars({ environments, filePath }); + if (verbose === true) { + console.info(`Found environments: ${environments} for .rc file at path: ${filePath}`); + } + return env; } catch (e) { - if (e.name !== 'PathError') - console.error(e); - throw new Error(`Unable to locate .rc file at location (${filePath})`); + if (e.name === 'PathError') { + if (verbose === true) { + console.info(`Failed to find .rc file at path: ${filePath}`); + } + } + if (e.name === 'EnvironmentError') { + if (verbose === true) { + console.info(`Failed to find environments: ${environments} for .rc file at path: ${filePath}`); + } + } + throw e; } } // Use the default .rc file locations - for (const filePath of RC_FILE_DEFAULT_LOCATIONS) { + for (const path of RC_FILE_DEFAULT_LOCATIONS) { try { - return await parse_rc_file_1.getRCFileVars({ environments, filePath }); + const env = await parse_rc_file_1.getRCFileVars({ environments, filePath: path }); + if (verbose === true) { + console.info(`Found environments: ${environments} for default .rc file at path: ${path}`); + } + return env; } catch (e) { - if (e.name !== 'PathError') - console.error(e); + if (e.name === 'EnvironmentError') { + const errorText = `Failed to find environments: ${environments} for .rc file at path: ${path}`; + if (verbose === true) { + console.info(errorText); + } + throw new Error(errorText); + } } } - throw new Error(`Unable to locate .rc file at default locations (${RC_FILE_DEFAULT_LOCATIONS})`); + const errorText = `Failed to find .rc file at default paths: ${RC_FILE_DEFAULT_LOCATIONS}`; + if (verbose === true) { + console.info(errorText); + } + throw new Error(errorText); } exports.getRCFile = getRCFile; diff --git a/dist/parse-args.js b/dist/parse-args.js index 11d93df..ba3630a 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -14,6 +14,7 @@ function parseArgs(args) { program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))); const noOverride = !program.override; const useShell = !!program.useShell; + const verbose = !!program.verbose; let rc; if (program.environments !== undefined && program.environments.length !== 0) { rc = { @@ -28,16 +29,21 @@ function parseArgs(args) { fallback: program.fallback }; } - return { + const options = { command, commandArgs, envFile, rc, options: { noOverride, - useShell + useShell, + verbose } }; + if (verbose === true) { + console.info(`Options: ${JSON.stringify(options, null, 0)}`); + } + return options; } exports.parseArgs = parseArgs; function parseArgsUsingCommander(args) { @@ -51,6 +57,7 @@ function parseArgsUsingCommander(args) { .option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') .option('--use-shell', 'Execute the command in a new shell with the given environment') + .option('--verbose', 'Print helpful debugging information') .parse(['_', '_', ...args]); } exports.parseArgsUsingCommander = parseArgsUsingCommander; diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index c049c5c..a5b27d7 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -10,7 +10,9 @@ const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js']; async function getEnvFileVars(envFilePath) { const absolutePath = utils_1.resolveEnvFilePath(envFilePath); if (!fs.existsSync(absolutePath)) { - throw new Error(`Invalid env file path (${envFilePath}).`); + const pathError = new Error(`Invalid env file path (${envFilePath}).`); + pathError.name = 'PathError'; + throw pathError; } // Get the file extension const ext = path.extname(absolutePath).toLowerCase(); diff --git a/dist/parse-rc-file.d.ts b/dist/parse-rc-file.d.ts index 1be55bd..053adf2 100644 --- a/dist/parse-rc-file.d.ts +++ b/dist/parse-rc-file.d.ts @@ -7,9 +7,3 @@ export declare function getRCFileVars({ environments, filePath }: { }): Promise<{ [key: string]: any; }>; -/** - * Reads and parses the .rc file - */ -export declare function parseRCFile(fileData: string): { - [key: string]: any; -}; diff --git a/dist/parse-rc-file.js b/dist/parse-rc-file.js index 7e047ec..b247a01 100644 --- a/dist/parse-rc-file.js +++ b/dist/parse-rc-file.js @@ -15,20 +15,27 @@ async function getRCFileVars({ environments, filePath }) { await statAsync(absolutePath); } catch (e) { - const pathError = new Error('Invalid .rc file path.'); + 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; - if (ext === '.json' || ext === '.js') { - const possiblePromise = require(absolutePath); /* eslint-disable-line */ - parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; + try { + if (ext === '.json' || ext === '.js') { + const possiblePromise = require(absolutePath); /* eslint-disable-line */ + parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; + } + else { + const file = await readFileAsync(absolutePath, { encoding: 'utf8' }); + parsedData = JSON.parse(file); + } } - else { - const file = await readFileAsync(absolutePath, { encoding: 'utf8' }); - parsedData = parseRCFile(file); + catch (e) { + const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`); + parseError.name = 'ParseError'; + throw parseError; } // Parse and merge multiple rc environments together let result = {}; @@ -41,30 +48,10 @@ async function getRCFileVars({ environments, filePath }) { } }); if (!environmentFound) { - console.error(`Error: - Could not find any environments: - ${environments} - in .rc file: - ${absolutePath}`); - throw new Error(`All environments (${environments}) are missing in in .rc file (${absolutePath}).`); + const environmentError = new Error(`Failed to find environments ${environments} at .rc file location: ${absolutePath}`); + environmentError.name = 'EnvironmentError'; + throw environmentError; } return result; } exports.getRCFileVars = getRCFileVars; -/** - * Reads and parses the .rc file - */ -function parseRCFile(fileData) { - let data; - try { - data = JSON.parse(fileData); - } - catch (e) { - console.error(`Error: - Failed to parse the .rc file. - Please make sure its a valid JSON format.`); - throw new Error('Unable to parse JSON in .rc file.'); - } - return data; -} -exports.parseRCFile = parseRCFile; diff --git a/dist/signal-termination.d.ts b/dist/signal-termination.d.ts index 18a2b35..6bd3313 100644 --- a/dist/signal-termination.d.ts +++ b/dist/signal-termination.d.ts @@ -2,7 +2,11 @@ import { ChildProcess } from 'child_process'; export declare class TermSignals { private readonly terminateSpawnedProcessFuncHandlers; + private readonly verbose; _exitCalled: boolean; + constructor(options?: { + verbose?: boolean; + }); handleTermSignals(proc: ChildProcess): void; /** * Enables catching of unhandled exceptions diff --git a/dist/signal-termination.js b/dist/signal-termination.js index 62e93dc..f31a884 100644 --- a/dist/signal-termination.js +++ b/dist/signal-termination.js @@ -4,9 +4,11 @@ const SIGNALS_TO_HANDLE = [ 'SIGINT', 'SIGTERM', 'SIGHUP' ]; class TermSignals { - constructor() { + 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 @@ -15,6 +17,9 @@ class TermSignals { (signal, code) => { this._removeProcessListeners(); if (!this._exitCalled) { + if (this.verbose === true) { + console.info(`Parent process exited with signal: ${signal}. Terminating child process...`); + } this._exitCalled = true; proc.kill(signal); this._terminateProcess(code, signal); @@ -28,6 +33,10 @@ class TermSignals { this._removeProcessListeners(); const convertedSignal = signal != null ? signal : undefined; if (!this._exitCalled) { + if (this.verbose === true) { + console.info(`Child process exited with code: ${code} and signal: ${signal}. ` + + 'Terminating parent process...'); + } this._exitCalled = true; this._terminateProcess(code, convertedSignal); } diff --git a/dist/types.d.ts b/dist/types.d.ts index ca1e952..5bc5646 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -7,6 +7,7 @@ export interface GetEnvVarOptions { environments: string[]; filePath?: string; }; + verbose?: boolean; } export interface EnvCmdOptions extends GetEnvVarOptions { command: string; @@ -14,5 +15,6 @@ export interface EnvCmdOptions extends GetEnvVarOptions { options?: { noOverride?: boolean; useShell?: boolean; + verbose?: boolean; }; } diff --git a/package.json b/package.json index ccf4319..4c90d42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "env-cmd", - "version": "10.0.1", + "version": "10.1.0", "description": "Executes a command using the environment variables in an env file", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -47,7 +47,7 @@ }, "homepage": "https://github.com/toddbluhm/env-cmd#readme", "dependencies": { - "commander": "^3.0.0", + "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "devDependencies": { @@ -63,7 +63,7 @@ "sinon": "^7.0.0", "ts-node": "^8.0.0", "ts-standard": "^3.0.0", - "typescript": "^3.0.0" + "typescript": "^3.7.0" }, "nyc": { "include": [ diff --git a/src/env-cmd.ts b/src/env-cmd.ts index bca2a91..4f4b83f 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -34,7 +34,7 @@ export async function CLI (args: string[]): Promise<{ [key: string]: any }> { export async function EnvCmd ( { command, commandArgs, envFile, rc, options = {} }: EnvCmdOptions ): Promise<{ [key: string]: any }> { - let env = await getEnvVars({ envFile, rc }) + let env = await getEnvVars({ envFile, rc, verbose: options.verbose }) // Override the merge order if --no-override flag set if (options.noOverride === true) { env = Object.assign({}, env, process.env) @@ -51,7 +51,7 @@ export async function EnvCmd ( }) // Handle any termination signals for parent and child proceses - const signals = new TermSignals() + const signals = new TermSignals({ verbose: options.verbose }) signals.handleUncaughtExceptions() signals.handleTermSignals(proc) diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index 92eb446..102b5ff 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -9,55 +9,106 @@ export async function getEnvVars (options: GetEnvVarOptions = {}): Promise<{ [ke options.envFile = options.envFile !== undefined ? options.envFile : {} // Check for rc file usage if (options.rc !== undefined) { - return getRCFile({ environments: options.rc.environments, filePath: options.rc.filePath }) + return getRCFile({ + environments: options.rc.environments, + filePath: options.rc.filePath, + verbose: options.verbose + }) } - return getEnvFile({ filePath: options.envFile.filePath, fallback: options.envFile.fallback }) + return getEnvFile({ + filePath: options.envFile.filePath, + fallback: options.envFile.fallback, + verbose: options.verbose + }) } export async function getEnvFile ( - { filePath, fallback }: { filePath?: string, fallback?: boolean } + { filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean } ): Promise<{ [key: string]: any }> { // Use env file if (filePath !== undefined) { try { - return await getEnvFileVars(filePath) - } catch (e) { } + const env = await getEnvFileVars(filePath) + if (verbose === true) { + console.info(`Found .env file at path: ${filePath}`) + } + return env + } catch (e) { + if (verbose === true) { + console.info(`Failed to find .env file at path: ${filePath}`) + } + } if (fallback !== true) { - throw new Error(`Unable to locate env file at location (${filePath})`) + throw new Error(`Failed to find .env file at path: ${filePath}`) } } // Use the default env file locations for (const path of ENV_FILE_DEFAULT_LOCATIONS) { try { - return await getEnvFileVars(path) + const env = await getEnvFileVars(path) + if (verbose === true) { + console.info(`Found .env file at default path: ${path}`) + } + return env } catch (e) { } } - throw new Error(`Unable to locate env file at default locations (${ENV_FILE_DEFAULT_LOCATIONS})`) + const error = `Failed to find .env file at default paths: ${ENV_FILE_DEFAULT_LOCATIONS}` + if (verbose === true) { + console.info(error) + } + throw new Error(error) } export async function getRCFile ( - { environments, filePath }: { environments: string[], filePath?: string } + { environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean } ): Promise<{ [key: string]: any }> { // User provided an .rc file path if (filePath !== undefined) { try { - return await getRCFileVars({ environments, filePath }) + const env = await getRCFileVars({ environments, filePath }) + if (verbose === true) { + console.info(`Found environments: ${environments} for .rc file at path: ${filePath}`) + } + return env } catch (e) { - if (e.name !== 'PathError') console.error(e) - throw new Error(`Unable to locate .rc file at location (${filePath})`) + if (e.name === 'PathError') { + if (verbose === true) { + console.info(`Failed to find .rc file at path: ${filePath}`) + } + } + if (e.name === 'EnvironmentError') { + if (verbose === true) { + console.info(`Failed to find environments: ${environments} for .rc file at path: ${filePath}`) + } + } + throw e } } // Use the default .rc file locations - for (const filePath of RC_FILE_DEFAULT_LOCATIONS) { + for (const path of RC_FILE_DEFAULT_LOCATIONS) { try { - return await getRCFileVars({ environments, filePath }) + const env = await getRCFileVars({ environments, filePath: path }) + if (verbose === true) { + console.info(`Found environments: ${environments} for default .rc file at path: ${path}`) + } + return env } catch (e) { - if (e.name !== 'PathError') console.error(e) + if (e.name === 'EnvironmentError') { + const errorText = `Failed to find environments: ${environments} for .rc file at path: ${path}` + if (verbose === true) { + console.info(errorText) + } + throw new Error(errorText) + } } } - throw new Error(`Unable to locate .rc file at default locations (${RC_FILE_DEFAULT_LOCATIONS})`) + const errorText = `Failed to find .rc file at default paths: ${RC_FILE_DEFAULT_LOCATIONS}` + if (verbose === true) { + console.info(errorText) + } + throw new Error(errorText) } diff --git a/src/parse-args.ts b/src/parse-args.ts index 7d09b1a..166dfbf 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -15,6 +15,7 @@ export function parseArgs (args: string[]): EnvCmdOptions { program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))) const noOverride = !(program.override as boolean) const useShell = !!(program.useShell as boolean) + const verbose = !!(program.verbose as boolean) let rc: any if (program.environments !== undefined && program.environments.length !== 0) { @@ -32,16 +33,21 @@ export function parseArgs (args: string[]): EnvCmdOptions { } } - return { + const options = { command, commandArgs, envFile, rc, options: { noOverride, - useShell + useShell, + verbose } } + if (verbose === true) { + console.info(`Options: ${JSON.stringify(options, null, 0)}`) + } + return options } export function parseArgsUsingCommander (args: string[]): Command { @@ -55,5 +61,6 @@ export function parseArgsUsingCommander (args: string[]): Command { .option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') .option('--use-shell', 'Execute the command in a new shell with the given environment') + .option('--verbose', 'Print helpful debugging information') .parse(['_', '_', ...args]) } diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index d505309..c3326b0 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -10,7 +10,9 @@ const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js'] export async function getEnvFileVars (envFilePath: string): Promise<{ [key: string]: any }> { const absolutePath = resolveEnvFilePath(envFilePath) if (!fs.existsSync(absolutePath)) { - throw new Error(`Invalid env file path (${envFilePath}).`) + const pathError = new Error(`Invalid env file path (${envFilePath}).`) + pathError.name = 'PathError' + throw pathError } // Get the file extension diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 77833f0..3370062 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -17,7 +17,7 @@ export async function getRCFileVars ( try { await statAsync(absolutePath) } catch (e) { - const pathError = new Error('Invalid .rc file path.') + const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`) pathError.name = 'PathError' throw pathError } @@ -25,12 +25,18 @@ export async function getRCFileVars ( // Get the file extension const ext = extname(absolutePath).toLowerCase() let parsedData: { [key: string]: any } - if (ext === '.json' || ext === '.js') { - const possiblePromise = require(absolutePath) /* eslint-disable-line */ - parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise - } else { - const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) - parsedData = parseRCFile(file) + try { + if (ext === '.json' || ext === '.js') { + 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) + } + } catch (e) { + const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`) + parseError.name = 'ParseError' + throw parseError } // Parse and merge multiple rc environments together @@ -48,29 +54,12 @@ export async function getRCFileVars ( }) if (!environmentFound) { - console.error(`Error: - Could not find any environments: - ${environments} - in .rc file: - ${absolutePath}`) - throw new Error(`All environments (${environments}) are missing in in .rc file (${absolutePath}).`) + const environmentError = new Error( + `Failed to find environments ${environments} at .rc file location: ${absolutePath}` + ) + environmentError.name = 'EnvironmentError' + throw environmentError } return result } - -/** - * Reads and parses the .rc file - */ -export function parseRCFile (fileData: string): { [key: string]: any } { - let data - try { - data = JSON.parse(fileData) - } catch (e) { - console.error(`Error: - Failed to parse the .rc file. - Please make sure its a valid JSON format.`) - throw new Error('Unable to parse JSON in .rc file.') - } - return data -} diff --git a/src/signal-termination.ts b/src/signal-termination.ts index a42f6d9..7401678 100644 --- a/src/signal-termination.ts +++ b/src/signal-termination.ts @@ -6,8 +6,13 @@ const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [ export class TermSignals { private readonly terminateSpawnedProcessFuncHandlers: { [key: string]: any } = {} + private readonly verbose: boolean = false; public _exitCalled = false + constructor (options: { verbose?: boolean } = {}) { + this.verbose = options.verbose === true + } + public handleTermSignals (proc: ChildProcess): void { // Terminate child process if parent process receives termination events SIGNALS_TO_HANDLE.forEach((signal): void => { @@ -15,6 +20,9 @@ export class TermSignals { (signal: any, code: any): void => { this._removeProcessListeners() if (!this._exitCalled) { + if (this.verbose === true) { + console.info(`Parent process exited with signal: ${signal}. Terminating child process...`) + } this._exitCalled = true proc.kill(signal) this._terminateProcess(code, signal) @@ -29,6 +37,12 @@ export class TermSignals { this._removeProcessListeners() const convertedSignal = signal != null ? signal : undefined if (!this._exitCalled) { + if (this.verbose === true) { + console.info( + `Child process exited with code: ${code} and signal: ${signal}. ` + + 'Terminating parent process...' + ) + } this._exitCalled = true this._terminateProcess(code, convertedSignal) } diff --git a/src/types.ts b/src/types.ts index a7fc9a4..f223490 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export interface GetEnvVarOptions { environments: string[] filePath?: string } + verbose?: boolean } export interface EnvCmdOptions extends GetEnvVarOptions { @@ -15,5 +16,6 @@ export interface EnvCmdOptions extends GetEnvVarOptions { options?: { noOverride?: boolean useShell?: boolean + verbose?: boolean } } diff --git a/test/get-env-vars.spec.ts b/test/get-env-vars.spec.ts index b42c8eb..942b9d9 100644 --- a/test/get-env-vars.spec.ts +++ b/test/get-env-vars.spec.ts @@ -7,15 +7,11 @@ import * as envFile from '../src/parse-env-file' describe('getEnvVars', (): void => { let getRCFileVarsStub: sinon.SinonStub let getEnvFileVarsStub: sinon.SinonStub - let logStub: sinon.SinonStub - - const pathError = new Error() - pathError.name = 'PathError' + let logInfoStub: sinon.SinonStub before((): void => { getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') getEnvFileVarsStub = sinon.stub(envFile, 'getEnvFileVars') - logStub = sinon.stub(console, 'error') }) after((): void => { @@ -25,9 +21,12 @@ describe('getEnvVars', (): void => { afterEach((): void => { sinon.resetHistory() sinon.resetBehavior() + if (logInfoStub !== undefined) { + logInfoStub.restore() + } }) - it('should parse the json .rc file from the default location with the given environment', + it('should parse the json .rc file from the default path with the given environment', async (): Promise => { getRCFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars({ rc: { environments: ['production'] } }) @@ -41,7 +40,18 @@ describe('getEnvVars', (): void => { } ) - it('should search all default .rc file locations', async (): Promise => { + it('should print path of custom .rc file and environments to info for verbose', + async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getRCFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) + assert.equal(logInfoStub.callCount, 1) + } + ) + + it('should search all default .rc file paths', async (): Promise => { + const pathError = new Error() + pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars({ rc: { environments: ['production'] } }) @@ -54,28 +64,60 @@ describe('getEnvVars', (): void => { assert.equal(getRCFileVarsStub.args[2][0].filePath, './.env-cmdrc.json') }) - it('should fail to find .rc file at default location', async (): Promise => { + it('should fail to find .rc file at default path', async (): Promise => { + const pathError = new Error() + pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { await getEnvVars({ rc: { environments: ['production'] } }) assert.fail('should not get here.') } catch (e) { - assert.match(e.message, /locate \.rc/gi) + assert.match(e.message, /failed to find/gi) + assert.match(e.message, /\.rc file/gi) + assert.match(e.message, /default paths/gi) } }) - it('should log to error console on non-path errors', async (): Promise => { - getRCFileVarsStub.rejects(new Error('Non-path Error')) + it('should print failure to find .rc file to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + const pathError = new Error() + pathError.name = 'PathError' + getRCFileVarsStub.rejects(pathError) try { - await getEnvVars({ rc: { environments: ['production'] } }) + await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.fail('should not get here.') } catch (e) { - assert.match(logStub.getCall(0).args[0].message, /non-path error/gi) - assert.match(e.message, /locate \.rc/gi) + assert.equal(logInfoStub.callCount, 1) } }) - it('should find .rc file at custom path location', async (): Promise => { + it('should fail to find environments at .rc file at default path', async (): Promise => { + const environmentError = new Error('Failed to find environments: for .rc file at path:') + environmentError.name = 'EnvironmentError' + getRCFileVarsStub.rejects(environmentError) + try { + await getEnvVars({ rc: { environments: ['bad'] } }) + assert.fail('should not get here.') + } catch (e) { + assert.match(e.message, /failed to find environments/gi) + assert.match(e.message, /\.rc file at path/gi) + } + }) + + it('should print failure to find environments in .rc file to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + const environmentError = new Error('Failed to find environments: for .rc file at path:') + environmentError.name = 'EnvironmentError' + getRCFileVarsStub.rejects(environmentError) + try { + await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) + assert.fail('should not get here.') + } catch (e) { + assert.equal(logInfoStub.callCount, 1) + } + }) + + it('should find .rc file at custom path path', async (): Promise => { getRCFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' } @@ -89,7 +131,19 @@ describe('getEnvVars', (): void => { assert.equal(getRCFileVarsStub.args[0][0].filePath, '../.custom-rc') }) - it('should fail to find .rc file at custom path location', async (): Promise => { + it('should print custom .rc file path to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getRCFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + await getEnvVars({ + rc: { environments: ['production'], filePath: '../.custom-rc' }, + verbose: true + }) + assert.equal(logInfoStub.callCount, 1) + }) + + it('should fail to find .rc file at custom path path', async (): Promise => { + const pathError = new Error('Failed to find .rc file at path:') + pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { await getEnvVars({ @@ -97,24 +151,61 @@ describe('getEnvVars', (): void => { }) assert.fail('should not get here.') } catch (e) { - assert.match(e.message, /locate \.rc/gi) + assert.match(e.message, /failed to find/gi) + assert.match(e.message, /\.rc file at path/gi) } }) - it('should log to error console on non-path errors', async (): Promise => { - getRCFileVarsStub.rejects(new Error('Non-path Error')) + it('should print failure to find custom .rc file path to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + const pathError = new Error('Failed to find .rc file at path:') + pathError.name = 'PathError' + getRCFileVarsStub.rejects(pathError) try { await getEnvVars({ - rc: { environments: ['production'], filePath: '../.custom-rc' } + rc: { environments: ['production'], filePath: '../.custom-rc' }, + verbose: true }) assert.fail('should not get here.') } catch (e) { - assert.match(logStub.getCall(0).args[0].message, /non-path error/gi) - assert.match(e.message, /locate \.rc/gi) + assert.equal(logInfoStub.callCount, 1) } }) - it('should parse the env file from a custom location', async (): Promise => { + it('should fail to find environments for .rc file at custom path', async (): Promise => { + const environmentError = new Error('Failed to find environments: for .rc file at path: ') + environmentError.name = 'EnvironmentError' + getRCFileVarsStub.rejects(environmentError) + try { + await getEnvVars({ + rc: { environments: ['bad'], filePath: '../.custom-rc' } + }) + assert.fail('should not get here.') + } catch (e) { + assert.match(e.message, /failed to find environments/gi) + assert.match(e.message, /\.rc file at path/gi) + } + }) + + it('should print failure to find environments for custom .rc file path to info for verbose', + async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + const environmentError = new Error('Failed to find environments: for .rc file at path: ') + environmentError.name = 'EnvironmentError' + getRCFileVarsStub.rejects(environmentError) + try { + await getEnvVars({ + rc: { environments: ['bad'], filePath: '../.custom-rc' }, + verbose: true + }) + assert.fail('should not get here.') + } catch (e) { + assert.equal(logInfoStub.callCount, 1) + } + } + ) + + it('should parse the env file from a custom path', async (): Promise => { getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars({ envFile: { filePath: '../.env-file' } }) assert.isOk(envs) @@ -124,31 +215,63 @@ describe('getEnvVars', (): void => { assert.equal(getEnvFileVarsStub.args[0][0], '../.env-file') }) - it('should fail to find env file at custom location', async (): Promise => { + it('should print path of .env file to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) + assert.equal(logInfoStub.callCount, 1) + }) + + it('should fail to find env file at custom path', async (): Promise => { getEnvFileVarsStub.rejects('Not found.') try { await getEnvVars({ envFile: { filePath: '../.env-file' } }) assert.fail('should not get here.') } catch (e) { - assert.match(e.message, /locate env/gi) + assert.match(e.message, /failed to find/gi) + assert.match(e.message, /\.env file at path/gi) } }) - it('should parse the env file from the default location if custom ' + - 'location not found and fallback option provided', - async (): Promise => { - getEnvFileVarsStub.onFirstCall().rejects('File not found.') - getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) - const envs = await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } }) - assert.isOk(envs) - assert.lengthOf(Object.keys(envs), 1) - assert.equal(envs.THANKS, 'FORALLTHEFISH') - assert.equal(getEnvFileVarsStub.callCount, 2) - assert.equal(getEnvFileVarsStub.args[1][0], './.env') - } + it('should print failure to find .env file path to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getEnvFileVarsStub.rejects('Not found.') + try { + await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) + assert.fail('should not get here.') + } catch (e) { + assert.equal(logInfoStub.callCount, 1) + } + }) + + it( + 'should parse the env file from the default path if custom ' + + 'path not found and fallback option provided', + async (): Promise => { + getEnvFileVarsStub.onFirstCall().rejects('File not found.') + getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + const envs = await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } }) + assert.isOk(envs) + assert.lengthOf(Object.keys(envs), 1) + assert.equal(envs.THANKS, 'FORALLTHEFISH') + assert.equal(getEnvFileVarsStub.callCount, 2) + assert.equal(getEnvFileVarsStub.args[1][0], './.env') + } ) - it('should parse the env file from the default location', async (): Promise => { + it( + 'should print multiple times for failure to find .env file and ' + + 'failure to find fallback file to infor for verbose', + async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getEnvFileVarsStub.onFirstCall().rejects('File not found.') + getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + await 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 => { getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars() assert.isOk(envs) @@ -158,7 +281,14 @@ describe('getEnvVars', (): void => { assert.equal(getEnvFileVarsStub.args[0][0], './.env') }) - it('should search all default env file locations', async (): Promise => { + it('should print path of .env file to info for verbose', async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getEnvFileVarsStub.returns({ THANKS: 'FORALLTHEFISH' }) + await getEnvVars({ verbose: true }) + assert.equal(logInfoStub.callCount, 1) + }) + + it('should search all default env file paths', async (): Promise => { getEnvFileVarsStub.throws('Not found.') getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FORALLTHEFISH' }) const envs = await getEnvVars() @@ -167,16 +297,31 @@ describe('getEnvVars', (): void => { assert.equal(envs.THANKS, 'FORALLTHEFISH') assert.equal(getEnvFileVarsStub.callCount, 3) assert.isTrue(getEnvFileVarsStub.calledWithExactly('./.env.json')) - assert.isTrue(getEnvFileVarsStub.calledWithExactly('./.env.json')) }) - it('should fail to find env file at default location', async (): Promise => { + it('should fail to find env file at default path', async (): Promise => { getEnvFileVarsStub.rejects('Not found.') try { await getEnvVars() assert.fail('should not get here.') } catch (e) { - assert.match(e.message, /locate env/gi) + assert.match(e.message, /failed to find/gi) + assert.match(e.message, /\.env file/gi) + assert.match(e.message, /default paths/gi) } }) + + it( + 'should print failure to find .env file at default paths to info for verbose', + async (): Promise => { + logInfoStub = sinon.stub(console, 'info') + getEnvFileVarsStub.rejects('Not found.') + try { + await getEnvVars({ verbose: true }) + assert.fail('should not get here.') + } catch (e) { + assert.equal(logInfoStub.callCount, 1) + } + } + ) }) diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index a67c267..04889e9 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -1,4 +1,5 @@ /* eslint @typescript-eslint/no-non-null-assertion: 0 */ +import * as sinon from 'sinon' import { assert } from 'chai' import { parseArgs } from '../src/parse-args' @@ -8,6 +9,21 @@ describe('parseArgs', (): void => { const environments = ['development', 'production'] const rcFilePath = './.env-cmdrc' const envFilePath = './.env' + let logInfoStub: sinon.SinonStub + + before((): void => { + logInfoStub = sinon.stub(console, 'info') + }) + + after((): void => { + sinon.restore() + }) + + afterEach((): void => { + sinon.resetHistory() + sinon.resetBehavior() + }) + it('should parse environment value', (): void => { const res = parseArgs(['-e', environments[0], command]) assert.exists(res.rc) @@ -69,4 +85,10 @@ describe('parseArgs', (): void => { assert.exists(res.envFile) assert.isTrue(res.envFile!.fallback) }) + + it('should print to console.info if --verbose flag is passed', (): void => { + const res = parseArgs(['-f', envFilePath, '--verbose', command, ...commandArgs]) + assert.exists(res.options!.verbose) + assert.equal(logInfoStub.callCount, 1) + }) }) diff --git a/test/parse-rc-file.spec.ts b/test/parse-rc-file.spec.ts index eb793e5..2b74213 100644 --- a/test/parse-rc-file.spec.ts +++ b/test/parse-rc-file.spec.ts @@ -1,30 +1,9 @@ import { assert } from 'chai' -import { readFileSync } from 'fs' -import { getRCFileVars, parseRCFile } from '../src/parse-rc-file' +import { getRCFileVars } from '../src/parse-rc-file' const rcFilePath = './test/test-files/.rc-test' const rcJSONFilePath = './test/test-files/.rc-test.json' -describe('parseRCFile', (): void => { - it('should parse an .rc file', (): void => { - const file = readFileSync(rcFilePath, { encoding: 'utf8' }) - const res = parseRCFile(file) - assert.exists(res) - assert.hasAllKeys(res, ['development', 'test', 'production']) - assert.equal(res.development.ANSWER, 0) - assert.equal(res.production.THANKS, 'FOR WHAT?!') - }) - - it('should fail to parse an .rc file', (): void => { - try { - parseRCFile('fdsjk dsjfksdjkla') - assert.fail('Should not get here!') - } catch (e) { - assert.match(e.message, /parse/gi) - } - }) -}) - describe('getRCFileVars', (): void => { it('should parse an .rc file with the given environment', async (): Promise => { const res = await getRCFileVars({ environments: ['production'], filePath: rcFilePath }) @@ -45,6 +24,15 @@ describe('getRCFileVars', (): void => { }) }) + it('should fail to find .rc file', async (): Promise => { + try { + await getRCFileVars({ environments: ['bad'], filePath: 'bad-path' }) + assert.fail('Should not get here!') + } catch (e) { + assert.match(e.message, /\.rc file at path/gi) + } + }) + it('should fail to parse a .rc file if environment does not exist', async (): Promise => { try { await getRCFileVars({ environments: ['bad'], filePath: rcFilePath }) @@ -56,10 +44,10 @@ describe('getRCFileVars', (): void => { it('should fail to parse an .rc file', async (): Promise => { try { - await getRCFileVars({ environments: ['bad'], filePath: './non-existent-file' }) + await getRCFileVars({ environments: ['bad'], filePath: './test/test-files/.rc-test-bad-format' }) assert.fail('Should not get here!') } catch (e) { - assert.match(e.message, /path/gi) + assert.match(e.message, /parse/gi) } }) diff --git a/test/signal-termination.spec.ts b/test/signal-termination.spec.ts index b26dc98..1d8e222 100644 --- a/test/signal-termination.spec.ts +++ b/test/signal-termination.spec.ts @@ -8,6 +8,7 @@ describe('signal-termination', (): void => { const term = new TermSignals() let logStub: sinon.SinonStub let processStub: sinon.SinonStub + beforeEach((): void => { logStub = sinon.stub(console, 'error') processStub = sinon.stub(process, 'exit') @@ -118,9 +119,11 @@ describe('signal-termination', (): void => { let processOnceStub: sinon.SinonStub let _removeProcessListenersStub: sinon.SinonStub let _terminateProcessStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub let proc: any - beforeEach((): void => { - term = new TermSignals() + + function setup (verbose: boolean = false): void { + term = new TermSignals({ verbose }) procKillStub = sinon.stub() procOnStub = sinon.stub() processOnceStub = sinon.stub(process, 'once') @@ -130,6 +133,10 @@ describe('signal-termination', (): void => { kill: procKillStub, on: procOnStub } + } + + beforeEach((): void => { + setup() }) afterEach((): void => { @@ -153,6 +160,16 @@ describe('signal-termination', (): void => { assert.isOk(term._exitCalled) }) + it('should print child process terminated to info for verbose', (): void => { + sinon.restore() + setup(true) + logInfoStub = sinon.stub(console, 'info') + assert.notOk(term._exitCalled) + term.handleTermSignals(proc) + processOnceStub.args[0][1]('SIGTERM', 1) + assert.equal(logInfoStub.callCount, 1) + }) + it('should not terminate child process if child process termination ' + 'has already been called by parent', (): void => { assert.notOk(term._exitCalled) @@ -176,6 +193,16 @@ describe('signal-termination', (): void => { assert.isOk(term._exitCalled) }) + it('should print parent process terminated to info for verbose', (): void => { + sinon.restore() + setup(true) + logInfoStub = sinon.stub(console, 'info') + assert.notOk(term._exitCalled) + term.handleTermSignals(proc) + procOnStub.args[0][1](1, 'SIGTERM') + assert.equal(logInfoStub.callCount, 1) + }) + it('should not terminate parent process if parent process already terminating', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) diff --git a/test/test-files/.rc-test-bad-format b/test/test-files/.rc-test-bad-format new file mode 100644 index 0000000..b26c2d1 --- /dev/null +++ b/test/test-files/.rc-test-bad-format @@ -0,0 +1,3 @@ +{ + "development": +} \ No newline at end of file