From 6f184e5e2c984bf8a78528e28e9982b0c9b22ffb Mon Sep 17 00:00:00 2001 From: ShaneK Date: Wed, 17 Dec 2025 10:14:57 -0800 Subject: [PATCH] refactor(react-router): extract helper functions from computeParentPath --- .../ReactRouter/utils/computeParentPath.ts | 221 ++++++++++-------- 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/packages/react-router/src/ReactRouter/utils/computeParentPath.ts b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts index 338efc2834..06dbf87876 100644 --- a/packages/react-router/src/ReactRouter/utils/computeParentPath.ts +++ b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts @@ -125,6 +125,103 @@ interface ComputeParentPathOptions { hasWildcardRoute: boolean; } +/** + * Checks if any route matches as a specific (non-wildcard, non-index) route. + */ +const findSpecificMatch = (routeChildren: React.ReactElement[], remainingPath: string): boolean => { + return routeChildren.some( + (route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath) + ); +}; + +/** + * Checks if any specific route could plausibly match the remaining path. + * Used to determine if we should fall back to a wildcard match. + */ +const couldSpecificRouteMatch = (routeChildren: React.ReactElement[], remainingPath: string): boolean => { + const remainingFirstSegment = remainingPath.split('/')[0]; + return routeChildren.some((route) => { + const routePath = route.props.path as string | undefined; + if (!routePath || routePath === '*' || routePath === '/*') return false; + if (route.props.index) return false; + + const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, ''); + if (!routeFirstSegment) return false; + + // Check for prefix overlap (either direction) + return ( + routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) || + remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)) + ); + }); +}; + +/** + * Checks for index route match when remaining path is empty. + * Index routes only match at the outlet's mount path level. + */ +const checkIndexMatch = ( + parentPath: string, + remainingPath: string, + hasIndexRoute: boolean, + outletMountPath: string | undefined +): string | undefined => { + if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) { + if (outletMountPath) { + // Index should only match at the existing mount path + return parentPath === outletMountPath ? parentPath : undefined; + } + // No mount path yet - this would establish it + return parentPath; + } + return undefined; +}; + +/** + * Determines the best parent path from the available matches. + * Priority: specific > wildcard > index + */ +const selectBestMatch = ( + specificMatch: string | undefined, + wildcardMatch: string | undefined, + indexMatch: string | undefined +): string | undefined => { + return specificMatch ?? wildcardMatch ?? indexMatch; +}; + +/** + * Handles outlets with only absolute routes by computing their common prefix. + */ +const computeAbsoluteRoutesParentPath = ( + routeChildren: React.ReactElement[], + currentPathname: string, + outletMountPath: string | undefined +): ParentPathResult | undefined => { + const absolutePathRoutes = routeChildren.filter((route) => { + const path = route.props.path; + return path && path.startsWith('/'); + }); + + if (absolutePathRoutes.length === 0) { + return undefined; + } + + const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string); + const commonPrefix = computeCommonPrefix(absolutePaths); + + if (!commonPrefix || commonPrefix === '/') { + return undefined; + } + + const newOutletMountPath = outletMountPath || commonPrefix; + + if (!currentPathname.startsWith(commonPrefix)) { + return { parentPath: undefined, outletMountPath: newOutletMountPath }; + } + + return { parentPath: commonPrefix, outletMountPath: newOutletMountPath }; +}; + /** * Computes the parent path for a nested outlet based on the current pathname * and the outlet's route configuration. @@ -139,9 +236,7 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options; - // If this outlet previously established a mount path and the current - // pathname is outside of that scope, do not attempt to re-compute a new - // parent path. + // If pathname is outside the established mount path scope, skip computation if (outletMountPath && !currentPathname.startsWith(outletMountPath)) { return { parentPath: undefined, outletMountPath }; } @@ -150,105 +245,49 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath const segments = currentPathname.split('/').filter(Boolean); if (segments.length >= 1) { - // Find matches at each level, keeping track of the FIRST (shortest) match - let firstSpecificMatch: string | undefined = undefined; - let firstWildcardMatch: string | undefined = undefined; - let indexMatchAtMount: string | undefined = undefined; + let firstSpecificMatch: string | undefined; + let firstWildcardMatch: string | undefined; + let indexMatchAtMount: string | undefined; - // Start at i = 1 (normal case: strip at least one segment for parent path) + // Iterate through path segments to find the shortest matching parent path for (let i = 1; i <= segments.length; i++) { const parentPath = '/' + segments.slice(0, i).join('/'); const remainingPath = segments.slice(i).join('/'); - // Check for specific route matches (non-wildcard-only, non-index) - // Also check routes with embedded wildcards (e.g., "tab1/*") - const hasSpecificMatch = routeChildren.some( - (route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath) - ); - if (hasSpecificMatch && !firstSpecificMatch) { + // Check for specific route match (highest priority) + if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) { firstSpecificMatch = parentPath; - // Found a specific match - this is our answer for non-index routes break; } - // Check if wildcard would match this remaining path - // Only if remaining is non-empty (wildcard needs something to match) - if (remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && !firstWildcardMatch) { - // Check if any specific route could plausibly match this remaining path - const remainingFirstSegment = remainingPath.split('/')[0]; - const couldAnyRouteMatch = routeChildren.some((route) => { - const routePath = route.props.path as string | undefined; - if (!routePath || routePath === '*' || routePath === '/*') return false; - if (route.props.index) return false; - - const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, ''); - if (!routeFirstSegment) return false; - - // Check for prefix overlap (either direction) - return ( - routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) || - remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)) - ); - }); - - // Only save wildcard match if no specific route could match - if (!couldAnyRouteMatch) { + // Check for wildcard match (only if remaining path is non-empty) + const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/'; + if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) { + if (!couldSpecificRouteMatch(routeChildren, remainingPath)) { firstWildcardMatch = parentPath; - // Continue looking - might find a specific match at a longer path } } - // Check for index route match when remaining path is empty - // BUT only at the outlet's mount path level - if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) { - // Index route matches when current path exactly matches the mount path - // If we already have an outletMountPath, index should only match there - if (outletMountPath) { - if (parentPath === outletMountPath) { - indexMatchAtMount = parentPath; - } - } else { - // No mount path set yet - index would establish this as mount path - // But only if we haven't found a better match - indexMatchAtMount = parentPath; - } + // Check for index route match + const indexMatch = checkIndexMatch(parentPath, remainingPath, hasIndexRoute, outletMountPath); + if (indexMatch) { + indexMatchAtMount = indexMatch; } } - // Fallback: check at root level (i = 0) for embedded wildcard routes. - // This handles outlets inside root-level splat routes where routes like - // "tab1/*" need to match the full pathname. + // Fallback: check root level for embedded wildcard routes (e.g., "tab1/*") if (!firstSpecificMatch) { const fullRemainingPath = segments.join('/'); - const hasRootLevelMatch = routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath)); - if (hasRootLevelMatch) { + if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) { firstSpecificMatch = '/'; } } - // Determine the best parent path: - // 1. Specific match (routes like tabs/*, favorites) - highest priority - // 2. Wildcard match (route path="*") - catches unmatched segments - // 3. Index match - only valid at the outlet's mount point, not deeper - let bestPath: string | undefined = undefined; + const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount); - if (firstSpecificMatch) { - bestPath = firstSpecificMatch; - } else if (firstWildcardMatch) { - bestPath = firstWildcardMatch; - } else if (indexMatchAtMount) { - // Only use index match if no specific or wildcard matched - // This handles the case where pathname exactly matches the mount path - bestPath = indexMatchAtMount; - } + // Establish mount path on first successful match + const newOutletMountPath = outletMountPath || bestPath; - // Store the mount path when we first successfully match a route - let newOutletMountPath = outletMountPath; - if (!outletMountPath && bestPath) { - newOutletMountPath = bestPath; - } - - // If we have a mount path, verify the current pathname is within scope if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) { return { parentPath: undefined, outletMountPath: newOutletMountPath }; } @@ -257,29 +296,11 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath } } - // Handle outlets with ONLY absolute routes (no relative routes or index routes) - // Compute the common prefix of all absolute routes to determine the outlet's scope + // Handle outlets with only absolute routes if (!hasRelativeRoutes && !hasIndexRoute) { - const absolutePathRoutes = routeChildren.filter((route) => { - const path = route.props.path; - return path && path.startsWith('/'); - }); - - if (absolutePathRoutes.length > 0) { - const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string); - const commonPrefix = computeCommonPrefix(absolutePaths); - - if (commonPrefix && commonPrefix !== '/') { - // Set the mount path based on common prefix of absolute routes - const newOutletMountPath = outletMountPath || commonPrefix; - - // Check if current pathname is within scope - if (!currentPathname.startsWith(commonPrefix)) { - return { parentPath: undefined, outletMountPath: newOutletMountPath }; - } - - return { parentPath: commonPrefix, outletMountPath: newOutletMountPath }; - } + const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath); + if (result) { + return result; } }