Skip to content

Commit

Permalink
feat(material/schematics): add template migration support within type…
Browse files Browse the repository at this point in the history
…script files (#25496)
  • Loading branch information
amysorto committed Aug 19, 2022
1 parent 2fc05f5 commit 983f664
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 44 deletions.
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

0 comments on commit 983f664

Please sign in to comment.