From 15ea811f051826f9bdcd8cb42e368d8b360f193a Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Fri, 5 Jun 2020 19:40:13 -0700 Subject: [PATCH] feat(router): Add `relativeLinkResolution` migration to update default value (#38698) The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'. This migration updates `RouterModule` configurations that use the default value to now specifically use 'legacy' to prevent breakages when updating. PR Close #38698 --- packages/core/schematics/BUILD.bazel | 1 + packages/core/schematics/migrations.json | 5 + .../schematics/migrations/google3/BUILD.bazel | 2 + .../relativeLinkResolutionDefaultRule.ts | 48 +++++ .../relative-link-resolution/BUILD.bazel | 23 ++ .../relative-link-resolution/README.md | 33 +++ .../relative-link-resolution/collector.ts | 87 ++++++++ .../google3/BUILD.bazel | 13 ++ .../google3/tslint_update_recorder.ts | 28 +++ .../relative-link-resolution/index.ts | 74 +++++++ .../relative-link-resolution/transform.ts | 64 ++++++ .../update_recorder.ts | 19 ++ .../relative-link-resolution/util.ts | 33 +++ packages/core/schematics/test/BUILD.bazel | 1 + .../relative_link_resolution_default_spec.ts | 202 ++++++++++++++++++ .../test/relative_link_resolution_spec.ts | 188 ++++++++++++++++ 16 files changed, 821 insertions(+) create mode 100644 packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel create mode 100644 packages/core/schematics/migrations/relative-link-resolution/README.md create mode 100644 packages/core/schematics/migrations/relative-link-resolution/collector.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel create mode 100644 packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/index.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/transform.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts create mode 100644 packages/core/schematics/migrations/relative-link-resolution/util.ts create mode 100644 packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts create mode 100644 packages/core/schematics/test/relative_link_resolution_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 4595724e09..375fcd5920 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -15,6 +15,7 @@ pkg_npm( "//packages/core/schematics/migrations/module-with-providers", "//packages/core/schematics/migrations/move-document", "//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/static-queries", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index ce899486eb..b4d86667f6 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -49,6 +49,11 @@ "version": "11.0.0-beta", "description": "NavigationExtras omissions migration. In version 11, some unsupported properties were omitted from the `extras` parameter of the `Router.navigateByUrl` and `Router.createUrlTree` methods.", "factory": "./migrations/navigation-extras-omissions/index" + }, + "migration-v11-router-relative-link-resolution-default": { + "version": "11.0.0-beta", + "description": "The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.\nThis migration updates `RouterModule` configurations that use the default value to \nnow specifically use 'legacy' to prevent breakages when updating.", + "factory": "./migrations/relative-link-resolution/index" } } } diff --git a/packages/core/schematics/migrations/google3/BUILD.bazel b/packages/core/schematics/migrations/google3/BUILD.bazel index 6093886f1d..217e7bc90d 100644 --- a/packages/core/schematics/migrations/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/google3/BUILD.bazel @@ -10,6 +10,8 @@ ts_library( "//packages/core/schematics/migrations/missing-injectable", "//packages/core/schematics/migrations/missing-injectable/google3", "//packages/core/schematics/migrations/navigation-extras-omissions", + "//packages/core/schematics/migrations/relative-link-resolution", + "//packages/core/schematics/migrations/relative-link-resolution/google3", "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts b/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts new file mode 100644 index 0000000000..3725981830 --- /dev/null +++ b/packages/core/schematics/migrations/google3/relativeLinkResolutionDefaultRule.ts @@ -0,0 +1,48 @@ +/** + * @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 {RelativeLinkResolutionCollector} from '../relative-link-resolution/collector'; +import {TslintUpdateRecorder} from '../relative-link-resolution/google3/tslint_update_recorder'; +import {RelativeLinkResolutionTransform} from '../relative-link-resolution/transform'; + +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + const typeChecker = program.getTypeChecker(); + const ruleName = this.ruleName; + const sourceFiles = program.getSourceFiles().filter( + s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s)); + const updateRecorders = new Map(); + const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker); + + // Analyze source files by detecting all modules. + sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile)); + + const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector; + const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder); + transformer.migrateRouterModuleForRootCalls(forRootCalls); + transformer.migrateObjectLiterals(extraOptionsLiterals); + + if (updateRecorders.has(sourceFile)) { + return updateRecorders.get(sourceFile)!.failures; + } + return []; + + /** 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; + } + } +} diff --git a/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel b/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel new file mode 100644 index 0000000000..baed6ddb5a --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/BUILD.bazel @@ -0,0 +1,23 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "relative-link-resolution", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/migrations/relative-link-resolution/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], + deps = [ + "//packages/compiler-cli/src/ngtsc/annotations", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/core/schematics/utils", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/relative-link-resolution/README.md b/packages/core/schematics/migrations/relative-link-resolution/README.md new file mode 100644 index 0000000000..3d99628992 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/README.md @@ -0,0 +1,33 @@ +## relativeLinkResolution migration + +The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'. +This migration updates `RouterModule` configurations that use the default value to +now specifically use 'legacy' to prevent breakages when updating. + +#### Before +```ts +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [ + RouterModule.forRoot(ROUTES), + ] +}) +export class AppModule { +} +``` + +#### After +```ts +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [ + RouterModule.forRoot(ROUTES, {relativeLinkResolution: 'legacy'}), + ] +}) +export class AppModule { +} +``` \ No newline at end of file diff --git a/packages/core/schematics/migrations/relative-link-resolution/collector.ts b/packages/core/schematics/migrations/relative-link-resolution/collector.ts new file mode 100644 index 0000000000..ee62dbc4a0 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/collector.ts @@ -0,0 +1,87 @@ +/** + * @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'; + + +/** + * Visitor that walks through specified TypeScript nodes and collects all + * found ExtraOptions#RelativeLinkResolution assignments. + */ +export class RelativeLinkResolutionCollector { + readonly forRootCalls: ts.CallExpression[] = []; + readonly extraOptionsLiterals: ts.ObjectLiteralExpression[] = []; + + constructor(private readonly typeChecker: ts.TypeChecker) {} + + visitNode(node: ts.Node) { + let forRootCall: ts.CallExpression|null = null; + let literal: ts.ObjectLiteralExpression|null = null; + if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) { + if (node.arguments.length === 1) { + forRootCall = node; + } else if (ts.isObjectLiteralExpression(node.arguments[1])) { + literal = node.arguments[1] as ts.ObjectLiteralExpression; + } else if (ts.isIdentifier(node.arguments[1])) { + literal = this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier); + } + } else if (ts.isVariableDeclaration(node)) { + literal = this.getLiteralNeedingMigration(node); + } + + if (literal !== null) { + this.extraOptionsLiterals.push(literal); + } else if (forRootCall !== null) { + this.forRootCalls.push(forRootCall); + } else { + // no match found, continue iteration + ts.forEachChild(node, n => this.visitNode(n)); + } + } + + 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; + } +} diff --git a/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel b/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel new file mode 100644 index 0000000000..1a2d4e6b78 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/google3/BUILD.bazel @@ -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/relative-link-resolution", + "@npm//tslint", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts b/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts new file mode 100644 index 0000000000..f36e06d5b3 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/google3/tslint_update_recorder.ts @@ -0,0 +1,28 @@ +/** + * @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) { + this.failures.unshift(new RuleFailure( + this.sourceFile, node.getStart(), 0, + 'The relativeLinkResolution default is changing from `legacy` to `corrected`. To keep behavior consistent' + + ' when the change is merged, specify `legacy` rather than using the default.', + this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), `${newText}`))); + } + + commitUpdate() {} +} diff --git a/packages/core/schematics/migrations/relative-link-resolution/index.ts b/packages/core/schematics/migrations/relative-link-resolution/index.ts new file mode 100644 index 0000000000..6c2f672c9e --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/index.ts @@ -0,0 +1,74 @@ +/** + * @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 {RelativeLinkResolutionCollector} from './collector'; +import {RelativeLinkResolutionTransform} from './transform'; +import {UpdateRecorder} from './update_recorder'; + +/** Entry point for the v11 "relativeLinkResolution 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 "relativeLinkResolution" option for RouterModule'); + } + + for (const tsconfigPath of [...buildPaths, ...testPaths]) { + runRelativeLinkResolutionMigration(tree, tsconfigPath, basePath); + } + }; +} + +function runRelativeLinkResolutionMigration(tree: Tree, tsconfigPath: string, basePath: string) { + const {program} = createMigrationProgram(tree, tsconfigPath, basePath); + const typeChecker = program.getTypeChecker(); + const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker); + const sourceFiles = program.getSourceFiles().filter( + f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); + + // Analyze source files by detecting all modules. + sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile)); + + const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector; + const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder); + const updateRecorders = new Map(); + transformer.migrateRouterModuleForRootCalls(forRootCalls); + transformer.migrateObjectLiterals(extraOptionsLiterals); + + // 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; + } +} diff --git a/packages/core/schematics/migrations/relative-link-resolution/transform.ts b/packages/core/schematics/migrations/relative-link-resolution/transform.ts new file mode 100644 index 0000000000..22798080ef --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/transform.ts @@ -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 * as ts from 'typescript'; + +import {UpdateRecorder} from './update_recorder'; + + +const RELATIVE_LINK_RESOLUTION = 'relativeLinkResolution'; + +export class RelativeLinkResolutionTransform { + private printer = ts.createPrinter(); + + constructor(private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {} + + /** Migrate the ExtraOptions#RelativeLinkResolution property assignments. */ + migrateRouterModuleForRootCalls(calls: ts.CallExpression[]) { + calls.forEach(c => { + this._updateCallExpressionWithoutExtraOptions(c); + }); + } + + migrateObjectLiterals(vars: ts.ObjectLiteralExpression[]) { + vars.forEach(v => this._maybeUpdateLiteral(v)); + } + + private _updateCallExpressionWithoutExtraOptions(callExpression: ts.CallExpression) { + const args = callExpression.arguments; + const emptyLiteral = ts.createObjectLiteral(); + const newNode = ts.updateCall( + callExpression, callExpression.expression, callExpression.typeArguments, + [args[0], this._getMigratedLiteralExpression(emptyLiteral)]); + this._updateNode(callExpression, newNode); + } + + private _getMigratedLiteralExpression(literal: ts.ObjectLiteralExpression) { + if (literal.properties.some( + prop => ts.isPropertyAssignment(prop) && + prop.name.getText() === RELATIVE_LINK_RESOLUTION)) { + // literal already defines a value for relativeLinkResolution. Skip it + return literal; + } + const legacyExpression = + ts.createPropertyAssignment(RELATIVE_LINK_RESOLUTION, ts.createStringLiteral('legacy')); + return ts.updateObjectLiteral(literal, [...literal.properties, legacyExpression]); + } + + private _maybeUpdateLiteral(literal: ts.ObjectLiteralExpression) { + const updatedLiteral = this._getMigratedLiteralExpression(literal); + if (updatedLiteral !== literal) { + this._updateNode(literal, updatedLiteral); + } + } + + 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); + } +} diff --git a/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts b/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts new file mode 100644 index 0000000000..6796262346 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/update_recorder.ts @@ -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; +} \ No newline at end of file diff --git a/packages/core/schematics/migrations/relative-link-resolution/util.ts b/packages/core/schematics/migrations/relative-link-resolution/util.ts new file mode 100644 index 0000000000..d8a7c56562 --- /dev/null +++ b/packages/core/schematics/migrations/relative-link-resolution/util.ts @@ -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; +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index 2030fef2d6..fc8cdb52e2 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages/core/schematics/migrations/module-with-providers", "//packages/core/schematics/migrations/move-document", "//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/static-queries", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts b/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts new file mode 100644 index 0000000000..c2f1202e9e --- /dev/null +++ b/packages/core/schematics/test/google3/relative_link_resolution_default_spec.ts @@ -0,0 +1,202 @@ +/** + * @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 relativeLinkResolution TSLint rule', () => { + const rulesDirectory = + dirname(require.resolve('../../migrations/google3/relativeLinkResolutionDefaultRule')); + + 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: boolean) { + const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); + const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); + const config = Configuration.parseConfigFile({ + rules: {'relative-link-resolution-default': 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 flag forRoot with no options', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([]), + ] + }) + export class AppModule { + } + `); + + const linter = runTSLint(false); + const failures = linter.getResult().failures.map(failure => failure.getFailure()); + + expect(failures.length).toBe(1); + expect(failures[0]) + .toBe( + 'The relativeLinkResolution default is changing from `legacy` to `corrected`. To keep' + + ' behavior consistent when the change is merged, specify `legacy` rather than using the default.'); + }); + + it('should migrate forRoot with no options', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([]), + ] + }) + export class AppModule { + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`RouterModule.forRoot([], { relativeLinkResolution: "legacy" })`); + }); + + it('should migrate options without relativeLinkResolution', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([], {useHash: true}), + ] + }) + export class AppModule { + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`RouterModule.forRoot([], { useHash: true, relativeLinkResolution: "legacy" })`); + }); + + it('should not migrate options containing relativeLinkResolution', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([], {relativeLinkResolution: 'corrected'}), + ] + }) + export class AppModule { + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`RouterModule.forRoot([], {relativeLinkResolution: 'corrected'})`); + }); + + it('should migrate when options is a variable with AsExpression', () => { + writeFile('/index.ts', ` + import { ExtraOptions } from '@angular/router'; + const options = {useHash: true} as ExtraOptions; + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain( + `const options = { useHash: true, relativeLinkResolution: "legacy" } as ExtraOptions;`); + }); + + it('should migrate when options is a variable', () => { + writeFile('/index.ts', ` + import { ExtraOptions } from '@angular/router'; + const options: ExtraOptions = {useHash: true}; + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain( + `const options: ExtraOptions = { useHash: true, relativeLinkResolution: "legacy" };`); + }); + + it('should migrate when options is a variable with no type', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { ExtraOptions, RouterModule } from '@angular/router'; + + const options = {useHash: true}; + + @NgModule({ + imports: [ + RouterModule.forRoot([], options), + ] + }) + export class AppModule { + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`const options = { useHash: true, relativeLinkResolution: "legacy" };`); + expect(getFile('/index.ts')).toContain(`RouterModule.forRoot([], options)`); + }); + + it('should migrate when aliased options is a variable', () => { + writeFile('/index.ts', ` + import { ExtraOptions as RouterExtraOptions } from '@angular/router'; + const options: RouterExtraOptions = {useHash: true}; + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain( + `const options: RouterExtraOptions = { useHash: true, relativeLinkResolution: "legacy" };`); + }); + + it('should migrate aliased RouterModule.forRoot', () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule as AngularRouterModule} from '@angular/router'; + @NgModule({ + imports: [ + AngularRouterModule.forRoot([]), + ] + }) + export class AppModule { + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`AngularRouterModule.forRoot([], { relativeLinkResolution: "legacy" }),`); + }); +}); diff --git a/packages/core/schematics/test/relative_link_resolution_spec.ts b/packages/core/schematics/test/relative_link_resolution_spec.ts new file mode 100644 index 0000000000..14d90303ef --- /dev/null +++ b/packages/core/schematics/test/relative_link_resolution_spec.ts @@ -0,0 +1,188 @@ +/** + * @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 forRoot with no options', async () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([]), + ] + }) + export class AppModule { + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`RouterModule.forRoot([], { relativeLinkResolution: "legacy" })`); + }); + + it('should migrate options without relativeLinkResolution', async () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([], {useHash: true}), + ] + }) + export class AppModule { + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`RouterModule.forRoot([], { useHash: true, relativeLinkResolution: "legacy" })`); + }); + + it('should not migrate options containing relativeLinkResolution', async () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forRoot([], {relativeLinkResolution: 'corrected'}), + ] + }) + export class AppModule { + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`RouterModule.forRoot([], {relativeLinkResolution: 'corrected'})`); + }); + + it('should migrate when options is a variable with AsExpression', async () => { + writeFile('/index.ts', ` + import { ExtraOptions } from '@angular/router'; + const options = {useHash: true} as ExtraOptions; + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain( + `const options = { useHash: true, relativeLinkResolution: "legacy" } as ExtraOptions;`); + }); + + it('should migrate when options is a variable', async () => { + writeFile('/index.ts', ` + import { ExtraOptions } from '@angular/router'; + const options: ExtraOptions = {useHash: true}; + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain( + `const options: ExtraOptions = { useHash: true, relativeLinkResolution: "legacy" };`); + }); + + it('should migrate when options is a variable with no type', async () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { ExtraOptions, RouterModule } from '@angular/router'; + + const options = {useHash: true}; + + @NgModule({ + imports: [ + RouterModule.forRoot([], options), + ] + }) + export class AppModule { + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`const options = { useHash: true, relativeLinkResolution: "legacy" };`); + expect(tree.readContent('/index.ts')).toContain(`RouterModule.forRoot([], options)`); + }); + + it('should migrate when aliased options is a variable', async () => { + writeFile('/index.ts', ` + import { ExtraOptions as RouterExtraOptions } from '@angular/router'; + const options: RouterExtraOptions = {useHash: true}; + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain( + `const options: RouterExtraOptions = { useHash: true, relativeLinkResolution: "legacy" };`); + }); + + it('should migrate aliased RouterModule.forRoot', async () => { + writeFile('/index.ts', ` + import { NgModule } from '@angular/core'; + import { RouterModule as AngularRouterModule} from '@angular/router'; + @NgModule({ + imports: [ + AngularRouterModule.forRoot([]), + ] + }) + export class AppModule { + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`AngularRouterModule.forRoot([], { relativeLinkResolution: "legacy" }),`); + }); + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner + .runSchematicAsync('migration-v11-router-relative-link-resolution-default', {}, tree) + .toPromise(); + } +});