From ca59758c2f0da709ea66ff53704d307981dc947b Mon Sep 17 00:00:00 2001 From: Mael Le Guen Date: Mon, 18 May 2020 18:25:29 +0200 Subject: [PATCH] chore(ts): tsify lib/command (#1654) --- lib/command-types.ts | 23 --- lib/{command.js => command.ts} | 264 ++++++++++++++++++++--------- lib/common-types.ts | 12 ++ lib/completion.ts | 4 +- lib/middleware.ts | 4 +- lib/parse-command.ts | 10 +- lib/typings/require-directory.d.ts | 84 +++++++++ lib/typings/which-module.d.ts | 14 ++ lib/usage.ts | 8 +- lib/validation.ts | 4 +- lib/yargs-types.ts | 35 +++- test/command.js | 29 ++++ yargs.js | 2 +- 13 files changed, 376 insertions(+), 117 deletions(-) delete mode 100644 lib/command-types.ts rename lib/{command.js => command.ts} (62%) create mode 100644 lib/typings/require-directory.d.ts create mode 100644 lib/typings/which-module.d.ts diff --git a/lib/command-types.ts b/lib/command-types.ts deleted file mode 100644 index 6a1ecd906..000000000 --- a/lib/command-types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Dictionary } from './common-types' -import { YargsInstance } from './yargs-types' - -/** Instance of the command module. */ -export interface CommandInstance { - getCommandHandlers (): Dictionary - getCommands (): string[] -} - -interface CommandHandler { - builder: CommandBuilder -} - -// To be completed later with other CommandBuilder flavours -type CommandBuilder = FunctionCommandBuilder - -interface FunctionCommandBuilder { - (y: YargsInstance): YargsInstance -} - -export function isFunctionCommandBuilder (builder: CommandBuilder): builder is FunctionCommandBuilder { - return typeof builder === 'function' -} diff --git a/lib/command.js b/lib/command.ts similarity index 62% rename from lib/command.js rename to lib/command.ts index 2deac2706..e508eecc9 100644 --- a/lib/command.js +++ b/lib/command.ts @@ -1,41 +1,59 @@ -'use strict' - -const inspect = require('util').inspect -const { isPromise } = require('../build/lib/is-promise') -const { applyMiddleware, commandMiddlewareFactory } = require('../build/lib/middleware') -const { parseCommand } = require('../build/lib/parse-command') -const path = require('path') -const Parser = require('yargs-parser') +import { Dictionary, assertNotUndefined, assertNotTrue } from './common-types' +import { isPromise } from './is-promise' +import { applyMiddleware, commandMiddlewareFactory, Middleware } from './middleware' +import { parseCommand, Positional } from './parse-command' +import * as path from 'path' +import { RequireDirectoryOptions } from 'require-directory' +import { UsageInstance } from './usage' +import { inspect } from 'util' +import { ValidationInstance } from './validation' +import { YargsInstance, isYargsInstance, Options, OptionDefinition } from './yargs-types' +import { DetailedArguments, Arguments } from 'yargs-parser' +import { Context } from 'vm' +import requireDirectory = require('require-directory') +import whichModule = require('which-module') +import Parser = require('yargs-parser') const DEFAULT_MARKER = /(^\*)|(^\$0)/ // handles parsing positional arguments, // and populating argv with said positional // arguments. -module.exports = function command (yargs, usage, validation, globalMiddleware) { - const self = {} - let handlers = {} - let aliasMap = {} - let defaultCommand - globalMiddleware = globalMiddleware || [] - - self.addHandler = function addHandler (cmd, description, builder, handler, commandMiddleware, deprecated) { - let aliases = [] +export function command ( + yargs: YargsInstance, + usage: UsageInstance, + validation: ValidationInstance, + globalMiddleware: Middleware[] = [] +) { + const self: CommandInstance = {} as CommandInstance + let handlers: Dictionary = {} + let aliasMap: Dictionary = {} + let defaultCommand: CommandHandler | undefined + + self.addHandler = function addHandler ( + cmd: string | string[] | CommandHandlerDefinition, + description?: string | false, + builder?: CommandBuilder | CommandBuilderDefinition, + handler?: CommandHandlerCallback, + commandMiddleware?: Middleware[], + deprecated?: boolean + ) { + let aliases: string[] = [] const middlewares = commandMiddlewareFactory(commandMiddleware) - handler = handler || (() => {}) + handler = handler || (() => { }) if (Array.isArray(cmd)) { aliases = cmd.slice(1) cmd = cmd[0] - } else if (typeof cmd === 'object') { + } else if (isCommandHandlerDefinition(cmd)) { let command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd) - if (cmd.aliases) command = [].concat(command).concat(cmd.aliases) + if (cmd.aliases) command = ([] as string[]).concat(command).concat(cmd.aliases) self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler, cmd.middlewares, cmd.deprecated) return } // allow a module to be provided instead of separate builder and handler - if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') { + if (isCommandBuilderDefinition(builder)) { self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares, builder.deprecated) return } @@ -77,7 +95,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { handlers[parsedCommand.cmd] = { original: cmd, - description: description, + description, handler, builder: builder || {}, middlewares, @@ -96,7 +114,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { // exclude 'json', 'coffee' from require-directory defaults if (!Array.isArray(opts.extensions)) opts.extensions = ['js'] // allow consumer to define their own visitor function - const parentVisit = typeof opts.visit === 'function' ? opts.visit : o => o + const parentVisit = typeof opts.visit === 'function' ? opts.visit : (o: any) => o // call addHandler via visitor function opts.visit = function visit (obj, joined, filename) { const visited = parentVisit(obj, joined, filename) @@ -111,26 +129,26 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { } return visited } - require('require-directory')({ require: req, filename: callerFile }, dir, opts) + requireDirectory({ require: req, filename: callerFile } as NodeModule, dir, opts) } // lookup module object from require()d command and derive name // if module was not require()d and no name given, throw error - function moduleName (obj) { - const mod = require('which-module')(obj) + function moduleName (obj: CommandHandlerDefinition) { + const mod = whichModule(obj) if (!mod) throw new Error(`No command name given for module: ${inspect(obj)}`) return commandFromFilename(mod.filename) } // derive command name from filename - function commandFromFilename (filename) { + function commandFromFilename (filename: string) { return path.basename(filename, path.extname(filename)) } - function extractDesc (obj) { - for (let keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) { - test = obj[keys[i]] - if (typeof test === 'string' || typeof test === 'boolean') return test + function extractDesc ({ describe, description, desc }: CommandHandlerDefinition) { + for (const test of [describe, description, desc]) { + if (typeof test === 'string' || test === false) return test + assertNotTrue(test) } return false } @@ -149,20 +167,18 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { const parentCommands = currentContext.commands.slice() // what does yargs look like after the builder is run? - let innerArgv = parsed.argv - let innerYargs = null - let positionalMap = {} + let innerArgv: Arguments | Promise = parsed.argv + let positionalMap: Dictionary = {} if (command) { currentContext.commands.push(command) currentContext.fullCommands.push(commandHandler.original) } - if (typeof commandHandler.builder === 'function') { + const builder = commandHandler.builder + if (isCommandBuilderCallback(builder)) { // a function can be provided, which builds // up a yargs chain and possibly returns it. - innerYargs = commandHandler.builder(yargs.reset(parsed.aliases)) - if (!innerYargs || (typeof innerYargs._parseArgs !== 'function')) { - innerYargs = yargs - } + const builderOutput = builder(yargs.reset(parsed.aliases)) + const innerYargs = isYargsInstance(builderOutput) ? builderOutput : yargs if (shouldUpdateUsage(innerYargs)) { innerYargs.getUsageInstance().usage( usageFromParentCommandsCommandHandler(parentCommands, commandHandler), @@ -171,10 +187,10 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { } innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) aliases = innerYargs.parsed.aliases - } else if (typeof commandHandler.builder === 'object') { + } else if (isCommandBuilderOptionDefinitions(builder)) { // as a short hand, an object can instead be provided, specifying // the options that a command takes. - innerYargs = yargs.reset(parsed.aliases) + const innerYargs = yargs.reset(parsed.aliases) if (shouldUpdateUsage(innerYargs)) { innerYargs.getUsageInstance().usage( usageFromParentCommandsCommandHandler(parentCommands, commandHandler), @@ -182,14 +198,14 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { ) } Object.keys(commandHandler.builder).forEach((key) => { - innerYargs.option(key, commandHandler.builder[key]) + innerYargs.option(key, builder[key]) }) innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) aliases = innerYargs.parsed.aliases } if (!yargs._hasOutput()) { - positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs) + positionalMap = populatePositionals(commandHandler, innerArgv, currentContext) } const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares) @@ -227,7 +243,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { try { yargs.getUsageInstance().fail(null, error) } catch (err) { - // fail's throwing would cause an unhandled rejection. + // fail's throwing would cause an unhandled rejection. } }) .then(() => { @@ -250,12 +266,12 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { return innerArgv } - function shouldUpdateUsage (yargs) { + function shouldUpdateUsage (yargs: YargsInstance) { return !yargs.getUsageInstance().getUsageDisabled() && yargs.getUsageInstance().getUsage().length === 0 } - function usageFromParentCommandsCommandHandler (parentCommands, commandHandler) { + function usageFromParentCommandsCommandHandler (parentCommands: string[], commandHandler: CommandHandler) { const c = DEFAULT_MARKER.test(commandHandler.original) ? commandHandler.original.replace(DEFAULT_MARKER, '').trim() : commandHandler.original const pc = parentCommands.filter((c) => { return !DEFAULT_MARKER.test(c) }) pc.push(c) @@ -263,6 +279,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { } self.runDefaultBuilderOn = function (yargs) { + assertNotUndefined(defaultCommand) if (shouldUpdateUsage(yargs)) { // build the root-level command string from the default string. const commandString = DEFAULT_MARKER.test(defaultCommand.original) @@ -273,7 +290,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { ) } const builder = defaultCommand.builder - if (typeof builder === 'function') { + if (isCommandBuilderCallback(builder)) { builder(yargs) } else { Object.keys(builder).forEach((key) => { @@ -284,21 +301,21 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { // transcribe all positional arguments "command [apple]" // onto argv. - function populatePositionals (commandHandler, argv, context, yargs) { + function populatePositionals (commandHandler: CommandHandler, argv: Arguments, context: Context) { argv._ = argv._.slice(context.commands.length) // nuke the current commands const demanded = commandHandler.demanded.slice(0) const optional = commandHandler.optional.slice(0) - const positionalMap = {} + const positionalMap: Dictionary = {} validation.positionalCount(demanded.length, argv._.length) while (demanded.length) { - const demand = demanded.shift() + const demand = demanded.shift()! populatePositional(demand, argv, positionalMap) } while (optional.length) { - const maybe = optional.shift() + const maybe = optional.shift()! populatePositional(maybe, argv, positionalMap) } @@ -309,7 +326,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { return positionalMap } - function populatePositional (positional, argv, positionalMap, parseOptions) { + function populatePositional (positional: Positional, argv: Arguments, positionalMap: Dictionary) { const cmd = positional.cmd[0] if (positional.variadic) { positionalMap[cmd] = argv._.splice(0).map(String) @@ -320,16 +337,18 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { // we run yargs-parser against the positional arguments // applying the same parsing logic used for flags. - function postProcessPositionals (argv, positionalMap, parseOptions) { + function postProcessPositionals (argv: Arguments, positionalMap: Dictionary, parseOptions: Positionals) { // combine the parsing hints we've inferred from the command // string with explicitly configured parsing hints. const options = Object.assign({}, yargs.getOptions()) options.default = Object.assign(parseOptions.default, options.default) - options.alias = Object.assign(parseOptions.alias, options.alias) + for (const key of Object.keys(parseOptions.alias)) { + options.alias[key] = (options.alias[key] || []).concat(parseOptions.alias[key]) + } options.array = options.array.concat(parseOptions.array) delete options.config // don't load config when processing positionals. - const unparsed = [] + const unparsed: string[] = [] Object.keys(positionalMap).forEach((key) => { positionalMap[key].map((value) => { if (options.configuration['unknown-options-as-args']) options.key[key] = true @@ -355,7 +374,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { // flag arguments that were already parsed). const positionalKeys = Object.keys(positionalMap) Object.keys(positionalMap).forEach((key) => { - [].push.apply(positionalKeys, parsed.aliases[key]) + positionalKeys.push(...parsed.aliases[key]) }) Object.keys(parsed.argv).forEach((key) => { @@ -370,7 +389,7 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { } self.cmdToParseOptions = function (cmdString) { - const parseOptions = { + const parseOptions: Positionals = { array: [], default: {}, alias: {}, @@ -379,28 +398,22 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { const parsed = parseCommand(cmdString) parsed.demanded.forEach((d) => { - const cmds = d.cmd.slice(0) - const cmd = cmds.shift() + const [cmd, ...aliases] = d.cmd if (d.variadic) { parseOptions.array.push(cmd) parseOptions.default[cmd] = [] } - cmds.forEach((c) => { - parseOptions.alias[cmd] = c - }) + parseOptions.alias[cmd] = aliases parseOptions.demand[cmd] = true }) parsed.optional.forEach((o) => { - const cmds = o.cmd.slice(0) - const cmd = cmds.shift() + const [cmd, ...aliases] = o.cmd if (o.variadic) { parseOptions.array.push(cmd) parseOptions.default[cmd] = [] } - cmds.forEach((c) => { - parseOptions.alias[cmd] = c - }) + parseOptions.alias[cmd] = aliases }) return parseOptions @@ -417,20 +430,119 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { // the state of commands such that // we can apply .parse() multiple times // with the same yargs instance. - const frozens = [] + const frozens: FrozenCommandInstance[] = [] self.freeze = () => { - const frozen = {} - frozens.push(frozen) - frozen.handlers = handlers - frozen.aliasMap = aliasMap - frozen.defaultCommand = defaultCommand + frozens.push({ + handlers, + aliasMap, + defaultCommand + }) } self.unfreeze = () => { const frozen = frozens.pop() - handlers = frozen.handlers - aliasMap = frozen.aliasMap - defaultCommand = frozen.defaultCommand + assertNotUndefined(frozen) + ;({ + handlers, + aliasMap, + defaultCommand + } = frozen) } return self } + +/** Instance of the command module. */ +export interface CommandInstance { + addDirectory( + dir: string, + context: Context, + req: NodeRequireFunction, + callerFile: string, + opts?: RequireDirectoryOptions + ): void + addHandler (handler: CommandHandlerDefinition): void + addHandler ( + cmd: string | string[], + description: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback, + commandMiddleware?: Middleware[], + deprecated?: boolean + ): void + cmdToParseOptions (cmdString: string): Positionals + freeze(): void + getCommandHandlers (): Dictionary + getCommands (): string[] + hasDefaultCommand (): boolean + reset (): CommandInstance + runCommand (command: string, yargs: YargsInstance, parsed: DetailedArguments, commandIndex: number): void + runDefaultBuilderOn (yargs: YargsInstance): void + unfreeze(): void +} + +export interface CommandHandlerDefinition extends Partial> { + aliases?: string[] + builder?: CommandBuilder | CommandBuilderDefinition + command?: string | string[] + desc?: CommandHandler['description'] + describe?: CommandHandler['description'] +} + +export function isCommandHandlerDefinition (cmd: string | string[] | CommandHandlerDefinition): cmd is CommandHandlerDefinition { + return typeof cmd === 'object' +} + +export interface CommandBuilderDefinition { + builder?: CommandBuilder + deprecated?: boolean + handler: CommandHandlerCallback + middlewares?: Middleware[] +} + +export function isCommandBuilderDefinition (builder?: CommandBuilder | CommandBuilderDefinition): builder is CommandBuilderDefinition { + return typeof builder === 'object' && + !!(builder as CommandBuilderDefinition).builder && + typeof (builder as CommandBuilderDefinition).handler === 'function' +} + +export interface CommandHandlerCallback { + (argv: Arguments): any +} + +export interface CommandHandler { + builder: CommandBuilder + demanded: Positional[] + deprecated?: boolean + description?: string | false + handler: CommandHandlerCallback + middlewares: Middleware[] + optional: Positional[] + original: string +} + +// To be completed later with other CommandBuilder flavours +export type CommandBuilder = CommandBuilderCallback | Dictionary + +interface CommandBuilderCallback { + (y: YargsInstance): YargsInstance | void +} + +export function isCommandBuilderCallback (builder: CommandBuilder): builder is CommandBuilderCallback { + return typeof builder === 'function' +} + +function isCommandBuilderOptionDefinitions (builder: CommandBuilder): builder is Dictionary { + return typeof builder === 'object' +} + +interface Positionals extends Pick { + demand: Dictionary +} + +type FrozenCommandInstance = { + handlers: Dictionary + aliasMap: Dictionary + defaultCommand: CommandHandler | undefined +} diff --git a/lib/common-types.ts b/lib/common-types.ts index a8ad88968..ee1c9fcb1 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -1 +1,13 @@ +import { notStrictEqual } from 'assert' + export type Dictionary = { [key: string]: T } + +export type NotEmptyArray = [T, ...T[]] + +export function assertNotTrue (actual: T | true): asserts actual is T { + notStrictEqual(actual, true) +} + +export function assertNotUndefined (actual: T | undefined): asserts actual is T { + notStrictEqual(actual, undefined) +} diff --git a/lib/completion.ts b/lib/completion.ts index 2f40d3f50..ff465bb36 100644 --- a/lib/completion.ts +++ b/lib/completion.ts @@ -1,4 +1,4 @@ -import { CommandInstance, isFunctionCommandBuilder } from './command-types' +import { CommandInstance, isCommandBuilderCallback } from './command' import * as templates from './completion-templates' import { isPromise } from './is-promise' import { parseCommand } from './parse-command' @@ -58,7 +58,7 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command: for (let i = 0, ii = args.length; i < ii; ++i) { if (handlers[args[i]] && handlers[args[i]].builder) { const builder = handlers[args[i]].builder - if (isFunctionCommandBuilder(builder)) { + if (isCommandBuilderCallback(builder)) { const y = yargs.reset() builder(y) return y.argv diff --git a/lib/middleware.ts b/lib/middleware.ts index a5fbec276..8ff51e950 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -3,7 +3,7 @@ import { isPromise } from './is-promise' import { YargsInstance } from './yargs-types' import { Arguments } from 'yargs-parser' -export function globalMiddlewareFactory (globalMiddleware: (Middleware | Middleware[])[], context: T) { +export function globalMiddlewareFactory (globalMiddleware: Middleware[], context: T) { return function (callback: MiddlewareCallback | MiddlewareCallback[], applyBeforeValidation = false) { argsert(' [boolean]', [callback, applyBeforeValidation], arguments.length) if (Array.isArray(callback)) { @@ -66,6 +66,6 @@ interface MiddlewareCallback { (argv: Arguments, yargs: YargsInstance): Partial | Promise> } -interface Middleware extends MiddlewareCallback { +export interface Middleware extends MiddlewareCallback { applyBeforeValidation: boolean } diff --git a/lib/parse-command.ts b/lib/parse-command.ts index 8b0c907ad..980fb4e8b 100644 --- a/lib/parse-command.ts +++ b/lib/parse-command.ts @@ -1,3 +1,5 @@ +import { NotEmptyArray } from './common-types' + export function parseCommand (cmd: string) { const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ') const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/) @@ -17,12 +19,12 @@ export function parseCommand (cmd: string) { if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true if (/^\[/.test(cmd)) { parsedCommand.optional.push({ - cmd: cmd.replace(bregex, '').split('|'), + cmd: cmd.replace(bregex, '').split('|') as NotEmptyArray, variadic }) } else { parsedCommand.demanded.push({ - cmd: cmd.replace(bregex, '').split('|'), + cmd: cmd.replace(bregex, '').split('|') as NotEmptyArray, variadic }) } @@ -36,7 +38,7 @@ export interface ParsedCommand { optional: Positional[] } -interface Positional { - cmd: string[] +export interface Positional { + cmd: NotEmptyArray variadic: boolean } diff --git a/lib/typings/require-directory.d.ts b/lib/typings/require-directory.d.ts new file mode 100644 index 000000000..c3e805029 --- /dev/null +++ b/lib/typings/require-directory.d.ts @@ -0,0 +1,84 @@ +// TODO: update @types/require-directory with this + +// Type definitions for require-directory 2.1 +// Project: https://github.com/troygoode/node-require-directory/ +// Definitions by: Ihor Chulinda +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.4 +/// + +/* eslint no-redeclare: "off" */ +declare namespace requireDirectory { + /** + * @description function that checks path for whitelisting/blacklisting + * @param path path of required module + * @returns true if path have to be whitelisted/blacklisted, false otherwise + */ + type CheckPathFn = (path: string) => boolean; + + interface RequireDirectoryResult { + /** + * @description module itself or hash of modules in subdirectory with name of this directory + */ + [index: string]: RequireDirectoryResult | T; + } + interface RequireDirectoryOptions { + /** + * @description array of file extensions that will be included in resulting hash as modules + * @default "['js', 'json', 'coffee']" + */ + extensions?: string[]; + /** + * @description option to include subdirectories + * @default true + */ + recurse?: boolean; + /** + * @description RegExp or function for whitelisting modules + * @default undefined + */ + include?: RegExp | CheckPathFn; + /** + * @description RegExp or function for blacklisting modules + * @default undefined + */ + exclude?: RegExp | CheckPathFn; + /** + * @description function for renaming modules in resulting hash + * @param name name of required module + * @returns transformed name of module + * @default "change nothing" + */ + rename?(name: string): string; + /** + * @description function that will be called for each required module + * @param obj required module + * @param joined required module path + * @param filename require module filename + * @returns transformed module OR nothing (in second case module itself will be added to hash) + * @default "change nothing" + */ + visit?(obj: T, joined: string, filename: string): U | void; + } + + /** + * @description default options that is used for "require-directory" module + */ + const defaults: RequireDirectoryOptions +} + +/** + * @description function for requiring directory content as hash of modules + * @param m module for which has will be created + * @param path path to directory, if you want to build hash for another one (default to __dirname) + * @param options object with options for require-directory call + * @returns hash of modules in specified directory + */ +declare function requireDirectory( + m: NodeModule, + path?: string | requireDirectory.RequireDirectoryOptions, + options?: requireDirectory.RequireDirectoryOptions): requireDirectory.RequireDirectoryResult; + +declare module 'require-directory' { + export = requireDirectory; +} diff --git a/lib/typings/which-module.d.ts b/lib/typings/which-module.d.ts new file mode 100644 index 000000000..035006cad --- /dev/null +++ b/lib/typings/which-module.d.ts @@ -0,0 +1,14 @@ +// TODO: create @types/which-module from this + +/** + * Return the module object, if any, that represents the given argument in the require.cache. + * + * @param exported anything that was previously require()d or imported as a module, submodule, or dependency + * - which means exported is identical to the module.exports returned by this method. + * If exported did not come from the exports of a module in require.cache, then this method returns null + */ +declare function whichModule(exported: any): NodeModule | null; + +declare module 'which-module' { + export = whichModule; +} diff --git a/lib/usage.ts b/lib/usage.ts index d7876e6a9..3e9878e0f 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 } from './common-types' +import { Dictionary, assertNotUndefined } from './common-types' import { objFilter } from './obj-filter' import * as path from 'path' import { YargsInstance } from './yargs-types' @@ -564,7 +564,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { } self.unfreeze = function unfreeze () { const frozen = frozens.pop() - if (!frozen) throw new Error('Nothing more to unfreeze') + assertNotUndefined(frozen) ;({ failMessage, failureOutput, @@ -584,7 +584,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { export interface UsageInstance { cacheHelpMessage(): void clearCachedHelpMessage(): void - command(cmd: string, description: string | undefined, isDefault: boolean, aliases: string[], deprecated: boolean): void + command(cmd: string, description: string | undefined, isDefault: boolean, aliases: string[], deprecated?: boolean): void deferY18nLookup(str: string): string describe(key: string, desc?: string): void describe(keys: Dictionary): void @@ -608,7 +608,7 @@ export interface UsageInstance { showVersion(): void stringifiedValues(values?: any[], separator?: string): string unfreeze(): void - usage(msg: string | null, description?: string): UsageInstance + usage(msg: string | null, description?: string | false): UsageInstance version(ver: any): void wrap(cols: number): void } diff --git a/lib/validation.ts b/lib/validation.ts index 731e18caa..cb5055eb6 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -1,5 +1,5 @@ import { argsert } from './argsert' -import { Dictionary } from './common-types' +import { Dictionary, assertNotUndefined } from './common-types' import { levenshtein as distance } from './levenshtein' import { objFilter } from './obj-filter' import { UsageInstance } from './usage' @@ -393,7 +393,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 } self.unfreeze = function unfreeze () { const frozen = frozens.pop() - if (!frozen) throw new Error('Nothing more to unfreeze') + assertNotUndefined(frozen) ;({ implied, checks, diff --git a/lib/yargs-types.ts b/lib/yargs-types.ts index 32a17a832..73f29b15b 100644 --- a/lib/yargs-types.ts +++ b/lib/yargs-types.ts @@ -1,7 +1,8 @@ -import { CommandInstance } from './command-types' +import { CommandInstance } from './command' import { Dictionary } from './common-types' import { Arguments, DetailedArguments, Configuration } from 'yargs-parser' import { YError } from './yerror' +import { UsageInstance } from './usage' /** Instance of the yargs module. */ export interface YargsInstance { @@ -9,9 +10,21 @@ export interface YargsInstance { argv: Arguments customScriptName: boolean parsed: DetailedArguments + _copyDoubleDash (argv: Arguments): void _getLoggerInstance (): LoggerInstance _getParseContext(): Object + _hasOutput (): boolean _hasParseCallback (): boolean + // TODO: to be precised once yargs is tsified + _parseArgs (args: null, shortCircuit: null, _calledFromCommand: boolean, commandIndex: number): Arguments + _runValidation ( + argv: Arguments, + aliases: Dictionary, + positionalMap: Dictionary, + parseErrors: Error | null, + isDefaultCommand: boolean + ): void + _setHasOutput (): void array (key: string): YargsInstance boolean (key: string): YargsInstance count (key: string): YargsInstance @@ -29,29 +42,41 @@ export interface YargsInstance { getDeprecatedOptions (): Dictionary getExitProcess (): boolean getGroups (): Dictionary + getHandlerFinishCommand (): (handlerResult: any) => any getOptions (): Options getParserConfiguration (): ParserConfiguration + getUsageInstance (): UsageInstance global (globals: string | string[], global?: boolean): YargsInstance normalize (key: string): YargsInstance number (key: string): YargsInstance + option (key: string, opt: OptionDefinition): YargsInstance parse (args: string[], shortCircuit: boolean): Arguments - reset (): YargsInstance + reset (aliases?: DetailedArguments['aliases']): YargsInstance showHelp (level: string): YargsInstance string (key: string): YargsInstance } +export function isYargsInstance (y: YargsInstance | void): y is YargsInstance { + return !!y && (typeof y._parseArgs === 'function') +} + /** Yargs' context. */ interface Context { commands: string[] + // TODO: to be precised once yargs is tsified + files: {}[] + fullCommands: string[] } type LoggerInstance = Pick -interface Options { +export interface Options { array: string[] alias: Dictionary boolean: string[] choices: Dictionary + // TODO: to be precised once yargs is tsified + config: {} configuration: ParserConfiguration count: string[] default: Dictionary @@ -69,3 +94,7 @@ interface ParserConfiguration extends Configuration { /** Should command be sorted in help */ 'sort-commands': boolean } + +export interface OptionDefinition { + // TODO: to be precised once yargs is tsified +} diff --git a/test/command.js b/test/command.js index f65c1c1c8..0d36bfd3e 100644 --- a/test/command.js +++ b/test/command.js @@ -948,6 +948,35 @@ describe('Command', () => { argv.sin.should.equal(113993) }) + it('allows several aliases to be defined for a required positional argument', () => { + const argv = yargs('yo bcoe 113993') + .command('yo [ssn]', 'Send someone a yo', + yargs => yargs.alias('user', 'somethingElse') + ) + .parse() + argv.user.should.equal('bcoe') + argv.email.should.equal('bcoe') + argv.id.should.equal('bcoe') + argv.somethingElse.should.equal('bcoe') + argv.ssn.should.equal(113993) + }) + + it('allows several aliases to be defined for an optional positional argument', () => { + let argv + yargs('yo 113993') + .command('yo [ssn|sin|id]', 'Send someone a yo', + yargs => yargs.alias('ssn', 'somethingElse'), + (_argv) => { + argv = _argv + } + ) + .parse() + argv.ssn.should.equal(113993) + argv.sin.should.equal(113993) + argv.id.should.equal(113993) + argv.somethingElse.should.equal(113993) + }) + it('allows variadic and positional arguments to be combined', () => { const parser = yargs .command('yo [ ssns | sins... ]', 'Send someone a yo') diff --git a/yargs.js b/yargs.js index e00fa0329..91bbf948c 100644 --- a/yargs.js +++ b/yargs.js @@ -6,7 +6,7 @@ requiresNode8OrGreater() const { argsert } = require('./build/lib/argsert') const fs = require('fs') -const Command = require('./lib/command') +const { command: Command } = require('./build/lib/command') const { completion: Completion } = require('./build/lib/completion') const Parser = require('yargs-parser') const path = require('path')