@tanstack/router-core 0.0.1-beta.185 → 0.0.1-beta.186

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
@@ -29,6 +29,7 @@ import {
29
29
  RegisteredRouteComponent,
30
30
  RegisteredErrorRouteComponent,
31
31
  RegisteredPendingRouteComponent,
32
+ RouteMask,
32
33
  } from './route'
33
34
  import {
34
35
  RoutesById,
@@ -50,13 +51,15 @@ import {
50
51
  Updater,
51
52
  replaceEqualDeep,
52
53
  partialDeepEqual,
54
+ NonNullableUpdater,
53
55
  } from './utils'
54
56
  import {
55
57
  createBrowserHistory,
56
58
  createMemoryHistory,
59
+ HistoryLocation,
60
+ LocationState,
57
61
  RouterHistory,
58
62
  } from './history'
59
- import warning from 'tiny-warning'
60
63
 
61
64
  //
62
65
 
@@ -78,25 +81,20 @@ export type RegisteredRouter = Register extends {
78
81
  ? TRouter
79
82
  : AnyRouter
80
83
 
81
- export interface LocationState {}
82
-
83
- export interface ParsedLocation<
84
- TSearchObj extends AnySearchSchema = {},
85
- // TState extends LocationState = LocationState,
86
- > {
84
+ export interface ParsedLocation<TSearchObj extends AnySearchSchema = {}> {
87
85
  href: string
88
86
  pathname: string
89
87
  search: TSearchObj
90
88
  searchStr: string
91
89
  state: LocationState
92
90
  hash: string
93
- key?: string
91
+ maskedLocation?: ParsedLocation<TSearchObj>
92
+ unmaskOnReload?: boolean
94
93
  }
95
94
 
96
95
  export interface FromLocation {
97
96
  pathname: string
98
97
  search?: unknown
99
- key?: string
100
98
  hash?: string
101
99
  }
102
100
 
@@ -158,7 +156,7 @@ export interface RouterOptions<
158
156
  parseSearch?: SearchParser
159
157
  defaultPreload?: false | 'intent'
160
158
  defaultPreloadDelay?: number
161
- refetchOnWindowFocus?: boolean
159
+ reloadOnWindowFocus?: boolean
162
160
  defaultComponent?: RegisteredRouteComponent<
163
161
  unknown,
164
162
  AnySearchSchema,
@@ -192,6 +190,8 @@ export interface RouterOptions<
192
190
  }>
193
191
  dehydrate?: () => TDehydrated
194
192
  hydrate?: (dehydrated: TDehydrated) => void
193
+ routeMasks?: RouteMask<TRouteTree>[]
194
+ unmaskOnReload?: boolean
195
195
  }
196
196
 
197
197
  export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
@@ -216,11 +216,21 @@ export interface BuildNextOptions {
216
216
  params?: true | Updater<unknown>
217
217
  search?: true | Updater<unknown>
218
218
  hash?: true | Updater<string>
219
- state?: LocationState
220
- key?: string
219
+ state?: true | NonNullableUpdater<LocationState>
220
+ mask?: {
221
+ to?: string | number | null
222
+ params?: true | Updater<unknown>
223
+ search?: true | Updater<unknown>
224
+ hash?: true | Updater<string>
225
+ state?: true | NonNullableUpdater<LocationState>
226
+ unmaskOnReload?: boolean
227
+ }
221
228
  from?: string
222
- fromCurrent?: boolean
223
- __matches?: AnyRouteMatch[]
229
+ }
230
+
231
+ export interface CommitLocationOptions {
232
+ replace?: boolean
233
+ resetScroll?: boolean
224
234
  }
225
235
 
226
236
  export interface MatchLocation {
@@ -228,7 +238,6 @@ export interface MatchLocation {
228
238
  fuzzy?: boolean
229
239
  caseSensitive?: boolean
230
240
  from?: string
231
- fromCurrent?: boolean
232
241
  }
233
242
 
234
243
  export interface MatchRouteOptions {
@@ -328,6 +337,8 @@ export class Router<
328
337
  state: RouterState<TRouteTree>
329
338
  dehydratedData?: TDehydrated
330
339
  resetNextScroll = false
340
+ tempLocationKey = `${Math.round(Math.random() * 10000000)}`
341
+ // nextTemporaryLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
331
342
 
332
343
  constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
333
344
  this.options = {
@@ -399,15 +410,15 @@ export class Router<
399
410
 
400
411
  this.update(options)
401
412
 
402
- const next = this.buildNext({
403
- hash: true,
404
- fromCurrent: true,
413
+ const nextLocation = this.buildLocation({
405
414
  search: true,
415
+ params: true,
416
+ hash: true,
406
417
  state: true,
407
418
  })
408
419
 
409
- if (this.state.location.href !== next.href) {
410
- this.#commitLocation({ ...next, replace: true })
420
+ if (this.state.location.href !== nextLocation.href) {
421
+ this.#commitLocation({ ...nextLocation, replace: true })
411
422
  }
412
423
  }
413
424
 
@@ -461,8 +472,10 @@ export class Router<
461
472
  }
462
473
 
463
474
  #onFocus = () => {
464
- if (this.options.refetchOnWindowFocus ?? true) {
465
- this.invalidate()
475
+ if (this.options.reloadOnWindowFocus ?? true) {
476
+ this.invalidate({
477
+ __fromFocus: true,
478
+ })
466
479
  }
467
480
  }
468
481
 
@@ -514,17 +527,6 @@ export class Router<
514
527
  return this
515
528
  }
516
529
 
517
- buildNext = (opts: BuildNextOptions): ParsedLocation => {
518
- const next = this.#buildLocation(opts)
519
-
520
- const __matches = this.matchRoutes(next.pathname, next.search)
521
-
522
- return this.#buildLocation({
523
- ...opts,
524
- __matches,
525
- })
526
- }
527
-
528
530
  cancelMatches = () => {
529
531
  this.state.matches.forEach((match) => {
530
532
  this.cancelMatch(match.id)
@@ -605,7 +607,7 @@ export class Router<
605
607
  try {
606
608
  // Load the matches
607
609
  try {
608
- await this.loadMatches(pendingMatches)
610
+ await this.loadMatches(pendingMatches.map((d) => d.id))
609
611
  } catch (err) {
610
612
  // swallow this error, since we'll display the
611
613
  // errors on the route components
@@ -680,10 +682,20 @@ export class Router<
680
682
  prevMatchesById: Record<string, RouteMatch<TRouteTree>>,
681
683
  nextMatches: AnyRouteMatch[],
682
684
  ): Record<string, RouteMatch<TRouteTree>> => {
683
- return {
684
- ...prevMatchesById,
685
- ...Object.fromEntries(nextMatches.map((match) => [match.id, match])),
686
- }
685
+ let matchesById = { ...prevMatchesById }
686
+
687
+ nextMatches.forEach((match) => {
688
+ if (!matchesById[match.id]) {
689
+ matchesById[match.id] = match
690
+ }
691
+
692
+ matchesById[match.id] = {
693
+ ...matchesById[match.id],
694
+ ...match,
695
+ }
696
+ })
697
+
698
+ return matchesById
687
699
  }
688
700
 
689
701
  getRoute = (id: string): Route => {
@@ -699,7 +711,8 @@ export class Router<
699
711
  maxAge?: number
700
712
  } = this.state.location,
701
713
  ) => {
702
- const next = this.buildNext(navigateOpts)
714
+ let next = this.buildLocation(navigateOpts)
715
+
703
716
  const matches = this.matchRoutes(next.pathname, next.search, {
704
717
  throwOnError: true,
705
718
  })
@@ -711,12 +724,15 @@ export class Router<
711
724
  }
712
725
  })
713
726
 
714
- await this.loadMatches(matches, {
715
- preload: true,
716
- maxAge: navigateOpts.maxAge,
717
- })
727
+ await this.loadMatches(
728
+ matches.map((d) => d.id),
729
+ {
730
+ preload: true,
731
+ maxAge: navigateOpts.maxAge,
732
+ },
733
+ )
718
734
 
719
- return matches
735
+ return [last(matches)!, matches] as const
720
736
  }
721
737
 
722
738
  cleanMatches = () => {
@@ -928,14 +944,13 @@ export class Router<
928
944
  }
929
945
 
930
946
  loadMatches = async (
931
- _resolvedMatches: AnyRouteMatch[],
947
+ matchIds: string[],
932
948
  opts?: {
933
949
  preload?: boolean
934
950
  maxAge?: number
935
951
  },
936
952
  ) => {
937
- const getFreshMatches = () =>
938
- _resolvedMatches.map((d) => this.getRouteMatch(d.id)!)
953
+ const getFreshMatches = () => matchIds.map((d) => this.getRouteMatch(d)!)
939
954
 
940
955
  if (!opts?.preload) {
941
956
  getFreshMatches().forEach((match) => {
@@ -1029,8 +1044,9 @@ export class Router<
1029
1044
  }
1030
1045
  }
1031
1046
  } catch (err) {
1032
- if (!opts?.preload) {
1033
- this.navigate(err as any)
1047
+ if (isRedirect(err)) {
1048
+ if (!opts?.preload) this.navigate(err as any)
1049
+ return
1034
1050
  }
1035
1051
 
1036
1052
  throw err
@@ -1155,6 +1171,8 @@ export class Router<
1155
1171
  navigate = async <
1156
1172
  TFrom extends RoutePaths<TRouteTree> = '/',
1157
1173
  TTo extends string = '',
1174
+ TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
1175
+ TMaskTo extends string = '',
1158
1176
  >({
1159
1177
  from,
1160
1178
  to = '' as any,
@@ -1163,7 +1181,7 @@ export class Router<
1163
1181
  replace,
1164
1182
  params,
1165
1183
  resetScroll,
1166
- }: NavigateOptions<TRouteTree, TFrom, TTo>) => {
1184
+ }: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => {
1167
1185
  // If this link simply reloads the current route,
1168
1186
  // make sure it has a new key so it will trigger a data refresh
1169
1187
 
@@ -1183,21 +1201,22 @@ export class Router<
1183
1201
  'Attempting to navigate to external url with this.navigate!',
1184
1202
  )
1185
1203
 
1186
- return this.#commitLocation({
1204
+ return this.#buildAndCommitLocation({
1187
1205
  from: fromString,
1188
1206
  to: toString,
1189
1207
  search,
1190
1208
  hash,
1191
- replace,
1192
1209
  params,
1210
+ replace,
1193
1211
  resetScroll,
1194
1212
  })
1195
1213
  }
1196
1214
 
1197
1215
  matchRoute = <
1216
+ TRouteTree extends AnyRoute = AnyRoute,
1198
1217
  TFrom extends RoutePaths<TRouteTree> = '/',
1199
1218
  TTo extends string = '',
1200
- TResolved extends string = ResolveRelativePath<TFrom, NoInfer<TTo>>,
1219
+ TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
1201
1220
  >(
1202
1221
  location: ToOptions<TRouteTree, TFrom, TTo>,
1203
1222
  opts?: MatchRouteOptions,
@@ -1209,7 +1228,7 @@ export class Router<
1209
1228
  : undefined,
1210
1229
  } as any
1211
1230
 
1212
- const next = this.buildNext(location)
1231
+ const next = this.buildLocation(location)
1213
1232
  if (opts?.pending && this.state.status !== 'pending') {
1214
1233
  return false
1215
1234
  }
@@ -1254,6 +1273,7 @@ export class Router<
1254
1273
  preloadDelay: userPreloadDelay,
1255
1274
  disabled,
1256
1275
  state,
1276
+ mask,
1257
1277
  resetScroll,
1258
1278
  }: LinkOptions<TRouteTree, TFrom, TTo>): LinkInfo => {
1259
1279
  // If this link simply reloads the current route,
@@ -1278,10 +1298,11 @@ export class Router<
1278
1298
  hash,
1279
1299
  replace,
1280
1300
  state,
1301
+ mask,
1281
1302
  resetScroll,
1282
1303
  }
