@@ -14,10 +14,11 @@ import {LoadedRouterConfig, Route, Routes} from './config';
1414import { CanLoadFn } from './interfaces' ;
1515import { prioritizedGuardValue } from './operators/prioritized_guard_value' ;
1616import { RouterConfigLoader } from './router_config_loader' ;
17- import { defaultUrlMatcher , navigationCancelingError , Params , PRIMARY_OUTLET } from './shared' ;
17+ import { navigationCancelingError , Params , PRIMARY_OUTLET } from './shared' ;
1818import { UrlSegment , UrlSegmentGroup , UrlSerializer , UrlTree } from './url_tree' ;
1919import { forEach , waitForMap , wrapIntoObservable } from './utils/collection' ;
2020import { getOutlet , groupRoutesByOutlet } from './utils/config' ;
21+ import { match , noLeftoversInUrl , split } from './utils/config_matching' ;
2122import { isCanLoad , isFunction , isUrlTree } from './utils/type_guards' ;
2223
2324class NoMatch {
@@ -79,9 +80,10 @@ class ApplyRedirects {
7980 apply ( ) : Observable < UrlTree > {
8081 const expanded$ =
8182 this . expandSegmentGroup ( this . ngModule , this . config , this . urlTree . root , PRIMARY_OUTLET ) ;
82- const urlTrees$ = expanded$ . pipe (
83- map ( ( rootSegmentGroup : UrlSegmentGroup ) => this . createUrlTree (
84- rootSegmentGroup , this . urlTree . queryParams , this . urlTree . fragment ! ) ) ) ;
83+ const urlTrees$ = expanded$ . pipe ( map ( ( rootSegmentGroup : UrlSegmentGroup ) => {
84+ return this . createUrlTree (
85+ squashSegmentGroup ( rootSegmentGroup ) , this . urlTree . queryParams , this . urlTree . fragment ! ) ;
86+ } ) ) ;
8587 return urlTrees$ . pipe ( catchError ( ( e : any ) => {
8688 if ( e instanceof AbsoluteRedirect ) {
8789 // after an absolute redirect we do not apply any more redirects!
@@ -101,9 +103,10 @@ class ApplyRedirects {
101103 private match ( tree : UrlTree ) : Observable < UrlTree > {
102104 const expanded$ =
103105 this . expandSegmentGroup ( this . ngModule , this . config , tree . root , PRIMARY_OUTLET ) ;
104- const mapped$ = expanded$ . pipe (
105- map ( ( rootSegmentGroup : UrlSegmentGroup ) =>
106- this . createUrlTree ( rootSegmentGroup , tree . queryParams , tree . fragment ! ) ) ) ;
106+ const mapped$ = expanded$ . pipe ( map ( ( rootSegmentGroup : UrlSegmentGroup ) => {
107+ return this . createUrlTree (
108+ squashSegmentGroup ( rootSegmentGroup ) , tree . queryParams , tree . fragment ! ) ;
109+ } ) ) ;
107110 return mapped$ . pipe ( catchError ( ( e : any ) : Observable < UrlTree > => {
108111 if ( e instanceof NoMatch ) {
109112 throw this . noMatchError ( e ) ;
@@ -172,7 +175,7 @@ class ApplyRedirects {
172175 first ( ( s : UrlSegmentGroup | null ) : s is UrlSegmentGroup => s !== null ) ,
173176 catchError ( e => {
174177 if ( e instanceof EmptyError || e . name === 'EmptyError' ) {
175- if ( this . noLeftoversInUrl ( segmentGroup , segments , outlet ) ) {
178+ if ( noLeftoversInUrl ( segmentGroup , segments , outlet ) ) {
176179 return of ( new UrlSegmentGroup ( [ ] , { } ) ) ;
177180 }
178181 throw new NoMatch ( segmentGroup ) ;
@@ -197,11 +200,6 @@ class ApplyRedirects {
197200 ) ;
198201 }
199202
200- private noLeftoversInUrl ( segmentGroup : UrlSegmentGroup , segments : UrlSegment [ ] , outlet : string ) :
201- boolean {
202- return segments . length === 0 && ! segmentGroup . children [ outlet ] ;
203- }
204-
205203 private expandSegmentAgainstRoute (
206204 ngModule : NgModuleRef < any > , segmentGroup : UrlSegmentGroup , routes : Route [ ] , route : Route ,
207205 paths : UrlSegment [ ] , outlet : string , allowRedirects : boolean ) : Observable < UrlSegmentGroup > {
@@ -256,8 +254,8 @@ class ApplyRedirects {
256254 match ( segmentGroup , route , segments ) ;
257255 if ( ! matched ) return noMatch ( segmentGroup ) ;
258256
259- const newTree = this . applyRedirectCommands (
260- consumedSegments , route . redirectTo ! , < any > positionalParamSegments ) ;
257+ const newTree =
258+ this . applyRedirectCommands ( consumedSegments , route . redirectTo ! , positionalParamSegments ) ;
261259 if ( route . redirectTo ! . startsWith ( '/' ) ) {
262260 return absoluteRedirect ( newTree ) ;
263261 }
@@ -296,6 +294,14 @@ class ApplyRedirects {
296294
297295 const { segmentGroup, slicedSegments} =
298296 split ( rawSegmentGroup , consumedSegments , rawSlicedSegments , childConfig ) ;
297+ // TODO(atscott): clearing the source segment and segment index shift is only necessary to
298+ // prevent failures in tests which assert exact object matches. The `split` is now shared
299+ // between applyRedirects and recognize and only the `recognize` step needs these properties.
300+ // Before the implementations were merged, the applyRedirects would not assign them.
301+ // We should be able to remove this logic as a "breaking change" but should do some more
302+ // investigation into the failures first.
303+ segmentGroup . _sourceSegment = undefined ;
304+ segmentGroup . _segmentIndexShift = undefined ;
299305
300306 if ( slicedSegments . length === 0 && segmentGroup . hasChildren ( ) ) {
301307 const expanded$ = this . expandChildren ( childModule , childConfig , segmentGroup ) ;
@@ -467,64 +473,15 @@ class ApplyRedirects {
467473 }
468474}
469475
470- function match ( segmentGroup : UrlSegmentGroup , route : Route , segments : UrlSegment [ ] ) : {
471- matched : boolean ,
472- consumedSegments : UrlSegment [ ] ,
473- lastChild : number ,
474- positionalParamSegments : { [ k : string ] : UrlSegment }
475- } {
476- if ( route . path === '' ) {
477- if ( ( route . pathMatch === 'full' ) && ( segmentGroup . hasChildren ( ) || segments . length > 0 ) ) {
478- return { matched : false , consumedSegments : [ ] , lastChild : 0 , positionalParamSegments : { } } ;
479- }
480-
481- return { matched : true , consumedSegments : [ ] , lastChild : 0 , positionalParamSegments : { } } ;
482- }
483-
484- const matcher = route . matcher || defaultUrlMatcher ;
485- const res = matcher ( segments , segmentGroup , route ) ;
486-
487- if ( ! res ) {
488- return {
489- matched : false ,
490- consumedSegments : < any [ ] > [ ] ,
491- lastChild : 0 ,
492- positionalParamSegments : { } ,
493- } ;
494- }
495-
496- return {
497- matched : true ,
498- consumedSegments : res . consumed ! ,
499- lastChild : res . consumed . length ! ,
500- positionalParamSegments : res . posParams ! ,
501- } ;
502- }
503-
504- function split (
505- segmentGroup : UrlSegmentGroup , consumedSegments : UrlSegment [ ] , slicedSegments : UrlSegment [ ] ,
506- config : Route [ ] ) {
507- if ( slicedSegments . length > 0 &&
508- containsEmptyPathRedirectsWithNamedOutlets ( segmentGroup , slicedSegments , config ) ) {
509- const s = new UrlSegmentGroup (
510- consumedSegments ,
511- createChildrenForEmptySegments (
512- config , new UrlSegmentGroup ( slicedSegments , segmentGroup . children ) ) ) ;
513- return { segmentGroup : mergeTrivialChildren ( s ) , slicedSegments : [ ] } ;
514- }
515-
516- if ( slicedSegments . length === 0 &&
517- containsEmptyPathRedirects ( segmentGroup , slicedSegments , config ) ) {
518- const s = new UrlSegmentGroup (
519- segmentGroup . segments ,
520- addEmptySegmentsToChildrenIfNeeded (
521- segmentGroup , slicedSegments , config , segmentGroup . children ) ) ;
522- return { segmentGroup : mergeTrivialChildren ( s ) , slicedSegments} ;
523- }
524-
525- return { segmentGroup, slicedSegments} ;
526- }
527476
477+ /**
478+ * When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
479+ *
480+ * When a segment group has only one child which is a primary outlet, merges that child into the
481+ * parent. That is, the child segment group's segments are merged into the `s` and the child's
482+ * children become the children of `s`. Think of this like a 'squash', merging the child segment
483+ * group into the parent.
484+ */
528485function mergeTrivialChildren ( s : UrlSegmentGroup ) : UrlSegmentGroup {
529486 if ( s . numberOfChildren === 1 && s . children [ PRIMARY_OUTLET ] ) {
530487 const c = s . children [ PRIMARY_OUTLET ] ;
@@ -534,46 +491,20 @@ function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
534491 return s ;
535492}
536493
537- function addEmptySegmentsToChildrenIfNeeded (
538- segmentGroup : UrlSegmentGroup , slicedSegments : UrlSegment [ ] , routes : Route [ ] ,
539- children : { [ name : string ] : UrlSegmentGroup } ) : { [ name : string ] : UrlSegmentGroup } {
540- const res : { [ name : string ] : UrlSegmentGroup } = { } ;
541- for ( const r of routes ) {
542- if ( isEmptyPathRedirect ( segmentGroup , slicedSegments , r ) && ! children [ getOutlet ( r ) ] ) {
543- res [ getOutlet ( r ) ] = new UrlSegmentGroup ( [ ] , { } ) ;
544- }
545- }
546- return { ...children , ...res } ;
547- }
548-
549- function createChildrenForEmptySegments (
550- routes : Route [ ] , primarySegmentGroup : UrlSegmentGroup ) : { [ name : string ] : UrlSegmentGroup } {
551- const res : { [ name : string ] : UrlSegmentGroup } = { } ;
552- res [ PRIMARY_OUTLET ] = primarySegmentGroup ;
553- for ( const r of routes ) {
554- if ( r . path === '' && getOutlet ( r ) !== PRIMARY_OUTLET ) {
555- res [ getOutlet ( r ) ] = new UrlSegmentGroup ( [ ] , { } ) ;
494+ /**
495+ * Recursively merges primary segment children into their parents and also drops empty children
496+ * (those which have no segments and no children themselves). The latter prevents serializing a
497+ * group into something like `/a(aux:)`, where `aux` is an empty child segment.
498+ */
499+ function squashSegmentGroup ( segmentGroup : UrlSegmentGroup ) : UrlSegmentGroup {
500+ const newChildren = { } as any ;
501+ for ( const [ childOutlet , child ] of Object . entries ( segmentGroup . children ) ) {
502+ const childCandidate = squashSegmentGroup ( child ) ;
503+ // don't add empty children
504+ if ( childCandidate . segments . length > 0 || childCandidate . hasChildren ( ) ) {
505+ newChildren [ childOutlet ] = childCandidate ;
556506 }
557507 }
558- return res ;
559- }
560-
561- function containsEmptyPathRedirectsWithNamedOutlets (
562- segmentGroup : UrlSegmentGroup , segments : UrlSegment [ ] , routes : Route [ ] ) : boolean {
563- return routes . some (
564- r => isEmptyPathRedirect ( segmentGroup , segments , r ) && getOutlet ( r ) !== PRIMARY_OUTLET ) ;
565- }
566-
567- function containsEmptyPathRedirects (
568- segmentGroup : UrlSegmentGroup , segments : UrlSegment [ ] , routes : Route [ ] ) : boolean {
569- return routes . some ( r => isEmptyPathRedirect ( segmentGroup , segments , r ) ) ;
570- }
571-
572- function isEmptyPathRedirect (
573- segmentGroup : UrlSegmentGroup , segments : UrlSegment [ ] , r : Route ) : boolean {
574- if ( ( segmentGroup . hasChildren ( ) || segments . length > 0 ) && r . pathMatch === 'full' ) {
575- return false ;
576- }
577-
578- return r . path === '' && r . redirectTo !== undefined ;
508+ const s = new UrlSegmentGroup ( segmentGroup . segments , newChildren ) ;
509+ return mergeTrivialChildren ( s ) ;
579510}
0 commit comments