Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/src/content/agents/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/src/content/inspectors/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions packages/devtools/src/__tests__/router-forroot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ 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);
return detectSetup(ng, app.injector.get(Router) as never, 1, root).setupKind;
};
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', () => {
Expand Down
60 changes: 60 additions & 0 deletions packages/devtools/src/__tests__/router-setup-legacy.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof bootstrapApplication>>;
let ng: RouterDebugApi;
let util: unknown;

beforeEach(async () => {
document.body.innerHTML = '<app-root></app-root>';
app = await bootstrapApplication(Root, {
providers: [provideZonelessChangeDetection(), provideRouter([{ path: '', component: Root }])],
});
const published = (globalThis as { ng?: RouterDebugApi & Record<string, unknown> }).ng!;
util = published['ɵgetRouterInstance'];
delete published['ɵgetRouterInstance'];
ng = {
getInjector: published.getInjector,
ɵgetInjectorResolutionPath: published.ɵgetInjectorResolutionPath,
ɵgetInjectorProviders: published.ɵgetInjectorProviders,
};
});

afterEach(() => {
(globalThis as { ng?: Record<string, unknown> }).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');
});
2 changes: 1 addition & 1 deletion packages/devtools/src/devframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
22 changes: 15 additions & 7 deletions packages/devtools/src/router-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, unknown> = {
...options,
initialNavigation: initialNavigation ?? options['initialNavigation'],
Expand Down Expand Up @@ -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,
Expand Down
Loading