From 24982cb92f0f55336950a6a0dd9cb4ea21f92f27 Mon Sep 17 00:00:00 2001 From: Joram van den Boezem Date: Mon, 3 Apr 2023 11:59:09 +0200 Subject: [PATCH] feat: add requires and excludes (#434) --- README.md | 11 ++++++++++- examples/types.ts | 30 +++++++++++++++++++++++++++++- src/argument.ts | 2 +- src/baseArg.ts | 12 ++++++++++++ src/option.ts | 8 +++++--- tests/types/command.test-d.ts | 18 +++++++++++++++++- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d4c25b4b..b41f8a4e 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,10 @@ Adds a positional argument to the command. string, it will be used as the question text. - `alias` (string|array) alias or aliases for the argument. - `coerce` (function) transform function for this argument value (untyped). + - `requires` (string|array) make another option or argument required if the + argument is present + - `excludes` (string|array) exclude another options or argument if the + argument is present #### `command.option(name, options)` @@ -504,7 +508,7 @@ Adds an option to the command. - Name (string, required) is used to identify the option. - Options (object, optional) can be provided to change the behavior of the option. Object with any of these keys: - - `description` (string, optional) is used in help output. + - `description` (string) is used in help output. - `type` (string) one of `"array"|"boolean"|"count"|"number"|"string"` which determines the runtime type of the argument. Use count for the number of times an option was provided (e.g. verbosity levels). @@ -516,6 +520,11 @@ Adds an option to the command. string, it will be used as the question text. - `alias` (string|array) alias or aliases for the option. - `coerce` (function) transform function for this option value (untyped). + - `required` (boolean) makes the option required. + - `requires` (string|array) make another option or argument required if the + option is present + - `excludes` (string|array) exclude another options or argument if the option + is present #### `command.add(command)` diff --git a/examples/types.ts b/examples/types.ts index d39fa914..b3313b81 100644 --- a/examples/types.ts +++ b/examples/types.ts @@ -87,12 +87,40 @@ const defaultValues = command("default") console.log("Args are", args); }); +const constraints = command("constraint") + .argument("arg", { + description: "Required argument", + }) + .argument("optionalArg", { + description: "Required argument", + optional: true, + }) + .option("opt", { description: "Required option", required: true }) + .option("opt1a", { + description: "Also requires option 1a", + requires: "opt1b", + }) + .option("opt1b", { + description: "Also requires option 1b", + requires: "opt1a", + }) + .option("opt2", { description: "Forbids option 1a", excludes: "opt1a" }) + .option("opt3", { + description: "Also requires optionalArg", + requires: "optionalArg", + }) + + .action((args) => { + console.log("Args are", args); + }); + const app = program() .description("All argument and option types") .add(string) .add(number) .add(boolean) .add(choices) - .add(defaultValues); + .add(defaultValues) + .add(constraints); app.runOrRepl(); diff --git a/src/argument.ts b/src/argument.ts index b344ad2c..eda6637a 100644 --- a/src/argument.ts +++ b/src/argument.ts @@ -72,6 +72,6 @@ export class Argument extends BaseArg { * it. See http://yargs.js.org/docs/#api-positionalkey-opt */ toYargs(yargs: Argv) { - return yargs.positional(this.name, this.options); + return yargs.positional(this.name, BaseArg.getYargsOptions(this.options)); } } diff --git a/src/baseArg.ts b/src/baseArg.ts index 0b6d7fe7..0b0026ba 100644 --- a/src/baseArg.ts +++ b/src/baseArg.ts @@ -4,6 +4,8 @@ import type { OptionOptions } from "./option.js"; export interface BaseArgOptions { prompt?: true | string; + requires?: string | string[]; + excludes?: string | string[]; } // prettier-ignore @@ -121,4 +123,14 @@ export class BaseArg { getOptions() { return this.options; } + + protected static getYargsOptions(options: T) { + const { requires, excludes, ...rest } = options; + + return { + implies: requires, + conflicts: excludes, + ...rest, + }; + } } diff --git a/src/option.ts b/src/option.ts index 60957894..e933732f 100644 --- a/src/option.ts +++ b/src/option.ts @@ -25,7 +25,6 @@ type IgnoreOptions = | "normalize" | "number" | "require" - | "required" | "requiresArg" | "skipValidation" | "string" @@ -33,7 +32,9 @@ type IgnoreOptions = export interface OptionOptions extends Omit, - BaseArgOptions {} + BaseArgOptions { + required?: true; +} export function option(name: string) { return new Option(name); @@ -50,6 +51,7 @@ export class Option extends BaseArg { configure(options: OptionOptions) { this.options = options; + return this; } @@ -58,6 +60,6 @@ export class Option extends BaseArg { * it. See http://yargs.js.org/docs/#api-positionalkey-opt */ toYargs(yargs: Argv) { - return yargs.option(this.name, this.options); + return yargs.option(this.name, BaseArg.getYargsOptions(this.options)); } } diff --git a/tests/types/command.test-d.ts b/tests/types/command.test-d.ts index 6a9dee20..5afc93a5 100644 --- a/tests/types/command.test-d.ts +++ b/tests/types/command.test-d.ts @@ -106,7 +106,23 @@ cmd.argument("foo", { optional: true, default: false }).action((args) => { expectType<{ foo: boolean }>(args); }); -// @todo: option types +// String option types +cmd.option("foo").action((args) => { + // Default type + expectType<{ foo: unknown }>(args); +}); +cmd.option("foo", { type: "string" }).action((args) => { + // Explicit string type + expectType<{ foo: string | undefined }>(args); +}); +cmd.option("foo", { default: "string" }).action((args) => { + // Implicit string type + expectType<{ foo: string }>(args); +}); +cmd.option("foo", { type: "string", required: true }).action((args) => { + // Required explicit string type + expectType<{ foo: string }>(args); +}); // @fixme: unspecified options are omitted by yargs but are always present in // the args.