From 871bbc072c25edd4e341436a245b1a33ce675cee Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 20 Jan 2025 15:34:56 +0000 Subject: [PATCH] fix(@angular/ssr): unblock route extraction with `withEnabledBlockingInitialNavigation` This fix ensures that route extraction is not blocked when `withEnabledBlockingInitialNavigation` is used. Closes #29400 --- packages/angular/ssr/src/routes/ng-routes.ts | 10 ++++-- .../angular/ssr/test/routes/ng-routes_spec.ts | 33 +++++++++++++++++++ packages/angular/ssr/test/testing-utils.ts | 12 ++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index b0b8848569d5..770cb3d6c7ce 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -523,11 +523,17 @@ export async function getRoutesFromAngularRouterConfig( applicationRef = await bootstrap(); } + const injector = applicationRef.injector; + const router = injector.get(Router); + + // Workaround to unblock navigation when `withEnabledBlockingInitialNavigation()` is used. + // This is necessary because route extraction disables component bootstrapping. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (router as any).navigationTransitions.afterPreactivation()?.next?.(); + // Wait until the application is stable. await applicationRef.whenStable(); - const injector = applicationRef.injector; - const router = injector.get(Router); const routesResults: RouteTreeNodeMetadata[] = []; const errors: string[] = []; diff --git a/packages/angular/ssr/test/routes/ng-routes_spec.ts b/packages/angular/ssr/test/routes/ng-routes_spec.ts index ca4b3f757fc7..81b949cd098e 100644 --- a/packages/angular/ssr/test/routes/ng-routes_spec.ts +++ b/packages/angular/ssr/test/routes/ng-routes_spec.ts @@ -12,6 +12,7 @@ import '@angular/compiler'; /* eslint-enable import/no-unassigned-import */ import { Component } from '@angular/core'; +import { Routes, provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router'; import { extractRoutesAndCreateRouteTree } from '../../src/routes/ng-routes'; import { PrerenderFallback, RenderMode } from '../../src/routes/route-config'; import { setAngularAppTestingManifest } from '../testing-utils'; @@ -495,4 +496,36 @@ describe('extractRoutesAndCreateRouteTree', () => { expect(errors).toHaveSize(0); expect(routeTree.toObject()).toHaveSize(2); }); + + it('should not bootstrap the root component when using `withEnabledBlockingInitialNavigation`', async () => { + @Component({ + standalone: true, + selector: 'app-root', + template: '', + }) + class RootComponent { + constructor() { + throw new Error('RootComponent should not be bootstrapped.'); + } + } + + const routes: Routes = [ + { path: '', component: DummyComponent }, + { path: 'home', component: DummyComponent }, + ]; + + setAngularAppTestingManifest( + routes, + [{ path: '**', renderMode: RenderMode.Server }], + undefined, + undefined, + undefined, + RootComponent, + [provideRouter(routes, withEnabledBlockingInitialNavigation())], + ); + + 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 4d7e8a1cfb28..fdfad95983e7 100644 --- a/packages/angular/ssr/test/testing-utils.ts +++ b/packages/angular/ssr/test/testing-utils.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Component, Type, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { + Component, + EnvironmentProviders, + Provider, + Type, + provideExperimentalZonelessChangeDetection, +} from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { provideServerRendering } from '@angular/platform-server'; import { RouterOutlet, Routes, provideRouter } from '@angular/router'; @@ -35,6 +41,8 @@ class AppComponent {} * 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. + * @param extraProviders - An optional array of additional providers that should be available to the + * root component and all its children. */ export function setAngularAppTestingManifest( routes: Routes, @@ -43,6 +51,7 @@ export function setAngularAppTestingManifest( additionalServerAssets: Record = {}, locale?: string, rootComponent: Type = AppComponent, + extraProviders: Array = [], ): void { destroyAngularServerApp(); @@ -89,6 +98,7 @@ export function setAngularAppTestingManifest( provideExperimentalZonelessChangeDetection(), provideRouter(routes), provideServerRoutesConfig(serverRoutes), + ...extraProviders, ], }); },