From 4adc3725dd08ef3cf3868f9c752e16c8c1492466 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 24 Sep 2024 20:05:03 -0400 Subject: [PATCH] feat(material/schematics): create v19 core removal schematic (#29768) * Removes uses of the core mixin and replaces them with two mixins. One to generate the mat-app-background class, and one to generate the mat-elevation classes. --- src/material/schematics/ng-update/index.ts | 3 +- .../ng-update/migrations/mat-core-removal.ts | 87 +++++++++++++++++++ .../test-cases/v19-mat-core-removal.spec.ts | 77 ++++++++++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/material/schematics/ng-update/migrations/mat-core-removal.ts create mode 100644 src/material/schematics/ng-update/test-cases/v19-mat-core-removal.spec.ts diff --git a/src/material/schematics/ng-update/index.ts b/src/material/schematics/ng-update/index.ts index adc1ba8dfad1..d89f7212ea25 100644 --- a/src/material/schematics/ng-update/index.ts +++ b/src/material/schematics/ng-update/index.ts @@ -14,8 +14,9 @@ import { } from '@angular/cdk/schematics'; import {materialUpgradeData} from './upgrade-data'; +import {MatCoreMigration} from './migrations/mat-core-removal'; -const materialMigrations: NullableDevkitMigration[] = []; +const materialMigrations: NullableDevkitMigration[] = [MatCoreMigration]; /** Entry point for the migration schematics with target of Angular Material v19 */ export function updateToV19(): Rule { diff --git a/src/material/schematics/ng-update/migrations/mat-core-removal.ts b/src/material/schematics/ng-update/migrations/mat-core-removal.ts new file mode 100644 index 000000000000..dac48916476c --- /dev/null +++ b/src/material/schematics/ng-update/migrations/mat-core-removal.ts @@ -0,0 +1,87 @@ +/** + * @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.dev/license + */ + +import * as postcss from 'postcss'; +import * as scss from 'postcss-scss'; +import { + DevkitContext, + Migration, + ResolvedResource, + UpgradeData, + WorkspacePath, +} from '@angular/cdk/schematics'; + +export class MatCoreMigration extends Migration { + override enabled = true; + private _namespace: string | undefined; + + override init() { + // TODO: Check if mat-app-background is used in the application. + } + + override visitStylesheet(stylesheet: ResolvedResource): void { + const processor = new postcss.Processor([ + { + postcssPlugin: 'mat-core-removal-v19-plugin', + AtRule: { + use: node => this._getNamespace(node), + include: node => this._handleAtInclude(node, stylesheet.filePath), + }, + }, + ]); + processor.process(stylesheet.content, {syntax: scss}).sync(); + } + + /** Handles updating the at-include rules of uses of the core mixin. */ + private _handleAtInclude(node: postcss.AtRule, filePath: WorkspacePath): void { + if (!this._namespace || !node.source?.start || !node.source.end) { + return; + } + + if (this._isMatCoreMixin(node)) { + const end = node.source.end.offset; + const start = node.source.start.offset; + + const prefix = '\n' + (node.raws.before?.split('\n').pop() || ''); + const snippet = prefix + node.source.input.css.slice(start, end); + + const elevation = prefix + `@include ${this._namespace}.elevation-classes();`; + const background = prefix + `@include ${this._namespace}.app-background();`; + + this._replaceAt(filePath, node.source.start.offset - prefix.length, { + old: snippet, + new: elevation + background, + }); + } + } + + /** Returns true if the given at-rule is a use of the core mixin. */ + private _isMatCoreMixin(node: postcss.AtRule): boolean { + if (node.params.startsWith(`${this._namespace}.core`)) { + return true; + } + return false; + } + + /** Sets the namespace if the given at-rule if it is importing from @angular/material. */ + private _getNamespace(node: postcss.AtRule): void { + if (!this._namespace && node.params.startsWith('@angular/material', 1)) { + this._namespace = node.params.split(/\s+/)[2] || 'material'; + } + } + + /** Updates the source file with the given replacements. */ + private _replaceAt( + filePath: WorkspacePath, + offset: number, + str: {old: string; new: string}, + ): void { + const index = this.fileSystem.read(filePath)!.indexOf(str.old, offset); + this.fileSystem.edit(filePath).remove(index, str.old.length).insertRight(index, str.new); + } +} diff --git a/src/material/schematics/ng-update/test-cases/v19-mat-core-removal.spec.ts b/src/material/schematics/ng-update/test-cases/v19-mat-core-removal.spec.ts new file mode 100644 index 000000000000..e249f8983b54 --- /dev/null +++ b/src/material/schematics/ng-update/test-cases/v19-mat-core-removal.spec.ts @@ -0,0 +1,77 @@ +import {UnitTestTree} from '@angular-devkit/schematics/testing'; +import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; +import {join} from 'path'; +import {MIGRATION_PATH} from '../../paths'; + +const PROJECT_ROOT_DIR = '/projects/cdk-testing'; +const THEME_FILE_PATH = join(PROJECT_ROOT_DIR, 'src/theme.scss'); + +describe('v15 legacy components migration', () => { + let tree: UnitTestTree; + + /** Writes multiple lines to a file. */ + let writeLines: (path: string, lines: string[]) => void; + + /** Reads multiple lines from a file. */ + let readLines: (path: string) => string[]; + + /** Runs the v15 migration on the test application. */ + let runMigration: () => Promise<{logOutput: string}>; + + beforeEach(async () => { + const testSetup = await createTestCaseSetup('migration-v19', MIGRATION_PATH, []); + tree = testSetup.appTree; + runMigration = testSetup.runFixers; + readLines = (path: string) => tree.readContent(path).split('\n'); + writeLines = (path: string, lines: string[]) => testSetup.writeFile(path, lines.join('\n')); + }); + + describe('style migrations', () => { + async function runSassMigrationTest(ctx: string, opts: {old: string[]; new: string[]}) { + writeLines(THEME_FILE_PATH, opts.old); + await runMigration(); + expect(readLines(THEME_FILE_PATH)).withContext(ctx).toEqual(opts.new); + } + + it('should remove uses of the core mixin', async () => { + await runSassMigrationTest('', { + old: [`@use '@angular/material' as mat;`, `@include mat.core();`], + new: [ + `@use '@angular/material' as mat;`, + `@include mat.elevation-classes();`, + `@include mat.app-background();`, + ], + }); + + await runSassMigrationTest('w/ unique namespace', { + old: [`@use '@angular/material' as material;`, `@include material.core();`], + new: [ + `@use '@angular/material' as material;`, + `@include material.elevation-classes();`, + `@include material.app-background();`, + ], + }); + + await runSassMigrationTest('w/ no namespace', { + old: [`@use '@angular/material';`, `@include material.core();`], + new: [ + `@use '@angular/material';`, + `@include material.elevation-classes();`, + `@include material.app-background();`, + ], + }); + + await runSassMigrationTest('w/ unique whitespace', { + old: [ + ` @use '@angular/material' as material ; `, + ` @include material.core( ) ; `, + ], + new: [ + ` @use '@angular/material' as material ; `, + ` @include material.elevation-classes();`, + ` @include material.app-background(); `, + ], + }); + }); + }); +});