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:
Adam Plumer 2019-10-12 23:25:58 -05:00 committed by atscott
parent c4becca0e4
commit 0ec7043490
17 changed files with 932 additions and 1 deletions

View File

@ -12,6 +12,7 @@ pkg_npm(
deps = [
"//packages/core/schematics/migrations/abstract-control-parent",
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/initial-navigation",
"//packages/core/schematics/migrations/missing-injectable",
"//packages/core/schematics/migrations/module-with-providers",
"//packages/core/schematics/migrations/move-document",

View File

@ -74,6 +74,11 @@
"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"
},
"migration-v11-router-initial-navigation-options": {
"version": "11.0.0-beta",
"description": "Updates the `initialNavigation` property for `RouterModule.forRoot`.",
"factory": "./migrations/initial-navigation/index"
}
}
}

View File

@ -7,6 +7,8 @@ ts_library(
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
deps = [
"//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/google3",
"//packages/core/schematics/migrations/navigation-extras-omissions",

View File

@ -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;
}
}
}

View File

@ -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",
],
)

View File

@ -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)

View File

@ -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;
}

View File

@ -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",
],
)

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -20,7 +20,7 @@ export interface AnalysisFailure {
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 {
private printer = ts.createPrinter();

View File

@ -10,6 +10,7 @@ ts_library(
deps = [
"//packages/core/schematics/migrations/abstract-control-parent",
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/initial-navigation",
"//packages/core/schematics/migrations/missing-injectable",
"//packages/core/schematics/migrations/module-with-providers",
"//packages/core/schematics/migrations/move-document",

View File

@ -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'}`);
});
});

View File

@ -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();
}
});