Skip to content

Commit 0554f18

Browse files
authored
feat(material/schematics): add style migration support within typescript files (#25339)
1 parent a2eb778 commit 0554f18

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
1717
namespace: string;
1818

1919
override visitStylesheet(stylesheet: ResolvedResource) {
20+
this.fileSystem.overwrite(stylesheet.filePath, this.migrateStyles(stylesheet.content));
21+
}
22+
23+
migrateStyles(styles: string): string {
2024
const processor = new postcss.Processor([
2125
{
2226
postcssPlugin: 'theming-styles-migration-plugin',
@@ -28,8 +32,7 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
2832
},
2933
]);
3034

31-
const result = processor.process(stylesheet.content, {syntax: scss});
32-
this.fileSystem.overwrite(stylesheet.filePath, result.toString());
35+
return processor.process(styles, {syntax: scss}).toString();
3336
}
3437

3538
atUseHandler(atRule: postcss.AtRule) {

src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migration.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import {Migration} from '@angular/cdk/schematics';
1010
import {SchematicContext} from '@angular-devkit/schematics';
1111
import {ComponentMigrator} from '../index';
1212
import * as ts from 'typescript';
13+
import {ThemingStylesMigration} from '../theming-styles';
1314

1415
export class RuntimeCodeMigration extends Migration<ComponentMigrator[], SchematicContext> {
1516
enabled = true;
1617

1718
private _printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
19+
private _stylesMigration: ThemingStylesMigration;
1820

1921
override visitNode(node: ts.Node): void {
2022
if (this._isImportExpression(node)) {
@@ -24,9 +26,86 @@ export class RuntimeCodeMigration extends Migration<ComponentMigrator[], Schemat
2426
} else if (ts.isImportDeclaration(node)) {
2527
// Note: TypeScript enforces the `moduleSpecifier` to be a string literal in its syntax.
2628
this._migrateModuleSpecifier(node.moduleSpecifier as ts.StringLiteral);
29+
} else if (this._isComponentDecorator(node)) {
30+
this._migrateTemplatesAndStyles(node);
2731
}
2832
}
2933

34+
private _migrateTemplatesAndStyles(node: ts.Node): void {
35+
if (node.getChildCount() > 0) {
36+
if (node.kind === ts.SyntaxKind.PropertyAssignment) {
37+
// The first child node will always be the identifier for a property
38+
// assignment node
39+
const identifier = node.getChildAt(0);
40+
if (identifier.getText() === 'styles') {
41+
this._migrateStyles(node);
42+
}
43+
} else {
44+
node.forEachChild(child => this._migrateTemplatesAndStyles(child));
45+
}
46+
}
47+
}
48+
49+
private _migrateStyles(node: ts.Node) {
50+
// Create styles migration if no styles have been migrated yet. Needs to be
51+
// additionally created because the migrations run in isolation.
52+
if (!this._stylesMigration) {
53+
this._stylesMigration = new ThemingStylesMigration(
54+
this.program,
55+
this.typeChecker,
56+
this.targetVersion,
57+
this.context,
58+
this.upgradeData,
59+
this.fileSystem,
60+
this.logger,
61+
);
62+
}
63+
64+
node.forEachChild(childNode => {
65+
if (childNode.kind === ts.SyntaxKind.ArrayLiteralExpression) {
66+
childNode.forEachChild(stringLiteralNode => {
67+
if (stringLiteralNode.kind === ts.SyntaxKind.StringLiteral) {
68+
let nodeText = stringLiteralNode.getText();
69+
const trimmedNodeText = nodeText.trimStart().trimEnd();
70+
// Remove quotation marks from string since not valid CSS to migrate
71+
const nodeTextWithoutQuotes = trimmedNodeText.substring(1, trimmedNodeText.length - 1);
72+
let migratedStyles = this._stylesMigration.migrateStyles(nodeTextWithoutQuotes);
73+
const migratedStylesLines = migratedStyles.split('\n');
74+
const isMultiline = migratedStylesLines.length > 1;
75+
76+
// If migrated text is now multiline, update quotes to avoid
77+
// compilation errors
78+
if (isMultiline) {
79+
nodeText = nodeText.replace(trimmedNodeText, '`' + nodeTextWithoutQuotes + '`');
80+
}
81+
82+
this._printAndUpdateNode(
83+
stringLiteralNode.getSourceFile(),
84+
stringLiteralNode,
85+
ts.factory.createRegularExpressionLiteral(
86+
nodeText.replace(
87+
nodeTextWithoutQuotes,
88+
migratedStylesLines
89+
.map((line, index) => {
90+
// Only need to worry about indentation when adding new lines
91+
if (isMultiline && index !== 0 && line != '\n') {
92+
const leadingWidth = stringLiteralNode.getLeadingTriviaWidth();
93+
if (leadingWidth > 0) {
94+
line = ' '.repeat(leadingWidth - 1) + line;
95+
}
96+
}
97+
return line;
98+
})
99+
.join('\n'),
100+
),
101+
),
102+
);
103+
}
104+
});
105+
}
106+
});
107+
}
108+
30109
private _migrateModuleSpecifier(specifierLiteral: ts.StringLiteralLike) {
31110
const sourceFile = specifierLiteral.getSourceFile();
32111

@@ -43,6 +122,11 @@ export class RuntimeCodeMigration extends Migration<ComponentMigrator[], Schemat
43122
}
44123
}
45124

125+
/** Gets whether the specified node is a component decorator for a class */
126+
private _isComponentDecorator(node: ts.Node): boolean {
127+
return node.kind === ts.SyntaxKind.Decorator && node.getText().startsWith('@Component');
128+
}
129+
46130
/** Gets whether the specified node is an import expression. */
47131
private _isImportExpression(
48132
node: ts.Node,

src/material/schematics/ng-generate/mdc-migration/rules/ts-migration/runtime-migrator.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,58 @@ describe('button runtime code', () => {
107107
`,
108108
);
109109
});
110+
111+
it('should migrate styles for a component', async () => {
112+
await runMigrationTest(
113+
`
114+
@Component({
115+
selector: 'button-example',
116+
template: '<button mat-button>Learn More</button>',
117+
styles: ['.mat-button { background: lavender; }'],
118+
})
119+
class ButtonExample {}
120+
`,
121+
`
122+
@Component({
123+
selector: 'button-example',
124+
template: '<button mat-button>Learn More</button>',
125+
styles: ['.mat-mdc-button { background: lavender; }'],
126+
})
127+
class ButtonExample {}
128+
`,
129+
);
130+
});
131+
132+
it('should migrate multiline styles for a component', async () => {
133+
// Note: The spaces in the last style are to perserve indentation on the
134+
// new line between the comment and rule
135+
await runMigrationTest(
136+
`
137+
@Component({
138+
selector: "button-example",
139+
template: "<button mat-button>Learn More</button>",
140+
styles: [
141+
".mat-button { padding: 12px; }",
142+
"::ng-deep .mat-button-wrapper{ color: darkblue; }"
143+
],
144+
})
145+
class ButtonExample {}
146+
`,
147+
`
148+
@Component({
149+
selector: "button-example",
150+
template: "<button mat-button>Learn More</button>",
151+
styles: [
152+
".mat-mdc-button { padding: 12px; }",
153+
\`
154+
/* TODO: The following rule targets internal classes of button that may no longer apply for the MDC version. */
155+
\n ::ng-deep .mat-button-wrapper{ color: darkblue; }\`
156+
],
157+
})
158+
class ButtonExample {}
159+
`,
160+
);
161+
});
110162
});
111163

112164
describe('import expressions', () => {

0 commit comments

Comments
 (0)