args-tokens
Version:
parseArgs tokens compatibility and more high-performance parser
561 lines (559 loc) • 17.4 kB
TypeScript
import { ArgToken } from "./parser-C6MbpZjd.js";
//#region src/resolver.d.ts
/**
* An argument schema definition for command-line argument parsing.
*
* This schema is similar to the schema of Node.js `util.parseArgs` but with extended features:
* - Additional `required` and `description` properties
* - Extended `type` support: 'string', 'boolean', 'number', 'enum', 'positional', 'custom'
* - Simplified `default` property (single type, not union types)
*
* @example
* Basic string argument:
* ```ts
* const schema: ArgSchema = {
* type: 'string',
* description: 'Server hostname',
* default: 'localhost'
* }
* ```
*
* @example
* Required number argument with alias:
* ```ts
* const schema: ArgSchema = {
* type: 'number',
* short: 'p',
* description: 'Port number to listen on',
* required: true
* }
* ```
*
* @example
* Enum argument with choices:
* ```ts
* const schema: ArgSchema = {
* type: 'enum',
* choices: ['info', 'warn', 'error'],
* description: 'Logging level',
* default: 'info'
* }
* ```
*/
interface ArgSchema {
/**
* Type of the argument value.
*
* - `'string'`: Text value (default if not specified)
* - `'boolean'`: `true`/`false` flag (can be negatable with `--no-` prefix)
* - `'number'`: Numeric value (parsed as integer or float)
* - `'enum'`: One of predefined string values (requires `choices` property)
* - `'positional'`: Non-option argument by position
* - `'custom'`: Custom parsing with user-defined `parse` function
*
* @example
* Different argument types:
* ```ts
* {
* name: { type: 'string' }, // --name value
* verbose: { type: 'boolean' }, // --verbose or --no-verbose
* port: { type: 'number' }, // --port 3000
* level: { type: 'enum', choices: ['debug', 'info'] },
* file: { type: 'positional' }, // first positional arg
* config: { type: 'custom', parse: JSON.parse }
* }
* ```
*/
type: 'string' | 'boolean' | 'number' | 'enum' | 'positional' | 'custom';
/**
* Single character alias for the long option name.
*
* As example, allows users to use `-x` instead of `--extended-option`.
* Only valid for non-positional argument types.
*
* @example
* Short alias usage:
* ```ts
* {
* verbose: {
* type: 'boolean',
* short: 'v' // Enables both --verbose and -v
* },
* port: {
* type: 'number',
* short: 'p' // Enables both --port 3000 and -p 3000
* }
* }
* ```
*/
short?: string;
/**
* Human-readable description of the argument's purpose.
*
* Used for help text generation and documentation.
* Should be concise but descriptive enough to understand the argument's role.
*
* @example
* Descriptive help text:
* ```ts
* {
* config: {
* type: 'string',
* description: 'Path to configuration file'
* },
* timeout: {
* type: 'number',
* description: 'Request timeout in milliseconds'
* }
* }
* ```
*/
description?: string;
/**
* Marks the argument as required.
*
* When `true`, the argument must be provided by the user.
* If missing, an `ArgResolveError` with type 'required' will be thrown.
*
* Note: Only `true` is allowed (not `false`) to make intent explicit.
*
* @example
* Required arguments:
* ```ts
* {
* input: {
* type: 'string',
* required: true, // Must be provided: --input file.txt
* description: 'Input file path'
* },
* source: {
* type: 'positional',
* required: true // First positional argument must exist
* }
* }
* ```
*/
required?: true;
/**
* Allows the argument to accept multiple values.
*
* When `true`, the resolved value becomes an array.
* For options: can be specified multiple times (--tag foo --tag bar)
* For positional: collects remaining positional arguments
*
* Note: Only `true` is allowed (not `false`) to make intent explicit.
*
* @example
* Multiple values:
* ```ts
* {
* tags: {
* type: 'string',
* multiple: true, // --tags foo --tags bar → ['foo', 'bar']
* description: 'Tags to apply'
* },
* files: {
* type: 'positional',
* multiple: true // Collects all remaining positional args
* }
* }
* ```
*/
multiple?: true;
/**
* Enables negation for boolean arguments using `--no-` prefix.
*
* When `true`, allows users to explicitly set the boolean to `false`
* using `--no-option-name`. When `false` or omitted, only positive
* form is available.
*
* Only applicable to `type: 'boolean'` arguments.
*
* @example
* Negatable boolean:
* ```ts
* {
* color: {
* type: 'boolean',
* negatable: true,
* default: true,
* description: 'Enable colorized output'
* }
* // Usage: --color (true), --no-color (false)
* }
* ```
*/
negatable?: boolean;
/**
* Array of allowed string values for enum-type arguments.
*
* Required when `type: 'enum'`. The argument value must be one of these choices,
* otherwise an `ArgResolveError` with type 'type' will be thrown.
*
* Supports both mutable arrays and readonly arrays for type safety.
*
* @example
* Enum choices:
* ```ts
* {
* logLevel: {
* type: 'enum',
* choices: ['debug', 'info', 'warn', 'error'] as const,
* default: 'info',
* description: 'Logging verbosity level'
* },
* format: {
* type: 'enum',
* choices: ['json', 'yaml', 'toml'],
* description: 'Output format'
* }
* }
* ```
*/
choices?: string[] | readonly string[];
/**
* Default value used when the argument is not provided.
*
* The type must match the argument's `type` property:
* - `string` type: string default
* - `boolean` type: boolean default
* - `number` type: number default
* - `enum` type: must be one of the `choices` values
* - `positional`/`custom` type: any appropriate default
*
* @example
* Default values by type:
* ```ts
* {
* host: {
* type: 'string',
* default: 'localhost' // string default
* },
* verbose: {
* type: 'boolean',
* default: false // boolean default
* },
* port: {
* type: 'number',
* default: 8080 // number default
* },
* level: {
* type: 'enum',
* choices: ['low', 'high'],
* default: 'low' // must be in choices
* }
* }
* ```
*/
default?: string | boolean | number;
/**
* Converts the argument name from camelCase to kebab-case for CLI usage.
*
* When `true`, a property like `maxCount` becomes available as `--max-count`.
* This allows [CAC](https://github.com/cacjs/cac) user-friendly property names while maintaining CLI conventions.
*
* Can be overridden globally with `resolveArgs({ toKebab: true })`.
*
* Note: Only `true` is allowed (not `false`) to make intent explicit.
*
* @example
* Kebab-case conversion:
* ```ts
* {
* maxRetries: {
* type: 'number',
* toKebab: true, // Accessible as --max-retries
* description: 'Maximum retry attempts'
* },
* enableLogging: {
* type: 'boolean',
* toKebab: true // Accessible as --enable-logging
* }
* }
* ```
*/
toKebab?: true;
/**
* Names of other options that conflict with this option.
*
* When this option is used together with any of the conflicting options,
* an `ArgResolveError` with type 'conflict' will be thrown.
*
* Conflicts only need to be defined on one side - if option A defines a conflict
* with option B, the conflict is automatically detected when both are used,
* regardless of whether B also defines a conflict with A.
*
* Supports both single option name or array of option names.
* Option names must match the property keys in the schema object exactly
* (no automatic conversion between camelCase and kebab-case).
*
* @example
* Single conflict (bidirectional definition):
* ```ts
* {
* summer: {
* type: 'boolean',
* conflicts: 'autumn' // Cannot use --summer with --autumn
* },
* autumn: {
* type: 'boolean',
* conflicts: 'summer' // Can define on both sides for clarity
* }
* }
* ```
*
* @example
* Single conflict (one-way definition):
* ```ts
* {
* summer: {
* type: 'boolean',
* conflicts: 'autumn' // Only defined on summer side
* },
* autumn: {
* type: 'boolean'
* // No conflicts defined, but still cannot use with --summer
* }
* }
* // Usage: --summer --autumn will throw error
* // Error: "Optional argument '--summer' conflicts with '--autumn'"
* ```
*
* @example
* Multiple conflicts:
* ```ts
* {
* port: {
* type: 'number',
* conflicts: ['socket', 'pipe'], // Cannot use with --socket or --pipe
* description: 'TCP port number'
* },
* socket: {
* type: 'string',
* conflicts: ['port', 'pipe'], // Cannot use with --port or --pipe
* description: 'Unix socket path'
* },
* pipe: {
* type: 'string',
* conflicts: ['port', 'socket'], // Cannot use with --port or --socket
* description: 'Named pipe path'
* }
* }
* // These three options are mutually exclusive
* ```
*
* @example
* With kebab-case conversion:
* ```ts
* {
* summerSeason: {
* type: 'boolean',
* toKebab: true, // Accessible as --summer-season
* conflicts: 'autumnSeason' // Must use property key, not CLI name
* },
* autumnSeason: {
* type: 'boolean',
* toKebab: true // Accessible as --autumn-season
* }
* }
* // Error: "Optional argument '--summer-season' conflicts with '--autumn-season'"
* ```
*/
conflicts?: string | string[];
/**
* Custom parsing function for `type: 'custom'` arguments.
*
* Required when `type: 'custom'`. Receives the raw string value and must
* return the parsed result. Should throw an Error (or subclass) if parsing fails.
*
* The function's return type becomes the resolved argument type.
*
* @param value - Raw string value from command line
* @returns Parsed value of any type
* @throws Error or subclass when value is invalid
*
* @example
* Custom parsing functions:
* ```ts
* {
* config: {
* type: 'custom',
* parse: (value: string) => {
* try {
* return JSON.parse(value) // Parse JSON config
* } catch {
* throw new Error('Invalid JSON configuration')
* }
* },
* description: 'JSON configuration object'
* },
* date: {
* type: 'custom',
* parse: (value: string) => {
* const date = new Date(value)
* if (isNaN(date.getTime())) {
* throw new Error('Invalid date format')
* }
* return date
* }
* }
* }
* ```
*/
parse?: (value: string) => any;
}
/**
* An object that contains {@link ArgSchema | argument schema}.
*
* This type is used to define the structure and validation rules for command line arguments.
*/
interface Args {
[option: string]: ArgSchema;
}
/**
* An object that contains the values of the arguments.
*
* @typeParam T - {@link Args | Arguments} which is an object that defines the command line arguments.
*/
type ArgValues<T> = T extends Args ? ResolveArgValues<T, { [Arg in keyof T]: ExtractOptionValue<T[Arg]> }> : {
[option: string]: string | boolean | number | (string | boolean | number)[] | undefined;
};
type IsFunction<T> = T extends ((...args: any[]) => any) ? true : false;
/**
* Extracts the value type from the argument schema.
*
* @typeParam A - {@link ArgSchema | Argument schema} which is an object that defines command line arguments.
*
* @internal
*/
type ExtractOptionValue<A extends ArgSchema> = A['type'] extends 'string' ? ResolveOptionValue<A, string> : A['type'] extends 'boolean' ? ResolveOptionValue<A, boolean> : A['type'] extends 'number' ? ResolveOptionValue<A, number> : A['type'] extends 'positional' ? ResolveOptionValue<A, string> : A['type'] extends 'enum' ? A['choices'] extends string[] | readonly string[] ? ResolveOptionValue<A, A['choices'][number]> : never : A['type'] extends 'custom' ? IsFunction<A['parse']> extends true ? ResolveOptionValue<A, ReturnType<NonNullable<A['parse']>>> : never : ResolveOptionValue<A, string | boolean | number>;
type ResolveOptionValue<A extends ArgSchema, T> = A['multiple'] extends true ? T[] : T;
/**
* Resolved argument values.
*
* @typeParam A - {@link Arguments | Args} which is an object that defines the command line arguments.
* @typeParam V - Resolvable argument values.
*
* @internal
*/
type ResolveArgValues<A extends Args, V extends Record<keyof A, unknown>> = { -readonly [Arg in keyof A]?: V[Arg] } & FilterArgs<A, V, 'default'> & FilterArgs<A, V, 'required'> & FilterPositionalArgs<A, V> extends infer P ? { [K in keyof P]: P[K] } : never;
/**
* Filters the arguments based on their default values.
*
* @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments.
* @typeParam V - Resolvable argument values.
* @typeParam K - Key of the {@link ArgSchema | argument schema} to filter by.
*
* @internal
*/
type FilterArgs<A extends Args, V extends Record<keyof A, unknown>, K extends keyof ArgSchema> = { [Arg in keyof A as A[Arg][K] extends {} ? Arg : never]: V[Arg] };
/**
* Filters positional arguments from the argument schema.
*
* @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments.
* @typeParam V - Resolvable argument values.
*
* @internal
*/
type FilterPositionalArgs<A extends Args, V extends Record<keyof A, unknown>> = { [Arg in keyof A as A[Arg]['type'] extends 'positional' ? Arg : never]: V[Arg] };
/**
* An arguments for {@link resolveArgs | resolve arguments}.
*/
interface ResolveArgs {
/**
* Whether to group short arguments.
*
* @see guideline 5 in https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html
*
* @default false
*/
shortGrouping?: boolean;
/**
* Skip positional arguments index.
*
* @default -1
*/
skipPositional?: number;
/**
* Whether to convert the argument name to kebab-case. This option is applied to all arguments as `toKebab: true`, if set to `true`.
*
* @default false
*/
toKebab?: boolean;
}
/**
* Tracks which arguments were explicitly provided by the user.
*
* Each property indicates whether the corresponding argument was explicitly
* provided (true) or is using a default value or not provided (false).
*
* @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments.
*/
type ArgExplicitlyProvided<A extends Args> = { [K in keyof A]: boolean };
/**
* Resolve command line arguments.
*
* @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments.
*
* @param args - An arguments that contains {@link ArgSchema | arguments schema}.
* @param tokens - An array of {@link ArgToken | tokens}.
* @param resolveArgs - An arguments that contains {@link ResolveArgs | resolve arguments}.
* @returns An object that contains the values of the arguments, positional arguments, rest arguments, {@link AggregateError | validation errors}, and explicit provision status.
*
* @example
* ```typescript
* // passed tokens: --port 3000
*
* const { values, explicit } = resolveArgs({
* port: {
* type: 'number',
* default: 8080
* },
* host: {
* type: 'string',
* default: 'localhost'
* }
* }, parsedTokens)
*
* values.port // 3000
* values.host // 'localhost'
*
* explicit.port // true (explicitly provided)
* explicit.host // false (not provided, fallback to default)
* ```
*/
declare function resolveArgs<A extends Args>(args: A, tokens: ArgToken[], {
shortGrouping,
skipPositional,
toKebab
}?: ResolveArgs): {
values: ArgValues<A>;
positionals: string[];
rest: string[];
error: AggregateError | undefined;
explicit: ArgExplicitlyProvided<A>;
};
/**
* An error type for {@link ArgResolveError}.
*/
type ArgResolveErrorType = 'type' | 'required' | 'conflict';
/**
* An error that occurs when resolving arguments.
* This error is thrown when the argument is not valid.
*/
declare class ArgResolveError extends Error {
name: string;
schema: ArgSchema;
type: ArgResolveErrorType;
/**
* Create an `ArgResolveError` instance.
*
* @param message - the error message
* @param name - the name of the argument
* @param type - the type of the error, either 'type' or 'required'
* @param schema - the argument schema that caused the error
*/
constructor(message: string, name: string, type: ArgResolveErrorType, schema: ArgSchema);
}
//#endregion
export { ArgExplicitlyProvided, ArgResolveError, ArgResolveErrorType, ArgSchema, ArgValues, Args, ExtractOptionValue, FilterArgs, FilterPositionalArgs, ResolveArgValues, ResolveArgs, resolveArgs };