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,183 +51,210 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
export function applyRedirects(
injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree,
config: Routes): Observable<UrlTree> {
return expandSegmentGroup(injector, configLoader, config, urlTree.root, PRIMARY_OUTLET)
.map(rootSegmentGroup => createUrlTree(urlTree, rootSegmentGroup))
.catch(e => {
if (e instanceof AbsoluteRedirect) {
return of (createUrlTree(
urlTree,
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: new UrlSegmentGroup(e.segments, {})})));
} else if (e instanceof NoMatch) {
throw new Error(`Cannot match any routes: '${e.segmentGroup}'`);
return new ApplyRedirects(injector, configLoader, urlTree, config).apply();
}
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 => {
if (e instanceof AbsoluteRedirect) {
// after an absolute redirect we do not apply any more redirects!
this.allowRedirects = false;
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) {
throw this.noMatchError(e);
} else {
throw e;
}
});
}
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 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate;
return new UrlTree(root, this.urlTree.queryParams, this.urlTree.fragment);
}
private expandSegmentGroup(
injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup,
outlet: string): Observable<UrlSegmentGroup> {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return this.expandChildren(injector, routes, segmentGroup)
.map(children => new UrlSegmentGroup([], children));
} else {
return this.expandSegment(
injector, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
}
private expandChildren(injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup):
Observable<{[name: string]: UrlSegmentGroup}> {
return waitForMap(
segmentGroup.children,
(childOutlet, child) => this.expandSegmentGroup(injector, routes, child, childOutlet));
}
private expandSegment(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], segments: UrlSegment[],
outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
const processRoutes =
of (...routes)
.map(r => {
return this
.expandSegmentAgainstRoute(
injector, segmentGroup, routes, r, segments, outlet, allowRedirects)
.catch((e) => {
if (e instanceof NoMatch)
return of (null);
else
throw e;
});
})
.concatAll();
return processRoutes.first(s => !!s).catch((e: any, _: any): Observable<UrlSegmentGroup> => {
if (e instanceof EmptyError) {
throw new NoMatch(segmentGroup);
} else {
throw e;
}
});
}
private expandSegmentAgainstRoute(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
if (getOutlet(route) !== outlet) return noMatch(segmentGroup);
if (route.redirectTo !== undefined && !(allowRedirects && this.allowRedirects))
return noMatch(segmentGroup);
if (route.redirectTo === undefined) {
return this.matchSegmentAgainstRoute(injector, segmentGroup, route, paths);
} else {
return this.expandSegmentAgainstRouteUsingRedirect(
injector, segmentGroup, routes, route, paths, outlet);
}
}
private expandSegmentAgainstRouteUsingRedirect(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return this.expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else {
return this.expandRegularSegmentAgainstRouteUsingRedirect(
injector, segmentGroup, routes, route, segments, outlet);
}
}
private expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route):
Observable<UrlSegmentGroup> {
const newSegments = applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments);
} else {
return of (new UrlSegmentGroup(newSegments, {}));
}
}
private expandRegularSegmentAgainstRouteUsingRedirect(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
const {matched, consumedSegments, lastChild, positionalParamSegments} =
match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup);
const newSegments =
applyRedirectCommands(consumedSegments, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments);
} else {
return this.expandSegment(
injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
false);
}
}
private matchSegmentAgainstRoute(
injector: Injector, rawSegmentGroup: UrlSegmentGroup, route: Route,
segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return of (new UrlSegmentGroup(segments, {}));
} else {
const {matched, consumedSegments, lastChild} = match(rawSegmentGroup, route, segments);
if (!matched) return noMatch(rawSegmentGroup);
const rawSlicedSegments = segments.slice(lastChild);
return this.getChildConfig(injector, route).mergeMap(routerConfig => {
const childInjector = routerConfig.injector;
const childConfig = routerConfig.routes;
const {segmentGroup, slicedSegments} =
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
return this.expandChildren(childInjector, childConfig, segmentGroup)
.map(children => new UrlSegmentGroup(consumedSegments, children));
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
return of (new UrlSegmentGroup(consumedSegments, {}));
} else {
throw e;
return this
.expandSegment(
childInjector, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true)
.map(cs => new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
}
});
}
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegmentGroup): UrlTree {
const root = rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate;
return new UrlTree(root, urlTree.queryParams, urlTree.fragment);
}
function expandSegmentGroup(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[],
segmentGroup: UrlSegmentGroup, outlet: string): Observable<UrlSegmentGroup> {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(injector, configLoader, routes, segmentGroup)
.map(children => new UrlSegmentGroup([], children));
} else {
return expandSegment(
injector, configLoader, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
}
function expandChildren(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[],
segmentGroup: UrlSegmentGroup): Observable<{[name: string]: UrlSegmentGroup}> {
return waitForMap(
segmentGroup.children, (childOutlet, child) => expandSegmentGroup(
injector, configLoader, routes, child, childOutlet));
}
function expandSegment(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], segments: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
const processRoutes = of (...routes)
.map(r => {
return expandSegmentAgainstRoute(
injector, configLoader, segmentGroup, routes, r, segments,
outlet, allowRedirects)
.catch((e) => {
if (e instanceof NoMatch)
return of (null);
else
throw e;
});
})
.concatAll();
return processRoutes.first(s => !!s).catch((e: any, _: any): Observable<UrlSegmentGroup> => {
if (e instanceof EmptyError) {
throw new NoMatch(segmentGroup);
} else {
throw e;
}
});
}
function expandSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, paths: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
if (getOutlet(route) !== outlet) return noMatch(segmentGroup);
if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segmentGroup);
if (route.redirectTo !== undefined) {
return expandSegmentAgainstRouteUsingRedirect(
injector, configLoader, segmentGroup, routes, route, paths, outlet);
} else {
return matchSegmentAgainstRoute(injector, configLoader, segmentGroup, route, paths);
}
}
function expandSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, segments: UrlSegment[],
outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else {
return expandRegularSegmentAgainstRouteUsingRedirect(
injector, configLoader, segmentGroup, routes, route, segments, outlet);
}
}
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route):
Observable<UrlSegmentGroup> {
const newSegments = applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments);
} else {
return of (new UrlSegmentGroup(newSegments, {}));
}
}
function expandRegularSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, segments: UrlSegment[],
outlet: string): Observable<UrlSegmentGroup> {
const {matched, consumedSegments, lastChild, positionalParamSegments} =
match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup);
const newSegments =
applyRedirectCommands(consumedSegments, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments);
} else {
return expandSegment(
injector, configLoader, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)),
outlet, false);
}
}
function matchSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, rawSegmentGroup: UrlSegmentGroup,
route: Route, segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return of (new UrlSegmentGroup(segments, {}));
} else {
const {matched, consumedSegments, lastChild} = match(rawSegmentGroup, route, segments);
if (!matched) return noMatch(rawSegmentGroup);
const rawSlicedSegments = segments.slice(lastChild);
return getChildConfig(injector, configLoader, route).mergeMap(routerConfig => {
const childInjector = routerConfig.injector;
const childConfig = routerConfig.routes;
const {segmentGroup, slicedSegments} =
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(childInjector, configLoader, childConfig, segmentGroup)
.map(children => new UrlSegmentGroup(consumedSegments, children));
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
return of (new UrlSegmentGroup(consumedSegments, {}));
} else {
return expandSegment(
childInjector, configLoader, segmentGroup, childConfig, slicedSegments,
PRIMARY_OUTLET, true)
.map(cs => new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
}
});
}
}
function getChildConfig(injector: Injector, configLoader: RouterConfigLoader, route: Route):
Observable<LoadedRouterConfig> {
if (route.children) {
return of (new LoadedRouterConfig(route.children, injector, null));
} else if (route.loadChildren) {
return runGuards(injector, route).mergeMap(shouldLoad => {
if (shouldLoad) {
return configLoader.load(injector, route.loadChildren).map(r => {
(<any>route)._loadedConfig = r;
return r;
});
} else {
return canLoadFails(route);
}
});
} else {
return of (new LoadedRouterConfig([], injector, null));
private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
if (route.children) {
return of (new LoadedRouterConfig(route.children, injector, null));
} else if (route.loadChildren) {
return runGuards(injector, route).mergeMap(shouldLoad => {
if (shouldLoad) {
return this.configLoader.load(injector, route.loadChildren).map(r => {
(<any>route)._loadedConfig = r;
return r;
});
} else {
return canLoadFails(route);
}
});
} else {
return of (new LoadedRouterConfig([], injector, null));
}
}
}

View File

@ -18,9 +18,7 @@ import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_
import {last, merge} from './utils/collection';
import {TreeNode} from './utils/tree';
class NoMatch {
constructor(public segmentGroup: UrlSegmentGroup = null) {}
}
class NoMatch {}
class InheritedFromParent {
constructor(
@ -65,14 +63,8 @@ class Recognizer {
return of (new RouterStateSnapshot(this.url, rootNode));
} 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>(
(obs: Observer<RouterStateSnapshot>) => obs.error(e));
}
return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) => obs.error(e));
}
}
@ -108,7 +100,7 @@ class Recognizer {
if (!(e instanceof NoMatch)) throw e;
}
}
throw new NoMatch(segmentGroup);
throw new NoMatch();
}
processSegmentAgainstRoute(

View File

@ -251,6 +251,21 @@ describe('applyRedirects', () => {
(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', () => {

View File

@ -13,7 +13,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../
import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree';
fdescribe('createUrlTree', () => {
describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer();
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)', () => {
checkRecognize(
[{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\'.');
});
});
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');
});
});
});
});