Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(material/schematics): add template migration support within typescript files #25496

Merged
merged 1 commit into from Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -16,13 +16,15 @@ export class TemplateMigration extends Migration<ComponentMigrator[], SchematicC
enabled = true;

override visitTemplate(template: ResolvedResource) {
const ast = parseTemplate(template.content, template.filePath);
const migrators = this.upgradeData.filter(m => 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);
}
}
Expand Up @@ -17,10 +17,10 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
namespace: string;

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

migrateStyles(styles: string): string {
migrate(styles: string): string {
const processor = new postcss.Processor([
{
postcssPlugin: 'theming-styles-migration-plugin',
Expand Down
Expand Up @@ -11,12 +11,15 @@ import {SchematicContext} from '@angular-devkit/schematics';
import {ComponentMigrator} from '../index';
import * as ts from 'typescript';
import {ThemingStylesMigration} from '../theming-styles';
import {TemplateMigration} from '../template-migration';

export class RuntimeCodeMigration extends Migration<ComponentMigrator[], SchematicContext> {
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)) {
Expand All @@ -39,6 +42,8 @@ export class RuntimeCodeMigration extends Migration<ComponentMigrator[], Schemat
const identifier = node.getChildAt(0);
if (identifier.getText() === 'styles') {
this._migrateStyles(node);
} else if (this._hasPossibleTemplateMigrations && identifier.getText() === 'template') {
this._migrateTemplate(node);
}
} else {
node.forEachChild(child => this._migrateTemplatesAndStyles(child));
Expand All @@ -64,48 +69,86 @@ export class RuntimeCodeMigration extends Migration<ComponentMigrator[], Schemat
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'),
),
),
);
}
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();

Expand Down
Expand Up @@ -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);
}

Expand Down Expand Up @@ -159,6 +159,73 @@ describe('button runtime code', () => {
`,
);
});

it('should migrate template for a component', async () => {
await runMigrationTest(
`
@Component({
selector: 'card-example',
template: '<mat-card>Learn More</mat-card>',
})
class CardExample {}
`,
`
@Component({
selector: 'card-example',
template: '<mat-card appearance="outlined">Learn More</mat-card>',
})
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: \`<mat-card>
Learn More
</mat-card>
\`,
})
class CardExample {}
`,
`
@Component({
selector: 'card-example',
template: \`<mat-card appearance="outlined">
Learn More
</mat-card>
\`,
})
class CardExample {}
`,
);
});

it('should migrate template and styles for a component', async () => {
await runMigrationTest(
`
@Component({
selector: 'card-example',
template: '<mat-card>Learn More</mat-card>',
styles: ['.mat-card { padding-right: 4px; }'],
})
class CardExample {}
`,
`
@Component({
selector: 'card-example',
template: '<mat-card appearance="outlined">Learn More</mat-card>',
styles: ['.mat-mdc-card { padding-right: 4px; }'],
})
class CardExample {}
`,
);
});
});

describe('import expressions', () => {
Expand Down