Skip to content

Commit fad4f9b

Browse files
wagnermacielmmalerba
authored andcommitted
feat(material/schematics): v15 migrate imports (#25133)
* 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
1 parent d388adf commit fad4f9b

File tree

2 files changed

+153
-11
lines changed

2 files changed

+153
-11
lines changed

src/material/schematics/ng-update/migrations/legacy-components-v15/index.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,111 @@
77
*/
88

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

1112
export class LegacyComponentsMigration extends Migration<null> {
1213
enabled = this.targetVersion === TargetVersion.V15;
14+
15+
override visitNode(node: ts.Node): void {
16+
if (ts.isImportDeclaration(node)) {
17+
this._handleImportDeclaration(node);
18+
return;
19+
}
20+
if (this._isDestructuredAsyncImport(node)) {
21+
this._handleDestructuredAsyncImport(node);
22+
return;
23+
}
24+
if (this._isImportCallExpression(node)) {
25+
this._handleImportExpression(node);
26+
return;
27+
}
28+
}
29+
30+
/** Handles updating the named bindings of awaited @angular/material import expressions. */
31+
private _handleDestructuredAsyncImport(
32+
node: ts.VariableDeclaration & {name: ts.ObjectBindingPattern},
33+
): void {
34+
for (let i = 0; i < node.name.elements.length; i++) {
35+
const n = node.name.elements[i];
36+
const name = n.propertyName ? n.propertyName : n.name;
37+
if (ts.isIdentifier(name)) {
38+
const oldExport = name.escapedText.toString();
39+
const suffix = oldExport.slice('Mat'.length);
40+
const newExport = n.propertyName
41+
? `MatLegacy${suffix}`
42+
: `MatLegacy${suffix}: Mat${suffix}`;
43+
this._replaceAt(name, {old: oldExport, new: newExport});
44+
}
45+
}
46+
}
47+
48+
/** Handles updating the module specifier of @angular/material imports. */
49+
private _handleImportDeclaration(node: ts.ImportDeclaration): void {
50+
const moduleSpecifier = node.moduleSpecifier as ts.StringLiteral;
51+
if (moduleSpecifier.text.startsWith('@angular/material/')) {
52+
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
53+
54+
if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
55+
this._handleNamedImportBindings(node.importClause.namedBindings);
56+
}
57+
}
58+
}
59+
60+
/** Handles updating the module specifier of @angular/material import expressions. */
61+
private _handleImportExpression(node: ts.CallExpression): void {
62+
const moduleSpecifier = node.arguments[0] as ts.StringLiteral;
63+
if (moduleSpecifier.text.startsWith('@angular/material/')) {
64+
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
65+
}
66+
}
67+
68+
/** Handles updating the named bindings of @angular/material imports. */
69+
private _handleNamedImportBindings(node: ts.NamedImports): void {
70+
for (let i = 0; i < node.elements.length; i++) {
71+
const n = node.elements[i];
72+
const name = n.propertyName ? n.propertyName : n.name;
73+
const oldExport = name.escapedText.toString();
74+
const suffix = oldExport.slice('Mat'.length);
75+
const newExport = n.propertyName
76+
? `MatLegacy${suffix}`
77+
: `MatLegacy${suffix} as Mat${suffix}`;
78+
this._replaceAt(name, {old: oldExport, new: newExport});
79+
}
80+
}
81+
82+
/**
83+
* Returns true if the given node is a variable declaration assigns
84+
* the awaited result of an import expression using an object binding.
85+
*/
86+
private _isDestructuredAsyncImport(
87+
node: ts.Node,
88+
): node is ts.VariableDeclaration & {name: ts.ObjectBindingPattern} {
89+
return (
90+
ts.isVariableDeclaration(node) &&
91+
!!node.initializer &&
92+
ts.isAwaitExpression(node.initializer) &&
93+
ts.isCallExpression(node.initializer.expression) &&
94+
ts.SyntaxKind.ImportKeyword === node.initializer.expression.expression.kind &&
95+
ts.isObjectBindingPattern(node.name)
96+
);
97+
}
98+
99+
/** Gets whether the specified node is an import expression. */
100+
private _isImportCallExpression(
101+
node: ts.Node,
102+
): node is ts.CallExpression & {arguments: [ts.StringLiteralLike]} {
103+
return (
104+
ts.isCallExpression(node) &&
105+
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
106+
node.arguments.length === 1 &&
107+
ts.isStringLiteralLike(node.arguments[0])
108+
);
109+
}
110+
111+
/** Updates the source file of the given node with the given replacements. */
112+
private _replaceAt(node: ts.Node, str: {old: string; new: string}): void {
113+
const filePath = this.fileSystem.resolve(node.getSourceFile().fileName);
114+
const index = this.fileSystem.read(filePath)!.indexOf(str.old, node.pos);
115+
this.fileSystem.edit(filePath).remove(index, str.old.length).insertRight(index, str.new);
116+
}
13117
}

