Skip to content

Commit

Permalink
feat: arg types are working
Browse files Browse the repository at this point in the history
  • Loading branch information
hongaar committed Dec 16, 2019
1 parent f9c04b7 commit 8a2cdf9
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 57 deletions.
16 changes: 9 additions & 7 deletions examples/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { program, command } from '../src'

const app = program('simple cli app')

app.add(
command('say', 'Say something to the terminal')
.argument('word', 'The word to say', { required: true })
.action(async function(argv) {
console.log(argv)
})
)
const say = command('say', 'Say something to the terminal')
.argument('word', 'The word to say', { default: 54 })
.argument('any', 'The word to say', { optional: true, type: 'boolean' })
.argument('some', 'Say some words', { variadic: true })
.action(async function(argv) {
console.log(argv)
})

app.add(say)

app.run()
69 changes: 44 additions & 25 deletions src/argument.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Argv } from 'yargs'
import { ArgValue } from './command'
import { Argv, PositionalOptions } from 'yargs'

export function argument(name: string) {
return new Argument(name)
// We ignore some not-so-common use cases from the type to make using this
// library easier. They could still be used at runtime but won't be documented
// here.
type IgnoreOptions = 'desc' | 'describe' | 'conflicts' | 'implies'

export interface Options extends Omit<PositionalOptions, IgnoreOptions> {}

export function argument(name: string, description?: string) {
return new Argument(name, description)
}

export class Argument {
private name: string
private description?: string
private required = false
private variadic = false
private defaultValue?: ArgValue
private defaultDescription?: string
private choices?: Function
private required = true
private vary = false
private opts: Options = {}

constructor(name: string, description?: string) {
this.name = name
Expand All @@ -26,36 +30,51 @@ export class Argument {
return this
}

require() {
this.required = true
optional() {
this.required = false
return this
}

vary() {
this.variadic = true
isOptional() {
return !this.required
}

default(value: ArgValue, description?: string) {
this.defaultValue = value
variadic() {
this.vary = true
return this
}

if (description) {
this.defaultDescription = description
}
isVariadic() {
return this.vary
}

options(options: Options) {
this.opts = options
return this
}

/**
* Returns the formatted positional argument to be used in a command. See
* https://github.com/yargs/yargs/blob/master/docs/advanced.md#positional-arguments
*/
toCommand() {
if (this.vary) {
return `[${this.name}..]`
}
if (this.required) {
return `<${this.name}>`
}
if (this.variadic) {
return `[...${this.name}]`
}
return `[${this.name}]`
}

toBuilder(yargs: Argv) {
yargs.positional(this.name, {
default: this.defaultValue,
defaultDescription: this.defaultDescription
/**
* Calls the positional() method on the passed in yargs instance and returns
* it. See http://yargs.js.org/docs/#api-positionalkey-opt
*/
toPositional<T>(yargs: Argv<T>) {
return yargs.positional(this.name, {
description: this.description,
...this.opts
})
}
}
87 changes: 63 additions & 24 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
import { CommandModule } from 'yargs'
import { Argument } from './argument'
import { Argv, CommandModule, InferredOptionType } from 'yargs'
import { Argument, Options as BaseArgumentOptions } from './argument'
import { Option } from './option'

export type ArgValue = string | number | boolean

export interface ArgumentOptions {
required?: boolean
export interface ArgumentOptions extends BaseArgumentOptions {
optional?: boolean
variadic?: boolean
default?: any
}

export interface HandlerFn<T = {}> {
(args: T): Promise<void>
(args: Omit<T, '_' | '$0'>): Promise<void>
}

function isArgument(argOrOption: Argument | Option): argOrOption is Argument {
return argOrOption.constructor.name === 'Argument'
}

function isOption(argOrOption: Argument | Option): argOrOption is Option {
return argOrOption.constructor.name === 'Option'
}

export function command(command: string, description?: string) {
return new Command(command, description)
}

export class Command {
export class Command<T = {}> {
private command: string
private description?: string
private arguments: Argument[] = []
private options: Option[] = []
private handler?: Function
private handler?: HandlerFn<T>

constructor(command: string, description?: string) {
this.command = command
Expand All @@ -37,44 +42,78 @@ export class Command {
return this
}

argument(name: string, description?: string, options: ArgumentOptions = {}) {
/*
* This is shorthand for .add(command())
*/
argument<K extends string, O extends ArgumentOptions>(
name: K,
description?: string,
options?: O
) {
const argument = new Argument(name, description)
const { optional, variadic, ...yargOptions } = options || {}

if (options.required) {
argument.require()
}
if (options.variadic) {
argument.vary()
}
optional && argument.optional()
variadic && argument.variadic()
argument.options(yargOptions)

this.arguments.push(argument)
this.add(argument)

return this
return (this as unknown) as Command<
T & { [key in K]: InferredOptionType<O> }
>
}

/*
* This is shorthand for .add(option())
*/
option() {}

add(argOrOption: Argument | Option) {
if (isArgument(argOrOption)) {
// If last argument is variadic, we should not add more arguments. See
// https://github.com/yargs/yargs/blob/master/docs/advanced.md#variadic-positional-arguments
const lastArgument = this.arguments[this.arguments.length - 1]
if (lastArgument && lastArgument.isVariadic()) {
throw new Error("Can't add more arguments")
}

this.arguments.push(argOrOption)
} else {
throw new Error('Not implemented')
}
}

action(fn: HandlerFn) {
action(fn: HandlerFn<T>) {
this.handler = fn
return this
}

toYargs() {
const module: CommandModule = {
const module: CommandModule<{}, T> = {
command: this.buildCommand(),
aliases: [],
describe: this.description,
builder: yargs => {
this.arguments.forEach(argument => argument.toBuilder(yargs))
return yargs
return this.arguments.reduce(
(yargs, argument) => argument.toPositional(yargs),
yargs as Argv<T>
)
},
handler: async argv => {
if (this.handler) {
await this.handler(argv)
const { _, $0, ...rest } = argv
await this.handler(rest)
}
}
}
return module
}

/**
* Returns a formatted command which can be used in the command() function
* of yargs
*/
private buildCommand() {
const args = this.arguments.map(arg => arg.toCommand()).join(' ')

Expand Down
2 changes: 1 addition & 1 deletion src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class Program {
this.description = description
}

add(command: Command) {
add<T>(command: Command<T>) {
// See https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
this.yargs.command(command.toYargs())
return this
Expand Down
3 changes: 3 additions & 0 deletions tests/__snapshots__/command.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`variadic argument must be the last 1`] = `"Can't add more arguments"`;
15 changes: 15 additions & 0 deletions tests/command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { command, Command, argument } from '../src'

test('command should return new Command object', () => {
expect(command('test')).toBeInstanceOf(Command)
})

test('variadic argument must be the last', () => {
const cmd = command('cmd')
const variadicArg = argument('var').variadic()
const regularArg = argument('reg')
cmd.add(variadicArg)
expect(() => {
cmd.add(regularArg)
}).toThrowErrorMatchingSnapshot()
})

0 comments on commit 8a2cdf9

Please sign in to comment.