fix(router): lazy loaded modules without RouterModule.forChild() won't cause an infinite loop (#36605)

When loading a module that doesn't provide `RouterModule.forChild()` preloader will get stuck
in an infinite loop and throw `ERROR Error: Maximum call stack size exceeded.`
The issue is that child module's `Injector` will look to its parent `Injector` when it doesn't
find any `ROUTES` so it will return routes for it's parent module instead. This will load the
child again that returns its parent's routes and so on.

Closes #29164

PR Close #36605
This commit is contained in:
Martin Sikora 2020-04-11 16:27:12 +02:00 committed by Andrew Kushnir
parent 96690ed3a4
commit b37a9eba1a
3 changed files with 55 additions and 2 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {Compiler, InjectFlags, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {from, Observable, of} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';
@ -41,8 +41,13 @@ export class RouterConfigLoader {
const module = factory.create(parentInjector);
// When loading a module that doesn't provide `RouterModule.forChild()` preloader will get
// stuck in an infinite loop. The child module's Injector will look to its parent `Injector`
// when it doesn't find any ROUTES so it will return routes for it's parent module instead.
return new LoadedRouterConfig(
flatten(module.injector.get(ROUTES)).map(standardizeConfig), module);
flatten(module.injector.get(ROUTES, undefined, InjectFlags.Self | InjectFlags.Optional))
.map(standardizeConfig),
module);
}));
}

View File

@ -5291,6 +5291,10 @@ describe('Integration', () => {
class LoadedModule1 {
}
@NgModule({})
class EmptyModule {
}
beforeEach(() => {
log.length = 0;
TestBed.configureTestingModule({
@ -5355,6 +5359,19 @@ describe('Integration', () => {
expect(firstConfig).toBeUndefined();
expect(log.length).toBe(0);
}));
it('should allow navigation to modules with no routes', fakeAsync(() => {
(TestBed.inject(NgModuleFactoryLoader) as SpyNgModuleFactoryLoader).stubbedModules = {
empty: EmptyModule,
};
const router = TestBed.inject(Router);
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'empty'}]);
router.navigateByUrl('/lazy');
advance(fixture);
}));
});
describe('custom url handling strategies', () => {

View File

@ -261,4 +261,35 @@ describe('RouterPreloader', () => {
expect(c[0]._loadedConfig!.routes[0].component).toBe(configs[0].component);
})));
});
describe(
'should work with lazy loaded modules that don\'t provide RouterModule.forChild()', () => {
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
})
class LoadedModule {
}
@NgModule({})
class EmptyModule {
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(
[{path: 'lazyEmptyModule', loadChildren: 'expected2'}])],
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
});
});
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader) => {
loader.stubbedModules = {expected2: EmptyModule};
preloader.preload().subscribe();
})));
});
});