UNPKG

cbf

Version:

A package for creating scripts to store and run your most commonly used CLI commands for a repo or just in general

561 lines (521 loc) 16.8 kB
#!/usr/bin/env node const os = require('os'); const path = require('path'); const fse = require('fs-extra'); const chalk = require('chalk'); const yaml = require('yamljs'); const { printMessage, formatMessage } = require('formatted-messages'); const isString = require('lodash/isString'); const isEmpty = require('lodash/isEmpty'); const isPlainObject = require('lodash/isPlainObject'); const { Env } = require('../constants'); const globalMessages = require('../messages'); const { ScriptTypes, SCRIPTS_DIRECTORY_PATH, PATH_TO_PACKAGE_JSON, BACK_COMMAND, QUIT_COMMAND, CHOICE_DOCUMENTATION, KEY_SEPARATOR, SIMPLE_SCRIPT_OPTION_SEPARATOR, JSON_SPACES_FORMATTING, } = require('../constants'); const { prompts } = require('../shims/inquirer'); const messages = require('./messages'); /** * Returns true if variable is an empty string * * @param {*} variable - a variable to be tested to see if it is an empty string * * @returns {boolean} isEmptyString - true if the variable passed was an empty string and false otherwise */ const isEmptyString = variable => isString(variable) && isEmpty(variable); /** * Listens for uncaught exceptions and prints the error to the console and exits */ const uncaughtExceptionListener = () => { process.on('uncaughtException', error => { if (process.env.NODE_ENV === Env.TEST) { // eslint-disable-next-line no-console console.error('Uncaught Exception thrown\n', error); } else { printMessage(formatMessage(globalMessages.unknownError)); } process.exit(1); }); }; /** * Listens for unhandled rejections and prints the error to the console and exits */ const unhandledRejectionListener = () => { process.on('unhandledRejection', (reason, p) => { if (process.env.NODE_ENV === Env.TEST) { // eslint-disable-next-line no-console console.error('Unhandled Rejection at promise\n', p, reason); } else { printMessage(formatMessage(globalMessages.unknownError)); } process.exit(1); }); }; /** * Cleans up and then exits program */ const safeExit = () => { prompts.complete(); }; /** * Force exits program */ const forceExit = () => { prompts.complete(); process.exit(); }; /** * Throw an error with an optional error message * * @param {string} errorMessage - an error message to display to the user when throwing the error */ const throwError = (errorMessage = '') => { if (errorMessage) { throw new Error(errorMessage); } throw new Error('Unknown error'); }; /** * Return true if string contains any whitespace and false otherwise * * @param {string} string - string to check for whitespace * * @returns {boolean} endsWithWhitespace - true if string contained white space; false otherwise */ const endsWithWhitespace = string => string !== string.trim(); /** * Replace the whitespace with the provided character and return string * * @param {string} string - string to replace whitespace in * @param {string} delimiter - string to replace whitespace with * * @returns {string} newString - string without whitespace */ const replaceWhitespace = (string, delimiter) => { const newString = `${string}`; return newString.replace(/\s+/g, delimiter); }; /** * Return the first key in an object * * @param {object} object - the object to get the first key from * * @returns {string} firstKey - the first key encountered */ const getFirstKey = object => { const keys = Object.keys(object); return keys[0] ? keys[0] : null; }; /** * Return the name of the key (which is just the last word after the last period) * * @param {string} key - key to use to return the name from * * @returns {string} name - the name of the key */ const getNameFromKey = key => key.split(KEY_SEPARATOR).pop(); /** * Return the name of the simple script key (which is just the last word after the last colon) * * @param {string} key - key to use to return the name from * * @returns {string} name - the name of the key */ const getNameFromSimpleScriptKey = key => key.split(SIMPLE_SCRIPT_OPTION_SEPARATOR).pop(); /** * Return the key of the parent (the key is everything before the last occurrence of a period) * * @param {string} key - key to use to return the parent key from * * @returns {string} parentKey - the key of the parent */ const getParentKey = key => key.substr(0, key.lastIndexOf(KEY_SEPARATOR)); /** * Returns true if is an option and a command and false otherwise * * @param {object} param - object parameter * @param {string} param.key - key of an option, command or both * @param {string[]} param.keys - keys of options and commands * * @returns {boolean} isAnOptionAndCommand - true if is an option and a command and false otherwise */ const isAnOptionAndCommand = ({ key, keys }) => { return keys.some(k => k.indexOf(`${key}${SIMPLE_SCRIPT_OPTION_SEPARATOR}`) === 0); }; /** * Gets the option keys from a complete key * * @param {string} key - complete key to parse for option keys * * @returns {string[]} optionKeys - options keys parse from complete key */ const getOptionsKeysFromKey = key => { const optionKeys = []; let partialKey = key; const re = new RegExp(SIMPLE_SCRIPT_OPTION_SEPARATOR, 'g'); let optionKey = partialKey .substring(0, partialKey.lastIndexOf(SIMPLE_SCRIPT_OPTION_SEPARATOR)) .replace(re, KEY_SEPARATOR); while (!isEmptyString(optionKey)) { optionKeys.push(optionKey); partialKey = partialKey.substring(0, partialKey.lastIndexOf(SIMPLE_SCRIPT_OPTION_SEPARATOR)); optionKey = partialKey .substring(0, partialKey.lastIndexOf(SIMPLE_SCRIPT_OPTION_SEPARATOR)) .replace(re, KEY_SEPARATOR); } return optionKeys; }; /** * Returns true if script is a simple script and false otherwise * * @param {string} fileName - name of file to check if is simple * * @returns {boolean} isSimpleScript - true if script is simple and false otherwise */ const isSimpleScript = fileName => fileName.split('.').includes(ScriptTypes.SIMPLE); /** * Returns true if script is a advanced script and false otherwise * * @param {string} fileName - name of file to check if is simple * * @returns {boolean} isAdvancedScript - true if script is advanced and false otherwise */ const isAdvancedScript = fileName => fileName.split('.').includes(ScriptTypes.ADVANCED); /** * Checks the file name for `.simple` to determine whether or not it is a simple or advanced script * * @param {string} fileName - file name used to check if script is a simple or advanced script * * @returns {string} scriptType - the scripts type */ const getScriptType = fileName => isSimpleScript(fileName) ? ScriptTypes.SIMPLE : ScriptTypes.ADVANCED; /** * Check if a file path ends in a valid .yaml file name * * @param {string} fileName - a yaml file name to validate * * @returns {boolean} isValidYamlFileName - true if file is a valid yaml file path */ const isValidYamlFileName = fileName => /.*\.yml/.test(fileName); /** * Load a yaml file into the program * * @param {string} yamlFileName - name of yaml file to load into memory * * @returns {object} yamlFile - yaml file to be loaded into memory */ const loadYamlFile = yamlFileName => { let yamlFile; try { yamlFile = yaml.load(yamlFileName); } catch (exception) { printMessage( formatMessage(messages.errorLoadingYamlFile, { yamlFileName, exception, }), ); forceExit(); } return yamlFile; }; /** * Save a yaml file * * @param {string} yamlFileName - name of the yaml file to save * @param {object} yamlFile - yaml file to be saved * * @returns {Promise} yamlFileSaved - a promise that the yaml file has been saved */ const saveYamlFile = (yamlFileName, yamlFile) => { return new Promise((resolve, reject) => { try { const name = getFirstKey(yamlFile); const filePath = `${SCRIPTS_DIRECTORY_PATH}/${name}${ isSimpleScript(yamlFileName) ? '.simple' : '' }.yml`; const yamlString = yaml.stringify(yamlFile, 10, 2); fse.outputFileSync(filePath, yamlString); resolve(); } catch (exception) { reject(exception); } }); }; /** * Delete a yaml file * * @param {string} yamlFileName - name of yaml file to be deleted * * @returns {Promise} yamlFileDeletedPromise - a promise that the yaml file has been deleted */ const deleteYamlFile = yamlFileName => { return new Promise((resolve, reject) => { try { fse.removeSync(yamlFileName); resolve(); } catch (exception) { reject(exception); } }); }; /** * Check if a file path ends in a valid json file name * * @param {string} fileName - a json file name to validate * * @returns {boolean} isValidJsonFileName - true if file is a valid json file path */ const isValidJsonFileName = fileName => /.*\.json/.test(fileName); /** * Save a json file * * @param {string} jsonFileName - name of json file to be saved * @param {object} jsonFile - json object to be saved to file * * @returns {Promise} jsonFileSaved - a promise that the json file has been saved */ const saveJsonFile = (jsonFileName, jsonFile) => { return new Promise((resolve, reject) => { try { const name = getFirstKey(jsonFile); const filePath = `${SCRIPTS_DIRECTORY_PATH}/${name}${ isSimpleScript(jsonFileName) ? '.simple' : '' }.json`; fse.outputJsonSync(filePath, jsonFile, { spaces: JSON_SPACES_FORMATTING, }); resolve(); } catch (exception) { reject(exception); } }); }; /** * Delete a json file * * @param {string} jsonFileName - name of the json file to be deleted * * @returns {Promise} jsonFileDeleted - a promise that the json file has been deleted */ const deleteJsonFile = jsonFileName => { return new Promise((resolve, reject) => { try { fse.removeSync(jsonFileName); resolve(); } catch (exception) { reject(exception); } }); }; /** * Load a json file into the program * * @param {string} jsonFileName - name of json file to load into memory * * @returns {object} jsonFile - json file */ const loadJsonFile = jsonFileName => { let jsonFile; try { jsonFile = fse.readJsonSync(jsonFileName); } catch (exception) { printMessage( formatMessage(messages.errorLoadingJsonFile, { jsonFileName, exception, }), ); forceExit(); } return jsonFile; }; /** * Returns true if object only has string values * * @param {object} obj - object to check properties are all strings * * @returns {boolean} valuesInKeyValuePairAreAllStrings - true if all properties are strings */ const valuesInKeyValuePairAreAllStrings = obj => Object.values(obj).every(value => isString(value)); /** * Returns true if the object is a valid variables object shape * * @param {object} variables - variables to be validated * * @returns {boolean} isValid - true if variables is a valid shape; false otherwise */ const isValidVariablesShape = variables => isPlainObject(variables) && valuesInKeyValuePairAreAllStrings(variables); /** * Return choice with command directive stripped * * @param {string} documentedChoice - documented choice to be undocumented * * @returns {string} undocumentedChoice - choice with documented command directive stripped */ const getUndocumentedChoice = documentedChoice => { let undocumentedChoice = documentedChoice; CHOICE_DOCUMENTATION.forEach(choiceDocumentation => { if (documentedChoice.indexOf(choiceDocumentation) !== -1) { [undocumentedChoice] = documentedChoice.split(` ${choiceDocumentation}`); } }); return undocumentedChoice; }; /** * Returns undocumented choices * * @param {string[]} documentedChoices - a list of documented choices to become undocumented * @returns {string[]} undocumentedChoices - a list of undocumented choices */ const getUndocumentedChoices = documentedChoices => documentedChoices.map(documentedChoice => getUndocumentedChoice(documentedChoice)); /** * Return choices with command directives appended to commands * * @param {object} param - object parameter * @param {Script} param.script - script to lookup options and commands * @param {string} param.optionKey - key of the option having it's choices documented * @param {string} param.choice - choice to be documented * @param {boolean} param.documented - is in documented mode * @param {number} param.index - index of choice to be documented; * * @returns {string} documentedChoice - choice with command directives appended to commands */ const getDocumentedChoice = ({ script, optionKey, choice, documented, index }) => { if (!script) { const scriptDocumentationSymbols = ['♚', '♛', '♜', '♝', '♞', '♟']; // No script has been passed must be in menu, decorate choice as a script return `${choice} ${chalk.magenta.bold( scriptDocumentationSymbols[index % scriptDocumentationSymbols.length], )}`; } const commandKey = `${optionKey}.${choice}`; const command = script.getCommand(commandKey); if (documented && command) { const directives = command.getDirectives(); if (directives.length === 1) { return `${choice} ${chalk.blue.bold('→')} ${chalk.green.bold(directives[0])}`; } return `${choice} ${chalk.blue.bold('→')} ${chalk.green.bold(directives[0])} . . .`; } if (choice.indexOf(BACK_COMMAND) !== -1 || choice.indexOf(QUIT_COMMAND) !== -1) { return choice; } if (!command) { return `${choice} ${chalk.blue.bold('↓')}`; } return choice; }; /** * Return choices with command directives appended to commands * * @param {object} param - object parameter * @param {Script} param.script - script to lookup options and commands * @param {string} param.optionKey - key of the option having it's choices documented * @param {string[]} param.choices - choices to be documented * @param {boolean} param.documented - is in documented mode * * @returns {string[]} documentedChoices - choices with command directives appended to commands */ const getDocumentedChoices = ({ script = undefined, optionKey = '', choices = [], documented = false, }) => choices.map((choice, index) => getDocumentedChoice({ script, optionKey, choice, documented, index }), ); /** * Returns true if params length is valid and false otherwise * * @param {object} param - object parameter * @param {number} param.actual - actual param length * @param {number} param.exact - exact param length expected * @param {number} param.min - minimum param length expected * @param {number} param.max - maximum param length expected * * @returns {boolean} validLength - true if param length is valid; false otherwise */ const isValidParametersLength = ({ actual, min = undefined, max = undefined, exact = undefined, }) => { let validLength = true; if (typeof exact !== 'undefined' && exact !== actual) { validLength = false; } if (typeof min !== 'undefined' && min > actual) { validLength = false; } if (typeof max !== 'undefined' && max < actual) { validLength = false; } return validLength; }; /** * Returns true if a package.json file exists * * @returns {boolean} hasPackageJsonFile - true if a package.json file exists */ const hasPackageJsonFile = () => fse.pathExistsSync(PATH_TO_PACKAGE_JSON); /** * If path is a relative path; resolve it and return absolute path * * @param {string} relativePath - a relative path to be converted to an absolute path * * @returns {string} absolutePath - an absolute path converted from the relative path */ const absolutePath = relativePath => { if (relativePath[0] === '~') { return path.resolve(path.join(os.homedir(), relativePath.slice(1))); } return relativePath; }; module.exports = { isEmptyString, uncaughtExceptionListener, unhandledRejectionListener, absolutePath, safeExit, forceExit, isValidParametersLength, getUndocumentedChoices, getUndocumentedChoice, getDocumentedChoices, endsWithWhitespace, replaceWhitespace, isValidYamlFileName, saveYamlFile, loadYamlFile, deleteYamlFile, isValidJsonFileName, saveJsonFile, loadJsonFile, deleteJsonFile, getFirstKey, getNameFromKey, getNameFromSimpleScriptKey, getParentKey, isAnOptionAndCommand, getOptionsKeysFromKey, throwError, isValidVariablesShape, isSimpleScript, isAdvancedScript, getScriptType, hasPackageJsonFile, };