From 6e05b0ed8c4061a889ed21b3b0bd65ed4707c57b Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Thu, 26 Mar 2020 17:50:19 -0400 Subject: [PATCH 01/42] Add HelpBase abstract class --- src/index.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 728483a1..86f8c85c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,21 @@ function getHelpSubject(args: string[]): string | undefined { } } -export default class Help { +abstract class HelpBase { + constructor(public config: Config.IConfig) {} + + abstract showHelp(argv: string[]): void; + + abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; + + abstract root(): string; + + abstract topic(topic: Config.Topic): string; + + abstract topics(topics: Config.Topic[]): string | undefined; +} + +export default class Help implements HelpBase { opts: HelpOptions render: (input: string) => string @@ -52,12 +66,9 @@ export default class Help { const subject = getHelpSubject(argv) let command: Config.Command | undefined + let topic: Config.Topic | undefined if (subject) { command = this.config.findCommand(subject) - } - - let topic: Config.Topic | undefined - if (subject && !command) { topic = this.config.findTopic(subject) } From 4b90ef1563966e47f817db6f12bb907a15842ae0 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 27 Mar 2020 11:19:23 -0400 Subject: [PATCH 02/42] Add tests for topic output --- src/index.ts | 5 +---- test/topic.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 test/topic.test.ts diff --git a/src/index.ts b/src/index.ts index 86f8c85c..44cf973d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ function getHelpSubject(args: string[]): string | undefined { } abstract class HelpBase { + // eslint-disable-next-line no-useless-constructor constructor(public config: Config.IConfig) {} abstract showHelp(argv: string[]): void; @@ -155,7 +156,3 @@ export default class Help implements HelpBase { ].join('\n') } } - -// function id(c: Config.Command | Config.Topic): string { -// return (c as any).id || (c as any).name -// } diff --git a/test/topic.test.ts b/test/topic.test.ts new file mode 100644 index 00000000..b77dd71c --- /dev/null +++ b/test/topic.test.ts @@ -0,0 +1,44 @@ +import * as Config from '@oclif/config' +import {expect, test as base} from '@oclif/test' +import stripAnsi = require('strip-ansi') + +const g: any = global +g.columns = 80 +import Help from '../src' + +const test = base +.loadConfig() +.add('help', ctx => new Help(ctx.config)) +.register('topicHelp', (topic: Config.Topic) => ({ + run(ctx: {help: Help; commandHelp: string; expectation: string}) { + const topicHelpOutput = ctx.help.topic(topic) + if (process.env.TEST_OUTPUT === '1') { + console.log(topicHelpOutput) + } + ctx.commandHelp = stripAnsi(topicHelpOutput).split('\n').map(s => s.trimRight()).join('\n') + ctx.expectation = 'has topicHelp' + }, +})) + +describe('topic help', () => { + test + .topicHelp({ + name: 'topic', + description: 'this is a description of my topic', + hidden: false, + }) + .it('shows topic output', ctx => expect(ctx.commandHelp).to.equal(`this is a description of my topic + +USAGE + $ oclif topic:COMMAND +`)) + + test + .topicHelp({ + name: 'topic', + hidden: false, + }) + .it('shows topic usage without a description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif topic:COMMAND +`)) +}) From 4a1bcff02d5de81c76ca534d100ce1e87e0a1eb5 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 27 Mar 2020 13:53:16 -0400 Subject: [PATCH 03/42] Add topics tests --- test/topics.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/topics.test.ts diff --git a/test/topics.test.ts b/test/topics.test.ts new file mode 100644 index 00000000..a339bd25 --- /dev/null +++ b/test/topics.test.ts @@ -0,0 +1,49 @@ +import * as Config from '@oclif/config' +import {expect, test as base} from '@oclif/test' +import stripAnsi = require('strip-ansi') + +const g: any = global +g.columns = 80 +import Help from '../src' + +const test = base +.loadConfig() +.add('help', ctx => new Help(ctx.config)) +.register('topicsHelp', (topics: Config.Topic[]) => ({ + run(ctx: {help: Help; commandHelp: string; expectation: string}) { + const topicsHelpOutput = ctx.help.topics(topics) || '' + + if (process.env.TEST_OUTPUT === '1') { + console.log(topicsHelpOutput) + } + + ctx.commandHelp = stripAnsi(topicsHelpOutput).split('\n').map(s => s.trimRight()).join('\n') + ctx.expectation = 'has topicsHelp' + }, +})) + +describe('topics help', () => { + test + .topicsHelp([{ + name: 'topic', + description: 'this is a description of my topic', + }]) + .it('shows ouputs a single topic in the list', ctx => expect(ctx.commandHelp).to.equal(`COMMANDS + topic this is a description of my topic`)) + + test + .topicsHelp([{ + name: 'topic', + description: 'this is a description of my topic', + }, { + name: 'othertopic', + description: 'here we have a description for othertopic', + }, { + name: 'thirdtopic', + description: 'description for thirdtopic', + }]) + .it('shows ouputs a single topic in the list', ctx => expect(ctx.commandHelp).to.equal(`COMMANDS + topic this is a description of my topic + othertopic here we have a description for othertopic + thirdtopic description for thirdtopic`)) +}) From eb706821ea500f379fa3aef874b137b8372d69b1 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 27 Mar 2020 13:56:21 -0400 Subject: [PATCH 04/42] Move render and config setup to HelpBase --- src/index.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 44cf973d..d536122d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,8 +35,14 @@ function getHelpSubject(args: string[]): string | undefined { } abstract class HelpBase { - // eslint-disable-next-line no-useless-constructor - constructor(public config: Config.IConfig) {} + constructor(config: Config.IConfig) { + this.render = template(this) + this.config = config + } + + render: (input: string) => string + + config: Config.IConfig abstract showHelp(argv: string[]): void; @@ -46,17 +52,15 @@ abstract class HelpBase { abstract topic(topic: Config.Topic): string; - abstract topics(topics: Config.Topic[]): string | undefined; + abstract topics(topics: Config.Topic[]): string | undefined } -export default class Help implements HelpBase { +export default class Help extends HelpBase { opts: HelpOptions - render: (input: string) => string - - constructor(public config: Config.IConfig, opts: Partial = {}) { + constructor(config: Config.IConfig, opts: Partial = {}) { + super(config) this.opts = {maxWidth: stdtermwidth, ...opts} - this.render = template(this) } showHelp(argv: string[]) { From ac483620da7b8173c94fe2e3f40020c219adfe92 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 27 Mar 2020 14:48:14 -0400 Subject: [PATCH 05/42] Move the constructor opts to the base class --- src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index d536122d..ba3123f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,15 +35,18 @@ function getHelpSubject(args: string[]): string | undefined { } abstract class HelpBase { - constructor(config: Config.IConfig) { + constructor(config: Config.IConfig, opts: Partial = {}) { this.render = template(this) this.config = config + this.opts = {maxWidth: stdtermwidth, ...opts} } render: (input: string) => string config: Config.IConfig + opts: HelpOptions + abstract showHelp(argv: string[]): void; abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; @@ -56,11 +59,8 @@ abstract class HelpBase { } export default class Help extends HelpBase { - opts: HelpOptions - constructor(config: Config.IConfig, opts: Partial = {}) { - super(config) - this.opts = {maxWidth: stdtermwidth, ...opts} + super(config, opts) } showHelp(argv: string[]) { From 276d2552bcd3e57c6fb0cb864c176185680ccd3f Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 27 Mar 2020 16:20:41 -0400 Subject: [PATCH 06/42] Add more tests --- test/root.test.ts | 88 ++++++++++++++++++++++++++++++++++++++++++---- test/topic.test.ts | 17 ++++++++- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/test/root.test.ts b/test/root.test.ts index 6d09bcc3..e51cea08 100644 --- a/test/root.test.ts +++ b/test/root.test.ts @@ -1,4 +1,4 @@ -import {expect, test as base} from '@oclif/test' +import {expect, test as base, Config} from '@oclif/test' import stripAnsi = require('strip-ansi') const g: any = global @@ -10,25 +10,99 @@ const UA = `@oclif/plugin-help/${VERSION} ${process.platform}-${process.arch} no const test = base .loadConfig() -.add('help', ctx => new Help(ctx.config)) -.register('rootHelp', () => ({ - run(ctx: {help: Help; commandHelp: string; expectation: string}) { - const help = ctx.help.root() +.register('rootHelp', (ctxOverride?: (config: Config.IConfig) => Config.IConfig) => ({ + run(ctx: {config: Config.IConfig; help: Help; commandHelp: string; expectation: string}) { + const config = ctxOverride ? ctxOverride(ctx.config) : ctx.config + const help = new Help(config) + const root = help.root() if (process.env.TEST_OUTPUT === '1') { console.log(help) } - ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') + ctx.commandHelp = stripAnsi(root).split('\n').map(s => s.trimRight()).join('\n') }, })) describe('root help', () => { test .rootHelp() - .it(ctx => expect(ctx.commandHelp).to.equal(`standard help for oclif + .it('renders the root help', ctx => expect(ctx.commandHelp).to.equal(`standard help for oclif VERSION ${UA} USAGE $ oclif [COMMAND]`)) + + describe('description', () => { + test + .rootHelp(config => { + return { + ...config, + pjson: { + ...config.pjson, + description: 'This is the top-level description that appears in the root\nThis appears in the description section after usage', + }, + } + }) + .it('splits on \\n for the description into the top-level and description sections', ctx => { + expect(ctx.commandHelp).to.equal(`This is the top-level description that appears in the root + +VERSION + ${UA} + +USAGE + $ oclif [COMMAND] + +DESCRIPTION + This appears in the description section after usage`) + }) + + test + .rootHelp(config => { + return { + ...config, + pjson: { + ...config.pjson, + description: 'THIS IS THE PJSON DESCRIPTION', + oclif: { + ...config.pjson?.oclif, + description: 'THIS IS THE OCLIF DESCRIPTION IN PJSON', + }, + }, + } + }) + .it('prefers the oclif description over the package.json description', ctx => { + expect(ctx.commandHelp).to.equal(`THIS IS THE OCLIF DESCRIPTION IN PJSON + +VERSION + ${UA} + +USAGE + $ oclif [COMMAND]`) + }) + + test + .rootHelp(config => { + return { + ...config, + pjson: { + ...config.pjson, + description: 'THIS IS THE PJSON DESCRIPTION', + oclif: { + ...config.pjson?.oclif, + description: undefined, + }, + }, + } + }) + .it('uses package.json description when the oclif description is not set', ctx => { + expect(ctx.commandHelp).to.equal(`THIS IS THE PJSON DESCRIPTION + +VERSION + ${UA} + +USAGE + $ oclif [COMMAND]`) + }) + }) }) diff --git a/test/topic.test.ts b/test/topic.test.ts index b77dd71c..1a283803 100644 --- a/test/topic.test.ts +++ b/test/topic.test.ts @@ -38,7 +38,22 @@ USAGE name: 'topic', hidden: false, }) - .it('shows topic usage without a description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('shows topic without a description', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif topic:COMMAND `)) + + test + .topicHelp({ + name: 'topic', + hidden: false, + description: 'This is the top level description\nDescription that shows up in the DESCRIPTION section', + }) + .it('shows topic descriptions split from \\n for top-level and description section descriptions', ctx => expect(ctx.commandHelp).to.equal(`This is the top level description + +USAGE + $ oclif topic:COMMAND + +DESCRIPTION + Description that shows up in the DESCRIPTION section +`)) }) From 6cfa8a942d28953ed3c0bc706dbafc8aade9c63e Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 31 Mar 2020 10:25:33 -0400 Subject: [PATCH 07/42] Clean up --- src/command.ts | 1 - src/index.ts | 20 ++++----- test/command.test.ts | 105 +------------------------------------------ test/topic.test.ts | 14 ++++++ 4 files changed, 24 insertions(+), 116 deletions(-) diff --git a/src/command.ts b/src/command.ts index 369ef6ce..079a0f23 100644 --- a/src/command.ts +++ b/src/command.ts @@ -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(' ') } diff --git a/src/index.ts b/src/index.ts index ba3123f8..6ee6f82e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,16 +16,12 @@ const { } = chalk export interface HelpOptions { - all?: boolean; maxWidth: number; stripAnsi?: boolean; + all?: boolean; } 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 @@ -36,13 +32,10 @@ function getHelpSubject(args: string[]): string | undefined { abstract class HelpBase { constructor(config: Config.IConfig, opts: Partial = {}) { - this.render = template(this) this.config = config this.opts = {maxWidth: stdtermwidth, ...opts} } - render: (input: string) => string - config: Config.IConfig opts: HelpOptions @@ -51,16 +44,21 @@ abstract class HelpBase { abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; - abstract root(): string; + abstract root(): string | undefined; + + abstract topic(topic: Config.Topic): string | undefined; - abstract topic(topic: Config.Topic): string; + abstract topics(topics: Config.Topic[]): string | undefined; - abstract topics(topics: Config.Topic[]): string | undefined + abstract command(command: Config.Command): string | undefined; } export default class Help extends HelpBase { + render: (input: string) => string + constructor(config: Config.IConfig, opts: Partial = {}) { super(config, opts) + this.render = template(this) } showHelp(argv: string[]) { diff --git a/test/command.test.ts b/test/command.test.ts index d128cede..e4c281ac 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -49,7 +49,7 @@ multiline help` label: flags.string({char: 'l', helpLabel: '-l'}), } }) - .it('shows lots of output', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('handles multi-line outputs', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -243,107 +243,4 @@ OPTIONS .it('outputs usage arrays with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif $ oclif apps:create usage`)) - - // class AppsCreate3 extends Command { - // static id = 'apps:create' - // static flags = { - // app: flags.string({char: 'a', hidden: true}), - // foo: flags.string({char: 'f', description: 'foobar'}), - // force: flags.boolean({description: 'force it'}), - // remote: flags.string({char: 'r'}), - // } - // } - // test('has just flags', () => { - // expect(help.command(AppsCreate3)).toEqual(`Usage: cli-engine apps:create [flags] - - // Flags: - // -f, --foo FOO foobar - // -r, --remote REMOTE - // --force force it - // `) - // }) - - // test('has flags + description', () => { - // class CMD extends Command { - // static id = 'apps:create' - // static description = 'description of apps:create' - // static flags = { - // app: flags.string({char: 'a', hidden: true}), - // foo: flags.string({char: 'f', description: 'foobar'}), - // force: flags.boolean({description: 'force it'}), - // remote: flags.string({char: 'r'}), - // } - // } - // expect(help.command(CMD)).toEqual(`Usage: cli-engine apps:create [flags] - - // description of apps:create - - // Flags: - // -f, --foo FOO foobar - // -r, --remote REMOTE - // --force force it - // `) - // }) - - // class AppsCreate1 extends Command { - // static id = 'apps:create' - // static help = 'description of apps:create' - // static flags = { - // app: flags.string({char: 'a', hidden: true}), - // foo: flags.string({char: 'f', description: 'foobar'}), - // force: flags.boolean({description: 'force it'}), - // remote: flags.string({char: 'r'}), - // } - // } - // test('has description + help', () => { - // expect(help.command(AppsCreate1)).toEqual(`Usage: cli-engine apps:create [flags] - - // Flags: - // -f, --foo FOO foobar - // -r, --remote REMOTE - // --force force it - - // description of apps:create - // `) - // }) - - // class AppsCreate2 extends Command { - // static id = 'apps:create' - // static description = 'description of apps:create' - // static args = [{name: 'app_name', description: 'app to use'}] - // } - - // test('has description + args', () => { - // expect(help.command(AppsCreate2)).toEqual(`Usage: cli-engine apps:create [APP_NAME] - - // description of apps:create - - // APP_NAME app to use - // `) - // }) - - // class CMD extends Command { - // static id = 'apps:create2' - // static description = 'description of apps:create2' - // static args = [{name: 'app_name', description: 'app to use'}] - // static aliases = ['foo', 'bar'] - // } - // test('has aliases', () => { - // expect(help.command(CMD)).toEqual(`Usage: cli-engine apps:create2 [APP_NAME] - - // description of apps:create2 - - // Aliases: - // $ cli-engine foo - // $ cli-engine bar - -// APP_NAME app to use -// `) - // }) }) - -// describe('command()', () => { -// test('has command help', () => { -// expect(help.commandLine(AppsCreate)).toEqual(['apps:create [APP_NAME]', 'description of apps:create']) -// }) -// }) diff --git a/test/topic.test.ts b/test/topic.test.ts index 1a283803..0134b196 100644 --- a/test/topic.test.ts +++ b/test/topic.test.ts @@ -55,5 +55,19 @@ USAGE DESCRIPTION Description that shows up in the DESCRIPTION section +`)) + test + .topicHelp({ + name: 'topic', + hidden: false, + description: '<%= config.bin %>: This is the top level description\n<%= config.bin %>: Description that shows up in the DESCRIPTION section', + }) + .it('shows topic descriptions split from \\n for top-level and description section descriptions', ctx => expect(ctx.commandHelp).to.equal(`oclif: This is the top level description + +USAGE + $ oclif topic:COMMAND + +DESCRIPTION + oclif: Description that shows up in the DESCRIPTION section `)) }) From 52ee8f4f3f2b79162bf8dee3325098b3bd826fcb Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 31 Mar 2020 16:21:29 -0400 Subject: [PATCH 08/42] Restructure command tests --- test/command.test.ts | 198 +++++++++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 63 deletions(-) diff --git a/test/command.test.ts b/test/command.test.ts index e4c281ac..95d0b456 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -49,7 +49,7 @@ multiline help` label: flags.string({char: 'l', helpLabel: '-l'}), } }) - .it('handles multi-line outputs', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('handles multi-line help output', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -79,29 +79,83 @@ ALIASES $ oclif app:init $ oclif create`)) - test - .commandHelp(class extends Command { - static id = 'apps:create' + describe('arg and flag multiline handling', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' - static description = 'description of apps:create' + static description = 'description of apps:create' - static aliases = ['app:init', 'create'] + static aliases = ['app:init', 'create'] - static args = [{name: 'app_name', description: 'app to use'}] + static args = [{name: 'app_name', description: 'app to use'.repeat(35)}] - static flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), - force: flags.boolean({description: 'force it '.repeat(29)}), - ss: flags.boolean({description: 'newliney\n'.repeat(5)}), - remote: flags.string({char: 'r'}), - } - }) - .it('shows alternate output when many lines', ctx => expect(ctx.commandHelp).to.equal(`USAGE + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + } + }) + .it('show args and flags side by side when their output do not exceed 4 lines ', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS - APP_NAME app to use + APP_NAME + app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to + useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp + to useapp to useapp to useapp to useapp to useapp to useapp to useapp to + useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp + to useapp to useapp to useapp to useapp to use + +OPTIONS + -f, --foo=foo foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo + barfoobarfoobarfoobarfoobarfoobar + + -r, --remote=remote + + --force force it force it force it force it force it force + it force it force it force it force it force it + force it force it force it force it + + --ss newliney + newliney + newliney + newliney + +ALIASES + $ oclif app:init + $ oclif create`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static description = 'description of apps:create' + + static aliases = ['app:init', 'create'] + + static args = [{name: 'app_name', description: 'app to use'.repeat(35)}] + + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), + force: flags.boolean({description: 'force it '.repeat(29)}), + ss: flags.boolean({description: 'newliney\n'.repeat(5)}), + remote: flags.string({char: 'r'}), + } + }) + .it('shows stacked args and flags when the lines exceed 4', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create [APP_NAME] + +ARGUMENTS + APP_NAME + app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to + useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp + to useapp to useapp to useapp to useapp to useapp to useapp to useapp to + useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp + to useapp to useapp to useapp to useapp to use OPTIONS -f, --foo=foo @@ -126,12 +180,13 @@ OPTIONS ALIASES $ oclif app:init $ oclif create`)) + }) test .commandHelp(class extends Command { static id = 'apps:create' - static description = 'description of apps:create' + static description = 'description of apps:create\nthese values are after and will show up in the command description' static aliases = ['app:init', 'create'] @@ -141,7 +196,7 @@ ALIASES force: flags.boolean({description: 'forces'}), } }) - .it('outputs with description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs command description with values after a \\n newline character', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -150,26 +205,30 @@ ARGUMENTS OPTIONS --force forces +DESCRIPTION + these values are after and will show up in the command description + ALIASES $ oclif app:init $ oclif create`)) - test - .commandHelp(class extends Command { - static id = 'apps:create' + describe(('flags'), () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' - static flags = { - myenum: flags.string({options: ['a', 'b', 'c']}), - } - }) - .it('outputs with description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + static flags = { + myenum: flags.string({options: ['a', 'b', 'c']}), + } + }) + .it('outputs flag enum', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create OPTIONS --myenum=a|b|c`)) - test - .commandHelp(class extends Command { + test + .commandHelp(class extends Command { static id = 'apps:create' static args = [ @@ -183,8 +242,7 @@ OPTIONS flag2: flags.string({default: '.', description: 'flag2 desc'}), flag3: flags.string({description: 'flag3 desc'}), } - }) - .it('outputs with default options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + }).it('outputs with default flag options', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] [ARG2] [ARG3] ARGUMENTS @@ -197,50 +255,64 @@ OPTIONS --flag2=flag2 [default: .] flag2 desc --flag3=flag3 flag3 desc`)) - test - .commandHelp(class extends Command { - static id = 'apps:create' + test + .commandHelp(class extends Command { + static id = 'apps:create' - static args = [ - {name: 'arg1', description: 'Show the options', options: ['option1', 'option2']}, - ] + static flags = { + opt: flags.boolean({allowNo: true}), + } + }) + .it('outputs with with no options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create + +OPTIONS + --[no-]opt`)) }) - .it('outputs with possible options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + + describe('args', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static args = [ + {name: 'arg1', description: 'Show the options', options: ['option1', 'option2']}, + ] + }) + .it('outputs with arg options', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] ARGUMENTS ARG1 (option1|option2) Show the options`)) - - test - .commandHelp(class extends Command { - static id = 'apps:create' - - static flags = { - opt: flags.boolean({allowNo: true}), - } }) - .it('outputs with possible options', ctx => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif apps:create -OPTIONS - --[no-]opt`)) - - test - .commandHelp(class extends Command { - static id = 'apps:create' + describe('usage', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' - static usage = '<%= config.bin %> <%= command.id %> usage' - }) - .it('outputs usage with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + static usage = '<%= config.bin %> <%= command.id %> usage' + }) + .it('outputs usage with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif apps:create usage`)) - test - .commandHelp(class extends Command { - static id = 'apps:create' + test + .commandHelp(class extends Command { + static id = 'apps:create' - static usage = ['<%= config.bin %>', '<%= command.id %> usage'] - }) - .it('outputs usage arrays with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + static usage = ['<%= config.bin %>', '<%= command.id %> usage'] + }) + .it('outputs usage arrays with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif $ oclif apps:create usage`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static usage = undefined + }) + .it('defaults usage when not specified', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create`)) + }) }) From 44edacc9d6f30782adea0fad3daab22a9d17f7c9 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 1 Apr 2020 12:59:53 -0400 Subject: [PATCH 09/42] Add command description template test --- test/command.test.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/test/command.test.ts b/test/command.test.ts index 95d0b456..9f2bea37 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -182,21 +182,22 @@ ALIASES $ oclif create`)) }) - test - .commandHelp(class extends Command { - static id = 'apps:create' + describe('description', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' - static description = 'description of apps:create\nthese values are after and will show up in the command description' + static description = 'description of apps:create\nthese values are after and will show up in the command description' - static aliases = ['app:init', 'create'] + static aliases = ['app:init', 'create'] - static args = [{name: 'app_name', description: 'app to use'}] + static args = [{name: 'app_name', description: 'app to use'}] - static flags = { - force: flags.boolean({description: 'forces'}), - } - }) - .it('outputs command description with values after a \\n newline character', ctx => expect(ctx.commandHelp).to.equal(`USAGE + static flags = { + force: flags.boolean({description: 'forces'}), + } + }) + .it('outputs command description with values after a \\n newline character', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -212,6 +213,19 @@ ALIASES $ oclif app:init $ oclif create`)) + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>' + }) + .it('renders template string from description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create + +DESCRIPTION + The oclif CLI has apps:create`)) + }) + describe(('flags'), () => { test .commandHelp(class extends Command { From e60f40ba877426a583fb6743ee2fab0ac321d395 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 1 Apr 2020 13:47:45 -0400 Subject: [PATCH 10/42] Add command usage tests --- test/command.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/command.test.ts b/test/command.test.ts index 9f2bea37..459f6961 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -329,4 +329,40 @@ ARGUMENTS .it('defaults usage when not specified', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create`)) }) + + describe('examples', () => { + test + .commandHelp(class extends Command { + static examples = ['it handles a list of examples', 'more example text'] + }) + .it('outputs multiple examples', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif + +EXAMPLES + it handles a list of examples + more example text`)) + + test + .commandHelp(class extends Command { + static examples = ['it handles a single example'] + }) + .it('outputs a single example', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif + +EXAMPLE + it handles a single example`)) + + test + .commandHelp(class extends Command { + static id = 'oclif:command' + + static examples = ['the bin is <%= config.bin %>', 'the command id is <%= command.id %>'] + }) + .it('outputs examples using templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif oclif:command + +EXAMPLES + the bin is oclif + the command id is oclif:command`)) + }) }) From 55446cd1cf3ffd2d5de6b757be8684fa73538545 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 1 Apr 2020 14:22:57 -0400 Subject: [PATCH 11/42] Add tests for template support in root descriptions --- test/root.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/root.test.ts b/test/root.test.ts index e51cea08..83d35392 100644 --- a/test/root.test.ts +++ b/test/root.test.ts @@ -57,6 +57,29 @@ DESCRIPTION This appears in the description section after usage`) }) + test + .rootHelp(config => { + return { + ...config, + pjson: { + ...config.pjson, + description: 'This is the top-level description for <%= config.bin %>\nThis <%= config.bin %> appears in the description section after usage', + }, + } + }) + .it('shows description from a template', ctx => { + expect(ctx.commandHelp).to.equal(`This is the top-level description for oclif + +VERSION + ${UA} + +USAGE + $ oclif [COMMAND] + +DESCRIPTION + This oclif appears in the description section after usage`) + }) + test .rootHelp(config => { return { From dbef558d797829410e5bb39518d069f7d87c640a Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 1 Apr 2020 16:08:08 -0400 Subject: [PATCH 12/42] Export abstract HelpBase --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 6ee6f82e..94e06beb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,7 @@ function getHelpSubject(args: string[]): string | undefined { } } -abstract class HelpBase { +export abstract class HelpBase { constructor(config: Config.IConfig, opts: Partial = {}) { this.config = config this.opts = {maxWidth: stdtermwidth, ...opts} From 7211fd91b2dd020a58f3659cce35bfee04af0c76 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Thu, 2 Apr 2020 15:15:00 -0400 Subject: [PATCH 13/42] Reduce HelpBase to the currently use public interface --- src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 94e06beb..a106c677 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,14 +43,6 @@ export abstract class HelpBase { abstract showHelp(argv: string[]): void; abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; - - abstract root(): string | undefined; - - abstract topic(topic: Config.Topic): string | undefined; - - abstract topics(topics: Config.Topic[]): string | undefined; - - abstract command(command: Config.Command): string | undefined; } export default class Help extends HelpBase { From 1c949785f358138bda5337a193cac82b4f5460e1 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Thu, 2 Apr 2020 15:54:34 -0400 Subject: [PATCH 14/42] Add command method to BaseHelp, used by dev-cl to generate README --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index a106c677..8da2c49b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,8 @@ export abstract class HelpBase { abstract showHelp(argv: string[]): void; abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; + + abstract command(command: Config.Command): string; } export default class Help extends HelpBase { From fadb41c6edde98ff07d88a14515f3a48f282a99d Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 3 Apr 2020 10:29:06 -0400 Subject: [PATCH 15/42] Fix HelpOptions typing --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8da2c49b..46627d50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,9 +16,9 @@ const { } = chalk export interface HelpOptions { + all?: boolean; maxWidth: number; stripAnsi?: boolean; - all?: boolean; } function getHelpSubject(args: string[]): string | undefined { From ce9ba9a3a39b71e798ee1d41aab6c52e19073bba Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 7 Apr 2020 15:24:23 -0400 Subject: [PATCH 16/42] Refactor Help class to have easier extension methods --- src/index.ts | 52 ++++++++++++++++++++++++++------------------- test/root.test.ts | 10 ++++++++- test/topic.test.ts | 13 ++++++++++-- test/topics.test.ts | 11 ++++++++-- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index 46627d50..72f947d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,11 +40,11 @@ export abstract class HelpBase { opts: HelpOptions - abstract showHelp(argv: string[]): void; + public abstract showHelp(argv: string[]): void; - abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; + public abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; - abstract command(command: Config.Command): string; + public abstract command(command: Config.Command): string; } export default class Help extends HelpBase { @@ -55,7 +55,7 @@ export default class Help extends HelpBase { this.render = template(this) } - showHelp(argv: string[]) { + public showHelp(argv: string[]) { let topics = this.config.topics topics = topics.filter(t => this.opts.all || !t.hidden) topics = sortBy(topics, t => t.name) @@ -70,30 +70,38 @@ export default class Help extends HelpBase { } 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('') + this.showRootHelp(topics) } 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('') - } + const siblingTopics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + this.showTopicHelp(topic, siblingTopics) } else { error(`command ${subject} not found`) } } - showCommandHelp(command: Config.Command, topics: Config.Topic[]) { + protected showRootHelp(topics: Config.Topic[]) { + console.log(this.root()) + console.log('') + if (!this.opts.all) { + topics = topics.filter(t => !t.name.includes(':')) + } + console.log(this.topics(topics)) + console.log('') + } + + protected showTopicHelp(topic: Config.Topic, siblingTopics: Config.Topic[]) { + console.log(this.topic(topic)) + if (siblingTopics.length > 0) { + console.log(this.topics(siblingTopics)) + console.log('') + } + } + + public showCommandHelp(command: Config.Command, topics: Config.Topic[]) { const name = command.id const depth = name.split(':').length topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) @@ -107,12 +115,12 @@ export default class Help extends HelpBase { } } - root(): string { + protected root(): string { const help = new RootHelp(this.config, this.opts) return help.root() } - topic(topic: Config.Topic): string { + protected topic(topic: Config.Topic): string { let description = this.render(topic.description || '') const title = description.split('\n')[0] description = description.split('\n').slice(1).join('\n') @@ -131,12 +139,12 @@ export default class Help extends HelpBase { return output + '\n' } - command(command: Config.Command): string { + public command(command: Config.Command): string { const help = new CommandHelp(command, this.config, this.opts) return help.generate() } - topics(topics: Config.Topic[]): string | undefined { + protected topics(topics: Config.Topic[]): string | undefined { if (topics.length === 0) return const body = renderList(topics.map(c => [ c.name, diff --git a/test/root.test.ts b/test/root.test.ts index 83d35392..13e7fa11 100644 --- a/test/root.test.ts +++ b/test/root.test.ts @@ -8,12 +8,20 @@ import Help from '../src' const VERSION = require('../package.json').version const UA = `@oclif/plugin-help/${VERSION} ${process.platform}-${process.arch} node-${process.version}` +// extensions to expose method as public for testing +class TestHelp extends Help { + public root() { + return super.root() + } +} + const test = base .loadConfig() .register('rootHelp', (ctxOverride?: (config: Config.IConfig) => Config.IConfig) => ({ run(ctx: {config: Config.IConfig; help: Help; commandHelp: string; expectation: string}) { const config = ctxOverride ? ctxOverride(ctx.config) : ctx.config - const help = new Help(config) + + const help = new TestHelp(config) const root = help.root() if (process.env.TEST_OUTPUT === '1') { console.log(help) diff --git a/test/topic.test.ts b/test/topic.test.ts index 0134b196..63ce5143 100644 --- a/test/topic.test.ts +++ b/test/topic.test.ts @@ -6,11 +6,20 @@ const g: any = global g.columns = 80 import Help from '../src' +// extensions to expose method as public for testing +class TestHelp extends Help { + public topic(topic: Config.Topic) { + return super.topic(topic) + } +} + const test = base .loadConfig() -.add('help', ctx => new Help(ctx.config)) +.add('help', ctx => { + return new TestHelp(ctx.config) +}) .register('topicHelp', (topic: Config.Topic) => ({ - run(ctx: {help: Help; commandHelp: string; expectation: string}) { + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { const topicHelpOutput = ctx.help.topic(topic) if (process.env.TEST_OUTPUT === '1') { console.log(topicHelpOutput) diff --git a/test/topics.test.ts b/test/topics.test.ts index a339bd25..405c9337 100644 --- a/test/topics.test.ts +++ b/test/topics.test.ts @@ -6,11 +6,18 @@ const g: any = global g.columns = 80 import Help from '../src' +// extensions to expose method as public for testing +class TestHelp extends Help { + public topics(topics: Config.Topic[]) { + return super.topics(topics) + } +} + const test = base .loadConfig() -.add('help', ctx => new Help(ctx.config)) +.add('help', ctx => new TestHelp(ctx.config)) .register('topicsHelp', (topics: Config.Topic[]) => ({ - run(ctx: {help: Help; commandHelp: string; expectation: string}) { + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { const topicsHelpOutput = ctx.help.topics(topics) || '' if (process.env.TEST_OUTPUT === '1') { From a82331e1546de33a94344baae23acf0c854d4518 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 7 Apr 2020 17:01:34 -0400 Subject: [PATCH 17/42] Add test to track showHelp routing --- package.json | 1 + test/show-help.test.ts | 134 +++++++++++++++++++++++++++++++++++++++++ yarn.lock | 96 ++++++++++++++++++++++++++++- 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 test/show-help.test.ts diff --git a/package.json b/package.json index 5d84d6cf..636c66a4 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "eslint-config-oclif-typescript": "^0.1.0", "globby": "^9.0.0", "mocha": "^5.2.0", + "sinon": "^9.0.1", "ts-node": "^8.0.2", "typescript": "^3.7.2" }, diff --git a/test/show-help.test.ts b/test/show-help.test.ts new file mode 100644 index 00000000..a87264c2 --- /dev/null +++ b/test/show-help.test.ts @@ -0,0 +1,134 @@ +import * as Config from '@oclif/config' +import {expect, test as base} from '@oclif/test' +import {stub, SinonStub} from 'sinon' + +const g: any = global +g.columns = 80 +import Help from '../src' + +// extension makes previously protected methods public +class TestHelp extends Help { + public showRootHelp(topics: Config.Topic[]) { + return super.showRootHelp(topics) + } + + public showTopicHelp(topic: Config.Topic, siblingTopics: Config.Topic[]) { + return super.showTopicHelp(topic, siblingTopics) + } +} + +const test = base +.loadConfig() +.register('setupHelp', () => ({ + async run(ctx: { help: TestHelp; stubs: { [k: string]: SinonStub }}) { + ctx.stubs = { + showRootHelp: stub(TestHelp.prototype, 'showRootHelp').returns(), + showTopicHelp: stub(TestHelp.prototype, 'showTopicHelp').returns(), + showCommandHelp: stub(TestHelp.prototype, 'showCommandHelp').returns(), + } + + const config = await Config.load() + ctx.help = new TestHelp(config) + }, + finally(ctx) { + Object.values(ctx.stubs).forEach(stub => stub.restore()) + }, +})) +.register('makeTopicsWithoutCommand', () => ({ + async run(ctx: {help: TestHelp; makeTopicOnlyStub: SinonStub}) { + // by returning no matching command for a subject, it becomes a topic only + // with no corresponding command (in which case the showCommandHelp is shown) + ctx.makeTopicOnlyStub = stub(ctx.help.config, 'findCommand').returns(undefined) + }, + finally(ctx) { + ctx.makeTopicOnlyStub.restore() + }, +})) + +describe('showHelp routing', () => { + describe('shows root help', () => { + test + .setupHelp() + .it('shows root help when no subject is provided', ({help, stubs}) => { + help.showHelp([]) + expect(stubs.showRootHelp.called).to.be.true + + expect(stubs.showCommandHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) + + test + .setupHelp() + .it('shows root help when help is the only arg', ({help, stubs}) => { + help.showHelp(['help']) + expect(stubs.showRootHelp.called).to.be.true + + expect(stubs.showCommandHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) + }) + + describe('shows topic help', () => { + test + .setupHelp() + .makeTopicsWithoutCommand() + .it('shows the topic help when a topic has no matching command', ({help, stubs}) => { + help.showHelp(['plugins']) + expect(stubs.showTopicHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showCommandHelp.called).to.be.false + }) + + test + .setupHelp() + .makeTopicsWithoutCommand() + .it('shows the topic help when a topic has no matching command and is preceded by help', ({help, stubs}) => { + help.showHelp(['help', 'plugins']) + expect(stubs.showTopicHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showCommandHelp.called).to.be.false + }) + }) + + describe('shows command help', () => { + test + .setupHelp() + .it('calls showCommandHelp when a topic that is also a command is called', ({help, stubs}) => { + help.showHelp(['plugins']) + expect(stubs.showCommandHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) + + test + .setupHelp() + .it('calls showCommandHelp when a command is called', ({help, stubs}) => { + help.showHelp(['plugins:install']) + expect(stubs.showCommandHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) + + test + .setupHelp() + .it('calls showCommandHelp when a command is preceded by the help arg', ({help, stubs}) => { + help.showHelp(['help', 'plugins:install']) + expect(stubs.showCommandHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) + }) + + describe('errors', () => { + test + .setupHelp() + .it('shows an error when there is a subject but it does not match a topic or command', ({help}) => { + expect(() => help.showHelp(['meow'])).to.throw('command meow not found') + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 04bc5cf0..ab481118 100644 --- a/yarn.lock +++ b/yarn.lock @@ -210,6 +210,42 @@ dependencies: fancy-test "^1.4.1" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1" + integrity sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/chai@*", "@types/chai@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" @@ -830,6 +866,11 @@ diff@3.5.0, diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" @@ -1389,6 +1430,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -1686,6 +1732,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1743,6 +1794,11 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2010,6 +2066,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913" + integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + normalize-package-data@^2.4.0: version "2.4.2" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.2.tgz#6b2abd85774e51f7936f1395e45acb905dc849b2" @@ -2165,6 +2232,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -2409,6 +2483,19 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +sinon@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.1.tgz#dbb18f7d8f5835bcf91578089c0a97b2fffdd73b" + integrity sha512-iTTyiQo5T94jrOx7X7QLBZyucUJ2WvL9J13+96HMfm2CGoJYbIPqRfl6wgNcqmzk0DI28jeGx5bUTXizkrqBmg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -2636,6 +2723,13 @@ supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -2771,7 +2865,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== From 7729357dc23e48b3a4badf97c8faa5577e28be8e Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 7 Apr 2020 17:10:05 -0400 Subject: [PATCH 18/42] Refactor showHelp to return after specific cases --- src/index.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 72f947d0..23053ae0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,9 +36,9 @@ export abstract class HelpBase { this.opts = {maxWidth: stdtermwidth, ...opts} } - config: Config.IConfig + public config: Config.IConfig - opts: HelpOptions + public opts: HelpOptions public abstract showHelp(argv: string[]): void; @@ -60,27 +60,29 @@ export default class Help extends HelpBase { 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 - let topic: Config.Topic | undefined - if (subject) { - command = this.config.findCommand(subject) - topic = this.config.findTopic(subject) - } + const subject = getHelpSubject(argv) if (!subject) { this.showRootHelp(topics) - } else if (command) { + return + } + + const command = this.config.findCommand(subject) + if (command) { this.showCommandHelp(command, topics) - } else if (topic) { + return + } + + const topic = this.config.findTopic(subject) + if (topic) { const name = topic.name const depth = name.split(':').length const siblingTopics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) this.showTopicHelp(topic, siblingTopics) - } else { - error(`command ${subject} not found`) + return } + + error(`command ${subject} not found`) } protected showRootHelp(topics: Config.Topic[]) { From 6c05385fc1f96c79b8bf589be3da7f544c0ba9d5 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 10 Apr 2020 14:00:15 -0400 Subject: [PATCH 19/42] Add getCommandHelpForReadme as preferred method for readme command help generation --- src/index.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 23053ae0..c8f780c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,11 +40,24 @@ export abstract class HelpBase { public 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; - public abstract command(command: Config.Command): string; + /** + * Returned string is used for given the command in readme generation + * @param command + */ + public abstract getCommandHelpForReadme(command: Config.Command): string; } export default class Help extends HelpBase { @@ -141,6 +154,15 @@ export default class Help extends HelpBase { return output + '\n' } + public getCommandHelpForReadme(command: Config.Command): string { + return this.command(command) + } + + /** + * @deprecated replaced by getCommandHelpForReadme + * @param {Command} command to generate help for + * @returns {string} help string for the given c ommand + */ public command(command: Config.Command): string { const help = new CommandHelp(command, this.config, this.opts) return help.generate() From 0be84da1cee3658d1160e701c1fff8d83d6842e7 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 13 Apr 2020 11:06:18 -0400 Subject: [PATCH 20/42] Move getHelpPlugin into @oclif/plugin-help --- src/index.ts | 1 + src/util.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/index.ts b/src/index.ts index c8f780c0..4550777d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import {renderList} from './list' import RootHelp from './root' import {stdtermwidth} from './screen' import {compact, sortBy, template, uniqBy} from './util' +export {getHelpPlugin} from './util' const wrap = require('wrap-ansi') const { diff --git a/src/util.ts b/src/util.ts index bf64c02f..300450f5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,7 @@ +import {tsPath} from '@oclif/config/lib/ts-node' import lodashTemplate = require('lodash.template') +import {IConfig} from '@oclif/config' +import {HelpBase, HelpOptions} from '.' export function uniqBy(arr: T[], fn: (cur: T) => any): T[] { return arr.filter((a, i) => { @@ -46,3 +49,37 @@ export function template(context: any): (t: string) => string { } return render } + +interface HelpBaseDerived { + new(config: IConfig, opts?: Partial): HelpBase; +} + +export function extractPlugin(config: IConfig, pluginPath: string): HelpBaseDerived { + const helpPlugin = tsPath(config.root, pluginPath) + return require(helpPlugin) as HelpBaseDerived +} + +export function extractExport(exported: any): HelpBaseDerived { + return exported && exported.default ? exported.default : exported +} + +export function getHelpPlugin(config: IConfig, defaultPlugin = '@oclif/plugin-help'): HelpBaseDerived { + const pjson = config.pjson + const configuredPlugin = pjson && pjson.oclif && pjson.oclif.helpPlugin + + if (configuredPlugin) { + try { + const exported = extractPlugin(config, configuredPlugin) + return extractExport(exported) as HelpBaseDerived + } catch (error) { + throw new Error(`Unable to load configured help plugin "${configuredPlugin}" from package.json, failed with message:\n${error.message}`) + } + } + + try { + const exported = require(defaultPlugin) + return extractExport(exported) as HelpBaseDerived + } catch (error) { + throw new Error(`Could not load a help plugin, consider installing the @oclif/plugin-help package, failed with message:\n${error.message}`) + } +} From 9a2dd54b7e9ed9cd23a1f4677d40ae13141d0f56 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 13 Apr 2020 11:24:36 -0400 Subject: [PATCH 21/42] Use getHelpPlugin for internal help command --- src/commands/help.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/help.ts b/src/commands/help.ts index fdd385d4..affa61fd 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,6 +1,6 @@ import {Command, flags} from '@oclif/command' -import Help from '..' +import {getHelpPlugin} from '..' export default class HelpCommand extends Command { static description = 'display help for <%= config.bin %>' @@ -17,6 +17,7 @@ export default class HelpCommand extends Command { async run() { const {flags, argv} = this.parse(HelpCommand) + const Help = getHelpPlugin(this.config) const help = new Help(this.config, {all: flags.all}) help.showHelp(argv) } From 25b6da8d2e81d9cc883a8245e8564129a4641855 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 13 Apr 2020 13:37:51 -0400 Subject: [PATCH 22/42] Use helpClass instead of helpPlugin --- src/commands/help.ts | 4 ++-- src/index.ts | 8 ++++++-- src/util.ts | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/commands/help.ts b/src/commands/help.ts index affa61fd..06a923b6 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,6 +1,6 @@ import {Command, flags} from '@oclif/command' -import {getHelpPlugin} from '..' +import {getHelpClass} from '..' export default class HelpCommand extends Command { static description = 'display help for <%= config.bin %>' @@ -17,7 +17,7 @@ export default class HelpCommand extends Command { async run() { const {flags, argv} = this.parse(HelpCommand) - const Help = getHelpPlugin(this.config) + const Help = getHelpClass(this.config) const help = new Help(this.config, {all: flags.all}) help.showHelp(argv) } diff --git a/src/index.ts b/src/index.ts index 4550777d..f39f361b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import {renderList} from './list' import RootHelp from './root' import {stdtermwidth} from './screen' import {compact, sortBy, template, uniqBy} from './util' -export {getHelpPlugin} from './util' +export {getHelpClass} from './util' const wrap = require('wrap-ansi') const { @@ -155,6 +155,10 @@ export default class Help extends HelpBase { return output + '\n' } + /** + * @param {Command} command to generate help for + * @returns {string} help string for the given command + */ public getCommandHelpForReadme(command: Config.Command): string { return this.command(command) } @@ -162,7 +166,7 @@ export default class Help extends HelpBase { /** * @deprecated replaced by getCommandHelpForReadme * @param {Command} command to generate help for - * @returns {string} help string for the given c ommand + * @returns {string} help string for the given command */ public command(command: Config.Command): string { const help = new CommandHelp(command, this.config, this.opts) diff --git a/src/util.ts b/src/util.ts index 300450f5..ea047cc7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -54,32 +54,32 @@ interface HelpBaseDerived { new(config: IConfig, opts?: Partial): HelpBase; } -export function extractPlugin(config: IConfig, pluginPath: string): HelpBaseDerived { - const helpPlugin = tsPath(config.root, pluginPath) - return require(helpPlugin) as HelpBaseDerived +export function extractClass(config: IConfig, classPath: string): HelpBaseDerived { + const helpClassPath = tsPath(config.root, classPath) + return require(helpClassPath) as HelpBaseDerived } export function extractExport(exported: any): HelpBaseDerived { return exported && exported.default ? exported.default : exported } -export function getHelpPlugin(config: IConfig, defaultPlugin = '@oclif/plugin-help'): HelpBaseDerived { +export function getHelpClass(config: IConfig, defaultClass = '@oclif/plugin-help'): HelpBaseDerived { const pjson = config.pjson - const configuredPlugin = pjson && pjson.oclif && pjson.oclif.helpPlugin + const configuredClass = pjson && pjson.oclif && pjson.oclif.helpClass - if (configuredPlugin) { + if (configuredClass) { try { - const exported = extractPlugin(config, configuredPlugin) + const exported = extractClass(config, configuredClass) return extractExport(exported) as HelpBaseDerived } catch (error) { - throw new Error(`Unable to load configured help plugin "${configuredPlugin}" from package.json, failed with message:\n${error.message}`) + throw new Error(`Unable to load configured help class "${configuredClass}", failed with message:\n${error.message}`) } } try { - const exported = require(defaultPlugin) + const exported = require(defaultClass) return extractExport(exported) as HelpBaseDerived } catch (error) { - throw new Error(`Could not load a help plugin, consider installing the @oclif/plugin-help package, failed with message:\n${error.message}`) + throw new Error(`Could not load a help class, consider installing the @oclif/plugin-help package, failed with message:\n${error.message}`) } } From 6633fe3bf32b74bb35e7312b3ea653dbe467adba Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 10:52:57 -0400 Subject: [PATCH 23/42] Bump @oclif/config --- yarn.lock | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index ab481118..5d119e8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -95,10 +95,12 @@ tslib "^1.9.3" "@oclif/config@^1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.13.0.tgz#fc2bd82a9cb30a73faf7d2aa5ae937c719492bd1" - integrity sha512-ttb4l85q7SBx+WlUJY4A9eXLgv4i7hGDNGaXnY9fDKrYD7PBMwNOQ3Ssn2YT2yARAjyOxVE/5LfcwhQGq4kzqg== + version "1.15.1" + resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.15.1.tgz#39950c70811ab82d75bb3cdb33679ed0a4c21c57" + integrity sha512-GdyHpEZuWlfU8GSaZoiywtfVBsPcfYn1KuSLT1JTfvZGpPG6vShcGr24YZ3HG2jXUFlIuAqDcYlTzOrqOdTPNQ== dependencies: + "@oclif/errors" "^1.0.0" + "@oclif/parser" "^3.8.0" debug "^4.1.1" tslib "^1.9.3" @@ -119,7 +121,7 @@ qqjs "^0.3.10" tslib "^1.9.3" -"@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2": +"@oclif/errors@^1.0.0", "@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.2.2.tgz#9d8f269b15f13d70aa93316fed7bebc24688edc2" integrity sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg== @@ -153,6 +155,15 @@ chalk "^2.4.2" tslib "^1.9.3" +"@oclif/parser@^3.8.0": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.4.tgz#1a90fc770a42792e574fb896325618aebbe8c9e4" + integrity sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA== + dependencies: + "@oclif/linewrap" "^1.0.0" + chalk "^2.4.2" + tslib "^1.9.3" + "@oclif/plugin-help@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-2.1.4.tgz#b530fa3147d5ae91ba9c84d085f53a829b2914dc" @@ -1410,11 +1421,16 @@ globby@^9.0.0: pify "^4.0.1" slash "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@^4.1.11: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -2017,9 +2033,9 @@ ms@2.0.0: integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= ms@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== mute-stream@0.0.8: version "0.0.8" @@ -2834,7 +2850,7 @@ ts-node@^8.0.2: source-map-support "^0.5.6" yn "^3.0.0" -tslib@^1, tslib@^1.9.3: +tslib@^1: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== @@ -2844,6 +2860,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tslib@^1.9.3: + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" From c413d153f85dd45c114c92707348b002a9c55531 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 11:13:24 -0400 Subject: [PATCH 24/42] Fix tests --- test/command.test.ts | 32 ++++++++++++++++---------------- test/commands/help.test.ts | 24 ------------------------ 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/test/command.test.ts b/test/command.test.ts index 459f6961..f321b334 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -16,7 +16,7 @@ class Command extends Base { const test = base .loadConfig() .add('help', ctx => new Help(ctx.config)) -.register('commandHelp', (command?: Config.Command.Class) => ({ +.register('commandHelp', (command?: any) => ({ run(ctx: {help: Help; commandHelp: string; expectation: string}) { const cached = Config.Command.toCached(command!, {} as any) const help = ctx.help.command(cached) @@ -49,7 +49,7 @@ multiline help` label: flags.string({char: 'l', helpLabel: '-l'}), } }) - .it('handles multi-line help output', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('handles multi-line help output', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -98,7 +98,7 @@ ALIASES remote: flags.string({char: 'r'}), } }) - .it('show args and flags side by side when their output do not exceed 4 lines ', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -146,7 +146,7 @@ ALIASES remote: flags.string({char: 'r'}), } }) - .it('shows stacked args and flags when the lines exceed 4', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -197,7 +197,7 @@ ALIASES force: flags.boolean({description: 'forces'}), } }) - .it('outputs command description with values after a \\n newline character', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs command description with values after a \\n newline character', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] ARGUMENTS @@ -219,7 +219,7 @@ ALIASES static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>' }) - .it('renders template string from description', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('renders template string from description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create DESCRIPTION @@ -235,7 +235,7 @@ DESCRIPTION myenum: flags.string({options: ['a', 'b', 'c']}), } }) - .it('outputs flag enum', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create OPTIONS @@ -256,7 +256,7 @@ OPTIONS flag2: flags.string({default: '.', description: 'flag2 desc'}), flag3: flags.string({description: 'flag3 desc'}), } - }).it('outputs with default flag options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + }).it('outputs with default flag options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] [ARG2] [ARG3] ARGUMENTS @@ -277,7 +277,7 @@ OPTIONS opt: flags.boolean({allowNo: true}), } }) - .it('outputs with with no options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs with with no options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create OPTIONS @@ -293,7 +293,7 @@ OPTIONS {name: 'arg1', description: 'Show the options', options: ['option1', 'option2']}, ] }) - .it('outputs with arg options', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs with arg options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] ARGUMENTS @@ -307,7 +307,7 @@ ARGUMENTS static usage = '<%= config.bin %> <%= command.id %> usage' }) - .it('outputs usage with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs usage with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif apps:create usage`)) test @@ -316,7 +316,7 @@ ARGUMENTS static usage = ['<%= config.bin %>', '<%= command.id %> usage'] }) - .it('outputs usage arrays with templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs usage arrays with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif $ oclif apps:create usage`)) @@ -326,7 +326,7 @@ ARGUMENTS static usage = undefined }) - .it('defaults usage when not specified', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('defaults usage when not specified', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create`)) }) @@ -335,7 +335,7 @@ ARGUMENTS .commandHelp(class extends Command { static examples = ['it handles a list of examples', 'more example text'] }) - .it('outputs multiple examples', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs multiple examples', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif EXAMPLES @@ -346,7 +346,7 @@ EXAMPLES .commandHelp(class extends Command { static examples = ['it handles a single example'] }) - .it('outputs a single example', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs a single example', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif EXAMPLE @@ -358,7 +358,7 @@ EXAMPLE static examples = ['the bin is <%= config.bin %>', 'the command id is <%= command.id %>'] }) - .it('outputs examples using templates', ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('outputs examples using templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif:command EXAMPLES diff --git a/test/commands/help.test.ts b/test/commands/help.test.ts index 4ab2af15..fe9df314 100644 --- a/test/commands/help.test.ts +++ b/test/commands/help.test.ts @@ -4,30 +4,6 @@ const VERSION = require('../../package.json').version const UA = `@oclif/plugin-help/${VERSION} ${process.platform}-${process.arch} node-${process.version}` describe('help command', () => { - test - .stdout() - .command(['help', 'plugins']) - .it('shows plugins command help', ctx => { - expect(ctx.stdout).to.equal(`list installed plugins - -USAGE - $ oclif plugins - -OPTIONS - --core show core plugins - -EXAMPLE - $ oclif plugins - -COMMANDS - plugins:install installs a plugin into the CLI - plugins:link links a plugin into the CLI for development - plugins:uninstall removes a plugin from the CLI - plugins:update update installed plugins - -`) - }) - test .stdout() .command(['help', 'help']) From bd804fae6f185d204885d1503c18705e00b1e489 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 12:22:52 -0400 Subject: [PATCH 25/42] Add additional unit tests to cover getHelpClass loading cases --- src/_test-help-class.ts | 16 ++++++++++++++ test/util.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/_test-help-class.ts create mode 100644 test/util.test.ts diff --git a/src/_test-help-class.ts b/src/_test-help-class.ts new file mode 100644 index 00000000..82678186 --- /dev/null +++ b/src/_test-help-class.ts @@ -0,0 +1,16 @@ +// used in tests for getHelpClass +import {HelpBase} from '.' + +export default class extends HelpBase { + showHelp() { + console.log('help') + } + + showCommandHelp() { + console.log('command help') + } + + getCommandHelpForReadme() { + return 'help for readme' + } +} diff --git a/test/util.test.ts b/test/util.test.ts new file mode 100644 index 00000000..5f3dddee --- /dev/null +++ b/test/util.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable max-nested-callbacks */ +import {resolve} from 'path' +import * as Config from '@oclif/config' +import {expect, test} from '@oclif/test' +import {getHelpClass} from '../src/util' +import configuredHelpClass from '../src/_test-help-class' + +describe('util', () => { + let config: Config.IConfig + + beforeEach(async () => { + config = await Config.load() + }) + + describe('#getHelpClass', () => { + test + .it('defaults to the class exported', () => { + // eslint-disable-next-line node/no-extraneous-require + const defaultHelpClass = require('@oclif/plugin-help').default + delete config.pjson.oclif.helpClass + + expect(defaultHelpClass).not.be.undefined + expect(getHelpClass(config)).to.deep.equal(defaultHelpClass) + }) + + test + .it('loads help class defined in pjson.oclif.helpClass', () => { + config.pjson.oclif.helpClass = './lib/_test-help-class' + config.root = resolve(__dirname, '..') + + expect(configuredHelpClass).to.not.be.undefined + expect(getHelpClass(config)).to.deep.equal(configuredHelpClass) + }) + + describe('error cases', () => { + test + .it('throws an error when failing to load the default help class', () => { + delete config.pjson.oclif.helpClass + expect(() => getHelpClass(config, 'does-not-exist-default-plugin')).to.throw('Could not load a help class, consider installing the @oclif/plugin-help package, failed with message:') + }) + + test + .it('throws an error when failing to load the help class defined in pjson.oclif.helpClass', () => { + config.pjson.oclif.helpClass = './lib/does-not-exist-help-class' + expect(() => getHelpClass(config)).to.throw('Unable to load configured help class "./lib/does-not-exist-help-class", failed with message:') + }) + }) + }) +}) From 9abfa5525e08a93597fc6b2a96097fb50e85ed91 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 13:37:38 -0400 Subject: [PATCH 26/42] Fix extract helper function names --- src/util.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/util.ts b/src/util.ts index ea047cc7..68cd2fa8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -54,12 +54,12 @@ interface HelpBaseDerived { new(config: IConfig, opts?: Partial): HelpBase; } -export function extractClass(config: IConfig, classPath: string): HelpBaseDerived { +function extractExport(config: IConfig, classPath: string): HelpBaseDerived { const helpClassPath = tsPath(config.root, classPath) return require(helpClassPath) as HelpBaseDerived } -export function extractExport(exported: any): HelpBaseDerived { +function extractClass(exported: any): HelpBaseDerived { return exported && exported.default ? exported.default : exported } @@ -69,8 +69,8 @@ export function getHelpClass(config: IConfig, defaultClass = '@oclif/plugin-help if (configuredClass) { try { - const exported = extractClass(config, configuredClass) - return extractExport(exported) as HelpBaseDerived + const exported = extractExport(config, configuredClass) + return extractClass(exported) as HelpBaseDerived } catch (error) { throw new Error(`Unable to load configured help class "${configuredClass}", failed with message:\n${error.message}`) } @@ -78,7 +78,7 @@ export function getHelpClass(config: IConfig, defaultClass = '@oclif/plugin-help try { const exported = require(defaultClass) - return extractExport(exported) as HelpBaseDerived + return extractClass(exported) as HelpBaseDerived } catch (error) { throw new Error(`Could not load a help class, consider installing the @oclif/plugin-help package, failed with message:\n${error.message}`) } From 717968c715e449c5a654e9437df056a983597306 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 15:08:45 -0400 Subject: [PATCH 27/42] Ensure @oclif/config 1.15.1 is used --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 636c66a4..583a8091 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5d119e8a..243bcbc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -94,7 +94,7 @@ debug "^4.1.1" tslib "^1.9.3" -"@oclif/config@^1.13.0": +"@oclif/config@^1.15.1": version "1.15.1" resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.15.1.tgz#39950c70811ab82d75bb3cdb33679ed0a4c21c57" integrity sha512-GdyHpEZuWlfU8GSaZoiywtfVBsPcfYn1KuSLT1JTfvZGpPG6vShcGr24YZ3HG2jXUFlIuAqDcYlTzOrqOdTPNQ== From 8d94e15d178a546a832d7143cdbc41e68062dae8 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 16:56:09 -0400 Subject: [PATCH 28/42] Remove the readme-related methods --- src/index.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index f39f361b..67b0c063 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,12 +53,6 @@ export abstract class HelpBase { * @param topics */ public abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; - - /** - * Returned string is used for given the command in readme generation - * @param command - */ - public abstract getCommandHelpForReadme(command: Config.Command): string; } export default class Help extends HelpBase { @@ -155,19 +149,6 @@ export default class Help extends HelpBase { return output + '\n' } - /** - * @param {Command} command to generate help for - * @returns {string} help string for the given command - */ - public getCommandHelpForReadme(command: Config.Command): string { - return this.command(command) - } - - /** - * @deprecated replaced by getCommandHelpForReadme - * @param {Command} command to generate help for - * @returns {string} help string for the given command - */ public command(command: Config.Command): string { const help = new CommandHelp(command, this.config, this.opts) return help.generate() From f9d80ec809dd81f14a0f6483e81e53d73f21c740 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 16:58:26 -0400 Subject: [PATCH 29/42] Rename to format* for the formatting methods --- src/index.ts | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 67b0c063..42797b56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,44 +93,49 @@ export default class Help extends HelpBase { error(`command ${subject} not found`) } + public showCommandHelp(command: Config.Command, topics: Config.Topic[]) { + const name = command.id + const depth = name.split(':').length + topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const title = command.description && this.render(command.description).split('\n')[0] + if (title) console.log(title + '\n') + console.log(this.formatCommand(command)) + console.log('') + if (topics.length > 0) { + console.log(this.formatTopics(topics)) + console.log('') + } + } + protected showRootHelp(topics: Config.Topic[]) { - console.log(this.root()) + console.log(this.formatRoot()) console.log('') if (!this.opts.all) { topics = topics.filter(t => !t.name.includes(':')) } - console.log(this.topics(topics)) + console.log(this.formatTopics(topics)) console.log('') } protected showTopicHelp(topic: Config.Topic, siblingTopics: Config.Topic[]) { - console.log(this.topic(topic)) + console.log(this.formatTopic(topic)) if (siblingTopics.length > 0) { - console.log(this.topics(siblingTopics)) - console.log('') - } - } - - public showCommandHelp(command: Config.Command, topics: Config.Topic[]) { - const name = command.id - const depth = name.split(':').length - topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.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('') - if (topics.length > 0) { - console.log(this.topics(topics)) + console.log(this.formatTopics(siblingTopics)) console.log('') } } - protected root(): string { + protected formatRoot(): string { const help = new RootHelp(this.config, this.opts) return help.root() } - protected topic(topic: Config.Topic): string { + protected formatCommand(command: Config.Command): string { + const help = new CommandHelp(command, this.config, this.opts) + return help.generate() + } + + 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') @@ -149,12 +154,7 @@ export default class Help extends HelpBase { return output + '\n' } - public command(command: Config.Command): string { - const help = new CommandHelp(command, this.config, this.opts) - return help.generate() - } - - protected topics(topics: Config.Topic[]): string | undefined { + protected formatTopics(topics: Config.Topic[]): string | undefined { if (topics.length === 0) return const body = renderList(topics.map(c => [ c.name, From 7b730aa0d3daf8db65943c0c076fc814897395b3 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 17:17:05 -0400 Subject: [PATCH 30/42] Change up tests to use the format methods --- test/{command.test.ts => format-command.test.ts} | 15 +++++++++++---- test/{root.test.ts => format-root.test.ts} | 8 ++++---- test/{topic.test.ts => format-topic.test.ts} | 8 ++++---- test/{topics.test.ts => format-topics.test.ts} | 8 ++++---- test/show-help.test.ts | 5 +++-- 5 files changed, 26 insertions(+), 18 deletions(-) rename test/{command.test.ts => format-command.test.ts} (96%) rename test/{root.test.ts => format-root.test.ts} (96%) rename test/{topic.test.ts => format-topic.test.ts} (92%) rename test/{topics.test.ts => format-topics.test.ts} (89%) diff --git a/test/command.test.ts b/test/format-command.test.ts similarity index 96% rename from test/command.test.ts rename to test/format-command.test.ts index f321b334..96bc8d19 100644 --- a/test/command.test.ts +++ b/test/format-command.test.ts @@ -13,13 +13,20 @@ class Command extends Base { } } +// extensions to expose method as public for testing +class TestHelp extends Help { + public formatCommand(command: Config.Command) { + return super.formatCommand(command) + } +} + const test = base .loadConfig() -.add('help', ctx => new Help(ctx.config)) +.add('help', ctx => new TestHelp(ctx.config)) .register('commandHelp', (command?: any) => ({ - run(ctx: {help: Help; commandHelp: string; expectation: string}) { + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { const cached = Config.Command.toCached(command!, {} as any) - const help = ctx.help.command(cached) + const help = ctx.help.formatCommand(cached) if (process.env.TEST_OUTPUT === '1') { console.log(help) } @@ -28,7 +35,7 @@ const test = base }, })) -describe('command help', () => { +describe('formatCommand', () => { test .commandHelp(class extends Command { static id = 'apps:create' diff --git a/test/root.test.ts b/test/format-root.test.ts similarity index 96% rename from test/root.test.ts rename to test/format-root.test.ts index 13e7fa11..8350761b 100644 --- a/test/root.test.ts +++ b/test/format-root.test.ts @@ -10,8 +10,8 @@ const UA = `@oclif/plugin-help/${VERSION} ${process.platform}-${process.arch} no // extensions to expose method as public for testing class TestHelp extends Help { - public root() { - return super.root() + public formatRoot() { + return super.formatRoot() } } @@ -22,7 +22,7 @@ const test = base const config = ctxOverride ? ctxOverride(ctx.config) : ctx.config const help = new TestHelp(config) - const root = help.root() + const root = help.formatRoot() if (process.env.TEST_OUTPUT === '1') { console.log(help) } @@ -30,7 +30,7 @@ const test = base }, })) -describe('root help', () => { +describe('formatRoot', () => { test .rootHelp() .it('renders the root help', ctx => expect(ctx.commandHelp).to.equal(`standard help for oclif diff --git a/test/topic.test.ts b/test/format-topic.test.ts similarity index 92% rename from test/topic.test.ts rename to test/format-topic.test.ts index 63ce5143..3b14e468 100644 --- a/test/topic.test.ts +++ b/test/format-topic.test.ts @@ -8,8 +8,8 @@ import Help from '../src' // extensions to expose method as public for testing class TestHelp extends Help { - public topic(topic: Config.Topic) { - return super.topic(topic) + public formatTopic(topic: Config.Topic) { + return super.formatTopic(topic) } } @@ -20,7 +20,7 @@ const test = base }) .register('topicHelp', (topic: Config.Topic) => ({ run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { - const topicHelpOutput = ctx.help.topic(topic) + const topicHelpOutput = ctx.help.formatTopic(topic) if (process.env.TEST_OUTPUT === '1') { console.log(topicHelpOutput) } @@ -29,7 +29,7 @@ const test = base }, })) -describe('topic help', () => { +describe('formatHelp', () => { test .topicHelp({ name: 'topic', diff --git a/test/topics.test.ts b/test/format-topics.test.ts similarity index 89% rename from test/topics.test.ts rename to test/format-topics.test.ts index 405c9337..7561b333 100644 --- a/test/topics.test.ts +++ b/test/format-topics.test.ts @@ -8,8 +8,8 @@ import Help from '../src' // extensions to expose method as public for testing class TestHelp extends Help { - public topics(topics: Config.Topic[]) { - return super.topics(topics) + public formatTopics(topics: Config.Topic[]) { + return super.formatTopics(topics) } } @@ -18,7 +18,7 @@ const test = base .add('help', ctx => new TestHelp(ctx.config)) .register('topicsHelp', (topics: Config.Topic[]) => ({ run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { - const topicsHelpOutput = ctx.help.topics(topics) || '' + const topicsHelpOutput = ctx.help.formatTopics(topics) || '' if (process.env.TEST_OUTPUT === '1') { console.log(topicsHelpOutput) @@ -29,7 +29,7 @@ const test = base }, })) -describe('topics help', () => { +describe('formatTopics', () => { test .topicsHelp([{ name: 'topic', diff --git a/test/show-help.test.ts b/test/show-help.test.ts index a87264c2..d8959395 100644 --- a/test/show-help.test.ts +++ b/test/show-help.test.ts @@ -1,6 +1,7 @@ import * as Config from '@oclif/config' import {expect, test as base} from '@oclif/test' import {stub, SinonStub} from 'sinon' +import * as path from 'path' const g: any = global g.columns = 80 @@ -18,7 +19,6 @@ class TestHelp extends Help { } const test = base -.loadConfig() .register('setupHelp', () => ({ async run(ctx: { help: TestHelp; stubs: { [k: string]: SinonStub }}) { ctx.stubs = { @@ -27,7 +27,8 @@ const test = base showCommandHelp: stub(TestHelp.prototype, 'showCommandHelp').returns(), } - const config = await Config.load() + // use devPlugins: true to bring in plugins-plugin with topic commands for testing + const config = await Config.load({devPlugins: true, root: path.resolve(__dirname, '..')}) ctx.help = new TestHelp(config) }, finally(ctx) { From 5bfa82996e36cf62b9382208248ffb53469c313c Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Wed, 15 Apr 2020 17:30:15 -0400 Subject: [PATCH 31/42] Fix variable naming --- src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 42797b56..772ba78b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,8 +85,8 @@ export default class Help extends HelpBase { if (topic) { const name = topic.name const depth = name.split(':').length - const siblingTopics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - this.showTopicHelp(topic, siblingTopics) + const subTopics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + this.showTopicHelp(topic, subTopics) return } @@ -117,10 +117,10 @@ export default class Help extends HelpBase { console.log('') } - protected showTopicHelp(topic: Config.Topic, siblingTopics: Config.Topic[]) { + protected showTopicHelp(topic: Config.Topic, subTopics: Config.Topic[]) { console.log(this.formatTopic(topic)) - if (siblingTopics.length > 0) { - console.log(this.formatTopics(siblingTopics)) + if (subTopics.length > 0) { + console.log(this.formatTopics(subTopics)) console.log('') } } From 9aaa951003340979f685a7a383d550899eb6336a Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Thu, 16 Apr 2020 09:55:20 -0400 Subject: [PATCH 32/42] Add disclaimer on test help class included in src --- src/_test-help-class.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_test-help-class.ts b/src/_test-help-class.ts index 82678186..82a593d6 100644 --- a/src/_test-help-class.ts +++ b/src/_test-help-class.ts @@ -1,4 +1,8 @@ -// used in tests for getHelpClass +// `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 { From 8d8b50f306c0c4b3de0db9a002bfc0781664ca81 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Fri, 17 Apr 2020 16:12:20 -0400 Subject: [PATCH 33/42] Add subtopic and topic command lists for a topic Use filteredTopics getter to clean up showHelp --- src/index.ts | 83 ++++++++++++++++++------ test/format-commands.test.ts | 60 +++++++++++++++++ test/format-topics.test.ts | 4 +- test/helpers/mocks.ts | 98 ++++++++++++++++++++++++++++ test/show-help.test.ts | 121 +++++++++++++++++++++++++++++++++-- 5 files changed, 342 insertions(+), 24 deletions(-) create mode 100644 test/format-commands.test.ts create mode 100644 test/helpers/mocks.ts diff --git a/src/index.ts b/src/index.ts index 772ba78b..75a5dc7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,58 +58,64 @@ export abstract class HelpBase { export default class Help extends HelpBase { render: (input: string) => string + protected get filteredTopics() { + let topics = this.config.topics + topics = topics.filter(t => this.opts.all || !t.hidden) + topics = sortBy(topics, t => t.name) + topics = uniqBy(topics, t => t.name) + + return topics + } + constructor(config: Config.IConfig, opts: Partial = {}) { super(config, opts) this.render = template(this) } public showHelp(argv: string[]) { - let topics = this.config.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) + if (!subject) { - this.showRootHelp(topics) + this.showRootHelp() return } const command = this.config.findCommand(subject) if (command) { - this.showCommandHelp(command, topics) + this.showCommandHelp(command) return } const topic = this.config.findTopic(subject) if (topic) { - const name = topic.name - const depth = name.split(':').length - const subTopics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - this.showTopicHelp(topic, subTopics) + this.showTopicHelp(topic) return } error(`command ${subject} not found`) } - public 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 topics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) const title = command.description && this.render(command.description).split('\n')[0] if (title) console.log(title + '\n') console.log(this.formatCommand(command)) console.log('') + if (topics.length > 0) { console.log(this.formatTopics(topics)) console.log('') } } - protected showRootHelp(topics: Config.Topic[]) { + protected showRootHelp() { + let topics = this.filteredTopics + console.log(this.formatRoot()) console.log('') + if (!this.opts.all) { topics = topics.filter(t => !t.name.includes(':')) } @@ -117,12 +123,35 @@ export default class Help extends HelpBase { console.log('') } - protected showTopicHelp(topic: Config.Topic, subTopics: Config.Topic[]) { + protected showTopicHelp(topic: Config.Topic) { + const name = topic.name + const depth = name.split(':').length + const topicsAndCommands = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const topicCommands: Config.Command[] = [] + const subTopics: Config.Topic[] = [] + + topicsAndCommands.forEach((topic: Config.Topic) => { + const hasSubTopics = Boolean(this.filteredTopics.find(t => t.name.startsWith(topic.name + ':') && t.name.split(':').length > depth)) + if (hasSubTopics) { + subTopics.push(topic) + } + + const command = this.config.findCommand(topic.name) + if (command) { + topicCommands.push(command) + } + }) + console.log(this.formatTopic(topic)) if (subTopics.length > 0) { console.log(this.formatTopics(subTopics)) console.log('') } + + if (topicCommands.length > 0) { + console.log(this.formatCommands(topicCommands)) + console.log('') + } } protected formatRoot(): string { @@ -135,6 +164,24 @@ export default class Help extends HelpBase { 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] @@ -154,8 +201,8 @@ export default class Help extends HelpBase { return output + '\n' } - protected formatTopics(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]), @@ -165,7 +212,7 @@ export default class Help extends HelpBase { maxWidth: this.opts.maxWidth - 2, }) return [ - bold('COMMANDS'), + bold('TOPICS'), indent(body, 2), ].join('\n') } diff --git a/test/format-commands.test.ts b/test/format-commands.test.ts new file mode 100644 index 00000000..2605f480 --- /dev/null +++ b/test/format-commands.test.ts @@ -0,0 +1,60 @@ +import {Command} from '@oclif/command' +import * as Config from '@oclif/config' +import {expect, test as base} from '@oclif/test' +import stripAnsi = require('strip-ansi') + +const g: any = global +g.columns = 80 +import Help from '../src' +import { AppsDestroy, AppsCreate } from './helpers/mocks' + +// extensions to expose method as public for testing +class TestHelp extends Help { + public formatCommands(commands: Config.Command[]) { + return super.formatCommands(commands) + } +} + +const test = base +.loadConfig() +.add('help', ctx => new TestHelp(ctx.config)) +.register('formatCommands', (commands: Config.Command[] = []) => ({ + run(ctx: {help: TestHelp; output: string}) { + const help = ctx.help.formatCommands(commands) + if (process.env.TEST_OUTPUT === '1') { + console.log(help) + } + + ctx.output = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') + }, +})) + +describe('formatCommand', () => { + test + .formatCommands([]) + .it('outputs an empty string when no commands are given', (ctx: any) => expect(ctx.output).to.equal('')) + + test + .formatCommands([AppsDestroy, AppsCreate]) + .it('shows a list of the provided commands', (ctx: any) => expect(ctx.output).to.equal(`COMMANDS + apps:destroy Destroy an app + apps:create Create an app`)) + + test + .formatCommands([class extends Command { + static id = 'hello:world' + + static description = 'This is a very long command description that should wrap after too many characters have been entered' + + static flags = {} + + static args = [] + + async run() { + 'run' + } + }]) + .it('handles wraps long descriptions', (ctx: any) => expect(ctx.output).to.equal(`COMMANDS + hello:world This is a very long command description that should wrap after + too many characters have been entered`)) +}) diff --git a/test/format-topics.test.ts b/test/format-topics.test.ts index 7561b333..a7106d5a 100644 --- a/test/format-topics.test.ts +++ b/test/format-topics.test.ts @@ -35,7 +35,7 @@ describe('formatTopics', () => { name: 'topic', description: 'this is a description of my topic', }]) - .it('shows ouputs a single topic in the list', ctx => expect(ctx.commandHelp).to.equal(`COMMANDS + .it('shows ouputs a single topic in the list', ctx => expect(ctx.commandHelp).to.equal(`TOPICS topic this is a description of my topic`)) test @@ -49,7 +49,7 @@ describe('formatTopics', () => { name: 'thirdtopic', description: 'description for thirdtopic', }]) - .it('shows ouputs a single topic in the list', ctx => expect(ctx.commandHelp).to.equal(`COMMANDS + .it('shows ouputs for multiple topics in the list', ctx => expect(ctx.commandHelp).to.equal(`TOPICS topic this is a description of my topic othertopic here we have a description for othertopic thirdtopic description for thirdtopic`)) diff --git a/test/helpers/mocks.ts b/test/helpers/mocks.ts new file mode 100644 index 00000000..66ba7dc4 --- /dev/null +++ b/test/helpers/mocks.ts @@ -0,0 +1,98 @@ +import {Command} from '@oclif/command' +import {Topic} from '@oclif/config' + +// apps + +export class AppsCreate extends Command { + static id = 'apps:create' + + static description = `Create an app + this does not show up`; + + static flags = {}; + + static args = []; + + async run() { + 'run' + } +} + +export class AppsDestroy extends Command { + static id = 'apps:destroy' + + static description = `Destroy an app + this does not show up`; + + static flags: {}; + + static args = []; + + async run() { + 'run' + } +} + +export const AppsTopic: Topic = { + name: 'apps', + description: 'This topic is for the apps topic', +} + +// apps:admin + +export const AppsAdminTopic: Topic = { + name: 'apps:admin', + description: 'This topic is for the apps topic', +} + +export class AppsAdminIndex extends Command { + static id = 'apps:admin' + + static description = `List of admins for an app + this does not show up`; + + static flags: {}; + + static args = []; + + async run() { + 'run' + } +} + +export class AppsAdminAdd extends Command { + static id = 'apps:admin:add' + + static description = `Add user to an app + this does not show up`; + + static flags: {}; + + static args = []; + + async run() { + 'run' + } +} + +// db + +export class DbCreate extends Command { + static id = 'db:create' + + static description = `Create a db + this does not show up`; + + static flags = {}; + + static args = []; + + async run() { + 'run' + } +} + +export const DbTopic: Topic = { + name: 'db', + description: 'This topic is for the db topic', +} diff --git a/test/show-help.test.ts b/test/show-help.test.ts index d8959395..f3ee2294 100644 --- a/test/show-help.test.ts +++ b/test/show-help.test.ts @@ -6,15 +6,16 @@ import * as path from 'path' const g: any = global g.columns = 80 import Help from '../src' +import { AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic } from './helpers/mocks' // extension makes previously protected methods public class TestHelp extends Help { - public showRootHelp(topics: Config.Topic[]) { - return super.showRootHelp(topics) + public showRootHelp() { + return super.showRootHelp() } - public showTopicHelp(topic: Config.Topic, siblingTopics: Config.Topic[]) { - return super.showTopicHelp(topic, siblingTopics) + public showTopicHelp(topic: Config.Topic) { + return super.showTopicHelp(topic) } } @@ -46,6 +47,118 @@ const test = base }, })) +describe('showHelp for a topic', () => { + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsCreate, AppsDestroy], + topics: [AppsTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps']) + }) + .it('shows topic help with commands', ({stdout}) => { + expect(stdout.trim()).to.equal(`This topic is for the apps topic + +USAGE + $ oclif apps:COMMAND + +COMMANDS + apps:create Create an app + apps:destroy Destroy an app`) + }) + + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsCreate, AppsDestroy, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps']) + }) + .it('shows topic help with topic and commands', ({stdout}) => { + expect(stdout.trim()).to.equal(`This topic is for the apps topic + +USAGE + $ oclif apps:COMMAND + +TOPICS + apps:admin This topic is for the apps topic + +COMMANDS + apps:create Create an app + apps:destroy Destroy an app`) + }) + + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsCreate, AppsDestroy, AppsAdminIndex, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps']) + }) + .it('shows topic help with topic and commands and topic command', ({stdout}) => { + expect(stdout.trim()).to.equal(`This topic is for the apps topic + +USAGE + $ oclif apps:COMMAND + +TOPICS + apps:admin This topic is for the apps topic + +COMMANDS + apps:admin List of admins for an app + apps:create Create an app + apps:destroy Destroy an app`) + }) + + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsCreate, AppsDestroy, AppsAdminAdd, DbCreate], + topics: [AppsTopic, AppsAdminTopic, DbTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps']) + }) + .it('ignores other topics and commands', ({stdout}) => { + expect(stdout.trim()).to.equal(`This topic is for the apps topic + +USAGE + $ oclif apps:COMMAND + +TOPICS + apps:admin This topic is for the apps topic + +COMMANDS + apps:create Create an app + apps:destroy Destroy an app`) + }) +}) + describe('showHelp routing', () => { describe('shows root help', () => { test From bad63972a31a99f2ef3bce45b3f71297fa2f594f Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 11:11:17 -0400 Subject: [PATCH 34/42] Add topic/command split to root help --- src/index.ts | 71 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/index.ts b/src/index.ts index 75a5dc7c..04f62313 100644 --- a/src/index.ts +++ b/src/index.ts @@ -111,45 +111,44 @@ export default class Help extends HelpBase { } protected showRootHelp() { - let topics = this.filteredTopics + let rootChildren = this.filteredTopics console.log(this.formatRoot()) console.log('') if (!this.opts.all) { - topics = topics.filter(t => !t.name.includes(':')) + rootChildren = rootChildren.filter(t => !t.name.includes(':')) + } + + const {topics, commands} = this.categorizeTopicsAndCommands(rootChildren) + + if (topics.length > 0) { + console.log(this.formatTopics(topics)) + console.log('') + } + + if (commands.length > 0) { + console.log(this.formatCommands(commands)) + console.log('') } - console.log(this.formatTopics(topics)) - console.log('') } protected showTopicHelp(topic: Config.Topic) { const name = topic.name const depth = name.split(':').length - const topicsAndCommands = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const topicCommands: Config.Command[] = [] - const subTopics: Config.Topic[] = [] - - topicsAndCommands.forEach((topic: Config.Topic) => { - const hasSubTopics = Boolean(this.filteredTopics.find(t => t.name.startsWith(topic.name + ':') && t.name.split(':').length > depth)) - if (hasSubTopics) { - subTopics.push(topic) - } - const command = this.config.findCommand(topic.name) - if (command) { - topicCommands.push(command) - } - }) + const topicChildren = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const {topics, commands} = this.categorizeTopicsAndCommands(topicChildren) console.log(this.formatTopic(topic)) - if (subTopics.length > 0) { - console.log(this.formatTopics(subTopics)) + + if (topics.length > 0) { + console.log(this.formatTopics(topics)) console.log('') } - if (topicCommands.length > 0) { - console.log(this.formatCommands(topicCommands)) + if (commands.length > 0) { + console.log(this.formatCommands(commands)) console.log('') } } @@ -216,4 +215,32 @@ export default class Help extends HelpBase { indent(body, 2), ].join('\n') } + + /** + * @description config.topics are a list of commands and topics mixed. + * For the purposes of this help plugin, it's important to categorize + * which are categorized by a topic or a command. A topic has child + * commands. A topic may also be a command. A command is "runnable". + */ + protected categorizeTopicsAndCommands(configTopics: Config.Topic[]) { + const commands: Config.Command[] = [] + const topics: Config.Topic[] = [] + + configTopics.forEach((topic: Config.Topic) => { + // if the current topic can find children that include the same name with a ":" + // then it should have children, ie: "apps" has children if "apps:" exists in any + // config topic like "apps:create" + const hasChildren = Boolean(this.filteredTopics.find(t => t.name.startsWith(topic.name + ':'))) + if (hasChildren) { + topics.push(topic) + } + + const command = this.config.findCommand(topic.name) + if (command) { + commands.push(command) + } + }) + + return {topics, commands} + } } From 965cc760731ea0c5e2fa34285c5f26af0034936a Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 11:31:31 -0400 Subject: [PATCH 35/42] Fix eslint jsdoc --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 04f62313..faf611fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -217,6 +217,10 @@ export default class Help extends HelpBase { } /** + * @arg {array} configTopics an array of topics from Config, which is a + * mix of commands and topics + * @returns {object} with properties `topics` and `commands`, each are + * an array of the categorized based on the config topics given * @description config.topics are a list of commands and topics mixed. * For the purposes of this help plugin, it's important to categorize * which are categorized by a topic or a command. A topic has child From a71b774a3924583307851de7c4ee43560a76594f Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 13:37:56 -0400 Subject: [PATCH 36/42] Add back deprecated command in case non-pinned dev-cli uses the plugin via npx --- src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/index.ts b/src/index.ts index faf611fb..9cecdad7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -247,4 +247,13 @@ export default class Help extends HelpBase { return {topics, commands} } + + /** + * @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) + } } From c7563b74d95d2b197502b49e4254c17ae662301d Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 15:46:08 -0400 Subject: [PATCH 37/42] Bump dependencies --- package.json | 6 ++--- yarn.lock | 72 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 583a8091..a9c7937c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -34,8 +34,8 @@ "globby": "^9.0.0", "mocha": "^5.2.0", "sinon": "^9.0.1", - "ts-node": "^8.0.2", - "typescript": "^3.7.2" + "ts-node": "^8.8.2", + "typescript": "^3.8.3" }, "engines": { "node": ">=8.0.0" diff --git a/yarn.lock b/yarn.lock index 243bcbc9..022e178e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,15 +86,19 @@ debug "^4.1.1" semver "^5.6.0" -"@oclif/config@^1.10.2": - version "1.12.0" - resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.12.0.tgz#384c1502927d03581862ae3b68d8790b17590a7f" - integrity sha512-RB6A+N7Dq5DcFOQEhPpB8DdXtMQm2VDgdtgBKUdot815tj4gW7nDmRZBEwU85x4Xhep7Dx3tpaXobA6bFlSOWg== +"@oclif/command@^1.5.20": + version "1.5.20" + resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.5.20.tgz#bb0693586d7d66a457c49b719e394c02ff0169a7" + integrity sha512-lzst5RU/STfoutJJv4TLE/cm1WtW3xy6Aqvqy3r1lPsGdNifgbEq4dCOYyc/ZEuhV/IStQLDFTnAlqTdolkz1Q== dependencies: + "@oclif/config" "^1" + "@oclif/errors" "^1.2.2" + "@oclif/parser" "^3.8.3" + "@oclif/plugin-help" "^2" debug "^4.1.1" - tslib "^1.9.3" + semver "^5.6.0" -"@oclif/config@^1.15.1": +"@oclif/config@^1", "@oclif/config@^1.15.1": version "1.15.1" resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.15.1.tgz#39950c70811ab82d75bb3cdb33679ed0a4c21c57" integrity sha512-GdyHpEZuWlfU8GSaZoiywtfVBsPcfYn1KuSLT1JTfvZGpPG6vShcGr24YZ3HG2jXUFlIuAqDcYlTzOrqOdTPNQ== @@ -104,6 +108,14 @@ debug "^4.1.1" tslib "^1.9.3" +"@oclif/config@^1.10.2": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.12.0.tgz#384c1502927d03581862ae3b68d8790b17590a7f" + integrity sha512-RB6A+N7Dq5DcFOQEhPpB8DdXtMQm2VDgdtgBKUdot815tj4gW7nDmRZBEwU85x4Xhep7Dx3tpaXobA6bFlSOWg== + dependencies: + debug "^4.1.1" + tslib "^1.9.3" + "@oclif/dev-cli@^1.21.0": version "1.21.0" resolved "https://registry.yarnpkg.com/@oclif/dev-cli/-/dev-cli-1.21.0.tgz#679fbf42b44129036a26b6b88ee867a6aaee5d87" @@ -155,7 +167,7 @@ chalk "^2.4.2" tslib "^1.9.3" -"@oclif/parser@^3.8.0": +"@oclif/parser@^3.8.0", "@oclif/parser@^3.8.3": version "3.8.4" resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.4.tgz#1a90fc770a42792e574fb896325618aebbe8c9e4" integrity sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA== @@ -164,6 +176,20 @@ chalk "^2.4.2" tslib "^1.9.3" +"@oclif/plugin-help@^2": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-2.2.3.tgz#b993041e92047f0e1762668aab04d6738ac06767" + integrity sha512-bGHUdo5e7DjPJ0vTeRBMIrfqTRDBfyR5w0MP41u0n3r7YG5p14lvMmiCXxi6WDaP2Hw5nqx3PnkAIntCKZZN7g== + dependencies: + "@oclif/command" "^1.5.13" + chalk "^2.4.1" + indent-string "^4.0.0" + lodash.template "^4.4.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + widest-line "^2.0.1" + wrap-ansi "^4.0.0" + "@oclif/plugin-help@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-2.1.4.tgz#b530fa3147d5ae91ba9c84d085f53a829b2914dc" @@ -872,12 +898,12 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -diff@3.5.0, diff@^3.1.0: +diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@^4.0.2: +diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -2839,16 +2865,16 @@ treeify@^1.1.0: resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== -ts-node@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.2.tgz#9ecdf8d782a0ca4c80d1d641cbb236af4ac1b756" - integrity sha512-MosTrinKmaAcWgO8tqMjMJB22h+sp3Rd1i4fdoWY4mhBDekOwIAKI/bzmRi7IcbCmjquccYg2gcF6NBkLgr0Tw== +ts-node@^8.8.2: + version "8.8.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" + integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== dependencies: arg "^4.1.0" - diff "^3.1.0" + diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.6" - yn "^3.0.0" + yn "3.1.1" tslib@^1: version "1.9.3" @@ -2896,10 +2922,10 @@ type-fest@^0.5.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== -typescript@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" - integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== union-value@^1.0.0: version "1.0.0" @@ -3030,7 +3056,7 @@ yarn@^1.13.0: resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.17.3.tgz#60e0b77d079eb78e753bb616f7592b51b6a9adce" integrity sha512-CgA8o7nRZaQvmeF/WBx2FC7f9W/0X59T2IaLYqgMo6637wfp5mMEsM3YXoJtKUspnpmDJKl/gGFhnqS+sON7hA== -yn@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.0.0.tgz#0073c6b56e92aed652fbdfd62431f2d6b9a7a091" - integrity sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q== +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From f7d199149a464cfeff1996a14cff069d141fee21 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 18:38:36 -0400 Subject: [PATCH 38/42] Rename mocks to fixtures --- test/format-commands.test.ts | 2 +- test/helpers/{mocks.ts => fixtures.ts} | 0 test/show-help.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename test/helpers/{mocks.ts => fixtures.ts} (100%) diff --git a/test/format-commands.test.ts b/test/format-commands.test.ts index 2605f480..8c41baf9 100644 --- a/test/format-commands.test.ts +++ b/test/format-commands.test.ts @@ -6,7 +6,7 @@ import stripAnsi = require('strip-ansi') const g: any = global g.columns = 80 import Help from '../src' -import { AppsDestroy, AppsCreate } from './helpers/mocks' +import {AppsDestroy, AppsCreate} from './helpers/fixtures' // extensions to expose method as public for testing class TestHelp extends Help { diff --git a/test/helpers/mocks.ts b/test/helpers/fixtures.ts similarity index 100% rename from test/helpers/mocks.ts rename to test/helpers/fixtures.ts diff --git a/test/show-help.test.ts b/test/show-help.test.ts index f3ee2294..0ad55a1e 100644 --- a/test/show-help.test.ts +++ b/test/show-help.test.ts @@ -6,7 +6,7 @@ import * as path from 'path' const g: any = global g.columns = 80 import Help from '../src' -import { AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic } from './helpers/mocks' +import {AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic} from './helpers/fixtures' // extension makes previously protected methods public class TestHelp extends Help { From c520661a3ba846f456feb3657f89055cee5ee517 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Mon, 20 Apr 2020 18:42:06 -0400 Subject: [PATCH 39/42] Make opts and config protected --- src/index.ts | 4 ++-- test/show-help.test.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9cecdad7..0755cf50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,9 +37,9 @@ export abstract class HelpBase { this.opts = {maxWidth: stdtermwidth, ...opts} } - public config: Config.IConfig + protected config: Config.IConfig - public opts: HelpOptions + protected opts: HelpOptions /** * Show help, used in multi-command CLIs diff --git a/test/show-help.test.ts b/test/show-help.test.ts index 0ad55a1e..405741f5 100644 --- a/test/show-help.test.ts +++ b/test/show-help.test.ts @@ -10,6 +10,8 @@ import {AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAd // extension makes previously protected methods public class TestHelp extends Help { + public config: any; + public showRootHelp() { return super.showRootHelp() } From 9dcf818918b1cec6ce740f1a9f19ac6b53049564 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 21 Apr 2020 14:46:19 -0400 Subject: [PATCH 40/42] Add tests around listed split topic and command help --- src/index.ts | 10 +++- test/helpers/fixtures.ts | 25 +++++++-- test/show-help.test.ts | 117 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0755cf50..0a9b9c26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,6 @@ export default class Help extends HelpBase { public showHelp(argv: string[]) { const subject = getHelpSubject(argv) - if (!subject) { this.showRootHelp() return @@ -98,16 +97,23 @@ export default class Help extends HelpBase { public showCommandHelp(command: Config.Command) { const name = command.id const depth = name.split(':').length - const topics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const rootChildren = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) const title = command.description && this.render(command.description).split('\n')[0] if (title) console.log(title + '\n') console.log(this.formatCommand(command)) console.log('') + const {topics, commands} = this.categorizeTopicsAndCommands(rootChildren) + if (topics.length > 0) { console.log(this.formatTopics(topics)) console.log('') } + + if (commands.length > 0) { + console.log(this.formatCommands(commands)) + console.log('') + } } protected showRootHelp() { diff --git a/test/helpers/fixtures.ts b/test/helpers/fixtures.ts index 66ba7dc4..6dc0ee10 100644 --- a/test/helpers/fixtures.ts +++ b/test/helpers/fixtures.ts @@ -7,7 +7,7 @@ export class AppsCreate extends Command { static id = 'apps:create' static description = `Create an app - this does not show up`; + this only shows up in command help under DESCRIPTION`; static flags = {}; @@ -22,7 +22,22 @@ export class AppsDestroy extends Command { static id = 'apps:destroy' static description = `Destroy an app - this does not show up`; + this only shows up in command help under DESCRIPTION`; + + static flags: {}; + + static args = []; + + async run() { + 'run' + } +} + +export class AppsIndex extends Command { + static id = 'apps' + + static description = `List all apps (app index command) + this only shows up in command help under DESCRIPTION`; static flags: {}; @@ -49,7 +64,7 @@ export class AppsAdminIndex extends Command { static id = 'apps:admin' static description = `List of admins for an app - this does not show up`; + this only shows up in command help under DESCRIPTION`; static flags: {}; @@ -64,7 +79,7 @@ export class AppsAdminAdd extends Command { static id = 'apps:admin:add' static description = `Add user to an app - this does not show up`; + this only shows up in command help under DESCRIPTION`; static flags: {}; @@ -81,7 +96,7 @@ export class DbCreate extends Command { static id = 'db:create' static description = `Create a db - this does not show up`; + this only shows up in command help under DESCRIPTION`; static flags = {}; diff --git a/test/show-help.test.ts b/test/show-help.test.ts index 405741f5..5720f793 100644 --- a/test/show-help.test.ts +++ b/test/show-help.test.ts @@ -6,7 +6,7 @@ import * as path from 'path' const g: any = global g.columns = 80 import Help from '../src' -import {AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic} from './helpers/fixtures' +import {AppsIndex, AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic} from './helpers/fixtures' // extension makes previously protected methods public class TestHelp extends Help { @@ -49,6 +49,65 @@ const test = base }, })) +describe('showHelp for root', () => { + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsIndex, AppsCreate, AppsDestroy], + topics: [], + }] + + const help = new TestHelp(config) + help.showHelp([]) + }) + .it('shows a command and topic when the index has siblings', ({stdout, config}) => { + expect(stdout.trim()).to.equal(`standard help for oclif + +VERSION + ${config.userAgent} + +USAGE + $ oclif [COMMAND] + +TOPICS + apps List all apps (app index command) + +COMMANDS + apps List all apps (app index command)`) + }) + + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsIndex], + topics: [], + }] + + const help = new TestHelp(config) + help.showHelp([]) + }) + .it('shows a command only when the topic only contains an index', ({stdout, config}) => { + expect(stdout.trim()).to.equal(`standard help for oclif + +VERSION + ${config.userAgent} + +USAGE + $ oclif [COMMAND] + +COMMANDS + apps List all apps (app index command)`) + }) +}) + describe('showHelp for a topic', () => { test .loadConfig() @@ -161,6 +220,62 @@ COMMANDS }) }) +describe('showHelp for a command', () => { + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsCreate], + topics: [AppsTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps:create']) + }) + .it('shows help for a leaf (or childless) command', ({stdout}) => { + expect(stdout.trim()).to.equal(`Create an app + +USAGE + $ oclif apps:create + +DESCRIPTION + this only shows up in command help under DESCRIPTION`) + }) + + test + .loadConfig() + .stdout() + .do(ctx => { + const config = ctx.config; + + (config as any).plugins = [{ + commands: [AppsIndex, AppsCreate, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }] + + const help = new TestHelp(config) + help.showHelp(['apps']) + }) + .it('shows help for a command that has children topics and commands', ({stdout}) => { + expect(stdout.trim()).to.equal(`List all apps (app index command) + +USAGE + $ oclif apps + +DESCRIPTION + this only shows up in command help under DESCRIPTION + +TOPICS + apps:admin This topic is for the apps topic + +COMMANDS + apps:create Create an app`) + }) +}) + describe('showHelp routing', () => { describe('shows root help', () => { test From eef3ca3ac812978f556caf7bcafc78f5e969ddb0 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 21 Apr 2020 14:47:07 -0400 Subject: [PATCH 41/42] Remove categorizeTopicsAndCommands in favor of a correct _topics property config.topics will include things that are not topics. This should be fixed upstream but until then we will use _topics and when the upstream fix lands the references to this._topics can be replaced with this.config.topics --- src/index.ts | 97 ++++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0a9b9c26..678ce619 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,8 +58,32 @@ export abstract class HelpBase { export default class Help extends HelpBase { render: (input: string) => string + /* + * _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. + */ + protected 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 + }) + } + + protected get filteredCommands() { + 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 filteredTopics() { - let topics = this.config.topics + 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) @@ -97,44 +121,45 @@ export default class Help extends HelpBase { public showCommandHelp(command: Config.Command) { const name = command.id const depth = name.split(':').length - const rootChildren = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + + const subTopics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const subCommands = this.filteredCommands.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.formatCommand(command)) console.log('') - const {topics, commands} = this.categorizeTopicsAndCommands(rootChildren) - - if (topics.length > 0) { - console.log(this.formatTopics(topics)) + if (subTopics.length > 0) { + console.log(this.formatTopics(subTopics)) console.log('') } - if (commands.length > 0) { - console.log(this.formatCommands(commands)) + if (subCommands.length > 0) { + console.log(this.formatCommands(subCommands)) console.log('') } } protected showRootHelp() { - let rootChildren = this.filteredTopics + let rootTopics = this.filteredTopics + let rootCommands = this.filteredCommands console.log(this.formatRoot()) console.log('') if (!this.opts.all) { - rootChildren = rootChildren.filter(t => !t.name.includes(':')) + rootTopics = rootTopics.filter(t => !t.name.includes(':')) + rootCommands = rootCommands.filter(c => !c.id.includes(':')) } - const {topics, commands} = this.categorizeTopicsAndCommands(rootChildren) - - if (topics.length > 0) { - console.log(this.formatTopics(topics)) + if (rootTopics.length > 0) { + console.log(this.formatTopics(rootTopics)) console.log('') } - if (commands.length > 0) { - console.log(this.formatCommands(commands)) + if (rootCommands.length > 0) { + console.log(this.formatCommands(rootCommands)) console.log('') } } @@ -143,13 +168,13 @@ export default class Help extends HelpBase { const name = topic.name const depth = name.split(':').length - const topicChildren = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const {topics, commands} = this.categorizeTopicsAndCommands(topicChildren) + const subTopics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const commands = this.filteredCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) console.log(this.formatTopic(topic)) - if (topics.length > 0) { - console.log(this.formatTopics(topics)) + if (subTopics.length > 0) { + console.log(this.formatTopics(subTopics)) console.log('') } @@ -222,38 +247,6 @@ export default class Help extends HelpBase { ].join('\n') } - /** - * @arg {array} configTopics an array of topics from Config, which is a - * mix of commands and topics - * @returns {object} with properties `topics` and `commands`, each are - * an array of the categorized based on the config topics given - * @description config.topics are a list of commands and topics mixed. - * For the purposes of this help plugin, it's important to categorize - * which are categorized by a topic or a command. A topic has child - * commands. A topic may also be a command. A command is "runnable". - */ - protected categorizeTopicsAndCommands(configTopics: Config.Topic[]) { - const commands: Config.Command[] = [] - const topics: Config.Topic[] = [] - - configTopics.forEach((topic: Config.Topic) => { - // if the current topic can find children that include the same name with a ":" - // then it should have children, ie: "apps" has children if "apps:" exists in any - // config topic like "apps:create" - const hasChildren = Boolean(this.filteredTopics.find(t => t.name.startsWith(topic.name + ':'))) - if (hasChildren) { - topics.push(topic) - } - - const command = this.config.findCommand(topic.name) - if (command) { - commands.push(command) - } - }) - - return {topics, commands} - } - /** * @deprecated used for readme generation * @param {object} command The command to generate readme help for From 41198f2dbb6ffe7a2309f32552742efcdb1da6a6 Mon Sep 17 00:00:00 2001 From: Chad Carbert Date: Tue, 21 Apr 2020 17:25:07 -0400 Subject: [PATCH 42/42] Small method naming tweaks --- src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 678ce619..cc04a390 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,7 @@ export default class Help extends HelpBase { * this is fixed upstream config.topics should return *only* topics with children, * and this can be removed. */ - protected get _topics(): Config.Topic[] { + 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}:`)) @@ -72,7 +72,7 @@ export default class Help extends HelpBase { }) } - protected get filteredCommands() { + protected get sortedCommands() { let commands = this.config.commands commands = commands.filter(c => this.opts.all || !c.hidden) @@ -82,7 +82,7 @@ export default class Help extends HelpBase { return commands } - protected get filteredTopics() { + protected get sortedTopics() { let topics = this._topics topics = topics.filter(t => this.opts.all || !t.hidden) topics = sortBy(topics, t => t.name) @@ -122,8 +122,8 @@ export default class Help extends HelpBase { const name = command.id const depth = name.split(':').length - const subTopics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const subCommands = this.filteredCommands.filter(c => c.id.startsWith(name + ':') && c.id.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') @@ -142,8 +142,8 @@ export default class Help extends HelpBase { } protected showRootHelp() { - let rootTopics = this.filteredTopics - let rootCommands = this.filteredCommands + let rootTopics = this.sortedTopics + let rootCommands = this.sortedCommands console.log(this.formatRoot()) console.log('') @@ -168,8 +168,8 @@ export default class Help extends HelpBase { const name = topic.name const depth = name.split(':').length - const subTopics = this.filteredTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const commands = this.filteredCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) + 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))