diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index bbb9fd8f042..63495f44027 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -2044,10 +2044,16 @@ describe('Link', () => {
const postRoute = createRoute({
getParentRoute: () => postsRoute,
- path: '$postId/',
+ path: '$postId',
component: PostComponent,
})
+ const postIndexRoute = createRoute({
+ getParentRoute: () => postRoute,
+ path: '/',
+ component: () =>
Post Index
,
+ })
+
const DetailsComponent = () => {
return (
<>
@@ -2080,7 +2086,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
- postRoute.addChildren([detailsRoute, informationRoute]),
+ postRoute.addChildren([
+ postIndexRoute,
+ detailsRoute,
+ informationRoute,
+ ]),
]),
]),
]),
diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts
index 64abb685b6a..9fa3f91a0b1 100644
--- a/packages/router-core/src/new-process-route-tree.ts
+++ b/packages/router-core/src/new-process-route-tree.ts
@@ -7,13 +7,22 @@ export const SEGMENT_TYPE_PATHNAME = 0
export const SEGMENT_TYPE_PARAM = 1
export const SEGMENT_TYPE_WILDCARD = 2
export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
+const SEGMENT_TYPE_INDEX = 4
+/**
+ * All the kinds of segments that can be present in a route path.
+ */
export type SegmentKind =
| typeof SEGMENT_TYPE_PATHNAME
| typeof SEGMENT_TYPE_PARAM
| typeof SEGMENT_TYPE_WILDCARD
| typeof SEGMENT_TYPE_OPTIONAL_PARAM
+/**
+ * All the kinds of segments that can be present in the segment tree.
+ */
+type ExtendedSegmentKind = SegmentKind | typeof SEGMENT_TYPE_INDEX
+
const PARAM_W_CURLY_BRACES_RE =
/^([^{]*)\{\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/ // prefix{$paramName}suffix
const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
@@ -326,20 +335,26 @@ function parseSegments(
}
node = nextNode
}
- if ((route.path || !route.children) && !route.isRoot) {
- const isIndex = path.endsWith('/')
- // we cannot fuzzy match an index route,
- // but if there is *also* a layout route at this path, save it as notFound
- // we can use it when fuzzy matching to display the NotFound component in the layout route
- if (!isIndex) node.notFound = route
- // does the new route take precedence over an existing one?
- // yes if previous is not an index route and new one is an index route
- if (!node.route || (!node.isIndex && isIndex)) {
- node.route = route
- // when replacing, replace all attributes that are route-specific (`fullPath` only at the moment)
- node.fullPath = route.fullPath ?? route.from
- }
- node.isIndex ||= isIndex
+
+ const isLeaf = (route.path || !route.children) && !route.isRoot
+
+ // create index node
+ if (isLeaf && path.endsWith('/')) {
+ const indexNode = createStaticNode(
+ route.fullPath ?? route.from,
+ )
+ indexNode.kind = SEGMENT_TYPE_INDEX
+ indexNode.parent = node
+ depth++
+ indexNode.depth = depth
+ node.index = indexNode
+ node = indexNode
+ }
+
+ // make node "matchable"
+ if (isLeaf && !node.route) {
+ node.route = route
+ node.fullPath = route.fullPath ?? route.from
}
}
if (route.children)
@@ -417,6 +432,7 @@ function createStaticNode(
return {
kind: SEGMENT_TYPE_PATHNAME,
depth: 0,
+ index: null,
static: null,
staticInsensitive: null,
dynamic: null,
@@ -425,8 +441,6 @@ function createStaticNode(
route: null,
fullPath,
parent: null,
- isIndex: false,
- notFound: null,
}
}
@@ -447,6 +461,7 @@ function createDynamicNode(
return {
kind,
depth: 0,
+ index: null,
static: null,
staticInsensitive: null,
dynamic: null,
@@ -455,8 +470,6 @@ function createDynamicNode(
route: null,
fullPath,
parent: null,
- isIndex: false,
- notFound: null,
caseSensitive,
prefix,
suffix,
@@ -464,7 +477,7 @@ function createDynamicNode(
}
type StaticSegmentNode = SegmentNode & {
- kind: typeof SEGMENT_TYPE_PATHNAME
+ kind: typeof SEGMENT_TYPE_PATHNAME | typeof SEGMENT_TYPE_INDEX
}
type DynamicSegmentNode = SegmentNode & {
@@ -482,12 +495,15 @@ type AnySegmentNode =
| DynamicSegmentNode
type SegmentNode = {
- kind: SegmentKind
+ kind: ExtendedSegmentKind
+
+ /** Exact index segment (highest priority) */
+ index: StaticSegmentNode | null
- /** Static segments (highest priority) */
+ /** Static segments (2nd priority) */
static: Map> | null
- /** Case insensitive static segments (second highest priority) */
+ /** Case insensitive static segments (3rd highest priority) */
staticInsensitive: Map> | null
/** Dynamic segments ($param) */
@@ -508,12 +524,6 @@ type SegmentNode = {
parent: AnySegmentNode | null
depth: number
-
- /** is it an index route (trailing / path), only valid for nodes with a `route` */
- isIndex: boolean
-
- /** Same as `route`, but only present if both an "index route" and a "layout route" exist at this path */
- notFound: T | null
}
type RouteLike = {
@@ -713,11 +723,8 @@ function findMatch(
const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
if (!leaf) return null
const params = extractParams(path, parts, leaf)
- const isFuzzyMatch = '**' in leaf
- if (isFuzzyMatch) params['**'] = leaf['**']
- const route = isFuzzyMatch
- ? (leaf.node.notFound ?? leaf.node.route!)
- : leaf.node.route!
+ if ('**' in leaf) params['**'] = leaf['**']!
+ const route = leaf.node.route!
return {
route,
params,
@@ -837,6 +844,11 @@ function getNodeMatch(
segmentTree: AnySegmentNode,
fuzzy: boolean,
) {
+ // quick check for root index
+ // this is an optimization, algorithm should work correctly without this block
+ if (path === '/' && segmentTree.index)
+ return { node: segmentTree.index, skipped: 0 }
+
const trailingSlash = !last(parts)
const pathIsIndex = trailingSlash && path !== '/'
const partsLength = parts.length - (trailingSlash ? 1 : 0)
@@ -872,22 +884,36 @@ function getNodeMatch(
let { node, index, skipped, depth, statics, dynamics, optionals } = frame
// In fuzzy mode, track the best partial match we've found so far
- if (fuzzy && node.notFound && isFrameMoreSpecific(bestFuzzy, frame)) {
+ if (
+ fuzzy &&
+ node.route &&
+ node.kind !== SEGMENT_TYPE_INDEX &&
+ isFrameMoreSpecific(bestFuzzy, frame)
+ ) {
bestFuzzy = frame
}
const isBeyondPath = index === partsLength
if (isBeyondPath) {
- if (node.route && (!pathIsIndex || node.isIndex)) {
+ if (node.route && (!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX)) {
if (isFrameMoreSpecific(bestMatch, frame)) {
bestMatch = frame
}
// perfect match, no need to continue
- if (statics === partsLength && node.isIndex) return bestMatch
+ // this is an optimization, algorithm should work correctly without this block
+ if (
+ statics === partsLength &&
+ !dynamics &&
+ !optionals &&
+ !skipped &&
+ node.kind === SEGMENT_TYPE_INDEX
+ ) {
+ return bestMatch
+ }
}
- // beyond the length of the path parts, only skipped optional segments or wildcard segments can match
- if (!node.optional && !node.wildcard) continue
+ // beyond the length of the path parts, only index segments, or skipped optional segments, or wildcard segments can match
+ if (!node.optional && !node.wildcard && !node.index) continue
}
const part = isBeyondPath ? undefined : parts[index]!
@@ -1022,6 +1048,19 @@ function getNodeMatch(
})
}
}
+
+ // 0. Try index match
+ if (isBeyondPath && node.index) {
+ stack.push({
+ node: node.index,
+ index,
+ skipped,
+ depth: depth + 1,
+ statics,
+ dynamics,
+ optionals,
+ })
+ }
}
if (bestMatch && wildcardMatch) {
@@ -1064,8 +1103,10 @@ function isFrameMoreSpecific(
(next.dynamics === prev.dynamics &&
(next.optionals > prev.optionals ||
(next.optionals === prev.optionals &&
- (next.node.isIndex > prev.node.isIndex ||
- (next.node.isIndex === prev.node.isIndex &&
+ ((next.node.kind === SEGMENT_TYPE_INDEX) >
+ (prev.node.kind === SEGMENT_TYPE_INDEX) ||
+ ((next.node.kind === SEGMENT_TYPE_INDEX) ===
+ (prev.node.kind === SEGMENT_TYPE_INDEX) &&
next.depth > prev.depth)))))))
)
}
diff --git a/packages/router-core/tests/new-process-route-tree.test.ts b/packages/router-core/tests/new-process-route-tree.test.ts
index 746cd470928..47083567e21 100644
--- a/packages/router-core/tests/new-process-route-tree.test.ts
+++ b/packages/router-core/tests/new-process-route-tree.test.ts
@@ -718,7 +718,7 @@ describe('findRouteMatch', () => {
path: 'dashboard/',
},
{
- id: '/dashboard',
+ id: '/dashboard/invoices',
fullPath: '/dashboard/invoices',
path: 'invoices',
},
diff --git a/packages/router-plugin/src/core/route-hmr-statement.ts b/packages/router-plugin/src/core/route-hmr-statement.ts
index a5771ebee7e..b910c1340b4 100644
--- a/packages/router-plugin/src/core/route-hmr-statement.ts
+++ b/packages/router-plugin/src/core/route-hmr-statement.ts
@@ -40,8 +40,7 @@ function handleRouteUpdate(
node: AnyRouter['processedTree']['segmentTree'],
) {
if (node.route?.id === route.id) node.route = route
- if (node.notFound?.id === route.id) node.notFound = route
-
+ if (node.index) walkReplaceSegmentTree(route, node.index)
node.static?.forEach((child) => walkReplaceSegmentTree(route, child))
node.staticInsensitive?.forEach((child) =>
walkReplaceSegmentTree(route, child),
diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx
index f80eab81e9f..5a3588a7dc8 100644
--- a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx
@@ -34,7 +34,7 @@ if (import.meta.hot) {
;
function walkReplaceSegmentTree(route, node) {
if (node.route?.id === route.id) node.route = route;
- if (node.notFound?.id === route.id) node.notFound = route;
+ if (node.index) walkReplaceSegmentTree(route, node.index);
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));
diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx
index 1664f20bbbc..bee310532ff 100644
--- a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx
+++ b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx
@@ -34,7 +34,7 @@ if (import.meta.hot) {
;
function walkReplaceSegmentTree(route, node) {
if (node.route?.id === route.id) node.route = route;
- if (node.notFound?.id === route.id) node.notFound = route;
+ if (node.index) walkReplaceSegmentTree(route, node.index);
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));
diff --git a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx
index e140124caa2..3ad31b8eb2b 100644
--- a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx
+++ b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx
@@ -33,7 +33,7 @@ if (import.meta.hot) {
;
function walkReplaceSegmentTree(route, node) {
if (node.route?.id === route.id) node.route = route;
- if (node.notFound?.id === route.id) node.notFound = route;
+ if (node.index) walkReplaceSegmentTree(route, node.index);
node.static?.forEach(child => walkReplaceSegmentTree(route, child));
node.staticInsensitive?.forEach(child => walkReplaceSegmentTree(route, child));
node.dynamic?.forEach(child => walkReplaceSegmentTree(route, child));
diff --git a/packages/solid-router/tests/link.test.tsx b/packages/solid-router/tests/link.test.tsx
index 8d9f36374d8..5adfee22474 100644
--- a/packages/solid-router/tests/link.test.tsx
+++ b/packages/solid-router/tests/link.test.tsx
@@ -2064,10 +2064,16 @@ describe('Link', () => {
const postRoute = createRoute({
getParentRoute: () => postsRoute,
- path: '$postId/',
+ path: '$postId',
component: PostComponent,
})
+ const postIndexRoute = createRoute({
+ getParentRoute: () => postRoute,
+ path: '/',
+ component: () => Post Index
,
+ })
+
const DetailsComponent = () => {
return (
<>
@@ -2100,7 +2106,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
- postRoute.addChildren([detailsRoute, informationRoute]),
+ postRoute.addChildren([
+ postIndexRoute,
+ detailsRoute,
+ informationRoute,
+ ]),
]),
]),
]),
diff --git a/packages/vue-router/tests/link.test.tsx b/packages/vue-router/tests/link.test.tsx
index b4abc5c5818..b49cc08fb8b 100644
--- a/packages/vue-router/tests/link.test.tsx
+++ b/packages/vue-router/tests/link.test.tsx
@@ -2068,10 +2068,16 @@ describe('Link', () => {
const postRoute = createRoute({
getParentRoute: () => postsRoute,
- path: '$postId/',
+ path: '$postId',
component: PostComponent,
})
+ const postIndexRoute = createRoute({
+ getParentRoute: () => postRoute,
+ path: '/',
+ component: () => Post Index
,
+ })
+
const DetailsComponent = () => {
return (
<>
@@ -2104,7 +2110,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
- postRoute.addChildren([detailsRoute, informationRoute]),
+ postRoute.addChildren([
+ postIndexRoute,
+ detailsRoute,
+ informationRoute,
+ ]),
]),
]),
]),