From c940b08b7a3a1639d2ae237b8f261a096c401128 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 20 Sep 2026 18:29:55 +0300 Subject: [PATCH 1/8] fix: repair devtools build, CLI entry, and test suite - use .ts import specifiers in devframe.ts; the package has no build step and Node's type stripping does not remap .js to .ts - enable allowImportingTsExtensions and rewriteRelativeImportExtensions in tsconfig.app.json so ng build still compiles src/server.ts - declare cac, an optional peer of devframe that nothing installs but both bin.mjs call through createCac - provide Router in app.spec.ts and drop the assertion on an h1 that app.html no longer renders --- packages/ng-devtools/package.json | 1 + packages/ng-devtools/src/devframe.ts | 12 ++++++------ pnpm-lock.yaml | 20 +++++++++++++++----- src/app/app.spec.ts | 7 +++++-- tsconfig.app.json | 4 +++- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/ng-devtools/package.json b/packages/ng-devtools/package.json index a80394f..a851130 100644 --- a/packages/ng-devtools/package.json +++ b/packages/ng-devtools/package.json @@ -25,6 +25,7 @@ "mcp" ], "dependencies": { + "cac": "^7.0.0", "devframe": "^1.0.0", "valibot": "^1.5.0" }, diff --git a/packages/ng-devtools/src/devframe.ts b/packages/ng-devtools/src/devframe.ts index 412cf7f..0eaab82 100644 --- a/packages/ng-devtools/src/devframe.ts +++ b/packages/ng-devtools/src/devframe.ts @@ -1,11 +1,11 @@ import type { RemoteAssets } from 'devframe' import { defineDevframe } from 'devframe' -import { getRoutes } from './rpc/get-routes.js' -import { getComponents } from './rpc/get-components.js' -import { getBuildMeta } from './rpc/build-meta.js' -import { getSignals } from './rpc/get-signals.js' -import { getProviders } from './rpc/get-providers.js' -import type {} from './types.js' +import { getRoutes } from './rpc/get-routes.ts' +import { getComponents } from './rpc/get-components.ts' +import { getBuildMeta } from './rpc/build-meta.ts' +import { getSignals } from './rpc/get-signals.ts' +import { getProviders } from './rpc/get-providers.ts' +import type {} from './types.ts' import pkg from '../package.json' with { type: 'json' } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89fd42b..4f5bf58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: link:packages/ng-devtools devframe: specifier: ^1.0.0 - version: 1.0.0(@devframes/agentic@1.0.0)(srvx@1.0.5) + version: 1.0.0(@devframes/agentic@1.0.0)(cac@7.0.0)(srvx@1.0.5) express: specifier: ^5.1.0 version: 5.2.1 @@ -99,9 +99,12 @@ importers: '@devframes/agentic': specifier: ^1.0.0 version: 1.0.0(crossws@0.4.12(srvx@1.0.5))(devframe@1.0.0) + cac: + specifier: ^7.0.0 + version: 7.0.0 devframe: specifier: ^1.0.0 - version: 1.0.0(@devframes/agentic@1.0.0)(srvx@1.0.5) + version: 1.0.0(@devframes/agentic@1.0.0)(cac@7.0.0)(srvx@1.0.5) valibot: specifier: ^1.5.0 version: 1.5.0(typescript@6.0.3) @@ -1813,6 +1816,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3376,7 +3383,7 @@ snapshots: dependencies: '@modelcontextprotocol/client': 2.0.0 '@modelcontextprotocol/server': 2.0.0 - devframe: 1.0.0(@devframes/agentic@1.0.0)(srvx@1.0.5) + devframe: 1.0.0(@devframes/agentic@1.0.0)(cac@7.0.0)(srvx@1.0.5) h3: 2.0.1-rc.32(crossws@0.4.12(srvx@1.0.5)) pathe: 2.0.3 transitivePeerDependencies: @@ -3385,7 +3392,7 @@ snapshots: '@devframes/vite@1.0.0(devframe@1.0.0)(vite@8.3.0(@types/node@20.19.43)(esbuild@0.28.2)(sass@1.101.0))': dependencies: - devframe: 1.0.0(@devframes/agentic@1.0.0)(srvx@1.0.5) + devframe: 1.0.0(@devframes/agentic@1.0.0)(cac@7.0.0)(srvx@1.0.5) pathe: 2.0.3 optionalDependencies: vite: 8.3.0(@types/node@20.19.43)(esbuild@0.28.2)(sass@1.101.0) @@ -4367,6 +4374,8 @@ snapshots: bytes@3.1.2: {} + cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4476,7 +4485,7 @@ snapshots: detect-libc@2.1.2: {} - devframe@1.0.0(@devframes/agentic@1.0.0)(srvx@1.0.5): + devframe@1.0.0(@devframes/agentic@1.0.0)(cac@7.0.0)(srvx@1.0.5): dependencies: crossws: 0.4.12(srvx@1.0.5) h3: 2.0.1-rc.32(crossws@0.4.12(srvx@1.0.5)) @@ -4484,6 +4493,7 @@ snapshots: pathe: 2.0.3 optionalDependencies: '@devframes/agentic': 1.0.0(crossws@0.4.12(srvx@1.0.5))(devframe@1.0.0) + cac: 7.0.0 transitivePeerDependencies: - ocache - srvx diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index bb1b59a..526f3bf 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,10 +1,12 @@ import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { App } from './app'; describe('App', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [App], + providers: [provideRouter([])], }) .compileComponents(); }); @@ -15,10 +17,11 @@ describe('App', () => { expect(app).toBeTruthy(); }); - it('should render title', async () => { + it('should render the nav links', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-devtools'); + const hrefs = Array.from(compiled.querySelectorAll('nav a')).map((a) => a.getAttribute('href')); + expect(hrefs).toEqual(['/', '/about']); }); }); diff --git a/tsconfig.app.json b/tsconfig.app.json index 11c10da..e7e0a12 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -5,7 +5,9 @@ "compilerOptions": { "types": [ "node" - ] + ], + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true }, "include": [ "src/**/*.ts" From 08477a4fb8796988042b2abbbb2465bc35a846c3 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 20 Sep 2026 19:15:19 +0300 Subject: [PATCH 2/8] feat: declare input schemas for the selector-taking agent tools highlight, inspect-signals and inspect-providers each advertised `inputSchema: { type: 'object', properties: {} }` while their handlers read `args.selector`, so an MCP client had no way to discover the argument and a wrong guess failed silently. --- packages/ng-devtools/src/devframe.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/ng-devtools/src/devframe.ts b/packages/ng-devtools/src/devframe.ts index 0eaab82..4437a34 100644 --- a/packages/ng-devtools/src/devframe.ts +++ b/packages/ng-devtools/src/devframe.ts @@ -137,6 +137,13 @@ const ngDevtools = defineDevframe({ id: 'ng-devtools:highlight', description: 'Highlight a component in the running Angular app by its selector.', safety: 'action', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'CSS selector of the component to highlight, e.g. app-root.' }, + }, + required: ['selector'], + }, handler: async (args: { selector: string }) => { await ctx.rpc.invokeLocal('ng-devtools:select-component' as any, args.selector) void my.rpc.broadcast({ method: 'highlight-in-page', args: [args.selector], optional: true }) @@ -148,6 +155,13 @@ const ngDevtools = defineDevframe({ id: 'ng-devtools:inspect-signals', description: 'Get the signal graph for a specific component by CSS selector. Returns signal nodes (signal, computed, linkedSignal, effect) and their dependency edges. Call this to understand reactive data flow before suggesting state changes.', safety: 'read', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'CSS selector of the component to inspect, e.g. app-root.' }, + }, + required: ['selector'], + }, handler: async (args: { selector: string }) => { try { const result = await my.rpc.broadcast({ method: 'get-signal-graph-for', args: [args.selector] }) @@ -163,6 +177,13 @@ const ngDevtools = defineDevframe({ id: 'ng-devtools:inspect-providers', description: 'Get DI providers and the injector resolution path for a component by CSS selector. Call this to understand dependency injection before suggesting provider changes.', safety: 'read', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'CSS selector of the component to inspect, e.g. app-root.' }, + }, + required: ['selector'], + }, handler: async (args: { selector: string }) => { try { const result = await my.rpc.broadcast({ method: 'get-providers-for', args: [args.selector] }) From 8a7e7cfd45493484f070fb228ce22f879447cd24 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 21 Sep 2026 19:56:16 +0300 Subject: [PATCH 3/8] ci: add workflow for tests and builds Runs on push and pull requests to main: frozen-lockfile install, then the test suite, the devtools UI build, the SSR app build, the Chrome extension build, and a CLI smoke test. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..58aa55e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v5 + + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Test + run: pnpm test + + - name: Build devtools UI + run: pnpm devtools:build + + - name: Build Angular host app (SSR) + run: pnpm build + + - name: Build Chrome extension + run: pnpm extension:build + + - name: Smoke-test the CLI + run: node bin.mjs --help From 46cc2201970ace51362b0dd4508f7310ac0a353b Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 21 Sep 2026 20:33:35 +0300 Subject: [PATCH 4/8] ci: restrict workflow token to read-only contents --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58aa55e..2fc3db5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 0c05612c6d9c6dfba307581b04ae3b0478850a9c Mon Sep 17 00:00:00 2001 From: erKam Date: Tue, 22 Sep 2026 11:00:35 +0300 Subject: [PATCH 5/8] fix: report the right component for lazy and component-less routes (#4) * fix: report the right component for lazy and component-less routes Lazy routes were reported as `import`, and a route without a component took the next route's, because each search ran to the end of the file. Scope each route to the text before the next `path:` and match eager and lazy components explicitly. Adds the first scanner tests, run with `pnpm test:devtools`. * fix: read each route from its own object literal Splitting at each `path:` broke when `component` came before `path`, let a parent route pick up its child's component, and let the lazy match run into later properties such as a resolver's `.then(...)`. Walk the file's object literals, skipping strings and comments, and read `path`, `component` and `loadComponent` from each object's top-level properties only. * fix: ignore comments when reading route properties A comment above `path` hid the whole route, and one before `loadComponent` hid its component, because property text still carried the comment. Strip comments from the file once, respecting strings, so neither the object scan nor the property split sees them. * fix: only read routes from array elements A nested object such as `data: { path: 'label' }` was reported as its own route. Only take object literals whose direct parent is an array, which covers the routes array, `children` and `provideRouter([...])`. * fix: only read routes from route configuration arrays Objects were taken from any array, so a nested metadata array such as `data: { breadcrumbs: [{ path: 'label' }] }` produced a phantom route. Track which arrays hold routes while scanning: the route configuration itself (a top-level array, or one passed to provideRouter, forRoot or forChild) and `children` arrays. Objects in any other array, including `providers` and metadata under `data`, are no longer reported as routes, while routes nested below a `children` array still are. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HiFGG73GY1m7uGtSQjhVNo --------- Co-authored-by: Claude Opus 5 --- package.json | 1 + .../src/rpc/__tests__/get-routes.test.ts | 166 ++++++++++++++++++ packages/ng-devtools/src/rpc/get-routes.ts | 105 ++++++++++- 3 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 packages/ng-devtools/src/rpc/__tests__/get-routes.test.ts diff --git a/package.json b/package.json index 868029b..f273bf7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", + "test:devtools": "vitest run --root packages/ng-devtools", "serve:ssr:angular-devtools": "node dist/angular-devtools/server/server.mjs", "devtools:dev": "vite --config app/vite.config.ts", "devtools:build": "vite build --config app/vite.config.ts", diff --git a/packages/ng-devtools/src/rpc/__tests__/get-routes.test.ts b/packages/ng-devtools/src/rpc/__tests__/get-routes.test.ts new file mode 100644 index 0000000..b77a29f --- /dev/null +++ b/packages/ng-devtools/src/rpc/__tests__/get-routes.test.ts @@ -0,0 +1,166 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { getRoutes } from '../get-routes.ts' + +let dir: string + +afterEach(() => rmSync(dir, { recursive: true, force: true })) + +async function routesFor(source: string) { + dir = mkdtempSync(join(tmpdir(), 'ng-devtools-routes-')) + mkdirSync(join(dir, 'src')) + writeFileSync(join(dir, 'src', 'app.routes.ts'), source) + const { handler } = getRoutes.setup({ cwd: dir } as never) + return handler() +} + +describe('get-routes', () => { + it('reads an eager component', async () => { + const routes = await routesFor(`[{ path: 'about', component: AboutComponent }]`) + expect(routes).toEqual([ + { path: 'about', component: 'AboutComponent', hasChildren: false, file: 'src/app.routes.ts' }, + ]) + }) + + it('reads the export name of a lazy component', async () => { + const routes = await routesFor(`[ + { path: '', loadComponent: () => import('./home').then(m => m.Home) }, + { + path: 'settings', + loadComponent: () => + import('./settings').then((mod) => mod.Settings), + }, + ]`) + expect(routes.map(r => r.component)).toEqual(['Home', 'Settings']) + }) + + it('reports no component for a lazy default export', async () => { + const routes = await routesFor(`[{ path: '', loadComponent: () => import('./home') }]`) + expect(routes[0].component).toBeUndefined() + }) + + it('does not take the component of the next route', async () => { + const routes = await routesFor(`[ + { path: 'old', redirectTo: 'about', pathMatch: 'full' }, + { path: 'about', component: AboutComponent }, + ]`) + expect(routes.map(r => r.component)).toEqual([undefined, 'AboutComponent']) + }) + + it('reads the component whatever the property order', async () => { + const routes = await routesFor(`[ + { component: HomeComponent, path: 'home' }, + { path: 'admin', children: [{ component: UsersComponent, path: 'users' }] }, + ]`) + expect(routes.map(r => [r.path, r.component])).toEqual([ + ['home', 'HomeComponent'], + ['admin', undefined], + ['users', 'UsersComponent'], + ]) + }) + + it('only reads the lazy component from loadComponent', async () => { + const routes = await routesFor(`[{ + path: 'lazy', + loadComponent: () => import('./lazy'), + resolve: { data: () => import('./data').then(m => m.Data) }, + }]`) + expect(routes.map(r => r.component)).toEqual([undefined]) + }) + + it('ignores braces and paths inside strings and comments', async () => { + const routes = await routesFor(`[ + // { path: 'commented', component: Nope }, + { path: 'about', title: 'About {us}', component: AboutComponent }, + ]`) + expect(routes.map(r => [r.path, r.component])).toEqual([['about', 'AboutComponent']]) + }) + + it('reads routes that contain comments', async () => { + const routes = await routesFor(`[ + { + // landing page + path: 'home', + component: HomeComponent, + }, + { + path: 'shop', + /* lazy (see [docs]) */ loadComponent: () => import('./shop').then(m => m.Shop), + }, + ]`) + expect(routes.map(r => [r.path, r.component])).toEqual([ + ['home', 'HomeComponent'], + ['shop', 'Shop'], + ]) + }) + + it('does not treat nested route metadata as a route', async () => { + const routes = await routesFor(`[ + { path: 'home', component: HomeComponent, data: { path: 'label', breadcrumb: 'Home' } }, + ]`) + expect(routes.map(r => [r.path, r.component])).toEqual([['home', 'HomeComponent']]) + }) + + it('does not treat objects in metadata arrays as routes', async () => { + const routes = await routesFor(`[ + { + path: 'home', + component: HomeComponent, + data: { breadcrumbs: [{ path: 'label', component: Nope }] }, + }, + ]`) + expect(routes.map(r => [r.path, r.component])).toEqual([['home', 'HomeComponent']]) + }) + + it('does not treat objects in a providers array as routes', async () => { + const routes = await routesFor(`[ + { + path: 'home', + component: HomeComponent, + providers: [{ provide: CONFIG, useValue: { path: 'nope' } }], + }, + ]`) + expect(routes.map(r => r.path)).toEqual(['home']) + }) + + it('reads routes nested in metadata below a children array', async () => { + const routes = await routesFor(`[ + { + path: 'admin', + children: [ + { path: 'users', component: Users, data: { tabs: [{ path: 'nope' }] } }, + ], + }, + ]`) + expect(routes.map(r => r.path)).toEqual(['admin', 'users']) + }) + + it('reads routes passed straight to provideRouter', async () => { + const routes = await routesFor( + `bootstrapApplication(App, { providers: [provideRouter([{ path: 'home', component: HomeComponent }])] })`, + ) + expect(routes.map(r => [r.path, r.component])).toEqual([['home', 'HomeComponent']]) + }) + + it('reads routes passed straight to RouterModule.forRoot', async () => { + const routes = await routesFor( + `@NgModule({ imports: [RouterModule.forRoot([{ path: 'home', component: HomeComponent }])] }) + export class AppRoutingModule {}`, + ) + expect(routes.map(r => [r.path, r.component])).toEqual([['home', 'HomeComponent']]) + }) + + it('only flags children on the route that has them', async () => { + const routes = await routesFor(`[ + { path: 'admin', component: Admin, children: [{ path: 'users', component: Users }] }, + { path: 'about', component: About }, + ]`) + expect(routes.map(r => [r.path, r.hasChildren])).toEqual([ + ['admin', true], + ['users', false], + ['about', false], + ]) + }) +}) diff --git a/packages/ng-devtools/src/rpc/get-routes.ts b/packages/ng-devtools/src/rpc/get-routes.ts index 0e6adf5..08ff532 100644 --- a/packages/ng-devtools/src/rpc/get-routes.ts +++ b/packages/ng-devtools/src/rpc/get-routes.ts @@ -61,14 +61,14 @@ function findRouteFiles( const content = readFileSync(full, 'utf-8') const relPath = relative(cwd, full) - for (const match of content.matchAll(/path:\s*['"`]([^'"`]*)['"`]/g)) { - const after = content.slice(match.index!) - const componentMatch = after.match(/(?:component|loadComponent).*?(\w+)/) - const hasChildren = /children\s*:\s*\[/.test(after.slice(0, 200)) + for (const body of objectLiterals(stripComments(content))) { + const props = topLevelProps(body) + const path = props.get('path')?.match(/^['"`]([^'"`]*)['"`]$/)?.[1] + if (path === undefined) continue routes.push({ - path: match[1], - component: componentMatch?.[1], - hasChildren, + path, + component: routeComponent(props), + hasChildren: props.has('children'), file: relPath, }) } @@ -77,3 +77,94 @@ function findRouteFiles( } } } + +function routeComponent(props: Map): string | undefined { + const eager = props.get('component')?.match(/^(\w+)/)?.[1] + if (eager) return eager + return props.get('loadComponent')?.match(/\.then\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*\1\.(\w+)/)?.[2] +} + +type Bracket = { ch: string; at: number; routeArray: boolean; routeObject: boolean } + +// An array holds routes when it is the route configuration itself (a top-level +// array, or one passed to provideRouter/forRoot/forChild) or a `children` array. +// Any other array is metadata, so objects inside it are never routes. +const ROUTE_ARRAY = /(?:\bchildren\s*:|\b(?:provideRouter|forRoot|forChild)\s*\()\s*$/ + +function objectLiterals(source: string): string[] { + const spans: [number, number][] = [] + const open: Bracket[] = [] + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (ch === '"' || ch === "'" || ch === '`') i = skipString(source, i) + else if ('([{'.includes(ch)) { + const parent = open.at(-1) + open.push({ + ch, + at: i, + routeArray: ch === '[' && (!parent || ROUTE_ARRAY.test(source.slice(Math.max(0, i - 64), i))), + routeObject: ch === '{' && parent?.ch === '[' && parent.routeArray, + }) + } + else if (')]}'.includes(ch)) { + const closed = open.pop() + if (ch === '}' && closed?.routeObject) spans.push([closed.at, i]) + } + } + return spans.sort((a, b) => a[0] - b[0]).map(([start, end]) => source.slice(start + 1, end)) +} + +function topLevelProps(body: string): Map { + const props = new Map() + const add = (text: string) => { + const prop = text.match(/^\s*(\w+)\s*:\s*([\s\S]*?)\s*$/) + if (prop) props.set(prop[1], prop[2]) + } + let depth = 0 + let start = 0 + for (let i = 0; i < body.length; i++) { + const ch = body[i] + if (ch === '"' || ch === "'" || ch === '`') i = skipString(body, i) + else if ('([{'.includes(ch)) depth++ + else if (')]}'.includes(ch)) depth-- + else if (ch === ',' && depth === 0) { + add(body.slice(start, i)) + start = i + 1 + } + } + add(body.slice(start)) + return props +} + +function skipString(source: string, start: number): number { + for (let i = start + 1; i < source.length; i++) { + if (source[i] === '\\') i++ + else if (source[i] === source[start]) return i + } + return source.length +} + +function stripComments(source: string): string { + let out = '' + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (ch === '"' || ch === "'" || ch === '`') { + const end = skipString(source, i) + out += source.slice(i, end + 1) + i = end + } + else if (source.startsWith('//', i)) { + const end = source.indexOf('\n', i) + i = (end === -1 ? source.length : end) - 1 + } + else if (source.startsWith('/*', i)) { + const end = source.indexOf('*/', i + 2) + i = end === -1 ? source.length : end + 1 + out += ' ' + } + else { + out += ch + } + } + return out +} From d6127610bccac0c061ea3a6df37089303c0f7d69 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 22 Sep 2026 10:24:50 +0000 Subject: [PATCH 6/8] chore: format the repo with Prettier The repo carries a .prettierrc that nothing ran, so 43 files had drifted from it. Run `prettier --write .` over the tree and add a .prettierignore for the directories that hold generated output (dist, .angular, extension/ui, the assets package's dist) and the lockfile. No behaviour changes: whitespace, quoting and wrapping only. --- .prettierignore | 6 + README.md | 60 ++- angular.json | 4 +- app/index.html | 16 +- app/main.ts | 10 +- app/src/app.ts | 162 +++++--- app/src/pages/component-tree.ts | 166 +++++--- app/src/pages/dashboard.ts | 104 +++-- app/src/pages/di-inspector.ts | 354 +++++++++++------ app/src/pages/route-inspector.ts | 130 +++++-- app/src/pages/signal-inspector.ts | 363 +++++++++++++----- app/vite.config.ts | 18 +- bin.mjs | 6 +- docs/privacy-policy.html | 182 ++++++--- extension/background.js | 32 +- extension/content-script.js | 14 +- extension/detect-angular.js | 19 +- extension/devtools.html | 8 +- extension/devtools.js | 20 +- extension/panel-bridge.js | 40 +- extension/panel.html | 35 +- packages/ng-devtools/bin.mjs | 6 +- packages/ng-devtools/src/devframe.ts | 144 ++++--- packages/ng-devtools/src/overlay.ts | 301 ++++++++------- .../src/rpc/__tests__/get-routes.test.ts | 122 +++--- packages/ng-devtools/src/rpc/build-meta.ts | 47 +-- .../ng-devtools/src/rpc/get-components.ts | 68 ++-- packages/ng-devtools/src/rpc/get-providers.ts | 109 +++--- packages/ng-devtools/src/rpc/get-routes.ts | 147 ++++--- packages/ng-devtools/src/rpc/get-signals.ts | 64 +-- packages/ng-devtools/src/types.ts | 90 ++--- src/app/app.config.server.ts | 4 +- src/app/app.config.ts | 5 +- src/app/app.routes.server.ts | 4 +- src/app/app.spec.ts | 3 +- src/app/app.ts | 4 +- src/app/pages/about.ts | 13 +- src/app/pages/home.ts | 14 +- src/index.html | 20 +- src/main.server.ts | 3 +- src/main.ts | 3 +- src/server.ts | 4 +- tsconfig.app.json | 12 +- tsconfig.spec.json | 9 +- 44 files changed, 1800 insertions(+), 1145 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ff55408 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +.angular +pnpm-lock.yaml +extension/ui +packages/ng-devtools-assets/dist diff --git a/README.md b/README.md index 89a2e80..85acaf0 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ Add the devframe middleware to your Express server: ```ts // server.ts -import { initDevframe } from 'devframe/initiate' -import ngDevtools from '@santoshyadavdev/ng-devtools/devframe' +import { initDevframe } from 'devframe/initiate'; +import ngDevtools from '@santoshyadavdev/ng-devtools/devframe'; -const devtools = initDevframe(ngDevtools, { base: '/__ng-devtools/' }) -app.use(devtools.nodeMiddleware) +const devtools = initDevframe(ngDevtools, { base: '/__ng-devtools/' }); +app.use(devtools.nodeMiddleware); ``` Open `http://localhost:4000/__ng-devtools/` to see the devtools UI. @@ -87,22 +87,22 @@ When embedded in Express, the MCP endpoint is also available over HTTP at `/__ng #### Agent Tools -| Tool | Description | -|---|---| -| `ng-devtools:get-routes` | List Angular routes from source | -| `ng-devtools:get-components` | Discover components, inputs, outputs | -| `ng-devtools:build-meta` | Angular/TS versions, SSR status | -| `ng-devtools:highlight` | Highlight a component in the page | -| `ng-devtools:inspect-signals` | Signal graph for a component | -| `ng-devtools:inspect-providers` | DI providers and resolution path | +| Tool | Description | +| ------------------------------- | ------------------------------------ | +| `ng-devtools:get-routes` | List Angular routes from source | +| `ng-devtools:get-components` | Discover components, inputs, outputs | +| `ng-devtools:build-meta` | Angular/TS versions, SSR status | +| `ng-devtools:highlight` | Highlight a component in the page | +| `ng-devtools:inspect-signals` | Signal graph for a component | +| `ng-devtools:inspect-providers` | DI providers and resolution path | #### Agent Resources -| Resource | Content | -|---|---| +| Resource | Content | +| ---------------------------- | ------------------------ | | `ng-devtools:component-tree` | Live component hierarchy | -| `ng-devtools:signal-graph` | Signal dependency graph | -| `ng-devtools:injector-tree` | DI injector hierarchy | +| `ng-devtools:signal-graph` | Signal dependency graph | +| `ng-devtools:injector-tree` | DI injector hierarchy | ### Vite DevTools Dock @@ -110,9 +110,9 @@ Mount as a dock panel inside Vite DevTools: ```ts // vite.config.ts -import { viteDevframeHub } from '@devframes/vite/hub' -import { createUi } from '@devframes/hub-ui' -import ngDevtools from '@santoshyadavdev/ng-devtools/devframe' +import { viteDevframeHub } from '@devframes/vite/hub'; +import { createUi } from '@devframes/hub-ui'; +import ngDevtools from '@santoshyadavdev/ng-devtools/devframe'; export default defineConfig({ plugins: [ @@ -121,7 +121,7 @@ export default defineConfig({ ui: createUi({ branding: { productName: 'Angular DevTools' } }), }), ], -}) +}); ``` ### Chrome DevTools Extension @@ -133,9 +133,9 @@ See the [Chrome Extension](#chrome-devtools-extension-1) section below for how t The overlay runs inside the user's Angular page and collects live component, signal, and DI data: ```ts -import { initOverlay } from '@santoshyadavdev/ng-devtools/overlay' +import { initOverlay } from '@santoshyadavdev/ng-devtools/overlay'; -const dispose = await initOverlay() +const dispose = await initOverlay(); ``` ## Development @@ -161,9 +161,9 @@ pnpm start The devtool ships as two npm packages: -| Package | Contents | -|---|---| -| `@santoshyadavdev/ng-devtools` | Node-side logic, RPC, CLI, overlay | +| Package | Contents | +| ------------------------------------- | ------------------------------------------------------ | +| `@santoshyadavdev/ng-devtools` | Node-side logic, RPC, CLI, overlay | | `@santoshyadavdev/ng-devtools-assets` | Built SPA (served at runtime via CDN or local install) | ```sh @@ -216,11 +216,7 @@ extension/ ```js // devtools.js — creates the panel in Chrome DevTools -chrome.devtools.panels.create( - 'Angular', - 'icon-128.png', - 'panel.html' -) +chrome.devtools.panels.create('Angular', 'icon-128.png', 'panel.html'); ``` ### 4. `extension/panel.html` @@ -231,7 +227,9 @@ This is where the built SPA loads. Copy the built assets (`dist/devtools-ui/`) i - + + + diff --git a/angular.json b/angular.json index 1cc56d3..f2daf6b 100644 --- a/angular.json +++ b/angular.json @@ -24,9 +24,7 @@ "input": "public" } ], - "styles": [ - "src/styles.css" - ], + "styles": ["src/styles.css"], "server": "src/main.server.ts", "outputMode": "server", "security": { diff --git a/app/index.html b/app/index.html index 0cab928..d724123 100644 --- a/app/index.html +++ b/app/index.html @@ -5,8 +5,20 @@ Angular DevTools diff --git a/app/main.ts b/app/main.ts index e8c9efd..794270a 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,6 +1,6 @@ -import 'zone.js' -import '@angular/compiler' -import { bootstrapApplication } from '@angular/platform-browser' -import { App } from './src/app' +import 'zone.js'; +import '@angular/compiler'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { App } from './src/app'; -bootstrapApplication(App).catch(console.error) +bootstrapApplication(App).catch(console.error); diff --git a/app/src/app.ts b/app/src/app.ts index 450085c..d2a9207 100644 --- a/app/src/app.ts +++ b/app/src/app.ts @@ -1,12 +1,12 @@ -import { Component, signal, OnInit, OnDestroy } from '@angular/core' -import { connectDevframe, type DevframeRpcClient } from 'devframe/client' -import { Dashboard } from './pages/dashboard' -import { ComponentTree } from './pages/component-tree' -import { RouteInspector } from './pages/route-inspector' -import { SignalInspector } from './pages/signal-inspector' -import { DiInspector } from './pages/di-inspector' +import { Component, signal, OnInit, OnDestroy } from '@angular/core'; +import { connectDevframe, type DevframeRpcClient } from 'devframe/client'; +import { Dashboard } from './pages/dashboard'; +import { ComponentTree } from './pages/component-tree'; +import { RouteInspector } from './pages/route-inspector'; +import { SignalInspector } from './pages/signal-inspector'; +import { DiInspector } from './pages/di-inspector'; -type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' +type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors'; @Component({ selector: 'app-root', @@ -14,19 +14,23 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' template: `
- - - - + + + + Angular DevTools
@@ -35,36 +39,84 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors'
@switch (tab()) { - @case ('dashboard') { } - @case ('components') { } - @case ('routes') { } - @case ('signals') { } - @case ('injectors') { } + @case ('dashboard') { + + } + @case ('components') { + + } + @case ('routes') { + + } + @case ('signals') { + + } + @case ('injectors') { + + } }
`, styles: ` - :host { display: flex; flex-direction: column; height: 100vh; } + :host { + display: flex; + flex-direction: column; + height: 100vh; + } header { - display: flex; align-items: center; gap: 16px; + display: flex; + align-items: center; + gap: 16px; padding: 8px 16px; - background: #18181b; border-bottom: 1px solid #27272a; + background: #18181b; + border-bottom: 1px solid #27272a; + } + .brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: #a78bfa; + } + nav { + display: flex; + gap: 4px; + flex: 1; } - .brand { display: flex; align-items: center; gap: 8px; font-weight: 600; color: #a78bfa; } - nav { display: flex; gap: 4px; flex: 1; } nav button { - padding: 6px 14px; border: none; border-radius: 6px; - background: transparent; color: #a1a1aa; cursor: pointer; - font-size: 13px; transition: all 0.15s; + padding: 6px 14px; + border: none; + border-radius: 6px; + background: transparent; + color: #a1a1aa; + cursor: pointer; + font-size: 13px; + transition: all 0.15s; + } + nav button:hover { + background: #27272a; + color: #e4e4e7; + } + nav button.active { + background: #3f3f46; + color: #fff; } - nav button:hover { background: #27272a; color: #e4e4e7; } - nav button.active { background: #3f3f46; color: #fff; } .status { - font-size: 12px; padding: 3px 10px; border-radius: 99px; - background: #44403c; color: #a8a29e; + font-size: 12px; + padding: 3px 10px; + border-radius: 99px; + background: #44403c; + color: #a8a29e; + } + .status.connected { + background: #14532d; + color: #4ade80; + } + main { + flex: 1; + overflow: auto; + padding: 16px; } - .status.connected { background: #14532d; color: #4ade80; } - main { flex: 1; overflow: auto; padding: 16px; } `, }) export class App implements OnInit, OnDestroy { @@ -74,28 +126,28 @@ export class App implements OnInit, OnDestroy { { id: 'routes' as Tab, label: 'Routes' }, { id: 'signals' as Tab, label: 'Signals' }, { id: 'injectors' as Tab, label: 'Injectors' }, - ] + ]; - tab = signal('dashboard') - rpc = signal(null) - connected = signal(false) + tab = signal('dashboard'); + rpc = signal(null); + connected = signal(false); ngOnInit() { // Deep link: read tab from hash - const params = new URLSearchParams(location.hash.replace(/^#/, '')) - const hashTab = params.get('tab') + const params = new URLSearchParams(location.hash.replace(/^#/, '')); + const hashTab = params.get('tab'); if (hashTab && this.tabs.some((t) => t.id === hashTab)) { - this.tab.set(hashTab as Tab) + this.tab.set(hashTab as Tab); } - const baseURL = detectBaseURL() + const baseURL = detectBaseURL(); connectDevframe(baseURL ? { baseURL } : {}).then((client) => { - this.rpc.set(client) - this.connected.set(true) + this.rpc.set(client); + this.connected.set(true); client.events.on('connection:status', (status) => { - this.connected.set(status === 'connected') - }) - }) + this.connected.set(status === 'connected'); + }); + }); } ngOnDestroy() { @@ -103,17 +155,17 @@ export class App implements OnInit, OnDestroy { } switchTab(id: Tab) { - this.tab.set(id) - history.replaceState(history.state, '', `#tab=${id}`) + this.tab.set(id); + history.replaceState(history.state, '', `#tab=${id}`); } } // Chrome extension passes ?baseURL=...; embedded uses /__ng-devtools/; standalone uses default function detectBaseURL(): string | undefined { - const params = new URLSearchParams(location.search) - const fromQuery = params.get('baseURL') - if (fromQuery) return fromQuery + const params = new URLSearchParams(location.search); + const fromQuery = params.get('baseURL'); + if (fromQuery) return fromQuery; - if (location.pathname.includes('__ng-devtools')) return undefined - return '/__ng-devtools/' + if (location.pathname.includes('__ng-devtools')) return undefined; + return '/__ng-devtools/'; } diff --git a/app/src/pages/component-tree.ts b/app/src/pages/component-tree.ts index efc169d..7f52324 100644 --- a/app/src/pages/component-tree.ts +++ b/app/src/pages/component-tree.ts @@ -1,13 +1,13 @@ -import { Component, input, signal, effect } from '@angular/core' -import { JsonPipe } from '@angular/common' -import type { DevframeRpcClient } from 'devframe/client' +import { Component, input, signal, effect } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import type { DevframeRpcClient } from 'devframe/client'; interface ComponentInfo { - selector: string - file: string - inputs: string[] - outputs: string[] - isStandalone: boolean + selector: string; + file: string; + inputs: string[]; + outputs: string[]; + isStandalone: boolean; } @Component({ @@ -59,79 +59,137 @@ interface ComponentInfo { } `, styles: ` - .toolbar { display: flex; gap: 8px; margin-bottom: 16px; } + .toolbar { + display: flex; + gap: 8px; + margin-bottom: 16px; + } input { - flex: 1; padding: 8px 12px; background: #18181b; border: 1px solid #27272a; - border-radius: 6px; color: #e4e4e7; font-size: 14px; outline: none; + flex: 1; + padding: 8px 12px; + background: #18181b; + border: 1px solid #27272a; + border-radius: 6px; + color: #e4e4e7; + font-size: 14px; + outline: none; + } + input:focus { + border-color: #a78bfa; } - input:focus { border-color: #a78bfa; } button { - padding: 8px 16px; background: #3f3f46; border: none; border-radius: 6px; - color: #e4e4e7; cursor: pointer; font-size: 13px; + padding: 8px 16px; + background: #3f3f46; + border: none; + border-radius: 6px; + color: #e4e4e7; + cursor: pointer; + font-size: 13px; + } + button:hover { + background: #52525b; + } + .muted { + color: #71717a; + font-size: 14px; + } + .component-list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; } - button:hover { background: #52525b; } - .muted { color: #71717a; font-size: 14px; } - .component-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 8px; } .component-item { - background: #18181b; border: 1px solid #27272a; border-radius: 8px; - padding: 12px 16px; cursor: pointer; transition: border-color 0.15s; - } - .component-item:hover { border-color: #a78bfa; } - .selector { font-family: monospace; font-size: 15px; color: #a78bfa; font-weight: 600; } - .file { font-size: 12px; color: #71717a; margin-top: 2px; } - .io { font-size: 13px; color: #a1a1aa; margin-top: 4px; } - .io .label { color: #71717a; } + background: #18181b; + border: 1px solid #27272a; + border-radius: 8px; + padding: 12px 16px; + cursor: pointer; + transition: border-color 0.15s; + } + .component-item:hover { + border-color: #a78bfa; + } + .selector { + font-family: monospace; + font-size: 15px; + color: #a78bfa; + font-weight: 600; + } + .file { + font-size: 12px; + color: #71717a; + margin-top: 2px; + } + .io { + font-size: 13px; + color: #a1a1aa; + margin-top: 4px; + } + .io .label { + color: #71717a; + } .detail { - margin-top: 16px; padding: 16px; background: #18181b; - border: 1px solid #27272a; border-radius: 8px; + margin-top: 16px; + padding: 16px; + background: #18181b; + border: 1px solid #27272a; + border-radius: 8px; + } + .detail h3 { + font-family: monospace; + color: #a78bfa; + margin-bottom: 8px; + } + pre { + font-size: 12px; + color: #a1a1aa; + white-space: pre-wrap; } - .detail h3 { font-family: monospace; color: #a78bfa; margin-bottom: 8px; } - pre { font-size: 12px; color: #a1a1aa; white-space: pre-wrap; } `, }) export class ComponentTree { - rpc = input(null) + rpc = input(null); - components = signal([]) - filter = signal('') - loading = signal(false) - selected = signal(null) + components = signal([]); + filter = signal(''); + loading = signal(false); + selected = signal(null); - filtered = signal([]) + filtered = signal([]); constructor() { effect(() => { - const q = this.filter().toLowerCase() - const all = this.components() - this.filtered.set( - q ? all.filter((c) => c.selector.includes(q) || c.file.includes(q)) : all, - ) - }) + const q = this.filter().toLowerCase(); + const all = this.components(); + this.filtered.set(q ? all.filter((c) => c.selector.includes(q) || c.file.includes(q)) : all); + }); effect(() => { - const client = this.rpc() - if (client) this.refresh() - }) + const client = this.rpc(); + if (client) this.refresh(); + }); } async refresh() { - const client = this.rpc() - if (!client) return - this.loading.set(true) + const client = this.rpc(); + if (!client) return; + this.loading.set(true); try { - const my = client.scope('ng-devtools') - const result = (await my.rpc.call('get-components')) as ComponentInfo[] - this.components.set(result) + const my = client.scope('ng-devtools'); + const result = (await my.rpc.call('get-components')) as ComponentInfo[]; + this.components.set(result); } finally { - this.loading.set(false) + this.loading.set(false); } } select(comp: ComponentInfo) { - this.selected.set(comp) - const client = this.rpc() + this.selected.set(comp); + const client = this.rpc(); if (client) { - client.scope('ng-devtools').rpc.callEvent('select-component', comp.selector) + client.scope('ng-devtools').rpc.callEvent('select-component', comp.selector); } } } diff --git a/app/src/pages/dashboard.ts b/app/src/pages/dashboard.ts index 819d7d2..096261b 100644 --- a/app/src/pages/dashboard.ts +++ b/app/src/pages/dashboard.ts @@ -1,5 +1,5 @@ -import { Component, input, signal, effect, output } from '@angular/core' -import type { DevframeRpcClient } from 'devframe/client' +import { Component, input, signal, effect, output } from '@angular/core'; +import type { DevframeRpcClient } from 'devframe/client'; @Component({ selector: 'app-dashboard', @@ -8,10 +8,14 @@ import type { DevframeRpcClient } from 'devframe/client'

Project

-
Name
{{ meta()?.projectName ?? '…' }}
-
Angular
{{ meta()?.angularVersion ?? '…' }}
-
TypeScript
{{ meta()?.typescript ?? '…' }}
-
SSR
{{ meta()?.ssr ? 'Yes' : 'No' }}
+
Name
+
{{ meta()?.projectName ?? '…' }}
+
Angular
+
{{ meta()?.angularVersion ?? '…' }}
+
TypeScript
+
{{ meta()?.typescript ?? '…' }}
+
SSR
+
{{ meta()?.ssr ? 'Yes' : 'No' }}
@@ -37,41 +41,77 @@ import type { DevframeRpcClient } from 'devframe/client'
`, styles: ` - .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } + .grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + } .card { - background: #18181b; border: 1px solid #27272a; border-radius: 10px; padding: 20px; + background: #18181b; + border: 1px solid #27272a; + border-radius: 10px; + padding: 20px; + } + .card.clickable { + cursor: pointer; + transition: border-color 0.15s; + } + .card.clickable:hover { + border-color: #a78bfa; + } + h3 { + font-size: 13px; + text-transform: uppercase; + color: #71717a; + margin-bottom: 12px; + letter-spacing: 0.05em; + } + dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 6px 12px; + font-size: 14px; + } + dt { + color: #a1a1aa; + } + dd { + color: #e4e4e7; + font-weight: 500; + } + .big { + font-size: 36px; + font-weight: 700; + color: #a78bfa; + } + .sub { + font-size: 13px; + color: #71717a; + margin-top: 4px; } - .card.clickable { cursor: pointer; transition: border-color 0.15s; } - .card.clickable:hover { border-color: #a78bfa; } - h3 { font-size: 13px; text-transform: uppercase; color: #71717a; margin-bottom: 12px; letter-spacing: 0.05em; } - dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; font-size: 14px; } - dt { color: #a1a1aa; } - dd { color: #e4e4e7; font-weight: 500; } - .big { font-size: 36px; font-weight: 700; color: #a78bfa; } - .sub { font-size: 13px; color: #71717a; margin-top: 4px; } `, }) export class Dashboard { - rpc = input(null) - navigate = output() + rpc = input(null); + navigate = output(); - meta = signal(null) - componentCount = signal(0) - routeCount = signal(0) - signalCount = signal(0) - providerCount = signal(0) + meta = signal(null); + componentCount = signal(0); + routeCount = signal(0); + signalCount = signal(0); + providerCount = signal(0); constructor() { effect(() => { - const client = this.rpc() - if (!client) return + const client = this.rpc(); + if (!client) return; - const my = client.scope('ng-devtools') - my.rpc.call('build-meta').then((m: any) => this.meta.set(m)) - my.rpc.call('get-components').then((c: any[]) => this.componentCount.set(c.length)) - my.rpc.call('get-routes').then((r: any[]) => this.routeCount.set(r.length)) - my.rpc.call('get-signals').then((s: any[]) => this.signalCount.set(s.length)) - my.rpc.call('get-providers').then((p: any[]) => this.providerCount.set(p.length)) - }) + const my = client.scope('ng-devtools'); + my.rpc.call('build-meta').then((m: any) => this.meta.set(m)); + my.rpc.call('get-components').then((c: any[]) => this.componentCount.set(c.length)); + my.rpc.call('get-routes').then((r: any[]) => this.routeCount.set(r.length)); + my.rpc.call('get-signals').then((s: any[]) => this.signalCount.set(s.length)); + my.rpc.call('get-providers').then((p: any[]) => this.providerCount.set(p.length)); + }); } } diff --git a/app/src/pages/di-inspector.ts b/app/src/pages/di-inspector.ts index 6c7afe6..5f808d9 100644 --- a/app/src/pages/di-inspector.ts +++ b/app/src/pages/di-inspector.ts @@ -1,32 +1,32 @@ -import { Component, input, signal, effect, computed } from '@angular/core' -import type { DevframeRpcClient } from 'devframe/client' +import { Component, input, signal, effect, computed } from '@angular/core'; +import type { DevframeRpcClient } from 'devframe/client'; interface ProviderInfo { - token: string - type: string - isViewProvider: boolean + token: string; + type: string; + isViewProvider: boolean; } interface SourceProvider { - token: string - source: string - file: string - line: number - providedIn?: string - type: string + token: string; + source: string; + file: string; + line: number; + providedIn?: string; + type: string; } interface InjectorNode { - injector: { id: string; type: string; name: string; providerCount: number } - providers: ProviderInfo[] - children: InjectorNode[] + injector: { id: string; type: string; name: string; providerCount: number }; + providers: ProviderInfo[]; + children: InjectorNode[]; } const TYPE_COLORS: Record = { element: '#60a5fa', environment: '#34d399', null: '#71717a', -} +}; @Component({ selector: 'app-di-inspector', @@ -47,7 +47,10 @@ const TYPE_COLORS: Record = { @if (roots().length === 0 && sourceProviders().length === 0) {

No DI data found.

-

No providers, injectables, or inject() calls found. Runtime tree requires Angular 17+ with the overlay connected.

+

+ No providers, injectables, or inject() calls found. Runtime tree requires Angular 17+ with + the overlay connected. +

} @@ -98,7 +101,9 @@ const TYPE_COLORS: Record = { {{ item.node.injector.name }} @if (item.node.injector.providerCount > 0) { - {{ item.node.injector.providerCount }} providers + {{ item.node.injector.providerCount }} providers } } @@ -110,7 +115,10 @@ const TYPE_COLORS: Record = { @if (selectedInjector()) {