From b37a9eba1a2d84c686d738d8a31c9dd0cdec62fd Mon Sep 17 00:00:00 2001 From: Martin Sikora Date: Sat, 11 Apr 2020 16:27:12 +0200 Subject: [PATCH] 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 --- packages/router/src/router_config_loader.ts | 9 ++++-- packages/router/test/integration.spec.ts | 17 ++++++++++ packages/router/test/router_preloader.spec.ts | 31 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/router/src/router_config_loader.ts b/packages/router/src/router_config_loader.ts index 4f2bda6975..e9d21974ee 100644 --- a/packages/router/src/router_config_loader.ts +++ b/packages/router/src/router_config_loader.ts @@ -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); })); } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index ebcaf1e86d..4bec29733c 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -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', () => { diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index f186e1796b..db4edfbd38 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -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(); + }))); + }); });