diff --git a/apps/docs/src/content/agents/tools.md b/apps/docs/src/content/agents/tools.md index e572816..c4e41fe 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 516fc9a..6a8926f 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 8c45d42..e016185 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 0000000..c530110 --- /dev/null +++ b/packages/devtools/src/__tests__/router-setup-legacy.test.ts @@ -0,0 +1,60 @@ +// @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.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')!; + 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 14493f0..d566bae 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 510dd43..fa3214f 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 (descriptions.includes('ROUTES')) return 'provideRouter'; + return publishesRouterUtil(version) ? 'forRoot or other' : '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,