From 7203893f5c0ec092bc56ff50b3fc4f3d64d53ebd Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 28 Mar 2021 11:49:03 -0700 Subject: [PATCH 1/6] refactor(yargs-factory)!: refactor yargs-factory to use class refactor!: methods that used _ to indicate privacy are now actually private --- index.cjs | 5 +- lib/command.ts | 16 +- lib/usage.ts | 15 +- lib/validation.ts | 2 +- lib/yargs-factory.ts | 2682 +++++++++++++++++++--------------------- package.json | 1 + test/command.cjs | 52 +- test/helpers/utils.cjs | 3 + 8 files changed, 1294 insertions(+), 1482 deletions(-) diff --git a/index.cjs b/index.cjs index 7ac4d3588..d5bccf820 100644 --- a/index.cjs +++ b/index.cjs @@ -22,7 +22,10 @@ function Argv(processArgs, cwd) { to get a parsed version of process.argv. */ function singletonify(inst) { - Object.keys(inst).forEach(key => { + [ + ...Object.keys(inst), + ...Object.getOwnPropertyNames(inst.constructor.prototype), + ].forEach(key => { if (key === 'argv') { Argv.__defineGetter__(key, inst.__lookupGetter__(key)); } else if (typeof inst[key] === 'function') { diff --git a/lib/command.ts b/lib/command.ts index 6dde6c6bc..9b237f5b9 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -319,7 +319,7 @@ export class CommandInstance { commandHandler.description ); } - const innerArgv = innerYargs._parseArgs( + const innerArgv = innerYargs.runYargsParserAndExecuteCommands( null, undefined, true, @@ -364,7 +364,7 @@ export class CommandInstance { // execute middleware or handlers (these may perform expensive operations // like creating a DB connection). if (helpOnly) return innerArgv; - if (!yargs._hasOutput()) { + if (!yargs.getHasOutput()) { positionalMap = this.populatePositionals( commandHandler, innerArgv as Arguments, @@ -380,8 +380,8 @@ export class CommandInstance { // we apply validation post-hoc, so that custom // checks get passed populated positional arguments. - if (!yargs._hasOutput()) { - const validation = yargs._runValidation( + if (!yargs.getHasOutput()) { + const validation = yargs.runValidation( aliases, positionalMap, (yargs.parsed as DetailedArguments).error, @@ -393,14 +393,14 @@ export class CommandInstance { }); } - if (commandHandler.handler && !yargs._hasOutput()) { - yargs._setHasOutput(); + if (commandHandler.handler && !yargs.getHasOutput()) { + yargs.setHasOutput(); // to simplify the parsing of positionals in commands, // we temporarily populate '--' rather than _, with arguments const populateDoubleDash = !!yargs.getOptions().configuration[ 'populate--' ]; - yargs._postProcess(innerArgv, populateDoubleDash, false, false); + yargs.postProcess(innerArgv, populateDoubleDash, false, false); innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false); innerArgv = maybeAsyncResult(innerArgv, result => { @@ -413,7 +413,7 @@ export class CommandInstance { }); yargs.getUsageInstance().cacheHelpMessage(); - if (isPromise(innerArgv) && !yargs._hasParseCallback()) { + if (isPromise(innerArgv) && !yargs.hasParseCallback()) { innerArgv.catch(error => { try { yargs.getUsageInstance().fail(null, error); diff --git a/lib/usage.ts b/lib/usage.ts index 5b4989674..3ea1b08d2 100644 --- a/lib/usage.ts +++ b/lib/usage.ts @@ -1,11 +1,6 @@ // this file handles outputting usage instructions, // failures, etc. keeps logging in one place. -import { - Dictionary, - assertNotStrictEqual, - PlatformShim, - Y18N, -} from './typings/common-types.js'; +import {Dictionary, PlatformShim, Y18N} from './typings/common-types.js'; import {objFilter} from './utils/obj-filter.js'; import {YargsInstance} from './yargs-factory.js'; import {YError} from './yerror.js'; @@ -42,7 +37,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { let failureOutput = false; self.fail = function fail(msg, err) { - const logger = yargs._getLoggerInstance(); + const logger = yargs.getLoggerInstance(); if (fails.length) { for (let i = fails.length - 1; i >= 0; --i) { @@ -74,7 +69,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { err = err || new YError(msg); if (yargs.getExitProcess()) { return yargs.exit(1); - } else if (yargs._hasParseCallback()) { + } else if (yargs.hasParseCallback()) { return yargs.exit(1, err); } else { throw err; @@ -603,7 +598,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { } self.showHelp = (level: 'error' | 'log' | ((message: string) => void)) => { - const logger = yargs._getLoggerInstance(); + const logger = yargs.getLoggerInstance(); if (!level) level = 'error'; const emit = typeof level === 'function' ? level : logger[level]; emit(self.help()); @@ -675,7 +670,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { }; self.showVersion = level => { - const logger = yargs._getLoggerInstance(); + const logger = yargs.getLoggerInstance(); if (!level) level = 'error'; const emit = typeof level === 'function' ? level : logger[level]; emit(version); diff --git a/lib/validation.ts b/lib/validation.ts index 871d4784a..6cab24430 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -154,7 +154,7 @@ export function validation( if ( specialKeys.indexOf(key) === -1 && !Object.prototype.hasOwnProperty.call(positionalMap, key) && - !Object.prototype.hasOwnProperty.call(yargs._getParseContext(), key) && + !Object.prototype.hasOwnProperty.call(yargs.getParseContext(), key) && !self.isValidAndSomeAliasIsNotNew(key, aliases) ) { unknown.push(key); diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 50f310560..7df5732b0 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -22,6 +22,7 @@ import type { RequireDirectoryOptions, PlatformShim, RequireType, + Y18N, } from './typings/common-types.js'; import { assertNotStrictEqual, @@ -64,76 +65,145 @@ import setBlocking from './utils/set-blocking.js'; let shim: PlatformShim; export function YargsWithShim(_shim: PlatformShim) { shim = _shim; - return Yargs; + return YargsFactory; } -function Yargs( - processArgs: string | string[] = [], - cwd = shim.process.cwd(), - parentRequire?: RequireType -): YargsInstance { - const self = {} as YargsInstance; - let command: CommandInstance; - let completion: CompletionInstance | null = null; - let groups: Dictionary = {}; - let output = ''; - const preservedGroups: Dictionary = {}; - const globalMiddleware = new GlobalMiddleware(self); - let usage: UsageInstance; - let validation: ValidationInstance; - - const y18n = shim.y18n; - - self.scriptName = function (scriptName) { - self.customScriptName = true; - self.$0 = scriptName; - return self; - }; - - // ignore the node bin, specify this in your - // bin file with #!/usr/bin/env node - let default$0: string[]; - if (/\b(node|iojs|electron)(\.exe)?$/.test(shim.process.argv()[0])) { - default$0 = shim.process.argv().slice(1, 2); - } else { - default$0 = shim.process.argv().slice(0, 1); - } - - self.$0 = default$0 - .map(x => { - const b = rebase(cwd, x); - return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x; - }) - .join(' ') - .trim(); - - if (shim.getEnv('_') && shim.getProcessArgvBin() === shim.getEnv('_')) { - self.$0 = shim - .getEnv('_')! - .replace(`${shim.path.dirname(shim.process.execPath())}/`, ''); - } +export class YargsInstance { + $0: string; + argv?: Arguments; + customScriptName = false; + parsed: DetailedArguments | false = false; + #command?: CommandInstance; + #cwd: string; // use context object to keep track of resets, subcommand execution, etc., // submodules should modify and check the state of context as necessary: - const context = {commands: [], fullCommands: []}; - self.getContext = () => context; - - let hasOutput = false; - let exitError: YError | string | undefined | null = null; - // maybe exit, always capture - // context about why we wanted to exit. - self.exit = (code, err) => { - hasOutput = true; - exitError = err; - if (exitProcess) shim.process.exit(code); - }; - - let completionCommand: string | null = null; - self.completion = function ( + #context: Context = {commands: [], fullCommands: []}; + #completion?: CompletionInstance | null = null; + #completionCommand: string | null = null; + #defaultShowHiddenOpt = 'show-hidden'; + #exitError: YError | string | undefined | null = null; + #detectLocale = true; + #exitProcess = true; + #frozens: FrozenYargsInstance[] = []; + #globalMiddleware: GlobalMiddleware; + #groups: Dictionary = {}; + #hasOutput = false; + #helpOpt: string | null = null; + #logger: LoggerInstance; + #output = ''; + #options?: Options; + #parentRequire?: RequireType; + #parserConfig: Configuration = {}; + #parseFn: ParseCallback | null = null; + #parseContext: object | null = null; + #pkgs: Dictionary<{[key: string]: string | {[key: string]: string}}> = {}; + #preservedGroups: Dictionary = {}; + #processArgs: string | string[]; + #recommendCommands = false; + #strict = false; + #strictCommands = false; + #strictOptions = false; + #usage?: UsageInstance; + #versionOpt: string | null = null; + #validation?: ValidationInstance; + #y18n: Y18N; + + constructor( + processArgs: string | string[] = [], + cwd: string, + parentRequire: RequireType | undefined + ) { + this.#processArgs = processArgs; + this.#cwd = cwd; + this.#parentRequire = parentRequire; + this.#globalMiddleware = new GlobalMiddleware(this); + this.$0 = this.getDollarZero(); + this.#y18n = shim.y18n; + // TODO(@bcoe): make reset initialize dependent classes so ! is not required. + this.reset(); + this.#options!.showHiddenOpt = this.#defaultShowHiddenOpt; + this.#logger = this.createLogger(); + } + // TODO(bcoe): arrange public methods alphabetically: + addHelpOpt(opt?: string | false, msg?: string): YargsInstance { + const defaultHelpOpt = 'help'; + argsert('[string|boolean] [string]', [opt, msg], arguments.length); + + // nuke the key previously configured + // to return help. + if (this.#helpOpt) { + this.deleteFromParserHintObject(this.#helpOpt); + this.#helpOpt = null; + } + + if (opt === false && msg === undefined) return this; + + // use arguments, fallback to defaults for opt and msg + this.#helpOpt = typeof opt === 'string' ? opt : defaultHelpOpt; + this.boolean(this.#helpOpt); + this.describe( + this.#helpOpt, + msg || this.#usage!.deferY18nLookup('Show help') + ); + return this; + } + help(opt?: string, msg?: string): YargsInstance { + return this.addHelpOpt(opt, msg); + } + addShowHiddenOpt(opt?: string | false, msg?: string): YargsInstance { + argsert('[string|boolean] [string]', [opt, msg], arguments.length); + if (opt === false && msg === undefined) return this; + const showHiddenOpt = + typeof opt === 'string' ? opt : this.#defaultShowHiddenOpt; + this.boolean(showHiddenOpt); + this.describe( + showHiddenOpt, + msg || this.#usage!.deferY18nLookup('Show hidden options') + ); + this.#options!.showHiddenOpt = showHiddenOpt; + return this; + } + showHidden(opt?: string | false, msg?: string): YargsInstance { + return this.addShowHiddenOpt(opt, msg); + } + boolean(keys: string | string[]): YargsInstance { + argsert('', [keys], arguments.length); + this.populateParserHintArray('boolean', keys); + return this; + } + describe( + keys: string | string[] | Dictionary, + description?: string + ): YargsInstance { + argsert( + ' [string]', + [keys, description], + arguments.length + ); + this.setKey(keys, true); + this.#usage!.describe(keys, description); + return this; + } + scriptName(scriptName: string): YargsInstance { + this.customScriptName = true; + this.$0 = scriptName; + return this; + } + getContext(): Context { + return this.#context; + } + // maybe exit, always capture context about why we wanted to exit: + exit(code: number, err?: YError | string): void { + this.#hasOutput = true; + this.#exitError = err; + if (this.#exitProcess) shim.process.exit(code); + } + completion( cmd?: string, desc?: string | false | CompletionFunction, fn?: CompletionFunction - ) { + ): YargsInstance { argsert( '[string] [string|boolean|function] [function]', [cmd, desc, fn], @@ -149,282 +219,191 @@ function Yargs( } // register the completion command. - completionCommand = cmd || completionCommand || 'completion'; + this.#completionCommand = cmd || this.#completionCommand || 'completion'; if (!desc && desc !== false) { desc = 'generate completion script'; } - self.command(completionCommand, desc); + this.command(this.#completionCommand, desc); // a function can be provided - if (fn) completion!.registerFunction(fn); - - return self; - }; - - // puts yargs back into an initial state. any keys - // that have been set to "global" will not be reset - // by this action. - let options: Options; - self.resetOptions = self.reset = function resetOptions(aliases = {}) { - options = options || {}; - // put yargs back into an initial state, this - // logic is used to build a nested command - // hierarchy. - const tmpOptions = {} as Options; - tmpOptions.local = options.local ? options.local : []; - tmpOptions.configObjects = options.configObjects - ? options.configObjects - : []; - - // if a key has been explicitly set as local, - // we should reset it before passing options to command. - const localLookup: Dictionary = {}; - tmpOptions.local.forEach(l => { - localLookup[l] = true; - (aliases[l] || []).forEach(a => { - localLookup[a] = true; - }); - }); + if (fn) this.#completion!.registerFunction(fn); - // add all groups not set to local to preserved groups - Object.assign( - preservedGroups, - Object.keys(groups).reduce((acc, groupName) => { - const keys = groups[groupName].filter(key => !(key in localLookup)); - if (keys.length > 0) { - acc[groupName] = keys; - } - return acc; - }, {} as Dictionary) + return this; + } + command( + cmd: string | CommandHandlerDefinition | DefinitionOrCommandName[], + description?: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback, + middlewares?: Middleware[], + deprecated?: boolean + ): YargsInstance { + argsert( + ' [string|boolean] [function|object] [function] [array] [boolean|string]', + [cmd, description, builder, handler, middlewares, deprecated], + arguments.length ); - // groups can now be reset - groups = {}; - - const arrayOptions: KeyOf[] = [ - 'array', - 'boolean', - 'string', - 'skipValidation', - 'count', - 'normalize', - 'number', - 'hiddenOptions', - ]; - - const objectOptions: DictionaryKeyof[] = [ - 'narg', - 'key', - 'alias', - 'default', - 'defaultDescription', - 'config', - 'choices', - 'demandedOptions', - 'demandedCommands', - 'deprecatedOptions', - ]; - - arrayOptions.forEach(k => { - tmpOptions[k] = (options[k] || []).filter((k: string) => !localLookup[k]); - }); - - objectOptions.forEach(>(k: K) => { - tmpOptions[k] = objFilter(options[k], k => !localLookup[k as string]); - }); - - tmpOptions.envPrefix = options.envPrefix; - options = tmpOptions; - - // if this is the first time being executed, create - // instances of all our helpers -- otherwise just reset. - usage = usage ? usage.reset(localLookup) : Usage(self, y18n, shim); - validation = validation - ? validation.reset(localLookup) - : Validation(self, usage, y18n, shim); - command = command - ? command.reset() - : Command(usage, validation, globalMiddleware, shim); - if (!completion) completion = Completion(self, usage, command, shim); - globalMiddleware.reset(); - - completionCommand = null; - output = ''; - exitError = null; - hasOutput = false; - self.parsed = false; - - return self; - }; - self.resetOptions(); - - // temporary hack: allow "freezing" of reset-able state for parse(msg, cb) - const frozens: FrozenYargsInstance[] = []; - function freeze() { - frozens.push({ - options, - configObjects: options.configObjects.slice(0), - exitProcess, - groups, - strict, - strictCommands, - strictOptions, - completionCommand, - output, - exitError, - hasOutput, - parsed: self.parsed, - parseFn, - parseContext, - }); - usage.freeze(); - validation.freeze(); - command.freeze(); - globalMiddleware.freeze(); + this.#command!.addHandler( + cmd, + description, + builder, + handler, + middlewares, + deprecated + ); + return this; } - function unfreeze() { - const frozen = frozens.pop(); - assertNotStrictEqual(frozen, undefined, shim); - let configObjects: Dictionary[]; - ({ - options, - configObjects, - exitProcess, - groups, - output, - exitError, - hasOutput, - parsed: self.parsed, - strict, - strictCommands, - strictOptions, - completionCommand, - parseFn, - parseContext, - } = frozen); - options.configObjects = configObjects; - usage.unfreeze(); - validation.unfreeze(); - command.unfreeze(); - globalMiddleware.unfreeze(); + commands( + cmd: string | CommandHandlerDefinition | DefinitionOrCommandName[], + description?: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback, + middlewares?: Middleware[], + deprecated?: boolean + ): YargsInstance { + return this.command( + cmd, + description, + builder, + handler, + middlewares, + deprecated + ); } - - self.boolean = function (keys) { - argsert('', [keys], arguments.length); - populateParserHintArray('boolean', keys); - return self; - }; - - self.array = function (keys) { - argsert('', [keys], arguments.length); - populateParserHintArray('array', keys); - return self; - }; - - self.number = function (keys) { + commandDir(dir: string, opts?: RequireDirectoryOptions): YargsInstance { + argsert(' [object]', [dir, opts], arguments.length); + const req = this.#parentRequire || shim.require; + this.#command!.addDirectory(dir, req, shim.getCallerFile(), opts); + return this; + } + async getCompletion( + args: string[], + done?: (err: Error | null, completions: string[] | undefined) => void + ): Promise { + argsert(' [function]', [args, done], arguments.length); + if (!done) { + return new Promise((resolve, reject) => { + this.#completion!.getCompletion(args, (err, completions) => { + if (err) reject(err); + else resolve(completions); + }); + }); + } else { + return this.#completion!.getCompletion(args, done); + } + } + getParserConfiguration() { + return this.#parserConfig; + } + parserConfiguration(config: Configuration) { + argsert('', [config], arguments.length); + this.#parserConfig = config; + return this; + } + array(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - populateParserHintArray('number', keys); - return self; - }; - - self.normalize = function (keys) { + this.populateParserHintArray('array', keys); + return this; + } + number(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - populateParserHintArray('normalize', keys); - return self; - }; - - self.count = function (keys) { + this.populateParserHintArray('number', keys); + return this; + } + normalize(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - populateParserHintArray('count', keys); - return self; - }; - - self.string = function (keys) { + this.populateParserHintArray('normalize', keys); + return this; + } + count(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - populateParserHintArray('string', keys); - return self; - }; - - self.requiresArg = function (keys) { + this.populateParserHintArray('count', keys); + return this; + } + string(key: string | string[]): YargsInstance { + argsert('', [key], arguments.length); + this.populateParserHintArray('string', key); + return this; + } + requiresArg(keys: string | string[] | Dictionary): YargsInstance { // the 2nd paramter [number] in the argsert the assertion is mandatory // as populateParserHintSingleValueDictionary recursively calls requiresArg // with Nan as a 2nd parameter, although we ignore it argsert(' [number]', [keys], arguments.length); // If someone configures nargs at the same time as requiresArg, - // nargs should take precedent, + // nargs should take precedence, // see: https://github.com/yargs/yargs/pull/1572 // TODO: make this work with aliases, using a check similar to // checkAllAliases() in yargs-parser. - if (typeof keys === 'string' && options.narg[keys]) { - return self; + if (typeof keys === 'string' && this.#options!.narg[keys]) { + return this; } else { - populateParserHintSingleValueDictionary( - self.requiresArg, + this.populateParserHintSingleValueDictionary( + this.requiresArg.bind(this), 'narg', keys, NaN ); } - return self; - }; - - self.skipValidation = function (keys) { + return this; + } + skipValidation(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - populateParserHintArray('skipValidation', keys); - return self; - }; - - function populateParserHintArray>( - type: T, - keys: string | string[] - ) { - keys = ([] as string[]).concat(keys); - keys.forEach(key => { - key = sanitizeKey(key); - options[type].push(key); - }); + this.populateParserHintArray('skipValidation', keys); + return this; } - - self.nargs = function ( + nargs( key: string | string[] | Dictionary, value?: number - ) { + ): YargsInstance { argsert(' [number]', [key, value], arguments.length); - populateParserHintSingleValueDictionary(self.nargs, 'narg', key, value); - return self; - }; - - self.choices = function ( + this.populateParserHintSingleValueDictionary( + this.nargs.bind(this), + 'narg', + key, + value + ); + return this; + } + choices( key: string | string[] | Dictionary, value?: string | string[] - ) { + ): YargsInstance { argsert( ' [string|array]', [key, value], arguments.length ); - populateParserHintArrayDictionary(self.choices, 'choices', key, value); - return self; - }; - - self.alias = function ( + this.populateParserHintArrayDictionary( + this.choices.bind(this), + 'choices', + key, + value + ); + return this; + } + alias( key: string | string[] | Dictionary, value?: string | string[] - ) { + ): YargsInstance { argsert( ' [string|array]', [key, value], arguments.length ); - populateParserHintArrayDictionary(self.alias, 'alias', key, value); - return self; - }; - - // TODO: actually deprecate self.defaults. - self.default = self.defaults = function ( + this.populateParserHintArrayDictionary( + this.alias.bind(this), + 'alias', + key, + value + ); + return this; + } + default( key: string | string[] | Dictionary, value?: any, defaultDescription?: string - ) { + ): YargsInstance { argsert( ' [*] [string]', [key, value, defaultDescription], @@ -432,60 +411,48 @@ function Yargs( ); if (defaultDescription) { assertSingleKey(key, shim); - options.defaultDescription[key] = defaultDescription; + this.#options!.defaultDescription[key] = defaultDescription; } if (typeof value === 'function') { assertSingleKey(key, shim); - if (!options.defaultDescription[key]) - options.defaultDescription[key] = usage.functionDescription(value); + if (!this.#options!.defaultDescription[key]) + this.#options!.defaultDescription[ + key + ] = this.#usage!.functionDescription(value); value = value.call(); } - populateParserHintSingleValueDictionary<'default'>( - self.default, + this.populateParserHintSingleValueDictionary<'default'>( + this.default.bind(this), 'default', key, value ); - return self; - }; - - self.describe = function ( - key: string | string[] | Dictionary, - desc?: string - ) { - argsert(' [string]', [key, desc], arguments.length); - setKey(key, true); - usage.describe(key, desc); - return self; - }; - - function setKey( - key: string | string[] | Dictionary, - set?: boolean | string - ) { - populateParserHintSingleValueDictionary(setKey, 'key', key, set); - return self; + return this; } - - function demandOption( + defaults( + key: string | string[] | Dictionary, + value?: any, + defaultDescription?: string + ): YargsInstance { + return this.default(key, value, defaultDescription); + } + demandOption( keys: string | string[] | Dictionary, msg?: string - ) { + ): YargsInstance { argsert(' [string]', [keys, msg], arguments.length); - populateParserHintSingleValueDictionary( - self.demandOption, + this.populateParserHintSingleValueDictionary( + this.demandOption.bind(this), 'demandedOptions', keys, msg ); - return self; + return this; } - self.demandOption = demandOption; - - self.coerce = function ( + coerce( keys: string | string[] | Dictionary, value?: CoerceCallback - ) { + ): YargsInstance { argsert( ' [function]', [keys, value], @@ -496,14 +463,14 @@ function Yargs( throw new YError('coerce callback must be provided'); } for (const key of keys) { - self.coerce(key, value); + this.coerce(key, value); } - return self; + return this; } else if (typeof keys === 'object') { for (const key of Object.keys(keys)) { - self.coerce(key, keys[key]); + this.coerce(key, keys[key]); } - return self; + return this; } if (!value) { throw new YError('coerce callback must be provided'); @@ -511,8 +478,8 @@ function Yargs( // This noop tells yargs-parser about the existence of the option // represented by "keys", so that it can apply camel case expansion // if needed: - self.alias(keys, keys); - globalMiddleware.addCoerceMiddleware( + this.alias(keys, keys); + this.#globalMiddleware.addCoerceMiddleware( ( argv: Arguments, yargs: YargsInstance @@ -541,211 +508,77 @@ function Yargs( }, keys ); - return self; - }; + return this; + } + getAliases(): Dictionary { + return this.parsed ? this.parsed.aliases : {}; + } + config( + key: string | string[] | Dictionary = 'config', + msg?: string | ConfigCallback, + parseFn?: ConfigCallback + ): YargsInstance { + argsert( + '[object|string] [string|function] [function]', + [key, msg, parseFn], + arguments.length + ); + // allow a config object to be provided directly. + if (typeof key === 'object' && !Array.isArray(key)) { + key = applyExtends( + key, + this.#cwd, + this.getParserConfiguration()['deep-merge-config'] || false, + shim + ); + this.#options!.configObjects = ( + this.#options!.configObjects || [] + ).concat(key); + return this; + } - function populateParserHintSingleValueDictionary< - T extends - | Exclude, DictionaryKeyof> - | 'default', - K extends keyof Options[T] & string = keyof Options[T] & string, - V extends ValueOf = ValueOf - >( - builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, - type: T, - key: K | K[] | {[key in K]: V}, - value?: V - ) { - populateParserHintDictionary( - builder, - type, + // allow for a custom parsing function. + if (typeof msg === 'function') { + parseFn = msg; + msg = undefined; + } + + this.describe( key, - value, - (type, key, value) => { - options[type][key] = value as ValueOf; - } + msg || this.#usage!.deferY18nLookup('Path to JSON config file') ); + (Array.isArray(key) ? key : [key]).forEach(k => { + this.#options!.config[k] = parseFn || true; + }); + + return this; } - - function populateParserHintArrayDictionary< - T extends DictionaryKeyof, - K extends keyof Options[T] & string = keyof Options[T] & string, - V extends ValueOf> | ValueOf>[] = - | ValueOf> - | ValueOf>[] - >( - builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, - type: T, - key: K | K[] | {[key in K]: V}, - value?: V - ) { - populateParserHintDictionary( - builder, - type, - key, - value, - (type, key, value) => { - options[type][key] = ( - options[type][key] || ([] as Options[T][keyof Options[T]]) - ).concat(value); - } - ); - } - - function populateParserHintDictionary< - T extends keyof Options, - K extends keyof Options[T], - V - >( - builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, - type: T, - key: K | K[] | {[key in K]: V}, - value: V | undefined, - singleKeyHandler: (type: T, key: K, value?: V) => void - ) { - if (Array.isArray(key)) { - // an array of keys with one value ['x', 'y', 'z'], function parse () {} - key.forEach(k => { - builder(k, value!); - }); - } else if ( - ((key): key is {[key in K]: V} => typeof key === 'object')(key) - ) { - // an object of key value pairs: {'x': parse () {}, 'y': parse() {}} - for (const k of objectKeys(key)) { - builder(k, key[k]); - } - } else { - singleKeyHandler(type, sanitizeKey(key), value); - } - } - - // TODO(bcoe): in future major versions move more objects towards - // Object.create(null): - function sanitizeKey(key: K): K; - function sanitizeKey(key: '__proto__'): '___proto___'; - function sanitizeKey(key: any) { - if (key === '__proto__') return '___proto___'; - return key; - } - - function deleteFromParserHintObject(optionKey: string) { - // delete from all parsing hints: - // boolean, array, key, alias, etc. - objectKeys(options).forEach((hintKey: keyof Options) => { - // configObjects is not a parsing hint array - if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) - return; - const hint = options[hintKey]; - if (Array.isArray(hint)) { - if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1); - } else if (typeof hint === 'object') { - delete (hint as Dictionary)[optionKey]; - } - }); - // now delete the description from usage.js. - delete usage.getDescriptions()[optionKey]; - } - - self.config = function config( - key: string | string[] | Dictionary = 'config', - msg?: string | ConfigCallback, - parseFn?: ConfigCallback - ) { - argsert( - '[object|string] [string|function] [function]', - [key, msg, parseFn], - arguments.length - ); - // allow a config object to be provided directly. - if (typeof key === 'object' && !Array.isArray(key)) { - key = applyExtends( - key, - cwd, - self.getParserConfiguration()['deep-merge-config'] || false, - shim - ); - options.configObjects = (options.configObjects || []).concat(key); - return self; - } - - // allow for a custom parsing function. - if (typeof msg === 'function') { - parseFn = msg; - msg = undefined; - } - - self.describe( - key, - msg || usage.deferY18nLookup('Path to JSON config file') - ); - (Array.isArray(key) ? key : [key]).forEach(k => { - options.config[k] = parseFn || true; - }); - - return self; - }; - - self.example = function ( - cmd: string | [string, string?][], - description?: string - ) { - argsert(' [string]', [cmd, description], arguments.length); + example( + cmd: string | [string, string?][], + description?: string + ): YargsInstance { + argsert(' [string]', [cmd, description], arguments.length); if (Array.isArray(cmd)) { - cmd.forEach(exampleParams => self.example(...exampleParams)); + cmd.forEach(exampleParams => this.example(...exampleParams)); } else { - usage.example(cmd, description); + this.#usage!.example(cmd, description); } - return self; - }; - - self.command = self.commands = function ( - cmd: string | CommandHandlerDefinition | DefinitionOrCommandName[], - description?: CommandHandler['description'], - builder?: CommandBuilderDefinition | CommandBuilder, - handler?: CommandHandlerCallback, - middlewares?: Middleware[], - deprecated?: boolean - ) { - argsert( - ' [string|boolean] [function|object] [function] [array] [boolean|string]', - [cmd, description, builder, handler, middlewares, deprecated], - arguments.length - ); - command.addHandler( - cmd, - description, - builder, - handler, - middlewares, - deprecated - ); - return self; - }; - - self.commandDir = function (dir, opts) { - argsert(' [object]', [dir, opts], arguments.length); - const req = parentRequire || shim.require; - command.addDirectory(dir, req, shim.getCallerFile(), opts); - return self; - }; - - // TODO: deprecate self.demand in favor of - // .demandCommand() .demandOption(). - self.demand = self.required = self.require = function demand( + return this; + } + demand( keys: string | string[] | Dictionary | number, max?: number | string[] | string | true, msg?: string | true - ) { + ): YargsInstance { // you can optionally provide a 'max' key, // which will raise an exception if too many '_' // options are provided. if (Array.isArray(max)) { max.forEach(key => { assertNotStrictEqual(msg, true as const, shim); - demandOption(key, msg); + this.demandOption(key, msg); }); max = Infinity; } else if (typeof max !== 'number') { @@ -755,29 +588,42 @@ function Yargs( if (typeof keys === 'number') { assertNotStrictEqual(msg, true as const, shim); - self.demandCommand(keys, max, msg, msg); + this.demandCommand(keys, max, msg, msg); } else if (Array.isArray(keys)) { keys.forEach(key => { assertNotStrictEqual(msg, true as const, shim); - demandOption(key, msg); + this.demandOption(key, msg); }); } else { if (typeof msg === 'string') { - demandOption(keys, msg); + this.demandOption(keys, msg); } else if (msg === true || typeof msg === 'undefined') { - demandOption(keys); + this.demandOption(keys); } } - return self; - }; - - self.demandCommand = function demandCommand( + return this; + } + required( + keys: string | string[] | Dictionary | number, + max?: number | string[] | string | true, + msg?: string | true + ): YargsInstance { + return this.demand(keys, max, msg); + } + require( + keys: string | string[] | Dictionary | number, + max?: number | string[] | string | true, + msg?: string | true + ): YargsInstance { + return this.demand(keys, max, msg); + } + demandCommand( min = 1, max?: number | string, minMsg?: string | null, maxMsg?: string | null - ) { + ): YargsInstance { argsert( '[number] [number|string] [string|null|undefined] [string|null|undefined]', [min, max, minMsg, maxMsg], @@ -789,71 +635,75 @@ function Yargs( max = Infinity; } - self.global('_', false); + this.global('_', false); - options.demandedCommands._ = { + this.#options!.demandedCommands._ = { min, max, minMsg, maxMsg, }; - return self; - }; - - self.getAliases = () => { - return self.parsed ? self.parsed.aliases : {}; - }; - - self.getDemandedOptions = () => { + return this; + } + global(globals: string | string[], global?: boolean): YargsInstance { + argsert(' [boolean]', [globals, global], arguments.length); + globals = ([] as string[]).concat(globals); + if (global !== false) { + this.#options!.local = this.#options!.local.filter( + l => globals.indexOf(l) === -1 + ); + } else { + globals.forEach(g => { + if (this.#options!.local.indexOf(g) === -1) + this.#options!.local.push(g); + }); + } + return this; + } + getDemandedOptions() { argsert([], 0); - return options.demandedOptions; - }; - - self.getDemandedCommands = () => { + return this.#options!.demandedOptions; + } + getDemandedCommands() { argsert([], 0); - return options.demandedCommands; - }; - - self.deprecateOption = function deprecateOption(option, message) { + return this.#options!.demandedCommands; + } + deprecateOption(option: string, message: string | boolean): YargsInstance { argsert(' [string|boolean]', [option, message], arguments.length); - options.deprecatedOptions[option] = message; - return self; - }; - - self.getDeprecatedOptions = () => { + this.#options!.deprecatedOptions[option] = message; + return this; + } + getDeprecatedOptions() { argsert([], 0); - return options.deprecatedOptions; - }; - - self.implies = function ( + return this.#options!.deprecatedOptions; + } + implies( key: string | Dictionary, value?: KeyOrPos | KeyOrPos[] - ) { + ): YargsInstance { argsert( ' [number|string|array]', [key, value], arguments.length ); - validation.implies(key, value); - return self; - }; - - self.conflicts = function ( + this.#validation!.implies(key, value); + return this; + } + conflicts( key1: string | Dictionary, key2?: string | string[] - ) { + ): YargsInstance { argsert(' [string|array]', [key1, key2], arguments.length); - validation.conflicts(key1, key2); - return self; - }; - - self.usage = function ( + this.#validation!.conflicts(key1, key2); + return this; + } + usage( msg: string | null, description?: CommandHandler['description'], builder?: CommandBuilderDefinition | CommandBuilder, handler?: CommandHandlerCallback - ) { + ): YargsInstance { argsert( ' [string|boolean] [function|object] [function]', [msg, description, builder, handler], @@ -865,38 +715,38 @@ function Yargs( // .usage() can be used as an alias for defining // a default command. if ((msg || '').match(/^\$0( |$)/)) { - return self.command(msg, description, builder, handler); + return this.command(msg, description, builder, handler); } else { throw new YError( '.usage() description must start with $0 if being used as alias for .command()' ); } } else { - usage.usage(msg); - return self; + this.#usage!.usage(msg); + return this; } - }; - - self.epilogue = self.epilog = function (msg) { + } + epilogue(msg: string): YargsInstance { argsert('', [msg], arguments.length); - usage.epilog(msg); - return self; - }; - - self.fail = function (f) { + this.#usage!.epilog(msg); + return this; + } + epilog(msg: string): YargsInstance { + return this.epilogue(msg); + } + fail(f: FailureFunction | boolean): YargsInstance { argsert('', [f], arguments.length); if (typeof f === 'boolean' && f !== false) { throw new YError( "Invalid first argument. Expected function or boolean 'false'" ); } - usage.failFn(f); - return self; - }; - - self.check = function (f, global) { + this.#usage!.failFn(f); + return this; + } + check(f: (argv: Arguments) => any, global?: boolean): YargsInstance { argsert(' [boolean]', [f, global], arguments.length); - self.middleware( + this.middleware( ( argv: Arguments, _yargs: YargsInstance @@ -909,14 +759,16 @@ function Yargs( }, (result: any): Partial | Promise> => { if (!result) { - usage.fail(y18n.__('Argument check failed: %s', f.toString())); + this.#usage!.fail( + this.#y18n.__('Argument check failed: %s', f.toString()) + ); } else if (typeof result === 'string' || result instanceof Error) { - usage.fail(result.toString(), result); + this.#usage!.fail(result.toString(), result); } return argv; }, (err: Error): Partial | Promise> => { - usage.fail(err.message ? err.message : err.toString(), err); + this.#usage!.fail(err.message ? err.message : err.toString(), err); return argv; } ); @@ -924,269 +776,198 @@ function Yargs( false, global ); - return self; - }; - - self.middleware = ( + return this; + } + middleware( callback: MiddlewareCallback | MiddlewareCallback[], applyBeforeValidation?: boolean, - global = true - ) => { - return globalMiddleware.addMiddleware( + global?: boolean + ): YargsInstance { + return this.#globalMiddleware.addMiddleware( callback, !!applyBeforeValidation, global ); - }; - - self.global = function global(globals, global) { - argsert(' [boolean]', [globals, global], arguments.length); - globals = ([] as string[]).concat(globals); - if (global !== false) { - options.local = options.local.filter(l => globals.indexOf(l) === -1); - } else { - globals.forEach(g => { - if (options.local.indexOf(g) === -1) options.local.push(g); - }); - } - return self; - }; - - self.pkgConf = function pkgConf(key, rootPath) { + } + pkgConf(key: string, rootPath?: string): YargsInstance { argsert(' [string]', [key, rootPath], arguments.length); let conf = null; // prefer cwd to require-main-filename in this method // since we're looking for e.g. "nyc" config in nyc consumer // rather than "yargs" config in nyc (where nyc is the main filename) - const obj = pkgUp(rootPath || cwd); + const obj = this.pkgUp(rootPath || this.#cwd); // If an object exists in the key, add it to options.configObjects if (obj[key] && typeof obj[key] === 'object') { conf = applyExtends( - obj[key], - rootPath || cwd, - self.getParserConfiguration()['deep-merge-config'] || false, + obj[key] as {[key: string]: string}, + rootPath || this.#cwd, + this.getParserConfiguration()['deep-merge-config'] || false, shim ); - options.configObjects = (options.configObjects || []).concat(conf); + this.#options!.configObjects = ( + this.#options!.configObjects || [] + ).concat(conf); } - - return self; - }; - const pkgs: Dictionary = {}; - function pkgUp(rootPath?: string) { - const npath = rootPath || '*'; - if (pkgs[npath]) return pkgs[npath]; - - let obj = {}; - try { - let startDir = rootPath || shim.mainFilename; - - // When called in an environment that lacks require.main.filename, such as a jest test runner, - // startDir is already process.cwd(), and should not be shortened. - // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. - if (!rootPath && shim.path.extname(startDir)) { - startDir = shim.path.dirname(startDir); - } - - const pkgJsonPath = shim.findUp( - startDir, - (dir: string[], names: string[]) => { - if (names.includes('package.json')) { - return 'package.json'; - } else { - return undefined; - } - } - ); - assertNotStrictEqual(pkgJsonPath, undefined, shim); - obj = JSON.parse(shim.readFileSync(pkgJsonPath, 'utf8')); - // eslint-disable-next-line no-empty - } catch (_noop) {} - - pkgs[npath] = obj || {}; - return pkgs[npath]; + return this; } - - let parseFn: ParseCallback | null = null; - let parseContext: object | null = null; - self.parse = function parse( - args?: string | string[], - shortCircuit?: object | ParseCallback | boolean, - _parseFn?: ParseCallback - ) { - argsert( - '[string|array] [function|boolean|object] [function]', - [args, shortCircuit, _parseFn], - arguments.length - ); - freeze(); // Push current state of parser onto stack. - if (typeof args === 'undefined') { - const argv = self._parseArgs(processArgs); - const tmpParsed = self.parsed; - unfreeze(); // Pop the stack. - self.parsed = tmpParsed; - return argv; - } - - // a context object can optionally be provided, this allows - // additional information to be passed to a command handler. - if (typeof shortCircuit === 'object') { - parseContext = shortCircuit; - shortCircuit = _parseFn; - } - - // by providing a function as a second argument to - // parse you can capture output that would otherwise - // default to printing to stdout/stderr. - if (typeof shortCircuit === 'function') { - parseFn = shortCircuit as ParseCallback; - shortCircuit = false; - } - // completion short-circuits the parsing process, - // skipping validation, etc. - if (!shortCircuit) processArgs = args; - - if (parseFn) exitProcess = false; - - const parsed = self._parseArgs(args, !!shortCircuit); - completion!.setParsed(self.parsed as DetailedArguments); - if (parseFn) parseFn(exitError, parsed, output); - unfreeze(); // Pop the stack. - - return parsed; - }; - - self._getParseContext = () => parseContext || {}; - - self._hasParseCallback = () => !!parseFn; - - self.option = self.options = function option( + option( key: string | Dictionary, opt?: OptionDefinition - ) { + ): YargsInstance { argsert(' [object]', [key, opt], arguments.length); if (typeof key === 'object') { Object.keys(key).forEach(k => { - self.options(k, key[k]); + this.options(k, key[k]); }); } else { if (typeof opt !== 'object') { opt = {}; } - options.key[key] = true; // track manually set keys. + this.#options!.key[key] = true; // track manually set keys. - if (opt.alias) self.alias(key, opt.alias); + if (opt.alias) this.alias(key, opt.alias); const deprecate = opt.deprecate || opt.deprecated; if (deprecate) { - self.deprecateOption(key, deprecate); + this.deprecateOption(key, deprecate); } const demand = opt.demand || opt.required || opt.require; // A required option can be specified via "demand: true". if (demand) { - self.demand(key, demand); + this.demand(key, demand); } if (opt.demandOption) { - self.demandOption( + this.demandOption( key, typeof opt.demandOption === 'string' ? opt.demandOption : undefined ); } if (opt.conflicts) { - self.conflicts(key, opt.conflicts); + this.conflicts(key, opt.conflicts); } if ('default' in opt) { - self.default(key, opt.default); + this.default(key, opt.default); } if (opt.implies !== undefined) { - self.implies(key, opt.implies); + this.implies(key, opt.implies); } if (opt.nargs !== undefined) { - self.nargs(key, opt.nargs); + this.nargs(key, opt.nargs); } if (opt.config) { - self.config(key, opt.configParser); + this.config(key, opt.configParser); } if (opt.normalize) { - self.normalize(key); + this.normalize(key); } if (opt.choices) { - self.choices(key, opt.choices); + this.choices(key, opt.choices); } if (opt.coerce) { - self.coerce(key, opt.coerce); + this.coerce(key, opt.coerce); } if (opt.group) { - self.group(key, opt.group); + this.group(key, opt.group); } if (opt.boolean || opt.type === 'boolean') { - self.boolean(key); - if (opt.alias) self.boolean(opt.alias); + this.boolean(key); + if (opt.alias) this.boolean(opt.alias); } if (opt.array || opt.type === 'array') { - self.array(key); - if (opt.alias) self.array(opt.alias); + this.array(key); + if (opt.alias) this.array(opt.alias); } if (opt.number || opt.type === 'number') { - self.number(key); - if (opt.alias) self.number(opt.alias); + this.number(key); + if (opt.alias) this.number(opt.alias); } if (opt.string || opt.type === 'string') { - self.string(key); - if (opt.alias) self.string(opt.alias); + this.string(key); + if (opt.alias) this.string(opt.alias); } if (opt.count || opt.type === 'count') { - self.count(key); + this.count(key); } if (typeof opt.global === 'boolean') { - self.global(key, opt.global); + this.global(key, opt.global); } if (opt.defaultDescription) { - options.defaultDescription[key] = opt.defaultDescription; + this.#options!.defaultDescription[key] = opt.defaultDescription; } if (opt.skipValidation) { - self.skipValidation(key); + this.skipValidation(key); } const desc = opt.describe || opt.description || opt.desc; - self.describe(key, desc); + this.describe(key, desc); if (opt.hidden) { - self.hide(key); + this.hide(key); } if (opt.requiresArg) { - self.requiresArg(key); + this.requiresArg(key); } } - return self; - }; - self.getOptions = () => options; - - self.positional = function (key, opts) { + return this; + } + options( + key: string | Dictionary, + opt?: OptionDefinition + ): YargsInstance { + return this.option(key, opt); + } + getOptions(): Options { + return this.#options!; + } + group(opts: string | string[], groupName: string): YargsInstance { + argsert(' ', [opts, groupName], arguments.length); + const existing = + this.#preservedGroups[groupName] || this.#groups[groupName]; + if (this.#preservedGroups[groupName]) { + // we now only need to track this group name in groups. + delete this.#preservedGroups[groupName]; + } + const seen: Dictionary = {}; + this.#groups[groupName] = (existing || []).concat(opts).filter(key => { + if (seen[key]) return false; + return (seen[key] = true); + }); + return this; + } + // combine explicit and preserved groups. explicit groups should be first + getGroups(): Dictionary { + return Object.assign({}, this.#groups, this.#preservedGroups); + } + hide(key: string): YargsInstance { + argsert('', [key], arguments.length); + this.#options!.hiddenOptions.push(key); + return this; + } + positional(key: string, opts: PositionalDefinition): YargsInstance { argsert(' ', [key, opts], arguments.length); // .positional() only supports a subset of the configuration // options available to .option(): @@ -1213,9 +994,11 @@ function Yargs( }); // copy over any settings that can be inferred from the command string. - const fullCommand = context.fullCommands[context.fullCommands.length - 1]; + const fullCommand = this.#context.fullCommands[ + this.#context.fullCommands.length - 1 + ]; const parseOptions = fullCommand - ? command.cmdToParseOptions(fullCommand) + ? this.#command!.cmdToParseOptions(fullCommand) : { array: [], alias: {}, @@ -1230,83 +1013,54 @@ function Yargs( if (parseOption[key] && !(pk in opts)) opts[pk] = parseOption[key]; } }); - self.group(key, usage.getPositionalGroupName()); - return self.option(key, opts); - }; - - self.group = function group(opts, groupName) { - argsert(' ', [opts, groupName], arguments.length); - const existing = preservedGroups[groupName] || groups[groupName]; - if (preservedGroups[groupName]) { - // we now only need to track this group name in groups. - delete preservedGroups[groupName]; - } - - const seen: Dictionary = {}; - groups[groupName] = (existing || []).concat(opts).filter(key => { - if (seen[key]) return false; - return (seen[key] = true); - }); - return self; - }; - // combine explicit and preserved groups. explicit groups should be first - self.getGroups = () => Object.assign({}, groups, preservedGroups); - + this.group(key, this.#usage!.getPositionalGroupName()); + return this.option(key, opts); + } // as long as options.envPrefix is not undefined, // parser will apply env vars matching prefix to argv - self.env = function (prefix) { + env(prefix?: string | false): YargsInstance { argsert('[string|boolean]', [prefix], arguments.length); - if (prefix === false) delete options.envPrefix; - else options.envPrefix = prefix || ''; - return self; - }; - - self.wrap = function (cols) { + if (prefix === false) delete this.#options!.envPrefix; + else this.#options!.envPrefix = prefix || ''; + return this; + } + wrap(cols: number | null | undefined): YargsInstance { argsert('', [cols], arguments.length); - usage.wrap(cols); - return self; - }; - - let strict = false; - self.strict = function (enabled) { + this.#usage!.wrap(cols); + return this; + } + strict(enabled?: boolean): YargsInstance { argsert('[boolean]', [enabled], arguments.length); - strict = enabled !== false; - return self; - }; - self.getStrict = () => strict; - - let strictCommands = false; - self.strictCommands = function (enabled) { + this.#strict = enabled !== false; + return this; + } + getStrict(): boolean { + return this.#strict; + } + strictCommands(enabled?: boolean): YargsInstance { argsert('[boolean]', [enabled], arguments.length); - strictCommands = enabled !== false; - return self; - }; - self.getStrictCommands = () => strictCommands; - - let strictOptions = false; - self.strictOptions = function (enabled) { + this.#strictCommands = enabled !== false; + return this; + } + getStrictCommands(): boolean { + return this.#strictCommands; + } + strictOptions(enabled?: boolean): YargsInstance { argsert('[boolean]', [enabled], arguments.length); - strictOptions = enabled !== false; - return self; - }; - self.getStrictOptions = () => strictOptions; - - let parserConfig: Configuration = {}; - self.parserConfiguration = function parserConfiguration(config) { - argsert('', [config], arguments.length); - parserConfig = config; - return self; - }; - self.getParserConfiguration = () => parserConfig; - - self.getHelp = async function () { - hasOutput = true; - if (!usage.hasCachedHelpMessage()) { - if (!self.parsed) { + this.#strictOptions = enabled !== false; + return this; + } + getStrictOptions(): boolean { + return this.#strictOptions; + } + getHelp(): Promise { + this.#hasOutput = true; + if (!this.#usage!.hasCachedHelpMessage()) { + if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). - const parse = self._parseArgs( - processArgs, + const parse = this.runYargsParserAndExecuteCommands( + this.#processArgs, undefined, undefined, 0, @@ -1314,23 +1068,24 @@ function Yargs( ); if (isPromise(parse)) { return parse.then(() => { - return usage.help(); + return this.#usage!.help(); }); } } } - return usage.help(); - }; - - self.showHelp = function (level) { + return Promise.resolve(this.#usage!.help()); + } + showHelp( + level: 'error' | 'log' | ((message: string) => void) + ): YargsInstance { argsert('[string|function]', [level], arguments.length); - hasOutput = true; - if (!usage.hasCachedHelpMessage()) { - if (!self.parsed) { + this.#hasOutput = true; + if (!this.#usage!.hasCachedHelpMessage()) { + if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). - const parse = self._parseArgs( - processArgs, + const parse = this.runYargsParserAndExecuteCommands( + this.#processArgs, undefined, undefined, 0, @@ -1338,28 +1093,23 @@ function Yargs( ); if (isPromise(parse)) { parse.then(() => { - usage.showHelp(level); + this.#usage!.showHelp(level); }); - return self; + return this; } } } - usage.showHelp(level); - return self; - }; - - self.showVersion = function (level) { + this.#usage!.showHelp(level); + return this; + } + showVersion( + level: 'error' | 'log' | ((message: string) => void) + ): YargsInstance { argsert('[string|function]', [level], arguments.length); - usage.showVersion(level); - return self; - }; - - let versionOpt: string | null = null; - self.version = function version( - opt?: string | false, - msg?: string, - ver?: string - ) { + this.#usage!.showVersion(level); + return this; + } + version(opt?: string | false, msg?: string, ver?: string): YargsInstance { const defaultVersionOpt = 'version'; argsert( '[boolean|string] [string] [string]', @@ -1369,19 +1119,19 @@ function Yargs( // nuke the key previously configured // to return version #. - if (versionOpt) { - deleteFromParserHintObject(versionOpt); - usage.version(undefined); - versionOpt = null; + if (this.#versionOpt) { + this.deleteFromParserHintObject(this.#versionOpt); + this.#usage!.version(undefined); + this.#versionOpt = null; } if (arguments.length === 0) { - ver = guessVersion(); + ver = this.guessVersion(); opt = defaultVersionOpt; } else if (arguments.length === 1) { if (opt === false) { // disable default 'version' key. - return self; + return this; } ver = opt; opt = defaultVersionOpt; @@ -1390,221 +1140,557 @@ function Yargs( msg = undefined; } - versionOpt = typeof opt === 'string' ? opt : defaultVersionOpt; - msg = msg || usage.deferY18nLookup('Show version number'); - - usage.version(ver || undefined); - self.boolean(versionOpt); - self.describe(versionOpt, msg); - return self; - }; - - function guessVersion() { - const obj = pkgUp(); + this.#versionOpt = typeof opt === 'string' ? opt : defaultVersionOpt; + msg = msg || this.#usage!.deferY18nLookup('Show version number'); - return obj.version || 'unknown'; + this.#usage!.version(ver || undefined); + this.boolean(this.#versionOpt); + this.describe(this.#versionOpt, msg); + return this; } + showHelpOnFail(enabled?: string | boolean, message?: string): YargsInstance { + argsert('[boolean|string] [string]', [enabled, message], arguments.length); + this.#usage!.showHelpOnFail(enabled, message); + return this; + } + exitProcess(enabled = true): YargsInstance { + argsert('[boolean]', [enabled], arguments.length); + this.#exitProcess = enabled; + return this; + } + getExitProcess(): boolean { + return this.#exitProcess; + } + showCompletionScript($0?: string, cmd?: string): YargsInstance { + argsert('[string] [string]', [$0, cmd], arguments.length); + $0 = $0 || this.$0; + this.#logger.log( + this.#completion!.generateCompletionScript( + $0, + cmd || this.#completionCommand || 'completion' + ) + ); + return this; + } + // TODO(bcoe): add getLocale() rather than overloading behavior. + locale(locale?: string): YargsInstance | string { + argsert('[string]', [locale], arguments.length); + if (!locale) { + this.guessLocale(); + return this.#y18n.getLocale(); + } + this.#detectLocale = false; + this.#y18n.setLocale(locale); + return this; + } + updateStrings(obj: Dictionary): YargsInstance { + argsert('', [obj], arguments.length); + this.#detectLocale = false; + this.#y18n.updateLocale(obj); + return this; + } + updateLocale(obj: Dictionary): YargsInstance { + return this.updateStrings(obj); + } + detectLocale(detect: boolean): YargsInstance { + argsert('', [detect], arguments.length); + this.#detectLocale = detect; + return this; + } + getDetectLocale(): boolean { + return this.#detectLocale; + } + recommendCommands(recommend = true): YargsInstance { + argsert('[boolean]', [recommend], arguments.length); + this.#recommendCommands = recommend; + return this; + } + getCommandInstance(): CommandInstance { + return this.#command!; + } + getUsageInstance(): UsageInstance { + return this.#usage!; + } + getValidationInstance(): ValidationInstance { + return this.#validation!; + } + terminalWidth(): number | null { + argsert([], 0); + return shim.process.stdColumns; + } + parse( + args?: string | string[], + shortCircuit?: object | ParseCallback | boolean, + _parseFn?: ParseCallback + ): Arguments | Promise { + argsert( + '[string|array] [function|boolean|object] [function]', + [args, shortCircuit, _parseFn], + arguments.length + ); + this.freeze(); // Push current state of parser onto stack. + if (typeof args === 'undefined') { + const argv = this.runYargsParserAndExecuteCommands(this.#processArgs); + const tmpParsed = this.parsed; + this.unfreeze(); // Pop the stack. + this.parsed = tmpParsed; + return argv; + } - let helpOpt: string | null = null; - self.addHelpOpt = self.help = function addHelpOpt( - opt?: string | false, - msg?: string - ) { - const defaultHelpOpt = 'help'; - argsert('[string|boolean] [string]', [opt, msg], arguments.length); - - // nuke the key previously configured - // to return help. - if (helpOpt) { - deleteFromParserHintObject(helpOpt); - helpOpt = null; + // a context object can optionally be provided, this allows + // additional information to be passed to a command handler. + if (typeof shortCircuit === 'object') { + this.#parseContext = shortCircuit; + shortCircuit = _parseFn; } - if (arguments.length === 1) { - if (opt === false) return self; + // by providing a function as a second argument to + // parse you can capture output that would otherwise + // default to printing to stdout/stderr. + if (typeof shortCircuit === 'function') { + this.#parseFn = shortCircuit as ParseCallback; + shortCircuit = false; } + // completion short-circuits the parsing process, + // skipping validation, etc. + if (!shortCircuit) this.#processArgs = args; - // use arguments, fallback to defaults for opt and msg - helpOpt = typeof opt === 'string' ? opt : defaultHelpOpt; - self.boolean(helpOpt); - self.describe(helpOpt, msg || usage.deferY18nLookup('Show help')); - return self; - }; - - const defaultShowHiddenOpt = 'show-hidden'; - options!.showHiddenOpt = defaultShowHiddenOpt; - self.addShowHiddenOpt = self.showHidden = function addShowHiddenOpt( - opt?: string | false, - msg?: string - ) { - argsert('[string|boolean] [string]', [opt, msg], arguments.length); + if (this.#parseFn) this.#exitProcess = false; - if (arguments.length === 1) { - if (opt === false) return self; + const parsed = this.runYargsParserAndExecuteCommands(args, !!shortCircuit); + this.#completion!.setParsed(this.parsed as DetailedArguments); + if (this.#parseFn) this.#parseFn(this.#exitError, parsed, this.#output); + this.unfreeze(); // Pop the stack. + return parsed; + } + // TODO(bcoe): arrange private methods alphabetically: + private getDollarZero(): string { + let $0 = ''; + // ignore the node bin, specify this in your + // bin file with #!/usr/bin/env node + let default$0: string[]; + if (/\b(node|iojs|electron)(\.exe)?$/.test(shim.process.argv()[0])) { + default$0 = shim.process.argv().slice(1, 2); + } else { + default$0 = shim.process.argv().slice(0, 1); } - const showHiddenOpt = typeof opt === 'string' ? opt : defaultShowHiddenOpt; - self.boolean(showHiddenOpt); - self.describe( - showHiddenOpt, - msg || usage.deferY18nLookup('Show hidden options') + $0 = default$0 + .map(x => { + const b = rebase(this.#cwd, x); + return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x; + }) + .join(' ') + .trim(); + + if (shim.getEnv('_') && shim.getProcessArgvBin() === shim.getEnv('_')) { + $0 = shim + .getEnv('_')! + .replace(`${shim.path.dirname(shim.process.execPath())}/`, ''); + } + return $0; + } + private populateParserHintArray>( + type: T, + keys: string | string[] + ) { + keys = ([] as string[]).concat(keys); + keys.forEach(key => { + key = this.sanitizeKey(key); + this.#options![type].push(key); + }); + } + private populateParserHintSingleValueDictionary< + T extends + | Exclude, DictionaryKeyof> + | 'default', + K extends keyof Options[T] & string = keyof Options[T] & string, + V extends ValueOf = ValueOf + >( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | {[key in K]: V}, + value?: V + ) { + this.populateParserHintDictionary( + builder, + type, + key, + value, + (type, key, value) => { + this.#options![type][key] = value as ValueOf; + } ); - options.showHiddenOpt = showHiddenOpt; - return self; - }; + } + private populateParserHintArrayDictionary< + T extends DictionaryKeyof, + K extends keyof Options[T] & string = keyof Options[T] & string, + V extends ValueOf> | ValueOf>[] = + | ValueOf> + | ValueOf>[] + >( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | {[key in K]: V}, + value?: V + ) { + this.populateParserHintDictionary( + builder, + type, + key, + value, + (type, key, value) => { + this.#options![type][key] = ( + this.#options![type][key] || ([] as Options[T][keyof Options[T]]) + ).concat(value); + } + ); + } + private populateParserHintDictionary< + T extends keyof Options, + K extends keyof Options[T], + V + >( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | {[key in K]: V}, + value: V | undefined, + singleKeyHandler: (type: T, key: K, value?: V) => void + ) { + if (Array.isArray(key)) { + // an array of keys with one value ['x', 'y', 'z'], function parse () {} + key.forEach(k => { + builder(k, value!); + }); + } else if ( + ((key): key is {[key in K]: V} => typeof key === 'object')(key) + ) { + // an object of key value pairs: {'x': parse () {}, 'y': parse() {}} + for (const k of objectKeys(key)) { + builder(k, key[k]); + } + } else { + singleKeyHandler(type, this.sanitizeKey(key), value); + } + } + private sanitizeKey(key: any) { + if (key === '__proto__') return '___proto___'; + return key; + } + private setKey( + key: string | string[] | Dictionary, + set?: boolean | string + ) { + this.populateParserHintSingleValueDictionary( + this.setKey.bind(this), + 'key', + key, + set + ); + return this; + } + private deleteFromParserHintObject(optionKey: string) { + // delete from all parsing hints: + // boolean, array, key, alias, etc. + objectKeys(this.#options).forEach((hintKey: keyof Options) => { + // configObjects is not a parsing hint array + if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) + return; + const hint = this.#options![hintKey]; + if (Array.isArray(hint)) { + if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1); + } else if (typeof hint === 'object') { + delete (hint as Dictionary)[optionKey]; + } + }); + // now delete the description from usage.js. + delete this.#usage!.getDescriptions()[optionKey]; + } + private freeze() { + this.#frozens.push({ + options: this.#options!, + configObjects: this.#options!.configObjects.slice(0), + exitProcess: this.#exitProcess, + groups: this.#groups, + strict: this.#strict, + strictCommands: this.#strictCommands, + strictOptions: this.#strictOptions, + completionCommand: this.#completionCommand, + output: this.#output, + exitError: this.#exitError!, + hasOutput: this.#hasOutput, + parsed: this.parsed, + parseFn: this.#parseFn!, + parseContext: this.#parseContext, + }); + this.#usage!.freeze(); + this.#validation!.freeze(); + this.#command!.freeze(); + this.#globalMiddleware.freeze(); + } + private unfreeze() { + const frozen = this.#frozens.pop(); + assertNotStrictEqual(frozen, undefined, shim); + let configObjects: Dictionary[]; + ({ + options: this.#options, + configObjects, + exitProcess: this.#exitProcess, + groups: this.#groups, + output: this.#output, + exitError: this.#exitError, + hasOutput: this.#hasOutput, + parsed: this.parsed, + strict: this.#strict, + strictCommands: this.#strictCommands, + strictOptions: this.#strictOptions, + completionCommand: this.#completionCommand, + parseFn: this.#parseFn, + parseContext: this.#parseContext, + } = frozen); + this.#options.configObjects = configObjects; + this.#usage!.unfreeze(); + this.#validation!.unfreeze(); + this.#command!.unfreeze(); + this.#globalMiddleware.unfreeze(); + } + // to simplify the parsing of positionals in commands, + // we temporarily populate '--' rather than _, with arguments + // after the '--' directive. After the parse, we copy these back. + private copyDoubleDash(argv: Arguments): any { + if (!argv._ || !argv['--']) return argv; + // eslint-disable-next-line prefer-spread + argv._.push.apply(argv._, argv['--']); - self.hide = function hide(key) { - argsert('', [key], arguments.length); - options.hiddenOptions.push(key); - return self; - }; + // We catch an error here, in case someone has called Object.seal() + // on the parsed object, see: https://github.com/babel/babel/pull/10733 + try { + delete argv['--']; + // eslint-disable-next-line no-empty + } catch (_err) {} - self.showHelpOnFail = function showHelpOnFail( - enabled?: string | boolean, - message?: string - ) { - argsert('[boolean|string] [string]', [enabled, message], arguments.length); - usage.showHelpOnFail(enabled, message); - return self; - }; + return argv; + } + // We wait to coerce numbers for positionals until after the initial parse. + // This allows commands to configure number parsing on a positional by + // positional basis: + private parsePositionalNumbers(argv: Arguments): any { + const args: (string | number)[] = argv['--'] ? argv['--'] : argv._; + + for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { + if ( + shim.Parser.looksLikeNumber(arg) && + Number.isSafeInteger(Math.floor(parseFloat(`${arg}`))) + ) { + args[i] = Number(arg); + } + } + return argv; + } + private pkgUp(rootPath?: string) { + const npath = rootPath || '*'; + if (this.#pkgs[npath]) return this.#pkgs[npath]; - let exitProcess = true; - self.exitProcess = function (enabled = true) { - argsert('[boolean]', [enabled], arguments.length); - exitProcess = enabled; - return self; - }; - self.getExitProcess = () => exitProcess; + let obj = {}; + try { + let startDir = rootPath || shim.mainFilename; - self.showCompletionScript = function ($0, cmd) { - argsert('[string] [string]', [$0, cmd], arguments.length); - $0 = $0 || self.$0; - _logger.log( - completion!.generateCompletionScript( - $0, - cmd || completionCommand || 'completion' - ) - ); - return self; - }; + // When called in an environment that lacks require.main.filename, such as a jest test runner, + // startDir is already process.cwd(), and should not be shortened. + // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. + if (!rootPath && shim.path.extname(startDir)) { + startDir = shim.path.dirname(startDir); + } - self.getCompletion = async function (args, done) { - argsert(' [function]', [args, done], arguments.length); - if (!done) { - return new Promise((resolve, reject) => { - completion!.getCompletion(args, (err, completions) => { - if (err) reject(err); - else resolve(completions); - }); - }); - } else { - return completion!.getCompletion(args, done); - } - }; + const pkgJsonPath = shim.findUp( + startDir, + (dir: string[], names: string[]) => { + if (names.includes('package.json')) { + return 'package.json'; + } else { + return undefined; + } + } + ); + assertNotStrictEqual(pkgJsonPath, undefined, shim); + obj = JSON.parse(shim.readFileSync(pkgJsonPath, 'utf8')); + // eslint-disable-next-line no-empty + } catch (_noop) {} - self.locale = function (locale?: string): any { - argsert('[string]', [locale], arguments.length); - if (!locale) { - guessLocale(); - return y18n.getLocale(); - } - detectLocale = false; - y18n.setLocale(locale); - return self; - }; + this.#pkgs[npath] = obj || {}; + return this.#pkgs[npath]; + } + guessLocale() { + if (!this.#detectLocale) return; + const locale = + shim.getEnv('LC_ALL') || + shim.getEnv('LC_MESSAGES') || + shim.getEnv('LANG') || + shim.getEnv('LANGUAGE') || + 'en_US'; + this.locale(locale.replace(/[.:].*/, '')); + } + private guessVersion(): string { + const obj = this.pkgUp(); + return (obj.version as string) || 'unknown'; + } + private createLogger(): LoggerInstance { + return { + log: (...args: any[]) => { + if (!this.hasParseCallback()) console.log(...args); + this.#hasOutput = true; + if (this.#output.length) this.#output += '\n'; + this.#output += args.join(' '); + }, + error: (...args: any[]) => { + if (!this.hasParseCallback()) console.error(...args); + this.#hasOutput = true; + if (this.#output.length) this.#output += '\n'; + this.#output += args.join(' '); + }, + }; + } + // If argv is a promise (which is possible if async middleware is used) + // delay applying validation until the promise has resolved: + private validateAsync( + validation: (argv: Arguments) => void, + argv: Arguments | Promise + ): Arguments | Promise { + return maybeAsyncResult(argv, result => { + validation(result); + return result; + }); + } + // TODO(bcoe): figure out how to make the following methods protected: + + // put yargs back into an initial state; this is used mainly for running + // commands in a breadth first manner: + // TODO(bcoe): in release notes document that .reset() has been removed + // from API. + reset(aliases: Aliases = {}) { + this.#options = this.#options || ({} as Options); + const tmpOptions = {} as Options; + tmpOptions.local = this.#options.local ? this.#options.local : []; + tmpOptions.configObjects = this.#options.configObjects + ? this.#options.configObjects + : []; - self.updateStrings = self.updateLocale = function (obj) { - argsert('', [obj], arguments.length); - detectLocale = false; - y18n.updateLocale(obj); - return self; - }; + // if a key has been explicitly set as local, + // we should reset it before passing options to command. + const localLookup: Dictionary = {}; + tmpOptions.local.forEach(l => { + localLookup[l] = true; + (aliases[l] || []).forEach(a => { + localLookup[a] = true; + }); + }); - let detectLocale = true; - self.detectLocale = function (detect) { - argsert('', [detect], arguments.length); - detectLocale = detect; - return self; - }; - self.getDetectLocale = () => detectLocale; - - // we use a custom logger that buffers output, - // so that we can print to non-CLIs, e.g., chat-bots. - const _logger = { - log(...args: any[]) { - if (!self._hasParseCallback()) console.log(...args); - hasOutput = true; - if (output.length) output += '\n'; - output += args.join(' '); - }, - error(...args: any[]) { - if (!self._hasParseCallback()) console.error(...args); - hasOutput = true; - if (output.length) output += '\n'; - output += args.join(' '); - }, - }; - self._getLoggerInstance = () => _logger; - // has yargs output an error our help - // message in the current execution context. - self._hasOutput = () => hasOutput; - - self._setHasOutput = () => { - hasOutput = true; - }; - - let recommendCommands: boolean; - self.recommendCommands = function (recommend = true) { - argsert('[boolean]', [recommend], arguments.length); - recommendCommands = recommend; - return self; - }; + // add all groups not set to local to preserved groups + Object.assign( + this.#preservedGroups, + Object.keys(this.#groups).reduce((acc, groupName) => { + const keys = this.#groups[groupName].filter( + key => !(key in localLookup) + ); + if (keys.length > 0) { + acc[groupName] = keys; + } + return acc; + }, {} as Dictionary) + ); + // groups can now be reset + this.#groups = {}; - self.getUsageInstance = () => usage; + const arrayOptions: KeyOf[] = [ + 'array', + 'boolean', + 'string', + 'skipValidation', + 'count', + 'normalize', + 'number', + 'hiddenOptions', + ]; - self.getValidationInstance = () => validation; + const objectOptions: DictionaryKeyof[] = [ + 'narg', + 'key', + 'alias', + 'default', + 'defaultDescription', + 'config', + 'choices', + 'demandedOptions', + 'demandedCommands', + 'deprecatedOptions', + ]; - self.getCommandInstance = () => command; + arrayOptions.forEach(k => { + tmpOptions[k] = (this.#options![k] || []).filter( + (k: string) => !localLookup[k] + ); + }); - self.terminalWidth = () => { - argsert([], 0); - return shim.process.stdColumns; - }; + objectOptions.forEach(>(k: K) => { + tmpOptions[k] = objFilter( + this.#options![k], + k => !localLookup[k as string] + ); + }); - Object.defineProperty(self, 'argv', { - get: () => { - return self.parse(); - }, - enumerable: true, - }); + tmpOptions.envPrefix = this.#options.envPrefix; + this.#options = tmpOptions; - self._parseArgs = function parseArgs( + // if this is the first time being executed, create + // instances of all our helpers -- otherwise just reset. + this.#usage = this.#usage + ? this.#usage.reset(localLookup) + : Usage(this, this.#y18n, shim); + this.#validation = this.#validation + ? this.#validation.reset(localLookup) + : Validation(this, this.#usage, this.#y18n, shim); + this.#command = this.#command + ? this.#command.reset() + : Command(this.#usage, this.#validation, this.#globalMiddleware, shim); + if (!this.#completion) + this.#completion = Completion(this, this.#usage, this.#command, shim); + this.#globalMiddleware.reset(); + + this.#completionCommand = null; + this.#output = ''; + this.#exitError = null; + this.#hasOutput = false; + this.parsed = false; + + return this; + } + runYargsParserAndExecuteCommands( args: string | string[] | null, shortCircuit?: boolean | null, calledFromCommand?: boolean, commandIndex = 0, helpOnly = false - ) { + ): Arguments | Promise { let skipValidation = !!calledFromCommand || helpOnly; - args = args || processArgs; + args = args || this.#processArgs; - options.__ = y18n.__; - options.configuration = self.getParserConfiguration(); + this.#options!.__ = this.#y18n.__; + this.#options!.configuration = this.getParserConfiguration(); - const populateDoubleDash = !!options.configuration['populate--']; - const config = Object.assign({}, options.configuration, { + const populateDoubleDash = !!this.#options!.configuration['populate--']; + const config = Object.assign({}, this.#options!.configuration, { 'populate--': true, }); const parsed = shim.Parser.detailed( args, - Object.assign({}, options, { + Object.assign({}, this.#options, { configuration: {'parse-positional-numbers': false, ...config}, }) ) as DetailedArguments; const argv: Arguments = Object.assign( parsed.argv, - parseContext + this.#parseContext ) as Arguments; let argvPromise: Arguments | Promise | undefined = undefined; const aliases = parsed.aliases; @@ -1612,32 +1698,32 @@ function Yargs( let helpOptSet = false; let versionOptSet = false; Object.keys(argv).forEach(key => { - if (key === helpOpt && argv[key]) { + if (key === this.#helpOpt && argv[key]) { helpOptSet = true; - } else if (key === versionOpt && argv[key]) { + } else if (key === this.#versionOpt && argv[key]) { versionOptSet = true; } }); - argv.$0 = self.$0; - self.parsed = parsed; + argv.$0 = this.$0; + this.parsed = parsed; // A single yargs instance may be used multiple times, e.g. // const y = yargs(); y.parse('foo --bar'); yargs.parse('bar --foo'). // When a prior parse has completed and a new parse is beginning, we // need to clear the cached help message from the previous parse: if (commandIndex === 0) { - usage.clearCachedHelpMessage(); + this.#usage!.clearCachedHelpMessage(); } try { - guessLocale(); // guess locale lazily, so that it can be turned off in chain. + this.guessLocale(); // guess locale lazily, so that it can be turned off in chain. // while building up the argv object, there // are two passes through the parser. If completion // is being performed short-circuit on the first pass. if (shortCircuit) { - return self._postProcess( + return this.postProcess( argv, populateDoubleDash, !!calledFromCommand, @@ -1647,12 +1733,12 @@ function Yargs( // if there's a handler associated with a // command defer processing to it. - if (helpOpt) { + if (this.#helpOpt) { // consider any multi-char helpOpt alias as a valid help command // unless all helpOpt aliases are single-char // note that parsed.aliases is a normalized bidirectional map :) - const helpCmds = [helpOpt] - .concat(aliases[helpOpt] || []) + const helpCmds = [this.#helpOpt] + .concat(aliases[this.#helpOpt] || []) .filter(k => k.length > 1); // check if help should trigger and strip it from _. if (~helpCmds.indexOf('' + argv._[argv._.length - 1])) { @@ -1661,8 +1747,8 @@ function Yargs( } } - const handlerKeys = command.getCommands(); - const requestCompletions = completion!.completionKey in argv; + const handlerKeys = this.#command!.getCommands(); + const requestCompletions = this.#completion!.completionKey in argv; const skipRecommendation = helpOptSet || requestCompletions || helpOnly; if (argv._.length) { @@ -1670,13 +1756,13 @@ function Yargs( let firstUnknownCommand; for (let i = commandIndex || 0, cmd; argv._[i] !== undefined; i++) { cmd = String(argv._[i]); - if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) { + if (~handlerKeys.indexOf(cmd) && cmd !== this.#completionCommand) { // commands are executed using a recursive algorithm that executes // the deepest command first; we keep track of the position in the // argv._ array that is currently being executed. - const innerArgv = command.runCommand( + const innerArgv = this.#command!.runCommand( cmd, - self, + this, parsed, i + 1, // Don't run a handler, just figure out the help string: @@ -1684,13 +1770,16 @@ function Yargs( // Passed to builder so that expensive commands can be deferred: helpOptSet || versionOptSet || helpOnly ); - return self._postProcess( + return this.postProcess( innerArgv, populateDoubleDash, !!calledFromCommand, false ); - } else if (!firstUnknownCommand && cmd !== completionCommand) { + } else if ( + !firstUnknownCommand && + cmd !== this.#completionCommand + ) { firstUnknownCommand = cmd; break; } @@ -1698,67 +1787,71 @@ function Yargs( // recommend a command if recommendCommands() has // been enabled, and no commands were found to execute if ( - !command.hasDefaultCommand() && - recommendCommands && + !this.#command!.hasDefaultCommand() && + this.#recommendCommands && firstUnknownCommand && !skipRecommendation ) { - validation.recommendCommands(firstUnknownCommand, handlerKeys); + this.#validation!.recommendCommands( + firstUnknownCommand, + handlerKeys + ); } } // generate a completion script for adding to ~/.bashrc. if ( - completionCommand && - ~argv._.indexOf(completionCommand) && + this.#completionCommand && + ~argv._.indexOf(this.#completionCommand) && !requestCompletions ) { - if (exitProcess) setBlocking(true); - self.showCompletionScript(); - self.exit(0); + if (this.#exitProcess) setBlocking(true); + this.showCompletionScript(); + this.exit(0); } } - if (command.hasDefaultCommand() && !skipRecommendation) { - const innerArgv = command.runCommand( + if (this.#command!.hasDefaultCommand() && !skipRecommendation) { + const innerArgv = this.#command!.runCommand( null, - self, + this, parsed, 0, helpOnly, helpOptSet || versionOptSet || helpOnly ); - return self._postProcess( + return this.postProcess( innerArgv, populateDoubleDash, !!calledFromCommand, false ); } else if (!calledFromCommand && helpOnly) { - // TODO: what if the default builder is async? - // TODO: add better comments. - command.runDefaultBuilderOn(self); + // TODO(bcoe): what if the default builder is async? + // TODO(bcoe): add better comments for runDefaultBuilderOn, why + // are we doing this? + this.#command!.runDefaultBuilderOn(this); } // we must run completions first, a user might // want to complete the --help or --version option. if (requestCompletions) { - if (exitProcess) setBlocking(true); + if (this.#exitProcess) setBlocking(true); // we allow for asynchronous completions, // e.g., loading in a list of commands from an API. args = ([] as string[]).concat(args); const completionArgs = args.slice( - args.indexOf(`--${completion!.completionKey}`) + 1 + args.indexOf(`--${this.#completion!.completionKey}`) + 1 ); - completion!.getCompletion(completionArgs, (err, completions) => { + this.#completion!.getCompletion(completionArgs, (err, completions) => { if (err) throw new YError(err.message); (completions || []).forEach(completion => { - _logger.log(completion); + this.#logger.log(completion); }); - self.exit(0); + this.exit(0); }); - return self._postProcess( + return this.postProcess( argv, !populateDoubleDash, !!calledFromCommand, @@ -1768,27 +1861,28 @@ function Yargs( // Handle 'help' and 'version' options // if we haven't already output help! - if (!hasOutput) { + if (!this.#hasOutput) { if (helpOptSet) { - if (exitProcess) setBlocking(true); + if (this.#exitProcess) setBlocking(true); skipValidation = true; // TODO: add appropriate comment. - if (!calledFromCommand) command.runDefaultBuilderOn(self); - self.showHelp('log'); - self.exit(0); + if (!calledFromCommand) this.#command!.runDefaultBuilderOn(this); + this.showHelp('log'); + this.exit(0); } else if (versionOptSet) { - if (exitProcess) setBlocking(true); - + if (this.#exitProcess) setBlocking(true); skipValidation = true; - usage.showVersion('log'); - self.exit(0); + this.#usage!.showVersion('log'); + this.exit(0); } } // Check if any of the options to skip validation were provided - if (!skipValidation && options.skipValidation.length > 0) { + if (!skipValidation && this.#options!.skipValidation.length > 0) { skipValidation = Object.keys(argv).some( - key => options.skipValidation.indexOf(key) >= 0 && argv[key] === true + key => + this.#options!.skipValidation.indexOf(key) >= 0 && + argv[key] === true ); } @@ -1800,22 +1894,22 @@ function Yargs( // if we're executed via bash completion, don't // bother with validation. if (!requestCompletions) { - const validation = self._runValidation(aliases, {}, parsed.error); + const validation = this.runValidation(aliases, {}, parsed.error); if (!calledFromCommand) { argvPromise = applyMiddleware( argv, - self, - globalMiddleware.getMiddleware(), + this, + this.#globalMiddleware.getMiddleware(), true ); } - argvPromise = validateAsync(validation, argvPromise ?? argv); + argvPromise = this.validateAsync(validation, argvPromise ?? argv); if (isPromise(argvPromise) && !calledFromCommand) { argvPromise = argvPromise.then(() => { return applyMiddleware( argv, - self, - globalMiddleware.getMiddleware(), + this, + this.#globalMiddleware.getMiddleware(), false ); }); @@ -1823,33 +1917,56 @@ function Yargs( } } } catch (err) { - if (err instanceof YError) usage.fail(err.message, err); + if (err instanceof YError) this.#usage!.fail(err.message, err); else throw err; } - return self._postProcess( + return this.postProcess( argvPromise ?? argv, populateDoubleDash, !!calledFromCommand, true ); - }; - - // If argv is a promise (which is possible if async middleware is used) - // delay applying validation until the promise has resolved: - function validateAsync( - validation: (argv: Arguments) => void, - argv: Arguments | Promise - ): Arguments | Promise { - return maybeAsyncResult(argv, result => { - validation(result); - return result; - }); } - - // Applies a couple post processing steps that are easier to perform - // as a final step. - self._postProcess = function ( + runValidation( + aliases: Dictionary, + positionalMap: Dictionary, + parseErrors: Error | null, + isDefaultCommand?: boolean + ): (argv: Arguments) => void { + aliases = {...aliases}; + positionalMap = {...positionalMap}; + const demandedOptions = {...this.getDemandedOptions()}; + return (argv: Arguments) => { + if (parseErrors) throw new YError(parseErrors.message); + this.#validation!.nonOptionCount(argv); + this.#validation!.requiredArguments(argv, demandedOptions); + let failedStrictCommands = false; + if (this.#strictCommands) { + failedStrictCommands = this.#validation!.unknownCommands(argv); + } + if (this.#strict && !failedStrictCommands) { + this.#validation!.unknownArguments( + argv, + aliases, + positionalMap, + !!isDefaultCommand + ); + } else if (this.#strictOptions) { + this.#validation!.unknownArguments(argv, aliases, {}, false, false); + } + this.#validation!.limitedChoices(argv); + this.#validation!.implications(argv); + this.#validation!.conflicting(argv); + }; + } + getHasOutput(): boolean { + return this.#hasOutput; + } + setHasOutput() { + this.#hasOutput = true; + } + postProcess>( argv: Arguments | Promise, populateDoubleDash: boolean, calledFromCommand: boolean, @@ -1858,110 +1975,57 @@ function Yargs( if (calledFromCommand) return argv; if (isPromise(argv)) return argv; if (!populateDoubleDash) { - argv = self._copyDoubleDash(argv); + argv = this.copyDoubleDash(argv); } const parsePositionalNumbers = - self.getParserConfiguration()['parse-positional-numbers'] || - self.getParserConfiguration()['parse-positional-numbers'] === undefined; + this.getParserConfiguration()['parse-positional-numbers'] || + this.getParserConfiguration()['parse-positional-numbers'] === undefined; if (parsePositionalNumbers) { - argv = self._parsePositionalNumbers(argv); + argv = this.parsePositionalNumbers(argv as Arguments); } if (runGlobalMiddleware) { argv = applyMiddleware( argv, - self, - globalMiddleware.getMiddleware(), + this, + this.#globalMiddleware.getMiddleware(), false ); } return argv; - }; - - // to simplify the parsing of positionals in commands, - // we temporarily populate '--' rather than _, with arguments - // after the '--' directive. After the parse, we copy these back. - self._copyDoubleDash = function (argv: Arguments): any { - if (!argv._ || !argv['--']) return argv; - // eslint-disable-next-line prefer-spread - argv._.push.apply(argv._, argv['--']); - - // We catch an error here, in case someone has called Object.seal() - // on the parsed object, see: https://github.com/babel/babel/pull/10733 - try { - delete argv['--']; - // eslint-disable-next-line no-empty - } catch (_err) {} - - return argv; - }; - - // We wait to coerce numbers for positionals until after the initial parse. - // This allows commands to configure number parsing on a positional by - // positional basis: - self._parsePositionalNumbers = function (argv: Arguments): any { - const args: (string | number)[] = argv['--'] ? argv['--'] : argv._; - - for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { - if ( - shim.Parser.looksLikeNumber(arg) && - Number.isSafeInteger(Math.floor(parseFloat(`${arg}`))) - ) { - args[i] = Number(arg); - } - } - return argv; - }; + } + hasParseCallback(): boolean { + return !!this.#parseFn; + } + getLoggerInstance(): LoggerInstance { + return this.#logger; + } + getParseContext(): Object { + return this.#parseContext || {}; + } +} - self._runValidation = function runValidation( - aliases, - positionalMap, - parseErrors, - isDefaultCommand = false - ): (argv: Arguments) => void { - aliases = {...aliases}; - positionalMap = {...positionalMap}; - const demandedOptions = {...self.getDemandedOptions()}; - return (argv: Arguments) => { - if (parseErrors) throw new YError(parseErrors.message); - validation.nonOptionCount(argv); - validation.requiredArguments(argv, demandedOptions); - let failedStrictCommands = false; - if (strictCommands) { - failedStrictCommands = validation.unknownCommands(argv); - } - if (strict && !failedStrictCommands) { - validation.unknownArguments( - argv, - aliases, - positionalMap, - isDefaultCommand - ); - } else if (strictOptions) { - validation.unknownArguments(argv, aliases, {}, false, false); - } - validation.limitedChoices(argv); - validation.implications(argv); - validation.conflicting(argv); - }; - }; +// TODO(bcoe): simplify bootstrapping process so that only a single factory +// method is used, and shim is not set globally. +function YargsFactory( + processArgs: string | string[] = [], + cwd = shim.process.cwd(), + parentRequire?: RequireType +): YargsInstance { + const yargs = new YargsInstance(processArgs, cwd, parentRequire); - function guessLocale() { - if (!detectLocale) return; - const locale = - shim.getEnv('LC_ALL') || - shim.getEnv('LC_MESSAGES') || - shim.getEnv('LANG') || - shim.getEnv('LANGUAGE') || - 'en_US'; - self.locale(locale.replace(/[.:].*/, '')); - } + // Legacy yargs.argv interface, it's recommended that you use .parse(). + Object.defineProperty(yargs, 'argv', { + get: () => { + return yargs.parse(); + }, + enumerable: true, + }); // an app should almost always have --version and --help, // if you *really* want to disable this use .help(false)/.version(false). - self.help(); - self.version(); - - return self; + yargs.help(); + yargs.version(); + return yargs; } // rebase an absolute path to a relative one with respect to a base directory @@ -1973,264 +2037,12 @@ export interface RebaseFunction { export const rebase: RebaseFunction = (base, dir) => shim.path.relative(base, dir); -/** Instance of the yargs module. */ -export interface YargsInstance { - $0: string; - argv: Arguments; - customScriptName: boolean; - parsed: DetailedArguments | false; - // The methods below are called after the parse in yargs-parser is complete - // and perform cleanup on the object generated: - _postProcess>( - argv: T, - populateDoubleDash: boolean, - calledFromCommand: boolean, - runGlobalMiddleware: boolean - ): T; - _copyDoubleDash(argv: T): T; - _parsePositionalNumbers(argv: T): T; - _getLoggerInstance(): LoggerInstance; - _getParseContext(): Object; - _hasOutput(): boolean; - _hasParseCallback(): boolean; - _parseArgs: { - ( - args: string | string[] | null, - shortCircuit?: boolean, - calledFromCommand?: boolean, - commandIndex?: number, - helpOnly?: boolean - ): Arguments | Promise; - (args: string | string[], shortCircuit?: boolean): - | Arguments - | Promise; - }; - _runValidation( - aliases: Dictionary, - positionalMap: Dictionary, - parseErrors: Error | null, - isDefaultCommand?: boolean - ): (argv: Arguments) => void; - _setHasOutput(): void; - addHelpOpt: { - (opt?: string | false): YargsInstance; - (opt?: string, msg?: string): YargsInstance; - }; - addShowHiddenOpt: { - (opt?: string | false): YargsInstance; - (opt?: string, msg?: string): YargsInstance; - }; - alias: { - (keys: string | string[], aliases: string | string[]): YargsInstance; - (keyAliases: Dictionary): YargsInstance; - }; - array(keys: string | string[]): YargsInstance; - boolean(keys: string | string[]): YargsInstance; - check(f: (argv: Arguments) => any, global?: boolean): YargsInstance; - choices: { - (keys: string | string[], choices: string | string[]): YargsInstance; - (keyChoices: Dictionary): YargsInstance; - }; - coerce: { - (keys: string | string[], coerceCallback: CoerceCallback): YargsInstance; - (keyCoerceCallbacks: Dictionary): YargsInstance; - }; - command: { - ( - cmd: string | string[], - description: CommandHandler['description'], - builder?: CommandBuilderDefinition | CommandBuilder, - handler?: CommandHandlerCallback, - commandMiddleware?: Middleware[], - deprecated?: boolean - ): YargsInstance; - (handler: CommandHandlerDefinition): YargsInstance; - }; - commands: YargsInstance['command']; - commandDir(dir: string, opts?: RequireDirectoryOptions): YargsInstance; - completion: { - (cmd?: string, fn?: CompletionFunction): YargsInstance; - ( - cmd?: string, - desc?: string | false, - fn?: CompletionFunction - ): YargsInstance; - }; - config: { - (config: Dictionary): YargsInstance; - (keys?: string | string[], configCallback?: ConfigCallback): YargsInstance; - ( - keys?: string | string[], - msg?: string, - configCallback?: ConfigCallback - ): YargsInstance; - }; - conflicts: { - (key: string, conflictsWith: string | string[]): YargsInstance; - (keyConflicts: Dictionary): YargsInstance; - }; - count(keys: string | string[]): YargsInstance; - default: { - (key: string, value: any, defaultDescription?: string): YargsInstance; - (keys: string[], value: Exclude): YargsInstance; - (keys: Dictionary): YargsInstance; - }; - defaults: YargsInstance['default']; - demand: { - (min: number, max?: number | string, msg?: string): YargsInstance; - (keys: string | string[], msg?: string | true): YargsInstance; - ( - keys: string | string[], - max: string[], - msg?: string | true - ): YargsInstance; - (keyMsgs: Dictionary): YargsInstance; - ( - keyMsgs: Dictionary, - max: string[], - msg?: string - ): YargsInstance; - }; - demandCommand(): YargsInstance; - demandCommand(min: number, minMsg?: string): YargsInstance; - demandCommand( - min: number, - max: number, - minMsg?: string | null, - maxMsg?: string | null - ): YargsInstance; - demandOption: { - (keys: string | string[], msg?: string): YargsInstance; - (keyMsgs: Dictionary): YargsInstance; - }; - deprecateOption(option: string, message?: string | boolean): YargsInstance; - describe: { - (keys: string | string[], description?: string): YargsInstance; - (keyDescriptions: Dictionary): YargsInstance; - }; - detectLocale(detect: boolean): YargsInstance; - env(prefix?: string | false): YargsInstance; - epilog: YargsInstance['epilogue']; - epilogue(msg: string): YargsInstance; - example( - cmd: string | [string, string?][], - description?: string - ): YargsInstance; - exit(code: number, err?: YError | string): void; - exitProcess(enabled: boolean): YargsInstance; - fail(f: FailureFunction | boolean): YargsInstance; - getCommandInstance(): CommandInstance; - getCompletion( - args: string[], - done?: (err: Error | null, completions: string[] | undefined) => void - ): Promise | any; - getHelp(): Promise; - getContext(): Context; - getAliases(): Dictionary; - getDemandedCommands(): Options['demandedCommands']; - getDemandedOptions(): Options['demandedOptions']; - getDeprecatedOptions(): Options['deprecatedOptions']; - getDetectLocale(): boolean; - getExitProcess(): boolean; - getGroups(): Dictionary; - getOptions(): Options; - getParserConfiguration(): Configuration; - getStrict(): boolean; - getStrictCommands(): boolean; - getStrictOptions(): boolean; - getUsageInstance(): UsageInstance; - getValidationInstance(): ValidationInstance; - global(keys: string | string[], global?: boolean): YargsInstance; - group(keys: string | string[], groupName: string): YargsInstance; - help: YargsInstance['addHelpOpt']; - hide(key: string): YargsInstance; - implies: { - (key: string, implication: KeyOrPos | KeyOrPos[]): YargsInstance; - (keyImplications: Dictionary): YargsInstance; - }; - locale: { - (): string; - (locale: string): YargsInstance; - }; - middleware( - callback: MiddlewareCallback | MiddlewareCallback[], - applyBeforeValidation?: boolean, - global?: boolean - ): YargsInstance; - nargs: { - (keys: string | string[], nargs: number): YargsInstance; - (keyNargs: Dictionary): YargsInstance; - }; - normalize(keys: string | string[]): YargsInstance; - number(keys: string | string[]): YargsInstance; - option: { - (key: string, optionDefinition: OptionDefinition): YargsInstance; - (keyOptionDefinitions: Dictionary): YargsInstance; - }; - options: YargsInstance['option']; - parse: { - (): Arguments | Promise; - (args: string | string[]): Arguments | Promise; - (args: string | string[], context: object, parseCallback?: ParseCallback): - | Arguments - | Promise; - (args: string | string[], parseCallback: ParseCallback): - | Arguments - | Promise; - (args: string | string[], shortCircuit: boolean): - | Arguments - | Promise; - }; - parserConfiguration(config: Configuration): YargsInstance; - pkgConf(key: string, rootPath?: string): YargsInstance; - positional( - key: string, - positionalDefinition: PositionalDefinition - ): YargsInstance; - recommendCommands(recommend: boolean): YargsInstance; - require: YargsInstance['demand']; - required: YargsInstance['demand']; - requiresArg(keys: string | string[] | Dictionary): YargsInstance; - reset(aliases?: DetailedArguments['aliases']): YargsInstance; - resetOptions(aliases?: DetailedArguments['aliases']): YargsInstance; - scriptName(scriptName: string): YargsInstance; - showCompletionScript($0?: string, cmd?: string): YargsInstance; - showHelp(level: 'error' | 'log' | ((message: string) => void)): YargsInstance; - showVersion( - level: 'error' | 'log' | ((message: string) => void) - ): YargsInstance; - showHelpOnFail: { - (message?: string): YargsInstance; - (enabled: boolean, message: string): YargsInstance; - }; - showHidden: YargsInstance['addShowHiddenOpt']; - skipValidation(keys: string | string[]): YargsInstance; - strict(enable?: boolean): YargsInstance; - strictCommands(enable?: boolean): YargsInstance; - strictOptions(enable?: boolean): YargsInstance; - string(key: string | string[]): YargsInstance; - terminalWidth(): number | null; - updateStrings(obj: Dictionary): YargsInstance; - updateLocale: YargsInstance['updateStrings']; - usage: { - (msg: string | null): YargsInstance; - ( - msg: string, - description: CommandHandler['description'], - builder?: CommandBuilderDefinition | CommandBuilder, - handler?: CommandHandlerCallback - ): YargsInstance; - }; - version: { - (ver?: string | false): YargsInstance; - (key?: string, ver?: string): YargsInstance; - (key?: string, msg?: string, ver?: string): YargsInstance; - }; - wrap(cols: number | null | undefined): YargsInstance; -} - export function isYargsInstance(y: YargsInstance | void): y is YargsInstance { - return !!y && typeof y._parseArgs === 'function'; + return ( + !!y && + typeof y.demandOption === 'function' && + typeof y.getCommandInstance === 'function' + ); } /** Yargs' context. */ @@ -2360,6 +2172,10 @@ interface ParseCallback { ): void; } +interface Aliases { + [key: string]: Array; +} + export interface Arguments { /** The script name or node command */ $0: string; diff --git a/package.json b/package.json index 60c83ecea..554c021ed 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "cpr": "^3.0.1", "cross-env": "^7.0.2", "cross-spawn": "^7.0.0", + "eslint": "^7.23.0", "gts": "^3.0.0", "hashish": "0.0.4", "mocha": "^8.0.0", diff --git a/test/command.cjs b/test/command.cjs index 642b715cd..e8f1dbbb8 100644 --- a/test/command.cjs +++ b/test/command.cjs @@ -1845,39 +1845,33 @@ describe('Command', () => { }); // see: https://github.com/yargs/yargs/issues/1144 - it('displays error and appropriate help message when handler fails', done => { - const y = yargs('foo') - .command( - 'foo', - 'foo command', - yargs => { - yargs.option('bar', { - describe: 'bar option', - }); - }, - argv => { - return Promise.reject(Error('foo error')); - } - ) - .exitProcess(false); + it('displays error and appropriate help message when handler fails', async () => { // the bug reported in #1144 only happens when // usage.help() is called, this does not occur when // console output is suppressed. tldr; we capture // the log output: - let errorLog = ''; - y._getLoggerInstance().error = out => { - if (out) errorLog += `${out}\n`; - }; - y.parse(); - // the promise returned by the handler rejects immediately, - // so we can wait for a small number of ms: - setTimeout(() => { - // ensure the appropriate help is displayed: - errorLog.should.include('bar option'); - // ensure error was displayed: - errorLog.should.include('foo error'); - return done(); - }, 5); + const r = await checkOutput(async () => { + return yargs('foo') + .command( + 'foo', + 'foo command', + yargs => { + yargs.option('bar', { + describe: 'bar option', + }); + }, + argv => { + return Promise.reject(Error('foo error')); + } + ) + .exitProcess(false) + .parse(); + }); + const errorLog = r.errors.join('\n'); + // ensure the appropriate help is displayed: + errorLog.should.include('bar option'); + // ensure error was displayed: + errorLog.should.include('foo error'); }); }); diff --git a/test/helpers/utils.cjs b/test/helpers/utils.cjs index ec2ef8e5f..59f855137 100644 --- a/test/helpers/utils.cjs +++ b/test/helpers/utils.cjs @@ -60,6 +60,9 @@ exports.checkOutput = function checkOutput(f, argv, cb) { return result.then((r) => { reset(); return done(); + }).catch(() => { + reset(); + return done(); }); } else { reset(); From cc5c970f78616da1645023854f54724ff4761b42 Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 28 Mar 2021 14:27:11 -0700 Subject: [PATCH 2/6] refactor!: make reset() method private --- docs/api.md | 37 - index.cjs | 1 + lib/command.ts | 73 +- lib/completion.ts | 5 +- lib/usage.ts | 15 +- lib/validation.ts | 22 +- lib/yargs-factory.ts | 1651 ++++++++++++++++++++++-------------------- test/command.cjs | 2 +- test/completion.cjs | 4 +- test/usage.cjs | 2 +- test/validation.cjs | 2 +- test/yargs.cjs | 8 +- 12 files changed, 938 insertions(+), 884 deletions(-) diff --git a/docs/api.md b/docs/api.md index 9bad6fcac..9bcf3e2cb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1439,43 +1439,6 @@ usage information and exit. The default behavior is to set the value of any key not followed by an option value to `true`. -.reset() [DEPRECATED] --------- - -Reset the argument object built up so far. This is useful for -creating nested command line interfaces. Use [global](#global) -to specify keys that should not be reset. - -```js -var yargs = require('yargs/yargs')(process.argv.slice(2)) - .usage('$0 command') - .command('hello', 'hello command') - .command('world', 'world command') - .demandCommand(1, 'must provide a valid command'), - argv = yargs.argv, - command = argv._[0]; - -if (command === 'hello') { - yargs.reset() - .usage('$0 hello') - .help('h') - .example('$0 hello', 'print the hello message!') - .argv - - console.log('hello!'); -} else if (command === 'world'){ - yargs.reset() - .usage('$0 world') - .help('h') - .example('$0 world', 'print the world message!') - .argv - - console.log('world!'); -} else { - yargs.showHelp(); -} -``` - .scriptName($0) ------------------ diff --git a/index.cjs b/index.cjs index d5bccf820..d1a169885 100644 --- a/index.cjs +++ b/index.cjs @@ -11,6 +11,7 @@ module.exports = Argv; function Argv(processArgs, cwd) { const argv = Yargs(processArgs, cwd, require); singletonify(argv); + // TODO(bcoe): warn if argv.parse() or argv.argv is used directly. return argv; } diff --git a/lib/command.ts b/lib/command.ts index 9b237f5b9..727d32a44 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -207,7 +207,7 @@ export class CommandInstance { this.handlers[command!] || this.handlers[this.aliasMap[command!]] || this.defaultCommand; - const currentContext = yargs.getContext(); + const currentContext = yargs.getInternalMethods().getContext(); const parentCommands = currentContext.commands.slice(); if (command) { currentContext.commands.push(command); @@ -264,7 +264,10 @@ export class CommandInstance { if (isCommandBuilderCallback(builder)) { // A function can be provided, which builds // up a yargs chain and possibly returns it. - const builderOutput = builder(yargs.reset(aliases), helpOrVersionSet); + const builderOutput = builder( + yargs.getInternalMethods().reset(aliases), + helpOrVersionSet + ); // Support the use-case of async builders: if (isPromise(builderOutput)) { return builderOutput.then(output => { @@ -282,7 +285,7 @@ export class CommandInstance { } else if (isCommandBuilderOptionDefinitions(builder)) { // as a short hand, an object can instead be provided, specifying // the options that a command takes. - innerYargs = yargs.reset(aliases); + innerYargs = yargs.getInternalMethods().reset(aliases); Object.keys(commandHandler.builder).forEach(key => { innerYargs.option(key, builder[key]); }); @@ -307,9 +310,10 @@ export class CommandInstance { // 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 (!command) innerYargs.getUsageInstance().unfreeze(); + if (!command) innerYargs.getInternalMethods().getUsageInstance().unfreeze(); if (this.shouldUpdateUsage(innerYargs)) { innerYargs + .getInternalMethods() .getUsageInstance() .usage( this.usageFromParentCommandsCommandHandler( @@ -319,13 +323,15 @@ export class CommandInstance { commandHandler.description ); } - const innerArgv = innerYargs.runYargsParserAndExecuteCommands( - null, - undefined, - true, - commandIndex, - helpOnly - ); + const innerArgv = innerYargs + .getInternalMethods() + .runYargsParserAndExecuteCommands( + null, + undefined, + true, + commandIndex, + helpOnly + ); return { aliases: (innerYargs.parsed as DetailedArguments).aliases, innerArgv: innerArgv as Arguments, @@ -333,8 +339,8 @@ export class CommandInstance { } private shouldUpdateUsage(yargs: YargsInstance) { return ( - !yargs.getUsageInstance().getUsageDisabled() && - yargs.getUsageInstance().getUsage().length === 0 + !yargs.getInternalMethods().getUsageInstance().getUsageDisabled() && + yargs.getInternalMethods().getUsageInstance().getUsage().length === 0 ); } private usageFromParentCommandsCommandHandler( @@ -364,7 +370,7 @@ export class CommandInstance { // execute middleware or handlers (these may perform expensive operations // like creating a DB connection). if (helpOnly) return innerArgv; - if (!yargs.getHasOutput()) { + if (!yargs.getInternalMethods().getHasOutput()) { positionalMap = this.populatePositionals( commandHandler, innerArgv as Arguments, @@ -380,27 +386,31 @@ export class CommandInstance { // we apply validation post-hoc, so that custom // checks get passed populated positional arguments. - if (!yargs.getHasOutput()) { - const validation = yargs.runValidation( - aliases, - positionalMap, - (yargs.parsed as DetailedArguments).error, - !command - ); + if (!yargs.getInternalMethods().getHasOutput()) { + const validation = yargs + .getInternalMethods() + .runValidation( + aliases, + positionalMap, + (yargs.parsed as DetailedArguments).error, + !command + ); innerArgv = maybeAsyncResult(innerArgv, result => { validation(result); return result; }); } - if (commandHandler.handler && !yargs.getHasOutput()) { - yargs.setHasOutput(); + if (commandHandler.handler && !yargs.getInternalMethods().getHasOutput()) { + yargs.getInternalMethods().setHasOutput(); // to simplify the parsing of positionals in commands, // we temporarily populate '--' rather than _, with arguments const populateDoubleDash = !!yargs.getOptions().configuration[ 'populate--' ]; - yargs.postProcess(innerArgv, populateDoubleDash, false, false); + yargs + .getInternalMethods() + .postProcess(innerArgv, populateDoubleDash, false, false); innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false); innerArgv = maybeAsyncResult(innerArgv, result => { @@ -412,11 +422,14 @@ export class CommandInstance { } }); - yargs.getUsageInstance().cacheHelpMessage(); - if (isPromise(innerArgv) && !yargs.hasParseCallback()) { + yargs.getInternalMethods().getUsageInstance().cacheHelpMessage(); + if ( + isPromise(innerArgv) && + !yargs.getInternalMethods().hasParseCallback() + ) { innerArgv.catch(error => { try { - yargs.getUsageInstance().fail(null, error); + yargs.getInternalMethods().getUsageInstance().fail(null, error); } catch (_err) { // If .fail(false) is not set, and no parse cb() has been // registered, run usage's default fail method. @@ -560,7 +573,10 @@ export class CommandInstance { ); if (parsed.error) { - yargs.getUsageInstance().fail(parsed.error.message, parsed.error); + yargs + .getInternalMethods() + .getUsageInstance() + .fail(parsed.error.message, parsed.error); } else { // only copy over positional keys (don't overwrite // flag arguments that were already parsed). @@ -587,6 +603,7 @@ export class CommandInstance { ? this.defaultCommand.original : this.defaultCommand.original.replace(/^[^[\]<>]*/, '$0 '); yargs + .getInternalMethods() .getUsageInstance() .usage(commandString, this.defaultCommand.description); } diff --git a/lib/completion.ts b/lib/completion.ts index 1f9cbeff6..15463af1e 100644 --- a/lib/completion.ts +++ b/lib/completion.ts @@ -57,7 +57,7 @@ export class Completion implements CompletionInstance { if (handlers[args[i]] && handlers[args[i]].builder) { const builder = handlers[args[i]].builder; if (isCommandBuilderCallback(builder)) { - const y = this.yargs.reset(); + const y = this.yargs.getInternalMethods().reset(); builder(y, true); return y.argv; } @@ -77,7 +77,8 @@ export class Completion implements CompletionInstance { args: string[], current: string ) { - const parentCommands = this.yargs.getContext().commands; + const parentCommands = this.yargs.getInternalMethods().getContext() + .commands; if ( !current.match(/^-/) && parentCommands[parentCommands.length - 1] !== current diff --git a/lib/usage.ts b/lib/usage.ts index 3ea1b08d2..be242843e 100644 --- a/lib/usage.ts +++ b/lib/usage.ts @@ -37,7 +37,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { let failureOutput = false; self.fail = function fail(msg, err) { - const logger = yargs.getLoggerInstance(); + const logger = yargs.getInternalMethods().getLoggerInstance(); if (fails.length) { for (let i = fails.length - 1; i >= 0; --i) { @@ -69,7 +69,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { err = err || new YError(msg); if (yargs.getExitProcess()) { return yargs.exit(1); - } else if (yargs.hasParseCallback()) { + } else if (yargs.getInternalMethods().hasParseCallback()) { return yargs.exit(1, err); } else { throw err; @@ -234,12 +234,15 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { if (commands.length > 1 || (commands.length === 1 && !commands[0][2])) { ui.div(__('Commands:')); - const context = yargs.getContext(); + const context = yargs.getInternalMethods().getContext(); const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''; - if (yargs.getParserConfiguration()['sort-commands'] === true) { + if ( + yargs.getInternalMethods().getParserConfiguration()['sort-commands'] === + true + ) { commands = commands.sort((a, b) => a[0].localeCompare(b[0])); } @@ -598,7 +601,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { } self.showHelp = (level: 'error' | 'log' | ((message: string) => void)) => { - const logger = yargs.getLoggerInstance(); + const logger = yargs.getInternalMethods().getLoggerInstance(); if (!level) level = 'error'; const emit = typeof level === 'function' ? level : logger[level]; emit(self.help()); @@ -670,7 +673,7 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { }; self.showVersion = level => { - const logger = yargs.getLoggerInstance(); + const logger = yargs.getInternalMethods().getLoggerInstance(); if (!level) level = 'error'; const emit = typeof level === 'function' ? level : logger[level]; emit(version); diff --git a/lib/validation.ts b/lib/validation.ts index 6cab24430..25fa695b0 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -32,7 +32,8 @@ export function validation( // don't count currently executing commands const positionalCount = argv._.length + (argv['--'] ? argv['--'].length : 0); - const _s = positionalCount - yargs.getContext().commands.length; + const _s = + positionalCount - yargs.getInternalMethods().getContext().commands.length; if ( demandedCommands._ && @@ -146,15 +147,21 @@ export function validation( isDefaultCommand, checkPositionals = true ) { - const commandKeys = yargs.getCommandInstance().getCommands(); + const commandKeys = yargs + .getInternalMethods() + .getCommandInstance() + .getCommands(); const unknown: string[] = []; - const currentContext = yargs.getContext(); + const currentContext = yargs.getInternalMethods().getContext(); Object.keys(argv).forEach(key => { if ( specialKeys.indexOf(key) === -1 && !Object.prototype.hasOwnProperty.call(positionalMap, key) && - !Object.prototype.hasOwnProperty.call(yargs.getParseContext(), key) && + !Object.prototype.hasOwnProperty.call( + yargs.getInternalMethods().getParseContext(), + key + ) && !self.isValidAndSomeAliasIsNotNew(key, aliases) ) { unknown.push(key); @@ -187,9 +194,12 @@ export function validation( }; self.unknownCommands = function unknownCommands(argv) { - const commandKeys = yargs.getCommandInstance().getCommands(); + const commandKeys = yargs + .getInternalMethods() + .getCommandInstance() + .getCommands(); const unknown: string[] = []; - const currentContext = yargs.getContext(); + const currentContext = yargs.getInternalMethods().getContext(); if (currentContext.commands.length > 0 || commandKeys.length > 0) { argv._.slice(currentContext.commands.length).forEach(key => { diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 7df5732b0..327dd039f 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -68,6 +68,43 @@ export function YargsWithShim(_shim: PlatformShim) { return YargsFactory; } +// Used to expose private methods to other module-level classes, +// such as the command parser and usage printer. +const kReset = Symbol('reset'); + +export interface YargsInternalMethods { + getCommandInstance(): CommandInstance; + getContext(): Context; + getHasOutput(): boolean; + getLoggerInstance(): LoggerInstance; + getParseContext(): Object; + getParserConfiguration(): Configuration; + getUsageInstance(): UsageInstance; + getValidationInstance(): ValidationInstance; + hasParseCallback(): boolean; + postProcess>( + argv: Arguments | Promise, + populateDoubleDash: boolean, + calledFromCommand: boolean, + runGlobalMiddleware: boolean + ): any; + reset(aliases?: Aliases): YargsInstance; + runValidation( + aliases: Dictionary, + positionalMap: Dictionary, + parseErrors: Error | null, + isDefaultCommand?: boolean + ): (argv: Arguments) => void; + runYargsParserAndExecuteCommands( + args: string | string[] | null, + shortCircuit?: boolean | null, + calledFromCommand?: boolean, + commandIndex?: number, + helpOnly?: boolean + ): Arguments | Promise; + setHasOutput(): void; +} + export class YargsInstance { $0: string; argv?: Arguments; @@ -121,11 +158,10 @@ export class YargsInstance { this.$0 = this.getDollarZero(); this.#y18n = shim.y18n; // TODO(@bcoe): make reset initialize dependent classes so ! is not required. - this.reset(); + this[kReset](); this.#options!.showHiddenOpt = this.#defaultShowHiddenOpt; this.#logger = this.createLogger(); } - // TODO(bcoe): arrange public methods alphabetically: addHelpOpt(opt?: string | false, msg?: string): YargsInstance { const defaultHelpOpt = 'help'; argsert('[string|boolean] [string]', [opt, msg], arguments.length); @@ -151,6 +187,7 @@ export class YargsInstance { help(opt?: string, msg?: string): YargsInstance { return this.addHelpOpt(opt, msg); } + addShowHiddenOpt(opt?: string | false, msg?: string): YargsInstance { argsert('[string|boolean] [string]', [opt, msg], arguments.length); if (opt === false && msg === undefined) return this; @@ -167,37 +204,193 @@ export class YargsInstance { showHidden(opt?: string | false, msg?: string): YargsInstance { return this.addShowHiddenOpt(opt, msg); } + + alias( + key: string | string[] | Dictionary, + value?: string | string[] + ): YargsInstance { + argsert( + ' [string|array]', + [key, value], + arguments.length + ); + this.populateParserHintArrayDictionary( + this.alias.bind(this), + 'alias', + key, + value + ); + return this; + } + array(keys: string | string[]): YargsInstance { + argsert('', [keys], arguments.length); + this.populateParserHintArray('array', keys); + return this; + } boolean(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); this.populateParserHintArray('boolean', keys); return this; } - describe( - keys: string | string[] | Dictionary, - description?: string + check(f: (argv: Arguments) => any, global?: boolean): YargsInstance { + argsert(' [boolean]', [f, global], arguments.length); + this.middleware( + ( + argv: Arguments, + _yargs: YargsInstance + ): Partial | Promise> => { + return maybeAsyncResult< + Partial | Promise> | any + >( + () => { + return f(argv); + }, + (result: any): Partial | Promise> => { + if (!result) { + this.#usage!.fail( + this.#y18n.__('Argument check failed: %s', f.toString()) + ); + } else if (typeof result === 'string' || result instanceof Error) { + this.#usage!.fail(result.toString(), result); + } + return argv; + }, + (err: Error): Partial | Promise> => { + this.#usage!.fail(err.message ? err.message : err.toString(), err); + return argv; + } + ); + }, + false, + global + ); + return this; + } + choices( + key: string | string[] | Dictionary, + value?: string | string[] ): YargsInstance { argsert( - ' [string]', - [keys, description], + ' [string|array]', + [key, value], arguments.length ); - this.setKey(keys, true); - this.#usage!.describe(keys, description); + this.populateParserHintArrayDictionary( + this.choices.bind(this), + 'choices', + key, + value + ); return this; } - scriptName(scriptName: string): YargsInstance { - this.customScriptName = true; - this.$0 = scriptName; + coerce( + keys: string | string[] | Dictionary, + value?: CoerceCallback + ): YargsInstance { + argsert( + ' [function]', + [keys, value], + arguments.length + ); + if (Array.isArray(keys)) { + if (!value) { + throw new YError('coerce callback must be provided'); + } + for (const key of keys) { + this.coerce(key, value); + } + return this; + } else if (typeof keys === 'object') { + for (const key of Object.keys(keys)) { + this.coerce(key, keys[key]); + } + return this; + } + if (!value) { + throw new YError('coerce callback must be provided'); + } + // This noop tells yargs-parser about the existence of the option + // represented by "keys", so that it can apply camel case expansion + // if needed: + this.alias(keys, keys); + this.#globalMiddleware.addCoerceMiddleware( + ( + argv: Arguments, + yargs: YargsInstance + ): Partial | Promise> => { + let aliases: Dictionary; + return maybeAsyncResult< + Partial | Promise> | any + >( + () => { + aliases = yargs.getAliases(); + return value(argv[keys]); + }, + (result: any): Partial => { + argv[keys] = result; + if (aliases[keys]) { + for (const alias of aliases[keys]) { + argv[alias] = result; + } + } + return argv; + }, + (err: Error): Partial | Promise> => { + throw new YError(err.message); + } + ); + }, + keys + ); return this; } - getContext(): Context { - return this.#context; + conflicts( + key1: string | Dictionary, + key2?: string | string[] + ): YargsInstance { + argsert(' [string|array]', [key1, key2], arguments.length); + this.#validation!.conflicts(key1, key2); + return this; } - // maybe exit, always capture context about why we wanted to exit: - exit(code: number, err?: YError | string): void { - this.#hasOutput = true; - this.#exitError = err; - if (this.#exitProcess) shim.process.exit(code); + config( + key: string | string[] | Dictionary = 'config', + msg?: string | ConfigCallback, + parseFn?: ConfigCallback + ): YargsInstance { + argsert( + '[object|string] [string|function] [function]', + [key, msg, parseFn], + arguments.length + ); + // allow a config object to be provided directly. + if (typeof key === 'object' && !Array.isArray(key)) { + key = applyExtends( + key, + this.#cwd, + this.getParserConfiguration()['deep-merge-config'] || false, + shim + ); + this.#options!.configObjects = ( + this.#options!.configObjects || [] + ).concat(key); + return this; + } + + // allow for a custom parsing function. + if (typeof msg === 'function') { + parseFn = msg; + msg = undefined; + } + + this.describe( + key, + msg || this.#usage!.deferY18nLookup('Path to JSON config file') + ); + (Array.isArray(key) ? key : [key]).forEach(k => { + this.#options!.config[k] = parseFn || true; + }); + + return this; } completion( cmd?: string, @@ -276,129 +469,11 @@ export class YargsInstance { this.#command!.addDirectory(dir, req, shim.getCallerFile(), opts); return this; } - async getCompletion( - args: string[], - done?: (err: Error | null, completions: string[] | undefined) => void - ): Promise { - argsert(' [function]', [args, done], arguments.length); - if (!done) { - return new Promise((resolve, reject) => { - this.#completion!.getCompletion(args, (err, completions) => { - if (err) reject(err); - else resolve(completions); - }); - }); - } else { - return this.#completion!.getCompletion(args, done); - } - } - getParserConfiguration() { - return this.#parserConfig; - } - parserConfiguration(config: Configuration) { - argsert('', [config], arguments.length); - this.#parserConfig = config; - return this; - } - array(keys: string | string[]): YargsInstance { - argsert('', [keys], arguments.length); - this.populateParserHintArray('array', keys); - return this; - } - number(keys: string | string[]): YargsInstance { - argsert('', [keys], arguments.length); - this.populateParserHintArray('number', keys); - return this; - } - normalize(keys: string | string[]): YargsInstance { - argsert('', [keys], arguments.length); - this.populateParserHintArray('normalize', keys); - return this; - } count(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); this.populateParserHintArray('count', keys); return this; } - string(key: string | string[]): YargsInstance { - argsert('', [key], arguments.length); - this.populateParserHintArray('string', key); - return this; - } - requiresArg(keys: string | string[] | Dictionary): YargsInstance { - // the 2nd paramter [number] in the argsert the assertion is mandatory - // as populateParserHintSingleValueDictionary recursively calls requiresArg - // with Nan as a 2nd parameter, although we ignore it - argsert(' [number]', [keys], arguments.length); - // If someone configures nargs at the same time as requiresArg, - // nargs should take precedence, - // see: https://github.com/yargs/yargs/pull/1572 - // TODO: make this work with aliases, using a check similar to - // checkAllAliases() in yargs-parser. - if (typeof keys === 'string' && this.#options!.narg[keys]) { - return this; - } else { - this.populateParserHintSingleValueDictionary( - this.requiresArg.bind(this), - 'narg', - keys, - NaN - ); - } - return this; - } - skipValidation(keys: string | string[]): YargsInstance { - argsert('', [keys], arguments.length); - this.populateParserHintArray('skipValidation', keys); - return this; - } - nargs( - key: string | string[] | Dictionary, - value?: number - ): YargsInstance { - argsert(' [number]', [key, value], arguments.length); - this.populateParserHintSingleValueDictionary( - this.nargs.bind(this), - 'narg', - key, - value - ); - return this; - } - choices( - key: string | string[] | Dictionary, - value?: string | string[] - ): YargsInstance { - argsert( - ' [string|array]', - [key, value], - arguments.length - ); - this.populateParserHintArrayDictionary( - this.choices.bind(this), - 'choices', - key, - value - ); - return this; - } - alias( - key: string | string[] | Dictionary, - value?: string | string[] - ): YargsInstance { - argsert( - ' [string|array]', - [key, value], - arguments.length - ); - this.populateParserHintArrayDictionary( - this.alias.bind(this), - 'alias', - key, - value - ); - return this; - } default( key: string | string[] | Dictionary, value?: any, @@ -436,134 +511,31 @@ export class YargsInstance { ): YargsInstance { return this.default(key, value, defaultDescription); } - demandOption( - keys: string | string[] | Dictionary, - msg?: string - ): YargsInstance { - argsert(' [string]', [keys, msg], arguments.length); - this.populateParserHintSingleValueDictionary( - this.demandOption.bind(this), - 'demandedOptions', - keys, - msg - ); - return this; - } - coerce( - keys: string | string[] | Dictionary, - value?: CoerceCallback - ): YargsInstance { - argsert( - ' [function]', - [keys, value], - arguments.length - ); - if (Array.isArray(keys)) { - if (!value) { - throw new YError('coerce callback must be provided'); - } - for (const key of keys) { - this.coerce(key, value); - } - return this; - } else if (typeof keys === 'object') { - for (const key of Object.keys(keys)) { - this.coerce(key, keys[key]); - } - return this; - } - if (!value) { - throw new YError('coerce callback must be provided'); - } - // This noop tells yargs-parser about the existence of the option - // represented by "keys", so that it can apply camel case expansion - // if needed: - this.alias(keys, keys); - this.#globalMiddleware.addCoerceMiddleware( - ( - argv: Arguments, - yargs: YargsInstance - ): Partial | Promise> => { - let aliases: Dictionary; - return maybeAsyncResult< - Partial | Promise> | any - >( - () => { - aliases = yargs.getAliases(); - return value(argv[keys]); - }, - (result: any): Partial => { - argv[keys] = result; - if (aliases[keys]) { - for (const alias of aliases[keys]) { - argv[alias] = result; - } - } - return argv; - }, - (err: Error): Partial | Promise> => { - throw new YError(err.message); - } - ); - }, - keys - ); - return this; - } - getAliases(): Dictionary { - return this.parsed ? this.parsed.aliases : {}; - } - config( - key: string | string[] | Dictionary = 'config', - msg?: string | ConfigCallback, - parseFn?: ConfigCallback + demandCommand( + min = 1, + max?: number | string, + minMsg?: string | null, + maxMsg?: string | null ): YargsInstance { argsert( - '[object|string] [string|function] [function]', - [key, msg, parseFn], + '[number] [number|string] [string|null|undefined] [string|null|undefined]', + [min, max, minMsg, maxMsg], arguments.length ); - // allow a config object to be provided directly. - if (typeof key === 'object' && !Array.isArray(key)) { - key = applyExtends( - key, - this.#cwd, - this.getParserConfiguration()['deep-merge-config'] || false, - shim - ); - this.#options!.configObjects = ( - this.#options!.configObjects || [] - ).concat(key); - return this; - } - // allow for a custom parsing function. - if (typeof msg === 'function') { - parseFn = msg; - msg = undefined; + if (typeof max !== 'number') { + minMsg = max; + max = Infinity; } - this.describe( - key, - msg || this.#usage!.deferY18nLookup('Path to JSON config file') - ); - (Array.isArray(key) ? key : [key]).forEach(k => { - this.#options!.config[k] = parseFn || true; - }); - - return this; - } - example( - cmd: string | [string, string?][], - description?: string - ): YargsInstance { - argsert(' [string]', [cmd, description], arguments.length); + this.global('_', false); - if (Array.isArray(cmd)) { - cmd.forEach(exampleParams => this.example(...exampleParams)); - } else { - this.#usage!.example(cmd, description); - } + this.#options!.demandedCommands._ = { + min, + max, + minMsg, + maxMsg, + }; return this; } @@ -604,48 +576,168 @@ export class YargsInstance { return this; } - required( - keys: string | string[] | Dictionary | number, - max?: number | string[] | string | true, - msg?: string | true + demandOption( + keys: string | string[] | Dictionary, + msg?: string ): YargsInstance { - return this.demand(keys, max, msg); + argsert(' [string]', [keys, msg], arguments.length); + this.populateParserHintSingleValueDictionary( + this.demandOption.bind(this), + 'demandedOptions', + keys, + msg + ); + return this; } - require( - keys: string | string[] | Dictionary | number, - max?: number | string[] | string | true, - msg?: string | true - ): YargsInstance { - return this.demand(keys, max, msg); + deprecateOption(option: string, message: string | boolean): YargsInstance { + argsert(' [string|boolean]', [option, message], arguments.length); + this.#options!.deprecatedOptions[option] = message; + return this; } - demandCommand( - min = 1, - max?: number | string, - minMsg?: string | null, - maxMsg?: string | null + describe( + keys: string | string[] | Dictionary, + description?: string ): YargsInstance { argsert( - '[number] [number|string] [string|null|undefined] [string|null|undefined]', - [min, max, minMsg, maxMsg], + ' [string]', + [keys, description], arguments.length ); + this.setKey(keys, true); + this.#usage!.describe(keys, description); + return this; + } + detectLocale(detect: boolean): YargsInstance { + argsert('', [detect], arguments.length); + this.#detectLocale = detect; + return this; + } + // as long as options.envPrefix is not undefined, + // parser will apply env vars matching prefix to argv + env(prefix?: string | false): YargsInstance { + argsert('[string|boolean]', [prefix], arguments.length); + if (prefix === false) delete this.#options!.envPrefix; + else this.#options!.envPrefix = prefix || ''; + return this; + } + epilogue(msg: string): YargsInstance { + argsert('', [msg], arguments.length); + this.#usage!.epilog(msg); + return this; + } + epilog(msg: string): YargsInstance { + return this.epilogue(msg); + } + example( + cmd: string | [string, string?][], + description?: string + ): YargsInstance { + argsert(' [string]', [cmd, description], arguments.length); + + if (Array.isArray(cmd)) { + cmd.forEach(exampleParams => this.example(...exampleParams)); + } else { + this.#usage!.example(cmd, description); + } - if (typeof max !== 'number') { - minMsg = max; - max = Infinity; - } - - this.global('_', false); - - this.#options!.demandedCommands._ = { - min, - max, - minMsg, - maxMsg, - }; - return this; } + // maybe exit, always capture context about why we wanted to exit: + exit(code: number, err?: YError | string): void { + this.#hasOutput = true; + this.#exitError = err; + if (this.#exitProcess) shim.process.exit(code); + } + exitProcess(enabled = true): YargsInstance { + argsert('[boolean]', [enabled], arguments.length); + this.#exitProcess = enabled; + return this; + } + fail(f: FailureFunction | boolean): YargsInstance { + argsert('', [f], arguments.length); + if (typeof f === 'boolean' && f !== false) { + throw new YError( + "Invalid first argument. Expected function or boolean 'false'" + ); + } + this.#usage!.failFn(f); + return this; + } + getAliases(): Dictionary { + return this.parsed ? this.parsed.aliases : {}; + } + async getCompletion( + args: string[], + done?: (err: Error | null, completions: string[] | undefined) => void + ): Promise { + argsert(' [function]', [args, done], arguments.length); + if (!done) { + return new Promise((resolve, reject) => { + this.#completion!.getCompletion(args, (err, completions) => { + if (err) reject(err); + else resolve(completions); + }); + }); + } else { + return this.#completion!.getCompletion(args, done); + } + } + getDemandedOptions() { + argsert([], 0); + return this.#options!.demandedOptions; + } + getDemandedCommands() { + argsert([], 0); + return this.#options!.demandedCommands; + } + getDeprecatedOptions() { + argsert([], 0); + return this.#options!.deprecatedOptions; + } + getDetectLocale(): boolean { + return this.#detectLocale; + } + getExitProcess(): boolean { + return this.#exitProcess; + } + // combine explicit and preserved groups. explicit groups should be first + getGroups(): Dictionary { + return Object.assign({}, this.#groups, this.#preservedGroups); + } + getHelp(): Promise { + this.#hasOutput = true; + if (!this.#usage!.hasCachedHelpMessage()) { + if (!this.parsed) { + // Run the parser as if --help was passed to it (this is what + // the last parameter `true` indicates). + const parse = this.runYargsParserAndExecuteCommands( + this.#processArgs, + undefined, + undefined, + 0, + true + ); + if (isPromise(parse)) { + return parse.then(() => { + return this.#usage!.help(); + }); + } + } + } + return Promise.resolve(this.#usage!.help()); + } + getOptions(): Options { + return this.#options!; + } + getStrict(): boolean { + return this.#strict; + } + getStrictCommands(): boolean { + return this.#strictCommands; + } + getStrictOptions(): boolean { + return this.#strictOptions; + } global(globals: string | string[], global?: boolean): YargsInstance { argsert(' [boolean]', [globals, global], arguments.length); globals = ([] as string[]).concat(globals); @@ -661,22 +753,25 @@ export class YargsInstance { } return this; } - getDemandedOptions() { - argsert([], 0); - return this.#options!.demandedOptions; - } - getDemandedCommands() { - argsert([], 0); - return this.#options!.demandedCommands; - } - deprecateOption(option: string, message: string | boolean): YargsInstance { - argsert(' [string|boolean]', [option, message], arguments.length); - this.#options!.deprecatedOptions[option] = message; + group(opts: string | string[], groupName: string): YargsInstance { + argsert(' ', [opts, groupName], arguments.length); + const existing = + this.#preservedGroups[groupName] || this.#groups[groupName]; + if (this.#preservedGroups[groupName]) { + // we now only need to track this group name in groups. + delete this.#preservedGroups[groupName]; + } + const seen: Dictionary = {}; + this.#groups[groupName] = (existing || []).concat(opts).filter(key => { + if (seen[key]) return false; + return (seen[key] = true); + }); return this; } - getDeprecatedOptions() { - argsert([], 0); - return this.#options!.deprecatedOptions; + hide(key: string): YargsInstance { + argsert('', [key], arguments.length); + this.#options!.hiddenOptions.push(key); + return this; } implies( key: string | Dictionary, @@ -690,92 +785,15 @@ export class YargsInstance { this.#validation!.implies(key, value); return this; } - conflicts( - key1: string | Dictionary, - key2?: string | string[] - ): YargsInstance { - argsert(' [string|array]', [key1, key2], arguments.length); - this.#validation!.conflicts(key1, key2); - return this; - } - usage( - msg: string | null, - description?: CommandHandler['description'], - builder?: CommandBuilderDefinition | CommandBuilder, - handler?: CommandHandlerCallback - ): YargsInstance { - argsert( - ' [string|boolean] [function|object] [function]', - [msg, description, builder, handler], - arguments.length - ); - - if (description !== undefined) { - assertNotStrictEqual(msg, null, shim); - // .usage() can be used as an alias for defining - // a default command. - if ((msg || '').match(/^\$0( |$)/)) { - return this.command(msg, description, builder, handler); - } else { - throw new YError( - '.usage() description must start with $0 if being used as alias for .command()' - ); - } - } else { - this.#usage!.usage(msg); - return this; - } - } - epilogue(msg: string): YargsInstance { - argsert('', [msg], arguments.length); - this.#usage!.epilog(msg); - return this; - } - epilog(msg: string): YargsInstance { - return this.epilogue(msg); - } - fail(f: FailureFunction | boolean): YargsInstance { - argsert('', [f], arguments.length); - if (typeof f === 'boolean' && f !== false) { - throw new YError( - "Invalid first argument. Expected function or boolean 'false'" - ); + // TODO(bcoe): add getLocale() rather than overloading behavior. + locale(locale?: string): YargsInstance | string { + argsert('[string]', [locale], arguments.length); + if (!locale) { + this.guessLocale(); + return this.#y18n.getLocale(); } - this.#usage!.failFn(f); - return this; - } - check(f: (argv: Arguments) => any, global?: boolean): YargsInstance { - argsert(' [boolean]', [f, global], arguments.length); - this.middleware( - ( - argv: Arguments, - _yargs: YargsInstance - ): Partial | Promise> => { - return maybeAsyncResult< - Partial | Promise> | any - >( - () => { - return f(argv); - }, - (result: any): Partial | Promise> => { - if (!result) { - this.#usage!.fail( - this.#y18n.__('Argument check failed: %s', f.toString()) - ); - } else if (typeof result === 'string' || result instanceof Error) { - this.#usage!.fail(result.toString(), result); - } - return argv; - }, - (err: Error): Partial | Promise> => { - this.#usage!.fail(err.message ? err.message : err.toString(), err); - return argv; - } - ); - }, - false, - global - ); + this.#detectLocale = false; + this.#y18n.setLocale(locale); return this; } middleware( @@ -789,26 +807,27 @@ export class YargsInstance { global ); } - pkgConf(key: string, rootPath?: string): YargsInstance { - argsert(' [string]', [key, rootPath], arguments.length); - let conf = null; - // prefer cwd to require-main-filename in this method - // since we're looking for e.g. "nyc" config in nyc consumer - // rather than "yargs" config in nyc (where nyc is the main filename) - const obj = this.pkgUp(rootPath || this.#cwd); - - // If an object exists in the key, add it to options.configObjects - if (obj[key] && typeof obj[key] === 'object') { - conf = applyExtends( - obj[key] as {[key: string]: string}, - rootPath || this.#cwd, - this.getParserConfiguration()['deep-merge-config'] || false, - shim - ); - this.#options!.configObjects = ( - this.#options!.configObjects || [] - ).concat(conf); - } + nargs( + key: string | string[] | Dictionary, + value?: number + ): YargsInstance { + argsert(' [number]', [key, value], arguments.length); + this.populateParserHintSingleValueDictionary( + this.nargs.bind(this), + 'narg', + key, + value + ); + return this; + } + normalize(keys: string | string[]): YargsInstance { + argsert('', [keys], arguments.length); + this.populateParserHintArray('normalize', keys); + return this; + } + number(keys: string | string[]): YargsInstance { + argsert('', [keys], arguments.length); + this.populateParserHintArray('number', keys); return this; } option( @@ -940,31 +959,76 @@ export class YargsInstance { ): YargsInstance { return this.option(key, opt); } - getOptions(): Options { - return this.#options!; - } - group(opts: string | string[], groupName: string): YargsInstance { - argsert(' ', [opts, groupName], arguments.length); - const existing = - this.#preservedGroups[groupName] || this.#groups[groupName]; - if (this.#preservedGroups[groupName]) { - // we now only need to track this group name in groups. - delete this.#preservedGroups[groupName]; + parse( + args?: string | string[], + shortCircuit?: object | ParseCallback | boolean, + _parseFn?: ParseCallback + ): Arguments | Promise { + argsert( + '[string|array] [function|boolean|object] [function]', + [args, shortCircuit, _parseFn], + arguments.length + ); + this.freeze(); // Push current state of parser onto stack. + if (typeof args === 'undefined') { + const argv = this.runYargsParserAndExecuteCommands(this.#processArgs); + const tmpParsed = this.parsed; + this.unfreeze(); // Pop the stack. + this.parsed = tmpParsed; + return argv; } - const seen: Dictionary = {}; - this.#groups[groupName] = (existing || []).concat(opts).filter(key => { - if (seen[key]) return false; - return (seen[key] = true); - }); - return this; + + // a context object can optionally be provided, this allows + // additional information to be passed to a command handler. + if (typeof shortCircuit === 'object') { + this.#parseContext = shortCircuit; + shortCircuit = _parseFn; + } + + // by providing a function as a second argument to + // parse you can capture output that would otherwise + // default to printing to stdout/stderr. + if (typeof shortCircuit === 'function') { + this.#parseFn = shortCircuit as ParseCallback; + shortCircuit = false; + } + // completion short-circuits the parsing process, + // skipping validation, etc. + if (!shortCircuit) this.#processArgs = args; + + if (this.#parseFn) this.#exitProcess = false; + + const parsed = this.runYargsParserAndExecuteCommands(args, !!shortCircuit); + this.#completion!.setParsed(this.parsed as DetailedArguments); + if (this.#parseFn) this.#parseFn(this.#exitError, parsed, this.#output); + this.unfreeze(); // Pop the stack. + return parsed; } - // combine explicit and preserved groups. explicit groups should be first - getGroups(): Dictionary { - return Object.assign({}, this.#groups, this.#preservedGroups); + parserConfiguration(config: Configuration) { + argsert('', [config], arguments.length); + this.#parserConfig = config; + return this; } - hide(key: string): YargsInstance { - argsert('', [key], arguments.length); - this.#options!.hiddenOptions.push(key); + pkgConf(key: string, rootPath?: string): YargsInstance { + argsert(' [string]', [key, rootPath], arguments.length); + let conf = null; + // prefer cwd to require-main-filename in this method + // since we're looking for e.g. "nyc" config in nyc consumer + // rather than "yargs" config in nyc (where nyc is the main filename) + const obj = this.pkgUp(rootPath || this.#cwd); + + // If an object exists in the key, add it to options.configObjects + if (obj[key] && typeof obj[key] === 'object') { + conf = applyExtends( + obj[key] as {[key: string]: string}, + rootPath || this.#cwd, + this.getParserConfiguration()['deep-merge-config'] || false, + shim + ); + this.#options!.configObjects = ( + this.#options!.configObjects || [] + ).concat(conf); + } return this; } positional(key: string, opts: PositionalDefinition): YargsInstance { @@ -1016,44 +1080,62 @@ export class YargsInstance { this.group(key, this.#usage!.getPositionalGroupName()); return this.option(key, opts); } - // as long as options.envPrefix is not undefined, - // parser will apply env vars matching prefix to argv - env(prefix?: string | false): YargsInstance { - argsert('[string|boolean]', [prefix], arguments.length); - if (prefix === false) delete this.#options!.envPrefix; - else this.#options!.envPrefix = prefix || ''; - return this; - } - wrap(cols: number | null | undefined): YargsInstance { - argsert('', [cols], arguments.length); - this.#usage!.wrap(cols); + recommendCommands(recommend = true): YargsInstance { + argsert('[boolean]', [recommend], arguments.length); + this.#recommendCommands = recommend; return this; } - strict(enabled?: boolean): YargsInstance { - argsert('[boolean]', [enabled], arguments.length); - this.#strict = enabled !== false; - return this; + required( + keys: string | string[] | Dictionary | number, + max?: number | string[] | string | true, + msg?: string | true + ): YargsInstance { + return this.demand(keys, max, msg); } - getStrict(): boolean { - return this.#strict; + require( + keys: string | string[] | Dictionary | number, + max?: number | string[] | string | true, + msg?: string | true + ): YargsInstance { + return this.demand(keys, max, msg); } - strictCommands(enabled?: boolean): YargsInstance { - argsert('[boolean]', [enabled], arguments.length); - this.#strictCommands = enabled !== false; + requiresArg(keys: string | string[] | Dictionary): YargsInstance { + // the 2nd paramter [number] in the argsert the assertion is mandatory + // as populateParserHintSingleValueDictionary recursively calls requiresArg + // with Nan as a 2nd parameter, although we ignore it + argsert(' [number]', [keys], arguments.length); + // If someone configures nargs at the same time as requiresArg, + // nargs should take precedence, + // see: https://github.com/yargs/yargs/pull/1572 + // TODO: make this work with aliases, using a check similar to + // checkAllAliases() in yargs-parser. + if (typeof keys === 'string' && this.#options!.narg[keys]) { + return this; + } else { + this.populateParserHintSingleValueDictionary( + this.requiresArg.bind(this), + 'narg', + keys, + NaN + ); + } return this; } - getStrictCommands(): boolean { - return this.#strictCommands; - } - strictOptions(enabled?: boolean): YargsInstance { - argsert('[boolean]', [enabled], arguments.length); - this.#strictOptions = enabled !== false; + showCompletionScript($0?: string, cmd?: string): YargsInstance { + argsert('[string] [string]', [$0, cmd], arguments.length); + $0 = $0 || this.$0; + this.#logger.log( + this.#completion!.generateCompletionScript( + $0, + cmd || this.#completionCommand || 'completion' + ) + ); return this; } - getStrictOptions(): boolean { - return this.#strictOptions; - } - getHelp(): Promise { + showHelp( + level: 'error' | 'log' | ((message: string) => void) + ): YargsInstance { + argsert('[string|function]', [level], arguments.length); this.#hasOutput = true; if (!this.#usage!.hasCachedHelpMessage()) { if (!this.parsed) { @@ -1067,47 +1149,98 @@ export class YargsInstance { true ); if (isPromise(parse)) { - return parse.then(() => { - return this.#usage!.help(); + parse.then(() => { + this.#usage!.showHelp(level); }); + return this; } } } - return Promise.resolve(this.#usage!.help()); + this.#usage!.showHelp(level); + return this; } - showHelp( + scriptName(scriptName: string): YargsInstance { + this.customScriptName = true; + this.$0 = scriptName; + return this; + } + showHelpOnFail(enabled?: string | boolean, message?: string): YargsInstance { + argsert('[boolean|string] [string]', [enabled, message], arguments.length); + this.#usage!.showHelpOnFail(enabled, message); + return this; + } + showVersion( level: 'error' | 'log' | ((message: string) => void) ): YargsInstance { argsert('[string|function]', [level], arguments.length); - this.#hasOutput = true; - if (!this.#usage!.hasCachedHelpMessage()) { - if (!this.parsed) { - // Run the parser as if --help was passed to it (this is what - // the last parameter `true` indicates). - const parse = this.runYargsParserAndExecuteCommands( - this.#processArgs, - undefined, - undefined, - 0, - true + this.#usage!.showVersion(level); + return this; + } + skipValidation(keys: string | string[]): YargsInstance { + argsert('', [keys], arguments.length); + this.populateParserHintArray('skipValidation', keys); + return this; + } + strict(enabled?: boolean): YargsInstance { + argsert('[boolean]', [enabled], arguments.length); + this.#strict = enabled !== false; + return this; + } + strictCommands(enabled?: boolean): YargsInstance { + argsert('[boolean]', [enabled], arguments.length); + this.#strictCommands = enabled !== false; + return this; + } + strictOptions(enabled?: boolean): YargsInstance { + argsert('[boolean]', [enabled], arguments.length); + this.#strictOptions = enabled !== false; + return this; + } + string(key: string | string[]): YargsInstance { + argsert('', [key], arguments.length); + this.populateParserHintArray('string', key); + return this; + } + terminalWidth(): number | null { + argsert([], 0); + return shim.process.stdColumns; + } + updateLocale(obj: Dictionary): YargsInstance { + return this.updateStrings(obj); + } + updateStrings(obj: Dictionary): YargsInstance { + argsert('', [obj], arguments.length); + this.#detectLocale = false; + this.#y18n.updateLocale(obj); + return this; + } + usage( + msg: string | null, + description?: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback + ): YargsInstance { + argsert( + ' [string|boolean] [function|object] [function]', + [msg, description, builder, handler], + arguments.length + ); + + if (description !== undefined) { + assertNotStrictEqual(msg, null, shim); + // .usage() can be used as an alias for defining + // a default command. + if ((msg || '').match(/^\$0( |$)/)) { + return this.command(msg, description, builder, handler); + } else { + throw new YError( + '.usage() description must start with $0 if being used as alias for .command()' ); - if (isPromise(parse)) { - parse.then(() => { - this.#usage!.showHelp(level); - }); - return this; - } } + } else { + this.#usage!.usage(msg); + return this; } - this.#usage!.showHelp(level); - return this; - } - showVersion( - level: 'error' | 'log' | ((message: string) => void) - ): YargsInstance { - argsert('[string|function]', [level], arguments.length); - this.#usage!.showVersion(level); - return this; } version(opt?: string | false, msg?: string, ver?: string): YargsInstance { const defaultVersionOpt = 'version'; @@ -1148,122 +1281,84 @@ export class YargsInstance { this.describe(this.#versionOpt, msg); return this; } - showHelpOnFail(enabled?: string | boolean, message?: string): YargsInstance { - argsert('[boolean|string] [string]', [enabled, message], arguments.length); - this.#usage!.showHelpOnFail(enabled, message); - return this; - } - exitProcess(enabled = true): YargsInstance { - argsert('[boolean]', [enabled], arguments.length); - this.#exitProcess = enabled; - return this; - } - getExitProcess(): boolean { - return this.#exitProcess; - } - showCompletionScript($0?: string, cmd?: string): YargsInstance { - argsert('[string] [string]', [$0, cmd], arguments.length); - $0 = $0 || this.$0; - this.#logger.log( - this.#completion!.generateCompletionScript( - $0, - cmd || this.#completionCommand || 'completion' - ) - ); - return this; - } - // TODO(bcoe): add getLocale() rather than overloading behavior. - locale(locale?: string): YargsInstance | string { - argsert('[string]', [locale], arguments.length); - if (!locale) { - this.guessLocale(); - return this.#y18n.getLocale(); - } - this.#detectLocale = false; - this.#y18n.setLocale(locale); - return this; - } - updateStrings(obj: Dictionary): YargsInstance { - argsert('', [obj], arguments.length); - this.#detectLocale = false; - this.#y18n.updateLocale(obj); - return this; - } - updateLocale(obj: Dictionary): YargsInstance { - return this.updateStrings(obj); - } - detectLocale(detect: boolean): YargsInstance { - argsert('', [detect], arguments.length); - this.#detectLocale = detect; - return this; - } - getDetectLocale(): boolean { - return this.#detectLocale; - } - recommendCommands(recommend = true): YargsInstance { - argsert('[boolean]', [recommend], arguments.length); - this.#recommendCommands = recommend; + wrap(cols: number | null | undefined): YargsInstance { + argsert('', [cols], arguments.length); + this.#usage!.wrap(cols); return this; } - getCommandInstance(): CommandInstance { - return this.#command!; - } - getUsageInstance(): UsageInstance { - return this.#usage!; - } - getValidationInstance(): ValidationInstance { - return this.#validation!; - } - terminalWidth(): number | null { - argsert([], 0); - return shim.process.stdColumns; - } - parse( - args?: string | string[], - shortCircuit?: object | ParseCallback | boolean, - _parseFn?: ParseCallback - ): Arguments | Promise { - argsert( - '[string|array] [function|boolean|object] [function]', - [args, shortCircuit, _parseFn], - arguments.length - ); - this.freeze(); // Push current state of parser onto stack. - if (typeof args === 'undefined') { - const argv = this.runYargsParserAndExecuteCommands(this.#processArgs); - const tmpParsed = this.parsed; - this.unfreeze(); // Pop the stack. - this.parsed = tmpParsed; - return argv; - } - - // a context object can optionally be provided, this allows - // additional information to be passed to a command handler. - if (typeof shortCircuit === 'object') { - this.#parseContext = shortCircuit; - shortCircuit = _parseFn; - } - // by providing a function as a second argument to - // parse you can capture output that would otherwise - // default to printing to stdout/stderr. - if (typeof shortCircuit === 'function') { - this.#parseFn = shortCircuit as ParseCallback; - shortCircuit = false; - } - // completion short-circuits the parsing process, - // skipping validation, etc. - if (!shortCircuit) this.#processArgs = args; + // to simplify the parsing of positionals in commands, + // we temporarily populate '--' rather than _, with arguments + // after the '--' directive. After the parse, we copy these back. + private copyDoubleDash(argv: Arguments): any { + if (!argv._ || !argv['--']) return argv; + // eslint-disable-next-line prefer-spread + argv._.push.apply(argv._, argv['--']); - if (this.#parseFn) this.#exitProcess = false; + // We catch an error here, in case someone has called Object.seal() + // on the parsed object, see: https://github.com/babel/babel/pull/10733 + try { + delete argv['--']; + // eslint-disable-next-line no-empty + } catch (_err) {} - const parsed = this.runYargsParserAndExecuteCommands(args, !!shortCircuit); - this.#completion!.setParsed(this.parsed as DetailedArguments); - if (this.#parseFn) this.#parseFn(this.#exitError, parsed, this.#output); - this.unfreeze(); // Pop the stack. - return parsed; + return argv; + } + private createLogger(): LoggerInstance { + return { + log: (...args: any[]) => { + if (!this.hasParseCallback()) console.log(...args); + this.#hasOutput = true; + if (this.#output.length) this.#output += '\n'; + this.#output += args.join(' '); + }, + error: (...args: any[]) => { + if (!this.hasParseCallback()) console.error(...args); + this.#hasOutput = true; + if (this.#output.length) this.#output += '\n'; + this.#output += args.join(' '); + }, + }; + } + private deleteFromParserHintObject(optionKey: string) { + // delete from all parsing hints: + // boolean, array, key, alias, etc. + objectKeys(this.#options).forEach((hintKey: keyof Options) => { + // configObjects is not a parsing hint array + if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) + return; + const hint = this.#options![hintKey]; + if (Array.isArray(hint)) { + if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1); + } else if (typeof hint === 'object') { + delete (hint as Dictionary)[optionKey]; + } + }); + // now delete the description from usage.js. + delete this.#usage!.getDescriptions()[optionKey]; + } + private freeze() { + this.#frozens.push({ + options: this.#options!, + configObjects: this.#options!.configObjects.slice(0), + exitProcess: this.#exitProcess, + groups: this.#groups, + strict: this.#strict, + strictCommands: this.#strictCommands, + strictOptions: this.#strictOptions, + completionCommand: this.#completionCommand, + output: this.#output, + exitError: this.#exitError!, + hasOutput: this.#hasOutput, + parsed: this.parsed, + parseFn: this.#parseFn!, + parseContext: this.#parseContext, + }); + this.#usage!.freeze(); + this.#validation!.freeze(); + this.#command!.freeze(); + this.#globalMiddleware.freeze(); } - // TODO(bcoe): arrange private methods alphabetically: private getDollarZero(): string { let $0 = ''; // ignore the node bin, specify this in your @@ -1275,20 +1370,86 @@ export class YargsInstance { default$0 = shim.process.argv().slice(0, 1); } - $0 = default$0 - .map(x => { - const b = rebase(this.#cwd, x); - return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x; - }) - .join(' ') - .trim(); + $0 = default$0 + .map(x => { + const b = rebase(this.#cwd, x); + return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x; + }) + .join(' ') + .trim(); + + if (shim.getEnv('_') && shim.getProcessArgvBin() === shim.getEnv('_')) { + $0 = shim + .getEnv('_')! + .replace(`${shim.path.dirname(shim.process.execPath())}/`, ''); + } + return $0; + } + private getParserConfiguration(): Configuration { + return this.#parserConfig; + } + private guessLocale() { + if (!this.#detectLocale) return; + const locale = + shim.getEnv('LC_ALL') || + shim.getEnv('LC_MESSAGES') || + shim.getEnv('LANG') || + shim.getEnv('LANGUAGE') || + 'en_US'; + this.locale(locale.replace(/[.:].*/, '')); + } + private guessVersion(): string { + const obj = this.pkgUp(); + return (obj.version as string) || 'unknown'; + } + // We wait to coerce numbers for positionals until after the initial parse. + // This allows commands to configure number parsing on a positional by + // positional basis: + private parsePositionalNumbers(argv: Arguments): any { + const args: (string | number)[] = argv['--'] ? argv['--'] : argv._; + + for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { + if ( + shim.Parser.looksLikeNumber(arg) && + Number.isSafeInteger(Math.floor(parseFloat(`${arg}`))) + ) { + args[i] = Number(arg); + } + } + return argv; + } + private pkgUp(rootPath?: string) { + const npath = rootPath || '*'; + if (this.#pkgs[npath]) return this.#pkgs[npath]; + + let obj = {}; + try { + let startDir = rootPath || shim.mainFilename; - if (shim.getEnv('_') && shim.getProcessArgvBin() === shim.getEnv('_')) { - $0 = shim - .getEnv('_')! - .replace(`${shim.path.dirname(shim.process.execPath())}/`, ''); - } - return $0; + // When called in an environment that lacks require.main.filename, such as a jest test runner, + // startDir is already process.cwd(), and should not be shortened. + // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. + if (!rootPath && shim.path.extname(startDir)) { + startDir = shim.path.dirname(startDir); + } + + const pkgJsonPath = shim.findUp( + startDir, + (dir: string[], names: string[]) => { + if (names.includes('package.json')) { + return 'package.json'; + } else { + return undefined; + } + } + ); + assertNotStrictEqual(pkgJsonPath, undefined, shim); + obj = JSON.parse(shim.readFileSync(pkgJsonPath, 'utf8')); + // eslint-disable-next-line no-empty + } catch (_noop) {} + + this.#pkgs[npath] = obj || {}; + return this.#pkgs[npath]; } private populateParserHintArray>( type: T, @@ -1389,45 +1550,6 @@ export class YargsInstance { ); return this; } - private deleteFromParserHintObject(optionKey: string) { - // delete from all parsing hints: - // boolean, array, key, alias, etc. - objectKeys(this.#options).forEach((hintKey: keyof Options) => { - // configObjects is not a parsing hint array - if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) - return; - const hint = this.#options![hintKey]; - if (Array.isArray(hint)) { - if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1); - } else if (typeof hint === 'object') { - delete (hint as Dictionary)[optionKey]; - } - }); - // now delete the description from usage.js. - delete this.#usage!.getDescriptions()[optionKey]; - } - private freeze() { - this.#frozens.push({ - options: this.#options!, - configObjects: this.#options!.configObjects.slice(0), - exitProcess: this.#exitProcess, - groups: this.#groups, - strict: this.#strict, - strictCommands: this.#strictCommands, - strictOptions: this.#strictOptions, - completionCommand: this.#completionCommand, - output: this.#output, - exitError: this.#exitError!, - hasOutput: this.#hasOutput, - parsed: this.parsed, - parseFn: this.#parseFn!, - parseContext: this.#parseContext, - }); - this.#usage!.freeze(); - this.#validation!.freeze(); - this.#command!.freeze(); - this.#globalMiddleware.freeze(); - } private unfreeze() { const frozen = this.#frozens.pop(); assertNotStrictEqual(frozen, undefined, shim); @@ -1454,102 +1576,6 @@ export class YargsInstance { this.#command!.unfreeze(); this.#globalMiddleware.unfreeze(); } - // to simplify the parsing of positionals in commands, - // we temporarily populate '--' rather than _, with arguments - // after the '--' directive. After the parse, we copy these back. - private copyDoubleDash(argv: Arguments): any { - if (!argv._ || !argv['--']) return argv; - // eslint-disable-next-line prefer-spread - argv._.push.apply(argv._, argv['--']); - - // We catch an error here, in case someone has called Object.seal() - // on the parsed object, see: https://github.com/babel/babel/pull/10733 - try { - delete argv['--']; - // eslint-disable-next-line no-empty - } catch (_err) {} - - return argv; - } - // We wait to coerce numbers for positionals until after the initial parse. - // This allows commands to configure number parsing on a positional by - // positional basis: - private parsePositionalNumbers(argv: Arguments): any { - const args: (string | number)[] = argv['--'] ? argv['--'] : argv._; - - for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { - if ( - shim.Parser.looksLikeNumber(arg) && - Number.isSafeInteger(Math.floor(parseFloat(`${arg}`))) - ) { - args[i] = Number(arg); - } - } - return argv; - } - private pkgUp(rootPath?: string) { - const npath = rootPath || '*'; - if (this.#pkgs[npath]) return this.#pkgs[npath]; - - let obj = {}; - try { - let startDir = rootPath || shim.mainFilename; - - // When called in an environment that lacks require.main.filename, such as a jest test runner, - // startDir is already process.cwd(), and should not be shortened. - // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. - if (!rootPath && shim.path.extname(startDir)) { - startDir = shim.path.dirname(startDir); - } - - const pkgJsonPath = shim.findUp( - startDir, - (dir: string[], names: string[]) => { - if (names.includes('package.json')) { - return 'package.json'; - } else { - return undefined; - } - } - ); - assertNotStrictEqual(pkgJsonPath, undefined, shim); - obj = JSON.parse(shim.readFileSync(pkgJsonPath, 'utf8')); - // eslint-disable-next-line no-empty - } catch (_noop) {} - - this.#pkgs[npath] = obj || {}; - return this.#pkgs[npath]; - } - guessLocale() { - if (!this.#detectLocale) return; - const locale = - shim.getEnv('LC_ALL') || - shim.getEnv('LC_MESSAGES') || - shim.getEnv('LANG') || - shim.getEnv('LANGUAGE') || - 'en_US'; - this.locale(locale.replace(/[.:].*/, '')); - } - private guessVersion(): string { - const obj = this.pkgUp(); - return (obj.version as string) || 'unknown'; - } - private createLogger(): LoggerInstance { - return { - log: (...args: any[]) => { - if (!this.hasParseCallback()) console.log(...args); - this.#hasOutput = true; - if (this.#output.length) this.#output += '\n'; - this.#output += args.join(' '); - }, - error: (...args: any[]) => { - if (!this.hasParseCallback()) console.error(...args); - this.#hasOutput = true; - if (this.#output.length) this.#output += '\n'; - this.#output += args.join(' '); - }, - }; - } // If argv is a promise (which is possible if async middleware is used) // delay applying validation until the promise has resolved: private validateAsync( @@ -1561,13 +1587,83 @@ export class YargsInstance { return result; }); } - // TODO(bcoe): figure out how to make the following methods protected: + // Note: these method names could change at any time, and should not be + // depended upon externally: + getInternalMethods(): YargsInternalMethods { + return { + getCommandInstance: this.getCommandInstance.bind(this), + getContext: this.getContext.bind(this), + getHasOutput: this.getHasOutput.bind(this), + getLoggerInstance: this.getLoggerInstance.bind(this), + getParseContext: this.getParseContext.bind(this), + getParserConfiguration: this.getParserConfiguration.bind(this), + getUsageInstance: this.getUsageInstance.bind(this), + getValidationInstance: this.getValidationInstance.bind(this), + hasParseCallback: this.hasParseCallback.bind(this), + postProcess: this.postProcess.bind(this), + reset: this[kReset].bind(this), + runValidation: this.runValidation.bind(this), + runYargsParserAndExecuteCommands: this.runYargsParserAndExecuteCommands.bind( + this + ), + setHasOutput: this.setHasOutput.bind(this), + }; + } + private getCommandInstance(): CommandInstance { + return this.#command!; + } + private getContext(): Context { + return this.#context; + } + private getHasOutput(): boolean { + return this.#hasOutput; + } + private getLoggerInstance(): LoggerInstance { + return this.#logger; + } + private getParseContext(): Object { + return this.#parseContext || {}; + } + private getUsageInstance(): UsageInstance { + return this.#usage!; + } + private getValidationInstance(): ValidationInstance { + return this.#validation!; + } + private hasParseCallback(): boolean { + return !!this.#parseFn; + } + private postProcess>( + argv: Arguments | Promise, + populateDoubleDash: boolean, + calledFromCommand: boolean, + runGlobalMiddleware: boolean + ): any { + if (calledFromCommand) return argv; + if (isPromise(argv)) return argv; + if (!populateDoubleDash) { + argv = this.copyDoubleDash(argv); + } + const parsePositionalNumbers = + this.getParserConfiguration()['parse-positional-numbers'] || + this.getParserConfiguration()['parse-positional-numbers'] === undefined; + if (parsePositionalNumbers) { + argv = this.parsePositionalNumbers(argv as Arguments); + } + if (runGlobalMiddleware) { + argv = applyMiddleware( + argv, + this, + this.#globalMiddleware.getMiddleware(), + false + ); + } + return argv; + } // put yargs back into an initial state; this is used mainly for running // commands in a breadth first manner: - // TODO(bcoe): in release notes document that .reset() has been removed - // from API. - reset(aliases: Aliases = {}) { + [kReset](aliases: Aliases = {}): YargsInstance { this.#options = this.#options || ({} as Options); const tmpOptions = {} as Options; tmpOptions.local = this.#options.local ? this.#options.local : []; @@ -1664,7 +1760,7 @@ export class YargsInstance { return this; } - runYargsParserAndExecuteCommands( + private runYargsParserAndExecuteCommands( args: string | string[] | null, shortCircuit?: boolean | null, calledFromCommand?: boolean, @@ -1928,7 +2024,7 @@ export class YargsInstance { true ); } - runValidation( + private runValidation( aliases: Dictionary, positionalMap: Dictionary, parseErrors: Error | null, @@ -1960,48 +2056,9 @@ export class YargsInstance { this.#validation!.conflicting(argv); }; } - getHasOutput(): boolean { - return this.#hasOutput; - } - setHasOutput() { + private setHasOutput() { this.#hasOutput = true; } - postProcess>( - argv: Arguments | Promise, - populateDoubleDash: boolean, - calledFromCommand: boolean, - runGlobalMiddleware: boolean - ): any { - if (calledFromCommand) return argv; - if (isPromise(argv)) return argv; - if (!populateDoubleDash) { - argv = this.copyDoubleDash(argv); - } - const parsePositionalNumbers = - this.getParserConfiguration()['parse-positional-numbers'] || - this.getParserConfiguration()['parse-positional-numbers'] === undefined; - if (parsePositionalNumbers) { - argv = this.parsePositionalNumbers(argv as Arguments); - } - if (runGlobalMiddleware) { - argv = applyMiddleware( - argv, - this, - this.#globalMiddleware.getMiddleware(), - false - ); - } - return argv; - } - hasParseCallback(): boolean { - return !!this.#parseFn; - } - getLoggerInstance(): LoggerInstance { - return this.#logger; - } - getParseContext(): Object { - return this.#parseContext || {}; - } } // TODO(bcoe): simplify bootstrapping process so that only a single factory @@ -2038,11 +2095,7 @@ export const rebase: RebaseFunction = (base, dir) => shim.path.relative(base, dir); export function isYargsInstance(y: YargsInstance | void): y is YargsInstance { - return ( - !!y && - typeof y.demandOption === 'function' && - typeof y.getCommandInstance === 'function' - ); + return !!y && typeof y.getInternalMethods === 'function'; } /** Yargs' context. */ diff --git a/test/command.cjs b/test/command.cjs index e8f1dbbb8..9662c9948 100644 --- a/test/command.cjs +++ b/test/command.cjs @@ -16,7 +16,7 @@ async function wait() { describe('Command', () => { beforeEach(() => { - yargs.reset(); + yargs.getInternalMethods().reset(); }); describe('positional arguments', () => { diff --git a/test/completion.cjs b/test/completion.cjs index f99d9b0b5..001d09edd 100644 --- a/test/completion.cjs +++ b/test/completion.cjs @@ -8,11 +8,11 @@ require('chai').should(); describe('Completion', () => { beforeEach(() => { - yargs.reset(); + yargs.getInternalMethods().reset(); }); after(() => { - yargs.reset(); + yargs.getInternalMethods().reset(); }); describe('default completion behavior', () => { diff --git a/test/usage.cjs b/test/usage.cjs index a2aaca950..24bdfc4b0 100644 --- a/test/usage.cjs +++ b/test/usage.cjs @@ -19,7 +19,7 @@ async function wait(n = 10) { describe('usage tests', () => { beforeEach(() => { - yargs.reset(); + yargs.getInternalMethods().reset(); }); describe('demand options', () => { diff --git a/test/validation.cjs b/test/validation.cjs index 3db8d2835..def19aa7d 100644 --- a/test/validation.cjs +++ b/test/validation.cjs @@ -11,7 +11,7 @@ require('chai').should(); describe('validation tests', () => { beforeEach(() => { - yargs.reset(); + yargs.getInternalMethods().reset(); }); describe('implies', () => { diff --git a/test/yargs.cjs b/test/yargs.cjs index b682d078a..c6def0129 100644 --- a/test/yargs.cjs +++ b/test/yargs.cjs @@ -297,6 +297,7 @@ describe('yargs dsl tests', () => { .global('foo', false) .global('qux', false) .env('YARGS') + .getInternalMethods() .reset(); const emptyOptions = { @@ -343,7 +344,7 @@ describe('yargs dsl tests', () => { y.parse('hello', err => { err.message.should.match(/Missing required argument/); }); - y.reset(); + y.getInternalMethods().reset(); y.parse('cake', err => { expect(err).to.equal(null); return done(); @@ -1601,6 +1602,7 @@ describe('yargs dsl tests', () => { global: false, }) .global('foo') + .getInternalMethods() .reset(); const options = y.getOptions(); options.key.foo.should.equal(true); @@ -1622,6 +1624,7 @@ describe('yargs dsl tests', () => { global: false, }) .global('foo') + .getInternalMethods() .reset({ foo: ['awesome-sauce', 'awesomeSauce'], }); @@ -1654,6 +1657,7 @@ describe('yargs dsl tests', () => { .describe('foo', 'my awesome foo option') .global('foo') .global('bar', false) + .getInternalMethods() .reset(); const descriptions = y.getUsageInstance().getDescriptions(); Object.keys(descriptions).should.include('foo'); @@ -1669,6 +1673,7 @@ describe('yargs dsl tests', () => { z: 'w', }) .global(['z'], false) + .getInternalMethods() .reset(); const implied = y.getValidationInstance().getImplied(); Object.keys(implied).should.include('x'); @@ -1684,6 +1689,7 @@ describe('yargs dsl tests', () => { nargs: 2, global: false, }) + .getInternalMethods() .reset(); const options = y.getOptions(); options.key.foo.should.equal(true); From abfc29b3f197bb8acbf387af7e538bf9b4b0330d Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 28 Mar 2021 17:49:11 -0700 Subject: [PATCH 3/6] refactor: finish adding privacy --- lib/yargs-factory.ts | 234 +++++++++++++++++++++++++------------------ test/command.cjs | 164 ++++++++++++++++++++++-------- test/yargs.cjs | 28 ++++-- 3 files changed, 281 insertions(+), 145 deletions(-) diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 327dd039f..874996480 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -70,8 +70,43 @@ export function YargsWithShim(_shim: PlatformShim) { // Used to expose private methods to other module-level classes, // such as the command parser and usage printer. +const kCopyDoubleDash = Symbol('copyDoubleDash'); +const kCreateLogger = Symbol('copyDoubleDash'); +const kDeleteFromParserHintObject = Symbol('deleteFromParserHintObject'); +const kFreeze = Symbol('freeze'); +const kGetDollarZero = Symbol('getDollarZero'); +const kGetParserConfiguration = Symbol('getParserConfiguration'); +const kGuessLocale = Symbol('guessLocale'); +const kGuessVersion = Symbol('guessVersion'); +const kParsePositionalNumbers = Symbol('parsePositionalNumbers'); +const kPkgUp = Symbol('pkgUp'); +const kPopulateParserHintArray = Symbol('populateParserHintArray'); +const kPopulateParserHintSingleValueDictionary = Symbol( + 'populateParserHintSingleValueDictionary' +); +const kPopulateParserHintArrayDictionary = Symbol( + 'populateParserHintArrayDictionary' +); +const kPopulateParserHintDictionary = Symbol('populateParserHintDictionary'); +const kSanitizeKey = Symbol('sanitizeKey'); +const kSetKey = Symbol('setKey'); +const kUnfreeze = Symbol('unfreeze'); +const kValidateAsync = Symbol('validateAsync'); +const kGetCommandInstance = Symbol('getCommandInstance'); +const kGetContext = Symbol('getContext'); +const kGetHasOutput = Symbol('getHasOutput'); +const kGetLoggerInstance = Symbol('getLoggerInstance'); +const kGetParseContext = Symbol('getParseContext'); +const kGetUsageInstance = Symbol('getUsageInstance'); +const kGetValidationInstance = Symbol('getValidationInstance'); +const kHasParseCallback = Symbol('hasParseCallback'); +const kPostProcess = Symbol('postProcess'); const kReset = Symbol('reset'); - +const kRunYargsParserAndExecuteCommands = Symbol( + 'runYargsParserAndExecuteCommands' +); +const kRunValidation = Symbol('runValidation'); +const kSetHasOutput = Symbol('setHasOutput'); export interface YargsInternalMethods { getCommandInstance(): CommandInstance; getContext(): Context; @@ -155,12 +190,12 @@ export class YargsInstance { this.#cwd = cwd; this.#parentRequire = parentRequire; this.#globalMiddleware = new GlobalMiddleware(this); - this.$0 = this.getDollarZero(); + this.$0 = this[kGetDollarZero](); this.#y18n = shim.y18n; // TODO(@bcoe): make reset initialize dependent classes so ! is not required. this[kReset](); this.#options!.showHiddenOpt = this.#defaultShowHiddenOpt; - this.#logger = this.createLogger(); + this.#logger = this[kCreateLogger](); } addHelpOpt(opt?: string | false, msg?: string): YargsInstance { const defaultHelpOpt = 'help'; @@ -169,7 +204,7 @@ export class YargsInstance { // nuke the key previously configured // to return help. if (this.#helpOpt) { - this.deleteFromParserHintObject(this.#helpOpt); + this[kDeleteFromParserHintObject](this.#helpOpt); this.#helpOpt = null; } @@ -214,7 +249,7 @@ export class YargsInstance { [key, value], arguments.length ); - this.populateParserHintArrayDictionary( + this[kPopulateParserHintArrayDictionary]( this.alias.bind(this), 'alias', key, @@ -224,12 +259,12 @@ export class YargsInstance { } array(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('array', keys); + this[kPopulateParserHintArray]('array', keys); return this; } boolean(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('boolean', keys); + this[kPopulateParserHintArray]('boolean', keys); return this; } check(f: (argv: Arguments) => any, global?: boolean): YargsInstance { @@ -275,7 +310,7 @@ export class YargsInstance { [key, value], arguments.length ); - this.populateParserHintArrayDictionary( + this[kPopulateParserHintArrayDictionary]( this.choices.bind(this), 'choices', key, @@ -367,7 +402,7 @@ export class YargsInstance { key = applyExtends( key, this.#cwd, - this.getParserConfiguration()['deep-merge-config'] || false, + this[kGetParserConfiguration]()['deep-merge-config'] || false, shim ); this.#options!.configObjects = ( @@ -471,7 +506,7 @@ export class YargsInstance { } count(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('count', keys); + this[kPopulateParserHintArray]('count', keys); return this; } default( @@ -496,7 +531,7 @@ export class YargsInstance { ] = this.#usage!.functionDescription(value); value = value.call(); } - this.populateParserHintSingleValueDictionary<'default'>( + this[kPopulateParserHintSingleValueDictionary]<'default'>( this.default.bind(this), 'default', key, @@ -581,7 +616,7 @@ export class YargsInstance { msg?: string ): YargsInstance { argsert(' [string]', [keys, msg], arguments.length); - this.populateParserHintSingleValueDictionary( + this[kPopulateParserHintSingleValueDictionary]( this.demandOption.bind(this), 'demandedOptions', keys, @@ -603,7 +638,7 @@ export class YargsInstance { [keys, description], arguments.length ); - this.setKey(keys, true); + this[kSetKey](keys, true); this.#usage!.describe(keys, description); return this; } @@ -710,7 +745,7 @@ export class YargsInstance { if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). - const parse = this.runYargsParserAndExecuteCommands( + const parse = this[kRunYargsParserAndExecuteCommands]( this.#processArgs, undefined, undefined, @@ -789,7 +824,7 @@ export class YargsInstance { locale(locale?: string): YargsInstance | string { argsert('[string]', [locale], arguments.length); if (!locale) { - this.guessLocale(); + this[kGuessLocale](); return this.#y18n.getLocale(); } this.#detectLocale = false; @@ -812,7 +847,7 @@ export class YargsInstance { value?: number ): YargsInstance { argsert(' [number]', [key, value], arguments.length); - this.populateParserHintSingleValueDictionary( + this[kPopulateParserHintSingleValueDictionary]( this.nargs.bind(this), 'narg', key, @@ -822,12 +857,12 @@ export class YargsInstance { } normalize(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('normalize', keys); + this[kPopulateParserHintArray]('normalize', keys); return this; } number(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('number', keys); + this[kPopulateParserHintArray]('number', keys); return this; } option( @@ -969,11 +1004,11 @@ export class YargsInstance { [args, shortCircuit, _parseFn], arguments.length ); - this.freeze(); // Push current state of parser onto stack. + this[kFreeze](); // Push current state of parser onto stack. if (typeof args === 'undefined') { - const argv = this.runYargsParserAndExecuteCommands(this.#processArgs); + const argv = this[kRunYargsParserAndExecuteCommands](this.#processArgs); const tmpParsed = this.parsed; - this.unfreeze(); // Pop the stack. + this[kUnfreeze](); // Pop the stack. this.parsed = tmpParsed; return argv; } @@ -998,10 +1033,13 @@ export class YargsInstance { if (this.#parseFn) this.#exitProcess = false; - const parsed = this.runYargsParserAndExecuteCommands(args, !!shortCircuit); + const parsed = this[kRunYargsParserAndExecuteCommands]( + args, + !!shortCircuit + ); this.#completion!.setParsed(this.parsed as DetailedArguments); if (this.#parseFn) this.#parseFn(this.#exitError, parsed, this.#output); - this.unfreeze(); // Pop the stack. + this[kUnfreeze](); // Pop the stack. return parsed; } parserConfiguration(config: Configuration) { @@ -1015,14 +1053,14 @@ export class YargsInstance { // prefer cwd to require-main-filename in this method // since we're looking for e.g. "nyc" config in nyc consumer // rather than "yargs" config in nyc (where nyc is the main filename) - const obj = this.pkgUp(rootPath || this.#cwd); + const obj = this[kPkgUp](rootPath || this.#cwd); // If an object exists in the key, add it to options.configObjects if (obj[key] && typeof obj[key] === 'object') { conf = applyExtends( obj[key] as {[key: string]: string}, rootPath || this.#cwd, - this.getParserConfiguration()['deep-merge-config'] || false, + this[kGetParserConfiguration]()['deep-merge-config'] || false, shim ); this.#options!.configObjects = ( @@ -1112,7 +1150,7 @@ export class YargsInstance { if (typeof keys === 'string' && this.#options!.narg[keys]) { return this; } else { - this.populateParserHintSingleValueDictionary( + this[kPopulateParserHintSingleValueDictionary]( this.requiresArg.bind(this), 'narg', keys, @@ -1141,7 +1179,7 @@ export class YargsInstance { if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). - const parse = this.runYargsParserAndExecuteCommands( + const parse = this[kRunYargsParserAndExecuteCommands]( this.#processArgs, undefined, undefined, @@ -1178,7 +1216,7 @@ export class YargsInstance { } skipValidation(keys: string | string[]): YargsInstance { argsert('', [keys], arguments.length); - this.populateParserHintArray('skipValidation', keys); + this[kPopulateParserHintArray]('skipValidation', keys); return this; } strict(enabled?: boolean): YargsInstance { @@ -1198,7 +1236,7 @@ export class YargsInstance { } string(key: string | string[]): YargsInstance { argsert('', [key], arguments.length); - this.populateParserHintArray('string', key); + this[kPopulateParserHintArray]('string', key); return this; } terminalWidth(): number | null { @@ -1253,13 +1291,13 @@ export class YargsInstance { // nuke the key previously configured // to return version #. if (this.#versionOpt) { - this.deleteFromParserHintObject(this.#versionOpt); + this[kDeleteFromParserHintObject](this.#versionOpt); this.#usage!.version(undefined); this.#versionOpt = null; } if (arguments.length === 0) { - ver = this.guessVersion(); + ver = this[kGuessVersion](); opt = defaultVersionOpt; } else if (arguments.length === 1) { if (opt === false) { @@ -1290,7 +1328,7 @@ export class YargsInstance { // to simplify the parsing of positionals in commands, // we temporarily populate '--' rather than _, with arguments // after the '--' directive. After the parse, we copy these back. - private copyDoubleDash(argv: Arguments): any { + [kCopyDoubleDash](argv: Arguments): any { if (!argv._ || !argv['--']) return argv; // eslint-disable-next-line prefer-spread argv._.push.apply(argv._, argv['--']); @@ -1304,23 +1342,23 @@ export class YargsInstance { return argv; } - private createLogger(): LoggerInstance { + [kCreateLogger](): LoggerInstance { return { log: (...args: any[]) => { - if (!this.hasParseCallback()) console.log(...args); + if (!this[kHasParseCallback]()) console.log(...args); this.#hasOutput = true; if (this.#output.length) this.#output += '\n'; this.#output += args.join(' '); }, error: (...args: any[]) => { - if (!this.hasParseCallback()) console.error(...args); + if (!this[kHasParseCallback]()) console.error(...args); this.#hasOutput = true; if (this.#output.length) this.#output += '\n'; this.#output += args.join(' '); }, }; } - private deleteFromParserHintObject(optionKey: string) { + [kDeleteFromParserHintObject](optionKey: string) { // delete from all parsing hints: // boolean, array, key, alias, etc. objectKeys(this.#options).forEach((hintKey: keyof Options) => { @@ -1337,7 +1375,7 @@ export class YargsInstance { // now delete the description from usage.js. delete this.#usage!.getDescriptions()[optionKey]; } - private freeze() { + [kFreeze]() { this.#frozens.push({ options: this.#options!, configObjects: this.#options!.configObjects.slice(0), @@ -1359,7 +1397,7 @@ export class YargsInstance { this.#command!.freeze(); this.#globalMiddleware.freeze(); } - private getDollarZero(): string { + [kGetDollarZero](): string { let $0 = ''; // ignore the node bin, specify this in your // bin file with #!/usr/bin/env node @@ -1385,10 +1423,10 @@ export class YargsInstance { } return $0; } - private getParserConfiguration(): Configuration { + [kGetParserConfiguration](): Configuration { return this.#parserConfig; } - private guessLocale() { + [kGuessLocale]() { if (!this.#detectLocale) return; const locale = shim.getEnv('LC_ALL') || @@ -1398,14 +1436,14 @@ export class YargsInstance { 'en_US'; this.locale(locale.replace(/[.:].*/, '')); } - private guessVersion(): string { - const obj = this.pkgUp(); + [kGuessVersion](): string { + const obj = this[kPkgUp](); return (obj.version as string) || 'unknown'; } // We wait to coerce numbers for positionals until after the initial parse. // This allows commands to configure number parsing on a positional by // positional basis: - private parsePositionalNumbers(argv: Arguments): any { + [kParsePositionalNumbers](argv: Arguments): any { const args: (string | number)[] = argv['--'] ? argv['--'] : argv._; for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { @@ -1418,7 +1456,7 @@ export class YargsInstance { } return argv; } - private pkgUp(rootPath?: string) { + [kPkgUp](rootPath?: string) { const npath = rootPath || '*'; if (this.#pkgs[npath]) return this.#pkgs[npath]; @@ -1451,17 +1489,17 @@ export class YargsInstance { this.#pkgs[npath] = obj || {}; return this.#pkgs[npath]; } - private populateParserHintArray>( + [kPopulateParserHintArray]>( type: T, keys: string | string[] ) { keys = ([] as string[]).concat(keys); keys.forEach(key => { - key = this.sanitizeKey(key); + key = this[kSanitizeKey](key); this.#options![type].push(key); }); } - private populateParserHintSingleValueDictionary< + [kPopulateParserHintSingleValueDictionary]< T extends | Exclude, DictionaryKeyof> | 'default', @@ -1473,7 +1511,7 @@ export class YargsInstance { key: K | K[] | {[key in K]: V}, value?: V ) { - this.populateParserHintDictionary( + this[kPopulateParserHintDictionary]( builder, type, key, @@ -1483,7 +1521,7 @@ export class YargsInstance { } ); } - private populateParserHintArrayDictionary< + [kPopulateParserHintArrayDictionary]< T extends DictionaryKeyof, K extends keyof Options[T] & string = keyof Options[T] & string, V extends ValueOf> | ValueOf>[] = @@ -1495,7 +1533,7 @@ export class YargsInstance { key: K | K[] | {[key in K]: V}, value?: V ) { - this.populateParserHintDictionary( + this[kPopulateParserHintDictionary]( builder, type, key, @@ -1507,7 +1545,7 @@ export class YargsInstance { } ); } - private populateParserHintDictionary< + [kPopulateParserHintDictionary]< T extends keyof Options, K extends keyof Options[T], V @@ -1531,26 +1569,26 @@ export class YargsInstance { builder(k, key[k]); } } else { - singleKeyHandler(type, this.sanitizeKey(key), value); + singleKeyHandler(type, this[kSanitizeKey](key), value); } } - private sanitizeKey(key: any) { + [kSanitizeKey](key: any) { if (key === '__proto__') return '___proto___'; return key; } - private setKey( + [kSetKey]( key: string | string[] | Dictionary, set?: boolean | string ) { - this.populateParserHintSingleValueDictionary( - this.setKey.bind(this), + this[kPopulateParserHintSingleValueDictionary]( + this[kSetKey].bind(this), 'key', key, set ); return this; } - private unfreeze() { + [kUnfreeze]() { const frozen = this.#frozens.pop(); assertNotStrictEqual(frozen, undefined, shim); let configObjects: Dictionary[]; @@ -1578,7 +1616,7 @@ export class YargsInstance { } // If argv is a promise (which is possible if async middleware is used) // delay applying validation until the promise has resolved: - private validateAsync( + [kValidateAsync]( validation: (argv: Arguments) => void, argv: Arguments | Promise ): Arguments | Promise { @@ -1592,49 +1630,49 @@ export class YargsInstance { // depended upon externally: getInternalMethods(): YargsInternalMethods { return { - getCommandInstance: this.getCommandInstance.bind(this), - getContext: this.getContext.bind(this), - getHasOutput: this.getHasOutput.bind(this), - getLoggerInstance: this.getLoggerInstance.bind(this), - getParseContext: this.getParseContext.bind(this), - getParserConfiguration: this.getParserConfiguration.bind(this), - getUsageInstance: this.getUsageInstance.bind(this), - getValidationInstance: this.getValidationInstance.bind(this), - hasParseCallback: this.hasParseCallback.bind(this), - postProcess: this.postProcess.bind(this), + getCommandInstance: this[kGetCommandInstance].bind(this), + getContext: this[kGetContext].bind(this), + getHasOutput: this[kGetHasOutput].bind(this), + getLoggerInstance: this[kGetLoggerInstance].bind(this), + getParseContext: this[kGetParseContext].bind(this), + getParserConfiguration: this[kGetParserConfiguration].bind(this), + getUsageInstance: this[kGetUsageInstance].bind(this), + getValidationInstance: this[kGetValidationInstance].bind(this), + hasParseCallback: this[kHasParseCallback].bind(this), + postProcess: this[kPostProcess].bind(this), reset: this[kReset].bind(this), - runValidation: this.runValidation.bind(this), - runYargsParserAndExecuteCommands: this.runYargsParserAndExecuteCommands.bind( - this - ), - setHasOutput: this.setHasOutput.bind(this), + runValidation: this[kRunValidation].bind(this), + runYargsParserAndExecuteCommands: this[ + kRunYargsParserAndExecuteCommands + ].bind(this), + setHasOutput: this[kSetHasOutput].bind(this), }; } - private getCommandInstance(): CommandInstance { + [kGetCommandInstance](): CommandInstance { return this.#command!; } - private getContext(): Context { + [kGetContext](): Context { return this.#context; } - private getHasOutput(): boolean { + [kGetHasOutput](): boolean { return this.#hasOutput; } - private getLoggerInstance(): LoggerInstance { + [kGetLoggerInstance](): LoggerInstance { return this.#logger; } - private getParseContext(): Object { + [kGetParseContext](): Object { return this.#parseContext || {}; } - private getUsageInstance(): UsageInstance { + [kGetUsageInstance](): UsageInstance { return this.#usage!; } - private getValidationInstance(): ValidationInstance { + [kGetValidationInstance](): ValidationInstance { return this.#validation!; } - private hasParseCallback(): boolean { + [kHasParseCallback](): boolean { return !!this.#parseFn; } - private postProcess>( + [kPostProcess]>( argv: Arguments | Promise, populateDoubleDash: boolean, calledFromCommand: boolean, @@ -1643,13 +1681,13 @@ export class YargsInstance { if (calledFromCommand) return argv; if (isPromise(argv)) return argv; if (!populateDoubleDash) { - argv = this.copyDoubleDash(argv); + argv = this[kCopyDoubleDash](argv); } const parsePositionalNumbers = - this.getParserConfiguration()['parse-positional-numbers'] || - this.getParserConfiguration()['parse-positional-numbers'] === undefined; + this[kGetParserConfiguration]()['parse-positional-numbers'] || + this[kGetParserConfiguration]()['parse-positional-numbers'] === undefined; if (parsePositionalNumbers) { - argv = this.parsePositionalNumbers(argv as Arguments); + argv = this[kParsePositionalNumbers](argv as Arguments); } if (runGlobalMiddleware) { argv = applyMiddleware( @@ -1760,7 +1798,7 @@ export class YargsInstance { return this; } - private runYargsParserAndExecuteCommands( + [kRunYargsParserAndExecuteCommands]( args: string | string[] | null, shortCircuit?: boolean | null, calledFromCommand?: boolean, @@ -1771,7 +1809,7 @@ export class YargsInstance { args = args || this.#processArgs; this.#options!.__ = this.#y18n.__; - this.#options!.configuration = this.getParserConfiguration(); + this.#options!.configuration = this[kGetParserConfiguration](); const populateDoubleDash = !!this.#options!.configuration['populate--']; const config = Object.assign({}, this.#options!.configuration, { @@ -1813,13 +1851,13 @@ export class YargsInstance { } try { - this.guessLocale(); // guess locale lazily, so that it can be turned off in chain. + this[kGuessLocale](); // guess locale lazily, so that it can be turned off in chain. // while building up the argv object, there // are two passes through the parser. If completion // is being performed short-circuit on the first pass. if (shortCircuit) { - return this.postProcess( + return this[kPostProcess]( argv, populateDoubleDash, !!calledFromCommand, @@ -1866,7 +1904,7 @@ export class YargsInstance { // Passed to builder so that expensive commands can be deferred: helpOptSet || versionOptSet || helpOnly ); - return this.postProcess( + return this[kPostProcess]( innerArgv, populateDoubleDash, !!calledFromCommand, @@ -1916,7 +1954,7 @@ export class YargsInstance { helpOnly, helpOptSet || versionOptSet || helpOnly ); - return this.postProcess( + return this[kPostProcess]( innerArgv, populateDoubleDash, !!calledFromCommand, @@ -1947,7 +1985,7 @@ export class YargsInstance { }); this.exit(0); }); - return this.postProcess( + return this[kPostProcess]( argv, !populateDoubleDash, !!calledFromCommand, @@ -1990,7 +2028,7 @@ export class YargsInstance { // if we're executed via bash completion, don't // bother with validation. if (!requestCompletions) { - const validation = this.runValidation(aliases, {}, parsed.error); + const validation = this[kRunValidation](aliases, {}, parsed.error); if (!calledFromCommand) { argvPromise = applyMiddleware( argv, @@ -1999,7 +2037,7 @@ export class YargsInstance { true ); } - argvPromise = this.validateAsync(validation, argvPromise ?? argv); + argvPromise = this[kValidateAsync](validation, argvPromise ?? argv); if (isPromise(argvPromise) && !calledFromCommand) { argvPromise = argvPromise.then(() => { return applyMiddleware( @@ -2017,14 +2055,14 @@ export class YargsInstance { else throw err; } - return this.postProcess( + return this[kPostProcess]( argvPromise ?? argv, populateDoubleDash, !!calledFromCommand, true ); } - private runValidation( + [kRunValidation]( aliases: Dictionary, positionalMap: Dictionary, parseErrors: Error | null, @@ -2056,7 +2094,7 @@ export class YargsInstance { this.#validation!.conflicting(argv); }; } - private setHasOutput() { + [kSetHasOutput]() { this.#hasOutput = true; } } diff --git a/test/command.cjs b/test/command.cjs index 9662c9948..32787b42c 100644 --- a/test/command.cjs +++ b/test/command.cjs @@ -26,7 +26,7 @@ describe('Command', () => { 'my awesome command', yargs => yargs ); - const command = y.getCommandInstance(); + const command = y.getInternalMethods().getCommandInstance(); const handlers = command.getCommandHandlers(); handlers.foo.demanded.should.deep.include({ cmd: ['bar'], @@ -116,7 +116,7 @@ describe('Command', () => { ['foo [awesome]', 'wat '], 'my awesome command' ); - const command = y.getCommandInstance(); + const command = y.getInternalMethods().getCommandInstance(); const handlers = command.getCommandHandlers(); handlers.foo.optional.should.deep.include({ cmd: ['awesome'], @@ -299,7 +299,7 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(cmd, desc); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ cmd, desc, @@ -317,7 +317,10 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command([cmd].concat(aliases), desc); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ cmd, desc, @@ -325,7 +328,10 @@ describe('Command', () => { aliases, deprecated, ]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar', 'baz']); }); @@ -334,7 +340,7 @@ describe('Command', () => { const desc = false; const y = yargs([]).command(cmd, desc); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands.should.deep.equal([]); }); @@ -344,9 +350,15 @@ describe('Command', () => { const desc = false; const y = yargs([]).command([cmd].concat(aliases), desc); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands.should.deep.equal([]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar', 'baz']); }); @@ -356,7 +368,10 @@ describe('Command', () => { const builder = yargs => yargs; const y = yargs([]).command(cmd, desc, builder); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(cmd); handlers.foo.builder.should.equal(builder); }); @@ -369,7 +384,10 @@ describe('Command', () => { }; const y = yargs([]).command(cmd, desc, builder); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(cmd); handlers.foo.builder.should.equal(builder); }); @@ -385,7 +403,10 @@ describe('Command', () => { }; const y = yargs([]).command(cmd, desc, module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(cmd); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); @@ -402,7 +423,10 @@ describe('Command', () => { }; const y = yargs([]).command(cmd, desc, module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(cmd); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); @@ -422,11 +446,14 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ module.command, module.describe, @@ -450,11 +477,14 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ module.command, module.description, @@ -478,11 +508,14 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ module.command, module.desc, @@ -503,11 +536,14 @@ describe('Command', () => { }; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands.should.deep.equal([]); }); @@ -521,11 +557,14 @@ describe('Command', () => { }; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands.should.deep.equal([]); }); @@ -545,11 +584,14 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ module.command, module.describe, @@ -574,11 +616,14 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); expect(typeof handlers.foo.handler).to.equal('function'); - const commands = y.getUsageInstance().getCommands(); + const commands = y.getInternalMethods().getUsageInstance().getCommands(); commands[0].should.deep.equal([ module.command, module.describe, @@ -601,11 +646,17 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command[0]); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ module.command[0], module.describe, @@ -613,7 +664,10 @@ describe('Command', () => { ['bar', 'baz'], deprecated, ]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar', 'baz']); }); @@ -631,11 +685,17 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ module.command, module.describe, @@ -643,7 +703,10 @@ describe('Command', () => { module.aliases, deprecated, ]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar', 'baz']); }); @@ -661,11 +724,17 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command[0]); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ module.command[0], module.describe, @@ -673,7 +742,10 @@ describe('Command', () => { ['bar', 'baz', 'nat'], deprecated, ]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar', 'baz', 'nat']); }); @@ -691,11 +763,17 @@ describe('Command', () => { const deprecated = false; const y = yargs([]).command(module); - const handlers = y.getCommandInstance().getCommandHandlers(); + const handlers = y + .getInternalMethods() + .getCommandInstance() + .getCommandHandlers(); handlers.foo.original.should.equal(module.command); handlers.foo.builder.should.equal(module.builder); handlers.foo.handler.should.equal(module.handler); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ module.command, module.describe, @@ -703,7 +781,10 @@ describe('Command', () => { ['bar'], deprecated, ]); - const cmdCommands = y.getCommandInstance().getCommands(); + const cmdCommands = y + .getInternalMethods() + .getCommandInstance() + .getCommands(); cmdCommands.should.deep.equal(['foo', 'bar']); }); @@ -721,7 +802,10 @@ describe('Command', () => { [], deprecated ); - const usageCommands = y.getUsageInstance().getCommands(); + const usageCommands = y + .getInternalMethods() + .getUsageInstance() + .getCommands(); usageCommands[0].should.deep.equal([ command, description, @@ -1086,7 +1170,7 @@ describe('Command', () => { 'my awesome command', yargs => yargs ); - const command = y.getCommandInstance(); + const command = y.getInternalMethods().getCommandInstance(); const handlers = command.getCommandHandlers(); handlers.foo.demanded.should.not.include({ cmd: '', diff --git a/test/yargs.cjs b/test/yargs.cjs index c6def0129..738878623 100644 --- a/test/yargs.cjs +++ b/test/yargs.cjs @@ -325,13 +325,21 @@ describe('yargs dsl tests', () => { }; expect(y.getOptions()).to.deep.equal(emptyOptions); - expect(y.getUsageInstance().getDescriptions()).to.deep.equal({ + expect( + y.getInternalMethods().getUsageInstance().getDescriptions() + ).to.deep.equal({ help: '__yargsString__:Show help', version: '__yargsString__:Show version number', }); - expect(y.getValidationInstance().getImplied()).to.deep.equal({}); - expect(y.getValidationInstance().getConflicting()).to.deep.equal({}); - expect(y.getCommandInstance().getCommandHandlers()).to.deep.equal({}); + expect( + y.getInternalMethods().getValidationInstance().getImplied() + ).to.deep.equal({}); + expect( + y.getInternalMethods().getValidationInstance().getConflicting() + ).to.deep.equal({}); + expect( + y.getInternalMethods().getCommandInstance().getCommandHandlers() + ).to.deep.equal({}); expect(y.getExitProcess()).to.equal(false); expect(y.getDemandedOptions()).to.deep.equal({}); expect(y.getDemandedCommands()).to.deep.equal({}); @@ -1659,7 +1667,10 @@ describe('yargs dsl tests', () => { .global('bar', false) .getInternalMethods() .reset(); - const descriptions = y.getUsageInstance().getDescriptions(); + const descriptions = y + .getInternalMethods() + .getUsageInstance() + .getDescriptions(); Object.keys(descriptions).should.include('foo'); Object.keys(descriptions).should.not.include('bar'); }); @@ -1675,7 +1686,10 @@ describe('yargs dsl tests', () => { .global(['z'], false) .getInternalMethods() .reset(); - const implied = y.getValidationInstance().getImplied(); + const implied = y + .getInternalMethods() + .getValidationInstance() + .getImplied(); Object.keys(implied).should.include('x'); Object.keys(implied).should.not.include('z'); }); @@ -2184,7 +2198,7 @@ describe('yargs dsl tests', () => { 'one', 'level one', yargs => { - context = yargs.getContext(); + context = yargs.getInternalMethods().getContext(); context.commands.should.deep.equal(['one']); return yargs.command( 'two', From 4871451278126affc90e0262e1131500f17b6533 Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 28 Mar 2021 20:47:43 -0700 Subject: [PATCH 4/6] refactor: simplify wrapper logic --- browser.mjs | 4 +- deno.ts | 4 +- index.mjs | 4 +- lib/cjs.ts | 5 +- lib/usage.ts | 6 +- lib/validation.ts | 6 +- lib/yargs-factory.ts | 384 ++++++++++++++++++++++--------------------- test/usage.cjs | 15 -- 8 files changed, 208 insertions(+), 220 deletions(-) diff --git a/browser.mjs b/browser.mjs index d8a9f3de6..2d0d6e9e5 100644 --- a/browser.mjs +++ b/browser.mjs @@ -1,7 +1,7 @@ // Bootstrap yargs for browser: import browserPlatformShim from './lib/platform-shims/browser.mjs'; -import {YargsWithShim} from './build/lib/yargs-factory.js'; +import {YargsFactory} from './build/lib/yargs-factory.js'; -const Yargs = YargsWithShim(browserPlatformShim); +const Yargs = YargsFactory(browserPlatformShim); export default Yargs; diff --git a/deno.ts b/deno.ts index 62613e5c9..2002f2093 100644 --- a/deno.ts +++ b/deno.ts @@ -1,8 +1,8 @@ // Bootstrap yargs for Deno platform: import denoPlatformShim from './lib/platform-shims/deno.ts'; -import {YargsWithShim} from './build/lib/yargs-factory.js'; +import {YargsFactory} from './build/lib/yargs-factory.js'; -const WrappedYargs = YargsWithShim(denoPlatformShim); +const WrappedYargs = YargsFactory(denoPlatformShim); function Yargs(args?: string[]) { return WrappedYargs(args); diff --git a/index.mjs b/index.mjs index 23d908013..c6440b9ed 100644 --- a/index.mjs +++ b/index.mjs @@ -2,7 +2,7 @@ // Bootstraps yargs for ESM: import esmPlatformShim from './lib/platform-shims/esm.mjs'; -import {YargsWithShim} from './build/lib/yargs-factory.js'; +import {YargsFactory} from './build/lib/yargs-factory.js'; -const Yargs = YargsWithShim(esmPlatformShim); +const Yargs = YargsFactory(esmPlatformShim); export default Yargs; diff --git a/lib/cjs.ts b/lib/cjs.ts index 64b57f8ba..82dae13e6 100644 --- a/lib/cjs.ts +++ b/lib/cjs.ts @@ -7,7 +7,7 @@ import {isPromise} from './utils/is-promise.js'; import {objFilter} from './utils/obj-filter.js'; import {parseCommand} from './parse-command.js'; import * as processArgv from './utils/process-argv.js'; -import {YargsWithShim, rebase} from './yargs-factory.js'; +import {YargsFactory} from './yargs-factory.js'; import {YError} from './yerror.js'; import cjsPlatformShim from './platform-shims/cjs.js'; @@ -27,7 +27,7 @@ if (process && process.version) { } const Parser = require('yargs-parser'); -const Yargs = YargsWithShim(cjsPlatformShim); +const Yargs = YargsFactory(cjsPlatformShim); export default { applyExtends, @@ -39,6 +39,5 @@ export default { parseCommand, Parser, processArgv, - rebase, YError, }; diff --git a/lib/usage.ts b/lib/usage.ts index be242843e..7650eec99 100644 --- a/lib/usage.ts +++ b/lib/usage.ts @@ -1,6 +1,6 @@ // this file handles outputting usage instructions, // failures, etc. keeps logging in one place. -import {Dictionary, PlatformShim, Y18N} from './typings/common-types.js'; +import {Dictionary, PlatformShim} from './typings/common-types.js'; import {objFilter} from './utils/obj-filter.js'; import {YargsInstance} from './yargs-factory.js'; import {YError} from './yerror.js'; @@ -11,8 +11,8 @@ function isBoolean(fail: FailureFunction | boolean): fail is boolean { return typeof fail === 'boolean'; } -export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { - const __ = y18n.__; +export function usage(yargs: YargsInstance, shim: PlatformShim) { + const __ = shim.y18n.__; const self = {} as UsageInstance; // methods for ouputting/building failure message. diff --git a/lib/validation.ts b/lib/validation.ts index 25fa695b0..b550a2957 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -2,7 +2,6 @@ import {argsert} from './argsert.js'; import { Dictionary, assertNotStrictEqual, - Y18N, PlatformShim, } from './typings/common-types.js'; import {levenshtein as distance} from './utils/levenshtein.js'; @@ -18,11 +17,10 @@ const specialKeys = ['$0', '--', '_']; export function validation( yargs: YargsInstance, usage: UsageInstance, - y18n: Y18N, shim: PlatformShim ) { - const __ = y18n.__; - const __n = y18n.__n; + const __ = shim.y18n.__; + const __n = shim.y18n.__n; const self = {} as ValidationInstance; // validate appropriate # of non-option diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 874996480..112d1e570 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -22,7 +22,6 @@ import type { RequireDirectoryOptions, PlatformShim, RequireType, - Y18N, } from './typings/common-types.js'; import { assertNotStrictEqual, @@ -61,11 +60,28 @@ import { import {isPromise} from './utils/is-promise.js'; import {maybeAsyncResult} from './utils/maybe-async-result.js'; import setBlocking from './utils/set-blocking.js'; +import {assert} from 'node:console'; -let shim: PlatformShim; -export function YargsWithShim(_shim: PlatformShim) { - shim = _shim; - return YargsFactory; +export function YargsFactory(_shim: PlatformShim) { + return ( + processArgs: string | string[] = [], + cwd = _shim.process.cwd(), + parentRequire?: RequireType + ): YargsInstance => { + const yargs = new YargsInstance(processArgs, cwd, parentRequire, _shim); + // Legacy yargs.argv interface, it's recommended that you use .parse(). + Object.defineProperty(yargs, 'argv', { + get: () => { + return yargs.parse(); + }, + enumerable: true, + }); + // an app should almost always have --version and --help, + // if you *really* want to disable this use .help(false)/.version(false). + yargs.help(); + yargs.version(); + return yargs; + }; } // Used to expose private methods to other module-level classes, @@ -101,6 +117,7 @@ const kGetUsageInstance = Symbol('getUsageInstance'); const kGetValidationInstance = Symbol('getValidationInstance'); const kHasParseCallback = Symbol('hasParseCallback'); const kPostProcess = Symbol('postProcess'); +const kRebase = Symbol('rebase'); const kReset = Symbol('reset'); const kRunYargsParserAndExecuteCommands = Symbol( 'runYargsParserAndExecuteCommands' @@ -146,12 +163,12 @@ export class YargsInstance { customScriptName = false; parsed: DetailedArguments | false = false; - #command?: CommandInstance; + #command: CommandInstance; #cwd: string; // use context object to keep track of resets, subcommand execution, etc., // submodules should modify and check the state of context as necessary: #context: Context = {commands: [], fullCommands: []}; - #completion?: CompletionInstance | null = null; + #completion: CompletionInstance | null = null; #completionCommand: string | null = null; #defaultShowHiddenOpt = 'show-hidden'; #exitError: YError | string | undefined | null = null; @@ -164,7 +181,7 @@ export class YargsInstance { #helpOpt: string | null = null; #logger: LoggerInstance; #output = ''; - #options?: Options; + #options: Options; #parentRequire?: RequireType; #parserConfig: Configuration = {}; #parseFn: ParseCallback | null = null; @@ -173,28 +190,33 @@ export class YargsInstance { #preservedGroups: Dictionary = {}; #processArgs: string | string[]; #recommendCommands = false; + #shim: PlatformShim; #strict = false; #strictCommands = false; #strictOptions = false; - #usage?: UsageInstance; + #usage: UsageInstance; #versionOpt: string | null = null; - #validation?: ValidationInstance; - #y18n: Y18N; + #validation: ValidationInstance; constructor( processArgs: string | string[] = [], cwd: string, - parentRequire: RequireType | undefined + parentRequire: RequireType | undefined, + shim: PlatformShim ) { + this.#shim = shim; this.#processArgs = processArgs; this.#cwd = cwd; this.#parentRequire = parentRequire; this.#globalMiddleware = new GlobalMiddleware(this); this.$0 = this[kGetDollarZero](); - this.#y18n = shim.y18n; - // TODO(@bcoe): make reset initialize dependent classes so ! is not required. + // #command, #validation, and #usage are initialized on first reset: this[kReset](); - this.#options!.showHiddenOpt = this.#defaultShowHiddenOpt; + this.#command = this!.#command; + this.#usage = this!.#usage; + this.#validation = this!.#validation; + this.#options = this!.#options; + this.#options.showHiddenOpt = this.#defaultShowHiddenOpt; this.#logger = this[kCreateLogger](); } addHelpOpt(opt?: string | false, msg?: string): YargsInstance { @@ -215,7 +237,7 @@ export class YargsInstance { this.boolean(this.#helpOpt); this.describe( this.#helpOpt, - msg || this.#usage!.deferY18nLookup('Show help') + msg || this.#usage.deferY18nLookup('Show help') ); return this; } @@ -231,9 +253,9 @@ export class YargsInstance { this.boolean(showHiddenOpt); this.describe( showHiddenOpt, - msg || this.#usage!.deferY18nLookup('Show hidden options') + msg || this.#usage.deferY18nLookup('Show hidden options') ); - this.#options!.showHiddenOpt = showHiddenOpt; + this.#options.showHiddenOpt = showHiddenOpt; return this; } showHidden(opt?: string | false, msg?: string): YargsInstance { @@ -282,16 +304,16 @@ export class YargsInstance { }, (result: any): Partial | Promise> => { if (!result) { - this.#usage!.fail( - this.#y18n.__('Argument check failed: %s', f.toString()) + this.#usage.fail( + this.#shim.y18n.__('Argument check failed: %s', f.toString()) ); } else if (typeof result === 'string' || result instanceof Error) { - this.#usage!.fail(result.toString(), result); + this.#usage.fail(result.toString(), result); } return argv; }, (err: Error): Partial | Promise> => { - this.#usage!.fail(err.message ? err.message : err.toString(), err); + this.#usage.fail(err.message ? err.message : err.toString(), err); return argv; } ); @@ -384,7 +406,7 @@ export class YargsInstance { key2?: string | string[] ): YargsInstance { argsert(' [string|array]', [key1, key2], arguments.length); - this.#validation!.conflicts(key1, key2); + this.#validation.conflicts(key1, key2); return this; } config( @@ -403,11 +425,11 @@ export class YargsInstance { key, this.#cwd, this[kGetParserConfiguration]()['deep-merge-config'] || false, - shim + this.#shim + ); + this.#options.configObjects = (this.#options.configObjects || []).concat( + key ); - this.#options!.configObjects = ( - this.#options!.configObjects || [] - ).concat(key); return this; } @@ -419,10 +441,10 @@ export class YargsInstance { this.describe( key, - msg || this.#usage!.deferY18nLookup('Path to JSON config file') + msg || this.#usage.deferY18nLookup('Path to JSON config file') ); (Array.isArray(key) ? key : [key]).forEach(k => { - this.#options!.config[k] = parseFn || true; + this.#options.config[k] = parseFn || true; }); return this; @@ -471,7 +493,7 @@ export class YargsInstance { [cmd, description, builder, handler, middlewares, deprecated], arguments.length ); - this.#command!.addHandler( + this.#command.addHandler( cmd, description, builder, @@ -500,8 +522,8 @@ export class YargsInstance { } commandDir(dir: string, opts?: RequireDirectoryOptions): YargsInstance { argsert(' [object]', [dir, opts], arguments.length); - const req = this.#parentRequire || shim.require; - this.#command!.addDirectory(dir, req, shim.getCallerFile(), opts); + const req = this.#parentRequire || this.#shim.require; + this.#command.addDirectory(dir, req, this.#shim.getCallerFile(), opts); return this; } count(keys: string | string[]): YargsInstance { @@ -520,15 +542,15 @@ export class YargsInstance { arguments.length ); if (defaultDescription) { - assertSingleKey(key, shim); - this.#options!.defaultDescription[key] = defaultDescription; + assertSingleKey(key, this.#shim); + this.#options.defaultDescription[key] = defaultDescription; } if (typeof value === 'function') { - assertSingleKey(key, shim); - if (!this.#options!.defaultDescription[key]) - this.#options!.defaultDescription[ - key - ] = this.#usage!.functionDescription(value); + assertSingleKey(key, this.#shim); + if (!this.#options.defaultDescription[key]) + this.#options.defaultDescription[key] = this.#usage.functionDescription( + value + ); value = value.call(); } this[kPopulateParserHintSingleValueDictionary]<'default'>( @@ -565,7 +587,7 @@ export class YargsInstance { this.global('_', false); - this.#options!.demandedCommands._ = { + this.#options.demandedCommands._ = { min, max, minMsg, @@ -584,7 +606,7 @@ export class YargsInstance { // options are provided. if (Array.isArray(max)) { max.forEach(key => { - assertNotStrictEqual(msg, true as const, shim); + assertNotStrictEqual(msg, true as const, this.#shim); this.demandOption(key, msg); }); max = Infinity; @@ -594,11 +616,11 @@ export class YargsInstance { } if (typeof keys === 'number') { - assertNotStrictEqual(msg, true as const, shim); + assertNotStrictEqual(msg, true as const, this.#shim); this.demandCommand(keys, max, msg, msg); } else if (Array.isArray(keys)) { keys.forEach(key => { - assertNotStrictEqual(msg, true as const, shim); + assertNotStrictEqual(msg, true as const, this.#shim); this.demandOption(key, msg); }); } else { @@ -626,7 +648,7 @@ export class YargsInstance { } deprecateOption(option: string, message: string | boolean): YargsInstance { argsert(' [string|boolean]', [option, message], arguments.length); - this.#options!.deprecatedOptions[option] = message; + this.#options.deprecatedOptions[option] = message; return this; } describe( @@ -639,7 +661,7 @@ export class YargsInstance { arguments.length ); this[kSetKey](keys, true); - this.#usage!.describe(keys, description); + this.#usage.describe(keys, description); return this; } detectLocale(detect: boolean): YargsInstance { @@ -651,13 +673,13 @@ export class YargsInstance { // parser will apply env vars matching prefix to argv env(prefix?: string | false): YargsInstance { argsert('[string|boolean]', [prefix], arguments.length); - if (prefix === false) delete this.#options!.envPrefix; - else this.#options!.envPrefix = prefix || ''; + if (prefix === false) delete this.#options.envPrefix; + else this.#options.envPrefix = prefix || ''; return this; } epilogue(msg: string): YargsInstance { argsert('', [msg], arguments.length); - this.#usage!.epilog(msg); + this.#usage.epilog(msg); return this; } epilog(msg: string): YargsInstance { @@ -672,7 +694,7 @@ export class YargsInstance { if (Array.isArray(cmd)) { cmd.forEach(exampleParams => this.example(...exampleParams)); } else { - this.#usage!.example(cmd, description); + this.#usage.example(cmd, description); } return this; @@ -681,7 +703,7 @@ export class YargsInstance { exit(code: number, err?: YError | string): void { this.#hasOutput = true; this.#exitError = err; - if (this.#exitProcess) shim.process.exit(code); + if (this.#exitProcess) this.#shim.process.exit(code); } exitProcess(enabled = true): YargsInstance { argsert('[boolean]', [enabled], arguments.length); @@ -695,7 +717,7 @@ export class YargsInstance { "Invalid first argument. Expected function or boolean 'false'" ); } - this.#usage!.failFn(f); + this.#usage.failFn(f); return this; } getAliases(): Dictionary { @@ -719,15 +741,15 @@ export class YargsInstance { } getDemandedOptions() { argsert([], 0); - return this.#options!.demandedOptions; + return this.#options.demandedOptions; } getDemandedCommands() { argsert([], 0); - return this.#options!.demandedCommands; + return this.#options.demandedCommands; } getDeprecatedOptions() { argsert([], 0); - return this.#options!.deprecatedOptions; + return this.#options.deprecatedOptions; } getDetectLocale(): boolean { return this.#detectLocale; @@ -741,7 +763,7 @@ export class YargsInstance { } getHelp(): Promise { this.#hasOutput = true; - if (!this.#usage!.hasCachedHelpMessage()) { + if (!this.#usage.hasCachedHelpMessage()) { if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). @@ -754,15 +776,15 @@ export class YargsInstance { ); if (isPromise(parse)) { return parse.then(() => { - return this.#usage!.help(); + return this.#usage.help(); }); } } } - return Promise.resolve(this.#usage!.help()); + return Promise.resolve(this.#usage.help()); } getOptions(): Options { - return this.#options!; + return this.#options; } getStrict(): boolean { return this.#strict; @@ -777,13 +799,12 @@ export class YargsInstance { argsert(' [boolean]', [globals, global], arguments.length); globals = ([] as string[]).concat(globals); if (global !== false) { - this.#options!.local = this.#options!.local.filter( + this.#options.local = this.#options.local.filter( l => globals.indexOf(l) === -1 ); } else { globals.forEach(g => { - if (this.#options!.local.indexOf(g) === -1) - this.#options!.local.push(g); + if (this.#options.local.indexOf(g) === -1) this.#options.local.push(g); }); } return this; @@ -805,7 +826,7 @@ export class YargsInstance { } hide(key: string): YargsInstance { argsert('', [key], arguments.length); - this.#options!.hiddenOptions.push(key); + this.#options.hiddenOptions.push(key); return this; } implies( @@ -817,7 +838,7 @@ export class YargsInstance { [key, value], arguments.length ); - this.#validation!.implies(key, value); + this.#validation.implies(key, value); return this; } // TODO(bcoe): add getLocale() rather than overloading behavior. @@ -825,10 +846,10 @@ export class YargsInstance { argsert('[string]', [locale], arguments.length); if (!locale) { this[kGuessLocale](); - return this.#y18n.getLocale(); + return this.#shim.y18n.getLocale(); } this.#detectLocale = false; - this.#y18n.setLocale(locale); + this.#shim.y18n.setLocale(locale); return this; } middleware( @@ -879,7 +900,7 @@ export class YargsInstance { opt = {}; } - this.#options!.key[key] = true; // track manually set keys. + this.#options.key[key] = true; // track manually set keys. if (opt.alias) this.alias(key, opt.alias); @@ -968,7 +989,7 @@ export class YargsInstance { } if (opt.defaultDescription) { - this.#options!.defaultDescription[key] = opt.defaultDescription; + this.#options.defaultDescription[key] = opt.defaultDescription; } if (opt.skipValidation) { @@ -1061,11 +1082,11 @@ export class YargsInstance { obj[key] as {[key: string]: string}, rootPath || this.#cwd, this[kGetParserConfiguration]()['deep-merge-config'] || false, - shim + this.#shim + ); + this.#options.configObjects = (this.#options.configObjects || []).concat( + conf ); - this.#options!.configObjects = ( - this.#options!.configObjects || [] - ).concat(conf); } return this; } @@ -1100,7 +1121,7 @@ export class YargsInstance { this.#context.fullCommands.length - 1 ]; const parseOptions = fullCommand - ? this.#command!.cmdToParseOptions(fullCommand) + ? this.#command.cmdToParseOptions(fullCommand) : { array: [], alias: {}, @@ -1115,7 +1136,7 @@ export class YargsInstance { if (parseOption[key] && !(pk in opts)) opts[pk] = parseOption[key]; } }); - this.group(key, this.#usage!.getPositionalGroupName()); + this.group(key, this.#usage.getPositionalGroupName()); return this.option(key, opts); } recommendCommands(recommend = true): YargsInstance { @@ -1147,7 +1168,7 @@ export class YargsInstance { // see: https://github.com/yargs/yargs/pull/1572 // TODO: make this work with aliases, using a check similar to // checkAllAliases() in yargs-parser. - if (typeof keys === 'string' && this.#options!.narg[keys]) { + if (typeof keys === 'string' && this.#options.narg[keys]) { return this; } else { this[kPopulateParserHintSingleValueDictionary]( @@ -1175,7 +1196,7 @@ export class YargsInstance { ): YargsInstance { argsert('[string|function]', [level], arguments.length); this.#hasOutput = true; - if (!this.#usage!.hasCachedHelpMessage()) { + if (!this.#usage.hasCachedHelpMessage()) { if (!this.parsed) { // Run the parser as if --help was passed to it (this is what // the last parameter `true` indicates). @@ -1188,13 +1209,13 @@ export class YargsInstance { ); if (isPromise(parse)) { parse.then(() => { - this.#usage!.showHelp(level); + this.#usage.showHelp(level); }); return this; } } } - this.#usage!.showHelp(level); + this.#usage.showHelp(level); return this; } scriptName(scriptName: string): YargsInstance { @@ -1204,14 +1225,14 @@ export class YargsInstance { } showHelpOnFail(enabled?: string | boolean, message?: string): YargsInstance { argsert('[boolean|string] [string]', [enabled, message], arguments.length); - this.#usage!.showHelpOnFail(enabled, message); + this.#usage.showHelpOnFail(enabled, message); return this; } showVersion( level: 'error' | 'log' | ((message: string) => void) ): YargsInstance { argsert('[string|function]', [level], arguments.length); - this.#usage!.showVersion(level); + this.#usage.showVersion(level); return this; } skipValidation(keys: string | string[]): YargsInstance { @@ -1241,7 +1262,7 @@ export class YargsInstance { } terminalWidth(): number | null { argsert([], 0); - return shim.process.stdColumns; + return this.#shim.process.stdColumns; } updateLocale(obj: Dictionary): YargsInstance { return this.updateStrings(obj); @@ -1249,7 +1270,7 @@ export class YargsInstance { updateStrings(obj: Dictionary): YargsInstance { argsert('', [obj], arguments.length); this.#detectLocale = false; - this.#y18n.updateLocale(obj); + this.#shim.y18n.updateLocale(obj); return this; } usage( @@ -1265,7 +1286,7 @@ export class YargsInstance { ); if (description !== undefined) { - assertNotStrictEqual(msg, null, shim); + assertNotStrictEqual(msg, null, this.#shim); // .usage() can be used as an alias for defining // a default command. if ((msg || '').match(/^\$0( |$)/)) { @@ -1276,7 +1297,7 @@ export class YargsInstance { ); } } else { - this.#usage!.usage(msg); + this.#usage.usage(msg); return this; } } @@ -1292,7 +1313,7 @@ export class YargsInstance { // to return version #. if (this.#versionOpt) { this[kDeleteFromParserHintObject](this.#versionOpt); - this.#usage!.version(undefined); + this.#usage.version(undefined); this.#versionOpt = null; } @@ -1312,16 +1333,16 @@ export class YargsInstance { } this.#versionOpt = typeof opt === 'string' ? opt : defaultVersionOpt; - msg = msg || this.#usage!.deferY18nLookup('Show version number'); + msg = msg || this.#usage.deferY18nLookup('Show version number'); - this.#usage!.version(ver || undefined); + this.#usage.version(ver || undefined); this.boolean(this.#versionOpt); this.describe(this.#versionOpt, msg); return this; } wrap(cols: number | null | undefined): YargsInstance { argsert('', [cols], arguments.length); - this.#usage!.wrap(cols); + this.#usage.wrap(cols); return this; } @@ -1365,7 +1386,7 @@ export class YargsInstance { // configObjects is not a parsing hint array if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) return; - const hint = this.#options![hintKey]; + const hint = this.#options[hintKey]; if (Array.isArray(hint)) { if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1); } else if (typeof hint === 'object') { @@ -1373,12 +1394,12 @@ export class YargsInstance { } }); // now delete the description from usage.js. - delete this.#usage!.getDescriptions()[optionKey]; + delete this.#usage.getDescriptions()[optionKey]; } [kFreeze]() { this.#frozens.push({ - options: this.#options!, - configObjects: this.#options!.configObjects.slice(0), + options: this.#options, + configObjects: this.#options.configObjects.slice(0), exitProcess: this.#exitProcess, groups: this.#groups, strict: this.#strict, @@ -1392,9 +1413,9 @@ export class YargsInstance { parseFn: this.#parseFn!, parseContext: this.#parseContext, }); - this.#usage!.freeze(); - this.#validation!.freeze(); - this.#command!.freeze(); + this.#usage.freeze(); + this.#validation.freeze(); + this.#command.freeze(); this.#globalMiddleware.freeze(); } [kGetDollarZero](): string { @@ -1402,24 +1423,30 @@ export class YargsInstance { // ignore the node bin, specify this in your // bin file with #!/usr/bin/env node let default$0: string[]; - if (/\b(node|iojs|electron)(\.exe)?$/.test(shim.process.argv()[0])) { - default$0 = shim.process.argv().slice(1, 2); + if (/\b(node|iojs|electron)(\.exe)?$/.test(this.#shim.process.argv()[0])) { + default$0 = this.#shim.process.argv().slice(1, 2); } else { - default$0 = shim.process.argv().slice(0, 1); + default$0 = this.#shim.process.argv().slice(0, 1); } $0 = default$0 .map(x => { - const b = rebase(this.#cwd, x); + const b = this[kRebase](this.#cwd, x); return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x; }) .join(' ') .trim(); - if (shim.getEnv('_') && shim.getProcessArgvBin() === shim.getEnv('_')) { - $0 = shim + if ( + this.#shim.getEnv('_') && + this.#shim.getProcessArgvBin() === this.#shim.getEnv('_') + ) { + $0 = this.#shim .getEnv('_')! - .replace(`${shim.path.dirname(shim.process.execPath())}/`, ''); + .replace( + `${this.#shim.path.dirname(this.#shim.process.execPath())}/`, + '' + ); } return $0; } @@ -1429,10 +1456,10 @@ export class YargsInstance { [kGuessLocale]() { if (!this.#detectLocale) return; const locale = - shim.getEnv('LC_ALL') || - shim.getEnv('LC_MESSAGES') || - shim.getEnv('LANG') || - shim.getEnv('LANGUAGE') || + this.#shim.getEnv('LC_ALL') || + this.#shim.getEnv('LC_MESSAGES') || + this.#shim.getEnv('LANG') || + this.#shim.getEnv('LANGUAGE') || 'en_US'; this.locale(locale.replace(/[.:].*/, '')); } @@ -1448,7 +1475,7 @@ export class YargsInstance { for (let i = 0, arg; (arg = args[i]) !== undefined; i++) { if ( - shim.Parser.looksLikeNumber(arg) && + this.#shim.Parser.looksLikeNumber(arg) && Number.isSafeInteger(Math.floor(parseFloat(`${arg}`))) ) { args[i] = Number(arg); @@ -1462,16 +1489,16 @@ export class YargsInstance { let obj = {}; try { - let startDir = rootPath || shim.mainFilename; + let startDir = rootPath || this.#shim.mainFilename; // When called in an environment that lacks require.main.filename, such as a jest test runner, // startDir is already process.cwd(), and should not be shortened. // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. - if (!rootPath && shim.path.extname(startDir)) { - startDir = shim.path.dirname(startDir); + if (!rootPath && this.#shim.path.extname(startDir)) { + startDir = this.#shim.path.dirname(startDir); } - const pkgJsonPath = shim.findUp( + const pkgJsonPath = this.#shim.findUp( startDir, (dir: string[], names: string[]) => { if (names.includes('package.json')) { @@ -1481,8 +1508,8 @@ export class YargsInstance { } } ); - assertNotStrictEqual(pkgJsonPath, undefined, shim); - obj = JSON.parse(shim.readFileSync(pkgJsonPath, 'utf8')); + assertNotStrictEqual(pkgJsonPath, undefined, this.#shim); + obj = JSON.parse(this.#shim.readFileSync(pkgJsonPath, 'utf8')); // eslint-disable-next-line no-empty } catch (_noop) {} @@ -1496,7 +1523,7 @@ export class YargsInstance { keys = ([] as string[]).concat(keys); keys.forEach(key => { key = this[kSanitizeKey](key); - this.#options![type].push(key); + this.#options[type].push(key); }); } [kPopulateParserHintSingleValueDictionary]< @@ -1517,7 +1544,7 @@ export class YargsInstance { key, value, (type, key, value) => { - this.#options![type][key] = value as ValueOf; + this.#options[type][key] = value as ValueOf; } ); } @@ -1539,8 +1566,8 @@ export class YargsInstance { key, value, (type, key, value) => { - this.#options![type][key] = ( - this.#options![type][key] || ([] as Options[T][keyof Options[T]]) + this.#options[type][key] = ( + this.#options[type][key] || ([] as Options[T][keyof Options[T]]) ).concat(value); } ); @@ -1590,7 +1617,7 @@ export class YargsInstance { } [kUnfreeze]() { const frozen = this.#frozens.pop(); - assertNotStrictEqual(frozen, undefined, shim); + assertNotStrictEqual(frozen, undefined, this.#shim); let configObjects: Dictionary[]; ({ options: this.#options, @@ -1609,9 +1636,9 @@ export class YargsInstance { parseContext: this.#parseContext, } = frozen); this.#options.configObjects = configObjects; - this.#usage!.unfreeze(); - this.#validation!.unfreeze(); - this.#command!.unfreeze(); + this.#usage.unfreeze(); + this.#validation.unfreeze(); + this.#command.unfreeze(); this.#globalMiddleware.unfreeze(); } // If argv is a promise (which is possible if async middleware is used) @@ -1649,7 +1676,7 @@ export class YargsInstance { }; } [kGetCommandInstance](): CommandInstance { - return this.#command!; + return this.#command; } [kGetContext](): Context { return this.#context; @@ -1664,10 +1691,10 @@ export class YargsInstance { return this.#parseContext || {}; } [kGetUsageInstance](): UsageInstance { - return this.#usage!; + return this.#usage; } [kGetValidationInstance](): ValidationInstance { - return this.#validation!; + return this.#validation; } [kHasParseCallback](): boolean { return !!this.#parseFn; @@ -1760,14 +1787,14 @@ export class YargsInstance { ]; arrayOptions.forEach(k => { - tmpOptions[k] = (this.#options![k] || []).filter( + tmpOptions[k] = (this.#options[k] || []).filter( (k: string) => !localLookup[k] ); }); objectOptions.forEach(>(k: K) => { tmpOptions[k] = objFilter( - this.#options![k], + this.#options[k], k => !localLookup[k as string] ); }); @@ -1779,15 +1806,25 @@ export class YargsInstance { // instances of all our helpers -- otherwise just reset. this.#usage = this.#usage ? this.#usage.reset(localLookup) - : Usage(this, this.#y18n, shim); + : Usage(this, this.#shim); this.#validation = this.#validation ? this.#validation.reset(localLookup) - : Validation(this, this.#usage, this.#y18n, shim); + : Validation(this, this.#usage, this.#shim); this.#command = this.#command ? this.#command.reset() - : Command(this.#usage, this.#validation, this.#globalMiddleware, shim); + : Command( + this.#usage, + this.#validation, + this.#globalMiddleware, + this.#shim + ); if (!this.#completion) - this.#completion = Completion(this, this.#usage, this.#command, shim); + this.#completion = Completion( + this, + this.#usage, + this.#command, + this.#shim + ); this.#globalMiddleware.reset(); this.#completionCommand = null; @@ -1798,6 +1835,9 @@ export class YargsInstance { return this; } + [kRebase](base: string, dir: string): string { + return this.#shim.path.relative(base, dir); + } [kRunYargsParserAndExecuteCommands]( args: string | string[] | null, shortCircuit?: boolean | null, @@ -1808,14 +1848,14 @@ export class YargsInstance { let skipValidation = !!calledFromCommand || helpOnly; args = args || this.#processArgs; - this.#options!.__ = this.#y18n.__; - this.#options!.configuration = this[kGetParserConfiguration](); + this.#options.__ = this.#shim.y18n.__; + this.#options.configuration = this[kGetParserConfiguration](); - const populateDoubleDash = !!this.#options!.configuration['populate--']; - const config = Object.assign({}, this.#options!.configuration, { + const populateDoubleDash = !!this.#options.configuration['populate--']; + const config = Object.assign({}, this.#options.configuration, { 'populate--': true, }); - const parsed = shim.Parser.detailed( + const parsed = this.#shim.Parser.detailed( args, Object.assign({}, this.#options, { configuration: {'parse-positional-numbers': false, ...config}, @@ -1847,7 +1887,7 @@ export class YargsInstance { // When a prior parse has completed and a new parse is beginning, we // need to clear the cached help message from the previous parse: if (commandIndex === 0) { - this.#usage!.clearCachedHelpMessage(); + this.#usage.clearCachedHelpMessage(); } try { @@ -1881,7 +1921,7 @@ export class YargsInstance { } } - const handlerKeys = this.#command!.getCommands(); + const handlerKeys = this.#command.getCommands(); const requestCompletions = this.#completion!.completionKey in argv; const skipRecommendation = helpOptSet || requestCompletions || helpOnly; @@ -1894,7 +1934,7 @@ export class YargsInstance { // commands are executed using a recursive algorithm that executes // the deepest command first; we keep track of the position in the // argv._ array that is currently being executed. - const innerArgv = this.#command!.runCommand( + const innerArgv = this.#command.runCommand( cmd, this, parsed, @@ -1921,12 +1961,12 @@ export class YargsInstance { // recommend a command if recommendCommands() has // been enabled, and no commands were found to execute if ( - !this.#command!.hasDefaultCommand() && + !this.#command.hasDefaultCommand() && this.#recommendCommands && firstUnknownCommand && !skipRecommendation ) { - this.#validation!.recommendCommands( + this.#validation.recommendCommands( firstUnknownCommand, handlerKeys ); @@ -1945,8 +1985,8 @@ export class YargsInstance { } } - if (this.#command!.hasDefaultCommand() && !skipRecommendation) { - const innerArgv = this.#command!.runCommand( + if (this.#command.hasDefaultCommand() && !skipRecommendation) { + const innerArgv = this.#command.runCommand( null, this, parsed, @@ -1964,7 +2004,7 @@ export class YargsInstance { // TODO(bcoe): what if the default builder is async? // TODO(bcoe): add better comments for runDefaultBuilderOn, why // are we doing this? - this.#command!.runDefaultBuilderOn(this); + this.#command.runDefaultBuilderOn(this); } // we must run completions first, a user might @@ -2000,23 +2040,22 @@ export class YargsInstance { if (this.#exitProcess) setBlocking(true); skipValidation = true; // TODO: add appropriate comment. - if (!calledFromCommand) this.#command!.runDefaultBuilderOn(this); + if (!calledFromCommand) this.#command.runDefaultBuilderOn(this); this.showHelp('log'); this.exit(0); } else if (versionOptSet) { if (this.#exitProcess) setBlocking(true); skipValidation = true; - this.#usage!.showVersion('log'); + this.#usage.showVersion('log'); this.exit(0); } } // Check if any of the options to skip validation were provided - if (!skipValidation && this.#options!.skipValidation.length > 0) { + if (!skipValidation && this.#options.skipValidation.length > 0) { skipValidation = Object.keys(argv).some( key => - this.#options!.skipValidation.indexOf(key) >= 0 && - argv[key] === true + this.#options.skipValidation.indexOf(key) >= 0 && argv[key] === true ); } @@ -2051,7 +2090,7 @@ export class YargsInstance { } } } catch (err) { - if (err instanceof YError) this.#usage!.fail(err.message, err); + if (err instanceof YError) this.#usage.fail(err.message, err); else throw err; } @@ -2073,25 +2112,25 @@ export class YargsInstance { const demandedOptions = {...this.getDemandedOptions()}; return (argv: Arguments) => { if (parseErrors) throw new YError(parseErrors.message); - this.#validation!.nonOptionCount(argv); - this.#validation!.requiredArguments(argv, demandedOptions); + this.#validation.nonOptionCount(argv); + this.#validation.requiredArguments(argv, demandedOptions); let failedStrictCommands = false; if (this.#strictCommands) { - failedStrictCommands = this.#validation!.unknownCommands(argv); + failedStrictCommands = this.#validation.unknownCommands(argv); } if (this.#strict && !failedStrictCommands) { - this.#validation!.unknownArguments( + this.#validation.unknownArguments( argv, aliases, positionalMap, !!isDefaultCommand ); } else if (this.#strictOptions) { - this.#validation!.unknownArguments(argv, aliases, {}, false, false); + this.#validation.unknownArguments(argv, aliases, {}, false, false); } - this.#validation!.limitedChoices(argv); - this.#validation!.implications(argv); - this.#validation!.conflicting(argv); + this.#validation.limitedChoices(argv); + this.#validation.implications(argv); + this.#validation.conflicting(argv); }; } [kSetHasOutput]() { @@ -2099,39 +2138,6 @@ export class YargsInstance { } } -// TODO(bcoe): simplify bootstrapping process so that only a single factory -// method is used, and shim is not set globally. -function YargsFactory( - processArgs: string | string[] = [], - cwd = shim.process.cwd(), - parentRequire?: RequireType -): YargsInstance { - const yargs = new YargsInstance(processArgs, cwd, parentRequire); - - // Legacy yargs.argv interface, it's recommended that you use .parse(). - Object.defineProperty(yargs, 'argv', { - get: () => { - return yargs.parse(); - }, - enumerable: true, - }); - - // an app should almost always have --version and --help, - // if you *really* want to disable this use .help(false)/.version(false). - yargs.help(); - yargs.version(); - return yargs; -} - -// rebase an absolute path to a relative one with respect to a base directory -// exported for tests -export interface RebaseFunction { - (base: string, dir: string): string; -} - -export const rebase: RebaseFunction = (base, dir) => - shim.path.relative(base, dir); - export function isYargsInstance(y: YargsInstance | void): y is YargsInstance { return !!y && typeof y.getInternalMethods === 'function'; } diff --git a/test/usage.cjs b/test/usage.cjs index 24bdfc4b0..26ff3f276 100644 --- a/test/usage.cjs +++ b/test/usage.cjs @@ -1707,21 +1707,6 @@ describe('usage tests', () => { }); }); - it('should succeed when rebase', () => { - rebase( - ['home', 'chevex'].join(path.sep), - ['home', 'chevex', 'foo', 'bar', 'baz'].join(path.sep) - ).should.equal(['foo', 'bar', 'baz'].join(path.sep)); - rebase( - ['home', 'chevex', 'foo', 'bar', 'baz'].join(path.sep), - ['home', 'chevex'].join(path.sep) - ).should.equal(['..', '..', '..'].join(path.sep)); - rebase( - ['home', 'chevex', 'foo'].join(path.sep), - ['home', 'chevex', 'pow', 'zoom.txt'].join(path.sep) - ).should.equal(['..', 'pow', 'zoom.txt'].join(path.sep)); - }); - it('should not print usage string if help() is called without arguments', () => { const r = checkUsage(() => yargs([]).usage('foo').help().parse()); From f4c37b82810c08cd832e0f0d56410a208685048a Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 28 Mar 2021 21:09:29 -0700 Subject: [PATCH 5/6] refactor: simplify factory --- lib/yargs-factory.ts | 1 - test/usage.cjs | 3 +-- test/yargs.cjs | 16 +++++++++++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 112d1e570..2fdda079f 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -60,7 +60,6 @@ import { import {isPromise} from './utils/is-promise.js'; import {maybeAsyncResult} from './utils/maybe-async-result.js'; import setBlocking from './utils/set-blocking.js'; -import {assert} from 'node:console'; export function YargsFactory(_shim: PlatformShim) { return ( diff --git a/test/usage.cjs b/test/usage.cjs index 26ff3f276..2b672bbea 100644 --- a/test/usage.cjs +++ b/test/usage.cjs @@ -4,9 +4,8 @@ const checkUsage = require('./helpers/utils.cjs').checkOutput; const chalk = require('chalk'); -const path = require('path'); const yargs = require('../index.cjs'); -const {rebase, YError} = require('../build/index.cjs'); +const {YError} = require('../build/index.cjs'); const should = require('chai').should(); diff --git a/test/yargs.cjs b/test/yargs.cjs index 738878623..bb3806a94 100644 --- a/test/yargs.cjs +++ b/test/yargs.cjs @@ -1721,7 +1721,7 @@ describe('yargs dsl tests', () => { it('combines yargs defaults with package.json values', () => { const argv = yargs('--foo a') - .default('b', 99) + .defaults('b', 99) .pkgConf('repository') .parse(); @@ -3120,4 +3120,18 @@ describe('yargs dsl tests', () => { help.should.match(/foo command/); }); }); + describe('getters', () => { + it('has getter for strict commands', () => { + const y1 = yargs('foo').strictCommands(); + const y2 = yargs('bar'); + assert.strictEqual(y1.getStrictCommands(), true); + assert.strictEqual(y2.getStrictCommands(), false); + }); + it('has getter for strict options', () => { + const y1 = yargs('foo').strictOptions(); + const y2 = yargs('bar'); + assert.strictEqual(y1.getStrictOptions(), true); + assert.strictEqual(y2.getStrictOptions(), false); + }); + }); }); From bb3b4c678405fb2ba0a7733648be33ab730e8a06 Mon Sep 17 00:00:00 2001 From: bcoe Date: Mon, 29 Mar 2021 22:55:09 -0700 Subject: [PATCH 6/6] test: update to c8 with exclude-after-remap --- .nycrc | 3 ++- lib/yargs-factory.ts | 1 - package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nycrc b/.nycrc index 5243a3489..d4b247808 100644 --- a/.nycrc +++ b/.nycrc @@ -3,6 +3,7 @@ "build/test/**", "test/**" ], + "exclude-after-remap": true, "reporter": [ "html", "text" @@ -10,4 +11,4 @@ "lines": 100, "branches": "96", "statements": "100" -} \ No newline at end of file +} diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 2fdda079f..d83767608 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -840,7 +840,6 @@ export class YargsInstance { this.#validation.implies(key, value); return this; } - // TODO(bcoe): add getLocale() rather than overloading behavior. locale(locale?: string): YargsInstance | string { argsert('[string]', [locale], arguments.length); if (!locale) { diff --git a/package.json b/package.json index 554c021ed..eeb57e79f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/mocha": "^8.0.0", "@types/node": "^14.11.2", "@wessberg/rollup-plugin-ts": "^1.3.2", - "c8": "^7.0.0", + "c8": "^7.7.0", "chai": "^4.2.0", "chalk": "^4.0.0", "coveralls": "^3.0.9",