feat: improve help config, update styles, fixes

This commit is contained in:
2023-11-24 01:47:58 +02:00
committed by Chen Asraf
parent 451dd5676d
commit c397ec9fd7
5 changed files with 115 additions and 43 deletions

View File

@@ -31,13 +31,13 @@ export const StringStyle = z.object({
bold: z.boolean().optional(), bold: z.boolean().optional(),
underline: z.boolean().optional(), underline: z.boolean().optional(),
color: zodEnumFromObjKeys(ansiColors).optional(), color: zodEnumFromObjKeys(ansiColors).optional(),
reset: z.boolean().optional(), reset: z.boolean().default(true).optional(),
}) })
export type StringStyle = z.infer<typeof StringStyle> export type StringStyle = z.infer<typeof StringStyle>
export function format(string: string, style: StringStyle = {}): string { export function format(string: string, style: StringStyle = {}): string {
const { color, bold, underline, reset } = style const { color, bold, underline, reset = true } = style
const colorCode = color ? ansiColors[color] : '' const colorCode = color ? ansiColors[color] : ''
const boldCode = bold ? ansiStyles.bold : '' const boldCode = bold ? ansiStyles.bold : ''
const underlineCode = underline ? ansiStyles.underline : '' const underlineCode = underline ? ansiStyles.underline : ''
@@ -46,5 +46,5 @@ export function format(string: string, style: StringStyle = {}): string {
} }
export function stripColors(string: string): string { export function stripColors(string: string): string {
return string.replace(/\x1b\[\d+m/g, '') return string.replace(/\x1b\[\d+m/gi, '')
} }

View File

@@ -7,7 +7,7 @@ import MassargOption, {
TypedOptionConfig, TypedOptionConfig,
MassargHelpFlag, MassargHelpFlag,
} from './option' } from './option'
import { setOrPush, deepMerge } from './utils' import { DeepRequired, setOrPush, deepMerge } from './utils'
import MassargExample, { ExampleConfig } from './example' import MassargExample, { ExampleConfig } from './example'
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) => export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
@@ -48,15 +48,24 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
options: MassargOption[] = [] options: MassargOption[] = []
examples: MassargExample[] = [] examples: MassargExample[] = []
args: Partial<Args> = {} args: Partial<Args> = {}
helpConfig: Required<HelpConfig> private _helpConfig: HelpConfig
parent?: MassargCommand<any>
constructor(options: CommandConfig<Args>) { constructor(options: CommandConfig<Args>, parent?: MassargCommand<any>) {
CommandConfig(z.any()).parse(options) CommandConfig(z.any()).parse(options)
this.name = options.name this.name = options.name
this.description = options.description this.description = options.description
this.aliases = options.aliases ?? [] this.aliases = options.aliases ?? []
this._run = options.run this._run = options.run
this.helpConfig = HelpConfig.required().parse(defaultHelpConfig) this._helpConfig = HelpConfig.required().parse(defaultHelpConfig)
this.parent = parent
}
get helpConfig(): DeepRequired<HelpConfig> {
if (this.parent) {
return deepMerge(this.parent.helpConfig, this._helpConfig)
}
return deepMerge(defaultHelpConfig, this._helpConfig) as Required<HelpConfig>
} }
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args> command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
@@ -74,6 +83,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
path: [this.name, command.name], path: [this.name, command.name],
}) })
} }
command.parent = this
this.commands.push(command) this.commands.push(command)
return this return this
} catch (e) { } catch (e) {
@@ -161,9 +171,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
} }
help(config: HelpConfig): MassargCommand<Args> { help(config: HelpConfig): MassargCommand<Args> {
this.helpConfig = HelpConfig.required().parse( this._helpConfig = HelpConfig.parse(config)
deepMerge(defaultHelpConfig, config) as HelpConfig,
)
if (this.helpConfig.bindCommand) { if (this.helpConfig.bindCommand) {
this.command(new MassargHelpCommand()) this.command(new MassargHelpCommand())

View File

@@ -1,7 +1,7 @@
import z from 'zod' import z from 'zod'
import { format, StringStyle, stripColors } from './color' import { format, StringStyle, stripColors } from './color'
import MassargCommand from './command' import MassargCommand from './command'
import { chainStr, indent } from './utils' import { DeepRequired, strConcat, indent } from './utils'
export const GenerateTableCommandConfig = z.object({ export const GenerateTableCommandConfig = z.object({
maxRowLength: z.number().optional(), maxRowLength: z.number().optional(),
@@ -44,11 +44,14 @@ export const HelpConfig = z.object({
output: StringStyle.optional(), output: StringStyle.optional(),
}) })
.optional(), .optional(),
usageText: z.string().optional(),
headerText: z.string().optional(),
footerText: z.string().optional(),
}) })
export type HelpConfig = z.infer<typeof HelpConfig> export type HelpConfig = z.infer<typeof HelpConfig>
export const defaultHelpConfig: HelpConfig = { export const defaultHelpConfig: DeepRequired<HelpConfig> = {
maxRowLength: 80, maxRowLength: 80,
commandOptions: { commandOptions: {
namePrefix: '', namePrefix: '',
@@ -97,6 +100,9 @@ export const defaultHelpConfig: HelpConfig = {
color: 'brightWhite', color: 'brightWhite',
underline: true, underline: true,
}, },
headerText: '',
footerText: '',
usageText: '',
} }
export type HelpItem = { export type HelpItem = {
@@ -107,15 +113,18 @@ export type HelpItem = {
export class HelpGenerator { export class HelpGenerator {
entry: MassargCommand<any> entry: MassargCommand<any>
config: HelpConfig config: DeepRequired<HelpConfig>
constructor(entry: MassargCommand<any>, config?: HelpConfig) { constructor(entry: MassargCommand<any>, config?: HelpConfig) {
this.entry = entry this.entry = entry
this.config = HelpConfig.parse({ this.config = HelpConfig.required().parse({
...entry.helpConfig,
commandOptions: { commandOptions: {
...entry.helpConfig?.commandOptions,
...config?.commandOptions, ...config?.commandOptions,
}, },
optionOptions: { optionOptions: {
...entry.helpConfig?.optionOptions,
...config?.optionOptions, ...config?.optionOptions,
}, },
}) })
@@ -123,40 +132,53 @@ export class HelpGenerator {
generate(): string { generate(): string {
const entry = this.entry const entry = this.entry
const options = generateHelpTable(entry.options, this.config.optionOptions) const CMD_OPT_INDENT = 4
const commands = generateHelpTable(entry.commands, this.config.commandOptions) const _wrap = (text: string, indent = 0) => wrap(text, this.config.maxRowLength - indent)
const options = generateHelpTable(entry.options, {
...this.config.optionOptions,
maxRowLength:
(this.config.optionOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
})
const commands = generateHelpTable(entry.commands, {
...this.config.commandOptions,
maxRowLength:
(this.config.commandOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
})
const examples = entry.examples const examples = entry.examples
.map((example) => { .map((example) => {
const { description, input, output } = example const { description, input, output } = example
return chainStr( return strConcat(
description && [format(description, this.config.exampleStyles?.description), ''], description && [_wrap(format(description, this.config.exampleStyles.description), 4), ''],
input && format(input, this.config.exampleStyles?.input), input && _wrap(format('$ ' + input, this.config.exampleStyles.input), 4),
output && format(output, this.config.exampleStyles?.output), output && _wrap(format('> ' + output, this.config.exampleStyles.output), 4),
) )
}) })
.join('\n') .join('\n')
const { headerText, footerText, usageText } = this.config
return ( return (
chainStr( strConcat(
format(`Usage: ${entry.name} [...options]`, this.config.usageStyle), _wrap(format(usageText || `Usage: ${entry.name} [...options]`, this.config.usageStyle)),
headerText.length && ['', format(headerText, this.config.descriptionStyle)],
'', '',
format(entry.description, this.config.descriptionStyle), _wrap(format(entry.description, this.config.descriptionStyle)),
commands.length && commands.length &&
indent([ indent([
'', '',
format(`Commands for ${entry.name}:`, this.config.subtitleStyle), format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
'', '',
indent(commands), indent(commands),
]), ]),
options.length && options.length &&
indent([ indent([
'', '',
format(`Options for ${entry.name}:`, this.config.subtitleStyle), format(`Options for ${entry.name}:`, this.config.subtitleStyle),
'', '',
indent(options), indent(options),
]), ]),
examples.length && examples.length &&
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]), indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
footerText.length && ['', _wrap(format(footerText, this.config.descriptionStyle))],
) + '\n' ) + '\n'
) )
} }
@@ -166,6 +188,28 @@ export class HelpGenerator {
} }
} }
function wrap(text: string, maxRowLength: number): string {
const length = stripColors(text).length
if (length <= maxRowLength) {
return text
}
const subRows: string[] = []
const words = text.split(' ')
let currentRow = ''
console.log('words', words)
for (const word of words) {
if (stripColors(currentRow).length + stripColors(word).length + 1 > maxRowLength) {
subRows.push(currentRow)
currentRow = ''
}
currentRow += `${word} `
}
subRows.push(currentRow)
return subRows.join('\n')
}
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>( function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
items: HelpItem[], items: HelpItem[],
{ {
@@ -177,8 +221,9 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
}: Partial<T> = {}, }: Partial<T> = {},
): string { ): string {
const rows = items.map((o) => { const rows = items.map((o) => {
const name = `${namePrefix}${o.name}${o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : '' const name = `${namePrefix}${o.name}${
}` o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
}`
const description = o.description const description = o.description
return { name, description } return { name, description }
}) })

View File

@@ -75,9 +75,13 @@ const removeCmd = new MassargCommand<{ component: string }>({
const main = massarg<A>({ const main = massarg<A>({
name: 'my-cli', name: 'my-cli',
description: 'This is an example CLI', description: 'This is an example CLI',
bindHelpOption: true,
bindHelpCommand: true,
}) })
.help({
bindOption: true,
bindCommand: true,
headerText: 'This is a header',
footerText: 'This is a footer',
})
.main((opts, parser) => { .main((opts, parser) => {
console.log('Main command - printing all opts') console.log('Main command - printing all opts')
console.log(opts, '\n') console.log(opts, '\n')
@@ -97,6 +101,17 @@ const main = massarg<A>({
aliases: ['n'], aliases: ['n'],
type: 'number', type: 'number',
}) })
.example({
description: 'Example main command',
input: 'my-cli --bool --number 123',
output: 'Main command - printing all opts\n{ bool: true, number: 123 }\n',
})
.example({
description: 'Example add command',
input: 'my-cli add --component foo --classes bar --classes baz --custom 123',
output:
'Duis ad consectetur dolore elit laborum do et aute consequat magna eu consequat dolore dolor commodo sit enim reprehenderit lorem consectetur adipiscing officia nisi adipiscing consequat consequat labore sint incididunt',
})
// console.log("Opts:", main.getArgs(process.argv.slice(2)), "\n") // console.log("Opts:", main.getArgs(process.argv.slice(2)), "\n")

View File

@@ -12,7 +12,11 @@ export function setOrPush<T>(
} }
type Parseable = string | number | boolean | null | undefined | Record<string, unknown> type Parseable = string | number | boolean | null | undefined | Record<string, unknown>
export function chainStr(...strs: (Parseable | Parseable[])[]) { export type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : NonNullable<T[P]>
}
export function strConcat(...strs: (Parseable | Parseable[])[]) {
const res: string[] = [] const res: string[] = []
for (const str of strs) { for (const str of strs) {
if (typeof str === 'string') { if (typeof str === 'string') {
@@ -20,7 +24,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
continue continue
} }
if (Array.isArray(str)) { if (Array.isArray(str)) {
res.push(chainStr(...str)) res.push(strConcat(...str))
continue continue
} }
if (str == null) { if (str == null) {
@@ -43,7 +47,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
} }
export function indent(str: Parseable | Parseable[], indent = 2): string { export function indent(str: Parseable | Parseable[], indent = 2): string {
return chainStr(str) return strConcat(str)
.split('\n') .split('\n')
.map((s) => ' '.repeat(indent) + s) .map((s) => ' '.repeat(indent) + s)
.join('\n') .join('\n')