Skip to content

Commit 7b4dcee

Browse files
authored
refactor(router-core): index routes have index nodes in the segment tree (#6171)
1 parent 6839bfb commit 7b4dcee

9 files changed

Lines changed: 122 additions & 52 deletions

File tree

packages/react-router/tests/link.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,10 +2044,16 @@ describe('Link', () => {
20442044

20452045
const postRoute = createRoute({
20462046
getParentRoute: () => postsRoute,
2047-
path: '$postId/',
2047+
path: '$postId',
20482048
component: PostComponent,
20492049
})
20502050

2051+
const postIndexRoute = createRoute({
2052+
getParentRoute: () => postRoute,
2053+
path: '/',
2054+
component: () => <div>Post Index</div>,
2055+
})
2056+
20512057
const DetailsComponent = () => {
20522058
return (
20532059
<>
@@ -2080,7 +2086,11 @@ describe('Link', () => {
20802086
indexRoute,
20812087
layoutRoute.addChildren([
20822088
postsRoute.addChildren([
2083-
postRoute.addChildren([detailsRoute, informationRoute]),
2089+
postRoute.addChildren([
2090+
postIndexRoute,
2091+
detailsRoute,
2092+
informationRoute,
2093+
]),
20842094
]),
20852095
]),
20862096
]),

packages/router-core/src/new-process-route-tree.ts

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ export const SEGMENT_TYPE_PATHNAME = 0
77
export const SEGMENT_TYPE_PARAM = 1
88
export const SEGMENT_TYPE_WILDCARD = 2
99
export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
10+
const SEGMENT_TYPE_INDEX = 4
1011

12+
/**
13+
* All the kinds of segments that can be present in a route path.
14+
*/
1115
export type SegmentKind =
1216
| typeof SEGMENT_TYPE_PATHNAME
1317
| typeof SEGMENT_TYPE_PARAM
1418
| typeof SEGMENT_TYPE_WILDCARD
1519
| typeof SEGMENT_TYPE_OPTIONAL_PARAM
1620

21+
/**
22+
* All the kinds of segments that can be present in the segment tree.
23+
*/
24+
type ExtendedSegmentKind = SegmentKind | typeof SEGMENT_TYPE_INDEX
25+
1726
const PARAM_W_CURLY_BRACES_RE =
1827
/^([^{]*)\{\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/ // prefix{$paramName}suffix
1928
const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
@@ -326,20 +335,26 @@ function parseSegments<TRouteLike extends RouteLike>(
326335
}
327336
node = nextNode
328337
}
329-
if ((route.path || !route.children) && !route.isRoot) {
330-
const isIndex = path.endsWith('/')
331-
// we cannot fuzzy match an index route,
332-
// but if there is *also* a layout route at this path, save it as notFound
333-
// we can use it when fuzzy matching to display the NotFound component in the layout route
334-
if (!isIndex) node.notFound = route
335-
// does the new route take precedence over an existing one?
336-
// yes if previous is not an index route and new one is an index route
337-
if (!node.route || (!node.isIndex && isIndex)) {
338-
node.route = route
339-
// when replacing, replace all attributes that are route-specific (`fullPath` only at the moment)
340-
node.fullPath = route.fullPath ?? route.from
341-
}
342-
node.isIndex ||= isIndex
338+
339+
const isLeaf = (route.path || !route.children) && !route.isRoot
340+
341+
// create index node
342+
if (isLeaf && path.endsWith('/')) {
343+
const indexNode = createStaticNode<TRouteLike>(
344+
route.fullPath ?? route.from,
345+
)
346+
indexNode.kind = SEGMENT_TYPE_INDEX
347+
indexNode.parent = node
348+
depth++
349+
indexNode.depth = depth
350+
node.index = indexNode
351+
node = indexNode
352+
}
353+
354+
// make node "matchable"
355+
if (isLeaf && !node.route) {
356+
node.route = route
357+
node.fullPath = route.fullPath ?? route.from
343358
}
344359
}
345360
if (route.children)
@@ -417,6 +432,7 @@ function createStaticNode<T extends RouteLike>(
417432
return {
418433
kind: SEGMENT_TYPE_PATHNAME,
419434
depth: 0,
435+
index: null,
420436
static: null,
421437
staticInsensitive: null,
422438
dynamic: null,
@@ -425,8 +441,6 @@ function createStaticNode<T extends RouteLike>(
425441
route: null,
426442
fullPath,
427443
parent: null,
428-
isIndex: false,
429-
notFound: null,
430444
}
431445
}
432446

@@ -447,6 +461,7 @@ function createDynamicNode<T extends RouteLike>(
447461
return {
448462
kind,
449463
depth: 0,
464+
index: null,
450465
static: null,
451466
staticInsensitive: null,
452467
dynamic: null,
@@ -455,16 +470,14 @@ function createDynamicNode<T extends RouteLike>(
455470
route: null,
456471
fullPath,
457472
parent: null,
458-
isIndex: false,
459-
notFound: null,
460473
caseSensitive,
461474
prefix,
462475
suffix,
463476
}
464477
}
465478

466479
type StaticSegmentNode<T extends RouteLike> = SegmentNode<T> & {
467-
kind: typeof SEGMENT_TYPE_PATHNAME
480+
kind: typeof SEGMENT_TYPE_PATHNAME | typeof SEGMENT_TYPE_INDEX
468481
}
469482

470483
type DynamicSegmentNode<T extends RouteLike> = SegmentNode<T> & {
@@ -482,12 +495,15 @@ type AnySegmentNode<T extends RouteLike> =
482495
| DynamicSegmentNode<T>
483496

484497
type SegmentNode<T extends RouteLike> = {
485-
kind: SegmentKind
498+
kind: ExtendedSegmentKind
499+
500+
/** Exact index segment (highest priority) */
501+
index: StaticSegmentNode<T> | null
486502

487-
/** Static segments (highest priority) */
503+
/** Static segments (2nd priority) */
488504
static: Map<string, StaticSegmentNode<T>> | null
489505

490-
/** Case insensitive static segments (second highest priority) */
506+
/** Case insensitive static segments (3rd highest priority) */
491507
staticInsensitive: Map<string, StaticSegmentNode<T>> | null
492508

493509
/** Dynamic segments ($param) */
@@ -508,12 +524,6 @@ type SegmentNode<T extends RouteLike> = {
508524
parent: AnySegmentNode<T> | null
509525

510526
depth: number
511-
512-
/** is it an index route (trailing / path), only valid for nodes with a `route` */
513-
isIndex: boolean
514-
515-
/** Same as `route`, but only present if both an "index route" and a "layout route" exist at this path */
516-
notFound: T | null
517527
}
518528

519529
type RouteLike = {
@@ -713,11 +723,8 @@ function findMatch<T extends RouteLike>(
713723
const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
714724
if (!leaf) return null
715725
const params = extractParams(path, parts, leaf)
716-
const isFuzzyMatch = '**' in leaf
717-
if (isFuzzyMatch) params['**'] = leaf['**']
718-
const route = isFuzzyMatch
719-
? (leaf.node.notFound ?? leaf.node.route!)
720-
: leaf.node.route!
726+
if ('**' in leaf) params['**'] = leaf['**']!
727+
const route = leaf.node.route!
721728
return {
722729
route,
723730
params,
@@ -837,6 +844,11 @@ function getNodeMatch<T extends RouteLike>(
837844
segmentTree: AnySegmentNode<T>,
838845
fuzzy: boolean,
839846
) {
847+
// quick check for root index
848+
// this is an optimization, algorithm should work correctly without this block
849+
if (path === '/' && segmentTree.index)
850+
return { node: segmentTree.index, skipped: 0 }
851+
840852
const trailingSlash = !last(parts)
841853
const pathIsIndex = trailingSlash && path !== '/'
842854
const partsLength = parts.length - (trailingSlash ? 1 : 0)
@@ -872,22 +884,36 @@ function getNodeMatch<T extends RouteLike>(
872884
let { node, index, skipped, depth, statics, dynamics, optionals } = frame
873885

874886
// In fuzzy mode, track the best partial match we've found so far
875-
if (fuzzy && node.notFound && isFrameMoreSpecific(bestFuzzy, frame)) {
887+
if (
888+
fuzzy &&
889+
node.route &&
890+
node.kind !== SEGMENT_TYPE_INDEX &&
891+
isFrameMoreSpecific(bestFuzzy, frame)
892+
) {
876893
bestFuzzy = frame
877894
}
878895

879896
const isBeyondPath = index === partsLength
880897
if (isBeyondPath) {
881-
if (node.route && (!pathIsIndex || node.isIndex)) {
898+
if (node.route && (!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX)) {
882899
if (isFrameMoreSpecific(bestMatch, frame)) {
883900
bestMatch = frame
884901
}
885902

886903
// perfect match, no need to continue
887-
if (statics === partsLength && node.isIndex) return bestMatch
904+
// this is an optimization, algorithm should work correctly without this block
905+
if (
906+
statics === partsLength &&
907+
!dynamics &&
908+
!optionals &&
909+
!skipped &&
910+
node.kind === SEGMENT_TYPE_INDEX
911+
) {
912+
return bestMatch
913+
}
888914
}
889-
// beyond the length of the path parts, only skipped optional segments or wildcard segments can match
890-
if (!node.optional && !node.wildcard) continue
915+
// beyond the length of the path parts, only index segments, or skipped optional segments, or wildcard segments can match
916+
if (!node.optional && !node.wildcard && !node.index) continue
891917
}
892918

893919
const part = isBeyondPath ? undefined : parts[index]!
@@ -1022,6 +1048,19 @@ function getNodeMatch<T extends RouteLike>(
10221048
})
10231049
}
10241050
}
1051+
1052+
// 0. Try index match
1053+
if (isBeyondPath && node.index) {
1054+
stack.push({
1055+
node: node.index,
1056+
index,
1057+
skipped,
1058+
depth: depth + 1,
1059+
statics,
1060+
dynamics,
1061+
optionals,
1062+
})
1063+
}
10251064
}
10261065

10271066
if (bestMatch && wildcardMatch) {
@@ -1064,8 +1103,10 @@ function isFrameMoreSpecific(
10641103
(next.dynamics === prev.dynamics &&
10651104
(next.optionals > prev.optionals ||
10661105
(next.optionals === prev.optionals &&
1067-
(next.node.isIndex > prev.node.isIndex ||
1068-
(next.node.isIndex === prev.node.isIndex &&
1106+
((next.node.kind === SEGMENT_TYPE_INDEX) >
1107+
(prev.node.kind === SEGMENT_TYPE_INDEX) ||
1108+
((next.node.kind === SEGMENT_TYPE_INDEX) ===
1109+
(prev.node.kind === SEGMENT_TYPE_INDEX) &&
10691110
next.depth > prev.depth)))))))
10701111
)
10711112
}

packages/router-core/tests/new-process-route-tree.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ describe('findRouteMatch', () => {
718718
path: 'dashboard/',
719719
},
720720
{
721-
id: '/dashboard',
721+
id: '/dashboard/invoices',
722722
fullPath: '/dashboard/invoices',
723723
path: 'invoices',
724724
},

packages/router-plugin/src/core/route-hmr-statement.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ function handleRouteUpdate(
4040
node: AnyRouter['processedTree']['segmentTree'],
4141
) {
4242
if (node.route?.id === route.id) node.route = route
43-
if (node.notFound?.id === route.id) node.notFound = route
44-
43+
if (node.index) walkReplaceSegmentTree(route, node.index)
4544
node.static?.forEach((child) => walkReplaceSegmentTree(route, child))
4645
node.staticInsensitive?.forEach((child) =>
4746
walkReplaceSegmentTree(route, child),

packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ if (import.meta.hot) {
3434
;
3535
function walkReplaceSegmentTree(route, node) {
3636
if (node.route?.id === route.id) node.route = route;
37-
if (node.notFound?.id === route.id) node.notFound = route;
37+
if (node.index) walkReplaceSegmentTree(route, node.index);
3838
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
3939
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
4040
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));

packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ if (import.meta.hot) {
3434
;
3535
function walkReplaceSegmentTree(route, node) {
3636
if (node.route?.id === route.id) node.route = route;
37-
if (node.notFound?.id === route.id) node.notFound = route;
37+
if (node.index) walkReplaceSegmentTree(route, node.index);
3838
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
3939
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
4040
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));

packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ if (import.meta.hot) {
3333
;
3434
function walkReplaceSegmentTree(route, node) {
3535
if (node.route?.id === route.id) node.route = route;
36-
if (node.notFound?.id === route.id) node.notFound = route;
36+
if (node.index) walkReplaceSegmentTree(route, node.index);
3737
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
3838
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
3939
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));

packages/solid-router/tests/link.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2064,10 +2064,16 @@ describe('Link', () => {
20642064

20652065
const postRoute = createRoute({
20662066
getParentRoute: () => postsRoute,
2067-
path: '$postId/',
2067+
path: '$postId',
20682068
component: PostComponent,
20692069
})
20702070

2071+
const postIndexRoute = createRoute({
2072+
getParentRoute: () => postRoute,
2073+
path: '/',
2074+
component: () => <div>Post Index</div>,
2075+
})
2076+
20712077
const DetailsComponent = () => {
20722078
return (
20732079
<>
@@ -2100,7 +2106,11 @@ describe('Link', () => {
21002106
indexRoute,
21012107
layoutRoute.addChildren([
21022108
postsRoute.addChildren([
2103-
postRoute.addChildren([detailsRoute, informationRoute]),
2109+
postRoute.addChildren([
2110+
postIndexRoute,
2111+
detailsRoute,
2112+
informationRoute,
2113+
]),
21042114
]),
21052115
]),
21062116
]),

packages/vue-router/tests/link.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,10 +2068,16 @@ describe('Link', () => {
20682068

20692069
const postRoute = createRoute({
20702070
getParentRoute: () => postsRoute,
2071-
path: '$postId/',
2071+
path: '$postId',
20722072
component: PostComponent,
20732073
})
20742074

2075+
const postIndexRoute = createRoute({
2076+
getParentRoute: () => postRoute,
2077+
path: '/',
2078+
component: () => <div>Post Index</div>,
2079+
})
2080+
20752081
const DetailsComponent = () => {
20762082
return (
20772083
<>
@@ -2104,7 +2110,11 @@ describe('Link', () => {
21042110
indexRoute,
21052111
layoutRoute.addChildren([
21062112
postsRoute.addChildren([
2107-
postRoute.addChildren([detailsRoute, informationRoute]),
2113+
postRoute.addChildren([
2114+
postIndexRoute,
2115+
detailsRoute,
2116+
informationRoute,
2117+
]),
21082118
]),
21092119
]),
21102120
]),

0 commit comments

Comments
 (0)