@tanstack/router-core 1.154.13 → 1.155.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/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,
@@ -56,7 +57,7 @@ import type {
56
57
  PickAsRequired,
57
58
  Updater,
58
59
  } from './utils'
59
- import type { MatchSnapshot, ParsedLocation } from './location'
60
+ import type { ParsedLocation } from './location'
60
61
  import type {
61
62
  AnyContext,
62
63
  AnyRoute,
@@ -590,14 +591,6 @@ export interface MatchRoutesOpts {
590
591
  throwOnError?: boolean
591
592
  _buildLocation?: boolean
592
593
  dest?: BuildNextOptions
593
- /** Optional match snapshot hint for fast-path (skips path matching) */
594
- snapshot?: MatchSnapshot
595
- }
596
-
597
- export interface MatchRoutesResult {
598
- matches: Array<AnyRouteMatch>
599
- /** Raw string params extracted from path (before parsing) */
600
- rawParams: Record<string, string>
601
594
  }
602
595
 
603
596
  export type InferRouterContext<TRouteTree extends AnyRoute> =
@@ -710,17 +703,14 @@ export type GetMatchRoutesFn = (pathname: string) => {
710
703
  /** exhaustive params, still in their string form */
711
704
  routeParams: Record<string, string>
712
705
  /** partial params, parsed from routeParams during matching */
713
- parsedParams: Record<string, unknown>
706
+ parsedParams: Record<string, unknown> | undefined
714
707
  foundRoute: AnyRoute | undefined
715
708
  parseError?: unknown
716
709
  }
717
710
 
718
711
  export type EmitFn = (routerEvent: RouterEvent) => void
719
712
 
720
- export type LoadFn = (opts?: {
721
- sync?: boolean
722
- _skipUpdateLatestLocation?: boolean
723
- }) => Promise<void>
713
+ export type LoadFn = (opts?: { sync?: boolean }) => Promise<void>
724
714
 
725
715
  export type CommitLocationFn = ({
726
716
  viewTransition,
@@ -903,6 +893,7 @@ export class RouterCore<
903
893
  tempLocationKey: string | undefined = `${Math.round(
904
894
  Math.random() * 10000000,
905
895
  )}`
896
+ resetNextScroll = true
906
897
  shouldViewTransition?: boolean | ViewTransitionOptions = undefined
907
898
  isViewTransitionTypesSupported?: boolean = undefined
908
899
  subscribers = new Set<RouterListener<RouterEvent>>()
@@ -927,15 +918,13 @@ export class RouterCore<
927
918
  origin?: string
928
919
  latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
929
920
  pendingBuiltLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
930
- /** Session id for cached history snapshots */
931
- private sessionId!: string
932
921
  basepath!: string
933
922
  routeTree!: TRouteTree
934
923
  routesById!: RoutesById<TRouteTree>
935
924
  routesByPath!: RoutesByPath<TRouteTree>
936
925
  processedTree!: ProcessedTree<TRouteTree, any, any>
937
926
  isServer!: boolean
938
- pathParamsDecodeCharMap?: Map<string, string>
927
+ pathParamsDecoder?: (encoded: string) => string
939
928
 
940
929
  /**
941
930
  * @deprecated Use the `createRouter` function instead
@@ -949,11 +938,6 @@ export class RouterCore<
949
938
  TDehydrated
950
939
  >,
951
940
  ) {
952
- this.sessionId =
953
- typeof crypto !== 'undefined' && 'randomUUID' in crypto
954
- ? crypto.randomUUID()
955
- : `${Date.now()}-${Math.random().toString(36).slice(2)}`
956
-
957
941
  this.update({
958
942
  defaultPreloadDelay: 50,
959
943
  defaultPendingMs: 1000,
@@ -1009,14 +993,10 @@ export class RouterCore<
1009
993
 
1010
994
  this.isServer = this.options.isServer ?? typeof document === 'undefined'
1011
995
 
1012
- this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
1013
- ? new Map(
1014
- this.options.pathParamsAllowedCharacters.map((char) => [
1015
- encodeURIComponent(char),
1016
- char,
1017
- ]),
1018
- )
1019
- : undefined
996
+ if (this.options.pathParamsAllowedCharacters)
997
+ this.pathParamsDecoder = compileDecodeCharMap(
998
+ this.options.pathParamsAllowedCharacters,
999
+ )
1020
1000
 
1021
1001
  if (
1022
1002
  !this.history ||
@@ -1271,70 +1251,42 @@ export class RouterCore<
1271
1251
  search: locationSearchOrOpts,
1272
1252
  } as ParsedLocation,
1273
1253
  opts,
1274
- ).matches
1254
+ )
1275
1255
  }
1276
1256
 
1277
1257
  return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1278
- .matches
1279
1258
  }
1280
1259
 
1281
1260
  private matchRoutesInternal(
1282
1261
  next: ParsedLocation,
1283
1262
  opts?: MatchRoutesOpts,
1284
- ): MatchRoutesResult {
1285
- // Fast-path: use snapshot hint if valid
1286
- const snapshot = opts?.snapshot
1287
- const snapshotValid =
1288
- snapshot &&
1289
- snapshot.routeIds.length > 0 &&
1290
- snapshot.routeIds.every((id) => this.routesById[id])
1291
-
1292
- let matchedRoutes: ReadonlyArray<AnyRoute>
1293
- let routeParams: Record<string, string>
1294
- let rawParams: Record<string, string>
1295
- let globalNotFoundRouteId: string | undefined
1296
- let parsedParams: Record<string, unknown>
1297
-
1298
- if (snapshotValid) {
1299
- // Rebuild matched routes from snapshot
1300
- matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
1301
- routeParams = { ...snapshot.params }
1302
- rawParams = { ...snapshot.params }
1303
- globalNotFoundRouteId = snapshot.globalNotFoundRouteId
1304
- parsedParams = snapshot.parsedParams
1305
- } else {
1306
- // Normal path matching
1307
- const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
1308
- const { foundRoute, routeParams: rp } = matchedRoutesResult
1309
- routeParams = rp
1310
- rawParams = { ...rp } // Capture before routeParams gets modified
1311
- matchedRoutes = matchedRoutesResult.matchedRoutes
1312
- parsedParams = matchedRoutesResult.parsedParams
1263
+ ): Array<AnyRouteMatch> {
1264
+ const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
1265
+ const { foundRoute, routeParams, parsedParams } = matchedRoutesResult
1266
+ let { matchedRoutes } = matchedRoutesResult
1267
+ let isGlobalNotFound = false
1313
1268
 
1314
- let isGlobalNotFound = false
1315
-
1316
- // Check to see if the route needs a 404 entry
1317
- if (
1318
- // If we found a route, and it's not an index route and we have left over path
1319
- foundRoute
1320
- ? foundRoute.path !== '/' && routeParams['**']
1321
- : // Or if we didn't find a route and we have left over path
1322
- trimPathRight(next.pathname)
1323
- ) {
1324
- // If the user has defined an (old) 404 route, use it
1325
- if (this.options.notFoundRoute) {
1326
- matchedRoutes = [...matchedRoutes, this.options.notFoundRoute]
1327
- } else {
1328
- // If there is no routes found during path matching
1329
- isGlobalNotFound = true
1330
- }
1269
+ // Check to see if the route needs a 404 entry
1270
+ if (
1271
+ // If we found a route, and it's not an index route and we have left over path
1272
+ foundRoute
1273
+ ? foundRoute.path !== '/' && routeParams['**']
1274
+ : // Or if we didn't find a route and we have left over path
1275
+ trimPathRight(next.pathname)
1276
+ ) {
1277
+ // If the user has defined an (old) 404 route, use it
1278
+ if (this.options.notFoundRoute) {
1279
+ matchedRoutes = [...matchedRoutes, this.options.notFoundRoute]
1280
+ } else {
1281
+ // If there is no routes found during path matching
1282
+ isGlobalNotFound = true
1331
1283
  }
1332
-
1333
- globalNotFoundRouteId = isGlobalNotFound
1334
- ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes)
1335
- : undefined
1336
1284
  }
1337
1285
 
1286
+ const globalNotFoundRouteId = isGlobalNotFound
1287
+ ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes)
1288
+ : undefined
1289
+
1338
1290
  const matches: Array<AnyRouteMatch> = []
1339
1291
 
1340
1292
  const getParentContext = (parentMatch?: AnyRouteMatch) => {
@@ -1347,19 +1299,6 @@ export class RouterCore<
1347
1299
  return parentContext
1348
1300
  }
1349
1301
 
1350
- // Check if we can use cached validated searches from snapshot
1351
- // Valid if: snapshot exists, searchStr matches, and validatedSearches has correct length
1352
- const canUseCachedSearch =
1353
- snapshotValid &&
1354
- snapshot.searchStr === next.searchStr &&
1355
- snapshot.validatedSearches?.length === matchedRoutes.length
1356
-
1357
- // Collect validated searches to cache in snapshot (only when not using cache)
1358
- const validatedSearchesToCache: Array<{
1359
- search: Record<string, unknown>
1360
- strictSearch: Record<string, unknown>
1361
- }> = []
1362
-
1363
1302
  matchedRoutes.forEach((route, index) => {
1364
1303
  // Take each matched route and resolve + validate its search params
1365
1304
  // This has to happen serially because each route's search params
@@ -1375,12 +1314,6 @@ export class RouterCore<
1375
1314
  Record<string, any>,
1376
1315
  any,
1377
1316
  ] = (() => {
1378
- // Fast-path: use cached validated search from snapshot
1379
- if (canUseCachedSearch) {
1380
- const cached = snapshot.validatedSearches![index]!
1381
- return [cached.search, cached.strictSearch, undefined]
1382
- }
1383
-
1384
1317
  // Validate the search params and stabilize them
1385
1318
  const parentSearch = parentMatch?.search ?? next.search
1386
1319
  const parentStrictSearch = parentMatch?._strictSearch ?? undefined
@@ -1414,14 +1347,6 @@ export class RouterCore<
1414
1347
  }
1415
1348
  })()
1416
1349
 
1417
- // Cache the validated search for future pop navigations
1418
- if (!canUseCachedSearch) {
1419
- validatedSearchesToCache.push({
1420
- search: preMatchSearch,
1421
- strictSearch: strictMatchSearch,
1422
- })
1423
- }
1424
-
1425
1350
  // This is where we need to call route.options.loaderDeps() to get any additional
1426
1351
  // deps that the route's loader function might need to run. We need to do this
1427
1352
  // before we create the match so that we can pass the deps to the route's
@@ -1437,7 +1362,7 @@ export class RouterCore<
1437
1362
  const { interpolatedPath, usedParams } = interpolatePath({
1438
1363
  path: route.fullPath,
1439
1364
  params: routeParams,
1440
- decodeCharMap: this.pathParamsDecodeCharMap,
1365
+ decoder: this.pathParamsDecoder,
1441
1366
  })
1442
1367
 
1443
1368
  // Waste not, want not. If we already have a match for this route,
@@ -1588,18 +1513,6 @@ export class RouterCore<
1588
1513
  matches.push(match)
1589
1514
  })
1590
1515
 
1591
- // Cache validated searches in snapshot for future pop navigations
1592
- // Only update if we computed fresh values (not using cached)
1593
- if (!canUseCachedSearch && validatedSearchesToCache.length > 0) {
1594
- const existingSnapshot = next.state?.__TSR_matches as
1595
- | MatchSnapshot
1596
- | undefined
1597
- if (existingSnapshot) {
1598
- existingSnapshot.searchStr = next.searchStr
1599
- existingSnapshot.validatedSearches = validatedSearchesToCache
1600
- }
1601
- }
1602
-
1603
1516
  matches.forEach((match, index) => {
1604
1517
  const route = this.looseRoutesById[match.routeId]!
1605
1518
  const existingMatch = this.getMatch(match.id)
@@ -1639,7 +1552,7 @@ export class RouterCore<
1639
1552
  }
1640
1553
  })
1641
1554
 
1642
- return { matches, rawParams }
1555
+ return matches
1643
1556
  }
1644
1557
 
1645
1558
  getMatchedRoutes: GetMatchRoutesFn = (pathname) => {
@@ -1767,7 +1680,6 @@ export class RouterCore<
1767
1680
  // which are expensive and not needed for buildLocation
1768
1681
  const destMatchResult = this.getMatchedRoutes(interpolatedNextTo)
1769
1682
  let destRoutes = destMatchResult.matchedRoutes
1770
- const rawParams = destMatchResult.routeParams
1771
1683
 
1772
1684
  // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal
1773
1685
  const isGlobalNotFound = destMatchResult.foundRoute
@@ -1806,7 +1718,7 @@ export class RouterCore<
1806
1718
  interpolatePath({
1807
1719
  path: nextTo,
1808
1720
  params: nextParams,
1809
- decodeCharMap: this.pathParamsDecodeCharMap,
1721
+ decoder: this.pathParamsDecoder,
1810
1722
  }).interpolatedPath,
1811
1723
  )
1812
1724
 
@@ -1867,20 +1779,6 @@ export class RouterCore<
1867
1779
  // Replace the equal deep
1868
1780
  nextState = replaceEqualDeep(currentLocation.state, nextState)
1869
1781
 
1870
- // Build match snapshot for fast-path on back/forward navigation
1871
- // Use raw params captured during matchRoutesInternal (needed for literal path navigation
1872
- // where nextParams may be empty but path contains param values)
1873
- const snapshotParams = {
1874
- ...rawParams,
1875
- ...nextParams,
1876
- }
1877
- const matchSnapshot = buildMatchSnapshotFromRoutes({
1878
- routes: destRoutes,
1879
- params: snapshotParams,
1880
- searchStr,
1881
- globalNotFoundRouteId,
1882
- })
1883
-
1884
1782
  // Create the full path of the location
1885
1783
  const fullPath = `${nextPathname}${searchStr}${hashStr}`
1886
1784
 
@@ -1890,13 +1788,10 @@ export class RouterCore<
1890
1788
  // If a rewrite function is provided, use it to rewrite the URL
1891
1789
  const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1892
1790
 
1893
- // Use encoded URL path for href (consistent with parseLocation)
1894
- const encodedHref = url.href.replace(url.origin, '')
1895
-
1896
1791
  return {
1897
1792
  publicHref:
1898
1793
  rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
1899
- href: encodedHref,
1794
+ href: fullPath,
1900
1795
  url: rewrittenUrl,
1901
1796
  pathname: nextPathname,
1902
1797
  search: nextSearch,
@@ -1904,7 +1799,6 @@ export class RouterCore<
1904
1799
  state: nextState as any,
1905
1800
  hash: hash ?? '',
1906
1801
  unmaskOnReload: dest.unmaskOnReload,
1907
- _matchSnapshot: matchSnapshot,
1908
1802
  }
1909
1803
  }
1910
1804
 
@@ -2010,105 +1904,65 @@ export class RouterCore<
2010
1904
  // Don't commit to history if nothing changed
2011
1905
  if (isSameUrl && isSameState()) {
2012
1906
  this.load()
2013
- return this.commitLocationPromise
2014
- }
2015
-
2016
- let {
2017
- // eslint-disable-next-line prefer-const
2018
- maskedLocation,
2019
- // eslint-disable-next-line prefer-const
2020
- hashScrollIntoView,
2021
- // don't pass url into history since it is a URL instance that cannot be serialized
2022
- // eslint-disable-next-line prefer-const
2023
- url: _url,
2024
- ...nextHistory
2025
- } = next
2026
-
2027
- if (maskedLocation) {
2028
- nextHistory = {
2029
- ...maskedLocation,
2030
- state: {
2031
- ...maskedLocation.state,
2032
- __tempKey: undefined,
2033
- __tempLocation: {
2034
- ...nextHistory,
2035
- search: nextHistory.searchStr,
2036
- state: {
2037
- ...nextHistory.state,
2038
- __tempKey: undefined!,
2039
- __tempLocation: undefined!,
2040
- __TSR_key: undefined!,
2041
- key: undefined!, // TODO: Remove in v2 - use __TSR_key instead
1907
+ } else {
1908
+ let {
1909
+ // eslint-disable-next-line prefer-const
1910
+ maskedLocation,
1911
+ // eslint-disable-next-line prefer-const
1912
+ hashScrollIntoView,
1913
+ // don't pass url into history since it is a URL instance that cannot be serialized
1914
+ // eslint-disable-next-line prefer-const
1915
+ url: _url,
1916
+ ...nextHistory
1917
+ } = next
1918
+
1919
+ if (maskedLocation) {
1920
+ nextHistory = {
1921
+ ...maskedLocation,
1922
+ state: {
1923
+ ...maskedLocation.state,
1924
+ __tempKey: undefined,
1925
+ __tempLocation: {
1926
+ ...nextHistory,
1927
+ search: nextHistory.searchStr,
1928
+ state: {
1929
+ ...nextHistory.state,
1930
+ __tempKey: undefined!,
1931
+ __tempLocation: undefined!,
1932
+ __TSR_key: undefined!,
1933
+ key: undefined!, // TODO: Remove in v2 - use __TSR_key instead
1934
+ },
2042
1935
  },
2043
1936
  },
2044
- },
2045
- }
1937
+ }
2046
1938
 
2047
- if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
2048
- nextHistory.state.__tempKey = this.tempLocationKey
1939
+ if (
1940
+ nextHistory.unmaskOnReload ??
1941
+ this.options.unmaskOnReload ??
1942
+ false
1943
+ ) {
1944
+ nextHistory.state.__tempKey = this.tempLocationKey
1945
+ }
2049
1946
  }
2050
- }
2051
1947
 
2052
- nextHistory.state.__hashScrollIntoViewOptions =
2053
- hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
1948
+ nextHistory.state.__hashScrollIntoViewOptions =
1949
+ hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
2054
1950
 
2055
- // Store resetScroll in history state so it survives back/forward navigation
2056
- nextHistory.state.__TSR_resetScroll = next.resetScroll ?? true
1951
+ this.shouldViewTransition = viewTransition
2057
1952
 
2058
- this.shouldViewTransition = viewTransition
2059
-
2060
- // Store session id for this router lifetime
2061
- nextHistory.state.__TSR_sessionId = this.sessionId
2062
-
2063
- // Use match snapshot from buildLocation if available, otherwise compute it.
2064
- // Stored in history state for pop/back/forward fast-path.
2065
- nextHistory.state.__TSR_matches =
2066
- next._matchSnapshot ??
2067
- buildMatchSnapshot({
2068
- matchResult: this.getMatchedRoutes(next.pathname),
2069
- pathname: next.pathname,
2070
- searchStr: next.searchStr,
2071
- notFoundRoute: this.options.notFoundRoute,
2072
- notFoundMode: this.options.notFoundMode,
2073
- })
2074
-
2075
- // Build the pre-computed ParsedLocation to avoid re-parsing after push
2076
- // Spread next (which has href, pathname, search, etc.) and override with final state
2077
- const precomputedLocation: ParsedLocation = {
2078
- ...next,
2079
- publicHref: nextHistory.publicHref,
2080
- state: nextHistory.state,
2081
- maskedLocation,
1953
+ this.history[next.replace ? 'replace' : 'push'](
1954
+ nextHistory.publicHref,
1955
+ nextHistory.state,
1956
+ { ignoreBlocker },
1957
+ )
2082
1958
  }
2083
1959
 
2084
- // Await push/replace to handle blockers before proceeding
2085
- // Pass skipTransitionerLoad so Transitioner doesn't call load() - we handle it below
2086
- const result = await this.history[next.replace ? 'replace' : 'push'](
2087
- nextHistory.publicHref,
2088
- nextHistory.state,
2089
- { ignoreBlocker, skipTransitionerLoad: true },
2090
- )
1960
+ this.resetNextScroll = next.resetScroll ?? true
2091
1961
 
2092
- // If blocked, resolve promise and return
2093
- if (result.type === 'BLOCKED') {
2094
- this.commitLocationPromise?.resolve()
2095
- return this.commitLocationPromise
2096
- }
2097
-
2098
- // Check if another navigation has superseded this one while we awaited
2099
- // If so, let the newer navigation handle things - don't overwrite latestLocation
2100
- if (this.history.location.href !== nextHistory.publicHref) {
2101
- return this.commitLocationPromise
1962
+ if (!this.history.subscribers.size) {
1963
+ this.load()
2102
1964
  }
2103
1965
 
2104
- // Success: set latestLocation directly (we skip updateLatestLocation in load)
2105
- this.latestLocation = precomputedLocation as unknown as ParsedLocation<
2106
- FullSearchSchema<TRouteTree>
2107
- >
2108
-
2109
- // Call load() with _skipUpdateLatestLocation since we already set latestLocation
2110
- this.load({ _skipUpdateLatestLocation: true })
2111
-
2112
1966
  return this.commitLocationPromise
2113
1967
  }
2114
1968
 
@@ -2260,14 +2114,10 @@ export class RouterCore<
2260
2114
 
2261
2115
  latestLoadPromise: undefined | Promise<void>
2262
2116
 
2263
- beforeLoad = (opts?: { _skipUpdateLatestLocation?: boolean }) => {
2117
+ beforeLoad = () => {
2264
2118
  // Cancel any pending matches
2265
2119
  this.cancelMatches()
2266
- if (!opts?._skipUpdateLatestLocation) {
2267
- this.updateLatestLocation()
2268
- } else {
2269
- // Already have latestLocation from commitLocation, skip parsing
2270
- }
2120
+ this.updateLatestLocation()
2271
2121
 
2272
2122
  if (this.isServer) {
2273
2123
  // for SPAs on the initial load, this is handled by the Transitioner
@@ -2291,12 +2141,7 @@ export class RouterCore<
2291
2141
  }
2292
2142
 
2293
2143
  // Match the routes
2294
- // Use snapshot from history state for fast-path only within same router lifetime
2295
- const snapshot =
2296
- this.latestLocation.state.__TSR_sessionId === this.sessionId
2297
- ? this.latestLocation.state.__TSR_matches
2298
- : undefined
2299
- const pendingMatches = this.matchRoutes(this.latestLocation, { snapshot })
2144
+ const pendingMatches = this.matchRoutes(this.latestLocation)
2300
2145
 
2301
2146
  // Ingest the new matches
2302
2147
  this.__store.setState((s) => ({
@@ -2313,10 +2158,7 @@ export class RouterCore<
2313
2158
  }))
2314
2159
  }
2315
2160
 
2316
- load: LoadFn = async (opts?: {
2317
- sync?: boolean
2318
- _skipUpdateLatestLocation?: boolean
2319
- }): Promise<void> => {
2161
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
2320
2162
  let redirect: AnyRedirect | undefined
2321
2163
  let notFound: NotFoundError | undefined
2322
2164
  let loadPromise: Promise<void>
@@ -2325,9 +2167,7 @@ export class RouterCore<
2325
2167
  loadPromise = new Promise<void>((resolve) => {
2326
2168
  this.startTransition(async () => {
2327
2169
  try {
2328
- this.beforeLoad({
2329
- _skipUpdateLatestLocation: opts?._skipUpdateLatestLocation,
2330
- })
2170
+ this.beforeLoad()
2331
2171
  const next = this.latestLocation
2332
2172
  const prevLocation = this.state.resolvedLocation
2333
2173
 
@@ -2935,7 +2775,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
2935
2775
  const trimmedPath = trimPathRight(pathname)
2936
2776
 
2937
2777
  let foundRoute: TRouteLike | undefined = undefined
2938
- let parsedParams: Record<string, unknown> = {}
2778
+ let parsedParams: Record<string, unknown> | undefined = undefined
2939
2779
  const match = findRouteMatch<TRouteLike>(trimmedPath, processedTree, true)
2940
2780
  if (match) {
2941
2781
  foundRoute = match.route
@@ -2948,96 +2788,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
2948
2788
  return { matchedRoutes, routeParams, foundRoute, parsedParams }
2949
2789
  }
2950
2790
 
2951
- /**
2952
- * Build a MatchSnapshot from a getMatchedRoutes result.
2953
- * Determines globalNotFoundRouteId using the same logic as matchRoutesInternal.
2954
- */
2955
- export function buildMatchSnapshot({
2956
- matchResult,
2957
- pathname,
2958
- searchStr,
2959
- notFoundRoute,
2960
- notFoundMode,
2961
- }: {
2962
- matchResult: ReturnType<typeof getMatchedRoutes>
2963
- pathname: string
2964
- searchStr?: string
2965
- notFoundRoute?: AnyRoute
2966
- notFoundMode?: 'root' | 'fuzzy'
2967
- }): MatchSnapshot {
2968
- const snapshot: MatchSnapshot = {
2969
- routeIds: matchResult.matchedRoutes.map((r) => r.id),
2970
- params: matchResult.routeParams,
2971
- parsedParams: matchResult.parsedParams,
2972
- searchStr,
2973
- }
2974
-
2975
- const isGlobalNotFound = matchResult.foundRoute
2976
- ? matchResult.foundRoute.path !== '/' && matchResult.routeParams['**']
2977
- : trimPathRight(pathname)
2978
-
2979
- if (isGlobalNotFound) {
2980
- if (notFoundRoute) {
2981
- // Custom notFoundRoute provided - use its id
2982
- snapshot.globalNotFoundRouteId = notFoundRoute.id
2983
- } else {
2984
- if (notFoundMode !== 'root') {
2985
- for (let i = matchResult.matchedRoutes.length - 1; i >= 0; i--) {
2986
- const route = matchResult.matchedRoutes[i]!
2987
- if (route.children) {
2988
- snapshot.globalNotFoundRouteId = route.id
2989
- break
2990
- }
2991
- }
2992
- }
2993
- if (!snapshot.globalNotFoundRouteId) {
2994
- snapshot.globalNotFoundRouteId = rootRouteId
2995
- }
2996
- }
2997
- }
2998
-
2999
- return snapshot
3000
- }
3001
-
3002
- /**
3003
- * Build a MatchSnapshot from routes and params directly.
3004
- * Used by buildLocation to avoid duplicate getMatchedRoutes call.
3005
- */
3006
- export function buildMatchSnapshotFromRoutes({
3007
- routes,
3008
- params,
3009
- searchStr,
3010
- globalNotFoundRouteId,
3011
- }: {
3012
- routes: ReadonlyArray<AnyRoute>
3013
- params: Record<string, unknown>
3014
- searchStr?: string
3015
- globalNotFoundRouteId?: string
3016
- }): MatchSnapshot {
3017
- // Convert all params to strings for snapshot storage
3018
- // (params from path matching are always strings)
3019
- const stringParams: Record<string, string> = {}
3020
- for (const key in params) {
3021
- const value = params[key]
3022
- if (value != null) {
3023
- stringParams[key] = String(value)
3024
- }
3025
- }
3026
-
3027
- const snapshot: MatchSnapshot = {
3028
- routeIds: routes.map((r) => r.id),
3029
- params: stringParams,
3030
- parsedParams: params,
3031
- searchStr,
3032
- }
3033
-
3034
- if (globalNotFoundRouteId) {
3035
- snapshot.globalNotFoundRouteId = globalNotFoundRouteId
3036
- }
3037
-
3038
- return snapshot
3039
- }
3040
-
3041
2791
  /**
3042
2792
  * TODO: once caches are persisted across requests on the server,
3043
2793
  * we can cache the built middleware chain using `last(destRoutes)` as the key
@@ -340,8 +340,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
340
340
 
341
341
  // If the user doesn't want to restore the scroll position,
342
342
  // we don't need to do anything.
343
- const resetScroll = event.toLocation.state.__TSR_resetScroll ?? true
344
- if (!resetScroll) {
343
+ if (!router.resetNextScroll) {
344
+ router.resetNextScroll = true
345
345
  return
346
346
  }
347
347
  if (typeof router.options.scrollRestoration === 'function') {