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:
parent
300d6d1e38
commit
95ff5ecb23
@ -28,5 +28,6 @@ pkg_npm(
|
|||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||||
"//packages/core/schematics/migrations/wait-for-async",
|
"//packages/core/schematics/migrations/wait-for-async",
|
||||||
|
"//packages/core/schematics/migrations/xhr-factory",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -89,6 +89,11 @@
|
|||||||
"version": "12.0.0-beta",
|
"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.",
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
packages/core/schematics/migrations/xhr-factory/BUILD.bazel
Normal file
17
packages/core/schematics/migrations/xhr-factory/BUILD.bazel
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
13
packages/core/schematics/migrations/xhr-factory/README.md
Normal file
13
packages/core/schematics/migrations/xhr-factory/README.md
Normal 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';
|
||||||
|
```
|
132
packages/core/schematics/migrations/xhr-factory/index.ts
Normal file
132
packages/core/schematics/migrations/xhr-factory/index.ts
Normal 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;
|
||||||
|
}
|
@ -26,6 +26,7 @@ ts_library(
|
|||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||||
"//packages/core/schematics/migrations/wait-for-async",
|
"//packages/core/schematics/migrations/wait-for-async",
|
||||||
|
"//packages/core/schematics/migrations/xhr-factory",
|
||||||
"//packages/core/schematics/utils",
|
"//packages/core/schematics/utils",
|
||||||
"@npm//@angular-devkit/core",
|
"@npm//@angular-devkit/core",
|
||||||
"@npm//@angular-devkit/schematics",
|
"@npm//@angular-devkit/schematics",
|
||||||
|
79
packages/core/schematics/test/xhr_factory_spec.ts
Normal file
79
packages/core/schematics/test/xhr_factory_spec.ts
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
@ -3,7 +3,9 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": ["es2015"],
|
"moduleResolution": "node",
|
||||||
|
"target": "es2019",
|
||||||
|
"lib": ["es2019"],
|
||||||
"types": ["jasmine"],
|
"types": ["jasmine"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
@ -110,7 +110,7 @@ export function replaceImport(
|
|||||||
|
|
||||||
|
|
||||||
/** Finds an import specifier with a particular name. */
|
/** Finds an import specifier with a particular name. */
|
||||||
function findImportSpecifier(
|
export function findImportSpecifier(
|
||||||
nodes: ts.NodeArray<ts.ImportSpecifier>, specifierName: string): ts.ImportSpecifier|undefined {
|
nodes: ts.NodeArray<ts.ImportSpecifier>, specifierName: string): ts.ImportSpecifier|undefined {
|
||||||
return nodes.find(element => {
|
return nodes.find(element => {
|
||||||
const {name, propertyName} = element;
|
const {name, propertyName} = element;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user