mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(core): Add a schematics to migrate provideHttpClient to keep using the HttpXhrBackend implementation.
Exisiting applications will be migrated to keep using the XHR backend to prevent any breaking changes. `withXhr()` is to the `provideHttpClient` provider function.
This commit is contained in:
parent
5c432fb8bb
commit
3bc095d508
9 changed files with 354 additions and 31 deletions
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* @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 {FetchBackend} from './fetch';
|
||||
|
||||
/**
|
||||
* A constant defining the default the default Http Backend.
|
||||
* Extracted to a separate file to facilitate G3 patches.
|
||||
*/
|
||||
export const NG_DEFAULT_HTTP_BACKEND = FetchBackend;
|
||||
|
|
@ -8,11 +8,10 @@
|
|||
|
||||
import {
|
||||
DestroyRef,
|
||||
ɵformatRuntimeError as formatRuntimeError,
|
||||
inject,
|
||||
Injectable,
|
||||
InjectionToken,
|
||||
NgZone,
|
||||
ɵformatRuntimeError as formatRuntimeError,
|
||||
} from '@angular/core';
|
||||
import {Observable, Observer} from 'rxjs';
|
||||
import {RuntimeErrorCode} from './errors';
|
||||
|
|
@ -35,14 +34,6 @@ import type {} from 'zone.js';
|
|||
|
||||
const XSSI_PREFIX = /^\)\]\}',?\n/;
|
||||
|
||||
/**
|
||||
* An internal injection token to reference `FetchBackend` implementation
|
||||
* in a tree-shakable way.
|
||||
*/
|
||||
export const FETCH_BACKEND = new InjectionToken<FetchBackend>(
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'FETCH_BACKEND' : '',
|
||||
);
|
||||
|
||||
/**
|
||||
* Uses `fetch` to send requests to a backend server.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ import {
|
|||
} from '@angular/core';
|
||||
|
||||
import {HttpBackend, HttpHandler, HttpInterceptorHandler} from './backend';
|
||||
import {NG_DEFAULT_HTTP_BACKEND} from './backend-default-value';
|
||||
import {HttpClient} from './client';
|
||||
import {FETCH_BACKEND, FetchBackend} from './fetch';
|
||||
import {FetchBackend} from './fetch';
|
||||
import {HTTP_INTERCEPTOR_FNS, HttpInterceptorFn, legacyInterceptorFnFactory} from './interceptor';
|
||||
import {
|
||||
jsonpCallbackContext,
|
||||
|
|
@ -27,7 +26,6 @@ import {
|
|||
} from './jsonp';
|
||||
import {HttpXhrBackend} from './xhr';
|
||||
import {XSRF_COOKIE_NAME, XSRF_ENABLED, XSRF_HEADER_NAME, xsrfInterceptorFn} from './xsrf';
|
||||
import {NG_DEFAULT_HTTP_BACKEND} from './backend-default-value';
|
||||
|
||||
/**
|
||||
* Identifies a particular kind of `HttpFeature`.
|
||||
|
|
@ -113,13 +111,13 @@ export function provideHttpClient(
|
|||
|
||||
const providers: Provider[] = [
|
||||
HttpClient,
|
||||
NG_DEFAULT_HTTP_BACKEND,
|
||||
FetchBackend,
|
||||
HttpInterceptorHandler,
|
||||
{provide: HttpHandler, useExisting: HttpInterceptorHandler},
|
||||
{
|
||||
provide: HttpBackend,
|
||||
useFactory: () => {
|
||||
return inject(FETCH_BACKEND, {optional: true}) ?? inject(NG_DEFAULT_HTTP_BACKEND);
|
||||
return inject(FetchBackend);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -298,7 +296,6 @@ export function withRequestsMadeViaParent(): HttpFeature<HttpFeatureKind.Request
|
|||
export function withFetch(): HttpFeature<HttpFeatureKind.Fetch> {
|
||||
return makeHttpFeature(HttpFeatureKind.Fetch, [
|
||||
FetchBackend,
|
||||
{provide: FETCH_BACKEND, useExisting: FetchBackend},
|
||||
{provide: HttpBackend, useExisting: FetchBackend},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ bundle_entrypoints = [
|
|||
"router-testing-module-migration",
|
||||
"packages/core/schematics/ng-generate/router-testing-module-migration/index.js",
|
||||
],
|
||||
[
|
||||
"http-xhr-backend",
|
||||
"packages/core/schematics/migrations/http-xhr-backend/index.js",
|
||||
],
|
||||
]
|
||||
|
||||
rollup.rollup(
|
||||
|
|
@ -133,6 +137,7 @@ rollup.rollup(
|
|||
"//:node_modules/semver",
|
||||
"//packages/core/schematics:tsconfig_build",
|
||||
"//packages/core/schematics/migrations/change-detection-eager",
|
||||
"//packages/core/schematics/migrations/http-xhr-backend",
|
||||
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
|
||||
"//packages/core/schematics/ng-generate/common-to-standalone-migration",
|
||||
"//packages/core/schematics/ng-generate/control-flow-migration",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
"version": "22.0.0",
|
||||
"description": "Adds `ChangeDetectionStrategy.Eager` to all components.",
|
||||
"factory": "./bundles/change-detection-eager.cjs#migrate"
|
||||
},
|
||||
"http-xhr-backend": {
|
||||
"version": "22.0.0",
|
||||
"description": "Adds 'withXhr' to 'provideHttpClient' function calls when the 'HttpXhrBackend' is used. For more information see: https://angular.dev/api/common/http/withXhr",
|
||||
"factory": "./bundles/http-xhr-backend.cjs#migrate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
load("//tools:defaults.bzl", "jasmine_test", "ts_project")
|
||||
|
||||
package(
|
||||
default_visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "http-xhr-backend",
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = ["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//:node_modules/@angular-devkit/schematics",
|
||||
"//:node_modules/typescript",
|
||||
"//packages/compiler-cli/private",
|
||||
"//packages/core/schematics/utils",
|
||||
"//packages/core/schematics/utils/tsurge",
|
||||
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["*.spec.ts"]),
|
||||
deps = [
|
||||
":http-xhr-backend",
|
||||
"//:node_modules/typescript",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/core/schematics/utils/tsurge",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_test(
|
||||
name = "test",
|
||||
data = [":test_lib"],
|
||||
)
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* @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 {absoluteFrom} from '@angular/compiler-cli';
|
||||
import {initMockFileSystem} from '@angular/compiler-cli/private/testing';
|
||||
import {runTsurgeMigration} from '../../utils/tsurge/testing';
|
||||
import {XhrBackendMigration} from './migration';
|
||||
|
||||
describe('http fetch backend migration', () => {
|
||||
beforeEach(() => {
|
||||
initMockFileSystem('Native');
|
||||
});
|
||||
|
||||
it('should update an empty provideHttpClient', async () => {
|
||||
const {fs} = await runTsurgeMigration(new XhrBackendMigration(), [
|
||||
{
|
||||
name: absoluteFrom('/index.ts'),
|
||||
isProgramRootFile: true,
|
||||
contents: `
|
||||
import {AppConfig} from '@angular/core';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
|
||||
const config: AppConfig = [
|
||||
provideHttpClient(),
|
||||
]
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
const actual = fs.readFile(absoluteFrom('/index.ts'));
|
||||
expect(actual).toContain('provideHttpClient(withXhr())');
|
||||
});
|
||||
|
||||
it('should update provideHttpClient without withFetch', async () => {
|
||||
const {fs} = await runTsurgeMigration(new XhrBackendMigration(), [
|
||||
{
|
||||
name: absoluteFrom('/index.ts'),
|
||||
isProgramRootFile: true,
|
||||
contents: `
|
||||
import {AppConfig} from '@angular/core';
|
||||
import {provideHttpClient, withInterceptorsFromDi, withXsrfConfiguration} from '@angular/common/http';
|
||||
|
||||
const config: AppConfig = [
|
||||
provideHttpClient(withInterceptorsFromDi(), withXsrfConfiguration({})),
|
||||
]
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
const actual = fs.readFile(absoluteFrom('/index.ts'));
|
||||
expect(actual).toContain(
|
||||
'provideHttpClient(withXhr(), withInterceptorsFromDi(), withXsrfConfiguration({}))',
|
||||
);
|
||||
expect(actual).toMatch(/import \{.*withXhr.*\}/);
|
||||
});
|
||||
|
||||
it('should update provideHttpClient to remove withFetch', async () => {
|
||||
const {fs} = await runTsurgeMigration(new XhrBackendMigration(), [
|
||||
{
|
||||
name: absoluteFrom('/index.ts'),
|
||||
isProgramRootFile: true,
|
||||
contents: `
|
||||
import {AppConfig} from '@angular/core';
|
||||
import {provideHttpClient, withFetch, withInterceptorsFromDi, withXsrfConfiguration} from '@angular/common/http';
|
||||
|
||||
const config: AppConfig = [
|
||||
provideHttpClient(withFetch(), withInterceptorsFromDi(), withXsrfConfiguration({})),
|
||||
]
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
const actual = fs.readFile(absoluteFrom('/index.ts'));
|
||||
expect(actual).toContain(
|
||||
'provideHttpClient(withInterceptorsFromDi(), withXsrfConfiguration({}))',
|
||||
);
|
||||
expect(actual).not.toContain('withFetch');
|
||||
});
|
||||
|
||||
it('should update provideHttpClient to remove withFetch as only arg', async () => {
|
||||
const {fs} = await runTsurgeMigration(new XhrBackendMigration(), [
|
||||
{
|
||||
name: absoluteFrom('/index.ts'),
|
||||
isProgramRootFile: true,
|
||||
contents: `
|
||||
import {AppConfig} from '@angular/core';
|
||||
import {provideHttpClient, withFetch, withInterceptorsFromDi, withXsrfConfiguration} from '@angular/common/http';
|
||||
|
||||
const config: AppConfig = [
|
||||
provideHttpClient(withFetch()),
|
||||
]
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
const actual = fs.readFile(absoluteFrom('/index.ts'));
|
||||
expect(actual).toContain('provideHttpClient()');
|
||||
expect(actual).not.toContain('withFetch');
|
||||
});
|
||||
|
||||
it('should not update provideHttpClient if withXhr is already present', async () => {
|
||||
const {fs} = await runTsurgeMigration(new XhrBackendMigration(), [
|
||||
{
|
||||
name: absoluteFrom('/index.ts'),
|
||||
isProgramRootFile: true,
|
||||
contents: `
|
||||
import {AppConfig} from '@angular/core';
|
||||
import {provideHttpClient, withXhr, withInterceptorsFromDi, withXsrfConfiguration} from '@angular/common/http';
|
||||
|
||||
const config: AppConfig = [
|
||||
provideHttpClient(withXhr(), withInterceptorsFromDi(), withXsrfConfiguration({})),
|
||||
]
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
const actual = fs.readFile(absoluteFrom('/index.ts'));
|
||||
expect(actual).toContain(
|
||||
'provideHttpClient(withXhr(), withInterceptorsFromDi(), withXsrfConfiguration({})),',
|
||||
);
|
||||
expect(actual).not.toContain('withFetch');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @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.dev/license
|
||||
*/
|
||||
|
||||
import {Rule} from '@angular-devkit/schematics';
|
||||
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
|
||||
import {XhrBackendMigration} from './migration';
|
||||
|
||||
export function migrate(): Rule {
|
||||
return async (tree) => {
|
||||
await runMigrationInDevkit({
|
||||
tree,
|
||||
getMigration: () => new XhrBackendMigration(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* @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 {ImportManager} from '@angular/compiler-cli/private/migrations';
|
||||
import ts from 'typescript';
|
||||
import {
|
||||
confirmAsSerializable,
|
||||
ProgramInfo,
|
||||
projectFile,
|
||||
ProjectFile,
|
||||
Replacement,
|
||||
Serializable,
|
||||
TextUpdate,
|
||||
TsurgeFunnelMigration,
|
||||
} from '../../utils/tsurge';
|
||||
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
|
||||
import {getImportSpecifier, getNamedImports} from '../../utils/typescript/imports';
|
||||
|
||||
const HTTP = '@angular/common/http';
|
||||
const provideHttpClient = 'provideHttpClient';
|
||||
|
||||
const WITH_FETCH = 'withFetch';
|
||||
const WITH_XHR = 'withXhr';
|
||||
const CORE_PACKAGE = '@angular/core';
|
||||
const HTTP_PACKAGE = '@angular/common/http';
|
||||
const PROVIDE_HTTP_CLIENT = 'provideHttpClient';
|
||||
|
||||
export interface CompilationUnitData {
|
||||
replacements: Replacement[];
|
||||
}
|
||||
|
||||
export interface MigrationConfig {
|
||||
/**
|
||||
* Whether to migrate this component template to self-closing tags.
|
||||
*/
|
||||
shouldMigrate?: (containingFile: ProjectFile) => boolean;
|
||||
}
|
||||
|
||||
const provideHttpClientIdentifier = ts.factory.createIdentifier('provideHttpClient');
|
||||
|
||||
export class XhrBackendMigration extends TsurgeFunnelMigration<
|
||||
CompilationUnitData,
|
||||
CompilationUnitData
|
||||
> {
|
||||
constructor(private readonly config: MigrationConfig = {}) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
|
||||
const replacements: Replacement[] = [];
|
||||
const importManager = new ImportManager();
|
||||
|
||||
for (const sourceFile of info.sourceFiles) {
|
||||
const walk = (node: ts.Node): void => {
|
||||
const file = projectFile(sourceFile, info);
|
||||
if (this.config.shouldMigrate && !this.config.shouldMigrate(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpImports = getNamedImports(sourceFile, HTTP);
|
||||
if (!httpImports) {
|
||||
return;
|
||||
}
|
||||
const importSpecifier = getImportSpecifier(sourceFile, HTTP, provideHttpClient);
|
||||
if (!importSpecifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.forEachChild(walk);
|
||||
|
||||
if (!ts.isCallExpression(node)) return;
|
||||
if (!ts.isIdentifier(node.expression)) return;
|
||||
if (node.expression.text !== 'provideHttpClient') return;
|
||||
const withFetchNode = node.arguments.find((arg) => {
|
||||
return (
|
||||
ts.isCallExpression(arg) &&
|
||||
ts.isIdentifier(arg.expression) &&
|
||||
arg.expression.text === WITH_FETCH
|
||||
);
|
||||
});
|
||||
const withXhrNode = node.arguments.find((arg) => {
|
||||
return (
|
||||
ts.isCallExpression(arg) &&
|
||||
ts.isIdentifier(arg.expression) &&
|
||||
arg.expression.text === WITH_XHR
|
||||
);
|
||||
});
|
||||
|
||||
if (!withFetchNode && !withXhrNode) {
|
||||
replacements.push(
|
||||
new Replacement(
|
||||
projectFile(sourceFile, info),
|
||||
new TextUpdate({
|
||||
position: node.arguments.pos,
|
||||
end: node.arguments.pos,
|
||||
toInsert: node.arguments.length ? 'withXhr(), ' : 'withXhr()',
|
||||
}),
|
||||
),
|
||||
);
|
||||
importManager.addImport({
|
||||
exportModuleSpecifier: HTTP_PACKAGE,
|
||||
exportSymbolName: WITH_XHR,
|
||||
requestedFile: sourceFile,
|
||||
});
|
||||
} else if (withFetchNode) {
|
||||
const isLastArg = node.arguments[node.arguments.length - 1] === withFetchNode;
|
||||
replacements.push(
|
||||
new Replacement(
|
||||
projectFile(sourceFile, info),
|
||||
new TextUpdate({
|
||||
position: withFetchNode.getStart(),
|
||||
end: isLastArg ? withFetchNode.getEnd() : withFetchNode.getEnd() + 2, // +2 to remove the comma and space, could be improved
|
||||
toInsert: '',
|
||||
}),
|
||||
),
|
||||
);
|
||||
importManager.removeImport(sourceFile, 'withFetch', HTTP_PACKAGE);
|
||||
}
|
||||
};
|
||||
sourceFile.forEachChild(walk);
|
||||
}
|
||||
|
||||
applyImportManagerChanges(importManager, replacements, info.sourceFiles, info);
|
||||
return confirmAsSerializable({replacements});
|
||||
}
|
||||
|
||||
override async combine(
|
||||
unitA: CompilationUnitData,
|
||||
unitB: CompilationUnitData,
|
||||
): Promise<Serializable<CompilationUnitData>> {
|
||||
const combined = [...unitA.replacements, ...unitB.replacements];
|
||||
return confirmAsSerializable({replacements: combined});
|
||||
}
|
||||
|
||||
override async globalMeta(data: CompilationUnitData): Promise<Serializable<CompilationUnitData>> {
|
||||
return confirmAsSerializable(data);
|
||||
}
|
||||
|
||||
override async stats(data: CompilationUnitData) {
|
||||
return confirmAsSerializable({});
|
||||
}
|
||||
|
||||
override async migrate(data: CompilationUnitData) {
|
||||
return {replacements: data.replacements};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue