diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 436040c..1aa2c9e 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - name: Checkout Project diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index be4526b..8bcb1b4 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - name: Checkout Project diff --git a/dist/env-cmd.d.ts b/dist/env-cmd.d.ts index 859a7da..5141864 100644 --- a/dist/env-cmd.d.ts +++ b/dist/env-cmd.d.ts @@ -5,9 +5,7 @@ import { EnvCmdOptions } from './types'; * @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; -}>; +export declare function CLI(args: string[]): Promise>; /** * The main env-cmd program. This will spawn a new process and run the given command using * various environment file solutions. @@ -16,6 +14,4 @@ export declare function CLI(args: string[]): Promise<{ * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } * @returns {Promise<{ [key: string]: any }>} 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>; diff --git a/dist/expand-envs.d.ts b/dist/expand-envs.d.ts index cd06ffa..5a68b32 100644 --- a/dist/expand-envs.d.ts +++ b/dist/expand-envs.d.ts @@ -2,6 +2,4 @@ * expandEnvs Replaces $var in args and command with environment variables * 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: Record): string; diff --git a/dist/get-env-vars.d.ts b/dist/get-env-vars.d.ts index dc7ea2c..aaf968e 100644 --- a/dist/get-env-vars.d.ts +++ b/dist/get-env-vars.d.ts @@ -1,18 +1,12 @@ import { GetEnvVarOptions } from './types'; -export declare function getEnvVars(options?: GetEnvVarOptions): Promise<{ - [key: string]: any; -}>; +export declare function getEnvVars(options?: GetEnvVarOptions): Promise>; export declare function getEnvFile({ filePath, fallback, verbose }: { filePath?: string; fallback?: boolean; verbose?: boolean; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; export declare function getRCFile({ environments, filePath, verbose }: { environments: string[]; filePath?: string; verbose?: boolean; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; diff --git a/dist/parse-args.js b/dist/parse-args.js index 5e1f894..c650ea5 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -3,7 +3,7 @@ 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 */ +const packageJson = require('../package.json'); /** * Parses the arguments passed into the cli */ diff --git a/dist/parse-env-file.d.ts b/dist/parse-env-file.d.ts index 530f487..c298868 100644 --- a/dist/parse-env-file.d.ts +++ b/dist/parse-env-file.d.ts @@ -1,21 +1,15 @@ /** * 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>; /** * 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): Record; /** * 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): Record; /** * Strips out comments from env file string */ diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index 6b6dafe..a4370ce 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -18,7 +18,7 @@ async function getEnvFileVars(envFilePath) { const ext = path.extname(absolutePath).toLowerCase(); let env = {}; if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise = require(absolutePath); /* eslint-disable-line */ + const possiblePromise = require(absolutePath); env = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; } else { diff --git a/dist/parse-rc-file.d.ts b/dist/parse-rc-file.d.ts index 053adf2..bd193e1 100644 --- a/dist/parse-rc-file.d.ts +++ b/dist/parse-rc-file.d.ts @@ -4,6 +4,4 @@ export declare function getRCFileVars({ environments, filePath }: { environments: string[]; filePath: string; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; diff --git a/dist/parse-rc-file.js b/dist/parse-rc-file.js index f3df690..07bb65e 100644 --- a/dist/parse-rc-file.js +++ b/dist/parse-rc-file.js @@ -24,7 +24,7 @@ async function getRCFileVars({ environments, filePath }) { let parsedData; try { if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath); /* eslint-disable-line */ + const possiblePromise = require(absolutePath); parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; } else { diff --git a/dist/utils.d.ts b/dist/utils.d.ts index d3714a7..9a731d9 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -9,4 +9,4 @@ export declare function parseArgList(list: string): string[]; /** * A simple function to test if the value is a promise */ -export declare function isPromise(value: any | PromiseLike): value is Promise; +export declare function isPromise(value: any | PromiseLike): value is Promise; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9edb080 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,43 @@ +const eslint = require('@eslint/js') +const tseslint = require('typescript-eslint') +const globals = require('globals') +const stylistic = require('@stylistic/eslint-plugin') + +module.exports = tseslint.config( + { + ignores: ['dist/*', 'bin/*'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + languageOptions: { + globals: { + ...globals.node, + }, + parserOptions: { + projectService: { + allowDefaultProject: ['test/*.ts'], + }, + }, + }, + extends: [ + eslint.configs.recommended, + stylistic.configs['recommended-flat'], + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + ], + }, + // Disable Type Checking JS files + { + files: ['**/*.js'], + extends: [tseslint.configs.disableTypeChecked], + }, + { + // For test files ignore some rules + files: ['test/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + }, + }, +) diff --git a/eslint.config.js.test b/eslint.config.js.test new file mode 100644 index 0000000..00e63e2 --- /dev/null +++ b/eslint.config.js.test @@ -0,0 +1,26 @@ +module.exports = (async function config() { + const { default: love } = await import('eslint-config-love') + + return [ + love, + { + files: [ + 'src/**/*.[j|t]s', + // 'src/**/*.ts', + 'test/**/*.[j|t]s', + // 'test/**/*.ts' + ], + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['eslint.config.js', 'bin/env-cmd.js'], + defaultProject: './tsconfig.json', + }, + }, + }, + }, + { + ignores: ['dist/'], + } + ] +})() diff --git a/package.json b/package.json index 99ef782..4c8ace4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "env-cmd", - "version": "10.1.0", + "version": "10.2.0", "description": "Executes a command using the environment variables in an env file", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -14,7 +14,7 @@ "test": "mocha -r ts-node/register ./test/**/*.ts", "test-cover": "nyc npm test", "coveralls": "coveralls < coverage/lcov.info", - "lint": "ts-standard --fix && tsc --noEmit", + "lint": "npx eslint .", "build": "tsc", "watch": "tsc -w" }, @@ -53,6 +53,8 @@ "devDependencies": { "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.0", + "@eslint/js": "^9.15.0", + "@stylistic/eslint-plugin": "^2.11.0", "@types/chai": "^4.0.0", "@types/cross-spawn": "^6.0.0", "@types/mocha": "^7.0.0", @@ -60,13 +62,14 @@ "@types/sinon": "^9.0.0", "chai": "^4.0.0", "coveralls": "^3.0.0", + "globals": "^15.12.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": "^5.7.2" + "typescript": "^5.7.2", + "typescript-eslint": "^8.15.0" }, "nyc": { "include": [ @@ -85,12 +88,6 @@ "sourceMap": true, "instrument": true }, - "ts-standard": { - "project": "./tsconfig.eslint.json", - "ignore": [ - "dist" - ] - }, "greenkeeper": { "ignore": [ "@types/node" diff --git a/src/env-cmd.ts b/src/env-cmd.ts index cc3179a..424998b 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -1,5 +1,5 @@ import { spawn } from './spawn' -import { EnvCmdOptions } from './types' +import { EnvCmdOptions, Environment } from './types' import { TermSignals } from './signal-termination' import { parseArgs } from './parse-args' import { getEnvVars } from './get-env-vars' @@ -9,16 +9,17 @@ 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 }>} + * @returns {Promise} */ -export async function CLI (args: string[]): Promise<{ [key: string]: any }> { +export async function CLI(args: string[]): Promise { // 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) { + return await (exports as { EnvCmd: typeof EnvCmd }).EnvCmd(parsedArgs) + } + catch (e) { console.error(e) return process.exit(1) } @@ -30,21 +31,22 @@ 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} 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 { + let env: Environment = {} try { env = await getEnvVars({ envFile, rc, verbose: options.verbose }) - } catch (e) { + } + catch (e) { if (!(options.silent ?? false)) { throw e } @@ -52,7 +54,8 @@ export async function EnvCmd ( // Override the merge order if --no-override flag set if (options.noOverride === true) { env = Object.assign({}, env, process.env) - } else { + } + else { // Add in the system environment variables to our environment list env = Object.assign({}, process.env, env) } @@ -66,7 +69,7 @@ export async function EnvCmd ( const proc = spawn(command, commandArgs, { stdio: 'inherit', shell: options.useShell, - env + env: env as Record, }) // Handle any termination signals for parent and child proceses diff --git a/src/expand-envs.ts b/src/expand-envs.ts index cd77317..f3c3b3a 100644 --- a/src/expand-envs.ts +++ b/src/expand-envs.ts @@ -1,11 +1,13 @@ +import { Environment } from './types' /** * 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(/(? { +export function expandEnvs(str: string, envs: Environment): string { + return str.replace(/(? { const varValue = envs[varName.slice(1)] - return varValue === undefined ? varName : varValue + // const test = 42; + return varValue === undefined ? varName : varValue.toString() }) } diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index cd5db92..e4e6b8d 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -1,30 +1,30 @@ -import { GetEnvVarOptions } from './types' +import { GetEnvVarOptions, Environment } from './types' import { getRCFileVars } from './parse-rc-file' import { getEnvFileVars } from './parse-env-file' 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 { + 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 { // 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 { // User provided an .rc file path if (filePath !== undefined) { try { @@ -72,15 +77,18 @@ export async function getRCFile ( console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`) } return env - } catch (e) { - if (e.name === 'PathError') { - if (verbose === true) { - console.info(`Failed to find .rc file at path: ${filePath}`) + } + catch (e) { + if (e instanceof Error) { + 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.join(',')}] for .rc file at path: ${filePath}`) + if (e.name === 'EnvironmentError') { + if (verbose === true) { + console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`) + } } } throw e @@ -95,19 +103,22 @@ export async function getRCFile ( console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`) } return env - } catch (e) { - if (e.name === 'EnvironmentError') { - const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}` - if (verbose === true) { - console.info(errorText) + } + 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) { + console.info(errorText) + } + throw new Error(errorText) } - throw new Error(errorText) - } - if (e.name === 'ParseError') { - if (verbose === true) { - console.info(e.message) + if (e.name === 'ParseError') { + if (verbose === true) { + console.info(e.message) + } + throw new Error(e.message) } - throw new Error(e.message) } } } diff --git a/src/parse-args.ts b/src/parse-args.ts index bbd4c56..cdf0927 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,14 +1,14 @@ import * as commander from 'commander' -import { EnvCmdOptions } from './types' +import { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } 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 */ +const packageJson: { version: string } = require('../package.json') /* eslint-disable-line */ /** * 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 +42,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 +72,8 @@ export function parseArgs (args: string[]): EnvCmdOptions { noOverride, silent, useShell, - verbose - } + verbose, + }, } if (verbose) { console.info(`Options: ${JSON.stringify(options, null, 0)}`) @@ -77,8 +81,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] [...args]') diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index bbd6334..488da52 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -1,13 +1,14 @@ import * as fs from 'fs' import * as path from 'path' import { resolveEnvFilePath, isPromise } from './utils' +import { Environment } from './types' const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs'] /** * 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 { const absolutePath = resolveEnvFilePath(envFilePath) if (!fs.existsSync(absolutePath)) { const pathError = new Error(`Invalid env file path (${envFilePath}).`) @@ -17,11 +18,12 @@ export async function getEnvFileVars (envFilePath: string): Promise<{ [key: stri // Get the file extension const ext = path.extname(absolutePath).toLowerCase() - let env = {} + let env: Environment = {} if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise = require(absolutePath) /* eslint-disable-line */ + const possiblePromise: Environment | Promise = require(absolutePath) /* eslint-disable-line */ env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise - } else { + } + else { const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) env = parseEnvString(file) } @@ -31,7 +33,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 +47,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 + } + else if (value === 'true') { + matches[key] = true + } + else if (value === 'false') { + matches[key] = false + } + else { + matches[key] = value + } } - return matches + 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 +95,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, '') } diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 3f1e036..1c0c43b 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -2,6 +2,7 @@ import { stat, readFile } from 'fs' import { promisify } from 'util' import { extname } from 'path' import { resolveEnvFilePath, isPromise } from './utils' +import { Environment, RCEnvironment } from './types' const statAsync = promisify(stat) const readFileAsync = promisify(readFile) @@ -9,14 +10,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 { 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,41 +26,45 @@ export async function getRCFileVars ( // Get the file extension const ext = extname(absolutePath).toLowerCase() - let parsedData: { [key: string]: any } + let parsedData: Partial try { if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath) /* eslint-disable-line */ + const possiblePromise = require(absolutePath) as PromiseLike | RCEnvironment parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise - } else { + } + else { const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) - parsedData = JSON.parse(file) + parsedData = JSON.parse(file) as Partial } - } catch (e) { + } + 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}` + `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 => { - const envVars = parsedData[name] - if (envVars !== undefined) { - environmentFound = true - result = { - ...result, - ...envVars + for (const name of environments) { + if (name in parsedData) { + const envVars = parsedData[name] + if (envVars != null && typeof envVars === 'object') { + environmentFound = true + result = { + ...result, + ...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 diff --git a/src/signal-termination.ts b/src/signal-termination.ts index 5e4120f..73f45e4 100644 --- a/src/signal-termination.ts +++ b/src/signal-termination.ts @@ -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 = {} + 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 => { - this._removeProcessListeners() - if (!this._exitCalled) { - if (this.verbose) { - 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: NodeJS.Signals | undefined - if (typeof signal === 'number') { - if (signal > (code ?? 0)) { - code = signal - correctSignal = 'SIGINT' - } - } else { - correctSignal = signal - } - // Kill the child process - proc.kill(correctSignal ?? code) - // Terminate the parent process - this._terminateProcess(code, correctSignal) + 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...') + } + // 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 + if (typeof signal === 'number') { + if (signal > 0) { + // code = signal + signal = 'SIGINT' } } + // else { + // correctSignal = signal + // } + // Kill the child process + proc.kill(signal) + // Terminate the parent process + 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,21 +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) { - process.kill(process.pid, signal) - return - } - if (code !== undefined) { - process.exit(code) - return // eslint-disable-line no-unreachable + 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 + } } throw new Error('Unable to terminate parent process successfully') } @@ -102,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) } diff --git a/src/spawn.ts b/src/spawn.ts index 2da09b0..b4d9d5f 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -1,4 +1,4 @@ import * as spawn from 'cross-spawn' export { - spawn + spawn, } diff --git a/src/types.ts b/src/types.ts index a1c1486..092c637 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,39 @@ +import { Command } from 'commander' + +// Define an export type +export type Environment = Partial> + +export type RCEnvironment = Partial> + +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?: { - filePath?: string - fallback?: boolean - } - rc?: { - environments: string[] - filePath?: string - } + envFile?: EnvFileOptions + rc?: RCFileOptions verbose?: boolean } -export interface EnvCmdOptions extends Pick { +export interface EnvCmdOptions extends GetEnvVarOptions { command: string commandArgs: string[] options?: { diff --git a/src/utils.ts b/src/utils.ts index 12b6d3c..e5c6e50 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,10 +4,10 @@ import * as os from 'os' /** * 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 = os.homedir() as string | undefined + if (home != null) { userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`) } return path.resolve(process.cwd(), userPath) @@ -15,13 +15,16 @@ export function resolveEnvFilePath (userPath: string): string { /** * 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): value is Promise { - return value != null && typeof value.then === 'function' +export function isPromise(value?: T | PromiseLike): value is PromiseLike { + return value != null + && typeof value === 'object' + && 'then' in value + && typeof value.then === 'function' } diff --git a/test/env-cmd.spec.ts b/test/env-cmd.spec.ts index 2db408e..3e08264 100644 --- a/test/env-cmd.spec.ts +++ b/test/env-cmd.spec.ts @@ -9,9 +9,9 @@ import * as envCmdLib from '../src/env-cmd' describe('CLI', (): void => { let sandbox: sinon.SinonSandbox - let parseArgsStub: sinon.SinonStub - let envCmdStub: sinon.SinonStub - let processExitStub: sinon.SinonStub + let parseArgsStub: sinon.SinonStub + let envCmdStub: sinon.SinonStub + let processExitStub: sinon.SinonStub before((): void => { sandbox = sinon.createSandbox() parseArgsStub = sandbox.stub(parseArgsLib, 'parseArgs') @@ -49,16 +49,16 @@ describe('CLI', (): void => { describe('EnvCmd', (): void => { let sandbox: sinon.SinonSandbox - let getEnvVarsStub: sinon.SinonStub - let spawnStub: sinon.SinonStub - let expandEnvsSpy: sinon.SinonSpy + let getEnvVarsStub: sinon.SinonStub + let spawnStub: sinon.SinonStub + let expandEnvsSpy: sinon.SinonSpy before((): void => { sandbox = sinon.createSandbox() getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars') spawnStub = sandbox.stub(spawnLib, 'spawn') spawnStub.returns({ on: (): void => { /* Fake the on method */ }, - kill: (): void => { /* Fake the kill method */ } + kill: (): void => { /* Fake the kill method */ }, }) expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs') sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals') @@ -80,12 +80,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 +100,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 +122,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 +147,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 +171,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 +188,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 +201,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 +221,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.') - } + }, ) }) diff --git a/test/expand-envs.spec.ts b/test/expand-envs.spec.ts index e998409..7474831 100644 --- a/test/expand-envs.spec.ts +++ b/test/expand-envs.spec.ts @@ -7,7 +7,9 @@ describe('expandEnvs', (): void => { 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') + } }) }) diff --git a/test/get-env-vars.spec.ts b/test/get-env-vars.spec.ts index 59649b8..d1401ea 100644 --- a/test/get-env-vars.spec.ts +++ b/test/get-env-vars.spec.ts @@ -5,9 +5,9 @@ import * as rcFile from '../src/parse-rc-file' import * as envFile from '../src/parse-env-file' describe('getEnvVars', (): void => { - let getRCFileVarsStub: sinon.SinonStub - let getEnvFileVarsStub: sinon.SinonStub - let logInfoStub: sinon.SinonStub + let getRCFileVarsStub: sinon.SinonStub + let getEnvFileVarsStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub | undefined before((): void => { getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') @@ -21,9 +21,7 @@ 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', @@ -37,7 +35,7 @@ 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', @@ -46,7 +44,7 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.equal(logInfoStub.callCount, 1) - } + }, ) it('should search all default .rc file paths', async (): Promise => { @@ -71,7 +69,9 @@ describe('getEnvVars', (): void => { try { await 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) @@ -86,7 +86,8 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) @@ -98,7 +99,9 @@ describe('getEnvVars', (): void => { try { await 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) } @@ -112,7 +115,8 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) @@ -120,7 +124,7 @@ describe('getEnvVars', (): void => { it('should find .rc file at custom path path', async (): Promise => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) const envs = await getEnvVars({ - rc: { environments: ['production'], filePath: '../.custom-rc' } + rc: { environments: ['production'], filePath: '../.custom-rc' }, }) assert.isOk(envs) assert.lengthOf(Object.keys(envs), 1) @@ -136,7 +140,7 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) await getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, - verbose: true + verbose: true, }) assert.equal(logInfoStub.callCount, 1) }) @@ -147,10 +151,12 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.rejects(pathError) try { await getEnvVars({ - rc: { environments: ['production'], filePath: '../.custom-rc' } + 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) } @@ -164,10 +170,11 @@ describe('getEnvVars', (): void => { try { await 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) } }) @@ -178,10 +185,12 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.rejects(environmentError) try { await getEnvVars({ - rc: { environments: ['bad'], filePath: '../.custom-rc' } + 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) } @@ -196,13 +205,14 @@ describe('getEnvVars', (): void => { try { await 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 => { @@ -227,7 +237,9 @@ describe('getEnvVars', (): void => { try { await 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) } @@ -239,14 +251,15 @@ describe('getEnvVars', (): void => { try { await 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 => { getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) @@ -256,19 +269,19 @@ describe('getEnvVars', (): void => { 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 => { 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 }) assert.equal(logInfoStub.callCount, 2) - } + }, ) it('should parse the env file from the default path', async (): Promise => { @@ -304,7 +317,9 @@ describe('getEnvVars', (): void => { try { await 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) @@ -319,9 +334,10 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } - } + }, ) }) diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index b6fced0..e8443d2 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -9,7 +9,7 @@ describe('parseArgs', (): void => { const environments = ['development', 'production'] const rcFilePath = './.env-cmdrc' const envFilePath = './.env' - let logInfoStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub before((): void => { logInfoStub = sinon.stub(console, 'info') @@ -53,7 +53,7 @@ describe('parseArgs', (): void => { assert.sameOrderedMembers(res.commandArgs, commandFlags) assert.notOk(res.options!.useShell) assert.notOk(res.envFile) - } + }, ) it('should parse override option', (): void => { diff --git a/test/parse-env-file.spec.ts b/test/parse-env-file.spec.ts index 0ddcdcc..22043ca 100644 --- a/test/parse-env-file.spec.ts +++ b/test/parse-env-file.spec.ts @@ -1,7 +1,7 @@ import { assert } from 'chai' import { stripEmptyLines, stripComments, parseEnvVars, - parseEnvString, getEnvFileVars + parseEnvString, getEnvFileVars, } from '../src/parse-env-file' describe('stripEmptyLines', (): 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,7 +120,8 @@ describe('getEnvFileVars', (): void => { THANKS: 'FOR WHAT?!', ANSWER: 42, ONLY: 'IN\n PRODUCTION', - GALAXY: 'hitch\nhiking\n\n' + GALAXY: 'hitch\nhiking\n\n', + BRINGATOWEL: true, }) }) @@ -126,7 +130,7 @@ describe('getEnvFileVars', (): void => { assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', ANSWER: 0, - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', }) }) @@ -134,7 +138,7 @@ describe('getEnvFileVars', (): void => { const env = await getEnvFileVars('./test/test-files/test-async.js') assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', - ANSWER: 0 + ANSWER: 0, }) }) @@ -142,9 +146,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 +157,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) } }) diff --git a/test/parse-rc-file.spec.ts b/test/parse-rc-file.spec.ts index 2b74213..1f61a12 100644 --- a/test/parse-rc-file.spec.ts +++ b/test/parse-rc-file.spec.ts @@ -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,7 +51,9 @@ 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) } }) @@ -54,12 +61,13 @@ describe('getRCFileVars', (): void => { it('should parse an async js .rc file', async (): Promise => { const env = await getRCFileVars({ environments: ['production'], - filePath: './test/test-files/.rc-test-async.js' + filePath: './test/test-files/.rc-test-async.js', }) assert.deepEqual(env, { THANKS: 'FOR WHAT?!', ANSWER: 42, - ONLY: 'IN PRODUCTION' + ONLY: 'IN PRODUCTION', + BRINGATOWEL: true, }) }) }) diff --git a/test/signal-termination.spec.ts b/test/signal-termination.spec.ts index 96ad89c..4657eed 100644 --- a/test/signal-termination.spec.ts +++ b/test/signal-termination.spec.ts @@ -1,6 +1,9 @@ import { assert } from 'chai' import * as sinon from 'sinon' import { TermSignals } from '../src/signal-termination' +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 - let processStub: sinon.SinonStub + let logStub: sinon.SinonStub + let processStub: sinon.SinonStub 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 + let removeListenerStub: sinon.SinonStub 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 - let killStub: sinon.SinonStub + let exitStub: sinon.SinonStub + let killStub: sinon.SinonStub beforeEach((): void => { exitStub = sandbox.stub(process, 'exit') @@ -83,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') @@ -93,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) } }) @@ -101,8 +106,8 @@ describe('signal-termination', (): void => { describe('handleUncaughtExceptions', (): void => { const term = new TermSignals() - let processOnStub: sinon.SinonStub - let _uncaughtExceptionHandlerStub: sinon.SinonStub + let processOnStub: sinon.SinonStub + let _uncaughtExceptionHandlerStub: sinon.SinonStub before((): void => { processOnStub = sandbox.stub(process, 'on') @@ -117,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 - let procOnStub: sinon.SinonStub - let processOnceStub: sinon.SinonStub - let _removeProcessListenersStub: sinon.SinonStub - let _terminateProcessStub: sinon.SinonStub - let logInfoStub: sinon.SinonStub - let proc: any - - function setup (verbose: boolean = false): void { + let procKillStub: sinon.SinonStub + let procOnStub: sinon.SinonStub + let processOnceStub: sinon.SinonStub + let _removeProcessListenersStub: sinon.SinonStub + let _terminateProcessStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub + let proc: ChildProcess + + function setup(verbose = false): void { term = new TermSignals({ verbose }) procKillStub = sandbox.stub() procOnStub = sandbox.stub() @@ -141,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 => { @@ -163,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) @@ -176,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) @@ -196,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') @@ -204,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) }) @@ -218,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') @@ -229,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) @@ -241,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) @@ -274,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) @@ -284,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) }) }) diff --git a/test/test-files/.rc-test b/test/test-files/.rc-test index 57aea54..d76f2e2 100644 --- a/test/test-files/.rc-test +++ b/test/test-files/.rc-test @@ -10,6 +10,7 @@ "production": { "THANKS": "FOR WHAT?!", "ANSWER": 42, - "ONLY": "IN PRODUCTION" + "ONLY": "IN PRODUCTION", + "BRINGATOWEL": true } -} \ No newline at end of file +} diff --git a/test/test-files/.rc-test-async.js b/test/test-files/.rc-test-async.js index a3fb453..356914f 100644 --- a/test/test-files/.rc-test-async.js +++ b/test/test-files/.rc-test-async.js @@ -1,19 +1,20 @@ module.exports = new Promise((resolve) => { setTimeout(() => { resolve({ - 'development': { - 'THANKS': 'FOR ALL THE FISH', - 'ANSWER': 0 + development: { + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, }, - 'test': { - 'THANKS': 'FOR MORE FISHIES', - 'ANSWER': 21 + test: { + THANKS: 'FOR MORE FISHIES', + ANSWER: 21, + }, + production: { + THANKS: 'FOR WHAT?!', + ANSWER: 42, + ONLY: 'IN PRODUCTION', + BRINGATOWEL: true, }, - 'production': { - 'THANKS': 'FOR WHAT?!', - 'ANSWER': 42, - 'ONLY': 'IN PRODUCTION' - } }) }, 200) }) diff --git a/test/test-files/.rc-test.json b/test/test-files/.rc-test.json index 57aea54..d76f2e2 100644 --- a/test/test-files/.rc-test.json +++ b/test/test-files/.rc-test.json @@ -10,6 +10,7 @@ "production": { "THANKS": "FOR WHAT?!", "ANSWER": 42, - "ONLY": "IN PRODUCTION" + "ONLY": "IN PRODUCTION", + "BRINGATOWEL": true } -} \ No newline at end of file +} diff --git a/test/test-files/test b/test/test-files/test index bee4cae..bd9a1d2 100644 --- a/test/test-files/test +++ b/test/test-files/test @@ -1,4 +1,5 @@ THANKS = FOR WHAT?! ANSWER=42 ONLY= "IN=PRODUCTION" -GALAXY="hitch\nhiking" \ No newline at end of file +GALAXY="hitch\nhiking" +BRINGATOWEL=true diff --git a/test/test-files/test-async.js b/test/test-files/test-async.js index ba95d4d..7ba707d 100644 --- a/test/test-files/test-async.js +++ b/test/test-files/test-async.js @@ -2,7 +2,7 @@ module.exports = new Promise((resolve) => { setTimeout(() => { resolve({ THANKS: 'FOR ALL THE FISH', - ANSWER: 0 + ANSWER: 0, }) }, 200) }) diff --git a/test/test-files/test-newlines.json b/test/test-files/test-newlines.json index 912f322..52d404b 100644 --- a/test/test-files/test-newlines.json +++ b/test/test-files/test-newlines.json @@ -2,5 +2,6 @@ "THANKS": "FOR WHAT?!", "ANSWER": 42, "ONLY": "IN\n PRODUCTION", - "GALAXY": "hitch\nhiking\n\n" -} \ No newline at end of file + "GALAXY": "hitch\nhiking\n\n", + "BRINGATOWEL": true +} diff --git a/test/test-files/test.js b/test/test-files/test.js index 0ef99e6..5e25974 100644 --- a/test/test-files/test.js +++ b/test/test-files/test.js @@ -1,5 +1,5 @@ module.exports = { THANKS: 'FOR ALL THE FISH', ANSWER: 0, - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', } diff --git a/test/test-files/test.json b/test/test-files/test.json index a90c062..379f9c6 100644 --- a/test/test-files/test.json +++ b/test/test-files/test.json @@ -2,5 +2,6 @@ "THANKS": "FOR WHAT?!", "ANSWER": 42, "ONLY": "IN PRODUCTION", - "GALAXY": "hitch\nhiking" -} \ No newline at end of file + "GALAXY": "hitch\nhiking", + "BRINGATOWEL": true +} diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 479ad3d..8afac0e 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -48,5 +48,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) + }) }) }) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index 17ae7bb..0000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true - }, - "include": [ - "src/**/*", - "test/**/*", - "bin/**/*" - ] -} \ No newline at end of file