diff --git a/packages/router/src/config.ts b/packages/router/src/config.ts index 8b6f2ab078..044cdcd796 100644 --- a/packages/router/src/config.ts +++ b/packages/router/src/config.ts @@ -9,9 +9,7 @@ import {NgModuleFactory, NgModuleRef, Type} from '@angular/core'; import {Observable} from 'rxjs'; -import {EmptyOutletComponent} from './components/empty_outlet'; import {ActivatedRouteSnapshot} from './router_state'; -import {PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup} from './url_tree'; @@ -490,107 +488,3 @@ export interface Route { export class LoadedRouterConfig { constructor(public routes: Route[], public module: NgModuleRef) {} } - -export function validateConfig(config: Routes, parentPath: string = ''): void { - // forEach doesn't iterate undefined values - for (let i = 0; i < config.length; i++) { - const route: Route = config[i]; - const fullPath: string = getFullPath(parentPath, route); - validateNode(route, fullPath); - } -} - -function validateNode(route: Route, fullPath: string): void { - if (!route) { - throw new Error(` - Invalid configuration of route '${fullPath}': Encountered undefined route. - The reason might be an extra comma. - - Example: - const routes: Routes = [ - { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, - { path: 'dashboard', component: DashboardComponent },, << two commas - { path: 'detail/:id', component: HeroDetailComponent } - ]; - `); - } - if (Array.isArray(route)) { - throw new Error(`Invalid configuration of route '${fullPath}': Array cannot be specified`); - } - if (!route.component && !route.children && !route.loadChildren && - (route.outlet && route.outlet !== PRIMARY_OUTLET)) { - throw new Error(`Invalid configuration of route '${ - fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`); - } - if (route.redirectTo && route.children) { - throw new Error(`Invalid configuration of route '${ - fullPath}': redirectTo and children cannot be used together`); - } - if (route.redirectTo && route.loadChildren) { - throw new Error(`Invalid configuration of route '${ - fullPath}': redirectTo and loadChildren cannot be used together`); - } - if (route.children && route.loadChildren) { - throw new Error(`Invalid configuration of route '${ - fullPath}': children and loadChildren cannot be used together`); - } - if (route.redirectTo && route.component) { - throw new Error(`Invalid configuration of route '${ - fullPath}': redirectTo and component cannot be used together`); - } - if (route.path && route.matcher) { - throw new Error( - `Invalid configuration of route '${fullPath}': path and matcher cannot be used together`); - } - if (route.redirectTo === void 0 && !route.component && !route.children && !route.loadChildren) { - throw new Error(`Invalid configuration of route '${ - fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`); - } - if (route.path === void 0 && route.matcher === void 0) { - throw new Error(`Invalid configuration of route '${ - fullPath}': routes must have either a path or a matcher specified`); - } - if (typeof route.path === 'string' && route.path.charAt(0) === '/') { - throw new Error(`Invalid configuration of route '${fullPath}': path cannot start with a slash`); - } - if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) { - const exp = - `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`; - throw new Error(`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${ - route.redirectTo}"}': please provide 'pathMatch'. ${exp}`); - } - if (route.pathMatch !== void 0 && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') { - throw new Error(`Invalid configuration of route '${ - fullPath}': pathMatch can only be set to 'prefix' or 'full'`); - } - if (route.children) { - validateConfig(route.children, fullPath); - } -} - -function getFullPath(parentPath: string, currentRoute: Route): string { - if (!currentRoute) { - return parentPath; - } - if (!parentPath && !currentRoute.path) { - return ''; - } else if (parentPath && !currentRoute.path) { - return `${parentPath}/`; - } else if (!parentPath && currentRoute.path) { - return currentRoute.path; - } else { - return `${parentPath}/${currentRoute.path}`; - } -} - -/** - * Makes a copy of the config and adds any default required properties. - */ -export function standardizeConfig(r: Route): Route { - const children = r.children && r.children.map(standardizeConfig); - const c = children ? {...r, children} : {...r}; - if (!c.component && (children || c.loadChildren) && (c.outlet && c.outlet !== PRIMARY_OUTLET)) { - c.component = EmptyOutletComponent; - } - return c; -} diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 7809e17892..83b915fa3c 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -11,7 +11,7 @@ import {Compiler, Injectable, Injector, isDevMode, NgModuleFactoryLoader, NgModu import {BehaviorSubject, EMPTY, Observable, of, Subject, SubscriptionLike} from 'rxjs'; import {catchError, filter, finalize, map, switchMap, tap} from 'rxjs/operators'; -import {QueryParamsHandling, Route, Routes, standardizeConfig, validateConfig} from './config'; +import {QueryParamsHandling, Route, Routes} from './config'; import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; @@ -28,6 +28,7 @@ import {ActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from import {isNavigationCancelingError, navigationCancelingError, Params} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {containsTree, createEmptyUrlTree, UrlSerializer, UrlTree} from './url_tree'; +import {standardizeConfig, validateConfig} from './utils/config'; import {Checks, getAllRouteGuards} from './utils/preactivation'; import {isUrlTree} from './utils/type_guards'; diff --git a/packages/router/src/router_config_loader.ts b/packages/router/src/router_config_loader.ts index d9c014f345..4f2bda6975 100644 --- a/packages/router/src/router_config_loader.ts +++ b/packages/router/src/router_config_loader.ts @@ -10,8 +10,9 @@ import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoad import {from, Observable, of} from 'rxjs'; import {map, mergeMap} from 'rxjs/operators'; -import {LoadChildren, LoadedRouterConfig, Route, standardizeConfig} from './config'; +import {LoadChildren, LoadedRouterConfig, Route} from './config'; import {flatten, wrapIntoObservable} from './utils/collection'; +import {standardizeConfig} from './utils/config'; /** * The [DI token](guide/glossary/#di-token) for a router configuration. diff --git a/packages/router/src/utils/config.ts b/packages/router/src/utils/config.ts new file mode 100644 index 0000000000..961ee3c54f --- /dev/null +++ b/packages/router/src/utils/config.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {EmptyOutletComponent} from '../components/empty_outlet'; +import {Route, Routes} from '../config'; +import {PRIMARY_OUTLET} from '../shared'; + +export function validateConfig(config: Routes, parentPath: string = ''): void { + // forEach doesn't iterate undefined values + for (let i = 0; i < config.length; i++) { + const route: Route = config[i]; + const fullPath: string = getFullPath(parentPath, route); + validateNode(route, fullPath); + } +} + +function validateNode(route: Route, fullPath: string): void { + if (!route) { + throw new Error(` + Invalid configuration of route '${fullPath}': Encountered undefined route. + The reason might be an extra comma. + + Example: + const routes: Routes = [ + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent },, << two commas + { path: 'detail/:id', component: HeroDetailComponent } + ]; + `); + } + if (Array.isArray(route)) { + throw new Error(`Invalid configuration of route '${fullPath}': Array cannot be specified`); + } + if (!route.component && !route.children && !route.loadChildren && + (route.outlet && route.outlet !== PRIMARY_OUTLET)) { + throw new Error(`Invalid configuration of route '${ + fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`); + } + if (route.redirectTo && route.children) { + throw new Error(`Invalid configuration of route '${ + fullPath}': redirectTo and children cannot be used together`); + } + if (route.redirectTo && route.loadChildren) { + throw new Error(`Invalid configuration of route '${ + fullPath}': redirectTo and loadChildren cannot be used together`); + } + if (route.children && route.loadChildren) { + throw new Error(`Invalid configuration of route '${ + fullPath}': children and loadChildren cannot be used together`); + } + if (route.redirectTo && route.component) { + throw new Error(`Invalid configuration of route '${ + fullPath}': redirectTo and component cannot be used together`); + } + if (route.path && route.matcher) { + throw new Error( + `Invalid configuration of route '${fullPath}': path and matcher cannot be used together`); + } + if (route.redirectTo === void 0 && !route.component && !route.children && !route.loadChildren) { + throw new Error(`Invalid configuration of route '${ + fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`); + } + if (route.path === void 0 && route.matcher === void 0) { + throw new Error(`Invalid configuration of route '${ + fullPath}': routes must have either a path or a matcher specified`); + } + if (typeof route.path === 'string' && route.path.charAt(0) === '/') { + throw new Error(`Invalid configuration of route '${fullPath}': path cannot start with a slash`); + } + if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) { + const exp = + `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`; + throw new Error(`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${ + route.redirectTo}"}': please provide 'pathMatch'. ${exp}`); + } + if (route.pathMatch !== void 0 && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') { + throw new Error(`Invalid configuration of route '${ + fullPath}': pathMatch can only be set to 'prefix' or 'full'`); + } + if (route.children) { + validateConfig(route.children, fullPath); + } +} + +function getFullPath(parentPath: string, currentRoute: Route): string { + if (!currentRoute) { + return parentPath; + } + if (!parentPath && !currentRoute.path) { + return ''; + } else if (parentPath && !currentRoute.path) { + return `${parentPath}/`; + } else if (!parentPath && currentRoute.path) { + return currentRoute.path; + } else { + return `${parentPath}/${currentRoute.path}`; + } +} + +/** + * Makes a copy of the config and adds any default required properties. + */ +export function standardizeConfig(r: Route): Route { + const children = r.children && r.children.map(standardizeConfig); + const c = children ? {...r, children} : {...r}; + if (!c.component && (children || c.loadChildren) && (c.outlet && c.outlet !== PRIMARY_OUTLET)) { + c.component = EmptyOutletComponent; + } + return c; +} diff --git a/packages/router/test/config.spec.ts b/packages/router/test/config.spec.ts index fb4bb03d1e..76d32f3f5a 100644 --- a/packages/router/test/config.spec.ts +++ b/packages/router/test/config.spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {validateConfig} from '../src/config'; import {PRIMARY_OUTLET} from '../src/shared'; +import {validateConfig} from '../src/utils/config'; describe('config', () => { describe('validateConfig', () => {