mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 19:13:34 +08:00
feat(@angular/cli): add subcommand to options
SubCommands are not tied to the option that triggers them. They contain a subset of a CommandDescription interface, with at least a short and long description and usage notes. These are generated from the subcommand schema (e.g. schematics in case of generate).
This commit is contained in:
parent
6622aa9d1a
commit
34818b0346
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
// tslint:disable:no-global-tslint-disable no-any
|
// tslint:disable:no-global-tslint-disable no-any
|
||||||
import { terminal } from '@angular-devkit/core';
|
import { terminal } from '@angular-devkit/core';
|
||||||
import { Arguments, Option } from '../models/interface';
|
import { Arguments, SubCommandDescription } from '../models/interface';
|
||||||
import { SchematicCommand } from '../models/schematic-command';
|
import { SchematicCommand } from '../models/schematic-command';
|
||||||
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
|
import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema';
|
||||||
import { Schema as GenerateCommandSchema } from './generate';
|
import { Schema as GenerateCommandSchema } from './generate';
|
||||||
|
|
||||||
export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
|
export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
|
||||||
@ -21,7 +21,7 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
|
|||||||
const [collectionName, schematicName] = this.parseSchematicInfo(options);
|
const [collectionName, schematicName] = this.parseSchematicInfo(options);
|
||||||
|
|
||||||
const collection = this.getCollection(collectionName);
|
const collection = this.getCollection(collectionName);
|
||||||
this.description.suboptions = {};
|
const subcommands: { [name: string]: SubCommandDescription } = {};
|
||||||
|
|
||||||
const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames();
|
const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames();
|
||||||
// Sort as a courtesy for the user.
|
// Sort as a courtesy for the user.
|
||||||
@ -29,24 +29,28 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
|
|||||||
|
|
||||||
for (const name of schematicNames) {
|
for (const name of schematicNames) {
|
||||||
const schematic = this.getSchematic(collection, name, true);
|
const schematic = this.getSchematic(collection, name, true);
|
||||||
let options: Option[] = [];
|
let subcommand: SubCommandDescription;
|
||||||
if (schematic.description.schemaJson) {
|
if (schematic.description.schemaJson) {
|
||||||
options = await parseJsonSchemaToOptions(
|
subcommand = await parseJsonSchemaToSubCommandDescription(
|
||||||
|
name,
|
||||||
|
schematic.description.path,
|
||||||
this._workflow.registry,
|
this._workflow.registry,
|
||||||
schematic.description.schemaJson,
|
schematic.description.schemaJson,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getDefaultSchematicCollection() == collectionName) {
|
if (this.getDefaultSchematicCollection() == collectionName) {
|
||||||
this.description.suboptions[name] = options;
|
subcommands[name] = subcommand;
|
||||||
} else {
|
} else {
|
||||||
this.description.suboptions[`${collectionName}:${name}`] = options;
|
subcommands[`${collectionName}:${name}`] = subcommand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.description.options.forEach(option => {
|
this.description.options.forEach(option => {
|
||||||
if (option.name == 'schematic') {
|
if (option.name == 'schematic') {
|
||||||
option.type = 'suboption';
|
option.subcommands = subcommands;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -86,7 +90,9 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
|
|||||||
await super.printHelp(options);
|
await super.printHelp(options);
|
||||||
|
|
||||||
this.logger.info('');
|
this.logger.info('');
|
||||||
if (Object.keys(this.description.suboptions || {}).length == 1) {
|
// Find the generate subcommand.
|
||||||
|
const subcommand = this.description.options.filter(x => x.subcommands)[0];
|
||||||
|
if (Object.keys((subcommand && subcommand.subcommands) || {}).length == 1) {
|
||||||
this.logger.info(`\nTo see help for a schematic run:`);
|
this.logger.info(`\nTo see help for a schematic run:`);
|
||||||
this.logger.info(terminal.cyan(` ng generate <schematic> --help`));
|
this.logger.info(terminal.cyan(` ng generate <schematic> --help`));
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
CommandDescriptionMap,
|
CommandDescriptionMap,
|
||||||
CommandScope,
|
CommandScope,
|
||||||
CommandWorkspace,
|
CommandWorkspace,
|
||||||
Option,
|
Option, SubCommandDescription,
|
||||||
} from './interface';
|
} from './interface';
|
||||||
|
|
||||||
export interface BaseCommandOptions {
|
export interface BaseCommandOptions {
|
||||||
@ -76,6 +76,12 @@ export abstract class Command<T extends BaseCommandOptions = BaseCommandOptions>
|
|||||||
this.logger.info('');
|
this.logger.info('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async printHelpSubcommand(subcommand: SubCommandDescription) {
|
||||||
|
this.logger.info(subcommand.description);
|
||||||
|
|
||||||
|
await this.printHelpOptions(subcommand.options);
|
||||||
|
}
|
||||||
|
|
||||||
protected async printHelpOptions(options: Option[] = this.description.options) {
|
protected async printHelpOptions(options: Option[] = this.description.options) {
|
||||||
const args = options.filter(opt => opt.positional !== undefined);
|
const args = options.filter(opt => opt.positional !== undefined);
|
||||||
const opts = options.filter(opt => opt.positional === undefined);
|
const opts = options.filter(opt => opt.positional === undefined);
|
||||||
|
@ -88,13 +88,23 @@ export interface Option {
|
|||||||
* The type of option value. If multiple types exist, this type will be the first one, and the
|
* The type of option value. If multiple types exist, this type will be the first one, and the
|
||||||
* types array will contain all types accepted.
|
* types array will contain all types accepted.
|
||||||
*/
|
*/
|
||||||
type: OptionType | 'suboption';
|
type: OptionType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@see type}
|
* {@see type}
|
||||||
*/
|
*/
|
||||||
types?: OptionType[];
|
types?: OptionType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this option maps to a subcommand in the parent command, will contain all the subcommands
|
||||||
|
* supported. There is a maximum of 1 subcommand Option per command, and the type of this
|
||||||
|
* option will always be "string" (no other types). The value of this option will map into
|
||||||
|
* this map and return the extra information.
|
||||||
|
*/
|
||||||
|
subcommands?: {
|
||||||
|
[name: string]: SubCommandDescription;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aliases supported by this option.
|
* Aliases supported by this option.
|
||||||
*/
|
*/
|
||||||
@ -143,26 +153,26 @@ export enum CommandScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A description of a command, its metadata.
|
* A description of a command and its options.
|
||||||
*/
|
*/
|
||||||
export interface CommandDescription {
|
export interface SubCommandDescription {
|
||||||
/**
|
/**
|
||||||
* Name of the command.
|
* The name of the subcommand.
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Short description (1-2 lines) of this command.
|
* Short description (1-2 lines) of this sub command.
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A long description of the option, in Markdown format.
|
* A long description of the sub command, in Markdown format.
|
||||||
*/
|
*/
|
||||||
longDescription?: string;
|
longDescription?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional notes about usage of this command.
|
* Additional notes about usage of this sub command, in Markdown format.
|
||||||
*/
|
*/
|
||||||
usageNotes?: string;
|
usageNotes?: string;
|
||||||
|
|
||||||
@ -172,10 +182,15 @@ export interface CommandDescription {
|
|||||||
options: Option[];
|
options: Option[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aliases supported for this command.
|
* Aliases supported for this sub command.
|
||||||
*/
|
*/
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A description of a command, its metadata.
|
||||||
|
*/
|
||||||
|
export interface CommandDescription extends SubCommandDescription {
|
||||||
/**
|
/**
|
||||||
* Scope of the command, whether it can be executed in a project, outside of a project or
|
* Scope of the command, whether it can be executed in a project, outside of a project or
|
||||||
* anywhere.
|
* anywhere.
|
||||||
@ -191,13 +206,6 @@ export interface CommandDescription {
|
|||||||
* The constructor of the command, which should be extending the abstract Command<> class.
|
* The constructor of the command, which should be extending the abstract Command<> class.
|
||||||
*/
|
*/
|
||||||
impl: CommandConstructor;
|
impl: CommandConstructor;
|
||||||
|
|
||||||
/**
|
|
||||||
* Suboptions.
|
|
||||||
*/
|
|
||||||
suboptions?: {
|
|
||||||
[name: string]: Option[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionSmartDefault {
|
export interface OptionSmartDefault {
|
||||||
|
@ -60,8 +60,6 @@ function _coerceType(str: string | undefined, type: OptionType, v?: Value): Valu
|
|||||||
function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined {
|
function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined {
|
||||||
if (!o) {
|
if (!o) {
|
||||||
return _coerceType(str, OptionType.Any, v);
|
return _coerceType(str, OptionType.Any, v);
|
||||||
} else if (o.type == 'suboption') {
|
|
||||||
return _coerceType(str, OptionType.String, v);
|
|
||||||
} else {
|
} else {
|
||||||
return _coerceType(str, o.type, v);
|
return _coerceType(str, o.type, v);
|
||||||
}
|
}
|
||||||
|
@ -116,46 +116,60 @@ export abstract class SchematicCommand<
|
|||||||
await super.printHelp(options);
|
await super.printHelp(options);
|
||||||
this.logger.info('');
|
this.logger.info('');
|
||||||
|
|
||||||
const schematicNames = Object.keys(this.description.suboptions || {});
|
const subCommandOption = this.description.options.filter(x => x.subcommands)[0];
|
||||||
|
|
||||||
if (this.description.suboptions) {
|
if (!subCommandOption || !subCommandOption.subcommands) {
|
||||||
if (schematicNames.length > 1) {
|
return 0;
|
||||||
this.logger.info('Available Schematics:');
|
}
|
||||||
|
|
||||||
const namesPerCollection: { [c: string]: string[] } = {};
|
const schematicNames = Object.keys(subCommandOption.subcommands);
|
||||||
schematicNames.forEach(name => {
|
|
||||||
const [collectionName, schematicName] = name.split(/:/, 2);
|
|
||||||
|
|
||||||
if (!namesPerCollection[collectionName]) {
|
if (schematicNames.length > 1) {
|
||||||
namesPerCollection[collectionName] = [];
|
this.logger.info('Available Schematics:');
|
||||||
}
|
|
||||||
|
|
||||||
namesPerCollection[collectionName].push(schematicName);
|
const namesPerCollection: { [c: string]: string[] } = {};
|
||||||
|
schematicNames.forEach(name => {
|
||||||
|
let [collectionName, schematicName] = name.split(/:/, 2);
|
||||||
|
if (!schematicName) {
|
||||||
|
schematicName = collectionName;
|
||||||
|
collectionName = this.collectionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!namesPerCollection[collectionName]) {
|
||||||
|
namesPerCollection[collectionName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
namesPerCollection[collectionName].push(schematicName);
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultCollection = this.getDefaultSchematicCollection();
|
||||||
|
Object.keys(namesPerCollection).forEach(collectionName => {
|
||||||
|
const isDefault = defaultCollection == collectionName;
|
||||||
|
this.logger.info(
|
||||||
|
` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`,
|
||||||
|
);
|
||||||
|
|
||||||
|
namesPerCollection[collectionName].forEach(schematicName => {
|
||||||
|
this.logger.info(` ${schematicName}`);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
const defaultCollection = this.getDefaultSchematicCollection();
|
} else if (schematicNames.length == 1) {
|
||||||
Object.keys(namesPerCollection).forEach(collectionName => {
|
this.logger.info('Help for schematic ' + schematicNames[0]);
|
||||||
const isDefault = defaultCollection == collectionName;
|
await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]);
|
||||||
this.logger.info(
|
|
||||||
` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`,
|
|
||||||
);
|
|
||||||
|
|
||||||
namesPerCollection[collectionName].forEach(schematicName => {
|
|
||||||
this.logger.info(` ${schematicName}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (schematicNames.length == 1) {
|
|
||||||
this.logger.info('Options for schematic ' + schematicNames[0]);
|
|
||||||
await this.printHelpOptions(this.description.suboptions[schematicNames[0]]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async printHelpUsage() {
|
async printHelpUsage() {
|
||||||
const schematicNames = Object.keys(this.description.suboptions || {});
|
const subCommandOption = this.description.options.filter(x => x.subcommands)[0];
|
||||||
if (this.description.suboptions && schematicNames.length == 1) {
|
|
||||||
|
if (!subCommandOption || !subCommandOption.subcommands) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schematicNames = Object.keys(subCommandOption.subcommands);
|
||||||
|
if (schematicNames.length == 1) {
|
||||||
this.logger.info(this.description.description);
|
this.logger.info(this.description.description);
|
||||||
|
|
||||||
const opts = this.description.options.filter(x => x.positional === undefined);
|
const opts = this.description.options.filter(x => x.positional === undefined);
|
||||||
@ -167,7 +181,7 @@ export abstract class SchematicCommand<
|
|||||||
? schematicName
|
? schematicName
|
||||||
: schematicNames[0];
|
: schematicNames[0];
|
||||||
|
|
||||||
const schematicOptions = this.description.suboptions[schematicNames[0]];
|
const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options;
|
||||||
const schematicArgs = schematicOptions.filter(x => x.positional !== undefined);
|
const schematicArgs = schematicOptions.filter(x => x.positional !== undefined);
|
||||||
const argDisplay = schematicArgs.length > 0
|
const argDisplay = schematicArgs.length > 0
|
||||||
? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ')
|
? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ')
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
CommandDescription,
|
CommandDescription,
|
||||||
CommandScope,
|
CommandScope,
|
||||||
Option,
|
Option,
|
||||||
OptionType,
|
OptionType, SubCommandDescription,
|
||||||
} from '../models/interface';
|
} from '../models/interface';
|
||||||
|
|
||||||
function _getEnumFromValue<E, T extends string>(v: json.JsonValue, e: E, d: T): T {
|
function _getEnumFromValue<E, T extends string>(v: json.JsonValue, e: E, d: T): T {
|
||||||
@ -29,23 +29,12 @@ function _getEnumFromValue<E, T extends string>(v: json.JsonValue, e: E, d: T):
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseJsonSchemaToCommandDescription(
|
export async function parseJsonSchemaToSubCommandDescription(
|
||||||
name: string,
|
name: string,
|
||||||
jsonPath: string,
|
jsonPath: string,
|
||||||
registry: json.schema.SchemaRegistry,
|
registry: json.schema.SchemaRegistry,
|
||||||
schema: json.JsonObject,
|
schema: json.JsonObject,
|
||||||
): Promise<CommandDescription> {
|
): Promise<SubCommandDescription> {
|
||||||
// Before doing any work, let's validate the implementation.
|
|
||||||
if (typeof schema.$impl != 'string') {
|
|
||||||
throw new Error(`Command ${name} has an invalid implementation.`);
|
|
||||||
}
|
|
||||||
const ref = new ExportStringRef<CommandConstructor>(schema.$impl, dirname(jsonPath));
|
|
||||||
const impl = ref.ref;
|
|
||||||
|
|
||||||
if (impl === undefined || typeof impl !== 'function') {
|
|
||||||
throw new Error(`Command ${name} has an invalid implementation.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = await parseJsonSchemaToOptions(registry, schema);
|
const options = await parseJsonSchemaToOptions(registry, schema);
|
||||||
|
|
||||||
const aliases: string[] = [];
|
const aliases: string[] = [];
|
||||||
@ -78,20 +67,44 @@ export async function parseJsonSchemaToCommandDescription(
|
|||||||
usageNotes = readFileSync(unPath, 'utf-8');
|
usageNotes = readFileSync(unPath, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default);
|
|
||||||
const type = _getEnumFromValue(schema.$type, CommandType, CommandType.Default);
|
|
||||||
const description = '' + (schema.description === undefined ? '' : schema.description);
|
const description = '' + (schema.description === undefined ? '' : schema.description);
|
||||||
const hidden = !!schema.$hidden;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
...(longDescription ? { longDescription } : {}),
|
...(longDescription ? { longDescription } : {}),
|
||||||
...(usageNotes ? { usageNotes } : {}),
|
...(usageNotes ? { usageNotes } : {}),
|
||||||
hidden,
|
|
||||||
options,
|
options,
|
||||||
aliases,
|
aliases,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseJsonSchemaToCommandDescription(
|
||||||
|
name: string,
|
||||||
|
jsonPath: string,
|
||||||
|
registry: json.schema.SchemaRegistry,
|
||||||
|
schema: json.JsonObject,
|
||||||
|
): Promise<CommandDescription> {
|
||||||
|
const subcommand = await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema);
|
||||||
|
|
||||||
|
// Before doing any work, let's validate the implementation.
|
||||||
|
if (typeof schema.$impl != 'string') {
|
||||||
|
throw new Error(`Command ${name} has an invalid implementation.`);
|
||||||
|
}
|
||||||
|
const ref = new ExportStringRef<CommandConstructor>(schema.$impl, dirname(jsonPath));
|
||||||
|
const impl = ref.ref;
|
||||||
|
|
||||||
|
if (impl === undefined || typeof impl !== 'function') {
|
||||||
|
throw new Error(`Command ${name} has an invalid implementation.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default);
|
||||||
|
const hidden = !!schema.$hidden;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...subcommand,
|
||||||
scope,
|
scope,
|
||||||
|
hidden,
|
||||||
impl,
|
impl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user