Skip to content

Commit 366cabc

Browse files
alan-agius4dgp1130
authored andcommitted
feat(@angular/cli): add support for multiple schematics collections
The `schematicCollections` can be placed under the `cli` option in the global `.angular.json` configuration, at the root or at project level in `angular.json` . ```jsonc { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "schematicCollections": ["@schematics/angular", "@angular/material"] } // ... } ``` **Rationale** When this option is not configured and a user would like to run a schematic which is not part of `@schematics/angular`, the collection name needs to be provided to `ng generate` command in the form of `[collection-name:schematic-name]`. This make the `ng generate` command too verbose for repeated usages. This is where `schematicCollections` comes handle. When adding `@angular/material` to the list of `schematicCollections`, the generate command will try to locate the schematic in the specified collections. ``` ng generate navigation ``` is equivalent to: ``` ng generate @angular/material:navigation ``` **Conflicting schematic names** When multiple collections have a schematic with the same name. Both `ng generate` and `ng new` will run the first schematic matched based on the ordering (as specified) of `schematicCollections`. DEPRECATED: The `defaultCollection` workspace option has been deprecated in favor of `schematicCollections`. Before ```json "defaultCollection": "@angular/material" ``` After ```json "schematicCollections": ["@angular/material"] ``` Closes #12157
1 parent c9c781c commit 366cabc

File tree

12 files changed

+435
-62
lines changed

12 files changed

+435
-62
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Schematics Collections (`schematicCollections`)
2+
3+
The `schematicCollections` can be placed under the `cli` option in the global `.angular.json` configuration, at the root or at project level in `angular.json` .
4+
5+
```jsonc
6+
{
7+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
8+
"version": 1,
9+
"cli": {
10+
"schematicCollections": ["@schematics/angular", "@angular/material"]
11+
}
12+
// ...
13+
}
14+
```
15+
16+
## Rationale
17+
18+
When this option is not configured and a user would like to run a schematic which is not part of `@schematics/angular`,
19+
the collection name needs to be provided to `ng generate` command in the form of `[collection-name:schematic-name]`. This make the `ng generate` command too verbose for repeated usages.
20+
21+
This is where the `schematicCollections` option can be useful. When adding `@angular/material` to the list of `schematicCollections`, the generate command will try to locate the schematic in the specified collections.
22+
23+
```
24+
ng generate navigation
25+
```
26+
27+
is equivalent to:
28+
29+
```
30+
ng generate @angular/material:navigation
31+
```
32+
33+
## Conflicting schematic names
34+
35+
When multiple collections have a schematic with the same name. Both `ng generate` and `ng new` will run the first schematic matched based on the ordering (as specified) of `schematicCollections`.

packages/angular/cli/lib/config/workspace-schema.json

