-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(material/schematics): initial foundation for TS code migrators
- Loading branch information
1 parent
8ec4864
commit 1a99002
Showing
9 changed files
with
269 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
...erial/schematics/ng-generate/mdc-migration/rules/components/button/button-runtime.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import {APP_MODULE_FILE, createNewTestRunner, migrateComponents} from '../test-setup-helper'; | ||
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; | ||
import {createTestApp, patchDevkitTreeToExposeTypeScript} from '@angular/cdk/schematics/testing'; | ||
|
||
describe('button runtime code', () => { | ||
let runner: SchematicTestRunner; | ||
let cliAppTree: UnitTestTree; | ||
|
||
beforeEach(async () => { | ||
runner = createNewTestRunner(); | ||
cliAppTree = patchDevkitTreeToExposeTypeScript(await createTestApp(runner)); | ||
}); | ||
|
||
async function runMigrationTest(oldFileContent: string, newFileContent: string) { | ||
cliAppTree.overwrite(APP_MODULE_FILE, oldFileContent); | ||
const tree = await migrateComponents(['button'], runner, cliAppTree); | ||
expect(tree.readContent(APP_MODULE_FILE)).toBe(newFileContent); | ||
} | ||
|
||
describe('import statements', () => { | ||
it('should replace the old import with the new one', async () => { | ||
await runMigrationTest( | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButtonModule} from '@angular/material/button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButtonModule} from '@angular/material-experimental/mdc-button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
); | ||
}); | ||
|
||
it('should migrate multi-line imports', async () => { | ||
await runMigrationTest( | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import { | ||
MatButton, | ||
MatButtonModule, | ||
} from '@angular/material/button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import { | ||
MatButton, | ||
MatButtonModule, | ||
} from '@angular/material-experimental/mdc-button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
); | ||
}); | ||
|
||
it('should migrate multiple statements', async () => { | ||
await runMigrationTest( | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButton} from '@angular/material/button'; | ||
import {MatButtonModule} from '@angular/material/button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButton} from '@angular/material-experimental/mdc-button'; | ||
import {MatButtonModule} from '@angular/material-experimental/mdc-button'; | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
); | ||
}); | ||
|
||
it('should preserve import comments', async () => { | ||
await runMigrationTest( | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButton /* comment */} from '@angular/material/button'; | ||
import {MatButtonModule} from '@angular/material/button'; // a comment | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatButton /* comment */} from '@angular/material-experimental/mdc-button'; | ||
import {MatButtonModule} from '@angular/material-experimental/mdc-button'; // a comment | ||
@NgModule({imports: [MatButtonModule]}) | ||
export class AppModule {} | ||
`, | ||
); | ||
}); | ||
}); | ||
|
||
describe('import expressions', () => { | ||
it('should replace the old import with the new one', async () => { | ||
await runMigrationTest( | ||
` | ||
const buttonModule = import('@angular/material/button'); | ||
`, | ||
` | ||
const buttonModule = import('@angular/material-experimental/mdc-button'); | ||
`, | ||
); | ||
}); | ||
|
||
it('should replace type import expressions', async () => { | ||
await runMigrationTest( | ||
` | ||
let buttonModule: typeof import("@angular/material/button"); | ||
`, | ||
` | ||
let buttonModule: typeof import("@angular/material-experimental/mdc-button"); | ||
`, | ||
); | ||
}); | ||
}); | ||
}); |
14 changes: 14 additions & 0 deletions
14
src/material/schematics/ng-generate/mdc-migration/rules/components/button/button-runtime.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {RuntimeMigrator} from '../../runtime-migrator'; | ||
|
||
export class ButtonRuntimeMigrator extends RuntimeMigrator { | ||
oldImportModule = '@angular/material/button'; | ||
newImportModule = '@angular/material-experimental/mdc-button'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
src/material/schematics/ng-generate/mdc-migration/rules/runtime-migration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Migration} from '@angular/cdk/schematics'; | ||
import {SchematicContext} from '@angular-devkit/schematics'; | ||
import {ComponentMigrator} from './index'; | ||
import * as ts from 'typescript'; | ||
|
||
export class RuntimeCodeMigration extends Migration<ComponentMigrator[], SchematicContext> { | ||
enabled = true; | ||
|
||
private _printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); | ||
|
||
override visitNode(node: ts.Node): void { | ||
if (this._isImportExpression(node)) { | ||
this._migrateModuleSpecifier(node.arguments[0]); | ||
} else if (this._isTypeImportExpression(node)) { | ||
this._migrateModuleSpecifier(node.argument.literal); | ||
} 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); | ||
} | ||
} | ||
|
||
private _migrateModuleSpecifier(specifierLiteral: ts.StringLiteralLike) { | ||
const sourceFile = specifierLiteral.getSourceFile(); | ||
|
||
// Iterate through all activated migrators and check if the import can be migrated. | ||
for (const migrator of this.upgradeData) { | ||
const newModuleSpecifier = migrator.runtime?.updateModuleSpecifier(specifierLiteral) ?? null; | ||
|
||
if (newModuleSpecifier !== null) { | ||
this._printAndUpdateNode(sourceFile, specifierLiteral, newModuleSpecifier); | ||
|
||
// If the import has been replaced, break the loop as no others can match. | ||
break; | ||
} | ||
} | ||
} | ||
|
||
/** Gets whether the specified node is an import expression. */ | ||
private _isImportExpression( | ||
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]) | ||
); | ||
} | ||
|
||
/** Gets whether the specified node is a type import expression. */ | ||
private _isTypeImportExpression( | ||
node: ts.Node, | ||
): node is ts.ImportTypeNode & {argument: {literal: ts.StringLiteralLike}} { | ||
return ( | ||
ts.isImportTypeNode(node) && | ||
ts.isLiteralTypeNode(node.argument) && | ||
ts.isStringLiteralLike(node.argument.literal) | ||
); | ||
} | ||
|
||
private _printAndUpdateNode(sourceFile: ts.SourceFile, oldNode: ts.Node, newNode: ts.Node) { | ||
const filePath = this.fileSystem.resolve(sourceFile.fileName); | ||
const newNodeText = this._printer.printNode(ts.EmitHint.Unspecified, newNode, sourceFile); | ||
const start = oldNode.getStart(); | ||
const width = oldNode.getWidth(); | ||
|
||
this.fileSystem.edit(filePath).remove(start, width).insertRight(start, newNodeText); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/material/schematics/ng-generate/mdc-migration/rules/runtime-migrator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import * as ts from 'typescript'; | ||
|
||
export abstract class RuntimeMigrator { | ||
abstract oldImportModule: string; | ||
abstract newImportModule: string; | ||
|
||
updateModuleSpecifier(specifier: ts.StringLiteralLike): ts.StringLiteral | null { | ||
if (specifier.text !== this.oldImportModule) { | ||
return null; | ||
} | ||
|
||
return ts.factory.createStringLiteral( | ||
this.newImportModule, | ||
this._isSingleQuoteLiteral(specifier), | ||
); | ||
} | ||
|
||
private _isSingleQuoteLiteral(literal: ts.StringLiteralLike): boolean { | ||
// Note: We prefer single-quote for no-substitution literals as well. | ||
return literal.getText()[0] !== `"`; | ||
} | ||
} |