@@ -14,18 +14,46 @@ import { findRoutesNode } from './utils/findRoutesNode';
1414import { derivePathnameToMatch } from './utils/derivePathnameToMatch' ;
1515import { matchPath } from './utils/matchPath' ;
1616
17+ /**
18+ * Checks if a route matches the remaining path.
19+ * Note: This function is used for checking if ANY route could match, not for determining priority.
20+ * Wildcard routes are handled specially - they're always considered potential matches but should
21+ * be used as fallbacks when no specific route matches.
22+ */
1723const doesRouteMatchRemainingPath = ( route : React . ReactElement , remainingPath : string ) => {
1824 const routePath = route . props . path ;
1925 const isWildcardOnly = routePath === '*' || routePath === '/*' ;
26+ const isIndex = route . props . index ;
2027
21- if ( isWildcardOnly ) {
22- // Treat wildcard-only routes as matching only when we're already at the outlet root.
23- // This prevents them from forcing parent paths to resolve too early (e.g. "/routing"
24- // instead of "/routing/tabs") while still letting them act as fallbacks when no
25- // additional segments remain.
28+ // Index routes only match when remaining path is empty
29+ if ( isIndex ) {
2630 return remainingPath === '' || remainingPath === '/' ;
2731 }
2832
33+ // Wildcard routes can match any path (used as fallback)
34+ if ( isWildcardOnly ) {
35+ return true ; // Wildcards can always potentially match
36+ }
37+
38+ return ! ! matchPath ( {
39+ pathname : remainingPath ,
40+ componentProps : route . props ,
41+ } ) ;
42+ } ;
43+
44+ /**
45+ * Checks if a route is a specific match (not wildcard or index).
46+ */
47+ const isSpecificRouteMatch = ( route : React . ReactElement , remainingPath : string ) => {
48+ const routePath = route . props . path ;
49+ const isWildcardOnly = routePath === '*' || routePath === '/*' ;
50+ const isIndex = route . props . index ;
51+
52+ // Skip wildcards and index routes
53+ if ( isIndex || isWildcardOnly ) {
54+ return false ;
55+ }
56+
2957 return ! ! matchPath ( {
3058 pathname : remainingPath ,
3159 componentProps : route . props ,
@@ -118,19 +146,32 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
118146 const segments = currentPathname . split ( '/' ) . filter ( Boolean ) ;
119147
120148 if ( segments . length >= 1 ) {
149+ let wildcardFallbackPath : string | undefined = undefined ;
150+ let foundPath : string | undefined = undefined ;
151+
121152 // Store the mount path when we first successfully match a route
122153 // This helps us determine our scope for future navigations
154+ // We need to find specific matches, not just wildcard fallbacks
123155 if ( ! this . outletMountPath ) {
124- // Try to find the mount path by looking for successful matches
125156 for ( let i = 1 ; i <= segments . length ; i ++ ) {
126157 const testParentPath = '/' + segments . slice ( 0 , i ) . join ( '/' ) ;
127158 const testRemainingPath = segments . slice ( i ) . join ( '/' ) ;
128- const hasMatch = routeChildren . some ( ( route ) => doesRouteMatchRemainingPath ( route , testRemainingPath ) ) ;
129159
130- if ( hasMatch ) {
160+ // Check for specific matches first
161+ const hasSpecific = routeChildren . some ( ( route ) => isSpecificRouteMatch ( route , testRemainingPath ) ) ;
162+ if ( hasSpecific ) {
131163 this . outletMountPath = testParentPath ;
132164 break ;
133165 }
166+
167+ // Check for index match when remaining is empty
168+ if ( testRemainingPath === '' || testRemainingPath === '/' ) {
169+ const hasIndex = routeChildren . some ( ( route ) => route . props . index ) ;
170+ if ( hasIndex ) {
171+ this . outletMountPath = testParentPath ;
172+ break ;
173+ }
174+ }
134175 }
135176 }
136177
@@ -141,23 +182,69 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
141182 }
142183
143184 // Try different parent path possibilities
144- // Start with shorter parent paths first to find the most appropriate match
185+ // Prefer specific routes over wildcards and index routes
145186 for ( let i = 1 ; i <= segments . length ; i ++ ) {
146187 const parentPath = '/' + segments . slice ( 0 , i ) . join ( '/' ) ;
147188 const remainingPath = segments . slice ( i ) . join ( '/' ) ;
148189
149- // Check if any relative route could match the remaining path
150- const couldMatchRemaining = routeChildren . some ( ( route ) => doesRouteMatchRemainingPath ( route , remainingPath ) ) ;
190+ // Check for specific (non-wildcard, non-index) route matches
191+ const hasSpecificMatch = routeChildren . some ( ( route ) => isSpecificRouteMatch ( route , remainingPath ) ) ;
192+ if ( hasSpecificMatch ) {
193+ foundPath = parentPath ;
194+ break ;
195+ }
151196
152- if ( couldMatchRemaining ) {
153- return parentPath ;
197+ // Check for index route match (only when remaining path is empty AND no wildcard fallback)
198+ // If we already found a wildcard fallback at a shorter path, it means
199+ // the remaining path at that level didn't match any routes, so the
200+ // index match at this longer path is not valid.
201+ if ( ! wildcardFallbackPath && ( remainingPath === '' || remainingPath === '/' ) ) {
202+ const hasIndexMatch = routeChildren . some ( ( route ) => route . props . index ) ;
203+ if ( hasIndexMatch ) {
204+ foundPath = parentPath ;
205+ break ;
206+ }
154207 }
208+
209+ // Track wildcard fallback only if:
210+ // 1. Remaining is non-empty (wildcard matches something)
211+ // 2. No specific route could even START to match the remaining path
212+ if ( ! wildcardFallbackPath && remainingPath !== '' && remainingPath !== '/' ) {
213+ const hasWildcard = routeChildren . some ( ( route ) => {
214+ const routePath = route . props . path ;
215+ return routePath === '*' || routePath === '/*' ;
216+ } ) ;
217+
218+ if ( hasWildcard ) {
219+ // Check if any specific route could plausibly match this remaining path
220+ const remainingFirstSegment = remainingPath . split ( '/' ) [ 0 ] ;
221+ const couldAnyRouteMatch = routeChildren . some ( ( route ) => {
222+ const routePath = route . props . path as string | undefined ;
223+ if ( ! routePath || routePath === '*' || routePath === '/*' ) return false ;
224+ if ( route . props . index ) return false ;
225+
226+ const routeFirstSegment = routePath . split ( '/' ) [ 0 ] . replace ( / [ * : ] / g, '' ) ;
227+ if ( ! routeFirstSegment ) return false ;
228+
229+ // Check for prefix overlap (either direction)
230+ return routeFirstSegment . startsWith ( remainingFirstSegment . slice ( 0 , 3 ) ) ||
231+ remainingFirstSegment . startsWith ( routeFirstSegment . slice ( 0 , 3 ) ) ;
232+ } ) ;
233+
234+ // Only save wildcard fallback if no specific route could match
235+ if ( ! couldAnyRouteMatch ) {
236+ wildcardFallbackPath = parentPath ;
237+ }
238+ }
239+ }
240+ }
241+
242+ // If no specific match found, use wildcard fallback
243+ if ( ! foundPath && wildcardFallbackPath ) {
244+ return wildcardFallbackPath ;
155245 }
156246
157- // If we couldn't find any parent path that makes sense for our relative routes,
158- // it means this outlet is being rendered outside its expected routing context
159- // Return undefined to signal that this outlet shouldn't handle routes
160- return undefined ;
247+ return foundPath ;
161248 }
162249 }
163250 }
0 commit comments