Skip to content

Commit cf99055

Browse files
authored
fix: proper error handling in CLI (#187)
* fix: proper error handling in CLI * test: add CLI error handling tests
1 parent c59e569 commit cf99055

3 files changed

Lines changed: 84 additions & 8 deletions

File tree

packages/cli/src/brownfield/commands/packageIos.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const packageIosCommand = curryOptions(
4747

4848
options.buildFolder ??= path.join(brownieCacheDir, 'build');
4949

50-
packageIosAction(
50+
await packageIosAction(
5151
options,
5252
{
5353
projectRoot,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as rockTools from '@rock-js/tools';
2+
3+
import { expect, Mock, test, vi } from 'vitest';
4+
5+
import { actionRunner } from '../cli.js';
6+
7+
vi.mock('@rock-js/tools', async (importOriginal) => {
8+
const actual = await importOriginal<typeof rockTools>();
9+
return {
10+
...actual,
11+
logger: {
12+
...actual.logger,
13+
error: vi.fn(),
14+
warn: vi.fn(),
15+
info: vi.fn(),
16+
success: vi.fn(),
17+
},
18+
};
19+
});
20+
21+
// @ts-expect-error - override typings
22+
const processExitMock = vi.spyOn(process, 'exit').mockImplementation(() => {
23+
// no-op
24+
});
25+
26+
const mockLoggerError = rockTools.logger.error as Mock;
27+
28+
const FAILING_ACTION_ERROR_MESSAGE = 'Test error';
29+
30+
const createWrappedFailingAction = (ErrorCls: new (message: string) => Error) =>
31+
actionRunner(async (_a: number, _b: number) => {
32+
throw new ErrorCls(FAILING_ACTION_ERROR_MESSAGE);
33+
});
34+
35+
test('actionRunner should call the wrapped function', async () => {
36+
const mockAction = vi.fn(async () => Promise.resolve());
37+
const wrappedAction = actionRunner(mockAction);
38+
39+
await wrappedAction();
40+
41+
expect(mockAction).toHaveBeenCalledOnce();
42+
});
43+
44+
test('actionRunner should gracefully handle Errors', async () => {
45+
const wrappedActionExpectation = expect(
46+
createWrappedFailingAction(Error)(1, 2)
47+
);
48+
49+
await wrappedActionExpectation.resolves.not.toThrowError();
50+
expect(processExitMock).toHaveBeenCalledExactlyOnceWith(1);
51+
expect(mockLoggerError).toHaveBeenCalled();
52+
});
53+
54+
test('actionRunner should gracefully handle RockErrors', async () => {
55+
const wrappedActionExpectation = expect(
56+
createWrappedFailingAction(rockTools.RockError)(1, 2)
57+
);
58+
59+
await wrappedActionExpectation.resolves.not.toThrowError();
60+
expect(processExitMock).toHaveBeenCalledExactlyOnceWith(1);
61+
expect(mockLoggerError).toHaveBeenCalled();
62+
});

packages/cli/src/shared/utils/cli.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { logger, type RockCLIOptions } from '@rock-js/tools';
1+
import { logger, RockError, type RockCLIOptions } from '@rock-js/tools';
22

33
import type { Command } from 'commander';
44

@@ -23,11 +23,25 @@ export function curryOptions(programCommand: Command, options: RockCLIOptions) {
2323
return programCommand;
2424
}
2525

26-
function handleActionError(error: Error) {
27-
logger.error(`Error running command: ${error.message}`);
28-
process.exit(1);
29-
}
30-
3126
export function actionRunner<T, R>(fn: (...args: T[]) => Promise<R>) {
32-
return (...args: T[]) => fn(...args).catch(handleActionError);
27+
return async function wrappedCLIAction(...args: T[]) {
28+
try {
29+
await fn(...args);
30+
} catch (error) {
31+
if (error instanceof RockError) {
32+
if (logger.isVerbose()) {
33+
logger.error(error);
34+
} else {
35+
logger.error(error.message);
36+
if (error.cause) {
37+
logger.error(`Cause: ${error.cause}`);
38+
}
39+
}
40+
} else {
41+
logger.error(`Unexpected error while running command:`, error);
42+
}
43+
44+
process.exit(1);
45+
}
46+
};
3347
}

0 commit comments

Comments
 (0)