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
19 changes: 19 additions & 0 deletions app/src/__tests__/store-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
16 changes: 14 additions & 2 deletions app/src/pages/di-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +44,7 @@ interface ProviderInfo {
interface DependencyInfo {
from: string;
token: string;
tokenId?: string;
flags: string[];
providedBy: string | null;
}
Expand Down Expand Up @@ -457,7 +460,10 @@ function isTree(value: unknown): value is InjectorNode[] {
}
@if (sel.dependencies?.length) {
<ul class="deps">
@for (dep of sel.dependencies; track dep.from + dep.token + $index) {
@for (
dep of sel.dependencies;
track dep.from + (dep.tokenId ?? dep.token) + $index
) {
<li class="dep" [class.hit]="matches(dep.token)">
<div class="dep-main">
<span class="token mono">{{ dep.token }}</span>
Expand Down Expand Up @@ -512,7 +518,11 @@ function isTree(value: unknown): value is InjectorNode[] {
}
@if (providerCount(sel)) {
<ul class="providers">
@for (p of sel.providers; track p.token + $index; let index = $index) {
@for (
p of sel.providers;
track (p.tokenId ?? p.token) + $index;
let index = $index
) {
@if (!isHidden(p)) {
<li class="provider" [class.hit]="matches(p.token)">
<div class="dep-main">
Expand Down Expand Up @@ -541,6 +551,7 @@ function isTree(value: unknown): value is InjectorNode[] {
kind: 'provider',
injectorId: sel.injector.id,
token: p.token,
tokenId: p.tokenId,
index: index,
}"
[label]="p.token + positionSuffix(sel.providers, index)"
Expand All @@ -551,6 +562,7 @@ function isTree(value: unknown): value is InjectorNode[] {
[pageId]="shownPageId()"
[injectorId]="sel.injector.id"
[token]="p.token"
[tokenId]="p.tokenId"
[index]="index"
[position]="tokenPosition(sel.providers, index)"
/>
Expand Down
5 changes: 4 additions & 1 deletion app/src/pages/di-provider-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export class ProviderValue {
readonly pageId = input<string | null>(null);
readonly injectorId = input.required<string>();
readonly token = input.required<string>();
/** The page's id for the token, which the page matches before the name. */
readonly tokenId = input<string | undefined>(undefined);
/** The provider's index in its injector's `providers`. */
readonly index = input.required<number>();
/** Tells apart providers of one token (multi providers), such as `(2 of 2)`. */
Expand All @@ -217,7 +219,7 @@ export class ProviderValue {

/** The provider this shows; another one closes it. */
readonly key = computed(
() => `${this.pageId()}|${this.injectorId()}|${this.index()}|${this.token()}`,
() => `${this.pageId()}|${this.injectorId()}|${this.index()}|${this.tokenId() ?? this.token()}`,
);
readonly open = linkedSignal<string, boolean>({ source: this.key, computation: () => false });
readonly answer = linkedSignal<string, MembersAnswer | null>({
Expand Down Expand Up @@ -263,6 +265,7 @@ export class ProviderValue {
request: {
injectorId: this.injectorId(),
token: this.token(),
...(this.tokenId() ? { tokenId: this.tokenId() } : {}),
index: this.index(),
...request,
},
Expand Down
2 changes: 1 addition & 1 deletion app/src/ui/store-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isStaticReport } from '../rpc';
/** What the page stores: a component, a provider or NgRx state (see `console-handles.ts`). */
export type ConsoleTarget =
| { kind: 'component'; id: string }
| { kind: 'provider'; injectorId: string; token: string; index: number }
| { kind: 'provider'; injectorId: string; token: string; index: number; tokenId?: string }
| { kind: 'ngrx'; storeId: string };

interface StoreResult {
Expand Down
9 changes: 8 additions & 1 deletion apps/docs/src/content/agents/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,17 @@ The injectors a page reported. Element injectors list what each component inject
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `injector` | no | A tag name, a component or directive class name, an environment injector name like `Root`, `Platform` or `Route: admin`, an injector id, or a component instance id like `c12`. Returns only the matching injectors. |
| `token` | no | A token name, like `HttpClient`. Returns the injectors that provide it and the components or services that inject it. |
| `tokenId` | no | The id of one token, like `tok-3`, from `tokenId` in the tree. Keeps only that token when several share a name. |

An `injector` that names an element injector returns it with its lookup path resolved to names and provided tokens. One that names an environment injector, in any case, returns that injector's providers, what the services it already created inject, and its parent injectors. When nothing matches, the answer lists the tags and environment injector names on the page to retry with.

An element injector that belongs to a component carries `componentId`, the instance id that `inspect-component`, `list-components` and the `component-tree` resource use. `framework: true` marks the injectors and providers that come from the installed `@angular/*` packages; see [Framework injectors](../inspectors/injectors.md#framework-injectors).

Without `injector` or `token`, the answer is the whole tree and says which change detection mode the page runs. The answer stays within 20,000 characters and its JSON always parses. A larger tree shows element injectors as an outline (id, name, type, component, and provider and dependency counts), and when that is still too long it leaves out the deepest injectors and says how many. The environment injectors are always there, as an outline when they are large, and without their deepest injectors when even that outline is too long. An `injector` or `token` answer that is too long cuts each injector's `providers` and `dependencies` lists, with `providersTotal` and `dependenciesTotal` giving the full counts, so a large injector such as `Root` still shows. An `@ngrx/signals` store that an environment injector created carries the name the NgRx tools use, such as `TravelStore`, in `token`, and its class name `SignalStore` in `tokenClass`; `token` finds it by either name, and so do `read-provider-value` and `explain-injection`. `injector` and `token` take at most 500 characters each. When the page reported only part of its injector tree (it keeps at most 2000 element injectors and 2500 in all, and leaves out injectors nested more than 1000 levels deep), the answer says so.
Without `injector` or `token`, the answer is the whole tree and says which change detection mode the page runs. The answer stays within 20,000 characters and its JSON always parses. A larger tree shows element injectors as an outline (id, name, type, component, and provider and dependency counts), and when that is still too long it leaves out the deepest injectors and says how many. The environment injectors are always there, as an outline when they are large, and without their deepest injectors when even that outline is too long. An `injector` or `token` answer that is too long cuts each injector's `providers` and `dependencies` lists, with `providersTotal` and `dependenciesTotal` giving the full counts, so a large injector such as `Root` still shows. An `@ngrx/signals` store that an environment injector created carries the name the NgRx tools use, such as `TravelStore`, in `token`, and its class name `SignalStore` in `tokenClass`; `token` finds it by either name, and so do `read-provider-value` and `explain-injection`. `injector` and `token` take at most 500 characters each.

Each provider and dependency carries `tokenId`, the page's id for the token object, so two tokens with the same name, such as two `new InjectionToken('config')`, stay apart. A `token` that several tokens share lists every match with its `tokenId`, and `tokenId` keeps one of them. `read-provider-value` and `explain-injection` answer such a name with the candidates instead, and take `tokenId` to pick one. `read-provider-value` needs no `tokenId` when its `injector` leaves only one token with the name. Ids last until the page reloads. A tree from an older overlay has no ids, and the tools match by name.

When the page reported only part of its injector tree (it keeps at most 2000 element injectors and 2500 in all, and leaves out injectors nested more than 1000 levels deep), the answer says so.

### read-provider-value

Expand All @@ -513,6 +518,7 @@ The current value of a service or other provider that an injector on the page al
| Argument | Required | Value |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token` | yes | A class name like `CartStore` or an `InjectionToken` description like `API_URL`. |
| `tokenId` | no | The id of one token, like `tok-3`. Needed when several tokens share the name and `injector` doesn't narrow them to one; the answer then lists them. |
| `injector` | no | An injector id, `Root`, `Platform`, `Route: admin`, a tag or a component class name. Defaults to the first injector that provides the token, environment injectors first. |
| `path` | no | A member, then keys or indexes, like `items` or `cart.items[0]`. Without it, the answer lists the members. |
| `depth` | no | With `path`, the levels to read below it, 1 to 5. |
Expand All @@ -528,6 +534,7 @@ How Angular resolves one token for one component, element or environment injecto
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `injector` | yes | Where the injection happens: a component or directive class name, a tag, a component instance id like `c12` (an error's owner id works), an injector id, or an environment injector name like `Root`. |
| `token` | yes | The token as the Injectors tab names it: a class name like `HttpClient` or an `InjectionToken` description like `API_URL`. |
| `tokenId` | no | The id of one token, like `tok-3`. Needed when several tokens share the name; the answer then lists them. |
| `self` | no | `true` asks only the first injector. |
| `skipSelf` | no | `true` starts at the parent injector. |
| `host` | no | `true` stops at the host element of the component that declares the start, where only its `viewProviders` and the component count. |
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/content/inspectors/injectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ For environment injectors, **Injected by its services** lists what the services

Each live provider row has a **Show value** button. It reads the provider's current value from the page and lists it like the properties of a component: its own fields, with signals and resources unwrapped (marked **signal** or **resource**), and methods left out. An object, array, Map or Set opens one level at a time, and each level is read from the page when you open it. A value without fields, such as a string or an array from `useValue`, shows whole. **Refresh** reads it again, since the list does not follow the app on its own. When several providers share a token, as `multi` providers do, their buttons and values are named with their position, such as **(2 of 2)**.

Two tokens can have the same name, such as two `new InjectionToken('config')`. The overlay gives each token object its own id (`tokenId` in the injector tree, such as `tok-3`), so the value, the **Injected here** rows and the check for whether something injected a token follow the token itself, not its name. Their buttons are named with their position, like the providers of one token.

Reading never creates a provider, with the same rules as **Store as global** below. A provider the app has not created yet says **… has not been created yet** instead. Getters never run, and secret-looking names and values are shown as `[redacted]`. See [what the devtools redact](../security.md).

### Store a provider as a global
Expand Down Expand Up @@ -222,7 +224,7 @@ If the selected injector leaves the page because its component or directive was
| `pangular:scan-providers` | tool | DI providers from source: `@Injectable` services, `inject()` calls, constructor parameters and `providers` arrays. |
| `pangular:explain-injection` | tool | The lookup of one token from a component, element or environment injector, step by step, with `self`, `skipSelf`, `host` and `optional`. Says at each injector whether it provides the token and why the lookup stops. See [Explain a lookup with flags](#explain-a-lookup-with-flags). |
| `pangular:inspect-providers` | tool | The injector tree a page reported, with what components and created services inject. `injector` narrows it to matching element or environment injectors (`Root`, `Platform`, `Route: admin`) or a component instance id, `token` to where a token is provided and injected. `componentId` and `framework` mark the owning component and Angular's own injectors and providers. |
| `pangular:read-provider-value` | tool | The current value of a provider an injector already created, by `token` and optional `injector`, with `path` to read deeper. See [Peek at a provider value](#peek-at-a-provider-value). |
| `pangular:read-provider-value` | tool | The current value of a provider an injector already created, by `token` and optional `injector`, with `path` to read deeper. When two tokens share the name, it lists them with their `tokenId`; pass `tokenId` to pick one. See [Peek at a provider value](#peek-at-a-provider-value). |
| `pangular:startup-timing` | tool | How the page started: milestones from navigation start to the first stable state, and the slowest providers to construct with own and total time. See [Startup timing](#startup-timing). |
| `pangular:injector-tree` | resource | The live tree last reported by a page. |

Expand Down
Loading
Loading