Skip to content
This repository has been archived by the owner on Apr 21, 2022. It is now read-only.

Commit

Permalink
feat: add support for custom help classes (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
chadian committed Nov 19, 2020
1 parent e0cc3b0 commit 810e7c1
Show file tree
Hide file tree
Showing 23 changed files with 405 additions and 56 deletions.
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -8,10 +8,10 @@
},
"bugs": "https://github.com/oclif/dev-cli/issues",
"dependencies": {
"@oclif/command": "^1.5.13",
"@oclif/config": "^1.12.12",
"@oclif/errors": "^1.2.2",
"@oclif/plugin-help": "^2.1.6",
"@oclif/command": "^1.8.0",
"@oclif/config": "^1.17.0",
"@oclif/errors": "^1.3.3",
"@oclif/plugin-help": "^3.2.0",
"cli-ux": "^5.2.1",
"debug": "^4.1.1",
"fs-extra": "^8.1",
Expand Down
44 changes: 28 additions & 16 deletions src/commands/readme.ts
@@ -1,14 +1,14 @@
// tslint:disable no-implicit-dependencies

import {Command, flags} from '@oclif/command'
import * as Config from '@oclif/config'
import Help from '@oclif/plugin-help'
import {getHelpClass} from '@oclif/plugin-help'
import * as fs from 'fs-extra'
import * as _ from 'lodash'
import * as path from 'path'
import {URL} from 'url'

import {castArray, compact, sortBy, template, uniqBy} from '../util'
import {HelpCompatibilityWrapper} from '../help-compatibility'

const normalize = require('normalize-package-data')
const columns = parseInt(process.env.COLUMNS!, 10) || 120
Expand All @@ -32,15 +32,19 @@ Customize the code URL prefix by setting oclif.repositoryPrefix in package.json.

async run() {
const {flags} = this.parse(Readme)
const config = await Config.load({root: process.cwd(), devPlugins: false, userPlugins: false})
const cwd = process.cwd()
const readmePath = path.resolve(cwd, 'README.md')
const config = await Config.load({root: cwd, devPlugins: false, userPlugins: false})

try {
const p = require.resolve('@oclif/plugin-legacy', {paths: [process.cwd()]})
const p = require.resolve('@oclif/plugin-legacy', {paths: [cwd]})
const plugin = new Config.Plugin({root: p, type: 'core'})
await plugin.load()
config.plugins.push(plugin)
} catch {}
await (config as Config.Config).runHook('init', {id: 'readme', argv: this.argv})
let readme = await fs.readFile('README.md', 'utf8')
let readme = await fs.readFile(readmePath, 'utf8')

let commands = config.commands
commands = commands.filter(c => !c.hidden)
commands = commands.filter(c => c.pluginType === 'core')
Expand All @@ -53,7 +57,8 @@ Customize the code URL prefix by setting oclif.repositoryPrefix in package.json.

readme = readme.trimRight()
readme += '\n'
await fs.outputFile('README.md', readme)

await fs.outputFile(readmePath, readme)
}

replaceTag(readme: string, tag: string, body: string): string {
Expand Down Expand Up @@ -141,15 +146,22 @@ USAGE

renderCommand(config: Config.IConfig, c: Config.Command): string {
this.debug('rendering command', c.id)
const title = template({config, command: c})(c.description || '').trim().split('\n')[0]
const help = new Help(config, {stripAnsi: true, maxWidth: columns})

const HelpClass = getHelpClass(config)
const help = new HelpClass(config, {stripAnsi: true, maxWidth: columns})
const wrapper = new HelpCompatibilityWrapper(help)

const header = () => `## \`${config.bin} ${this.commandUsage(config, c)}\``
return compact([
header(),
title,
'```\n' + help.command(c).trim() + '\n```',
this.commandCode(config, c),
]).join('\n\n')

try {
return compact([
header(),
'```\n' + wrapper.formatCommand(c).trim() + '\n```',
this.commandCode(config, c),
]).join('\n\n')
} catch (error) {
this.error(error.message)
}
}

commandCode(config: Config.IConfig, c: Config.Command): string | undefined {
Expand Down Expand Up @@ -194,7 +206,7 @@ USAGE
p = path.join(p, 'index.js')
} else if (fs.pathExistsSync(p + '.js')) {
p += '.js'
} else if (plugin.pjson.devDependencies.typescript) {
} else if (plugin.pjson.devDependencies && plugin.pjson.devDependencies.typescript) {
// check if non-compiled scripts are available
const base = p.replace(plugin.root + path.sep, '')
p = path.join(plugin.root, base.replace(libRegex, 'src' + path.sep))
Expand All @@ -205,7 +217,7 @@ USAGE
} else return
} else return
p = p.replace(plugin.root + path.sep, '')
if (plugin.pjson.devDependencies.typescript) {
if (plugin.pjson.devDependencies && plugin.pjson.devDependencies.typescript) {
p = p.replace(libRegex, 'src' + path.sep)
p = p.replace(/\.js$/, '.ts')
}
Expand Down
31 changes: 31 additions & 0 deletions src/help-compatibility.ts
@@ -0,0 +1,31 @@
import {HelpBase} from '@oclif/plugin-help'
import {Command} from '@oclif/config'

interface MaybeCompatibleHelp extends HelpBase {
formatCommand?: (command: Command) => string;
command?: (command: Command) => string;
}

class IncompatibleHelpError extends Error {
message = 'Please implement `formatCommand` in your custom help class.\nSee https://oclif.io/docs/help_classes for more.'
}

export class HelpCompatibilityWrapper {
inner: MaybeCompatibleHelp

constructor(inner: MaybeCompatibleHelp) {
this.inner = inner
}

formatCommand(command: Command) {
if (this.inner.formatCommand) {
return this.inner.formatCommand(command)
}

if (this.inner.command) {
return command.description + '\n\n' + this.inner.command(command)
}

throw new IncompatibleHelpError()
}
}
19 changes: 19 additions & 0 deletions test/fixtures/cli-with-custom-help-no-format-command/README.md
@@ -0,0 +1,19 @@
# cli-with-custom-help

This file is a test for running `oclif-dev readme` in the presence of
a custom help class. It should use the custom help class to generate
the command documentation below. The test suite resets this file after
each test.

<!-- toc -->
<!-- tocstop -->

# Usage

<!-- usage -->
<!-- usagestop -->

# Commands

<!-- commands -->
<!-- commandsstop -->
11 changes: 11 additions & 0 deletions test/fixtures/cli-with-custom-help-no-format-command/package.json
@@ -0,0 +1,11 @@
{
"name": "cli-with-custom-help-no-format-command",
"files": [
"/lib"
],
"oclif": {
"commands": "./lib/commands",
"bin": "cli-with-custom-help",
"helpClass": "./lib/help"
}
}
@@ -0,0 +1,31 @@
import {Command, flags} from '@oclif/command'

export default class Hello extends Command {
static description = 'describe the command here'

static examples = [
`$ cli-with-custom-help hello
hello world from ./src/hello.ts!
`,
]

static flags = {
help: flags.help({char: 'h'}),
// flag with a value (-n, --name=VALUE)
name: flags.string({char: 'n', description: 'name to print'}),
// flag with no value (-f, --force)
force: flags.boolean({char: 'f'}),
}

static args = [{name: 'file'}]

async run() {
const {args, flags} = this.parse(Hello)

const name = flags.name ?? 'world'
this.log(`hello ${name} from ./src/commands/hello.ts`)
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`)
}
}
}
12 changes: 12 additions & 0 deletions test/fixtures/cli-with-custom-help-no-format-command/src/help.ts
@@ -0,0 +1,12 @@
import {HelpBase} from '@oclif/plugin-help'
import {Command} from '@oclif/config'

export default class CustomHelp extends HelpBase {
showHelp() {
console.log('TODO: showHelp')
}

showCommandHelp(command: Command) {
console.log(`Custom help for ${command.id}`)
}
}
@@ -0,0 +1 @@
export {run} from '@oclif/command'
14 changes: 14 additions & 0 deletions test/fixtures/cli-with-custom-help-no-format-command/tsconfig.json
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"outDir": "lib",
"rootDir": "src",
"strict": true,
"target": "es2017"
},
"include": [
"src/**/*"
]
}
19 changes: 19 additions & 0 deletions test/fixtures/cli-with-custom-help/README.md
@@ -0,0 +1,19 @@
# cli-with-custom-help

This file is a test for running `oclif-dev readme` in the presence of
a custom help class. It should use the custom help class to generate
the command documentation below. The test suite resets this file after
each test.

<!-- toc -->
<!-- tocstop -->

# Usage

<!-- usage -->
<!-- usagestop -->

# Commands

<!-- commands -->
<!-- commandsstop -->
11 changes: 11 additions & 0 deletions test/fixtures/cli-with-custom-help/package.json
@@ -0,0 +1,11 @@
{
"name": "cli-with-custom-help",
"files": [
"/lib"
],
"oclif": {
"commands": "./lib/commands",
"bin": "cli-with-custom-help",
"helpClass": "./lib/help"
}
}
31 changes: 31 additions & 0 deletions test/fixtures/cli-with-custom-help/src/commands/hello.ts
@@ -0,0 +1,31 @@
import {Command, flags} from '@oclif/command'

export default class Hello extends Command {
static description = 'describe the command here'

static examples = [
`$ cli-with-custom-help hello
hello world from ./src/hello.ts!
`,
]

static flags = {
help: flags.help({char: 'h'}),
// flag with a value (-n, --name=VALUE)
name: flags.string({char: 'n', description: 'name to print'}),
// flag with no value (-f, --force)
force: flags.boolean({char: 'f'}),
}

static args = [{name: 'file'}]

async run() {
const {args, flags} = this.parse(Hello)

const name = flags.name ?? 'world'
this.log(`hello ${name} from ./src/commands/hello.ts`)
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`)
}
}
}
8 changes: 8 additions & 0 deletions test/fixtures/cli-with-custom-help/src/help.ts
@@ -0,0 +1,8 @@
import {Help} from '@oclif/plugin-help'
import {Command} from '@oclif/config'

