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

449 lines (419 loc) 14.1 kB
#!/usr/bin/env node const isString = require('lodash/isString'); const { printMessage, formatMessage } = require('formatted-messages'); const globalMessages = require('../messages'); const { CurrentOperatingModes } = require('../operating-modes'); const { ScriptKeys, ScriptTypes, OperatingModes, BACK_COMMAND, QUIT_COMMAND, KEY_SEPARATOR, SIMPLE_SCRIPT_OPTION_SEPARATOR, } = require('../constants'); const { Script, Option, Command, Directory } = require('../config/script'); const { isEmptyString, getFirstKey, getNameFromKey, isValidYamlFileName, isValidJsonFileName, loadYamlFile, loadJsonFile, isValidVariablesShape, getUndocumentedChoices, getOptionsKeysFromKey, getNameFromSimpleScriptKey, isAnOptionAndCommand, getScriptType, forceExit, } = require('../utility'); const messages = require('./messages'); /** * Add a directory to the script * * @param {object} param - object parameter * @param {Script} param.script - the script to add the directory to * @param {object} param.file - the file to get the directory from to add to the script * @param {string} param.key - the current key of file to get the directory from to add to the script */ const addDirectoryToAdvancedScript = ({ script, file, key }) => { const directory = new Directory(file.directory); script.updateDirectory({ directoryKey: key, directory, }); }; /** * Add a command to the script * * @param {object} param - object parameter * @param {Script} param.script - the script to add a command to * @param {object} param.file - the file that contains the command to be added to the script * @param {string} param.fileName - the name of the file with the command to add into the script * @param {string} param.key - the current key of the file to get the command from */ const addCommandToAdvancedScript = ({ script, file, fileName, key }) => { const directives = []; if (isString(file.command)) { directives.push(file.command); } else { let line = 1; while (line in file.command) { directives.push(file.command[line]); line += 1; } } const command = new Command({ directives, }); if (ScriptKeys.MESSAGE in file) { command.updateMessage(file.message); } if (ScriptKeys.VARIABLES in file) { if (isValidVariablesShape(file.variables)) { command.updateVariables(file.variables); } else { const error = formatMessage(messages.incorrectlyFormattedVariables); const message = isValidYamlFileName(fileName) ? formatMessage(messages.errorParsingYamlFile, { yamlFileName: fileName, error, }) : formatMessage(messages.errorParsingJsonFile, { jsonFileName: fileName, error, }); printMessage(message); forceExit(); } } script.updateCommand({ commandKey: key, command, }); }; /** * Throws an error and stops parsing an advanced script if a script key is used as an option * * @param {object} param - object parameter * @param {string} param.fileName - name of file being parsed * @param {string} param.optionsKey - option key to check * @param {string} param.parentKey - parent key to use in error message if invalid */ const checkIfAdvancedOptionKeyIsValid = ({ fileName, optionsKey, parentKey }) => { Object.values(ScriptKeys).forEach(scriptKey => { if (optionsKey === scriptKey) { printMessage( formatMessage(messages.scriptKeyUsedAsOption, { fileName, parentKey, scriptKey, }), ); forceExit(); } }); }; /** * Add an option to the script * * @param {object} param - object parameter * @param {Script} param.script - the script to add an option to * @param {object} param.file - the file that contains the options to be added to the script * @param {string} param.fileName - the name of the file with the options to add into the script * @param {string} param.key - the current key of the file to get the options from */ const addOptionToAdvancedScript = ({ script, file, fileName, key }) => { const optionsKeys = Object.keys(file.options); const choices = []; optionsKeys.forEach(optionsKey => { checkIfAdvancedOptionKeyIsValid({ fileName, optionsKey, parentKey: key }); // eslint-disable-next-line no-use-before-define parseAdvancedScript({ script, file: file.options[optionsKey], fileName, key: `${key}.${optionsKey}`, }); choices.push(optionsKey); }); if (key !== script.getName()) { // Add back command as second last option choices.push(BACK_COMMAND); } // Add quit command as last option choices.push(QUIT_COMMAND); const name = getNameFromKey(key); const option = new Option({ name, choices, }); if (ScriptKeys.MESSAGE in file) { option.updateMessage(file.message); } script.updateOption({ optionKey: key, option, }); }; /** * Recursively parse an advanced script * * @param {object} param - object parameter * @param {Script} param.script - the advanced script parsed from the file * @param {object} param.file - the file to parse the advanced script from * @param {string} param.fileName - name of the file * @param {string} param.key - current file key to be parsed */ const parseAdvancedScript = ({ script, file, fileName, key }) => { if (ScriptKeys.DIRECTORY in file) { addDirectoryToAdvancedScript({ script, file, key }); } if (ScriptKeys.COMMAND in file) { addCommandToAdvancedScript({ script, file, fileName, key }); } else if (ScriptKeys.OPTIONS in file) { addOptionToAdvancedScript({ script, file, fileName, key }); } }; /** * Returns choices from keys * * @param {object} param - object parameter * @param {string[]} param.keys - keys to parse choices from * @param {string} param.startingKey - part of key to ignore * * @returns {string[]} choices - choices parsed from keys */ const getChoicesFromSimpleScriptKeys = ({ keys, startingKey = '' }) => { return ( keys // Filter keys that don't start with the starting key .filter(key => { if (key.indexOf(`${startingKey.replace(/\./g, SIMPLE_SCRIPT_OPTION_SEPARATOR)}:`) === 0) { return true; } if (key === startingKey.replace(/\./g, SIMPLE_SCRIPT_OPTION_SEPARATOR)) { return true; } return startingKey === ''; }) // Remove starting key from keys .map(key => { if (!isEmptyString(startingKey)) { if (key === startingKey.replace(/\./g, SIMPLE_SCRIPT_OPTION_SEPARATOR)) { return getNameFromSimpleScriptKey(key); } const re = new RegExp(`${startingKey.replace(/\./g, SIMPLE_SCRIPT_OPTION_SEPARATOR)}:`); return key.replace(re, ''); } return key; }) // Get just the choices from keys e.g. 'abc:def' => 'abc' .map(key => { if (key.includes(SIMPLE_SCRIPT_OPTION_SEPARATOR)) { return key.substring(0, key.indexOf(SIMPLE_SCRIPT_OPTION_SEPARATOR)); } return key; }) // Remove empty strings .filter(key => !isEmptyString(key)) // Remove duplicates .filter((key, index, arr) => arr.indexOf(key) === index) ); }; /** * Add or update option in simple script * * @param {object} param - object parameter * @param {Script} param.script - script to add or update with option * @param {string} param.optionKey - key of the option to either add or update * @param {string[]} param.choices - choices to add to update option with */ const addOrUpdateOptionToSimpleScript = ({ script, optionKey, choices }) => { const option = new Option({ name: optionKey, choices }); if (!script.hasOption(optionKey)) { script.addOption({ optionKey, option }); } else { const originalChoices = getUndocumentedChoices(script.getOption(optionKey).getChoices()); const unionChoices = [...new Set([...originalChoices, ...option.getChoices()])]; option.updateChoices(unionChoices); script.updateOption({ optionKey, option }); } }; /** * Add a command to simple script * * @param {object} param - object parameter * @param {Script} param.script - simple script to a command to * @param {object} param.file - file to parse the command from * @param {string} param.key - key to use to parse the command from the file * @param {string[]} param.keys - keys used to check if the command is also an option */ const addCommandToSimpleScript = ({ script, file, key, keys, npmAlias }) => { const re = new RegExp(SIMPLE_SCRIPT_OPTION_SEPARATOR, 'g'); let commandKey = `${script.getName()}.${key.replace(re, KEY_SEPARATOR)}`; if (isAnOptionAndCommand({ key, keys })) { // Command is also an option, store the command one level deeper so it will be in the correct options choices list commandKey += `${KEY_SEPARATOR}${getNameFromSimpleScriptKey(key)}`; } const directive = file[key]; const directives = [directive]; const hiddenDirectives = []; if (CurrentOperatingModes.includes(OperatingModes.RUNNING_PACKAGE_JSON)) { const hiddenDirective = `${npmAlias} run ${key}`; hiddenDirectives.push(hiddenDirective); } const command = new Command({ directives, hiddenDirectives }); script.addCommand({ commandKey, command }); }; /** * Parse a simple script * * @param {object} param - object parameter * @param {Script} param.script - the simple script parsed from the file * @param {object|string} param.file - the file to parse the simple script from * @param {object|string} param.npmAlias - the NPM alias to use in the hidden directive to call the command with * * @returns {Script} script - the simple script parsed from the file */ const parseSimpleScript = ({ script, file, npmAlias }) => { if (isString(file)) { const command = new Command({ directives: [file] }); script.addCommand({ commandKey: script.getName(), command }); return script; } const keys = Object.keys(file); // Add top level option to script addOrUpdateOptionToSimpleScript({ script, optionKey: script.getName(), choices: [...getChoicesFromSimpleScriptKeys({ keys }), QUIT_COMMAND], }); keys.forEach(key => { // Add command addCommandToSimpleScript({ script, file, key, keys, npmAlias, }); // Add options const optionKeys = getOptionsKeysFromKey(key); optionKeys.forEach(optionKey => { // Add option const choices = [ ...getChoicesFromSimpleScriptKeys({ keys, startingKey: optionKey }), BACK_COMMAND, QUIT_COMMAND, ]; addOrUpdateOptionToSimpleScript({ script, choices, optionKey: `${script.getName()}${KEY_SEPARATOR}${optionKey}`, }); }); }); return script; }; class Parser { /** * Parse a yaml file into commands, options, messages, variables and directories * * @param {string} fileName - the name of the yaml file to be loaded and parsed * * @returns {Script} script - the script loaded into memory */ static getScriptFromYamlFile(fileName) { if (!isValidYamlFileName(fileName)) { printMessage(formatMessage(globalMessages.invalidYamlFile, { fileName })); forceExit(); } const yamlFile = loadYamlFile(fileName); const name = getFirstKey(yamlFile); const script = new Script({ name, }); const scriptType = getScriptType(fileName); if (scriptType === ScriptTypes.SIMPLE) { parseSimpleScript({ script, file: yamlFile[script.getName()], }); } else { parseAdvancedScript({ script, fileName, file: yamlFile[script.getName()], key: name, }); } return script; } /** * Parse a json file int commands, options, messages, variables and directories * * @param {object} param - object parameter * @param {string} param.fileName - the name of the json file to be loaded and parsed * @param {string} param.scriptStartingKey - the key to start at e.g. 'scripts' in a NPM package.json file * @param {string[]} param.filterProperties - properties to filter out * @param {string} param.scriptType - the type of script to parse (simple or advanced) * @param {string} param.npmAlias - the NPM alias used to build the hidden directive used to call the command * * @returns {Script} script - the script loaded into memory */ static getScriptFromJsonFile({ fileName, scriptType = getScriptType(fileName), scriptStartingKey = '', filterProperties, npmAlias, }) { if (!isValidJsonFileName(fileName)) { printMessage(formatMessage(globalMessages.invalidJsonFile, { fileName })); forceExit(); } const jsonFile = loadJsonFile(fileName); const name = scriptStartingKey && isString(scriptStartingKey) ? scriptStartingKey : getFirstKey(jsonFile); const script = new Script({ name, }); if (!(script.getName() in jsonFile)) { printMessage( formatMessage(messages.missingScriptStartingKey, { fileName, scriptStartingKey: script.getName(), }), ); forceExit(); } const file = jsonFile[script.getName()]; if (filterProperties) { filterProperties.forEach(property => { if (file[property]) { // Found filtered property; removing property delete file[property]; } }); } if (scriptType === ScriptTypes.SIMPLE) { parseSimpleScript({ script, file, npmAlias, }); } else { parseAdvancedScript({ script, fileName, file, key: name, }); } return script; } } module.exports = Parser;