diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts b/src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts index ceb91f73ca91..9aeb15f7f2f9 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts @@ -16,13 +16,15 @@ export class TemplateMigration extends Migration m.template).map(m => m.template!); + this.fileSystem.overwrite(template.filePath, this.migrate(template.content, template.filePath)); + } + migrate(template: string, templateUrl?: string): string { + const ast = parseTemplate(template, templateUrl); + const migrators = this.upgradeData.filter(m => m.template).map(m => m.template!); const updates: Update[] = []; migrators.forEach(m => updates.push(...m.getUpdates(ast))); - const content = writeUpdates(template.content, updates); - this.fileSystem.overwrite(template.filePath, content); + return writeUpdates(template, updates); } } 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 39767421f8d6..11e01c0d26c5 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,10 +17,10 @@ export class ThemingStylesMigration extends Migration { enabled = true; private _printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); private _stylesMigration: ThemingStylesMigration; + private _templateMigration: TemplateMigration; + private _hasPossibleTemplateMigrations = true; override visitNode(node: ts.Node): void { if (this._isImportExpression(node)) { @@ -39,6 +42,8 @@ export class RuntimeCodeMigration extends Migration this._migrateTemplatesAndStyles(child)); @@ -64,48 +69,86 @@ export class RuntimeCodeMigration extends Migration { 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'), - ), - ), - ); - } + this._migratePropertyAssignment(stringLiteralNode, this._stylesMigration); }); } }); } + private _migrateTemplate(node: ts.Node) { + // Create template migration if no template has been migrated yet. Needs to + // be additionally created because the migrations run in isolation. + if (!this._templateMigration) { + const templateUpgradeData = this.upgradeData.filter(component => component.template); + // If no component in the upgrade data has a a template migrator, stop + // trying to migrate any templates from now on + if (templateUpgradeData.length === 0) { + this._hasPossibleTemplateMigrations = false; + return; + } else { + this._templateMigration = new TemplateMigration( + this.program, + this.typeChecker, + this.targetVersion, + this.context, + templateUpgradeData, + this.fileSystem, + this.logger, + ); + } + } + + node.forEachChild(childNode => { + this._migratePropertyAssignment(childNode, this._templateMigration); + }); + } + + private _migratePropertyAssignment( + node: ts.Node, + migration: TemplateMigration | ThemingStylesMigration, + ) { + if ( + node.kind === ts.SyntaxKind.StringLiteral || + node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral + ) { + let nodeText = node.getText(); + const trimmedNodeText = nodeText.trimStart().trimEnd(); + // Remove quotation marks from string since not apart of the CSS or HTML + const nodeTextWithoutQuotes = trimmedNodeText.substring(1, trimmedNodeText.length - 1); + const migratedText = migration.migrate(nodeTextWithoutQuotes); + const migratedTextLines = migratedText.split('\n'); + const isMultiline = migratedTextLines.length > 1; + + // If migrated text is now multiline, update quotes to avoid compilation + // errors + if (isMultiline) { + nodeText = nodeText.replace(trimmedNodeText, '`' + nodeTextWithoutQuotes + '`'); + } + + this._printAndUpdateNode( + node.getSourceFile(), + node, + ts.factory.createRegularExpressionLiteral( + nodeText.replace( + nodeTextWithoutQuotes, + migratedTextLines + .map((line: string, index: number) => { + // Only need to worry about indentation when adding new lines + if (isMultiline && index !== 0 && line != '\n') { + const leadingWidth = node.getLeadingTriviaWidth(); + if (leadingWidth > 0) { + line = ' '.repeat(leadingWidth - 1) + line; + } + } + return line; + }) + .join('\n'), + ), + ), + ); + } + } + private _migrateModuleSpecifier(specifierLiteral: ts.StringLiteralLike) { const sourceFile = specifierLiteral.getSourceFile(); diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migrator.spec.ts b/src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migrator.spec.ts index 85510e17362d..664876d26f94 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migrator.spec.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migrator.spec.ts @@ -17,7 +17,7 @@ describe('button runtime code', () => { async function runMigrationTest(oldFileContent: string, newFileContent: string) { cliAppTree.overwrite(APP_MODULE_FILE, oldFileContent); - const tree = await migrateComponents(['button'], runner, cliAppTree); + const tree = await migrateComponents(['button', 'card'], runner, cliAppTree); expect(tree.readContent(APP_MODULE_FILE)).toBe(newFileContent); } @@ -159,6 +159,73 @@ describe('button runtime code', () => { `, ); }); + + it('should migrate template for a component', async () => { + await runMigrationTest( + ` + @Component({ + selector: 'card-example', + template: 'Learn More', + }) + class CardExample {} + `, + ` + @Component({ + selector: 'card-example', + template: 'Learn More', + }) + class CardExample {} + `, + ); + }); + + it('should migrate multiline template 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: 'card-example', + template: \` + Learn More + + \`, + }) + class CardExample {} + `, + ` + @Component({ + selector: 'card-example', + template: \` + Learn More + + \`, + }) + class CardExample {} + `, + ); + }); + + it('should migrate template and styles for a component', async () => { + await runMigrationTest( + ` + @Component({ + selector: 'card-example', + template: 'Learn More', + styles: ['.mat-card { padding-right: 4px; }'], + }) + class CardExample {} + `, + ` + @Component({ + selector: 'card-example', + template: 'Learn More', + styles: ['.mat-mdc-card { padding-right: 4px; }'], + }) + class CardExample {} + `, + ); + }); }); describe('import expressions', () => {