From 022a5310e42a1b7567e040b67f0a380665756d2a Mon Sep 17 00:00:00 2001 From: kricsleo Date: Tue, 8 Apr 2025 19:24:52 +0800 Subject: [PATCH] feat: allow overriding `--version` and `--help` with custom args --- src/command.ts | 28 ++++++++++++- src/main.ts | 18 ++++---- test/main.test.ts | 104 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 9 deletions(-) diff --git a/src/command.ts b/src/command.ts index 4878197..5916c59 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,6 +1,6 @@ import type { CommandContext, CommandDef, ArgsDef } from "./types"; import { CLIError, resolveValue } from "./_utils"; -import { parseArgs } from "./args"; +import { parseArgs, resolveArgs } from "./args"; export function defineCommand( def: CommandDef, @@ -92,3 +92,29 @@ export async function resolveSubCommand( } return [cmd, parent]; } + +export async function extendCmd( + cmd: CommandDef, + rawArgs: string[], + extendedArgs: string[], + extendedCmd: ( + cmd: CommandDef, + parent?: CommandDef, + ) => void | Promise, +) { + const extendedArg = rawArgs.find((arg) => extendedArgs.includes(arg)); + + if (extendedArg) { + const [subCmd, parent] = await resolveSubCommand(cmd, rawArgs); + const cmdArgs = resolveArgs(await resolveValue(subCmd.args || {})); + + const supportedArgs = cmdArgs.flatMap((arg) => [ + `--${arg.name}`, + ...arg.alias.map((a) => `-${a}`), + ]); + + if (!supportedArgs.includes(extendedArg)) { + await extendedCmd(subCmd, parent); + } + } +} diff --git a/src/main.ts b/src/main.ts index e342392..d1e8516 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import consola from "consola"; import type { ArgsDef, CommandDef } from "./types"; -import { resolveSubCommand, runCommand } from "./command"; +import { extendCmd, resolveSubCommand, runCommand } from "./command"; import { CLIError } from "./_utils"; import { showUsage as _showUsage } from "./usage"; @@ -15,20 +15,24 @@ export async function runMain( ) { const rawArgs = opts.rawArgs || process.argv.slice(2); const showUsage = opts.showUsage || _showUsage; + try { - if (rawArgs.includes("--help") || rawArgs.includes("-h")) { - await showUsage(...(await resolveSubCommand(cmd, rawArgs))); + await extendCmd(cmd, rawArgs, ["--help", "-h"], async (cmd, parent) => { + await showUsage(cmd, parent); process.exit(0); - } else if (rawArgs.length === 1 && rawArgs[0] === "--version") { + }); + + await extendCmd(cmd, rawArgs, ["--version", "-v"], async (cmd) => { const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta; if (!meta?.version) { throw new CLIError("No version specified", "E_NO_VERSION"); } consola.log(meta.version); - } else { - await runCommand(cmd, { rawArgs }); - } + process.exit(0); + }); + + await runCommand(cmd, { rawArgs }); } catch (error: any) { const isCLIError = error instanceof CLIError; if (isCLIError) { diff --git a/test/main.test.ts b/test/main.test.ts index 2bbfa41..4992526 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, afterAll } from "vitest"; +import { describe, it, expect, vi, afterAll, beforeEach } from "vitest"; import consola from "consola"; import { createMain, @@ -20,6 +20,11 @@ describe("runMain", () => { .spyOn(consola, "error") .mockImplementation(() => undefined); + beforeEach(() => { + consoleMock.mockClear(); + consolaErrorMock.mockClear(); + }); + afterAll(() => { consoleMock.mockReset(); }); @@ -62,6 +67,54 @@ describe("runMain", () => { expect(consolaErrorMock).toHaveBeenCalledWith("No version specified"); }); + it.each([["--version"], ["-v"]])( + "can override default `%s` behavior", + async (flag) => { + const calls: string[] = []; + const command = defineCommand({ + meta: { + version: "1.0.0", + }, + args: { + version: { + type: "boolean", + description: "Override default `version` behavior", + alias: "v", + }, + }, + subCommands: { + foo: { + args: { + version: { + type: "boolean", + description: "Override subcommand's default `version` behavior", + alias: "v", + }, + }, + run() { + calls.push("SubCommand Overridden"); + }, + }, + }, + run() { + calls.push("TopCommand Overridden"); + }, + }); + + await runMain(command, { rawArgs: [flag] }); + expect(calls).toMatchObject(["TopCommand Overridden"]); + expect(consoleMock).not.toHaveBeenCalledWith("1.0.0"); + + calls.length = 0; + await runMain(command, { rawArgs: ["foo", flag] }); + expect(calls).toMatchObject([ + "SubCommand Overridden", + "TopCommand Overridden", + ]); + expect(consoleMock).not.toHaveBeenCalledWith("1.0.0"); + }, + ); + it.each([["--help"], ["-h"]])("shows usage with flag `%s`", async (flag) => { const command = defineCommand({ meta: { @@ -96,6 +149,55 @@ describe("runMain", () => { }, ); + it.each([["--help"], ["-h"]])( + "can override default `%s` behavior", + async (flag) => { + const calls: string[] = []; + const command = defineCommand({ + meta: { + name: "test", + description: "Test command", + }, + args: { + help: { + type: "boolean", + description: "Override default `help` behavior", + alias: "h", + }, + }, + subCommands: { + foo: { + args: { + help: { + type: "boolean", + description: "Override subcommand's default `help` behavior", + alias: "h", + }, + }, + run() { + calls.push("SubCommand Overridden"); + }, + }, + }, + run() { + calls.push("TopCommand Overridden"); + }, + }); + + await runMain(command, { rawArgs: [flag] }); + expect(calls).toMatchObject(["TopCommand Overridden"]); + expect(consoleMock).not.toHaveBeenCalledWith("Test command"); + + calls.length = 0; + await runMain(command, { rawArgs: ["foo", flag] }); + expect(calls).toMatchObject([ + "SubCommand Overridden", + "TopCommand Overridden", + ]); + expect(consoleMock).not.toHaveBeenCalledWith("Test command"); + }, + ); + it("runs the command", async () => { const mockRunCommand = vi.spyOn(commandModule, "runCommand");