Skip to content

Commit f07e321

Browse files
committed
feat(plugin-help): throw errors when command not found and let other handlers handle it
1 parent 72efdea commit f07e321

7 files changed

Lines changed: 95 additions & 33 deletions

File tree

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,49 @@
11
import { TestBaseCli } from "@clerc/test-utils";
2-
import { beforeAll, describe, expect, it, vi } from "vitest";
2+
import * as kons from "kons";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
34

45
import { friendlyErrorPlugin } from "../src";
56

7+
vi.mock("kons", () => ({
8+
error: vi.fn(),
9+
}));
10+
611
describe("plugin-friendly-error", () => {
7-
beforeAll(() => {
8-
vi.spyOn(process, "exit").mockImplementation(() => ({}) as never);
12+
vi.spyOn(process, "exit").mockImplementation(() => ({}) as never);
13+
const spy = vi.spyOn(kons, "error").mockImplementation(() => {});
14+
vi.mocked(kons.error).mockImplementation(() => {});
15+
16+
afterEach(() => {
17+
spy.mockClear();
918
});
1019

1120
it("should catch error", async () => {
12-
TestBaseCli()
13-
.use(
14-
friendlyErrorPlugin({
15-
target: (s) => {
16-
expect(s).toMatchInlineSnapshot(`"No such command: "foo"."`);
17-
},
18-
}),
19-
)
20-
.parse(["foo"]);
21+
await TestBaseCli().use(friendlyErrorPlugin()).parse(["foo"]);
22+
23+
expect(spy.mock.calls).toMatchInlineSnapshot(`
24+
[
25+
[
26+
"No such command: "foo".",
27+
],
28+
]
29+
`);
2130
});
2231

23-
it("should catch async error", () => {
24-
TestBaseCli()
25-
.use(
26-
friendlyErrorPlugin({
27-
target: (s) => expect(s).toMatchInlineSnapshot(`"foo error"`),
28-
}),
29-
)
32+
it("should catch async error", async () => {
33+
await TestBaseCli()
34+
.use(friendlyErrorPlugin())
3035
.command("foo", "foo command")
3136
.on("foo", async () => {
3237
throw new Error("foo error");
3338
})
3439
.parse(["foo"]);
40+
41+
expect(spy.mock.calls).toMatchInlineSnapshot(`
42+
[
43+
[
44+
"foo error",
45+
],
46+
]
47+
`);
3548
});
3649
});

packages/plugin-help/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"devDependencies": {
5454
"@clerc/core": "workspace:*",
5555
"@clerc/parser": "workspace:*",
56-
"@clerc/utils": "workspace:*"
56+
"@clerc/utils": "workspace:*",
57+
"kons": "^0.7.1"
5758
},
5859
"peerDependencies": {
5960
"@clerc/core": "*"

packages/plugin-help/src/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Plugin } from "@clerc/core";
2-
import { definePlugin, resolveCommand } from "@clerc/core";
2+
import { NoSuchCommandError, definePlugin, resolveCommand } from "@clerc/core";
33
import { isTruthy } from "@clerc/utils";
44

55
import { defaultFormatters } from "./formatters";
@@ -155,9 +155,7 @@ export const helpPlugin = ({
155155
[command] = resolveCommand(cli._commands, commandName);
156156

157157
if (!command) {
158-
console.error(`Command "${commandName.join(" ")}" not found.`);
159-
160-
return;
158+
throw new NoSuchCommandError(commandName.join(" "));
161159
}
162160
}
163161

@@ -183,16 +181,22 @@ export const helpPlugin = ({
183181
}
184182

185183
cli.interceptor({
186-
enforce: "pre",
184+
enforce: "post",
187185
handler: async (ctx, next) => {
188186
if (ctx.flags.help) {
187+
const command = ctx.command;
188+
// If no command resolved, but parameters are present, just let the next interceptor handle it
189+
if (!command && ctx.rawParsed.parameters.length > 0) {
190+
await next();
191+
}
192+
189193
const renderer = new HelpRenderer(
190194
mergedFormatters,
191195
cli,
192196
cli._globalFlags,
193-
ctx.command,
194-
ctx.command ? ctx.command.help?.notes : effectiveNotes,
195-
ctx.command ? ctx.command.help?.examples : effectiveExamples,
197+
command,
198+
command ? command.help?.notes : effectiveNotes,
199+
command ? command.help?.examples : effectiveExamples,
196200
groups,
197201
);
198202
printHelp(renderer.render());

packages/plugin-help/test/__snapshots__/plugin-help.test.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,11 @@ exports[`plugin-help > should use [] as placeholder when root command exists 1`]
279279
],
280280
]
281281
`;
282+
283+
exports[`plugin-help > should work with friendly-error 1`] = `
284+
[
285+
[
286+
"No such command: "not-exist".",
287+
],
288+
]
289+
`;

packages/plugin-help/test/plugin-help.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import { TestBaseCli, getConsoleMock } from "@clerc/test-utils";
2-
import { afterAll, afterEach, describe, expect, it } from "vitest";
2+
import { NoSuchCommandError, friendlyErrorPlugin } from "clerc";
3+
import * as kons from "kons";
4+
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
35
import { mockConsole } from "vitest-console";
46

57
import { helpPlugin } from "../src";
68

9+
vi.mock("kons", () => ({
10+
error: vi.fn(),
11+
}));
12+
713
describe("plugin-help", () => {
14+
vi.spyOn(process, "exit").mockImplementation(() => ({}) as never);
15+
const spy = vi.spyOn(kons, "error").mockImplementation(() => {});
16+
vi.mocked(kons.error).mockImplementation(() => {});
17+
18+
afterEach(() => {
19+
spy.mockClear();
20+
});
21+
822
const { clearConsole, restoreConsole } = mockConsole({ quiet: true });
923

1024
afterEach(clearConsole);
@@ -182,4 +196,25 @@ describe("plugin-help", () => {
182196
expect(getConsoleMock("log").mock.calls).toMatchSnapshot();
183197
});
184198
});
199+
200+
it("should throw error when command not found", async () => {
201+
await expect(async () => {
202+
await TestBaseCli().use(helpPlugin()).parse(["not-exist", "--help"]);
203+
}).rejects.toThrow(NoSuchCommandError);
204+
205+
await expect(async () => {
206+
await TestBaseCli().use(helpPlugin()).parse(["help", "not-exist"]);
207+
}).rejects.toThrow(NoSuchCommandError);
208+
});
209+
210+
it("should work with friendly-error", async () => {
211+
expect(async () => {
212+
await TestBaseCli()
213+
.use(helpPlugin())
214+
.use(friendlyErrorPlugin())
215+
.parse(["not-exist", "--help"]);
216+
217+
expect(spy.mock.calls).toMatchSnapshot();
218+
}).not.toThrow();
219+
});
185220
});

packages/plugin-not-found/test/plugin-not-found.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { TestBaseCli } from "@clerc/test-utils";
2-
import { beforeAll, describe, expect, it, vi } from "vitest";
2+
import { describe, expect, it, vi } from "vitest";
33

44
import { notFoundPlugin } from "../src";
55

66
describe("plugin-not-found", () => {
7-
beforeAll(() => {
8-
vi.spyOn(process, "exit").mockImplementation(() => ({}) as never);
9-
});
7+
vi.spyOn(process, "exit").mockImplementation(() => ({}) as never);
108

119
it("should show commands", async () => {
1210
await expect(async () => {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)