From 020a73fe20bbb44e6478bcdf413a05dd4bce98f5 Mon Sep 17 00:00:00 2001 From: Brian D Date: Thu, 11 Jan 2024 12:12:22 -0600 Subject: [PATCH 1/2] 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/2] 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)); + }); + }); +});