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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ MCP clients see these with an underscore, as `ng-devtools_get-routes`.
| `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) |
| `ng-devtools:analog-routes` | Analog file routes with their page, layout and server files |
| `ng-devtools:analog-explain-url` | Which Analog files render a URL, or why nothing matches |
| `ng-devtools:analog-current-page` | The open page's files, load() data and hydration state |
| `ng-devtools:analog-server-calls` | Page renders, load(), server function and API calls |
| `ng-devtools:analog-api-routes` | Server routes with method, URL and file |
| `ng-devtools:analog-call-api` | Send a request to a server route (non-GET needs confirm) |
| `ng-devtools:analog-render-modes` | SSR, prerendered or client only, per page |
| `ng-devtools:analog-prerender-plan` | prerender.routes compared with pages and build output |
| `ng-devtools:analog-content` | Markdown content with slug and frontmatter |
| `ng-devtools:analog-lint` | Analog routing, server, prerender and content mistakes |

#### Forms

Expand Down Expand Up @@ -163,6 +173,37 @@ Without instrumentation, the guards listed for a navigation are candidates (the

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.

#### Analog

For [Analog](https://analogjs.org) apps, add the Vite plugin next to `analog()` and load the overlay in `main.ts`:

```ts
// vite.config.ts
import analog from '@analogjs/platform';
import ngDevtools from '@santoshyadavdev/ng-devtools/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [analog(), ngDevtools()],
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

```ts
// src/main.ts
bootstrapApplication(App, appConfig).then(() => {
if (import.meta.env.DEV) void import('@santoshyadavdev/ng-devtools/overlay');
});
```

The panel is then at `/__ng-devtools/` on the Vite dev server, and the MCP endpoint at `/__ng-devtools/__mcp`. The Analog tab shows:

- Routes: every page, layout and markdown file with its URL, route groups, `[param]` and catch-all segments, `.server.ts` files and routeMeta. Test a URL to see which files render it.
- Server: page renders (server rendered or client only), `load()` fetches, server functions and API calls with status, time and a redacted preview, plus a request playground for API routes. A `load()` that runs on the server and again in the browser is flagged.
- Render: SSR, prerendered or client only per page, from config, build output and the last request.
- Content and Lint: markdown files, and checks for duplicate URLs, missing default exports, layouts without `<router-outlet>`, orphan `.server.ts` files, API method suffixes, prerender entries and frontmatter.

The Analog tab appears only in Analog apps, and the Routes tab and Dashboard switch to Analog's file routes and SSR setting there. Tested with Analog 2.7 on Angular 20 (a fresh app from the official template, npm and pnpm) and Angular 22. The demo lives in `examples/analog` (`pnpm analog:dev`).

#### Agent Resources

| Resource | Content |
Expand Down
32 changes: 27 additions & 5 deletions app/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, signal, OnInit, OnDestroy } from '@angular/core';
import { Component, computed, signal, OnInit, OnDestroy } from '@angular/core';
import { connectDevframe, type DevframeRpcClient } from 'devframe/client';
import { Dashboard } from './pages/dashboard';
import { ComponentTree } from './pages/component-tree';
Expand All @@ -7,8 +7,10 @@ import { SignalInspector } from './pages/signal-inspector';
import { DiInspector } from './pages/di-inspector';
import { StoreInspector } from './pages/store-inspector';
import { FormsInspector } from './pages/forms-inspector';
import { AnalogInspector } from './pages/analog-inspector';

type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'store' | 'forms';
type Tab =
'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'store' | 'forms' | 'analog';

@Component({
selector: 'app-root',
Expand All @@ -20,6 +22,7 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
DiInspector,
StoreInspector,
FormsInspector,
AnalogInspector,
],
template: `
<header>
Expand Down Expand Up @@ -50,7 +53,7 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
<span>Angular DevTools</span>
</h1>
<nav>
@for (t of tabs; track t.id) {
@for (t of tabs(); track t.id) {
<button [class.active]="tab() === t.id" (click)="switchTab(t.id)">{{ t.label }}</button>
}
</nav>
Expand Down Expand Up @@ -85,6 +88,9 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
(focusHandled)="formFocus.set(null)"
/>
}
@case ('analog') {
<app-analog-inspector [rpc]="rpc()" />
}
}
</main>
`,
Expand Down Expand Up @@ -167,15 +173,18 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
`,
})
export class App implements OnInit, OnDestroy {
readonly tabs = [
readonly analog = signal(false);
private readonly allTabs = [
{ id: 'dashboard' as Tab, label: 'Dashboard' },
{ id: 'analog' as Tab, label: 'Analog' },
{ id: 'components' as Tab, label: 'Components' },
{ id: 'routes' as Tab, label: 'Routes' },
{ id: 'signals' as Tab, label: 'Signals' },
{ id: 'injectors' as Tab, label: 'Injectors' },
{ id: 'store' as Tab, label: 'Store' },
{ id: 'forms' as Tab, label: 'Forms' },
];
readonly tabs = computed(() => this.allTabs.filter((t) => t.id !== 'analog' || this.analog()));

tab = signal<Tab>('dashboard');
rpc = signal<DevframeRpcClient | null>(null);
Expand All @@ -185,14 +194,27 @@ export class App implements OnInit, OnDestroy {
// Deep link: read tab from hash
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
const hashTab = params.get('tab');
if (hashTab && this.tabs.some((t) => t.id === hashTab)) {
if (hashTab && this.allTabs.some((t) => t.id === hashTab)) {
this.tab.set(hashTab as Tab);
}

const baseURL = detectBaseURL();
connectDevframe(baseURL ? { baseURL } : {}).then((client) => {
this.rpc.set(client);
this.connected.set(true);
const scoped = client.scope('ng-devtools').rpc as unknown as {
call: (name: string) => Promise<unknown>;
};
scoped.call('analog-project').then(
(project) => {
const isAnalog = !!(project as { analog?: boolean } | null)?.analog;
this.analog.set(isAnalog);
if (!isAnalog && this.tab() === 'analog') this.tab.set('dashboard');
},
() => {
if (this.tab() === 'analog') this.tab.set('dashboard');
},
);
client.events.on('connection:status', (status) => {
this.connected.set(status === 'connected');
});
Expand Down
Loading
Loading