Merge branch 'master' into master

This commit is contained in:
Todd Bluhm 2024-12-03 06:05:58 -06:00 committed by GitHub
commit 6f0d50b83c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 1327 additions and 890 deletions

67
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,67 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "11:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: "@types/node"
versions:
- ">= 13.a, < 14"
- dependency-name: "@types/node"
versions:
- 14.14.22
- 14.14.25
- 14.14.27
- 14.14.28
- 14.14.30
- 14.14.31
- 14.14.32
- 14.14.33
- 14.14.34
- 14.14.35
- 14.14.36
- 14.14.37
- 14.14.39
- 14.14.41
- 15.0.0
- dependency-name: husky
versions:
- 5.0.9
- 5.1.0
- 5.1.1
- 5.1.2
- 5.1.3
- 5.2.0
- dependency-name: mocha
versions:
- 8.2.1
- 8.3.0
- 8.3.1
- dependency-name: typescript
versions:
- 4.1.3
- 4.1.4
- 4.1.5
- 4.2.2
- 4.2.3
- dependency-name: "@commitlint/cli"
versions:
- 11.0.0
- 12.0.0
- 12.0.1
- dependency-name: "@commitlint/config-conventional"
versions:
- 11.0.0
- 12.0.0
- 12.0.1
- dependency-name: "@types/mocha"
versions:
- 8.2.0
- 8.2.1
- dependency-name: commander
versions:
- 7.0.0
- 7.1.0

View File

@ -3,17 +3,19 @@ name: linux tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
HUSKY: 0
strategy: strategy:
matrix: matrix:
node-version: [8.x, 10.x, 12.x] node-version: [18.x, 20.x, 22.x]
steps: steps:
- name: Checkout project - name: Checkout Project
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -23,7 +25,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -33,20 +35,24 @@ jobs:
- name: Lint Files - name: Lint Files
run: npm run lint run: npm run lint
- name: Run Tests and Converage - name: Run Tests and Coverage
env: env:
CI: true CI: true
run: npm run test-cover run: npm run test-cover
- name: Coveralls Parallel - name: Send Coverage to Coveralls
uses: coverallsapp/github-action@master uses: coverallsapp/github-action@v2
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true parallel: true
path-to-lcov: ./coverage/lcov.info path-to-lcov: ./coverage/lcov.info
- name: Coveralls Finished finish:
uses: coverallsapp/github-action@master needs: test
with: runs-on: ubuntu-latest
github-token: ${{ secrets.GITHUB_TOKEN }} steps:
parallel-finished: true - name: Close Coveralls Parallel Build
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

View File

@ -3,22 +3,25 @@ name: windows tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build: test:
runs-on: windows-latest runs-on: windows-latest
env:
HUSKY: 0
strategy: strategy:
matrix: matrix:
node-version: [8.x, 10.x, 12.x] node-version: [18.x, 20.x, 22.x]
steps: steps:
- name: Checkout project - name: Checkout Project
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -28,20 +31,24 @@ jobs:
- name: Lint Files - name: Lint Files
run: npm run lint run: npm run lint
- name: Run Tests and Converage - name: Run Tests and Coverage
env: env:
CI: true CI: true
run: npm run test-cover run: npm run test-cover
- name: Coveralls Parallel - name: Send Coverage to Coveralls
uses: coverallsapp/github-action@master uses: coverallsapp/github-action@v2
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true parallel: true
path-to-lcov: ./coverage/lcov.info path-to-lcov: ./coverage/lcov.info
- name: Coveralls Finished finish:
uses: coverallsapp/github-action@master needs: test
with: runs-on: ubuntu-latest
github-token: ${{ secrets.GITHUB_TOKEN }} steps:
parallel-finished: true - name: Close Coveralls Parallel Build
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

1
.husky/commit-msg Normal file
View File

@ -0,0 +1 @@
npx commitlint --edit

