UNPKG

@black-flag/extensions

Version:

A collection of set-theoretic declarative-first APIs for yargs and Black Flag

563 lines 26.6 kB
import { $executionContext } from '@black-flag/core'; import { BfeErrorMessage } from "./error.js"; import { $artificiallyInvoked } from "./symbols.js"; import type { SafeDeepCloneOptions } from '@-xun/js'; import type { Arguments, Configuration, ImportedConfigurationModule } from '@black-flag/core'; import type { ExecutionContext, FrameworkArguments } from '@black-flag/core/util'; import type { LiteralUnion, Merge, OmitIndexSignature, Promisable } from 'type-fest'; export { $artificiallyInvoked, BfeErrorMessage }; /** * The function type of the `builder` export accepted by Black Flag. */ export type BfBuilderFunction<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = Extract<Configuration<CustomCliArguments, CustomExecutionContext>['builder'], Function>; /** * The object type of the `builder` export accepted by Black Flag. */ export type BfBuilderObject<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = Exclude<Configuration<CustomCliArguments, CustomExecutionContext>['builder'], Function>; /** * The object value type of a {@link BfBuilderObject}. * * Equivalent to `yargs.Options` as of yargs\@17.7.2. */ export type BfBuilderObjectValue<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = BfBuilderObject<CustomCliArguments, CustomExecutionContext>[string]; /** * The generic object value type of a {@link BfBuilderObject}. */ export type BfGenericBuilderObjectValue = BfBuilderObjectValue<Record<string, unknown>, ExecutionContext>; /** * A version of the object type of the `builder` export accepted by Black Flag * that supports BFE's additional functionality. */ export type BfeBuilderObject<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = { [key: string]: BfeBuilderObjectValue<CustomCliArguments, CustomExecutionContext>; }; /** * The object value type of a {@link BfeBuilderObject}. */ export type BfeBuilderObjectValue<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = BfeBuilderObjectValueWithoutExtensions & BfeBuilderObjectValueExtensions<CustomCliArguments, CustomExecutionContext>; /** * An object containing only those properties recognized by * BFE. * * This type + {@link BfeBuilderObjectValueWithoutExtensions} = * {@link BfeBuilderObjectValue}. */ export type BfeBuilderObjectValueExtensions<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = { /** * `requires` enables checks to ensure the specified arguments, or * argument-value pairs, are given conditioned on the existence of another * argument. For example: * * ```jsonc * { * "x": { "requires": "y" }, // ◄ Disallows x without y * "y": {} * } * ``` * * Note: if an argument-value pair is specified and said argument is * configured as an array (`{ array: true }`), it will be searched for the * specified value. Otherwise, a strict deep equality check is performed. */ requires?: BfeBuilderObjectValueExtensionValue; /** * `conflicts` enables checks to ensure the specified arguments, or * argument-value pairs, are _never_ given conditioned on the existence of * another argument. For example: * * ```jsonc * { * "x": { "conflicts": "y" }, // ◄ Disallows y if x is given * "y": {} * } * ``` * * Note: if an argument-value pair is specified and said argument is * configured as an array (`{ array: true }`), it will be searched for the * specified value. Otherwise, a strict deep equality check is performed. */ conflicts?: BfeBuilderObjectValueExtensionValue; /** * `demandThisOptionIf` enables checks to ensure an argument is given when at * least one of the specified groups of arguments, or argument-value pairs, is * also given. For example: * * ```jsonc * { * "x": {}, * "y": { "demandThisOptionIf": "x" }, // ◄ Demands y if x is given * "z": { "demandThisOptionIf": "x" } // ◄ Demands z if x is given * } * ``` * * Note: if an argument-value pair is specified and said argument is * configured as an array (`{ array: true }`), it will be searched for the * specified value. Otherwise, a strict deep equality check is performed. */ demandThisOptionIf?: BfeBuilderObjectValueExtensionValue; /** * `demandThisOption` enables checks to ensure an argument is always given. * This is equivalent to `demandOption` from vanilla yargs. For example: * * ```jsonc * { * "x": { "demandThisOption": true }, // ◄ Disallows ∅, y * "y": { "demandThisOption": false } * } * ``` */ demandThisOption?: BfGenericBuilderObjectValue['demandOption']; /** * `demandThisOptionOr` enables non-optional inclusive disjunction checks per * group. Put another way, `demandThisOptionOr` enforces a "logical or" * relation within groups of required options. For example: * * ```jsonc * { * "x": { "demandThisOptionOr": ["y", "z"] }, // ◄ Demands x or y or z * "y": { "demandThisOptionOr": ["x", "z"] }, * "z": { "demandThisOptionOr": ["x", "y"] } * } * ``` * * Note: if an argument-value pair is specified and said argument is * configured as an array (`{ array: true }`), it will be searched for the * specified value. Otherwise, a strict deep equality check is performed. */ demandThisOptionOr?: BfeBuilderObjectValueExtensionValue; /** * `demandThisOptionXor` enables non-optional exclusive disjunction checks per * exclusivity group. Put another way, `demandThisOptionXor` enforces mutual * exclusivity within groups of required options. For example: * * ```jsonc * { * // ▼ Disallows ∅, z, w, xy, xyw, xyz, xyzw * "x": { "demandThisOptionXor": ["y"] }, * "y": { "demandThisOptionXor": ["x"] }, * // ▼ Disallows ∅, x, y, zw, xzw, yzw, xyzw * "z": { "demandThisOptionXor": ["w"] }, * "w": { "demandThisOptionXor": ["z"] } * } * ``` * * Note: if an argument-value pair is specified and said argument is * configured as an array (`{ array: true }`), it will be searched for the * specified value. Otherwise, a strict deep equality check is performed. */ demandThisOptionXor?: BfeBuilderObjectValueExtensionValue; /** * `implies` will set default values for the specified arguments conditioned * on the existence of another argument. These implied defaults will override * any `default` configurations of the specified arguments. * * If any of the specified arguments are explicitly given on the command line, * their values must match the specified argument-value pairs respectively * (which is the behavior of `requires`/`conflicts`). Use `looseImplications` * to modify this behavior. * * Hence, `implies` only accepts one or more argument-value pairs and not raw * strings. For example: * * ```jsonc * { * "x": { "implies": { "y": true } }, // ◄ x is now synonymous with xy * "y": {} * } * ``` * * @see {@link BfeBuilderObjectValueExtensions.looseImplications} * @see {@link BfeBuilderObjectValueExtensions.vacuousImplications} */ implies?: Exclude<BfeBuilderObjectValueExtensionValue, string | unknown[]> | Exclude<BfeBuilderObjectValueExtensionValue, string | unknown[]>[]; /** * When `looseImplications` is set to `true`, any implied arguments, when * explicitly given on the command line, will _override_ their configured * implications instead of causing an error. * * @default false * @see {@link BfeBuilderObjectValueExtensions.implies} */ looseImplications?: boolean; /** * When `vacuousImplications` is set to `true` and the option is also * configured as a "boolean" type, the implications configured via `implies` * will still be applied to `argv` even if said option has a `false` value in * `argv`. In the same scenario except with `vacuousImplications` set to * `false`, the implications configured via `implies` are instead ignored. * * @default false * @see {@link BfeBuilderObjectValueExtensions.implies} */ vacuousImplications?: boolean; /** * `check` is the declarative option-specific version of vanilla yargs's * `yargs::check()`. It also supports async and promise-returning functions. * * This function receives the `currentArgumentValue`, which you are free to * type as you please, and the fully parsed `argv`. If this function throws, * the exception will bubble. If this function returns an instance of `Error`, * a string, or any non-truthy value (including `undefined` or not returning * anything), Black Flag will throw a `CliError` on your behalf. * * You may also pass an array of check functions, each being executed after * the other. Note that providing an array of one or more async check * functions will result in them being awaited concurrently. * * Note that `check` runs _at the very end of Black Flag's second parsing * pass_, meaning it runs _after_ things like `coerce` and `subOptionOf`, and * therefore receives the _final_ version of `argv` (the one passed to a * command's `handler`). * * See [the * documentation](https://github.com/Xunnamius/black-flag/tree/main/packages/extensions/README.md#check) * for details. */ check?: BfeCheckFunction<CustomCliArguments, CustomExecutionContext> | BfeCheckFunction<CustomCliArguments, CustomExecutionContext>[]; /** * `subOptionOf` is declarative sugar around Black Flag's support for double * argument parsing, allowing you to describe the relationship between options * and the suboptions whose configurations they determine. * * See [the * documentation](https://github.com/Xunnamius/black-flag/tree/main/packages/extensions/README.md#suboptionof) * for details. * * For describing simpler implicative relations, see `implies`. */ subOptionOf?: Record<string, BfeSubOptionOfExtensionValue<CustomCliArguments, CustomExecutionContext> | BfeSubOptionOfExtensionValue<CustomCliArguments, CustomExecutionContext>[]>; /** * `default` will set a default value for an argument. This is equivalent to * `default` from vanilla yargs. * * However, unlike vanilla yargs and Black Flag, this default value is applied * towards the end of BFE's execution, enabling its use alongside keys like * `conflicts`. See [the * documentation](https://github.com/Xunnamius/black-flag/tree/main/packages/extensions/README.md#support-for-default-with-conflictsrequiresetc) * for details. * * Note also that a defaulted argument will not be coerced by the `coerce` * setting. Only arguments given via `argv` trigger `coerce`. This is vanilla * yargs behavior. */ default?: unknown; /** * `coerce` transforms an original `argv` value into another one. This is * equivalent to `coerce` from vanilla yargs. * * However, unlike vanilla yargs and Black Flag, the `coerce` function will * _always_ receive an array if the option was configured with `{ array: true * }`. * * Note that **a defaulted argument will not result in this function being * called.** Only arguments given via `argv` trigger `coerce`. This is vanilla * yargs behavior. */ coerce?: BfGenericBuilderObjectValue['coerce']; }; /** * The string/object/array type of a {@link BfeBuilderObjectValueExtensions}. * * This type is a superset of {@link BfeBuilderObjectValueExtensionObject}. */ export type BfeBuilderObjectValueExtensionValue = string | BfeBuilderObjectValueExtensionObject | (string | BfeBuilderObjectValueExtensionObject)[]; /** * The object type of a {@link BfeBuilderObjectValueExtensions}. * * This type is a subset of {@link BfeBuilderObjectValueExtensionValue}. */ export type BfeBuilderObjectValueExtensionObject = Record<string, unknown>; /** * An object containing a subset of only those properties recognized by * Black Flag (and, consequentially, vanilla yargs). Also excludes * properties that conflict with {@link BfeBuilderObjectValueExtensions} and/or * are deprecated by vanilla yargs. * * This type + {@link BfeBuilderObjectValueExtensions} = * {@link BfeBuilderObjectValue}. * * This type is a subset of {@link BfBuilderObjectValue}. */ export type BfeBuilderObjectValueWithoutExtensions = Omit<BfGenericBuilderObjectValue, 'conflicts' | 'implies' | 'demandOption' | 'demand' | 'require' | 'required' | 'default' | 'coerce'>; /** * A {@link BfeBuilderObjectValue} instance with the `subOptionOf` BFE key * omitted. */ export type BfeBuilderObjectValueWithoutSubOptionOfExtension<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = Omit<BfeBuilderObjectValue<CustomCliArguments, CustomExecutionContext>, 'subOptionOf'>; /** * The array element type of * {@link BfeBuilderObjectValueExtensions.subOptionOf}. */ export type BfeSubOptionOfExtensionValue<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = { /** * This function receives the `superOptionValue` of the so-called "super * option" (i.e. `key` in `{ subOptionOf: { key: { when: ... }}}`), which you * are free to type as you please, and the fully parsed `argv` (not including * any default values). This function must return a boolean indicating whether * the `update` function should run or not. * * Note that this function is only invoked if the super option is given on the * command line. If it is not, neither this function nor `update` will ever * run. Therefore, if your updater should run whenever the super option is * given, regardless of its value, it is acceptable to use `{ when: () => true * }` */ when: (superOptionValue: any, argv: Arguments<CustomCliArguments, CustomExecutionContext>) => boolean; /** * This function receives the current configuration for this option * (`oldOptionConfig`) and the fully parsed `argv` (not including any default * values), and must return the new configuration for this option. * * This configuration will completely overwrite the old configuration. To * extend the old configuration instead, spread it. For example: * * ```javascript * return { * ...oldOptionConfig, * description: 'New description' * } * ``` */ update: ((oldOptionConfig: BfeBuilderObjectValueWithoutSubOptionOfExtension<CustomCliArguments, CustomExecutionContext>, argv: Arguments<CustomCliArguments, CustomExecutionContext>) => BfeBuilderObjectValueWithoutSubOptionOfExtension<CustomCliArguments, CustomExecutionContext>) | BfeBuilderObjectValueWithoutSubOptionOfExtension<CustomCliArguments, CustomExecutionContext>; }; /** * This function is used to validate an argument passed to Black Flag. * * @see {@link BfeBuilderObjectValueExtensions.check} */ export type BfeCheckFunction<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = (currentArgumentValue: any, argv: Arguments<CustomCliArguments, CustomExecutionContext>) => Promisable<unknown>; /** * This function implements several additional optionals-related units of * functionality. This function is meant to take the place of a command's * `builder` export. * * This type cannot be instantiated by direct means. Instead, it is created and * returned by {@link withBuilderExtensions}. * * @see {@link withBuilderExtensions} */ export type BfeBuilderFunction<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = (...args: Parameters<BfBuilderFunction<CustomCliArguments, CustomExecutionContext>>) => BfBuilderObject<CustomCliArguments, CustomExecutionContext>; /** * A stricter version of Black Flag's * [Arguments](https://github.com/Xunnamius/black-flag/blob/main/docs/api/src/exports/type-aliases/Arguments.md) * type that explicitly omits the fallback indexers for unrecognized arguments. * Even though it is the runtime equivalent of `Arguments`, using this type * allows intellisense to report bad/misspelled/missing arguments from `argv` in * various places where it otherwise couldn't. * * **This type is intended for intellisense purposes only.** */ export type BfeStrictArguments<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = OmitIndexSignature<Arguments<CustomCliArguments, CustomExecutionContext>> & FrameworkArguments<CustomExecutionContext> & { [$artificiallyInvoked]?: boolean; }; /** * A stricter version of Black Flag's * [ExecutionContext](https://github.com/Xunnamius/black-flag/blob/main/docs/api/src/exports/util/type-aliases/ExecutionContext.md) * type that explicitly omits the fallback indexers for unrecognized properties. * Even though it is the runtime equivalent of `ExecutionContext`, using this * type allows intellisense to report bad/misspelled/missing arguments from * `context` in various places where it otherwise couldn't. * * **This type is intended for intellisense purposes only.** */ export type AsStrictExecutionContext<CustomExecutionContext extends ExecutionContext> = OmitIndexSignature<Exclude<CustomExecutionContext, 'state'>> & OmitIndexSignature<CustomExecutionContext['state']>; /** * A version of Black Flag's `builder` function parameters that exclude yargs * methods that are not supported by BFE. * * @see {@link withBuilderExtensions} */ export type BfeCustomBuilderFunctionParameters<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext, P = Parameters<BfBuilderFunction<CustomCliArguments, CustomExecutionContext>>> = P extends [infer R, ...infer S] ? S extends [infer T, ...infer _U] ? [blackFlag: R & { options: never; option: never; }, T, (BfeStrictArguments<Partial<CustomCliArguments>, CustomExecutionContext> | undefined)] : [blackFlag: R & { options: never; option: never; }, ...S] : never; /** * This function implements several additional optionals-related units of * functionality. The return value of this function is meant to take the place * of a command's `handler` export. * * This type cannot be instantiated by direct means. Instead, it is created and * returned by {@link withBuilderExtensions}. * * Note that `customHandler` provides a stricter constraint than Black Flag's * `handler` command export in that `customHandler`'s `argv` parameter type * explicitly omits the fallback indexer for unrecognized arguments. This * means all possible arguments must be included in {@link CustomCliArguments}. * * @see {@link withBuilderExtensions} */ export type WithHandlerExtensions<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = (customHandler?: (argv: BfeStrictArguments<CustomCliArguments, CustomExecutionContext>) => Promisable<void>) => Configuration<CustomCliArguments, CustomExecutionContext>['handler']; /** * The array of extended exports and high-order functions returned by * {@link withBuilderExtensions}. */ export type WithBuilderExtensionsReturnType<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext> = [builder: BfeBuilderFunction<CustomCliArguments, CustomExecutionContext>, withHandlerExtensions: WithHandlerExtensions<CustomCliArguments, CustomExecutionContext>]; /** * A configuration object that further configures the behavior of * {@link withBuilderExtensions}. */ export type WithBuilderExtensionsConfig<CustomCliArguments extends Record<string, unknown>> = { /** * Set to `true` to disable BFE's support for automatic grouping of related * options. * * See [the * documentation](https://github.com/Xunnamius/black-flag/blob/main/packages/extensions/README.md#automatic-grouping-of-related-options) * for details. * * @default false */ disableAutomaticGrouping?: boolean; /** * Set to `true` to enable BFE's support for automatic sorting of options. * * See [the * documentation](https://github.com/Xunnamius/black-flag/blob/main/packages/extensions/README.md#automatic-sorting-of-options) * for details. * * @default false */ enableAutomaticSorting?: boolean; /** * An array of zero or more string keys of `CustomCliArguments`, with the * optional addition of `'help'` and `'version'`, that should be grouped under * _"Common Options"_ when [automatic grouping of related * options](https://github.com/Xunnamius/black-flag/blob/main/packages/extensions/README.md#automatic-sorting-of-options) * is enabled. * * This setting is ignored if `disableAutomaticGrouping === true`. * * @default ['help'] */ commonOptions?: readonly LiteralUnion<keyof CustomCliArguments | 'help' | 'version', string>[]; }; /** * A configuration object that further configures the behavior of * {@link withUsageExtensions}. */ export type WithUsageExtensionsConfig = { /** * The result of calling this function defaults to: `Usage: * $000\n\n${altDescription}`. * * @default "$1." */ altDescription?: string; /** * Whether `altDescription` will be `trim()`'d or not. * * @default true */ trim?: boolean; /** * Whether a period will be appended to the resultant string or not. A * period is only appended if one is not already appended. * * @default true */ appendPeriod?: boolean; /** * Whether newlines will be prepended to `altDescription` or not. * * @default true */ prependNewlines?: boolean; /** * Whether the string `' [...options]'` will be appended to the first line * of usage text (after `includeSubCommand`). * * @default options.prependNewlines */ includeOptions?: boolean; /** * Whether some variation of the string `' [subcommand]'` will be appended * to the first line of usage text (before `includeOptions`). Set to `true` * or `required` when generating usage for a command with subcommands. * * @default false */ includeSubCommand?: boolean | 'required'; }; /** * This function enables several additional options-related units of * functionality via analysis of the returned options configuration object and * the parsed command line arguments (argv). * * @see {@link WithBuilderExtensionsReturnType} */ export declare function withBuilderExtensions<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext>(customBuilder?: BfeBuilderObject<CustomCliArguments, CustomExecutionContext> | ((...args: BfeCustomBuilderFunctionParameters<CustomCliArguments, CustomExecutionContext>) => BfeBuilderObject<CustomCliArguments, CustomExecutionContext> | void), { commonOptions, disableAutomaticGrouping, enableAutomaticSorting }?: WithBuilderExtensionsConfig<CustomCliArguments>): WithBuilderExtensionsReturnType<CustomCliArguments, CustomExecutionContext>; /** * Generate command usage text consistently yet flexibly. * * Defaults to: `Usage: $000\n\n${altDescription}` where `altDescription` is * `"$1."`. */ export declare function withUsageExtensions(altDescription?: string): string; export declare function withUsageExtensions(altDescription?: string, config?: Omit<WithUsageExtensionsConfig, 'altDescription'>): string; export declare function withUsageExtensions(config?: WithUsageExtensionsConfig): string; export declare function withUsageExtensions(config?: WithUsageExtensionsConfig | string, moreConfig?: Omit<WithUsageExtensionsConfig, 'altDescription'>): string; /** * This function returns a version of `maybeCommand`'s handler function that is * ready to invoke immediately. It can be used with both BFE and normal Black * Flag command exports. * * It returns a handler that expects to be passed a "reified argv," i.e. the * object normally given to the command handler after all checks have passed and * all updates to argv have been applied (including `subOptionOf` and BFE's * `implies`). * * For this reason, invoking the returned handler will not run any BF or BFE * builder configurations on the given argv object. **Whatever you pass the * returned handler function will be (safely) deep cloned and then re-gifted to * the command's handler _without_ any correctness checks.** * * Use `CustomCliArguments` (and `CustomExecutionContext`) to assert the * expected shape of the "reified argv". * * Note that, like the `argv` passed to the returned handler function, the * `context` argument passed to this function will be (safely) deep cloned, * meaning any context changes effected by the handler will not persist outside * of that handler's scope. * * Also note that the `$executionContext` key, if included in `argv`, will be * ignored. * * See [the * documentation](https://github.com/Xunnamius/black-flag/blob/main/packages/extensions/README.md#getinvocableextendedhandler) * for more details. */ export declare function getInvocableExtendedHandler<CustomCliArguments extends Record<string, unknown>, CustomExecutionContext extends ExecutionContext & { state: { extensions?: SafeDeepCloneOptions; }; }>(maybeCommand: Promisable<ImportedConfigurationModule<CustomCliArguments, CustomExecutionContext> | ImportedConfigurationModule<CustomCliArguments, AsStrictExecutionContext<CustomExecutionContext>>>, context: CustomExecutionContext): Promise<(argv: Merge<BfeStrictArguments<CustomCliArguments, CustomExecutionContext>, { /** * Do not manually provide this key. It will be included in `argv` * automatically. */ [$executionContext]?: unknown; /** * The script name or node command that, when omitted from the `argv` * passed to a handler returned by `getInvocableExtendedHandler`, * defaults to the `name` value exported from the command's module file * (or `"???"` if no name was exported). * * **Note that this default value IS LIKELY DIFFERENT THAN the _full * name_ to which Black Flag sets `$0`!** If this is an issue, manually * provide a value for `$0` in `argv`. */ $0?: string; /** * Non-option arguments that, when omitted from the `argv` passed to a * handler returned by `getInvocableExtendedHandler`, defaults to an * empty array (`[]`). */ _?: BfeStrictArguments<CustomCliArguments, CustomExecutionContext>["_"]; }>) => Promise<void>>;