diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index 2dcaceebd35..f6397275e80 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -846,6 +846,13 @@ function findMatch( } } +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 @@ -859,27 +866,42 @@ function extractParams( leaf: { node: AnySegmentNode skipped: number - extract?: { part: number; node: number; path: number } + extract?: ParamExtractionState rawParams?: Record }, -): [ - rawParams: Record, - state: { part: number; node: number; path: number }, -] { +): [rawParams: Record, state: ParamExtractionState] { const list = buildBranch(leaf.node) let nodeParts: Array | null = null const rawParams: Record = {} + /** 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 // '{' @@ -903,7 +925,7 @@ function extractParams( 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( @@ -929,7 +951,15 @@ function extractParams( } } 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(route: T) { @@ -968,7 +998,7 @@ type MatchStackFrame = { 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 parsedParams?: Record 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 6c2007cd0ec..936df988c14 100644 --- a/packages/router-core/tests/new-process-route-tree.test.ts +++ b/packages/router-core/tests/new-process-route-tree.test.ts @@ -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) => 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) => 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' }) + } + }) }) })