feat(core): add initialNavigation schematic (#36926)
Add a schematic to update users to the new v11 `initialNavigation` options for `RouterModule`. This replaces the deprecated/removed `true`, `false`, `legacy_disabled`, and `legacy_enabled` options with the newer `enabledBlocking` and `enabledNonBlocking` options. PR Close #36926
This commit is contained in:
parent
c4becca0e4
commit
0ec7043490
|
@ -12,6 +12,7 @@ pkg_npm(
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/core/schematics/migrations/abstract-control-parent",
|
"//packages/core/schematics/migrations/abstract-control-parent",
|
||||||
"//packages/core/schematics/migrations/dynamic-queries",
|
"//packages/core/schematics/migrations/dynamic-queries",
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation",
|
||||||
"//packages/core/schematics/migrations/missing-injectable",
|
"//packages/core/schematics/migrations/missing-injectable",
|
||||||
"//packages/core/schematics/migrations/module-with-providers",
|
"//packages/core/schematics/migrations/module-with-providers",
|
||||||
"//packages/core/schematics/migrations/move-document",
|
"//packages/core/schematics/migrations/move-document",
|
||||||
|
|
|
@ -74,6 +74,11 @@
|
||||||
"version": "11.0.0-beta",
|
"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.",
|
"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"
|
"factory": "./migrations/router-preserve-query-params/index"
|
||||||
|
},
|
||||||
|
"migration-v11-router-initial-navigation-options": {
|
||||||
|
"version": "11.0.0-beta",
|
||||||
|
"description": "Updates the `initialNavigation` property for `RouterModule.forRoot`.",
|
||||||
|
"factory": "./migrations/initial-navigation/index"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ ts_library(
|
||||||
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
|
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/core/schematics/migrations/dynamic-queries",
|
"//packages/core/schematics/migrations/dynamic-queries",
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation",
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation/google3",
|
||||||
"//packages/core/schematics/migrations/missing-injectable",
|
"//packages/core/schematics/migrations/missing-injectable",
|
||||||
"//packages/core/schematics/migrations/missing-injectable/google3",
|
"//packages/core/schematics/migrations/missing-injectable/google3",
|
||||||
"//packages/core/schematics/migrations/navigation-extras-omissions",
|
"//packages/core/schematics/migrations/navigation-extras-omissions",
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* @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 {RuleFailure, Rules} from 'tslint';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {InitialNavigationCollector} from '../initial-navigation/collector';
|
||||||
|
import {TslintUpdateRecorder} from '../initial-navigation/google3/tslint_update_recorder';
|
||||||
|
import {InitialNavigationTransform} from '../initial-navigation/transform';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TSLint rule that updates RouterModule `forRoot` options to be in line with v10 updates.
|
||||||
|
*/
|
||||||
|
export class Rule extends Rules.TypedRule {
|
||||||
|
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||||
|
const ruleName = this.ruleName;
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const sourceFiles = program.getSourceFiles().filter(
|
||||||
|
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
|
||||||
|
const initialNavigationCollector = new InitialNavigationCollector(typeChecker);
|
||||||
|
const failures: RuleFailure[] = [];
|
||||||
|
|
||||||
|
// Analyze source files by detecting all ExtraOptions#InitialNavigation assignments
|
||||||
|
sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile));
|
||||||
|
|
||||||
|
const {assignments} = initialNavigationCollector;
|
||||||
|
const transformer = new InitialNavigationTransform(typeChecker, getUpdateRecorder);
|
||||||
|
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
|
||||||
|
|
||||||
|
transformer.migrateInitialNavigationAssignments(Array.from(assignments));
|
||||||
|
|
||||||
|
if (updateRecorders.has(sourceFile)) {
|
||||||
|
failures.push(...updateRecorders.get(sourceFile)!.failures);
|
||||||
|
}
|
||||||
|
|
||||||
|
return failures;
|
||||||
|
|
||||||
|
/** Gets the update recorder for the specified source file. */
|
||||||
|
function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder {
|
||||||
|
if (updateRecorders.has(sourceFile)) {
|
||||||
|
return updateRecorders.get(sourceFile)!;
|
||||||
|
}
|
||||||
|
const recorder = new TslintUpdateRecorder(ruleName, sourceFile);
|
||||||
|
updateRecorders.set(sourceFile, recorder);
|
||||||
|
return recorder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "initial-navigation",
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||||
|
visibility = [
|
||||||
|
"//packages/core/schematics:__pkg__",
|
||||||
|
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation/google3:__pkg__",
|
||||||
|
"//packages/core/schematics/test:__pkg__",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//packages/core/schematics/utils",
|
||||||
|
"@npm//@angular-devkit/schematics",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,43 @@
|
||||||
|
## initialNavigation migration
|
||||||
|
|
||||||
|
Automatically migrates the `initialNavigation` property of the `RouterModule` to the newly
|
||||||
|
available options: `enabledNonBlocking` (default), `enabledBlocking`, and `disabled`.
|
||||||
|
|
||||||
|
#### Before
|
||||||
|
```ts
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot(ROUTES, {initialNavigation: 'legacy_disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After
|
||||||
|
```ts
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot(ROUTES, {initialNavigation: 'disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disclaimer
|
||||||
|
|
||||||
|
The migration only covers the most common patterns where developers set the `ExtraOptions#InitialNavigation`
|
||||||
|
option to an outdated value. Therefore, if a user declares the option using a number of other methods,
|
||||||
|
e.g. shorthand notation, variable declaration, or some other crafty method, they will have to migrate
|
||||||
|
those options by hand. Otherwise, the compiler will error if the types are sufficiently enforced.
|
||||||
|
|
||||||
|
The basic migration strategy is as follows:
|
||||||
|
* `legacy_disabled` || `false` => `disabled`
|
||||||
|
* `legacy_enabled` || `true` => `enabledNonBlocking` (new default)
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* @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 {isExtraOptions, isRouterModuleForRoot} from './util';
|
||||||
|
|
||||||
|
|
||||||
|
/** The property name for the options that need to be migrated */
|
||||||
|
const INITIAL_NAVIGATION = 'initialNavigation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor that walks through specified TypeScript nodes and collects all
|
||||||
|
* found ExtraOptions#InitialNavigation assignments.
|
||||||
|
*/
|
||||||
|
export class InitialNavigationCollector {
|
||||||
|
public assignments: Set<ts.PropertyAssignment> = new Set();
|
||||||
|
|
||||||
|
constructor(private readonly typeChecker: ts.TypeChecker) {}
|
||||||
|
|
||||||
|
visitNode(node: ts.Node) {
|
||||||
|
let extraOptionsLiteral: ts.ObjectLiteralExpression|null = null;
|
||||||
|
if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) {
|
||||||
|
if (node.arguments.length === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isObjectLiteralExpression(node.arguments[1])) {
|
||||||
|
extraOptionsLiteral = node.arguments[1] as ts.ObjectLiteralExpression;
|
||||||
|
} else if (ts.isIdentifier(node.arguments[1])) {
|
||||||
|
extraOptionsLiteral =
|
||||||
|
this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier);
|
||||||
|
}
|
||||||
|
} else if (ts.isVariableDeclaration(node)) {
|
||||||
|
extraOptionsLiteral = this.getLiteralNeedingMigration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraOptionsLiteral !== null) {
|
||||||
|
this.visitExtraOptionsLiteral(extraOptionsLiteral);
|
||||||
|
} else {
|
||||||
|
// no match found, continue iteration
|
||||||
|
ts.forEachChild(node, n => this.visitNode(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitExtraOptionsLiteral(extraOptionsLiteral: ts.ObjectLiteralExpression) {
|
||||||
|
for (const prop of extraOptionsLiteral.properties) {
|
||||||
|
if (ts.isPropertyAssignment(prop) &&
|
||||||
|
(ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name))) {
|
||||||
|
if (prop.name.text === INITIAL_NAVIGATION && isValidInitialNavigationValue(prop)) {
|
||||||
|
this.assignments.add(prop);
|
||||||
|
}
|
||||||
|
} else if (ts.isSpreadAssignment(prop) && ts.isIdentifier(prop.expression)) {
|
||||||
|
const literalFromSpreadAssignment =
|
||||||
|
this.getLiteralNeedingMigrationFromIdentifier(prop.expression);
|
||||||
|
if (literalFromSpreadAssignment !== null) {
|
||||||
|
this.visitExtraOptionsLiteral(literalFromSpreadAssignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLiteralNeedingMigrationFromIdentifier(id: ts.Identifier): ts.ObjectLiteralExpression
|
||||||
|
|null {
|
||||||
|
const symbolForIdentifier = this.typeChecker.getSymbolAtLocation(id);
|
||||||
|
if (symbolForIdentifier === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbolForIdentifier.declarations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const declarationNode = symbolForIdentifier.declarations[0];
|
||||||
|
if (!ts.isVariableDeclaration(declarationNode) || declarationNode.initializer === undefined ||
|
||||||
|
!ts.isObjectLiteralExpression(declarationNode.initializer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return declarationNode.initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLiteralNeedingMigration(node: ts.VariableDeclaration): ts.ObjectLiteralExpression
|
||||||
|
|null {
|
||||||
|
if (node.initializer === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declaration could be `x: ExtraOptions = {}` or `x = {} as ExtraOptions`
|
||||||
|
if (ts.isAsExpression(node.initializer) &&
|
||||||
|
ts.isObjectLiteralExpression(node.initializer.expression) &&
|
||||||
|
isExtraOptions(this.typeChecker, node.initializer.type)) {
|
||||||
|
return node.initializer.expression;
|
||||||
|
} else if (
|
||||||
|
node.type !== undefined && ts.isObjectLiteralExpression(node.initializer) &&
|
||||||
|
isExtraOptions(this.typeChecker, node.type)) {
|
||||||
|
return node.initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the value assigned to an `initialNavigation` assignment
|
||||||
|
* conforms to the expected types for ExtraOptions#InitialNavigation
|
||||||
|
* @param node the property assignment to check
|
||||||
|
*/
|
||||||
|
function isValidInitialNavigationValue(node: ts.PropertyAssignment): boolean {
|
||||||
|
return ts.isStringLiteralLike(node.initializer) ||
|
||||||
|
node.initializer.kind === ts.SyntaxKind.FalseKeyword ||
|
||||||
|
node.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "google3",
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||||
|
visibility = ["//packages/core/schematics/migrations/google3:__pkg__"],
|
||||||
|
deps = [
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation",
|
||||||
|
"@npm//tslint",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* @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} from 'tslint';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {UpdateRecorder} from '../update_recorder';
|
||||||
|
|
||||||
|
export class TslintUpdateRecorder implements UpdateRecorder {
|
||||||
|
failures: RuleFailure[] = [];
|
||||||
|
|
||||||
|
constructor(private ruleName: string, private sourceFile: ts.SourceFile) {}
|
||||||
|
|
||||||
|
updateNode(node: ts.Node, newText: string): void {
|
||||||
|
this.failures.push(new RuleFailure(
|
||||||
|
this.sourceFile, node.getStart(), node.getEnd(), `Node needs to be updated to: ${newText}`,
|
||||||
|
this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), newText)));
|
||||||
|
}
|
||||||
|
|
||||||
|
commitUpdate() {}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* @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 {InitialNavigationCollector} from './collector';
|
||||||
|
import {InitialNavigationTransform} from './transform';
|
||||||
|
import {UpdateRecorder} from './update_recorder';
|
||||||
|
|
||||||
|
/** Entry point for the v10 "initialNavigation RouterModule options" schematic. */
|
||||||
|
export default function(): Rule {
|
||||||
|
return (tree: Tree) => {
|
||||||
|
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
|
||||||
|
const basePath = process.cwd();
|
||||||
|
|
||||||
|
if (!buildPaths.length && !testPaths.length) {
|
||||||
|
throw new SchematicsException(
|
||||||
|
'Could not find any tsconfig file. Cannot update the "initialNavigation" option for RouterModule');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
|
||||||
|
runInitialNavigationMigration(tree, tsconfigPath, basePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInitialNavigationMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||||
|
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const initialNavigationCollector = new InitialNavigationCollector(typeChecker);
|
||||||
|
const sourceFiles = program.getSourceFiles().filter(
|
||||||
|
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
|
||||||
|
|
||||||
|
// Analyze source files by detecting all modules.
|
||||||
|
sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile));
|
||||||
|
|
||||||
|
const {assignments} = initialNavigationCollector;
|
||||||
|
const transformer = new InitialNavigationTransform(typeChecker, getUpdateRecorder);
|
||||||
|
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||||
|
transformer.migrateInitialNavigationAssignments(Array.from(assignments));
|
||||||
|
|
||||||
|
// Walk through each update recorder and commit the update. We need to commit the
|
||||||
|
// updates in batches per source file as there can be only one recorder per source
|
||||||
|
// file in order to avoid shift character offsets.
|
||||||
|
updateRecorders.forEach(recorder => recorder.commitUpdate());
|
||||||
|
|
||||||
|
/** Gets the update recorder for the specified source file. */
|
||||||
|
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||||
|
if (updateRecorders.has(sourceFile)) {
|
||||||
|
return updateRecorders.get(sourceFile)!;
|
||||||
|
}
|
||||||
|
const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||||
|
const recorder: UpdateRecorder = {
|
||||||
|
updateNode(node: ts.Node, newText: string) {
|
||||||
|
treeRecorder.remove(node.getStart(), node.getWidth());
|
||||||
|
treeRecorder.insertRight(node.getStart(), newText);
|
||||||
|
},
|
||||||
|
commitUpdate() {
|
||||||
|
tree.commitUpdate(treeRecorder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateRecorders.set(sourceFile, recorder);
|
||||||
|
return recorder;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* @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 {UpdateRecorder} from './update_recorder';
|
||||||
|
|
||||||
|
|
||||||
|
export class InitialNavigationTransform {
|
||||||
|
private printer = ts.createPrinter();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private typeChecker: ts.TypeChecker,
|
||||||
|
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {}
|
||||||
|
|
||||||
|
/** Migrate the ExtraOptions#InitialNavigation property assignments. */
|
||||||
|
migrateInitialNavigationAssignments(literals: ts.PropertyAssignment[]) {
|
||||||
|
literals.forEach(l => this.migrateAssignment(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Migrate an ExtraOptions#InitialNavigation expression to use the new options format. */
|
||||||
|
migrateAssignment(assignment: ts.PropertyAssignment) {
|
||||||
|
const newInitializer = getUpdatedInitialNavigationValue(assignment.initializer);
|
||||||
|
if (newInitializer) {
|
||||||
|
const newAssignment =
|
||||||
|
ts.updatePropertyAssignment(assignment, assignment.name, newInitializer);
|
||||||
|
this._updateNode(assignment, newAssignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateNode(node: ts.Node, newNode: ts.Node) {
|
||||||
|
const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
|
||||||
|
const recorder = this.getUpdateRecorder(node.getSourceFile());
|
||||||
|
recorder.updateNode(node, newText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the deprecated initialNavigation options to their v10 equivalents
|
||||||
|
* (or as close as we can get).
|
||||||
|
* @param initializer the old initializer to update
|
||||||
|
*/
|
||||||
|
function getUpdatedInitialNavigationValue(initializer: ts.Expression): ts.Expression|null {
|
||||||
|
const oldText: string|boolean = ts.isStringLiteralLike(initializer) ?
|
||||||
|
initializer.text :
|
||||||
|
initializer.kind === ts.SyntaxKind.TrueKeyword;
|
||||||
|
let newText: string|undefined;
|
||||||
|
switch (oldText) {
|
||||||
|
case false:
|
||||||
|
case 'legacy_disabled':
|
||||||
|
newText = 'disabled';
|
||||||
|
break;
|
||||||
|
case true:
|
||||||
|
case 'legacy_enabled':
|
||||||
|
newText = 'enabledNonBlocking';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!newText ? ts.createIdentifier(`'${newText}'`) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the value assigned to an `initialNavigation` assignment
|
||||||
|
* conforms to the expected types for ExtraOptions#InitialNavigation
|
||||||
|
* @param node the property assignment to check
|
||||||
|
*/
|
||||||
|
function isValidInitialNavigationValue(node: ts.PropertyAssignment): boolean {
|
||||||
|
return ts.isStringLiteralLike(node.initializer) ||
|
||||||
|
node.initializer.kind === ts.SyntaxKind.FalseKeyword ||
|
||||||
|
node.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update recorder interface that is used to transform source files in a non-colliding
|
||||||
|
* way. Also this indirection makes it possible to re-use logic for both TSLint rules
|
||||||
|
* and CLI devkit schematic updates.
|
||||||
|
*/
|
||||||
|
export interface UpdateRecorder {
|
||||||
|
updateNode(node: ts.Node, newText: string): void;
|
||||||
|
commitUpdate(): void;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* @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 {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||||
|
|
||||||
|
/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */
|
||||||
|
export function isRouterModuleForRoot(
|
||||||
|
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression {
|
||||||
|
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
||||||
|
!ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'forRoot') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const imp = getImportOfIdentifier(typeChecker, node.expression.expression);
|
||||||
|
return !!imp && imp.name === 'RouterModule' && imp.importModule === '@angular/router' &&
|
||||||
|
!node.typeArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExtraOptions(
|
||||||
|
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode {
|
||||||
|
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imp = getImportOfIdentifier(typeChecker, node.typeName);
|
||||||
|
return imp !== null && imp.name === 'ExtraOptions' && imp.importModule === '@angular/router' &&
|
||||||
|
!node.typeArguments;
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ export interface AnalysisFailure {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TODO_COMMENT = 'TODO: The following node requires a generic type for `ModuleWithProviders';
|
const TODO_COMMENT = 'TODO: The following node requires a generic type for `ModuleWithProviders`';
|
||||||
|
|
||||||
export class ModuleWithProvidersTransform {
|
export class ModuleWithProvidersTransform {
|
||||||
private printer = ts.createPrinter();
|
private printer = ts.createPrinter();
|
||||||
|
|
|
@ -10,6 +10,7 @@ ts_library(
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/core/schematics/migrations/abstract-control-parent",
|
"//packages/core/schematics/migrations/abstract-control-parent",
|
||||||
"//packages/core/schematics/migrations/dynamic-queries",
|
"//packages/core/schematics/migrations/dynamic-queries",
|
||||||
|
"//packages/core/schematics/migrations/initial-navigation",
|
||||||
"//packages/core/schematics/migrations/missing-injectable",
|
"//packages/core/schematics/migrations/missing-injectable",
|
||||||
"//packages/core/schematics/migrations/module-with-providers",
|
"//packages/core/schematics/migrations/module-with-providers",
|
||||||
"//packages/core/schematics/migrations/move-document",
|
"//packages/core/schematics/migrations/move-document",
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
/**
|
||||||
|
* @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 initial navigation tslint rule', () => {
|
||||||
|
const rulesDirectory = dirname(require.resolve('../../migrations/google3/initialNavigationRule'));
|
||||||
|
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test');
|
||||||
|
shx.mkdir('-p', tmpDir);
|
||||||
|
|
||||||
|
writeFile('tsconfig.json', JSON.stringify({compilerOptions: {module: 'es2015'}}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => shx.rm('-r', tmpDir));
|
||||||
|
|
||||||
|
function runTSLint(fix = true) {
|
||||||
|
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
|
||||||
|
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
|
||||||
|
const config = Configuration.parseConfigFile({rules: {'initial-navigation': 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should migrate legacy_disabled to disabled', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
|
||||||
|
runTSLint();
|
||||||
|
|
||||||
|
expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should migrate false to disabled`, () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: false}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint();
|
||||||
|
|
||||||
|
expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate legacy_enabled to enabledNonBlocking', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_enabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
|
||||||
|
expect(getFile('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should migrate true to enabledNonBlocking`, () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
mport { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: true}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
|
||||||
|
expect(getFile('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_disabled', ...options}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(getFile('/index.ts')).toContain(`{initialNavigation: 'disabled', ...options}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects mixed validity', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'disabled', ...options}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects opposite order', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {...options, initialNavigation: 'legacy_disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(getFile('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects mixed validity opposite order', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {...options, initialNavigation: 'disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(getFile('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,239 @@
|
||||||
|
/**
|
||||||
|
* @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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
|
||||||
|
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||||
|
import {HostTree} from '@angular-devkit/schematics';
|
||||||
|
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||||
|
import * as shx from 'shelljs';
|
||||||
|
|
||||||
|
describe('initial navigation migration', () => {
|
||||||
|
let runner: SchematicTestRunner;
|
||||||
|
let host: TempScopedNodeJsSyncHost;
|
||||||
|
let tree: UnitTestTree;
|
||||||
|
let tmpDirPath: string;
|
||||||
|
let previousWorkingDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
|
||||||
|
host = new TempScopedNodeJsSyncHost();
|
||||||
|
tree = new UnitTestTree(new HostTree(host));
|
||||||
|
|
||||||
|
writeFile('/tsconfig.json', JSON.stringify({
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ['es2015'],
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
writeFile('/angular.json', JSON.stringify({
|
||||||
|
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
|
||||||
|
}));
|
||||||
|
|
||||||
|
previousWorkingDir = shx.pwd();
|
||||||
|
tmpDirPath = getSystemPath(host.root);
|
||||||
|
|
||||||
|
// Switch into the temporary directory path. This allows us to run
|
||||||
|
// the schematic against our custom unit test tree.
|
||||||
|
shx.cd(tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
shx.cd(previousWorkingDir);
|
||||||
|
shx.rm('-r', tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate legacy_disabled to disabled', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate false to disabled', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: false}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate legacy_enabled to enabledNonBlocking', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_enabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate true to enabledNonBlocking', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: true}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'enabledNonBlocking'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'legacy_disabled', ...options}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{initialNavigation: 'disabled', ...options}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects mixed validity', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {initialNavigation: 'disabled', ...options}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should migrate nested objects opposite order', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {...options, initialNavigation: 'legacy_disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`{...options, initialNavigation: 'disabled'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should migrate nested objects mixed validity opposite order', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([], {...options, initialNavigation: 'disabled'}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'enabledNonBlocking'};`);
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`disabled`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not migrate variable not used in forRoot', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const options = {initialNavigation: 'legacy_enabled'};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forRoot([]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const options = {initialNavigation: 'legacy_enabled'};`);
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`RouterModule.forRoot([])`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeFile(filePath: string, contents: string) {
|
||||||
|
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigration() {
|
||||||
|
return runner.runSchematicAsync('migration-v11-router-initial-navigation-options', {}, tree)
|
||||||
|
.toPromise();
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue