diff --git a/package.json b/package.json index 5d84d6cf..ee34f957 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "@oclif/plugin-help", "description": "standard help for oclif", - "version": "2.2.3", + "version": "3.0.0-rc1.2", "author": "Jeff Dickey @jdxcode", "bugs": "https://github.com/oclif/plugin-help/issues", "dependencies": { - "@oclif/command": "^1.5.13", + "@oclif/command": "^1.5.20", + "@oclif/config": "^1.15.1", "chalk": "^2.4.1", "indent-string": "^4.0.0", "lodash.template": "^4.4.0", @@ -15,7 +16,6 @@ "wrap-ansi": "^4.0.0" }, "devDependencies": { - "@oclif/config": "^1.13.0", "@oclif/dev-cli": "^1.21.0", "@oclif/errors": "^1.2.2", "@oclif/plugin-legacy": "^1.1.3", @@ -33,8 +33,9 @@ "eslint-config-oclif-typescript": "^0.1.0", "globby": "^9.0.0", "mocha": "^5.2.0", - "ts-node": "^8.0.2", - "typescript": "^3.7.2" + "sinon": "^9.0.1", + "ts-node": "^8.8.2", + "typescript": "^3.8.3" }, "engines": { "node": ">=8.0.0" diff --git a/src/_test-help-class.ts b/src/_test-help-class.ts new file mode 100644 index 00000000..82a593d6 --- /dev/null +++ b/src/_test-help-class.ts @@ -0,0 +1,20 @@ +// `getHelpClass` tests require an oclif project for testing so +// it is re-using the setup here to be able to do a lookup for +// this sample help class file in tests, although it is not needed +// for @oclif/plugin-help itself. + +import {HelpBase} from '.' + +export default class extends HelpBase { + showHelp() { + console.log('help') + } + + showCommandHelp() { + console.log('command help') + } + + getCommandHelpForReadme() { + return 'help for readme' + } +} 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/commands/help.ts b/src/commands/help.ts index fdd385d4..06a923b6 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 {getHelpClass} 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 = 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 728483a1..cc04a390 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 {getHelpClass} from './util' const wrap = require('wrap-ansi') const { @@ -22,10 +23,6 @@ export interface HelpOptions { } function getHelpSubject(args: string[]): string | undefined { - // special case - // if (['help:help', 'help:--help', '--help:help'].includes(argv.slice(0, 2).join(':'))) { - // if (argv[0] === 'help') return 'help' - for (const arg of args) { if (arg === '--') return if (arg.startsWith('-')) continue @@ -34,77 +31,188 @@ function getHelpSubject(args: string[]): string | undefined { } } -export default class Help { - opts: HelpOptions +export abstract class HelpBase { + constructor(config: Config.IConfig, opts: Partial = {}) { + this.config = config + this.opts = {maxWidth: stdtermwidth, ...opts} + } + + protected config: Config.IConfig + + protected opts: HelpOptions + + /** + * Show help, used in multi-command CLIs + * @param args passed into your command, useful for determining which type of help to display + */ + public abstract showHelp(argv: string[]): void; + /** + * Show help for an individual command + * @param command + * @param topics + */ + public abstract showCommandHelp(command: Config.Command, topics: Config.Topic[]): void; +} + +export default class Help extends HelpBase { render: (input: string) => string - constructor(public config: Config.IConfig, opts: Partial = {}) { - this.opts = {maxWidth: stdtermwidth, ...opts} - this.render = template(this) + /* + * _topics is to work around Config.topics mistakenly including commands that do + * not have children, as well as topics. A topic has children, either commands or other topics. When + * this is fixed upstream config.topics should return *only* topics with children, + * and this can be removed. + */ + private get _topics(): Config.Topic[] { + return this.config.topics.filter((topic: Config.Topic) => { + // it is assumed a topic has a child if it has children + const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) + return hasChild + }) } - showHelp(argv: string[]) { - let topics = this.config.topics + protected get sortedCommands() { + let commands = this.config.commands + + commands = commands.filter(c => this.opts.all || !c.hidden) + commands = sortBy(commands, c => c.id) + commands = uniqBy(commands, c => c.id) + + return commands + } + + protected get sortedTopics() { + let topics = this._topics topics = topics.filter(t => this.opts.all || !t.hidden) topics = sortBy(topics, t => t.name) topics = uniqBy(topics, t => t.name) - const subject = getHelpSubject(argv) - let command: Config.Command | undefined - if (subject) { - command = this.config.findCommand(subject) + return topics + } + + constructor(config: Config.IConfig, opts: Partial = {}) { + super(config, opts) + this.render = template(this) + } + + public showHelp(argv: string[]) { + const subject = getHelpSubject(argv) + if (!subject) { + this.showRootHelp() + return } - let topic: Config.Topic | undefined - if (subject && !command) { - topic = this.config.findTopic(subject) + const command = this.config.findCommand(subject) + if (command) { + this.showCommandHelp(command) + return } - if (!subject) { - console.log(this.root()) - console.log('') - if (!this.opts.all) { - topics = topics.filter(t => !t.name.includes(':')) - } - console.log(this.topics(topics)) - console.log('') - } else if (command) { - this.showCommandHelp(command, topics) - } else if (topic) { - const name = topic.name - const depth = name.split(':').length - topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - console.log(this.topic(topic)) - if (topics.length > 0) { - console.log(this.topics(topics)) - console.log('') - } - } else { - error(`command ${subject} not found`) + const topic = this.config.findTopic(subject) + if (topic) { + this.showTopicHelp(topic) + return } + + error(`command ${subject} not found`) } - showCommandHelp(command: Config.Command, topics: Config.Topic[]) { + public showCommandHelp(command: Config.Command) { const name = command.id const depth = name.split(':').length - topics = topics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + + const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const subCommands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) + const title = command.description && this.render(command.description).split('\n')[0] if (title) console.log(title + '\n') - console.log(this.command(command)) + console.log(this.formatCommand(command)) + console.log('') + + if (subTopics.length > 0) { + console.log(this.formatTopics(subTopics)) + console.log('') + } + + if (subCommands.length > 0) { + console.log(this.formatCommands(subCommands)) + console.log('') + } + } + + protected showRootHelp() { + let rootTopics = this.sortedTopics + let rootCommands = this.sortedCommands + + console.log(this.formatRoot()) console.log('') - if (topics.length > 0) { - console.log(this.topics(topics)) + + if (!this.opts.all) { + rootTopics = rootTopics.filter(t => !t.name.includes(':')) + rootCommands = rootCommands.filter(c => !c.id.includes(':')) + } + + if (rootTopics.length > 0) { + console.log(this.formatTopics(rootTopics)) + console.log('') + } + + if (rootCommands.length > 0) { + console.log(this.formatCommands(rootCommands)) console.log('') } } - root(): string { + protected showTopicHelp(topic: Config.Topic) { + const name = topic.name + const depth = name.split(':').length + + const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) + const commands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) + + console.log(this.formatTopic(topic)) + + if (subTopics.length > 0) { + console.log(this.formatTopics(subTopics)) + console.log('') + } + + if (commands.length > 0) { + console.log(this.formatCommands(commands)) + console.log('') + } + } + + protected formatRoot(): string { const help = new RootHelp(this.config, this.opts) return help.root() } - topic(topic: Config.Topic): string { + protected formatCommand(command: Config.Command): string { + const help = new CommandHelp(command, this.config, this.opts) + return help.generate() + } + + protected formatCommands(commands: Config.Command[]): string { + if (commands.length === 0) return '' + + const body = renderList(commands.map(c => [ + c.id, + c.description && this.render(c.description.split('\n')[0]), + ]), { + spacer: '\n', + stripAnsi: this.opts.stripAnsi, + maxWidth: this.opts.maxWidth - 2, + }) + + return [ + bold('COMMANDS'), + indent(body, 2), + ].join('\n') + } + + protected formatTopic(topic: Config.Topic): string { let description = this.render(topic.description || '') const title = description.split('\n')[0] description = description.split('\n').slice(1).join('\n') @@ -123,13 +231,8 @@ export default class Help { return output + '\n' } - command(command: Config.Command): string { - const help = new CommandHelp(command, this.config, this.opts) - return help.generate() - } - - topics(topics: Config.Topic[]): string | undefined { - if (topics.length === 0) return + protected formatTopics(topics: Config.Topic[]): string { + if (topics.length === 0) return '' const body = renderList(topics.map(c => [ c.name, c.description && this.render(c.description.split('\n')[0]), @@ -139,12 +242,17 @@ export default class Help { maxWidth: this.opts.maxWidth - 2, }) return [ - bold('COMMANDS'), + bold('TOPICS'), indent(body, 2), ].join('\n') } -} -// function id(c: Config.Command | Config.Topic): string { -// return (c as any).id || (c as any).name -// } + /** + * @deprecated used for readme generation + * @param {object} command The command to generate readme help for + * @return {string} the readme help string for the given command + */ + protected command(command: Config.Command) { + return this.formatCommand(command) + } +} diff --git a/src/util.ts b/src/util.ts index bf64c02f..2a71ef12 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,38 @@ export function template(context: any): (t: string) => string { } return render } + +interface HelpBaseDerived { + new(config: IConfig, opts?: Partial): HelpBase; +} + +function extractExport(config: IConfig, classPath: string): HelpBaseDerived { + const helpClassPath = tsPath(config.root, classPath) + return require(helpClassPath) as HelpBaseDerived +} + +function extractClass(exported: any): HelpBaseDerived { + return exported && exported.default ? exported.default : exported +} + +export function getHelpClass(config: IConfig, defaultClass = '@oclif/plugin-help'): HelpBaseDerived { + const pjson = config.pjson + const configuredClass = pjson && pjson.oclif && pjson.oclif.helpClass + + if (configuredClass) { + try { + 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}`) + } + } + + try { + const defaultModulePath = require.resolve(defaultClass, {paths: [config.root]}) + const exported = require(defaultModulePath) + 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}`) + } +} diff --git a/test/command.test.ts b/test/command.test.ts deleted file mode 100644 index d128cede..00000000 --- a/test/command.test.ts +++ /dev/null @@ -1,349 +0,0 @@ -import {Command as Base, flags} 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' - -class Command extends Base { - async run() { - return null - } -} - -const test = base -.loadConfig() -.add('help', ctx => new Help(ctx.config)) -.register('commandHelp', (command?: Config.Command.Class) => ({ - run(ctx: {help: Help; commandHelp: string; expectation: string}) { - const cached = Config.Command.toCached(command!, {} as any) - const help = ctx.help.command(cached) - if (process.env.TEST_OUTPUT === '1') { - console.log(help) - } - ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') - ctx.expectation = 'has commandHelp' - }, -})) - -describe('command help', () => { - test - .commandHelp(class extends Command { - static id = 'apps:create' - - static aliases = ['app:init', 'create'] - - static description = `first line -multiline help` - - static args = [{name: 'app_name', description: 'app to use'}] - - static flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), - force: flags.boolean({description: 'force it '.repeat(15)}), - ss: flags.boolean({description: 'newliney\n'.repeat(4)}), - remote: flags.string({char: 'r'}), - label: flags.string({char: 'l', helpLabel: '-l'}), - } - }) - .it('shows lots of output', ctx => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif apps:create [APP_NAME] - -ARGUMENTS - APP_NAME app to use - -OPTIONS - -f, --foo=foo foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo - barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar - - -l=label - - -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 - -DESCRIPTION - multiline help - -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'}] - - 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 - $ oclif apps:create [APP_NAME] - -ARGUMENTS - APP_NAME app to use - -OPTIONS - -f, --foo=foo - foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob - arfoobarfoobarfoobarfoobarfoobarfoobarfoobar - - -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 - 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 - 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'}] - - static flags = { - force: flags.boolean({description: 'forces'}), - } - }) - .it('outputs with description', ctx => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif apps:create [APP_NAME] - -ARGUMENTS - APP_NAME app to use - -OPTIONS - --force forces - -ALIASES - $ oclif app:init - $ oclif create`)) - - 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 - $ oclif apps:create - -OPTIONS - --myenum=a|b|c`)) - - test - .commandHelp(class extends Command { - static id = 'apps:create' - - static args = [ - {name: 'arg1', default: '.'}, - {name: 'arg2', default: '.', description: 'arg2 desc'}, - {name: 'arg3', description: 'arg3 desc'}, - ] - - static flags = { - flag1: flags.string({default: '.'}), - 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 - $ oclif apps:create [ARG1] [ARG2] [ARG3] - -ARGUMENTS - ARG1 [default: .] - ARG2 [default: .] arg2 desc - ARG3 arg3 desc - -OPTIONS - --flag1=flag1 [default: .] - --flag2=flag2 [default: .] flag2 desc - --flag3=flag3 flag3 desc`)) - - test - .commandHelp(class extends Command { - static id = 'apps:create' - - static args = [ - {name: 'arg1', description: 'Show the options', options: ['option1', 'option2']}, - ] - }) - .it('outputs with possible 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' - - 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' - - 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`)) - - // 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/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']) diff --git a/test/format-command.test.ts b/test/format-command.test.ts new file mode 100644 index 00000000..96bc8d19 --- /dev/null +++ b/test/format-command.test.ts @@ -0,0 +1,375 @@ +import {Command as Base, flags} 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' + +class Command extends Base { + async run() { + return null + } +} + +// 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 TestHelp(ctx.config)) +.register('commandHelp', (command?: any) => ({ + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { + const cached = Config.Command.toCached(command!, {} as any) + const help = ctx.help.formatCommand(cached) + if (process.env.TEST_OUTPUT === '1') { + console.log(help) + } + ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') + ctx.expectation = 'has commandHelp' + }, +})) + +describe('formatCommand', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static aliases = ['app:init', 'create'] + + static description = `first line +multiline help` + + static args = [{name: 'app_name', description: 'app to use'}] + + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + label: flags.string({char: 'l', helpLabel: '-l'}), + } + }) + .it('handles multi-line help output', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create [APP_NAME] + +ARGUMENTS + APP_NAME app to use + +OPTIONS + -f, --foo=foo foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo + barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar + + -l=label + + -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 + +DESCRIPTION + multiline help + +ALIASES + $ oclif app:init + $ oclif create`)) + + describe('arg and flag multiline handling', () => { + 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(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: any) => 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 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: any) => 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 + foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob + arfoobarfoobarfoobarfoobarfoobarfoobarfoobar + + -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 + 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 + newliney + +ALIASES + $ oclif app:init + $ oclif 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 aliases = ['app:init', 'create'] + + 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: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create [APP_NAME] + +ARGUMENTS + APP_NAME app to use + +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' + + static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>' + }) + .it('renders template string from description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create + +DESCRIPTION + The oclif CLI has apps:create`)) + }) + + describe(('flags'), () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static flags = { + myenum: flags.string({options: ['a', 'b', 'c']}), + } + }) + .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create + +OPTIONS + --myenum=a|b|c`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static args = [ + {name: 'arg1', default: '.'}, + {name: 'arg2', default: '.', description: 'arg2 desc'}, + {name: 'arg3', description: 'arg3 desc'}, + ] + + static flags = { + flag1: flags.string({default: '.'}), + flag2: flags.string({default: '.', description: 'flag2 desc'}), + flag3: flags.string({description: 'flag3 desc'}), + } + }).it('outputs with default flag options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create [ARG1] [ARG2] [ARG3] + +ARGUMENTS + ARG1 [default: .] + ARG2 [default: .] arg2 desc + ARG3 arg3 desc + +OPTIONS + --flag1=flag1 [default: .] + --flag2=flag2 [default: .] flag2 desc + --flag3=flag3 flag3 desc`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static flags = { + opt: flags.boolean({allowNo: true}), + } + }) + .it('outputs with with no options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create + +OPTIONS + --[no-]opt`)) + }) + + 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: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create [ARG1] + +ARGUMENTS + ARG1 (option1|option2) Show the options`)) + }) + + describe('usage', () => { + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static usage = '<%= config.bin %> <%= command.id %> usage' + }) + .it('outputs usage with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif oclif apps:create usage`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + + static usage = ['<%= config.bin %>', '<%= command.id %> usage'] + }) + .it('outputs usage arrays with templates', (ctx: any) => 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: any) => 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: any) => 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: any) => 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: any) => expect(ctx.commandHelp).to.equal(`USAGE + $ oclif oclif:command + +EXAMPLES + the bin is oclif + the command id is oclif:command`)) + }) +}) diff --git a/test/format-commands.test.ts b/test/format-commands.test.ts new file mode 100644 index 00000000..8c41baf9 --- /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/fixtures' + +// 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-root.test.ts b/test/format-root.test.ts new file mode 100644 index 00000000..8350761b --- /dev/null +++ b/test/format-root.test.ts @@ -0,0 +1,139 @@ +import {expect, test as base, Config} from '@oclif/test' +import stripAnsi = require('strip-ansi') + +const g: any = global +g.columns = 80 +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 formatRoot() { + return super.formatRoot() + } +} + +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 TestHelp(config) + const root = help.formatRoot() + if (process.env.TEST_OUTPUT === '1') { + console.log(help) + } + ctx.commandHelp = stripAnsi(root).split('\n').map(s => s.trimRight()).join('\n') + }, +})) + +describe('formatRoot', () => { + test + .rootHelp() + .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 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 { + ...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/format-topic.test.ts b/test/format-topic.test.ts new file mode 100644 index 00000000..3b14e468 --- /dev/null +++ b/test/format-topic.test.ts @@ -0,0 +1,82 @@ +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' + +// extensions to expose method as public for testing +class TestHelp extends Help { + public formatTopic(topic: Config.Topic) { + return super.formatTopic(topic) + } +} + +const test = base +.loadConfig() +.add('help', ctx => { + return new TestHelp(ctx.config) +}) +.register('topicHelp', (topic: Config.Topic) => ({ + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { + const topicHelpOutput = ctx.help.formatTopic(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('formatHelp', () => { + 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 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 +`)) + 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 +`)) +}) diff --git a/test/format-topics.test.ts b/test/format-topics.test.ts new file mode 100644 index 00000000..a7106d5a --- /dev/null +++ b/test/format-topics.test.ts @@ -0,0 +1,56 @@ +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' + +// extensions to expose method as public for testing +class TestHelp extends Help { + public formatTopics(topics: Config.Topic[]) { + return super.formatTopics(topics) + } +} + +const test = base +.loadConfig() +.add('help', ctx => new TestHelp(ctx.config)) +.register('topicsHelp', (topics: Config.Topic[]) => ({ + run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { + const topicsHelpOutput = ctx.help.formatTopics(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('formatTopics', () => { + 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(`TOPICS + 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 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/fixtures.ts b/test/helpers/fixtures.ts new file mode 100644 index 00000000..6dc0ee10 --- /dev/null +++ b/test/helpers/fixtures.ts @@ -0,0 +1,113 @@ +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 only shows up in command help under DESCRIPTION`; + + static flags = {}; + + static args = []; + + async run() { + 'run' + } +} + +export class AppsDestroy extends Command { + static id = 'apps:destroy' + + static description = `Destroy an app + 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: {}; + + 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 only shows up in command help under DESCRIPTION`; + + 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 only shows up in command help under DESCRIPTION`; + + static flags: {}; + + static args = []; + + async run() { + 'run' + } +} + +// db + +export class DbCreate extends Command { + static id = 'db:create' + + static description = `Create a db + this only shows up in command help under DESCRIPTION`; + + 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/root.test.ts b/test/root.test.ts deleted file mode 100644 index 6d09bcc3..00000000 --- a/test/root.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 VERSION = require('../package.json').version -const UA = `@oclif/plugin-help/${VERSION} ${process.platform}-${process.arch} node-${process.version}` - -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() - if (process.env.TEST_OUTPUT === '1') { - console.log(help) - } - ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') - }, -})) - -describe('root help', () => { - test - .rootHelp() - .it(ctx => expect(ctx.commandHelp).to.equal(`standard help for oclif - -VERSION - ${UA} - -USAGE - $ oclif [COMMAND]`)) -}) diff --git a/test/show-help.test.ts b/test/show-help.test.ts new file mode 100644 index 00000000..5720f793 --- /dev/null +++ b/test/show-help.test.ts @@ -0,0 +1,365 @@ +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 +import Help from '../src' +import {AppsIndex, AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd, AppsAdminIndex, DbCreate, DbTopic} from './helpers/fixtures' + +// extension makes previously protected methods public +class TestHelp extends Help { + public config: any; + + public showRootHelp() { + return super.showRootHelp() + } + + public showTopicHelp(topic: Config.Topic) { + return super.showTopicHelp(topic) + } +} + +const test = base +.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(), + } + + // 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) { + 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 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() + .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 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 + .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/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:') + }) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 712748e1..927e22ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,18 +86,32 @@ 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" + semver "^5.6.0" + +"@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== dependencies: + "@oclif/errors" "^1.0.0" + "@oclif/parser" "^3.8.0" debug "^4.1.1" 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== +"@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" @@ -119,7 +133,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 +167,29 @@ chalk "^2.4.2" tslib "^1.9.3" +"@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== + dependencies: + "@oclif/linewrap" "^1.0.0" + 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" @@ -210,6 +247,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" @@ -825,11 +898,16 @@ 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.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== + dir-glob@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" @@ -1369,11 +1447,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" @@ -1389,6 +1472,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 +1774,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 +1836,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" @@ -1961,9 +2059,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" @@ -2010,6 +2108,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 +2274,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 +2525,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 +2765,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" @@ -2729,18 +2865,18 @@ 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, 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== @@ -2750,6 +2886,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" @@ -2771,7 +2912,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== @@ -2781,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" @@ -2915,7 +3056,7 @@ yarn@^1.13.0: resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.0.tgz#acf82906e36bcccd1ccab1cfb73b87509667c881" integrity sha512-KMHP/Jq53jZKTY9iTUt3dIVl/be6UPs2INo96+BnZHLKxYNTfwMmlgHTaMWyGZoO74RI4AIFvnWhYrXq2USJkg== -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==