Skip to content

Commit

Permalink
feat(core): implement ɵgetInjectorMetadata debug API (#51900)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AleksanderBodurri authored and alxhub committed Sep 29, 2023
1 parent 6b6a44c commit a54713c
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 6 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/render3/util/global_utils.ts
Expand Up @@ -12,7 +12,7 @@ import {setProfiler} from '../profiler';

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



Expand Down Expand Up @@ -48,6 +48,7 @@ export function publishDefaultGlobalUtils() {
publishGlobalUtil('ɵgetDependenciesFromInjectable', getDependenciesFromInjectable);
publishGlobalUtil('ɵgetInjectorProviders', getInjectorProviders);
publishGlobalUtil('ɵgetInjectorResolutionPath', getInjectorResolutionPath);
publishGlobalUtil('ɵgetInjectorMetadata', getInjectorMetadata);
/**
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
* The contract of the function might be changed in any release and/or the function can be
Expand Down
43 changes: 41 additions & 2 deletions packages/core/src/render3/util/injector_discovery_utils.ts
Expand Up @@ -16,17 +16,19 @@ import {EnvironmentInjector, R3Injector} from '../../di/r3_injector';
import {Type} from '../../interface/type';
import {NgModuleRef as viewEngine_NgModuleRef} from '../../linker/ng_module_factory';
import {deepForEach} from '../../util/array_utils';
import {throwError} from '../../util/assert';
import {assertDefined, throwError} from '../../util/assert';
import type {ChainedInjector} from '../component_ref';
import {getComponentDef} from '../definition';
import {getNodeInjectorLView, getNodeInjectorTNode, getParentInjectorLocation, NodeInjector} from '../di';
import {getFrameworkDIDebugData} from '../debug/framework_injector_profiler';
import {InjectedService, ProviderRecord} from '../debug/injector_profiler';
import {NodeInjectorOffset} from '../interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from '../interfaces/node';
import {INJECTOR, LView, TVIEW} from '../interfaces/view';
import {HOST, INJECTOR, LView, TVIEW} from '../interfaces/view';

import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './injector_utils';
import {assertTNodeForLView} from '../assert';
import {RElement} from '../interfaces/renderer_dom';

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

/**
*
* Given an injector, this function will return
* an object containing the type and source of the injector.
*
* | | type | source |
* |--------------|-------------|-------------------------------------------------------------|
* | NodeInjector | element | DOM element that created this injector |
* | R3Injector | environment | `injector.source` |
* | NullInjector | null | null |
*
* @param injector the Injector to get metadata for
* @returns an object containing the type and source of the given injector. If the injector metadata
* cannot be determined, returns null.
*/
export function getInjectorMetadata(injector: Injector):
{type: string; source: RElement | string | null}|null {
if (injector instanceof NodeInjector) {
const lView = getNodeInjectorLView(injector);
const tNode = getNodeInjectorTNode(injector)!;
assertTNodeForLView(tNode, lView);
assertDefined(lView[tNode.index][HOST], 'Could not find node in element view.');

return {type: 'element', source: lView[tNode.index][HOST]};
}

if (injector instanceof R3Injector) {
return {type: 'environment', source: injector.source ?? null};
}

if (injector instanceof NullInjector) {
return {type: 'null', source: null};
}

return null;
}

export function getInjectorResolutionPath(injector: Injector): Injector[] {
const resolutionPath: Injector[] = [injector];
getInjectorResolutionPathHelper(injector, resolutionPath);
Expand Down
114 changes: 111 additions & 3 deletions packages/core/test/acceptance/injector_profiler_spec.ts
Expand Up @@ -8,14 +8,14 @@

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

describe('getInjectorMetadata', () => {
it('should be able to determine injector type and name', fakeAsync(() => {
class MyServiceA {}
@NgModule({providers: [MyServiceA]})
class ModuleA {
}

class MyServiceB {}
@NgModule({providers: [MyServiceB]})
class ModuleB {
}

@Component({
selector: 'lazy-comp',
template: `lazy component`,
standalone: true,
imports: [ModuleB]
})
class LazyComponent {
lazyComponentNodeInjector = inject(Injector);
elementRef = inject(ElementRef);

constructor() {
afterRender(() => afterLazyComponentRendered(this));
}
}

@Component({
standalone: true,
imports: [RouterOutlet, ModuleA],
template: `<router-outlet/>`,
})
class MyStandaloneComponent {
@ViewChild(RouterOutlet, {read: ElementRef}) routerOutlet: ElementRef|undefined;
elementRef = inject(ElementRef);
}

TestBed.configureTestingModule({
imports: [RouterModule.forRoot([{
path: 'lazy',
loadComponent: () => LazyComponent,
}])]
});

const root = TestBed.createComponent(MyStandaloneComponent);
TestBed.inject(Router).navigateByUrl('/lazy');
tick();
root.detectChanges();

function afterLazyComponentRendered(lazyComponent: LazyComponent) {
const {lazyComponentNodeInjector} = lazyComponent;
const myStandaloneComponent =
lazyComponentNodeInjector.get(MyStandaloneComponent, null, {skipSelf: true})!;
expect(myStandaloneComponent).toBeInstanceOf(MyStandaloneComponent);
expect(myStandaloneComponent.routerOutlet).toBeInstanceOf(ElementRef);

const injectorPath = getInjectorResolutionPath(lazyComponentNodeInjector);
const injectorMetadata = injectorPath.map(injector => getInjectorMetadata(injector));

expect(injectorMetadata[0]).toBeDefined();
expect(injectorMetadata[1]).toBeDefined();
expect(injectorMetadata[2]).toBeDefined();
expect(injectorMetadata[3]).toBeDefined();
expect(injectorMetadata[4]).toBeDefined();
expect(injectorMetadata[5]).toBeDefined();
expect(injectorMetadata[6]).toBeDefined();
expect(injectorMetadata[7]).toBeDefined();

expect(injectorMetadata[0]!.source).toBe(lazyComponent.elementRef.nativeElement);
expect(injectorMetadata[1]!.source)
.toBe(myStandaloneComponent.routerOutlet!.nativeElement);
expect(injectorMetadata[2]!.source).toBe(myStandaloneComponent.elementRef.nativeElement);
expect(injectorMetadata[3]!.source).toBe('Standalone[LazyComponent]');
expect(injectorMetadata[4]!.source).toBe('Standalone[MyStandaloneComponent]');
expect(injectorMetadata[5]!.source).toBe('DynamicTestModule');
expect(injectorMetadata[6]!.source).toBe('Platform: core');
expect(injectorMetadata[7]!.source).toBeNull();

expect(injectorMetadata[0]!.type).toBe('element');
expect(injectorMetadata[1]!.type).toBe('element');
expect(injectorMetadata[2]!.type).toBe('element');
expect(injectorMetadata[3]!.type).toBe('environment');
expect(injectorMetadata[4]!.type).toBe('environment');
expect(injectorMetadata[5]!.type).toBe('environment');
expect(injectorMetadata[6]!.type).toBe('environment');
expect(injectorMetadata[7]!.type).toBe('null');
}
}));

it('should return null for injectors it does not recognize', () => {
class MockInjector extends Injector {
override get(): void {
throw new Error('Method not implemented.');
}
}
const mockInjector = new MockInjector();
expect(getInjectorMetadata(mockInjector)).toBeNull();
});

it('should return null as the source for an R3Injector with no source.', () => {
const emptyR3Injector = new R3Injector([], new NullInjector(), null, new Set());
const r3InjectorMetadata = getInjectorMetadata(emptyR3Injector);
expect(r3InjectorMetadata).toBeDefined();
expect(r3InjectorMetadata!.source).toBeNull();
expect(r3InjectorMetadata!.type).toBe('environment');
});
});

describe('getInjectorProviders', () => {
beforeEach(() => setupFrameworkInjectorProfiler());
afterAll(() => setInjectorProfiler(null));
Expand Down

0 comments on commit a54713c

Please sign in to comment.