Skip to content

Commit

Permalink
feat(material/schematics): add style migration support within typescr…
Browse files Browse the repository at this point in the history
…ipt files (#25339)
  • Loading branch information
amysorto committed Aug 16, 2022
1 parent a2eb778 commit 0554f18
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 2 deletions.
Expand Up @@ -17,6 +17,10 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
namespace: string;

override visitStylesheet(stylesheet: ResolvedResource) {
this.fileSystem.overwrite(stylesheet.filePath, this.migrateStyles(stylesheet.content));
}

migrateStyles(styles: string): string {
const processor = new postcss.Processor([
{
postcssPlugin: 'theming-styles-migration-plugin',
Expand All @@ -28,8 +32,7 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
},
]);

const result = processor.process(stylesheet.content, {syntax: scss});
this.fileSystem.overwrite(stylesheet.filePath, result.toString());
return processor.process(styles, {syntax: scss}).toString();
}

atUseHandler(atRule: postcss.AtRule) {
Expand Down
Expand Up @@ -10,11 +10,13 @@ import {Migration} from '@angular/cdk/schematics';
import {SchematicContext} from '@angular-devkit/schematics';
import {ComponentMigrator} from '../index';
import * as ts from 'typescript';
import {ThemingStylesMigration} from '../theming-styles';

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

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

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

private _migrateTemplatesAndStyles(node: ts.Node): void {
if (node.getChildCount() > 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();

Expand All @@ -43,6 +122,11 @@ export class RuntimeCodeMigration extends Migration<ComponentMigrator[], Schemat
}
}

/** Gets whether the specified node is a component decorator for a class */
private _isComponentDecorator(node: ts.Node): boolean {
return node.kind === ts.SyntaxKind.Decorator && node.getText().startsWith('@Component');
}

/** Gets whether the specified node is an import expression. */
private _isImportExpression(
node: ts.Node,
Expand Down
Expand Up @@ -107,6 +107,58 @@ describe('button runtime code', () => {
`,
);
});

it('should migrate styles for a component', async () => {
await runMigrationTest(
`
@Component({
selector: 'button-example',
template: '<button mat-button>Learn More</button>',
styles: ['.mat-button { background: lavender; }'],
})
class ButtonExample {}
`,
`
@Component({
selector: 'button-example',
template: '<button mat-button>Learn More</button>',
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: "<button mat-button>Learn More</button>",
styles: [
".mat-button { padding: 12px; }",
"::ng-deep .mat-button-wrapper{ color: darkblue; }"
],
})
class ButtonExample {}
`,
`
@Component({
selector: "button-example",
template: "<button mat-button>Learn More</button>",
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', () => {
Expand Down

0 comments on commit 0554f18

Please sign in to comment.