Sitelet https://github.com/angular/angular/commit/93ee05d
Skip to content

Commit 93ee05d

Browse files
josephperrottatscott
authored andcommitted
fix(router): create schematic for preserveQueryParams (#38762)
Create a schematic for migrating preserveQueryParams to use queryParamsHandler instead. PR Close #38762
1 parent 783a5bd commit 93ee05d

8 files changed

Lines changed: 461 additions & 0 deletions

File tree

‎packages/core/schematics/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pkg_npm(
1919
"//packages/core/schematics/migrations/navigation-extras-omissions",
2020
"//packages/core/schematics/migrations/relative-link-resolution",
2121
"//packages/core/schematics/migrations/renderer-to-renderer2",
22+
"//packages/core/schematics/migrations/router-preserve-query-params",
2223
"//packages/core/schematics/migrations/static-queries",
2324
"//packages/core/schematics/migrations/template-var-assignment",
2425
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",

‎packages/core/schematics/migrations.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
"version": "11.0.0-beta",
7070
"description": "`async` to `waitForAsync` migration. The `async` testing function has been renamed to `waitForAsync` to avoid confusion with the native `async` keyword.",
7171
"factory": "./migrations/wait-for-async/index"
72+
},
73+
"migration-v11-router-preserve-query-params": {
74+
"version": "11.0.0-beta",
75+
"description": "NavigationExtras.preserveQueryParams has been removed as of Angular version 11. This migration replaces any usages with the appropriate assignment of the queryParamsHandler key.",
76+
"factory": "./migrations/router-preserve-query-params/index"
7277
}
7378
}
7479
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "router-preserve-query-params",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = [
8+
"//packages/core/schematics:__pkg__",
9+
"//packages/core/schematics/migrations/google3:__pkg__",
10+
"//packages/core/schematics/test:__pkg__",
11+
],
12+
deps = [
13+
"//packages/core/schematics/utils",
14+
"@npm//@angular-devkit/schematics",
15+
"@npm//@types/node",
16+
"@npm//typescript",
17+
],
18+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## Router's NavigationExtras.preserveQueryParams migration
2+
3+
Previously the `NatigationExtras` property of `preserveQueryParams` defined what should be done with
4+
query parameters on navigation. This migration updates the usages of `preserveQueryParams` to
5+
instead use the `queryParamsHandler` property.
6+
7+
#### Before
8+
```ts
9+
import { Component } from '@angular/core';
10+
import { Router } from '@angular/router';
11+
12+
@Component({})
13+
export class MyComponent {
14+
constructor(private _router: Router) {}
15+
16+
goHome() {
17+
this._router.navigate('/', {preserveQueryParams: true, skipLocationChange: 'foo'});
18+
}
19+
}
20+
```
21+
22+
#### After
23+
```ts
24+
import { Component } from '@angular/core';
25+
import { Router } from '@angular/router';
26+
27+
@Component({})
28+
export class MyComponent {
29+
constructor(private _router: Router) {}
30+
31+
goHome() {
32+
this._router.navigate('/', { queryParamsHandler: 'preserve', skipLocationChange: 'foo' });
33+
}
34+
}
35+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {relative} from 'path';
11+
import * as ts from 'typescript';
12+
13+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
14+
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
15+
import {findLiteralsToMigrate, migrateLiteral} from './util';
16+
17+
18+
/**
19+
* Migration that switches `NavigationExtras.preserveQueryParams` to set the coresponding value via
20+
* `NavigationExtras`'s `queryParamsHandling` attribute.
21+
*/
22+
export default function(): Rule {
23+
return (tree: Tree) => {
24+
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
25+
const basePath = process.cwd();
26+
const allPaths = [...buildPaths, ...testPaths];
27+
28+
if (!allPaths.length) {
29+
throw new SchematicsException(
30+
'Could not find any tsconfig file. Cannot migrate ' +
31+
'NavigationExtras.preserveQueryParams usages.');
32+
}
33+
34+
for (const tsconfigPath of allPaths) {
35+
runPreserveQueryParamsMigration(tree, tsconfigPath, basePath);
36+
}
37+
};
38+
}
39+
40+
function runPreserveQueryParamsMigration(tree: Tree, tsconfigPath: string, basePath: string) {
41+
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
42+
const typeChecker = program.getTypeChecker();
43+
const printer = ts.createPrinter();
44+
const sourceFiles = program.getSourceFiles().filter(
45+
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
46+
47+
sourceFiles.forEach(sourceFile => {
48+
const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker);
49+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
50+
51+
literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => {
52+
const migratedNode = migrateLiteral(methodName, instance);
53+
54+
if (migratedNode !== instance) {
55+
update.remove(instance.getStart(), instance.getWidth());
56+
update.insertRight(
57+
instance.getStart(),
58+
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile));
59+
}
60+
}));
61+
62+
tree.commitUpdate(update);
63+
});
64+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {getImportSpecifier} from '../../utils/typescript/imports';
12+
import {isReferenceToImport} from '../../utils/typescript/symbol';
13+
14+
/**
15+
* Configures the methods that the migration should be looking for
16+
* and the properties from `NavigationExtras` that should be preserved.
17+
*/
18+
const methodConfig = new Set<string>(['navigate', 'createUrlTree']);
19+
20+
const preserveQueryParamsKey = 'preserveQueryParams';
21+
22+
export function migrateLiteral(
23+
methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
24+
const isMigratableMethod = methodConfig.has(methodName);
25+
26+
if (!isMigratableMethod) {
27+
throw Error(`Attempting to migrate unconfigured method called ${methodName}.`);
28+
}
29+
30+
31+
const propertiesToKeep: ts.ObjectLiteralElementLike[] = [];
32+
let propertyToMigrate: ts.PropertyAssignment|ts.ShorthandPropertyAssignment|undefined = undefined;
33+
34+
for (const property of node.properties) {
35+
// Only look for regular and shorthand property assignments since resolving things
36+
// like spread operators becomes too complicated for this migration.
37+
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
38+
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) ||
39+
ts.isIdentifier(property.name)) &&
40+
(property.name.text === preserveQueryParamsKey)) {
41+
propertyToMigrate = property;
42+
continue;
43+
}
44+
propertiesToKeep.push(property);
45+
}
46+
47+
// Don't modify the node if there's nothing to migrate.
48+
if (propertyToMigrate === undefined) {
49+
return node;
50+
}
51+
52+
if ((ts.isShorthandPropertyAssignment(propertyToMigrate) &&
53+
propertyToMigrate.objectAssignmentInitializer?.kind === ts.SyntaxKind.TrueKeyword) ||
54+
(ts.isPropertyAssignment(propertyToMigrate) &&
55+
propertyToMigrate.initializer.kind === ts.SyntaxKind.TrueKeyword)) {
56+
return ts.updateObjectLiteral(
57+
node,
58+
propertiesToKeep.concat(
59+
ts.createPropertyAssignment('queryParamsHandler', ts.createIdentifier(`'preserve'`))));
60+
}
61+
62+
return ts.updateObjectLiteral(node, propertiesToKeep);
63+
}
64+
65+
export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) {
66+
const results = new Map<string, Set<ts.ObjectLiteralExpression>>(
67+
Array.from(methodConfig.keys(), key => [key, new Set()]));
68+
const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router');
69+
const seenLiterals = new Map<ts.ObjectLiteralExpression, string>();
70+
71+
if (routerImport) {
72+
sourceFile.forEachChild(function visitNode(node: ts.Node) {
73+
// Look for calls that look like `foo.<method to migrate>` with more than one parameter.
74+
if (ts.isCallExpression(node) && node.arguments.length > 1 &&
75+
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) &&
76+
methodConfig.has(node.expression.name.text)) {
77+
// Check whether the type of the object on which the
78+
// function is called refers to the Router import.
79+
if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) {
80+
const methodName = node.expression.name.text;
81+
const parameterDeclaration =
82+
typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration;
83+
84+
// Find the source of the object literal.
85+
if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) {
86+
if (!seenLiterals.has(parameterDeclaration)) {
87+
results.get(methodName)!.add(parameterDeclaration);
88+
seenLiterals.set(parameterDeclaration, methodName);
89+
// If the same literal has been passed into multiple different methods, we can't
90+
// migrate it, because the supported properties are different. When we detect such
91+
// a case, we drop it from the results so that it gets ignored. If it's used multiple
92+
// times for the same method, it can still be migrated.
93+
} else if (seenLiterals.get(parameterDeclaration) !== methodName) {
94+
results.forEach(literals => literals.delete(parameterDeclaration));
95+
}
96+
}
97+
}
98+
} else {
99+
node.forEachChild(visitNode);
100+
}
101+
});
102+
}
103+
104+
return results;
105+
}

‎packages/core/schematics/test/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ts_library(
1717
"//packages/core/schematics/migrations/navigation-extras-omissions",
1818
"//packages/core/schematics/migrations/relative-link-resolution",
1919
"//packages/core/schematics/migrations/renderer-to-renderer2",
20+
"//packages/core/schematics/migrations/router-preserve-query-params",
2021
"//packages/core/schematics/migrations/static-queries",
2122
"//packages/core/schematics/migrations/template-var-assignment",
2223
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",

0 commit comments

Comments
 (0)