refactor(react-router): extract helper functions from computeParentPath

This commit is contained in:
ShaneK
2025-12-17 10:14:57 -08:00
parent d0451eda09
commit 6f184e5e2c

View File

@@ -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;
}
}