Skip to content

Commit

Permalink
feat(material/schematics): v15 migrate imports (#25133)
Browse files Browse the repository at this point in the history
* feat(material/schematics): v15 migrate import declarations
* implement import declaration migrations for
  v15 legacy components ng-update

* feat(material/schematics): v15 migrate import expressions
* implement import expression migrations for
  v15 legacy components ng-update
  • Loading branch information
wagnermaciel authored and mmalerba committed Jul 15, 2022
1 parent d388adf commit fad4f9b
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 11 deletions.
Expand Up @@ -7,7 +7,111 @@
*/

import {Migration, TargetVersion} from '@angular/cdk/schematics';
import * as ts from 'typescript';

export class LegacyComponentsMigration extends Migration<null> {
enabled = this.targetVersion === TargetVersion.V15;

override visitNode(node: ts.Node): void {
if (ts.isImportDeclaration(node)) {
this._handleImportDeclaration(node);
return;
}
if (this._isDestructuredAsyncImport(node)) {
this._handleDestructuredAsyncImport(node);
return;
}
if (this._isImportCallExpression(node)) {
this._handleImportExpression(node);
return;
}
}

/** Handles updating the named bindings of awaited @angular/material import expressions. */
private _handleDestructuredAsyncImport(
node: ts.VariableDeclaration & {name: ts.ObjectBindingPattern},
): void {
for (let i = 0; i < node.name.elements.length; i++) {
const n = node.name.elements[i];
const name = n.propertyName ? n.propertyName : n.name;
if (ts.isIdentifier(name)) {
const oldExport = name.escapedText.toString();
const suffix = oldExport.slice('Mat'.length);
const newExport = n.propertyName
? `MatLegacy${suffix}`
: `MatLegacy${suffix}: Mat${suffix}`;
this._replaceAt(name, {old: oldExport, new: newExport});
}
}
}

/** Handles updating the module specifier of @angular/material imports. */
private _handleImportDeclaration(node: ts.ImportDeclaration): void {
const moduleSpecifier = node.moduleSpecifier as ts.StringLiteral;
if (moduleSpecifier.text.startsWith('@angular/material/')) {
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});

if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
this._handleNamedImportBindings(node.importClause.namedBindings);
}
}
}

/** Handles updating the module specifier of @angular/material import expressions. */
private _handleImportExpression(node: ts.CallExpression): void {
const moduleSpecifier = node.arguments[0] as ts.StringLiteral;
if (moduleSpecifier.text.startsWith('@angular/material/')) {
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
}
}

/** Handles updating the named bindings of @angular/material imports. */
private _handleNamedImportBindings(node: ts.NamedImports): void {
for (let i = 0; i < node.elements.length; i++) {
const n = node.elements[i];
const name = n.propertyName ? n.propertyName : n.name;
const oldExport = name.escapedText.toString();
const suffix = oldExport.slice('Mat'.length);
const newExport = n.propertyName
? `MatLegacy${suffix}`
: `MatLegacy${suffix} as Mat${suffix}`;
this._replaceAt(name, {old: oldExport, new: newExport});
}
}

/**
* Returns true if the given node is a variable declaration assigns
* the awaited result of an import expression using an object binding.
*/
private _isDestructuredAsyncImport(
node: ts.Node,
): node is ts.VariableDeclaration & {name: ts.ObjectBindingPattern} {
return (
ts.isVariableDeclaration(node) &&
!!node.initializer &&
ts.isAwaitExpression(node.initializer) &&
ts.isCallExpression(node.initializer.expression) &&
ts.SyntaxKind.ImportKeyword === node.initializer.expression.expression.kind &&
ts.isObjectBindingPattern(node.name)
);
}

/** Gets whether the specified node is an import expression. */
private _isImportCallExpression(
node: ts.Node,
): node is ts.CallExpression & {arguments: [ts.StringLiteralLike]} {
return (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length === 1 &&
ts.isStringLiteralLike(node.arguments[0])
);
}

