From 9fff77401e297bb270bc7531456614d8912ab3f2 Mon Sep 17 00:00:00 2001 From: erKam Date: Sat, 7 Nov 2026 13:58:29 +0000 Subject: [PATCH 1/3] fix(injectors): tell apart DI tokens that share a name Two tokens with the same name, such as two `new InjectionToken('config')`, collided: the injected pairs were keyed `injector|name`, so injecting one token let "Show value" and "Store as global" read (and create) the other, and a provider value read checked only the name at its index. Give each token object a stable id on the page through a WeakMap (`token-id.ts`, `tok-N`) and carry it as `tokenId` on providers and dependencies. Key the injected pairs and the provider value check by it, and keep it through the server's report checks. The agent tools still take a token name: `read-provider-value` and `explain-injection` answer a shared name with the candidates and their ids and take `tokenId` to pick one, and `inspect-providers` lists each match with its id and takes `tokenId` to keep one. Reports without ids fall back to names. The panel keys provider rows by id and sends it with value reads. Refs #367 --- app/src/__tests__/store-history.test.ts | 19 ++ app/src/pages/di-inspector.ts | 16 +- app/src/pages/di-provider-value.ts | 5 +- app/src/ui/store-global.ts | 2 +- apps/docs/src/content/agents/tools.md | 9 +- apps/docs/src/content/inspectors/injectors.md | 4 +- packages/devtools/CHANGELOG.md | 1 + .../src/__tests__/component-tree.test.ts | 9 +- .../src/__tests__/injector-tree.test.ts | 78 +++-- .../devtools/src/__tests__/token-ids.test.ts | 288 ++++++++++++++++++ packages/devtools/src/console-handles.ts | 6 +- packages/devtools/src/devframe.ts | 9 +- packages/devtools/src/injector-tree.ts | 25 +- packages/devtools/src/provider-values.ts | 18 +- .../src/rpc/component-report-guard.ts | 3 +- .../devtools/src/rpc/injection-explain.ts | 26 +- packages/devtools/src/rpc/injector-report.ts | 5 + packages/devtools/src/rpc/injector-tools.ts | 36 ++- packages/devtools/src/rpc/provider-values.ts | 28 +- packages/devtools/src/rpc/token-match.ts | 151 +++++++++ packages/devtools/src/token-id.ts | 31 ++ packages/devtools/src/types.ts | 8 + 22 files changed, 717 insertions(+), 60 deletions(-) create mode 100644 packages/devtools/src/__tests__/token-ids.test.ts create mode 100644 packages/devtools/src/rpc/token-match.ts create mode 100644 packages/devtools/src/token-id.ts diff --git a/app/src/__tests__/store-history.test.ts b/app/src/__tests__/store-history.test.ts index 6140b7b2..250adcd2 100644 --- a/app/src/__tests__/store-history.test.ts +++ b/app/src/__tests__/store-history.test.ts @@ -414,6 +414,25 @@ describe('Provider value', () => { expect(region.textContent).toContain('id:'); }); + it('sends the token id, and closes when another token with the same name takes the row', async () => { + const call = vi.fn(async () => ({ ok: true, type: 'Object', members: [] })); + const fixture = await mount(call); + fixture.componentRef.setInput('tokenId', 'tok-3'); + await settle(fixture); + const toggle = button(fixture, 'Show the value of CartService'); + toggle.click(); + await settle(fixture); + expect(call).toHaveBeenCalledWith('request-provider-value', { + pageId: 'p1', + request: { injectorId: 'inj-1', token: 'CartService', tokenId: 'tok-3', index: 2, path: [] }, + }); + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + // Same name and index, different token (#367): the open value belonged to the other one. + fixture.componentRef.setInput('tokenId', 'tok-4'); + await settle(fixture); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + }); + it('shows only the latest read when it is closed and reopened while a read is pending', async () => { const pending: ((answer: unknown) => void)[] = []; const call = vi.fn(() => new Promise((resolve) => pending.push(resolve))); diff --git a/app/src/pages/di-inspector.ts b/app/src/pages/di-inspector.ts index 66f66fdc..6a910884 100644 --- a/app/src/pages/di-inspector.ts +++ b/app/src/pages/di-inspector.ts @@ -32,6 +32,8 @@ import { StartupSection } from './startup-section'; interface ProviderInfo { token: string; + /** The page's id for the token object; two tokens can share a name. Missing in older reports. */ + tokenId?: string; type: string; isViewProvider: boolean; multi?: boolean; @@ -42,6 +44,7 @@ interface ProviderInfo { interface DependencyInfo { from: string; token: string; + tokenId?: string; flags: string[]; providedBy: string | null; } @@ -457,7 +460,10 @@ function isTree(value: unknown): value is InjectorNode[] { } @if (sel.dependencies?.length) {