@tanstack/router-core 1.151.6 → 1.153.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.
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/location.d.cts +27 -0
- package/dist/cjs/router.cjs +212 -81
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +32 -5
- package/dist/cjs/scroll-restoration.cjs +2 -2
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/location.d.ts +27 -0
- package/dist/esm/router.d.ts +32 -5
- package/dist/esm/router.js +212 -81
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.js +2 -2
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +5 -1
- package/src/location.ts +35 -0
- package/src/router.ts +331 -93
- package/src/scroll-restoration.ts +2 -2
package/src/router.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
ParsedHistoryState,
|
|
47
47
|
RouterHistory,
|
|
48
48
|
} from '@tanstack/history'
|
|
49
|
+
|
|
49
50
|
import type {
|
|
50
51
|
Awaitable,
|
|
51
52
|
Constrain,
|
|
@@ -55,7 +56,7 @@ import type {
|
|
|
55
56
|
PickAsRequired,
|
|
56
57
|
Updater,
|
|
57
58
|
} from './utils'
|
|
58
|
-
import type { ParsedLocation } from './location'
|
|
59
|
+
import type { MatchSnapshot, ParsedLocation } from './location'
|
|
59
60
|
import type {
|
|
60
61
|
AnyContext,
|
|
61
62
|
AnyRoute,
|
|
@@ -589,6 +590,8 @@ export interface MatchRoutesOpts {
|
|
|
589
590
|
throwOnError?: boolean
|
|
590
591
|
_buildLocation?: boolean
|
|
591
592
|
dest?: BuildNextOptions
|
|
593
|
+
/** Optional match snapshot hint for fast-path (skips path matching) */
|
|
594
|
+
snapshot?: MatchSnapshot
|
|
592
595
|
}
|
|
593
596
|
|
|
594
597
|
export type InferRouterContext<TRouteTree extends AnyRoute> =
|
|
@@ -701,14 +704,17 @@ export type GetMatchRoutesFn = (pathname: string) => {
|
|
|
701
704
|
/** exhaustive params, still in their string form */
|
|
702
705
|
routeParams: Record<string, string>
|
|
703
706
|
/** partial params, parsed from routeParams during matching */
|
|
704
|
-
parsedParams: Record<string, unknown>
|
|
707
|
+
parsedParams: Record<string, unknown>
|
|
705
708
|
foundRoute: AnyRoute | undefined
|
|
706
709
|
parseError?: unknown
|
|
707
710
|
}
|
|
708
711
|
|
|
709
712
|
export type EmitFn = (routerEvent: RouterEvent) => void
|
|
710
713
|
|
|
711
|
-
export type LoadFn = (opts?: {
|
|
714
|
+
export type LoadFn = (opts?: {
|
|
715
|
+
sync?: boolean
|
|
716
|
+
_skipUpdateLatestLocation?: boolean
|
|
717
|
+
}) => Promise<void>
|
|
712
718
|
|
|
713
719
|
export type CommitLocationFn = ({
|
|
714
720
|
viewTransition,
|
|
@@ -891,7 +897,6 @@ export class RouterCore<
|
|
|
891
897
|
tempLocationKey: string | undefined = `${Math.round(
|
|
892
898
|
Math.random() * 10000000,
|
|
893
899
|
)}`
|
|
894
|
-
resetNextScroll = true
|
|
895
900
|
shouldViewTransition?: boolean | ViewTransitionOptions = undefined
|
|
896
901
|
isViewTransitionTypesSupported?: boolean = undefined
|
|
897
902
|
subscribers = new Set<RouterListener<RouterEvent>>()
|
|
@@ -916,6 +921,8 @@ export class RouterCore<
|
|
|
916
921
|
origin?: string
|
|
917
922
|
latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
918
923
|
pendingBuiltLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
924
|
+
/** Session id for cached history snapshots */
|
|
925
|
+
private sessionId!: string
|
|
919
926
|
basepath!: string
|
|
920
927
|
routeTree!: TRouteTree
|
|
921
928
|
routesById!: RoutesById<TRouteTree>
|
|
@@ -936,6 +943,11 @@ export class RouterCore<
|
|
|
936
943
|
TDehydrated
|
|
937
944
|
>,
|
|
938
945
|
) {
|
|
946
|
+
this.sessionId =
|
|
947
|
+
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
948
|
+
? crypto.randomUUID()
|
|
949
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
950
|
+
|
|
939
951
|
this.update({
|
|
940
952
|
defaultPreloadDelay: 50,
|
|
941
953
|
defaultPendingMs: 1000,
|
|
@@ -1199,7 +1211,7 @@ export class RouterCore<
|
|
|
1199
1211
|
pathname: decodePath(url.pathname),
|
|
1200
1212
|
searchStr,
|
|
1201
1213
|
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
|
|
1202
|
-
hash: url.hash.split('#').reverse()[0] ?? '',
|
|
1214
|
+
hash: decodePath(url.hash.split('#').reverse()[0] ?? ''),
|
|
1203
1215
|
state: replaceEqualDeep(previousLocation?.state, state),
|
|
1204
1216
|
}
|
|
1205
1217
|
}
|
|
@@ -1263,44 +1275,68 @@ export class RouterCore<
|
|
|
1263
1275
|
next: ParsedLocation,
|
|
1264
1276
|
opts?: MatchRoutesOpts,
|
|
1265
1277
|
): Array<AnyRouteMatch> {
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1278
|
+
// Fast-path: use snapshot hint if valid
|
|
1279
|
+
const snapshot = opts?.snapshot
|
|
1280
|
+
const snapshotValid =
|
|
1281
|
+
snapshot &&
|
|
1282
|
+
snapshot.routeIds.length > 0 &&
|
|
1283
|
+
snapshot.routeIds.every((id) => this.routesById[id])
|
|
1284
|
+
|
|
1285
|
+
let matchedRoutes: ReadonlyArray<AnyRoute>
|
|
1286
|
+
let routeParams: Record<string, string>
|
|
1287
|
+
let globalNotFoundRouteId: string | undefined
|
|
1288
|
+
let parsedParams: Record<string, unknown>
|
|
1289
|
+
|
|
1290
|
+
if (snapshotValid) {
|
|
1291
|
+
// Rebuild matched routes from snapshot
|
|
1292
|
+
matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
|
|
1293
|
+
routeParams = { ...snapshot.params }
|
|
1294
|
+
globalNotFoundRouteId = snapshot.globalNotFoundRouteId
|
|
1295
|
+
parsedParams = snapshot.parsedParams
|
|
1296
|
+
} else {
|
|
1297
|
+
// Normal path matching
|
|
1298
|
+
const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
|
|
1299
|
+
const { foundRoute, routeParams: rp } = matchedRoutesResult
|
|
1300
|
+
routeParams = rp
|
|
1301
|
+
matchedRoutes = matchedRoutesResult.matchedRoutes
|
|
1302
|
+
parsedParams = matchedRoutesResult.parsedParams
|
|
1270
1303
|
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
// If we found a route, and it's not an index route and we have left over path
|
|
1274
|
-
foundRoute
|
|
1275
|
-
? foundRoute.path !== '/' && routeParams['**']
|
|
1276
|
-
: // Or if we didn't find a route and we have left over path
|
|
1277
|
-
trimPathRight(next.pathname)
|
|
1278
|
-
) {
|
|
1279
|
-
// If the user has defined an (old) 404 route, use it
|
|
1280
|
-
if (this.options.notFoundRoute) {
|
|
1281
|
-
matchedRoutes = [...matchedRoutes, this.options.notFoundRoute]
|
|
1282
|
-
} else {
|
|
1283
|
-
// If there is no routes found during path matching
|
|
1284
|
-
isGlobalNotFound = true
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1304
|
+
let isGlobalNotFound = false
|
|
1287
1305
|
|
|
1288
|
-
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
1306
|
+
// Check to see if the route needs a 404 entry
|
|
1307
|
+
if (
|
|
1308
|
+
// If we found a route, and it's not an index route and we have left over path
|
|
1309
|
+
foundRoute
|
|
1310
|
+
? foundRoute.path !== '/' && routeParams['**']
|
|
1311
|
+
: // Or if we didn't find a route and we have left over path
|
|
1312
|
+
trimPathRight(next.pathname)
|
|
1313
|
+
) {
|
|
1314
|
+
// If the user has defined an (old) 404 route, use it
|
|
1315
|
+
if (this.options.notFoundRoute) {
|
|
1316
|
+
matchedRoutes = [...matchedRoutes, this.options.notFoundRoute]
|
|
1317
|
+
} else {
|
|
1318
|
+
// If there is no routes found during path matching
|
|
1319
|
+
isGlobalNotFound = true
|
|
1320
|
+
}
|
|
1291
1321
|
}
|
|
1292
1322
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1323
|
+
globalNotFoundRouteId = (() => {
|
|
1324
|
+
if (!isGlobalNotFound) {
|
|
1325
|
+
return undefined
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (this.options.notFoundMode !== 'root') {
|
|
1329
|
+
for (let i = matchedRoutes.length - 1; i >= 0; i--) {
|
|
1330
|
+
const route = matchedRoutes[i]!
|
|
1331
|
+
if (route.children) {
|
|
1332
|
+
return route.id
|
|
1333
|
+
}
|
|
1298
1334
|
}
|
|
1299
1335
|
}
|
|
1300
|
-
}
|
|
1301
1336
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1337
|
+
return rootRouteId
|
|
1338
|
+
})()
|
|
1339
|
+
}
|
|
1304
1340
|
|
|
1305
1341
|
const matches: Array<AnyRouteMatch> = []
|
|
1306
1342
|
|
|
@@ -1314,6 +1350,19 @@ export class RouterCore<
|
|
|
1314
1350
|
return parentContext
|
|
1315
1351
|
}
|
|
1316
1352
|
|
|
1353
|
+
// Check if we can use cached validated searches from snapshot
|
|
1354
|
+
// Valid if: snapshot exists, searchStr matches, and validatedSearches has correct length
|
|
1355
|
+
const canUseCachedSearch =
|
|
1356
|
+
snapshotValid &&
|
|
1357
|
+
snapshot.searchStr === next.searchStr &&
|
|
1358
|
+
snapshot.validatedSearches?.length === matchedRoutes.length
|
|
1359
|
+
|
|
1360
|
+
// Collect validated searches to cache in snapshot (only when not using cache)
|
|
1361
|
+
const validatedSearchesToCache: Array<{
|
|
1362
|
+
search: Record<string, unknown>
|
|
1363
|
+
strictSearch: Record<string, unknown>
|
|
1364
|
+
}> = []
|
|
1365
|
+
|
|
1317
1366
|
matchedRoutes.forEach((route, index) => {
|
|
1318
1367
|
// Take each matched route and resolve + validate its search params
|
|
1319
1368
|
// This has to happen serially because each route's search params
|
|
@@ -1329,6 +1378,12 @@ export class RouterCore<
|
|
|
1329
1378
|
Record<string, any>,
|
|
1330
1379
|
any,
|
|
1331
1380
|
] = (() => {
|
|
1381
|
+
// Fast-path: use cached validated search from snapshot
|
|
1382
|
+
if (canUseCachedSearch) {
|
|
1383
|
+
const cached = snapshot.validatedSearches![index]!
|
|
1384
|
+
return [cached.search, cached.strictSearch, undefined]
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1332
1387
|
// Validate the search params and stabilize them
|
|
1333
1388
|
const parentSearch = parentMatch?.search ?? next.search
|
|
1334
1389
|
const parentStrictSearch = parentMatch?._strictSearch ?? undefined
|
|
@@ -1362,6 +1417,14 @@ export class RouterCore<
|
|
|
1362
1417
|
}
|
|
1363
1418
|
})()
|
|
1364
1419
|
|
|
1420
|
+
// Cache the validated search for future pop navigations
|
|
1421
|
+
if (!canUseCachedSearch) {
|
|
1422
|
+
validatedSearchesToCache.push({
|
|
1423
|
+
search: preMatchSearch,
|
|
1424
|
+
strictSearch: strictMatchSearch,
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1365
1428
|
// This is where we need to call route.options.loaderDeps() to get any additional
|
|
1366
1429
|
// deps that the route's loader function might need to run. We need to do this
|
|
1367
1430
|
// before we create the match so that we can pass the deps to the route's
|
|
@@ -1528,6 +1591,18 @@ export class RouterCore<
|
|
|
1528
1591
|
matches.push(match)
|
|
1529
1592
|
})
|
|
1530
1593
|
|
|
1594
|
+
// Cache validated searches in snapshot for future pop navigations
|
|
1595
|
+
// Only update if we computed fresh values (not using cached)
|
|
1596
|
+
if (!canUseCachedSearch && validatedSearchesToCache.length > 0) {
|
|
1597
|
+
const existingSnapshot = next.state?.__TSR_matches as
|
|
1598
|
+
| MatchSnapshot
|
|
1599
|
+
| undefined
|
|
1600
|
+
if (existingSnapshot) {
|
|
1601
|
+
existingSnapshot.searchStr = next.searchStr
|
|
1602
|
+
existingSnapshot.validatedSearches = validatedSearchesToCache
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1531
1606
|
matches.forEach((match, index) => {
|
|
1532
1607
|
const route = this.looseRoutesById[match.routeId]!
|
|
1533
1608
|
const existingMatch = this.getMatch(match.id)
|
|
@@ -1690,9 +1765,15 @@ export class RouterCore<
|
|
|
1690
1765
|
params: nextParams,
|
|
1691
1766
|
}).interpolatedPath
|
|
1692
1767
|
|
|
1693
|
-
const
|
|
1768
|
+
const destMatches = this.matchRoutes(interpolatedNextTo, undefined, {
|
|
1694
1769
|
_buildLocation: true,
|
|
1695
|
-
})
|
|
1770
|
+
})
|
|
1771
|
+
const destRoutes = destMatches.map(
|
|
1772
|
+
(d) => this.looseRoutesById[d.routeId]!,
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
// Check if any match indicates global not found
|
|
1776
|
+
const globalNotFoundMatch = destMatches.find((m) => m.globalNotFound)
|
|
1696
1777
|
|
|
1697
1778
|
// If there are any params, we need to stringify them
|
|
1698
1779
|
if (Object.keys(nextParams).length > 0) {
|
|
@@ -1774,6 +1855,15 @@ export class RouterCore<
|
|
|
1774
1855
|
// Replace the equal deep
|
|
1775
1856
|
nextState = replaceEqualDeep(currentLocation.state, nextState)
|
|
1776
1857
|
|
|
1858
|
+
// Build match snapshot for fast-path on back/forward navigation
|
|
1859
|
+
// Use destRoutes and nextParams directly (after stringify)
|
|
1860
|
+
const matchSnapshot = buildMatchSnapshotFromRoutes({
|
|
1861
|
+
routes: destRoutes,
|
|
1862
|
+
params: nextParams,
|
|
1863
|
+
searchStr,
|
|
1864
|
+
globalNotFoundRouteId: globalNotFoundMatch?.routeId,
|
|
1865
|
+
})
|
|
1866
|
+
|
|
1777
1867
|
// Create the full path of the location
|
|
1778
1868
|
const fullPath = `${nextPathname}${searchStr}${hashStr}`
|
|
1779
1869
|
|
|
@@ -1783,10 +1873,13 @@ export class RouterCore<
|
|
|
1783
1873
|
// If a rewrite function is provided, use it to rewrite the URL
|
|
1784
1874
|
const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
|
|
1785
1875
|
|
|
1876
|
+
// Use encoded URL path for href (consistent with parseLocation)
|
|
1877
|
+
const encodedHref = url.href.replace(url.origin, '')
|
|
1878
|
+
|
|
1786
1879
|
return {
|
|
1787
1880
|
publicHref:
|
|
1788
1881
|
rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
|
|
1789
|
-
href:
|
|
1882
|
+
href: encodedHref,
|
|
1790
1883
|
url: rewrittenUrl,
|
|
1791
1884
|
pathname: nextPathname,
|
|
1792
1885
|
search: nextSearch,
|
|
@@ -1794,6 +1887,7 @@ export class RouterCore<
|
|
|
1794
1887
|
state: nextState as any,
|
|
1795
1888
|
hash: hash ?? '',
|
|
1796
1889
|
unmaskOnReload: dest.unmaskOnReload,
|
|
1890
|
+
_matchSnapshot: matchSnapshot,
|
|
1797
1891
|
}
|
|
1798
1892
|
}
|
|
1799
1893
|
|
|
@@ -1863,7 +1957,7 @@ export class RouterCore<
|
|
|
1863
1957
|
* Commit a previously built location to history (push/replace), optionally
|
|
1864
1958
|
* using view transitions and scroll restoration options.
|
|
1865
1959
|
*/
|
|
1866
|
-
commitLocation: CommitLocationFn = ({
|
|
1960
|
+
commitLocation: CommitLocationFn = async ({
|
|
1867
1961
|
viewTransition,
|
|
1868
1962
|
ignoreBlocker,
|
|
1869
1963
|
...next
|
|
@@ -1899,65 +1993,105 @@ export class RouterCore<
|
|
|
1899
1993
|
// Don't commit to history if nothing changed
|
|
1900
1994
|
if (isSameUrl && isSameState()) {
|
|
1901
1995
|
this.load()
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1996
|
+
return this.commitLocationPromise
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
let {
|
|
2000
|
+
// eslint-disable-next-line prefer-const
|
|
2001
|
+
maskedLocation,
|
|
2002
|
+
// eslint-disable-next-line prefer-const
|
|
2003
|
+
hashScrollIntoView,
|
|
2004
|
+
// don't pass url into history since it is a URL instance that cannot be serialized
|
|
2005
|
+
// eslint-disable-next-line prefer-const
|
|
2006
|
+
url: _url,
|
|
2007
|
+
...nextHistory
|
|
2008
|
+
} = next
|
|
2009
|
+
|
|
2010
|
+
if (maskedLocation) {
|
|
2011
|
+
nextHistory = {
|
|
2012
|
+
...maskedLocation,
|
|
2013
|
+
state: {
|
|
2014
|
+
...maskedLocation.state,
|
|
2015
|
+
__tempKey: undefined,
|
|
2016
|
+
__tempLocation: {
|
|
2017
|
+
...nextHistory,
|
|
2018
|
+
search: nextHistory.searchStr,
|
|
2019
|
+
state: {
|
|
2020
|
+
...nextHistory.state,
|
|
2021
|
+
__tempKey: undefined!,
|
|
2022
|
+
__tempLocation: undefined!,
|
|
2023
|
+
__TSR_key: undefined!,
|
|
2024
|
+
key: undefined!, // TODO: Remove in v2 - use __TSR_key instead
|
|
1930
2025
|
},
|
|
1931
2026
|
},
|
|
1932
|
-
}
|
|
2027
|
+
},
|
|
2028
|
+
}
|
|
1933
2029
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
this.options.unmaskOnReload ??
|
|
1937
|
-
false
|
|
1938
|
-
) {
|
|
1939
|
-
nextHistory.state.__tempKey = this.tempLocationKey
|
|
1940
|
-
}
|
|
2030
|
+
if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
|
|
2031
|
+
nextHistory.state.__tempKey = this.tempLocationKey
|
|
1941
2032
|
}
|
|
2033
|
+
}
|
|
1942
2034
|
|
|
1943
|
-
|
|
1944
|
-
|
|
2035
|
+
nextHistory.state.__hashScrollIntoViewOptions =
|
|
2036
|
+
hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
|
|
1945
2037
|
|
|
1946
|
-
|
|
2038
|
+
// Store resetScroll in history state so it survives back/forward navigation
|
|
2039
|
+
nextHistory.state.__TSR_resetScroll = next.resetScroll ?? true
|
|
1947
2040
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2041
|
+
this.shouldViewTransition = viewTransition
|
|
2042
|
+
|
|
2043
|
+
// Store session id for this router lifetime
|
|
2044
|
+
nextHistory.state.__TSR_sessionId = this.sessionId
|
|
2045
|
+
|
|
2046
|
+
// Use match snapshot from buildLocation if available, otherwise compute it.
|
|
2047
|
+
// Stored in history state for pop/back/forward fast-path.
|
|
2048
|
+
nextHistory.state.__TSR_matches =
|
|
2049
|
+
next._matchSnapshot ??
|
|
2050
|
+
buildMatchSnapshot({
|
|
2051
|
+
matchResult: this.getMatchedRoutes(next.pathname),
|
|
2052
|
+
pathname: next.pathname,
|
|
2053
|
+
searchStr: next.searchStr,
|
|
2054
|
+
notFoundRoute: this.options.notFoundRoute,
|
|
2055
|
+
notFoundMode: this.options.notFoundMode,
|
|
2056
|
+
})
|
|
2057
|
+
|
|
2058
|
+
// Build the pre-computed ParsedLocation to avoid re-parsing after push
|
|
2059
|
+
// Spread next (which has href, pathname, search, etc.) and override with final state
|
|
2060
|
+
const precomputedLocation: ParsedLocation = {
|
|
2061
|
+
...next,
|
|
2062
|
+
publicHref: nextHistory.publicHref,
|
|
2063
|
+
state: nextHistory.state,
|
|
2064
|
+
maskedLocation,
|
|
1953
2065
|
}
|
|
1954
2066
|
|
|
1955
|
-
|
|
2067
|
+
// Await push/replace to handle blockers before proceeding
|
|
2068
|
+
// Pass skipTransitionerLoad so Transitioner doesn't call load() - we handle it below
|
|
2069
|
+
const result = await this.history[next.replace ? 'replace' : 'push'](
|
|
2070
|
+
nextHistory.publicHref,
|
|
2071
|
+
nextHistory.state,
|
|
2072
|
+
{ ignoreBlocker, skipTransitionerLoad: true },
|
|
2073
|
+
)
|
|
1956
2074
|
|
|
1957
|
-
|
|
1958
|
-
|
|
2075
|
+
// If blocked, resolve promise and return
|
|
2076
|
+
if (result.type === 'BLOCKED') {
|
|
2077
|
+
this.commitLocationPromise?.resolve()
|
|
2078
|
+
return this.commitLocationPromise
|
|
1959
2079
|
}
|
|
1960
2080
|
|
|
2081
|
+
// Check if another navigation has superseded this one while we awaited
|
|
2082
|
+
// If so, let the newer navigation handle things - don't overwrite latestLocation
|
|
2083
|
+
if (this.history.location.href !== nextHistory.publicHref) {
|
|
2084
|
+
return this.commitLocationPromise
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// Success: set latestLocation directly (we skip updateLatestLocation in load)
|
|
2088
|
+
this.latestLocation = precomputedLocation as unknown as ParsedLocation<
|
|
2089
|
+
FullSearchSchema<TRouteTree>
|
|
2090
|
+
>
|
|
2091
|
+
|
|
2092
|
+
// Call load() with _skipUpdateLatestLocation since we already set latestLocation
|
|
2093
|
+
this.load({ _skipUpdateLatestLocation: true })
|
|
2094
|
+
|
|
1961
2095
|
return this.commitLocationPromise
|
|
1962
2096
|
}
|
|
1963
2097
|
|
|
@@ -2109,10 +2243,14 @@ export class RouterCore<
|
|
|
2109
2243
|
|
|
2110
2244
|
latestLoadPromise: undefined | Promise<void>
|
|
2111
2245
|
|
|
2112
|
-
beforeLoad = () => {
|
|
2246
|
+
beforeLoad = (opts?: { _skipUpdateLatestLocation?: boolean }) => {
|
|
2113
2247
|
// Cancel any pending matches
|
|
2114
2248
|
this.cancelMatches()
|
|
2115
|
-
|
|
2249
|
+
if (!opts?._skipUpdateLatestLocation) {
|
|
2250
|
+
this.updateLatestLocation()
|
|
2251
|
+
} else {
|
|
2252
|
+
// Already have latestLocation from commitLocation, skip parsing
|
|
2253
|
+
}
|
|
2116
2254
|
|
|
2117
2255
|
if (this.isServer) {
|
|
2118
2256
|
// for SPAs on the initial load, this is handled by the Transitioner
|
|
@@ -2136,7 +2274,12 @@ export class RouterCore<
|
|
|
2136
2274
|
}
|
|
2137
2275
|
|
|
2138
2276
|
// Match the routes
|
|
2139
|
-
|
|
2277
|
+
// Use snapshot from history state for fast-path only within same router lifetime
|
|
2278
|
+
const snapshot =
|
|
2279
|
+
this.latestLocation.state.__TSR_sessionId === this.sessionId
|
|
2280
|
+
? this.latestLocation.state.__TSR_matches
|
|
2281
|
+
: undefined
|
|
2282
|
+
const pendingMatches = this.matchRoutes(this.latestLocation, { snapshot })
|
|
2140
2283
|
|
|
2141
2284
|
// Ingest the new matches
|
|
2142
2285
|
this.__store.setState((s) => ({
|
|
@@ -2153,7 +2296,10 @@ export class RouterCore<
|
|
|
2153
2296
|
}))
|
|
2154
2297
|
}
|
|
2155
2298
|
|
|
2156
|
-
load: LoadFn = async (opts?: {
|
|
2299
|
+
load: LoadFn = async (opts?: {
|
|
2300
|
+
sync?: boolean
|
|
2301
|
+
_skipUpdateLatestLocation?: boolean
|
|
2302
|
+
}): Promise<void> => {
|
|
2157
2303
|
let redirect: AnyRedirect | undefined
|
|
2158
2304
|
let notFound: NotFoundError | undefined
|
|
2159
2305
|
let loadPromise: Promise<void>
|
|
@@ -2162,7 +2308,9 @@ export class RouterCore<
|
|
|
2162
2308
|
loadPromise = new Promise<void>((resolve) => {
|
|
2163
2309
|
this.startTransition(async () => {
|
|
2164
2310
|
try {
|
|
2165
|
-
this.beforeLoad(
|
|
2311
|
+
this.beforeLoad({
|
|
2312
|
+
_skipUpdateLatestLocation: opts?._skipUpdateLatestLocation,
|
|
2313
|
+
})
|
|
2166
2314
|
const next = this.latestLocation
|
|
2167
2315
|
const prevLocation = this.state.resolvedLocation
|
|
2168
2316
|
|
|
@@ -2770,7 +2918,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
|
|
|
2770
2918
|
const trimmedPath = trimPathRight(pathname)
|
|
2771
2919
|
|
|
2772
2920
|
let foundRoute: TRouteLike | undefined = undefined
|
|
2773
|
-
let parsedParams: Record<string, unknown>
|
|
2921
|
+
let parsedParams: Record<string, unknown> = {}
|
|
2774
2922
|
const match = findRouteMatch<TRouteLike>(trimmedPath, processedTree, true)
|
|
2775
2923
|
if (match) {
|
|
2776
2924
|
foundRoute = match.route
|
|
@@ -2783,6 +2931,96 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
|
|
|
2783
2931
|
return { matchedRoutes, routeParams, foundRoute, parsedParams }
|
|
2784
2932
|
}
|
|
2785
2933
|
|
|
2934
|
+
/**
|
|
2935
|
+
* Build a MatchSnapshot from a getMatchedRoutes result.
|
|
2936
|
+
* Determines globalNotFoundRouteId using the same logic as matchRoutesInternal.
|
|
2937
|
+
*/
|
|
2938
|
+
export function buildMatchSnapshot({
|
|
2939
|
+
matchResult,
|
|
2940
|
+
pathname,
|
|
2941
|
+
searchStr,
|
|
2942
|
+
notFoundRoute,
|
|
2943
|
+
notFoundMode,
|
|
2944
|
+
}: {
|
|
2945
|
+
matchResult: ReturnType<typeof getMatchedRoutes>
|
|
2946
|
+
pathname: string
|
|
2947
|
+
searchStr?: string
|
|
2948
|
+
notFoundRoute?: AnyRoute
|
|
2949
|
+
notFoundMode?: 'root' | 'fuzzy'
|
|
2950
|
+
}): MatchSnapshot {
|
|
2951
|
+
const snapshot: MatchSnapshot = {
|
|
2952
|
+
routeIds: matchResult.matchedRoutes.map((r) => r.id),
|
|
2953
|
+
params: matchResult.routeParams,
|
|
2954
|
+
parsedParams: matchResult.parsedParams,
|
|
2955
|
+
searchStr,
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
const isGlobalNotFound = matchResult.foundRoute
|
|
2959
|
+
? matchResult.foundRoute.path !== '/' && matchResult.routeParams['**']
|
|
2960
|
+
: trimPathRight(pathname)
|
|
2961
|
+
|
|
2962
|
+
if (isGlobalNotFound) {
|
|
2963
|
+
if (notFoundRoute) {
|
|
2964
|
+
// Custom notFoundRoute provided - use its id
|
|
2965
|
+
snapshot.globalNotFoundRouteId = notFoundRoute.id
|
|
2966
|
+
} else {
|
|
2967
|
+
if (notFoundMode !== 'root') {
|
|
2968
|
+
for (let i = matchResult.matchedRoutes.length - 1; i >= 0; i--) {
|
|
2969
|
+
const route = matchResult.matchedRoutes[i]!
|
|
2970
|
+
if (route.children) {
|
|
2971
|
+
snapshot.globalNotFoundRouteId = route.id
|
|
2972
|
+
break
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
if (!snapshot.globalNotFoundRouteId) {
|
|
2977
|
+
snapshot.globalNotFoundRouteId = rootRouteId
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
return snapshot
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
/**
|
|
2986
|
+
* Build a MatchSnapshot from routes and params directly.
|
|
2987
|
+
* Used by buildLocation to avoid duplicate getMatchedRoutes call.
|
|
2988
|
+
*/
|
|
2989
|
+
export function buildMatchSnapshotFromRoutes({
|
|
2990
|
+
routes,
|
|
2991
|
+
params,
|
|
2992
|
+
searchStr,
|
|
2993
|
+
globalNotFoundRouteId,
|
|
2994
|
+
}: {
|
|
2995
|
+
routes: ReadonlyArray<AnyRoute>
|
|
2996
|
+
params: Record<string, unknown>
|
|
2997
|
+
searchStr?: string
|
|
2998
|
+
globalNotFoundRouteId?: string
|
|
2999
|
+
}): MatchSnapshot {
|
|
3000
|
+
// Convert all params to strings for snapshot storage
|
|
3001
|
+
// (params from path matching are always strings)
|
|
3002
|
+
const stringParams: Record<string, string> = {}
|
|
3003
|
+
for (const key in params) {
|
|
3004
|
+
const value = params[key]
|
|
3005
|
+
if (value != null) {
|
|
3006
|
+
stringParams[key] = String(value)
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
const snapshot: MatchSnapshot = {
|
|
3011
|
+
routeIds: routes.map((r) => r.id),
|
|
3012
|
+
params: stringParams,
|
|
3013
|
+
parsedParams: params,
|
|
3014
|
+
searchStr,
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
if (globalNotFoundRouteId) {
|
|
3018
|
+
snapshot.globalNotFoundRouteId = globalNotFoundRouteId
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
return snapshot
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2786
3024
|
function applySearchMiddleware({
|
|
2787
3025
|
search,
|
|
2788
3026
|
dest,
|
|
@@ -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
|
-
|
|
344
|
-
|
|
343
|
+
const resetScroll = event.toLocation.state.__TSR_resetScroll ?? true
|
|
344
|
+
if (!resetScroll) {
|
|
345
345
|
return
|
|
346
346
|
}
|
|
347
347
|
if (typeof router.options.scrollRestoration === 'function') {
|