8
.mocharc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": ["tsx/esm", "esmock"],
"extensions": ["ts"],
"spec": [
"test/**/*.ts"
]
}

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Todd Bluhm Copyright (c) Todd Bluhm
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -4,8 +4,7 @@
[![npm](https://badgen.net/npm/v/env-cmd)](https://www.npmjs.com/package/env-cmd) [![npm](https://badgen.net/npm/v/env-cmd)](https://www.npmjs.com/package/env-cmd)
[![npm](https://badgen.net/npm/dm/env-cmd)](https://www.npmjs.com/package/env-cmd) [![npm](https://badgen.net/npm/dm/env-cmd)](https://www.npmjs.com/package/env-cmd)
[![License](https://badgen.net/github/license/toddbluhm/env-cmd)](https://github.com/toddbluhm/env-cmd/blob/master/LICENSE) [![License](https://badgen.net/github/license/toddbluhm/env-cmd)](https://github.com/toddbluhm/env-cmd/blob/master/LICENSE)
[![TS-Standard - Typescript Standard Style Guide](https://badgen.net/badge/code%20style/ts-standard/blue?icon=typescript)](https://github.com/toddbluhm/ts-standard) [![Typescript-ESLint](https://badgen.net/badge/code%20style/typescript-eslint/blue?icon=typescript)](https://github.com/typescript-eslint/typescript-eslint)
[![Dependabot badge](https://badgen.net/dependabot/toddbluhm/env-cmd?icon=dependabot)](https://dependabot.com/)
# env-cmd # env-cmd

View File

@ -1,2 +1,3 @@
#! /usr/bin/env node #! /usr/bin/env node
require('../dist').CLI(process.argv.slice(2)) import { CLI } from '../dist/index.js'
CLI(process.argv.slice(2))

8
dist/cli.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import type { Environment } from './types.ts';
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<Environment>}
*/
export declare function CLI(args: string[]): Promise<Environment>;

21
dist/cli.js vendored Normal file
View File

@ -0,0 +1,21 @@
import * as processLib from 'node:process';
import { EnvCmd } from './env-cmd.js';
import { parseArgs } from './parse-args.js';
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<Environment>}
*/
export async function CLI(args) {
// Parse the args from the command line
const parsedArgs = parseArgs(args);
// Run EnvCmd
try {
return await EnvCmd(parsedArgs);
}
catch (e) {
console.error(e);
return processLib.exit(1);
}
}

17
dist/env-cmd.d.ts vendored
View File

@ -1,21 +1,10 @@
import { EnvCmdOptions } from './types'; import type { EnvCmdOptions, Environment } from './types.ts';
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<{ [key: string]: any }>}
*/
export declare function CLI(args: string[]): Promise<{
[key: string]: any;
}>;
/** /**
* The main env-cmd program. This will spawn a new process and run the given command using * The main env-cmd program. This will spawn a new process and run the given command using
* various environment file solutions. * various environment file solutions.
* *
* @export * @export
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value * @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
*/ */
export declare function EnvCmd({ command, commandArgs, envFile, rc, options }: EnvCmdOptions): Promise<{ export declare function EnvCmd({ command, commandArgs, envFile, rc, options, }: EnvCmdOptions): Promise<Environment>;
[key: string]: any;
}>;

55
dist/env-cmd.js vendored
View File

@ -1,70 +1,47 @@
"use strict"; import { default as spawn } from 'cross-spawn';
Object.defineProperty(exports, "__esModule", { value: true }); import { TermSignals } from './signal-termination.js';
const spawn_1 = require("./spawn"); import { getEnvVars } from './get-env-vars.js';
const signal_termination_1 = require("./signal-termination"); import { expandEnvs } from './expand-envs.js';
const parse_args_1 = require("./parse-args"); import * as processLib from 'node:process';
const get_env_vars_1 = require("./get-env-vars");
const expand_envs_1 = require("./expand-envs");
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<{ [key: string]: any }>}
*/
async function CLI(args) {
// Parse the args from the command line
const parsedArgs = parse_args_1.parseArgs(args);
// Run EnvCmd
try {
return await exports.EnvCmd(parsedArgs);
}
catch (e) {
console.error(e);
return process.exit(1);
}
}
exports.CLI = CLI;
/** /**
* The main env-cmd program. This will spawn a new process and run the given command using * The main env-cmd program. This will spawn a new process and run the given command using
* various environment file solutions. * various environment file solutions.
* *
* @export * @export
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value * @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
*/ */
async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) { export async function EnvCmd({ command, commandArgs, envFile, rc, options = {}, }) {
var _a;
let env = {}; let env = {};
try { try {
env = await get_env_vars_1.getEnvVars({ envFile, rc, verbose: options.verbose }); env = await getEnvVars({ envFile, rc, verbose: options.verbose });
} }
catch (e) { catch (e) {
if (!((_a = options.silent) !== null && _a !== void 0 ? _a : false)) { if (!(options.silent ?? false)) {
throw e; throw e;
} }
} }
// Override the merge order if --no-override flag set // Override the merge order if --no-override flag set
if (options.noOverride === true) { if (options.noOverride === true) {
env = Object.assign({}, env, process.env); env = Object.assign({}, env, processLib.env);
} }
else { else {
// Add in the system environment variables to our environment list // Add in the system environment variables to our environment list
env = Object.assign({}, process.env, env); env = Object.assign({}, processLib.env, env);
} }
if (options.expandEnvs === true) { if (options.expandEnvs === true) {
command = expand_envs_1.expandEnvs(command, env); command = expandEnvs(command, env);
commandArgs = commandArgs.map(arg => expand_envs_1.expandEnvs(arg, env)); commandArgs = commandArgs.map(arg => expandEnvs(arg, env));
} }
// Execute the command with the given environment variables // Execute the command with the given environment variables
const proc = spawn_1.spawn(command, commandArgs, { const proc = spawn(command, commandArgs, {
stdio: 'inherit', stdio: 'inherit',
shell: options.useShell, shell: options.useShell,
env env: env,
}); });
// Handle any termination signals for parent and child proceses // Handle any termination signals for parent and child proceses
const signals = new signal_termination_1.TermSignals({ verbose: options.verbose }); const signals = new TermSignals({ verbose: options.verbose });
signals.handleUncaughtExceptions(); signals.handleUncaughtExceptions();
signals.handleTermSignals(proc); signals.handleTermSignals(proc);
return env; return env;
} }
exports.EnvCmd = EnvCmd;

View File

@ -1,7 +1,6 @@
import type { Environment } from './types.ts';
/** /**
* expandEnvs Replaces $var in args and command with environment variables * expandEnvs Replaces $var in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is. * if the environment variable doesn't exist, it leaves it as is.
*/ */
export declare function expandEnvs(str: string, envs: { export declare function expandEnvs(str: string, envs: Environment): string;
[key: string]: any;
}): string;

12
dist/expand-envs.js vendored
View File

@ -1,13 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/** /**
* expandEnvs Replaces $var in args and command with environment variables * expandEnvs Replaces $var in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is. * if the environment variable doesn't exist, it leaves it as is.
*/ */
function expandEnvs(str, envs) { export function expandEnvs(str, envs) {
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => { return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, (varName) => {
const varValue = envs[varName.slice(1)]; const varValue = envs[varName.slice(1)];
return varValue === undefined ? varName : varValue; // const test = 42;
return varValue === undefined ? varName : varValue.toString();
}); });
} }
exports.expandEnvs = expandEnvs;

View File

@ -1,18 +1,12 @@
import { GetEnvVarOptions } from './types'; import type { GetEnvVarOptions, Environment } from './types.ts';
export declare function getEnvVars(options?: GetEnvVarOptions): Promise<{ export declare function getEnvVars(options?: GetEnvVarOptions): Promise<Environment>;
[key: string]: any;
}>;
export declare function getEnvFile({ filePath, fallback, verbose }: { export declare function getEnvFile({ filePath, fallback, verbose }: {
filePath?: string; filePath?: string;
fallback?: boolean; fallback?: boolean;
verbose?: boolean; verbose?: boolean;
}): Promise<{ }): Promise<Environment>;
[key: string]: any;
}>;
export declare function getRCFile({ environments, filePath, verbose }: { export declare function getRCFile({ environments, filePath, verbose }: {
environments: string[]; environments: string[];
filePath?: string; filePath?: string;
verbose?: boolean; verbose?: boolean;
}): Promise<{ }): Promise<Environment>;
[key: string]: any;
}>;

70
dist/get-env-vars.js vendored
View File

@ -1,40 +1,38 @@
"use strict"; import { getRCFileVars } from './parse-rc-file.js';
Object.defineProperty(exports, "__esModule", { value: true }); import { getEnvFileVars } from './parse-env-file.js';
const parse_rc_file_1 = require("./parse-rc-file");
const parse_env_file_1 = require("./parse-env-file");
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json']; const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'];
const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json']; const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'];
async function getEnvVars(options = {}) { export async function getEnvVars(options = {}) {
options.envFile = options.envFile !== undefined ? options.envFile : {}; options.envFile = options.envFile ?? {};
// Check for rc file usage // Check for rc file usage
if (options.rc !== undefined) { if (options.rc !== undefined) {
return await getRCFile({ return await getRCFile({
environments: options.rc.environments, environments: options.rc.environments,
filePath: options.rc.filePath, filePath: options.rc.filePath,
verbose: options.verbose verbose: options.verbose,
}); });
} }
return await getEnvFile({ return await getEnvFile({
filePath: options.envFile.filePath, filePath: options.envFile.filePath,
fallback: options.envFile.fallback, fallback: options.envFile.fallback,
verbose: options.verbose verbose: options.verbose,
}); });
} }
exports.getEnvVars = getEnvVars; export async function getEnvFile({ filePath, fallback, verbose }) {
async function getEnvFile({ filePath, fallback, verbose }) {
// Use env file // Use env file
if (filePath !== undefined) { if (filePath !== undefined) {
try { try {
const env = await parse_env_file_1.getEnvFileVars(filePath); const env = await getEnvFileVars(filePath);
if (verbose === true) { if (verbose === true) {
console.info(`Found .env file at path: ${filePath}`); console.info(`Found .env file at path: ${filePath}`);
} }
return env; return env;
} }
catch (e) { catch {
if (verbose === true) { if (verbose === true) {
console.info(`Failed to find .env file at path: ${filePath}`); console.info(`Failed to find .env file at path: ${filePath}`);
} }
// Ignore error as we are just trying this location
} }
if (fallback !== true) { if (fallback !== true) {
throw new Error(`Failed to find .env file at path: ${filePath}`); throw new Error(`Failed to find .env file at path: ${filePath}`);
@ -43,13 +41,15 @@ async function getEnvFile({ filePath, fallback, verbose }) {
// Use the default env file locations // Use the default env file locations
for (const path of ENV_FILE_DEFAULT_LOCATIONS) { for (const path of ENV_FILE_DEFAULT_LOCATIONS) {
try { try {
const env = await parse_env_file_1.getEnvFileVars(path); const env = await getEnvFileVars(path);
if (verbose === true) { if (verbose === true) {
console.info(`Found .env file at default path: ${path}`); console.info(`Found .env file at default path: ${path}`);
} }
return env; 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(',')}]`; const error = `Failed to find .env file at default paths: [${ENV_FILE_DEFAULT_LOCATIONS.join(',')}]`;
if (verbose === true) { if (verbose === true) {
@ -57,26 +57,27 @@ async function getEnvFile({ filePath, fallback, verbose }) {
} }
throw new Error(error); throw new Error(error);
} }
exports.getEnvFile = getEnvFile; export async function getRCFile({ environments, filePath, verbose }) {
async function getRCFile({ environments, filePath, verbose }) {
// User provided an .rc file path // User provided an .rc file path
if (filePath !== undefined) { if (filePath !== undefined) {
try { try {
const env = await parse_rc_file_1.getRCFileVars({ environments, filePath }); const env = await getRCFileVars({ environments, filePath });
if (verbose === true) { if (verbose === true) {
console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`); console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`);
} }
return env; return env;
} }
catch (e) { catch (e) {
if (e.name === 'PathError') { if (e instanceof Error) {
if (verbose === true) { if (e.name === 'PathError') {
console.info(`Failed to find .rc file at path: ${filePath}`); if (verbose === true) {
console.info(`Failed to find .rc file at path: ${filePath}`);
}
} }
} if (e.name === 'EnvironmentError') {
if (e.name === 'EnvironmentError') { if (verbose === true) {
if (verbose === true) { console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`);
console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`); }
} }
} }
throw e; throw e;
@ -85,19 +86,27 @@ async function getRCFile({ environments, filePath, verbose }) {
// Use the default .rc file locations // Use the default .rc file locations
for (const path of RC_FILE_DEFAULT_LOCATIONS) { for (const path of RC_FILE_DEFAULT_LOCATIONS) {
try { try {
const env = await parse_rc_file_1.getRCFileVars({ environments, filePath: path }); const env = await getRCFileVars({ environments, filePath: path });
if (verbose === true) { if (verbose === true) {
console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`); console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`);
} }
return env; return env;
} }
catch (e) { catch (e) {
if (e.name === 'EnvironmentError') { if (e instanceof Error) {
const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`; if (e.name === 'EnvironmentError') {
if (verbose === true) { const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`;
console.info(errorText); if (verbose === true) {
console.info(errorText);
}
throw new Error(errorText);
}
if (e.name === 'ParseError') {
if (verbose === true) {
console.info(e.message);
}
throw new Error(e.message);
} }
throw new Error(errorText);
} }
} }
} }
@ -107,4 +116,3 @@ async function getRCFile({ environments, filePath, verbose }) {
} }
throw new Error(errorText); throw new Error(errorText);
} }
exports.getRCFile = getRCFile;

7
dist/index.d.ts vendored
View File

@ -1,4 +1,5 @@
import { getEnvVars } from './get-env-vars'; import { getEnvVars } from './get-env-vars.js';
export * from './types'; export * from './types.js';
export * from './env-cmd'; export * from './cli.js';
export * from './env-cmd.js';
export declare const GetEnvVars: typeof getEnvVars; export declare const GetEnvVars: typeof getEnvVars;

14
dist/index.js vendored
View File

@ -1,8 +1,6 @@
"use strict"; import { getEnvVars } from './get-env-vars.js';
function __export(m) { // Export the core env-cmd API
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; export * from './types.js';
} export * from './cli.js';
Object.defineProperty(exports, "__esModule", { value: true }); export * from './env-cmd.js';
const get_env_vars_1 = require("./get-env-vars"); export const GetEnvVars = getEnvVars;
__export(require("./env-cmd"));
exports.GetEnvVars = get_env_vars_1.getEnvVars;

View File

@ -1,7 +1,6 @@
import * as commander from 'commander'; import type { EnvCmdOptions, CommanderOptions } from './types.ts';
import { EnvCmdOptions } from './types';
/** /**
* Parses the arguments passed into the cli * Parses the arguments passed into the cli
*/ */
export declare function parseArgs(args: string[]): EnvCmdOptions; export declare function parseArgs(args: string[]): EnvCmdOptions;
export declare function parseArgsUsingCommander(args: string[]): commander.Command; export declare function parseArgsUsingCommander(args: string[]): CommanderOptions;

29
dist/parse-args.js vendored
View File

@ -1,13 +1,10 @@
"use strict"; import * as commander from 'commander';
Object.defineProperty(exports, "__esModule", { value: true }); import { parseArgList } from './utils.js';
const commander = require("commander"); import { default as packageJson } from '../package.json' with { type: 'json' };
const utils_1 = require("./utils");
// Use commonjs require to prevent a weird folder hierarchy in dist
const packageJson = require('../package.json'); /* eslint-disable-line */
/** /**
* Parses the arguments passed into the cli * Parses the arguments passed into the cli
*/ */
function parseArgs(args) { export function parseArgs(args) {
// Run the initial arguments through commander in order to determine // Run the initial arguments through commander in order to determine
// which value in the args array is the `command` to execute // which value in the args array is the `command` to execute
let program = parseArgsUsingCommander(args); let program = parseArgsUsingCommander(args);
@ -39,17 +36,19 @@ function parseArgs(args) {
silent = true; silent = true;
} }
let rc; let rc;
if (program.environments !== undefined && program.environments.length !== 0) { if (program.environments !== undefined
&& Array.isArray(program.environments)
&& program.environments.length !== 0) {
rc = { rc = {
environments: program.environments, environments: program.environments,
filePath: program.rcFile filePath: program.rcFile,
}; };
} }
let envFile; let envFile;
if (program.file !== undefined) { if (program.file !== undefined) {
envFile = { envFile = {
filePath: program.file, filePath: program.file,
fallback: program.fallback fallback: program.fallback,
}; };
} }
const options = { const options = {
@ -62,21 +61,20 @@ function parseArgs(args) {
noOverride, noOverride,
silent, silent,
useShell, useShell,
verbose verbose,
} },
}; };
if (verbose) { if (verbose) {
console.info(`Options: ${JSON.stringify(options, null, 0)}`); console.info(`Options: ${JSON.stringify(options, null, 0)}`);
} }
return options; return options;
} }
exports.parseArgs = parseArgs; export function parseArgsUsingCommander(args) {
function parseArgsUsingCommander(args) {
const program = new commander.Command(); const program = new commander.Command();
return program return program
.version(packageJson.version, '-v, --version') .version(packageJson.version, '-v, --version')
.usage('[options] <command> [...args]') .usage('[options] <command> [...args]')
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', utils_1.parseArgList) .option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList)
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)') .option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
.option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--fallback', 'Fallback to default env file path, if custom env file path not found')
.option('--no-override', 'Do not override existing environment variables') .option('--no-override', 'Do not override existing environment variables')
@ -88,4 +86,3 @@ function parseArgsUsingCommander(args) {
.allowUnknownOption(true) .allowUnknownOption(true)
.parse(['_', '_', ...args]); .parse(['_', '_', ...args]);
} }
exports.parseArgsUsingCommander = parseArgsUsingCommander;

View File

@ -1,21 +1,16 @@
import type { Environment } from './types.ts';
/** /**
* Gets the environment vars from an env file * Gets the environment vars from an env file
*/ */
export declare function getEnvFileVars(envFilePath: string): Promise<{ export declare function getEnvFileVars(envFilePath: string): Promise<Environment>;
[key: string]: any;
}>;
/** /**
* Parse out all env vars from a given env file string and return an object * Parse out all env vars from a given env file string and return an object
*/ */
export declare function parseEnvString(envFileString: string): { export declare function parseEnvString(envFileString: string): Environment;
[key: string]: string;
};
/** /**
* Parse out all env vars from an env file string * Parse out all env vars from an env file string
*/ */
export declare function parseEnvVars(envString: string): { export declare function parseEnvVars(envString: string): Environment;
[key: string]: string;
};
/** /**
* Strips out comments from env file string * Strips out comments from env file string
*/ */

View File

@ -1,37 +1,48 @@
"use strict"; import { existsSync, readFileSync } from 'node:fs';
Object.defineProperty(exports, "__esModule", { value: true }); import { extname } from 'node:path';
const fs = require("fs"); import { pathToFileURL } from 'node:url';
const path = require("path"); import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
const utils_1 = require("./utils");
const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs'];
/** /**
* Gets the environment vars from an env file * Gets the environment vars from an env file
*/ */
async function getEnvFileVars(envFilePath) { export async function getEnvFileVars(envFilePath) {
const absolutePath = utils_1.resolveEnvFilePath(envFilePath); const absolutePath = resolveEnvFilePath(envFilePath);
if (!fs.existsSync(absolutePath)) { if (!existsSync(absolutePath)) {
const pathError = new Error(`Invalid env file path (${envFilePath}).`); const pathError = new Error(`Invalid env file path (${envFilePath}).`);
pathError.name = 'PathError'; pathError.name = 'PathError';
throw pathError; throw pathError;
} }
// Get the file extension // Get the file extension
const ext = path.extname(absolutePath).toLowerCase(); const ext = extname(absolutePath).toLowerCase();
let env = {}; let env = {};
if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise = require(absolutePath); /* eslint-disable-line */ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
env = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; let attributeTypes = {};
if (ext === '.json') {
attributeTypes = { with: { type: 'json' } };
}
const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
if ('default' in res) {
env = res.default;
}
else {
env = res;
}
// Check to see if the imported value is a promise
if (isPromise(env)) {
env = await env;
}
} }
else { else {
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }); const file = readFileSync(absolutePath, { encoding: 'utf8' });
env = parseEnvString(file); env = parseEnvString(file);
} }
return env; return env;
} }
exports.getEnvFileVars = getEnvFileVars;
/** /**
* Parse out all env vars from a given env file string and return an object * Parse out all env vars from a given env file string and return an object
*/ */
function parseEnvString(envFileString) { export function parseEnvString(envFileString) {
// First thing we do is stripe out all comments // First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString()); envFileString = stripComments(envFileString.toString());
// Next we stripe out all the empty lines // Next we stripe out all the empty lines
@ -39,30 +50,41 @@ function parseEnvString(envFileString) {
// Merge the file env vars with the current process env vars (the file vars overwrite process vars) // Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return parseEnvVars(envFileString); return parseEnvVars(envFileString);
} }
exports.parseEnvString = parseEnvString;
/** /**
* Parse out all env vars from an env file string * Parse out all env vars from an env file string
*/ */
function parseEnvVars(envString) { export function parseEnvVars(envString) {
const envParseRegex = /^((.+?)[=](.*))$/gim; const envParseRegex = /^((.+?)[=](.*))$/gim;
const matches = {}; const matches = {};
let match; let match;
while ((match = envParseRegex.exec(envString)) !== null) { while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line // Note: match[1] is the full env=var line
const key = match[2].trim(); const key = match[2].trim();
const value = match[3].trim(); let value = match[3].trim();
// remove any surrounding quotes // remove any surrounding quotes
matches[key] = value value = value
.replace(/(^['"]|['"]$)/g, '') .replace(/(^['"]|['"]$)/g, '')
.replace(/\\n/g, '\n'); .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));
} }
exports.parseEnvVars = parseEnvVars;
/** /**
* Strips out comments from env file string * Strips out comments from env file string
*/ */
function stripComments(envString) { export function stripComments(envString) {
const commentsRegex = /(^#.*$)/gim; const commentsRegex = /(^#.*$)/gim;
let match = commentsRegex.exec(envString); let match = commentsRegex.exec(envString);
let newString = envString; let newString = envString;
@ -72,12 +94,10 @@ function stripComments(envString) {
} }
return newString; return newString;
} }
exports.stripComments = stripComments;
/** /**
* Strips out newlines from env file string * Strips out newlines from env file string
*/ */
function stripEmptyLines(envString) { export function stripEmptyLines(envString) {
const emptyLinesRegex = /(^\n)/gim; const emptyLinesRegex = /(^\n)/gim;
return envString.replace(emptyLinesRegex, ''); return envString.replace(emptyLinesRegex, '');
} }
exports.stripEmptyLines = stripEmptyLines;

View File

@ -1,9 +1,8 @@
import type { Environment } from './types.ts';
/** /**
* Gets the env vars from the rc file and rc environments * Gets the env vars from the rc file and rc environments
*/ */
export declare function getRCFileVars({ environments, filePath }: { export declare function getRCFileVars({ environments, filePath }: {
environments: string[]; environments: string[];
filePath: string; filePath: string;
}): Promise<{ }): Promise<Environment>;
[key: string]: any;
}>;

66
dist/parse-rc-file.js vendored
View File

@ -1,31 +1,44 @@
"use strict"; import { stat, readFile } from 'node:fs';
Object.defineProperty(exports, "__esModule", { value: true }); import { promisify } from 'node:util';
const fs_1 = require("fs"); import { extname } from 'node:path';
const util_1 = require("util"); import { pathToFileURL } from 'node:url';
const path_1 = require("path"); import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
const utils_1 = require("./utils"); const statAsync = promisify(stat);
const statAsync = util_1.promisify(fs_1.stat); const readFileAsync = promisify(readFile);
const readFileAsync = util_1.promisify(fs_1.readFile);
/** /**
* Gets the env vars from the rc file and rc environments * Gets the env vars from the rc file and rc environments
*/ */
async function getRCFileVars({ environments, filePath }) { export async function getRCFileVars({ environments, filePath }) {
const absolutePath = utils_1.resolveEnvFilePath(filePath); const absolutePath = resolveEnvFilePath(filePath);
try { try {
await statAsync(absolutePath); await statAsync(absolutePath);
} }
catch (e) { catch {
const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`); const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`);
pathError.name = 'PathError'; pathError.name = 'PathError';
throw pathError; throw pathError;
} }
// Get the file extension // Get the file extension
const ext = path_1.extname(absolutePath).toLowerCase(); const ext = extname(absolutePath).toLowerCase();
let parsedData; let parsedData = {};
try { try {
if (ext === '.json' || ext === '.js' || ext === '.cjs') { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise = require(absolutePath); /* eslint-disable-line */ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; let attributeTypes = {};
if (ext === '.json') {
attributeTypes = { with: { type: 'json' } };
}
const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
if ('default' in res) {
parsedData = res.default;
}
else {
parsedData = res;
}
// Check to see if the imported value is a promise
if (isPromise(parsedData)) {
parsedData = await parsedData;
}
} }
else { else {
const file = await readFileAsync(absolutePath, { encoding: 'utf8' }); const file = await readFileAsync(absolutePath, { encoding: 'utf8' });
@ -33,20 +46,26 @@ async function getRCFileVars({ environments, filePath }) {
} }
} }
catch (e) { catch (e) {
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`); const errorMessage = e instanceof Error ? e.message : 'Unknown error';
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`);
parseError.name = 'ParseError'; parseError.name = 'ParseError';
throw parseError; throw parseError;
} }
// Parse and merge multiple rc environments together // Parse and merge multiple rc environments together
let result = {}; let result = {};
let environmentFound = false; let environmentFound = false;
environments.forEach((name) => { for (const name of environments) {
const envVars = parsedData[name]; if (name in parsedData) {
if (envVars !== undefined) { const envVars = parsedData[name];
environmentFound = true; if (envVars != null && typeof envVars === 'object') {
result = Object.assign(Object.assign({}, result), envVars); environmentFound = true;
result = {
...result,
...envVars,
};
}
} }
}); }
if (!environmentFound) { if (!environmentFound) {
const environmentError = new Error(`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`); const environmentError = new Error(`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`);
environmentError.name = 'EnvironmentError'; environmentError.name = 'EnvironmentError';
@ -54,4 +73,3 @@ async function getRCFileVars({ environments, filePath }) {
} }
return result; return result;
} }
exports.getRCFileVars = getRCFileVars;

View File

@ -1,7 +1,7 @@
/// <reference types="node" />
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
export declare class TermSignals { export declare class TermSignals {
private readonly terminateSpawnedProcessFuncHandlers; private readonly terminateSpawnedProcessFuncHandlers;
private terminateSpawnedProcessFuncExitHandler?;
private readonly verbose; private readonly verbose;
_exitCalled: boolean; _exitCalled: boolean;
constructor(options?: { constructor(options?: {
@ -15,7 +15,7 @@ export declare class TermSignals {
/** /**
* Terminate parent process helper * Terminate parent process helper
*/ */
_terminateProcess(code?: number, signal?: NodeJS.Signals): void; _terminateProcess(signal?: NodeJS.Signals | number): void;
/** /**
* Exit event listener clean up helper * Exit event listener clean up helper
*/ */

View File

@ -1,73 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const SIGNALS_TO_HANDLE = [ const SIGNALS_TO_HANDLE = [
'SIGINT', 'SIGTERM', 'SIGHUP' 'SIGINT', 'SIGTERM', 'SIGHUP',
]; ];
class TermSignals { export class TermSignals {
terminateSpawnedProcessFuncHandlers = {};
terminateSpawnedProcessFuncExitHandler;
verbose = false;
_exitCalled = false;
constructor(options = {}) { constructor(options = {}) {
this.terminateSpawnedProcessFuncHandlers = {};
this.verbose = false;
this._exitCalled = false;
this.verbose = options.verbose === true; this.verbose = options.verbose === true;
} }
handleTermSignals(proc) { handleTermSignals(proc) {
// Terminate child process if parent process receives termination events // Terminate child process if parent process receives termination events
SIGNALS_TO_HANDLE.forEach((signal) => { const terminationFunc = (signal) => {
this.terminateSpawnedProcessFuncHandlers[signal] = this._removeProcessListeners();
(signal, code) => { if (!this._exitCalled) {
this._removeProcessListeners(); if (this.verbose) {
if (!this._exitCalled) { console.info('Parent process exited with signal: '
if (this.verbose) { + signal.toString()
console.info('Parent process exited with signal: ' + + '. Terminating child process...');
signal.toString() + }
'. Terminating child process...'); // Mark shared state so we do not run into a signal/exit loop
} this._exitCalled = true;
// Mark shared state so we do not run into a signal/exit loop // Use the signal code if it is an error code
this._exitCalled = true; // let correctSignal: NodeJS.Signals | undefined
// Use the signal code if it is an error code if (typeof signal === 'number') {
let correctSignal; if (signal > 0) {
if (typeof signal === 'number') { // code = signal
if (signal > (code !== null && code !== void 0 ? code : 0)) { signal = 'SIGINT';
code = signal;
correctSignal = 'SIGINT';
}
}
else {
correctSignal = signal;
}
// Kill the child process
proc.kill(correctSignal !== null && correctSignal !== void 0 ? correctSignal : code);
// Terminate the parent process
this._terminateProcess(code, correctSignal);
} }
}; }
// 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(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 // Terminate parent process if child process receives termination events
proc.on('exit', (code, signal) => { proc.on('exit', (code, signal) => {
this._removeProcessListeners(); this._removeProcessListeners();
if (!this._exitCalled) { if (!this._exitCalled) {
if (this.verbose) { if (this.verbose) {
console.info(`Child process exited with code: ${(code !== null && code !== void 0 ? code : '').toString()} and signal:` + console.info(`Child process exited with code: ${(code ?? '').toString()} and signal:`
(signal !== null && signal !== void 0 ? signal : '').toString() + + (signal ?? '').toString()
'. Terminating parent process...'); + '. Terminating parent process...');
} }
// Mark shared state so we do not run into a signal/exit loop // Mark shared state so we do not run into a signal/exit loop
this._exitCalled = true; this._exitCalled = true;
// Use the signal code if it is an error code // Use the signal code if it is an error code
let correctSignal; let correctSignal;
if (typeof signal === 'number') { if (typeof signal === 'number') {
if (signal > (code !== null && code !== void 0 ? code : 0)) { if (signal > (code ?? 0)) {
code = signal; code = signal;
correctSignal = 'SIGINT'; correctSignal = 'SIGINT';
} }
} }
else { else {
correctSignal = signal !== null && signal !== void 0 ? signal : undefined; correctSignal = signal ?? undefined;
} }
// Terminate the parent process // Terminate the parent process
this._terminateProcess(code, correctSignal); this._terminateProcess(correctSignal ?? code);
} }
}); });
} }
@ -75,17 +75,23 @@ class TermSignals {
* Enables catching of unhandled exceptions * Enables catching of unhandled exceptions
*/ */
handleUncaughtExceptions() { handleUncaughtExceptions() {
process.on('uncaughtException', (e) => this._uncaughtExceptionHandler(e)); process.on('uncaughtException', (e) => {
this._uncaughtExceptionHandler(e);
});
} }
/** /**
* Terminate parent process helper * Terminate parent process helper
*/ */
_terminateProcess(code, signal) { _terminateProcess(signal) {
if (signal !== undefined) { if (signal != null) {
return process.kill(process.pid, signal); if (typeof signal === 'string') {
} process.kill(process.pid, signal);
if (code !== undefined) { return;
return process.exit(code); }
if (typeof signal === 'number') {
process.exit(signal);
return;
}
} }
throw new Error('Unable to terminate parent process successfully'); throw new Error('Unable to terminate parent process successfully');
} }
@ -96,7 +102,9 @@ class TermSignals {
SIGNALS_TO_HANDLE.forEach((signal) => { SIGNALS_TO_HANDLE.forEach((signal) => {
process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]); process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]);
}); });
process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM); if (this.terminateSpawnedProcessFuncExitHandler != null) {
process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler);
}
} }
/** /**
* General exception handler * General exception handler
@ -106,4 +114,3 @@ class TermSignals {
process.exit(1); process.exit(1);
} }
} }
exports.TermSignals = TermSignals;

2
dist/spawn.d.ts vendored
View File

@ -1,2 +0,0 @@
import * as spawn from 'cross-spawn';
export { spawn };

4
dist/spawn.js vendored
View File

@ -1,4 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const spawn = require("cross-spawn");
exports.spawn = spawn;

34
dist/types.d.ts vendored
View File

@ -1,15 +1,31 @@
import { Command } from 'commander';
export type Environment = Partial<Record<string, string | number | boolean>>;
export type RCEnvironment = Partial<Record<string, Environment>>;
export interface CommanderOptions extends Command {
override?: boolean;
useShell?: boolean;
expandEnvs?: boolean;
verbose?: boolean;
silent?: boolean;
fallback?: boolean;
environments?: string[];
rcFile?: string;
file?: string;
}
export interface RCFileOptions {
environments: string[];
filePath?: string;
}
export interface EnvFileOptions {
filePath?: string;
fallback?: boolean;
}
export interface GetEnvVarOptions { export interface GetEnvVarOptions {
envFile?: { envFile?: EnvFileOptions;
filePath?: string; rc?: RCFileOptions;
fallback?: boolean;
};
rc?: {
environments: string[];
filePath?: string;
};
verbose?: boolean; verbose?: boolean;
} }
export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'> { export interface EnvCmdOptions extends GetEnvVarOptions {
command: string; command: string;
commandArgs: string[]; commandArgs: string[];
options?: { options?: {

3
dist/types.js vendored
View File

@ -1,2 +1 @@
"use strict"; export {};
Object.defineProperty(exports, "__esModule", { value: true });

5
dist/utils.d.ts vendored
View File

@ -1,3 +1,4 @@
export declare const IMPORT_HOOK_EXTENSIONS: string[];
/** /**
* A simple function for resolving the path the user entered * A simple function for resolving the path the user entered
*/ */
@ -7,6 +8,6 @@ export declare function resolveEnvFilePath(userPath: string): string;
*/ */
export declare function parseArgList(list: string): string[]; export declare function parseArgList(list: string): string[];
/** /**
* A simple function to test if the value is a promise * A simple function to test if the value is a promise/thenable
*/ */
export declare function isPromise(value: any | PromiseLike<Object>): value is Promise<any>; export declare function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T>;

31
dist/utils.js vendored
View File

@ -1,30 +1,31 @@
"use strict"; import { resolve } from 'node:path';
Object.defineProperty(exports, "__esModule", { value: true }); import { homedir } from 'node:os';
const path = require("path"); import { cwd } from 'node:process';
const os = require("os"); // Special file extensions that node can natively import
export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs'];
/** /**
* A simple function for resolving the path the user entered * A simple function for resolving the path the user entered
*/ */
function resolveEnvFilePath(userPath) { export function resolveEnvFilePath(userPath) {
// Make sure a home directory exist // Make sure a home directory exist
const home = os.homedir(); const home = homedir();
if (home !== undefined) { if (home != null) {
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`); userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`);
} }
return path.resolve(process.cwd(), userPath); return resolve(cwd(), userPath);
} }
exports.resolveEnvFilePath = resolveEnvFilePath;
/** /**
* A simple function that parses a comma separated string into an array of strings * A simple function that parses a comma separated string into an array of strings
*/ */
function parseArgList(list) { export function parseArgList(list) {
return list.split(','); return list.split(',');
} }
exports.parseArgList = parseArgList;
/** /**
* A simple function to test if the value is a promise * A simple function to test if the value is a promise/thenable
*/ */
function isPromise(value) { export function isPromise(value) {
return value != null && typeof value.then === 'function'; return value != null
&& typeof value === 'object'
&& 'then' in value
&& typeof value.then === 'function';
} }
exports.isPromise = isPromise;

38
eslint.config.js Normal file
View File

@ -0,0 +1,38 @@
import { default as tseslint } from 'typescript-eslint'
import { default as globals } from 'globals'
import { default as eslint } from '@eslint/js'
export default tseslint.config(
{
// Ignore build folder
ignores: ['dist/*'],
},
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
// Enable Type generation
languageOptions: {
globals: {
...globals.node,
},
parserOptions: {
project: ['./tsconfig.json', './test/tsconfig.json'],
},
}
},
{
// For test files ignore some rules
files: ['test/**/*'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off'
},
},
// Disable Type Checking JS/CJS/MJS files
{
files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
extends: [tseslint.configs.disableTypeChecked],
},
)

View File

@ -4,17 +4,19 @@
"description": "Executes a command using the environment variables in an env file", "description": "Executes a command using the environment variables in an env file",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "module",
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=18.0.0"
}, },
"bin": { "bin": {
"env-cmd": "bin/env-cmd.js" "env-cmd": "bin/env-cmd.js"
}, },
"scripts": { "scripts": {
"test": "mocha -r ts-node/register ./test/**/*.ts", "prepare": "husky",
"test-cover": "nyc npm test", "test": "mocha",
"test-cover": "c8 npm test",
"coveralls": "coveralls < coverage/lcov.info", "coveralls": "coveralls < coverage/lcov.info",
"lint": "ts-standard --fix && tsc --noEmit", "lint": "npx eslint .",
"build": "tsc", "build": "tsc",
"watch": "tsc -w" "watch": "tsc -w"
}, },
@ -51,68 +53,35 @@
"cross-spawn": "^7.0.6" "cross-spawn": "^7.0.6"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^8.0.0", "@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^8.0.0", "@commitlint/config-conventional": "^19.6.0",
"@types/chai": "^4.0.0", "@eslint/js": "^9.16.0",
"@types/chai": "^5.0.1",
"@types/cross-spawn": "^6.0.6", "@types/cross-spawn": "^6.0.6",
"@types/mocha": "^7.0.0", "@types/mocha": "^10.0.10",
"@types/node": "^12.0.0", "@types/node": "^22.10.1",
"@types/sinon": "^9.0.0", "@types/sinon": "^17.0.3",
"chai": "^4.0.0", "c8": "^10.1.2",
"chai": "^5.1.2",
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"husky": "^4.0.0", "esmock": "^2.6.9",
"mocha": "^7.0.0", "globals": "^15.12.0",
"nyc": "^15.0.0", "husky": "^9.1.7",
"sinon": "^9.0.0", "mocha": "^11.0.0",
"ts-node": "^8.0.0", "sinon": "^19.0.2",
"ts-standard": "^8.0.0", "tsx": "^4.19.2",
"typescript": "^3.7.0" "typescript": "^5.7.2",
}, "typescript-eslint": "^8.15.0"
"nyc": {
"include": [
"src/**/*.ts"
],
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"lcov"
],
"sourceMap": true,
"instrument": true
},
"ts-standard": {
"project": "./tsconfig.eslint.json",
"ignore": [
"dist"
]
},
"greenkeeper": {
"ignore": [
"@types/node"
],
"commitMessages": {
"initialBadge": "docs: add greenkeeper badge",
"initialDependencies": "chore: update dependencies",
"initialBranches": "chore: whitelist greenkeeper branches",
"dependencyUpdate": "chore: update dependency ${dependency}",
"devDependencyUpdate": "chore: update devDependecy ${dependency}",
"dependencyPin": "fix: pin dependency ${dependency}",
"devDependencyPin": "fix: pin devDependecy ${dependency}"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": [
"@commitlint/config-conventional" "@commitlint/config-conventional"
] ]
},
"c8": {
"reporter": [
"text",
"lcov"
]
} }
} }

25
src/cli.ts Normal file
View File

@ -0,0 +1,25 @@
import * as processLib from 'node:process'
import type { Environment } from './types.ts'
import { EnvCmd } from './env-cmd.js'
import { parseArgs } from './parse-args.js'
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<Environment>}
*/
export async function CLI(args: string[]): Promise<Environment> {
// Parse the args from the command line
const parsedArgs = parseArgs(args)
// Run EnvCmd
try {
return await EnvCmd(parsedArgs)
}
catch (e) {
console.error(e)
return processLib.exit(1)
}
}

View File

@ -1,28 +1,9 @@
import { spawn } from './spawn' import { default as spawn } from 'cross-spawn'
import { EnvCmdOptions } from './types' import type { EnvCmdOptions, Environment } from './types.ts'
import { TermSignals } from './signal-termination' import { TermSignals } from './signal-termination.js'
import { parseArgs } from './parse-args' import { getEnvVars } from './get-env-vars.js'
import { getEnvVars } from './get-env-vars' import { expandEnvs } from './expand-envs.js'
import { expandEnvs } from './expand-envs' import * as processLib from 'node:process'
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<{ [key: string]: any }>}
*/
export async function CLI (args: string[]): Promise<{ [key: string]: any }> {
// Parse the args from the command line
const parsedArgs = parseArgs(args)
// Run EnvCmd
try {
return await (exports.EnvCmd(parsedArgs) as Promise<{ [key: string]: any }>)
} catch (e) {
console.error(e)
return process.exit(1)
}
}
/** /**
* The main env-cmd program. This will spawn a new process and run the given command using * The main env-cmd program. This will spawn a new process and run the given command using
@ -30,31 +11,33 @@ export async function CLI (args: string[]): Promise<{ [key: string]: any }> {
* *
* @export * @export
* @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
* @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value * @returns {Promise<Environment>} Returns an object containing [environment variable name]: value
*/ */
export async function EnvCmd ( export async function EnvCmd(
{ {
command, command,
commandArgs, commandArgs,
envFile, envFile,
rc, rc,
options = {} options = {},
}: EnvCmdOptions }: EnvCmdOptions,
): Promise<{ [key: string]: any }> { ): Promise<Environment> {
let env: { [name: string]: string } = {} let env: Environment = {}
try { try {
env = await getEnvVars({ envFile, rc, verbose: options.verbose }) env = await getEnvVars({ envFile, rc, verbose: options.verbose })
} catch (e) { }
catch (e) {
if (!(options.silent ?? false)) { if (!(options.silent ?? false)) {
throw e throw e
} }
} }
// Override the merge order if --no-override flag set // Override the merge order if --no-override flag set
if (options.noOverride === true) { if (options.noOverride === true) {
env = Object.assign({}, env, process.env) env = Object.assign({}, env, processLib.env)
} else { }
else {
// Add in the system environment variables to our environment list // Add in the system environment variables to our environment list
env = Object.assign({}, process.env, env) env = Object.assign({}, processLib.env, env)
} }
if (options.expandEnvs === true) { if (options.expandEnvs === true) {
@ -66,7 +49,7 @@ export async function EnvCmd (
const proc = spawn(command, commandArgs, { const proc = spawn(command, commandArgs, {
stdio: 'inherit', stdio: 'inherit',
shell: options.useShell, shell: options.useShell,
env env: env as Record<string, string>,
}) })
// Handle any termination signals for parent and child proceses // Handle any termination signals for parent and child proceses

View File

@ -1,11 +1,13 @@
import type { Environment } from './types.ts'
/** /**
* expandEnvs Replaces $var in args and command with environment variables * 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 { export function expandEnvs(str: string, envs: Environment): string {
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => { return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, (varName) => {
const varValue = envs[varName.slice(1)] const varValue = envs[varName.slice(1)]
return varValue === undefined ? varName : varValue // const test = 42;
return varValue === undefined ? varName : varValue.toString()
}) })
} }

View File

@ -1,30 +1,30 @@
import { GetEnvVarOptions } from './types' import type { GetEnvVarOptions, Environment } from './types.ts'
import { getRCFileVars } from './parse-rc-file' import { getRCFileVars } from './parse-rc-file.js'
import { getEnvFileVars } from './parse-env-file' import { getEnvFileVars } from './parse-env-file.js'
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'] const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json']
const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'] const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json']
export async function getEnvVars (options: GetEnvVarOptions = {}): Promise<{ [key: string]: any }> { export async function getEnvVars(options: GetEnvVarOptions = {}): Promise<Environment> {
options.envFile = options.envFile !== undefined ? options.envFile : {} options.envFile = options.envFile ?? {}
// Check for rc file usage // Check for rc file usage
if (options.rc !== undefined) { if (options.rc !== undefined) {
return await getRCFile({ return await getRCFile({
environments: options.rc.environments, environments: options.rc.environments,
filePath: options.rc.filePath, filePath: options.rc.filePath,
verbose: options.verbose verbose: options.verbose,
}) })
} }
return await getEnvFile({ return await getEnvFile({
filePath: options.envFile.filePath, filePath: options.envFile.filePath,
fallback: options.envFile.fallback, fallback: options.envFile.fallback,
verbose: options.verbose verbose: options.verbose,
}) })
} }
export async function getEnvFile ( export async function getEnvFile(
{ filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean } { filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean },
): Promise<{ [key: string]: any }> { ): Promise<Environment> {
// Use env file // Use env file
if (filePath !== undefined) { if (filePath !== undefined) {
try { try {
@ -33,10 +33,12 @@ export async function getEnvFile (
console.info(`Found .env file at path: ${filePath}`) console.info(`Found .env file at path: ${filePath}`)
} }
return env return env
} catch (e) { }
catch {
if (verbose === true) { if (verbose === true) {
console.info(`Failed to find .env file at path: ${filePath}`) console.info(`Failed to find .env file at path: ${filePath}`)
} }
// Ignore error as we are just trying this location
} }
if (fallback !== true) { if (fallback !== true) {
throw new Error(`Failed to find .env file at path: ${filePath}`) 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}`) console.info(`Found .env file at default path: ${path}`)
} }
return env 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(',')}]` 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) throw new Error(error)
} }
export async function getRCFile ( export async function getRCFile(
{ environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean } { environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean },
): Promise<{ [key: string]: any }> { ): Promise<Environment> {
// User provided an .rc file path // User provided an .rc file path
if (filePath !== undefined) { if (filePath !== undefined) {
try { try {
@ -72,15 +77,18 @@ export async function getRCFile (
console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`) console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`)
} }
return env return env
} catch (e) { }
if (e.name === 'PathError') { catch (e) {
if (verbose === true) { if (e instanceof Error) {
console.info(`Failed to find .rc file at path: ${filePath}`) if (e.name === 'PathError') {
if (verbose === true) {
console.info(`Failed to find .rc file at path: ${filePath}`)
}
} }
} if (e.name === 'EnvironmentError') {
if (e.name === 'EnvironmentError') { if (verbose === true) {
if (verbose === true) { console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`)
console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`) }
} }
} }
throw e throw e
@ -95,13 +103,22 @@ export async function getRCFile (
console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`) console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`)
} }
return env return env
} catch (e) { }
if (e.name === 'EnvironmentError') { catch (e) {
const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}` if (e instanceof Error) {
if (verbose === true) { if (e.name === 'EnvironmentError') {
console.info(errorText) const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`
if (verbose === true) {
console.info(errorText)
}
throw new Error(errorText)
}
if (e.name === 'ParseError') {
if (verbose === true) {
console.info(e.message)
}
throw new Error(e.message)
} }
throw new Error(errorText)
} }
} }
} }

View File

@ -1,6 +1,7 @@
import { getEnvVars } from './get-env-vars' import { getEnvVars } from './get-env-vars.js'
// Export the core env-cmd API // Export the core env-cmd API
export * from './types' export * from './types.js'
export * from './env-cmd' export * from './cli.js'
export * from './env-cmd.js'
export const GetEnvVars = getEnvVars export const GetEnvVars = getEnvVars

View File

@ -1,14 +1,12 @@
import * as commander from 'commander' import * as commander from 'commander'
import { EnvCmdOptions } from './types' import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts'
import { parseArgList } from './utils' import { parseArgList } from './utils.js'
import { default as packageJson } from '../package.json' with { type: 'json' };
// Use commonjs require to prevent a weird folder hierarchy in dist
const packageJson = require('../package.json') /* eslint-disable-line */
/** /**
* Parses the arguments passed into the cli * 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 // Run the initial arguments through commander in order to determine
// which value in the args array is the `command` to execute // which value in the args array is the `command` to execute
let program = parseArgsUsingCommander(args) let program = parseArgsUsingCommander(args)
@ -42,23 +40,27 @@ export function parseArgs (args: string[]): EnvCmdOptions {
silent = true silent = true
} }
let rc: any let rc: RCFileOptions | undefined
if (program.environments !== undefined && program.environments.length !== 0) { if (
program.environments !== undefined
&& Array.isArray(program.environments)
&& program.environments.length !== 0
) {
rc = { rc = {
environments: program.environments, environments: program.environments,
filePath: program.rcFile filePath: program.rcFile,
} }
} }
let envFile: any let envFile: EnvFileOptions | undefined
if (program.file !== undefined) { if (program.file !== undefined) {
envFile = { envFile = {
filePath: program.file, filePath: program.file,
fallback: program.fallback fallback: program.fallback,
} }
} }
const options = { const options: EnvCmdOptions = {
command, command,
commandArgs, commandArgs,
envFile, envFile,
@ -68,8 +70,8 @@ export function parseArgs (args: string[]): EnvCmdOptions {
noOverride, noOverride,
silent, silent,
useShell, useShell,
verbose verbose,
} },
} }
if (verbose) { if (verbose) {
console.info(`Options: ${JSON.stringify(options, null, 0)}`) console.info(`Options: ${JSON.stringify(options, null, 0)}`)
@ -77,8 +79,8 @@ export function parseArgs (args: string[]): EnvCmdOptions {
return options return options
} }
export function parseArgsUsingCommander (args: string[]): commander.Command { export function parseArgsUsingCommander(args: string[]): CommanderOptions {
const program = new commander.Command() const program = new commander.Command() as CommanderOptions
return program return program
.version(packageJson.version, '-v, --version') .version(packageJson.version, '-v, --version')
.usage('[options] <command> [...args]') .usage('[options] <command> [...args]')

View File

@ -1,28 +1,42 @@
import * as fs from 'fs' import { existsSync, readFileSync } from 'node:fs'
import * as path from 'path' import { extname } from 'node:path'
import { resolveEnvFilePath, isPromise } from './utils' import { pathToFileURL } from 'node:url'
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs'] import type { Environment } from './types.ts'
/** /**
* Gets the environment vars from an env file * Gets the environment vars from an env file
*/ */
export async function getEnvFileVars (envFilePath: string): Promise<{ [key: string]: any }> { export async function getEnvFileVars(envFilePath: string): Promise<Environment> {
const absolutePath = resolveEnvFilePath(envFilePath) const absolutePath = resolveEnvFilePath(envFilePath)
if (!fs.existsSync(absolutePath)) { if (!existsSync(absolutePath)) {
const pathError = new Error(`Invalid env file path (${envFilePath}).`) const pathError = new Error(`Invalid env file path (${envFilePath}).`)
pathError.name = 'PathError' pathError.name = 'PathError'
throw pathError throw pathError
} }
// Get the file extension // Get the file extension
const ext = path.extname(absolutePath).toLowerCase() const ext = extname(absolutePath).toLowerCase()
let env = {} let env: Environment = {}
if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise = require(absolutePath) /* eslint-disable-line */ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise let attributeTypes = {}
} else { if (ext === '.json') {
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) attributeTypes = { with: { type: 'json' } }
}
const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as Environment | { default: Environment }
if ('default' in res) {
env = res.default as Environment
} else {
env = res
}
// Check to see if the imported value is a promise
if (isPromise(env)) {
env = await env
}
}
else {
const file = readFileSync(absolutePath, { encoding: 'utf8' })
env = parseEnvString(file) env = parseEnvString(file)
} }
return env return env
@ -31,7 +45,7 @@ export async function getEnvFileVars (envFilePath: string): Promise<{ [key: stri
/** /**
* Parse out all env vars from a given env file string and return an object * 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 // First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString()) envFileString = stripComments(envFileString.toString())
@ -45,27 +59,41 @@ export function parseEnvString (envFileString: string): { [key: string]: string
/** /**
* Parse out all env vars from an env file string * 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 envParseRegex = /^((.+?)[=](.*))$/gim
const matches: { [key: string]: string } = {} const matches: Environment = {}
let match let match
while ((match = envParseRegex.exec(envString)) !== null) { while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line // Note: match[1] is the full env=var line
const key = match[2].trim() const key = match[2].trim()
const value = match[3].trim() let value: string | number | boolean = match[3].trim()
// remove any surrounding quotes // remove any surrounding quotes
matches[key] = value value = value
.replace(/(^['"]|['"]$)/g, '') .replace(/(^['"]|['"]$)/g, '')
.replace(/\\n/g, '\n') .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 * Strips out comments from env file string
*/ */
export function stripComments (envString: string): string { export function stripComments(envString: string): string {
const commentsRegex = /(^#.*$)/gim const commentsRegex = /(^#.*$)/gim
let match = commentsRegex.exec(envString) let match = commentsRegex.exec(envString)
let newString = envString let newString = envString
@ -79,7 +107,7 @@ export function stripComments (envString: string): string {
/** /**
* Strips out newlines from env file string * Strips out newlines from env file string
*/ */
export function stripEmptyLines (envString: string): string { export function stripEmptyLines(envString: string): string {
const emptyLinesRegex = /(^\n)/gim const emptyLinesRegex = /(^\n)/gim
return envString.replace(emptyLinesRegex, '') return envString.replace(emptyLinesRegex, '')
} }

View File

@ -1,7 +1,9 @@
import { stat, readFile } from 'fs' import { stat, readFile } from 'node:fs'
import { promisify } from 'util' import { promisify } from 'node:util'
import { extname } from 'path' import { extname } from 'node:path'
import { resolveEnvFilePath, isPromise } from './utils' import { pathToFileURL } from 'node:url'
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import type { Environment, RCEnvironment } from './types.ts'
const statAsync = promisify(stat) const statAsync = promisify(stat)
const readFileAsync = promisify(readFile) const readFileAsync = promisify(readFile)
@ -9,14 +11,15 @@ const readFileAsync = promisify(readFile)
/** /**
* Gets the env vars from the rc file and rc environments * Gets the env vars from the rc file and rc environments
*/ */
export async function getRCFileVars ( export async function getRCFileVars(
{ environments, filePath }: { environments, filePath }:
{ environments: string[], filePath: string } { environments: string[], filePath: string },
): Promise<{ [key: string]: any }> { ): Promise<Environment> {
const absolutePath = resolveEnvFilePath(filePath) const absolutePath = resolveEnvFilePath(filePath)
try { try {
await statAsync(absolutePath) await statAsync(absolutePath)
} catch (e) { }
catch {
const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`) const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`)
pathError.name = 'PathError' pathError.name = 'PathError'
throw pathError throw pathError
@ -24,38 +27,58 @@ export async function getRCFileVars (
// Get the file extension // Get the file extension
const ext = extname(absolutePath).toLowerCase() const ext = extname(absolutePath).toLowerCase()
let parsedData: { [key: string]: any } let parsedData: Partial<RCEnvironment> = {}
try { try {
if (ext === '.json' || ext === '.js' || ext === '.cjs') { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise = require(absolutePath) /* eslint-disable-line */ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise let attributeTypes = {}
} else { if (ext === '.json') {
const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) attributeTypes = { with: { type: 'json' } }
parsedData = JSON.parse(file) }
const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as RCEnvironment | { default: RCEnvironment }
if ('default' in res) {
parsedData = res.default as RCEnvironment
} else {
parsedData = res
}
// Check to see if the imported value is a promise
if (isPromise(parsedData)) {
parsedData = await parsedData
}
} }
} catch (e) { else {
const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`) const file = await readFileAsync(absolutePath, { encoding: 'utf8' })
parsedData = JSON.parse(file) as Partial<RCEnvironment>
}
}
catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error'
const parseError = new Error(
`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`,
)
parseError.name = 'ParseError' parseError.name = 'ParseError'
throw parseError throw parseError
} }
// Parse and merge multiple rc environments together // Parse and merge multiple rc environments together
let result = {} let result: Environment = {}
let environmentFound = false let environmentFound = false
environments.forEach((name): void => { for (const name of environments) {
const envVars = parsedData[name] if (name in parsedData) {
if (envVars !== undefined) { const envVars = parsedData[name]
environmentFound = true if (envVars != null && typeof envVars === 'object') {
result = { environmentFound = true
...result, result = {
...envVars ...result,
...envVars,
}
} }
} }
}) }
if (!environmentFound) { if (!environmentFound) {
const environmentError = new Error( 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' environmentError.name = 'EnvironmentError'
throw environmentError throw environmentError

View File

@ -1,52 +1,56 @@
import { ChildProcess } from 'child_process' import { ChildProcess } from 'child_process'
const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [ const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [
'SIGINT', 'SIGTERM', 'SIGHUP' 'SIGINT', 'SIGTERM', 'SIGHUP',
] ]
export class TermSignals { export class TermSignals {
private readonly terminateSpawnedProcessFuncHandlers: { [key: string]: any } = {} private readonly terminateSpawnedProcessFuncHandlers: Record<string, NodeJS.SignalsListener> = {}
private terminateSpawnedProcessFuncExitHandler?: NodeJS.ExitListener
private readonly verbose: boolean = false private readonly verbose: boolean = false
public _exitCalled = false public _exitCalled = false
constructor (options: { verbose?: boolean } = {}) { constructor(options: { verbose?: boolean } = {}) {
this.verbose = options.verbose === true this.verbose = options.verbose === true
} }
public handleTermSignals (proc: ChildProcess): void { public handleTermSignals(proc: ChildProcess): void {
// Terminate child process if parent process receives termination events // Terminate child process if parent process receives termination events
SIGNALS_TO_HANDLE.forEach((signal): void => { const terminationFunc = (signal: NodeJS.Signals | number): void => {
this.terminateSpawnedProcessFuncHandlers[signal] = this._removeProcessListeners()
(signal: NodeJS.Signals | number, code: number): void => { if (!this._exitCalled) {
this._removeProcessListeners() if (this.verbose) {
if (!this._exitCalled) { console.info(
if (this.verbose) { 'Parent process exited with signal: '
console.info( + signal.toString()
'Parent process exited with signal: ' + + '. Terminating child process...')
signal.toString() + }
'. Terminating child process...') // Mark shared state so we do not run into a signal/exit loop
} this._exitCalled = true
// Mark shared state so we do not run into a signal/exit loop // Use the signal code if it is an error code
this._exitCalled = true // let correctSignal: NodeJS.Signals | undefined
// Use the signal code if it is an error code if (typeof signal === 'number') {
let correctSignal: NodeJS.Signals | undefined if (signal > 0) {
if (typeof signal === 'number') { // code = signal
if (signal > (code ?? 0)) { signal = 'SIGINT'
code = signal
correctSignal = 'SIGINT'
}
} else {
correctSignal = signal
}
// Kill the child process
proc.kill(correctSignal ?? code)
// Terminate the parent process
this._terminateProcess(code, correctSignal)
} }
} }
// 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(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 // Terminate parent process if child process receives termination events
proc.on('exit', (code: number | undefined, signal: NodeJS.Signals | number | null): void => { proc.on('exit', (code: number | undefined, signal: NodeJS.Signals | number | null): void => {
@ -54,9 +58,9 @@ export class TermSignals {
if (!this._exitCalled) { if (!this._exitCalled) {
if (this.verbose) { if (this.verbose) {
console.info( console.info(
`Child process exited with code: ${(code ?? '').toString()} and signal:` + `Child process exited with code: ${(code ?? '').toString()} and signal:`
(signal ?? '').toString() + + (signal ?? '').toString()
'. Terminating parent process...' + '. Terminating parent process...',
) )
} }
// Mark shared state so we do not run into a signal/exit loop // Mark shared state so we do not run into a signal/exit loop
@ -68,11 +72,12 @@ export class TermSignals {
code = signal code = signal
correctSignal = 'SIGINT' correctSignal = 'SIGINT'
} }
} else { }
else {
correctSignal = signal ?? undefined correctSignal = signal ?? undefined
} }
// Terminate the parent process // Terminate the parent process
this._terminateProcess(code, correctSignal) this._terminateProcess(correctSignal ?? code)
} }
}) })
} }
@ -80,19 +85,25 @@ export class TermSignals {
/** /**
* Enables catching of unhandled exceptions * Enables catching of unhandled exceptions
*/ */
public handleUncaughtExceptions (): void { public handleUncaughtExceptions(): void {
process.on('uncaughtException', (e): void => this._uncaughtExceptionHandler(e)) process.on('uncaughtException', (e): void => {
this._uncaughtExceptionHandler(e)
})
} }
/** /**
* Terminate parent process helper * Terminate parent process helper
*/ */
public _terminateProcess (code?: number, signal?: NodeJS.Signals): void { public _terminateProcess(signal?: NodeJS.Signals | number): void {
if (signal !== undefined) { if (signal != null) {
return process.kill(process.pid, signal) if (typeof signal === 'string') {
} process.kill(process.pid, signal)
if (code !== undefined) { return
return process.exit(code) }
if (typeof signal === 'number') {
process.exit(signal)
return
}
} }
throw new Error('Unable to terminate parent process successfully') throw new Error('Unable to terminate parent process successfully')
} }
@ -100,17 +111,19 @@ export class TermSignals {
/** /**
* Exit event listener clean up helper * Exit event listener clean up helper
*/ */
public _removeProcessListeners (): void { public _removeProcessListeners(): void {
SIGNALS_TO_HANDLE.forEach((signal): void => { SIGNALS_TO_HANDLE.forEach((signal): void => {
process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]) process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal])
}) })
process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM) if (this.terminateSpawnedProcessFuncExitHandler != null) {
process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler)
}
} }
/** /**
* General exception handler * General exception handler
*/ */
public _uncaughtExceptionHandler (e: Error): void { public _uncaughtExceptionHandler(e: Error): void {
console.error(e.message) console.error(e.message)
process.exit(1) process.exit(1)
} }

View File

@ -1,4 +0,0 @@
import * as spawn from 'cross-spawn'
export {
spawn
}

View File

@ -1,16 +1,39 @@
import { Command } from 'commander'
// Define an export type
export type Environment = Partial<Record<string, string | number | boolean>>
export type RCEnvironment = Partial<Record<string, Environment>>
export interface CommanderOptions extends Command {
override?: boolean // Default: false
useShell?: boolean // Default: false
expandEnvs?: boolean // Default: false
verbose?: boolean // Default: false
silent?: boolean // Default: false
fallback?: boolean // Default false
environments?: string[]
rcFile?: string
file?: string
}
export interface RCFileOptions {
environments: string[]
filePath?: string
}
export interface EnvFileOptions {
filePath?: string
fallback?: boolean
}
export interface GetEnvVarOptions { export interface GetEnvVarOptions {
envFile?: { envFile?: EnvFileOptions
filePath?: string rc?: RCFileOptions
fallback?: boolean
}
rc?: {
environments: string[]
filePath?: string
}
verbose?: boolean verbose?: boolean
} }
export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'> { export interface EnvCmdOptions extends GetEnvVarOptions {
command: string command: string
commandArgs: string[] commandArgs: string[]
options?: { options?: {

View File

@ -1,27 +1,34 @@
import * as path from 'path' import { resolve } from 'node:path'
import * as os from 'os' import { homedir } from 'node:os'
import { cwd } from 'node:process'
// Special file extensions that node can natively import
export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']
/** /**
* A simple function for resolving the path the user entered * 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 // Make sure a home directory exist
const home = os.homedir() const home = homedir() as string | undefined
if (home !== undefined) { if (home != null) {
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`) userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`)
} }
return path.resolve(process.cwd(), userPath) return resolve(cwd(), userPath)
} }
/** /**
* A simple function that parses a comma separated string into an array of strings * 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(',') return list.split(',')
} }
/** /**
* A simple function to test if the value is a promise * A simple function to test if the value is a promise/thenable
*/ */
export function isPromise (value: any | PromiseLike<Object>): value is Promise<any> { export function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T> {
return value != null && typeof value.then === 'function' return value != null
&& typeof value === 'object'
&& 'then' in value
&& typeof value.then === 'function'
} }

57
test/cli.spec.ts Normal file
View File

@ -0,0 +1,57 @@
import { default as sinon } from 'sinon'
import { assert } from 'chai'
import { default as esmock } from 'esmock'
import type { CLI } from '../src/cli.ts'
describe('CLI', (): void => {
let sandbox: sinon.SinonSandbox
let parseArgsStub: sinon.SinonStub<any>
let envCmdStub: sinon.SinonStub<any>
let processExitStub: sinon.SinonStub<any>
let cliLib: { CLI: typeof CLI }
before(async (): Promise<void> => {
sandbox = sinon.createSandbox()
envCmdStub = sandbox.stub()
parseArgsStub = sandbox.stub()
processExitStub = sandbox.stub()
cliLib = await esmock('../src/cli.ts', {
'../src/env-cmd': {
EnvCmd: envCmdStub,
},
'../src/parse-args': {
parseArgs: parseArgsStub,
},
'node:process': {
exit: processExitStub,
},
})
})
after((): void => {
sandbox.restore()
})
afterEach((): void => {
sandbox.resetHistory()
sandbox.resetBehavior()
})
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
parseArgsStub.returns({})
await cliLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 0)
})
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
parseArgsStub.returns({})
envCmdStub.throwsException('Error')
await cliLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 1)
assert.equal(processExitStub.args[0][0], 1)
})
})

