Skip to content

Commit

Permalink
feat: support flag and command deprecations (#511)
Browse files Browse the repository at this point in the history
* feat: support flag and command deprecations

* fix: allow boolean

* chore: clean up

* test: json parsing

* chore: fix tests
  • Loading branch information
mdonnalley committed Oct 14, 2022
1 parent 2efc7b2 commit b0bf379
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 25 deletions.
31 changes: 22 additions & 9 deletions src/command.ts
@@ -1,13 +1,15 @@
import {fileURLToPath} from 'url'

import {format, inspect} from 'util'
import {CliUx} from './index'
import {CliUx, toConfiguredId} from './index'
import {Config} from './config'
import * as Interfaces from './interfaces'
import * as Errors from './errors'
import {PrettyPrintableError} from './errors'
import * as Parser from './parser'
import * as Flags from './flags'
import {Deprecation} from './interfaces/parser'
import {formatCommandDeprecationWarning, formatFlagDeprecationWarning} from './help/util'

const pjson = require('../package.json')

Expand Down Expand Up @@ -52,11 +54,13 @@ export default abstract class Command {
*/
static description: string | undefined

/** Hide the command from help? */
/** Hide the command from help */
static hidden: boolean

/** Mark the command as a given state (e.g. beta) in help? */
static state?: string;
/** Mark the command as a given state (e.g. beta or deprecated) in help */
static state?: 'beta' | 'deprecated' | string;

static deprecationOptions?: Deprecation;

/**
* An override string (or strings) for the default usage documentation.
Expand Down Expand Up @@ -239,13 +243,23 @@ export default abstract class Command {
this.debug('init version: %s argv: %o', this.ctor._base, this.argv)
if (this.config.debug) Errors.config.debug = true
if (this.config.errlog) Errors.config.errlog = this.config.errlog
// global['cli-ux'].context = global['cli-ux'].context || {
// command: compact([this.id, ...this.argv]).join(' '),
// version: this.config.userAgent,
// }
const g: any = global
g['http-call'] = g['http-call'] || {}
g['http-call']!.userAgent = this.config.userAgent
this.checkForDeprecations()
}

protected checkForDeprecations() {
if (this.ctor.state === 'deprecated') {
const cmdName = toConfiguredId(this.ctor.id, this.config)
this.warn(formatCommandDeprecationWarning(cmdName, this.ctor.deprecationOptions))
}

for (const [flag, opts] of Object.entries(this.ctor.flags ?? {})) {
if (opts.deprecated) {
this.warn(formatFlagDeprecationWarning(flag, opts.deprecated))
}
}
}

protected async parse<F extends Interfaces.FlagOutput, G extends Interfaces.FlagOutput, A extends { [name: string]: any }>(options?: Interfaces.Input<F, G>, argv = this.argv): Promise<Interfaces.ParserOutput<F, G, A>> {
Expand Down Expand Up @@ -276,7 +290,6 @@ export default abstract class Command {
try {
const config = Errors.config
if (config.errorLogger) await config.errorLogger.flush()
// tslint:disable-next-line no-console
} catch (error: any) {
console.error(error)
}
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.ts
Expand Up @@ -752,6 +752,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
allowNo: flag.allowNo,
dependsOn: flag.dependsOn,
exclusive: flag.exclusive,
deprecated: flag.deprecated,
aliases: flag.aliases,
}
} else {
Expand All @@ -772,6 +773,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
relationships: flag.relationships,
exclusive: flag.exclusive,
default: await defaultToCached(flag),
deprecated: flag.deprecated,
aliases: flag.aliases,
}
// a command-level placeholder in the manifest so that oclif knows it should regenerate the command during help-time
Expand Down Expand Up @@ -805,6 +807,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
state: c.state,
aliases: c.aliases || [],
examples: c.examples || (c as any).example,
deprecationOptions: c.deprecationOptions,
flags,
args,
}
Expand Down
20 changes: 17 additions & 3 deletions src/help/index.ts
Expand Up @@ -5,7 +5,7 @@ import {error} from '../errors'
import CommandHelp from './command'
import RootHelp from './root'
import {compact, sortBy, uniqBy} from '../util'
import {getHelpFlagAdditions, standardizeIDFromArgv} from './util'
import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv, toConfiguredId} from './util'
import {HelpFormatter} from './formatter'
import {toCached} from '../config/config'
export {CommandHelp} from './command'
Expand Down Expand Up @@ -139,7 +139,14 @@ export class Help extends HelpBase {
const plugin = this.config.plugins.find(p => p.name === command.pluginName)

const state = this.config.pjson?.oclif?.state || plugin?.pjson?.oclif?.state || command.state
if (state) this.log(`This command is in ${state}.\n`)

if (state) {
this.log(
state === 'deprecated' ?
`${formatCommandDeprecationWarning(toConfiguredId(name, this.config), command.deprecationOptions)}` :
`This command is in ${state}.\n`,
)
}

const summary = this.summary(command)
if (summary) {
Expand Down Expand Up @@ -170,7 +177,14 @@ export class Help extends HelpBase {
let rootCommands = this.sortedCommands

const state = this.config.pjson?.oclif?.state
if (state) this.log(`${this.config.bin} is in ${state}.\n`)
if (state) {
this.log(
state === 'deprecated' ?
`${this.config.bin} is deprecated` :
`${this.config.bin} is in ${state}.\n`,
)
}

this.log(this.formatRoot())
this.log('')

Expand Down
34 changes: 32 additions & 2 deletions src/help/util.ts
Expand Up @@ -3,6 +3,7 @@ import {Config as IConfig, HelpOptions} from '../interfaces'
import {Help, HelpBase} from '.'
import ModuleLoader from '../module-loader'
import {collectUsableIds} from '../config/util'
import {Deprecation} from '../interfaces/parser'

interface HelpBaseDerived {
new(config: IConfig, opts?: Partial<HelpOptions>): HelpBase;
Expand Down Expand Up @@ -81,8 +82,8 @@ export function toStandardizedId(commandID: string, config: IConfig): string {
}

export function toConfiguredId(commandID: string, config: IConfig): string {
const defaultTopicSeperator = ':'
return commandID.replace(new RegExp(defaultTopicSeperator, 'g'), config.topicSeparator || defaultTopicSeperator)
const defaultTopicSeparator = ':'
return commandID.replace(new RegExp(defaultTopicSeparator, 'g'), config.topicSeparator || defaultTopicSeparator)
}

export function standardizeIDFromArgv(argv: string[], config: IConfig): string[] {
Expand All @@ -97,3 +98,32 @@ export function getHelpFlagAdditions(config: IConfig): string[] {
const additionalHelpFlags = config.pjson.oclif.additionalHelpFlags ?? []
return [...new Set([...helpFlags, ...additionalHelpFlags]).values()]
}

export function formatFlagDeprecationWarning(flag: string, opts: true | Deprecation): string {
let message = `The "${flag}" flag has been deprecated`
if (opts === true) return `${message}.`
if (opts.message) return opts.message

if (opts.version) {
message += ` and will be removed in version ${opts.version}`
}

message += opts.to ? `. Use "${opts.to}" instead.` : '.'

return message
}

export function formatCommandDeprecationWarning(command: string, opts?: Deprecation): string {
let message = `The "${command}" command has been deprecated`
if (!opts) return `${message}.`

if (opts.message) return opts.message

if (opts.version) {
message += ` and will be removed in version ${opts.version}`
}

message += opts.to ? `. Use "${opts.to}" instead.` : '.'

return message
}
13 changes: 9 additions & 4 deletions src/interfaces/command.ts
@@ -1,5 +1,5 @@
import {Config, LoadOptions} from './config'
import {ArgInput, BooleanFlagProps, FlagInput, OptionFlagProps} from './parser'
import {ArgInput, BooleanFlagProps, Deprecation, FlagInput, OptionFlagProps} from './parser'
import {Plugin as IPlugin} from './plugin'

export type Example = string | {
Expand All @@ -11,11 +11,16 @@ export interface CommandProps {
/** A command ID, used mostly in error or verbose reporting. */
id: string;

/** Hide the command from help? */
/** Hide the command from help */
hidden: boolean;

/** Mark the command as a given state (e.g. beta) in help? */
state?: string;
/** Mark the command as a given state (e.g. beta or deprecated) in help */
state?: 'beta' | 'deprecated' | string;

/**
* Provide details to the deprecation warning if state === 'deprecated'
*/
deprecationOptions?: Deprecation;

/** An array of aliases for this command. */
aliases: string[];
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/parser.ts
Expand Up @@ -92,6 +92,12 @@ export type Relationship = {
flags: FlagRelationship[];
}

export type Deprecation = {
to?: string;
message?: string;
version?: string;
}

export type FlagProps = {
name: string;
char?: AlphabetLowercase | AlphabetUppercase;
Expand Down Expand Up @@ -143,6 +149,10 @@ export type FlagProps = {
* Define complex relationships between flags.
*/
relationships?: Relationship[];
/**
* Make the flag as deprecated.
*/
deprecated?: true | Deprecation;
/**
* Alternate names that can be used for this flag.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/pjson.ts
Expand Up @@ -46,7 +46,7 @@ export namespace PJSON {
};
additionalHelpFlags?: string[];
additionalVersionFlags?: string[];
state?: string;
state?: 'beta' | 'deprecated' | string;
};
}

Expand Down
74 changes: 72 additions & 2 deletions test/command/command.test.ts
@@ -1,7 +1,7 @@
import {expect, fancy} from 'fancy-test'
// import path = require('path')

import {Command as Base, Flags as flags} from '../../src'
import {Command as Base, Flags} from '../../src'
// import {TestHelpClassConfig} from './helpers/test-help-in-src/src/test-help-plugin'

// const pjson = require('../package.json')
Expand Down Expand Up @@ -204,7 +204,7 @@ describe('command', () => {
.it('has a flag', async ctx => {
class CMD extends Base {
static flags = {
foo: flags.string(),
foo: Flags.string(),
}

async run() {
Expand Down Expand Up @@ -233,6 +233,76 @@ describe('command', () => {
.it('uses util.format()')
})

describe('deprecated flags', () => {
fancy
.stdout()
.stderr()
.do(async () => {
class CMD extends Command {
static flags = {
name: Flags.string({
deprecated: {
to: '--full-name',
version: '2.0.0',
},
}),
}

async run() {
this.log('running command')
}
}
await CMD.run([])
})
.do(ctx => expect(ctx.stderr).to.include('Warning: The "name" flag has been deprecated'))
.it('shows warning for deprecated flags')
})

describe('deprecated state', () => {
fancy
.stdout()
.stderr()
.do(async () => {
class CMD extends Command {
static id = 'my:command'
static state = 'deprecated'
async run() {
this.log('running command')
}
}
await CMD.run([])
})
.do(ctx => expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated'))
.it('shows warning for deprecated flags')
})

describe('deprecated state with options', () => {
fancy
.stdout()
.stderr()
.do(async () => {
class CMD extends Command {
static id = 'my:command'
static state = 'deprecated'
static deprecationOptions = {
version: '2.0.0',
to: 'my:other:command',
}

async run() {
this.log('running command')
}
}
await CMD.run([])
})
.do(ctx => {
expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated')
expect(ctx.stderr).to.include('in version 2.0.0')
expect(ctx.stderr).to.include('Use "my:other:command" instead')
})
.it('shows warning for deprecated flags')
})

describe('stdout err', () => {
fancy
.stdout()
Expand Down
8 changes: 4 additions & 4 deletions test/config/config.flexible.test.ts
Expand Up @@ -18,7 +18,7 @@ interface Options {
}

// @ts-expect-error
class MyComandClass implements ICommand.Class {
class MyCommandClass implements ICommand.Class {
_base = ''

aliases: string[] = []
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('Config with flexible taxonomy', () => {

const load = async (): Promise<void> => {}
const findCommand = async (): Promise<ICommand.Class> => {
return new MyComandClass() as unknown as ICommand.Class
return new MyCommandClass() as unknown as ICommand.Class
}

const commandPluginA: ICommand.Loadable = {
Expand All @@ -70,7 +70,7 @@ describe('Config with flexible taxonomy', () => {
hidden: false,
id: commandIds[0],
async load(): Promise<ICommand.Class> {
return new MyComandClass() as unknown as ICommand.Class
return new MyCommandClass() as unknown as ICommand.Class
},
pluginType: types[0] ?? 'core',
pluginAlias: '@My/plugina',
Expand All @@ -85,7 +85,7 @@ describe('Config with flexible taxonomy', () => {
hidden: false,
id: commandIds[1],
async load(): Promise<ICommand.Class> {
return new MyComandClass() as unknown as ICommand.Class
return new MyCommandClass() as unknown as ICommand.Class
},
pluginType: types[1] ?? 'core',
pluginAlias: '@My/pluginb',
Expand Down

0 comments on commit b0bf379

Please sign in to comment.