@@ -616,7 +616,7 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
616616 if ( child . index && ( path === '/' || path === '' ) ) {
617617 return true ;
618618 }
619- if ( child . path ) {
619+ if ( child . path !== undefined ) {
620620 const match = matchPath ( { path : child . path , end : false } , path ) ;
621621 if ( match ) return true ;
622622 }
@@ -628,8 +628,65 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
628628 return false ;
629629 } ;
630630
631+ // Check if a route pattern has a catch-all at the end
632+ const hasCatchAll = ( path : string ) : boolean => {
633+ return path . endsWith ( '/*' ) || path === '*' ;
634+ } ;
635+
636+ // Check if routeB is more specific than routeA when both match the same path
637+ // A route is more specific if it matches more segments with static/param patterns
638+ // before resorting to a catch-all
639+ const isMoreSpecific = ( pathA : string , pathB : string ) : boolean => {
640+ const segmentsA = pathA . split ( '/' ) . filter ( Boolean ) ;
641+ const segmentsB = pathB . split ( '/' ) . filter ( Boolean ) ;
642+
643+ // Count non-catchall segments
644+ const nonCatchallA = segmentsA . filter ( s => s !== '*' ) . length ;
645+ const nonCatchallB = segmentsB . filter ( s => s !== '*' ) . length ;
646+
647+ // More non-catchall segments = more specific
648+ if ( nonCatchallB > nonCatchallA ) return true ;
649+
650+ // Same number of non-catchall segments, but B has no catchall while A does
651+ if (
652+ nonCatchallB === nonCatchallA &&
653+ hasCatchAll ( pathA ) &&
654+ ! hasCatchAll ( pathB )
655+ ) {
656+ return true ;
657+ }
658+
659+ // If B has more static segments, it's more specific
660+ const staticA = segmentsA . filter (
661+ s => ! s . startsWith ( ':' ) && s !== '*'
662+ ) . length ;
663+ const staticB = segmentsB . filter (
664+ s => ! s . startsWith ( ':' ) && s !== '*'
665+ ) . length ;
666+ if ( staticB > staticA ) return true ;
667+
668+ return false ;
669+ } ;
670+
631671 // Find matching route and calculate the new matched path
632672 const matchResult = useMemo ( ( ) => {
673+ // Helper to calculate matched path
674+ const calcMatchedPath = ( matchedPortion : string ) : string => {
675+ if ( ! parentMatchedPath || parentMatchedPath === '/' ) {
676+ return matchedPortion ;
677+ } else if ( matchedPortion === '/' ) {
678+ return parentMatchedPath ;
679+ } else {
680+ return `${ parentMatchedPath } ${ matchedPortion } ` ;
681+ }
682+ } ;
683+
684+ let bestMatch : {
685+ route : RouteConfig ;
686+ matchedPath : string ;
687+ params : Record < string , string | undefined > ;
688+ } | null = null ;
689+
633690 for ( const route of routes ) {
634691 if ( route . index && ( pathname === '/' || pathname === '' ) ) {
635692 // Index route: matched path stays the same
@@ -641,7 +698,6 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
641698 // Match if any child route would match the pathname
642699 if ( route . path === undefined && route . children ) {
643700 if ( childRouteMatches ( route . children , pathname ) ) {
644- // Pathless layout doesn't consume any path
645701 const newMatchedPath = parentMatchedPath || '/' ;
646702 return { route, matchedPath : newMatchedPath , params : { } } ;
647703 }
@@ -650,27 +706,46 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
650706 if ( route . path !== undefined ) {
651707 const match = matchPath ( route . path , pathname ) ;
652708 if ( match ) {
653- // Calculate new matched path by combining parent + matched portion
654709 const matchedPortion = match . pathnameBase || '/' ;
655- let newMatchedPath : string ;
656- if ( ! parentMatchedPath || parentMatchedPath === '/' ) {
657- // No parent or root parent - just use matched portion
658- newMatchedPath = matchedPortion ;
659- } else if ( matchedPortion === '/' ) {
660- // Matched root - keep parent
661- newMatchedPath = parentMatchedPath ;
662- } else {
663- // Combine parent and matched portion
664- newMatchedPath = `${ parentMatchedPath } ${ matchedPortion } ` ;
665- }
666- return {
710+ const newMatchedPath = calcMatchedPath ( matchedPortion ) ;
711+
712+ const currentMatch = {
667713 route,
668714 matchedPath : newMatchedPath ,
669715 params : match . params ,
670716 } ;
717+
718+ // If no best match yet, use this one
719+ if ( ! bestMatch ) {
720+ bestMatch = currentMatch ;
721+ // If this match doesn't use a catch-all, return immediately
722+ if ( ! hasCatchAll ( route . path ) ) {
723+ return bestMatch ;
724+ }
725+ // Otherwise, keep looking for more specific matches
726+ continue ;
727+ }
728+
729+ // Check if this route is more specific than the current best
730+ if (
731+ bestMatch . route . path &&
732+ isMoreSpecific ( bestMatch . route . path , route . path )
733+ ) {
734+ bestMatch = currentMatch ;
735+ // If this match doesn't use a catch-all, return immediately
736+ if ( ! hasCatchAll ( route . path ) ) {
737+ return bestMatch ;
738+ }
739+ }
671740 }
672741 }
673742 }
743+
744+ // If we found a match (possibly a catch-all), return it
745+ if ( bestMatch ) {
746+ return bestMatch ;
747+ }
748+
674749 // Check for catch-all route (path="*")
675750 const catchAll = routes . find ( r => r . path === '*' ) ;
676751 if ( catchAll ) {
@@ -682,7 +757,14 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
682757 } ;
683758 }
684759 return null ;
685- } , [ routes , pathname , parentMatchedPath , fullPathname ] ) ;
760+ } , [
761+ routes ,
762+ parentMatchedPath ,
763+ pathname ,
764+ childRouteMatches ,
765+ isMoreSpecific ,
766+ fullPathname ,
767+ ] ) ;
686768
687769 // Now that all hooks have been called, we can safely return early
688770 // if we're outside the basename scope
0 commit comments