Skip to content

Commit

Permalink
feat(router): add a migration to remove relativeLinkResolution usag…
Browse files Browse the repository at this point in the history
…es (#47604)

As of Angular v15, the deprecated `relativeLinkResolution` config option of the Router is removed.  This migration cleans up (removes) the `relativeLinkResolution` fields from the Router config objects in applications code.

```ts
import { RouterModule } from '@angular/router';

RouterModule.forRoot([], {
  relativeLinkResolution: 'legacy',
  enableTracing: false,
});
```

```ts
import { RouterModule } from '@angular/router';

RouterModule.forRoot([], {
  // the `relativeLinkResolution` is removed
  enableTracing: false,
});
```

PR Close #47604
  • Loading branch information
AndrewKushnir authored and thePunderWoman committed Oct 7, 2022
1 parent 739e689 commit 7bee28d
Show file tree
Hide file tree
Showing 11 changed files with 426 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/relative-link-resolution",
"//packages/core/schematics/migrations/router-link-with-href",
"//packages/core/schematics/migrations/typed-forms",
],
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -19,6 +19,11 @@
"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"
},
"migration-v15-relative-link-resolution": {
"version": "15.0.0-beta",
"description": "In Angular version 15, the deprecated `relativeLinkResolution` config parameter of the Router is removed. This migration removes all `relativeLinkResolution` fields from the Router config objects.",
"factory": "./migrations/relative-link-resolution/index"
}
}
}
1 change: 1 addition & 0 deletions packages/core/schematics/migrations/google3/BUILD.bazel
Expand Up @@ -9,6 +9,7 @@ ts_library(
"//packages/core/schematics/migrations/entry-components",
"//packages/core/schematics/migrations/path-match-type",
"//packages/core/schematics/migrations/path-match-type/google3",
"//packages/core/schematics/migrations/relative-link-resolution",
"//packages/core/schematics/migrations/typed-forms",
"//packages/core/schematics/utils",
"//packages/core/schematics/utils/tslint",
Expand Down
@@ -0,0 +1,31 @@
/**
* @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 {Replacement, RuleFailure, Rules} from 'tslint';
import ts from 'typescript';

import {migrateFile} from '../relative-link-resolution/util';

/** TSLint rule for the `relativeLinkResolution` migration. */
export class Rule extends Rules.TypedRule {
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const failures: RuleFailure[] = [];

const rewriter = (startPos: number, origLength: number, text: string) => {
const failure = new RuleFailure(
sourceFile, startPos, startPos + origLength,
'The `relativeLinkResolution` Router config option is removed and should not be used.',
this.ruleName, new Replacement(startPos, origLength, text));
failures.push(failure);
};

migrateFile(sourceFile, rewriter);

return failures;
}
}
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "relative-link-resolution",
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,25 @@
## Relative link resolution migration

As of Angular v15, the deprecated `relativeLinkResolution` config option of the Router is removed.
This migration cleans up (removes) the `relativeLinkResolution` fields from the Router config objects
in applications code.

#### Before
```ts
import { RouterModule } from '@angular/router';

RouterModule.forRoot([], {
relativeLinkResolution: 'legacy',
enableTracing: false,
});
```

