diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts b/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts index f2099dae7c5e..39767421f8d6 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts @@ -17,6 +17,10 @@ export class ThemingStylesMigration extends Migration { enabled = true; private _printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); + private _stylesMigration: ThemingStylesMigration; override visitNode(node: ts.Node): void { if (this._isImportExpression(node)) { @@ -24,9 +26,86 @@ export class RuntimeCodeMigration extends Migration 0) { + if (node.kind === ts.SyntaxKind.PropertyAssignment) { + // The first child node will always be the identifier for a property + // assignment node + const identifier = node.getChildAt(0); + if (identifier.getText() === 'styles') { + this._migrateStyles(node); + } + } else { + node.forEachChild(child => this._migrateTemplatesAndStyles(child)); + } + } + } + + private _migrateStyles(node: ts.Node) { + // Create styles migration if no styles have been migrated yet. Needs to be + // additionally created because the migrations run in isolation. + if (!this._stylesMigration) { + this._stylesMigration = new ThemingStylesMigration( + this.program, + this.typeChecker, + this.targetVersion, + this.context, + this.upgradeData, + this.fileSystem, + this.logger, + ); + } + + node.forEachChild(childNode => { + if (childNode.kind === ts.SyntaxKind.ArrayLiteralExpression) { + childNode.forEachChild(stringLiteralNode => { + if (stringLiteralNode.kind === ts.SyntaxKind.StringLiteral) { + let nodeText = stringLiteralNode.getText(); + const trimmedNodeText = nodeText.trimStart().trimEnd(); + // Remove quotation marks from string since not valid CSS to migrate + const nodeTextWithoutQuotes = trimmedNodeText.substring(1, trimmedNodeText.length - 1); + let migratedStyles = this._stylesMigration.migrateStyles(nodeTextWithoutQuotes); + const migratedStylesLines = migratedStyles.split('\n'); + const isMultiline = migratedStylesLines.length > 1; + + // If migrated text is now multiline, update quotes to avoid + // compilation errors + if (isMultiline) { + nodeText = nodeText.replace(trimmedNodeText, '`' + nodeTextWithoutQuotes + '`'); + } + + this._printAndUpdateNode( + stringLiteralNode.getSourceFile(), + stringLiteralNode, + ts.factory.createRegularExpressionLiteral( + nodeText.replace( + nodeTextWithoutQuotes, + migratedStylesLines + .map((line, index) => { + // Only need to worry about indentation when adding new lines + if (isMultiline && index !== 0 && line != '\n') { + const leadingWidth = stringLiteralNode.getLeadingTriviaWidth(); + if (leadingWidth > 0) { + line = ' '.repeat(leadingWidth - 1) + line; + } + } + return line; + }) + .join('\n'), + ), + ), + ); + } + }); + } + }); + } + private _migrateModuleSpecifier(specifierLiteral: ts.StringLiteralLike) { const sourceFile = specifierLiteral.getSourceFile(); @@ -43,6 +122,11 @@ export class RuntimeCodeMigration extends Migration { `, ); }); + + it('should migrate styles for a component', async () => { + await runMigrationTest( + ` + @Component({ + selector: 'button-example', + template: '', + styles: ['.mat-button { background: lavender; }'], + }) + class ButtonExample {} + `, + ` + @Component({ + selector: 'button-example', + template: '', + styles: ['.mat-mdc-button { background: lavender; }'], + }) + class ButtonExample {} + `, + ); + }); + + it('should migrate multiline styles for a component', async () => { + // Note: The spaces in the last style are to perserve indentation on the + // new line between the comment and rule + await runMigrationTest( + ` + @Component({ + selector: "button-example", + template: "", + styles: [ + ".mat-button { padding: 12px; }", + "::ng-deep .mat-button-wrapper{ color: darkblue; }" + ], + }) + class ButtonExample {} + `, + ` + @Component({ + selector: "button-example", + template: "", + styles: [ + ".mat-mdc-button { padding: 12px; }", + \` + /* TODO: The following rule targets internal classes of button that may no longer apply for the MDC version. */ + \n ::ng-deep .mat-button-wrapper{ color: darkblue; }\` + ], + }) + class ButtonExample {} + `, + ); + }); }); describe('import expressions', () => {