src/material/schematics/ng-update/test-cases/v15/legacy-components-v15.spec.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ const TS_FILE_PATH = join(PROJECT_ROOT_DIR, 'src/app/app.component.ts');
1010
describe('v15 legacy components migration', () => {
1111
let tree: UnitTestTree;
1212

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

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

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

3030
describe('typescript migrations', () => {
31-
it('should do nothing yet', async () => {
32-
writeLines(TS_FILE_PATH, [' ']);
31+
async function runTypeScriptMigrationTest(ctx: string, opts: {old: string; new: string}) {
32+
writeLine(TS_FILE_PATH, opts.old);
3333
await runMigration();
34-
expect(splitFile(TS_FILE_PATH)).toEqual([' ']);
34+
expect(readFile(TS_FILE_PATH)).withContext(ctx).toEqual(opts.new);
35+
}
36+
37+
it('updates import declarations', async () => {
38+
await runTypeScriptMigrationTest('named binding', {
39+
old: `import {MatButton} from '@angular/material/button';`,
40+
new: `import {MatLegacyButton as MatButton} from '@angular/material/legacy-button';`,
41+
});
42+
await runTypeScriptMigrationTest('named binding w/ alias', {
43+
old: `import {MatButton as Button} from '@angular/material/button';`,
44+
new: `import {MatLegacyButton as Button} from '@angular/material/legacy-button';`,
45+
});
46+
await runTypeScriptMigrationTest('multiple named bindings', {
47+
old: `import {MatButton, MatButtonModule} from '@angular/material/button';`,
48+
new: `import {MatLegacyButton as MatButton, MatLegacyButtonModule as MatButtonModule} from '@angular/material/legacy-button';`,
49+
});
50+
await runTypeScriptMigrationTest('multiple named bindings w/ alias', {
51+
old: `import {MatButton, MatButtonModule as ButtonModule} from '@angular/material/button';`,
52+
new: `import {MatLegacyButton as MatButton, MatLegacyButtonModule as ButtonModule} from '@angular/material/legacy-button';`,
53+
});
54+
});
55+
56+
it('updates import expressions', async () => {
57+
await runTypeScriptMigrationTest('destructured & awaited', {
58+
old: `const {MatButton} = await import('@angular/material/button');`,
59+
new: `const {MatLegacyButton: MatButton} = await import('@angular/material/legacy-button');`,
60+
});
61+
await runTypeScriptMigrationTest('destructured & awaited w/ alias', {
62+
old: `const {MatButton: Button} = await import('@angular/material/button');`,
63+
new: `const {MatLegacyButton: Button} = await import('@angular/material/legacy-button');`,
64+
});
65+
await runTypeScriptMigrationTest('promise', {
66+
old: `const promise = import('@angular/material/button');`,
67+
new: `const promise = import('@angular/material/legacy-button');`,
68+
});
69+
await runTypeScriptMigrationTest('.then', {
70+
old: `import('@angular/material/button').then(() => {});`,
71+
new: `import('@angular/material/legacy-button').then(() => {});`,
72+
});
3573
});
3674
});
3775

3876
describe('style migrations', () => {
3977
it('should do nothing yet', async () => {
40-
writeLines(THEME_FILE_PATH, [' ']);
78+
writeLine(THEME_FILE_PATH, ' ');
4179
await runMigration();
42-
expect(splitFile(THEME_FILE_PATH)).toEqual([' ']);
80+
expect(readFile(THEME_FILE_PATH)).toEqual(' ');
4381
});
4482
});
4583
});

0 commit comments

Comments
 (0)