Skip to content

Commit

Permalink
feat(router): migrate RouterLinkWithHref references to RouterLink (
Browse files Browse the repository at this point in the history
…#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
AndrewKushnir authored and thePunderWoman committed Oct 6, 2022
1 parent db28bad commit 16c8f55
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -15,6 +15,7 @@ pkg_npm(
deps = [
"//packages/core/schematics/migrations/entry-components",
"//packages/core/schematics/migrations/path-match-type",
"//packages/core/schematics/migrations/router-link-with-href",
"//packages/core/schematics/migrations/typed-forms",
],
)
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -14,6 +14,11 @@
"version": "14.0.0-beta",
"description": "In Angular version 14, the `pathMatch` property of `Routes` was updated to be a strict union of the two valid options: `'full'|'prefix'`. `Routes` and `Route` variables need an explicit type so TypeScript does not infer the property as the looser `string`.",
"factory": "./migrations/path-match-type/index"
},
"migration-v15-router-link-with-href": {
"version": "15.0.0-beta",
"description": "Since Angular v15, the `RouterLink` contains the logic of the `RouterLinkWithHref` directive. This migration replaces all `RouterLinkWithHref` references with `RouterLink`.",
"factory": "./migrations/router-link-with-href/index"
}
}
}
@@ -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",
],
)
@@ -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 packages/core/schematics/migrations/router-link-with-href/index.ts
@@ -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 packages/core/schematics/migrations/router-link-with-href/util.ts
@@ -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);
}
1 change: 1 addition & 0 deletions packages/core/schematics/test/BUILD.bazel
Expand Up @@ -10,6 +10,7 @@ ts_library(
deps = [
"//packages/core/schematics/migrations/entry-components",
"//packages/core/schematics/migrations/path-match-type",
"//packages/core/schematics/migrations/router-link-with-href",
"//packages/core/schematics/migrations/typed-forms",
"//packages/core/schematics/utils",
"@npm//@angular-devkit/core",
Expand Down

0 comments on commit 16c8f55

Please sign in to comment.