Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support flag and command deprecations #511

Merged
merged 6 commits into from Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -275,7 +289,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,
}
} else {
flags[name] = {
Expand All @@ -771,6 +772,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,
}
// a command-level placeholder in the manifest so that oclif knows it should regenerate the command during help-time
if (typeof flag.defaultHelp === 'function') {
Expand Down Expand Up @@ -803,6 +805,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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there's only 1 word of difference, it could be consolidated?
formatDeprecationWarning(deprecationType: 'command' | 'flag', item: string, opts: true | Deprecation)

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;
}

export type BooleanFlagProps = FlagProps & {
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for that!

}

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