Skip to content

Commit

Permalink
Merge branch 'main' into mdonnalley/flag-relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 23, 2022
2 parents 57c1c97 + c59dc10 commit 1e2aceb
Show file tree
Hide file tree
Showing 13 changed files with 596 additions and 130 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,27 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [1.15.0](https://github.com/oclif/core/compare/v1.14.2...v1.15.0) (2022-08-23)


### Features

* add InferredFlags type ([#473](https://github.com/oclif/core/issues/473)) ([ee5ce65](https://github.com/oclif/core/commit/ee5ce651899c0ef586d425567ef3b78468dca627))

### [1.14.2](https://github.com/oclif/core/compare/v1.14.1...v1.14.2) (2022-08-18)


### Bug Fixes

* add overloads to enum flag ([799455b](https://github.com/oclif/core/commit/799455bbb526b221c806bf8feff6b625dcf50a56))

### [1.14.1](https://github.com/oclif/core/compare/v1.14.0...v1.14.1) (2022-08-16)


### Bug Fixes

* parser doesn't validate against options parameter if the value is provided through a env var ([#474](https://github.com/oclif/core/issues/474)) ([fe6dfea](https://github.com/oclif/core/commit/fe6dfea0bcc5cae69c91962430996670decf7887))

## [1.14.0](https://github.com/oclif/core/compare/v1.13.11...v1.14.0) (2022-08-16)


Expand Down
3 changes: 2 additions & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "@oclif/core",
"description": "base library for oclif CLIs",
"version": "1.14.0",
"version": "1.15.0",
"author": "Salesforce",
"bugs": "https://github.com/oclif/core/issues",
"dependencies": {
Expand Down 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
33 changes: 12 additions & 21 deletions src/flags.ts
@@ -1,36 +1,27 @@
import {OptionFlag, Definition, BooleanFlag, EnumFlagOptions} 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)()
}

const _enum = <T = string>(opts: EnumFlagOptions<T>): OptionFlag<T> => {
return build<T>({
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 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
},
helpValue: `(${opts.options.join('|')})`,
...opts,
})() as OptionFlag<T>
})()
}

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 @@ -41,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'
90 changes: 62 additions & 28 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 FlagRelationship = string | {name: string; when: (flags: Record<string, unknown>) => Promise<boolean>};
export type Relationship = {
Expand Down Expand Up @@ -115,11 +115,34 @@ 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[];
relationships?: Relationship[];
/**
* List of flags that cannot be used with this flag.
*/
exclusive?: string[];
/**
* Exactly one of these flags must be provided.
*/
exactlyOne?: string[];
/**
* Define complex relationships between flags.
*/
relationships?: Relationship[];
}

export type BooleanFlagProps = FlagProps & {
Expand All @@ -131,46 +154,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 1e2aceb

Please sign in to comment.