View File

@ -1,68 +1,44 @@
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import * as signalTermLib from '../src/signal-termination' import { default as esmock } from 'esmock'
import * as parseArgsLib from '../src/parse-args' import { expandEnvs } from '../src/expand-envs.js'
import * as getEnvVarsLib from '../src/get-env-vars' import type { EnvCmd } from '../src/env-cmd.ts'
import * as expandEnvsLib from '../src/expand-envs'
import * as spawnLib from '../src/spawn'
import * as envCmdLib from '../src/env-cmd'
describe('CLI', (): void => { let envCmdLib: { EnvCmd: typeof EnvCmd }
let sandbox: sinon.SinonSandbox
let parseArgsStub: sinon.SinonStub<any, any>
let envCmdStub: sinon.SinonStub<any, any>
let processExitStub: sinon.SinonStub<any, any>
before((): void => {
sandbox = sinon.createSandbox()
parseArgsStub = sandbox.stub(parseArgsLib, 'parseArgs')
envCmdStub = sandbox.stub(envCmdLib, 'EnvCmd')
processExitStub = sandbox.stub(process, 'exit')
})
after((): void => {
sandbox.restore()
})
afterEach((): void => {
sandbox.resetHistory()
sandbox.resetBehavior()
})
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
parseArgsStub.returns({})
await envCmdLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 0)
})
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
parseArgsStub.returns({})
envCmdStub.throwsException('Error')
await envCmdLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 1)
assert.equal(processExitStub.args[0][0], 1)
})
})
describe('EnvCmd', (): void => { describe('EnvCmd', (): void => {
let sandbox: sinon.SinonSandbox let sandbox: sinon.SinonSandbox
let getEnvVarsStub: sinon.SinonStub<any, any> let getEnvVarsStub: sinon.SinonStub<any>
let spawnStub: sinon.SinonStub<any, any> let spawnStub: sinon.SinonStub<any>
let expandEnvsSpy: sinon.SinonSpy<any, any> let expandEnvsSpy: sinon.SinonSpy<any>
before((): void => { before(async (): Promise<void> => {
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars') getEnvVarsStub = sandbox.stub()
spawnStub = sandbox.stub(spawnLib, 'spawn') spawnStub = sandbox.stub()
spawnStub.returns({ spawnStub.returns({
on: (): void => { /* Fake the on method */ }, on: sinon.stub(),
kill: (): void => { /* Fake the kill method */ } kill: sinon.stub(),
})
expandEnvsSpy = sandbox.spy(expandEnvs)
const TermSignals = sandbox.stub()
TermSignals.prototype.handleTermSignals = sandbox.stub()
TermSignals.prototype.handleUncaughtExceptions = sandbox.stub()
envCmdLib = await esmock('../src/env-cmd.ts', {
'../src/get-env-vars': {
getEnvVars: getEnvVarsStub,
},
'cross-spawn': {
default: spawnStub,
},
'../src/expand-envs': {
expandEnvs: expandEnvsSpy,
},
'../src/signal-termination': {
TermSignals,
},
}) })
expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleUncaughtExceptions')
}) })
after((): void => { after((): void => {
@ -80,12 +56,12 @@ describe('EnvCmd', (): void => {
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env', filePath: './.env',
fallback: true fallback: true,
}, },
rc: { rc: {
environments: ['dev'], environments: ['dev'],
filePath: './.rc' filePath: './.rc',
} },
}) })
assert.equal(getEnvVarsStub.callCount, 1) assert.equal(getEnvVarsStub.callCount, 1)
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
@ -100,17 +76,17 @@ describe('EnvCmd', (): void => {
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env', filePath: './.env',
fallback: true fallback: true,
}, },
rc: { rc: {
environments: ['dev'], environments: ['dev'],
filePath: './.rc' filePath: './.rc',
} },
}) })
assert.equal(getEnvVarsStub.callCount, 1) assert.equal(getEnvVarsStub.callCount, 1)
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
assert.equal(spawnStub.args[0][2].env.BOB, 'test') assert.equal(spawnStub.args[0][2].env.BOB, 'test')
} },
) )
it('should not override existing env vars if noOverride option is true', it('should not override existing env vars if noOverride option is true',
@ -122,20 +98,20 @@ describe('EnvCmd', (): void => {
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env', filePath: './.env',
fallback: true fallback: true,
}, },
rc: { rc: {
environments: ['dev'], environments: ['dev'],
filePath: './.rc' filePath: './.rc',
}, },
options: { options: {
noOverride: true noOverride: true,
} },
}) })
assert.equal(getEnvVarsStub.callCount, 1) assert.equal(getEnvVarsStub.callCount, 1)
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
assert.equal(spawnStub.args[0][2].env.BOB, 'cool') assert.equal(spawnStub.args[0][2].env.BOB, 'cool')
} },
) )
it('should spawn process with shell option if useShell option is true', it('should spawn process with shell option if useShell option is true',
@ -147,20 +123,20 @@ describe('EnvCmd', (): void => {
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env', filePath: './.env',
fallback: true fallback: true,
}, },
rc: { rc: {
environments: ['dev'], environments: ['dev'],
filePath: './.rc' filePath: './.rc',
}, },
options: { options: {
useShell: true useShell: true,
} },
}) })
assert.equal(getEnvVarsStub.callCount, 1) assert.equal(getEnvVarsStub.callCount, 1)
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
assert.equal(spawnStub.args[0][2].shell, true) assert.equal(spawnStub.args[0][2].shell, true)
} },
) )
it('should spawn process with command and args expanded if expandEnvs option is true', it('should spawn process with command and args expanded if expandEnvs option is true',
@ -171,15 +147,15 @@ describe('EnvCmd', (): void => {
commandArgs: ['$PING', '\\$IP'], commandArgs: ['$PING', '\\$IP'],
envFile: { envFile: {
filePath: './.env', filePath: './.env',
fallback: true fallback: true,
}, },
rc: { rc: {
environments: ['dev'], environments: ['dev'],
filePath: './.rc' filePath: './.rc',
}, },
options: { options: {
expandEnvs: true expandEnvs: true,
} },
}) })
const spawnArgs = spawnStub.args[0] const spawnArgs = spawnStub.args[0]
@ -188,9 +164,9 @@ describe('EnvCmd', (): void => {
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
assert.equal(expandEnvsSpy.callCount, 3, 'command + number of args') assert.equal(expandEnvsSpy.callCount, 3, 'command + number of args')
assert.equal(spawnArgs[0], 'node') 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') assert.equal(spawnArgs[2].env.PING, 'PONG')
} },
) )
it('should ignore errors if silent flag provided', it('should ignore errors if silent flag provided',
@ -201,16 +177,16 @@ describe('EnvCmd', (): void => {
command: 'node', command: 'node',
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env' filePath: './.env',
}, },
options: { options: {
silent: true silent: true,
} },
}) })
assert.equal(getEnvVarsStub.callCount, 1) assert.equal(getEnvVarsStub.callCount, 1)
assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.callCount, 1)
assert.isUndefined(spawnStub.args[0][2].env.BOB) assert.isUndefined(spawnStub.args[0][2].env.BOB)
} },
) )
it('should allow errors if silent flag not provided', it('should allow errors if silent flag not provided',
@ -221,14 +197,16 @@ describe('EnvCmd', (): void => {
command: 'node', command: 'node',
commandArgs: ['-v'], commandArgs: ['-v'],
envFile: { envFile: {
filePath: './.env' filePath: './.env',
} },
}) })
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.equal(e.name, 'MissingFile') assert.equal(e.name, 'MissingFile')
return return
} }
assert.fail('Should not get here.') assert.fail('Should not get here.')
} },
) )
}) })

