feat(@angular/cli): allow flags to have deprecation

The feature comes from the "x-deprecated" field in schemas (any schema that is used
to parse arguments), and can be a boolean or a string.

The parser now takes a logger and will warn users when encountering a deprecated
option. These options will also appear in JSON help.
This commit is contained in:
Hans Larsen 2018-11-01 15:24:33 -04:00 committed by Keen Yee Liau
parent 9da4bdca81
commit 456614828f
7 changed files with 84 additions and 14 deletions

View File

@ -137,7 +137,7 @@ export abstract class ArchitectCommand<
const builderConf = this._architect.getBuilderConfiguration(targetSpec);
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
const targetOptionArray = await parseJsonSchemaToOptions(this._registry, builderDesc.schema);
const overrides = parseArguments(options, targetOptionArray);
const overrides = parseArguments(options, targetOptionArray, this.logger);
if (overrides['--']) {
(overrides['--'] || []).forEach(additional => {

View File

@ -182,7 +182,7 @@ export async function runCommand(
}
try {
const parsedOptions = parser.parseArguments(args, description.options);
const parsedOptions = parser.parseArguments(args, description.options, logger);
Command.setCommandMap(commandMap);
const command = new description.impl({ workspace }, description, logger);

View File

@ -144,6 +144,12 @@ export interface Option {
*/
positional?: number;
/**
* Deprecation. If this flag is not false a warning will be shown on the console. Either `true`
* or a string to show the user as a notice.
*/
deprecated?: boolean | string;
/**
* Smart default object.
*/

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*
*/
import { BaseException, strings } from '@angular-devkit/core';
import { BaseException, logging, strings } from '@angular-devkit/core';
import { Arguments, Option, OptionType, Value } from './interface';
@ -112,12 +112,15 @@ function _getOptionFromName(name: string, options: Option[]): Option | undefined
function _assignOption(
arg: string,
args: string[],
options: Option[],
parsedOptions: Arguments,
_positionals: string[],
leftovers: string[],
ignored: string[],
errors: string[],
{ options, parsedOptions, leftovers, ignored, errors, deprecations }: {
options: Option[],
parsedOptions: Arguments,
positionals: string[],
leftovers: string[],
ignored: string[],
errors: string[],
deprecations: string[],
},
) {
const from = arg.startsWith('--') ? 2 : 1;
let key = arg.substr(from);
@ -185,6 +188,11 @@ function _assignOption(
const v = _coerce(value, option, parsedOptions[option.name]);
if (v !== undefined) {
parsedOptions[option.name] = v;
if (option.deprecated !== undefined && option.deprecated !== false) {
deprecations.push(`Option ${JSON.stringify(option.name)} is deprecated${
typeof option.deprecated == 'string' ? ': ' + option.deprecated : ''}.`);
}
} else {
let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`;
if (option.enum) {
@ -258,9 +266,14 @@ export function parseFreeFormArguments(args: string[]): Arguments {
*
* @param args The argument array to parse.
* @param options List of supported options. {@see Option}.
* @param logger Logger to use to warn users.
* @returns An object that contains a property per option.
*/
export function parseArguments(args: string[], options: Option[] | null): Arguments {
export function parseArguments(
args: string[],
options: Option[] | null,
logger?: logging.Logger,
): Arguments {
if (options === null) {
options = [];
}
@ -271,6 +284,9 @@ export function parseArguments(args: string[], options: Option[] | null): Argume
const ignored: string[] = [];
const errors: string[] = [];
const deprecations: string[] = [];
const state = { options, parsedOptions, positionals, leftovers, ignored, errors, deprecations };
for (let arg = args.shift(); arg !== undefined; arg = args.shift()) {
if (arg == '--') {
@ -280,7 +296,7 @@ export function parseArguments(args: string[], options: Option[] | null): Argume
}
if (arg.startsWith('--')) {
_assignOption(arg, args, options, parsedOptions, positionals, leftovers, ignored, errors);
_assignOption(arg, args, state);
} else if (arg.startsWith('-')) {
// Argument is of form -abcdef. Starts at 1 because we skip the `-`.
for (let i = 1; i < arg.length; i++) {
@ -288,14 +304,14 @@ export function parseArguments(args: string[], options: Option[] | null): Argume
// If the next character is an '=', treat it as a long flag.
if (arg[i + 1] == '=') {
const f = '-' + flag + arg.slice(i + 1);
_assignOption(f, args, options, parsedOptions, positionals, leftovers, ignored, errors);
_assignOption(f, args, state);
break;
}
// Treat the last flag as `--a` (as if full flag but just one letter). We do this in
// the loop because it saves us a check to see if the arg is just `-`.
if (i == arg.length - 1) {
const arg = '-' + flag;
_assignOption(arg, args, options, parsedOptions, positionals, leftovers, ignored, errors);
_assignOption(arg, args, state);
} else {
const maybeOption = _getOptionFromName(flag, options);
if (maybeOption) {
@ -352,6 +368,10 @@ export function parseArguments(args: string[], options: Option[] | null): Argume
parsedOptions['--'] = [...positionals, ...leftovers];
}
if (deprecations.length > 0 && logger) {
deprecations.forEach(message => logger.warn(message));
}
if (errors.length > 0) {
throw new ParseArgumentException(errors, parsedOptions, ignored);
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*
*/
import { logging } from '@angular-devkit/core';
import { Arguments, Option, OptionType } from './interface';
import { ParseArgumentException, parseArguments } from './parser';
@ -157,4 +158,41 @@ describe('parseArguments', () => {
}
});
});
it('handles deprecation', () => {
const options = [
{ name: 'bool', aliases: [], type: OptionType.Boolean, description: '' },
{ name: 'depr', aliases: [], type: OptionType.Boolean, description: '', deprecated: true },
{ name: 'deprM', aliases: [], type: OptionType.Boolean, description: '', deprecated: 'ABCD' },
];
const logger = new logging.Logger('');
const messages: string[] = [];
logger.subscribe(entry => messages.push(entry.message));
let result = parseArguments(['--bool'], options, logger);
expect(result).toEqual({ bool: true });
expect(messages).toEqual([]);
result = parseArguments(['--depr'], options, logger);
expect(result).toEqual({ depr: true });
expect(messages.length).toEqual(1);
expect(messages[0]).toMatch(/\bdepr\b/);
messages.shift();
result = parseArguments(['--depr', '--bool'], options, logger);
expect(result).toEqual({ depr: true, bool: true });
expect(messages.length).toEqual(1);
expect(messages[0]).toMatch(/\bdepr\b/);
messages.shift();
result = parseArguments(['--depr', '--bool', '--deprM'], options, logger);
expect(result).toEqual({ depr: true, deprM: true, bool: true });
expect(messages.length).toEqual(2);
expect(messages[0]).toMatch(/\bdepr\b/);
expect(messages[1]).toMatch(/\bdeprM\b/);
expect(messages[1]).toMatch(/\bABCD\b/);
messages.shift();
});
});

View File

@ -533,7 +533,7 @@ export abstract class SchematicCommand<
schematicOptions: string[],
options: Option[] | null,
): Promise<Arguments> {
return parseArguments(schematicOptions, options);
return parseArguments(schematicOptions, options, this.logger);
}
private async _loadWorkspace() {

View File

@ -248,6 +248,11 @@ export async function parseJsonSchemaToOptions(
const visible = current.visible === undefined || current.visible === true;
const hidden = !!current.hidden || !visible;
// Deprecated is set only if it's true or a string.
const xDeprecated = current['x-deprecated'];
const deprecated = (xDeprecated === true || typeof xDeprecated == 'string')
? xDeprecated : undefined;
const option: Option = {
name,
description: '' + (current.description === undefined ? '' : current.description),
@ -258,6 +263,7 @@ export async function parseJsonSchemaToOptions(
aliases,
...format !== undefined ? { format } : {},
hidden,
...deprecated !== undefined ? { deprecated } : {},
...positional !== undefined ? { positional } : {},
};