Skip to content

Commit

Permalink
feat: add InferredFlags type (#473)
Browse files Browse the repository at this point in the history
* feat: add InferredFlags type

* chore: add test for flag types

* chore: more type tests

* chore: clean up

* feat: add custom flag builder

* chore: fix tests

* fix: custom flag builder

Co-authored-by: Shane McLaughlin <m.shane.mclaughlin@gmail.com>
  • Loading branch information
mdonnalley and mshanemc committed Aug 23, 2022
1 parent 56698d6 commit ee5ce65
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 124 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -74,6 +74,7 @@
"shx": "^0.3.4",
"sinon": "^11.1.2",
"ts-node": "^9.1.1",
"tsd": "^0.22.0",
"typescript": "4.5.5"
},
"engines": {
Expand Down
29 changes: 8 additions & 21 deletions src/flags.ts
@@ -1,23 +1,14 @@
import {OptionFlag, Definition, BooleanFlag, EnumFlagOptions, Default} from './interfaces'
import * as Parser from './parser'
import {OptionFlag, BooleanFlag, EnumFlagOptions, Default} from './interfaces'
import {custom, boolean} from './parser'
import Command from './command'
export {boolean, integer, url, directory, file, string, build, option, custom} from './parser'

export function build<T>(defaults: {parse: OptionFlag<T>['parse']} & Partial<OptionFlag<T>>): Definition<T>
export function build(defaults: Partial<OptionFlag<string>>): Definition<string>
export function build<T>(defaults: Partial<OptionFlag<T>>): Definition<T> {
return Parser.flags.build<T>(defaults as any)
}

export function option<T>(options: {parse: OptionFlag<T>['parse']} & Partial<OptionFlag<T>>) {
return build<T>(options)()
}

export function _enum<T = string>(opts: EnumFlagOptions<T> & {multiple: true} & ({required: true} | { default: Default<T> })): OptionFlag<T[]>
export function _enum<T = string>(opts: EnumFlagOptions<T> & {multiple: true}): OptionFlag<T[] | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T, true> & {multiple: true} & ({required: true} | { default: Default<T[]> })): OptionFlag<T[]>
export function _enum<T = string>(opts: EnumFlagOptions<T, true> & {multiple: true}): OptionFlag<T[] | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T> & ({required: true} | { default: Default<T> })): OptionFlag<T>
export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T> | OptionFlag<T[]> | OptionFlag<T | undefined> | OptionFlag<T[] | undefined> {
return build<T>({
return custom<T, EnumFlagOptions<T>>({
async parse(input) {
if (!opts.options.includes(input)) throw new Error(`Expected --${this.name}=${input} to be one of: ${opts.options.join(', ')}`)
return input as unknown as T
Expand All @@ -29,12 +20,8 @@ export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T> | Opt

export {_enum as enum}

const stringFlag = build({})
export {stringFlag as string}
export {boolean, integer, url, directory, file} from './parser'

export const version = (opts: Partial<BooleanFlag<boolean>> = {}) => {
return Parser.flags.boolean({
return boolean({
description: 'Show CLI version.',
...opts,
parse: async (_: any, cmd: Command) => {
Expand All @@ -45,7 +32,7 @@ export const version = (opts: Partial<BooleanFlag<boolean>> = {}) => {
}

export const help = (opts: Partial<BooleanFlag<boolean>> = {}) => {
return Parser.flags.boolean({
return boolean({
description: 'Show CLI help.',
...opts,
parse: async (_: any, cmd: Command) => {
Expand Down
33 changes: 33 additions & 0 deletions src/interfaces/flags.ts
@@ -0,0 +1,33 @@
import {FlagInput} from './parser'

/**
* Infer the flags that are returned by Command.parse. This is useful for when you want to assign the flags as a class property.
*
* @example
* export type StatusFlags = Interfaces.InferredFlags<typeof Status.flags & typeof Status.globalFlags>
*
* export abstract class BaseCommand extends Command {
* static enableJsonFlag = true
*
* static globalFlags = {
* config: Flags.string({
* description: 'specify config file',
* }),
* }
* }
*
* export default class Status extends BaseCommand {
* static flags = {
* force: Flags.boolean({char: 'f', description: 'a flag'}),
* }
*
* public flags!: StatusFlags
*
* public async run(): Promise<StatusFlags> {
* const result = await this.parse(Status)
* this.flags = result.flags
* return result.flags
* }
* }
*/
export type InferredFlags<T> = T extends FlagInput<infer F> ? F & { json: boolean | undefined; } : unknown
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Expand Up @@ -19,3 +19,4 @@ export {PJSON} from './pjson'
export {Plugin, PluginOptions, Options} from './plugin'
export {Topic} from './topic'
export {TSConfig} from './ts-config'
export {InferredFlags} from './flags'
85 changes: 58 additions & 27 deletions src/interfaces/parser.ts
Expand Up @@ -78,13 +78,13 @@ type MetadataFlag = {
export type ListItem = [string, string | undefined]
export type List = ListItem[]

export type DefaultContext<T> = {
options: OptionFlag<T>;
flags: { [k: string]: string };
export type DefaultContext<T, P> = {
options: P & OptionFlag<T>;
flags: Record<string, string>;
}

export type Default<T> = T | ((context: DefaultContext<T>) => Promise<T>)
export type DefaultHelp<T> = T | ((context: DefaultContext<T>) => Promise<string | undefined>)
export type Default<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<T>)
export type DefaultHelp<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<string | undefined>)

export type FlagProps = {
name: string;
Expand All @@ -109,10 +109,30 @@ export type FlagProps = {
* Shows this flag in a separate list in the help.
*/
helpGroup?: string;
/**
* Accept an environment variable as input
*/
env?: string;
/**
* If true, the flag will not be shown in the help.
*/
hidden?: boolean;
/**
* If true, the flag will be required.
*/
required?: boolean;
/**
* List of flags that this flag depends on.
*/
dependsOn?: string[];
/**
* List of flags that cannot be used with this flag.
*/
exclusive?: string[];
/**
* Exactly one of these flags must be provided.
*/
exactlyOne?: string[];
}

export type BooleanFlagProps = FlagProps & {
Expand All @@ -124,46 +144,57 @@ export type OptionFlagProps = FlagProps & {
type: 'option';
helpValue?: string;
options?: string[];
multiple: boolean;
multiple?: boolean;
}

export type FlagBase<T, I> = FlagProps & {
exactlyOne?: string[];
/**
* also accept an environment variable as input
*/
env?: string;
parse(input: I, context: any): Promise<T>;
export type FlagParser<T, I, P = any> = (input: I, context: any, opts: P & OptionFlag<T>) => Promise<T>

export type FlagBase<T, I, P = any> = FlagProps & {
parse: FlagParser<T, I, P>;
}

export type BooleanFlag<T> = FlagBase<T, boolean> & BooleanFlagProps & {
/**
* specifying a default of false is the same not specifying a default
* specifying a default of false is the same as not specifying a default
*/
default?: Default<boolean>;
}
export type OptionFlag<T> = FlagBase<T, string> & OptionFlagProps & {
default?: Default<T | undefined>;

export type CustomOptionFlag<T, P = any, M = false> = FlagBase<T, string, P> & OptionFlagProps & {
defaultHelp?: DefaultHelp<T>;
input: string[];
default?: M extends true ? Default<T[] | undefined, P> : Default<T | undefined, P>;
}

export type Definition<T> = {
export type OptionFlag<T> = FlagBase<T, string> & OptionFlagProps & {
defaultHelp?: DefaultHelp<T>;
input: string[];
} & ({
default?: Default<T | undefined>;
multiple: false;
} | {
default?: Default<T[] | undefined>;
multiple: true;
})

export type Definition<T, P = Record<string, unknown>> = {
(
options: { multiple: true } & ({ required: true } | { default: Default<T> }) &
Partial<OptionFlag<T>>,
options: P & { multiple: true } & ({ required: true } | { default: Default<T[]> }) & Partial<OptionFlag<T>>
): OptionFlag<T[]>;
(options: { multiple: true } & Partial<OptionFlag<T[]>>): OptionFlag<T[] | undefined>;
(
options: ({ required: true } | { default: Default<T> }) &
Partial<OptionFlag<T>>,
): OptionFlag<T>;
(options?: Partial<OptionFlag<T>>): OptionFlag<T | undefined>;
(options: P & { multiple: true } & Partial<OptionFlag<T>>): OptionFlag<T[] | undefined>;
(options: P & ({ required: true } | { default: Default<T> }) & Partial<OptionFlag<T>>): OptionFlag<T>;
(options?: P & Partial<OptionFlag<T>>): OptionFlag<T | undefined>;
}

export type EnumFlagOptions<T> = Partial<OptionFlag<T>> & {
export type EnumFlagOptions<T, M = false> = Partial<CustomOptionFlag<T, any, M>> & {
options: T[];
}
} & ({
default?: Default<T | undefined>;
multiple?: false;
} | {
default?: Default<T[] | undefined>;
multiple: true;
})

export type Flag<T> = BooleanFlag<T> | OptionFlag<T>

Expand Down

0 comments on commit ee5ce65

Please sign in to comment.