1283
1304
 
1284
- const next = this.buildNext(nextOpts)
1305
+ const next = this.buildLocation(nextOpts)
1285
1306
 
1286
1307
  preload = preload ?? this.options.defaultPreload
1287
1308
  const preloadDelay =
@@ -1320,7 +1341,7 @@ export class Router<
1320
1341
  e.preventDefault()
1321
1342
 
1322
1343
  // All is well? Navigate!
1323
- this.#commitLocation(nextOpts as any)
1344
+ this.#commitLocation({ ...next, replace, resetScroll })
1324
1345
  }
1325
1346
  }
1326
1347
 
@@ -1600,154 +1621,269 @@ export class Router<
1600
1621
  #parseLocation = (
1601
1622
  previousLocation?: ParsedLocation,
1602
1623
  ): ParsedLocation<FullSearchSchema<TRouteTree>> => {
1603
- let { pathname, search, hash, state } = this.history.location
1624
+ const parse = ({
1625
+ pathname,
1626
+ search,
1627
+ hash,
1628
+ state,
1629
+ }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
1630
+ const parsedSearch = this.options.parseSearch(search)
1631
+
1632
+ return {
1633
+ pathname: pathname,
1634
+ searchStr: search,
1635
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1636
+ hash: hash.split('#').reverse()[0] ?? '',
1637
+ href: `${pathname}${search}${hash}`,
1638
+ state: replaceEqualDeep(
1639
+ previousLocation?.state,
1640
+ state,
1641
+ ) as LocationState,
1642
+ }
1643
+ }
1604
1644
 
1605
- const parsedSearch = this.options.parseSearch(search)
1645
+ const location = parse(this.history.location)
1606
1646
 
1607
- return {
1608
- pathname: pathname,
1609
- searchStr: search,
1610
- search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1611
- hash: hash.split('#').reverse()[0] ?? '',
1612
- href: `${pathname}${search}${hash}`,
1613
- state: state as LocationState,
1614
- key: state?.key || '__init__',
1647
+ let { __tempLocation, __tempKey } = location.state
1648
+
1649
+ if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1650
+ // Sync up the location keys
1651
+ const parsedTempLocation = parse(__tempLocation) as any
1652
+ parsedTempLocation.state.key = location.state.key
1653
+
1654
+ delete parsedTempLocation.state.__tempLocation
1655
+
1656
+ return {
1657
+ ...parsedTempLocation,
1658
+ maskedLocation: location,
1659
+ }
1615
1660
  }
1661
+
1662
+ return location
1616
1663
  }
