Skip to content

Commit a54713c

Browse files
AleksanderBodurrialxhub
authored andcommitted
feat(core): implement ɵgetInjectorMetadata debug API (#51900)
This API allows for inspection of a given injector to determine it's type (Element, Environment, Null) as well as it's "source". - For Environment injectors the source is the source of the injector; `injector.source`. - For Element injectors the name is the DOM Element that created the injector. - For the Null Injector this is the string `"Null Injector"`. PR Close #51900
1 parent 6b6a44c commit a54713c

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

packages/core/src/render3/util/global_utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {setProfiler} from '../profiler';
1212

1313
import {applyChanges} from './change_detection_utils';
1414
import {getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils';
15-
import {getDependenciesFromInjectable, getInjectorProviders, getInjectorResolutionPath} from './injector_discovery_utils';
15+
import {getDependenciesFromInjectable, getInjectorMetadata, getInjectorProviders, getInjectorResolutionPath} from './injector_discovery_utils';
1616

1717

1818

@@ -48,6 +48,7 @@ export function publishDefaultGlobalUtils() {
4848
publishGlobalUtil('ɵgetDependenciesFromInjectable', getDependenciesFromInjectable);
4949
publishGlobalUtil('ɵgetInjectorProviders', getInjectorProviders);
5050
publishGlobalUtil('ɵgetInjectorResolutionPath', getInjectorResolutionPath);
51+
publishGlobalUtil('ɵgetInjectorMetadata', getInjectorMetadata);
5152
/**
5253
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
5354
* The contract of the function might be changed in any release and/or the function can be

packages/core/src/render3/util/injector_discovery_utils.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ import {EnvironmentInjector, R3Injector} from '../../di/r3_injector';
1616
import {Type} from '../../interface/type';
1717
import {NgModuleRef as viewEngine_NgModuleRef} from '../../linker/ng_module_factory';
1818
import {deepForEach} from '../../util/array_utils';
19-
import {throwError} from '../../util/assert';
19+
import {assertDefined, throwError} from '../../util/assert';
2020
import type {ChainedInjector} from '../component_ref';
2121
import {getComponentDef} from '../definition';
2222
import {getNodeInjectorLView, getNodeInjectorTNode, getParentInjectorLocation, NodeInjector} from '../di';
2323
import {getFrameworkDIDebugData} from '../debug/framework_injector_profiler';
2424
import {InjectedService, ProviderRecord} from '../debug/injector_profiler';
2525
import {NodeInjectorOffset} from '../interfaces/injector';
2626
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from '../interfaces/node';
27-
import {INJECTOR, LView, TVIEW} from '../interfaces/view';
27+
import {HOST, INJECTOR, LView, TVIEW} from '../interfaces/view';
2828

2929
import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './injector_utils';
30+
import {assertTNodeForLView} from '../assert';
31+
import {RElement} from '../interfaces/renderer_dom';
3032

3133
/**
3234
* Discovers the dependencies of an injectable instance. Provides DI information about each
@@ -390,6 +392,43 @@ export function getInjectorProviders(injector: Injector): ProviderRecord[] {
390392
throwError('getInjectorProviders only supports NodeInjector and EnvironmentInjector');
391393
}
392394

395+
/**
396+
*
397+
* Given an injector, this function will return
398+
* an object containing the type and source of the injector.
399+
*
400+
* | | type | source |
401+
* |--------------|-------------|-------------------------------------------------------------|
402+
* | NodeInjector | element | DOM element that created this injector |
403+
* | R3Injector | environment | `injector.source` |
404+
* | NullInjector | null | null |
405+
*
406+
* @param injector the Injector to get metadata for
407+
* @returns an object containing the type and source of the given injector. If the injector metadata
408+
* cannot be determined, returns null.
409+
*/
410+
export function getInjectorMetadata(injector: Injector):
411+
{type: string; source: RElement | string | null}|null {
412+
if (injector instanceof NodeInjector) {
413+
const lView = getNodeInjectorLView(injector);
414+
const tNode = getNodeInjectorTNode(injector)!;
415+
assertTNodeForLView(tNode, lView);
416+
assertDefined(lView[tNode.index][HOST], 'Could not find node in element view.');
417+
418+
return {type: 'element', source: lView[tNode.index][HOST]};
419+
}
420+
421+
if (injector instanceof R3Injector) {
422+
return {type: 'environment', source: injector.source ?? null};
423+
}
424+
425+
if (injector instanceof NullInjector) {
426+
return {type: 'null', source: null};
427+
}
428+
429+
return null;
430+
}
431+
393432
export function getInjectorResolutionPath(injector: Injector): Injector[] {
394433
const resolutionPath: Injector[] = [injector];
395434
getInjectorResolutionPathHelper(injector, resolutionPath);

packages/core/test/acceptance/injector_profiler_spec.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88

99
import {PercentPipe} from '@angular/common';
1010
import {inject} from '@angular/core';
11-
import {ClassProvider, Component, Directive, Inject, Injectable, InjectFlags, InjectionToken, Injector, NgModule, NgModuleRef, ViewChild} from '@angular/core/src/core';
11+
import {afterRender, ClassProvider, Component, Directive, ElementRef, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, NgModule, NgModuleRef, ProviderToken, ViewChild} from '@angular/core/src/core';
1212
import {NullInjector} from '@angular/core/src/di/null_injector';
1313
import {isClassProvider, isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider} from '@angular/core/src/di/provider_collection';
1414
import {EnvironmentInjector, R3Injector} from '@angular/core/src/di/r3_injector';
1515
import {setupFrameworkInjectorProfiler} from '@angular/core/src/render3/debug/framework_injector_profiler';
16-
import {getInjectorProfilerContext, InjectedService, InjectedServiceEvent, InjectorCreatedInstanceEvent, InjectorProfilerEvent, InjectorProfilerEventType, ProviderConfiguredEvent, ProviderRecord, setInjectorProfiler} from '@angular/core/src/render3/debug/injector_profiler';
16+
import {getInjectorProfilerContext, InjectedServiceEvent, InjectorCreatedInstanceEvent, InjectorProfilerEvent, InjectorProfilerEventType, ProviderConfiguredEvent, setInjectorProfiler} from '@angular/core/src/render3/debug/injector_profiler';
1717
import {getNodeInjectorLView, NodeInjector} from '@angular/core/src/render3/di';
18-
import {getDependenciesFromInjectable, getInjectorProviders, getInjectorResolutionPath} from '@angular/core/src/render3/util/injector_discovery_utils';
18+
import {getDependenciesFromInjectable, getInjectorMetadata, getInjectorProviders, getInjectorResolutionPath} from '@angular/core/src/render3/util/injector_discovery_utils';
1919
import {fakeAsync, tick} from '@angular/core/testing';
2020
import {TestBed} from '@angular/core/testing/src/test_bed';
2121
import {BrowserModule} from '@angular/platform-browser';
@@ -211,6 +211,114 @@ describe('setProfiler', () => {
211211
});
212212
});
213213

214+
describe('getInjectorMetadata', () => {
215+
it('should be able to determine injector type and name', fakeAsync(() => {
216+
class MyServiceA {}
217+
@NgModule({providers: [MyServiceA]})
218+
class ModuleA {
219+
}
220+
221+
class MyServiceB {}
222+
@NgModule({providers: [MyServiceB]})
223+
class ModuleB {
224+
}
225+
226+
@Component({
227+
selector: 'lazy-comp',
228+
template: `lazy component`,
229+
standalone: true,
230+
imports: [ModuleB]
231+
})
232+
class LazyComponent {
233+
lazyComponentNodeInjector = inject(Injector);
234+
elementRef = inject(ElementRef);
235+
236+
constructor() {
237+
afterRender(() => afterLazyComponentRendered(this));
238+
}
239+
}
240+
241+
@Component({
242+
standalone: true,
243+
imports: [RouterOutlet, ModuleA],
244+
template: `<router-outlet/>`,
245+
})
246+
class MyStandaloneComponent {
247+
@ViewChild(RouterOutlet, {read: ElementRef}) routerOutlet: ElementRef|undefined;
248+
elementRef = inject(ElementRef);
249+
}
250+
251+
TestBed.configureTestingModule({
252+
imports: [RouterModule.forRoot([{
253+
path: 'lazy',
254+
loadComponent: () => LazyComponent,
255+
}])]
256+
});
257+
258+
const root = TestBed.createComponent(MyStandaloneComponent);
259+
TestBed.inject(Router).navigateByUrl('/lazy');
260+
tick();
261+
root.detectChanges();
262+
263+
function afterLazyComponentRendered(lazyComponent: LazyComponent) {
264+
const {lazyComponentNodeInjector} = lazyComponent;
265+
const myStandaloneComponent =
266+
lazyComponentNodeInjector.get(MyStandaloneComponent, null, {skipSelf: true})!;
267+
expect(myStandaloneComponent).toBeInstanceOf(MyStandaloneComponent);
268+
expect(myStandaloneComponent.routerOutlet).toBeInstanceOf(ElementRef);
269+
270+
const injectorPath = getInjectorResolutionPath(lazyComponentNodeInjector);
271+
const injectorMetadata = injectorPath.map(injector => getInjectorMetadata(injector));
272+
273+
expect(injectorMetadata[0]).toBeDefined();
274+
expect(injectorMetadata[1]).toBeDefined();
275+
expect(injectorMetadata[2]).toBeDefined();
276+
expect(injectorMetadata[3]).toBeDefined();
277+
expect(injectorMetadata[4]).toBeDefined();
278+
expect(injectorMetadata[5]).toBeDefined();
279+
expect(injectorMetadata[6]).toBeDefined();
280+
expect(injectorMetadata[7]).toBeDefined();
281+
282+
expect(injectorMetadata[0]!.source).toBe(lazyComponent.elementRef.nativeElement);
283+
expect(injectorMetadata[1]!.source)
284+
.toBe(myStandaloneComponent.routerOutlet!.nativeElement);
285+
expect(injectorMetadata[2]!.source).toBe(myStandaloneComponent.elementRef.nativeElement);
286+
expect(injectorMetadata[3]!.source).toBe('Standalone[LazyComponent]');
287+
expect(injectorMetadata[4]!.source).toBe('Standalone[MyStandaloneComponent]');
288+
expect(injectorMetadata[5]!.source).toBe('DynamicTestModule');
289+
expect(injectorMetadata[6]!.source).toBe('Platform: core');
290+
expect(injectorMetadata[7]!.source).toBeNull();
291+
292+
expect(injectorMetadata[0]!.type).toBe('element');
293+
expect(injectorMetadata[1]!.type).toBe('element');
294+
expect(injectorMetadata[2]!.type).toBe('element');
295+
expect(injectorMetadata[3]!.type).toBe('environment');
296+
expect(injectorMetadata[4]!.type).toBe('environment');
297+
expect(injectorMetadata[5]!.type).toBe('environment');
298+
expect(injectorMetadata[6]!.type).toBe('environment');
299+
expect(injectorMetadata[7]!.type).toBe('null');
300+
}
301+
}));
302+
303+
it('should return null for injectors it does not recognize', () => {
304+
class MockInjector extends Injector {
305+
override get(): void {
306+
throw new Error('Method not implemented.');
307+
}
308+
}
309+
const mockInjector = new MockInjector();
310+
expect(getInjectorMetadata(mockInjector)).toBeNull();
311+
});
312+
313+
it('should return null as the source for an R3Injector with no source.', () => {
314+
const emptyR3Injector = new R3Injector([], new NullInjector(), null, new Set());
315+
const r3InjectorMetadata = getInjectorMetadata(emptyR3Injector);
316+
expect(r3InjectorMetadata).toBeDefined();
317+
expect(r3InjectorMetadata!.source).toBeNull();
318+
expect(r3InjectorMetadata!.type).toBe('environment');
319+
});
320+
});
321+
214322
describe('getInjectorProviders', () => {
215323
beforeEach(() => setupFrameworkInjectorProfiler());
216324
afterAll(() => setInjectorProfiler(null));

0 commit comments

Comments
 (0)