feat(core): add migration for `XhrFactory` import (#41313)

Automatically migrates `XhrFactory` from `@angular/common/http` to `@angular/common`.

PR Close #41313
This commit is contained in:
Alan Agius 2021-03-23 18:05:48 +01:00 committed by Alex Rickabaugh
parent 300d6d1e38
commit 95ff5ecb23
9 changed files with 252 additions and 2 deletions

View File

@ -28,5 +28,6 @@ pkg_npm(
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
"//packages/core/schematics/migrations/undecorated-classes-with-di",
"//packages/core/schematics/migrations/wait-for-async",
"//packages/core/schematics/migrations/xhr-factory",
],
)

View File

@ -89,6 +89,11 @@
"version": "12.0.0-beta",
"description": "In Angular version 12, the type of ActivatedRouteSnapshot.fragment is nullable. This migration automatically adds non-null assertions to it.",
"factory": "./migrations/activated-route-snapshot-fragment/index"
},
"migration-v12-xhr-factory": {
"version": "12.0.0-next.6",
"description": "`XhrFactory` has been moved from `@angular/common/http` to `@angular/common`.",
"factory": "./migrations/xhr-factory/index"
}
}
}

View File

@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "xhr-factory",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__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,13 @@
## XhrFactory migration
Automatically migrates `XhrFactory` from `@angular/common/http` to `@angular/common`.
#### Before
```ts
import { XhrFactory } from '@angular/common/http';
```
#### After
```ts
import { XhrFactory } from '@angular/common';
```

View File

