Skip to content

Commit 6f184e5

Browse files
committed
refactor(react-router): extract helper functions from computeParentPath
1 parent d0451ed commit 6f184e5

File tree

1 file changed

+121
-100
lines changed

1 file changed

+121
-100
lines changed

packages/react-router/src/ReactRouter/utils/computeParentPath.ts

Lines changed: 121 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)