export default class CustomHelp extends Help {
formatCommand(command: Command) {
return `Custom help for ${command.id}`
}
}
1 change: 1 addition & 0 deletions test/fixtures/cli-with-custom-help/src/index.ts
@@ -0,0 +1 @@
export {run} from '@oclif/command'
14 changes: 14 additions & 0 deletions test/fixtures/cli-with-custom-help/tsconfig.json
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"outDir": "lib",
"rootDir": "src",
"strict": true,
"target": "es2017"
},
"include": [
"src/**/*"
]
}
19 changes: 19 additions & 0 deletions test/fixtures/cli-with-old-school-custom-help/README.md
@@ -0,0 +1,19 @@
# cli-with-custom-help

This file is a test for running `oclif-dev readme` in the presence of
a custom help class. It should use the custom help class to generate
the command documentation below. The test suite resets this file after
each test.

<!-- toc -->
<!-- tocstop -->

# Usage

<!-- usage -->
<!-- usagestop -->

# Commands

<!-- commands -->
<!-- commandsstop -->
11 changes: 11 additions & 0 deletions test/fixtures/cli-with-old-school-custom-help/package.json
@@ -0,0 +1,11 @@
{
"name": "cli-with-old-school-custom-help",
"files": [
"/lib"
],
"oclif": {
"commands": "./lib/commands",
"bin": "cli-with-custom-help",
"helpClass": "./lib/help"
}
}
@@ -0,0 +1,31 @@
import {Command, flags} from '@oclif/command'

export default class Hello extends Command {
static description = 'describe the command here'

static examples = [
`$ cli-with-custom-help hello
hello world from ./src/hello.ts!
`,
]

static flags = {
help: flags.help({char: 'h'}),
// flag with a value (-n, --name=VALUE)
name: flags.string({char: 'n', description: 'name to print'}),
// flag with no value (-f, --force)
force: flags.boolean({char: 'f'}),
}

static args = [{name: 'file'}]

async run() {
const {args, flags} = this.parse(Hello)

const name = flags.name ?? 'world'
this.log(`hello ${name} from ./src/commands/hello.ts`)
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`)
}
}
}

0 comments on commit 810e7c1

Please sign in to comment.