Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…#47599) Since Angular v15, the `RouterLink` contains the logic of the `RouterLinkWithHref` directive and now developers can always import and use the `RouterLink` directive when they need to add a `[routerLink]` in templates. This migration finds all imports and usages of the `RouterLinkWithHref` class and rewrites them to `RouterLink` instead. ```ts import { RouterLinkWithHref } from '@angular/router'; @component({ standalone: true, template: `<a [routerLink]="'/abc'">`, imports: [RouterLinkWithHref] }) export class MyComponent { @ViewChild(RouterLinkWithHref) aLink!: RouterLinkWithHref; } ``` ```ts import { RouterLink } from '@angular/router'; @component({ standalone: true, template: `<a [routerLink]="'/abc'">`, imports: [RouterLink] }) export class MyComponent { @ViewChild(RouterLink) aLink!: RouterLink; } ``` PR Close #47599
- Loading branch information
1 parent
db28bad
commit 16c8f55
Showing
9 changed files
with
401 additions
and
0 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
18 changes: 18 additions & 0 deletions
18
packages/core/schematics/migrations/router-link-with-href/BUILD.bazel
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,18 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
ts_library( | ||
name = "router-link-with-href", | ||
srcs = glob(["**/*.ts"]), | ||
tsconfig = "//packages/core/schematics:tsconfig.json", | ||
visibility = [ | ||
"//packages/core/schematics:__pkg__", | ||
"//packages/core/schematics/migrations/google3:__pkg__", | ||
"//packages/core/schematics/test:__pkg__", | ||
], | ||
deps = [ | ||
"//packages/core/schematics/utils", | ||
"@npm//@angular-devkit/schematics", | ||
"@npm//@types/node", | ||
"@npm//typescript", | ||
], | ||
) |
31 changes: 31 additions & 0 deletions
31
packages/core/schematics/migrations/router-link-with-href/README.md
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,31 @@ | ||
## RouterLinkWithHref migration | ||
|
||
Since Angular v15, the `RouterLink` contains the logic of the `RouterLinkWithHref` directive and now developers can always import and use the `RouterLink` directive when they need to add a `[routerLink]` in templates. This migration finds all imports and usages of the `RouterLinkWithHref` class and rewrites them to `RouterLink` instead. | ||
|
||
#### Before | ||
```ts | ||
import { RouterLinkWithHref } from '@angular/router'; | ||
|
||
@Component({ | ||
standalone: true, | ||
template: `<a [routerLink]="'/abc'">`, | ||
imports: [RouterLinkWithHref] | ||
}) | ||
export class MyComponent { | ||
@ViewChild(RouterLinkWithHref) aLink!: RouterLinkWithHref; | ||
} | ||
``` | ||
|
||
#### After | ||
```ts | ||
import { RouterLink } from '@angular/router'; | ||
|
||
@Component({ | ||
standalone: true, | ||
template: `<a [routerLink]="'/abc'">`, | ||
imports: [RouterLink] | ||
}) | ||
export class MyComponent { | ||
@ViewChild(RouterLink) aLink!: RouterLink; | ||
} | ||
``` |
60 changes: 60 additions & 0 deletions
60
packages/core/schematics/migrations/router-link-with-href/index.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,60 @@ | ||
/** | ||
* @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 {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics'; | ||
import {relative} from 'path'; | ||
|
||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; | ||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; | ||
|
||
import {migrateFile} from './util'; | ||
|
||
export default function(): Rule { | ||
return async (tree: Tree) => { | ||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); | ||
const basePath = process.cwd(); | ||
const allPaths = [...buildPaths, ...testPaths]; | ||
|
||
if (!allPaths.length) { | ||
throw new SchematicsException( | ||
'Could not find any tsconfig file. Cannot run the `RouterLinkWithHref` migration.'); | ||
} | ||
|
||
for (const tsconfigPath of allPaths) { | ||
runRouterLinkWithHrefMigration(tree, tsconfigPath, basePath); | ||
} | ||
}; | ||
} | ||
|
||
function runRouterLinkWithHrefMigration(tree: Tree, tsconfigPath: string, basePath: string) { | ||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath); | ||
const typeChecker = program.getTypeChecker(); | ||
const sourceFiles = | ||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); | ||
|
||
for (const sourceFile of sourceFiles) { | ||
let update: UpdateRecorder|null = null; | ||
|
||
const rewriter = (startPos: number, width: number, text: string|null) => { | ||
if (update === null) { | ||
// Lazily initialize update, because most files will not require migration. | ||
update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); | ||
} | ||
update.remove(startPos, width); | ||
if (text !== null) { | ||
update.insertLeft(startPos, text); | ||
} | ||
}; | ||
|
||
migrateFile(sourceFile, typeChecker, rewriter); | ||
|
||
if (update !== null) { | ||
tree.commitUpdate(update); | ||
} | ||
} | ||
} |
113 changes: 113 additions & 0 deletions
113
packages/core/schematics/migrations/router-link-with-href/util.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,113 @@ | ||
/** | ||
* @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 ts from 'typescript'; | ||
|
||
import {getImportOfIdentifier, getImportSpecifier, removeSymbolFromNamedImports} from '../../utils/typescript/imports'; | ||
import {closestNode} from '../../utils/typescript/nodes'; | ||
|
||
export const routerLink = 'RouterLink'; | ||
export const routerLinkWithHref = 'RouterLinkWithHref'; | ||
export const routerModule = '@angular/router'; | ||
|
||
export interface RewriteEntity { | ||
startPos: number; | ||
width: number; | ||
replacement: string; | ||
} | ||
|
||
export type RewriteFn = (startPos: number, width: number, text: string) => void; | ||
|
||
export function migrateFile( | ||
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, rewrite: RewriteFn) { | ||
const routerLinkWithHrefSpec = getImportSpecifier(sourceFile, routerModule, routerLinkWithHref); | ||
|
||
// No `RouterLinkWithHref` found, nothing to migrate, exit early. | ||
if (routerLinkWithHrefSpec === null) return; | ||
|
||
let rewrites = findUsages(sourceFile, typeChecker); | ||
|
||
// There are some usages of the `RouterLinkWithHref` symbol, which need to | ||
// be rewritten to `RouterLink` instead. Let's check if the `RouterLink` is | ||
// already imported. | ||
const routerLinkSpec = getImportSpecifier(sourceFile, routerModule, routerLink); | ||
|
||
if (routerLinkSpec) { | ||
// The `RouterLink` symbol is already imported, just drop the `RouterLinkWithHref` one. | ||
const routerLinkNamedImports = routerLinkWithHrefSpec ? | ||
closestNode<ts.NamedImports>(routerLinkWithHrefSpec, ts.SyntaxKind.NamedImports) : | ||
null; | ||
if (routerLinkNamedImports !== null) { | ||
// Given an original import like this one: | ||
// ``` | ||
// import {RouterModule, RouterLinkWithHref, RouterLink} from '@angular/router'; | ||
// ``` | ||
// The code below removes the `RouterLinkWithHref` from the named imports section | ||
// (i.e. `{RouterModule, RouterLinkWithHref, RouterLink}`) and prints an updated | ||
// version (`{RouterModule, RouterLink}`) to a string, which is used as a | ||
// replacement. | ||
const rewrittenNamedImports = | ||
removeSymbolFromNamedImports(routerLinkNamedImports, routerLinkWithHrefSpec); | ||
const printer = ts.createPrinter(); | ||
const replacement = | ||
printer.printNode(ts.EmitHint.Unspecified, rewrittenNamedImports, sourceFile); | ||
rewrites.push({ | ||
startPos: routerLinkNamedImports.getStart(), | ||
width: routerLinkNamedImports.getWidth(), | ||
replacement: replacement, | ||
}); | ||
} | ||
} else { | ||
// The `RouterLink` symbol is not imported, but the `RouterLinkWithHref` is imported, | ||
// so rewrite `RouterLinkWithHref` -> `RouterLink`. | ||
rewrites.push({ | ||
startPos: routerLinkWithHrefSpec.getStart(), | ||
width: routerLinkWithHrefSpec.getWidth(), | ||
replacement: routerLink, | ||
}); | ||
} | ||
|
||
// Process rewrites last-to-first (based on start pos) to avoid offset shifts during rewrites. | ||
rewrites = sortByStartPosDescending(rewrites); | ||
for (const usage of rewrites) { | ||
rewrite(usage.startPos, usage.width, usage.replacement); | ||
} | ||
} | ||
|
||
function findUsages(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): RewriteEntity[] { | ||
const usages: RewriteEntity[] = []; | ||
const visitNode = (node: ts.Node) => { | ||
if (ts.isImportSpecifier(node)) { | ||
// Skip this node and all of its children; imports are a special case. | ||
return; | ||
} | ||
if (ts.isIdentifier(node)) { | ||
const importIdentifier = getImportOfIdentifier(typeChecker, node); | ||
if (importIdentifier?.importModule === routerModule && | ||
importIdentifier.name === routerLinkWithHref) { | ||
usages.push({ | ||
startPos: node.getStart(), | ||
width: node.getWidth(), | ||
replacement: routerLink, | ||
}); | ||
} | ||
} | ||
ts.forEachChild(node, visitNode); | ||
}; | ||
ts.forEachChild(sourceFile, visitNode); | ||
return usages; | ||
} | ||
|
||
/** | ||
* Sort all found usages based on their start positions in the source file in descending order (i.e. | ||
* last usage goes first on the list, etc). This is needed to avoid shifting offsets in the source | ||
* file (in case there are multiple usages) as we rewrite symbols. | ||
*/ | ||
function sortByStartPosDescending(rewrites: RewriteEntity[]): RewriteEntity[] { | ||
return rewrites.sort((entityA, entityB) => entityB.startPos - entityA.startPos); | ||
} |
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
Oops, something went wrong.