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
This commit is contained in:
parent
4baabf9cd3
commit
15ea811f05
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ts.SourceFile, TslintUpdateRecorder>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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 {
|
||||
}
|
||||
```
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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() {}
|
||||
}
|
|
@ -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<ts.SourceFile, UpdateRecorder>();
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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" }),`);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue