argumental
Version:
Framework for building CLI apps with Node.js
1,071 lines (1,070 loc) • 108 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArgumentalApp = void 0;
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const parser_1 = require("./parser");
const logger_1 = require("./logger");
const validators_1 = require("./validators");
class ArgumentalApp extends validators_1.BuiltInValidators {
constructor(exitOnError) {
super();
/** Global declaration flag. */
this._global = false;
/** Global declarations to prepend to all future command declarations (including top-level). */
this._globalDeclaration = {
arguments: [],
options: [],
actions: [],
events: {
'validators:before': [],
'validators:after': [],
'defaults:before': [],
'defaults:after': [],
'actions:before': [],
'actions:after': []
}
};
/** Shared declaration flag. */
this._shared = false;
/** Shared declarations to prepend to all future command declarations (excluding top-level). */
this._sharedDeclaration = {
arguments: [],
options: [],
actions: [],
events: {
'validators:before': [],
'validators:after': [],
'defaults:before': [],
'defaults:after': [],
'actions:before': [],
'actions:after': []
}
};
/** Command declarations. */
this._commands = {
// Top-level
'': {
name: '',
description: null,
aliases: [],
arguments: [],
options: [],
actions: [],
original: true,
order: 0,
events: {
'validators:before': [],
'validators:after': [],
'defaults:before': [],
'defaults:after': [],
'actions:before': [],
'actions:after': []
}
}
};
/** Current command declaration. */
this._currentCommand = '';
/** Current component type of the current command. */
this._currentComponent = null;
/** List of all command names and aliases for quick conflict checks. */
this._conflicts = [];
/** Application version. */
this._version = null;
/** Application name. */
this._name = null;
/** Parser. */
this._parser = new parser_1.Parser();
/** Logger. */
this._log = new logger_1.Logger();
/** Flag for displaying help without the --help option for plain top-level command. */
this._topLevelPlainHelp = true;
/** The last command order set. */
this._lastCommandOrder = 0;
/** Custom event declarations. */
this._events = new Map();
/** Shared data object throughout the application. */
this._data = {};
/** Whether to end process on error or not. */
this._exitOnError = true;
if (exitOnError !== undefined)
this._exitOnError = !!exitOnError;
// Define --help top-level
this._commands[''].options.push(this._parser.parseOption('--help', 'displays application help', false, null, false, undefined, true));
this._commands[''].actions.push({ destructuringParams: true, callback: ({ opts, suspend }) => {
if (opts.help) {
this._log.help(this._commands, '');
suspend();
}
} });
// Define --help on all levels
this._sharedDeclaration.options.push(this._parser.parseOption('--help', 'displays command help', false, null, false, undefined, true));
this._sharedDeclaration.actions.push({ destructuringParams: true, callback: ({ opts, cmd, suspend }) => {
if (!opts.help)
return;
this._log.help(this._commands, cmd);
suspend();
} });
}
/**
* Attaches an array of validators to the given component (argument or option's argument).
* @param component A reference to an argument or and option declaration.
* @param validators The array of validators.
* @param destructuringParams Whether the validators are defined with destructuring parameters.
*/
_attachValidator(component, validators, destructuringParams) {
if (this._currentComponent === 'arguments') {
component.validators =
component.validators
.concat(validators.map(validator => {
if (validator instanceof RegExp)
return validator;
return { destructuringParams, callback: validator };
}));
}
else if (component.argument) {
component.argument.validators =
component.argument.validators
.concat(validators.map(validator => {
if (validator instanceof RegExp)
return validator;
return { destructuringParams, callback: validator };
}));
}
}
/**
* Sets validators for an option or an argument.
* @param validators A single or an array of validators.
* @param destructuringParams Whether to use destructuring params for the validators.
*/
_validate(validators, destructuringParams) {
// If no component selected
if (!this._currentComponent)
throw new Error(`ARGUMENTAL_ERROR: Cannot set validators because no option or argument is selected!`);
// Wrap validator if not already an array
let wrappedValidators;
if (!validators || typeof validators !== 'object' || validators.constructor !== Array)
wrappedValidators = [validators];
// If global
if (this._global) {
// Set for all options and arguments
for (const commandName in this._commands) {
const component = this._commands[commandName][this._currentComponent];
const currentComponent = component[component.length - 1];
this._attachValidator(currentComponent, wrappedValidators, destructuringParams);
}
// Update global declaration
const component = this._globalDeclaration[this._currentComponent];
const currentComponent = component[component.length - 1];
this._attachValidator(currentComponent, wrappedValidators, destructuringParams);
}
// If shared
else if (this._shared) {
// Set for all options and arguments
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName][this._currentComponent];
const currentComponent = component[component.length - 1];
this._attachValidator(currentComponent, wrappedValidators, destructuringParams);
}
// Update global declaration
const component = this._sharedDeclaration[this._currentComponent];
const currentComponent = component[component.length - 1];
this._attachValidator(currentComponent, wrappedValidators, destructuringParams);
}
// Specific component
else {
const component = this._commands[this._currentCommand][this._currentComponent];
const currentComponent = component[component.length - 1];
this._attachValidator(currentComponent, wrappedValidators, destructuringParams);
}
}
/**
* Mounts an action handler to the current command (or globally).
* @param handler The action handler to attach.
* @param destructuringParams Whether to use destructuring params for the validators.
*/
_action(handler, destructuringParams) {
// Check if no command is being declared and global and shared flags are not set
if (this._currentCommand === null && !this._global && !this._shared)
throw new Error(`ARGUMENTAL_ERROR: Cannot add action handler because no command is being defined and global and shared definitions are disabled!`);
// Add the action handler globally and append to all commands (including top-level)
if (this._global) {
this._globalDeclaration.actions.push({ callback: handler, destructuringParams });
for (const commandName in this._commands) {
this._commands[commandName].actions.push({ callback: handler, destructuringParams });
}
}
// Add the action handler globally and append to all commands
else if (this._shared) {
this._sharedDeclaration.actions.push({ callback: handler, destructuringParams });
for (const commandName in this._commands) {
// Exclude top-level
if (commandName !== '')
this._commands[commandName].actions.push({ callback: handler, destructuringParams });
}
}
// Add the action handler to current command
else {
this._commands[this._currentCommand].actions.push({ callback: handler, destructuringParams });
}
// If top-level, set original to false
if (this._global || (!this._shared && this._currentCommand === ''))
this._commands[''].original = false;
}
/**
* Determines if argument already exists considering the global and shared flags.
* @param argument The argument declaration.
*/
_doesArgumentAlreadyExist(argument) {
// Command-specific
if (!this._global && !this._shared) {
return !!this._commands[this._currentCommand].arguments.filter(arg => arg.apiName === argument.apiName).length;
}
// Should not exist on any commands and on shared and global declarations
if (this._globalDeclaration.arguments.filter(arg => arg.apiName === argument.apiName).length)
return true;
if (this._sharedDeclaration.arguments.filter(arg => arg.apiName === argument.apiName).length)
return true;
for (const commandName in this._commands) {
if (this._shared && commandName === '')
continue;
for (const arg of this._commands[commandName].arguments) {
if (arg.apiName === argument.apiName)
return true;
}
}
return false;
}
/**
* Determines if option already exists considering the global and shared flags.
* @param option The option declaration.
*/
_doesOptionAlreadyExist(option) {
// Command-specific
if (!this._global && !this._shared) {
return !!this._commands[this._currentCommand].options
.filter(opt => (opt.longName && opt.longName === option.longName) || (opt.shortName && option.shortName === opt.shortName) || (opt.apiName && opt.apiName === option.apiName)).length;
}
// Should not exist on any commands and on shared and global declarations
if (this._globalDeclaration.options.filter(opt => (opt.longName && opt.longName === option.longName) || (opt.shortName && option.shortName === opt.shortName) || (opt.apiName && opt.apiName === option.apiName)).length)
return true;
if (this._sharedDeclaration.options.filter(opt => (opt.longName && opt.longName === option.longName) || (opt.shortName && option.shortName === opt.shortName) || (opt.apiName && opt.apiName === option.apiName)).length)
return true;
for (const commandName in this._commands) {
if (this._shared && commandName === '')
continue;
for (const opt of this._commands[commandName].options) {
if (opt.longName && opt.longName === option.longName)
return true;
if (opt.shortName && opt.shortName === option.shortName)
return true;
if (opt.apiName && opt.apiName === option.apiName)
return true;
}
}
return false;
}
/**
* Merges two event declarations (does not mutate objects).
* @param a First event declarations.
* @param b Second event declarations.
*/
_mergeEvents(a, b) {
const merged = {};
for (const key in a) {
merged[key] = a[key].concat(b[key]);
}
return merged;
}
/**
* Returns true if event is a default event.
* @param event The event name to check.
*/
_isEventDefault(event) {
return [
'validators:before',
'validators:after',
'defaults:before',
'defaults:after',
'actions:before',
'actions:after'
].includes(event);
}
/**
* Emits a context-based default event for a command.
* @param event The default event name.
* @param cmd The command name.
* @param data The event data.
*/
_emitDefault(event, cmd, data) {
return __awaiter(this, void 0, void 0, function* () {
// Check command name
if (!this._commands.hasOwnProperty(cmd))
throw new Error(`INTERNAL_ERROR: Command ${cmd} not defined!`);
// Emit all event handlers
for (const handler of this._commands[cmd].events[event]) {
yield handler(data);
}
});
}
/** Logs error and exits the process with code 1. */
_exitWithError(error) {
this._log.error(error);
if (this._exitOnError)
process.exit(1);
}
/** Shared data object throughout the application. */
data() {
return this._data;
}
/**
* Configures Argumental with the provided options.
* @param options The configuration options.
*/
config(options) {
// Apply config
if (options.hasOwnProperty('colors'))
this._log.colors = options.colors;
if (options.hasOwnProperty('topLevelPlainHelp'))
this._topLevelPlainHelp = options.topLevelPlainHelp;
if (options.hasOwnProperty('help'))
this._log.customHelpRenderer = options.help;
return this;
}
/**
* Sets the application version and defines the top-level option `-v --version`.
* @param version The application version.
*/
version(version) {
this._version = version.trim();
this._commands[''].options.push(this._parser.parseOption('-v --version', 'displays application version', false, null, false, undefined, true));
this._commands[''].actions.push({ destructuringParams: true, callback: ({ opts, suspend }) => {
if (opts.version) {
console.log(this._version);
suspend();
}
} });
return this;
}
/**
* Makes any following argument, option, and action declaration globally applied to all commands (appended to previously declared commands and prepended to future command declarations) unless the command() function is called.
*/
get shared() {
// Set the shared flag
this._shared = true;
// Reset the global flag
this._global = false;
// Reset the current component
this._currentComponent = null;
return this;
}
/**
* Makes any following argument, option, and action declaration globally applied to all commands excluding top-level (appended to previously declared commands and prepended to future command declarations) unless the command() function is called.
*/
get global() {
// Set the global flag
this._global = true;
// Reset the shared flag
this._shared = false;
// Reset the current component
this._currentComponent = null;
return this;
}
/**
* Makes any following argument, option, and action declaration applied to top-level.
*/
get top() {
// Reset the global flag
this._global = false;
// Reset the shared flag
this._shared = false;
// Reset the current component
this._currentComponent = null;
// Set the current command to top-level
this._currentCommand = '';
return this;
}
/**
* Sets the description for a command, option, or argument.
* @param text The description text to display in help.
*/
description(text) {
// Check if current component is not set, set for current command
if (!this._currentComponent && (!this._global || !this._shared)) {
this._commands[this._currentCommand].description = text;
}
// If global but no component selected
else if ((this._global || this._shared) && !this._currentComponent) {
throw new Error('ARGUMENTAL_ERROR: Cannot set description on the global or shared context because no command, option, or argument is selected!');
}
else {
// If global
if (this._global) {
// Set for all commands' components (last component)
for (const commandName in this._commands) {
const component = this._commands[commandName][this._currentComponent];
component[component.length - 1].description = text;
}
// Update global declaration
const component = this._globalDeclaration[this._currentComponent];
component[component.length - 1].description = text;
}
// If shared
else if (this._shared) {
// Set for all commands' components (last component)
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName][this._currentComponent];
component[component.length - 1].description = text;
}
// Update global declaration
const component = this._sharedDeclaration[this._currentComponent];
component[component.length - 1].description = text;
}
// Specific component
else {
const component = this._commands[this._currentCommand][this._currentComponent];
component[component.length - 1].description = text;
}
}
return this;
}
/**
* Sets the required flag on an option.
* @param value The required flag value (defaults to true).
*/
required(value = true) {
// If no component selected
if (this._currentComponent !== 'options')
throw new Error(`ARGUMENTAL_ERROR: Cannot set the required flag because no option is selected!`);
// If global
if (this._global) {
// Set for all options
for (const commandName in this._commands) {
const component = this._commands[commandName].options;
component[component.length - 1].required = value;
}
// Update global declaration
const component = this._globalDeclaration.options;
component[component.length - 1].required = value;
}
// If shared
else if (this._shared) {
// Set for all options
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName].options;
component[component.length - 1].required = value;
}
// Update global declaration
const component = this._sharedDeclaration.options;
component[component.length - 1].required = value;
}
// Specific component
else {
const component = this._commands[this._currentCommand].options;
component[component.length - 1].required = value;
}
return this;
}
/**
* Sets the multi flag on an option.
* @param value The multi flag value (defaults to true).
*/
multi(value = true) {
// If no component selected
if (this._currentComponent !== 'options')
throw new Error(`ARGUMENTAL_ERROR: Cannot set the multi flag because no option is selected!`);
// If global
if (this._global) {
// Set for all options
for (const commandName in this._commands) {
const component = this._commands[commandName].options;
component[component.length - 1].multi = value;
}
// Update global declaration
const component = this._globalDeclaration.options;
component[component.length - 1].multi = value;
}
// If shared
else if (this._shared) {
// Set for all options
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName].options;
component[component.length - 1].multi = value;
}
// Update global declaration
const component = this._sharedDeclaration.options;
component[component.length - 1].multi = value;
}
// Specific component
else {
const component = this._commands[this._currentCommand].options;
component[component.length - 1].multi = value;
}
return this;
}
/**
* Sets the default value for an option or an argument.
* @param value The default value.
*/
default(value) {
// If no component selected
if (!this._currentComponent)
throw new Error(`ARGUMENTAL_ERROR: Cannot set the default value because no option or argument is selected!`);
// If global
if (this._global) {
// Set for all options or arguments
for (const commandName in this._commands) {
const component = this._commands[commandName][this._currentComponent];
if (this._currentComponent === 'arguments')
component[component.length - 1].default = value;
else
component[component.length - 1].argument.default = value;
}
// Update global declaration
const component = this._globalDeclaration[this._currentComponent];
if (this._currentComponent === 'arguments')
component[component.length - 1].default = value;
else
component[component.length - 1].argument.default = value;
}
// If shared
else if (this._shared) {
// Set for all options or arguments
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName][this._currentComponent];
if (this._currentComponent === 'arguments')
component[component.length - 1].default = value;
else
component[component.length - 1].argument.default = value;
}
// Update global declaration
const component = this._sharedDeclaration[this._currentComponent];
if (this._currentComponent === 'arguments')
component[component.length - 1].default = value;
else
component[component.length - 1].argument.default = value;
}
// Specific component
else {
const component = this._commands[this._currentCommand][this._currentComponent];
if (this._currentComponent === 'arguments')
component[component.length - 1].default = value;
else
component[component.length - 1].argument.default = value;
}
return this;
}
/**
* Sets the immediate flag on an option.
* @param value The immediate flag value (defaults to true).
*/
immediate(value = true) {
// If no component selected
if (this._currentComponent !== 'options')
throw new Error(`ARGUMENTAL_ERROR: Cannot set the immediate flag because no option is selected!`);
// If global
if (this._global) {
// Set for all options
for (const commandName in this._commands) {
const component = this._commands[commandName].options;
component[component.length - 1].immediate = value;
}
// Update global declaration
const component = this._globalDeclaration.options;
component[component.length - 1].immediate = value;
}
// If shared
else if (this._shared) {
// Set for all options
for (const commandName in this._commands) {
if (commandName === '')
continue;
const component = this._commands[commandName].options;
component[component.length - 1].immediate = value;
}
// Update global declaration
const component = this._sharedDeclaration.options;
component[component.length - 1].immediate = value;
}
// Specific component
else {
const component = this._commands[this._currentCommand].options;
component[component.length - 1].immediate = value;
}
return this;
}
/**
* Sets validators for an option or an argument.
* @param validators A single or an array of validators.
*/
validate(validators) {
this._validate(validators, false);
return this;
}
/**
* Alias for <code>validate()</code>.
*/
sanitize(sanitizers) {
return this.validate(sanitizers);
}
/**
* Mounts an action handler to the current command (or globally).
* @param handler The action handler to attach.
*/
action(handler) {
this._action(handler, false);
return this;
}
/**
* Sets validators for an option or an argument.
* @param validators A single or an array of validators.
*/
validateDestruct(validators) {
this._validate(validators, true);
return this;
}
/**
* Alias for <code>validate()</code>.
*/
sanitizeDestruct(sanitizers) {
return this.validate(sanitizers);
}
/**
* Mounts an action handler to the current command (or globally).
* @param handler The action handler to attach.
*/
actionDestruct(handler) {
this._action(handler, true);
return this;
}
command(name, description) {
// Check if command already exists
if (this._commands.hasOwnProperty(name.trim()))
throw new Error(`ARGUMENTAL_ERROR: Command ${name.trim()} is already defined!`);
// Check if command name conflicts
if (this._conflicts.includes(name.trim()))
throw new Error(`ARGUMENTAL_ERROR: Cannot define command ${name.trim()} because it conflicts with a command or alias of the same name!`);
// Check if command contains invalid characters
if (!name.trim().match(/^[a-z0-9 ]+$/i) || name.trim().match(/ {2,}/))
throw new Error(`ARGUMENTAL_ERROR: Invalid command name ${name.trim()}! Commands can only contain alphanumeric characters and nonconsecutive spaces.`);
// Reset the global flag
this._global = false;
// Reset the shared flag
this._shared = false;
// Reset the current component
this._currentComponent = null;
// Set the current command pointer
this._currentCommand = name.trim();
// Create an empty command object and prepend all global declarations
this._commands[name.trim()] = {
name: name.trim(),
description: description || null,
aliases: [],
arguments: lodash_1.default.concat(this._globalDeclaration.arguments, this._sharedDeclaration.arguments),
options: lodash_1.default.concat(this._globalDeclaration.options, this._sharedDeclaration.options),
actions: lodash_1.default.concat(this._globalDeclaration.actions, this._sharedDeclaration.actions),
order: ++this._lastCommandOrder,
events: this._mergeEvents(this._globalDeclaration.events, this._sharedDeclaration.events),
};
// Register in conflicting names
this._conflicts.push(name.trim());
return this;
}
/**
* Defines an alias for the current command.
* @param name The alias name.
*/
alias(name) {
// Check if global flag is set
if (this._global)
throw new Error('ARGUMENTAL_ERROR: Cannot define alias globally!');
// Check if shared flag is set
if (this._shared)
throw new Error('ARGUMENTAL_ERROR: Cannot define shared alias!');
// Check if alias name conflicts
if (this._conflicts.includes(name.trim()))
throw new Error(`ARGUMENTAL_ERROR: Cannot define alias ${name.trim()} because it conflicts with a command or alias of the same name!`);
// Check if alias contains invalid characters
if (!name.trim().match(/^[a-z0-9 ]+$/i))
throw new Error(`ARGUMENTAL_ERROR: Invalid alias name ${name.trim()}! Aliases can only contain alphanumeric characters and spaces.`);
// Check if no command is being declared
if (this._currentCommand === null)
throw new Error(`ARGUMENTAL_ERROR: Cannot define alias ${name.trim()} because no command is being defined!`);
// Add alias if it isn't already defined for command
const command = this._commands[this._currentCommand];
if (!command.aliases.includes(name.trim()))
command.aliases.push(name.trim());
// Register in conflicting names
this._conflicts.push(name.trim());
// If top-level, set original to false
if (this._global || (!this._shared && this._currentCommand === ''))
this._commands[''].original = false;
return this;
}
argument(syntax, description, validators, defaultValue) {
// Check if no command is being declared and global flag is not set
if (this._currentCommand === null && !this._global && !this._shared)
throw new Error(`ARGUMENTAL_ERROR: Cannot define argument ${syntax} because no command is being defined and global and shared definitions are disabled!`);
// Check if last argument was rest
if (this._global || this._shared) {
for (const commandName in this._commands) {
if (this._shared && commandName === '')
continue;
const cmdArgs = this._commands[commandName].arguments;
if (cmdArgs.length && cmdArgs[cmdArgs.length - 1].rest)
throw new Error(`ARGUMENTAL_ERROR: Cannot define argument ${syntax} after a rest argument!`);
}
}
else {
const cmdArgs = this._commands[this._currentCommand].arguments;
if (cmdArgs.length && cmdArgs[cmdArgs.length - 1].rest)
throw new Error(`ARGUMENTAL_ERROR: Cannot define argument ${syntax} after a rest argument!`);
}
// Parse argument
const argument = lodash_1.default.assign(this._parser.parseArgument(syntax, true, validators, defaultValue), {
description: description || null
});
// Check if argument is already defined for current command or globally
if (this._doesArgumentAlreadyExist(argument))
throw new Error(`ARGUMENTAL_ERROR: Argument ${argument.apiName} is already defined!`);
// If global flag is enabled, add argument to global declaration and append to all commands (including top-level)
if (this._global) {
this._globalDeclaration.arguments.push(argument);
for (const commandName in this._commands) {
this._commands[commandName].arguments.push(argument);
}
}
// If shared flag is enabled, add argument to global declaration and append to all commands (excluding top-level)
else if (this._shared) {
this._sharedDeclaration.arguments.push(argument);
for (const commandName in this._commands) {
// Exclude top-level
if (commandName !== '')
this._commands[commandName].arguments.push(argument);
}
}
// Add argument to current command
else {
this._commands[this._currentCommand].arguments.push(argument);
}
// Update current component
this._currentComponent = 'arguments';
// If top-level, set original to false
if (this._global || (!this._shared && this._currentCommand === ''))
this._commands[''].original = false;
return this;
}
option(syntax, description, required, validators, multi, defaultValue, immediate) {
// Check if no command is being declared and global or shared flags are not set
if (this._currentCommand === null && !this._global && !this._shared)
throw new Error(`ARGUMENTAL_ERROR: Cannot define option ${syntax} because no command is being defined and global and shared definitions are disabled!`);
// Parse option
const option = this._parser.parseOption(syntax, description, required, validators, multi, defaultValue, immediate);
// Check if option is already defined for current command or globally
if (this._doesOptionAlreadyExist(option))
throw new Error(`ARGUMENTAL_ERROR: Option ${option.longName || option.shortName} is already defined!`);
// If global flag is enabled, add option to global declaration and append to all commands (including top-level)
if (this._global) {
this._globalDeclaration.options.push(option);
for (const commandName in this._commands) {
this._commands[commandName].options.push(option);
}
}
// If shared flag is enabled, add option to global declaration and append to all commands (excluding top-level)
else if (this._shared) {
this._sharedDeclaration.options.push(option);
for (const commandName in this._commands) {
// Exclude top-level
if (commandName !== '')
this._commands[commandName].options.push(option);
}
}
// Add option to current command
else {
this._commands[this._currentCommand].options.push(option);
}
// Update current component
this._currentComponent = 'options';
// If top-level, set original to false
if (this._global || (!this._shared && this._currentCommand === ''))
this._commands[''].original = false;
return this;
}
on(event, handler) {
// If event name is invalid
if (!event || typeof event !== 'string' || !event.trim())
throw new Error('ARGUMENTAL_ERROR: Invalid event name!');
event = event.trim().toLowerCase();
// If default event (context-based)
if (this._isEventDefault(event)) {
// If global context
if (this._global) {
// Add to global declaration
if (!this._globalDeclaration.events.hasOwnProperty(event))
this._globalDeclaration.events[event] = [];
this._globalDeclaration.events[event].push(handler);
// Append to all commands
for (const commandName in this._commands) {
if (!this._commands[commandName].events[event])
this._commands[commandName].events[event] = [];
this._commands[commandName].events[event].push(handler);
}
}
// If shared context
else if (this._shared) {
// Add to global declaration
if (!this._sharedDeclaration.events.hasOwnProperty(event))
this._sharedDeclaration.events[event] = [];
this._sharedDeclaration.events[event].push(handler);
// Append to all commands
for (const commandName in this._commands) {
// Skip top-level
if (commandName === '')
continue;
if (!this._commands[commandName].events[event])
this._commands[commandName].events[event] = [];
this._commands[commandName].events[event].push(handler);
}
}
// Command-specific (including top-level)
else {
if (!this._commands[this._currentCommand].events.hasOwnProperty(event))
this._commands[this._currentCommand].events[event] = [];
this._commands[this._currentCommand].events[event].push(handler);
}
}
// Custom event (context-free)
else {
if (!this._events.has(event))
this._events.set(event, []);
this._events.get(event).push(handler);
}
return this;
}
/**
* Emits a custom event within the current context.
* @param event The event name (case insensitive).
* @param data The custom event data.
*/
emit(event, data) {
return __awaiter(this, void 0, void 0, function* () {
// If event name is invalid
if (!event || typeof event !== 'string' || !event.trim())
throw new Error('ARGUMENTAL_ERROR: Invalid event name!');
event = event.trim().toLowerCase();
// If event name is illegal
if (this._isEventDefault(event))
throw new Error('ARGUMENTAL_ERROR: Cannot emit default events!');
// If event not found
if (!this._events.has(event))
throw new Error(`ARGUMENTAL_ERROR: Event ${event} not found!`);
// Emit custom event
const handlers = this._events.get(event);
for (const handler of handlers) {
yield handler(data);
}
});
}
/**
* Displays an error message in the console.
@param message An error message or object.
*/
error(message) {
this._log.error(message);
}
/**
* Parses the process arguments (argv) and runs the app.
* @param argv Process arguments to parse.
*/
parse(argv) {
return __awaiter(this, void 0, void 0, function* () {
// Extract app name
const appPath = argv.slice(1, 2)[0];
if (appPath)
this._name = path_1.default.basename(appPath);
this._log.appName = this._name;
// Display help for plain top-level argv
if (argv.length === 2 && this._topLevelPlainHelp) {
return this._log.help(this._commands, '');
}
// Parse arguments
const parsed = this._parser.parseCliArguments(argv.slice(2), this._commands);
// If parsing error
if (parsed instanceof Error)
return this._exitWithError(parsed);
const command = this._commands[parsed.cmd];
let immediateOption = null;
// Check for immediate options
for (const option of command.options) {
// If immediate option provided
if (option.immediate && ((option.argument && parsed.opts[option.apiName || option.shortName] !== undefined) || (!option.argument && parsed.opts[option.apiName || option.shortName] === true))) {
// Turn multi off
option.multi = false;
// Turn value array to single value if needed
if (parsed.opts[option.apiName || option.shortName] && typeof parsed.opts[option.apiName || option.shortName] === 'object' && parsed.opts[option.apiName || option.shortName].constructor === Array) {
if (option.apiName)
parsed.opts[option.apiName] = parsed.opts[option.apiName][0];
if (option.shortName)
parsed.opts[option.shortName] = parsed.opts[option.shortName][0];
}
// Delete all other parsed values
for (const name in parsed.opts) {
if ((option.apiName && name === option.apiName) || (option.shortName && name === option.shortName))
continue;
delete parsed.opts[name];
}
// Clear parsed arguments
parsed.args = {};
immediateOption = option;
break;
}
}
// Skip arguments validation if immediate option was provided
if (!immediateOption) {
// Validate arguments
for (const argument of command.arguments) {
if (argument.required && parsed.args[argument.apiName] === null)
return this._exitWithError(`Missing required argument <${argument.rest ? '...' : ''}${argument.name}>!`);
}
}
// Validate options
for (const option of command.options) {
// If immediate option provided, skip other options
if (immediateOption && option !== immediateOption)
continue;
const value = parsed.opts[option.apiName || option.shortName];
const logName = option.longName ? `--${option.longName}` : `-${option.shortName}`;
// Missing required option
if (option.required && ((!option.argument && value === false) || (option.argument && value === undefined)))
return this._exitWithError(`Missing required option ${logName}!`);
// Option is not multi but occurs multiple times
if (!option.multi && value && typeof value === 'object' && value.constructor === Array)
return this._exitWithError(`Option ${logName} cannot be provided more than once!`);
// Missing required argument of option
if (option.argument && option.argument.required) {
// If array
if (value && typeof value === 'object' && value.constructor === Array) {
for (const v of value) {
if (v === null)
return this._exitWithError(`Missing required value for option ${logName}!`);
}
}
else if (value === null) {
return this._exitWithError(`Missing required value for option ${logName}!`);
}
}
}
// Emit 'validators:before' event
yield this._emitDefault('validators:before', parsed.cmd, lodash_1.default.cloneDeep(parsed));
// Skip arguments validators if immediate option was provided
if (!immediateOption) {
// Run argument validators
for (const argument of command.arguments) {
// Skip validators if argument is optional and not provided
if (!argument.required && parsed.args[argument.apiName] === null)
continue;
for (const validator of argument.validators) {
// Regex validator
if (validator instanceof RegExp) {
// If rest argument and value is array
if (argument.rest && parsed.args[argument.apiName] && typeof parsed.args[argument.apiName] === 'object' && parsed.args[argument.apiName].constructor === Array) {
for (const value of parsed.args[argument.apiName]) {
if (typeof value !== 'string' || !value.match(validator))
return this._exitWithError(`Invalid value for argument ${argument.name}!`);
}
}
else {
if (typeof parsed.args[argument.apiName] !== 'string' || !parsed.args[argument.apiName].match(validator))
return this._exitWithError(`Invalid value for argument ${argument.name}!`);
}
continue;
}
// Validator function
let suspended = false;
try {
let newValue;
// Call with destructuring parameters
if (validator.destructuringParams) {
newValue = yield validator.callback({
value: lodash_1.default.cloneDeep(parsed.args[argument.apiName]),
name: argument.name,
arg: true,
cmd: parsed.cmd,
suspend: () => { suspended = true; }
});
}
// Call with normal parameters
else {
newValue = yield validator.callback(lodash_1.default.cloneDeep(parsed.args[argument.apiName]), argument.name, true, parsed.cmd, () => { suspended = true; });
}
// Throw error if return value is an error object
if (newValue instanceof Error)
throw newValue;
// Update the value
if (newValue !== undefined)
parsed.args[argument.apiName] = newValue;
}
catch (error) {
return this._exitWithError(error.message);
}
if (suspended)
break;
}
}
}
// Run option validators
for (const option of command.options) {
// If immediate option provided, skip other validators
if (immediateOption && immediateOption !== option)
continue;
const apiName = option.apiName || option.shortName;
let wrapped = false;
// If option is binary, skip validation
if (!option.argument)
continue;
// If option not required and not provided, skip validation
if (!option.required && parsed.opts[apiName] === undefined)
continue;
// If string option's argument not required and not provided (single occurrence only), skip validation
if (!option.argument.required && parsed.opts[apiName] === null)
continue;
// If value is not an array, wrap it temporarily
if (!parsed.opts[apiName] || typeof parsed.opts[apiName] !== 'object' || parsed.opts[apiName].constructor !== Array) {
if (option.shortName)
parsed.opts[option.shortName] = [parsed.opts[option.shortName]];
if (option.apiName)
parsed.opts[option.apiName] = [parsed.opts[option.apiName]];
wrapped = true;
}
// For each option's value
for (let i = 0; i < parsed.opts[apiName].length; i++) {
let value = parsed.opts[apiName][i];
// If string option's argument not required and not provided, skip validation
if (!option.argument.required && value === null)
continue;
// Run the validators
for (const validator of option.argument.validators) {
// Regex validator
if (validator instanceof RegExp) {