Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/react-router/tests/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2044,10 +2044,16 @@ describe('Link', () => {

const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId/',
path: '$postId',
component: PostComponent,
})

const postIndexRoute = createRoute({
getParentRoute: () => postRoute,
path: '/',
component: () => <div>Post Index</div>,
})

const DetailsComponent = () => {
return (
<>
Expand Down Expand Up @@ -2080,7 +2086,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
postRoute.addChildren([detailsRoute, informationRoute]),
postRoute.addChildren([
postIndexRoute,
detailsRoute,
informationRoute,
]),
]),
]),
]),
Expand Down
121 changes: 81 additions & 40 deletions packages/router-core/src/new-process-route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -326,20 +335,26 @@ function parseSegments<TRouteLike extends RouteLike>(
}
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<TRouteLike>(
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)
Expand Down Expand Up @@ -417,6 +432,7 @@ function createStaticNode<T extends RouteLike>(
return {
kind: SEGMENT_TYPE_PATHNAME,
depth: 0,
index: null,
static: null,
staticInsensitive: null,
dynamic: null,
Expand All @@ -425,8 +441,6 @@ function createStaticNode<T extends RouteLike>(
route: null,
fullPath,
parent: null,
isIndex: false,
notFound: null,
}
}

Expand All @@ -447,6 +461,7 @@ function createDynamicNode<T extends RouteLike>(
return {
kind,
depth: 0,
index: null,
static: null,
staticInsensitive: null,
dynamic: null,
Expand All @@ -455,16 +470,14 @@ function createDynamicNode<T extends RouteLike>(
route: null,
fullPath,
parent: null,
isIndex: false,
notFound: null,
caseSensitive,
prefix,
suffix,
}
}

type StaticSegmentNode<T extends RouteLike> = SegmentNode<T> & {
kind: typeof SEGMENT_TYPE_PATHNAME
kind: typeof SEGMENT_TYPE_PATHNAME | typeof SEGMENT_TYPE_INDEX
}

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

type SegmentNode<T extends RouteLike> = {
kind: SegmentKind
kind: ExtendedSegmentKind

/** Exact index segment (highest priority) */
index: StaticSegmentNode<T> | null

/** Static segments (highest priority) */
/** Static segments (2nd priority) */
static: Map<string, StaticSegmentNode<T>> | null

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

/** Dynamic segments ($param) */
Expand All @@ -508,12 +524,6 @@ type SegmentNode<T extends RouteLike> = {
parent: AnySegmentNode<T> | 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 = {
Expand Down Expand Up @@ -713,11 +723,8 @@ function findMatch<T extends RouteLike>(
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,
Expand Down Expand Up @@ -837,6 +844,11 @@ function getNodeMatch<T extends RouteLike>(
segmentTree: AnySegmentNode<T>,
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)
Expand Down Expand Up @@ -872,22 +884,36 @@ function getNodeMatch<T extends RouteLike>(
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]!
Expand Down Expand Up @@ -1022,6 +1048,19 @@ function getNodeMatch<T extends RouteLike>(
})
}
}

// 0. Try index match
if (isBeyondPath && node.index) {
stack.push({
node: node.index,
index,
skipped,
depth: depth + 1,
statics,
dynamics,
optionals,
})
}
}

if (bestMatch && wildcardMatch) {
Expand Down Expand Up @@ -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)))))))
)
}
2 changes: 1 addition & 1 deletion packages/router-core/tests/new-process-route-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ describe('findRouteMatch', () => {
path: 'dashboard/',
},
{
id: '/dashboard',
id: '/dashboard/invoices',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has nothing to do w/ the rest of this PR, it was just incorrect before so i did a drive-by fix

fullPath: '/dashboard/invoices',
path: 'invoices',
},
Expand Down
3 changes: 1 addition & 2 deletions packages/router-plugin/src/core/route-hmr-statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
14 changes: 12 additions & 2 deletions packages/solid-router/tests/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2064,10 +2064,16 @@ describe('Link', () => {

const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId/',
path: '$postId',
component: PostComponent,
})

const postIndexRoute = createRoute({
getParentRoute: () => postRoute,
path: '/',
component: () => <div>Post Index</div>,
})

const DetailsComponent = () => {
return (
<>
Expand Down Expand Up @@ -2100,7 +2106,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
postRoute.addChildren([detailsRoute, informationRoute]),
postRoute.addChildren([
postIndexRoute,
detailsRoute,
informationRoute,
]),
]),
]),
]),
Expand Down
14 changes: 12 additions & 2 deletions packages/vue-router/tests/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2068,10 +2068,16 @@ describe('Link', () => {

const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId/',
path: '$postId',
component: PostComponent,
})

const postIndexRoute = createRoute({
getParentRoute: () => postRoute,
path: '/',
component: () => <div>Post Index</div>,
})

const DetailsComponent = () => {
return (
<>
Expand Down Expand Up @@ -2104,7 +2110,11 @@ describe('Link', () => {
indexRoute,
layoutRoute.addChildren([
postsRoute.addChildren([
postRoute.addChildren([detailsRoute, informationRoute]),
postRoute.addChildren([
postIndexRoute,
detailsRoute,
informationRoute,
]),
]),
]),
]),
Expand Down
Loading