@tanstack/router-core 1.155.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.155.0",
3
+ "version": "1.156.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -778,6 +778,17 @@ export function trimPathRight(path: string) {
778
778
  return path === '/' ? path : path.replace(/\/{1,}$/, '')
779
779
  }
780
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
+
781
792
  /**
782
793
  * Processes a route tree into a segment trie for efficient path matching.
783
794
  * Also builds lookup maps for routes by ID and by trimmed full path.
@@ -791,14 +802,7 @@ export function processRouteTree<
791
802
  caseSensitive: boolean = false,
792
803
  /** Optional callback invoked for each route during processing. */
793
804
  initRoute?: (route: TRouteLike, index: number) => void,
794
- ): {
795
- /** Should be considered a black box, needs to be provided to all matching functions in this module. */
796
- processedTree: ProcessedTree<TRouteLike, any, any>
797
- /** A lookup map of routes by their unique IDs. */
798
- routesById: Record<string, TRouteLike>
799
- /** A lookup map of routes by their trimmed full paths. */
800
- routesByPath: Record<string, TRouteLike>
801
- } {
805
+ ): ProcessRouteTreeResult<TRouteLike> {
802
806
  const segmentTree = createStaticNode<TRouteLike>(routeTree.fullPath)
803
807
  const data = new Uint16Array(6)
804
808
  const routesById = {} as Record<string, TRouteLike>
package/src/router.ts CHANGED
@@ -38,7 +38,11 @@ import {
38
38
  executeRewriteOutput,
39
39
  rewriteBasepath,
40
40
  } from './rewrite'
41
- import type { ProcessedTree } from './new-process-route-tree'
41
+ import type { LRUCache } from './lru-cache'
42
+ import type {
43
+ ProcessRouteTreeResult,
44
+ ProcessedTree,
45
+ } from './new-process-route-tree'
42
46
  import type { SearchParser, SearchSerializer } from './searchParams'
43
47
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
44
48
  import type {
@@ -589,7 +593,6 @@ export type SubscribeFn = <TType extends keyof RouterEvents>(
589
593
  export interface MatchRoutesOpts {
590
594
  preload?: boolean
591
595
  throwOnError?: boolean
592
- _buildLocation?: boolean
593
596
  dest?: BuildNextOptions
594
597
  }
595
598
 
@@ -873,6 +876,17 @@ export type CreateRouterFn = <
873
876
  TDehydrated
874
877
  >
875
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
+
876
890
  /**
877
891
  * Core, framework-agnostic router engine that powers TanStack Router.
878
892
  *
@@ -923,6 +937,7 @@ export class RouterCore<
923
937
  routesById!: RoutesById<TRouteTree>
924
938
  routesByPath!: RoutesByPath<TRouteTree>
925
939
  processedTree!: ProcessedTree<TRouteTree, any, any>
940
+ resolvePathCache!: LRUCache<string, string>
926
941
  isServer!: boolean
927
942
  pathParamsDecoder?: (encoded: string) => string
928
943
 
@@ -1027,7 +1042,28 @@ export class RouterCore<
1027
1042
 
1028
1043
  if (this.options.routeTree !== this.routeTree) {
1029
1044
  this.routeTree = this.options.routeTree as TRouteTree
1030
- this.buildRouteTree()
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)
1031
1067
  }
1032
1068
 
1033
1069
  if (!this.__store && this.latestLocation) {
@@ -1110,7 +1146,7 @@ export class RouterCore<
1110
1146
  }
1111
1147
 
1112
1148
  buildRouteTree = () => {
1113
- const { routesById, routesByPath, processedTree } = processRouteTree(
1149
+ const result = processRouteTree(
1114
1150
  this.routeTree,
1115
1151
  this.options.caseSensitive,
1116
1152
  (route, i) => {
@@ -1120,9 +1156,17 @@ export class RouterCore<
1120
1156
  },
1121
1157
  )
1122
1158
  if (this.options.routeMasks) {
1123
- processRouteMasks(this.options.routeMasks, processedTree)
1159
+ processRouteMasks(this.options.routeMasks, result.processedTree)
1124
1160
  }
1125
1161
 
1162
+ return result
1163
+ }
1164
+
1165
+ setRoutes({
1166
+ routesById,
1167
+ routesByPath,
1168
+ processedTree,
1169
+ }: ProcessRouteTreeResult<TRouteTree>) {
1126
1170
  this.routesById = routesById as RoutesById<TRouteTree>
1127
1171
  this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
1128
1172
  this.processedTree = processedTree
@@ -1222,8 +1266,6 @@ export class RouterCore<
1222
1266
  return location
1223
1267
  }
1224
1268
 
1225
- resolvePathCache = createLRUCache<string, string>(1000)
1226
-
1227
1269
  /** Resolve a path against the router basepath and trailing-slash policy. */
1228
1270
  resolvePathWithBase = (from: string, path: string) => {
1229
1271
  const resolvedPath = resolvePath({
@@ -1390,35 +1432,19 @@ export class RouterCore<
1390
1432
  let paramsError: unknown = undefined
1391
1433
 
1392
1434
  if (!existingMatch) {
1393
- if (route.options.skipRouteOnParseError) {
1394
- for (const key in usedParams) {
1395
- if (key in parsedParams!) {
1396
- strictParams[key] = parsedParams![key]
1397
- }
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
+ })
1398
1444
  }
1399
- } else {
1400
- const strictParseParams =
1401
- route.options.params?.parse ?? route.options.parseParams
1402
1445
 
1403
- if (strictParseParams) {
1404
- try {
1405
- Object.assign(
1406
- strictParams,
1407
- strictParseParams(strictParams as Record<string, string>),
1408
- )
1409
- } catch (err: any) {
1410
- if (isNotFound(err) || isRedirect(err)) {
1411
- paramsError = err
1412
- } else {
1413
- paramsError = new PathParamError(err.message, {
1414
- cause: err,
1415
- })
1416
- }
1417
-
1418
- if (opts?.throwOnError) {
1419
- throw paramsError
1420
- }
1421
- }
1446
+ if (opts?.throwOnError) {
1447
+ throw paramsError
1422
1448
  }
1423
1449
  }
1424
1450
  }
@@ -1519,7 +1545,7 @@ export class RouterCore<
1519
1545
 
1520
1546
  // only execute `context` if we are not calling from router.buildLocation
1521
1547
 
1522
- if (!existingMatch && opts?._buildLocation !== true) {
1548
+ if (!existingMatch) {
1523
1549
  const parentMatch = matches[index - 1]
1524
1550
  const parentContext = getParentContext(parentMatch)
1525
1551
 
@@ -1563,6 +1589,80 @@ export class RouterCore<
1563
1589
  })
1564
1590
  }
1565
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
+
1566
1666
  cancelMatch = (id: string) => {
1567
1667
  const match = this.getMatch(id)
1568
1668
 
@@ -1607,13 +1707,9 @@ export class RouterCore<
1607
1707
  const currentLocation =
1608
1708
  dest._fromLocation || this.pendingBuiltLocation || this.latestLocation
1609
1709
 
1610
- const allCurrentLocationMatches = this.matchRoutes(currentLocation, {
1611
- _buildLocation: true,
1612
- })
1613
-
1614
- // Now let's find the starting pathname
1615
- // This should default to the current location if no from is provided
1616
- 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)
1617
1713
 
1618
1714
  // check that from path exists in the current route tree
1619
1715
  // do this check only on navigations during test or development
@@ -1624,12 +1720,12 @@ export class RouterCore<
1624
1720
  ) {
1625
1721
  const allFromMatches = this.getMatchedRoutes(dest.from).matchedRoutes
1626
1722
 
1627
- const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1723
+ const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => {
1628
1724
  return comparePaths(d.fullPath, dest.from!)
1629
1725
  })
1630
1726
 
1631
1727
  const matchedCurrent = findLast(allFromMatches, (d) => {
1632
- return comparePaths(d.fullPath, lastMatch.fullPath)
1728
+ return comparePaths(d.fullPath, lightweightResult.fullPath)
1633
1729
  })
1634
1730
 
1635
1731
  // for from to be invalid it shouldn't just be unmatched to currentLocation
@@ -1642,15 +1738,15 @@ export class RouterCore<
1642
1738
  const defaultedFromPath =
1643
1739
  dest.unsafeRelative === 'path'
1644
1740
  ? currentLocation.pathname
1645
- : (dest.from ?? lastMatch.fullPath)
1741
+ : (dest.from ?? lightweightResult.fullPath)
1646
1742
 
1647
1743
  // ensure this includes the basePath if set
1648
1744
  const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1649
1745
 
1650
1746
  // From search should always use the current location
1651
- const fromSearch = lastMatch.search
1747
+ const fromSearch = lightweightResult.search
1652
1748
  // Same with params. It can't hurt to provide as many as possible
1653
- const fromParams = { ...lastMatch.params }
1749
+ const fromParams = { ...lightweightResult.params }
1654
1750
 
1655
1751
  // Resolve the next to
1656
1752
  // ensure this includes the basePath if set
@@ -2799,7 +2895,7 @@ function applySearchMiddleware({
2799
2895
  _includeValidateSearch,
2800
2896
  }: {
2801
2897
  search: any
2802
- dest: BuildNextOptions
2898
+ dest: { search?: unknown }
2803
2899
  destRoutes: ReadonlyArray<AnyRoute>
2804
2900
  _includeValidateSearch: boolean | undefined
2805
2901
  }) {
@@ -2934,3 +3030,25 @@ function findGlobalNotFoundRouteId(
2934
3030
  }
2935
3031
  return rootRouteId
2936
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
+ }