Skip to content
Closed
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
105 changes: 48 additions & 57 deletions packages/router-core/src/new-process-route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export function parseSegment(
* @param route The current route to parse.
* @param start The starting index for parsing within the route's full path.
* @param node The current segment node in the trie to populate.
* @param sortable Dynamic sibling arrays that need sorting. Each array is recorded once when it reaches length 2.
* @param onRoute Callback invoked for each route processed.
*/
function parseSegments<TRouteLike extends RouteLike>(
Expand All @@ -199,6 +200,7 @@ function parseSegments<TRouteLike extends RouteLike>(
start: number,
node: AnySegmentNode<TRouteLike>,
depth: number,
sortable?: Array<Array<DynamicSegmentNode<TRouteLike>>>,
onRoute?: (route: TRouteLike) => void,
) {
onRoute?.(route)
Expand Down Expand Up @@ -291,6 +293,9 @@ function parseSegments<TRouteLike extends RouteLike>(
next.parent = node
node.dynamic ??= []
node.dynamic.push(next)
if (node.dynamic.length === 2) {
sortable?.push(node.dynamic)
}
}
break
}
Expand Down Expand Up @@ -333,6 +338,9 @@ function parseSegments<TRouteLike extends RouteLike>(
next.depth = depth
node.optional ??= []
node.optional.push(next)
if (node.optional.length === 2) {
sortable?.push(node.optional)
}
}
break
}
Expand Down Expand Up @@ -363,6 +371,9 @@ function parseSegments<TRouteLike extends RouteLike>(
next.depth = depth
node.wildcard ??= []
node.wildcard.push(next)
if (node.wildcard.length === 2) {
sortable?.push(node.wildcard)
}
}
}
node = nextNode
Expand Down Expand Up @@ -420,6 +431,7 @@ function parseSegments<TRouteLike extends RouteLike>(
cursor,
node,
depth,
sortable,
onRoute,
)
}
Expand Down Expand Up @@ -464,42 +476,6 @@ function sortDynamic(
return 0
}

function sortTreeNodes(node: SegmentNode<RouteLike>) {
if (node.pathless) {
for (const child of node.pathless) {
sortTreeNodes(child)
}
}
if (node.static) {
for (const child of node.static.values()) {
sortTreeNodes(child)
}
}
if (node.staticInsensitive) {
for (const child of node.staticInsensitive.values()) {
sortTreeNodes(child)
}
}
if (node.dynamic?.length) {
node.dynamic.sort(sortDynamic)
for (const child of node.dynamic) {
sortTreeNodes(child)
}
}
if (node.optional?.length) {
node.optional.sort(sortDynamic)
for (const child of node.optional) {
sortTreeNodes(child)
}
}
if (node.wildcard?.length) {
node.wildcard.sort(sortDynamic)
for (const child of node.wildcard) {
sortTreeNodes(child)
}
}
}

function createStaticNode<T extends RouteLike>(
fullPath: string,
): StaticSegmentNode<T> {
Expand Down Expand Up @@ -663,10 +639,13 @@ export function processRouteMasks<
) {
const segmentTree = createStaticNode<TRouteLike>('/')
const data = new Uint16Array(6)
const sortable: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
for (const route of routeList) {
parseSegments(false, data, route, 1, segmentTree, 0)
parseSegments(false, data, route, 1, segmentTree, 0, sortable)
}
for (const nodes of sortable) {
nodes.sort(sortDynamic)
}
sortTreeNodes(segmentTree)
processedTree.masksTree = segmentTree
processedTree.flatCache = createLRUCache<
string,
Expand Down Expand Up @@ -791,32 +770,44 @@ export function processRouteTree<
const data = new Uint16Array(6)
const routesById = {} as Record<string, TRouteLike>
const routesByPath = {} as Record<string, TRouteLike>
const sortable: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
let index = 0
parseSegments(caseSensitive, data, routeTree, 1, segmentTree, 0, (route) => {
initRoute?.(route, index)
parseSegments(
caseSensitive,
data,
routeTree,
1,
segmentTree,
0,
sortable,
(route) => {
initRoute?.(route, index)

if (route.id in routesById) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(
`Invariant failed: Duplicate routes found with id: ${String(route.id)}`,
)
}

if (route.id in routesById) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(
`Invariant failed: Duplicate routes found with id: ${String(route.id)}`,
)
invariant()
}

invariant()
}

routesById[route.id] = route
routesById[route.id] = route

if (index !== 0 && route.path) {
const trimmedFullPath = trimPathRight(route.fullPath)
if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) {
routesByPath[trimmedFullPath] = route
if (index !== 0 && route.path) {
const trimmedFullPath = trimPathRight(route.fullPath)
if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) {
routesByPath[trimmedFullPath] = route
}
}
}

index++
})
sortTreeNodes(segmentTree)
index++
},
)
for (const nodes of sortable) {
nodes.sort(sortDynamic)
}
const processedTree: ProcessedTree<TRouteLike, any, any> = {
segmentTree,
singleCache: createLRUCache<string, AnySegmentNode<any>>(1000),
Expand Down
33 changes: 33 additions & 0 deletions packages/router-core/tests/new-process-route-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ describe('findRouteMatch', () => {
const tree = makeTree(['/a/{-$b}b', '/a/{-$b}'])
expect(findRouteMatch('/a/bbb', tree)?.route.id).toBe('/a/{-$b}b')
})
it('prefix+suffix optional wins when declared after plain optional', () => {
const tree = makeTree(['/a/{-$b}', '/a/b{-$b}b'])
expect(findRouteMatch('/a/bbb', tree)?.route.id).toBe('/a/b{-$b}b')
})

it('prefix+suffix wildcard wins over plain wildcard', () => {
const tree = makeTree(['/a/b{$}b', '/a/$'])
Expand All @@ -127,6 +131,10 @@ describe('findRouteMatch', () => {
const tree = makeTree(['/a/{$}b', '/a/$'])
expect(findRouteMatch('/a/bbb', tree)?.route.id).toBe('/a/{$}b')
})
it('sorts a third, more specific wildcard declared last', () => {
const tree = makeTree(['/a/$', '/a/b{$}', '/a/b{$}b'])
expect(findRouteMatch('/a/bbb', tree)?.route.id).toBe('/a/b{$}b')
})
})

describe('prefix / suffix lengths', () => {
Expand Down Expand Up @@ -1672,4 +1680,29 @@ describe('processRouteMasks', { sequential: true }, () => {
expect(res?.route.from).toBe('/a/b/{$}.txt')
expect(res?.rawParams).toEqual({ '*': 'file/path', _splat: 'file/path' })
})
it('sorts competing route masks declared least-specific first', () => {
const localTree = processRouteTree(routeTree).processedTree
processRouteMasks(
[
{ from: '/dynamic/$param', routeTree },
{ from: '/dynamic/prefix{$param}', routeTree },
{ from: '/optional/{-$param}', routeTree },
{ from: '/optional/prefix{-$param}', routeTree },
{ from: '/wildcard/$', routeTree },
{ from: '/wildcard/prefix{$}', routeTree },
{ from: '/wildcard/prefix{$}.txt', routeTree },
],
localTree,
)

expect(findFlatMatch('/dynamic/prefixvalue', localTree)?.route.from).toBe(
'/dynamic/prefix{$param}',
)
expect(findFlatMatch('/optional/prefixvalue', localTree)?.route.from).toBe(
'/optional/prefix{-$param}',
)
expect(
findFlatMatch('/wildcard/prefixvalue.txt', localTree)?.route.from,
).toBe('/wildcard/prefix{$}.txt')
})
})
Loading