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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Inspect Angular component trees, signals, dependency injection, and routes — a
- **Component inspector** — discover components, inputs, outputs, and source files; view injected providers per component
- **Signal graph** — visualize signal, computed, linkedSignal, effect nodes and their dependency edges (Angular 19+)
- **DI inspector** — browse the injector hierarchy (element and environment) with providers at each level (Angular 17+)
- **Route inspector** — list registered routes from source
- **Route inspector** — the live route, every navigation as a full story (who started it, redirects, per-phase timing, which guard or resolver decided it, errors explained), the live route config with URL testing, router setup, route lint, and actions to navigate, replay, probe and abort
- **NgRx Store inspector** — detect `@ngrx/store` (actions, reducers, effects, selectors) and `@ngrx/signals` (`signalStore`, `signalState`, `signalMethod`) patterns from source; live state & action log via Redux DevTools protocol
- **Forms inspector** — every form on the page (Signal Forms, reactive and template-driven) with each field's value, status, touched/dirty state and readable errors, plus a timeline of recent changes; hover a field to highlight its input
- **Build metadata** — Angular version, TypeScript version, SSR status
Expand Down Expand Up @@ -101,6 +101,14 @@ MCP clients see these with an underscore, as `ng-devtools_get-routes`.
| `ng-devtools:get-ngrx-store` | Scan source for NgRx store patterns |
| `ng-devtools:inspect-forms` | Forms on the page with every field's state and errors |
| `ng-devtools:explain-form-invalid` | Which fields make a form invalid, and why |
| `ng-devtools:inspect-route` | The current route with params, data, guards and resolvers |
| `ng-devtools:explain-navigation` | Recent navigations and why each succeeded or not |
| `ng-devtools:list-routes` | Live route config; match a URL; audit guard protection |
| `ng-devtools:lint-routes` | Route config mistakes, with fixes |
| `ng-devtools:router-config` | Router options, features and strategies in effect |
| `ng-devtools:export-navigation` | Markdown repro of a navigation |
| `ng-devtools:explain-render-mode` | ServerRoute and render mode for a URL |
| `ng-devtools:navigate` | Navigate, abort, replay, probe, instrument (dev only) |

#### Forms

Expand All @@ -113,6 +121,33 @@ The Forms tab and the forms tools read Signal Forms, reactive forms and template

Form values leave the page: they are sent to the devtools server, shown in the Forms tab and returned to agents. Values of password fields, fields with a password, one-time-code or credit-card `autocomplete`, and fields whose name looks secret (password, token, card, cvv and similar) are replaced with `[redacted]`. Other values are sent as they are, so keep real credentials out of forms you inspect, and don't expose the dev server beyond localhost.

#### Router

The Routes tab and the router tools read the running app's Router, in development builds only. The Router is found through the debug helper `provideRouter()` publishes, or through the injector for `RouterModule.forRoot()` apps. Without debug utils (a production build) only navigation events are available, and the Setup view says so.

The Routes tab has five views:

- **Current**: the URL (and the browser URL when they differ), the navigation in flight with an Abort button, each active route with its component, params and data and where each value comes from (own, inherited, static or resolved), the route title and whether it is inherited, and the outlet tree with the inputs the router binds.
- **Navigations**: every navigation as one story: where it came from, who started it (a RouterLink, the code that called `navigate`, back/forward), extras, redirect chains and loops, a phase bar (recognize, guards, resolve, activate), guards and resolvers, lazy loads, reused components, HTTP requests, scroll, the title afterwards, router warnings, and the cancel or error reason. Turn on "Record each guard and resolver" to see each one's verdict and time (for example `authGuard returned UrlTree /login`). Replay a navigation, copy a markdown repro, or export the list as JSON.
- **Routes**: the live route config with lazy children merged in once they load and the active branch marked. Test a URL to predict which route matches it (or the nearest ones), probe it with the real matcher, navigate to any route (with its params), or read the routes of a lazy route that has not loaded.
- **Setup**: provideRouter or forRoot, effective options with set/default markers, enabled features, strategies, base href and hydration.
- **Lint**: route config mistakes (unreachable routes after `**`, a `:param` shadowing a literal, duplicate paths, empty-path redirects without `pathMatch: 'full'`, redirect cycles, deprecated class guards and `canLoad`, lazy chunks downloaded before a rejecting `canActivate`, missing or duplicate titles, param/input typos, `routerLinkActive` without `ariaCurrentWhenActive`, emails in URLs, return URLs taken from query params), each with a fix and whether Angular throws or stays silent.

Components rendered by the router show the route and outlet in the Components tab.

For agents:

- `ng-devtools:explain-navigation` answers "why did this navigation not work" or "why was I redirected": pass `url` or `id` to narrow it, `limit` for more than the last 5, or `perf` for the slowest navigations and preloads. NG04xxx and related errors are explained.
- `ng-devtools:inspect-route` describes the route the page is on right now; pass `selector` (a component class, tag or link text) to see which route a component was rendered for or whether a link counts as active.
- `ng-devtools:list-routes` lists the live config with source files and example URLs; `match` predicts which route a URL hits, `audit` lists the guards that protect each page.
- `ng-devtools:lint-routes`, `ng-devtools:router-config` and `ng-devtools:export-navigation` give the lint findings, the setup and a repro.
- `ng-devtools:explain-render-mode` reads the workspace's `*.routes.server.ts` and says which render mode a URL gets.
- `ng-devtools:navigate` acts on the router: `navigate` (a relative URL, or a pattern with params), `abort`, `replay`, `probe` (runs the real matcher without navigating; it runs `canMatch` and may load lazy chunks), `instrument` and `resolve-lazy`. It only accepts same-origin relative URLs.