+20-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,16 @@
4343
"properties": {
4444
"defaultCollection": {
4545
"description": "The default schematics collection to use.",
46-
"type": "string"
46+
"type": "string",
47+
"x-deprecated": "Use 'schematicCollections' instead."
48+
},
49+
"schematicCollections": {
50+
"type": "array",
51+
"description": "The list of schematic collections to use.",
52+
"items": {
53+
"type": "string",
54+
"uniqueItems": true
55+
}
4756
},
4857
"packageManager": {
4958
"description": "Specify which package manager tool to use.",
@@ -162,7 +171,16 @@
162171
"cli": {
163172
"defaultCollection": {
164173
"description": "The default schematics collection to use.",
165-
"type": "string"
174+
"type": "string",
175+
"x-deprecated": "Use 'schematicCollections' instead."
176+
},
177+
"schematicCollections": {
178+
"type": "array",
179+
"description": "The list of schematic collections to use.",
180+
"items": {
181+
"type": "string",
182+
"uniqueItems": true
183+
}
166184
}
167185
},
168186
"schematics": {

packages/angular/cli/src/command-builder/schematics-command-module.ts

+50-26
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { Option, parseJsonSchemaToOptions } from './utilities/json-schema';
3333
import { SchematicEngineHost } from './utilities/schematic-engine-host';
3434
import { subscribeToWorkflow } from './utilities/schematic-workflow';
3535

36-
const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular';
36+
export const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular';
3737

3838
export interface SchematicsCommandArgs {
3939
interactive: boolean;
@@ -95,16 +95,21 @@ export abstract class SchematicsCommandModule
9595
return parseJsonSchemaToOptions(workflow.registry, schemaJson);
9696
}
9797

98-
private _workflowForBuilder: NodeWorkflow | undefined;
98+
private _workflowForBuilder = new Map<string, NodeWorkflow>();
9999
protected getOrCreateWorkflowForBuilder(collectionName: string): NodeWorkflow {
100-
if (this._workflowForBuilder) {
101-
return this._workflowForBuilder;
100+
const cached = this._workflowForBuilder.get(collectionName);
101+
if (cached) {
102+
return cached;
102103
}
103104

104-
return (this._workflowForBuilder = new NodeWorkflow(this.context.root, {
105+
const workflow = new NodeWorkflow(this.context.root, {
105106
resolvePaths: this.getResolvePaths(collectionName),
106107
engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths),
107-
}));
108+
});
109+
110+
this._workflowForBuilder.set(collectionName, workflow);
111+
112+
return workflow;
108113
}
109114

110115
private _workflowForExecution: NodeWorkflow | undefined;
@@ -238,36 +243,55 @@ export abstract class SchematicsCommandModule
238243
return (this._workflowForExecution = workflow);
239244
}
240245

241-
private _defaultSchematicCollection: string | undefined;
242-
protected async getDefaultSchematicCollection(): Promise<string> {
243-
if (this._defaultSchematicCollection) {
244-
return this._defaultSchematicCollection;
246+
private _schematicCollections: Set<string> | undefined;
247+
protected async getSchematicCollections(): Promise<Set<string>> {
248+
if (this._schematicCollections) {
249+
return this._schematicCollections;
245250
}
246251

247-
let workspace = await getWorkspace('local');
252+
const getSchematicCollections = (
253+
configSection: Record<string, unknown> | undefined,
254+
): Set<string> | undefined => {
255+
if (!configSection) {
256+
return undefined;
257+
}
248258

249-
if (workspace) {
250-
const project = getProjectByCwd(workspace);
251-
if (project) {
252-
const value = workspace.getProjectCli(project)['defaultCollection'];
253-
if (typeof value == 'string') {
254-
return (this._defaultSchematicCollection = value);
255-
}
259+
const { schematicCollections, defaultCollection } = configSection;
260+
if (Array.isArray(schematicCollections)) {
261+
return new Set(schematicCollections);
262+
} else if (typeof defaultCollection === 'string') {
263+
return new Set([defaultCollection]);
256264
}
257265

258-
const value = workspace.getCli()['defaultCollection'];
259-
if (typeof value === 'string') {
260-
return (this._defaultSchematicCollection = value);
266+
return undefined;
267+
};
268+
269+
const localWorkspace = await getWorkspace('local');
270+
if (localWorkspace) {
271+
const project = getProjectByCwd(localWorkspace);
272+
if (project) {
273+
const value = getSchematicCollections(localWorkspace.getProjectCli(project));
274+
if (value) {
275+
this._schematicCollections = value;
276+
277+
return value;
278+
}
261279
}
262280
}
263281

264-
workspace = await getWorkspace('global');
265-
const value = workspace?.getCli()['defaultCollection'];
266-
if (typeof value === 'string') {
267-
return (this._defaultSchematicCollection = value);
282+
const globalWorkspace = await getWorkspace('global');
283+
const value =
284+
getSchematicCollections(localWorkspace?.getCli()) ??
285+
getSchematicCollections(globalWorkspace?.getCli());
286+
if (value) {
287+
this._schematicCollections = value;
288+
289+
return value;
268290
}
269291

270-
return (this._defaultSchematicCollection = DEFAULT_SCHEMATICS_COLLECTION);
292+
this._schematicCollections = new Set([DEFAULT_SCHEMATICS_COLLECTION]);
293+
294+
return this._schematicCollections;
271295
}
272296

273297
protected parseSchematicInfo(

packages/angular/cli/src/commands/config/cli.ts

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class ConfigCommandModule
103103
>([
104104
['cli.warnings.versionMismatch', undefined],
105105
['cli.defaultCollection', undefined],
106+
['cli.schematicCollections', undefined],
106107
['cli.packageManager', undefined],
107108
['cli.analytics', undefined],
108109

packages/angular/cli/src/commands/generate/cli.ts

+67-29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { strings } from '@angular-devkit/core';
1010
import { Argv } from 'yargs';
1111
import {
12+
CommandModuleError,
1213
CommandModuleImplementation,
1314
Options,
1415
OtherOptions,
@@ -48,28 +49,9 @@ export class GenerateCommandModule
4849
handler: (options) => this.handler(options),
4950
});
5051

51-
const collectionName = await this.getCollectionName();
52-
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
53-
const collection = workflow.engine.createCollection(collectionName);
54-
const schematicsInCollection = collection.description.schematics;
55-
56-
// We cannot use `collection.listSchematicNames()` as this doesn't return hidden schematics.
57-
const schematicNames = new Set(Object.keys(schematicsInCollection).sort());
58-
const [, schematicNameFromArgs] = this.parseSchematicInfo(
59-
// positional = [generate, component] or [generate]
60-
this.context.args.positional[1],
61-
);
62-
63-
if (schematicNameFromArgs && schematicNames.has(schematicNameFromArgs)) {
64-
// No need to process all schematics since we know which one the user invoked.
65-
schematicNames.clear();
66-
schematicNames.add(schematicNameFromArgs);
67-
}
68-
69-
for (const schematicName of schematicNames) {
70-
if (schematicsInCollection[schematicName].private) {
71-
continue;
72-
}
52+
for (const [schematicName, collectionName] of await this.getSchematicsToRegister()) {
53+
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
54+
const collection = workflow.engine.createCollection(collectionName);
7355

7456
const {
7557
description: {
@@ -110,8 +92,11 @@ export class GenerateCommandModule
11092
async run(options: Options<GenerateCommandArgs> & OtherOptions): Promise<number | void> {
11193
const { dryRun, schematic, defaults, force, interactive, ...schematicOptions } = options;
11294

113-
const [collectionName = await this.getCollectionName(), schematicName = ''] =
114-
this.parseSchematicInfo(schematic);
95+
const [collectionName, schematicName] = this.parseSchematicInfo(schematic);
96+
97+
if (!collectionName || !schematicName) {
98+
throw new CommandModuleError('A collection and schematic is required during execution.');
99+
}
115100

116101
return this.runSchematic({
117102
collectionName,
@@ -126,13 +111,13 @@ export class GenerateCommandModule
126111
});
127112
}
128113

129-
private async getCollectionName(): Promise<string> {
130-
const [collectionName = await this.getDefaultSchematicCollection()] = this.parseSchematicInfo(
114+
private async getCollectionNames(): Promise<string[]> {
115+
const [collectionName] = this.parseSchematicInfo(
131116
// positional = [generate, component] or [generate]
132117
this.context.args.positional[1],
133118
);
134119

135-
return collectionName;
120+
return collectionName ? [collectionName] : [...(await this.getSchematicCollections())];
136121
}
137122

138123
/**
@@ -151,12 +136,15 @@ export class GenerateCommandModule
151136
);
152137

153138
const dasherizedSchematicName = strings.dasherize(schematicName);
139+
const schematicCollectionsFromConfig = await this.getSchematicCollections();
140+
const collectionNames = await this.getCollectionNames();
154141

155-
// Only add the collection name as part of the command when it's not the default collection or when it has been provided via the CLI.
142+
// Only add the collection name as part of the command when it's not a known
143+
// schematics collection or when it has been provided via the CLI.
156144
// Ex:`ng generate @schematics/angular:component`
157145
const commandName =
158146
!!collectionNameFromArgs ||
159-
(await this.getDefaultSchematicCollection()) !== (await this.getCollectionName())
147+
!collectionNames.some((c) => schematicCollectionsFromConfig.has(c))
160148
? collectionName + ':' + dasherizedSchematicName
161149
: dasherizedSchematicName;
162150

@@ -171,4 +159,54 @@ export class GenerateCommandModule
171159

172160
return `${commandName}${positionalArgs ? ' ' + positionalArgs : ''}`;
173161
}
162+
163+
/**
164+
* Get schematics that can to be registered as subcommands.
165+
*/
166+
private async *getSchematics(): AsyncGenerator<{
167+
schematicName: string;
168+
collectionName: string;
169+
}> {
170+
const seenNames = new Set<string>();
171+
for (const collectionName of await this.getCollectionNames()) {
172+
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
173+
const collection = workflow.engine.createCollection(collectionName);
174+
175+
for (const schematicName of collection.listSchematicNames(true /** includeHidden */)) {
176+
// If a schematic with this same name is already registered skip.
177+
if (!seenNames.has(schematicName)) {
178+
seenNames.add(schematicName);
179+
yield { schematicName, collectionName };
180+
}
181+
}
182+
}
183+
}
184+
185+
/**
186+
* Get schematics that should to be registered as subcommands.
187+
*
188+
* @returns a sorted list of schematic that needs to be registered as subcommands.
189+
*/
190+
private async getSchematicsToRegister(): Promise<
191+
[schematicName: string, collectionName: string][]
192+
> {
193+
const schematicsToRegister: [schematicName: string, collectionName: string][] = [];
194+
const [, schematicNameFromArgs] = this.parseSchematicInfo(
195+
// positional = [generate, component] or [generate]
196+
this.context.args.positional[1],
197+
);
198+
199+
for await (const { schematicName, collectionName } of this.getSchematics()) {
200+
if (schematicName === schematicNameFromArgs) {
201+
return [[schematicName, collectionName]];
202+
}
203+
204+
schematicsToRegister.push([schematicName, collectionName]);
205+
}
206+
207+
// Didn't find the schematic or no schematic name was provided Ex: `ng generate --help`.
208+
return schematicsToRegister.sort(([nameA], [nameB]) =>
209+
nameA.localeCompare(nameB, undefined, { sensitivity: 'accent' }),
210+
);
211+
}
174212
}

packages/angular/cli/src/commands/new/cli.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
OtherOptions,
1515
} from '../../command-builder/command-module';
1616
import {
17+
DEFAULT_SCHEMATICS_COLLECTION,
1718
SchematicsCommandArgs,
1819
SchematicsCommandModule,
1920
} from '../../command-builder/schematics-command-module';
@@ -51,7 +52,7 @@ export class NewCommandModule
5152
const collectionName =
5253
typeof collectionNameFromArgs === 'string'
5354
? collectionNameFromArgs
54-
: await this.getDefaultSchematicCollection();
55+
: await this.getCollectionFromConfig();
5556

5657
const workflow = await this.getOrCreateWorkflowForBuilder(collectionName);
5758
const collection = workflow.engine.createCollection(collectionName);
@@ -62,7 +63,7 @@ export class NewCommandModule
6263

6364
async run(options: Options<NewCommandArgs> & OtherOptions): Promise<number | void> {
6465
// Register the version of the CLI in the registry.
65-
const collectionName = options.collection ?? (await this.getDefaultSchematicCollection());
66+
const collectionName = options.collection ?? (await this.getCollectionFromConfig());
6667
const workflow = await this.getOrCreateWorkflowForExecution(collectionName, options);
6768
workflow.registry.addSmartDefaultProvider('ng-cli-version', () => VERSION.full);
6869

@@ -89,4 +90,19 @@ export class NewCommandModule
8990
},
9091
});
9192
}
93+
94+
/** Find a collection from config that has an `ng-new` schematic. */
95+
private async getCollectionFromConfig(): Promise<string> {
96+
for (const collectionName of await this.getSchematicCollections()) {
97+
const workflow = this.getOrCreateWorkflowForBuilder(collectionName);
98+
const collection = workflow.engine.createCollection(collectionName);
99+
const schematicsInCollection = collection.description.schematics;
100+
101+
if (Object.keys(schematicsInCollection).includes(this.schematicName)) {
102+
return collectionName;
103+
}
104+
}
105+
106+
return DEFAULT_SCHEMATICS_COLLECTION;
107+
}
92108
}

packages/schematics/angular/migrations/migration-collection.json

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
"version": "14.0.0",
2020
"factory": "./update-14/remove-default-project-option",
2121
"description": "Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory."
22+
},
23+
"replace-default-collection-option": {
24+
"version": "14.0.0",
25+
"factory": "./update-14/replace-default-collection-option",
26+
"description": "Replace 'defaultCollection' option in workspace configuration with 'schematicCollections'."
2227
}
2328
}
2429
}

0 commit comments

Comments
 (0)