Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(router): create schematic for preserveQueryParams
Create a schematic for migrating preserveQueryParams to use queryParamsHandler instead.
- Loading branch information
1 parent
cb867aa
commit 5f96a67
Showing
8 changed files
with
461 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-preserve-query-params/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-preserve-query-params", | ||
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", | ||
], | ||
) |
35 changes: 35 additions & 0 deletions
35
packages/core/schematics/migrations/router-preserve-query-params/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,35 @@ | ||
## Router's NavigationExtras.preserveQueryParams migration | ||
|
||
Previously the `NatigationExtras` property of `preserveQueryParams` defined what should be done with | ||
query parameters on navigation. This migration updates the usages of `preserveQueryParams` to | ||
instead use the `queryParamsHandler` property. | ||
|
||
#### Before | ||
```ts | ||
import { Component } from '@angular/core'; | ||
import { Router } from '@angular/router'; | ||
|
||
@Component({}) | ||
export class MyComponent { | ||
constructor(private _router: Router) {} | ||
|
||
goHome() { | ||
this._router.navigate('/', {preserveQueryParams: true, skipLocationChange: 'foo'}); | ||
} | ||
} | ||
``` | ||
|
||
#### After | ||
```ts | ||
import { Component } from '@angular/core'; | ||
import { Router } from '@angular/router'; | ||
|
||
@Component({}) | ||
export class MyComponent { | ||
constructor(private _router: Router) {} | ||
|
||
goHome() { | ||
this._router.navigate('/', { skipLocationChange: 'foo', queryParamsHandler: 'preserve' }); | ||
} | ||
} | ||
``` |
64 changes: 64 additions & 0 deletions
64
packages/core/schematics/migrations/router-preserve-query-params/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,64 @@ | ||
/** | ||
* @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} from '@angular-devkit/schematics'; | ||
import {relative} from 'path'; | ||
import * as ts from 'typescript'; | ||
|
||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; | ||
import {createMigrationProgram} from '../../utils/typescript/compiler_host'; | ||
import {findLiteralsToMigrate, migrateLiteral} from './util'; | ||
|
||
|
||
/** | ||
* Migration that switches `NavigationExtras.preserveQueryParams` to set the coresponding value via | ||
* `NavigationExtras`'s `queryParamsHandling` attribute. | ||
*/ | ||
export default function(): Rule { | ||
return (tree: Tree) => { | ||
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree); | ||
const basePath = process.cwd(); | ||
const allPaths = [...buildPaths, ...testPaths]; | ||
|
||
if (!allPaths.length) { | ||
throw new SchematicsException( | ||
'Could not find any tsconfig file. Cannot migrate ' + | ||
'NavigationExtras.preserveQueryParams usages.'); | ||
} | ||
|
||
for (const tsconfigPath of allPaths) { | ||
runPreserveQueryParamsMigration(tree, tsconfigPath, basePath); | ||
} | ||
}; | ||
} | ||
|
||
function runPreserveQueryParamsMigration(tree: Tree, tsconfigPath: string, basePath: string) { | ||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath); | ||
const typeChecker = program.getTypeChecker(); | ||
const printer = ts.createPrinter(); | ||
const sourceFiles = program.getSourceFiles().filter( | ||
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); | ||
|
||
sourceFiles.forEach(sourceFile => { | ||
const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker); | ||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); | ||
|
||
literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => { | ||
const migratedNode = migrateLiteral(methodName, instance); | ||
|
||
if (migratedNode !== instance) { | ||
update.remove(instance.getStart(), instance.getWidth()); | ||
update.insertRight( | ||
instance.getStart(), | ||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); | ||
} | ||
})); | ||
|
||
tree.commitUpdate(update); | ||
}); | ||
} |
105 changes: 105 additions & 0 deletions
105
packages/core/schematics/migrations/router-preserve-query-params/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,105 @@ | ||
/** | ||
* @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'; | ||
|
||
import {getImportSpecifier} from '../../utils/typescript/imports'; | ||
import {isReferenceToImport} from '../../utils/typescript/symbol'; | ||
|
||
/** | ||
* Configures the methods that the migration should be looking for | ||
* and the properties from `NavigationExtras` that should be preserved. | ||
*/ | ||
const methodConfig = new Set<string>(['navigate', 'createUrlTree']); | ||
|
||
const preserveQueryParamsKey = 'preserveQueryParams'; | ||
|
||
export function migrateLiteral( | ||
methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression { | ||
const isMigratableMethod = methodConfig.has(methodName); | ||
|
||
if (!isMigratableMethod) { | ||
throw Error(`Attempting to migrate unconfigured method called ${methodName}.`); | ||
} | ||
|
||
|
||
const propertiesToKeep: ts.ObjectLiteralElementLike[] = []; | ||
let propertyToMigrate: ts.PropertyAssignment|ts.ShorthandPropertyAssignment|undefined = undefined; | ||
|
||
for (const property of node.properties) { | ||
// Only look for regular and shorthand property assignments since resolving things | ||
// like spread operators becomes too complicated for this migration. | ||
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && | ||
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) || | ||
ts.isIdentifier(property.name)) && | ||
(property.name.text === preserveQueryParamsKey)) { | ||
propertyToMigrate = property; | ||
continue; | ||
} | ||
propertiesToKeep.push(property); | ||
}; | ||
|
||
// Don't modify the node if there's nothing to migrate. | ||
if (propertyToMigrate === undefined) { | ||
return node; | ||
} | ||
|
||
if ((ts.isShorthandPropertyAssignment(propertyToMigrate) && | ||
propertyToMigrate.objectAssignmentInitializer?.kind === ts.SyntaxKind.TrueKeyword) || | ||
(ts.isPropertyAssignment(propertyToMigrate) && | ||
propertyToMigrate.initializer.kind === ts.SyntaxKind.TrueKeyword)) { | ||
return ts.updateObjectLiteral( | ||
node, | ||
propertiesToKeep.concat( | ||
ts.createPropertyAssignment('queryParamsHandler', ts.createIdentifier(`'preserve'`)))); | ||
} | ||
|
||
return ts.updateObjectLiteral(node, propertiesToKeep); | ||
} | ||
|
||
export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { | ||
const results = new Map<string, Set<ts.ObjectLiteralExpression>>( | ||
Array.from(methodConfig.keys(), key => [key, new Set()])); | ||
const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router'); | ||
const seenLiterals = new Map<ts.ObjectLiteralExpression, string>(); | ||
|
||
if (routerImport) { | ||
sourceFile.forEachChild(function visitNode(node: ts.Node) { | ||
// Look for calls that look like `foo.<method to migrate>` with more than one parameter. | ||
if (ts.isCallExpression(node) && node.arguments.length > 1 && | ||
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) && | ||
methodConfig.has(node.expression.name.text)) { | ||
// Check whether the type of the object on which the | ||
// function is called refers to the Router import. | ||
if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) { | ||
const methodName = node.expression.name.text; | ||
const parameterDeclaration = | ||
typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration; | ||
|
||
// Find the source of the object literal. | ||
if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) { | ||
if (!seenLiterals.has(parameterDeclaration)) { | ||
results.get(methodName)!.add(parameterDeclaration); | ||
seenLiterals.set(parameterDeclaration, methodName); | ||
// If the same literal has been passed into multiple different methods, we can't | ||
// migrate it, because the supported properties are different. When we detect such | ||
// a case, we drop it from the results so that it gets ignored. If it's used multiple | ||
// times for the same method, it can still be migrated. | ||
} else if (seenLiterals.get(parameterDeclaration) !== methodName) { | ||
results.forEach(literals => literals.delete(parameterDeclaration)); | ||
} | ||
} | ||
} | ||
} else { | ||
node.forEachChild(visitNode); | ||
} | ||
}); | ||
} | ||
|
||
return results; | ||
} |
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.