From 6ecd5df284a2d13f80ae3d2ff87df830c4aca492 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 11 Dec 2024 13:28:11 +0000 Subject: [PATCH] fix(@angular/ssr): disable component bootstrapping during route extraction This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks. Closes #29085 --- packages/angular/ssr/src/routes/ng-routes.ts | 23 ++++++++++++++- .../angular/ssr/test/routes/ng-routes_spec.ts | 29 +++++++++++++++++++ packages/angular/ssr/test/testing-utils.ts | 22 +++++++------- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index be91636ccd25..082e4a1f5b4d 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -7,7 +7,16 @@ */ import { APP_BASE_HREF, PlatformLocation } from '@angular/common'; -import { ApplicationRef, Compiler, Injector, runInInjectionContext, ɵConsole } from '@angular/core'; +import { + APP_INITIALIZER, + ApplicationRef, + Compiler, + ComponentRef, + Injector, + inject, + runInInjectionContext, + ɵConsole, +} from '@angular/core'; import { INITIAL_CONFIG, platformServer } from '@angular/platform-server'; import { Route as AngularRoute, @@ -479,6 +488,18 @@ export async function getRoutesFromAngularRouterConfig( provide: ɵConsole, useFactory: () => new Console(), }, + { + // We cannot replace `ApplicationRef` with a different provider here due to the dependency injection (DI) hierarchy. + // This code is running at the platform level, where `ApplicationRef` is provided in the root injector. + // As a result, any attempt to replace it will cause the root provider to override the platform provider. + // TODO(alanagius): investigate exporting the app config directly which would help with: https://github.com/angular/angular/issues/59144 + provide: APP_INITIALIZER, + multi: true, + useFactory: () => () => { + const appRef = inject(ApplicationRef); + appRef.bootstrap = () => undefined as unknown as ComponentRef; + }, + }, ]); try { diff --git a/packages/angular/ssr/test/routes/ng-routes_spec.ts b/packages/angular/ssr/test/routes/ng-routes_spec.ts index d1448e3b8b2c..ca4b3f757fc7 100644 --- a/packages/angular/ssr/test/routes/ng-routes_spec.ts +++ b/packages/angular/ssr/test/routes/ng-routes_spec.ts @@ -466,4 +466,33 @@ describe('extractRoutesAndCreateRouteTree', () => { { route: '/example/home', renderMode: RenderMode.Server }, ]); }); + + it('should not bootstrap the root component', async () => { + @Component({ + standalone: true, + selector: 'app-root', + template: '', + }) + class RootComponent { + constructor() { + throw new Error('RootComponent should not be bootstrapped.'); + } + } + + setAngularAppTestingManifest( + [ + { path: '', component: DummyComponent }, + { path: 'home', component: DummyComponent }, + ], + [{ path: '**', renderMode: RenderMode.Server }], + undefined, + undefined, + undefined, + RootComponent, + ); + + const { routeTree, errors } = await extractRoutesAndCreateRouteTree({ url }); + expect(errors).toHaveSize(0); + expect(routeTree.toObject()).toHaveSize(2); + }); }); diff --git a/packages/angular/ssr/test/testing-utils.ts b/packages/angular/ssr/test/testing-utils.ts index f872487b06d8..4d7e8a1cfb28 100644 --- a/packages/angular/ssr/test/testing-utils.ts +++ b/packages/angular/ssr/test/testing-utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Component, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { Component, Type, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { provideServerRendering } from '@angular/platform-server'; import { RouterOutlet, Routes, provideRouter } from '@angular/router'; @@ -14,6 +14,14 @@ import { destroyAngularServerApp } from '../src/app'; import { ServerAsset, setAngularAppManifest } from '../src/manifest'; import { ServerRoute, provideServerRoutesConfig } from '../src/routes/route-config'; +@Component({ + standalone: true, + selector: 'app-root', + template: '', + imports: [RouterOutlet], +}) +class AppComponent {} + /** * Configures the Angular application for testing by setting up the Angular app manifest, * configuring server-side rendering, and bootstrapping the application with the provided routes. @@ -26,6 +34,7 @@ import { ServerRoute, provideServerRoutesConfig } from '../src/routes/route-conf * @param additionalServerAssets - A record of additional server assets to include, * where the keys are asset paths and the values are asset details. * @param locale - An optional locale to configure for the application during testing. + * @param rootComponent - The root Angular component to bootstrap the application. */ export function setAngularAppTestingManifest( routes: Routes, @@ -33,17 +42,10 @@ export function setAngularAppTestingManifest( baseHref = '/', additionalServerAssets: Record = {}, locale?: string, + rootComponent: Type = AppComponent, ): void { destroyAngularServerApp(); - @Component({ - standalone: true, - selector: 'app-root', - template: '', - imports: [RouterOutlet], - }) - class AppComponent {} - setAngularAppManifest({ inlineCriticalCss: false, baseHref, @@ -81,7 +83,7 @@ export function setAngularAppTestingManifest( }, }, bootstrap: async () => () => { - return bootstrapApplication(AppComponent, { + return bootstrapApplication(rootComponent, { providers: [ provideServerRendering(), provideExperimentalZonelessChangeDetection(),