Skip to content

Commit

Permalink
fix(core): emit provider configured event when a service is configure…
Browse files Browse the repository at this point in the history
…d with `providedIn` (#52365)

Previously this case was missed by the default framework injector profiler. Now in ngDevMode this event emits correctly when a service is configured with `providedIn`. This includes the case where injection tokens are configured with a `providedIn`.

This commit also includes unit tests for this new case in the injector profiler.

PR Close #52365
  • Loading branch information
AleksanderBodurri authored and dylhunn committed Oct 25, 2023
1 parent 403d217 commit ea6d752
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 9 deletions.
9 changes: 8 additions & 1 deletion packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {catchInjectorError, convertToBitFlags, injectArgs, NG_TEMP_TOKEN_PATH, s
import {INJECTOR} from './injector_token';
import {getInheritedInjectableDef, getInjectableDef, InjectorType, ɵɵInjectableDeclaration} from './interface/defs';
import {InjectFlags, InjectOptions} from './interface/injector';
import {ClassProvider, ConstructorProvider, EnvironmentProviders, InternalEnvironmentProviders, isEnvironmentProviders, Provider, StaticClassProvider} from './interface/provider';
import {ClassProvider, ConstructorProvider, EnvironmentProviders, InternalEnvironmentProviders, isEnvironmentProviders, Provider, StaticClassProvider, TypeProvider} from './interface/provider';
import {INJECTOR_DEF_TYPES} from './internal_tokens';
import {NullInjector} from './null_injector';
import {isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider, SingleProvider} from './provider_collection';
Expand Down Expand Up @@ -270,6 +270,13 @@ export class R3Injector extends EnvironmentInjector {
if (def && this.injectableDefInScope(def)) {
// Found an injectable def and it's scoped to this injector. Pretend as if it was here
// all along.

if (ngDevMode) {
runInInjectorProfilerContext(this, token as Type<T>, () => {
emitProviderConfiguredEvent(token as TypeProvider);
});
}

record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
} else {
record = null;
Expand Down
36 changes: 28 additions & 8 deletions packages/core/src/render3/debug/injector_profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import type {FactoryProvider} from '../../di';
import {resolveForwardRef} from '../../di/forward_ref';
import {InjectionToken} from '../../di/injection_token';
import type {Injector} from '../../di/injector';
Expand Down Expand Up @@ -88,7 +89,7 @@ export interface ProviderRecord {
/**
* DI token that this provider is configuring
*/
token: Type<unknown>;
token: Type<unknown>|InjectionToken<unknown>;

/**
* Determines if provider is configured as view provider.
Expand Down Expand Up @@ -199,20 +200,39 @@ function injectorProfiler(event: InjectorProfilerEvent): void {
* Emits an InjectorProfilerEventType.ProviderConfigured to the injector profiler. The data in the
* emitted event includes the raw provider, as well as the token that provider is providing.
*
* @param provider A provider object
* @param eventProvider A provider object
*/
export function emitProviderConfiguredEvent(
provider: SingleProvider, isViewProvider: boolean = false): void {
eventProvider: SingleProvider, isViewProvider: boolean = false): void {
!ngDevMode && throwError('Injector profiler should never be called in production mode');

let token;
// if the provider is a TypeProvider (typeof provider is function) then the token is the
// provider itself
if (typeof eventProvider === 'function') {
token = eventProvider;
}
// if the provider is an injection token, then the token is the injection token.
else if (eventProvider instanceof InjectionToken) {
token = eventProvider;
}
// in all other cases we can access the token via the `provide` property of the provider
else {
token = resolveForwardRef(eventProvider.provide);
}

let provider = eventProvider;
// Injection tokens may define their own default provider which gets attached to the token itself
// as `ɵprov`. In this case, we want to emit the provider that is attached to the token, not the
// token itself.
if (eventProvider instanceof InjectionToken) {
provider = eventProvider.ɵprov as FactoryProvider || eventProvider;
}

injectorProfiler({
type: InjectorProfilerEventType.ProviderConfigured,
context: getInjectorProfilerContext(),
providerRecord: {
token: typeof provider === 'function' ? provider : resolveForwardRef(provider.provide),
provider,
isViewProvider
}
providerRecord: {token, provider, isViewProvider}
});
}

Expand Down
77 changes: 77 additions & 0 deletions packages/core/test/acceptance/injector_profiler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,83 @@ describe('setProfiler', () => {
expect(((myServiceProviderConfiguredEvent!.providerRecord)?.provider as ClassProvider).multi)
.toBeTrue();
});

it('should emit correct DI events when service providers are configured with providedIn', () => {
@Injectable({providedIn: 'root'})
class RootService {
}

@Injectable({providedIn: 'platform'})
class PlatformService {
}

const providedInRootInjectionToken = new InjectionToken(
'providedInRootInjectionToken', {providedIn: 'root', factory: () => 'hello world'});

const providedInPlatformToken = new InjectionToken(
'providedInPlatformToken', {providedIn: 'platform', factory: () => 'hello world'});

@Component({
selector: 'my-comp',
template: 'hello world',
})
class MyComponent {
rootService = inject(RootService);
platformService = inject(PlatformService);
fromRoot = inject(providedInRootInjectionToken);
fromPlatform = inject(providedInPlatformToken);
}

TestBed.configureTestingModule({declarations: [MyComponent]});
TestBed.createComponent(MyComponent);

// MyService should have been configured
const rootServiceProviderConfiguredEvent = searchForProfilerEvent<ProviderConfiguredEvent>(
providerConfiguredEvents, (event) => event.providerRecord.token === RootService);

expect(rootServiceProviderConfiguredEvent).toBeTruthy();
expect(rootServiceProviderConfiguredEvent!.context).toBeTruthy();
expect(rootServiceProviderConfiguredEvent!.context!.injector).toBeInstanceOf(R3Injector);
expect((rootServiceProviderConfiguredEvent!.context!.injector as R3Injector).scopes.has('root'))
.toBeTrue();

const platformServiceProviderConfiguredEvent = searchForProfilerEvent<ProviderConfiguredEvent>(
providerConfiguredEvents, (event) => event.providerRecord.token === PlatformService);
expect(platformServiceProviderConfiguredEvent).toBeTruthy();
expect(platformServiceProviderConfiguredEvent!.context).toBeTruthy();
expect(platformServiceProviderConfiguredEvent!.context!.injector).toBeInstanceOf(R3Injector);
expect((platformServiceProviderConfiguredEvent!.context!.injector as R3Injector)
.scopes.has('platform'))
.toBeTrue();

const providedInRootInjectionTokenProviderConfiguredEvent =
searchForProfilerEvent<ProviderConfiguredEvent>(
providerConfiguredEvents,
(event) => event.providerRecord.token === providedInRootInjectionToken);
expect(providedInRootInjectionTokenProviderConfiguredEvent).toBeTruthy();
expect(providedInRootInjectionTokenProviderConfiguredEvent!.context).toBeTruthy();
expect(providedInRootInjectionTokenProviderConfiguredEvent!.context!.injector)
.toBeInstanceOf(R3Injector);
expect((providedInRootInjectionTokenProviderConfiguredEvent!.context!.injector as R3Injector)
.scopes.has('root'))
.toBeTrue();
expect(providedInRootInjectionTokenProviderConfiguredEvent!.providerRecord.token)
.toBe(providedInRootInjectionToken);

const providedInPlatformTokenProviderConfiguredEvent =
searchForProfilerEvent<ProviderConfiguredEvent>(
providerConfiguredEvents,
(event) => event.providerRecord.token === providedInPlatformToken);
expect(providedInPlatformTokenProviderConfiguredEvent).toBeTruthy();
expect(providedInPlatformTokenProviderConfiguredEvent!.context).toBeTruthy();
expect(providedInPlatformTokenProviderConfiguredEvent!.context!.injector)
.toBeInstanceOf(R3Injector);
expect((providedInPlatformTokenProviderConfiguredEvent!.context!.injector as R3Injector)
.scopes.has('platform'))
.toBeTrue();
expect(providedInPlatformTokenProviderConfiguredEvent!.providerRecord.token)
.toBe(providedInPlatformToken);
});
});

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

0 comments on commit ea6d752

Please sign in to comment.