Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(help): add formatter to artisan help #172

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@athenna/artisan",
"version": "4.21.0",
"version": "4.22.0",
"description": "The Athenna CLI application. Built on top of commander and inspired in @adonisjs/ace.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down Expand Up @@ -104,6 +104,7 @@
"src/**/*.ts"
],
"exclude": [
"src/helpers/Formatter.ts",
"src/artisan/Command.ts",
"src/artisan/Commander.ts",
"src/artisan/Configurer.ts",
Expand Down
4 changes: 4 additions & 0 deletions src/annotations/Argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export function Argument(options?: ArgumentOptions): PropertyDecorator {
required: true
})

if (options.default) {
options.required = false
}

if (!options.required) {
options.signature = `[${options.signature}]`
} else {
Expand Down
3 changes: 2 additions & 1 deletion src/annotations/Option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function Option(options?: OptionOptions): PropertyDecorator {
key = key.toString()

options = Options.create(options, {
signature: `--${String.toDashCase(key.toString())}`
isGlobal: false,
signature: `--${String.toDashCase(key)}`
})

Annotation.setOption(target, key, options)
Expand Down
24 changes: 8 additions & 16 deletions src/artisan/ArtisanImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export class ArtisanImpl {

/** Set options */
Annotation.getOptions(command).forEach(option => {
if (option.isGlobal) {
return
}

commander.option(option.signature, option.description, option.default)
})

Expand Down Expand Up @@ -165,22 +169,10 @@ export class ArtisanImpl {
*/
public async parse(argv: string[], appName?: string): Promise<void> {
if (appName) {
/**
* If argv is less or equal two, means that
* the command that are being run is just
* the CLI entrypoint. Example:
*
* - node artisan
*
* In CLI entrypoint we are going to log the
* chalkRainbow with his application name.
*/
if (argv.length <= 2) {
const appNameFiglet = figlet.textSync(appName)
const appNameFigletColorized = chalkRainbow(appNameFiglet)

process.stdout.write(appNameFigletColorized + '\n')
}
const appNameFiglet = figlet.textSync(appName)
const appNameFigletColorized = chalkRainbow(appNameFiglet)

CommanderHandler.commander.addHelpText('before', appNameFigletColorized)
}

await this.parseWithSettings(argv)
Expand Down
21 changes: 21 additions & 0 deletions src/handlers/CommanderHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { Commander } from '#src/artisan/Commander'
import { Formatter } from '#src/helpers/Formatter'
import type { Argument, Option, OptionValues } from 'commander'

export class CommanderHandler {
Expand All @@ -26,6 +27,26 @@ export class CommanderHandler {
*/
public static reconstruct(): void {
CommanderHandler.commander = new Commander()
.addHelpCommand('help [command]', 'Display help for [command]')
.usage('[command] [arguments] [options]')
.option(
'--env',
'The environment the command should run under.',
Env('APP_ENV')
)
.configureHelp({
sortSubcommands: true,
showGlobalOptions: true,
formatHelp: (cmd, helper) =>
Formatter.builder(cmd, helper)
.setUsage()
.setDescription()
.setArguments()
.setOptions()
.setCommands()
.setGlobalOptions()
.getOutput()
})
}

/**
Expand Down
249 changes: 249 additions & 0 deletions src/helpers/Formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* @athenna/artisan
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Command, type Help } from 'commander'
import { Color, Options } from '@athenna/common'

export class Formatter {
public static builder(cmd: Command, help: Help) {
return new Formatter(cmd, help)
}

private cmd: Command
private help: Help
private termWidth: number = 0
private itemSeparatorWidth: number = 0
private helpWidth: number = 0
private indent: string = ''
private moveOptions: any = false
private matchRequiredArgs = /\[(.*?)\]/
private matchOptionsAndOptionalArgs = /\[(.*?)\]/
private sections = {
description: '',
usage: '',
arguments: '',
options: '',
commands: '',
globalOptions: ''
}

public constructor(cmd: Command, help: Help) {
this.cmd = cmd
this.help = help

this.termWidth = help.padWidth(cmd, help)
this.helpWidth = help.helpWidth || 80
this.itemSeparatorWidth = 2
this.indent = ' '.repeat(2)
this.moveOptions = !cmd.parent && cmd.commands.length
}

public setUsage() {
this.sections.usage = this.help.commandUsage(this.cmd)

return this
}

public setDescription() {
this.sections.description = this.help.commandDescription(this.cmd)

return this
}

public setOptions() {
this.sections.options = this.list(
this.help
.visibleOptions(this.cmd)
.filter(option => {
if (
this.cmd.parent &&
(option.long === '--help' || option.long === '--version')
) {
this.cmd.parent.addOption(option)
return false
}

return true
})
.map(opt =>
this.option(
this.help.optionTerm(opt),
this.help.optionDescription(opt)
)
)
)

return this
}

public setCommands() {
const commands = this.help.visibleCommands(this.cmd).map(c => {
return this.command(
this.help.subcommandTerm(c),
this.help.subcommandDescription(c)
)
})

this.sections.commands = this.list(commands) || ''

return this
}

public setArguments() {
const args = this.help.visibleArguments(this.cmd).map(argument => {
return this.item(
this.help.argumentTerm(argument),
this.help.argumentDescription(argument)
)
})

this.sections.arguments = this.list(args)

return this
}

public setGlobalOptions() {
this.sections.globalOptions = this.list(
this.help
.visibleGlobalOptions(this.cmd)
.filter(opt => opt.long !== '--help' && opt.long !== '--version')
.map(opt =>
this.option(
this.help.optionTerm(opt),
this.help.optionDescription(opt)
)
)
)

return this
}

public getOutput() {
const output = []

output.push(
Color.yellow.bold('Usage:'),
this.indent + this.sections.usage,
''
)

if (this.sections.description) {
output.push(
Color.yellow.bold('Description:'),
this.indent + this.sections.description,
''
)
}

if (this.sections.arguments) {
output.push(Color.yellow.bold('Arguments'), this.sections.arguments, '')
}

if (this.sections.options && !this.moveOptions) {
output.push(Color.yellow.bold('Options:'), this.sections.options, '')
}

if (this.sections.commands.length) {
output.push(
Color.yellow.bold('Available Commands:'),
this.sections.commands,
''
)
}

if (this.sections.options && this.moveOptions) {
output.push(Color.yellow.bold('Options:'), this.sections.options, '')
}

if (this.sections.globalOptions) {
output.push(
Color.yellow.bold('Global Options:'),
this.sections.globalOptions,
''
)
}

return output.join('\n')
}

public command(cmd: string, desc: string) {
if (desc) {
const formattedTerm = this.term(cmd, { padEnd: true, greenColor: false })

return this.help.wrap(
`${Color.green(formattedTerm)}${desc}`,
this.helpWidth - this.indent.length,
this.termWidth + this.itemSeparatorWidth
)
}

return this.term(cmd)
}

public option(term: string, desc: string) {
return this.command(term, desc)
}

public item(term: string, desc: string) {
if (desc) {
const formattedTerm = this.term(term, {
padEnd: true,
argsColor: false,
greenColor: false
})

return this.help.wrap(
`${formattedTerm}${desc}`,
this.helpWidth - this.indent.length,
this.termWidth + this.itemSeparatorWidth
)
}

return term
}

public list(values: string[]) {
const list = values.join('\n').replace(/^/gm, this.indent).trim()

return list ? this.indent + list : ''
}

public term(
term: string,
options: {
greenColor?: boolean
padEnd?: boolean
argsColor?: boolean
} = {}
) {
options = Options.create(options, {
argsColor: true,
greenColor: true,
padEnd: false
})

let formattedTerm = ''

if (options.padEnd) {
formattedTerm = term.padEnd(this.termWidth + this.itemSeparatorWidth)
}

if (options.argsColor) {
formattedTerm = formattedTerm
.replace(this.matchRequiredArgs, Color.yellow('$&'))
.replace(this.matchOptionsAndOptionalArgs, Color.yellow('$&'))
}

if (options.greenColor) {
return Color.green.bold(formattedTerm)
}

return formattedTerm
}
}
1 change: 1 addition & 0 deletions src/types/OptionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
export type OptionOptions = {
signature?: string
default?: any
isGlobal?: boolean
description?: string
}
5 changes: 4 additions & 1 deletion tests/unit/annotations/OptionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,25 @@ export default class OptionTest extends BaseTest {

const opts = Reflect.getMetadata(OPTIONS_KEY, annotatedCommand)

assert.deepEqual(opts[0], { key: 'withName', signature: '--with-name', signatureName: 'withName' })
assert.deepEqual(opts[0], { key: 'withName', isGlobal: false, signature: '--with-name', signatureName: 'withName' })
assert.deepEqual(opts[1], {
key: 'withAge',
isGlobal: false,
signature: '--with-age',
signatureName: 'withAge',
description: 'Add the age of the person'
})
assert.deepEqual(opts[2], {
default: true,
isGlobal: false,
key: 'withEmail',
signatureName: 'addEmail',
signature: '-am, --add-email',
description: 'Add the email of the person'
})
assert.deepEqual(opts[3], {
key: 'withFoo',
isGlobal: false,
default: false,
signature: '--no-foo',
signatureName: 'foo'
Expand Down
Loading
Loading