From 90269d54c082bed1144872bf60e4fc5dae37cfa0 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 30 Sep 2022 13:22:28 -0600 Subject: [PATCH 1/5] feat: support flag and command deprecations --- src/command.ts | 31 +++++++++---- src/config/config.ts | 3 ++ src/help/index.ts | 20 ++++++-- src/help/util.ts | 33 ++++++++++++- src/interfaces/command.ts | 13 ++++-- src/interfaces/parser.ts | 10 ++++ src/interfaces/pjson.ts | 2 +- test/command/command.test.ts | 72 ++++++++++++++++++++++++++++- test/config/config.flexible.test.ts | 8 ++-- 9 files changed, 168 insertions(+), 24 deletions(-) diff --git a/src/command.ts b/src/command.ts index d210852e..90e509db 100644 --- a/src/command.ts +++ b/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') @@ -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. @@ -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(options?: Interfaces.Input, argv = this.argv): Promise> { @@ -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) } diff --git a/src/config/config.ts b/src/config/config.ts index 3d032b87..c85fde17 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -752,6 +752,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise 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) { @@ -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('') diff --git a/src/help/util.ts b/src/help/util.ts index 0c8cacbc..a73652d8 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -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): HelpBase; @@ -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[] { @@ -97,3 +98,31 @@ export function getHelpFlagAdditions(config: IConfig): string[] { const additionalHelpFlags = config.pjson.oclif.additionalHelpFlags ?? [] return [...new Set([...helpFlags, ...additionalHelpFlags]).values()] } + +export function formatFlagDeprecationWarning(flag: string, opts: Deprecation): string { + if (opts.message) return opts.message + + let message = `The "${flag}" flag has been deprecated` + 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 +} diff --git a/src/interfaces/command.ts b/src/interfaces/command.ts index cf9fe587..438837be 100644 --- a/src/interfaces/command.ts +++ b/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 | { @@ -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[]; diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts index 92b2b5f3..c6b1d731 100644 --- a/src/interfaces/parser.ts +++ b/src/interfaces/parser.ts @@ -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; @@ -143,6 +149,10 @@ export type FlagProps = { * Define complex relationships between flags. */ relationships?: Relationship[]; + /** + * Make the flag as deprecated. + */ + deprecated?: Deprecation; } export type BooleanFlagProps = FlagProps & { diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index bb7285b1..7c7a8ee8 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -46,7 +46,7 @@ export namespace PJSON { }; additionalHelpFlags?: string[]; additionalVersionFlags?: string[]; - state?: string; + state?: 'beta' | 'deprecated' | string; }; } diff --git a/test/command/command.test.ts b/test/command/command.test.ts index 9d799a5b..ee1db728 100644 --- a/test/command/command.test.ts +++ b/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, Flags as flags} from '../../src' // import {TestHelpClassConfig} from './helpers/test-help-in-src/src/test-help-plugin' // const pjson = require('../package.json') @@ -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() diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts index 75ca5436..fce30c8c 100644 --- a/test/config/config.flexible.test.ts +++ b/test/config/config.flexible.test.ts @@ -18,7 +18,7 @@ interface Options { } // @ts-expect-error -class MyComandClass implements ICommand.Class { +class MyCommandClass implements ICommand.Class { _base = '' aliases: string[] = [] @@ -57,7 +57,7 @@ describe('Config with flexible taxonomy', () => { const load = async (): Promise => {} const findCommand = async (): Promise => { - return new MyComandClass() as unknown as ICommand.Class + return new MyCommandClass() as unknown as ICommand.Class } const commandPluginA: ICommand.Loadable = { @@ -70,7 +70,7 @@ describe('Config with flexible taxonomy', () => { hidden: false, id: commandIds[0], async load(): Promise { - return new MyComandClass() as unknown as ICommand.Class + return new MyCommandClass() as unknown as ICommand.Class }, pluginType: types[0] ?? 'core', pluginAlias: '@My/plugina', @@ -85,7 +85,7 @@ describe('Config with flexible taxonomy', () => { hidden: false, id: commandIds[1], async load(): Promise { - return new MyComandClass() as unknown as ICommand.Class + return new MyCommandClass() as unknown as ICommand.Class }, pluginType: types[1] ?? 'core', pluginAlias: '@My/pluginb', From bd75bbb190fb561c58245b4a006536252e8fa6c4 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 30 Sep 2022 13:31:23 -0600 Subject: [PATCH 2/5] fix: allow boolean --- src/help/util.ts | 5 +++-- src/interfaces/parser.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/help/util.ts b/src/help/util.ts index a73652d8..95c59420 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -99,10 +99,11 @@ export function getHelpFlagAdditions(config: IConfig): string[] { return [...new Set([...helpFlags, ...additionalHelpFlags]).values()] } -export function formatFlagDeprecationWarning(flag: string, opts: Deprecation): string { +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 - let message = `The "${flag}" flag has been deprecated` if (opts.version) { message += ` and will be removed in version ${opts.version}` } diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts index c6b1d731..e80d47e1 100644 --- a/src/interfaces/parser.ts +++ b/src/interfaces/parser.ts @@ -152,7 +152,7 @@ export type FlagProps = { /** * Make the flag as deprecated. */ - deprecated?: Deprecation; + deprecated?: true | Deprecation; } export type BooleanFlagProps = FlagProps & { From 25526399f0a634eb5ae1eec2f7274da0abaa81ef Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 30 Sep 2022 13:35:35 -0600 Subject: [PATCH 3/5] chore: clean up --- test/command/command.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/command/command.test.ts b/test/command/command.test.ts index ee1db728..72e8693c 100644 --- a/test/command/command.test.ts +++ b/test/command/command.test.ts @@ -1,7 +1,7 @@ import {expect, fancy} from 'fancy-test' // import path = require('path') -import {Command as Base, Flags, 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') @@ -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() { From 4413a61740da0ecc8869c86d65a8900441578a41 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 30 Sep 2022 13:51:47 -0600 Subject: [PATCH 4/5] test: json parsing --- test/integration/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/util.ts b/test/integration/util.ts index 7292b21b..f7f7a053 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -39,7 +39,7 @@ export class Executor { public executeCommand(cmd: string): Promise { const executable = process.platform === 'win32' ? path.join('bin', 'run.cmd') : path.join('bin', 'run') - return this.executeInTestDir(`${executable} ${cmd} 2>&1`) + return this.executeInTestDir(`${executable} ${cmd}`) } public exec(cmd: string, cwd = process.cwd(), silent = true): Promise { From f6b7f7d599b8abfc21ef61c80ccff0c2954d6be0 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 30 Sep 2022 14:24:05 -0600 Subject: [PATCH 5/5] chore: fix tests --- test/integration/plugins.e2e.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/plugins.e2e.ts b/test/integration/plugins.e2e.ts index 33131992..5ae240f9 100644 --- a/test/integration/plugins.e2e.ts +++ b/test/integration/plugins.e2e.ts @@ -193,7 +193,7 @@ describe('oclif plugins', () => { describe('installing a plugin by name', () => { it('should install the plugin', async () => { - const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available') + const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available 2>&1') expect(result.code).to.equal(0) expect(result.output).to.include('@oclif/plugin-warn-if-update-available... installed v') @@ -205,7 +205,7 @@ describe('oclif plugins', () => { describe('installing a plugin by github url', () => { after(async () => { - await executor.executeCommand('plugins:uninstall @oclif/plugin-warn-if-update-available') + await executor.executeCommand('plugins:uninstall @oclif/plugin-warn-if-update-available 2>&1') }) it('should install the plugin', async () => { @@ -220,7 +220,7 @@ describe('oclif plugins', () => { describe('forcefully installing a plugin', () => { it('should install the plugin', async () => { - const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available --force') + const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available --force 2>&1') expect(result.code).to.equal(0) expect(result.output).to.include('@oclif/plugin-warn-if-update-available... installed v') @@ -236,7 +236,7 @@ describe('oclif plugins', () => { }) it('should uninstall the plugin', async () => { - const result = await executor.executeCommand('plugins:uninstall @oclif/plugin-warn-if-update-available') + const result = await executor.executeCommand('plugins:uninstall @oclif/plugin-warn-if-update-available 2>&1') expect(result.code).to.equal(0) expect(result.output).to.include('success Uninstalled packages.Uninstalling @oclif/plugin-warn-if-update-available... done\n')