diff --git a/src/command.ts b/src/command.ts index 4878197..ae2d459 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,6 +1,7 @@ import type { CommandContext, CommandDef, ArgsDef } from "./types"; import { CLIError, resolveValue } from "./_utils"; import { parseArgs } from "./args"; +import { validateUnknownOptions } from "./validate"; export function defineCommand( def: CommandDef, @@ -18,9 +19,14 @@ export async function runCommand( cmd: CommandDef, opts: RunCommandOptions, ): Promise<{ result: unknown }> { + const cmdMeta = await resolveValue(cmd.meta); const cmdArgs = await resolveValue(cmd.args || {}); const parsedArgs = parseArgs(opts.rawArgs, cmdArgs); + if (!cmdMeta || !cmdMeta.allowUnknownOptions) { + validateUnknownOptions(cmdArgs, parsedArgs); + } + const context: CommandContext = { rawArgs: opts.rawArgs, args: parsedArgs, diff --git a/src/types.ts b/src/types.ts index e9892f6..a1887be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,6 +109,7 @@ export interface CommandMeta { version?: string; description?: string; hidden?: boolean; + allowUnknownOptions?: boolean; } // Command: Definition diff --git a/src/validate.ts b/src/validate.ts new file mode 100644 index 0000000..927bf8f --- /dev/null +++ b/src/validate.ts @@ -0,0 +1,23 @@ +import { CLIError, toArray } from "./_utils"; +import { ArgsDef, ParsedArgs } from "./types"; + +export function validateUnknownOptions( + argsDef: T, + args: ParsedArgs, +): void { + const names: string[] = []; + for (const [name, argDef] of Object.entries(argsDef || {})) { + names.push(name, ...toArray((argDef as any).alias)); + } + + for (const arg in args) { + if (arg === "_") continue; + + if (!names.includes(arg)) { + throw new CLIError( + `Unknown option \`${arg.length > 1 ? `--${arg}` : `-${arg}`}\``, + "E_UNKNOWN_OPTION", + ); + } + } +} diff --git a/test/main.test.ts b/test/main.test.ts index 2bbfa41..8a7c484 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, afterEach } from "vitest"; import consola from "consola"; import { createMain, @@ -20,8 +20,9 @@ describe("runMain", () => { .spyOn(consola, "error") .mockImplementation(() => undefined); - afterAll(() => { + afterEach(() => { consoleMock.mockReset(); + consolaErrorMock.mockReset(); }); it("shows version with flag `--version`", async () => { @@ -116,6 +117,22 @@ describe("runMain", () => { await runMain(command, { rawArgs }); expect(mockRunCommand).toHaveBeenCalledWith(command, { rawArgs }); + expect(consolaErrorMock).toHaveBeenCalledWith("Unknown option `--foo`"); + }); + + it("should allow unknown options with `allowUnknownOptions` enabled", async () => { + const mockRunCommand = vi.spyOn(commandModule, "runCommand"); + + const command = defineCommand({ + meta: { allowUnknownOptions: true }, + }); + + const rawArgs = ["--foo", "bar"]; + + await runMain(command, { rawArgs }); + + expect(mockRunCommand).toHaveBeenCalledWith(command, { rawArgs }); + expect(consolaErrorMock).not.toHaveBeenCalledWith("Unknown option `--foo`"); }); });