Skip to content
Draft
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
66 changes: 53 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,29 @@ When embedded in Express, the MCP endpoint is also available over HTTP at `/__ng

MCP clients see these with an underscore, as `ng-devtools_get-routes`.

| Tool | Description |
| ---------------------------------- | ----------------------------------------------------------- |
| `ng-devtools:get-routes` | List Angular routes from source |
| `ng-devtools:get-components` | Discover components and directives, with inputs and outputs |
| `ng-devtools:get-signals` | Signal declarations from source |
| `ng-devtools:get-providers` | DI providers from source |
| `ng-devtools:build-meta` | Angular/TS versions, SSR status |
| `ng-devtools:highlight` | Highlight a component in the page |
| `ng-devtools:inspect-signals` | Signal graph a connected page reported |
| `ng-devtools:inspect-providers` | Injector tree a connected page reported |
| `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 |
| Tool | Description |
| ----------------------------------- | ----------------------------------------------------------- |
| `ng-devtools:get-routes` | List Angular routes from source |
| `ng-devtools:get-components` | Discover components and directives, with inputs and outputs |
| `ng-devtools:get-signals` | Signal declarations from source |
| `ng-devtools:get-providers` | DI providers from source |
| `ng-devtools:build-meta` | Angular/TS versions, SSR status |
| `ng-devtools:highlight` | Highlight a component in the page |
| `ng-devtools:inspect-signals` | Signal graph a connected page reported |
| `ng-devtools:inspect-providers` | Injector tree a connected page reported |
| `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: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 All @@ -113,6 +123,36 @@ 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.

#### 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';

export default defineConfig({
plugins: [analog(), ngDevtools()],
});
```

```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 @@ -81,6 +84,9 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
@case ('forms') {
<app-forms-inspector [rpc]="rpc()" />
}
@case ('analog') {
<app-analog-inspector [rpc]="rpc()" />
}
}
</main>
`,
Expand Down Expand Up @@ -163,15 +169,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 @@ -181,14 +190,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