@benjavicente/router-core 1.168.9 → 1.169.2

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.
Files changed (169) hide show
  1. package/dist/cjs/Matches.cjs +1 -1
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/config.cjs +1 -1
  4. package/dist/cjs/config.cjs.map +1 -1
  5. package/dist/cjs/defer.cjs +1 -1
  6. package/dist/cjs/defer.cjs.map +1 -1
  7. package/dist/cjs/hash-scroll.cjs +1 -1
  8. package/dist/cjs/hash-scroll.cjs.map +1 -1
  9. package/dist/cjs/index.cjs +4 -0
  10. package/dist/cjs/index.d.cts +3 -3
  11. package/dist/cjs/isServer/client.cjs +1 -1
  12. package/dist/cjs/isServer/client.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.cjs +1 -1
  14. package/dist/cjs/isServer/development.cjs.map +1 -1
  15. package/dist/cjs/isServer/server.cjs +1 -1
  16. package/dist/cjs/isServer/server.cjs.map +1 -1
  17. package/dist/cjs/link.cjs +1 -1
  18. package/dist/cjs/link.cjs.map +1 -1
  19. package/dist/cjs/load-matches.cjs +26 -26
  20. package/dist/cjs/load-matches.cjs.map +1 -1
  21. package/dist/cjs/manifest.cjs +31 -0
  22. package/dist/cjs/manifest.cjs.map +1 -1
  23. package/dist/cjs/manifest.d.cts +8 -0
  24. package/dist/cjs/new-process-route-tree.cjs +55 -67
  25. package/dist/cjs/new-process-route-tree.cjs.map +1 -1
  26. package/dist/cjs/new-process-route-tree.d.cts +4 -26
  27. package/dist/cjs/not-found.cjs +1 -1
  28. package/dist/cjs/not-found.cjs.map +1 -1
  29. package/dist/cjs/path.cjs +1 -22
  30. package/dist/cjs/path.cjs.map +1 -1
  31. package/dist/cjs/root.cjs +1 -1
  32. package/dist/cjs/root.cjs.map +1 -1
  33. package/dist/cjs/route.cjs.map +1 -1
  34. package/dist/cjs/route.d.cts +6 -34
  35. package/dist/cjs/router.cjs +94 -80
  36. package/dist/cjs/router.cjs.map +1 -1
  37. package/dist/cjs/router.d.cts +6 -5
  38. package/dist/cjs/scroll-restoration-script/server.cjs +1 -1
  39. package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
  40. package/dist/cjs/scroll-restoration.cjs +7 -7
  41. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  42. package/dist/cjs/searchParams.cjs +2 -2
  43. package/dist/cjs/searchParams.cjs.map +1 -1
  44. package/dist/cjs/ssr/constants.cjs +2 -2
  45. package/dist/cjs/ssr/constants.cjs.map +1 -1
  46. package/dist/cjs/ssr/createRequestHandler.cjs +2 -2
  47. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  48. package/dist/cjs/ssr/serializer/RawStream.cjs +50 -41
  49. package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
  50. package/dist/cjs/ssr/serializer/RawStream.d.cts +12 -4
  51. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs +1 -1
  52. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
  53. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.d.cts +2 -2
  54. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +1 -1
  55. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
  56. package/dist/cjs/ssr/serializer/seroval-plugins.d.cts +2 -1
  57. package/dist/cjs/ssr/serializer/transformer.cjs +16 -14
  58. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
  59. package/dist/cjs/ssr/serializer/transformer.d.cts +24 -23
  60. package/dist/cjs/ssr/ssr-client.cjs +9 -9
  61. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  62. package/dist/cjs/ssr/ssr-server.cjs +110 -22
  63. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  64. package/dist/cjs/ssr/ssr-server.d.cts +4 -2
  65. package/dist/cjs/ssr/transformStreamWithRouter.cjs +10 -7
  66. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  67. package/dist/cjs/stores.cjs +57 -57
  68. package/dist/cjs/stores.cjs.map +1 -1
  69. package/dist/cjs/stores.d.cts +16 -16
  70. package/dist/cjs/utils.cjs +12 -7
  71. package/dist/cjs/utils.cjs.map +1 -1
  72. package/dist/cjs/utils.d.cts +1 -0
  73. package/dist/esm/Matches.js +1 -1
  74. package/dist/esm/Matches.js.map +1 -1
  75. package/dist/esm/config.js +1 -1
  76. package/dist/esm/config.js.map +1 -1
  77. package/dist/esm/defer.js +1 -1
  78. package/dist/esm/defer.js.map +1 -1
  79. package/dist/esm/hash-scroll.js +1 -1
  80. package/dist/esm/hash-scroll.js.map +1 -1
  81. package/dist/esm/index.d.ts +3 -3
  82. package/dist/esm/index.js +3 -3
  83. package/dist/esm/isServer/client.js +1 -1
  84. package/dist/esm/isServer/client.js.map +1 -1
  85. package/dist/esm/isServer/development.js +1 -1
  86. package/dist/esm/isServer/development.js.map +1 -1
  87. package/dist/esm/isServer/server.js +1 -1
  88. package/dist/esm/isServer/server.js.map +1 -1
  89. package/dist/esm/link.js +1 -1
  90. package/dist/esm/link.js.map +1 -1
  91. package/dist/esm/load-matches.js +26 -26
  92. package/dist/esm/load-matches.js.map +1 -1
  93. package/dist/esm/manifest.d.ts +8 -0
  94. package/dist/esm/manifest.js +28 -1
  95. package/dist/esm/manifest.js.map +1 -1
  96. package/dist/esm/new-process-route-tree.d.ts +4 -26
  97. package/dist/esm/new-process-route-tree.js +55 -68
  98. package/dist/esm/new-process-route-tree.js.map +1 -1
  99. package/dist/esm/not-found.js +1 -1
  100. package/dist/esm/not-found.js.map +1 -1
  101. package/dist/esm/path.js +1 -22
  102. package/dist/esm/path.js.map +1 -1
  103. package/dist/esm/root.js +1 -1
  104. package/dist/esm/root.js.map +1 -1
  105. package/dist/esm/route.d.ts +6 -34
  106. package/dist/esm/route.js.map +1 -1
  107. package/dist/esm/router.d.ts +6 -5
  108. package/dist/esm/router.js +96 -82
  109. package/dist/esm/router.js.map +1 -1
  110. package/dist/esm/scroll-restoration-script/server.js +1 -1
  111. package/dist/esm/scroll-restoration-script/server.js.map +1 -1
  112. package/dist/esm/scroll-restoration.js +7 -7
  113. package/dist/esm/scroll-restoration.js.map +1 -1
  114. package/dist/esm/searchParams.js +2 -2
  115. package/dist/esm/searchParams.js.map +1 -1
  116. package/dist/esm/ssr/constants.js +2 -2
  117. package/dist/esm/ssr/constants.js.map +1 -1
  118. package/dist/esm/ssr/createRequestHandler.js +2 -2
  119. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  120. package/dist/esm/ssr/serializer/RawStream.d.ts +12 -4
  121. package/dist/esm/ssr/serializer/RawStream.js +50 -41
  122. package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
  123. package/dist/esm/ssr/serializer/ShallowErrorPlugin.d.ts +2 -2
  124. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js +1 -1
  125. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
  126. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -1
  127. package/dist/esm/ssr/serializer/seroval-plugins.js +1 -1
  128. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
  129. package/dist/esm/ssr/serializer/transformer.d.ts +24 -23
  130. package/dist/esm/ssr/serializer/transformer.js +16 -14
  131. package/dist/esm/ssr/serializer/transformer.js.map +1 -1
  132. package/dist/esm/ssr/ssr-client.js +9 -9
  133. package/dist/esm/ssr/ssr-client.js.map +1 -1
  134. package/dist/esm/ssr/ssr-server.d.ts +4 -2
  135. package/dist/esm/ssr/ssr-server.js +110 -22
  136. package/dist/esm/ssr/ssr-server.js.map +1 -1
  137. package/dist/esm/ssr/transformStreamWithRouter.js +10 -7
  138. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  139. package/dist/esm/stores.d.ts +16 -16
  140. package/dist/esm/stores.js +58 -58
  141. package/dist/esm/stores.js.map +1 -1
  142. package/dist/esm/utils.d.ts +1 -0
  143. package/dist/esm/utils.js +12 -8
  144. package/dist/esm/utils.js.map +1 -1
  145. package/package.json +4 -8
  146. package/skills/router-core/auth-and-guards/SKILL.md +41 -0
  147. package/skills/router-core/ssr/SKILL.md +59 -5
  148. package/skills/router-core/type-safety/SKILL.md +37 -42
  149. package/src/hash-scroll.ts +1 -1
  150. package/src/index.ts +9 -2
  151. package/src/load-matches.ts +14 -15
  152. package/src/manifest.ts +46 -0
  153. package/src/new-process-route-tree.ts +93 -106
  154. package/src/not-found.ts +1 -1
  155. package/src/path.ts +1 -27
  156. package/src/route.ts +10 -36
  157. package/src/router.ts +151 -140
  158. package/src/scroll-restoration.ts +1 -1
  159. package/src/ssr/createRequestHandler.ts +4 -5
  160. package/src/ssr/serializer/RawStream.ts +65 -56
  161. package/src/ssr/serializer/ShallowErrorPlugin.ts +2 -2
  162. package/src/ssr/serializer/seroval-plugins.ts +2 -1
  163. package/src/ssr/serializer/transformer.ts +71 -76
  164. package/src/ssr/ssr-client.ts +8 -12
  165. package/src/ssr/ssr-server.ts +170 -14
  166. package/src/ssr/transformStreamWithRouter.ts +3 -0
  167. package/src/stores.ts +86 -86
  168. package/src/utils.ts +7 -0
  169. package/bin/intent.js +0 -25