Without instrumentation, the guards listed for a navigation are candidates (the `canDeactivate` guards of the page being left and the `canActivate`/`canActivateChild` guards of the target), because the router reports one result for all of them. Instrumentation wraps each guard and resolver in the live config to record its verdict; it is off by default and undone when turned off. A navigation that finished before the devtools connected is listed without timing or guard details.

Query, matrix and fragment keys that look secret (token, password, api key, code, sig, session, jwt and similar), including inside encoded return URLs, JWTs, bearer tokens, long opaque tokens and route params with such names are replaced with `[redacted]` in URLs, params, data and messages. A secret route param is only known once the route is recognized or found in the config, so a navigation that fails before that (for example inside a lazy route that failed to load) can still show it in its URL.

#### Agent Resources

| Resource | Content |
Expand All @@ -122,6 +157,7 @@ Form values leave the page: they are sent to the devtools server, shown in the F
| `ng-devtools:injector-tree` | DI injector hierarchy |
| `ng-devtools:ngrx-store` | Live NgRx state & action log |
| `ng-devtools:forms` | Live forms and recent changes |
| `ng-devtools:router` | Live route and navigations |

### Vite DevTools Dock

Expand Down
69 changes: 66 additions & 3 deletions app/src/pages/component-tree.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, input, signal, effect } from '@angular/core';
import { Component, DestroyRef, computed, effect, inject, input, signal } from '@angular/core';
import { JsonPipe } from '@angular/common';
import type { DevframeRpcClient } from 'devframe/client';

Expand All @@ -10,6 +10,14 @@ interface ComponentInfo {
isStandalone: boolean;
}

interface OutletInfo {
outlet: string;
route?: string;
element?: string;
activated: boolean;
children?: OutletInfo[];
}

interface ProviderEntry {
token: string;
source: string;
Expand Down Expand Up @@ -48,6 +56,9 @@ interface ProviderEntry {
>
<div class="selector">&lt;{{ comp.selector }}&gt;</div>
<div class="file">{{ comp.file }}</div>
@for (hit of routedBy().get(comp.selector) ?? []; track hit.outlet + hit.route) {
<span class="routed">routed {{ hit.route }} · outlet {{ hit.outlet }}</span>
}
</button>
@if (isSelected(comp)) {
<div class="inline-detail">
Expand Down Expand Up @@ -258,9 +269,19 @@ interface ProviderEntry {
background: #3f3f46;
color: #a1a1aa;
}
.routed {
display: inline-block;
margin-top: 4px;
padding: 1px 6px;
border: 1px solid #52525b;
border-radius: 4px;
color: #d4d4d8;
font-size: 11px;
font-family: monospace;
}
.provider-source {
font-size: 12px;
color: #71717a;
color: #a1a1aa;
}
.no-providers {
font-size: 13px;
Expand All @@ -279,6 +300,26 @@ export class ComponentTree {
selectedProviders = signal<ProviderEntry[]>([]);

filtered = signal<ComponentInfo[]>([]);
private readonly outlets = signal<OutletInfo[]>([]);
private unsubscribeRouter: (() => void) | null = null;
private readonly destroyRef = inject(DestroyRef);

readonly routedBy = computed(() => {
const map = new Map<string, { route: string; outlet: string }[]>();
const visit = (outlets: OutletInfo[]) => {
for (const outlet of outlets) {
if (outlet.activated && outlet.element && outlet.route) {
map.set(outlet.element, [
...(map.get(outlet.element) ?? []),
{ route: outlet.route, outlet: outlet.outlet },
]);
}
if (outlet.children) visit(outlet.children);
}
};
visit(this.outlets());
return map;
});

constructor() {
effect(() => {
Expand All @@ -289,8 +330,30 @@ export class ComponentTree {

effect(() => {
const client = this.rpc();
if (client) this.refresh();
if (client) {
this.refresh();
void this.watchRouter(client);
}
});
this.destroyRef.onDestroy(() => this.unsubscribeRouter?.());
}

private async watchRouter(client: DevframeRpcClient) {
try {
const state = await client.scope('ng-devtools').rpc.sharedState('router');
if (this.destroyRef.destroyed) return;
const apply = (value: unknown) => {
const pages =
(value as { pages?: { outlets?: OutletInfo[]; snapshot?: unknown }[] })?.pages ?? [];
const page = pages.find((p) => p.snapshot) ?? pages[0];
this.outlets.set(page?.outlets ?? []);
};
apply(state.value());
this.unsubscribeRouter?.();
this.unsubscribeRouter = state.on('updated', apply);
} catch {
this.outlets.set([]);
}
}

async refresh() {
Expand Down
Loading
Loading