Skip to content

Commit 34818b0

Browse files
committed
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).
1 parent 6622aa9 commit 34818b0

File tree

6 files changed

+118
-73
lines changed

6 files changed

+118
-73
lines changed

packages/angular/cli/commands/generate-impl.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
// tslint:disable:no-global-tslint-disable no-any
1010
import { terminal } from '@angular-devkit/core';
11-
import { Arguments, Option } from '../models/interface';
11+
import { Arguments, SubCommandDescription } from '../models/interface';
1212
import { SchematicCommand } from '../models/schematic-command';
13-
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
13+
import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema';
1414
import { Schema as GenerateCommandSchema } from './generate';
1515

1616
export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
@@ -21,32 +21,36 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
2121
const [collectionName, schematicName] = this.parseSchematicInfo(options);
2222

2323
const collection = this.getCollection(collectionName);
24-
this.description.suboptions = {};
24+
const subcommands: { [name: string]: SubCommandDescription } = {};
2525

2626
const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames();
2727
// Sort as a courtesy for the user.
2828
schematicNames.sort();
2929

3030
for (const name of schematicNames) {
3131
const schematic = this.getSchematic(collection, name, true);
32-
let options: Option[] = [];
32+
let subcommand: SubCommandDescription;
3333
if (schematic.description.schemaJson) {
34-
options = await parseJsonSchemaToOptions(
34+
subcommand = await parseJsonSchemaToSubCommandDescription(
35+
name,
36+
schematic.description.path,
3537
this._workflow.registry,
3638
schematic.description.schemaJson,
3739
);
40+
} else {
41+
continue;
3842
}
3943

4044
if (this.getDefaultSchematicCollection() == collectionName) {
41-
this.description.suboptions[name] = options;
45+
subcommands[name] = subcommand;
4246
} else {
43-
this.description.suboptions[`${collectionName}:${name}`] = options;
47+
subcommands[`${collectionName}:${name}`] = subcommand;
4448
}
4549
}
4650

4751
this.description.options.forEach(option => {
4852
if (option.name == 'schematic') {
49-
option.type = 'suboption';
53+
option.subcommands = subcommands;
5054
}
5155
});
5256
}
@@ -86,7 +90,9 @@ export class GenerateCommand extends SchematicCommand<GenerateCommandSchema> {
8690
await super.printHelp(options);
8791

8892
this.logger.info('');
89-
if (Object.keys(this.description.suboptions || {}).length == 1) {
93+
// Find the generate subcommand.
94+
const subcommand = this.description.options.filter(x => x.subcommands)[0];
95+
if (Object.keys((subcommand && subcommand.subcommands) || {}).length == 1) {
9096
this.logger.info(`\nTo see help for a schematic run:`);
9197
this.logger.info(terminal.cyan(` ng generate <schematic> --help`));
9298
}

packages/angular/cli/models/command.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
CommandDescriptionMap,
1717
CommandScope,
1818
CommandWorkspace,
19-
Option,
19+
Option, SubCommandDescription,
2020
} from './interface';
2121

2222
export interface BaseCommandOptions {
@@ -76,6 +76,12 @@ export abstract class Command<T extends BaseCommandOptions = BaseCommandOptions>
7676
this.logger.info('');
7777
}
7878

79+
protected async printHelpSubcommand(subcommand: SubCommandDescription) {
80+
this.logger.info(subcommand.description);
81+
82+
await this.printHelpOptions(subcommand.options);
83+
}
84+
7985
protected async printHelpOptions(options: Option[] = this.description.options) {
8086
const args = options.filter(opt => opt.positional !== undefined);
8187
const opts = options.filter(opt => opt.positional === undefined);

packages/angular/cli/models/interface.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,23 @@ export interface Option {
8888
* The type of option value. If multiple types exist, this type will be the first one, and the
8989
* types array will contain all types accepted.
9090
*/
91-
type: OptionType | 'suboption';
91+
type: OptionType;
9292

9393
/**
9494
* {@see type}
9595
*/
9696
types?: OptionType[];
9797

98+
/**
99+
* If this option maps to a subcommand in the parent command, will contain all the subcommands
100+
* supported. There is a maximum of 1 subcommand Option per command, and the type of this
101+
* option will always be "string" (no other types). The value of this option will map into
102+
* this map and return the extra information.
103+
*/
104+
subcommands?: {
105+
[name: string]: SubCommandDescription;
106+
};
107+
98108
/**
99109
* Aliases supported by this option.
100110
*/
@@ -143,26 +153,26 @@ export enum CommandScope {
143153
}
144154

145155
/**
146-
* A description of a command, its metadata.
156+
* A description of a command and its options.
147157
*/
148-
export interface CommandDescription {
158+
export interface SubCommandDescription {
149159
/**
150-
* Name of the command.
160+
* The name of the subcommand.
151161
*/
152162
name: string;
153163

154164
/**
155-
* Short description (1-2 lines) of this command.
165+
* Short description (1-2 lines) of this sub command.
156166
*/
157167
description: string;
158168

159169
/**
160-
* A long description of the option, in Markdown format.
170+
* A long description of the sub command, in Markdown format.
161171
*/
162172
longDescription?: string;
163173

164174
/**
165-
* Additional notes about usage of this command.
175+
* Additional notes about usage of this sub command, in Markdown format.
166176
*/
167177
usageNotes?: string;
168178

@@ -172,10 +182,15 @@ export interface CommandDescription {
172182
options: Option[];
173183

174184
/**
175-
* Aliases supported for this command.
185+
* Aliases supported for this sub command.
176186
*/
177187
aliases: string[];
188+
}
178189

190+
/**
191+
* A description of a command, its metadata.
192+
*/
193+
export interface CommandDescription extends SubCommandDescription {
179194
/**
180195
* Scope of the command, whether it can be executed in a project, outside of a project or
181196
* anywhere.
@@ -191,13 +206,6 @@ export interface CommandDescription {
191206
* The constructor of the command, which should be extending the abstract Command<> class.
192207
*/
193208
impl: CommandConstructor;
194-
195-
/**
196-
* Suboptions.
197-
*/
198-
suboptions?: {
199-
[name: string]: Option[];
200-
};
201209
}
202210

203211
export interface OptionSmartDefault {

packages/angular/cli/models/parser.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ function _coerceType(str: string | undefined, type: OptionType, v?: Value): Valu
6060
function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined {
6161
if (!o) {
6262
return _coerceType(str, OptionType.Any, v);
63-
} else if (o.type == 'suboption') {
64-
return _coerceType(str, OptionType.String, v);
6563
} else {
6664
return _coerceType(str, o.type, v);
6765
}

packages/angular/cli/models/schematic-command.ts

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -116,46 +116,60 @@ export abstract class SchematicCommand<
116116
await super.printHelp(options);
117117
this.logger.info('');
118118

119-
const schematicNames = Object.keys(this.description.suboptions || {});
119+
const subCommandOption = this.description.options.filter(x => x.subcommands)[0];
120120

121-
if (this.description.suboptions) {
122-
if (schematicNames.length > 1) {
123-
this.logger.info('Available Schematics:');
121+
if (!subCommandOption || !subCommandOption.subcommands) {
122+
return 0;
123+
}
124124

125-
const namesPerCollection: { [c: string]: string[] } = {};
126-
schematicNames.forEach(name => {
127-
const [collectionName, schematicName] = name.split(/:/, 2);
125+
const schematicNames = Object.keys(subCommandOption.subcommands);
128126

129-
if (!namesPerCollection[collectionName]) {
130-
namesPerCollection[collectionName] = [];
131-
}
127+
if (schematicNames.length > 1) {
128+
this.logger.info('Available Schematics:');
132129

133-
namesPerCollection[collectionName].push(schematicName);
134-
});
130+
const namesPerCollection: { [c: string]: string[] } = {};
131+
schematicNames.forEach(name => {
132+
let [collectionName, schematicName] = name.split(/:/, 2);
133+
if (!schematicName) {
134+
schematicName = collectionName;
135+
collectionName = this.collectionName;
136+
}
135137

136-
const defaultCollection = this.getDefaultSchematicCollection();
137-
Object.keys(namesPerCollection).forEach(collectionName => {
138-
const isDefault = defaultCollection == collectionName;
139-
this.logger.info(
140-
` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`,
141-
);
138+
if (!namesPerCollection[collectionName]) {
139+
namesPerCollection[collectionName] = [];
140+
}
141+
142+
namesPerCollection[collectionName].push(schematicName);
143+
});
142144

143-
namesPerCollection[collectionName].forEach(schematicName => {
144-
this.logger.info(` ${schematicName}`);
145-
});
145+
const defaultCollection = this.getDefaultSchematicCollection();
146+
Object.keys(namesPerCollection).forEach(collectionName => {
147+
const isDefault = defaultCollection == collectionName;
148+
this.logger.info(
149+
` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`,
150+
);
151+
152+
namesPerCollection[collectionName].forEach(schematicName => {
153+
this.logger.info(` ${schematicName}`);
146154
});
147-
} else if (schematicNames.length == 1) {
148-
this.logger.info('Options for schematic ' + schematicNames[0]);
149-
await this.printHelpOptions(this.description.suboptions[schematicNames[0]]);
150-
}
155+
});
156+
} else if (schematicNames.length == 1) {
157+
this.logger.info('Help for schematic ' + schematicNames[0]);
158+
await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]);
151159
}
152160

153161
return 0;
154162
}
155163

156164
async printHelpUsage() {
157-
const schematicNames = Object.keys(this.description.suboptions || {});
158-
if (this.description.suboptions && schematicNames.length == 1) {
165+
const subCommandOption = this.description.options.filter(x => x.subcommands)[0];
166+
167+
if (!subCommandOption || !subCommandOption.subcommands) {
168+
return;
169+
}
170+
171+
const schematicNames = Object.keys(subCommandOption.subcommands);
172+
if (schematicNames.length == 1) {
159173
this.logger.info(this.description.description);
160174

161175
const opts = this.description.options.filter(x => x.positional === undefined);
@@ -167,7 +181,7 @@ export abstract class SchematicCommand<
167181
? schematicName
168182
: schematicNames[0];
169183

170-
const schematicOptions = this.description.suboptions[schematicNames[0]];
184+
const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options;
171185
const schematicArgs = schematicOptions.filter(x => x.positional !== undefined);
172186
const argDisplay = schematicArgs.length > 0
173187
? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ')

packages/angular/cli/utilities/json-schema.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
CommandDescription,
1515
CommandScope,
1616
Option,
17-
OptionType,
17+
OptionType, SubCommandDescription,
1818
} from '../models/interface';
1919

2020
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):
2929
return d;
3030
}
3131

32-
export async function parseJsonSchemaToCommandDescription(
32+
export async function parseJsonSchemaToSubCommandDescription(
3333
name: string,
3434
jsonPath: string,
3535
registry: json.schema.SchemaRegistry,
3636
schema: json.JsonObject,
37-
): Promise<CommandDescription> {
38-
// Before doing any work, let's validate the implementation.
39-
if (typeof schema.$impl != 'string') {
40-
throw new Error(`Command ${name} has an invalid implementation.`);
41-
}
42-
const ref = new ExportStringRef<CommandConstructor>(schema.$impl, dirname(jsonPath));
43-
const impl = ref.ref;
44-
45-
if (impl === undefined || typeof impl !== 'function') {
46-
throw new Error(`Command ${name} has an invalid implementation.`);
47-
}
48-
37+
): Promise<SubCommandDescription> {
4938
const options = await parseJsonSchemaToOptions(registry, schema);
5039

5140
const aliases: string[] = [];
@@ -78,20 +67,44 @@ export async function parseJsonSchemaToCommandDescription(
7867
usageNotes = readFileSync(unPath, 'utf-8');
7968
}
8069

81-
const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default);
82-
const type = _getEnumFromValue(schema.$type, CommandType, CommandType.Default);
8370
const description = '' + (schema.description === undefined ? '' : schema.description);
84-
const hidden = !!schema.$hidden;
8571

8672
return {
8773
name,
8874
description,
8975
...(longDescription ? { longDescription } : {}),
9076
...(usageNotes ? { usageNotes } : {}),
91-
hidden,
9277
options,
9378
aliases,
79+
};
80+
}
81+
82+
export async function parseJsonSchemaToCommandDescription(
83+
name: string,
84+
jsonPath: string,
85+
registry: json.schema.SchemaRegistry,
86+
schema: json.JsonObject,
87+
): Promise<CommandDescription> {
88+
const subcommand = await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema);
89+
90+
// Before doing any work, let's validate the implementation.
91+
if (typeof schema.$impl != 'string') {
92+
throw new Error(`Command ${name} has an invalid implementation.`);
93+
}
94+
const ref = new ExportStringRef<CommandConstructor>(schema.$impl, dirname(jsonPath));
95+
const impl = ref.ref;
96+
97+
if (impl === undefined || typeof impl !== 'function') {
98+
throw new Error(`Command ${name} has an invalid implementation.`);
99+
}
100+
101+
const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default);
102+
const hidden = !!schema.$hidden;
103+
104+
return {
105+
...subcommand,
94106
scope,
107+
hidden,
95108
impl,
96109
};
97110
}

0 commit comments

Comments
 (0)