1617
1664
 
1618
- #buildLocation = (dest: BuildNextOptions = {}): ParsedLocation => {
1619
- dest.fromCurrent = dest.fromCurrent ?? dest.to === ''
1665
+ buildLocation = (opts: BuildNextOptions = {}): ParsedLocation => {
1666
+ const build = (
1667
+ dest: BuildNextOptions & {
1668
+ unmaskOnReload?: boolean
1669
+ } = {},
1670
+ matches?: AnyRouteMatch[],
1671
+ ): ParsedLocation => {
1672
+ const from = this.state.location
1673
+
1674
+ const fromPathname = dest.from ?? from.pathname
1675
+
1676
+ let pathname = resolvePath(
1677
+ this.basepath ?? '/',
1678
+ fromPathname,
1679
+ `${dest.to ?? ''}`,
1680
+ )
1620
1681
 
1621
- const fromPathname = dest.fromCurrent
1622
- ? this.state.location.pathname
1623
- : dest.from ?? this.state.location.pathname
1682
+ const fromMatches = this.matchRoutes(from.pathname, from.search)
1624
1683
 
1625
- let pathname = resolvePath(
1626
- this.basepath ?? '/',
1627
- fromPathname,
1628
- `${dest.to ?? ''}`,
1629
- )
1684
+ const prevParams = { ...last(fromMatches)?.params }
1630
1685
 
1631
- const fromMatches = this.matchRoutes(
1632
- this.state.location.pathname,
1633
- this.state.location.search,
1634
- )
1686
+ let nextParams =
1687
+ (dest.params ?? true) === true
1688
+ ? prevParams
1689
+ : functionalUpdate(dest.params!, prevParams)
1635
1690
 
1636
- const prevParams = { ...last(fromMatches)?.params }
1691
+ if (nextParams) {
1692
+ matches
1693
+ ?.map((d) => this.getRoute(d.routeId).options.stringifyParams)
1694
+ .filter(Boolean)
1695
+ .forEach((fn) => {
1696
+ nextParams = { ...nextParams!, ...fn!(nextParams!) }
1697
+ })
1698
+ }
1637
1699
 
1638
- let nextParams =
1639
- (dest.params ?? true) === true
1640
- ? prevParams
1641
- : functionalUpdate(dest.params!, prevParams)
1700
+ pathname = interpolatePath(pathname, nextParams ?? {})
1701
+
1702
+ const preSearchFilters =
1703
+ matches
1704
+ ?.map(
1705
+ (match) =>
1706
+ this.getRoute(match.routeId).options.preSearchFilters ?? [],
1707
+ )
1708
+ .flat()
1709
+ .filter(Boolean) ?? []
1710
+
1711
+ const postSearchFilters =
1712
+ matches
1713
+ ?.map(
1714
+ (match) =>
1715
+ this.getRoute(match.routeId).options.postSearchFilters ?? [],
1716
+ )
1717
+ .flat()
1718
+ .filter(Boolean) ?? []
1719
+
1720
+ // Pre filters first
1721
+ const preFilteredSearch = preSearchFilters?.length
1722
+ ? preSearchFilters?.reduce((prev, next) => next(prev), from.search)
1723
+ : from.search
1724
+
1725
+ // Then the link/navigate function
1726
+ const destSearch =
1727
+ dest.search === true
1728
+ ? preFilteredSearch // Preserve resolvedFrom true
1729
+ : dest.search
1730
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1731
+ : preSearchFilters?.length
1732
+ ? preFilteredSearch // Preserve resolvedFrom filters
1733
+ : {}
1734
+
1735
+ // Then post filters
1736
+ const postFilteredSearch = postSearchFilters?.length
1737
+ ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
1738
+ : destSearch
1739
+
1740
+ const search = replaceEqualDeep(from.search, postFilteredSearch)
1741
+
1742
+ const searchStr = this.options.stringifySearch(search)
1743
+
1744
+ const hash =
1745
+ dest.hash === true
1746
+ ? from.hash
1747
+ : dest.hash
1748
+ ? functionalUpdate(dest.hash!, from.hash)
1749
+ : from.hash
1750
+
1751
+ const hashStr = hash ? `#${hash}` : ''
1752
+
1753
+ let nextState =
1754
+ dest.state === true
1755
+ ? from.state
1756
+ : dest.state
1757
+ ? functionalUpdate(dest.state, from.state)
1758
+ : from.state
1759
+
1760
+ nextState = replaceEqualDeep(from.state, nextState)
1642
1761
 
1643
- if (nextParams) {
1644
- dest.__matches
1645
- ?.map((d) => this.getRoute(d.routeId).options.stringifyParams)
1646
- .filter(Boolean)
1647
- .forEach((fn) => {
1648
- nextParams = { ...nextParams!, ...fn!(nextParams!) }
1649
- })
1762
+ return {
1763
+ pathname,
1764
+ search,
1765
+ searchStr,
1766
+ state: nextState,
1767
+ hash,
1768
+ href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
1769
+ unmaskOnReload: dest.unmaskOnReload,
1770
+ }
1650
1771
  }
1651
1772
 
1652
- pathname = interpolatePath(pathname, nextParams ?? {})
1773
+ const buildWithMatches = (
1774
+ dest: BuildNextOptions = {},
1775
+ maskedDest?: BuildNextOptions,
1776
+ ) => {
1777
+ let next = build(dest)
1778
+ let maskedNext = maskedDest ? build(maskedDest) : undefined
1779
+
1780
+ if (!maskedNext) {
1781
+ const foundMask = this.options.routeMasks?.find((d) => {
1782
+ const match = matchPathname(this.basepath, next.pathname, {
1783
+ to: d.from,
1784
+ fuzzy: false,
1785
+ })
1653
1786
 
1654
- const preSearchFilters =
1655
- dest.__matches
1656
- ?.map(
1657
- (match) =>
1658
- this.getRoute(match.routeId).options.preSearchFilters ?? [],
1659
- )
1660
- .flat()
1661
- .filter(Boolean) ?? []
1662
-
1663
- const postSearchFilters =
1664
- dest.__matches
1665
- ?.map(
1666
- (match) =>
1667
- this.getRoute(match.routeId).options.postSearchFilters ?? [],
1668
- )
1669
- .flat()
1670
- .filter(Boolean) ?? []
1787
+ if (match) {
1788
+ return match
1789
+ }
1671
1790
 
1672
- // Pre filters first
1673
- const preFilteredSearch = preSearchFilters?.length
1674
- ? preSearchFilters?.reduce(
1675
- (prev, next) => next(prev),
1676
- this.state.location.search,
1677
- )
1678
- : this.state.location.search
1679
-
1680
- // Then the link/navigate function
1681
- const destSearch =
1682
- dest.search === true
1683
- ? preFilteredSearch // Preserve resolvedFrom true
1684
- : dest.search
1685
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1686
- : preSearchFilters?.length
1687
- ? preFilteredSearch // Preserve resolvedFrom filters
1688
- : {}
1689
-
1690
- // Then post filters
1691
- const postFilteredSearch = postSearchFilters?.length
1692
- ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
1693
- : destSearch
1694
-
1695
- const search = replaceEqualDeep(
1696
- this.state.location.search,
1697
- postFilteredSearch,
1698
- )
1791
+ return false
1792
+ })
1699
1793
 
1700
- const searchStr = this.options.stringifySearch(search)
1794
+ if (foundMask) {
1795
+ maskedDest = foundMask
1796
+ maskedNext = build(maskedDest)
1797
+ }
1798
+ }
1701
1799
 
1702
- const hash =
1703
- dest.hash === true
1704
- ? this.state.location.hash
1705
- : functionalUpdate(dest.hash!, this.state.location.hash)
1800
+ const nextMatches = this.matchRoutes(next.pathname, next.search)
1801
+ const maskedMatches = maskedNext
1802
+ ? this.matchRoutes(maskedNext.pathname, maskedNext.search)
1803
+ : undefined
1804
+ const maskedFinal = maskedNext
1805
+ ? build(maskedDest, maskedMatches)
1806
+ : undefined
1706
1807
 
1707
- const hashStr = hash ? `#${hash}` : ''
1808
+ const final = build(dest, nextMatches)
1708
1809
 
1709
- const nextState =
1710
- dest.state === true
1711
- ? this.state.location.state
1712
- : functionalUpdate(dest.state, this.state.location.state)!
1810
+ if (maskedFinal) {
1811
+ final.maskedLocation = maskedFinal
1812
+ }
1713
1813
 
1714
- return {
1715
- pathname,
1716
- search,
1717
- searchStr,
1718
- state: nextState,
1719
- hash,
1720
- href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
1721
- key: dest.key,
1814
+ return final
1815
+ }
1816
+
1817
+ if (opts.mask) {
1818
+ return buildWithMatches(opts, {
1819
+ ...pick(opts, ['from']),
1820
+ ...opts.mask,
1821
+ })
1722
1822
  }
1823
+
1824
+ return buildWithMatches(opts)
1723
1825
  }
1724
1826
 
1725
- #commitLocation = async (
1726
- location: BuildNextOptions & { replace?: boolean; resetScroll?: boolean },
1727
- ) => {
1728
- const next = this.buildNext(location)
1827
+ #buildAndCommitLocation = ({
1828
+ replace,
1829
+ resetScroll,
1830
+ ...rest
1831
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
1832
+ const location = this.buildLocation(rest)
1833
+ return this.#commitLocation({
1834
+ ...location,
1835
+ replace,
1836
+ resetScroll,
1837
+ })
1838
+ }
1729
1839
 
