diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index a47dc270f83f..b47808a7e1f9 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -8,9 +8,9 @@ // tslint:disable:no-global-tslint-disable no-any 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 { parseJsonSchemaToOptions } from '../utilities/json-schema'; +import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema'; import { Schema as GenerateCommandSchema } from './generate'; export class GenerateCommand extends SchematicCommand { @@ -21,7 +21,7 @@ export class GenerateCommand extends SchematicCommand { const [collectionName, schematicName] = this.parseSchematicInfo(options); const collection = this.getCollection(collectionName); - this.description.suboptions = {}; + const subcommands: { [name: string]: SubCommandDescription } = {}; const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); // Sort as a courtesy for the user. @@ -29,24 +29,28 @@ export class GenerateCommand extends SchematicCommand { for (const name of schematicNames) { const schematic = this.getSchematic(collection, name, true); - let options: Option[] = []; + let subcommand: SubCommandDescription; if (schematic.description.schemaJson) { - options = await parseJsonSchemaToOptions( + subcommand = await parseJsonSchemaToSubCommandDescription( + name, + schematic.description.path, this._workflow.registry, schematic.description.schemaJson, ); + } else { + continue; } if (this.getDefaultSchematicCollection() == collectionName) { - this.description.suboptions[name] = options; + subcommands[name] = subcommand; } else { - this.description.suboptions[`${collectionName}:${name}`] = options; + subcommands[`${collectionName}:${name}`] = subcommand; } } this.description.options.forEach(option => { if (option.name == 'schematic') { - option.type = 'suboption'; + option.subcommands = subcommands; } }); } @@ -86,7 +90,9 @@ export class GenerateCommand extends SchematicCommand { await super.printHelp(options); 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(terminal.cyan(` ng generate --help`)); } diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 4202d8bd4c6a..14442c4922bb 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -16,7 +16,7 @@ import { CommandDescriptionMap, CommandScope, CommandWorkspace, - Option, + Option, SubCommandDescription, } from './interface'; export interface BaseCommandOptions { @@ -76,6 +76,12 @@ export abstract class Command 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) { const args = options.filter(opt => opt.positional !== undefined); const opts = options.filter(opt => opt.positional === undefined); diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 0406d1af86cc..261540412d0c 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -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 * types array will contain all types accepted. */ - type: OptionType | 'suboption'; + type: OptionType; /** * {@see type} */ 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. */ @@ -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; /** - * Short description (1-2 lines) of this command. + * Short description (1-2 lines) of this sub command. */ description: string; /** - * A long description of the option, in Markdown format. + * A long description of the sub command, in Markdown format. */ longDescription?: string; /** - * Additional notes about usage of this command. + * Additional notes about usage of this sub command, in Markdown format. */ usageNotes?: string; @@ -172,10 +182,15 @@ export interface CommandDescription { options: Option[]; /** - * Aliases supported for this command. + * Aliases supported for this sub command. */ 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 * anywhere. @@ -191,13 +206,6 @@ export interface CommandDescription { * The constructor of the command, which should be extending the abstract Command<> class. */ impl: CommandConstructor; - - /** - * Suboptions. - */ - suboptions?: { - [name: string]: Option[]; - }; } export interface OptionSmartDefault { diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts index 2bf7a89098e0..94f190e75080 100644 --- a/packages/angular/cli/models/parser.ts +++ b/packages/angular/cli/models/parser.ts @@ -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 { if (!o) { return _coerceType(str, OptionType.Any, v); - } else if (o.type == 'suboption') { - return _coerceType(str, OptionType.String, v); } else { return _coerceType(str, o.type, v); } diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index a9081bf4fc51..d6ebcc96452c 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -116,46 +116,60 @@ export abstract class SchematicCommand< await super.printHelp(options); 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 (schematicNames.length > 1) { - this.logger.info('Available Schematics:'); + if (!subCommandOption || !subCommandOption.subcommands) { + return 0; + } - const namesPerCollection: { [c: string]: string[] } = {}; - schematicNames.forEach(name => { - const [collectionName, schematicName] = name.split(/:/, 2); + const schematicNames = Object.keys(subCommandOption.subcommands); - if (!namesPerCollection[collectionName]) { - namesPerCollection[collectionName] = []; - } + if (schematicNames.length > 1) { + 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; + } - const defaultCollection = this.getDefaultSchematicCollection(); - Object.keys(namesPerCollection).forEach(collectionName => { - const isDefault = defaultCollection == collectionName; - this.logger.info( - ` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`, - ); + if (!namesPerCollection[collectionName]) { + namesPerCollection[collectionName] = []; + } + + namesPerCollection[collectionName].push(schematicName); + }); - namesPerCollection[collectionName].forEach(schematicName => { - this.logger.info(` ${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}`); }); - } else if (schematicNames.length == 1) { - this.logger.info('Options for schematic ' + schematicNames[0]); - await this.printHelpOptions(this.description.suboptions[schematicNames[0]]); - } + }); + } else if (schematicNames.length == 1) { + this.logger.info('Help for schematic ' + schematicNames[0]); + await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]); } return 0; } async printHelpUsage() { - const schematicNames = Object.keys(this.description.suboptions || {}); - if (this.description.suboptions && schematicNames.length == 1) { + const subCommandOption = this.description.options.filter(x => x.subcommands)[0]; + + if (!subCommandOption || !subCommandOption.subcommands) { + return; + } + + const schematicNames = Object.keys(subCommandOption.subcommands); + if (schematicNames.length == 1) { this.logger.info(this.description.description); const opts = this.description.options.filter(x => x.positional === undefined); @@ -167,7 +181,7 @@ export abstract class SchematicCommand< ? schematicName : 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 argDisplay = schematicArgs.length > 0 ? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ') diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts index 990488cca359..8e8ec4cc29b4 100644 --- a/packages/angular/cli/utilities/json-schema.ts +++ b/packages/angular/cli/utilities/json-schema.ts @@ -14,7 +14,7 @@ import { CommandDescription, CommandScope, Option, - OptionType, + OptionType, SubCommandDescription, } from '../models/interface'; function _getEnumFromValue(v: json.JsonValue, e: E, d: T): T { @@ -29,23 +29,12 @@ function _getEnumFromValue(v: json.JsonValue, e: E, d: T): return d; } -export async function parseJsonSchemaToCommandDescription( +export async function parseJsonSchemaToSubCommandDescription( name: string, jsonPath: string, registry: json.schema.SchemaRegistry, schema: json.JsonObject, -): Promise { - // 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(schema.$impl, dirname(jsonPath)); - const impl = ref.ref; - - if (impl === undefined || typeof impl !== 'function') { - throw new Error(`Command ${name} has an invalid implementation.`); - } - +): Promise { const options = await parseJsonSchemaToOptions(registry, schema); const aliases: string[] = []; @@ -78,20 +67,44 @@ export async function parseJsonSchemaToCommandDescription( 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 hidden = !!schema.$hidden; return { name, description, ...(longDescription ? { longDescription } : {}), ...(usageNotes ? { usageNotes } : {}), - hidden, options, aliases, + }; +} + +export async function parseJsonSchemaToCommandDescription( + name: string, + jsonPath: string, + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + 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(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, + hidden, impl, }; }