From c5ab88020154e6997c02a332ec5ecd09808fa52e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 4 Oct 2023 09:09:34 +0200 Subject: [PATCH] fix(cdk/schematics): support both application and browser builders (#27875) In #27792 the schematics were updated to use the new default `application` builder instead of the `browser` builder. According to the CLI team we'll have to support both so these changes update our existing logic to account for both cases. --- .../schematics/utils/project-index-file.ts | 4 +- src/cdk/schematics/utils/project-main-file.ts | 5 +- src/cdk/schematics/utils/project-targets.ts | 36 +++++++--- src/material/schematics/ng-add/index.spec.ts | 69 +++++++++++++++++++ .../schematics/ng-add/theming/theming.ts | 9 +-- 5 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/cdk/schematics/utils/project-index-file.ts b/src/cdk/schematics/utils/project-index-file.ts index 061908e6e31d..5131a3327b1f 100644 --- a/src/cdk/schematics/utils/project-index-file.ts +++ b/src/cdk/schematics/utils/project-index-file.ts @@ -7,11 +7,11 @@ */ import {Path, workspaces} from '@angular-devkit/core'; -import {defaultTargetBuilders, getTargetsByBuilderName} from './project-targets'; +import {getProjectBuildTargets} from './project-targets'; /** Gets the path of the index file in the given project. */ export function getProjectIndexFiles(project: workspaces.ProjectDefinition): Path[] { - const paths = getTargetsByBuilderName(project, defaultTargetBuilders.build) + const paths = getProjectBuildTargets(project) .filter(t => t.options?.['index']) .map(t => t.options!['index'] as Path); diff --git a/src/cdk/schematics/utils/project-main-file.ts b/src/cdk/schematics/utils/project-main-file.ts index 6562146fd5ed..6ff991963edc 100644 --- a/src/cdk/schematics/utils/project-main-file.ts +++ b/src/cdk/schematics/utils/project-main-file.ts @@ -13,7 +13,10 @@ import {getProjectTargetOptions} from './project-targets'; /** Looks for the main TypeScript file in the given project and returns its path. */ export function getProjectMainFile(project: workspaces.ProjectDefinition): Path { const buildOptions = getProjectTargetOptions(project, 'build'); - const mainPath = buildOptions['browser'] as Path | undefined; + + // `browser` is for the `@angular-devkit/build-angular:application` builder while + // `main` is for the `@angular-devkit/build-angular:browser` builder. + const mainPath = (buildOptions['browser'] || buildOptions['main']) as Path | undefined; if (!mainPath) { throw new SchematicsException( diff --git a/src/cdk/schematics/utils/project-targets.ts b/src/cdk/schematics/utils/project-targets.ts index 0c56f8c87be7..3d2f4b0f9797 100644 --- a/src/cdk/schematics/utils/project-targets.ts +++ b/src/cdk/schematics/utils/project-targets.ts @@ -9,12 +9,6 @@ import {JsonValue, workspaces} from '@angular-devkit/core'; import {SchematicsException} from '@angular-devkit/schematics'; -/** Object that maps a CLI target to its default builder name. */ -export const defaultTargetBuilders = { - build: '@angular-devkit/build-angular:application', - test: '@angular-devkit/build-angular:karma', -}; - /** Resolves the architect options for the build target of the given project. */ export function getProjectTargetOptions( project: workspaces.ProjectDefinition, @@ -31,12 +25,34 @@ export function getProjectTargetOptions( return options; } -/** Gets all targets from the given project that match the specified builder name. */ -export function getTargetsByBuilderName( +/** Gets all of the default CLI-provided build targets in a project. */ +export function getProjectBuildTargets( + project: workspaces.ProjectDefinition, +): workspaces.TargetDefinition[] { + return getTargetsByBuilderName( + project, + builder => + builder === '@angular-devkit/build-angular:application' || + builder === '@angular-devkit/build-angular:browser', + ); +} + +/** Gets all of the default CLI-provided testing targets in a project. */ +export function getProjectTestTargets( + project: workspaces.ProjectDefinition, +): workspaces.TargetDefinition[] { + return getTargetsByBuilderName( + project, + builder => builder === '@angular-devkit/build-angular:karma', + ); +} + +/** Gets all targets from the given project that pass a predicate check. */ +function getTargetsByBuilderName( project: workspaces.ProjectDefinition, - builderName: string, + predicate: (name: string | undefined) => boolean, ): workspaces.TargetDefinition[] { return Array.from(project.targets.keys()) - .filter(name => project.targets.get(name)?.builder === builderName) + .filter(name => predicate(project.targets.get(name)?.builder)) .map(name => project.targets.get(name)!); } diff --git a/src/material/schematics/ng-add/index.spec.ts b/src/material/schematics/ng-add/index.spec.ts index edd13dc48ef4..79bde664ad05 100644 --- a/src/material/schematics/ng-add/index.spec.ts +++ b/src/material/schematics/ng-add/index.spec.ts @@ -599,6 +599,75 @@ describe('ng-add schematic', () => { expect(buffer.toString()).toContain(''); }); }); + + describe('using browser builder', () => { + beforeEach(() => { + const config = { + version: 1, + projects: { + material: { + projectType: 'application', + root: 'projects/material', + sourceRoot: 'projects/material/src', + prefix: 'app', + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + options: { + outputPath: 'dist/material', + index: 'projects/material/src/index.html', + main: 'projects/material/src/main.ts', + styles: ['projects/material/src/styles.css'], + }, + }, + test: { + builder: '@angular-devkit/build-angular:karma', + options: { + outputPath: 'dist/material', + index: 'projects/material/src/index.html', + browser: 'projects/material/src/main.ts', + styles: ['projects/material/src/styles.css'], + }, + }, + }, + }, + }, + }; + + appTree.overwrite('/angular.json', JSON.stringify(config, null, 2)); + }); + + it('should add a theme', async () => { + const tree = await runner.runSchematic('ng-add-setup-project', baseOptions, appTree); + const workspace = await getWorkspace(tree); + const project = getProjectFromWorkspace(workspace, baseOptions.project); + + expectProjectStyleFile(project, '@angular/material/prebuilt-themes/indigo-pink.css'); + }); + + it('should add material app styles', async () => { + const tree = await runner.runSchematic('ng-add-setup-project', baseOptions, appTree); + const workspace = await getWorkspace(tree); + const project = getProjectFromWorkspace(workspace, baseOptions.project); + + const defaultStylesPath = getProjectStyleFile(project)!; + const htmlContent = tree.read(defaultStylesPath)!.toString(); + + expect(htmlContent).toContain('html, body { height: 100%; }'); + expect(htmlContent).toContain( + 'body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }', + ); + }); + + it('should add the BrowserAnimationsModule to the project module', async () => { + const tree = await runner.runSchematic('ng-add-setup-project', baseOptions, appTree); + const fileContent = getFileContent(tree, '/projects/material/src/app/app.module.ts'); + + expect(fileContent) + .withContext('Expected the project app module to import the "BrowserAnimationsModule".') + .toContain('BrowserAnimationsModule'); + }); + }); }); describe('ng-add schematic - library project', () => { diff --git a/src/material/schematics/ng-add/theming/theming.ts b/src/material/schematics/ng-add/theming/theming.ts index bee0e4324588..5bc10e3b6a4b 100644 --- a/src/material/schematics/ng-add/theming/theming.ts +++ b/src/material/schematics/ng-add/theming/theming.ts @@ -17,11 +17,12 @@ import { } from '@angular-devkit/schematics'; import { addBodyClass, - defaultTargetBuilders, getProjectFromWorkspace, getProjectStyleFile, getProjectTargetOptions, getProjectIndexFiles, + getProjectTestTargets, + getProjectBuildTargets, } from '@angular/cdk/schematics'; import {InsertChange} from '@schematics/angular/utility/change'; import {getWorkspace, updateWorkspace} from '@schematics/angular/utility/workspace'; @@ -177,9 +178,9 @@ function validateDefaultTargetBuilder( targetName: 'build' | 'test', logger: logging.LoggerApi, ) { - const defaultBuilder = defaultTargetBuilders[targetName]; - const targetConfig = project.targets?.get(targetName); - const isDefaultBuilder = targetConfig?.['builder'] === defaultBuilder; + const targets = + targetName === 'test' ? getProjectTestTargets(project) : getProjectBuildTargets(project); + const isDefaultBuilder = targets.length > 0; // Because the build setup for the Angular CLI can be customized by developers, we can't know // where to put the theme file in the workspace configuration if custom builders are being