diff --git a/packages/common/http/src/backend-default-value.ts b/packages/common/http/src/backend-default-value.ts deleted file mode 100644 index 6c06e5fdd24..00000000000 --- a/packages/common/http/src/backend-default-value.ts +++ /dev/null @@ -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; diff --git a/packages/common/http/src/fetch.ts b/packages/common/http/src/fetch.ts index 7c7a4c3f61c..3df3c87fdbe 100644 --- a/packages/common/http/src/fetch.ts +++ b/packages/common/http/src/fetch.ts @@ -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( - typeof ngDevMode === 'undefined' || ngDevMode ? 'FETCH_BACKEND' : '', -); - /** * Uses `fetch` to send requests to a backend server. * diff --git a/packages/common/http/src/provider.ts b/packages/common/http/src/provider.ts index 1487fe9e212..de8c923dac1 100644 --- a/packages/common/http/src/provider.ts +++ b/packages/common/http/src/provider.ts @@ -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 { return makeHttpFeature(HttpFeatureKind.Fetch, [ FetchBackend, - {provide: FETCH_BACKEND, useExisting: FetchBackend}, {provide: HttpBackend, useExisting: FetchBackend}, ]); } diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 5c0985aaaf8..6c14c761ac2 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 919191c469f..616f270fced 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -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" } } } diff --git a/packages/core/schematics/migrations/http-xhr-backend/BUILD.bazel b/packages/core/schematics/migrations/http-xhr-backend/BUILD.bazel new file mode 100644 index 00000000000..15b6b4d900b --- /dev/null +++ b/packages/core/schematics/migrations/http-xhr-backend/BUILD.bazel @@ -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"], +) diff --git a/packages/core/schematics/migrations/http-xhr-backend/http-xhr-backend.spec.ts b/packages/core/schematics/migrations/http-xhr-backend/http-xhr-backend.spec.ts new file mode 100644 index 00000000000..7ccec459c5d --- /dev/null +++ b/packages/core/schematics/migrations/http-xhr-backend/http-xhr-backend.spec.ts @@ -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'); + }); +}); diff --git a/packages/core/schematics/migrations/http-xhr-backend/index.ts b/packages/core/schematics/migrations/http-xhr-backend/index.ts new file mode 100644 index 00000000000..c9508aaeaef --- /dev/null +++ b/packages/core/schematics/migrations/http-xhr-backend/index.ts @@ -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(), + }); + }; +} diff --git a/packages/core/schematics/migrations/http-xhr-backend/migration.ts b/packages/core/schematics/migrations/http-xhr-backend/migration.ts new file mode 100644 index 00000000000..fe098b29f57 --- /dev/null +++ b/packages/core/schematics/migrations/http-xhr-backend/migration.ts @@ -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> { + 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> { + const combined = [...unitA.replacements, ...unitB.replacements]; + return confirmAsSerializable({replacements: combined}); + } + + override async globalMeta(data: CompilationUnitData): Promise> { + return confirmAsSerializable(data); + } + + override async stats(data: CompilationUnitData) { + return confirmAsSerializable({}); + } + + override async migrate(data: CompilationUnitData) { + return {replacements: data.replacements}; + } +}