View File

@ -1,13 +1,15 @@
/* eslint @typescript-eslint/no-non-null-assertion: 0 */ /* eslint @typescript-eslint/no-non-null-assertion: 0 */
import { assert } from 'chai' import { assert } from 'chai'
import { expandEnvs } from '../src/expand-envs' import { expandEnvs } from '../src/expand-envs.js'
describe('expandEnvs', (): void => { describe('expandEnvs', (): void => {
const envs = { const envs = {
notvar: 'this is not used', notvar: 'this is not used',
dollar: 'money', dollar: 'money',
PING: 'PONG', 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 args = ['notvar', '$dollar', '\\$notvar', '-4', '$PING', '$IP1', '\\$IP1', '$NONEXIST']
const argsExpanded = ['notvar', 'money', '\\$notvar', '-4', 'PONG', '127.0.0.1', '\\$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 => { it('should replace environment variables in args', (): void => {
const res = args.map(arg => expandEnvs(arg, envs)) const res = args.map(arg => expandEnvs(arg, envs))
assert.sameOrderedMembers(res, argsExpanded) assert.sameOrderedMembers(res, argsExpanded)
for (const arg of args) {
assert.typeOf(arg, 'string')
}
}) })
}) })

View File

@ -1,17 +1,26 @@
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import { getEnvVars } from '../src/get-env-vars' import { default as esmock } from 'esmock'
import * as rcFile from '../src/parse-rc-file' import type { getEnvVars } from '../src/get-env-vars.ts'
import * as envFile from '../src/parse-env-file'
let getEnvVarsLib: { getEnvVars: typeof getEnvVars }
describe('getEnvVars', (): void => { describe('getEnvVars', (): void => {
let getRCFileVarsStub: sinon.SinonStub<any, any> let getRCFileVarsStub: sinon.SinonStub<any>
let getEnvFileVarsStub: sinon.SinonStub<any, any> let getEnvFileVarsStub: sinon.SinonStub<any>
let logInfoStub: sinon.SinonStub<any, any> let logInfoStub: sinon.SinonStub<any> | undefined
before((): void => { before(async (): Promise<void> => {
getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') getRCFileVarsStub = sinon.stub()
getEnvFileVarsStub = sinon.stub(envFile, 'getEnvFileVars') getEnvFileVarsStub = sinon.stub()
getEnvVarsLib = await esmock('../src/get-env-vars.ts', {
'../src/parse-rc-file': {
getRCFileVars: getRCFileVarsStub
},
'../src/parse-env-file': {
getEnvFileVars: getEnvFileVarsStub
}
})
}) })
after((): void => { after((): void => {
@ -21,15 +30,13 @@ describe('getEnvVars', (): void => {
afterEach((): void => { afterEach((): void => {
sinon.resetHistory() sinon.resetHistory()
sinon.resetBehavior() sinon.resetBehavior()
if (logInfoStub !== undefined) { logInfoStub?.restore()
logInfoStub.restore()
}
}) })
it('should parse the json .rc file from the default path with the given environment', it('should parse the json .rc file from the default path with the given environment',
async (): Promise<void> => { async (): Promise<void> => {
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ rc: { environments: ['production'] } }) const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -37,16 +44,16 @@ describe('getEnvVars', (): void => {
assert.lengthOf(getRCFileVarsStub.args[0][0].environments, 1) assert.lengthOf(getRCFileVarsStub.args[0][0].environments, 1)
assert.equal(getRCFileVarsStub.args[0][0].environments[0], 'production') assert.equal(getRCFileVarsStub.args[0][0].environments[0], 'production')
assert.equal(getRCFileVarsStub.args[0][0].filePath, './.env-cmdrc') assert.equal(getRCFileVarsStub.args[0][0].filePath, './.env-cmdrc')
} },
) )
it('should print path of custom .rc file and environments to info for verbose', it('should print path of custom .rc file and environments to info for verbose',
async (): Promise<void> => { async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} },
) )
it('should search all default .rc file paths', async (): Promise<void> => { it('should search all default .rc file paths', async (): Promise<void> => {
@ -54,7 +61,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ rc: { environments: ['production'] } }) const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -69,9 +76,11 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ rc: { environments: ['production'] } }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.fail('should not get here.') 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, /failed to find/gi)
assert.match(e.message, /\.rc file/gi) assert.match(e.message, /\.rc file/gi)
assert.match(e.message, /default paths/gi) assert.match(e.message, /default paths/gi)
@ -84,9 +93,10 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
}) })
@ -96,9 +106,11 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ rc: { environments: ['bad'] } }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] } })
assert.fail('should not get here.') 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, /failed to find environments/gi)
assert.match(e.message, /\.rc file at path/gi) assert.match(e.message, /\.rc file at path/gi)
} }
@ -110,17 +122,18 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
}) })
it('should find .rc file at custom path path', async (): Promise<void> => { it('should find .rc file at custom path path', async (): Promise<void> => {
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ const envs = await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' } rc: { environments: ['production'], filePath: '../.custom-rc' },
}) })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
@ -134,9 +147,9 @@ describe('getEnvVars', (): void => {
it('should print custom .rc file path to info for verbose', async (): Promise<void> => { it('should print custom .rc file path to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
verbose: true verbose: true,
}) })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
@ -146,11 +159,13 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' } rc: { environments: ['production'], filePath: '../.custom-rc' },
}) })
assert.fail('should not get here.') 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, /failed to find/gi)
assert.match(e.message, /\.rc file at path/gi) assert.match(e.message, /\.rc file at path/gi)
} }
@ -162,12 +177,13 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
verbose: true verbose: true,
}) })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
}) })
@ -177,11 +193,13 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['bad'], filePath: '../.custom-rc' } rc: { environments: ['bad'], filePath: '../.custom-rc' },
}) })
assert.fail('should not get here.') 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, /failed to find environments/gi)
assert.match(e.message, /\.rc file at path/gi) assert.match(e.message, /\.rc file at path/gi)
} }
@ -194,20 +212,21 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['bad'], filePath: '../.custom-rc' }, rc: { environments: ['bad'], filePath: '../.custom-rc' },
verbose: true verbose: true,
}) })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
} },
) )
it('should parse the env file from a custom path', async (): Promise<void> => { it('should parse the env file from a custom path', async (): Promise<void> => {
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ envFile: { filePath: '../.env-file' } }) const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -218,16 +237,18 @@ describe('getEnvVars', (): void => {
it('should print path of .env file to info for verbose', async (): Promise<void> => { it('should print path of .env file to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should fail to find env file at custom path', async (): Promise<void> => { it('should fail to find env file at custom path', async (): Promise<void> => {
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ envFile: { filePath: '../.env-file' } }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
assert.fail('should not get here.') 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, /failed to find/gi)
assert.match(e.message, /\.env file at path/gi) assert.match(e.message, /\.env file at path/gi)
} }
@ -237,43 +258,44 @@ describe('getEnvVars', (): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
}) })
it( it(
'should parse the env file from the default path if custom ' + 'should parse the env file from the default path if custom '
'path not found and fallback option provided', + 'path not found and fallback option provided',
async (): Promise<void> => { async (): Promise<void> => {
getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.onFirstCall().rejects('File not found.')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } }) const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
assert.equal(getEnvFileVarsStub.callCount, 2) assert.equal(getEnvFileVarsStub.callCount, 2)
assert.equal(getEnvFileVarsStub.args[1][0], './.env') assert.equal(getEnvFileVarsStub.args[1][0], './.env')
} },
) )
it( it(
'should print multiple times for failure to find .env file and ' + 'should print multiple times for failure to find .env file and '
'failure to find fallback file to infor for verbose', + 'failure to find fallback file to infor for verbose',
async (): Promise<void> => { async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.onFirstCall().rejects('File not found.')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true })
assert.equal(logInfoStub.callCount, 2) assert.equal(logInfoStub.callCount, 2)
} },
) )
it('should parse the env file from the default path', async (): Promise<void> => { it('should parse the env file from the default path', async (): Promise<void> => {
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars() const envs = await getEnvVarsLib.getEnvVars()
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -284,14 +306,14 @@ describe('getEnvVars', (): void => {
it('should print path of .env file to info for verbose', async (): Promise<void> => { it('should print path of .env file to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ verbose: true }) await getEnvVarsLib.getEnvVars({ verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should search all default env file paths', async (): Promise<void> => { it('should search all default env file paths', async (): Promise<void> => {
getEnvFileVarsStub.throws('Not found.') getEnvFileVarsStub.throws('Not found.')
getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars() const envs = await getEnvVarsLib.getEnvVars()
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -302,9 +324,11 @@ describe('getEnvVars', (): void => {
it('should fail to find env file at default path', async (): Promise<void> => { it('should fail to find env file at default path', async (): Promise<void> => {
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars() await getEnvVarsLib.getEnvVars()
assert.fail('should not get here.') 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, /failed to find/gi)
assert.match(e.message, /\.env file/gi) assert.match(e.message, /\.env file/gi)
assert.match(e.message, /default paths/gi) assert.match(e.message, /default paths/gi)
@ -317,11 +341,12 @@ describe('getEnvVars', (): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ verbose: true }) await getEnvVarsLib.getEnvVars({ verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} catch (e) { }
catch {
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} }
} },
) )
}) })

View File

@ -1,7 +1,7 @@
/* eslint @typescript-eslint/no-non-null-assertion: 0 */ /* eslint @typescript-eslint/no-non-null-assertion: 0 */
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import { parseArgs } from '../src/parse-args' import { parseArgs } from '../src/parse-args.js'
describe('parseArgs', (): void => { describe('parseArgs', (): void => {
const command = 'command' const command = 'command'
@ -9,7 +9,7 @@ describe('parseArgs', (): void => {
const environments = ['development', 'production'] const environments = ['development', 'production']
const rcFilePath = './.env-cmdrc' const rcFilePath = './.env-cmdrc'
const envFilePath = './.env' const envFilePath = './.env'
let logInfoStub: sinon.SinonStub<any, any> let logInfoStub: sinon.SinonStub<any>
before((): void => { before((): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
@ -27,13 +27,13 @@ describe('parseArgs', (): void => {
it('should parse environment value', (): void => { it('should parse environment value', (): void => {
const res = parseArgs(['-e', environments[0], command]) const res = parseArgs(['-e', environments[0], command])
assert.exists(res.rc) assert.exists(res.rc)
assert.sameOrderedMembers(res.rc!.environments, [environments[0]]) assert.sameOrderedMembers(res.rc.environments, [environments[0]])
}) })
it('should parse multiple environment values', (): void => { it('should parse multiple environment values', (): void => {
const res = parseArgs(['-e', environments.join(','), command]) const res = parseArgs(['-e', environments.join(','), command])
assert.exists(res.rc) assert.exists(res.rc)
assert.sameOrderedMembers(res.rc!.environments, environments) assert.sameOrderedMembers(res.rc.environments, environments)
}) })
it('should parse command value', (): void => { it('should parse command value', (): void => {
@ -53,37 +53,37 @@ describe('parseArgs', (): void => {
assert.sameOrderedMembers(res.commandArgs, commandFlags) assert.sameOrderedMembers(res.commandArgs, commandFlags)
assert.notOk(res.options!.useShell) assert.notOk(res.options!.useShell)
assert.notOk(res.envFile) assert.notOk(res.envFile)
} },
) )
it('should parse override option', (): void => { it('should parse override option', (): void => {
const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs])
assert.exists(res.options) assert.exists(res.options)
assert.isTrue(res.options!.noOverride) assert.isTrue(res.options.noOverride)
}) })
it('should parse use shell option', (): void => { it('should parse use shell option', (): void => {
const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs])
assert.exists(res.options) assert.exists(res.options)
assert.isTrue(res.options!.useShell) assert.isTrue(res.options.useShell)
}) })
it('should parse rc file path', (): void => { it('should parse rc file path', (): void => {
const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs])
assert.exists(res.rc) assert.exists(res.rc)
assert.equal(res.rc!.filePath, rcFilePath) assert.equal(res.rc.filePath, rcFilePath)
}) })
it('should parse env file path', (): void => { it('should parse env file path', (): void => {
const res = parseArgs(['-f', envFilePath, command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, command, ...commandArgs])
assert.exists(res.envFile) assert.exists(res.envFile)
assert.equal(res.envFile!.filePath, envFilePath) assert.equal(res.envFile.filePath, envFilePath)
}) })
it('should parse fallback option', (): void => { it('should parse fallback option', (): void => {
const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs])
assert.exists(res.envFile) assert.exists(res.envFile)
assert.isTrue(res.envFile!.fallback) assert.isTrue(res.envFile.fallback)
}) })
it('should print to console.info if --verbose flag is passed', (): void => { it('should print to console.info if --verbose flag is passed', (): void => {

View File

@ -1,8 +1,8 @@
import { assert } from 'chai' import { assert } from 'chai'
import { import {
stripEmptyLines, stripComments, parseEnvVars, stripEmptyLines, stripComments, parseEnvVars,
parseEnvString, getEnvFileVars parseEnvString, getEnvFileVars,
} from '../src/parse-env-file' } from '../src/parse-env-file.js'
describe('stripEmptyLines', (): void => { describe('stripEmptyLines', (): void => {
it('should strip out all empty lines', (): void => { it('should strip out all empty lines', (): void => {
@ -20,10 +20,12 @@ describe('stripComments', (): void => {
describe('parseEnvVars', (): void => { describe('parseEnvVars', (): void => {
it('should parse out all env vars in string when not ending with \'\\n\'', (): 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.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev') assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING') 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 => { 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') const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
assert(env.BOB === 'COOL') assert(env.BOB === 'COOL')
assert(env.NODE_ENV === 'dev') assert(env.NODE_ENV === 'dev')
assert(env.ANSWER === '42') assert(env.ANSWER === 42)
}) })
}) })
@ -107,7 +109,8 @@ describe('getEnvFileVars', (): void => {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',
ANSWER: 42, ANSWER: 42,
ONLY: 'IN PRODUCTION', ONLY: 'IN PRODUCTION',
GALAXY: 'hitch\nhiking' GALAXY: 'hitch\nhiking',
BRINGATOWEL: true,
}) })
}) })
@ -117,24 +120,42 @@ describe('getEnvFileVars', (): void => {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',
ANSWER: 42, ANSWER: 42,
ONLY: 'IN\n PRODUCTION', ONLY: 'IN\n PRODUCTION',
GALAXY: 'hitch\nhiking\n\n' GALAXY: 'hitch\nhiking\n\n',
BRINGATOWEL: true,
}) })
}) })
it('should parse a js file', async (): Promise<void> => { it('should parse a js/cjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test.js') const env = await getEnvFileVars('./test/test-files/test.cjs')
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0, ANSWER: 0,
GALAXY: 'hitch\nhiking' GALAXY: 'hitch\nhiking',
}) })
}) })
it('should parse an async js file', async (): Promise<void> => { it('should parse an async js/cjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test-async.js') const env = await getEnvFileVars('./test/test-files/test-async.cjs')
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0 ANSWER: 0,
})
})
it('should parse a mjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test.mjs')
assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
GALAXY: 'hitch\nhiking',
})
})
it('should parse an async mjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test-async.mjs')
assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
}) })
}) })
@ -142,9 +163,10 @@ describe('getEnvFileVars', (): void => {
const env = await getEnvFileVars('./test/test-files/test') const env = await getEnvFileVars('./test/test-files/test')
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',
ANSWER: '42', ANSWER: 42,
ONLY: 'IN=PRODUCTION', ONLY: 'IN=PRODUCTION',
GALAXY: 'hitch\nhiking' GALAXY: 'hitch\nhiking',
BRINGATOWEL: true,
}) })
}) })
@ -152,7 +174,9 @@ describe('getEnvFileVars', (): void => {
try { try {
await getEnvFileVars('./test/test-files/non-existent-file') await getEnvFileVars('./test/test-files/non-existent-file')
assert.fail('Should not get here!') assert.fail('Should not get here!')
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.match(e.message, /file path/gi) assert.match(e.message, /file path/gi)
} }
}) })

View File

@ -1,5 +1,5 @@
import { assert } from 'chai' import { assert } from 'chai'
import { getRCFileVars } from '../src/parse-rc-file' import { getRCFileVars } from '../src/parse-rc-file.js'
const rcFilePath = './test/test-files/.rc-test' const rcFilePath = './test/test-files/.rc-test'
const rcJSONFilePath = './test/test-files/.rc-test.json' const rcJSONFilePath = './test/test-files/.rc-test.json'
@ -11,7 +11,8 @@ describe('getRCFileVars', (): void => {
assert.deepEqual(res, { assert.deepEqual(res, {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',
ANSWER: 42, ANSWER: 42,
ONLY: 'IN PRODUCTION' ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
}) })
}) })
@ -20,7 +21,7 @@ describe('getRCFileVars', (): void => {
assert.exists(res) assert.exists(res)
assert.deepEqual(res, { assert.deepEqual(res, {
THANKS: 'FOR MORE FISHIES', THANKS: 'FOR MORE FISHIES',
ANSWER: 21 ANSWER: 21,
}) })
}) })
@ -28,7 +29,9 @@ describe('getRCFileVars', (): void => {
try { try {
await getRCFileVars({ environments: ['bad'], filePath: 'bad-path' }) await getRCFileVars({ environments: ['bad'], filePath: 'bad-path' })
assert.fail('Should not get here!') assert.fail('Should not get here!')
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.match(e.message, /\.rc file at path/gi) assert.match(e.message, /\.rc file at path/gi)
} }
}) })
@ -37,7 +40,9 @@ describe('getRCFileVars', (): void => {
try { try {
await getRCFileVars({ environments: ['bad'], filePath: rcFilePath }) await getRCFileVars({ environments: ['bad'], filePath: rcFilePath })
assert.fail('Should not get here!') assert.fail('Should not get here!')
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.match(e.message, /environments/gi) assert.match(e.message, /environments/gi)
} }
}) })
@ -46,20 +51,36 @@ describe('getRCFileVars', (): void => {
try { try {
await getRCFileVars({ environments: ['bad'], filePath: './test/test-files/.rc-test-bad-format' }) await getRCFileVars({ environments: ['bad'], filePath: './test/test-files/.rc-test-bad-format' })
assert.fail('Should not get here!') assert.fail('Should not get here!')
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.match(e.message, /parse/gi) assert.match(e.message, /parse/gi)
} }
}) })
it('should parse an async js .rc file', async (): Promise<void> => { it('should parse an async js/cjs .rc file', async (): Promise<void> => {
const env = await getRCFileVars({ const env = await getRCFileVars({
environments: ['production'], environments: ['production'],
filePath: './test/test-files/.rc-test-async.js' filePath: './test/test-files/.rc-test-async.cjs',
}) })
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',
ANSWER: 42, ANSWER: 42,
ONLY: 'IN PRODUCTION' ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
})
})
it('should parse an async mjs .rc file', async (): Promise<void> => {
const env = await getRCFileVars({
environments: ['production'],
filePath: './test/test-files/.rc-test-async.mjs',
})
assert.deepEqual(env, {
THANKS: 'FOR WHAT?!',
ANSWER: 42,
ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
}) })
}) })
}) })

