Skip to content

Commit

Permalink
refactor(ts): move and tsify most of root yargs.js to lib/yargs (#1670)
Browse files Browse the repository at this point in the history
  • Loading branch information
mleguen committed Jun 17, 2020
1 parent cb7fbb8 commit e68334b
Show file tree
Hide file tree
Showing 16 changed files with 2,060 additions and 1,517 deletions.
2 changes: 1 addition & 1 deletion lib/apply-extends.ts
Expand Up @@ -32,7 +32,7 @@ function mergeDeep (config1: Dictionary, config2: Dictionary) {
return target
}

export function applyExtends (config: Dictionary, cwd: string, mergeExtends: boolean) {
export function applyExtends (config: Dictionary, cwd: string, mergeExtends = false): Dictionary {
let defaultConfig = {}

if (Object.prototype.hasOwnProperty.call(config, 'extends')) {
Expand Down
37 changes: 23 additions & 14 deletions lib/command.ts
@@ -1,4 +1,4 @@
import { Dictionary, assertNotUndefined, assertNotTrue } from './common-types'
import { Dictionary, assertNotStrictEqual } from './common-types'
import { isPromise } from './is-promise'
import { applyMiddleware, commandMiddlewareFactory, Middleware } from './middleware'
import { parseCommand, Positional } from './parse-command'
Expand All @@ -7,9 +7,7 @@ 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 { YargsInstance, isYargsInstance, Options, OptionDefinition, Context, Configuration, Arguments, DetailedArguments } from './yargs'
import requireDirectory = require('require-directory')
import whichModule = require('which-module')
import Parser = require('yargs-parser')
Expand Down Expand Up @@ -148,7 +146,7 @@ export function command (
function extractDesc ({ describe, description, desc }: CommandHandlerDefinition) {
for (const test of [describe, description, desc]) {
if (typeof test === 'string' || test === false) return test
assertNotTrue(test)
assertNotStrictEqual(test, true as true)
}
return false
}
Expand All @@ -161,7 +159,7 @@ export function command (

self.runCommand = function runCommand (command, yargs, parsed, commandIndex) {
let aliases = parsed.aliases
const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand
const commandHandler = handlers[command!] || handlers[aliasMap[command!]] || defaultCommand
const currentContext = yargs.getContext()
let numFiles = currentContext.files.length
const parentCommands = currentContext.commands.slice()
Expand All @@ -186,7 +184,7 @@ export function command (
)
}
innerArgv = innerYargs._parseArgs(null, null, true, commandIndex)
aliases = innerYargs.parsed.aliases
aliases = (innerYargs.parsed as DetailedArguments).aliases
} else if (isCommandBuilderOptionDefinitions(builder)) {
// as a short hand, an object can instead be provided, specifying
// the options that a command takes.
Expand All @@ -201,19 +199,26 @@ export function command (
innerYargs.option(key, builder[key])
})
innerArgv = innerYargs._parseArgs(null, null, true, commandIndex)
aliases = innerYargs.parsed.aliases
aliases = (innerYargs.parsed as DetailedArguments).aliases
}

if (!yargs._hasOutput()) {
positionalMap = populatePositionals(commandHandler, innerArgv, currentContext)
positionalMap = populatePositionals(commandHandler, innerArgv as Arguments, currentContext)
}

const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares)
applyMiddleware(innerArgv, yargs, middlewares, true)

// we apply validation post-hoc, so that custom
// checks get passed populated positional arguments.
if (!yargs._hasOutput()) yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error, !command)
if (!yargs._hasOutput()) {
yargs._runValidation(
innerArgv as Arguments,
aliases,
positionalMap,
(yargs.parsed as DetailedArguments).error,
!command)
}

if (commandHandler.handler && !yargs._hasOutput()) {
yargs._setHasOutput()
Expand Down Expand Up @@ -279,7 +284,7 @@ export function command (
}

self.runDefaultBuilderOn = function (yargs) {
assertNotUndefined(defaultCommand)
assertNotStrictEqual(defaultCommand, undefined)
if (shouldUpdateUsage(yargs)) {
// build the root-level command string from the default string.
const commandString = DEFAULT_MARKER.test(defaultCommand.original)
Expand Down Expand Up @@ -360,7 +365,7 @@ export function command (
// short-circuit parse.
if (!unparsed.length) return

const config = Object.assign({}, options.configuration, {
const config: Configuration = Object.assign({}, options.configuration, {
'populate--': true
})
const parsed = Parser.detailed(unparsed, Object.assign({}, options, {
Expand Down Expand Up @@ -440,7 +445,7 @@ export function command (
}
self.unfreeze = () => {
const frozen = frozens.pop()
assertNotUndefined(frozen)
assertNotStrictEqual(frozen, undefined)
;({
handlers,
aliasMap,
Expand Down Expand Up @@ -475,7 +480,7 @@ export interface CommandInstance {
getCommands (): string[]
hasDefaultCommand (): boolean
reset (): CommandInstance
runCommand (command: string, yargs: YargsInstance, parsed: DetailedArguments, commandIndex: number): void
runCommand (command: string | null, yargs: YargsInstance, parsed: DetailedArguments, commandIndex?: number): Arguments | Promise<Arguments>
runDefaultBuilderOn (yargs: YargsInstance): void
unfreeze(): void
}
Expand Down Expand Up @@ -546,3 +551,7 @@ type FrozenCommandInstance = {
aliasMap: Dictionary<string>
defaultCommand: CommandHandler | undefined
}

export interface FinishCommandHandler {
(handlerResult: any): any
}
45 changes: 40 additions & 5 deletions lib/common-types.ts
@@ -1,13 +1,48 @@
import { notStrictEqual } from 'assert'
import { notStrictEqual, strictEqual } from 'assert'

/**
* An object whose all properties have the same type.
*/
export type Dictionary<T = any> = { [key: string]: T }

/**
* Returns the keys of T that match Dictionary<U> and are not arrays.
*/
export type DictionaryKeyof<T, U = any> = Exclude<KeyOf<T, Dictionary<U>>, KeyOf<T, any[]>>

/**
* Returns the keys of T that match U.
*/
export type KeyOf<T, U> = Exclude<{ [K in keyof T]: T[K] extends U ? K : never }[keyof T], undefined>

/**
* An array whose first element is not undefined.
*/
export type NotEmptyArray<T = any> = [T, ...T[]]

export function assertNotTrue<T = any> (actual: T | true): asserts actual is T {
notStrictEqual(actual, true)
/**
* Returns the type of a Dictionary or array values.
*/
export type ValueOf<T> = T extends (infer U)[] ? U : T[keyof T];

/**
* Typing wrapper around assert.notStrictEqual()
*/
export function assertNotStrictEqual<N, T> (actual: T|N, expected: N, message ?: string | Error)
: asserts actual is Exclude<T, N> {
notStrictEqual(actual, expected, message)
}

/**
* Asserts actual is a single key, not a key array or a key map.
*/
export function assertSingleKey (actual: string | string[] | Dictionary): asserts actual is string {
strictEqual(typeof actual, 'string')
}

export function assertNotUndefined<T = any> (actual: T | undefined): asserts actual is T {
notStrictEqual(actual, undefined)
/**
* Typing wrapper around Object.keys()
*/
export function objectKeys<T> (object: T) {
return Object.keys(object) as (keyof T)[]
}
15 changes: 11 additions & 4 deletions lib/completion.ts
Expand Up @@ -4,8 +4,9 @@ import { isPromise } from './is-promise'
import { parseCommand } from './parse-command'
import * as path from 'path'
import { UsageInstance } from './usage'
import { YargsInstance } from './yargs-types'
import { YargsInstance } from './yargs'
import { Arguments, DetailedArguments } from 'yargs-parser'
import { assertNotStrictEqual } from './common-types'

// add bash completions to your
// yargs-powered applications.
Expand All @@ -31,7 +32,9 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command:

// a custom completion function can be provided
// to completion().
if (completionFunction) {
function runCompletionFunction (argv: Arguments) {
assertNotStrictEqual(completionFunction, null)

if (isSyncCompletionFunction(completionFunction)) {
const result = completionFunction(current, argv)

Expand All @@ -54,6 +57,10 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command:
}
}

if (completionFunction) {
return isPromise(argv) ? argv.then(runCompletionFunction) : runCompletionFunction(argv)
}

const handlers = command.getCommandHandlers()
for (let i = 0, ii = args.length; i < ii; ++i) {
if (handlers[args[i]] && handlers[args[i]].builder) {
Expand Down Expand Up @@ -137,15 +144,15 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command:
}

/** Instance of the completion module. */
interface CompletionInstance {
export interface CompletionInstance {
completionKey: string
generateCompletionScript($0: string, cmd: string): string
getCompletion(args: string[], done: (completions: string[]) => any): any
registerFunction(fn: CompletionFunction): void
setParsed(parsed: DetailedArguments): void
}

type CompletionFunction = SyncCompletionFunction | AsyncCompletionFunction
export type CompletionFunction = SyncCompletionFunction | AsyncCompletionFunction

interface SyncCompletionFunction {
(current: string, argv: Arguments): string[] | Promise<string[]>
Expand Down
21 changes: 10 additions & 11 deletions lib/middleware.ts
@@ -1,7 +1,6 @@
import { argsert } from './argsert'
import { isPromise } from './is-promise'
import { YargsInstance } from './yargs-types'
import { Arguments } from 'yargs-parser'
import { YargsInstance, Arguments } from './yargs'

export function globalMiddlewareFactory<T> (globalMiddleware: Middleware[], context: T) {
return function (callback: MiddlewareCallback | MiddlewareCallback[], applyBeforeValidation = false) {
Expand Down Expand Up @@ -31,38 +30,38 @@ export function commandMiddlewareFactory (commandMiddleware?: MiddlewareCallback
}

export function applyMiddleware (
argv: Arguments,
argv: Arguments | Promise<Arguments>,
yargs: YargsInstance,
middlewares: Middleware[],
beforeValidation: boolean
) {
const beforeValidationError = new Error('middleware cannot return a promise when applyBeforeValidation is true')
return middlewares
.reduce<Arguments | Promise<Arguments>>((accumulation, middleware) => {
.reduce<Arguments | Promise<Arguments>>((acc, middleware) => {
if (middleware.applyBeforeValidation !== beforeValidation) {
return accumulation
return acc
}

if (isPromise(accumulation)) {
return accumulation
if (isPromise(acc)) {
return acc
.then(initialObj =>
Promise.all<Arguments, Partial<Arguments>>([initialObj, middleware(initialObj, yargs)])
)
.then(([initialObj, middlewareObj]) =>
Object.assign(initialObj, middlewareObj)
)
} else {
const result = middleware(argv, yargs)
const result = middleware(acc, yargs)
if (beforeValidation && isPromise(result)) throw beforeValidationError

return isPromise(result)
? result.then(middlewareObj => Object.assign(accumulation, middlewareObj))
: Object.assign(accumulation, result)
? result.then(middlewareObj => Object.assign(acc, middlewareObj))
: Object.assign(acc, result)
}
}, argv)
}

interface MiddlewareCallback {
export interface MiddlewareCallback {
(argv: Arguments, yargs: YargsInstance): Partial<Arguments> | Promise<Partial<Arguments>>
}

Expand Down
11 changes: 7 additions & 4 deletions lib/obj-filter.ts
@@ -1,8 +1,11 @@
import { Dictionary } from './common-types'
import { objectKeys } from './common-types'

export function objFilter<T = any> (original: Dictionary<T>, filter: (k: string, v: T) => boolean = () => true) {
const obj: Dictionary<T> = {}
Object.keys(original || {}).forEach((key) => {
export function objFilter<T extends object> (
original = {} as T,
filter: (k: keyof T, v: T[keyof T]) => boolean = () => true
) {
const obj = {} as T
objectKeys(original).forEach((key) => {
if (filter(key, original[key])) {
obj[key] = original[key]
}
Expand Down
21 changes: 21 additions & 0 deletions lib/typings/require-main-filename.d.ts
@@ -0,0 +1,21 @@
// TODO: either create @types/require-main-filename or or convert require-main-filename to typescript

/**
* Returns the entry point of the current application.
*
* `require.main.filename` is great for figuring out the entry point for the current application.
* This can be combined with a module like pkg-conf to, as if by magic, load top-level configuration.
*
* Unfortunately, `require.main.filename` sometimes fails when an application is executed
* with an alternative process manager, e.g., iisnode.
*
* `require-main-filename` is a shim that addresses this problem.
*
* @param _require require function
* @returns hash of modules in specified directory
*/
declare function requireMainFilename(_require: NodeRequire): string;

declare module 'require-main-filename' {
export = requireMainFilename;
}

0 comments on commit e68334b

Please sign in to comment.