Skip to content

feat: onError support #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions playground/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineCommand, runMain } from "../src";

const main = defineCommand({
export const main = defineCommand({
meta: {
name: "citty",
version: "1.0.0",
Expand All @@ -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);
}
39 changes: 39 additions & 0 deletions playground/commands/error.ts
Original file line number Diff line number Diff line change
@@ -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}`);
},
});
12 changes: 12 additions & 0 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ export async function runCommand<T extends ArgsDef = ArgsDef>(
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);
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type ParsedArg<T extends ArgDef> =

// prettier-ignore
export type ParsedArgs<T extends ArgsDef = ArgsDef> = RawArgs &
{ [K in keyof T]: ParsedArg<T[K]>; } &
{ [K in keyof T]: ParsedArg<T[K]>; } &
{ [K in keyof T as T[K] extends { alias: string } ? T[K]["alias"] : never]: ParsedArg<T[K]> } &
{ [K in keyof T as T[K] extends { alias: string[] } ? T[K]["alias"][number] : never]: ParsedArg<T[K]> } &
Record<string, string | number | boolean | string[]>;
Expand All @@ -121,6 +121,7 @@ export type CommandDef<T extends ArgsDef = ArgsDef> = {
subCommands?: Resolvable<SubCommandsDef>;
setup?: (context: CommandContext<T>) => any | Promise<any>;
cleanup?: (context: CommandContext<T>) => any | Promise<any>;
onError?: (error: Error, context: CommandContext<T>) => any | Promise<any>;
run?: (context: CommandContext<T>) => any | Promise<any>;
};

Expand Down
30 changes: 30 additions & 0 deletions test/error.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
Loading