Skip to content

Commit

Permalink
feat(@angular/cli): add subcommand to options
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
hansl committed Sep 19, 2018
1 parent 6622aa9 commit 34818b0
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 73 deletions.
24 changes: 15 additions & 9 deletions packages/angular/cli/commands/generate-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenerateCommandSchema> {
Expand All @@ -21,32 +21,36 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
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.
schematicNames.sort();

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;
}
});
}
Expand Down Expand Up @@ -86,7 +90,9 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
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 <schematic> --help`));
}
Expand Down
8 changes: 7 additions & 1 deletion packages/angular/cli/models/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
CommandDescriptionMap,
CommandScope,
CommandWorkspace,
Option,
Option, SubCommandDescription,
} from './interface';

export interface BaseCommandOptions {
Expand Down Expand Up @@ -76,6 +76,12 @@ export abstract class Command<T extends BaseCommandOptions = BaseCommandOptions>
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);
Expand Down
38 changes: 23 additions & 15 deletions packages/angular/cli/models/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions packages/angular/cli/models/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
70 changes: 42 additions & 28 deletions packages/angular/cli/models/schematic-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(' ')
Expand Down
49 changes: 31 additions & 18 deletions packages/angular/cli/utilities/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
CommandDescription,
CommandScope,
Option,
OptionType,
OptionType, SubCommandDescription,
} from '../models/interface';

function _getEnumFromValue<E, T extends string>(v: json.JsonValue, e: E, d: T): T {
Expand All @@ -29,23 +29,12 @@ function _getEnumFromValue<E, T extends string>(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<CommandDescription> {
// 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.`);
}

): Promise<SubCommandDescription> {
const options = await parseJsonSchemaToOptions(registry, schema);

const aliases: string[] = [];
Expand Down Expand Up @@ -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<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,
hidden,
impl,
};
}
Expand Down

0 comments on commit 34818b0

Please sign in to comment.