#### After
```ts
import { RouterModule } from '@angular/router';

RouterModule.forRoot([], {
// the `relativeLinkResolution` is removed
enableTracing: false,
});
```
@@ -0,0 +1,58 @@
/**
* @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 a migration to cleanup the deprecated `relativeLinkResolution` config option.');
}

for (const tsconfigPath of allPaths) {
runRelativeLinkResolutionMigration(tree, tsconfigPath, basePath);
}
};
}

function runRelativeLinkResolutionMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles =
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));

for (const sourceFile of sourceFiles) {
let update: UpdateRecorder|null = null;

const rewriter = (startPos: number, origLength: number, text: string) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
}
update.remove(startPos, origLength);
update.insertLeft(startPos, text);
};

migrateFile(sourceFile, rewriter);

if (update !== null) {
tree.commitUpdate(update);
}
}
}
@@ -0,0 +1,85 @@
/**
* @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';

const relativeLinkResolution = 'relativeLinkResolution';
const knownConfigValues = new Set([`'legacy'`, `'corrected'`]);

export interface RewriteEntity {
startPos: number;
width: number;
replacement: string;
}

export interface MigratableNode {
objectLiteral: ts.ObjectLiteralExpression;
property: ts.ObjectLiteralElementLike;
}

export type RewriteFn = (startPos: number, origLength: number, text: string) => void;

export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
let rewrites: RewriteEntity[] = [];
const usages = getUsages(sourceFile);
for (const {objectLiteral, property} of usages) {
const replacementNode = ts.factory.updateObjectLiteralExpression(
objectLiteral, objectLiteral.properties.filter(prop => prop !== property));
const printer = ts.createPrinter();
const replacementText = printer.printNode(ts.EmitHint.Unspecified, replacementNode, sourceFile);
rewrites.push({
startPos: objectLiteral.getStart(),
width: objectLiteral.getWidth(),
replacement: replacementText,
});
}

// Process rewrites last-to-first (based on start pos) to avoid offset shifts during rewrites.
rewrites = sortByStartPosDescending(rewrites);
for (const rewrite of rewrites) {
rewriteFn(rewrite.startPos, rewrite.width, rewrite.replacement);
}
}

function getUsages(sourceFile: ts.SourceFile): MigratableNode[] {
const usages: MigratableNode[] = [];
const visitNode = (node: ts.Node) => {
if (ts.isObjectLiteralExpression(node)) {
// Look for patterns like the following:
// ```
// { ... relativeLinkResolution: 'legacy', ... }
// ```
// or:
// ```
// { ... relativeLinkResolution: 'corrected', ... }
// ```
// If the value is unknown (i.e. not 'legacy' or 'corrected'),
// do not attempt to rewrite (this might be an application-specific
// configuration, not a part of Router).
const property = node.properties.find(
prop => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) &&
prop.name.text === relativeLinkResolution &&
knownConfigValues.has(prop.initializer.getText()));
if (property) {
usages.push({objectLiteral: node, property});
}
}
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/relative-link-resolution",
"//packages/core/schematics/migrations/router-link-with-href",
"//packages/core/schematics/migrations/typed-forms",
"//packages/core/schematics/utils",
Expand Down
@@ -0,0 +1,72 @@
/**
* @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 {readFileSync, writeFileSync} from 'fs';
import {dirname, join} from 'path';
import * as shx from 'shelljs';
import {Configuration, Linter} from 'tslint';

describe('Google3 relativeLinkResolution TSLint rule', () => {
const rulesDirectory =
dirname(require.resolve('../../migrations/google3/relativeLinkResolutionRule'));

let tmpDir: string;

beforeEach(() => {
tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test');
shx.mkdir('-p', tmpDir);

writeFile('tsconfig.json', JSON.stringify({
compilerOptions: {
module: 'es2015',
baseUrl: './',
},
}));
});

afterEach(() => shx.rm('-r', tmpDir));

function runTSLint(fix: boolean) {
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
const config = Configuration.parseConfigFile({rules: {'relativeLinkResolution': true}});

program.getRootFileNames().forEach(fileName => {
linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config);
});

return linter;
}

function writeFile(fileName: string, content: string) {
writeFileSync(join(tmpDir, fileName), content);
}

function getFile(fileName: string) {
return readFileSync(join(tmpDir, fileName), 'utf8');
}

// This is just a sanity check for the TSLint configuration;
// see test/relative_link_resolution_spec.ts for the full test suite.
it('should migrate a simple example', () => {
writeFile('/index.ts', `
import { RouterModule } from '@angular/router';
let providers = RouterModule.forRoot([], {
onSameUrlNavigation: 'reload',
paramsInheritanceStrategy: 'always',
relativeLinkResolution: 'legacy',
enableTracing: false,
});
`);

runTSLint(true);
const content = getFile(`/index.ts`);
expect(content).not.toContain('relativeLinkResolution');
});
});

0 comments on commit 7bee28d

Please sign in to comment.