@tanstack/router-core 1.129.7 → 1.129.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.129.7",
3
+ "version": "1.129.8",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -0,0 +1,68 @@
1
+ export type LRUCache<TKey, TValue> = {
2
+ get: (key: TKey) => TValue | undefined
3
+ set: (key: TKey, value: TValue) => void
4
+ }
5
+
6
+ export function createLRUCache<TKey, TValue>(
7
+ max: number,
8
+ ): LRUCache<TKey, TValue> {
9
+ type Node = { prev?: Node; next?: Node; key: TKey; value: TValue }
10
+ const cache = new Map<TKey, Node>()
11
+ let oldest: Node | undefined
12
+ let newest: Node | undefined
13
+
14
+ const touch = (entry: Node) => {
15
+ if (!entry.next) return
16
+ if (!entry.prev) {
17
+ entry.next.prev = undefined
18
+ oldest = entry.next
19
+ entry.next = undefined
20
+ if (newest) {
21
+ entry.prev = newest
22
+ newest.next = entry
23
+ }
24
+ } else {
25
+ entry.prev.next = entry.next
26
+ entry.next.prev = entry.prev
27
+ entry.next = undefined
28
+ if (newest) {
29
+ newest.next = entry
30
+ entry.prev = newest
31
+ }
32
+ }
33
+ newest = entry
34
+ }
35
+
36
+ return {
37
+ get(key) {
38
+ const entry = cache.get(key)
39
+ if (!entry) return undefined
40
+ touch(entry)
41
+ return entry.value
42
+ },
43
+ set(key, value) {
44
+ if (cache.size >= max && oldest) {
45
+ const toDelete = oldest
46
+ cache.delete(toDelete.key)
47
+ if (toDelete.next) {
48
+ oldest = toDelete.next
49
+ toDelete.next.prev = undefined
50
+ }
51
+ if (toDelete === newest) {
52
+ newest = undefined
53
+ }
54
+ }
55
+ const existing = cache.get(key)
56
+ if (existing) {
57
+ existing.value = value
58
+ touch(existing)
59
+ } else {
60
+ const entry: Node = { key, value, prev: newest }
61
+ if (newest) newest.next = entry
62
+ newest = entry
63
+ if (!oldest) oldest = entry
64
+ cache.set(key, entry)
65
+ }
66
+ },
67
+ }
68
+ }
package/src/path.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { last } from './utils'
2
+ import type { LRUCache } from './lru-cache'
2
3
  import type { MatchLocation } from './RouterProvider'
3
4
  import type { AnyPathParams } from './route'
4
5
 
@@ -101,6 +102,7 @@ interface ResolvePathOptions {
101
102
  to: string
102
103
  trailingSlash?: 'always' | 'never' | 'preserve'
103
104
  caseSensitive?: boolean
105
+ parseCache?: ParsePathnameCache
104
106
  }
105
107
 
