Skip to content

Commit 4772ca4

Browse files
authored
✨feat: custom help rendering (#32)
1 parent dcf0389 commit 4772ca4

File tree

8 files changed

+59
-57
lines changed

8 files changed

+59
-57
lines changed

packages/plugin-help/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"@clerc/core": "workspace:*",
5959
"@clerc/utils": "workspace:*",
6060
"@types/text-table": "^0.2.2",
61-
"get-func-name": "^2.0.0",
6261
"picocolors": "^1.0.0",
6362
"text-table": "^0.2.0"
6463
}

packages/plugin-help/src/index.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11

22
// TODO: unit tests
3-
43
import type { Clerc, Command, HandlerContext, RootType, TranslateFn } from "@clerc/core";
54
import { NoSuchCommandError, Root, definePlugin, formatCommandName, resolveCommandStrict, withBrackets } from "@clerc/core";
65

76
import { gracefulFlagName, toArray } from "@clerc/utils";
87
import pc from "picocolors";
98

10-
import type { Render, Section } from "./renderer";
11-
import { renderCliffy } from "./renderer";
12-
import { splitTable, stringifyType } from "./utils";
9+
import type { Render, Renderers, Section } from "./renderer";
10+
import { defaultRenderers, render } from "./renderer";
11+
import { splitTable } from "./utils";
1312
import { locales } from "./locales";
1413

14+
declare module "@clerc/core" {
15+
export interface CommandCustomProperties {
16+
help?: Renderers
17+
}
18+
}
19+
1520
const DELIMITER = pc.yellow("-");
1621

1722
const print = (s: string) => { process.stdout.write(s); };
@@ -48,7 +53,7 @@ const generateExamples = (sections: Section[], examples: [string, string][], t:
4853
const examplesFormatted = examples.map(([command, description]) => [command, DELIMITER, description]);
4954
sections.push({
5055
title: t("help.examples")!,
51-
body: splitTable(...examplesFormatted),
56+
body: splitTable(examplesFormatted),
5257
});
5358
};
5459