/** Updates the source file of the given node with the given replacements. */
private _replaceAt(node: ts.Node, str: {old: string; new: string}): void {
const filePath = this.fileSystem.resolve(node.getSourceFile().fileName);
const index = this.fileSystem.read(filePath)!.indexOf(str.old, node.pos);
this.fileSystem.edit(filePath).remove(index, str.old.length).insertRight(index, str.new);
}
}
Expand Up @@ -10,11 +10,11 @@ const TS_FILE_PATH = join(PROJECT_ROOT_DIR, 'src/app/app.component.ts');
describe('v15 legacy components migration', () => {
let tree: UnitTestTree;

/** Writes an array of lines as a single file. */
let writeLines: (path: string, lines: string[]) => void;
/** Writes an single line file. */
let writeLine: (path: string, line: string) => void;

/** Reads a file and split it into an array where each item is a new line. */
let splitFile: (path: string) => string[];
/** Reads a file. */
let readFile: (path: string) => string;

/** Runs the v15 migration on the test application. */
let runMigration: () => Promise<{logOutput: string}>;
Expand All @@ -23,23 +23,61 @@ describe('v15 legacy components migration', () => {
const testSetup = await createTestCaseSetup('migration-v15', MIGRATION_PATH, []);
tree = testSetup.appTree;
runMigration = testSetup.runFixers;
splitFile = (path: string) => tree.readContent(path).split('\n');
writeLines = (path: string, lines: string[]) => testSetup.writeFile(path, lines.join('\n'));
readFile = (path: string) => tree.readContent(path);
writeLine = (path: string, lines: string) => testSetup.writeFile(path, lines);
});

describe('typescript migrations', () => {
it('should do nothing yet', async () => {
writeLines(TS_FILE_PATH, [' ']);
async function runTypeScriptMigrationTest(ctx: string, opts: {old: string; new: string}) {
writeLine(TS_FILE_PATH, opts.old);
await runMigration();
expect(splitFile(TS_FILE_PATH)).toEqual([' ']);
expect(readFile(TS_FILE_PATH)).withContext(ctx).toEqual(opts.new);
}

it('updates import declarations', async () => {
await runTypeScriptMigrationTest('named binding', {
old: `import {MatButton} from '@angular/material/button';`,
new: `import {MatLegacyButton as MatButton} from '@angular/material/legacy-button';`,
});
await runTypeScriptMigrationTest('named binding w/ alias', {
old: `import {MatButton as Button} from '@angular/material/button';`,
new: `import {MatLegacyButton as Button} from '@angular/material/legacy-button';`,
});
await runTypeScriptMigrationTest('multiple named bindings', {
old: `import {MatButton, MatButtonModule} from '@angular/material/button';`,
new: `import {MatLegacyButton as MatButton, MatLegacyButtonModule as MatButtonModule} from '@angular/material/legacy-button';`,
});
await runTypeScriptMigrationTest('multiple named bindings w/ alias', {
old: `import {MatButton, MatButtonModule as ButtonModule} from '@angular/material/button';`,
new: `import {MatLegacyButton as MatButton, MatLegacyButtonModule as ButtonModule} from '@angular/material/legacy-button';`,
});
});

it('updates import expressions', async () => {
await runTypeScriptMigrationTest('destructured & awaited', {
old: `const {MatButton} = await import('@angular/material/button');`,
new: `const {MatLegacyButton: MatButton} = await import('@angular/material/legacy-button');`,
});
await runTypeScriptMigrationTest('destructured & awaited w/ alias', {
old: `const {MatButton: Button} = await import('@angular/material/button');`,
new: `const {MatLegacyButton: Button} = await import('@angular/material/legacy-button');`,
});
await runTypeScriptMigrationTest('promise', {
old: `const promise = import('@angular/material/button');`,
new: `const promise = import('@angular/material/legacy-button');`,
});
await runTypeScriptMigrationTest('.then', {
old: `import('@angular/material/button').then(() => {});`,
new: `import('@angular/material/legacy-button').then(() => {});`,
});
});
});

describe('style migrations', () => {
it('should do nothing yet', async () => {
writeLines(THEME_FILE_PATH, [' ']);
writeLine(THEME_FILE_PATH, ' ');
await runMigration();
expect(splitFile(THEME_FILE_PATH)).toEqual([' ']);
expect(readFile(THEME_FILE_PATH)).toEqual(' ');
});
});
});

0 comments on commit fad4f9b

Please sign in to comment.