106
108
  function segmentToString(segment: Segment): string {
@@ -154,12 +156,13 @@ export function resolvePath({
154
156
  to,
155
157
  trailingSlash = 'never',
156
158
  caseSensitive,
159
+ parseCache,
157
160
  }: ResolvePathOptions) {
158
161
  base = removeBasepath(basepath, base, caseSensitive)
159
162
  to = removeBasepath(basepath, to, caseSensitive)
160
163
 
161
- let baseSegments = parsePathname(base).slice()
162
- const toSegments = parsePathname(to)
164
+ let baseSegments = parsePathname(base, parseCache).slice()
165
+ const toSegments = parsePathname(to, parseCache)
163
166
 
164
167
  if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
165
168
  baseSegments.pop()
@@ -202,6 +205,19 @@ export function resolvePath({
202
205
  return joined
203
206
  }
204
207
 
208
+ export type ParsePathnameCache = LRUCache<string, ReadonlyArray<Segment>>
209
+ export const parsePathname = (
210
+ pathname?: string,
211
+ cache?: ParsePathnameCache,
212
+ ): ReadonlyArray<Segment> => {
213
+ if (!pathname) return []
214
+ const cached = cache?.get(pathname)
215
+ if (cached) return cached
216
+ const parsed = baseParsePathname(pathname)
217
+ cache?.set(pathname, parsed)
218
+ return parsed
219
+ }
220
+
205
221
  const PARAM_RE = /^\$.{1,}$/ // $paramName
206
222
  const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
207
223
  const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
@@ -227,11 +243,7 @@ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
227
243
  * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
228
244
  * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
229
245
  */
230
- export function parsePathname(pathname?: string): ReadonlyArray<Segment> {
231
- if (!pathname) {
232
- return []
233
- }
234
-
246
+ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
235
247
  pathname = cleanPath(pathname)
236
248
 
237
249
  const segments: Array<Segment> = []
@@ -348,6 +360,7 @@ interface InterpolatePathOptions {
348
360
  leaveParams?: boolean
349
361
  // Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
350
362
  decodeCharMap?: Map<string, string>
363
+ parseCache?: ParsePathnameCache
351
364
  }
352
365
 
353
366
  type InterPolatePathResult = {
@@ -361,8 +374,9 @@ export function interpolatePath({
361
374
  leaveWildcards,
362
375
  leaveParams,
363
376
  decodeCharMap,
377
+ parseCache,
364
378
  }: InterpolatePathOptions): InterPolatePathResult {
365
- const interpolatedPathSegments = parsePathname(path)
379
+ const interpolatedPathSegments = parsePathname(path, parseCache)
366
380
 
367
381
  function encodeParam(key: string): any {
368
382
  const value = params[key]
@@ -480,8 +494,14 @@ export function matchPathname(
480
494
  basepath: string,
481
495
  currentPathname: string,
482
496
  matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
497
+ parseCache?: ParsePathnameCache,
483
498
  ): AnyPathParams | undefined {
484
- const pathParams = matchByPath(basepath, currentPathname, matchLocation)
499
+ const pathParams = matchByPath(
500
+ basepath,
501
+ currentPathname,
502
+ matchLocation,
503
+ parseCache,
504
+ )
485
505
  // const searchMatched = matchBySearch(location.search, matchLocation)
486
506
 
487
507
  if (matchLocation.to && !pathParams) {
@@ -540,6 +560,7 @@ export function matchByPath(
540
560
  fuzzy,
541
561
  caseSensitive,
542
562
  }: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
563
+ parseCache?: ParsePathnameCache,
543
564
  ): Record<string, string> | undefined {
544
565
  // check basepath first
545
566
  if (basepath !== '/' && !from.startsWith(basepath)) {
@@ -551,8 +572,14 @@ export function matchByPath(
551
572
  to = removeBasepath(basepath, `${to ?? '$'}`, caseSensitive)
552
573
 
553
574
  // Parse the from and to
554
- const baseSegments = parsePathname(from.startsWith('/') ? from : `/${from}`)
555
- const routeSegments = parsePathname(to.startsWith('/') ? to : `/${to}`)
575
+ const baseSegments = parsePathname(
576
+ from.startsWith('/') ? from : `/${from}`,
577
+ parseCache,
578
+ )
579
+ const routeSegments = parsePathname(
580
+ to.startsWith('/') ? to : `/${to}`,
581
+ parseCache,
582
+ )
556
583
 
557
584
  const params: Record<string, string> = {}
558
585
 
package/src/router.ts CHANGED
@@ -33,7 +33,8 @@ import { setupScrollRestoration } from './scroll-restoration'
33
33
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
34
34
  import { rootRouteId } from './root'
35
35
  import { isRedirect, redirect } from './redirect'
36
- import type { Segment } from './path'
36
+ import { createLRUCache } from './lru-cache'
37
+ import type { ParsePathnameCache, Segment } from './path'
37
38
  import type { SearchParser, SearchSerializer } from './searchParams'
38
39
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
39
40
  import type {
@@ -1014,6 +1015,7 @@ export class RouterCore<
1014
1015
  to: cleanPath(path),
1015
1016
  trailingSlash: this.options.trailingSlash,
1016
1017
  caseSensitive: this.options.caseSensitive,
1018
+ parseCache: this.parsePathnameCache,
1017
1019
  })
1018
1020
  return resolvedPath
1019
1021
  }
@@ -1195,6 +1197,7 @@ export class RouterCore<
1195
1197
  params: routeParams,
1196
1198
  leaveWildcards: true,
1197
1199
  decodeCharMap: this.pathParamsDecodeCharMap,
1200
+ parseCache: this.parsePathnameCache,
1198
1201
  }).interpolatedPath + loaderDepsHash
1199
1202
 
1200
1203
  // Waste not, want not. If we already have a match for this route,
@@ -1333,6 +1336,9 @@ export class RouterCore<
1333
1336
  return matches
1334
1337
  }
1335
1338
 
1339
+ /** a cache for `parsePathname` */
1340
+ private parsePathnameCache: ParsePathnameCache = createLRUCache(1000)
1341
+
1336
1342
  getMatchedRoutes: GetMatchRoutesFn = (
1337
1343
  pathname: string,
1338
1344
  routePathname: string | undefined,
@@ -1345,6 +1351,7 @@ export class RouterCore<
1345
1351
  routesByPath: this.routesByPath,
1346
1352
  routesById: this.routesById,
1347
1353
  flatRoutes: this.flatRoutes,
1354
+ parseCache: this.parsePathnameCache,
1348
1355
  })
1349
1356
  }
1350
1357
 
@@ -1452,6 +1459,7 @@ export class RouterCore<
1452
1459
  const interpolatedNextTo = interpolatePath({
1453
1460
  path: nextTo,
1454
1461
  params: nextParams ?? {},
1462
+ parseCache: this.parsePathnameCache,
1455
1463
  }).interpolatedPath
1456
1464
 
1457
1465
  const destRoutes = this.matchRoutes(
@@ -1484,6 +1492,7 @@ export class RouterCore<
1484
1492
  leaveWildcards: false,
1485
1493
  leaveParams: opts.leaveParams,
1486
1494
  decodeCharMap: this.pathParamsDecodeCharMap,
1495
+ parseCache: this.parsePathnameCache,
1487
1496
  }).interpolatedPath
1488
1497
 
1489
1498
  // Resolve the next search
@@ -1567,11 +1576,16 @@ export class RouterCore<
1567
1576
  let params = {}
1568
1577
 
1569
1578
  const foundMask = this.options.routeMasks?.find((d) => {
1570
- const match = matchPathname(this.basepath, next.pathname, {
1571
- to: d.from,
1572
- caseSensitive: false,
1573
- fuzzy: false,
1574
- })
1579
+ const match = matchPathname(
1580
+ this.basepath,
1581
+ next.pathname,
1582
+ {
1583
+ to: d.from,
1584
+ caseSensitive: false,
1585
+ fuzzy: false,
1586
+ },
1587
+ this.parsePathnameCache,
1588
+ )
1575
1589
 
1576
1590
  if (match) {
1577
1591
  params = match
@@ -2971,10 +2985,15 @@ export class RouterCore<
2971
2985
  ? this.latestLocation
2972
2986
  : this.state.resolvedLocation || this.state.location
2973
2987
 
2974
- const match = matchPathname(this.basepath, baseLocation.pathname, {
2975
- ...opts,
2976
- to: next.pathname,
2977
- }) as any
2988
+ const match = matchPathname(
2989
+ this.basepath,
2990
+ baseLocation.pathname,
2991
+ {
2992
+ ...opts,
2993
+ to: next.pathname,
2994
+ },
2995
+ this.parsePathnameCache,
2996
+ ) as any
2978
2997
 
2979
2998
  if (!match) {
2980
2999
  return false
@@ -3368,6 +3387,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3368
3387
  routesByPath,
3369
3388
  routesById,
3370
3389
  flatRoutes,
3390
+ parseCache,
3371
3391
  }: {
3372
3392
  pathname: string
3373
3393
  routePathname?: string
@@ -3376,16 +3396,22 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3376
3396
  routesByPath: Record<string, TRouteLike>
3377
3397
  routesById: Record<string, TRouteLike>
3378
3398
  flatRoutes: Array<TRouteLike>
3399
+ parseCache?: ParsePathnameCache
3379
3400
  }) {
3380
3401
  let routeParams: Record<string, string> = {}
3381
3402
  const trimmedPath = trimPathRight(pathname)
3382
3403
  const getMatchedParams = (route: TRouteLike) => {
3383
- const result = matchPathname(basepath, trimmedPath, {
3384
- to: route.fullPath,
3385
- caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3386
- // we need fuzzy matching for `notFoundMode: 'fuzzy'`
3387
- fuzzy: true,
3388
- })
3404
+ const result = matchPathname(
3405
+ basepath,
3406
+ trimmedPath,
3407
+ {
3408
+ to: route.fullPath,
3409
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3410
+ // we need fuzzy matching for `notFoundMode: 'fuzzy'`
3411
+ fuzzy: true,
3412
+ },
3413
+ parseCache,
3414
+ )
3389
3415
  return result
3390
3416
  }
3391
3417