View File

@ -1,6 +1,9 @@
import { assert } from 'chai' import { assert } from 'chai'
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { TermSignals } from '../src/signal-termination' import { TermSignals } from '../src/signal-termination.js'
import { ChildProcess } from 'child_process'
type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void
describe('signal-termination', (): void => { describe('signal-termination', (): void => {
let sandbox: sinon.SinonSandbox let sandbox: sinon.SinonSandbox
@ -15,8 +18,8 @@ describe('signal-termination', (): void => {
describe('TermSignals', (): void => { describe('TermSignals', (): void => {
describe('_uncaughtExceptionHandler', (): void => { describe('_uncaughtExceptionHandler', (): void => {
const term = new TermSignals() const term = new TermSignals()
let logStub: sinon.SinonStub<any, any> let logStub: sinon.SinonStub<any>
let processStub: sinon.SinonStub<any, any> let processStub: sinon.SinonStub<any>
beforeEach((): void => { beforeEach((): void => {
logStub = sandbox.stub(console, 'error') logStub = sandbox.stub(console, 'error')
@ -39,7 +42,7 @@ describe('signal-termination', (): void => {
describe('_removeProcessListeners', (): void => { describe('_removeProcessListeners', (): void => {
const term = new TermSignals() const term = new TermSignals()
let removeListenerStub: sinon.SinonStub<any, any> let removeListenerStub: sinon.SinonStub<any>
before((): void => { before((): void => {
removeListenerStub = sandbox.stub(process, 'removeListener') 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 => { it('should remove all listeners from default signals and exit signal', (): void => {
term._removeProcessListeners() term._removeProcessListeners()
assert.equal(removeListenerStub.callCount, 4) assert.equal(removeListenerStub.callCount, 3)
assert.equal(removeListenerStub.args[3][0], 'exit') assert.oneOf(removeListenerStub.args[2][0], ['SIGTERM', 'SIGINT', 'SIGHUP'])
}) })
}) })
describe('_terminateProcess', (): void => { describe('_terminateProcess', (): void => {
const term = new TermSignals() const term = new TermSignals()
let exitStub: sinon.SinonStub<any, any> let exitStub: sinon.SinonStub<any>
let killStub: sinon.SinonStub<any, any> let killStub: sinon.SinonStub<any>
beforeEach((): void => { beforeEach((): void => {
exitStub = sandbox.stub(process, 'exit') exitStub = sandbox.stub(process, 'exit')
@ -71,6 +74,7 @@ describe('signal-termination', (): void => {
it('should call exit method on parent process if no signal provided', (): void => { it('should call exit method on parent process if no signal provided', (): void => {
term._terminateProcess(0) term._terminateProcess(0)
// We here test code that in reality is unreachable.
assert.equal(exitStub.callCount, 1) assert.equal(exitStub.callCount, 1)
}) })
@ -82,7 +86,7 @@ describe('signal-termination', (): void => {
}) })
it('should call kill method with correct kill signal', (): 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(killStub.callCount, 1)
assert.equal(exitStub.callCount, 0) assert.equal(exitStub.callCount, 0)
assert.equal(killStub.args[0][1], 'SIGINT') assert.equal(killStub.args[0][1], 'SIGINT')
@ -92,7 +96,9 @@ describe('signal-termination', (): void => {
try { try {
term._terminateProcess() term._terminateProcess()
assert.fail('should not get here') assert.fail('should not get here')
} catch (e) { }
catch (e) {
assert.instanceOf(e, Error)
assert.match(e.message, /unable to terminate parent process/gi) assert.match(e.message, /unable to terminate parent process/gi)
} }
}) })
@ -100,8 +106,8 @@ describe('signal-termination', (): void => {
describe('handleUncaughtExceptions', (): void => { describe('handleUncaughtExceptions', (): void => {
const term = new TermSignals() const term = new TermSignals()
let processOnStub: sinon.SinonStub<any, any> let processOnStub: sinon.SinonStub<any>
let _uncaughtExceptionHandlerStub: sinon.SinonStub<any, any> let _uncaughtExceptionHandlerStub: sinon.SinonStub<any>
before((): void => { before((): void => {
processOnStub = sandbox.stub(process, 'on') processOnStub = sandbox.stub(process, 'on')
@ -116,22 +122,22 @@ describe('signal-termination', (): void => {
term.handleUncaughtExceptions() term.handleUncaughtExceptions()
assert.equal(processOnStub.callCount, 1) assert.equal(processOnStub.callCount, 1)
assert.equal(_uncaughtExceptionHandlerStub.callCount, 0) assert.equal(_uncaughtExceptionHandlerStub.callCount, 0)
processOnStub.args[0][1]() ;(processOnStub.args[0][1] as () => void)()
assert.equal(_uncaughtExceptionHandlerStub.callCount, 1) assert.equal(_uncaughtExceptionHandlerStub.callCount, 1)
}) })
}) })
describe('handleTermSignals', (): void => { describe('handleTermSignals', (): void => {
let term: TermSignals let term: TermSignals
let procKillStub: sinon.SinonStub<any, any> let procKillStub: sinon.SinonStub<any>
let procOnStub: sinon.SinonStub<any, any> let procOnStub: sinon.SinonStub<any>
let processOnceStub: sinon.SinonStub<any, any> let processOnceStub: sinon.SinonStub<any>
let _removeProcessListenersStub: sinon.SinonStub<any, any> let _removeProcessListenersStub: sinon.SinonStub<any>
let _terminateProcessStub: sinon.SinonStub<any, any> let _terminateProcessStub: sinon.SinonStub<any>
let logInfoStub: sinon.SinonStub<any, any> let logInfoStub: sinon.SinonStub<any>
let proc: any let proc: ChildProcess
function setup (verbose: boolean = false): void { function setup(verbose = false): void {
term = new TermSignals({ verbose }) term = new TermSignals({ verbose })
procKillStub = sandbox.stub() procKillStub = sandbox.stub()
procOnStub = sandbox.stub() procOnStub = sandbox.stub()
@ -140,8 +146,8 @@ describe('signal-termination', (): void => {
_terminateProcessStub = sandbox.stub(term, '_terminateProcess') _terminateProcessStub = sandbox.stub(term, '_terminateProcess')
proc = { proc = {
kill: procKillStub, kill: procKillStub,
on: procOnStub on: procOnStub,
} } as unknown as ChildProcess
} }
beforeEach((): void => { beforeEach((): void => {
@ -162,7 +168,7 @@ describe('signal-termination', (): void => {
it('should terminate child process if parent process terminated', (): void => { it('should terminate child process if parent process terminated', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1]('SIGTERM', 1) ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM')
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
@ -175,17 +181,17 @@ describe('signal-termination', (): void => {
logInfoStub = sandbox.stub(console, 'info') logInfoStub = sandbox.stub(console, 'info')
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1]('SIGTERM', 1) ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM')
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should not terminate child process if child process termination ' + it('should not terminate child process if child process termination '
'has already been called by parent', (): void => { + 'has already been called by parent', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1]('SIGINT', 1) ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGINT')
assert.isOk(term._exitCalled) 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(_removeProcessListenersStub.callCount, 2)
assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
@ -195,7 +201,7 @@ describe('signal-termination', (): void => {
it('should convert and use number signal as code', (): void => { it('should convert and use number signal as code', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1](4, 1) ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(1)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.callCount, 1)
assert.equal(procKillStub.args[0][0], 'SIGINT') assert.equal(procKillStub.args[0][0], 'SIGINT')
@ -203,13 +209,13 @@ describe('signal-termination', (): void => {
assert.isOk(term._exitCalled) 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) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1](0, 1) ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(0)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(procKillStub.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.equal(_terminateProcessStub.callCount, 1)
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
}) })
@ -217,7 +223,7 @@ describe('signal-termination', (): void => {
it('should use signal value and default SIGINT signal if code is undefined', (): void => { it('should use signal value and default SIGINT signal if code is undefined', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
processOnceStub.args[0][1](4, undefined) ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(4)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.callCount, 1)
assert.equal(procKillStub.args[0][0], 'SIGINT') assert.equal(procKillStub.args[0][0], 'SIGINT')
@ -228,7 +234,7 @@ describe('signal-termination', (): void => {
it('should terminate parent process if child process terminated', (): void => { it('should terminate parent process if child process terminated', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](1, 'SIGTERM') ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM')
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
@ -240,31 +246,31 @@ describe('signal-termination', (): void => {
logInfoStub = sandbox.stub(console, 'info') logInfoStub = sandbox.stub(console, 'info')
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](1, 'SIGTERM') ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM')
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it( it(
'should print parent process terminated to info for verbose when ' + 'should print parent process terminated to info for verbose when '
'code and signal are undefined', + 'code and signal are undefined',
(): void => { (): void => {
sandbox.restore() sandbox.restore()
setup(true) setup(true)
logInfoStub = sandbox.stub(console, 'info') logInfoStub = sandbox.stub(console, 'info')
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](undefined, null) ;(procOnStub.args[0][1] as ChildExitListener)(null, null)
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
} },
) )
it('should not terminate parent process if parent process already terminating', (): void => { it('should not terminate parent process if parent process already terminating', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](1, 'SIGINT') ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGINT')
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.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(_removeProcessListenersStub.callCount, 2)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
@ -273,7 +279,7 @@ describe('signal-termination', (): void => {
it('should convert null signal value to undefined', (): void => { it('should convert null signal value to undefined', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](0, null) ;(procOnStub.args[0][1] as ChildExitListener)(0, null)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.strictEqual(_terminateProcessStub.firstCall.args[1], undefined) assert.strictEqual(_terminateProcessStub.firstCall.args[1], undefined)
@ -283,33 +289,30 @@ describe('signal-termination', (): void => {
it('should convert and use number signal as code', (): void => { it('should convert and use number signal as code', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](1, 4) ;(procOnStub.args[0][1] as ChildExitListener)(1, 4)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 4) assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT')
assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT')
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
}) })
it('should not use signal number as code if value is 0', (): void => { it('should not use signal number as code if value is 0', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](1, 0) ;(procOnStub.args[0][1] as ChildExitListener)(1, 0)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1) assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1)
assert.isUndefined(_terminateProcessStub.firstCall.args[1])
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
}) })
it('should use signal value and default SIGINT signal if code is undefined', (): void => { it('should use signal value and default SIGINT signal if code is undefined', (): void => {
assert.notOk(term._exitCalled) assert.notOk(term._exitCalled)
term.handleTermSignals(proc) term.handleTermSignals(proc)
procOnStub.args[0][1](null, 1) ;(procOnStub.args[0][1] as ChildExitListener)(null, 1)
assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_removeProcessListenersStub.callCount, 1)
assert.equal(_terminateProcessStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1)
assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1) assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT')
assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT')
assert.isOk(term._exitCalled) assert.isOk(term._exitCalled)
}) })
}) })

View File

@ -10,6 +10,7 @@
"production": { "production": {
"THANKS": "FOR WHAT?!", "THANKS": "FOR WHAT?!",
"ANSWER": 42, "ANSWER": 42,
"ONLY": "IN PRODUCTION" "ONLY": "IN PRODUCTION",
"BRINGATOWEL": true
} }
} }

View File

@ -0,0 +1,21 @@
module.exports = new Promise((resolve) => {
setTimeout(() => {
console.log('resolved')
resolve({
development: {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
},
test: {
THANKS: 'FOR MORE FISHIES',
ANSWER: 21,
},
production: {
THANKS: 'FOR WHAT?!',
ANSWER: 42,
ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
},
})
}, 200)
})

View File

@ -1,19 +0,0 @@
module.exports = new Promise((resolve) => {
setTimeout(() => {
resolve({
'development': {
'THANKS': 'FOR ALL THE FISH',
'ANSWER': 0
},
'test': {
'THANKS': 'FOR MORE FISHIES',
'ANSWER': 21
},
'production': {
'THANKS': 'FOR WHAT?!',
'ANSWER': 42,
'ONLY': 'IN PRODUCTION'
}
})
}, 200)
})

View File

@ -0,0 +1,20 @@
export default new Promise((resolve) => {
setTimeout(() => {
resolve({
development: {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
},
test: {
THANKS: 'FOR MORE FISHIES',
ANSWER: 21,
},
production: {
THANKS: 'FOR WHAT?!',
ANSWER: 42,
ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
},
})
}, 200)
})

View File

@ -10,6 +10,7 @@
"production": { "production": {
"THANKS": "FOR WHAT?!", "THANKS": "FOR WHAT?!",
"ANSWER": 42, "ANSWER": 42,
"ONLY": "IN PRODUCTION" "ONLY": "IN PRODUCTION",
"BRINGATOWEL": true
} }
} }

View File

@ -1,4 +1,5 @@
THANKS = FOR WHAT?! THANKS = FOR WHAT?!
ANSWER=42 ANSWER=42
ONLY= "IN=PRODUCTION" ONLY= "IN=PRODUCTION"
GALAXY="hitch\nhiking" GALAXY="hitch\nhiking"
BRINGATOWEL=true

View File

@ -2,7 +2,7 @@ module.exports = new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve({ resolve({
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0 ANSWER: 0,
}) })
}, 200) }, 200)
}) })

View File

@ -0,0 +1,8 @@
export default new Promise((resolve) => {
setTimeout(() => {
resolve({
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
})
}, 200)
})

View File

@ -2,5 +2,6 @@
"THANKS": "FOR WHAT?!", "THANKS": "FOR WHAT?!",
"ANSWER": 42, "ANSWER": 42,
"ONLY": "IN\n PRODUCTION", "ONLY": "IN\n PRODUCTION",
"GALAXY": "hitch\nhiking\n\n" "GALAXY": "hitch\nhiking\n\n",
} "BRINGATOWEL": true
}

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0, ANSWER: 0,
GALAXY: 'hitch\nhiking' GALAXY: 'hitch\nhiking',
} }

View File

@ -2,5 +2,6 @@
"THANKS": "FOR WHAT?!", "THANKS": "FOR WHAT?!",
"ANSWER": 42, "ANSWER": 42,
"ONLY": "IN PRODUCTION", "ONLY": "IN PRODUCTION",
"GALAXY": "hitch\nhiking" "GALAXY": "hitch\nhiking",
} "BRINGATOWEL": true
}

5
test/test-files/test.mjs Normal file
View File

@ -0,0 +1,5 @@
export default {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
GALAXY: 'hitch\nhiking',
}

18
test/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": false,
"lib": ["es2023"],
"module": "Node16",
"moduleDetection": "force",
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"target": "ES2022",
},
"include": [
"./**/*",
"./test-files/.rc-test-async.cjs",
"./test-files/.rc-test-async.mjs",
]
}

View File

@ -1,14 +1,31 @@
import * as os from 'os' import { homedir } from 'node:os'
import * as process from 'process' import { cwd } from 'node:process'
import * as path from 'path' import { normalize } from 'node:path'
import { assert } from 'chai' import { assert } from 'chai'
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils' import { default as esmock } from 'esmock'
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils.js'
let utilsLib: {
resolveEnvFilePath: typeof resolveEnvFilePath,
parseArgList: typeof parseArgList,
isPromise: typeof isPromise
}
describe('utils', (): void => { describe('utils', (): void => {
describe('resolveEnvFilePath', (): void => { describe('resolveEnvFilePath', (): void => {
const homePath = os.homedir() const homePath = homedir()
const currentDir = process.cwd() const currentDir = cwd()
let homedirStub: sinon.SinonStub<any>
before(async (): Promise<void> => {
homedirStub = sinon.stub()
utilsLib = await esmock('../src/utils.js', {
'node:os': {
homedir: homedirStub
},
})
})
afterEach((): void => { afterEach((): void => {
sinon.restore() sinon.restore()
@ -16,18 +33,17 @@ describe('utils', (): void => {
it('should return an absolute path, given a relative path', (): void => { it('should return an absolute path, given a relative path', (): void => {
const res = resolveEnvFilePath('./bob') const res = resolveEnvFilePath('./bob')
assert.equal(res, path.normalize(`${currentDir}/bob`)) assert.equal(res, normalize(`${currentDir}/bob`))
}) })
it('should return an absolute path, given a path with ~ for home directory', (): void => { it('should return an absolute path, given a path with ~ for home directory', (): void => {
const res = resolveEnvFilePath('~/bob') const res = resolveEnvFilePath('~/bob')
assert.equal(res, path.normalize(`${homePath}/bob`)) assert.equal(res, normalize(`${homePath}/bob`))
}) })
it('should not attempt to replace ~ if home dir does not exist', (): void => { it('should not attempt to replace ~ if home dir does not exist', (): void => {
sinon.stub(os, 'homedir') const res = utilsLib.resolveEnvFilePath('~/bob')
const res = resolveEnvFilePath('~/bob') assert.equal(res, normalize(`${currentDir}/~/bob`))
assert.equal(res, path.normalize(`${currentDir}/~/bob`))
}) })
}) })
@ -48,5 +64,13 @@ describe('utils', (): void => {
const res = isPromise({}) const res = isPromise({})
assert.isFalse(res) 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)
})
}) })
}) })

View File

@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true
},
"include": [
"src/**/*",
"test/**/*",
"bin/**/*"
]
}

View File

@ -1,18 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"declaration": true,
"esModuleInterop": false,
"lib": ["es2023"],
"module": "NodeNext",
"moduleDetection": "force",
"outDir": "./dist", "outDir": "./dist",
"target": "es2017",
"module": "commonjs",
"resolveJsonModule": true, "resolveJsonModule": true,
"strict": true, "strict": true,
"declaration": true, "target": "ES2022",
"lib": [ "rootDir": "src"
"es2018",
"es2019",
"es2020"
]
}, },
"include": [ "include": [
"./src/**/*" "src/**/*"
] ]
} }