package/src/router.ts CHANGED
@@ -8,12 +8,14 @@ import {
8
8
  encodePathLikeUrl,
9
9
  findLast,
10
10
  functionalUpdate,
11
+ hasKeys,
11
12
  isDangerousProtocol,
12
13
  last,
13
14
  nullReplaceEqualDeep,
14
15
  replaceEqualDeep,
15
16
  } from './utils'
16
17
  import {
18
+ buildRouteBranch,
17
19
  findFlatMatch,
18
20
  findRouteMatch,
19
21
  findSingleMatch,
@@ -732,8 +734,6 @@ export type GetMatchRoutesFn = (pathname: string) => {
732
734
  matchedRoutes: ReadonlyArray<AnyRoute>
733
735
  /** exhaustive params, still in their string form */
734
736
  routeParams: Record<string, string>
735
- /** partial params, parsed from routeParams during matching */
736
- parsedParams: Record<string, unknown> | undefined
737
737
  foundRoute: AnyRoute | undefined
738
738
  parseError?: unknown
739
739
  }
@@ -799,7 +799,9 @@ export interface ServerSsr {
799
799
  setRenderFinished: () => void
800
800
  cleanup: () => void
801
801
  onSerializationFinished: (listener: () => void) => void
802
- dehydrate: () => Promise<void>
802
+ dehydrate: (opts?: {
803
+ requestAssets?: Array<RouterManagedTag>
804
+ }) => Promise<void>
803
805
  takeBufferedScripts: () => RouterManagedTag | undefined
804
806
  /**
805
807
  * Takes any buffered HTML that was injected.
@@ -970,6 +972,7 @@ export class RouterCore<
970
972
  routesByPath!: RoutesByPath<TRouteTree>
971
973
  processedTree!: ProcessedTree<TRouteTree, any, any>
972
974
  resolvePathCache!: LRUCache<string, string>
975
+ private routeBranchCache = new WeakMap<AnyRoute, ReadonlyArray<AnyRoute>>()
973
976
  isServer!: boolean
974
977
  pathParamsDecoder?: (encoded: string) => string
975
978
  protocolAllowlist!: Set<string>
@@ -1165,7 +1168,7 @@ export class RouterCore<
1165
1168
  }
1166
1169
 
1167
1170
  if (needsLocationUpdate && this.stores) {
1168
- this.stores.location.setState(() => this.latestLocation)
1171
+ this.stores.location.set(this.latestLocation)
1169
1172
  }
1170
1173
 
1171
1174
  if (
@@ -1174,13 +1177,13 @@ export class RouterCore<
1174
1177
  typeof window.CSS?.supports === 'function'
1175
1178
  ) {
1176
1179
  this.isViewTransitionTypesSupported = window.CSS.supports(
1177
- 'selector(:active-view-transition-type(a)',
1180
+ 'selector(:active-view-transition-type(a))',
1178
1181
  )
1179
1182
  }
1180
1183
  }
1181
1184
 
1182
1185
  get state(): RouterState<TRouteTree> {
1183
- return this.stores.__store.state
1186
+ return this.stores.__store.get()
1184
1187
  }
1185
1188
 
1186
1189
  updateLatestLocation = () => {
@@ -1339,15 +1342,23 @@ export class RouterCore<
1339
1342
  return location
1340
1343
  }
1341
1344
 
1342
- /** Resolve a path against the router basepath and trailing-slash policy. */
1345
+ /** Resolve a path using the router's trailing-slash policy. */
1343
1346
  resolvePathWithBase = (from: string, path: string) => {
1344
- const resolvedPath = resolvePath({
1347
+ return resolvePath({
1345
1348
  base: from,
1346
- to: cleanPath(path),
1349
+ to: path.includes('//') ? cleanPath(path) : path,
1347
1350
  trailingSlash: this.options.trailingSlash,
1348
1351
  cache: this.resolvePathCache,
1349
1352
  })
1350
- return resolvedPath
1353
+ }
1354
+
1355
+ private getRouteBranch(route: AnyRoute) {
1356
+ let branch = this.routeBranchCache.get(route)
1357
+ if (!branch) {
1358
+ branch = buildRouteBranch(route)
1359
+ this.routeBranchCache.set(route, branch)
1360
+ }
1361
+ return branch
1351
1362
  }
1352
1363
 
1353
1364
  get looseRoutesById() {
@@ -1387,7 +1398,7 @@ export class RouterCore<
1387
1398
  opts?: MatchRoutesOpts,
1388
1399
  ): Array<AnyRouteMatch> {
1389
1400
  const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
1390
- const { foundRoute, routeParams, parsedParams } = matchedRoutesResult
1401
+ const { foundRoute, routeParams } = matchedRoutesResult
1391
1402
  let { matchedRoutes } = matchedRoutesResult
1392
1403
  let isGlobalNotFound = false
1393
1404
 
@@ -1416,9 +1427,9 @@ export class RouterCore<
1416
1427
  // Snapshot of active match state keyed by routeId, used to stabilise
1417
1428
  // params/search across navigations.
1418
1429
  const previousActiveMatchesByRouteId = new Map<string, AnyRouteMatch>()
1419
- for (const store of this.stores.activeMatchStoresById.values()) {
1430
+ for (const store of this.stores.matchStores.values()) {
1420
1431
  if (store.routeId) {
1421
- previousActiveMatchesByRouteId.set(store.routeId, store.state)
1432
+ previousActiveMatchesByRouteId.set(store.routeId, store.get())
1422
1433
  }
1423
1434
  }
1424
1435
 
@@ -1513,7 +1524,7 @@ export class RouterCore<
1513
1524
 
1514
1525
  if (!existingMatch) {
1515
1526
  try {
1516
- extractStrictParams(route, usedParams, parsedParams!, strictParams)
1527
+ extractStrictParams(route, strictParams)
1517
1528
  } catch (err: any) {
1518
1529
  if (isNotFound(err) || isRedirect(err)) {
1519
1530
  paramsError = err
@@ -1683,7 +1694,7 @@ export class RouterCore<
1683
1694
  search: Record<string, unknown>
1684
1695
  params: Record<string, unknown>
1685
1696
  } {
1686
- const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
1697
+ const { matchedRoutes, routeParams } = this.getMatchedRoutes(
1687
1698
  location.pathname,
1688
1699
  )
1689
1700
  const lastRoute = last(matchedRoutes)!
@@ -1711,10 +1722,9 @@ export class RouterCore<
1711
1722
  }
1712
1723
 
1713
1724
  // Determine params: reuse from state if possible, otherwise parse
1714
- const lastStateMatchId = last(this.stores.matchesId.state)
1725
+ const lastStateMatchId = last(this.stores.matchesId.get())
1715
1726
  const lastStateMatch =
1716
- lastStateMatchId &&
1717
- this.stores.activeMatchStoresById.get(lastStateMatchId)?.state
1727
+ lastStateMatchId && this.stores.matchStores.get(lastStateMatchId)?.get()
1718
1728
  const canReuseParams =
1719
1729
  lastStateMatch &&
1720
1730
  lastStateMatch.routeId === lastRoute.id &&
@@ -1731,12 +1741,7 @@ export class RouterCore<
1731
1741
  )
1732
1742
  for (const route of matchedRoutes) {
1733
1743
  try {
1734
- extractStrictParams(
1735
- route,
1736
- routeParams,
1737
- parsedParams ?? {},
1738
- strictParams,
1739
- )
1744
+ extractStrictParams(route, strictParams)
1740
1745
  } catch {
1741
1746
  // Ignore errors, we're not actually routing
1742
1747
  }
@@ -1763,16 +1768,16 @@ export class RouterCore<
1763
1768
  }
1764
1769
 
1765
1770
  cancelMatches = () => {
1766
- this.stores.pendingMatchesId.state.forEach((matchId) => {
1771
+ this.stores.pendingIds.get().forEach((matchId) => {
1767
1772
  this.cancelMatch(matchId)
1768
1773
  })
1769
1774
 
1770
- this.stores.matchesId.state.forEach((matchId) => {
1771
- if (this.stores.pendingMatchStoresById.has(matchId)) {
1775
+ this.stores.matchesId.get().forEach((matchId) => {
1776
+ if (this.stores.pendingMatchStores.has(matchId)) {
1772
1777
  return
1773
1778
  }
1774
1779
 
1775
- const match = this.stores.activeMatchStoresById.get(matchId)?.state
1780
+ const match = this.stores.matchStores.get(matchId)?.get()
1776
1781
  if (!match) {
1777
1782
  return
1778
1783
  }
@@ -1832,9 +1837,7 @@ export class RouterCore<
1832
1837
  dest.unsafeRelative === 'path'
1833
1838
  ? currentLocation.pathname
1834
1839
  : (dest.from ?? lightweightResult.fullPath)
1835
-
1836
- // ensure this includes the basePath if set
1837
- const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1840
+ const destTo = dest.to ? `${dest.to}` : undefined
1838
1841
 
1839
1842
  // From search should always use the current location
1840
1843
  const fromSearch = lightweightResult.search
@@ -1844,11 +1847,15 @@ export class RouterCore<
1844
1847
  lightweightResult.params,
1845
1848
  )
1846
1849
 
1847
- // Resolve the next to
1848
- // ensure this includes the basePath if set
1849
- const nextTo = dest.to
1850
- ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1851
- : this.resolvePathWithBase(fromPath, '.')
1850
+ const isAbsoluteTo = destTo?.charCodeAt(0) === 47
1851
+ const sourcePath = isAbsoluteTo
1852
+ ? '/'
1853
+ : this.resolvePathWithBase(defaultedFromPath, '.')
1854
+
1855
+ // Resolve the destination. Absolute destinations don't need the source path.
1856
+ const nextTo = destTo
1857
+ ? this.resolvePathWithBase(sourcePath, destTo)
1858
+ : sourcePath
1852
1859
 
1853
1860
  // Resolve the next params
1854
1861
  const nextParams =
@@ -1861,24 +1868,33 @@ export class RouterCore<
1861
1868
  functionalUpdate(dest.params as any, fromParams),
1862
1869
  )
1863
1870
 
1864
- // Use lightweight getMatchedRoutes instead of matchRoutesInternal
1865
- // This avoids creating full match objects (AbortController, ControlledPromise, etc.)
1866
- // which are expensive and not needed for buildLocation
1867
- const destMatchResult = this.getMatchedRoutes(nextTo)
1868
- let destRoutes = destMatchResult.matchedRoutes
1869
-
1870
- // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal
1871
- const isGlobalNotFound =
1872
- !destMatchResult.foundRoute ||
1873
- (destMatchResult.foundRoute.path !== '/' &&
1874
- destMatchResult.routeParams['**'])
1871
+ const destRoute = this.routesByPath[
1872
+ trimPathRight(nextTo) as keyof typeof this.routesByPath
1873
+ ] as AnyRoute | undefined
1874
+
1875
+ let destRoutes: ReadonlyArray<AnyRoute>
1876
+ if (destRoute) {
1877
+ destRoutes = this.getRouteBranch(destRoute)
1878
+ } else if (nextTo.includes('$')) {
1879
+ // Route templates must match routesByPath exactly. A miss here is a
1880
+ // typed destination mismatch, not a concrete URL to route-match.
1881
+ destRoutes = []
1882
+ } else {
1883
+ const destMatchResult = this.getMatchedRoutes(nextTo)
1884
+ destRoutes = destMatchResult.matchedRoutes
1875
1885
 
1876
- if (isGlobalNotFound && this.options.notFoundRoute) {
1877
- destRoutes = [...destRoutes, this.options.notFoundRoute]
1886
+ if (
1887
+ this.options.notFoundRoute &&
1888
+ (!destMatchResult.foundRoute ||
1889
+ (destMatchResult.foundRoute.path !== '/' &&
1890
+ destMatchResult.routeParams['**']))
1891
+ ) {
1892
+ destRoutes = [...destRoutes, this.options.notFoundRoute]
1893
+ }
1878
1894
  }
1879
1895
 
1880
1896
  // If there are any params, we need to stringify them
1881
- if (Object.keys(nextParams).length > 0) {
1897
+ if (destRoutes.length && hasKeys(nextParams)) {
1882
1898
  for (const route of destRoutes) {
1883
1899
  const fn =
1884
1900
  route.options.params?.stringify ?? route.options.stringifyParams
@@ -1897,8 +1913,7 @@ export class RouterCore<
1897
1913
  }
1898
1914
 
1899
1915
  const nextPathname = opts.leaveParams
1900
- ? // Use the original template path for interpolation
1901
- // This preserves the original parameter syntax including optional parameters
1916
+ ? // Keep path params uninterpolated for matchRoute/template matching.
1902
1917
  nextTo
1903
1918
  : decodePath(
1904
1919
  interpolatePath({
@@ -1909,6 +1924,24 @@ export class RouterCore<
1909
1924
  }).interpolatedPath,
1910
1925
  ).path
1911
1926
 
1927
+ if (
1928
+ process.env.NODE_ENV !== 'production' &&
1929
+ destRoute &&
1930
+ !opts.leaveParams
1931
+ ) {
1932
+ try {
1933
+ const roundTrip = this.getMatchedRoutes(nextPathname)
1934
+ if (roundTrip.foundRoute?.id !== destRoute.id) {
1935
+ console.warn(
1936
+ `Generated path "${nextPathname}" for route "${destRoute.id}" did not match the same route after params.stringify.`,
1937
+ )
1938
+ }
1939
+ } catch {
1940
+ // Ignore roundtrip validation errors. The generated location will be
1941
+ // handled by the normal navigation flow.
1942
+ }
1943
+ }
1944
+
1912
1945
  // Resolve the next search
1913
1946
  let nextSearch = fromSearch
1914
1947
  if (opts._includeValidateSearch && this.options.search?.strict) {
@@ -2352,19 +2385,19 @@ export class RouterCore<
2352
2385
  // Match the routes
2353
2386
  const pendingMatches = this.matchRoutes(this.latestLocation)
2354
2387
 
2355
- const nextCachedMatches = this.stores.cachedMatchesSnapshot.state.filter(
2356
- (d) => !pendingMatches.some((e) => e.id === d.id),
2357
- )
2388
+ const nextCachedMatches = this.stores.cachedMatches
2389
+ .get()
2390
+ .filter((d) => !pendingMatches.some((e) => e.id === d.id))
2358
2391
 
2359
2392
  // Ingest the new matches
2360
2393
  this.batch(() => {
2361
- this.stores.status.setState(() => 'pending')
2362
- this.stores.statusCode.setState(() => 200)
2363
- this.stores.isLoading.setState(() => true)
2364
- this.stores.location.setState(() => this.latestLocation)
2365
- this.stores.setPendingMatches(pendingMatches)
2394
+ this.stores.status.set('pending')
2395
+ this.stores.statusCode.set(200)
2396
+ this.stores.isLoading.set(true)
2397
+ this.stores.location.set(this.latestLocation)
2398
+ this.stores.setPending(pendingMatches)
2366
2399
  // If a cached match moved to pending matches, remove it from cached matches
2367
- this.stores.setCachedMatches(nextCachedMatches)
2400
+ this.stores.setCached(nextCachedMatches)
2368
2401
  })
2369
2402
  }
2370
2403
 
@@ -2373,7 +2406,7 @@ export class RouterCore<
2373
2406
  let notFound: NotFoundError | undefined
2374
2407
  let loadPromise: Promise<void>
2375
2408
  const previousLocation =
2376
- this.stores.resolvedLocation.state ?? this.stores.location.state
2409
+ this.stores.resolvedLocation.get() ?? this.stores.location.get()
2377
2410
 
2378
2411
  // eslint-disable-next-line prefer-const
2379
2412
  loadPromise = new Promise<void>((resolve) => {
@@ -2381,10 +2414,10 @@ export class RouterCore<
2381
2414
  try {
2382
2415
  this.beforeLoad()
2383
2416
  const next = this.latestLocation
2384
- const prevLocation = this.stores.resolvedLocation.state
2417
+ const prevLocation = this.stores.resolvedLocation.get()
2385
2418
  const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
2386
2419
 
2387
- if (!this.stores.redirect.state) {
2420
+ if (!this.stores.redirect.get()) {
2388
2421
  this.emit({
2389
2422
  type: 'onBeforeNavigate',
2390
2423
  ...locationChangeInfo,
@@ -2400,7 +2433,7 @@ export class RouterCore<
2400
2433
  router: this,
2401
2434
  sync: opts?.sync,
2402
2435
  forceStaleReload: previousLocation.href === next.href,
2403
- matches: this.stores.pendingMatchesSnapshot.state,
2436
+ matches: this.stores.pendingMatches.get(),
2404
2437
  location: next,
2405
2438
  updateMatch: this.updateMatch,
2406
2439
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -2424,27 +2457,25 @@ export class RouterCore<
2424
2457
  let hookStayingMatches: Array<AnyRouteMatch> | null = null
2425
2458
 
2426
2459
  this.batch(() => {
2427
- const pendingMatches =
2428
- this.stores.pendingMatchesSnapshot.state
2460
+ const pendingMatches = this.stores.pendingMatches.get()
2429
2461
  const mountPending = pendingMatches.length
2430
- const currentMatches =
2431
- this.stores.activeMatchesSnapshot.state
2462
+ const currentMatches = this.stores.matches.get()
2432
2463
 
2433
2464
  exitingMatches = mountPending
2434
2465
  ? currentMatches.filter(
2435
2466
  (match) =>
2436
- !this.stores.pendingMatchStoresById.has(match.id),
2467
+ !this.stores.pendingMatchStores.has(match.id),
2437
2468
  )
2438
2469
  : null
2439
2470
 
2440
2471
  // Lifecycle-hook identity: routeId only (route presence in tree)
2441
2472
  // Build routeId sets from pools to avoid derived stores.
2442
2473
  const pendingRouteIds = new Set<string>()
2443
- for (const s of this.stores.pendingMatchStoresById.values()) {
2474
+ for (const s of this.stores.pendingMatchStores.values()) {
2444
2475
  if (s.routeId) pendingRouteIds.add(s.routeId)
2445
2476
  }
2446
2477
  const activeRouteIds = new Set<string>()
2447
- for (const s of this.stores.activeMatchStoresById.values()) {
2478
+ for (const s of this.stores.matchStores.values()) {
2448
2479
  if (s.routeId) activeRouteIds.add(s.routeId)
2449
2480
  }
2450
2481
 
@@ -2464,8 +2495,8 @@ export class RouterCore<
2464
2495
  )
2465
2496
  : currentMatches
2466
2497
 
2467
- this.stores.isLoading.setState(() => false)
2468
- this.stores.loadedAt.setState(() => Date.now())
2498
+ this.stores.isLoading.set(false)
2499
+ this.stores.loadedAt.set(Date.now())
2469
2500
  /**
2470
2501
  * When committing new matches, cache any exiting matches that are still usable.
2471
2502
  * Routes that resolved with `status: 'error'` or `status: 'notFound'` are
@@ -2473,10 +2504,10 @@ export class RouterCore<
2473
2504
  * or reloads re-run their loaders instead of reusing the failed/not-found data.
2474
2505
  */
2475
2506
  if (mountPending) {
2476
- this.stores.setActiveMatches(pendingMatches)
2477
- this.stores.setPendingMatches([])
2478
- this.stores.setCachedMatches([
2479
- ...this.stores.cachedMatchesSnapshot.state,
2507
+ this.stores.setMatches(pendingMatches)
2508
+ this.stores.setPending([])
2509
+ this.stores.setCached([
2510
+ ...this.stores.cachedMatches.get(),
2480
2511
  ...exitingMatches!.filter(
2481
2512
  (d) =>
2482
2513
  d.status !== 'error' &&
@@ -2523,15 +2554,13 @@ export class RouterCore<
2523
2554
  ? redirect.status
2524
2555
  : notFound
2525
2556
  ? 404
2526
- : this.stores.activeMatchesSnapshot.state.some(
2527
- (d) => d.status === 'error',
2528
- )
2557
+ : this.stores.matches.get().some((d) => d.status === 'error')
2529
2558
  ? 500
2530
2559
  : 200
2531
2560
 
2532
2561
  this.batch(() => {
2533
- this.stores.statusCode.setState(() => nextStatusCode)
2534
- this.stores.redirect.setState(() => redirect)
2562
+ this.stores.statusCode.set(nextStatusCode)
2563
+ this.stores.redirect.set(redirect)
2535
2564
  })
2536
2565
  }
2537
2566
 
@@ -2559,13 +2588,11 @@ export class RouterCore<
2559
2588
  let newStatusCode: number | undefined = undefined
2560
2589
  if (this.hasNotFoundMatch()) {
2561
2590
  newStatusCode = 404
2562
- } else if (
2563
- this.stores.activeMatchesSnapshot.state.some((d) => d.status === 'error')
2564
- ) {
2591
+ } else if (this.stores.matches.get().some((d) => d.status === 'error')) {
2565
2592
  newStatusCode = 500
2566
2593
  }
2567
2594
  if (newStatusCode !== undefined) {
2568
- this.stores.statusCode.setState(() => newStatusCode)
2595
+ this.stores.statusCode.set(newStatusCode)
2569
2596
  }
2570
2597
  }
2571
2598
 
@@ -2594,7 +2621,7 @@ export class RouterCore<
2594
2621
  this.isViewTransitionTypesSupported
2595
2622
  ) {
2596
2623
  const next = this.latestLocation
2597
- const prevLocation = this.stores.resolvedLocation.state
2624
+ const prevLocation = this.stores.resolvedLocation.get()
2598
2625
 
2599
2626
  const resolvedViewTransitionTypes =
2600
2627
  typeof shouldViewTransition.types === 'function'
@@ -2624,30 +2651,30 @@ export class RouterCore<
2624
2651
 
2625
2652
  updateMatch: UpdateMatchFn = (id, updater) => {
2626
2653
  this.startTransition(() => {
2627
- const pendingMatch = this.stores.pendingMatchStoresById.get(id)
2654
+ const pendingMatch = this.stores.pendingMatchStores.get(id)
2628
2655
  if (pendingMatch) {
2629
- pendingMatch.setState(updater)
2656
+ pendingMatch.set(updater)
2630
2657
  return
2631
2658
  }
2632
2659
 
2633
- const activeMatch = this.stores.activeMatchStoresById.get(id)
2660
+ const activeMatch = this.stores.matchStores.get(id)
2634
2661
  if (activeMatch) {
2635
- activeMatch.setState(updater)
2662
+ activeMatch.set(updater)
2636
2663
  return
2637
2664
  }
2638
2665
 
2639
- const cachedMatch = this.stores.cachedMatchStoresById.get(id)
2666
+ const cachedMatch = this.stores.cachedMatchStores.get(id)
2640
2667
  if (cachedMatch) {
2641
- const next = updater(cachedMatch.state)
2668
+ const next = updater(cachedMatch.get())
2642
2669
  if (next.status === 'redirected') {
2643
- const deleted = this.stores.cachedMatchStoresById.delete(id)
2670
+ const deleted = this.stores.cachedMatchStores.delete(id)
2644
2671
  if (deleted) {
2645
- this.stores.cachedMatchesId.setState((prev) =>
2672
+ this.stores.cachedIds.set((prev) =>
2646
2673
  prev.filter((matchId) => matchId !== id),
2647
2674
  )
2648
2675
  }
2649
2676
  } else {
2650
- cachedMatch.setState(() => next)
2677
+ cachedMatch.set(next)
2651
2678
  }
2652
2679
  }
2653
2680
  })
@@ -2655,9 +2682,9 @@ export class RouterCore<
2655
2682
 
2656
2683
  getMatch: GetMatchFn = (matchId: string): AnyRouteMatch | undefined => {
2657
2684
  return (
2658
- this.stores.cachedMatchStoresById.get(matchId)?.state ??
2659
- this.stores.pendingMatchStoresById.get(matchId)?.state ??
2660
- this.stores.activeMatchStoresById.get(matchId)?.state
2685
+ this.stores.cachedMatchStores.get(matchId)?.get() ??
2686
+ this.stores.pendingMatchStores.get(matchId)?.get() ??
2687
+ this.stores.matchStores.get(matchId)?.get()
2661
2688
  )
2662
2689
  }
2663
2690
 
@@ -2694,15 +2721,9 @@ export class RouterCore<
2694
2721
  }
2695
2722
 
2696
2723
  this.batch(() => {
2697
- this.stores.setActiveMatches(
2698
- this.stores.activeMatchesSnapshot.state.map(invalidate),
2699
- )
2700
- this.stores.setCachedMatches(
2701
- this.stores.cachedMatchesSnapshot.state.map(invalidate),
2702
- )
2703
- this.stores.setPendingMatches(
2704
- this.stores.pendingMatchesSnapshot.state.map(invalidate),
2705
- )
2724
+ this.stores.setMatches(this.stores.matches.get().map(invalidate))
2725
+ this.stores.setCached(this.stores.cachedMatches.get().map(invalidate))
2726
+ this.stores.setPending(this.stores.pendingMatches.get().map(invalidate))
2706
2727
  })
2707
2728
 
2708
2729
  this.shouldViewTransition = false
@@ -2760,13 +2781,13 @@ export class RouterCore<
2760
2781
  clearCache: ClearCacheFn<this> = (opts) => {
2761
2782
  const filter = opts?.filter
2762
2783
  if (filter !== undefined) {
2763
- this.stores.setCachedMatches(
2764
- this.stores.cachedMatchesSnapshot.state.filter(
2765
- (m) => !filter(m as MakeRouteMatchUnion<this>),
2766
- ),
2784
+ this.stores.setCached(
2785
+ this.stores.cachedMatches
2786
+ .get()
2787
+ .filter((m) => !filter(m as MakeRouteMatchUnion<this>)),
2767
2788
  )
2768
2789
  } else {
2769
- this.stores.setCachedMatches([])
2790
+ this.stores.setCached([])
2770
2791
  }
2771
2792
  }
2772
2793
 
@@ -2814,13 +2835,13 @@ export class RouterCore<
2814
2835
  })
2815
2836
 
2816
2837
  const activeMatchIds = new Set([
2817
- ...this.stores.matchesId.state,
2818
- ...this.stores.pendingMatchesId.state,
2838
+ ...this.stores.matchesId.get(),
2839
+ ...this.stores.pendingIds.get(),
2819
2840
  ])
2820
2841
 
2821
2842
  const loadedMatchIds = new Set([
2822
2843
  ...activeMatchIds,
2823
- ...this.stores.cachedMatchesId.state,
2844
+ ...this.stores.cachedIds.get(),
2824
2845
  ])
2825
2846
 
2826
2847
  // If the matches are already loaded, we need to add them to the cached matches.
@@ -2828,8 +2849,8 @@ export class RouterCore<
2828
2849
  (match) => !loadedMatchIds.has(match.id),
2829
2850
  )
2830
2851
  if (matchesToCache.length) {
2831
- const cachedMatches = this.stores.cachedMatchesSnapshot.state
2832
- this.stores.setCachedMatches([...cachedMatches, ...matchesToCache])
2852
+ const cachedMatches = this.stores.cachedMatches.get()
2853
+ this.stores.setCached([...cachedMatches, ...matchesToCache])
2833
2854
  }
2834
2855
 
2835
2856
  try {
@@ -2884,16 +2905,16 @@ export class RouterCore<
2884
2905
  }
2885
2906
  const next = this.buildLocation(matchLocation as any)
2886
2907
 
2887
- if (opts?.pending && this.stores.status.state !== 'pending') {
2908
+ if (opts?.pending && this.stores.status.get() !== 'pending') {
2888
2909
  return false
2889
2910
  }
2890
2911
 
2891
2912
  const pending =
2892
- opts?.pending === undefined ? !this.stores.isLoading.state : opts.pending
2913
+ opts?.pending === undefined ? !this.stores.isLoading.get() : opts.pending
2893
2914
 
2894
2915
  const baseLocation = pending
2895
2916
  ? this.latestLocation
2896
- : this.stores.resolvedLocation.state || this.stores.location.state
2917
+ : this.stores.resolvedLocation.get() || this.stores.location.get()
2897
2918
 
2898
2919
  const match = findSingleMatch(
2899
2920
  next.pathname,
@@ -2929,9 +2950,9 @@ export class RouterCore<
2929
2950
  serverSsr?: ServerSsr
2930
2951
 
2931
2952
  hasNotFoundMatch = () => {
2932
- return this.stores.activeMatchesSnapshot.state.some(
2933
- (d) => d.status === 'notFound' || d.globalNotFound,
2934
- )
2953
+ return this.stores.matches
2954
+ .get()
2955
+ .some((d) => d.status === 'notFound' || d.globalNotFound)
2935
2956
  }
2936
2957
  }
2937
2958
 
@@ -3024,17 +3045,15 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3024
3045
  const trimmedPath = trimPathRight(pathname)
3025
3046
 
3026
3047
  let foundRoute: TRouteLike | undefined = undefined
3027
- let parsedParams: Record<string, unknown> | undefined = undefined
3028
3048
  const match = findRouteMatch<TRouteLike>(trimmedPath, processedTree, true)
3029
3049
  if (match) {
3030
3050
  foundRoute = match.route
3031
3051
  Object.assign(routeParams, match.rawParams) // Copy params, because they're cached
3032
- parsedParams = Object.assign(Object.create(null), match.parsedParams)
3033
3052
  }
3034
3053
 
3035
3054
  const matchedRoutes = match?.branch || [routesById[rootRouteId]!]
3036
3055
 
3037
- return { matchedRoutes, routeParams, foundRoute, parsedParams }
3056
+ return { matchedRoutes, routeParams, foundRoute }
3038
3057
  }
3039
3058
 
3040
3059
  /**
@@ -3186,22 +3205,14 @@ function findGlobalNotFoundRouteId(
3186
3205
 
3187
3206
  function extractStrictParams(
3188
3207
  route: AnyRoute,
3189
- referenceParams: Record<string, unknown>,
3190
- parsedParams: Record<string, unknown>,
3191
3208
  accumulatedParams: Record<string, unknown>,
3192
3209
  ) {
3193
3210
  const parseParams = route.options.params?.parse ?? route.options.parseParams
3194
3211
  if (parseParams) {
3195
- if (route.options.skipRouteOnParseError) {
3196
- // Use pre-parsed params from route matching for skipRouteOnParseError routes
3197
- for (const key in referenceParams) {
3198
- if (key in parsedParams) {
3199
- accumulatedParams[key] = parsedParams[key]
3200
- }
3201
- }
3202
- } else {
3203
- const result = parseParams(accumulatedParams as Record<string, string>)
3204
- Object.assign(accumulatedParams, result)
3212
+ const result = parseParams(accumulatedParams as Record<string, string>)
3213
+ if (result === false) {
3214
+ throw new Error('Route params.parse returned false for a matched route')
3205
3215
  }
3216
+ Object.assign(accumulatedParams, result)
3206
3217
  }
3207
3218
  }
@@ -233,7 +233,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
233
233
  window.addEventListener('pagehide', () => {
234
234
  snapshotCurrentScrollTargets(
235
235
  getKey(
236
- router.stores.resolvedLocation.state ?? router.stores.location.state,
236
+ router.stores.resolvedLocation.get() ?? router.stores.location.get(),
237
237
  ),
238
238
  )
239
239
  cache.persist()
@@ -78,13 +78,12 @@ export function createRequestHandler<TRouter extends AnyRouter>({
78
78
  }
79
79
 
80
80
  function getRequestHeaders(opts: { router: AnyRouter }): Headers {
81
- const matchHeaders =
82
- opts.router.stores.activeMatchesSnapshot.state.map<AnyHeaders>(
83
- (match) => match.headers,
84
- )
81
+ const matchHeaders = opts.router.stores.matches
82
+ .get()
83
+ .map<AnyHeaders>((match) => match.headers)
85
84
 
86
85
  // Handle Redirects
87
- const redirect = opts.router.stores.redirect.state
86
+ const redirect = opts.router.stores.redirect.get()
88
87
  if (redirect) {
89
88
  matchHeaders.push(redirect.headers)
90
89
  }