@@ -76,7 +81,7 @@ const generateHelp = (render: Render, ctx: HandlerContext, notes: string[] | und
7681
});
7782
sections.push({
7883
title: t("help.commands")!,
79-
body: splitTable(...commands),
84+
body: splitTable(commands),
8085
});
8186
if (notes) {
8287
sections.push({
@@ -97,7 +102,8 @@ const generateSubcommandHelp = (render: Render, ctx: HandlerContext, command: st
97102
if (!subcommand) {
98103
throw new NoSuchCommandError(formatCommandName(command), t);
99104
}
100-
const sections = [] as Section[];
105+
const renderers = Object.assign({}, defaultRenderers, subcommand.help);
106+
let sections = [] as Section[];
101107
if (command === Root) {
102108
generateCliDetail(sections, cli);
103109
} else {
@@ -118,16 +124,17 @@ const generateSubcommandHelp = (render: Render, ctx: HandlerContext, command: st
118124
sections.push({
119125
title: t("help.flags")!,
120126
body: splitTable(
121-
...Object.entries(subcommand.flags).map(([name, flag]) => {
122-
const flagNameWithAlias = [gracefulFlagName(name)];
127+
Object.entries(subcommand.flags).map(([name, flag]) => {
128+
const hasDefault = flag.default !== undefined;
129+
let flagNameWithAlias: string[] = [gracefulFlagName(name)];
123130
if (flag.alias) {
124131
flagNameWithAlias.push(gracefulFlagName(flag.alias));
125132
}
126-
const items = [pc.blue(flagNameWithAlias.join(", "))];
133+
flagNameWithAlias = flagNameWithAlias.map(renderers.renderFlagName);
134+
const items = [pc.blue(flagNameWithAlias.join(", ")), renderers.renderType(flag.type, hasDefault)];
127135
items.push(DELIMITER, flag.description || t("help.noDescription")!);
128-
if (flag.type) {
129-
const type = stringifyType(flag.type);
130-
items.push(pc.gray(`(${type})`));
136+
if (hasDefault) {
137+
items.push(`(${t("help.default", renderers.renderDefault(flag.default))})`);
131138
}
132139
return items;
133140
}),
@@ -143,6 +150,7 @@ const generateSubcommandHelp = (render: Render, ctx: HandlerContext, command: st
143150
if (subcommand.examples) {
144151
generateExamples(sections, subcommand.examples, t);
145152
}
153+
sections = renderers.renderSections(sections);
146154
return render(sections);
147155
};
148156

@@ -169,23 +177,17 @@ export interface HelpPluginOptions {
169177
* Banner
170178
*/
171179
banner?: string
172-
/**
173-
* Render type
174-
*/
175-
renderer?: "cliffy"
176180
}
177181
export const helpPlugin = ({
178182
command = true,
179183
showHelpWhenNoCommand = true,
180184
notes,
181185
examples,
182186
banner,
183-
renderer = "cliffy",
184187
}: HelpPluginOptions = {}) => definePlugin({
185188
setup: (cli) => {
186189
const { add, t } = cli.i18n;
187190
add(locales);
188-
const render: Render = renderer === "cliffy" ? renderCliffy : () => "";
189191
const printHelp = (s: string) => {
190192
banner && print(`${banner}\n`);
191193
print(s);

packages/plugin-help/src/locales.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const locales: Locales = {
1818
"help.examples.1": "Show help",
1919
"help.examples.2": "Show help for a specific command",
2020
"help.commandDescription": "Show help",
21+
"help.default": "Default: ",
2122
},
2223
"zh-CN": {
2324
"help.name": "名称",
@@ -36,5 +37,6 @@ export const locales: Locales = {
3637
"help.examples.1": "展示 CLI 的帮助信息",
3738
"help.examples.2": "展示指定命令的帮助信息",
3839
"help.commandDescription": "展示帮助信息",
40+
"help.default": "默认值: %s",
3941
},
4042
};
Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pc from "picocolors";
22

3-
import { table } from "./utils";
3+
import { stringifyType, table } from "./utils";
44

55
export interface BlockSection {
66
type?: "block"
@@ -18,8 +18,14 @@ export interface InlineSection {
1818

1919
export type Section = BlockSection | InlineSection;
2020
export type Render = (sections: Section[]) => string;
21+
export interface Renderers {
22+
renderSections?: (sections: Section[]) => Section[]
23+
renderFlagName?: (name: string) => string
24+
renderType?: (type: any, hasDefault: boolean) => string
25+
renderDefault?: (default_: any) => string
26+
}
2127

22-
export const renderCliffy: Render = (sections: Section[]) => {
28+
export const render: Render = (sections: Section[]) => {
2329
const rendered = [] as string[];
2430
for (const section of sections) {
2531
if (section.type === "block" || !section.type) {
@@ -28,34 +34,21 @@ export const renderCliffy: Render = (sections: Section[]) => {
2834
.map(line => indent + line);
2935
formattedBody.unshift("");
3036
const body = formattedBody.join("\n");
31-
rendered.push(table([pc.bold(`${section.title}:`)], [body]).toString());
37+
rendered.push(table([[pc.bold(`${section.title}:`)], [body]]).toString());
3238
} else if (section.type === "inline") {
3339
const formattedBody = section.items
3440
.map(item => [pc.bold(`${item.title}:`), item.body]);
35-
const tableGenerated = table(...formattedBody);
41+
const tableGenerated = table(formattedBody);
3642
rendered.push(tableGenerated.toString());
3743
}
3844
rendered.push("");
3945
}
4046
return rendered.join("\n");
4147
};
4248

43-
// export const renderTyper: Render = (sections: Section[]) => {
44-
// const rendered = [] as string[];
45-
// for (const section of sections) {
46-
// if (section.type === "block" || !section.type) {
47-
// rendered.push(boxen(section.body.join("\n\n"), {
48-
// title: section.title,
49-
// borderStyle: "round",
50-
// padding: 0.5,
51-
// }));
52-
// } else if (section.type === "inline") {
53-
// const formattedBody = section.items
54-
// .map(item => [pc.bold(`${item.title}:`), item.body]);
55-
// const tableGenerated = table(...formattedBody);
56-
// rendered.push(tableGenerated.toString());
57-
// }
58-
// rendered.push("");
59-
// }
60-
// return rendered.join("\n");
61-
// };
49+
export const defaultRenderers: Required<Renderers> = {
50+
renderFlagName: n => n,
51+
renderSections: s => s,
52+
renderType: (type, hasDefault) => stringifyType(type, hasDefault),
53+
renderDefault: default_ => JSON.stringify(default_),
54+
};

packages/plugin-help/src/shims.d.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/plugin-help/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Section } from "./renderer";
2+
3+
export interface Renderers {
4+
renderSections?: (sections: Section[]) => Section[]
5+
renderFlagName?: (name: string) => string
6+
renderDefault?: (type: any) => string
7+
}

packages/plugin-help/src/utils.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import getFuncName from "get-func-name";
21
import textTable from "text-table";
32

4-
export const table = (...items: string[][]) => textTable(items);
3+
export const table = (items: string[][]) => textTable(items);
54

6-
export const splitTable = (...items: string[][]) => {
7-
return table(...items).toString().split("\n");
5+
export const splitTable = (items: string[][]) => {
6+
return table(items).toString().split("\n");
87
};
98

10-
export const stringifyType = (type: any) => {
11-
return Array.isArray(type)
12-
? `Array<${getFuncName(type[0])}>`
13-
: getFuncName(type);
9+
const primitiveMap = new Map<any, string>([
10+
[Boolean, ""],
11+
[String, "string"],
12+
[Number, "number"],
13+
]);
14+
export const stringifyType = (type: any, hasDefault = false) => {
15+
const res = primitiveMap.has(type)
16+
? primitiveMap.get(type)
17+
: "value";
18+
return hasDefault ? `[${res}]` : `<${res}>`;
1419
};

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)