Skip to content

Commit b13308b

Browse files
authored
feat: error handler (#29)
1 parent 6901b6c commit b13308b

File tree

4 files changed

+103
-83
lines changed

4 files changed

+103
-83
lines changed

packages/core/src/cli.ts

Lines changed: 96 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
import { mapParametersToArguments, parseParameters } from "./parameters";
4646
import { locales } from "./locales";
4747

48-
export const Root = Symbol("Root");
48+
export const Root = Symbol.for("Clerc.Root");
4949
export type RootType = typeof Root;
5050

5151
export class Clerc<C extends CommandRecord = {}> {
@@ -57,6 +57,7 @@ export class Clerc<C extends CommandRecord = {}> {
5757
#commandEmitter = new LiteEmit<MakeEventMap<C>>();
5858
#usedNames = new Set<string | RootType>();
5959
#argv: string[] | undefined;
60+
#errorHandlers = [] as ((err: any) => void)[];
6061

6162
#isOtherMethodCalled = false;
6263
#defaultLocale = "en";
@@ -162,7 +163,7 @@ export class Clerc<C extends CommandRecord = {}> {
162163

163164
/**
164165
* Set the Locale
165-
* It's recommended to call this method once after you created the Clerc instance.
166+
* You must call this method once after you created the Clerc instance.
166167
* @param locale
167168
* @returns
168169
* @example
@@ -180,7 +181,7 @@ export class Clerc<C extends CommandRecord = {}> {
180181

181182
/**
182183
* Set the fallback Locale
183-
* It's recommended to call this method once after you created the Clerc instance.
184+
* You must call this method once after you created the Clerc instance.
184185
* @param fallbackLocale
185186
* @returns
186187
* @example
@@ -196,6 +197,21 @@ export class Clerc<C extends CommandRecord = {}> {
196197
return this;
197198
}
198199

200+
/**
201+
* Register a error handler
202+
* @param handler
203+
* @returns
204+
* @example
205+
* ```ts
206+
* Clerc.create()
207+
* .errorHandler((err) => { console.log(err); })
208+
* ```
209+
*/
210+
errorHandler(handler: (err: any) => void) {
211+
this.#errorHandlers.push(handler);
212+
return this;
213+
}
214+
199215
/**
200216
* Register a command
201217
* @param name
@@ -356,87 +372,74 @@ export class Clerc<C extends CommandRecord = {}> {
356372
}
357373
}
358374

359-
/**
360-
* Run matched command
361-
* @returns
362-
* @example
363-
* ```ts
364-
* Clerc.create()
365-
* .parse({ run: false })
366-
* .runMatchedCommand()
367-
* ```
368-
*/
369-
runMatchedCommand() {
375+
#getContext(getCommand: () => ReturnType<typeof resolveCommand>) {
376+
const argv = this.#argv!;
377+
const [command, called] = getCommand();
378+
const isCommandResolved = !!command;
379+
// [...argv] is a workaround since TypeFlag modifies argv
380+
const parsed = typeFlag(command?.flags || {}, [...argv]);
381+
const { _: args, flags, unknownFlags } = parsed;
382+
let parameters = !isCommandResolved || command.name === Root ? args : args.slice(command.name.split(" ").length);
383+
let commandParameters = command?.parameters || [];
384+
// eof handle
385+
const hasEof = commandParameters.indexOf("--");
386+
const eofParameters = commandParameters.slice(hasEof + 1) || [];
387+
const mapping: Record<string, string | string[]> = Object.create(null);
388+
// Support `--` eof parameters
389+
if (hasEof > -1 && eofParameters.length > 0) {
390+
commandParameters = commandParameters.slice(0, hasEof);
391+
const eofArguments = args["--"];
392+
parameters = parameters.slice(0, -eofArguments.length || undefined);
393+
394+
mapParametersToArguments(
395+
mapping,
396+
parseParameters(commandParameters),
397+
parameters,
398+
);
399+
mapParametersToArguments(
400+
mapping,
401+
parseParameters(eofParameters),
402+
eofArguments,
403+
);
404+
} else {
405+
mapParametersToArguments(
406+
mapping,
407+
parseParameters(commandParameters),
408+
parameters,
409+
);
410+
}
411+
const mergedFlags = { ...flags, ...unknownFlags };
412+
const context: InspectorContext | HandlerContext = {
413+
name: command?.name as any,
414+
called: Array.isArray(called) ? called.join(" ") : called,
415+
resolved: isCommandResolved as any,
416+
hasRootOrAlias: this.#hasRootOrAlias,
417+
hasRoot: this.#hasRoot,
418+
raw: { ...parsed, parameters, mergedFlags },
419+
parameters: mapping,
420+
flags,
421+
unknownFlags,
422+
cli: this as any,
423+
};
424+
return context;
425+
}
426+
427+
#runMatchedCommand() {
370428
this.#otherMethodCaled();
371429
const { t } = this.i18n;
372430
const argv = this.#argv;
373431
if (!argv) {
374-
throw new Error("cli.parse() must be called.");
432+
throw new Error(t("core.cliParseMustBeCalled"));
375433
}
376434
const name = resolveParametersBeforeFlag(argv);
377435
const stringName = name.join(" ");
378436
const getCommand = () => resolveCommand(this.#commands, name, t);
379-
const mapErrors = [] as (Error | undefined)[];
380-
const getContext = () => {
381-
mapErrors.length = 0;
382-
const [command, called] = getCommand();
383-
const isCommandResolved = !!command;
384-
// [...argv] is a workaround since TypeFlag modifies argv
385-
const parsed = typeFlag(command?.flags || {}, [...argv]);
386-
const { _: args, flags, unknownFlags } = parsed;
387-
let parameters = !isCommandResolved || command.name === Root ? args : args.slice(command.name.split(" ").length);
388-
let commandParameters = command?.parameters || [];
389-
// eof handle
390-
const hasEof = commandParameters.indexOf("--");
391-
const eofParameters = commandParameters.slice(hasEof + 1) || [];
392-
const mapping: Record<string, string | string[]> = Object.create(null);
393-
// Support `--` eof parameters
394-
if (hasEof > -1 && eofParameters.length > 0) {
395-
commandParameters = commandParameters.slice(0, hasEof);
396-
const eofArguments = args["--"];
397-
parameters = parameters.slice(0, -eofArguments.length || undefined);
398-
399-
mapErrors.push(mapParametersToArguments(
400-
mapping,
401-
parseParameters(commandParameters),
402-
parameters,
403-
));
404-
mapErrors.push(mapParametersToArguments(
405-
mapping,
406-
parseParameters(eofParameters),
407-
eofArguments,
408-
));
409-
} else {
410-
mapErrors.push(mapParametersToArguments(
411-
mapping,
412-
parseParameters(commandParameters),
413-
parameters,
414-
));
415-
}
416-
const mergedFlags = { ...flags, ...unknownFlags };
417-
const context: InspectorContext | HandlerContext = {
418-
name: command?.name as any,
419-
called: Array.isArray(called) ? called.join(" ") : called,
420-
resolved: isCommandResolved as any,
421-
hasRootOrAlias: this.#hasRootOrAlias,
422-
hasRoot: this.#hasRoot,
423-
raw: { ...parsed, parameters, mergedFlags },
424-
parameters: mapping,
425-
flags,
426-
unknownFlags,
427-
cli: this as any,
428-
};
429-
return context;
430-
};
437+
const getContext = () => this.#getContext(getCommand);
431438
const emitHandler: Inspector = {
432439
enforce: "post",
433440
fn: () => {
434441
const [command] = getCommand();
435442
const handlerContext = getContext();
436-
const errors = mapErrors.filter(Boolean) as Error[];
437-
if (errors.length > 0) {
438-
throw errors[0];
439-
}
440443
if (!command) {
441444
if (stringName) {
442445
throw new NoSuchCommandError(stringName, t);
@@ -450,6 +453,28 @@ export class Clerc<C extends CommandRecord = {}> {
450453
const inspectors = [...this.#inspectors, emitHandler];
451454
const callInspector = compose(inspectors);
452455
callInspector(getContext);
456+
}
457+
458+
/**
459+
* Run matched command
460+
* @returns
461+
* @example
462+
* ```ts
463+
* Clerc.create()
464+
* .parse({ run: false })
465+
* .runMatchedCommand()
466+
* ```
467+
*/
468+
runMatchedCommand() {
469+
try {
470+
this.#runMatchedCommand();
471+
} catch (e) {
472+
if (this.#errorHandlers.length > 0) {
473+
this.#errorHandlers.forEach(cb => cb(e));
474+
} else {
475+
throw e;
476+
}
477+
}
453478
return this;
454479
}
455480
}

packages/core/src/locales.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const locales: Locales = {
1111
"core.versionNotSet": "Version not set.",
1212
"core.badNameFormat": "Bad name format: %s.",
1313
"core.localeMustBeCalledFirst": "locale() or fallbackLocale() must be called at first.",
14+
"core.cliParseMustBeCalled": "cli.parse() must be called.",
1415
},
1516
"zh-CN": {
1617
"core.commandExists": "命令 \"%s\" 已存在。",
@@ -22,5 +23,6 @@ export const locales: Locales = {
2223
"core.versionNotSet": "未设置CLI版本。",
2324
"core.badNameFormat": "错误的命令名字格式: %s。",
2425
"core.localeMustBeCalledFirst": "locale() 或 fallbackLocale() 必须在最开始调用。",
26+
"core.cliParseMustBeCalled": "cli.parse() 必须被调用。",
2527
},
2628
};

packages/core/src/parameters.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function mapParametersToArguments(
7070
const { name, required, spread } = parameters[i];
7171
const camelCaseName = camelCase(name);
7272
if (camelCaseName in mapping) {
73-
return new Error(`Invalid parameter: ${stringify(name)} is used more than once.`);
73+
throw new Error(`Invalid parameter: ${stringify(name)} is used more than once.`);
7474
}
7575

7676
const value = spread ? cliArguments.slice(i) : cliArguments[i];
@@ -83,7 +83,7 @@ export function mapParametersToArguments(
8383
required
8484
&& (!value || (spread && value.length === 0))
8585
) {
86-
return new Error(`Error: Missing required parameter ${stringify(name)}`);
86+
throw new Error(`Missing required parameter ${stringify(name)}`);
8787
}
8888

8989
mapping[camelCaseName] = value;

packages/plugin-friendly-error/src/index.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,9 @@ import * as kons from "kons";
55

66
export const friendlyErrorPlugin = () => definePlugin({
77
setup: (cli) => {
8-
return cli.inspector({
9-
enforce: "pre",
10-
fn: (_ctx, next) => {
11-
try {
12-
next();
13-
} catch (e: any) {
14-
kons.error(e.message);
15-
process.exit(1);
16-
}
17-
},
8+
return cli.errorHandler((err) => {
9+
kons.error(err.message);
10+
process.exit(1);
1811
});
1912
},
2013
});

0 commit comments

Comments
 (0)