From 02d78ef6056f6a162d92701ffe5da2dd05235050 Mon Sep 17 00:00:00 2001 From: AleksanderBodurri Date: Fri, 10 Nov 2023 02:22:35 -0500 Subject: [PATCH] fix(core): handle non-container environment injector cases (#52774) Previously we had logic for a special case where a root injector in standalone apps would skip the import paths calculation step for the `getEnvironmentInjectorProviders` function. This commit intends to fix this for two other cases, namely: - When an injector is created by a route (via the `providers` field and lazy loading). - When an injector is manually created and attached to the injector tree It does this by assuming that any environment injector it cannot find a provider imports container for was created without one, and simply returns the raw provider records without the import paths calculation. PR Close #52774 --- .../render3/util/injector_discovery_utils.ts | 23 +++---- .../test/acceptance/injector_profiler_spec.ts | 60 +++++++++++++++++++ 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/packages/core/src/render3/util/injector_discovery_utils.ts b/packages/core/src/render3/util/injector_discovery_utils.ts index 6cb85c4758f10..cb5181f4260f4 100644 --- a/packages/core/src/render3/util/injector_discovery_utils.ts +++ b/packages/core/src/render3/util/injector_discovery_utils.ts @@ -405,16 +405,15 @@ function getEnvironmentInjectorProviders(injector: EnvironmentInjector): Provide const providerImportsContainer = getProviderImportsContainer(injector); if (providerImportsContainer === null) { - // There is a special case where the bootstrapped component does not - // import any NgModules. In this case the environment injector connected to - // that component is the root injector, which does not have a provider imports - // container (and thus no concept of module import paths). Therefore we simply - // return the provider records as is. - if (isRootInjector(injector)) { - return providerRecordsWithoutImportPaths; - } - - throwError('Could not determine where injector providers were configured.'); + // We assume that if an environment injector exists without an associated provider imports + // container, it was created without such a container. Some examples cases where this could + // happen: + // - The root injector of a standalone application + // - A router injector created by using the providers array in a lazy loaded route + // - A manually created injector that is attached to the injector tree + // Since each of these cases has no provider container, there is no concept of import paths, + // so we can simply return the provider records. + return providerRecordsWithoutImportPaths; } const providerToPath = getProviderImportPaths(providerImportsContainer); @@ -448,10 +447,6 @@ function isPlatformInjector(injector: Injector) { return injector instanceof R3Injector && injector.scopes.has('platform'); } -function isRootInjector(injector: Injector) { - return injector instanceof R3Injector && injector.scopes.has('root'); -} - /** * Gets the providers configured on an injector. * diff --git a/packages/core/test/acceptance/injector_profiler_spec.ts b/packages/core/test/acceptance/injector_profiler_spec.ts index d8595d5eb626f..45abc965f7457 100644 --- a/packages/core/test/acceptance/injector_profiler_spec.ts +++ b/packages/core/test/acceptance/injector_profiler_spec.ts @@ -731,6 +731,66 @@ describe('getInjectorProviders', () => { expect(myServiceProviderRecord!.importPath![1]).toBe(ModuleA); })); + it('should be able to determine providers in a lazy route that has providers', fakeAsync(() => { + class MyService {} + + @Component({selector: 'my-comp-b', template: 'hello world', standalone: true}) + class MyStandaloneComponentB { + injector = inject(Injector); + } + + @Component({ + selector: 'my-comp', + template: ``, + imports: [MyStandaloneComponentB, RouterOutlet], + standalone: true + }) + class MyStandaloneComponent { + injector = inject(Injector); + @ViewChild(RouterOutlet) routerOutlet: RouterOutlet|undefined; + } + + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([ + { + path: 'lazy', + loadComponent: () => MyStandaloneComponentB, + providers: [MyService], + }, + ])] + }); + const root = TestBed.createComponent(MyStandaloneComponent); + TestBed.inject(Router).navigateByUrl('/lazy'); + tick(); + root.detectChanges(); + + const myStandalonecomponentB = + root.componentRef.instance!.routerOutlet!.component as MyStandaloneComponentB; + const routeEnvironmentInjector = + (myStandalonecomponentB.injector.get(EnvironmentInjector) as R3Injector); + expect(routeEnvironmentInjector).toBeTruthy(); + expect(routeEnvironmentInjector.source).toBeTruthy(); + expect(routeEnvironmentInjector.source!.startsWith('Route:')).toBeTrue(); + + const myComponentBEnvironmentInjectorProviders = + getInjectorProviders(routeEnvironmentInjector); + const myServiceProviderRecord = + myComponentBEnvironmentInjectorProviders.find(provider => provider.token === MyService); + expect(myServiceProviderRecord).toBeTruthy(); + expect(myServiceProviderRecord!.provider).toBe(MyService); + expect(myServiceProviderRecord!.token).toBe(MyService); + })); + + it('should be able to determine providers in an injector that was created manually', + fakeAsync(() => { + class MyService {} + const injector = Injector.create({providers: [MyService]}) as EnvironmentInjector; + const providers = getInjectorProviders(injector); + expect(providers.length).toBe(1); + expect(providers[0].token).toBe(MyService); + expect(providers[0].provider).toBe(MyService); + })); + it('should be able to get injector providers for element injectors created by components rendering in an ngFor', () => { class MyService {}