Skip to content

Commit

Permalink
fix(core): use TNode instead of LView for mapping injector providers
Browse files Browse the repository at this point in the history
Previously, LViews were used here to be consistent with other debug APIs. Using LViews for tracking injector providers does not work because providers only get configured once per TNode type.

Now we use the TNode as the key to track element injector providers, allowing the injector for each item rendered in a list (`ngFor` or `@for`) to be targeted with debug APIs for inspecting providers
  • Loading branch information
AleksanderBodurri committed Oct 30, 2023
1 parent b7bc48a commit 00b651f
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 9 deletions.
19 changes: 14 additions & 5 deletions packages/core/src/render3/debug/framework_injector_profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ import {InjectedService, InjectorCreatedInstance, InjectorProfilerContext, Injec
* getDependenciesFromInjectable API, which takes in an injector and a token and returns it's
* dependencies.
*
* resolverToProviders: Maps a DI resolver (an Injector or an LView) to the providers configured
* resolverToProviders: Maps a DI resolver (an Injector or a TNode) to the providers configured
* within it This is used to support the getInjectorProviders API, which takes in an injector and
* returns the providers that it was configured with.
* returns the providers that it was configured with. Note that for the element injector case we
* use the TNode instead of the LView as the DI resolver. This is because the registration of
* providers happens only once per type of TNode. If an injector is created with an identical TNode,
* the providers for that injector will not be reconfigured.
*
* standaloneInjectorToComponent: Maps the injector of a standalone component to the standalone
* component that it is associated with. Used in the getInjectorProviders API, specificially in the
Expand All @@ -53,13 +56,13 @@ import {InjectedService, InjectorCreatedInstance, InjectorProfilerContext, Injec
class DIDebugData {
resolverToTokenToDependencies =
new WeakMap<Injector|LView, WeakMap<Type<unknown>, InjectedService[]>>();
resolverToProviders = new WeakMap<Injector|LView, ProviderRecord[]>();
resolverToProviders = new WeakMap<Injector|TNode, ProviderRecord[]>();
standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();

reset() {
this.resolverToTokenToDependencies =
new WeakMap<Injector|LView, WeakMap<Type<unknown>, InjectedService[]>>();
this.resolverToProviders = new WeakMap<Injector|LView, ProviderRecord[]>();
this.resolverToProviders = new WeakMap<Injector|TNode, ProviderRecord[]>();
this.standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();
}
}
Expand Down Expand Up @@ -237,7 +240,13 @@ function handleProviderConfiguredEvent(
context: InjectorProfilerContext, data: ProviderRecord): void {
const {resolverToProviders} = frameworkDIDebugData;

const diResolver = getDIResolver(context?.injector);
let diResolver: Injector|TNode;
if (context?.injector instanceof NodeInjector) {
diResolver = getNodeInjectorTNode(context.injector) as TNode;
} else {
diResolver = context.injector;
}

if (diResolver === null) {
throwError('A ProviderConfigured event must be run within an injection context.');
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/util/injector_discovery_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,9 @@ function getProviderImportsContainer(injector: Injector): Type<unknown>|null {
* injector
*/
function getNodeInjectorProviders(injector: NodeInjector): ProviderRecord[] {
const diResolver = getNodeInjectorLView(injector);
const diResolver = getNodeInjectorTNode(injector);
const {resolverToProviders} = getFrameworkDIDebugData();
return resolverToProviders.get(diResolver) ?? [];
return resolverToProviders.get(diResolver as TNode) ?? [];
}

/**
Expand Down
90 changes: 88 additions & 2 deletions packages/core/test/acceptance/injector_profiler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PercentPipe} from '@angular/common';
import {NgForOf, PercentPipe} from '@angular/common';
import {inject} from '@angular/core';
import {afterRender, ClassProvider, Component, Directive, ElementRef, ENVIRONMENT_INITIALIZER, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, NgModule, NgModuleRef, ViewChild} from '@angular/core/src/core';
import {afterRender, ClassProvider, Component, Directive, ElementRef, ENVIRONMENT_INITIALIZER, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, NgModule, NgModuleRef, ProviderToken, QueryList, ViewChild, ViewChildren} from '@angular/core/src/core';
import {INJECTOR_DEF_TYPES} from '@angular/core/src/di/internal_tokens';
import {NullInjector} from '@angular/core/src/di/null_injector';
import {isClassProvider, isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider} from '@angular/core/src/di/provider_collection';
Expand Down Expand Up @@ -768,6 +768,92 @@ describe('getInjectorProviders', () => {
expect(myServiceProviderRecord!.importPath![0]).toBe(MyStandaloneComponentB);
expect(myServiceProviderRecord!.importPath![1]).toBe(ModuleA);
}));

it('should be able to get injector providers for element injectors created by components rendering in an ngFor',
() => {
class MyService {}

@Component(
{selector: 'item-cmp', template: 'item', standalone: true, providers: [MyService]})
class ItemComponent {
injector = inject(Injector);
}

@Component({
selector: 'my-comp',
template: `
<item-cmp *ngFor="let item of items"></item-cmp>
`,
imports: [ItemComponent, NgForOf],
standalone: true
})
class MyStandaloneComponent {
injector = inject(Injector);
items = [1, 2, 3];

@ViewChildren(ItemComponent) itemComponents: QueryList<ItemComponent>|undefined;
}

const root = TestBed.createComponent(MyStandaloneComponent);
root.detectChanges();

const myStandaloneComponent = root.componentRef.instance;
const itemComponents = myStandaloneComponent.itemComponents;
expect(itemComponents).toBeInstanceOf(QueryList);
expect(itemComponents?.length).toBe(3);
itemComponents!.forEach(item => {
const itemProviders = getInjectorProviders(item.injector);
expect(itemProviders).toBeInstanceOf(Array);
expect(itemProviders.length).toBe(1);
expect(itemProviders[0].token).toBe(MyService);
expect(itemProviders[0].provider).toBe(MyService);
expect(itemProviders[0].isViewProvider).toBe(false);
});
});

it('should be able to get injector providers for element injectors created by components rendering in a @for',
() => {
class MyService {}

@Component(
{selector: 'item-cmp', template: 'item', standalone: true, providers: [MyService]})
class ItemComponent {
injector = inject(Injector);
}

@Component({
selector: 'my-comp',
template: `
@for (item of items; track item) {
<item-cmp></item-cmp>
}
`,
imports: [ItemComponent],
standalone: true
})
class MyStandaloneComponent {
injector = inject(Injector);
items = [1, 2, 3];

@ViewChildren(ItemComponent) itemComponents: QueryList<ItemComponent>|undefined;
}

const root = TestBed.createComponent(MyStandaloneComponent);
root.detectChanges();

const myStandaloneComponent = root.componentRef.instance;
const itemComponents = myStandaloneComponent.itemComponents;
expect(itemComponents).toBeInstanceOf(QueryList);
expect(itemComponents?.length).toBe(3);
itemComponents!.forEach(item => {
const itemProviders = getInjectorProviders(item.injector);
expect(itemProviders).toBeInstanceOf(Array);
expect(itemProviders.length).toBe(1);
expect(itemProviders[0].token).toBe(MyService);
expect(itemProviders[0].provider).toBe(MyService);
expect(itemProviders[0].isViewProvider).toBe(false);
});
});
});

describe('getDependenciesFromInjectable', () => {
Expand Down

0 comments on commit 00b651f

Please sign in to comment.