Skip to content

Commit

Permalink
fix(router): create schematic for preserveQueryParams
Browse files Browse the repository at this point in the history
Create a schematic for migrating preserveQueryParams to use queryParamsHandler
instead.
  • Loading branch information
josephperrott committed Oct 12, 2020
1 parent cb867aa commit 5f96a67
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -19,6 +19,7 @@ pkg_npm(
"//packages/core/schematics/migrations/navigation-extras-omissions",
"//packages/core/schematics/migrations/relative-link-resolution",
"//packages/core/schematics/migrations/renderer-to-renderer2",
"//packages/core/schematics/migrations/router-preserve-query-params",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -64,6 +64,11 @@
"version": "11.0.0-beta",
"description": "ViewEncapsulation.Native has been removed as of Angular version 11. This migration replaces any usages with ViewEncapsulation.ShadowDom.",
"factory": "./migrations/native-view-encapsulation/index"
},
"migration-v11-router-preserve-query-params": {
"version": "11.0.0-beta",
"description": "NavigationExtras.preserveQueryParams has been removed as of Angular version 11. This migration replaces any usages with the appropriate assignment of the queryParamsHandler key.",
"factory": "./migrations/router-preserve-query-params/index"
}
}
}
@@ -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",
],
)
@@ -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' });
}
}
```
@@ -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);
});
}
@@ -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;
}
1 change: 1 addition & 0 deletions packages/core/schematics/test/BUILD.bazel
Expand Up @@ -17,6 +17,7 @@ ts_library(
"//packages/core/schematics/migrations/navigation-extras-omissions",
"//packages/core/schematics/migrations/relative-link-resolution",
"//packages/core/schematics/migrations/renderer-to-renderer2",
"//packages/core/schematics/migrations/router-preserve-query-params",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
Expand Down

0 comments on commit 5f96a67

Please sign in to comment.