From 280bc5941c8a61c01c8b944d18aaa44b2463a648 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 6 Oct 2026 21:09:04 +0300 Subject: [PATCH 1/2] fix(router): tell provideRouter from forRoot without the router util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Angular 20.0 to 20.3.4 ng.ɵgetRouterInstance is missing, so the setup kind fell back to unknown for every app. It now reads the root providers: the RouterModule.forRoot guard means forRoot or other, the ROUTES token without that guard means provideRouter, and only neither gives unknown. Refs #100 --- apps/docs/src/content/agents/tools.md | 2 +- apps/docs/src/content/inspectors/router.md | 4 +- .../src/__tests__/router-forroot.test.ts | 6 +- .../src/__tests__/router-setup-legacy.test.ts | 57 +++++++++++++++++++ packages/devtools/src/devframe.ts | 2 +- packages/devtools/src/router-setup.ts | 22 ++++--- 6 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 packages/devtools/src/__tests__/router-setup-legacy.test.ts diff --git a/apps/docs/src/content/agents/tools.md b/apps/docs/src/content/agents/tools.md index e572816d..c4e41fe1 100644 --- a/apps/docs/src/content/agents/tools.md +++ b/apps/docs/src/content/agents/tools.md @@ -190,7 +190,7 @@ Use `explain-navigation` for "why was I redirected". Pass `perf: true` for "why | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `list-routes` | The live route config: every route with its full path, component or redirect, lazy state, guards, resolvers, title, source file and an example URL. | `match`, `audit`, `filter` | | `lint-routes` | Route config mistakes, such as routes after `**`, redirect cycles, redirect loops seen at runtime, deprecated class guards, missing titles and param typos. Each finding says how Angular reacts and how to fix it. | none | -| `router-config` | How the router is set up: `provideRouter` or `forRoot`, effective options, enabled features, strategies, base href and hydration. | none | +| `router-config` | How the router is set up (`provideRouter`, `forRoot or other` or `unknown`), effective options, enabled features, strategies, base href and hydration. | none | | `explain-render-mode` | The `ServerRoute` and render mode (Server, Client, Prerender) a URL gets, plus server entries that match no client route. | `url`, defaults to the page URL | `list-routes` takes three optional arguments: diff --git a/apps/docs/src/content/inspectors/router.md b/apps/docs/src/content/inspectors/router.md index 516fc9a9..6a8926f1 100644 --- a/apps/docs/src/content/inspectors/router.md +++ b/apps/docs/src/content/inspectors/router.md @@ -65,7 +65,7 @@ The result of **Go** and **Read lazy** shows under the row you clicked. ### Setup -How the router is set up: `provideRouter` or `forRoot`, the effective options with **set** or **default** badges, the enabled features, the strategies, the base href and hydration. +How the router is set up (`provideRouter`, `forRoot or other` or `unknown`), the effective options with **set** or **default** badges, the enabled features, the strategies, the base href and hydration. `initialNavigation` shows the mode that `withEnabledBlockingInitialNavigation()`, `withDisabledInitialNavigation()` or the `forRoot` option sets. The features map to these router features: @@ -204,7 +204,7 @@ Without that recording, the guards listed for a navigation are candidates: the ` ### Setup kind on Angular 20.0 to 20.3.4 -**Set up with** tells `provideRouter` from `forRoot` by the `ng.ɵgetRouterInstance` helper. Angular 20.0 to 20.3.4 never publish it, so on those versions the row shows `unknown`. +**Set up with** tells `provideRouter` from `forRoot` by the `ng.ɵgetRouterInstance` helper. Angular 20.0 to 20.3.4 never publish it, so on those versions the overlay reads the root providers instead. The `RouterModule.forRoot()` guard token means `forRoot or other`, and the `ROUTES` token without that guard means `provideRouter`. If neither is there, the row shows `unknown`. ### Abort and probe need Angular 20.2 diff --git a/packages/devtools/src/__tests__/router-forroot.test.ts b/packages/devtools/src/__tests__/router-forroot.test.ts index 8c45d423..e0161854 100644 --- a/packages/devtools/src/__tests__/router-forroot.test.ts +++ b/packages/devtools/src/__tests__/router-forroot.test.ts @@ -43,7 +43,7 @@ it('finds the router for the Analog report without ng.ɵgetRouterInstance', () = expect(routerOf(ng as never)).toBe(app.injector.get(Router)); }); -it('reports the setup kind as unknown on versions that never publish the router util', () => { +it('reports forRoot from its guard token on every version, with or without the router util', () => { const root = document.querySelector('app-root')!; const setupOn = (version: string) => { root.setAttribute('ng-version', version); @@ -51,8 +51,8 @@ it('reports the setup kind as unknown on versions that never publish the router }; expect(setupOn('22.1.7')).toBe('forRoot or other'); expect(setupOn('20.3.5')).toBe('forRoot or other'); - expect(setupOn('20.3.4')).toBe('unknown'); - expect(setupOn('20.0.0')).toBe('unknown'); + expect(setupOn('20.3.4')).toBe('forRoot or other'); + expect(setupOn('20.0.0')).toBe('forRoot or other'); }); it('knows which versions publish the router util', () => { diff --git a/packages/devtools/src/__tests__/router-setup-legacy.test.ts b/packages/devtools/src/__tests__/router-setup-legacy.test.ts new file mode 100644 index 00000000..1df8298a --- /dev/null +++ b/packages/devtools/src/__tests__/router-setup-legacy.test.ts @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import '@angular/compiler'; +import { Component, provideZonelessChangeDetection } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { Router, provideRouter } from '@angular/router'; +import { afterEach, beforeEach, expect, it } from 'vitest'; +import { routerOf } from '../analog-runtime.ts'; +import { findRouter, type RouterDebugApi } from '../router.ts'; +import { detectSetup } from '../router-setup.ts'; + +class Root {} +Component({ selector: 'app-root', template: '' })(Root); + +let app: Awaited>; +let ng: RouterDebugApi; +let util: unknown; + +beforeEach(async () => { + document.body.innerHTML = ''; + app = await bootstrapApplication(Root, { + providers: [provideZonelessChangeDetection(), provideRouter([{ path: '', component: Root }])], + }); + const published = (globalThis as { ng?: RouterDebugApi & Record }).ng!; + util = published['ɵgetRouterInstance']; + delete published['ɵgetRouterInstance']; + ng = { + getInjector: published.getInjector, + ɵgetInjectorResolutionPath: published.ɵgetInjectorResolutionPath, + ɵgetInjectorProviders: published.ɵgetInjectorProviders, + }; +}); + +afterEach(() => { + (globalThis as { ng?: Record }).ng!['ɵgetRouterInstance'] = util; + app.destroy(); +}); + +it('finds the router of a provideRouter app without ng.ɵgetRouterInstance', () => { + const roots = [document.querySelector('app-root')!]; + expect(findRouter(ng, roots)).toBe(app.injector.get(Router)); + expect(routerOf(ng as never)).toBe(app.injector.get(Router)); +}); + +it('reports provideRouter from the root ROUTES when the router util is missing', () => { + const root = document.querySelector('app-root')!; + root.setAttribute('ng-version', '20.3.4'); + expect(detectSetup(ng, app.injector.get(Router) as never, 1, root).setupKind).toBe( + 'provideRouter', + ); +}); + +it('reports unknown when neither the util nor the providers tell the setup apart', () => { + const root = document.querySelector('app-root')!; + root.setAttribute('ng-version', '20.3.4'); + const bare: RouterDebugApi = { ...ng, ɵgetInjectorProviders: () => [] }; + expect(detectSetup(bare, app.injector.get(Router) as never, 1, root).setupKind).toBe('unknown'); +}); diff --git a/packages/devtools/src/devframe.ts b/packages/devtools/src/devframe.ts index 14493f06..d566baea 100644 --- a/packages/devtools/src/devframe.ts +++ b/packages/devtools/src/devframe.ts @@ -1871,7 +1871,7 @@ const pangular = defineDevframe({ agent.registerTool({ id: 'pangular:router-config', description: - 'How the router is set up on the running page: provideRouter or forRoot, Angular version, effective options with which are set and which are defaults (onSameUrlNavigation, paramsInheritanceStrategy, urlUpdateStrategy, canceledNavigationResolution, scrolling, initial navigation), enabled features (input binding, view transitions, error handler, preloading strategy, scroller, resources), strategies (location, title, reuse, URL handling), base href, hydration and whether per-guard instrumentation is on.', + 'How the router is set up on the running page: provideRouter, forRoot or other, or unknown when the build cannot tell, Angular version, effective options with which are set and which are defaults (onSameUrlNavigation, paramsInheritanceStrategy, urlUpdateStrategy, canceledNavigationResolution, scrolling, initial navigation), enabled features (input binding, view transitions, error handler, preloading strategy, scroller, resources), strategies (location, title, reuse, URL handling), base href, hydration and whether per-guard instrumentation is on.', safety: 'read', inputSchema: { type: 'object', properties: { page: pageProperty } }, handler: async (args: { page?: string }) => { diff --git a/packages/devtools/src/router-setup.ts b/packages/devtools/src/router-setup.ts index 510dd434..07331b56 100644 --- a/packages/devtools/src/router-setup.ts +++ b/packages/devtools/src/router-setup.ts @@ -127,6 +127,18 @@ function hasDebugTracing(ng: RouterDebugApi, root: Element | null): boolean { ); } +function setupKindOf( + provided: ProvidedToken[], + version: string | undefined, +): RouterSetup['setupKind'] { + if (typeof (globalThis as AnyRecord)['ng']?.['ɵgetRouterInstance'] === 'function') + return 'provideRouter'; + const descriptions = provided.map((p) => tokenDescription(p.token)); + if (descriptions.includes('router duplicate forRoot guard')) return 'forRoot or other'; + if (publishesRouterUtil(version)) return 'forRoot or other'; + return descriptions.includes('ROUTES') ? 'provideRouter' : 'unknown'; +} + export function detectSetup( ng: RouterDebugApi, router: AnyRecord, @@ -137,7 +149,8 @@ export function detectSetup( const scroller = scrollerOf(ng, root); const scrollOptions = read(() => (scroller?.['options'] as AnyRecord) ?? {}, {}); const transitions = read(() => router['navigationTransitions'] as AnyRecord, null); - const initialNavigation = initialNavigationOf(ng, root, providersOf(ng, root)); + const provided = providersOf(ng, root); + const initialNavigation = initialNavigationOf(ng, root, provided); const effective: Record = { ...options, initialNavigation: initialNavigation ?? options['initialNavigation'], @@ -202,12 +215,7 @@ export function detectSetup( ); const setup: RouterSetup = { mode: 'full', - setupKind: - typeof (globalThis as AnyRecord)['ng']?.['ɵgetRouterInstance'] === 'function' - ? 'provideRouter' - : publishesRouterUtil(version) - ? 'forRoot or other' - : 'unknown', + setupKind: setupKindOf(provided, version), routers, options: list, features, From fc93990d1a7c2e8eb001aacc7147dc8f25053220 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 6 Oct 2026 21:37:03 +0300 Subject: [PATCH 2/2] fix(router): read ROUTES before the version fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A provideRouter app without ng.ɵgetRouterInstance on 20.3.5 or later, for example an uncompiled production build, was labelled forRoot or other by the version check. The ROUTES token is now checked first, so the version only decides when neither provider is found. --- .../src/__tests__/router-setup-legacy.test.ts | 17 ++++++++++------- packages/devtools/src/router-setup.ts | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/devtools/src/__tests__/router-setup-legacy.test.ts b/packages/devtools/src/__tests__/router-setup-legacy.test.ts index 1df8298a..c5301101 100644 --- a/packages/devtools/src/__tests__/router-setup-legacy.test.ts +++ b/packages/devtools/src/__tests__/router-setup-legacy.test.ts @@ -41,13 +41,16 @@ it('finds the router of a provideRouter app without ng.ɵgetRouterInstance', () expect(routerOf(ng as never)).toBe(app.injector.get(Router)); }); -it('reports provideRouter from the root ROUTES when the router util is missing', () => { - const root = document.querySelector('app-root')!; - root.setAttribute('ng-version', '20.3.4'); - expect(detectSetup(ng, app.injector.get(Router) as never, 1, root).setupKind).toBe( - 'provideRouter', - ); -}); +it.each(['20.3.4', '20.3.5'])( + 'reports provideRouter from the root ROUTES when the router util is missing on %s', + (version) => { + const root = document.querySelector('app-root')!; + root.setAttribute('ng-version', version); + expect(detectSetup(ng, app.injector.get(Router) as never, 1, root).setupKind).toBe( + 'provideRouter', + ); + }, +); it('reports unknown when neither the util nor the providers tell the setup apart', () => { const root = document.querySelector('app-root')!; diff --git a/packages/devtools/src/router-setup.ts b/packages/devtools/src/router-setup.ts index 07331b56..fa3214f2 100644 --- a/packages/devtools/src/router-setup.ts +++ b/packages/devtools/src/router-setup.ts @@ -135,8 +135,8 @@ function setupKindOf( return 'provideRouter'; const descriptions = provided.map((p) => tokenDescription(p.token)); if (descriptions.includes('router duplicate forRoot guard')) return 'forRoot or other'; - if (publishesRouterUtil(version)) return 'forRoot or other'; - return descriptions.includes('ROUTES') ? 'provideRouter' : 'unknown'; + if (descriptions.includes('ROUTES')) return 'provideRouter'; + return publishesRouterUtil(version) ? 'forRoot or other' : 'unknown'; } export function detectSetup(