lib/command.ts
876 LOC · 876 covered · 0 uncovered · 195 ranges · 1119 concepts · 93 introducers · 788 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
Dictionary,
assertNotStrictEqual,
RequireDirectoryOptions,
PlatformShim,
} from './typings/common-types.js';
import {isPromise} from './utils/is-promise.js';
import {
applyMiddleware,
commandMiddlewareFactory,
GlobalMiddleware,
Middleware,
} from './middleware.js';
import {parseCommand, Positional} from './parse-command.js';
import {UsageInstance} from './usage.js';
import {ValidationInstance} from './validation.js';
import {
YargsInstance,
isYargsInstance,
Options,
OptionDefinition,
Context,
Configuration,
Arguments,
DetailedArguments,
} from './yargs-factory.js';
import {maybeAsyncResult} from './utils/maybe-async-result.js';
const DEFAULT_MARKER = /(^\*)|(^\$0)/;
export type DefinitionOrCommandName = string | CommandHandlerDefinition;
export class CommandInstance {
shim: PlatformShim;
requireCache: Set<string> = new Set();
handlers: Dictionary<CommandHandler> = {};
aliasMap: Dictionary<string> = {};
defaultCommand?: CommandHandler;
usage: UsageInstance;
globalMiddleware: GlobalMiddleware;
validation: ValidationInstance;
// Used to cache state from prior invocations of commands.
// This allows the parser to push and pop state when running
// a nested command:
frozens: FrozenCommandInstance[] = [];
constructor(
validation: ValidationInstance,
globalMiddleware: GlobalMiddleware,
shim: PlatformShim
) {
this.shim = shim;
this.usage = usage;
this.globalMiddleware = globalMiddleware;
this.validation = validation;
}
req: Function,
callerFile: string,
opts?: RequireDirectoryOptions
): void {
opts = opts || {};
this.requireCache.add(callerFile);
const fullDirPath = this.shim.path.resolve(
this.shim.path.dirname(callerFile),
dir
);
const files = this.shim.readdirSync(fullDirPath, {
recursive: opts.recurse ? true : false,
});
// exclude 'json', 'coffee' from require-directory defaults
if (!Array.isArray(opts.extensions)) opts.extensions = ['js'];
// allow consumer to define their own visitor function
const visit = typeof opts.visit === 'function' ? opts.visit : (o: any) => o;
for (const fileb of files) {
const file = fileb.toString();
// Support include / exclude logic from require-directory.
if (opts.exclude) {
if (typeof opts.exclude === 'function') {
}
}
if (typeof opts.include === 'function') {
}
}
let supportedExtension = false;
for (const ext of opts.extensions) {
if (file.endsWith(ext)) supportedExtension = true;
}
if (supportedExtension) {
const joined = this.shim.path.join(fullDirPath, file);
const module = req(joined);
const extendableModule = Object.create(
null,
Object.getOwnPropertyDescriptors({...module})
);
const visited = visit(extendableModule, joined, file);
if (visited) {
else this.requireCache.add(joined);
// Infer command from directory structure if none is given:
if (!extendableModule.command) {
joined,
this.shim.path.extname(joined)
);
}
}
}
}
description?: CommandHandler['description'],
builder?: CommandBuilderDefinition | CommandBuilder,
handler?: CommandHandlerCallback,
commandMiddleware?: Middleware[],
deprecated?: boolean
): void {
let aliases: string[] = [];
const middlewares = commandMiddlewareFactory(commandMiddleware);
handler = handler || (() => {});
// If an array is provided that is all CommandHandlerDefinitions, add
// each handler individually:
if (Array.isArray(cmd)) {
this.addHandler(command);
}
}
Array.isArray(cmd.command) || typeof cmd.command === 'string'
? cmd.command
: null;
if (command === null) {
`No command name given for module: ${this.shim.inspect(cmd)}`
);
}
command = ([] as string[]).concat(command).concat(cmd.aliases);
this.addHandler(
command,
this.extractDesc(cmd),
cmd.builder,
cmd.handler,
cmd.middlewares,
cmd.deprecated
);
return;
this.addHandler(
[cmd].concat(aliases),
description,
builder.builder,
builder.handler,
builder.middlewares,
builder.deprecated
);
return;
}
// The 'cmd' provided was a string, we apply the command DSL:
// https://github.com/yargs/yargs/blob/main/docs/advanced.md#advanced-topics
if (typeof cmd === 'string') {
// parse positionals out of cmd string
const parsedCommand = parseCommand(cmd);
// remove positional args from aliases only
aliases = aliases.map(alias => parseCommand(alias).cmd);
// check for default and filter out '*'
let isDefault = false;
const parsedAliases = [parsedCommand.cmd].concat(aliases).filter(c => {
if (DEFAULT_MARKER.test(c)) {
return false;
}
// standardize on $0 for default command.
if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0');
// shift cmd and aliases after filtering out '*'
if (isDefault) {
aliases = parsedAliases.slice(1);
cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
}
// populate aliasMap
aliases.forEach(alias => {
if (description !== false) {
}
this.handlers[parsedCommand.cmd] = {
original: cmd,
description,
handler,
builder: (builder as CommandBuilder) || {},
middlewares,
deprecated,
demanded: parsedCommand.demanded,
optional: parsedCommand.optional,
};
if (isDefault) this.defaultCommand = this.handlers[parsedCommand.cmd];
}
}
}
}
yargs: YargsInstance,
parsed: DetailedArguments,
commandIndex: number,
helpOnly: boolean,
helpOrVersionSet: boolean
): Arguments | Promise<Arguments> {
const commandHandler =
this.handlers[command!] ||
this.handlers[this.aliasMap[command!]] ||
this.defaultCommand;
const currentContext = yargs.getInternalMethods().getContext();
const parentCommands = currentContext.commands.slice();
const isDefaultCommand = !command;
if (command) {
currentContext.fullCommands.push(commandHandler.original);
}
isDefaultCommand,
commandHandler,
yargs,
parsed.aliases,
parentCommands,
commandIndex,
helpOnly,
helpOrVersionSet
);
return isPromise(builderResult)
? builderResult.then(result =>
isDefaultCommand,
commandHandler,
result.innerArgv,
currentContext,
helpOnly,
result.aliases,
yargs
: this.applyMiddlewareAndGetResult(
commandHandler,
builderResult.innerArgv,
currentContext,
helpOnly,
builderResult.aliases,
yargs
}
commandHandler: CommandHandler,
yargs: YargsInstance,
aliases: Dictionary<string[]>,
parentCommands: string[],
commandIndex: number,
helpOnly: boolean,
helpOrVersionSet: boolean
):
| {aliases: Dictionary<string[]>; innerArgv: Arguments}
| Promise<{aliases: Dictionary<string[]>; innerArgv: Arguments}> {
const builder = commandHandler.builder;
let innerYargs: YargsInstance = yargs;
if (isCommandBuilderCallback(builder)) {
// up a yargs chain and possibly returns it.
yargs.getInternalMethods().getUsageInstance().freeze();
const builderOutput = builder(
yargs.getInternalMethods().reset(aliases),
helpOrVersionSet
);
// Support the use-case of async builders:
if (isPromise(builderOutput)) {
innerYargs = isYargsInstance(output) ? output : yargs;
return this.parseAndUpdateUsage(
isDefaultCommand,
commandHandler,
innerYargs,
parentCommands,
commandIndex,
helpOnly
);
});
}
// the options that a command takes.
yargs.getInternalMethods().getUsageInstance().freeze();
innerYargs = yargs.getInternalMethods().reset(aliases);
Object.keys(commandHandler.builder).forEach(key => {
}
isDefaultCommand,
commandHandler,
innerYargs,
parentCommands,
commandIndex,
helpOnly
);
}
commandHandler: CommandHandler,
innerYargs: YargsInstance,
parentCommands: string[],
commandIndex: number,
helpOnly: boolean
):
| {aliases: Dictionary<string[]>; innerArgv: Arguments}
| Promise<{aliases: Dictionary<string[]>; innerArgv: Arguments}> {
// A null command indicates we are running the default command,
// if this is the case, we should show the root usage instructions
// rather than the usage instructions for the nested default command:
if (isDefaultCommand)
innerYargs.getInternalMethods().getUsageInstance().unfreeze(true);
if (this.shouldUpdateUsage(innerYargs)) {
.getInternalMethods()
.getUsageInstance()
.usage(
this.usageFromParentCommandsCommandHandler(
parentCommands,
commandHandler
),
commandHandler.description
);
}
.getInternalMethods()
.runYargsParserAndExecuteCommands(
null,
undefined,
true,
commandIndex,
helpOnly
);
return isPromise(innerArgv)
? innerArgv.then(argv => ({
innerArgv: argv,
aliases: (innerYargs.parsed as DetailedArguments).aliases,
innerArgv: innerArgv,
};
}
!yargs.getInternalMethods().getUsageInstance().getUsageDisabled() &&
}
commandHandler: CommandHandler
) {
const c = DEFAULT_MARKER.test(commandHandler.original)
? commandHandler.original.replace(DEFAULT_MARKER, '').trim()
: commandHandler.original;
const pc = parentCommands.filter(c => {
pc.push(c);
return `$0 ${pc.join(' ')}`;
}
commandHandler: CommandHandler,
innerArgv: Arguments | Promise<Arguments>,
currentContext: Context,
aliases: Dictionary<string[]>,
yargs: YargsInstance,
middlewares: Middleware[],
positionalMap: Dictionary<string[]>
) {
// we apply validation post-hoc, so that custom
// checks get passed populated positional arguments.
if (!yargs.getInternalMethods().getHasOutput()) {
.getInternalMethods()
.runValidation(
aliases,
positionalMap,
(yargs.parsed as DetailedArguments).error,
isDefaultCommand
);
innerArgv = maybeAsyncResult<Arguments>(innerArgv, result => {
validation(result);
return result;
});
}
// to simplify the parsing of positionals in commands,
// we temporarily populate '--' rather than _, with arguments
const populateDoubleDash =
!!yargs.getOptions().configuration['populate--'];
yargs
.getInternalMethods()
.postProcess(innerArgv, populateDoubleDash, false, false);
innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
innerArgv = maybeAsyncResult<Arguments>(innerArgv, result => {
return isPromise(handlerResult)
? handlerResult.then(() => result)
: result;
if (!isDefaultCommand) {
}
if (
isPromise(innerArgv) &&
yargs.getInternalMethods().getUsageInstance().fail(null, error);
} catch (_err) {
// registered, run usage's default fail method.
}
}
if (!isDefaultCommand) {
currentContext.fullCommands.pop();
}
return innerArgv;
}
commandHandler: CommandHandler,
innerArgv: Arguments,
currentContext: Context,
helpOnly: boolean,
aliases: Dictionary<string[]>,
yargs: YargsInstance
): Arguments | Promise<Arguments> {
let positionalMap: Dictionary<string[]> = {};
// If showHelp() or getHelp() is being run, we should not
// execute middleware or handlers (these may perform expensive operations
// like creating a DB connection).
if (helpOnly) return innerArgv;
commandHandler,
innerArgv as Arguments,
currentContext,
yargs
);
}
.getMiddleware()
.slice(0)
.concat(commandHandler.middlewares);
const maybePromiseArgv = applyMiddleware(
innerArgv,
yargs,
middlewares,
true
);
return isPromise(maybePromiseArgv)
? maybePromiseArgv.then(resolvedInnerArgv =>
isDefaultCommand,
commandHandler,
resolvedInnerArgv,
currentContext,
aliases,
yargs,
middlewares,
positionalMap
)
: this.handleValidationAndGetResult(
commandHandler,
maybePromiseArgv,
currentContext,
aliases,
yargs,
middlewares,
positionalMap
}
// onto argv.
private populatePositionals(
argv: Arguments,
context: Context,
yargs: YargsInstance
) {
argv._ = argv._.slice(context.commands.length); // nuke the current commands
const demanded = commandHandler.demanded.slice(0);
const optional = commandHandler.optional.slice(0);
const positionalMap: Dictionary<string[]> = {};
this.validation.positionalCount(demanded.length, argv._.length);
while (demanded.length) {
this.populatePositional(demand, argv, positionalMap);
}
while (optional.length) {
this.populatePositional(maybe, argv, positionalMap);
}
argv._ = context.commands.concat(argv._.map(a => '' + a));
this.postProcessPositionals(
argv,
positionalMap,
this.cmdToParseOptions(commandHandler.original),
yargs
);
return positionalMap;
}
private populatePositional(
argv: Arguments,
positionalMap: Dictionary<string[]>
) {
const cmd = positional.cmd[0];
if (positional.variadic) {
}
// Based on parsing variadic markers '...', demand syntax '<foo>', etc.,
// populate parser hints:
public cmdToParseOptions(cmdString: string): Positionals {
array: [],
default: {},
alias: {},
demand: {},
};
const parsed = parseCommand(cmdString);
parsed.demanded.forEach(d => {
if (d.variadic) {
parseOptions.default[cmd] = [];
}
parseOptions.demand[cmd] = true;
parsed.optional.forEach(o => {
if (o.variadic) {
parseOptions.default[cmd] = [];
}
return parseOptions;
}
// we run yargs-parser against the positional arguments
// applying the same parsing logic used for flags.
private postProcessPositionals(
positionalMap: Dictionary<string[]>,
parseOptions: Positionals,
yargs: YargsInstance
) {
// combine the parsing hints we've inferred from the command
// string with explicitly configured parsing hints.
const options = Object.assign({}, yargs.getOptions());
options.default = Object.assign(parseOptions.default, options.default);
for (const key of Object.keys(parseOptions.alias)) {
parseOptions.alias[key]
);
}
options.config = {}; // don't load config when processing positionals.
const unparsed: string[] = [];
Object.keys(positionalMap).forEach(key => {
options.key[key] = true;
unparsed.push(`--${key}`);
unparsed.push(value);
// short-circuit parse.
if (!unparsed.length) return;
const config: Configuration = Object.assign({}, options.configuration, {
'populate--': false,
});
const parsed = this.shim.Parser.detailed(
unparsed,
Object.assign({}, options, {
configuration: config,
})
);
if (parsed.error) {
.getInternalMethods()
.getUsageInstance()
.fail(parsed.error.message, parsed.error);
// flag arguments that were already parsed).
const positionalKeys = Object.keys(positionalMap);
Object.keys(positionalMap).forEach(key => {
positionalKeys.push(...parsed.aliases[key]);
});
Object.keys(parsed.argv).forEach(key => {
if (positionalKeys.includes(key)) {
// any new aliases need to be placed in positionalMap, which
// is used for validation.
if (!positionalMap[key]) positionalMap[key] = parsed.argv[key];
// Addresses: https://github.com/yargs/yargs/issues/1637
// If both positionals/options provided,
// and no default or config values were set for that key,
// and if at least one is an array: don't overwrite, combine.
if (
!this.isInConfigs(yargs, key) &&
!this.isDefaulted(yargs, key) &&
Object.prototype.hasOwnProperty.call(argv, key) &&
Object.prototype.hasOwnProperty.call(parsed.argv, key) &&
}
});
}
isDefaulted(yargs: YargsInstance, key: string): boolean {
return (
Object.prototype.hasOwnProperty.call(defaults, key) ||
defaults,
this.shim.Parser.camelCase(key)
);
}
isInConfigs(yargs: YargsInstance, key: string): boolean {
return (
configObjects.some(c => Object.prototype.hasOwnProperty.call(c, key)) ||
configObjects.some(c =>
);
}
const commandString = DEFAULT_MARKER.test(this.defaultCommand.original)
? this.defaultCommand.original
: this.defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ');
yargs
.getInternalMethods()
.getUsageInstance()
.usage(commandString, this.defaultCommand.description);
}
if (isCommandBuilderCallback(builder)) {
}
return undefined;
}
private extractDesc({describe, description, desc}: CommandHandlerDefinition) {
if (typeof test === 'string' || test === false) return test;
}
}
freeze() {
handlers: this.handlers,
aliasMap: this.aliasMap,
defaultCommand: this.defaultCommand,
});
}
assertNotStrictEqual(frozen, undefined, this.shim);
({
handlers: this.handlers,
aliasMap: this.aliasMap,
defaultCommand: this.defaultCommand,
} = frozen);
}
reset(): CommandInstance {
this.aliasMap = {};
this.defaultCommand = undefined;
this.requireCache = new Set();
return this;
}
// Adds support to yargs for lazy loading a hierarchy of commands:
export function command(
validation: ValidationInstance,
globalMiddleware: GlobalMiddleware,
shim: PlatformShim
) {
return new CommandInstance(usage, validation, globalMiddleware, shim);
}
export interface CommandHandlerDefinition extends Partial<
Pick<CommandHandler, 'deprecated' | 'description' | 'handler' | 'middlewares'>
> {
aliases?: string[];
builder?: CommandBuilder | CommandBuilderDefinition;
command?: string | string[];
desc?: CommandHandler['description'];
describe?: CommandHandler['description'];
}
export interface CommandBuilderDefinition {
builder?: CommandBuilder;
deprecated?: boolean;
handler: CommandHandlerCallback;
middlewares?: Middleware[];
}
export function isCommandBuilderDefinition(
): builder is CommandBuilderDefinition {
return (
typeof builder === 'object' &&
!!(builder as CommandBuilderDefinition).builder &&
}
export interface CommandHandlerCallback {
(argv: Arguments): any;
}
export interface CommandHandler {
builder: CommandBuilder;
demanded: Positional[];
deprecated?: boolean;
description?: string | false;
handler: CommandHandlerCallback;
middlewares: Middleware[];
optional: Positional[];
original: string;
}
// To be completed later with other CommandBuilder flavours
export type CommandBuilder =
CommandBuilderCallback | Dictionary<OptionDefinition>;
interface CommandBuilderCallback {
(y: YargsInstance, helpOrVersionSet: boolean): YargsInstance | void;
}
cmd: DefinitionOrCommandName[]
): cmd is [CommandHandlerDefinition, ...string[]] {
return cmd.every(c => typeof c === 'string');
}
export function isCommandBuilderCallback(
): builder is CommandBuilderCallback {
return typeof builder === 'function';
}
builder: CommandBuilder
): builder is Dictionary<OptionDefinition> {
return typeof builder === 'object';
}
export function isCommandHandlerDefinition(
): cmd is CommandHandlerDefinition {
return typeof cmd === 'object' && !Array.isArray(cmd);
}
interface Positionals extends Pick<Options, 'alias' | 'array' | 'default'> {
demand: Dictionary<boolean>;
}
type FrozenCommandInstance = {
handlers: Dictionary<CommandHandler>;
aliasMap: Dictionary<string>;
defaultCommand: CommandHandler | undefined;
};