From 366cabc66c3dd836e2fdfea8dad6c4c7c2096b1d Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 22 Mar 2022 15:30:06 +0100 Subject: [PATCH] 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 --- .../schematic-collections-config.md | 35 ++++++ .../cli/lib/config/workspace-schema.json | 22 +++- .../schematics-command-module.ts | 76 ++++++++----- .../angular/cli/src/commands/config/cli.ts | 1 + .../angular/cli/src/commands/generate/cli.ts | 96 ++++++++++++----- packages/angular/cli/src/commands/new/cli.ts | 20 +++- .../migrations/migration-collection.json | 5 + .../replace-default-collection-option.ts | 35 ++++++ .../replace-default-collection-option_spec.ts | 101 ++++++++++++++++++ .../angular/utility/workspace-models.ts | 9 +- .../e2e/tests/generate/help-output.ts | 2 +- .../tests/generate/schematics-collections.ts | 95 ++++++++++++++++ 12 files changed, 435 insertions(+), 62 deletions(-) create mode 100644 docs/specifications/schematic-collections-config.md create mode 100644 packages/schematics/angular/migrations/update-14/replace-default-collection-option.ts create mode 100644 packages/schematics/angular/migrations/update-14/replace-default-collection-option_spec.ts create mode 100644 tests/legacy-cli/e2e/tests/generate/schematics-collections.ts diff --git a/docs/specifications/schematic-collections-config.md b/docs/specifications/schematic-collections-config.md new file mode 100644 index 000000000000..5d52fb394d16 --- /dev/null +++ b/docs/specifications/schematic-collections-config.md @@ -0,0 +1,35 @@ +# Schematics Collections (`schematicCollections`) + +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 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. + +``` +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`. diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index 4154e858ba53..f7a9833c269d 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -43,7 +43,16 @@ "properties": { "defaultCollection": { "description": "The default schematics collection to use.", - "type": "string" + "type": "string", + "x-deprecated": "Use 'schematicCollections' instead." + }, + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } }, "packageManager": { "description": "Specify which package manager tool to use.", @@ -162,7 +171,16 @@ "cli": { "defaultCollection": { "description": "The default schematics collection to use.", - "type": "string" + "type": "string", + "x-deprecated": "Use 'schematicCollections' instead." + }, + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } } }, "schematics": { diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index d72b5b265dad..ca840d03243f 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -33,7 +33,7 @@ import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; import { SchematicEngineHost } from './utilities/schematic-engine-host'; import { subscribeToWorkflow } from './utilities/schematic-workflow'; -const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; +export const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; export interface SchematicsCommandArgs { interactive: boolean; @@ -95,16 +95,21 @@ export abstract class SchematicsCommandModule return parseJsonSchemaToOptions(workflow.registry, schemaJson); } - private _workflowForBuilder: NodeWorkflow | undefined; + private _workflowForBuilder = new Map(); protected getOrCreateWorkflowForBuilder(collectionName: string): NodeWorkflow { - if (this._workflowForBuilder) { - return this._workflowForBuilder; + const cached = this._workflowForBuilder.get(collectionName); + if (cached) { + return cached; } - return (this._workflowForBuilder = new NodeWorkflow(this.context.root, { + const workflow = new NodeWorkflow(this.context.root, { resolvePaths: this.getResolvePaths(collectionName), engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), - })); + }); + + this._workflowForBuilder.set(collectionName, workflow); + + return workflow; } private _workflowForExecution: NodeWorkflow | undefined; @@ -238,36 +243,55 @@ export abstract class SchematicsCommandModule return (this._workflowForExecution = workflow); } - private _defaultSchematicCollection: string | undefined; - protected async getDefaultSchematicCollection(): Promise { - if (this._defaultSchematicCollection) { - return this._defaultSchematicCollection; + private _schematicCollections: Set | undefined; + protected async getSchematicCollections(): Promise> { + if (this._schematicCollections) { + return this._schematicCollections; } - let workspace = await getWorkspace('local'); + const getSchematicCollections = ( + configSection: Record | undefined, + ): Set | undefined => { + if (!configSection) { + return undefined; + } - if (workspace) { - const project = getProjectByCwd(workspace); - if (project) { - const value = workspace.getProjectCli(project)['defaultCollection']; - if (typeof value == 'string') { - return (this._defaultSchematicCollection = value); - } + const { schematicCollections, defaultCollection } = configSection; + if (Array.isArray(schematicCollections)) { + return new Set(schematicCollections); + } else if (typeof defaultCollection === 'string') { + return new Set([defaultCollection]); } - const value = workspace.getCli()['defaultCollection']; - if (typeof value === 'string') { - return (this._defaultSchematicCollection = value); + return undefined; + }; + + const localWorkspace = await getWorkspace('local'); + if (localWorkspace) { + const project = getProjectByCwd(localWorkspace); + if (project) { + const value = getSchematicCollections(localWorkspace.getProjectCli(project)); + if (value) { + this._schematicCollections = value; + + return value; + } } } - workspace = await getWorkspace('global'); - const value = workspace?.getCli()['defaultCollection']; - if (typeof value === 'string') { - return (this._defaultSchematicCollection = value); + const globalWorkspace = await getWorkspace('global'); + const value = + getSchematicCollections(localWorkspace?.getCli()) ?? + getSchematicCollections(globalWorkspace?.getCli()); + if (value) { + this._schematicCollections = value; + + return value; } - return (this._defaultSchematicCollection = DEFAULT_SCHEMATICS_COLLECTION); + this._schematicCollections = new Set([DEFAULT_SCHEMATICS_COLLECTION]); + + return this._schematicCollections; } protected parseSchematicInfo( diff --git a/packages/angular/cli/src/commands/config/cli.ts b/packages/angular/cli/src/commands/config/cli.ts index de5cb5596981..5f9bf9a20bd8 100644 --- a/packages/angular/cli/src/commands/config/cli.ts +++ b/packages/angular/cli/src/commands/config/cli.ts @@ -103,6 +103,7 @@ export class ConfigCommandModule >([ ['cli.warnings.versionMismatch', undefined], ['cli.defaultCollection', undefined], + ['cli.schematicCollections', undefined], ['cli.packageManager', undefined], ['cli.analytics', undefined], diff --git a/packages/angular/cli/src/commands/generate/cli.ts b/packages/angular/cli/src/commands/generate/cli.ts index 55f58a32dc2b..43e66ae18ffe 100644 --- a/packages/angular/cli/src/commands/generate/cli.ts +++ b/packages/angular/cli/src/commands/generate/cli.ts @@ -9,6 +9,7 @@ import { strings } from '@angular-devkit/core'; import { Argv } from 'yargs'; import { + CommandModuleError, CommandModuleImplementation, Options, OtherOptions, @@ -48,28 +49,9 @@ export class GenerateCommandModule handler: (options) => this.handler(options), }); - const collectionName = await this.getCollectionName(); - const workflow = this.getOrCreateWorkflowForBuilder(collectionName); - const collection = workflow.engine.createCollection(collectionName); - const schematicsInCollection = collection.description.schematics; - - // We cannot use `collection.listSchematicNames()` as this doesn't return hidden schematics. - const schematicNames = new Set(Object.keys(schematicsInCollection).sort()); - const [, schematicNameFromArgs] = this.parseSchematicInfo( - // positional = [generate, component] or [generate] - this.context.args.positional[1], - ); - - if (schematicNameFromArgs && schematicNames.has(schematicNameFromArgs)) { - // No need to process all schematics since we know which one the user invoked. - schematicNames.clear(); - schematicNames.add(schematicNameFromArgs); - } - - for (const schematicName of schematicNames) { - if (schematicsInCollection[schematicName].private) { - continue; - } + for (const [schematicName, collectionName] of await this.getSchematicsToRegister()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); const { description: { @@ -110,8 +92,11 @@ export class GenerateCommandModule async run(options: Options & OtherOptions): Promise { const { dryRun, schematic, defaults, force, interactive, ...schematicOptions } = options; - const [collectionName = await this.getCollectionName(), schematicName = ''] = - this.parseSchematicInfo(schematic); + const [collectionName, schematicName] = this.parseSchematicInfo(schematic); + + if (!collectionName || !schematicName) { + throw new CommandModuleError('A collection and schematic is required during execution.'); + } return this.runSchematic({ collectionName, @@ -126,13 +111,13 @@ export class GenerateCommandModule }); } - private async getCollectionName(): Promise { - const [collectionName = await this.getDefaultSchematicCollection()] = this.parseSchematicInfo( + private async getCollectionNames(): Promise { + const [collectionName] = this.parseSchematicInfo( // positional = [generate, component] or [generate] this.context.args.positional[1], ); - return collectionName; + return collectionName ? [collectionName] : [...(await this.getSchematicCollections())]; } /** @@ -151,12 +136,15 @@ export class GenerateCommandModule ); const dasherizedSchematicName = strings.dasherize(schematicName); + const schematicCollectionsFromConfig = await this.getSchematicCollections(); + const collectionNames = await this.getCollectionNames(); - // 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. + // Only add the collection name as part of the command when it's not a known + // schematics collection or when it has been provided via the CLI. // Ex:`ng generate @schematics/angular:component` const commandName = !!collectionNameFromArgs || - (await this.getDefaultSchematicCollection()) !== (await this.getCollectionName()) + !collectionNames.some((c) => schematicCollectionsFromConfig.has(c)) ? collectionName + ':' + dasherizedSchematicName : dasherizedSchematicName; @@ -171,4 +159,54 @@ export class GenerateCommandModule return `${commandName}${positionalArgs ? ' ' + positionalArgs : ''}`; } + + /** + * Get schematics that can to be registered as subcommands. + */ + private async *getSchematics(): AsyncGenerator<{ + schematicName: string; + collectionName: string; + }> { + const seenNames = new Set(); + for (const collectionName of await this.getCollectionNames()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + + for (const schematicName of collection.listSchematicNames(true /** includeHidden */)) { + // If a schematic with this same name is already registered skip. + if (!seenNames.has(schematicName)) { + seenNames.add(schematicName); + yield { schematicName, collectionName }; + } + } + } + } + + /** + * Get schematics that should to be registered as subcommands. + * + * @returns a sorted list of schematic that needs to be registered as subcommands. + */ + private async getSchematicsToRegister(): Promise< + [schematicName: string, collectionName: string][] + > { + const schematicsToRegister: [schematicName: string, collectionName: string][] = []; + const [, schematicNameFromArgs] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + for await (const { schematicName, collectionName } of this.getSchematics()) { + if (schematicName === schematicNameFromArgs) { + return [[schematicName, collectionName]]; + } + + schematicsToRegister.push([schematicName, collectionName]); + } + + // Didn't find the schematic or no schematic name was provided Ex: `ng generate --help`. + return schematicsToRegister.sort(([nameA], [nameB]) => + nameA.localeCompare(nameB, undefined, { sensitivity: 'accent' }), + ); + } } diff --git a/packages/angular/cli/src/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts index dba91126fbe4..c5c4e6f7e721 100644 --- a/packages/angular/cli/src/commands/new/cli.ts +++ b/packages/angular/cli/src/commands/new/cli.ts @@ -14,6 +14,7 @@ import { OtherOptions, } from '../../command-builder/command-module'; import { + DEFAULT_SCHEMATICS_COLLECTION, SchematicsCommandArgs, SchematicsCommandModule, } from '../../command-builder/schematics-command-module'; @@ -51,7 +52,7 @@ export class NewCommandModule const collectionName = typeof collectionNameFromArgs === 'string' ? collectionNameFromArgs - : await this.getDefaultSchematicCollection(); + : await this.getCollectionFromConfig(); const workflow = await this.getOrCreateWorkflowForBuilder(collectionName); const collection = workflow.engine.createCollection(collectionName); @@ -62,7 +63,7 @@ export class NewCommandModule async run(options: Options & OtherOptions): Promise { // Register the version of the CLI in the registry. - const collectionName = options.collection ?? (await this.getDefaultSchematicCollection()); + const collectionName = options.collection ?? (await this.getCollectionFromConfig()); const workflow = await this.getOrCreateWorkflowForExecution(collectionName, options); workflow.registry.addSmartDefaultProvider('ng-cli-version', () => VERSION.full); @@ -89,4 +90,19 @@ export class NewCommandModule }, }); } + + /** Find a collection from config that has an `ng-new` schematic. */ + private async getCollectionFromConfig(): Promise { + for (const collectionName of await this.getSchematicCollections()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + const schematicsInCollection = collection.description.schematics; + + if (Object.keys(schematicsInCollection).includes(this.schematicName)) { + return collectionName; + } + } + + return DEFAULT_SCHEMATICS_COLLECTION; + } } diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 747cfc8c74d8..e8ed0bb2e2e9 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -19,6 +19,11 @@ "version": "14.0.0", "factory": "./update-14/remove-default-project-option", "description": "Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory." + }, + "replace-default-collection-option": { + "version": "14.0.0", + "factory": "./update-14/replace-default-collection-option", + "description": "Replace 'defaultCollection' option in workspace configuration with 'schematicCollections'." } } } diff --git a/packages/schematics/angular/migrations/update-14/replace-default-collection-option.ts b/packages/schematics/angular/migrations/update-14/replace-default-collection-option.ts new file mode 100644 index 000000000000..0bb7d2f16d3b --- /dev/null +++ b/packages/schematics/angular/migrations/update-14/replace-default-collection-option.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { JsonValue, isJsonObject } from '@angular-devkit/core'; +import { Rule } from '@angular-devkit/schematics'; +import { updateWorkspace } from '../../utility/workspace'; + +/** Migration to replace 'defaultCollection' option in angular.json. */ +export default function (): Rule { + return updateWorkspace((workspace) => { + // workspace level + replaceDefaultCollection(workspace.extensions['cli']); + + // Project level + for (const project of workspace.projects.values()) { + replaceDefaultCollection(project.extensions['cli']); + } + }); +} + +function replaceDefaultCollection(cliExtension: JsonValue | undefined): void { + if (cliExtension && isJsonObject(cliExtension) && cliExtension['defaultCollection']) { + // If `schematicsCollection` defined `defaultCollection` is ignored hence no need to warn. + if (!cliExtension['schematicCollections']) { + cliExtension['schematicCollections'] = [cliExtension['defaultCollection']]; + } + + delete cliExtension['defaultCollection']; + } +} diff --git a/packages/schematics/angular/migrations/update-14/replace-default-collection-option_spec.ts b/packages/schematics/angular/migrations/update-14/replace-default-collection-option_spec.ts new file mode 100644 index 000000000000..4a31fbbeea42 --- /dev/null +++ b/packages/schematics/angular/migrations/update-14/replace-default-collection-option_spec.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +describe(`Migration to replace 'defaultCollection' option.`, () => { + const schematicName = 'replace-default-collection-option'; + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + }); + + it(`should replace 'defaultCollection' with 'schematicCollections' at the root level`, async () => { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: {}, + cli: { + defaultCollection: 'foo', + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { cli } = JSON.parse(newTree.readContent('/angular.json')); + + expect(cli.defaultCollection).toBeUndefined(); + expect(cli.schematicCollections).toEqual(['foo']); + }); + + it(`should not error when 'cli' is not defined`, async () => { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: {}, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { cli } = JSON.parse(newTree.readContent('/angular.json')); + + expect(cli).toBeUndefined(); + }); + + it(`should replace 'defaultCollection' with 'schematicCollections' at the project level`, async () => { + const angularConfig: WorkspaceSchema = { + version: 1, + cli: { + defaultCollection: 'foo', + }, + projects: { + test: { + sourceRoot: '', + root: '', + prefix: '', + projectType: ProjectType.Application, + cli: { + defaultCollection: 'bar', + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { + projects: { test }, + } = JSON.parse(newTree.readContent('/angular.json')); + + expect(test.cli.defaultCollection).toBeUndefined(); + expect(test.cli.schematicCollections).toEqual(['bar']); + }); + + it(`should not replace 'defaultCollection' with 'schematicCollections', when it is already defined`, async () => { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: {}, + cli: { + defaultCollection: 'foo', + schematicCollections: ['bar'], + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { cli } = JSON.parse(newTree.readContent('/angular.json')); + + expect(cli.defaultCollection).toBeUndefined(); + expect(cli.schematicCollections).toEqual(['bar']); + }); +}); diff --git a/packages/schematics/angular/utility/workspace-models.ts b/packages/schematics/angular/utility/workspace-models.ts index 5bfa9c0878f4..3fbaec1f5879 100644 --- a/packages/schematics/angular/utility/workspace-models.ts +++ b/packages/schematics/angular/utility/workspace-models.ts @@ -129,10 +129,15 @@ export type ServeBuilderTarget = BuilderTarget; export type E2EBuilderTarget = BuilderTarget; +interface WorkspaceCLISchema { + warnings?: Record; + schematicCollections?: string[]; + defaultCollection?: string; +} export interface WorkspaceSchema { version: 1; defaultProject?: string; - cli?: { warnings?: Record }; + cli?: WorkspaceCLISchema; projects: { [key: string]: WorkspaceProject; }; @@ -148,7 +153,7 @@ export interface WorkspaceProject }; + cli?: WorkspaceCLISchema; /** * Tool options. diff --git a/tests/legacy-cli/e2e/tests/generate/help-output.ts b/tests/legacy-cli/e2e/tests/generate/help-output.ts index 9b82e36a2ba2..54c5b4772365 100644 --- a/tests/legacy-cli/e2e/tests/generate/help-output.ts +++ b/tests/legacy-cli/e2e/tests/generate/help-output.ts @@ -86,7 +86,7 @@ export default function () { .then(() => updateJsonFile('angular.json', (json) => { json.cli = json.cli || ({} as any); - json.cli.defaultCollection = 'fake-schematics'; + json.cli.schematicCollections = ['fake-schematics']; }), ) .then(() => ng('generate', 'fake', '--help')) diff --git a/tests/legacy-cli/e2e/tests/generate/schematics-collections.ts b/tests/legacy-cli/e2e/tests/generate/schematics-collections.ts new file mode 100644 index 000000000000..e7fd1847b81d --- /dev/null +++ b/tests/legacy-cli/e2e/tests/generate/schematics-collections.ts @@ -0,0 +1,95 @@ +import { join } from 'path'; +import { ng } from '../../utils/process'; +import { writeMultipleFiles, createDir, expectFileToExist } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + // setup temp collection + const genRoot = join('node_modules/fake-schematics/'); + const fakeComponentSchematicDesc = 'Fake component schematic'; + + await createDir(genRoot); + await writeMultipleFiles({ + [join(genRoot, 'package.json')]: JSON.stringify({ + 'schematics': './collection.json', + }), + [join(genRoot, 'collection.json')]: JSON.stringify({ + 'schematics': { + 'fake': { + 'description': 'Fake schematic', + 'schema': './fake-schema.json', + 'factory': './fake', + }, + 'component': { + 'description': fakeComponentSchematicDesc, + 'schema': './fake-schema.json', + 'factory': './fake-component', + }, + }, + }), + [join(genRoot, 'fake-schema.json')]: JSON.stringify({ + '$id': 'FakeSchema', + 'title': 'Fake Schema', + 'type': 'object', + }), + [join(genRoot, 'fake.js')]: ` + exports.default = function (options) { + return (host, context) => { + console.log('fake schematic run.'); + }; + } + `, + [join(genRoot, 'fake-component.js')]: ` + exports.default = function (options) { + return (host, context) => { + console.log('fake component schematic run.'); + }; + } + `, + }); + + await updateJsonFile('angular.json', (json) => { + json.cli ??= {}; + json.cli.schematicCollections = ['fake-schematics', '@schematics/angular']; + }); + + // should display schematics for all schematics + const { stdout: stdout1 } = await ng('generate', '--help'); + if (!stdout1.includes('ng generate component')) { + throw new Error(`Didn't show schematics of '@schematics/angular'.`); + } + + if (!stdout1.includes('ng generate fake')) { + throw new Error(`Didn't show schematics of 'fake-schematics'.`); + } + + // check registration order. Both schematics contain a component schematic verify that the first one wins. + if (!stdout1.includes(fakeComponentSchematicDesc)) { + throw new Error(`Didn't show fake component description.`); + } + + // Verify execution based on ordering + const { stdout: stdout2 } = await ng('generate', 'component'); + if (!stdout2.includes('fake component schematic run')) { + throw new Error(`stdout didn't contain 'fake component schematic run'.`); + } + + await updateJsonFile('angular.json', (json) => { + json.cli ??= {}; + json.cli.schematicCollections = ['@schematics/angular', 'fake-schematics']; + }); + + const { stdout: stdout3 } = await ng('generate', '--help'); + if (!stdout3.includes('ng generate component [name]')) { + throw new Error(`Didn't show component description from @schematics/angular.`); + } + if (stdout3.includes(fakeComponentSchematicDesc)) { + throw new Error(`Shown fake component description, when it shouldn't.`); + } + + // Verify execution based on ordering + const projectDir = join('src', 'app'); + const componentDir = join(projectDir, 'test-component'); + await ng('generate', 'component', 'test-component'); + await expectFileToExist(componentDir); +}