diff --git a/playground/cli.ts b/playground/cli.ts index 159211b..d8c70c1 100644 --- a/playground/cli.ts +++ b/playground/cli.ts @@ -1,6 +1,6 @@ import { defineCommand, runMain } from "../src"; -const main = defineCommand({ +export const main = defineCommand({ meta: { name: "citty", version: "1.0.0", @@ -16,7 +16,12 @@ const main = defineCommand({ build: () => import("./commands/build").then((r) => r.default), deploy: () => import("./commands/deploy").then((r) => r.default), debug: () => import("./commands/debug").then((r) => r.default), + error: () => import("./commands/error").then((r) => r.error), + "error-handled": () => + import("./commands/error").then((r) => r.errorHandled), }, }); -runMain(main); +if (process.env.NODE_ENV !== "test") { + runMain(main); +} diff --git a/playground/commands/error.ts b/playground/commands/error.ts new file mode 100644 index 0000000..db7e1f3 --- /dev/null +++ b/playground/commands/error.ts @@ -0,0 +1,39 @@ +import consola from "consola"; +import { defineCommand } from "../../src"; + +export const error = defineCommand({ + args: { + throwType: { + type: "string", + }, + }, + run({ args }) { + switch (args.throwType) { + case "string": { + console.log("Throw string"); + // we intentionally are throwing something invalid for testing purposes + + throw "Not an error!"; + } + case "empty": { + console.log("Throw undefined"); + // we intentionally are throwing something invalid for testing purposes + + throw undefined; + } + default: { + console.log("Throw Error"); + throw new Error("Error!"); + } + } + }, +}); + +export const errorHandled = defineCommand({ + run() { + throw new Error("intentional error"); + }, + onError(error) { + consola.error(`Caught error: ${error}`); + }, +}); diff --git a/src/command.ts b/src/command.ts index 4878197..25a4c4d 100644 --- a/src/command.ts +++ b/src/command.ts @@ -64,6 +64,18 @@ export async function runCommand( if (typeof cmd.run === "function") { result = await cmd.run(context); } + } catch (originalError) { + const error = + originalError instanceof Error + ? originalError + : new Error((originalError as any) ?? "Unknown Error", { + cause: originalError, + }); + if (typeof cmd.onError === "function") { + await cmd.onError(error, context); + } else { + throw error; + } } finally { if (typeof cmd.cleanup === "function") { await cmd.cleanup(context); diff --git a/src/types.ts b/src/types.ts index e9892f6..3541ffc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,7 +95,7 @@ type ParsedArg = // prettier-ignore export type ParsedArgs = RawArgs & - { [K in keyof T]: ParsedArg; } & + { [K in keyof T]: ParsedArg; } & { [K in keyof T as T[K] extends { alias: string } ? T[K]["alias"] : never]: ParsedArg } & { [K in keyof T as T[K] extends { alias: string[] } ? T[K]["alias"][number] : never]: ParsedArg } & Record; @@ -121,6 +121,7 @@ export type CommandDef = { subCommands?: Resolvable; setup?: (context: CommandContext) => any | Promise; cleanup?: (context: CommandContext) => any | Promise; + onError?: (error: Error, context: CommandContext) => any | Promise; run?: (context: CommandContext) => any | Promise; }; diff --git a/test/error.test.ts b/test/error.test.ts new file mode 100644 index 0000000..1aa2c87 --- /dev/null +++ b/test/error.test.ts @@ -0,0 +1,30 @@ +import { expect, it, describe } from "vitest"; +import { main } from "../playground/cli"; +import { runCommand } from "../src/command"; + +describe("error", () => { + it("should catch thrown errors with onError", () => { + expect(() => + runCommand(main, { rawArgs: ["error-handled"] }), + ).not.toThrowError(); + }); + + it("should still receive an error when a string is thrown from the command", () => + expect( + runCommand(main, { + rawArgs: ["error", "--throwType", "string"], + }), + ).rejects.toThrowError()); + + it("should still receive an error when undefined is thrown from the command", () => + expect( + runCommand(main, { + rawArgs: ["error", "--throwType", "empty"], + }), + ).rejects.toThrowError()); + + it("should not interfere with default error handling when not present", () => + expect(() => + runCommand(main, { rawArgs: ["error"] }), + ).rejects.toBeInstanceOf(Error)); +});