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
50 changes: 40 additions & 10 deletions packages/router-core/src/new-process-route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,13 @@ function findMatch<T extends RouteLike>(
}
}

type ParamExtractionState = {
part: number
node: number
path: number
segment: number
}

/**
* This function is "resumable":
* - the `leaf` input can contain `extract` and `rawParams` properties from a previous `extractParams` call
Expand All @@ -859,27 +866,42 @@ function extractParams<T extends RouteLike>(
leaf: {
node: AnySegmentNode<T>
skipped: number
extract?: { part: number; node: number; path: number }
extract?: ParamExtractionState
rawParams?: Record<string, string>
},
): [
rawParams: Record<string, string>,
state: { part: number; node: number; path: number },
] {
): [rawParams: Record<string, string>, state: ParamExtractionState] {
const list = buildBranch(leaf.node)
let nodeParts: Array<string> | null = null
const rawParams: Record<string, string> = {}
/** which segment of the path we're currently processing */
let partIndex = leaf.extract?.part ?? 0
/** which node of the route tree branch we're currently processing */
let nodeIndex = leaf.extract?.node ?? 0
/** index of the 1st character of the segment we're processing in the path string */
let pathIndex = leaf.extract?.path ?? 0
for (; nodeIndex < list.length; partIndex++, nodeIndex++, pathIndex++) {
/** which fullPath segment we're currently processing */
let segmentCount = leaf.extract?.segment ?? 0
for (
;
nodeIndex < list.length;
partIndex++, nodeIndex++, pathIndex++, segmentCount++
) {
const node = list[nodeIndex]!
// index nodes are terminating nodes, nothing to extract, just leave
if (node.kind === SEGMENT_TYPE_INDEX) break
// pathless nodes do not consume a path segment
if (node.kind === SEGMENT_TYPE_PATHLESS) {
segmentCount--
partIndex--
pathIndex--
continue
}
const part = parts[partIndex]
const currentPathIndex = pathIndex
if (part) pathIndex += part.length
if (node.kind === SEGMENT_TYPE_PARAM) {
nodeParts ??= leaf.node.fullPath.split('/')
const nodePart = nodeParts[nodeIndex]!
const nodePart = nodeParts[segmentCount]!
const preLength = node.prefix?.length ?? 0
// we can't rely on the presence of prefix/suffix to know whether it's curly-braced or not, because `/{$param}/` is valid, but has no prefix/suffix
const isCurlyBraced = nodePart.charCodeAt(preLength) === 123 // '{'
Expand All @@ -903,7 +925,7 @@ function extractParams<T extends RouteLike>(
continue
}
nodeParts ??= leaf.node.fullPath.split('/')
const nodePart = nodeParts[nodeIndex]!
const nodePart = nodeParts[segmentCount]!
const preLength = node.prefix?.length ?? 0
const sufLength = node.suffix?.length ?? 0
const name = nodePart.substring(
Expand All @@ -929,7 +951,15 @@ function extractParams<T extends RouteLike>(
}
}
if (leaf.rawParams) Object.assign(rawParams, leaf.rawParams)
return [rawParams, { part: partIndex, node: nodeIndex, path: pathIndex }]
return [
rawParams,
{
part: partIndex,
node: nodeIndex,
path: pathIndex,
segment: segmentCount,
},
]
}

function buildRouteBranch<T extends RouteLike>(route: T) {
Expand Down Expand Up @@ -968,7 +998,7 @@ type MatchStackFrame<T extends RouteLike> = {
dynamics: number
optionals: number
/** intermediary state for param extraction */
extract?: { part: number; node: number; path: number }
extract?: ParamExtractionState
/** intermediary params from param extraction */
rawParams?: Record<string, string>
parsedParams?: Record<string, unknown>
Expand Down
77 changes: 77 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 @@ -1109,6 +1109,83 @@ describe('findRouteMatch', () => {
}
`)
})
it('does not consume a path segment during param extraction', () => {
const tree = {
id: '__root__',
isRoot: true,
fullPath: '/',
path: '/',
children: [
{
id: '/$foo/_layout',
fullPath: '/$foo',
path: '$foo',
options: {
params: {
parse: (params: Record<string, string>) => params,
},
// force the creation of a pathless node
skipRouteOnParseError: { params: true },
},
children: [
{
id: '/$foo/_layout/$bar',
fullPath: '/$foo/$bar',
path: '$bar',
options: {},
},
],
},
],
}
const { processedTree } = processRouteTree(tree)
const result = findRouteMatch('/abc/def', processedTree)
expect(result?.route.id).toBe('/$foo/_layout/$bar')
expect(result?.rawParams).toEqual({ foo: 'abc', bar: 'def' })
})
it('skipped optional uses the correct node index with pathless nodes', () => {
const tree = {
id: '__root__',
isRoot: true,
fullPath: '/',
path: '/',
children: [
{
id: '/one_{-$foo}/_layout',
fullPath: '/one_{-$foo}',
path: 'one_{-$foo}',
options: {
params: {
parse: (params: Record<string, string>) => params,
},
// force the creation of a pathless node
skipRouteOnParseError: { params: true },
},
children: [
{
id: '/one_{-$foo}/_layout/two_{-$bar}/baz',
fullPath: '/one_{-$foo}/two_{-$bar}/baz',
path: 'two_{-$bar}/baz',
options: {},
},
],
},
],
}
const { processedTree } = processRouteTree(tree)
// match first, skip second
{
const result = findRouteMatch('/one_value/baz', processedTree)
expect(result?.route.id).toBe('/one_{-$foo}/_layout/two_{-$bar}/baz')
expect(result?.rawParams).toEqual({ foo: 'value' })
}
// skip first, match second
{
const result = findRouteMatch('/two_value/baz', processedTree)
expect(result?.route.id).toBe('/one_{-$foo}/_layout/two_{-$bar}/baz')
expect(result?.rawParams).toEqual({ bar: 'value' })
}
})
})
})

Expand Down
Loading