Skip to content

Commit

Permalink
fix(core): handle non-container environment injector cases (angular#5…
Browse files Browse the repository at this point in the history
…2774)

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 angular#52774
  • Loading branch information
AleksanderBodurri authored and rlmestre committed Jan 26, 2024
1 parent 4070f07 commit 02d78ef
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 14 deletions.
23 changes: 9 additions & 14 deletions packages/core/src/render3/util/injector_discovery_utils.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand Down
60 changes: 60 additions & 0 deletions packages/core/test/acceptance/injector_profiler_spec.ts
Expand Up @@ -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: `<router-outlet/>`,
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 {}
Expand Down

0 comments on commit 02d78ef

Please sign in to comment.