Skip to content

Commit d9e403b

Browse files
committed
feat(router-core): validate params while matching
1 parent bc79c97 commit d9e403b

File tree

1 file changed

+93
-25
lines changed

1 file changed

+93
-25
lines changed

packages/router-core/src/new-process-route-tree.ts

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ function parseSegments<TRouteLike extends RouteLike>(
174174
const path = route.fullPath ?? route.from
175175
const length = path.length
176176
const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive
177+
const parse = route.options?.params?.parse ?? null
177178
while (cursor < length) {
178179
const segment = parseSegment(path, cursor, data)
179180
let nextNode: AnySegmentNode<TRouteLike>
@@ -232,12 +233,15 @@ function parseSegments<TRouteLike extends RouteLike>(
232233
: actuallyCaseSensitive
233234
? suffix_raw
234235
: suffix_raw.toLowerCase()
235-
const existingNode = node.dynamic?.find(
236-
(s) =>
237-
s.caseSensitive === actuallyCaseSensitive &&
238-
s.prefix === prefix &&
239-
s.suffix === suffix,
240-
)
236+
const existingNode =
237+
!parse &&
238+
node.dynamic?.find(
239+
(s) =>
240+
!s.parse &&
241+
s.caseSensitive === actuallyCaseSensitive &&
242+
s.prefix === prefix &&
243+
s.suffix === suffix,
244+
)
241245
if (existingNode) {
242246
nextNode = existingNode
243247
} else {
@@ -271,12 +275,15 @@ function parseSegments<TRouteLike extends RouteLike>(
271275
: actuallyCaseSensitive
272276
? suffix_raw
273277
: suffix_raw.toLowerCase()
274-
const existingNode = node.optional?.find(
275-
(s) =>
276-
s.caseSensitive === actuallyCaseSensitive &&
277-
s.prefix === prefix &&
278-
s.suffix === suffix,
279-
)
278+
const existingNode =
279+
!parse &&
280+
node.optional?.find(
281+
(s) =>
282+
!s.parse &&
283+
s.caseSensitive === actuallyCaseSensitive &&
284+
s.prefix === prefix &&
285+
s.suffix === suffix,
286+
)
280287
if (existingNode) {
281288
nextNode = existingNode
282289
} else {
@@ -326,6 +333,7 @@ function parseSegments<TRouteLike extends RouteLike>(
326333
}
327334
node = nextNode
328335
}
336+
node.parse = parse
329337
if ((route.path || !route.children) && !route.isRoot) {
330338
const isIndex = path.endsWith('/')
331339
// we cannot fuzzy match an index route,
@@ -351,9 +359,21 @@ function parseSegments<TRouteLike extends RouteLike>(
351359
}
352360

353361
function sortDynamic(
354-
a: { prefix?: string; suffix?: string; caseSensitive: boolean },
355-
b: { prefix?: string; suffix?: string; caseSensitive: boolean },
362+
a: {
363+
prefix?: string
364+
suffix?: string
365+
caseSensitive: boolean
366+
parse: null | ((params: Record<string, string>) => any)
367+
},
368+
b: {
369+
prefix?: string
370+
suffix?: string
371+
caseSensitive: boolean
372+
parse: null | ((params: Record<string, string>) => any)
373+
},
356374
) {
375+
if (a.parse && !b.parse) return -1
376+
if (!a.parse && b.parse) return 1
357377
if (a.prefix && b.prefix && a.prefix !== b.prefix) {
358378
if (a.prefix.startsWith(b.prefix)) return -1
359379
if (b.prefix.startsWith(a.prefix)) return 1
@@ -421,6 +441,7 @@ function createStaticNode<T extends RouteLike>(
421441
parent: null,
422442
isIndex: false,
423443
notFound: null,
444+
parse: null,
424445
}
425446
}
426447

@@ -451,6 +472,7 @@ function createDynamicNode<T extends RouteLike>(
451472
parent: null,
452473
isIndex: false,
453474
notFound: null,
475+
parse: null,
454476
caseSensitive,
455477
prefix,
456478
suffix,
@@ -508,6 +530,9 @@ type SegmentNode<T extends RouteLike> = {
508530

509531
/** Same as `route`, but only present if both an "index route" and a "layout route" exist at this path */
510532
notFound: T | null
533+
534+
/** route.options.params.parse function, set on the last node of the route */
535+
parse: null | ((params: Record<string, string>) => any)
511536
}
512537

513538
type RouteLike = {
@@ -517,6 +542,9 @@ type RouteLike = {
517542
isRoot?: boolean
518543
options?: {
519544
caseSensitive?: boolean
545+
params?: {
546+
parse?: (params: Record<string, string>) => any
547+
}
520548
}
521549
} &
522550
// router tree
@@ -706,7 +734,7 @@ function findMatch<T extends RouteLike>(
706734
const parts = path.split('/')
707735
const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
708736
if (!leaf) return null
709-
const params = extractParams(path, parts, leaf)
737+
const [params] = extractParams(path, parts, leaf)
710738
const isFuzzyMatch = '**' in leaf
711739
if (isFuzzyMatch) params['**'] = leaf['**']
712740
const route = isFuzzyMatch
@@ -721,16 +749,23 @@ function findMatch<T extends RouteLike>(
721749
function extractParams<T extends RouteLike>(
722750
path: string,
723751
parts: Array<string>,
724-
leaf: { node: AnySegmentNode<T>; skipped: number },
725-
) {
752+
leaf: {
753+
node: AnySegmentNode<T>
754+
skipped: number
755+
extract?: { part: number; node: number; path: number }
756+
params?: Record<string, string>
757+
},
758+
): [
759+
params: Record<string, string>,
760+
state: { part: number; node: number; path: number },
761+
] {
726762
const list = buildBranch(leaf.node)
727763
let nodeParts: Array<string> | null = null
728764
const params: Record<string, string> = {}
729-
for (
730-
let partIndex = 0, nodeIndex = 0, pathIndex = 0;
731-
nodeIndex < list.length;
732-
partIndex++, nodeIndex++, pathIndex++
733-
) {
765+
let partIndex = leaf.extract?.part ?? 0
766+
let nodeIndex = leaf.extract?.node ?? 0
767+
let pathIndex = leaf.extract?.path ?? 0
768+
for (; nodeIndex < list.length; partIndex++, nodeIndex++, pathIndex++) {
734769
const node = list[nodeIndex]!
735770
const part = parts[partIndex]
736771
const currentPathIndex = pathIndex
@@ -785,7 +820,8 @@ function extractParams<T extends RouteLike>(
785820
break
786821
}
787822
}
788-
return params
823+
if (leaf.params) Object.assign(params, leaf.params)
824+
return [params, { part: partIndex, node: nodeIndex, path: pathIndex }]
789825
}
790826

791827
function buildRouteBranch<T extends RouteLike>(route: T) {
@@ -823,6 +859,10 @@ type MatchStackFrame<T extends RouteLike> = {
823859
statics: number
824860
dynamics: number
825861
optionals: number
862+
/** intermediary state for param extraction */
863+
extract?: { part: number; node: number; path: number }
864+
/** intermediary params from param extraction */
865+
params?: Record<string, string>
826866
}
827867

828868
function getNodeMatch<T extends RouteLike>(
@@ -862,8 +902,22 @@ function getNodeMatch<T extends RouteLike>(
862902

863903
while (stack.length) {
864904
const frame = stack.pop()!
865-
// eslint-disable-next-line prefer-const
866-
let { node, index, skipped, depth, statics, dynamics, optionals } = frame
905+
const { node, index, skipped, depth, statics, dynamics, optionals } = frame
906+
let { extract, params } = frame
907+
908+
if (node.parse) {
909+
// if there is a parse function, we need to extract the params that we have so far and run it.
910+
// if this function throws, we cannot consider this a valid match
911+
try {
912+
;[params, extract] = extractParams(path, parts, frame)
913+
// TODO: can we store the parsed value somewhere to avoid re-parsing later?
914+
node.parse(params)
915+
frame.extract = extract
916+
frame.params = params
917+
} catch {
918+
continue
919+
}
920+
}
867921

868922
// In fuzzy mode, track the best partial match we've found so far
869923
if (fuzzy && node.notFound && isFrameMoreSpecific(bestFuzzy, frame)) {
@@ -913,6 +967,8 @@ function getNodeMatch<T extends RouteLike>(
913967
statics,
914968
dynamics,
915969
optionals,
970+
extract,
971+
params,
916972
}
917973
break
918974
}
@@ -933,6 +989,8 @@ function getNodeMatch<T extends RouteLike>(
933989
statics,
934990
dynamics,
935991
optionals,
992+
extract,
993+
params,
936994
}) // enqueue skipping the optional
937995
}
938996
if (!isBeyondPath) {
@@ -954,6 +1012,8 @@ function getNodeMatch<T extends RouteLike>(
9541012
statics,
9551013
dynamics,
9561014
optionals: optionals + 1,
1015+
extract,
1016+
params,
9571017
})
9581018
}
9591019
}
@@ -979,6 +1039,8 @@ function getNodeMatch<T extends RouteLike>(
9791039
statics,
9801040
dynamics: dynamics + 1,
9811041
optionals,
1042+
extract,
1043+
params,
9821044
})
9831045
}
9841046
}
@@ -997,6 +1059,8 @@ function getNodeMatch<T extends RouteLike>(
9971059
statics: statics + 1,
9981060
dynamics,
9991061
optionals,
1062+
extract,
1063+
params,
10001064
})
10011065
}
10021066
}
@@ -1013,6 +1077,8 @@ function getNodeMatch<T extends RouteLike>(
10131077
statics: statics + 1,
10141078
dynamics,
10151079
optionals,
1080+
extract,
1081+
params,
10161082
})
10171083
}
10181084
}
@@ -1031,6 +1097,8 @@ function getNodeMatch<T extends RouteLike>(
10311097
return {
10321098
node: bestFuzzy.node,
10331099
skipped: bestFuzzy.skipped,
1100+
extract: bestFuzzy.extract,
1101+
params: bestFuzzy.params,
10341102
'**': decodeURIComponent(splat),
10351103
}
10361104
}

0 commit comments

Comments
 (0)