From 020a73fe20bbb44e6478bcdf413a05dd4bce98f5 Mon Sep 17 00:00:00 2001 From: Brian D Date: Thu, 11 Jan 2024 12:12:22 -0600 Subject: [PATCH 1/5] chore: prevent process exit and command run when in tests --- package.json | 1 + playground/cli.ts | 4 +++- src/main.ts | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc44497..ad88bf1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dev": "vitest dev", "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test", "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w", + "format": "prettier -c src test -w", "prepack": "pnpm run build", "play": "jiti ./playground/cli.ts", "release": "pnpm test && changelogen --release --push && npm publish", diff --git a/playground/cli.ts b/playground/cli.ts index fa406b3..bce594e 100644 --- a/playground/cli.ts +++ b/playground/cli.ts @@ -18,4 +18,6 @@ const main = defineCommand({ }, }); -runMain(main); +if (process.env.NODE_ENV !== "test") { + runMain(main); +} diff --git a/src/main.ts b/src/main.ts index 49cb953..75ea7d9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,9 @@ export async function runMain( await showUsage(...(await resolveSubCommand(cmd, rawArgs))); } consola.error(error.message); - process.exit(1); + if (process.env.NODE_ENV !== "test") { + process.exit(1); + } } } From aa84d6ca3fe64dd6e569201f7b165ab5bdeda2ee Mon Sep 17 00:00:00 2001 From: Brian D Date: Thu, 11 Jan 2024 12:12:34 -0600 Subject: [PATCH 2/5] feat: add .catch to command interface --- playground/cli.ts | 5 +++- playground/commands/error-no-catch.ts | 36 +++++++++++++++++++++++++++ playground/commands/error.ts | 19 ++++++++++++++ src/command.ts | 11 ++++++++ src/types.ts | 1 + test/error.test.ts | 36 +++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 playground/commands/error-no-catch.ts create mode 100644 playground/commands/error.ts create mode 100644 test/error.test.ts diff --git a/playground/cli.ts b/playground/cli.ts index bce594e..ebc4123 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", @@ -15,6 +15,9 @@ const main = defineCommand({ subCommands: { build: () => import("./commands/build").then((r) => r.default), deploy: () => import("./commands/deploy").then((r) => r.default), + error: () => import("./commands/error").then((r) => r.default), + "error-no-catch": () => + import("./commands/error-no-catch").then((r) => r.default), }, }); diff --git a/playground/commands/error-no-catch.ts b/playground/commands/error-no-catch.ts new file mode 100644 index 0000000..2423514 --- /dev/null +++ b/playground/commands/error-no-catch.ts @@ -0,0 +1,36 @@ +import { defineCommand } from "../../src"; + +export default defineCommand({ + meta: { + name: "error-no-catch", + description: + "Throws an error to test .catch functionality, does not have error handling", + }, + + args: { + throwType: { + type: "string", + }, + }, + + run({ args }) { + switch (args.throwType) { + case "string": { + console.log("Throw string"); + // we intentionally are throwing something invalid for testing purposes + // eslint-disable-next-line no-throw-literal + throw "Not an error!"; + } + case "empty": { + console.log("Throw undefined"); + // we intentionally are throwing something invalid for testing purposes + // eslint-disable-next-line no-throw-literal + throw undefined; + } + default: { + console.log("Throw Error"); + throw new Error("Error!"); + } + } + }, +}); diff --git a/playground/commands/error.ts b/playground/commands/error.ts new file mode 100644 index 0000000..7b98f67 --- /dev/null +++ b/playground/commands/error.ts @@ -0,0 +1,19 @@ +import consola from "consola"; +import { defineCommand } from "../../src"; + +export default defineCommand({ + meta: { + name: "error", + description: "Throws an error to test .catch functionality", + }, + + run() { + throw new Error("Hello World"); + }, + catch(_, e) { + consola.error(`Caught error: ${e}`); + if (!(e instanceof Error)) { + throw new TypeError("Recieved non-error value"); + } + }, +}); diff --git a/src/command.ts b/src/command.ts index 1abfe44..1a284a5 100644 --- a/src/command.ts +++ b/src/command.ts @@ -64,6 +64,17 @@ export async function runCommand( if (typeof cmd.run === "function") { result = await cmd.run(context); } + } catch (error_) { + const error = + error_ instanceof Error + ? error_ + : new Error(error_?.toString() ?? "Unknown Error", { cause: error_ }); + if (typeof cmd.catch === "function") { + // Attempt to coerce e into an error to ensure type safety + await cmd.catch(context, error); + } else { + throw error; + } } finally { if (typeof cmd.cleanup === "function") { await cmd.cleanup(context); diff --git a/src/types.ts b/src/types.ts index cd71977..239271f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,7 @@ export type CommandDef = { subCommands?: Resolvable; setup?: (context: CommandContext) => any | Promise; cleanup?: (context: CommandContext) => any | Promise; + catch?: (context: CommandContext, e: Error) => 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..7f3f38a --- /dev/null +++ b/test/error.test.ts @@ -0,0 +1,36 @@ +import { expect, it, describe } from "vitest"; +import { main } from "../playground/cli"; +import { runCommand } from "../src/command"; + +describe("citty", () => { + it.todo("pass", () => { + expect(true).toBe(true); + }); + + describe("commands", () => { + describe("error", () => { + it("should catch thrown errors when present", () => { + expect(() => + runCommand(main, { rawArgs: ["error"] }), + ).not.toThrowError(); + }); + it("should still recieve an error when a string is thrown from the command", () => + expect( + runCommand(main, { + rawArgs: ["error-no-catch", "--throwType", "string"], + }), + ).rejects.toThrowError()); + it("should still recieve an error when undefined is thrown from the command", () => + expect( + runCommand(main, { + rawArgs: ["error-no-catch", "--throwType", "empty"], + }), + ).rejects.toThrowError()); + + it("should not interfere with default error handling when not present", () => + expect(() => + runCommand(main, { rawArgs: ["error-no-catch"] }), + ).rejects.toBeInstanceOf(Error)); + }); + }); +}); From ebe8fb30b413312041ab98c34b6052d469016325 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 8 Mar 2025 22:06:41 +0100 Subject: [PATCH 3/5] update --- playground/cli.ts | 6 ++-- playground/commands/error-no-catch.ts | 36 ------------------- playground/commands/error.ts | 40 +++++++++++++++------ src/command.ts | 5 ++- src/types.ts | 4 +-- test/error.test.ts | 50 ++++++++++++--------------- 6 files changed, 59 insertions(+), 82 deletions(-) delete mode 100644 playground/commands/error-no-catch.ts diff --git a/playground/cli.ts b/playground/cli.ts index 7c7525c..d8c70c1 100644 --- a/playground/cli.ts +++ b/playground/cli.ts @@ -15,10 +15,10 @@ export const main = defineCommand({ subCommands: { build: () => import("./commands/build").then((r) => r.default), deploy: () => import("./commands/deploy").then((r) => r.default), - error: () => import("./commands/error").then((r) => r.default), - "error-no-catch": () => - import("./commands/error-no-catch").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), }, }); diff --git a/playground/commands/error-no-catch.ts b/playground/commands/error-no-catch.ts deleted file mode 100644 index 2423514..0000000 --- a/playground/commands/error-no-catch.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { defineCommand } from "../../src"; - -export default defineCommand({ - meta: { - name: "error-no-catch", - description: - "Throws an error to test .catch functionality, does not have error handling", - }, - - args: { - throwType: { - type: "string", - }, - }, - - run({ args }) { - switch (args.throwType) { - case "string": { - console.log("Throw string"); - // we intentionally are throwing something invalid for testing purposes - // eslint-disable-next-line no-throw-literal - throw "Not an error!"; - } - case "empty": { - console.log("Throw undefined"); - // we intentionally are throwing something invalid for testing purposes - // eslint-disable-next-line no-throw-literal - throw undefined; - } - default: { - console.log("Throw Error"); - throw new Error("Error!"); - } - } - }, -}); diff --git a/playground/commands/error.ts b/playground/commands/error.ts index 7b98f67..db7e1f3 100644 --- a/playground/commands/error.ts +++ b/playground/commands/error.ts @@ -1,19 +1,39 @@ import consola from "consola"; import { defineCommand } from "../../src"; -export default defineCommand({ - meta: { - name: "error", - description: "Throws an error to test .catch functionality", +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("Hello World"); + throw new Error("intentional error"); }, - catch(_, e) { - consola.error(`Caught error: ${e}`); - if (!(e instanceof Error)) { - throw new TypeError("Recieved non-error value"); - } + onError(error) { + consola.error(`Caught error: ${error}`); }, }); diff --git a/src/command.ts b/src/command.ts index 8864edc..573245e 100644 --- a/src/command.ts +++ b/src/command.ts @@ -69,9 +69,8 @@ export async function runCommand( error_ instanceof Error ? error_ : new Error(error_?.toString() ?? "Unknown Error", { cause: error_ }); - if (typeof cmd.catch === "function") { - // Attempt to coerce e into an error to ensure type safety - await cmd.catch(context, error); + if (typeof cmd.onError === "function") { + await cmd.onError(error, context); } else { throw error; } diff --git a/src/types.ts b/src/types.ts index 6fc6056..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,7 +121,7 @@ export type CommandDef = { subCommands?: Resolvable; setup?: (context: CommandContext) => any | Promise; cleanup?: (context: CommandContext) => any | Promise; - catch?: (context: CommandContext, e: Error) => 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 index a19d018..1aa2c87 100644 --- a/test/error.test.ts +++ b/test/error.test.ts @@ -2,35 +2,29 @@ import { expect, it, describe } from "vitest"; import { main } from "../playground/cli"; import { runCommand } from "../src/command"; -describe("citty", () => { - it.todo("pass", () => { - expect(true).toBe(true); +describe("error", () => { + it("should catch thrown errors with onError", () => { + expect(() => + runCommand(main, { rawArgs: ["error-handled"] }), + ).not.toThrowError(); }); - describe("commands", () => { - describe("error", () => { - it("should catch thrown errors when present", () => { - expect(() => - runCommand(main, { rawArgs: ["error"] }), - ).not.toThrowError(); - }); - it("should still receive an error when a string is thrown from the command", () => - expect( - runCommand(main, { - rawArgs: ["error-no-catch", "--throwType", "string"], - }), - ).rejects.toThrowError()); - it("should still receive an error when undefined is thrown from the command", () => - expect( - runCommand(main, { - rawArgs: ["error-no-catch", "--throwType", "empty"], - }), - ).rejects.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 not interfere with default error handling when not present", () => - expect(() => - runCommand(main, { rawArgs: ["error-no-catch"] }), - ).rejects.toBeInstanceOf(Error)); - }); - }); + 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)); }); From d7d693dc45aadaf1e644e520ddd4f0f20f1a2185 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 8 Mar 2025 22:09:30 +0100 Subject: [PATCH 4/5] revert package.json --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index caa30dd..9a87ec0 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,8 @@ "scripts": { "build": "unbuild", "dev": "vitest dev", - "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test", - "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w", - "format": "prettier -c src test -w", + "lint": "eslint --cache . && prettier -c src test", + "lint:fix": "eslint --cache . --fix && prettier -c src test -w", "prepack": "pnpm run build", "play": "jiti ./playground/cli.ts", "release": "pnpm test && changelogen --release --push && npm publish", @@ -48,4 +47,4 @@ "vitest": "^3.0.8" }, "packageManager": "pnpm@10.6.1" -} +} \ No newline at end of file From a6d0472557a108e2184411d93481aa19215d8c02 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 8 Mar 2025 22:11:03 +0100 Subject: [PATCH 5/5] update --- src/command.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/command.ts b/src/command.ts index 573245e..25a4c4d 100644 --- a/src/command.ts +++ b/src/command.ts @@ -64,11 +64,13 @@ export async function runCommand( if (typeof cmd.run === "function") { result = await cmd.run(context); } - } catch (error_) { + } catch (originalError) { const error = - error_ instanceof Error - ? error_ - : new Error(error_?.toString() ?? "Unknown Error", { cause: 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 {