1840
+ #commitLocation = async (next: ParsedLocation & CommitLocationOptions) => {
1730
1841
  if (this.navigateTimeout) clearTimeout(this.navigateTimeout)
1731
1842
 
1732
1843
  let nextAction: 'push' | 'replace' = 'replace'
1733
1844
 
1734
- if (!location.replace) {
1845
+ if (!next.replace) {
1735
1846
  nextAction = 'push'
1736
1847
  }
1737
1848
 
1738
1849
  const isSameUrl = this.state.location.href === next.href
1739
1850
 
1740
- if (isSameUrl && !next.key) {
1851
+ if (isSameUrl) {
1741
1852
  nextAction = 'replace'
1742
1853
  }
1743
1854
 
1744
- const href = `${next.pathname}${next.searchStr}${
1745
- next.hash ? `#${next.hash}` : ''
1746
- }`
1855
+ let { maskedLocation, ...nextHistory } = next
1856
+
1857
+ if (maskedLocation) {
1858
+ nextHistory = {
1859
+ ...maskedLocation,
1860
+ state: {
1861
+ ...maskedLocation.state,
1862
+ __tempKey: undefined,
1863
+ __tempLocation: {
1864
+ ...nextHistory,
1865
+ search: nextHistory.searchStr,
1866
+ state: {
1867
+ ...nextHistory.state,
1868
+ __tempKey: undefined!,
1869
+ __tempLocation: undefined!,
1870
+ key: undefined!,
1871
+ },
1872
+ },
1873
+ },
1874
+ }
1875
+
1876
+ if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
1877
+ nextHistory.state.__tempKey = this.tempLocationKey
1878
+ }
1879
+ }
1747
1880
 
1748
- this.history[nextAction === 'push' ? 'push' : 'replace'](href, next.state)
1881
+ this.history[nextAction === 'push' ? 'push' : 'replace'](
1882
+ nextHistory.href,
1883
+ nextHistory.state,
1884
+ )
1749
1885
 
1750
- this.resetNextScroll = location.resetScroll ?? true
1886
+ this.resetNextScroll = next.resetScroll ?? true
1751
1887
 
1752
1888
  return this.latestLoadPromise
1753
1889
  }
@@ -1814,37 +1950,46 @@ export class Router<
1814
1950
  invalidate = async (opts?: {
1815
1951
  matchId?: string
1816
1952
  reload?: boolean
1953
+ __fromFocus?: boolean
1817
1954
  }): Promise<void> => {
1818
1955
  if (opts?.matchId) {
1819
1956
  this.setRouteMatch(opts.matchId, (s) => ({
1820
1957
  ...s,
1821
1958
  invalid: true,
1822
1959
  }))
1960
+
1823
1961
  const matchIndex = this.state.matches.findIndex(
1824
1962
  (d) => d.id === opts.matchId,
1825
1963
  )
1826
1964
  const childMatch = this.state.matches[matchIndex + 1]
1827
1965
 
1828
1966
  if (childMatch) {
1829
- return this.invalidate({ matchId: childMatch.id, reload: false })
1967
+ return this.invalidate({
1968
+ matchId: childMatch.id,
1969
+ reload: false,
1970
+ __fromFocus: opts.__fromFocus,
1971
+ })
1830
1972
  }
1831
1973
  } else {
1832
1974
  this.__store.batch(() => {
1833
1975
  Object.values(this.state.matchesById).forEach((match) => {
1834
- this.setRouteMatch(match.id, (s) => ({
1835
- ...s,
1836
- invalid: true,
1837
- }))
1976
+ const route = this.getRoute(match.routeId)
1977
+ const shouldInvalidate = opts?.__fromFocus
1978
+ ? route.options.reloadOnWindowFocus ?? true
1979
+ : true
1980
+
1981
+ if (shouldInvalidate) {
1982
+ this.setRouteMatch(match.id, (s) => ({
1983
+ ...s,
1984
+ invalid: true,
1985
+ }))
1986
+ }
1838
1987
  })
1839
1988
  })
1840
1989
  }
1841
1990
 
1842
1991
  if (opts?.reload ?? true) {
1843
- return this.navigate({
1844
- fromCurrent: true,
1845
- replace: true,
1846
- search: true,
1847
- } as any)
1992
+ return this.load()
1848
1993
  }
1849
1994
  }
1850
1995
  }
@@ -1879,7 +2024,9 @@ export type Redirect<
1879
2024
  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
1880
2025
  TFrom extends RoutePaths<TRouteTree> = '/',
1881
2026
  TTo extends string = '',
1882
- > = NavigateOptions<TRouteTree, TFrom, TTo> & {
2027
+ TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
2028
+ TMaskTo extends string = '',
2029
+ > = NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
1883
2030
  code?: number
1884
2031
  }
1885
2032