Skip to content

Commit

Permalink
feat: oclif/plugin-help version 3 (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
chadian committed Apr 27, 2020
1 parent c3568e0 commit 45eb985
Show file tree
Hide file tree
Showing 18 changed files with 1,642 additions and 502 deletions.
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "@oclif/plugin-help",
"description": "standard help for oclif",
"version": "2.2.3",
"version": "3.0.0-rc1.2",
"author": "Jeff Dickey @jdxcode",
"bugs": "https://github.com/oclif/plugin-help/issues",
"dependencies": {
"@oclif/command": "^1.5.13",
"@oclif/command": "^1.5.20",
"@oclif/config": "^1.15.1",
"chalk": "^2.4.1",
"indent-string": "^4.0.0",
"lodash.template": "^4.4.0",
Expand All @@ -15,7 +16,6 @@
"wrap-ansi": "^4.0.0"
},
"devDependencies": {
"@oclif/config": "^1.13.0",
"@oclif/dev-cli": "^1.21.0",
"@oclif/errors": "^1.2.2",
"@oclif/plugin-legacy": "^1.1.3",
Expand All @@ -33,8 +33,9 @@
"eslint-config-oclif-typescript": "^0.1.0",
"globby": "^9.0.0",
"mocha": "^5.2.0",
"ts-node": "^8.0.2",
"typescript": "^3.7.2"
"sinon": "^9.0.1",
"ts-node": "^8.8.2",
"typescript": "^3.8.3"
},
"engines": {
"node": ">=8.0.0"
Expand Down
20 changes: 20 additions & 0 deletions src/_test-help-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// `getHelpClass` tests require an oclif project for testing so
// it is re-using the setup here to be able to do a lookup for
// this sample help class file in tests, although it is not needed
// for @oclif/plugin-help itself.

import {HelpBase} from '.'

export default class extends HelpBase {
showHelp() {
console.log('help')
}

showCommandHelp() {
console.log('command help')
}

getCommandHelpForReadme() {
return 'help for readme'
}
}
1 change: 0 additions & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export default class CommandHelp {
return compact([
this.command.id,
this.command.args.filter(a => !a.hidden).map(a => this.arg(a)).join(' '),
// flags.length && '[OPTIONS]',
]).join(' ')
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, flags} from '@oclif/command'

import Help from '..'
import {getHelpClass} from '..'

export default class HelpCommand extends Command {
static description = 'display help for <%= config.bin %>'
Expand All @@ -17,6 +17,7 @@ export default class HelpCommand extends Command {

async run() {
const {flags, argv} = this.parse(HelpCommand)
const Help = getHelpClass(this.config)
const help = new Help(this.config, {all: flags.all})
help.showHelp(argv)
}
Expand Down
224 changes: 166 additions & 58 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {renderList} from './list'
import RootHelp from './root'
import {stdtermwidth} from './screen'
import {compact, sortBy, template, uniqBy} from './util'
export {getHelpClass} from './util'

const wrap = require('wrap-ansi')
const {
Expand All @@ -22,10 +23,6 @@ export interface HelpOptions {
}

function getHelpSubject(args: string[]): string | undefined {
// special case
// if (['help:help', 'help:--help', '--help:help'].includes(argv.slice(0, 2).join(':'))) {
// if (argv[0] === 'help') return 'help'

for (const arg of args) {
if (arg === '--') return
if (arg.startsWith('-')) continue
Expand All @@ -34,77 +31,188 @@ function getHelpSubject(args: string[]): string | undefined {
}
}

export default class Help {
opts: HelpOptions
export abstract class HelpBase {
constructor(config: Config.IConfig, opts: Partial<HelpOptions> = {}) {
this.config = config
this.opts = {maxWidth: stdtermwidth, ...opts}
}

protected config: Config.IConfig

protected opts: HelpOptions

/**
* Show help, used in multi-command CLIs
* @param args passed into your command, useful for determining which type of help to display
*/
public abstract showHelp(argv: string[]): void;

/**
* Show help for an individual command
* @param command
* @param topics
*/
public abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void;
}

export default class Help extends HelpBase {
render: (input: string) => string

constructor(public config: Config.IConfig, opts: Partial<HelpOptions> = {}) {
this.opts = {maxWidth: stdtermwidth, ...opts}
this.render = template(this)
/*
* _topics is to work around Config.topics mistakenly including commands that do
* not have children, as well as topics. A topic has children, either commands or other topics. When
* this is fixed upstream config.topics should return *only* topics with children,
* and this can be removed.
*/
private get _topics(): Config.Topic[] {
return this.config.topics.filter((topic: Config.Topic) => {
// it is assumed a topic has a child if it has children
const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`))
return hasChild
})
}

showHelp(argv: string[]) {
let topics = this.config.topics
protected get sortedCommands() {
let commands = this.config.commands

commands = commands.filter(c => this.opts.all || !c.hidden)
commands = sortBy(commands, c => c.id)
commands = uniqBy(commands, c => c.id)

return commands
}

protected get sortedTopics() {
let topics = this._topics
topics = topics.filter(t => this.opts.all || !t.hidden)
topics = sortBy(topics, t => t.name)
topics = uniqBy(topics, t => t.name)
const subject = getHelpSubject(argv)

let command: Config.Command | undefined
if (subject) {
command = this.config.findCommand(subject)
return topics
}

constructor(config: Config.IConfig, opts: Partial<HelpOptions> = {}) {
super(config, opts)
this.render = template(this)
}

public showHelp(argv: string[]) {
const subject = getHelpSubject(argv)
if (!subject) {
this.showRootHelp()
return
}

let topic: Config.Topic | undefined
if (subject && !command) {
topic = this.config.findTopic(subject)
const command = this.config.findCommand(subject)
if (command) {
this.showCommandHelp(command)
return
}

if (!subject) {
console.log(this.root())
console.log('')
if (!this.opts.all) {
topics = topics.filter(t => !t.name.includes(':'))
}
console.log(this.topics(topics))
console.log('')
} else if (command) {
this.showCommandHelp(command, topics)
} else if (topic) {
const name = topic.name
const depth = name.split(':').length
topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1)
console.log(this.topic(topic))
if (topics.length > 0) {
console.log(this.topics(topics))
console.log('')
}
} else {
error(`command ${subject} not found`)
const topic = this.config.findTopic(subject)
if (topic) {
this.showTopicHelp(topic)
return
}

error(`command ${subject} not found`)
}

showCommandHelp(command: Config.Command, topics: Config.Topic[]) {
public showCommandHelp(command: Config.Command) {
const name = command.id
const depth = name.split(':').length
topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1)

const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1)
const subCommands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1)

const title = command.description && this.render(command.description).split('\n')[0]
if (title) console.log(title + '\n')
console.log(this.command(command))
console.log(this.formatCommand(command))
console.log('')

if (subTopics.length > 0) {
console.log(this.formatTopics(subTopics))
console.log('')
}

if (subCommands.length > 0) {
console.log(this.formatCommands(subCommands))
console.log('')
}
}

protected showRootHelp() {
let rootTopics = this.sortedTopics
let rootCommands = this.sortedCommands

console.log(this.formatRoot())
console.log('')
if (topics.length > 0) {
console.log(this.topics(topics))

if (!this.opts.all) {
rootTopics = rootTopics.filter(t => !t.name.includes(':'))
rootCommands = rootCommands.filter(c => !c.id.includes(':'))
}

if (rootTopics.length > 0) {
console.log(this.formatTopics(rootTopics))
console.log('')
}

if (rootCommands.length > 0) {
console.log(this.formatCommands(rootCommands))
console.log('')
}
}

root(): string {
protected showTopicHelp(topic: Config.Topic) {
const name = topic.name
const depth = name.split(':').length

const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1)
const commands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1)

console.log(this.formatTopic(topic))

if (subTopics.length > 0) {
console.log(this.formatTopics(subTopics))
console.log('')
}

if (commands.length > 0) {
console.log(this.formatCommands(commands))
console.log('')
}
}

protected formatRoot(): string {
const help = new RootHelp(this.config, this.opts)
return help.root()
}

topic(topic: Config.Topic): string {
protected formatCommand(command: Config.Command): string {
const help = new CommandHelp(command, this.config, this.opts)
return help.generate()
}

protected formatCommands(commands: Config.Command[]): string {
if (commands.length === 0) return ''

const body = renderList(commands.map(c => [
c.id,
c.description && this.render(c.description.split('\n')[0]),
]), {
spacer: '\n',
stripAnsi: this.opts.stripAnsi,
maxWidth: this.opts.maxWidth - 2,
})

return [
bold('COMMANDS'),
indent(body, 2),
].join('\n')
}

protected formatTopic(topic: Config.Topic): string {
let description = this.render(topic.description || '')
const title = description.split('\n')[0]
description = description.split('\n').slice(1).join('\n')
Expand All @@ -123,13 +231,8 @@ export default class Help {
return output + '\n'
}

command(command: Config.Command): string {
const help = new CommandHelp(command, this.config, this.opts)
return help.generate()
}

topics(topics: Config.Topic[]): string | undefined {
if (topics.length === 0) return
protected formatTopics(topics: Config.Topic[]): string {
if (topics.length === 0) return ''
const body = renderList(topics.map(c => [
c.name,
c.description && this.render(c.description.split('\n')[0]),
Expand All @@ -139,12 +242,17 @@ export default class Help {
maxWidth: this.opts.maxWidth - 2,
})
return [
bold('COMMANDS'),
bold('TOPICS'),
indent(body, 2),
].join('\n')
}
}

// function id(c: Config.Command | Config.Topic): string {
// return (c as any).id || (c as any).name
// }
/**
* @deprecated used for readme generation
* @param {object} command The command to generate readme help for
* @return {string} the readme help string for the given command
*/
protected command(command: Config.Command) {
return this.formatCommand(command)
}
}
Loading

0 comments on commit 45eb985

Please sign in to comment.