/** * @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'; const MATERIAL_IMPORT_PATH = '@angular/material'; 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 { // Avoid parsing the template Material isn't used. if (!stylesheet.content.includes(MATERIAL_IMPORT_PATH)) { return; } try { 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(); } catch (e) { this.logger.warn( `Failed to migrate usages of mat.core in ${stylesheet.filePath} due to error:`, ); this.logger.warn(e + ''); } } /** 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(MATERIAL_IMPORT_PATH, 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); } }