@tanstack/router-core 1.154.14 → 1.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/new-process-route-tree.cjs +47 -42
- package/dist/cjs/new-process-route-tree.cjs.map +1 -1
- package/dist/cjs/new-process-route-tree.d.cts +13 -8
- package/dist/cjs/path.cjs +18 -14
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +11 -2
- package/dist/cjs/router.cjs +122 -57
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +19 -5
- package/dist/esm/new-process-route-tree.d.ts +13 -8
- package/dist/esm/new-process-route-tree.js +47 -42
- package/dist/esm/new-process-route-tree.js.map +1 -1
- package/dist/esm/path.d.ts +11 -2
- package/dist/esm/path.js +18 -14
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/router.d.ts +19 -5
- package/dist/esm/router.js +124 -59
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/new-process-route-tree.ts +78 -54
- package/src/path.ts +36 -16
- package/src/router.ts +174 -59
package/package.json
CHANGED
|
@@ -27,11 +27,17 @@ type ExtendedSegmentKind =
|
|
|
27
27
|
| typeof SEGMENT_TYPE_INDEX
|
|
28
28
|
| typeof SEGMENT_TYPE_PATHLESS
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
function getOpenAndCloseBraces(
|
|
31
|
+
part: string,
|
|
32
|
+
): [openBrace: number, closeBrace: number] | null {
|
|
33
|
+
const openBrace = part.indexOf('{')
|
|
34
|
+
if (openBrace === -1) return null
|
|
35
|
+
const closeBrace = part.indexOf('}', openBrace)
|
|
36
|
+
if (closeBrace === -1) return null
|
|
37
|
+
const afterOpen = openBrace + 1
|
|
38
|
+
if (afterOpen >= part.length) return null
|
|
39
|
+
return [openBrace, closeBrace]
|
|
40
|
+
}
|
|
35
41
|
|
|
36
42
|
type ParsedSegment = Uint16Array & {
|
|
37
43
|
/** segment type (0 = pathname, 1 = param, 2 = wildcard, 3 = optional param) */
|
|
@@ -110,47 +116,61 @@ export function parseSegment(
|
|
|
110
116
|
return output as ParsedSegment
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
119
|
+
const braces = getOpenAndCloseBraces(part)
|
|
120
|
+
if (braces) {
|
|
121
|
+
const [openBrace, closeBrace] = braces
|
|
122
|
+
const firstChar = part.charCodeAt(openBrace + 1)
|
|
123
|
+
|
|
124
|
+
// Check for {-$...} (optional param)
|
|
125
|
+
// prefix{-$paramName}suffix
|
|
126
|
+
// /^([^{]*)\{-\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/
|
|
127
|
+
if (firstChar === 45) {
|
|
128
|
+
// '-'
|
|
129
|
+
if (
|
|
130
|
+
openBrace + 2 < part.length &&
|
|
131
|
+
part.charCodeAt(openBrace + 2) === 36 // '$'
|
|
132
|
+
) {
|
|
133
|
+
const paramStart = openBrace + 3
|
|
134
|
+
const paramEnd = closeBrace
|
|
135
|
+
// Validate param name exists
|
|
136
|
+
if (paramStart < paramEnd) {
|
|
137
|
+
output[0] = SEGMENT_TYPE_OPTIONAL_PARAM
|
|
138
|
+
output[1] = start + openBrace
|
|
139
|
+
output[2] = start + paramStart
|
|
140
|
+
output[3] = start + paramEnd
|
|
141
|
+
output[4] = start + closeBrace + 1
|
|
142
|
+
output[5] = end
|
|
143
|
+
return output as ParsedSegment
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (firstChar === 36) {
|
|
147
|
+
// '$'
|
|
148
|
+
const dollarPos = openBrace + 1
|
|
149
|
+
const afterDollar = openBrace + 2
|
|
150
|
+
// Check for {$} (wildcard)
|
|
151
|
+
if (afterDollar === closeBrace) {
|
|
152
|
+
// For wildcard, value should be '$' (from dollarPos to afterDollar)
|
|
153
|
+
// prefix{$}suffix
|
|
154
|
+
// /^([^{]*)\{\$\}([^}]*)$/
|
|
155
|
+
output[0] = SEGMENT_TYPE_WILDCARD
|
|
156
|
+
output[1] = start + openBrace
|
|
157
|
+
output[2] = start + dollarPos
|
|
158
|
+
output[3] = start + afterDollar
|
|
159
|
+
output[4] = start + closeBrace + 1
|
|
160
|
+
output[5] = path.length
|
|
161
|
+
return output as ParsedSegment
|
|
162
|
+
}
|
|
163
|
+
// Regular param {$paramName} - value is the param name (after $)
|
|
164
|
+
// prefix{$paramName}suffix
|
|
165
|
+
// /^([^{]*)\{\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/
|
|
166
|
+
output[0] = SEGMENT_TYPE_PARAM
|
|
167
|
+
output[1] = start + openBrace
|
|
168
|
+
output[2] = start + afterDollar
|
|
169
|
+
output[3] = start + closeBrace
|
|
170
|
+
output[4] = start + closeBrace + 1
|
|
171
|
+
output[5] = end
|
|
172
|
+
return output as ParsedSegment
|
|
173
|
+
}
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
// fallback to static pathname (should never happen)
|
|
@@ -758,6 +778,17 @@ export function trimPathRight(path: string) {
|
|
|
758
778
|
return path === '/' ? path : path.replace(/\/{1,}$/, '')
|
|
759
779
|
}
|
|
760
780
|
|
|
781
|
+
export interface ProcessRouteTreeResult<
|
|
782
|
+
TRouteLike extends Extract<RouteLike, { fullPath: string }> & { id: string },
|
|
783
|
+
> {
|
|
784
|
+
/** Should be considered a black box, needs to be provided to all matching functions in this module. */
|
|
785
|
+
processedTree: ProcessedTree<TRouteLike, any, any>
|
|
786
|
+
/** A lookup map of routes by their unique IDs. */
|
|
787
|
+
routesById: Record<string, TRouteLike>
|
|
788
|
+
/** A lookup map of routes by their trimmed full paths. */
|
|
789
|
+
routesByPath: Record<string, TRouteLike>
|
|
790
|
+
}
|
|
791
|
+
|
|
761
792
|
/**
|
|
762
793
|
* Processes a route tree into a segment trie for efficient path matching.
|
|
763
794
|
* Also builds lookup maps for routes by ID and by trimmed full path.
|
|
@@ -771,14 +802,7 @@ export function processRouteTree<
|
|
|
771
802
|
caseSensitive: boolean = false,
|
|
772
803
|
/** Optional callback invoked for each route during processing. */
|
|
773
804
|
initRoute?: (route: TRouteLike, index: number) => void,
|
|
774
|
-
): {
|
|
775
|
-
/** Should be considered a black box, needs to be provided to all matching functions in this module. */
|
|
776
|
-
processedTree: ProcessedTree<TRouteLike, any, any>
|
|
777
|
-
/** A lookup map of routes by their unique IDs. */
|
|
778
|
-
routesById: Record<string, TRouteLike>
|
|
779
|
-
/** A lookup map of routes by their trimmed full paths. */
|
|
780
|
-
routesByPath: Record<string, TRouteLike>
|
|
781
|
-
} {
|
|
805
|
+
): ProcessRouteTreeResult<TRouteLike> {
|
|
782
806
|
const segmentTree = createStaticNode<TRouteLike>(routeTree.fullPath)
|
|
783
807
|
const data = new Uint16Array(6)
|
|
784
808
|
const routesById = {} as Record<string, TRouteLike>
|
package/src/path.ts
CHANGED
|
@@ -197,11 +197,33 @@ export function resolvePath({
|
|
|
197
197
|
return result
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Create a pre-compiled decode config from allowed characters.
|
|
202
|
+
* This should be called once at router initialization.
|
|
203
|
+
*/
|
|
204
|
+
export function compileDecodeCharMap(
|
|
205
|
+
pathParamsAllowedCharacters: ReadonlyArray<string>,
|
|
206
|
+
) {
|
|
207
|
+
const charMap = new Map(
|
|
208
|
+
pathParamsAllowedCharacters.map((char) => [encodeURIComponent(char), char]),
|
|
209
|
+
)
|
|
210
|
+
// Escape special regex characters and join with |
|
|
211
|
+
const pattern = Array.from(charMap.keys())
|
|
212
|
+
.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
213
|
+
.join('|')
|
|
214
|
+
const regex = new RegExp(pattern, 'g')
|
|
215
|
+
return (encoded: string) =>
|
|
216
|
+
encoded.replace(regex, (match) => charMap.get(match) ?? match)
|
|
217
|
+
}
|
|
218
|
+
|
|
200
219
|
interface InterpolatePathOptions {
|
|
201
220
|
path?: string
|
|
202
221
|
params: Record<string, unknown>
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
/**
|
|
223
|
+
* A function that decodes a path parameter value.
|
|
224
|
+
* Obtained from `compileDecodeCharMap(pathParamsAllowedCharacters)`.
|
|
225
|
+
*/
|
|
226
|
+
decoder?: (encoded: string) => string
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
type InterPolatePathResult = {
|
|
@@ -213,7 +235,7 @@ type InterPolatePathResult = {
|
|
|
213
235
|
function encodeParam(
|
|
214
236
|
key: string,
|
|
215
237
|
params: InterpolatePathOptions['params'],
|
|
216
|
-
|
|
238
|
+
decoder: InterpolatePathOptions['decoder'],
|
|
217
239
|
): any {
|
|
218
240
|
const value = params[key]
|
|
219
241
|
if (typeof value !== 'string') return value
|
|
@@ -222,7 +244,7 @@ function encodeParam(
|
|
|
222
244
|
// the splat/catch-all routes shouldn't have the '/' encoded out
|
|
223
245
|
return encodeURI(value)
|
|
224
246
|
} else {
|
|
225
|
-
return encodePathParam(value,
|
|
247
|
+
return encodePathParam(value, decoder)
|
|
226
248
|
}
|
|
227
249
|
}
|
|
228
250
|
|
|
@@ -235,7 +257,7 @@ function encodeParam(
|
|
|
235
257
|
export function interpolatePath({
|
|
236
258
|
path,
|
|
237
259
|
params,
|
|
238
|
-
|
|
260
|
+
decoder,
|
|
239
261
|
}: InterpolatePathOptions): InterPolatePathResult {
|
|
240
262
|
// Tracking if any params are missing in the `params` object
|
|
241
263
|
// when interpolating the path
|
|
@@ -286,7 +308,7 @@ export function interpolatePath({
|
|
|
286
308
|
continue
|
|
287
309
|
}
|
|
288
310
|
|
|
289
|
-
const value = encodeParam('_splat', params,
|
|
311
|
+
const value = encodeParam('_splat', params, decoder)
|
|
290
312
|
joined += '/' + prefix + value + suffix
|
|
291
313
|
continue
|
|
292
314
|
}
|
|
@@ -300,7 +322,7 @@ export function interpolatePath({
|
|
|
300
322
|
|
|
301
323
|
const prefix = path.substring(start, segment[1])
|
|
302
324
|
const suffix = path.substring(segment[4], end)
|
|
303
|
-
const value = encodeParam(key, params,
|
|
325
|
+
const value = encodeParam(key, params, decoder) ?? 'undefined'
|
|
304
326
|
joined += '/' + prefix + value + suffix
|
|
305
327
|
continue
|
|
306
328
|
}
|
|
@@ -316,7 +338,7 @@ export function interpolatePath({
|
|
|
316
338
|
|
|
317
339
|
const prefix = path.substring(start, segment[1])
|
|
318
340
|
const suffix = path.substring(segment[4], end)
|
|
319
|
-
const value = encodeParam(key, params,
|
|
341
|
+
const value = encodeParam(key, params, decoder) ?? ''
|
|
320
342
|
joined += '/' + prefix + value + suffix
|
|
321
343
|
continue
|
|
322
344
|
}
|
|
@@ -329,12 +351,10 @@ export function interpolatePath({
|
|
|
329
351
|
return { usedParams, interpolatedPath, isMissingParams }
|
|
330
352
|
}
|
|
331
353
|
|
|
332
|
-
function encodePathParam(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
return encoded
|
|
354
|
+
function encodePathParam(
|
|
355
|
+
value: string,
|
|
356
|
+
decoder?: InterpolatePathOptions['decoder'],
|
|
357
|
+
) {
|
|
358
|
+
const encoded = encodeURIComponent(value)
|
|
359
|
+
return decoder?.(encoded) ?? encoded
|
|
340
360
|
}
|
package/src/router.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from './new-process-route-tree'
|
|
20
20
|
import {
|
|
21
21
|
cleanPath,
|
|
22
|
+
compileDecodeCharMap,
|
|
22
23
|
interpolatePath,
|
|
23
24
|
resolvePath,
|
|
24
25
|
trimPath,
|
|
@@ -37,7 +38,11 @@ import {
|
|
|
37
38
|
executeRewriteOutput,
|
|
38
39
|
rewriteBasepath,
|
|
39
40
|
} from './rewrite'
|
|
40
|
-
import type {
|
|
41
|
+
import type { LRUCache } from './lru-cache'
|
|
42
|
+
import type {
|
|
43
|
+
ProcessRouteTreeResult,
|
|
44
|
+
ProcessedTree,
|
|
45
|
+
} from './new-process-route-tree'
|
|
41
46
|
import type { SearchParser, SearchSerializer } from './searchParams'
|
|
42
47
|
import type { AnyRedirect, ResolvedRedirect } from './redirect'
|
|
43
48
|
import type {
|
|
@@ -588,7 +593,6 @@ export type SubscribeFn = <TType extends keyof RouterEvents>(
|
|
|
588
593
|
export interface MatchRoutesOpts {
|
|
589
594
|
preload?: boolean
|
|
590
595
|
throwOnError?: boolean
|
|
591
|
-
_buildLocation?: boolean
|
|
592
596
|
dest?: BuildNextOptions
|
|
593
597
|
}
|
|
594
598
|
|
|
@@ -872,6 +876,17 @@ export type CreateRouterFn = <
|
|
|
872
876
|
TDehydrated
|
|
873
877
|
>
|
|
874
878
|
|
|
879
|
+
declare global {
|
|
880
|
+
// eslint-disable-next-line no-var
|
|
881
|
+
var __TSR_CACHE__:
|
|
882
|
+
| {
|
|
883
|
+
routeTree: AnyRoute
|
|
884
|
+
processRouteTreeResult: ProcessRouteTreeResult<AnyRoute>
|
|
885
|
+
resolvePathCache: LRUCache<string, string>
|
|
886
|
+
}
|
|
887
|
+
| undefined
|
|
888
|
+
}
|
|
889
|
+
|
|
875
890
|
/**
|
|
876
891
|
* Core, framework-agnostic router engine that powers TanStack Router.
|
|
877
892
|
*
|
|
@@ -922,8 +937,9 @@ export class RouterCore<
|
|
|
922
937
|
routesById!: RoutesById<TRouteTree>
|
|
923
938
|
routesByPath!: RoutesByPath<TRouteTree>
|
|
924
939
|
processedTree!: ProcessedTree<TRouteTree, any, any>
|
|
940
|
+
resolvePathCache!: LRUCache<string, string>
|
|
925
941
|
isServer!: boolean
|
|
926
|
-
|
|
942
|
+
pathParamsDecoder?: (encoded: string) => string
|
|
927
943
|
|
|
928
944
|
/**
|
|
929
945
|
* @deprecated Use the `createRouter` function instead
|
|
@@ -992,14 +1008,10 @@ export class RouterCore<
|
|
|
992
1008
|
|
|
993
1009
|
this.isServer = this.options.isServer ?? typeof document === 'undefined'
|
|
994
1010
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
char,
|
|
1000
|
-
]),
|
|
1001
|
-
)
|
|
1002
|
-
: undefined
|
|
1011
|
+
if (this.options.pathParamsAllowedCharacters)
|
|
1012
|
+
this.pathParamsDecoder = compileDecodeCharMap(
|
|
1013
|
+
this.options.pathParamsAllowedCharacters,
|
|
1014
|
+
)
|
|
1003
1015
|
|
|
1004
1016
|
if (
|
|
1005
1017
|
!this.history ||
|
|
@@ -1030,7 +1042,28 @@ export class RouterCore<
|
|
|
1030
1042
|
|
|
1031
1043
|
if (this.options.routeTree !== this.routeTree) {
|
|
1032
1044
|
this.routeTree = this.options.routeTree as TRouteTree
|
|
1033
|
-
|
|
1045
|
+
let processRouteTreeResult: ProcessRouteTreeResult<TRouteTree>
|
|
1046
|
+
if (
|
|
1047
|
+
this.isServer &&
|
|
1048
|
+
globalThis.__TSR_CACHE__ &&
|
|
1049
|
+
globalThis.__TSR_CACHE__.routeTree === this.routeTree
|
|
1050
|
+
) {
|
|
1051
|
+
const cached = globalThis.__TSR_CACHE__
|
|
1052
|
+
this.resolvePathCache = cached.resolvePathCache
|
|
1053
|
+
processRouteTreeResult = cached.processRouteTreeResult as any
|
|
1054
|
+
} else {
|
|
1055
|
+
this.resolvePathCache = createLRUCache(1000)
|
|
1056
|
+
processRouteTreeResult = this.buildRouteTree()
|
|
1057
|
+
// only cache if nothing else is cached yet
|
|
1058
|
+
if (this.isServer && globalThis.__TSR_CACHE__ === undefined) {
|
|
1059
|
+
globalThis.__TSR_CACHE__ = {
|
|
1060
|
+
routeTree: this.routeTree,
|
|
1061
|
+
processRouteTreeResult: processRouteTreeResult as any,
|
|
1062
|
+
resolvePathCache: this.resolvePathCache,
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
this.setRoutes(processRouteTreeResult)
|
|
1034
1067
|
}
|
|
1035
1068
|
|
|
1036
1069
|
if (!this.__store && this.latestLocation) {
|
|
@@ -1113,7 +1146,7 @@ export class RouterCore<
|
|
|
1113
1146
|
}
|
|
1114
1147
|
|
|
1115
1148
|
buildRouteTree = () => {
|
|
1116
|
-
const
|
|
1149
|
+
const result = processRouteTree(
|
|
1117
1150
|
this.routeTree,
|
|
1118
1151
|
this.options.caseSensitive,
|
|
1119
1152
|
(route, i) => {
|
|
@@ -1123,9 +1156,17 @@ export class RouterCore<
|
|
|
1123
1156
|
},
|
|
1124
1157
|
)
|
|
1125
1158
|
if (this.options.routeMasks) {
|
|
1126
|
-
processRouteMasks(this.options.routeMasks, processedTree)
|
|
1159
|
+
processRouteMasks(this.options.routeMasks, result.processedTree)
|
|
1127
1160
|
}
|
|
1128
1161
|
|
|
1162
|
+
return result
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
setRoutes({
|
|
1166
|
+
routesById,
|
|
1167
|
+
routesByPath,
|
|
1168
|
+
processedTree,
|
|
1169
|
+
}: ProcessRouteTreeResult<TRouteTree>) {
|
|
1129
1170
|
this.routesById = routesById as RoutesById<TRouteTree>
|
|
1130
1171
|
this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
|
|
1131
1172
|
this.processedTree = processedTree
|
|
@@ -1225,8 +1266,6 @@ export class RouterCore<
|
|
|
1225
1266
|
return location
|
|
1226
1267
|
}
|
|
1227
1268
|
|
|
1228
|
-
resolvePathCache = createLRUCache<string, string>(1000)
|
|
1229
|
-
|
|
1230
1269
|
/** Resolve a path against the router basepath and trailing-slash policy. */
|
|
1231
1270
|
resolvePathWithBase = (from: string, path: string) => {
|
|
1232
1271
|
const resolvedPath = resolvePath({
|
|
@@ -1365,7 +1404,7 @@ export class RouterCore<
|
|
|
1365
1404
|
const { interpolatedPath, usedParams } = interpolatePath({
|
|
1366
1405
|
path: route.fullPath,
|
|
1367
1406
|
params: routeParams,
|
|
1368
|
-
|
|
1407
|
+
decoder: this.pathParamsDecoder,
|
|
1369
1408
|
})
|
|
1370
1409
|
|
|
1371
1410
|
// Waste not, want not. If we already have a match for this route,
|
|
@@ -1393,35 +1432,19 @@ export class RouterCore<
|
|
|
1393
1432
|
let paramsError: unknown = undefined
|
|
1394
1433
|
|
|
1395
1434
|
if (!existingMatch) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1435
|
+
try {
|
|
1436
|
+
extractStrictParams(route, usedParams, parsedParams!, strictParams)
|
|
1437
|
+
} catch (err: any) {
|
|
1438
|
+
if (isNotFound(err) || isRedirect(err)) {
|
|
1439
|
+
paramsError = err
|
|
1440
|
+
} else {
|
|
1441
|
+
paramsError = new PathParamError(err.message, {
|
|
1442
|
+
cause: err,
|
|
1443
|
+
})
|
|
1401
1444
|
}
|
|
1402
|
-
} else {
|
|
1403
|
-
const strictParseParams =
|
|
1404
|
-
route.options.params?.parse ?? route.options.parseParams
|
|
1405
1445
|
|
|
1406
|
-
if (
|
|
1407
|
-
|
|
1408
|
-
Object.assign(
|
|
1409
|
-
strictParams,
|
|
1410
|
-
strictParseParams(strictParams as Record<string, string>),
|
|
1411
|
-
)
|
|
1412
|
-
} catch (err: any) {
|
|
1413
|
-
if (isNotFound(err) || isRedirect(err)) {
|
|
1414
|
-
paramsError = err
|
|
1415
|
-
} else {
|
|
1416
|
-
paramsError = new PathParamError(err.message, {
|
|
1417
|
-
cause: err,
|
|
1418
|
-
})
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
if (opts?.throwOnError) {
|
|
1422
|
-
throw paramsError
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1446
|
+
if (opts?.throwOnError) {
|
|
1447
|
+
throw paramsError
|
|
1425
1448
|
}
|
|
1426
1449
|
}
|
|
1427
1450
|
}
|
|
@@ -1522,7 +1545,7 @@ export class RouterCore<
|
|
|
1522
1545
|
|
|
1523
1546
|
// only execute `context` if we are not calling from router.buildLocation
|
|
1524
1547
|
|
|
1525
|
-
if (!existingMatch
|
|
1548
|
+
if (!existingMatch) {
|
|
1526
1549
|
const parentMatch = matches[index - 1]
|
|
1527
1550
|
const parentContext = getParentContext(parentMatch)
|
|
1528
1551
|
|
|
@@ -1566,6 +1589,80 @@ export class RouterCore<
|
|
|
1566
1589
|
})
|
|
1567
1590
|
}
|
|
1568
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Lightweight route matching for buildLocation.
|
|
1594
|
+
* Only computes fullPath, accumulated search, and params - skipping expensive
|
|
1595
|
+
* operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
|
|
1596
|
+
*/
|
|
1597
|
+
private matchRoutesLightweight(location: ParsedLocation): {
|
|
1598
|
+
matchedRoutes: ReadonlyArray<AnyRoute>
|
|
1599
|
+
fullPath: string
|
|
1600
|
+
search: Record<string, unknown>
|
|
1601
|
+
params: Record<string, unknown>
|
|
1602
|
+
} {
|
|
1603
|
+
const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
|
|
1604
|
+
location.pathname,
|
|
1605
|
+
)
|
|
1606
|
+
const lastRoute = last(matchedRoutes)!
|
|
1607
|
+
|
|
1608
|
+
// I don't know if we should run the full search middleware chain, or just validateSearch
|
|
1609
|
+
// // Accumulate search validation through the route chain
|
|
1610
|
+
// const accumulatedSearch: Record<string, unknown> = applySearchMiddleware({
|
|
1611
|
+
// search: { ...location.search },
|
|
1612
|
+
// dest: location,
|
|
1613
|
+
// destRoutes: matchedRoutes,
|
|
1614
|
+
// _includeValidateSearch: true,
|
|
1615
|
+
// })
|
|
1616
|
+
|
|
1617
|
+
// Accumulate search validation through route chain
|
|
1618
|
+
const accumulatedSearch = { ...location.search }
|
|
1619
|
+
for (const route of matchedRoutes) {
|
|
1620
|
+
try {
|
|
1621
|
+
Object.assign(
|
|
1622
|
+
accumulatedSearch,
|
|
1623
|
+
validateSearch(route.options.validateSearch, accumulatedSearch),
|
|
1624
|
+
)
|
|
1625
|
+
} catch {
|
|
1626
|
+
// Ignore errors, we're not actually routing
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Determine params: reuse from state if possible, otherwise parse
|
|
1631
|
+
const lastStateMatch = last(this.state.matches)
|
|
1632
|
+
const canReuseParams =
|
|
1633
|
+
lastStateMatch &&
|
|
1634
|
+
lastStateMatch.routeId === lastRoute.id &&
|
|
1635
|
+
location.pathname === this.state.location.pathname
|
|
1636
|
+
|
|
1637
|
+
let params: Record<string, unknown>
|
|
1638
|
+
if (canReuseParams) {
|
|
1639
|
+
params = lastStateMatch.params
|
|
1640
|
+
} else {
|
|
1641
|
+
// Parse params through the route chain
|
|
1642
|
+
const strictParams: Record<string, unknown> = { ...routeParams }
|
|
1643
|
+
for (const route of matchedRoutes) {
|
|
1644
|
+
try {
|
|
1645
|
+
extractStrictParams(
|
|
1646
|
+
route,
|
|
1647
|
+
routeParams,
|
|
1648
|
+
parsedParams ?? {},
|
|
1649
|
+
strictParams,
|
|
1650
|
+
)
|
|
1651
|
+
} catch {
|
|
1652
|
+
// Ignore errors, we're not actually routing
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
params = strictParams
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return {
|
|
1659
|
+
matchedRoutes,
|
|
1660
|
+
fullPath: lastRoute.fullPath,
|
|
1661
|
+
search: accumulatedSearch,
|
|
1662
|
+
params,
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1569
1666
|
cancelMatch = (id: string) => {
|
|
1570
1667
|
const match = this.getMatch(id)
|
|
1571
1668
|
|
|
@@ -1610,13 +1707,9 @@ export class RouterCore<
|
|
|
1610
1707
|
const currentLocation =
|
|
1611
1708
|
dest._fromLocation || this.pendingBuiltLocation || this.latestLocation
|
|
1612
1709
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
// Now let's find the starting pathname
|
|
1618
|
-
// This should default to the current location if no from is provided
|
|
1619
|
-
const lastMatch = last(allCurrentLocationMatches)!
|
|
1710
|
+
// Use lightweight matching - only computes what buildLocation needs
|
|
1711
|
+
// (fullPath, search, params) without creating full match objects
|
|
1712
|
+
const lightweightResult = this.matchRoutesLightweight(currentLocation)
|
|
1620
1713
|
|
|
1621
1714
|
// check that from path exists in the current route tree
|
|
1622
1715
|
// do this check only on navigations during test or development
|
|
@@ -1627,12 +1720,12 @@ export class RouterCore<
|
|
|
1627
1720
|
) {
|
|
1628
1721
|
const allFromMatches = this.getMatchedRoutes(dest.from).matchedRoutes
|
|
1629
1722
|
|
|
1630
|
-
const matchedFrom = findLast(
|
|
1723
|
+
const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => {
|
|
1631
1724
|
return comparePaths(d.fullPath, dest.from!)
|
|
1632
1725
|
})
|
|
1633
1726
|
|
|
1634
1727
|
const matchedCurrent = findLast(allFromMatches, (d) => {
|
|
1635
|
-
return comparePaths(d.fullPath,
|
|
1728
|
+
return comparePaths(d.fullPath, lightweightResult.fullPath)
|
|
1636
1729
|
})
|
|
1637
1730
|
|
|
1638
1731
|
// for from to be invalid it shouldn't just be unmatched to currentLocation
|
|
@@ -1645,15 +1738,15 @@ export class RouterCore<
|
|
|
1645
1738
|
const defaultedFromPath =
|
|
1646
1739
|
dest.unsafeRelative === 'path'
|
|
1647
1740
|
? currentLocation.pathname
|
|
1648
|
-
: (dest.from ??
|
|
1741
|
+
: (dest.from ?? lightweightResult.fullPath)
|
|
1649
1742
|
|
|
1650
1743
|
// ensure this includes the basePath if set
|
|
1651
1744
|
const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
|
|
1652
1745
|
|
|
1653
1746
|
// From search should always use the current location
|
|
1654
|
-
const fromSearch =
|
|
1747
|
+
const fromSearch = lightweightResult.search
|
|
1655
1748
|
// Same with params. It can't hurt to provide as many as possible
|
|
1656
|
-
const fromParams = { ...
|
|
1749
|
+
const fromParams = { ...lightweightResult.params }
|
|
1657
1750
|
|
|
1658
1751
|
// Resolve the next to
|
|
1659
1752
|
// ensure this includes the basePath if set
|
|
@@ -1721,7 +1814,7 @@ export class RouterCore<
|
|
|
1721
1814
|
interpolatePath({
|
|
1722
1815
|
path: nextTo,
|
|
1723
1816
|
params: nextParams,
|
|
1724
|
-
|
|
1817
|
+
decoder: this.pathParamsDecoder,
|
|
1725
1818
|
}).interpolatedPath,
|
|
1726
1819
|
)
|
|
1727
1820
|
|
|
@@ -2802,7 +2895,7 @@ function applySearchMiddleware({
|
|
|
2802
2895
|
_includeValidateSearch,
|
|
2803
2896
|
}: {
|
|
2804
2897
|
search: any
|
|
2805
|
-
dest:
|
|
2898
|
+
dest: { search?: unknown }
|
|
2806
2899
|
destRoutes: ReadonlyArray<AnyRoute>
|
|
2807
2900
|
_includeValidateSearch: boolean | undefined
|
|
2808
2901
|
}) {
|
|
@@ -2937,3 +3030,25 @@ function findGlobalNotFoundRouteId(
|
|
|
2937
3030
|
}
|
|
2938
3031
|
return rootRouteId
|
|
2939
3032
|
}
|
|
3033
|
+
|
|
3034
|
+
function extractStrictParams(
|
|
3035
|
+
route: AnyRoute,
|
|
3036
|
+
referenceParams: Record<string, unknown>,
|
|
3037
|
+
parsedParams: Record<string, unknown>,
|
|
3038
|
+
accumulatedParams: Record<string, unknown>,
|
|
3039
|
+
) {
|
|
3040
|
+
const parseParams = route.options.params?.parse ?? route.options.parseParams
|
|
3041
|
+
if (parseParams) {
|
|
3042
|
+
if (route.options.skipRouteOnParseError) {
|
|
3043
|
+
// Use pre-parsed params from route matching for skipRouteOnParseError routes
|
|
3044
|
+
for (const key in referenceParams) {
|
|
3045
|
+
if (key in parsedParams) {
|
|
3046
|
+
accumulatedParams[key] = parsedParams[key]
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
} else {
|
|
3050
|
+
const result = parseParams(accumulatedParams as Record<string, string>)
|
|
3051
|
+
Object.assign(accumulatedParams, result)
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|