fix(router): absolute redirects should work with lazy loading

This commit is contained in:
vsavkin 2016-08-04 18:56:22 -07:00 committed by Alex Rickabaugh
parent 4f17dbc721
commit 3a307c2794
5 changed files with 218 additions and 211 deletions

View File

@ -51,57 +51,86 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
export function applyRedirects( export function applyRedirects(
injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree, injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree,
config: Routes): Observable<UrlTree> { config: Routes): Observable<UrlTree> {
return expandSegmentGroup(injector, configLoader, config, urlTree.root, PRIMARY_OUTLET) return new ApplyRedirects(injector, configLoader, urlTree, config).apply();
.map(rootSegmentGroup => createUrlTree(urlTree, rootSegmentGroup)) }
class ApplyRedirects {
private allowRedirects: boolean = true;
constructor(
private injector: Injector, private configLoader: RouterConfigLoader,
private urlTree: UrlTree, private config: Routes) {}
apply(): Observable<UrlTree> {
return this.expandSegmentGroup(this.injector, this.config, this.urlTree.root, PRIMARY_OUTLET)
.map(rootSegmentGroup => this.createUrlTree(rootSegmentGroup))
.catch(e => { .catch(e => {
if (e instanceof AbsoluteRedirect) { if (e instanceof AbsoluteRedirect) {
return of (createUrlTree( // after an absolute redirect we do not apply any more redirects!
urlTree, this.allowRedirects = false;
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: new UrlSegmentGroup(e.segments, {})}))); const group =
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: new UrlSegmentGroup(e.segments, {})});
// we need to run matching, so we can fetch all lazy-loaded modules
return this.match(group);
} else if (e instanceof NoMatch) { } else if (e instanceof NoMatch) {
throw new Error(`Cannot match any routes: '${e.segmentGroup}'`); throw this.noMatchError(e);
} else { } else {
throw e; throw e;
} }
}); });
} }
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegmentGroup): UrlTree { private match(segmentGroup: UrlSegmentGroup): Observable<UrlTree> {
return this.expandSegmentGroup(this.injector, this.config, segmentGroup, PRIMARY_OUTLET)
.map(rootSegmentGroup => this.createUrlTree(rootSegmentGroup))
.catch((e): Observable<UrlTree> => {
if (e instanceof NoMatch) {
throw this.noMatchError(e);
} else {
throw e;
}
});
}
private noMatchError(e: NoMatch): any {
return new Error(`Cannot match any routes: '${e.segmentGroup}'`);
}
private createUrlTree(rootCandidate: UrlSegmentGroup): UrlTree {
const root = rootCandidate.segments.length > 0 ? const root = rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) : new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate; rootCandidate;
return new UrlTree(root, urlTree.queryParams, urlTree.fragment); return new UrlTree(root, this.urlTree.queryParams, this.urlTree.fragment);
} }
function expandSegmentGroup( private expandSegmentGroup(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[], injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup,
segmentGroup: UrlSegmentGroup, outlet: string): Observable<UrlSegmentGroup> { outlet: string): Observable<UrlSegmentGroup> {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) { if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(injector, configLoader, routes, segmentGroup) return this.expandChildren(injector, routes, segmentGroup)
.map(children => new UrlSegmentGroup([], children)); .map(children => new UrlSegmentGroup([], children));
} else { } else {
return expandSegment( return this.expandSegment(
injector, configLoader, segmentGroup, routes, segmentGroup.segments, outlet, true); injector, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
} }
}
function expandChildren( private expandChildren(injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup):
injector: Injector, configLoader: RouterConfigLoader, routes: Route[], Observable<{[name: string]: UrlSegmentGroup}> {
segmentGroup: UrlSegmentGroup): Observable<{[name: string]: UrlSegmentGroup}> {
return waitForMap( return waitForMap(
segmentGroup.children, (childOutlet, child) => expandSegmentGroup( segmentGroup.children,
injector, configLoader, routes, child, childOutlet)); (childOutlet, child) => this.expandSegmentGroup(injector, routes, child, childOutlet));
} }
function expandSegment( private expandSegment(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup, injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], segments: UrlSegment[],
routes: Route[], segments: UrlSegment[], outlet: string, outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
allowRedirects: boolean): Observable<UrlSegmentGroup> { const processRoutes =
const processRoutes = of (...routes) of (...routes)
.map(r => { .map(r => {
return expandSegmentAgainstRoute( return this
injector, configLoader, segmentGroup, routes, r, segments, .expandSegmentAgainstRoute(
outlet, allowRedirects) injector, segmentGroup, routes, r, segments, outlet, allowRedirects)
.catch((e) => { .catch((e) => {
if (e instanceof NoMatch) if (e instanceof NoMatch)
return of (null); return of (null);
@ -118,36 +147,35 @@ function expandSegment(
throw e; throw e;
} }
}); });
} }
function expandSegmentAgainstRoute( private expandSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup, injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
routes: Route[], route: Route, paths: UrlSegment[], outlet: string, paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
allowRedirects: boolean): Observable<UrlSegmentGroup> {
if (getOutlet(route) !== outlet) return noMatch(segmentGroup); if (getOutlet(route) !== outlet) return noMatch(segmentGroup);
if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segmentGroup); if (route.redirectTo !== undefined && !(allowRedirects && this.allowRedirects))
return noMatch(segmentGroup);
if (route.redirectTo !== undefined) { if (route.redirectTo === undefined) {
return expandSegmentAgainstRouteUsingRedirect( return this.matchSegmentAgainstRoute(injector, segmentGroup, route, paths);
injector, configLoader, segmentGroup, routes, route, paths, outlet);
} else { } else {
return matchSegmentAgainstRoute(injector, configLoader, segmentGroup, route, paths); return this.expandSegmentAgainstRouteUsingRedirect(
injector, segmentGroup, routes, route, paths, outlet);
}
} }
}
function expandSegmentAgainstRouteUsingRedirect( private expandSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup, injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
routes: Route[], route: Route, segments: UrlSegment[], segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') { if (route.path === '**') {
return expandWildCardWithParamsAgainstRouteUsingRedirect(route); return this.expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else { } else {
return expandRegularSegmentAgainstRouteUsingRedirect( return this.expandRegularSegmentAgainstRouteUsingRedirect(
injector, configLoader, segmentGroup, routes, route, segments, outlet); injector, segmentGroup, routes, route, segments, outlet);
}
} }
}
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): private expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route):
Observable<UrlSegmentGroup> { Observable<UrlSegmentGroup> {
const newSegments = applyRedirectCommands([], route.redirectTo, {}); const newSegments = applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
@ -155,12 +183,11 @@ function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route):
} else { } else {
return of (new UrlSegmentGroup(newSegments, {})); return of (new UrlSegmentGroup(newSegments, {}));
} }
} }
function expandRegularSegmentAgainstRouteUsingRedirect( private expandRegularSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup, injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
routes: Route[], route: Route, segments: UrlSegment[], segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
outlet: string): Observable<UrlSegmentGroup> {
const {matched, consumedSegments, lastChild, positionalParamSegments} = const {matched, consumedSegments, lastChild, positionalParamSegments} =
match(segmentGroup, route, segments); match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup); if (!matched) return noMatch(segmentGroup);
@ -170,15 +197,15 @@ function expandRegularSegmentAgainstRouteUsingRedirect(
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments); return absoluteRedirect(newSegments);
} else { } else {
return expandSegment( return this.expandSegment(
injector, configLoader, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
outlet, false); false);
}
} }
}
function matchSegmentAgainstRoute( private matchSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, rawSegmentGroup: UrlSegmentGroup, injector: Injector, rawSegmentGroup: UrlSegmentGroup, route: Route,
route: Route, segments: UrlSegment[]): Observable<UrlSegmentGroup> { segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') { if (route.path === '**') {
return of (new UrlSegmentGroup(segments, {})); return of (new UrlSegmentGroup(segments, {}));
@ -188,37 +215,36 @@ function matchSegmentAgainstRoute(
const rawSlicedSegments = segments.slice(lastChild); const rawSlicedSegments = segments.slice(lastChild);
return getChildConfig(injector, configLoader, route).mergeMap(routerConfig => { return this.getChildConfig(injector, route).mergeMap(routerConfig => {
const childInjector = routerConfig.injector; const childInjector = routerConfig.injector;
const childConfig = routerConfig.routes; const childConfig = routerConfig.routes;
const {segmentGroup, slicedSegments} = const {segmentGroup, slicedSegments} =
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig); split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) { if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(childInjector, configLoader, childConfig, segmentGroup) return this.expandChildren(childInjector, childConfig, segmentGroup)
.map(children => new UrlSegmentGroup(consumedSegments, children)); .map(children => new UrlSegmentGroup(consumedSegments, children));
} else if (childConfig.length === 0 && slicedSegments.length === 0) { } else if (childConfig.length === 0 && slicedSegments.length === 0) {
return of (new UrlSegmentGroup(consumedSegments, {})); return of (new UrlSegmentGroup(consumedSegments, {}));
} else { } else {
return expandSegment( return this
childInjector, configLoader, segmentGroup, childConfig, slicedSegments, .expandSegment(
PRIMARY_OUTLET, true) childInjector, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true)
.map(cs => new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children)); .map(cs => new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
} }
}); });
} }
} }
function getChildConfig(injector: Injector, configLoader: RouterConfigLoader, route: Route): private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
Observable<LoadedRouterConfig> {
if (route.children) { if (route.children) {
return of (new LoadedRouterConfig(route.children, injector, null)); return of (new LoadedRouterConfig(route.children, injector, null));
} else if (route.loadChildren) { } else if (route.loadChildren) {
return runGuards(injector, route).mergeMap(shouldLoad => { return runGuards(injector, route).mergeMap(shouldLoad => {
if (shouldLoad) { if (shouldLoad) {
return configLoader.load(injector, route.loadChildren).map(r => { return this.configLoader.load(injector, route.loadChildren).map(r => {
(<any>route)._loadedConfig = r; (<any>route)._loadedConfig = r;
return r; return r;
}); });
@ -229,6 +255,7 @@ function getChildConfig(injector: Injector, configLoader: RouterConfigLoader, ro
} else { } else {
return of (new LoadedRouterConfig([], injector, null)); return of (new LoadedRouterConfig([], injector, null));
} }
}
} }
function runGuards(injector: Injector, route: Route): Observable<boolean> { function runGuards(injector: Injector, route: Route): Observable<boolean> {

View File

@ -18,9 +18,7 @@ import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_
import {last, merge} from './utils/collection'; import {last, merge} from './utils/collection';
import {TreeNode} from './utils/tree'; import {TreeNode} from './utils/tree';
class NoMatch { class NoMatch {}
constructor(public segmentGroup: UrlSegmentGroup = null) {}
}
class InheritedFromParent { class InheritedFromParent {
constructor( constructor(
@ -65,16 +63,10 @@ class Recognizer {
return of (new RouterStateSnapshot(this.url, rootNode)); return of (new RouterStateSnapshot(this.url, rootNode));
} catch (e) { } catch (e) {
if (e instanceof NoMatch) {
return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) =>
obs.error(new Error(`Cannot match any routes: '${e.segmentGroup}'`)));
} else {
return new Observable<RouterStateSnapshot>( return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) => obs.error(e)); (obs: Observer<RouterStateSnapshot>) => obs.error(e));
} }
} }
}
processSegmentGroup( processSegmentGroup(
@ -108,7 +100,7 @@ class Recognizer {
if (!(e instanceof NoMatch)) throw e; if (!(e instanceof NoMatch)) throw e;
} }
} }
throw new NoMatch(segmentGroup); throw new NoMatch();
} }
processSegmentAgainstRoute( processSegmentAgainstRoute(

View File

@ -251,6 +251,21 @@ describe('applyRedirects', () => {
(r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; }); (r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; });
}); });
it('should work with absolute redirects', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, tree(''), config).forEach(r => {
compareTrees(r, tree('a'));
expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
});
});
}); });
describe('empty paths', () => { describe('empty paths', () => {

View File

@ -13,7 +13,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../
import {PRIMARY_OUTLET, Params} from '../src/shared'; import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree'; import {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree';
fdescribe('createUrlTree', () => { describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer(); const serializer = new DefaultUrlSerializer();
it('should navigate to the root', () => { it('should navigate to the root', () => {

View File

@ -259,19 +259,6 @@ describe('recognize', () => {
}); });
}); });
it('should not match when terminal', () => {
recognize(
RootComponent, [{
path: '',
pathMatch: 'full',
component: ComponentA,
children: [{path: 'b', component: ComponentB}]
}],
tree('b'), '')
.subscribe(
() => {}, (e) => { expect(e.message).toEqual('Cannot match any routes: \'b\''); });
});
it('should work (nested case)', () => { it('should work (nested case)', () => {
checkRecognize( checkRecognize(
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '', [{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '',
@ -678,20 +665,6 @@ describe('recognize', () => {
'Two segments cannot have the same outlet name: \'aux:b\' and \'aux:c\'.'); 'Two segments cannot have the same outlet name: \'aux:b\' and \'aux:c\'.');
}); });
}); });
it('should error when no matching routes', () => {
recognize(RootComponent, [{path: 'a', component: ComponentA}], tree('invalid'), 'invalid')
.subscribe((_) => {}, (s: RouterStateSnapshot) => {
expect(s.toString()).toContain('Cannot match any routes');
});
});
it('should error when no matching routes (too short)', () => {
recognize(RootComponent, [{path: 'a/:id', component: ComponentA}], tree('a'), 'a')
.subscribe((_) => {}, (s: RouterStateSnapshot) => {
expect(s.toString()).toContain('Cannot match any routes');
});
});
}); });
}); });