@@ -125,6 +125,103 @@ interface ComputeParentPathOptions {
125125 hasWildcardRoute : boolean ;
126126}
127127
128+ /**
129+ * Checks if any route matches as a specific (non-wildcard, non-index) route.
130+ */
131+ const findSpecificMatch = ( routeChildren : React . ReactElement [ ] , remainingPath : string ) : boolean => {
132+ return routeChildren . some (
133+ ( route ) => isSpecificRouteMatch ( route , remainingPath ) || matchesEmbeddedWildcardRoute ( route , remainingPath )
134+ ) ;
135+ } ;
136+
137+ /**
138+ * Checks if any specific route could plausibly match the remaining path.
139+ * Used to determine if we should fall back to a wildcard match.
140+ */
141+ const couldSpecificRouteMatch = ( routeChildren : React . ReactElement [ ] , remainingPath : string ) : boolean => {
142+ const remainingFirstSegment = remainingPath . split ( '/' ) [ 0 ] ;
143+ return routeChildren . some ( ( route ) => {
144+ const routePath = route . props . path as string | undefined ;
145+ if ( ! routePath || routePath === '*' || routePath === '/*' ) return false ;
146+ if ( route . props . index ) return false ;
147+
148+ const routeFirstSegment = routePath . split ( '/' ) [ 0 ] . replace ( / [ * : ] / g, '' ) ;
149+ if ( ! routeFirstSegment ) return false ;
150+
151+ // Check for prefix overlap (either direction)
152+ return (
153+ routeFirstSegment . startsWith ( remainingFirstSegment . slice ( 0 , 3 ) ) ||
154+ remainingFirstSegment . startsWith ( routeFirstSegment . slice ( 0 , 3 ) )
155+ ) ;
156+ } ) ;
157+ } ;
158+
159+ /**
160+ * Checks for index route match when remaining path is empty.
161+ * Index routes only match at the outlet's mount path level.
162+ */
163+ const checkIndexMatch = (
164+ parentPath : string ,
165+ remainingPath : string ,
166+ hasIndexRoute : boolean ,
167+ outletMountPath : string | undefined
168+ ) : string | undefined => {
169+ if ( ( remainingPath === '' || remainingPath === '/' ) && hasIndexRoute ) {
170+ if ( outletMountPath ) {
171+ // Index should only match at the existing mount path
172+ return parentPath === outletMountPath ? parentPath : undefined ;
173+ }
174+ // No mount path yet - this would establish it
175+ return parentPath ;
176+ }
177+ return undefined ;
178+ } ;
179+
180+ /**
181+ * Determines the best parent path from the available matches.
182+ * Priority: specific > wildcard > index
183+ */
184+ const selectBestMatch = (
185+ specificMatch : string | undefined ,
186+ wildcardMatch : string | undefined ,
187+ indexMatch : string | undefined
188+ ) : string | undefined => {
189+ return specificMatch ?? wildcardMatch ?? indexMatch ;
190+ } ;
191+
192+ /**
193+ * Handles outlets with only absolute routes by computing their common prefix.
194+ */
195+ const computeAbsoluteRoutesParentPath = (
196+ routeChildren : React . ReactElement [ ] ,
197+ currentPathname : string ,
198+ outletMountPath : string | undefined
199+ ) : ParentPathResult | undefined => {
200+ const absolutePathRoutes = routeChildren . filter ( ( route ) => {
201+ const path = route . props . path ;
202+ return path && path . startsWith ( '/' ) ;
203+ } ) ;
204+
205+ if ( absolutePathRoutes . length === 0 ) {
206+ return undefined ;
207+ }
208+
209+ const absolutePaths = absolutePathRoutes . map ( ( r ) => r . props . path as string ) ;
210+ const commonPrefix = computeCommonPrefix ( absolutePaths ) ;
211+
212+ if ( ! commonPrefix || commonPrefix === '/' ) {
213+ return undefined ;
214+ }
215+
216+ const newOutletMountPath = outletMountPath || commonPrefix ;
217+
218+ if ( ! currentPathname . startsWith ( commonPrefix ) ) {
219+ return { parentPath : undefined , outletMountPath : newOutletMountPath } ;
220+ }
221+
222+ return { parentPath : commonPrefix , outletMountPath : newOutletMountPath } ;
223+ } ;
224+
128225/**
129226 * Computes the parent path for a nested outlet based on the current pathname
130227 * and the outlet's route configuration.
@@ -139,9 +236,7 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
139236 const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } =
140237 options ;
141238
142- // If this outlet previously established a mount path and the current
143- // pathname is outside of that scope, do not attempt to re-compute a new
144- // parent path.
239+ // If pathname is outside the established mount path scope, skip computation
145240 if ( outletMountPath && ! currentPathname . startsWith ( outletMountPath ) ) {
146241 return { parentPath : undefined , outletMountPath } ;
147242 }
@@ -150,105 +245,49 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
150245 const segments = currentPathname . split ( '/' ) . filter ( Boolean ) ;
151246
152247 if ( segments . length >= 1 ) {
153- // Find matches at each level, keeping track of the FIRST (shortest) match
154- let firstSpecificMatch : string | undefined = undefined ;
155- let firstWildcardMatch : string | undefined = undefined ;
156- let indexMatchAtMount : string | undefined = undefined ;
248+ let firstSpecificMatch : string | undefined ;
249+ let firstWildcardMatch : string | undefined ;
250+ let indexMatchAtMount : string | undefined ;
157251
158- // Start at i = 1 (normal case: strip at least one segment for parent path)
252+ // Iterate through path segments to find the shortest matching parent path
159253 for ( let i = 1 ; i <= segments . length ; i ++ ) {
160254 const parentPath = '/' + segments . slice ( 0 , i ) . join ( '/' ) ;
161255 const remainingPath = segments . slice ( i ) . join ( '/' ) ;
162256
163- // Check for specific route matches (non-wildcard-only, non-index)
164- // Also check routes with embedded wildcards (e.g., "tab1/*")
165- const hasSpecificMatch = routeChildren . some (
166- ( route ) => isSpecificRouteMatch ( route , remainingPath ) || matchesEmbeddedWildcardRoute ( route , remainingPath )
167- ) ;
168- if ( hasSpecificMatch && ! firstSpecificMatch ) {
257+ // Check for specific route match (highest priority)
258+ if ( ! firstSpecificMatch && findSpecificMatch ( routeChildren , remainingPath ) ) {
169259 firstSpecificMatch = parentPath ;
170- // Found a specific match - this is our answer for non-index routes
171260 break ;
172261 }
173262
174- // Check if wildcard would match this remaining path
175- // Only if remaining is non-empty (wildcard needs something to match)
176- if ( remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && ! firstWildcardMatch ) {
177- // Check if any specific route could plausibly match this remaining path
178- const remainingFirstSegment = remainingPath . split ( '/' ) [ 0 ] ;
179- const couldAnyRouteMatch = routeChildren . some ( ( route ) => {
180- const routePath = route . props . path as string | undefined ;
181- if ( ! routePath || routePath === '*' || routePath === '/*' ) return false ;
182- if ( route . props . index ) return false ;
183-
184- const routeFirstSegment = routePath . split ( '/' ) [ 0 ] . replace ( / [ * : ] / g, '' ) ;
185- if ( ! routeFirstSegment ) return false ;
186-
187- // Check for prefix overlap (either direction)
188- return (
189- routeFirstSegment . startsWith ( remainingFirstSegment . slice ( 0 , 3 ) ) ||
190- remainingFirstSegment . startsWith ( routeFirstSegment . slice ( 0 , 3 ) )
191- ) ;
192- } ) ;
193-
194- // Only save wildcard match if no specific route could match
195- if ( ! couldAnyRouteMatch ) {
263+ // Check for wildcard match (only if remaining path is non-empty)
264+ const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/' ;
265+ if ( ! firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute ) {
266+ if ( ! couldSpecificRouteMatch ( routeChildren , remainingPath ) ) {
196267 firstWildcardMatch = parentPath ;
197- // Continue looking - might find a specific match at a longer path
198268 }
199269 }
200270
201- // Check for index route match when remaining path is empty
202- // BUT only at the outlet's mount path level
203- if ( ( remainingPath === '' || remainingPath === '/' ) && hasIndexRoute ) {
204- // Index route matches when current path exactly matches the mount path
205- // If we already have an outletMountPath, index should only match there
206- if ( outletMountPath ) {
207- if ( parentPath === outletMountPath ) {
208- indexMatchAtMount = parentPath ;
209- }
210- } else {
211- // No mount path set yet - index would establish this as mount path
212- // But only if we haven't found a better match
213- indexMatchAtMount = parentPath ;
214- }
271+ // Check for index route match
272+ const indexMatch = checkIndexMatch ( parentPath , remainingPath , hasIndexRoute , outletMountPath ) ;
273+ if ( indexMatch ) {
274+ indexMatchAtMount = indexMatch ;
215275 }
216276 }
217277
218- // Fallback: check at root level (i = 0) for embedded wildcard routes.
219- // This handles outlets inside root-level splat routes where routes like
220- // "tab1/*" need to match the full pathname.
278+ // Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
221279 if ( ! firstSpecificMatch ) {
222280 const fullRemainingPath = segments . join ( '/' ) ;
223- const hasRootLevelMatch = routeChildren . some ( ( route ) => matchesEmbeddedWildcardRoute ( route , fullRemainingPath ) ) ;
224- if ( hasRootLevelMatch ) {
281+ if ( routeChildren . some ( ( route ) => matchesEmbeddedWildcardRoute ( route , fullRemainingPath ) ) ) {
225282 firstSpecificMatch = '/' ;
226283 }
227284 }
228285
229- // Determine the best parent path:
230- // 1. Specific match (routes like tabs/*, favorites) - highest priority
231- // 2. Wildcard match (route path="*") - catches unmatched segments
232- // 3. Index match - only valid at the outlet's mount point, not deeper
233- let bestPath : string | undefined = undefined ;
234-
235- if ( firstSpecificMatch ) {
236- bestPath = firstSpecificMatch ;
237- } else if ( firstWildcardMatch ) {
238- bestPath = firstWildcardMatch ;
239- } else if ( indexMatchAtMount ) {
240- // Only use index match if no specific or wildcard matched
241- // This handles the case where pathname exactly matches the mount path
242- bestPath = indexMatchAtMount ;
243- }
286+ const bestPath = selectBestMatch ( firstSpecificMatch , firstWildcardMatch , indexMatchAtMount ) ;
244287
245- // Store the mount path when we first successfully match a route
246- let newOutletMountPath = outletMountPath ;
247- if ( ! outletMountPath && bestPath ) {
248- newOutletMountPath = bestPath ;
249- }
288+ // Establish mount path on first successful match
289+ const newOutletMountPath = outletMountPath || bestPath ;
250290
251- // If we have a mount path, verify the current pathname is within scope
252291 if ( newOutletMountPath && ! currentPathname . startsWith ( newOutletMountPath ) ) {
253292 return { parentPath : undefined , outletMountPath : newOutletMountPath } ;
254293 }
@@ -257,29 +296,11 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
257296 }
258297 }
259298
260- // Handle outlets with ONLY absolute routes (no relative routes or index routes)
261- // Compute the common prefix of all absolute routes to determine the outlet's scope
299+ // Handle outlets with only absolute routes
262300 if ( ! hasRelativeRoutes && ! hasIndexRoute ) {
263- const absolutePathRoutes = routeChildren . filter ( ( route ) => {
264- const path = route . props . path ;
265- return path && path . startsWith ( '/' ) ;
266- } ) ;
267-
268- if ( absolutePathRoutes . length > 0 ) {
269- const absolutePaths = absolutePathRoutes . map ( ( r ) => r . props . path as string ) ;
270- const commonPrefix = computeCommonPrefix ( absolutePaths ) ;
271-
272- if ( commonPrefix && commonPrefix !== '/' ) {
273- // Set the mount path based on common prefix of absolute routes
274- const newOutletMountPath = outletMountPath || commonPrefix ;
275-
276- // Check if current pathname is within scope
277- if ( ! currentPathname . startsWith ( commonPrefix ) ) {
278- return { parentPath : undefined , outletMountPath : newOutletMountPath } ;
279- }
280-
281- return { parentPath : commonPrefix , outletMountPath : newOutletMountPath } ;
282- }
301+ const result = computeAbsoluteRoutesParentPath ( routeChildren , currentPathname , outletMountPath ) ;
302+ if ( result ) {
303+ return result ;
283304 }
284305 }
285306
0 commit comments