Initial conversion of lib over to typescript

- All env-cmd-examples repo cases passing
- Added support for default .env-cmdrc.json file
- Added flag and help text lib
- Split up project into more reasonable files/chunks of code
- Updated copyright year to 2019
This commit is contained in:
Todd Bluhm 2019-01-31 16:12:55 -05:00
parent 9fcfd621dd
commit 0738042dbb
20 changed files with 1397 additions and 921 deletions

View File

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

10
lib/help.js Normal file
View File

@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Prints out some minor help text
* @return {String} Help text
*/
function PrintHelp() {
return "\nUsage: env-cmd [options] [env_file | env_name] command [command options]\n\nA simple utility for running a cli application using an env config file.\n\nAlso supports using a .env-cmdrc json file in the execution directory to support multiple\nenvironment configs in one file.\n\nOptions:\n --no-override - do not override existing process env vars with file env vars\n --fallback - if provided env file does not exist, attempt to use fallback .env file in root dir\n ";
}
exports.PrintHelp = PrintHelp;

View File

@ -1,375 +1,32 @@
'use strict'
const spawn = require('cross-spawn').spawn
const path = require('path')
const fs = require('fs')
const os = require('os')
const rcFileLocation = path.join(process.cwd(), '.env-cmdrc')
const envFilePathDefault = path.join(process.cwd(), '.env')
const terminateSpawnedProcessFuncHandlers = {}
let terminateProcessFuncHandler
const SIGNALS_TO_HANDLE = [
'SIGINT', 'SIGTERM', 'SIGHUP'
]
const sharedState = {
exitCalled: false
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var spawn = require("cross-spawn");
var signal_termination_1 = require("./signal-termination");
var parse_args_1 = require("./parse-args");
/**
* The main process for reading, parsing, applying and then running the process with env vars
* @param {Array<String>} args And array if strings representing cli args
*
* @return {Object} The child process
*/
function EnvCmd (args) {
// First Parse the args from the command line
const parsedArgs = ParseArgs(args)
// If a .rc file was found then use that
let parsedEnv
if (fs.existsSync(rcFileLocation)) {
parsedEnv = UseRCFile({ envFile: parsedArgs.envFile })
} else {
// Try to use a .env file
parsedEnv = UseCmdLine({ envFile: parsedArgs.envFile, useFallback: parsedArgs.useFallback })
}
let env
// Override the merge order if --no-override flag set
if (parsedArgs.noOverride) {
env = Object.assign({}, parsedEnv, process.env)
} else {
// Add in the system environment variables to our environment list
env = Object.assign({}, process.env, parsedEnv)
}
// Execute the command with the given environment variables
const proc = spawn(parsedArgs.command, parsedArgs.commandArgs, {
stdio: 'inherit',
env
})
// Handle a few special signals and then the general node exit event
// on both parent and spawned process
SIGNALS_TO_HANDLE.forEach(signal => {
terminateSpawnedProcessFuncHandlers[signal] = TerminateSpawnedProc.bind(sharedState, proc, signal)
process.once(signal, terminateSpawnedProcessFuncHandlers[signal])
})
process.once('exit', terminateSpawnedProcessFuncHandlers['SIGTERM'])
terminateProcessFuncHandler = TerminateParentProcess.bind(sharedState)
proc.on('exit', terminateProcessFuncHandler)
return proc
}
/**
* Parses the arguments passed into the cli
* @param {Array<String>} args An array of strings to parse the options out of
*
* @return {Object} An object containing cli options and commands
*/
function ParseArgs (args) {
if (args.length < 2) {
throw new Error('Error! Too few arguments passed to env-cmd.')
}
let envFile
let command
let noOverride
let useFallback
let commandArgs = args.slice()
while (commandArgs.length) {
const arg = commandArgs.shift()
if (arg === '--fallback') {
useFallback = true
continue
function EnvCmd(args) {
// First Parse the args from the command line
var parsedArgs = parse_args_1.parseArgs(args);
var env;
// Override the merge order if --no-override flag set
if (parsedArgs.options.noOverride) {
env = Object.assign({}, parsedArgs.envValues, process.env);
}
if (arg === '--no-override') {
noOverride = true
continue
else {
// Add in the system environment variables to our environment list
env = Object.assign({}, process.env, parsedArgs.envValues);
}
// assume the first arg is the env file (or if using .rc the environment name)
if (!envFile) {
envFile = arg
} else {
command = arg
break
}
}
return {
envFile,
command,
commandArgs,
noOverride,
useFallback
}
// Execute the command with the given environment variables
var proc = spawn(parsedArgs.command, parsedArgs.commandArgs, {
stdio: 'inherit',
env: env
});
// Handle any termination signals for parent and child proceses
signal_termination_1.handleTermSignals(proc);
return env;
}
/**
* Strips out comments from env file string
* @param {String} envString The .env file string
*
* @return {String} The .env file string with comments stripped out
*/
function StripComments (envString) {
const commentsRegex = /(^#.*$)/gim
let match = commentsRegex.exec(envString)
let newString = envString
while (match != null) {
newString = newString.replace(match[1], '')
match = commentsRegex.exec(envString)
}
return newString
}
/**
* Strips out newlines from env file string
* @param {String} envString The .env file string
*
* @return {String} The .env file string with newlines stripped out
*/
function StripEmptyLines (envString) {
const emptyLinesRegex = /(^\n)/gim
return envString.replace(emptyLinesRegex, '')
}
/**
* Parse out all env vars from an env file string
* @param {String} envString The .env file string
*
* @return {Object} Key/Value pairs corresponding to the .env file data
*/
function ParseEnvVars (envString) {
const envParseRegex = /^((.+?)[=](.*))$/gim
const matches = {}
let match
while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line
const key = match[2].trim()
let value = match[3].trim() || ''
// remove any surrounding quotes
value = value.replace(/(^['"]|['"]$)/g, '')
matches[key] = value
}
return matches
}
/**
* Parse out all env vars from a given env file string and return an object
* @param {String} envString The .env file string
*
* @return {Object} Key/Value pairs of all env vars parsed from files
*/
function ParseEnvString (envFileString) {
// First thing we do is stripe out all comments
envFileString = StripComments(envFileString.toString())
// Next we stripe out all the empty lines
envFileString = StripEmptyLines(envFileString)
// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return ParseEnvVars(envFileString)
}
/**
* Reads and parses the .env-cmdrc file
* @param {String} fileData the .env-cmdrc file data (which should be a valid json string)
*
* @return {Object} The .env-cmdrc as a parsed JSON object
*/
function ParseRCFile (fileData) {
let data
try {
data = JSON.parse(fileData)
} catch (e) {
console.error(`Error:
Could not parse the .env-cmdrc file.
Please make sure its in a valid JSON format.`)
throw new Error(`Unable to parse JSON in .env-cmdrc file.`)
}
return data
}
/**
* Uses the rc file to get env vars
* @param {Object} options
* @param {String} options.envFile The .env-cmdrc file environment to use
*
* @return {Object} Key/Value pair of env vars from the .env-cmdrc file
*/
function UseRCFile (options) {
const fileData = fs.readFileSync(rcFileLocation, { encoding: 'utf8' })
const parsedData = ParseRCFile(fileData)
let result = {}
const envNames = options.envFile.split(',')
if (envNames.length === 1 && !parsedData[envNames[0]]) {
console.error(`Error:
Could not find environment:
${options.envFile}
in .rc file:
${rcFileLocation}`)
throw new Error(`Missing environment ${options.envFile} in .env-cmdrc file.`)
}
envNames.forEach(function (name) {
const envVars = parsedData[name]
if (envVars) {
result = Object.assign(result, envVars)
}
})
return result
}
/**
* Uses the cli passed env file to get env vars
* @param {Object} options
* @param {String} options.envFile The .env file name/relative path
* @param {Boolean} options.useFallback Should we attempt to find a fallback file
*
* @return {Object} Key/Value pairing of env vars found in .env file
*/
function UseCmdLine (options) {
const envFilePath = ResolveEnvFilePath(options.envFile)
// Attempt to open the provided file
let file
try {
file = fs.readFileSync(envFilePath, { encoding: 'utf8' })
} catch (err) {
if (!options.useFallback) {
return {}
}
}
// If we don't have a main file try the fallback file
if (!file && options.useFallback) {
try {
file = fs.readFileSync(envFilePathDefault)
} catch (e) {
throw new Error(`Error! Could not find fallback file or read env file at ${envFilePathDefault}`)
}
}
// Get the file extension
const ext = path.extname(envFilePath).toLowerCase()
// Parse the env file string using the correct parser
const env = ext === '.json' || ext === '.js'
? require(envFilePath)
: ParseEnvString(file)
return env
}
/**
* Prints out some minor help text
* @return {String} Help text
*/
function PrintHelp () {
return `
Usage: env-cmd [options] [env_file | env_name] command [command options]
A simple utility for running a cli application using an env config file.
Also supports using a .env-cmdrc json file in the execution directory to support multiple
environment configs in one file.
Options:
--no-override - do not override existing process env vars with file env vars
--fallback - if provided env file does not exist, attempt to use fallback .env file in root dir
`
}
/**
* General exception handler
* @param {Error} e The exception error to handle
*/
function HandleUncaughtExceptions (e) {
if (e.message.match(/passed/gi)) {
console.log(PrintHelp())
}
console.log(e.message)
process.exit(1)
}
/**
* A simple function for resolving the path the user entered
* @param {String} userPath A path
* @return {String} The fully qualified absolute path
*/
function ResolveEnvFilePath (userPath) {
// Make sure a home directory exist
const home = os.homedir()
if (home) {
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`)
}
return path.resolve(process.cwd(), userPath)
}
/**
* Helper for terminating the spawned process
* @param {ProccessHandler} proc The spawned process handler
*/
function TerminateSpawnedProc (proc, signal, code) {
RemoveProcessListeners()
if (!this.exitCalled) {
this.exitCalled = true
proc.kill(signal)
}
if (code) {
return process.exit(code)
}
process.kill(process.pid, signal)
}
/**
* Helper for terminating the parent process
*/
function TerminateParentProcess (code, signal) {
RemoveProcessListeners()
if (!this.exitCalled) {
this.exitCalled = true
if (signal) {
return process.kill(process.pid, signal)
}
process.exit(code)
}
}
/**
* Helper for removing all termination signal listeners from parent process
*/
function RemoveProcessListeners () {
SIGNALS_TO_HANDLE.forEach(signal => {
process.removeListener(signal, terminateSpawnedProcessFuncHandlers[signal])
})
process.removeListener('exit', terminateSpawnedProcessFuncHandlers['SIGTERM'])
}
process.on('uncaughtException', HandleUncaughtExceptions)
module.exports = {
EnvCmd,
ParseArgs,
ParseEnvString,
PrintHelp,
HandleUncaughtExceptions,
TerminateSpawnedProc,
TerminateParentProcess,
StripComments,
StripEmptyLines,
ParseEnvVars,
ParseRCFile,
UseRCFile,
UseCmdLine,
ResolveEnvFilePath
}
EnvCmd: EnvCmd
};

99
lib/parse-args.js Normal file
View File

@ -0,0 +1,99 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var program = require("commander");
var utils_1 = require("./utils");
var parse_rc_file_1 = require("./parse-rc-file");
var parse_env_file_1 = require("./parse-env-file");
var RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.json'];
var ENV_FILE_DEFAULT_LOCATION = './.env';
/**
* Parses the arguments passed into the cli
*/
function parseArgs(args) {
program
.version('1.0.0', '-v, --version')
.usage('[options] <command> [...args]')
.option('-f, --file [path]', 'Custom .env file location')
.option('-r, --rc-file [path]', 'Custom .env-cmdrc file location')
.option('-e, --environments [env...]', 'The rc-file environment to select', utils_1.parseArgList)
.option('--fallback', 'Enables auto fallback to default env file location')
.option('--no-override', 'Do not override existing env vars')
.parse(['_', '_'].concat(args));
// get the command and command args
var command = program.args[0];
var commandArgs = program.args.slice(1);
var noOverride = !program.override;
var returnValue = {
command: command,
commandArgs: commandArgs,
options: {
noOverride: noOverride
}
};
// Check for rc file usage
var env;
if (program.environments) {
// user provided an .rc file path
if (program.rcFile) {
try {
env = parse_rc_file_1.useRCFile({ environments: program.environments, path: program.rcFile });
}
catch (e) {
console.log(program.outputHelp());
throw new Error("Unable to locate .rc file at location (" + program.rcFile + ")");
}
// Use the default .rc file locations
}
else {
for (var _i = 0, RC_FILE_DEFAULT_LOCATIONS_1 = RC_FILE_DEFAULT_LOCATIONS; _i < RC_FILE_DEFAULT_LOCATIONS_1.length; _i++) {
var path = RC_FILE_DEFAULT_LOCATIONS_1[_i];
try {
env = parse_rc_file_1.useRCFile({ environments: program.environments, path: path });
break;
}
catch (e) { }
}
if (!env) {
console.log(program.outputHelp());
throw new Error("Unable to locate .rc file at default locations (" + RC_FILE_DEFAULT_LOCATIONS + ")");
}
}
if (env) {
returnValue.envValues = env;
return returnValue;
}
}
// Use env file
if (program.file) {
try {
env = parse_env_file_1.useCmdLine(program.file);
}
catch (e) {
if (!program.fallback) {
console.log(program.outputHelp());
console.error("Unable to locate env file at location (" + program.file + ")");
throw e;
}
}
if (env) {
returnValue.envValues = env;
return returnValue;
}
}
// Use the default env file location
try {
env = parse_env_file_1.useCmdLine(ENV_FILE_DEFAULT_LOCATION);
}
catch (e) {
console.log(program.outputHelp());
console.error("Unable to locate env file at default locations (" + ENV_FILE_DEFAULT_LOCATION + ")");
throw e;
}
if (env) {
returnValue.envValues = env;
return returnValue;
}
console.log(program.outputHelp());
throw Error('Unable to locate any files to read environment data!');
}
exports.parseArgs = parseArgs;

77
lib/parse-env-file.js Normal file
View File

@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var fs = require("fs");
var path = require("path");
var utils_1 = require("./utils");
/**
* Uses the cli passed env file path to get env vars
*/
function useCmdLine(envFilePath) {
var absolutePath = utils_1.resolveEnvFilePath(envFilePath);
if (!fs.existsSync(absolutePath)) {
throw new Error("Invalid env file path (" + envFilePath + ").");
}
// Get the file extension
var ext = path.extname(absolutePath).toLowerCase();
var env;
if (ext === '.json') {
env = require(absolutePath);
}
else {
var file = fs.readFileSync(absolutePath, { encoding: 'utf8' });
env = parseEnvString(file);
}
return env;
}
exports.useCmdLine = useCmdLine;
/**
* Parse out all env vars from a given env file string and return an object
*/
function parseEnvString(envFileString) {
// First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString());
// Next we stripe out all the empty lines
envFileString = stripEmptyLines(envFileString);
// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return parseEnvVars(envFileString);
}
exports.parseEnvString = parseEnvString;
/**
* Parse out all env vars from an env file string
*/
function parseEnvVars(envString) {
var envParseRegex = /^((.+?)[=](.*))$/gim;
var matches = {};
var match;
while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line
var key = match[2].trim();
var value = match[3].trim() || '';
// remove any surrounding quotes
matches[key] = value.replace(/(^['"]|['"]$)/g, '');
}
return matches;
}
exports.parseEnvVars = parseEnvVars;
/**
* Strips out comments from env file string
*/
function stripComments(envString) {
var commentsRegex = /(^#.*$)/gim;
var match = commentsRegex.exec(envString);
var newString = envString;
while (match != null) {
newString = newString.replace(match[1], '');
match = commentsRegex.exec(envString);
}
return newString;
}
exports.stripComments = stripComments;
/**
* Strips out newlines from env file string
*/
function stripEmptyLines(envString) {
var emptyLinesRegex = /(^\n)/gim;
return envString.replace(emptyLinesRegex, '');
}
exports.stripEmptyLines = stripEmptyLines;

63
lib/parse-rc-file.js Normal file
View File

@ -0,0 +1,63 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
var fs = require("fs");
var utils_1 = require("./utils");
/**
* Uses the rc file and rc environment to get env vars
*/
function useRCFile(_a) {
var environments = _a.environments, path = _a.path;
var absolutePath = utils_1.resolveEnvFilePath(path);
console.log(absolutePath);
if (!fs.existsSync(absolutePath)) {
throw new Error('Invalid .rc file path.');
}
var fileData = fs.readFileSync(absolutePath, { encoding: 'utf8' });
var parsedData = parseRCFile(fileData);
if (environments.length === 1 && !parsedData[environments[0]]) {
console.error("Error:\n Could not find environment:\n " + environments[0] + "\n in .rc file:\n " + absolutePath);
throw new Error("Missing environment " + environments[0] + " in .env-cmdrc file.");
}
// Parse and merge multiple rc environments together
var result = {};
var environmentFound = false;
environments.forEach(function (name) {
var envVars = parsedData[name];
if (envVars) {
environmentFound = true;
result = __assign({}, result, envVars);
}
});
if (!environmentFound) {
console.error("Error:\n Could not find any environments:\n " + environments + "\n in .rc file:\n " + absolutePath);
throw new Error("All environments (" + environments + ") are missing in in .rc file (" + absolutePath + ").");
}
return result;
}
exports.useRCFile = useRCFile;
/**
* Reads and parses the .env-cmdrc file
*/
function parseRCFile(fileData) {
var data;
try {
data = JSON.parse(fileData);
}
catch (e) {
console.error("Error:\n Could not parse the .env-cmdrc file.\n Please make sure its in a valid JSON format.");
throw new Error("Unable to parse JSON in .env-cmdrc file.");
}
return data;
}
exports.parseRCFile = parseRCFile;

74
lib/signal-termination.js Normal file
View File

@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var program = require("commander");
var SIGNALS_TO_HANDLE = [
'SIGINT', 'SIGTERM', 'SIGHUP'
];
var terminateSpawnedProcessFuncHandlers = {};
var sharedState = {
exitCalled: false
};
function handleTermSignals(proc) {
// Handle a few special signals and then the general node exit event
// on both parent and spawned process
SIGNALS_TO_HANDLE.forEach(function (signal) {
terminateSpawnedProcessFuncHandlers[signal] =
function (signal, code) { return terminateSpawnedProc(proc, signal, sharedState, code); };
process.once(signal, terminateSpawnedProcessFuncHandlers[signal]);
});
process.once('exit', terminateSpawnedProcessFuncHandlers['SIGTERM']);
var terminateProcessFuncHandler = function (code, signal) { return terminateParentProcess(code, signal, sharedState); };
proc.on('exit', terminateProcessFuncHandler);
}
exports.handleTermSignals = handleTermSignals;
/**
* Helper for terminating the spawned process
*/
function terminateSpawnedProc(proc, signal, sharedState, code) {
removeProcessListeners();
if (!sharedState.exitCalled) {
sharedState.exitCalled = true;
proc.kill(signal);
}
if (code) {
return process.exit(code);
}
process.kill(process.pid, signal);
}
exports.terminateSpawnedProc = terminateSpawnedProc;
/**
* Helper for terminating the parent process
*/
function terminateParentProcess(code, signal, sharedState) {
removeProcessListeners();
if (!sharedState.exitCalled) {
sharedState.exitCalled = true;
if (signal) {
return process.kill(process.pid, signal);
}
process.exit(code);
}
}
exports.terminateParentProcess = terminateParentProcess;
/**
* Helper for removing all termination signal listeners from parent process
*/
function removeProcessListeners() {
SIGNALS_TO_HANDLE.forEach(function (signal) {
process.removeListener(signal, terminateSpawnedProcessFuncHandlers[signal]);
});
process.removeListener('exit', terminateSpawnedProcessFuncHandlers['SIGTERM']);
}
exports.removeProcessListeners = removeProcessListeners;
/**
* General exception handler
*/
function handleUncaughtExceptions(e) {
if (e.message.match(/passed/gi)) {
console.log(program.outputHelp());
}
console.log(e.message);
process.exit(1);
}
exports.handleUncaughtExceptions = handleUncaughtExceptions;
process.on('uncaughtException', handleUncaughtExceptions);

23
lib/utils.js Normal file
View File

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var path = require("path");
var os = require("os");
/**
* A simple function for resolving the path the user entered
*/
function resolveEnvFilePath(userPath) {
// Make sure a home directory exist
var home = os.homedir();
if (home) {
userPath = userPath.replace(/^~($|\/|\\)/, home + "$1");
}
return path.resolve(process.cwd(), userPath);
}
exports.resolveEnvFilePath = resolveEnvFilePath;
/**
* A simple function that parses a comma separated string into an array of strings
*/
function parseArgList(list) {
return list.split(',');
}
exports.parseArgList = parseArgList;

View File

@ -10,11 +10,12 @@
"env-cmd": "bin/env-cmd.js"
},
"scripts": {
"test": "mocha",
"test": "mocha -r ts-node/register ./**/*.ts",
"test-cover": "nyc --reporter=lcov --reporter=text npm test",
"test-lint": "standard",
"coveralls": "coveralls < coverage/lcov.info",
"lint": "standard --fix"
"lint": "standard --env mocha --fix ./**/*.ts",
"build": "tsc"
},
"repository": {
"type": "git",
@ -41,15 +42,28 @@
},
"homepage": "https://github.com/toddbluhm/env-cmd#readme",
"dependencies": {
"commander": "^2.19.0",
"cross-spawn": "^6.0.5"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.0",
"@types/node": "^10.12.20",
"@typescript-eslint/eslint-plugin": "^1.1.1",
"@typescript-eslint/parser": "^1.1.1",
"better-assert": "^1.0.2",
"coveralls": "^3.0.1",
"mocha": "^5.1.1",
"nyc": "^13.1.0",
"proxyquire": "^2.0.1",
"sinon": "^7.0.0",
"standard": "^12.0.1"
"sinon": "^7.2.3",
"standard": "^12.0.1",
"ts-node": "^8.0.2",
"typescript": "~3.2.1"
},
"standard": {
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
]
}
}

35
src/index.ts Normal file
View File

@ -0,0 +1,35 @@
import * as spawn from 'cross-spawn'
import { handleTermSignals } from './signal-termination'
import { parseArgs } from './parse-args'
/**
* The main process for reading, parsing, applying and then running the process with env vars
*/
function EnvCmd (args: string[]): { [key: string]: any } {
// First Parse the args from the command line
const parsedArgs = parseArgs(args)
let env
// Override the merge order if --no-override flag set
if (parsedArgs.options.noOverride) {
env = Object.assign({}, parsedArgs.envValues, process.env)
} else {
// Add in the system environment variables to our environment list
env = Object.assign({}, process.env, parsedArgs.envValues)
}
// Execute the command with the given environment variables
const proc = spawn(parsedArgs.command, parsedArgs.commandArgs, {
stdio: 'inherit',
env
})
// Handle any termination signals for parent and child proceses
handleTermSignals(proc)
return env
}
module.exports = {
EnvCmd
}

110
src/parse-args.ts Normal file
View File

@ -0,0 +1,110 @@
import * as program from 'commander'
import { parseArgList } from './utils'
import { useRCFile } from './parse-rc-file'
import { useCmdLine } from './parse-env-file'
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.json']
const ENV_FILE_DEFAULT_LOCATION = './.env'
type parseArgsReturnVal = {
envValues: { [key: string]: any }
command: string
commandArgs: string[]
options: {
noOverride: boolean
}
}
/**
* Parses the arguments passed into the cli
*/
export function parseArgs (args: string[]): parseArgsReturnVal {
program
.version('1.0.0', '-v, --version')
.usage('[options] <command> [...args]')
.option('-f, --file [path]', 'Custom .env file location')
.option('-r, --rc-file [path]', 'Custom .env-cmdrc file location')
.option('-e, --environments [env...]', 'The rc-file environment to select', parseArgList)
.option('--fallback', 'Enables auto fallback to default env file location')
.option('--no-override', 'Do not override existing env vars')
.parse(['_', '_', ...args])
// get the command and command args
const command = program.args[0]
const commandArgs = program.args.slice(1)
const noOverride = !program.override
const returnValue: any = {
command,
commandArgs,
options: {
noOverride
}
}
// Check for rc file usage
let env
if (program.environments) {
// user provided an .rc file path
if (program.rcFile) {
try {
env = useRCFile({ environments: program.environments, path: program.rcFile })
} catch (e) {
console.log(program.outputHelp())
throw new Error(`Unable to locate .rc file at location (${program.rcFile})`)
}
// Use the default .rc file locations
} else {
for (const path of RC_FILE_DEFAULT_LOCATIONS) {
try {
env = useRCFile({ environments: program.environments, path })
break
} catch (e) {}
}
if (!env) {
console.log(program.outputHelp())
throw new Error(`Unable to locate .rc file at default locations (${RC_FILE_DEFAULT_LOCATIONS})`)
}
}
if (env) {
returnValue.envValues = env
return returnValue
}
}
// Use env file
if (program.file) {
try {
env = useCmdLine(program.file)
} catch (e) {
if (!program.fallback) {
console.log(program.outputHelp())
console.error(`Unable to locate env file at location (${program.file})`)
throw e
}
}
if (env) {
returnValue.envValues = env
return returnValue
}
}
// Use the default env file location
try {
env = useCmdLine(ENV_FILE_DEFAULT_LOCATION)
} catch (e) {
console.log(program.outputHelp())
console.error(`Unable to locate env file at default locations (${ENV_FILE_DEFAULT_LOCATION})`)
throw e
}
if (env) {
returnValue.envValues = env
return returnValue
}
console.log(program.outputHelp())
throw Error('Unable to locate any files to read environment data!')
}

79
src/parse-env-file.ts Normal file
View File

@ -0,0 +1,79 @@
import * as fs from 'fs'
import * as path from 'path'
import { resolveEnvFilePath } from './utils'
/**
* Uses the cli passed env file path to get env vars
*/
export function useCmdLine (envFilePath: string): { [key: string]: any } {
const absolutePath = resolveEnvFilePath(envFilePath)
if (!fs.existsSync(absolutePath)) {
throw new Error(`Invalid env file path (${envFilePath}).`)
}
// Get the file extension
const ext = path.extname(absolutePath).toLowerCase()
let env
if (ext === '.json') {
env = require(absolutePath)
} else {
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' })
env = parseEnvString(file)
}
return env
}
/**
* Parse out all env vars from a given env file string and return an object
*/
export function parseEnvString (envFileString: string): { [key: string]: string } {
// First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString())
// Next we stripe out all the empty lines
envFileString = stripEmptyLines(envFileString)
// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return parseEnvVars(envFileString)
}
/**
* Parse out all env vars from an env file string
*/
export function parseEnvVars (envString: string): { [key: string]: string } {
const envParseRegex = /^((.+?)[=](.*))$/gim
const matches: { [key: string]: string } = {}
let match
while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line
const key = match[2].trim()
const value = match[3].trim() || ''
// remove any surrounding quotes
matches[key] = value.replace(/(^['"]|['"]$)/g, '')
}
return matches
}
/**
* Strips out comments from env file string
*/
export function stripComments (envString: string): string {
const commentsRegex = /(^#.*$)/gim
let match = commentsRegex.exec(envString)
let newString = envString
while (match != null) {
newString = newString.replace(match[1], '')
match = commentsRegex.exec(envString)
}
return newString
}
/**
* Strips out newlines from env file string
*/
export function stripEmptyLines (envString: string): string {
const emptyLinesRegex = /(^\n)/gim
return envString.replace(emptyLinesRegex, '')
}

68
src/parse-rc-file.ts Normal file
View File

@ -0,0 +1,68 @@
import * as fs from 'fs'
import { resolveEnvFilePath } from './utils'
/**
* Uses the rc file and rc environment to get env vars
*/
export function useRCFile (
{ environments, path }:
{ environments: string[], path: string }
): { [key: string]: any } {
const absolutePath = resolveEnvFilePath(path)
console.log(absolutePath)
if (!fs.existsSync(absolutePath)) {
throw new Error('Invalid .rc file path.')
}
const fileData = fs.readFileSync(absolutePath, { encoding: 'utf8' })
const parsedData = parseRCFile(fileData)
if (environments.length === 1 && !parsedData[environments[0]]) {
console.error(`Error:
Could not find environment:
${environments[0]}
in .rc file:
${absolutePath}`)
throw new Error(`Missing environment ${environments[0]} in .env-cmdrc file.`)
}
// Parse and merge multiple rc environments together
let result = {}
let environmentFound = false
environments.forEach(name => {
const envVars = parsedData[name]
if (envVars) {
environmentFound = true
result = {
...result,
...envVars
}
}
})
if (!environmentFound) {
console.error(`Error:
Could not find any environments:
${environments}
in .rc file:
${absolutePath}`)
throw new Error(`All environments (${environments}) are missing in in .rc file (${absolutePath}).`)
}
return result
}
/**
* Reads and parses the .env-cmdrc file
*/
export function parseRCFile (fileData: string): { [key: string]: any } {
let data
try {
data = JSON.parse(fileData)
} catch (e) {
console.error(`Error:
Could not parse the .env-cmdrc file.
Please make sure its in a valid JSON format.`)
throw new Error(`Unable to parse JSON in .env-cmdrc file.`)
}
return data
}

84
src/signal-termination.ts Normal file
View File

@ -0,0 +1,84 @@
import { ChildProcess } from 'child_process' // eslint-disable-line
import * as program from 'commander'
const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [
'SIGINT', 'SIGTERM', 'SIGHUP'
]
const terminateSpawnedProcessFuncHandlers: {[key: string]: any } = {}
const sharedState = {
exitCalled: false
}
export function handleTermSignals (proc: ChildProcess) {
// Handle a few special signals and then the general node exit event
// on both parent and spawned process
SIGNALS_TO_HANDLE.forEach(signal => {
terminateSpawnedProcessFuncHandlers[signal] =
(signal: any, code: any) => terminateSpawnedProc(proc, signal, sharedState, code)
process.once(signal, terminateSpawnedProcessFuncHandlers[signal])
})
process.once('exit', terminateSpawnedProcessFuncHandlers['SIGTERM'])
const terminateProcessFuncHandler =
(code: any, signal: any) => terminateParentProcess(code, signal, sharedState)
proc.on('exit', terminateProcessFuncHandler)
}
/**
* Helper for terminating the spawned process
*/
export function terminateSpawnedProc (
proc: ChildProcess, signal: string, sharedState: any, code?: number
) {
removeProcessListeners()
if (!sharedState.exitCalled) {
sharedState.exitCalled = true
proc.kill(signal)
}
if (code) {
return process.exit(code)
}
process.kill(process.pid, signal)
}
/**
* Helper for terminating the parent process
*/
export function terminateParentProcess (code: number, signal: string, sharedState: any) {
removeProcessListeners()
if (!sharedState.exitCalled) {
sharedState.exitCalled = true
if (signal) {
return process.kill(process.pid, signal)
}
process.exit(code)
}
}
/**
* Helper for removing all termination signal listeners from parent process
*/
export function removeProcessListeners () {
SIGNALS_TO_HANDLE.forEach(signal => {
process.removeListener(signal, terminateSpawnedProcessFuncHandlers[signal])
})
process.removeListener('exit', terminateSpawnedProcessFuncHandlers['SIGTERM'])
}
/**
* General exception handler
*/
export function handleUncaughtExceptions (e: Error) {
if (e.message.match(/passed/gi)) {
console.log(program.outputHelp())
}
console.log(e.message)
process.exit(1)
}
process.on('uncaughtException', handleUncaughtExceptions)

20
src/utils.ts Normal file
View File

@ -0,0 +1,20 @@
import * as path from 'path'
import * as os from 'os'
/**
* A simple function for resolving the path the user entered
*/
export function resolveEnvFilePath (userPath: string): string {
// Make sure a home directory exist
const home = os.homedir()
if (home) {
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`)
}
return path.resolve(process.cwd(), userPath)
}
/**
* A simple function that parses a comma separated string into an array of strings
*/
export function parseArgList (list: string): string[] {
return list.split(',')
}

417
test/index.ts Normal file
View File

@ -0,0 +1,417 @@
// import * as assert from 'better-assert'
// import { describe, it, afterEach, beforeEach, before, after } from 'mocha'
// import * as path from 'path'
// import * as proxyquire from 'proxyquire'
// import * as sinon from 'sinon'
// import * as fs from 'fs'
// let userHomeDir = '/Users/hitchhikers-guide-to-the-galaxy'
// const spawnStub = sinon.spy(() => ({
// on: sinon.stub(),
// exit: sinon.stub(),
// kill: sinon.stub()
// }))
// const lib = proxyquire('../lib', {
// 'cross-spawn': {
// spawn: spawnStub
// },
// [path.resolve(process.cwd(), 'test/.env.json')]: {
// BOB: 'COOL',
// NODE_ENV: 'dev',
// ANSWER: '42'
// },
// [path.resolve(process.cwd(), 'test/.env.js')]: {
// BOB: 'COOL',
// NODE_ENV: 'dev',
// ANSWER: '42'
// },
// 'os': {
// homedir: () => userHomeDir
// }
// })
// const EnvCmd = lib.EnvCmd
// const parseArgs = lib.parseArgs
// const ParseEnvString = lib.ParseEnvString
// const StripComments = lib.StripComments
// const StripEmptyLines = lib.StripEmptyLines
// const ParseEnvVars = lib.ParseEnvVars
// const ResolveEnvFilePath = lib.ResolveEnvFilePath
// describe('env-cmd', function () {
// describe('parseArgs', function () {
// it('should parse out --no-override option ', function () {
// const parsedArgs = parseArgs(['--no-override', './test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.option. === true)
// })
// it('should parse out the envfile', function () {
// const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.envFile === './test/envFile')
// })
// it('should parse out the command', function () {
// const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.command === 'command')
// })
// it('should parse out the command args', function () {
// const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.commandArgs.length === 2)
// assert(parsedArgs.commandArgs[0] === 'cmda1')
// assert(parsedArgs.commandArgs[1] === 'cmda2')
// })
// it('should error out if incorrect number of args passed', function () {
// try {
// ParseArgs(['./test/envFile'])
// } catch (e) {
// assert(e.message === 'Error! Too few arguments passed to env-cmd.')
// return
// }
// assert(!'No exception thrown')
// })
// })
// describe('ParseEnvString', function () {
// it('should parse env vars and merge (overwrite) with process.env vars', function () {
// const env = ParseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
// assert(env.BOB === 'COOL')
// assert(env.NODE_ENV === 'dev')
// assert(env.ANSWER === '42')
// })
// })
// describe('StripComments', function () {
// it('should strip out all full line comments', function () {
// const envString = StripComments('#BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n#AnotherComment\n')
// assert(envString === '\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n\n')
// })
// })
// describe('StripEmptyLines', function () {
// it('should strip out all empty lines', function () {
// const envString = StripEmptyLines('\nBOB=COOL\n\nNODE_ENV=dev\n\nANSWER=42 AND COUNTING\n\n')
// assert(envString === 'BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n')
// })
// })
// describe('ParseEnvVars', function () {
// it('should parse out all env vars in string when not ending with \'\\n\'', function () {
// const envVars = ParseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING')
// assert(envVars.BOB === 'COOL')
// assert(envVars.NODE_ENV === 'dev')
// assert(envVars.ANSWER === '42 AND COUNTING')
// })
// it('should parse out all env vars in string with format \'key=value\'', function () {
// const envVars = ParseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n')
// assert(envVars.BOB === 'COOL')
// assert(envVars.NODE_ENV === 'dev')
// assert(envVars.ANSWER === '42 AND COUNTING')
// })
// it('should ignore invalid lines', function () {
// const envVars = ParseEnvVars('BOB=COOL\nTHISIS$ANDINVALIDLINE\nANSWER=42 AND COUNTING\n')
// assert(Object.keys(envVars).length === 2)
// assert(envVars.BOB === 'COOL')
// assert(envVars.ANSWER === '42 AND COUNTING')
// })
// it('should default an empty value to an empty string', function () {
// const envVars = ParseEnvVars('EMPTY=\n')
// assert(envVars.EMPTY === '')
// })
// it('should escape double quoted values', function () {
// const envVars = ParseEnvVars('DOUBLE_QUOTES="double_quotes"\n')
// assert(envVars.DOUBLE_QUOTES === 'double_quotes')
// })
// it('should escape single quoted values', function () {
// const envVars = ParseEnvVars('SINGLE_QUOTES=\'single_quotes\'\n')
// assert(envVars.SINGLE_QUOTES === 'single_quotes')
// })
// it('should preserve embedded double quotes', function () {
// const envVars = ParseEnvVars('DOUBLE=""""\nDOUBLE_ONE=\'"double_one"\'\nDOUBLE_TWO=""double_two""\n')
// assert(envVars.DOUBLE === '""')
// assert(envVars.DOUBLE_ONE === '"double_one"')
// assert(envVars.DOUBLE_TWO === '"double_two"')
// })
// it('should preserve embedded single quotes', function () {
// const envVars = ParseEnvVars('SINGLE=\'\'\'\'\nSINGLE_ONE=\'\'single_one\'\'\nSINGLE_TWO="\'single_two\'"\n')
// assert(envVars.SINGLE === '\'\'')
// assert(envVars.SINGLE_ONE === '\'single_one\'')
// assert(envVars.SINGLE_TWO === '\'single_two\'')
// })
// it('should parse out all env vars ignoring spaces around = sign', function () {
// const envVars = ParseEnvVars('BOB = COOL\nNODE_ENV =dev\nANSWER= 42 AND COUNTING')
// assert(envVars.BOB === 'COOL')
// assert(envVars.NODE_ENV === 'dev')
// assert(envVars.ANSWER === '42 AND COUNTING')
// })
// it('should parse out all env vars ignoring spaces around = sign', function () {
// const envVars = ParseEnvVars('BOB = "COOL "\nNODE_ENV = dev\nANSWER= \' 42 AND COUNTING\'')
// assert(envVars.BOB === 'COOL ')
// assert(envVars.NODE_ENV === 'dev')
// assert(envVars.ANSWER === ' 42 AND COUNTING')
// })
// })
// describe('JSON and JS format support', function () {
// before(function () {
// this.readFileStub = sinon.stub(fs, 'readFileSync')
// proxyquire.noCallThru()
// })
// after(function () {
// this.readFileStub.restore()
// proxyquire.callThru()
// })
// afterEach(function () {
// spawnStub.resetHistory()
// })
// it('should parse env vars from JSON with node module loader if file extension is .json', function () {
// EnvCmd(['./test/.env.json', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// it('should parse env vars from JavaScript with node module loader if file extension is .js', function () {
// EnvCmd(['./test/.env.js', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// })
// describe('.RC file support (.env-cmdrc)', function () {
// before(function () {
// this.readFileStub = sinon.stub(fs, 'readFileSync')
// this.readFileStub.returns(`{
// "development": {
// "BOB": "COOL",
// "NODE_ENV": "dev",
// "ANSWER": "42",
// "TEST_CASES": true
// },
// "production": {
// "BOB": "COOL",
// "NODE_ENV": "prod",
// "ANSWER": "43"
// }
// }`)
// this.existsSyncStub = sinon.stub(fs, 'existsSync')
// this.existsSyncStub.returns(true)
// proxyquire.noCallThru()
// })
// after(function () {
// this.readFileStub.restore()
// this.existsSyncStub.restore()
// proxyquire.callThru()
// })
// afterEach(function () {
// spawnStub.resetHistory()
// })
// it('should parse env vars from .env-cmdrc file using development env', function () {
// EnvCmd(['development', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// it('should parse env vars from .env-cmdrc file using production env', function () {
// EnvCmd(['production', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
// assert(spawnStub.args[0][2].env.ANSWER === '43')
// })
// it('should throw error if env not in .rc file', function () {
// try {
// EnvCmd(['staging', 'echo', '$BOB'])
// assert(!'Should throw missing environment error.')
// } catch (e) {
// assert(e.message.includes('staging'))
// assert(e.message.includes(`.env-cmdrc`))
// }
// })
// it('should parse env vars from .env-cmdrc file using both development and production env', function () {
// EnvCmd(['development,production', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
// assert(spawnStub.args[0][2].env.ANSWER === '43')
// assert(spawnStub.args[0][2].env.TEST_CASES === true)
// })
// it('should parse env vars from .env-cmdrc file using both development and production env in reverse order', function () {
// EnvCmd(['production,development', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// assert(spawnStub.args[0][2].env.TEST_CASES === true)
// })
// it('should not fail if only one environment name exists', function () {
// EnvCmd(['production,test', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
// assert(spawnStub.args[0][2].env.ANSWER === '43')
// })
// it('should throw error if .rc file is not valid JSON', function () {
// this.readFileStub.returns(`{
// "development": {
// "BOB": "COOL",
// "NODE_ENV": "dev",
// "ANSWER": "42"
// },
// "production": {
// "BOB": 'COOL',
// "NODE_ENV": "prod",
// "ANSWER": "43"
// }
// }`)
// try {
// EnvCmd(['staging', 'echo', '$BOB'])
// assert(!'Should throw invalid JSON error.')
// } catch (e) {
// assert(e.message.includes(`.env-cmdrc`))
// assert(e.message.includes(`parse`))
// assert(e.message.includes(`JSON`))
// }
// })
// })
// describe('EnvCmd', function () {
// before(function () {
// this.readFileStub = sinon.stub(fs, 'readFileSync')
// })
// after(function () {
// this.readFileStub.restore()
// })
// afterEach(function () {
// spawnStub.resetHistory()
// this.readFileStub.resetHistory()
// process.removeAllListeners()
// })
// it('should spawn a new process with the env vars set', function () {
// this.readFileStub.returns('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
// EnvCmd(['./test/.env', 'echo', '$BOB'])
// assert(this.readFileStub.args[0][0] === path.join(process.cwd(), 'test/.env'))
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'COOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// it('should spawn a new process without overriding shell env vars', function () {
// process.env.NODE_ENV = 'development'
// process.env.BOB = 'SUPERCOOL'
// this.readFileStub.returns('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
// EnvCmd(['--no-override', './test/.env', 'echo', '$BOB'])
// assert(this.readFileStub.args[0][0] === path.join(process.cwd(), 'test/.env'))
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === 'SUPERCOOL')
// assert(spawnStub.args[0][2].env.NODE_ENV === 'development')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// it('should throw error if file and fallback does not exist with --fallback option', function () {
// this.readFileStub.restore()
// try {
// EnvCmd(['--fallback', './test/.non-existent-file', 'echo', '$BOB'])
// } catch (e) {
// const resolvedPath = path.join(process.cwd(), '.env')
// assert(e.message === `Error! Could not find fallback file or read env file at ${resolvedPath}`)
// return
// }
// assert(!'No exception thrown')
// })
// it('should execute successfully if no env file found', function () {
// this.readFileStub.restore()
// process.env.NODE_ENV = 'dev'
// process.env.ANSWER = '42'
// delete process.env.BOB
// EnvCmd(['./test/.non-existent-file', 'echo', '$BOB'])
// assert(spawnStub.args[0][0] === 'echo')
// assert(spawnStub.args[0][1][0] === '$BOB')
// assert(spawnStub.args[0][2].env.BOB === undefined)
// assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
// assert(spawnStub.args[0][2].env.ANSWER === '42')
// })
// })
// describe('PrintHelp', function () {
// it('should return help text when run', function () {
// const helpText = PrintHelp()
// assert(typeof helpText === 'string')
// assert(helpText.match(/Usage/g).length !== 0)
// assert(helpText.match(/env-cmd/).length !== 0)
// assert(helpText.match(/env_file/).length !== 0)
// assert(helpText.match(/env_name/).length !== 0)
// })
// })
// describe('ResolveEnvFilePath', function () {
// beforeEach(function () {
// this.cwdStub = sinon.stub(process, 'cwd')
// this.cwdStub.returns('/Users/hitchhikers-guide-to-the-galaxy/Thanks')
// })
// afterEach(function () {
// this.cwdStub.restore()
// })
// it('should add "fish.env" to the end of the current directory', function () {
// const abPath = ResolveEnvFilePath('fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/fish.env')
// })
// it('should add "./fish.env" to the end of the current directory', function () {
// const abPath = ResolveEnvFilePath('./fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/fish.env')
// })
// it('should add "../fish.env" to the end of the current directory', function () {
// const abPath = ResolveEnvFilePath('../fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/fish.env')
// })
// it('should add "for-all-the/fish.env" to the end of the current directory', function () {
// const abPath = ResolveEnvFilePath('for-all-the/fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/for-all-the/fish.env')
// })
// it('should set the absolute path to "/thanks/for-all-the/fish.env"', function () {
// const abPath = ResolveEnvFilePath('/thanks/for-all-the/fish.env')
// assert(abPath === '/thanks/for-all-the/fish.env')
// })
// it('should use "~" to add "fish.env" to the end of user directory', function () {
// const abPath = ResolveEnvFilePath('~/fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/fish.env')
// })
// it('should leave "~" in path if no user home directory found', function () {
// userHomeDir = ''
// const abPath = ResolveEnvFilePath('~/fish.env')
// assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/~/fish.env')
// })
// })
// })

49
test/parse-args.ts Normal file
View File

@ -0,0 +1,49 @@
// import * as assert from 'better-assert'
// import * as path from 'path'
// import * as proxyquire from 'proxyquire'
// import * as sinon from 'sinon'
// import * as fs from 'fs'
// import { parseArgs } from '../src/parse-args'
// let userHomeDir = '/Users/hitchhikers-guide-to-the-galaxy'
// const spawnStub = sinon.spy(() => ({
// on: sinon.stub(),
// exit: sinon.stub(),
// kill: sinon.stub()
// }))
// describe('env-cmd', function () {
// describe('parseArgs', function () {
// it('should parse out --no-override option ', function () {
// const parsedArgs = parseArgs(['--no-override', './test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.options.noOverride === true)
// })
// it('should parse out the envfile', function () {
// const parsedArgs = parseArgs(['-f', './test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.envValues === './test/envFile')
// })
// it('should parse out the command', function () {
// const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.command === 'command')
// })
// it('should parse out the command args', function () {
// const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
// assert(parsedArgs.commandArgs.length === 2)
// assert(parsedArgs.commandArgs[0] === 'cmda1')
// assert(parsedArgs.commandArgs[1] === 'cmda2')
// })
// it('should error out if incorrect number of args passed', function () {
// try {
// ParseArgs(['./test/envFile'])
// } catch (e) {
// assert(e.message === 'Error! Too few arguments passed to env-cmd.')
// return
// }
// assert(!'No exception thrown')
// })
// })
// })

128
test/signal-termination.ts Normal file
View File

@ -0,0 +1,128 @@
import * as assert from 'better-assert'
import * as sinon from 'sinon'
import {
handleUncaughtExceptions, terminateSpawnedProc, terminateParentProcess
} from '../src/signal-termination'
describe('signal-termination', () => {
describe('handleUncaughtExceptions', function () {
beforeEach(function () {
this.logStub = sinon.stub(console, 'log')
this.processStub = sinon.stub(process, 'exit')
})
afterEach(function () {
this.logStub.restore()
this.processStub.restore()
})
it('should print help text and error if error contains \'passed\'', function () {
handleUncaughtExceptions(new Error('print help text passed now'))
assert(this.logStub.calledTwice)
this.logStub.restore() // restore here so test success logs get printed
})
it('should print just there error if error does not contain \'passed\'', function () {
handleUncaughtExceptions(new Error('do not print help text now'))
assert(this.logStub.calledOnce)
this.logStub.restore() // restore here so test success logs get printed
})
})
describe('terminateSpawnedProc', function () {
beforeEach(function () {
this.procStub = sinon.stub()
this.exitStub = sinon.stub(process, 'exit')
this.killStub = sinon.stub(process, 'kill')
this.proc = {
kill: this.procStub
}
this.exitCalled = false
})
afterEach(function () {
this.exitStub.restore()
this.killStub.restore()
})
it('should call kill method on spawned process', function () {
terminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 1)
})
it('should not call kill method more than once', function () {
terminateSpawnedProc.call(this, this.proc)
terminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 1)
})
it('should not call kill method if the spawn process is already dying', function () {
this.exitCalled = true
terminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 0)
})
it('should call kill method on spawned process with correct signal', function () {
terminateSpawnedProc.call(this, this.proc, 'SIGINT')
assert(this.procStub.callCount === 1)
assert(this.procStub.args[0][0] === 'SIGINT')
})
it('should call kill method on parent process with correct signal', function () {
terminateSpawnedProc.call(this, this.proc, 'SIGINT')
assert(this.exitStub.callCount === 0)
assert(this.killStub.callCount === 1)
assert(this.killStub.args[0][1] === 'SIGINT')
})
it('should call exit method on parent process with correct exit code', function () {
terminateSpawnedProc.call(this, this.proc, 'SIGINT', 1)
assert(this.killStub.callCount === 0)
assert(this.exitStub.callCount === 1)
assert(this.exitStub.args[0][0] === 1)
})
})
describe('terminateParentProcess', function () {
beforeEach(function () {
this.exitStub = sinon.stub(process, 'exit')
this.killStub = sinon.stub(process, 'kill')
this.exitCalled = false
})
afterEach(function () {
this.exitStub.restore()
this.killStub.restore()
})
it('should call exit method on parent process', function () {
terminateParentProcess.call(this)
assert(this.exitStub.callCount === 1)
})
it('should not call exit method more than once', function () {
terminateParentProcess.call(this)
terminateParentProcess.call(this)
assert(this.exitStub.callCount === 1)
})
it('should not call exit method if the process is already dying', function () {
this.exitCalled = true
terminateParentProcess.call(this)
assert(this.exitStub.callCount === 0)
})
it('should call exit method with correct error code', function () {
terminateParentProcess.call(this, 0)
assert(this.killStub.callCount === 0)
assert(this.exitStub.callCount === 1)
assert(this.exitStub.args[0][0] === 0)
})
it('should call kill method with correct kill signal', function () {
terminateParentProcess.call(this, null, 'SIGINT')
assert(this.killStub.callCount === 1)
assert(this.exitStub.callCount === 0)
assert(this.killStub.args[0][1] === 'SIGINT')
})
})
})

View File

@ -1,548 +0,0 @@
'use strict'
const assert = require('better-assert')
const describe = require('mocha').describe
const it = require('mocha').it
const afterEach = require('mocha').afterEach
const beforeEach = require('mocha').beforeEach
const before = require('mocha').before
const after = require('mocha').after
const path = require('path')
const proxyquire = require('proxyquire')
const sinon = require('sinon')
const fs = require('fs')
let userHomeDir = '/Users/hitchhikers-guide-to-the-galaxy'
const spawnStub = sinon.spy(() => ({
on: sinon.stub(),
exit: sinon.stub(),
kill: sinon.stub()
}))
const lib = proxyquire('../lib', {
'cross-spawn': {
spawn: spawnStub
},
[path.resolve(process.cwd(), 'test/.env.json')]: {
BOB: 'COOL',
NODE_ENV: 'dev',
ANSWER: '42'
},
[path.resolve(process.cwd(), 'test/.env.js')]: {
BOB: 'COOL',
NODE_ENV: 'dev',
ANSWER: '42'
},
'os': {
homedir: () => userHomeDir
}
})
const EnvCmd = lib.EnvCmd
const ParseArgs = lib.ParseArgs
const ParseEnvString = lib.ParseEnvString
const PrintHelp = lib.PrintHelp
const HandleUncaughtExceptions = lib.HandleUncaughtExceptions
const StripComments = lib.StripComments
const StripEmptyLines = lib.StripEmptyLines
const ParseEnvVars = lib.ParseEnvVars
const ResolveEnvFilePath = lib.ResolveEnvFilePath
const TerminateSpawnedProc = lib.TerminateSpawnedProc
const TerminateParentProcess = lib.TerminateParentProcess
describe('env-cmd', function () {
describe('ParseArgs', function () {
it('should parse out --no-override option ', function () {
const parsedArgs = ParseArgs(['--no-override', './test/envFile', 'command', 'cmda1', 'cmda2'])
assert(parsedArgs.noOverride === true)
})
it('should parse out the envfile', function () {
const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
assert(parsedArgs.envFile === './test/envFile')
})
it('should parse out the command', function () {
const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
assert(parsedArgs.command === 'command')
})
it('should parse out the command args', function () {
const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2'])
assert(parsedArgs.commandArgs.length === 2)
assert(parsedArgs.commandArgs[0] === 'cmda1')
assert(parsedArgs.commandArgs[1] === 'cmda2')
})
it('should error out if incorrect number of args passed', function () {
try {
ParseArgs(['./test/envFile'])
} catch (e) {
assert(e.message === 'Error! Too few arguments passed to env-cmd.')
return
}
assert(!'No exception thrown')
})
})
describe('ParseEnvString', function () {
it('should parse env vars and merge (overwrite) with process.env vars', function () {
const env = ParseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
assert(env.BOB === 'COOL')
assert(env.NODE_ENV === 'dev')
assert(env.ANSWER === '42')
})
})
describe('StripComments', function () {
it('should strip out all full line comments', function () {
const envString = StripComments('#BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n#AnotherComment\n')
assert(envString === '\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n\n')
})
})
describe('StripEmptyLines', function () {
it('should strip out all empty lines', function () {
const envString = StripEmptyLines('\nBOB=COOL\n\nNODE_ENV=dev\n\nANSWER=42 AND COUNTING\n\n')
assert(envString === 'BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n')
})
})
describe('ParseEnvVars', function () {
it('should parse out all env vars in string when not ending with \'\\n\'', function () {
const envVars = ParseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING')
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
})
it('should parse out all env vars in string with format \'key=value\'', function () {
const envVars = ParseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n')
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
})
it('should ignore invalid lines', function () {
const envVars = ParseEnvVars('BOB=COOL\nTHISIS$ANDINVALIDLINE\nANSWER=42 AND COUNTING\n')
assert(Object.keys(envVars).length === 2)
assert(envVars.BOB === 'COOL')
assert(envVars.ANSWER === '42 AND COUNTING')
})
it('should default an empty value to an empty string', function () {
const envVars = ParseEnvVars('EMPTY=\n')
assert(envVars.EMPTY === '')
})
it('should escape double quoted values', function () {
const envVars = ParseEnvVars('DOUBLE_QUOTES="double_quotes"\n')
assert(envVars.DOUBLE_QUOTES === 'double_quotes')
})
it('should escape single quoted values', function () {
const envVars = ParseEnvVars('SINGLE_QUOTES=\'single_quotes\'\n')
assert(envVars.SINGLE_QUOTES === 'single_quotes')
})
it('should preserve embedded double quotes', function () {
const envVars = ParseEnvVars('DOUBLE=""""\nDOUBLE_ONE=\'"double_one"\'\nDOUBLE_TWO=""double_two""\n')
assert(envVars.DOUBLE === '""')
assert(envVars.DOUBLE_ONE === '"double_one"')
assert(envVars.DOUBLE_TWO === '"double_two"')
})
it('should preserve embedded single quotes', function () {
const envVars = ParseEnvVars('SINGLE=\'\'\'\'\nSINGLE_ONE=\'\'single_one\'\'\nSINGLE_TWO="\'single_two\'"\n')
assert(envVars.SINGLE === '\'\'')
assert(envVars.SINGLE_ONE === '\'single_one\'')
assert(envVars.SINGLE_TWO === '\'single_two\'')
})
it('should parse out all env vars ignoring spaces around = sign', function () {
const envVars = ParseEnvVars('BOB = COOL\nNODE_ENV =dev\nANSWER= 42 AND COUNTING')
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
})
it('should parse out all env vars ignoring spaces around = sign', function () {
const envVars = ParseEnvVars('BOB = "COOL "\nNODE_ENV = dev\nANSWER= \' 42 AND COUNTING\'')
assert(envVars.BOB === 'COOL ')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === ' 42 AND COUNTING')
})
})
describe('JSON and JS format support', function () {
before(function () {
this.readFileStub = sinon.stub(fs, 'readFileSync')
proxyquire.noCallThru()
})
after(function () {
this.readFileStub.restore()
proxyquire.callThru()
})
afterEach(function () {
spawnStub.resetHistory()
})
it('should parse env vars from JSON with node module loader if file extension is .json', function () {
EnvCmd(['./test/.env.json', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
it('should parse env vars from JavaScript with node module loader if file extension is .js', function () {
EnvCmd(['./test/.env.js', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
})
describe('.RC file support (.env-cmdrc)', function () {
before(function () {
this.readFileStub = sinon.stub(fs, 'readFileSync')
this.readFileStub.returns(`{
"development": {
"BOB": "COOL",
"NODE_ENV": "dev",
"ANSWER": "42",
"TEST_CASES": true
},
"production": {
"BOB": "COOL",
"NODE_ENV": "prod",
"ANSWER": "43"
}
}`)
this.existsSyncStub = sinon.stub(fs, 'existsSync')
this.existsSyncStub.returns(true)
proxyquire.noCallThru()
})
after(function () {
this.readFileStub.restore()
this.existsSyncStub.restore()
proxyquire.callThru()
})
afterEach(function () {
spawnStub.resetHistory()
})
it('should parse env vars from .env-cmdrc file using development env', function () {
EnvCmd(['development', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
it('should parse env vars from .env-cmdrc file using production env', function () {
EnvCmd(['production', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
assert(spawnStub.args[0][2].env.ANSWER === '43')
})
it('should throw error if env not in .rc file', function () {
try {
EnvCmd(['staging', 'echo', '$BOB'])
assert(!'Should throw missing environment error.')
} catch (e) {
assert(e.message.includes('staging'))
assert(e.message.includes(`.env-cmdrc`))
}
})
it('should parse env vars from .env-cmdrc file using both development and production env', function () {
EnvCmd(['development,production', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
assert(spawnStub.args[0][2].env.ANSWER === '43')
assert(spawnStub.args[0][2].env.TEST_CASES === true)
})
it('should parse env vars from .env-cmdrc file using both development and production env in reverse order', function () {
EnvCmd(['production,development', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
assert(spawnStub.args[0][2].env.TEST_CASES === true)
})
it('should not fail if only one environment name exists', function () {
EnvCmd(['production,test', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'prod')
assert(spawnStub.args[0][2].env.ANSWER === '43')
})
it('should throw error if .rc file is not valid JSON', function () {
this.readFileStub.returns(`{
"development": {
"BOB": "COOL",
"NODE_ENV": "dev",
"ANSWER": "42"
},
"production": {
"BOB": 'COOL',
"NODE_ENV": "prod",
"ANSWER": "43"
}
}`)
try {
EnvCmd(['staging', 'echo', '$BOB'])
assert(!'Should throw invalid JSON error.')
} catch (e) {
assert(e.message.includes(`.env-cmdrc`))
assert(e.message.includes(`parse`))
assert(e.message.includes(`JSON`))
}
})
})
describe('EnvCmd', function () {
before(function () {
this.readFileStub = sinon.stub(fs, 'readFileSync')
})
after(function () {
this.readFileStub.restore()
})
afterEach(function () {
spawnStub.resetHistory()
this.readFileStub.resetHistory()
process.removeAllListeners()
})
it('should spawn a new process with the env vars set', function () {
this.readFileStub.returns('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
EnvCmd(['./test/.env', 'echo', '$BOB'])
assert(this.readFileStub.args[0][0] === path.join(process.cwd(), 'test/.env'))
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'COOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
it('should spawn a new process without overriding shell env vars', function () {
process.env.NODE_ENV = 'development'
process.env.BOB = 'SUPERCOOL'
this.readFileStub.returns('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
EnvCmd(['--no-override', './test/.env', 'echo', '$BOB'])
assert(this.readFileStub.args[0][0] === path.join(process.cwd(), 'test/.env'))
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === 'SUPERCOOL')
assert(spawnStub.args[0][2].env.NODE_ENV === 'development')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
it('should throw error if file and fallback does not exist with --fallback option', function () {
this.readFileStub.restore()
try {
EnvCmd(['--fallback', './test/.non-existent-file', 'echo', '$BOB'])
} catch (e) {
const resolvedPath = path.join(process.cwd(), '.env')
assert(e.message === `Error! Could not find fallback file or read env file at ${resolvedPath}`)
return
}
assert(!'No exception thrown')
})
it('should execute successfully if no env file found', function () {
this.readFileStub.restore()
process.env.NODE_ENV = 'dev'
process.env.ANSWER = '42'
delete process.env.BOB
EnvCmd(['./test/.non-existent-file', 'echo', '$BOB'])
assert(spawnStub.args[0][0] === 'echo')
assert(spawnStub.args[0][1][0] === '$BOB')
assert(spawnStub.args[0][2].env.BOB === undefined)
assert(spawnStub.args[0][2].env.NODE_ENV === 'dev')
assert(spawnStub.args[0][2].env.ANSWER === '42')
})
})
describe('PrintHelp', function () {
it('should return help text when run', function () {
const helpText = PrintHelp()
assert(typeof helpText === 'string')
assert(helpText.match(/Usage/g).length !== 0)
assert(helpText.match(/env-cmd/).length !== 0)
assert(helpText.match(/env_file/).length !== 0)
assert(helpText.match(/env_name/).length !== 0)
})
})
describe('HandleUncaughtExceptions', function () {
beforeEach(function () {
this.logStub = sinon.stub(console, 'log')
this.processStub = sinon.stub(process, 'exit')
})
afterEach(function () {
this.logStub.restore()
this.processStub.restore()
})
it('should print help text and error if error contains \'passed\'', function () {
HandleUncaughtExceptions(new Error('print help text passed now'))
assert(this.logStub.calledTwice)
this.logStub.restore() // restore here so test success logs get printed
})
it('should print just there error if error does not contain \'passed\'', function () {
HandleUncaughtExceptions(new Error('do not print help text now'))
assert(this.logStub.calledOnce)
this.logStub.restore() // restore here so test success logs get printed
})
})
describe('ResolveEnvFilePath', function () {
beforeEach(function () {
this.cwdStub = sinon.stub(process, 'cwd')
this.cwdStub.returns('/Users/hitchhikers-guide-to-the-galaxy/Thanks')
})
afterEach(function () {
this.cwdStub.restore()
})
it('should add "fish.env" to the end of the current directory', function () {
const abPath = ResolveEnvFilePath('fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/fish.env')
})
it('should add "./fish.env" to the end of the current directory', function () {
const abPath = ResolveEnvFilePath('./fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/fish.env')
})
it('should add "../fish.env" to the end of the current directory', function () {
const abPath = ResolveEnvFilePath('../fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/fish.env')
})
it('should add "for-all-the/fish.env" to the end of the current directory', function () {
const abPath = ResolveEnvFilePath('for-all-the/fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/for-all-the/fish.env')
})
it('should set the absolute path to "/thanks/for-all-the/fish.env"', function () {
const abPath = ResolveEnvFilePath('/thanks/for-all-the/fish.env')
assert(abPath === '/thanks/for-all-the/fish.env')
})
it('should use "~" to add "fish.env" to the end of user directory', function () {
const abPath = ResolveEnvFilePath('~/fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/fish.env')
})
it('should leave "~" in path if no user home directory found', function () {
userHomeDir = ''
const abPath = ResolveEnvFilePath('~/fish.env')
assert(abPath === '/Users/hitchhikers-guide-to-the-galaxy/Thanks/~/fish.env')
})
})
describe('TerminateSpawnedProc', function () {
beforeEach(function () {
this.procStub = sinon.stub()
this.exitStub = sinon.stub(process, 'exit')
this.killStub = sinon.stub(process, 'kill')
this.proc = {
kill: this.procStub
}
this.exitCalled = false
})
afterEach(function () {
this.exitStub.restore()
this.killStub.restore()
})
it('should call kill method on spawned process', function () {
TerminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 1)
})
it('should not call kill method more than once', function () {
TerminateSpawnedProc.call(this, this.proc)
TerminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 1)
})
it('should not call kill method if the spawn process is already dying', function () {
this.exitCalled = true
TerminateSpawnedProc.call(this, this.proc)
assert(this.procStub.callCount === 0)
})
it('should call kill method on spawned process with correct signal', function () {
TerminateSpawnedProc.call(this, this.proc, 'SIGINT')
assert(this.procStub.callCount === 1)
assert(this.procStub.args[0][0] === 'SIGINT')
})
it('should call kill method on parent process with correct signal', function () {
TerminateSpawnedProc.call(this, this.proc, 'SIGINT')
assert(this.exitStub.callCount === 0)
assert(this.killStub.callCount === 1)
assert(this.killStub.args[0][1] === 'SIGINT')
})
it('should call exit method on parent process with correct exit code', function () {
TerminateSpawnedProc.call(this, this.proc, 'SIGINT', 1)
assert(this.killStub.callCount === 0)
assert(this.exitStub.callCount === 1)
assert(this.exitStub.args[0][0] === 1)
})
})
describe('TerminateParentProcess', function () {
beforeEach(function () {
this.exitStub = sinon.stub(process, 'exit')
this.killStub = sinon.stub(process, 'kill')
this.exitCalled = false
})
afterEach(function () {
this.exitStub.restore()
this.killStub.restore()
})
it('should call exit method on parent process', function () {
TerminateParentProcess.call(this)
assert(this.exitStub.callCount === 1)
})
it('should not call exit method more than once', function () {
TerminateParentProcess.call(this)
TerminateParentProcess.call(this)
assert(this.exitStub.callCount === 1)
})
it('should not call exit method if the process is already dying', function () {
this.exitCalled = true
TerminateParentProcess.call(this)
assert(this.exitStub.callCount === 0)
})
it('should call exit method with correct error code', function () {
TerminateParentProcess.call(this, 0)
assert(this.killStub.callCount === 0)
assert(this.exitStub.callCount === 1)
assert(this.exitStub.args[0][0] === 0)
})
it('should call kill method with correct kill signal', function () {
TerminateParentProcess.call(this, null, 'SIGINT')
assert(this.killStub.callCount === 1)
assert(this.exitStub.callCount === 0)
assert(this.killStub.args[0][1] === 'SIGINT')
})
})
})

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "./lib",
"allowJs": true,
"target": "es5",
"strict": true,
"lib": [
"es2015",
"es2016",
"es2017",
"es2018"
]
},
"include": [
"./src/**/*"
]
}