Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6e05b0e
Add HelpBase abstract class
chadian Mar 26, 2020
4b90ef1
Add tests for topic output
chadian Mar 27, 2020
4a1bcff
Add topics tests
chadian Mar 27, 2020
eb70682
Move render and config setup to HelpBase
chadian Mar 27, 2020
ac48362
Move the constructor opts to the base class
chadian Mar 27, 2020
276d255
Add more tests
chadian Mar 27, 2020
6cfa8a9
Clean up
chadian Mar 31, 2020
52ee8f4
Restructure command tests
chadian Mar 31, 2020
44edacc
Add command description template test
chadian Apr 1, 2020
e60f40b
Add command usage tests
chadian Apr 1, 2020
55446cd
Add tests for template support in root descriptions
chadian Apr 1, 2020
dbef558
Export abstract HelpBase
chadian Apr 1, 2020
7211fd9
Reduce HelpBase to the currently use public interface
chadian Apr 2, 2020
1c94978
Add command method to BaseHelp, used by dev-cl to generate README
chadian Apr 2, 2020
fadb41c
Fix HelpOptions typing
chadian Apr 3, 2020
ce9ba9a
Refactor Help class to have easier extension methods
chadian Apr 7, 2020
a82331e
Add test to track showHelp routing
chadian Apr 7, 2020
7729357
Refactor showHelp to return after specific cases
chadian Apr 7, 2020
6c05385
Add getCommandHelpForReadme as preferred method for readme command he…
chadian Apr 10, 2020
0be84da
Move getHelpPlugin into @oclif/plugin-help
chadian Apr 13, 2020
9a2dd54
Use getHelpPlugin for internal help command
chadian Apr 13, 2020
25b6da8
Use helpClass instead of helpPlugin
chadian Apr 13, 2020
6633fe3
Bump @oclif/config
chadian Apr 15, 2020
c413d15
Fix tests
chadian Apr 15, 2020
bd804fa
Add additional unit tests to cover getHelpClass loading cases
chadian Apr 15, 2020
9abfa55
Fix extract helper function names
chadian Apr 15, 2020
717968c
Ensure @oclif/config 1.15.1 is used
chadian Apr 15, 2020
8d94e15
Remove the readme-related methods
chadian Apr 15, 2020
f9d80ec
Rename to format* for the formatting methods
chadian Apr 15, 2020
7b730aa
Change up tests to use the format methods
chadian Apr 15, 2020
5bfa829
Fix variable naming
chadian Apr 15, 2020
9aaa951
Add disclaimer on test help class included in src
chadian Apr 16, 2020
8d8b50f
Add subtopic and topic command lists for a topic
chadian Apr 17, 2020
bad6397
Add topic/command split to root help
chadian Apr 20, 2020
965cc76
Fix eslint jsdoc
chadian Apr 20, 2020
a71b774
Add back deprecated command in case non-pinned dev-cli uses the plugi…
chadian Apr 20, 2020
c7563b7
Bump dependencies
chadian Apr 20, 2020
f7d1991
Rename mocks to fixtures
chadian Apr 20, 2020
c520661
Make opts and config protected
chadian Apr 20, 2020
9dcf818
Add tests around listed split topic and command help
chadian Apr 21, 2020
eef3ca3
Remove categorizeTopicsAndCommands in favor of a correct _topics prop…
chadian Apr 21, 2020
41198f2
Small method naming tweaks
chadian Apr 21, 2020
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
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "Jeff Dickey @jdxcode",
"bugs": "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/oclif/plugin-help/issues",
"dependencies": {
"@oclif/command": "^1.5.13",
"@oclif/command": "^1.5.20",
"chalk": "^2.4.1",
"indent-string": "^4.0.0",
"lodash.template": "^4.4.0",
Expand All @@ -15,7 +15,7 @@
"wrap-ansi": "^4.0.0"
},
"devDependencies": {
"@oclif/config": "^1.13.0",
"@oclif/config": "^1.15.1",
"@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) {
Comment thread
chadian marked this conversation as resolved.
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('')
}
Comment thread
chadian marked this conversation as resolved.
}

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