diff --git a/packages/router/src/config.ts b/packages/router/src/config.ts index 0b711e8bd6..bd4404b5b3 100644 --- a/packages/router/src/config.ts +++ b/packages/router/src/config.ts @@ -457,3 +457,9 @@ function getFullPath(parentPath: string, currentRoute: Route): string { return `${parentPath}/${currentRoute.path}`; } } + + +export function copyConfig(r: Route): Route { + const children = r.children && r.children.map(copyConfig); + return children ? {...r, children} : {...r}; +} \ No newline at end of file diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 330f4ed1cc..7aedb1115a 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -18,7 +18,7 @@ import {map} from 'rxjs/operator/map'; import {mergeMap} from 'rxjs/operator/mergeMap'; import {applyRedirects} from './apply_redirects'; -import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config'; +import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, copyConfig, validateConfig} from './config'; import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; @@ -34,6 +34,7 @@ import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tr import {forEach} from './utils/collection'; import {TreeNode, nodeChildrenAsMap} from './utils/tree'; + /** * @whatItDoes Represents the extra options used during navigation. * @@ -342,7 +343,7 @@ export class Router { */ resetConfig(config: Routes): void { validateConfig(config); - this.config = config; + this.config = config.map(copyConfig); this.navigated = false; this.lastSuccessfulId = -1; } diff --git a/packages/router/src/router_config_loader.ts b/packages/router/src/router_config_loader.ts index 08d28c777a..b62a3ef24b 100644 --- a/packages/router/src/router_config_loader.ts +++ b/packages/router/src/router_config_loader.ts @@ -12,7 +12,7 @@ import {fromPromise} from 'rxjs/observable/fromPromise'; import {of } from 'rxjs/observable/of'; import {map} from 'rxjs/operator/map'; import {mergeMap} from 'rxjs/operator/mergeMap'; -import {LoadChildren, LoadedRouterConfig, Route} from './config'; +import {LoadChildren, LoadedRouterConfig, Route, copyConfig} from './config'; import {flatten, wrapIntoObservable} from './utils/collection'; /** @@ -41,7 +41,7 @@ export class RouterConfigLoader { const module = factory.create(parentInjector); - return new LoadedRouterConfig(flatten(module.injector.get(ROUTES)), module); + return new LoadedRouterConfig(flatten(module.injector.get(ROUTES)).map(copyConfig), module); }); } diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index c9dd46045b..66b606d740 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -22,6 +22,38 @@ import {RouterTestingModule} from '../testing/src/router_testing_module'; import {Logger, createActivatedRouteSnapshot, provideTokenLogger} from './helpers'; describe('Router', () => { + + describe('resetConfig', () => { + class TestComponent {} + + beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); }); + + it('should copy config to avoid mutations of user-provided objects', () => { + const r: Router = TestBed.get(Router); + const configs = [{ + path: 'a', + component: TestComponent, + children: [{path: 'b', component: TestComponent}, {path: 'c', component: TestComponent}] + }]; + r.resetConfig(configs); + + let rConfigs = r.config; + + // routes array and shallow copy + expect(configs).not.toBe(rConfigs); + expect(configs[0]).not.toBe(rConfigs[0]); + expect(configs[0].path).toBe(rConfigs[0].path); + expect(configs[0].component).toBe(rConfigs[0].component); + + // children should be new array and routes shallow copied + expect(configs[0].children).not.toBe(rConfigs[0].children); + expect(configs[0].children[0]).not.toBe(rConfigs[0].children ![0]); + expect(configs[0].children[0].path).toBe(rConfigs[0].children ![0].path); + expect(configs[0].children[1]).not.toBe(rConfigs[0].children ![1]); + expect(configs[0].children[1].path).toBe(rConfigs[0].children ![1].path); + }); + }); + describe('resetRootComponentType', () => { class NewRootComponent {} diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index 7e63bd62ab..8df0e8bc46 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -210,4 +210,35 @@ describe('RouterPreloader', () => { expect((c[1] as any)._loadedConfig).toBeDefined(); }))); }); + + describe('should copy loaded configs', () => { + const configs = [{path: 'LoadedModule1', component: LazyLoadedCmp}]; + @NgModule({declarations: [LazyLoadedCmp], imports: [RouterModule.forChild(configs)]}) + class LoadedModule { + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([{path: 'lazy1', loadChildren: 'expected'}])], + providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}] + }); + }); + + it('should work', + fakeAsync(inject( + [NgModuleFactoryLoader, RouterPreloader, Router], + (loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => { + loader.stubbedModules = {expected: LoadedModule}; + + preloader.preload().subscribe(() => {}); + + tick(); + + const c = router.config as{_loadedConfig: LoadedRouterConfig}[]; + expect(c[0]._loadedConfig).toBeDefined(); + expect(c[0]._loadedConfig !.routes).not.toBe(configs); + expect(c[0]._loadedConfig !.routes[0]).not.toBe(configs[0]); + expect(c[0]._loadedConfig !.routes[0].component).toBe(configs[0].component); + }))); + }); });