@ -0,0 +1,132 @@
/**
* @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 {DirEntry, Rule, UpdateRecorder} from '@angular-devkit/schematics';
import * as ts from 'typescript';
import {findImportSpecifier} from '../../utils/typescript/imports';
function* visit(directory: DirEntry): IterableIterator<ts.SourceFile> {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if (content.includes('XhrFactory')) {
const source = ts.createSourceFile(
entry.path,
content.toString().replace(/^\uFEFF/, ''),
ts.ScriptTarget.Latest,
true,
);
yield source;
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
export default function(): Rule {
return tree => {
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
for (const sourceFile of visit(tree.root)) {
let recorder: UpdateRecorder|undefined;
const allImportDeclarations =
sourceFile.statements.filter(n => ts.isImportDeclaration(n)) as ts.ImportDeclaration[];
if (allImportDeclarations.length === 0) {
continue;
}
const httpCommonImport = findImportDeclaration('@angular/common/http', allImportDeclarations);
if (!httpCommonImport) {
continue;
}
const commonHttpNamedBinding = getNamedImports(httpCommonImport);
if (commonHttpNamedBinding) {
const commonHttpNamedImports = commonHttpNamedBinding.elements;
const xhrFactorySpecifier = findImportSpecifier(commonHttpNamedImports, 'XhrFactory');
if (!xhrFactorySpecifier) {
continue;
}
recorder = tree.beginUpdate(sourceFile.fileName);
// Remove 'XhrFactory' from '@angular/common/http'
if (commonHttpNamedImports.length > 1) {
// Remove 'XhrFactory' named import
const index = commonHttpNamedBinding.getStart();
const length = commonHttpNamedBinding.getWidth();
const newImports = printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.updateNamedImports(
commonHttpNamedBinding,
commonHttpNamedBinding.elements.filter(e => e !== xhrFactorySpecifier)),
sourceFile);
recorder.remove(index, length).insertLeft(index, newImports);
} else {
// Remove '@angular/common/http' import
const index = httpCommonImport.getFullStart();
const length = httpCommonImport.getFullWidth();
recorder.remove(index, length);
}
// Import XhrFactory from @angular/common
const commonImport = findImportDeclaration('@angular/common', allImportDeclarations);
const commonNamedBinding = getNamedImports(commonImport);
if (commonNamedBinding) {
// Already has an import for '@angular/common', just add the named import.
const index = commonNamedBinding.getStart();
const length = commonNamedBinding.getWidth();
const newImports = printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.updateNamedImports(
commonNamedBinding, [...commonNamedBinding.elements, xhrFactorySpecifier]),
sourceFile);
recorder.remove(index, length).insertLeft(index, newImports);
} else {
// Add import to '@angular/common'
const index = httpCommonImport.getFullStart();
recorder.insertLeft(index, `\nimport { XhrFactory } from '@angular/common';`);
}
}
if (recorder) {
tree.commitUpdate(recorder);
}
}
};
}
function findImportDeclaration(moduleSpecifier: string, importDeclarations: ts.ImportDeclaration[]):
ts.ImportDeclaration|undefined {
return importDeclarations.find(
n => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === moduleSpecifier);
}
function getNamedImports(importDeclaration: ts.ImportDeclaration|undefined): ts.NamedImports|
undefined {
const namedBindings = importDeclaration?.importClause?.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
return namedBindings;
}
return undefined;
}

View File

@ -26,6 +26,7 @@ ts_library(
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
"//packages/core/schematics/migrations/undecorated-classes-with-di",
"//packages/core/schematics/migrations/wait-for-async",
"//packages/core/schematics/migrations/xhr-factory",
"//packages/core/schematics/utils",
"@npm//@angular-devkit/core",
"@npm//@angular-devkit/schematics",

View File

@ -0,0 +1,79 @@
/**
* @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 {tags} from '@angular-devkit/core';
import {EmptyTree} from '@angular-devkit/schematics';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
describe('XhrFactory migration', () => {
let tree: UnitTestTree;
const runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
});
it(`should replace 'XhrFactory' from '@angular/common/http' to '@angular/common'`, async () => {
tree.create('/index.ts', tags.stripIndents`
import { HttpClient } from '@angular/common';
import { HttpErrorResponse, HttpResponse, XhrFactory } from '@angular/common/http';
`);
await runMigration();
expect(tree.readContent('/index.ts')).toBe(tags.stripIndents`
import { HttpClient, XhrFactory } from '@angular/common';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
`);
});
it(`should replace import for 'XhrFactory' to '@angular/common'`, async () => {
tree.create('/index.ts', tags.stripIndents`
import { Injecable } from '@angular/core';
import { XhrFactory } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
`);
await runMigration();
expect(tree.readContent('/index.ts')).toBe(tags.stripIndents`
import { Injecable } from '@angular/core';
import { XhrFactory } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
`);
});
it(`should remove http import when 'XhrFactory' is the only imported symbol`, async () => {
tree.create('/index.ts', tags.stripIndents`
import { HttpClient } from '@angular/common';
import { XhrFactory as XhrFactory2 } from '@angular/common/http';
import { Injecable } from '@angular/core';
`);
await runMigration();
expect(tree.readContent('/index.ts')).toBe(tags.stripIndents`
import { HttpClient, XhrFactory as XhrFactory2 } from '@angular/common';
import { Injecable } from '@angular/core';
`);
});
it(`should add named import when '@angular/common' is a namespace import`, async () => {
tree.create('/index.ts', tags.stripIndents`
import * as common from '@angular/common';
import { XhrFactory } from '@angular/common/http';
`);
await runMigration();
expect(tree.readContent('/index.ts')).toBe(tags.stripIndents`
import * as common from '@angular/common';
import { XhrFactory } from '@angular/common';
`);
});
async function runMigration(): Promise<void> {
await runner.runSchematicAsync('migration-v12-xhr-factory', {}, tree).toPromise();
}
});

View File

@ -3,7 +3,9 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
"lib": ["es2015"],
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019"],
"types": ["jasmine"],
"baseUrl": ".",
"paths": {

View File

@ -110,7 +110,7 @@ export function replaceImport(
/** Finds an import specifier with a particular name. */
function findImportSpecifier(
export function findImportSpecifier(
nodes: ts.NodeArray<ts.ImportSpecifier>, specifierName: string): ts.ImportSpecifier|undefined {
return nodes.find(element => {
const {name, propertyName} = element;