diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e5fe080..8d71af7 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: chenasraf patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: casraf diff --git a/release.config.cjs b/release.config.cjs index 6f74530..ba9a9f5 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -30,6 +30,7 @@ module.exports = { '@semantic-release/changelog', { changelogFile: 'CHANGELOG.md', + changelogTitle: '# Change Log', }, ], [ diff --git a/src/command.ts b/src/command.ts index 22ca181..0044a2f 100644 --- a/src/command.ts +++ b/src/command.ts @@ -11,7 +11,7 @@ import { import { DeepRequired, setOrPush, deepMerge } from './utils' import { MassargExample, ExampleConfig } from './example' -export const CommandConfig = (args: RunArgs) => +export const CommandConfig = (args: z.ZodType) => z.object({ /** Command name */ name: z.string(), @@ -26,16 +26,18 @@ export const CommandConfig = (args: RunArgs) => run: z .function() .args(args, z.any()) - .returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType>>, + .returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType>, }) -export type CommandConfig = z.infer>>> +export type CommandConfig = z.infer< + ReturnType> +> -export type ArgsObject = Record +export type ArgsObject = object -export type Runner = ( - options: A, - instance: MassargCommand, +export type Runner = ( + options: Args, + instance: MassargCommand, ) => Promise | void /** @@ -178,12 +180,18 @@ export class MassargCommand { * value passed to the command. This is useful if you want to parse a string * into a more complex type, or if you want to validate the value. */ - option(config: MassargOption): MassargCommand - option(config: TypedOptionConfig): MassargCommand - option(config: TypedOptionConfig | MassargOption): MassargCommand { + option(config: MassargOption): MassargCommand + option( + config: TypedOptionConfig, + ): MassargCommand + option( + config: TypedOptionConfig | MassargOption, + ): MassargCommand { try { const option = - config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config) + config instanceof MassargOption + ? config + : MassargOption.fromTypedConfig(config as TypedOptionConfig) const existing = this.options.find((c) => c.name === option.name) if (existing) { throw new ValidationError({ @@ -256,7 +264,7 @@ export class MassargCommand { * * If none is provided, help will be printed. */ - main(run: Runner): MassargCommand { + main(run: Runner): MassargCommand { this._run = run return this } @@ -281,7 +289,7 @@ export class MassargCommand { message: 'Unknown option', }) } - const res = option._parseDetails([arg, ...argv]) + const res = option._parseDetails([arg, ...argv], { ...this.args }) this.args[res.key as keyof Args] = setOrPush( res.value, this.args[res.key as keyof Args], @@ -361,7 +369,7 @@ export class MassargCommand { // no sub command found, run main command if (this._run) { - this._run(this.args, parent ?? this) + this._run(this.args as Args, parent ?? this) } } @@ -380,13 +388,15 @@ export class MassargCommand { } } -export class MassargHelpCommand extends MassargCommand { +export class MassargHelpCommand< + T extends { command?: string } = { command?: string }, +> extends MassargCommand { constructor(config: Partial, 'run'>> = {}) { super({ name: 'help', aliases: ['h'], description: 'Print help for this command, or a subcommand if specified', - run: (args, parent) => { + run: (args: { command?: string }, parent) => { if (args.command) { const command = parent.commands.find((c) => c.name === args.command) if (command) { diff --git a/src/option.ts b/src/option.ts index 18760e6..57b13d7 100644 --- a/src/option.ts +++ b/src/option.ts @@ -1,8 +1,11 @@ import { z } from 'zod' import { isZodError, ParseError } from './error' import { toCamelCase } from './utils' +import { ArgsObject } from './command' -export const OptionConfig = (type: T) => +export const OptionConfig = ( + type: z.ZodType, +) => z.object({ /** Name of the option */ name: z.string(), @@ -16,7 +19,9 @@ export const OptionConfig = (type: T) => * Parse the value of the option. You can return any type here, or throw an error if the value * is invalid. */ - parse: z.function().args(z.string()).returns(type).optional(), + parse: z.function().args(z.string(), z.any()).returns(type).optional() as z.ZodOptional< + z.ZodType> + >, /** * Whether the option is an array. * @@ -41,24 +46,33 @@ export const OptionConfig = (type: T) => /** Specify a custom name for the output, which will be used when parsing the args. */ outputName: z.string().optional(), }) -export type OptionConfig = z.infer>>> +export type OptionConfig = z.infer< + ReturnType> +> + +export type Parser = ( + x: string, + y: Args, +) => OptionType -export const TypedOptionConfig = (type: T) => - OptionConfig(type).merge( +export const TypedOptionConfig = ( + type: z.ZodType, +) => + OptionConfig(type).merge( z.object({ type: z.enum(['number']).optional(), }), ) -export type TypedOptionConfig = z.infer< - ReturnType>> +export type TypedOptionConfig = z.infer< + ReturnType> > /** * @see OptionConfig * @see ArrayOptionConfig */ -export const ArrayOptionConfig = (type: T) => - TypedOptionConfig(z.array(type)).merge( +export const ArrayOptionConfig = (type: z.ZodType) => + TypedOptionConfig(z.array(type)).merge( // OptionConfig(z.array(type)).merge( z.object({ defaultValue: z.array(type).optional(), @@ -105,29 +119,31 @@ export type ArgvValue = { argv: string[]; value: T; key: string } * }) * ``` */ -export class MassargOption { +export class MassargOption { name: string description: string - defaultValue?: T + defaultValue?: OptionType aliases: string[] - parse: (value: string) => T + parse: Parser isArray: boolean isDefault: boolean outputName?: string - constructor(options: OptionConfig) { + constructor(options: OptionConfig) { OptionConfig(z.any()).parse(options) this.name = options.name this.description = options.description this.defaultValue = options.defaultValue this.aliases = options.aliases - this.parse = options.parse ?? ((x) => x as unknown as T) + this.parse = options.parse ?? ((x: string) => x as OptionType) this.isArray = options.array ?? false this.isDefault = options.isDefault ?? false this.outputName = options.outputName } - static fromTypedConfig(config: TypedOptionConfig): MassargOption { + static fromTypedConfig( + config: TypedOptionConfig, + ): MassargOption { switch (config.type) { case 'number': return new MassargNumber(config as OptionConfig) as MassargOption @@ -139,7 +155,7 @@ export class MassargOption { return this.outputName || toCamelCase(this.name) } - _parseDetails(argv: string[]): ArgvValue { + _parseDetails(argv: string[], options: ArgsObject): ArgvValue { // TODO: support --option=value let input = '' try { @@ -153,7 +169,7 @@ export class MassargOption { } argv.shift() input = argv.shift()! - const value = this.parse(input) + const value = this.parse(input, options as Args) return { key: this.getOutputName(), value, argv } } catch (e) { if (isZodError(e)) { @@ -243,13 +259,13 @@ export class MassargNumber extends MassargOption { constructor(options: Omit, 'parse'>) { super({ ...options, - parse: (value) => Number(value), + parse: (value) => Number(value) as any, }) } - _parseDetails(argv: string[]): ArgvValue { + _parseDetails(argv: string[], options: ArgsObject): ArgvValue { try { - const { argv: _argv, value } = super._parseDetails(argv) + const { argv: _argv, value } = super._parseDetails(argv, options) if (isNaN(value)) { throw new ParseError({ path: [this.name], @@ -298,7 +314,7 @@ export class MassargFlag extends MassargOption { constructor(options: Omit, 'parse'>) { super({ ...options, - parse: () => true, + parse: () => true as any, }) }