@tanstack/router-core 1.171.14 → 1.171.16-pre.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.
Files changed (139) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +3 -12
  3. package/dist/cjs/await-signal.cjs +21 -0
  4. package/dist/cjs/await-signal.cjs.map +1 -0
  5. package/dist/cjs/await-signal.d.cts +1 -0
  6. package/dist/cjs/index.cjs +3 -1
  7. package/dist/cjs/index.d.cts +3 -2
  8. package/dist/cjs/isServer/client.cjs +2 -0
  9. package/dist/cjs/isServer/client.cjs.map +1 -1
  10. package/dist/cjs/isServer/client.d.cts +1 -0
  11. package/dist/cjs/isServer/development.cjs +2 -0
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.d.cts +1 -0
  14. package/dist/cjs/isServer/server.cjs +2 -0
  15. package/dist/cjs/isServer/server.cjs.map +1 -1
  16. package/dist/cjs/isServer/server.d.cts +1 -0
  17. package/dist/cjs/load-client.cjs +1470 -0
  18. package/dist/cjs/load-client.cjs.map +1 -0
  19. package/dist/cjs/load-client.d.cts +147 -0
  20. package/dist/cjs/load-server.cjs +565 -0
  21. package/dist/cjs/load-server.cjs.map +1 -0
  22. package/dist/cjs/load-server.d.cts +16 -0
  23. package/dist/cjs/redirect.cjs.map +1 -1
  24. package/dist/cjs/redirect.d.cts +0 -1
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/router.cjs +187 -445
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +62 -52
  29. package/dist/cjs/scroll-restoration.cjs +27 -34
  30. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  31. package/dist/cjs/ssr/client.cjs +2 -2
  32. package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
  33. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  34. package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
  35. package/dist/cjs/ssr/handlerCallback.cjs +49 -4
  36. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  37. package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
  38. package/dist/cjs/ssr/server.cjs +4 -0
  39. package/dist/cjs/ssr/server.d.cts +2 -2
  40. package/dist/cjs/ssr/ssr-client.d.cts +1 -10
  41. package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
  42. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  43. package/dist/cjs/ssr/ssr-server.cjs +6 -6
  44. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  45. package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
  46. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  47. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
  48. package/dist/cjs/ssr/types.d.cts +0 -1
  49. package/dist/cjs/stores.cjs +30 -104
  50. package/dist/cjs/stores.cjs.map +1 -1
  51. package/dist/cjs/stores.d.cts +7 -32
  52. package/dist/cjs/utils.cjs +0 -4
  53. package/dist/cjs/utils.cjs.map +1 -1
  54. package/dist/esm/Matches.d.ts +3 -12
  55. package/dist/esm/Matches.js.map +1 -1
  56. package/dist/esm/await-signal.d.ts +1 -0
  57. package/dist/esm/await-signal.js +21 -0
  58. package/dist/esm/await-signal.js.map +1 -0
  59. package/dist/esm/index.d.ts +3 -2
  60. package/dist/esm/index.js +3 -2
  61. package/dist/esm/isServer/client.d.ts +1 -0
  62. package/dist/esm/isServer/client.js +2 -1
  63. package/dist/esm/isServer/client.js.map +1 -1
  64. package/dist/esm/isServer/development.d.ts +1 -0
  65. package/dist/esm/isServer/development.js +2 -1
  66. package/dist/esm/isServer/development.js.map +1 -1
  67. package/dist/esm/isServer/server.d.ts +1 -0
  68. package/dist/esm/isServer/server.js +2 -1
  69. package/dist/esm/isServer/server.js.map +1 -1
  70. package/dist/esm/load-client.d.ts +147 -0
  71. package/dist/esm/load-client.js +1462 -0
  72. package/dist/esm/load-client.js.map +1 -0
  73. package/dist/esm/load-server.d.ts +16 -0
  74. package/dist/esm/load-server.js +565 -0
  75. package/dist/esm/load-server.js.map +1 -0
  76. package/dist/esm/redirect.d.ts +0 -1
  77. package/dist/esm/redirect.js.map +1 -1
  78. package/dist/esm/route.js.map +1 -1
  79. package/dist/esm/router.d.ts +62 -52
  80. package/dist/esm/router.js +188 -446
  81. package/dist/esm/router.js.map +1 -1
  82. package/dist/esm/scroll-restoration.js +27 -34
  83. package/dist/esm/scroll-restoration.js.map +1 -1
  84. package/dist/esm/ssr/client.js +1 -1
  85. package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
  86. package/dist/esm/ssr/createRequestHandler.js +61 -11
  87. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  88. package/dist/esm/ssr/handlerCallback.d.ts +3 -0
  89. package/dist/esm/ssr/handlerCallback.js +47 -5
  90. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  91. package/dist/esm/ssr/server.d.ts +2 -2
  92. package/dist/esm/ssr/server.js +3 -3
  93. package/dist/esm/ssr/ssr-client.d.ts +1 -10
  94. package/dist/esm/ssr/ssr-match-id.js +2 -2
  95. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  96. package/dist/esm/ssr/ssr-server.js +6 -6
  97. package/dist/esm/ssr/ssr-server.js.map +1 -1
  98. package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
  99. package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
  100. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  101. package/dist/esm/ssr/types.d.ts +0 -1
  102. package/dist/esm/stores.d.ts +7 -32
  103. package/dist/esm/stores.js +30 -104
  104. package/dist/esm/stores.js.map +1 -1
  105. package/dist/esm/utils.js +1 -4
  106. package/dist/esm/utils.js.map +1 -1
  107. package/package.json +3 -3
  108. package/src/Matches.ts +6 -21
  109. package/src/await-signal.ts +27 -0
  110. package/src/index.ts +1 -3
  111. package/src/isServer/client.ts +1 -0
  112. package/src/isServer/development.ts +1 -0
  113. package/src/isServer/server.ts +1 -0
  114. package/src/load-client.ts +2701 -0
  115. package/src/load-server.ts +849 -0
  116. package/src/redirect.ts +0 -1
  117. package/src/route.ts +2 -11
  118. package/src/router.ts +380 -774
  119. package/src/scroll-restoration.ts +40 -56
  120. package/src/ssr/createRequestHandler.ts +109 -16
  121. package/src/ssr/handlerCallback.ts +76 -10
  122. package/src/ssr/server.ts +4 -1
  123. package/src/ssr/ssr-client.ts +1 -310
  124. package/src/ssr/ssr-match-id.ts +11 -2
  125. package/src/ssr/ssr-server.ts +9 -8
  126. package/src/ssr/transformStreamWithRouter.ts +37 -1
  127. package/src/ssr/types.ts +0 -1
  128. package/src/stores.ts +46 -208
  129. package/dist/cjs/load-matches.cjs +0 -658
  130. package/dist/cjs/load-matches.cjs.map +0 -1
  131. package/dist/cjs/load-matches.d.cts +0 -18
  132. package/dist/cjs/ssr/ssr-client.cjs +0 -183
  133. package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
  134. package/dist/esm/load-matches.d.ts +0 -18
  135. package/dist/esm/load-matches.js +0 -656
  136. package/dist/esm/load-matches.js.map +0 -1
  137. package/dist/esm/ssr/ssr-client.js +0 -183
  138. package/dist/esm/ssr/ssr-client.js.map +0 -1
  139. package/src/load-matches.ts +0 -1278
package/src/router.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { createBrowserHistory, parseHref } from '@tanstack/history'
2
- import { isServer } from '@tanstack/router-core/isServer'
2
+ import { isServer, loadServerRoute } from '@tanstack/router-core/isServer'
3
3
  import {
4
4
  DEFAULT_PROTOCOL_ALLOWLIST,
5
- createControlledPromise,
6
5
  decodePath,
7
6
  deepEqual,
8
7
  encodePathLikeUrl,
@@ -35,8 +34,15 @@ import { isNotFound } from './not-found'
35
34
  import { setupScrollRestoration } from './scroll-restoration'
36
35
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
37
36
  import { rootRouteId } from './root'
38
- import { isRedirect, redirect } from './redirect'
39
- import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches'
37
+ import { isRedirect } from './redirect'
38
+ import {
39
+ loadClientRoute,
40
+ loadRouteChunk,
41
+ preloadClientRoute,
42
+ refreshClientRoute,
43
+ replaceRouteChunk,
44
+ transferMatchResources,
45
+ } from './load-client'
40
46
  import {
41
47
  composeRewrites,
42
48
  executeRewriteInput,
@@ -51,6 +57,13 @@ import type {
51
57
  } from './new-process-route-tree'
52
58
  import type { SearchParser, SearchSerializer } from './searchParams'
53
59
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
60
+ import type {
61
+ ActivePreload,
62
+ LoadTransaction,
63
+ LoaderFlight,
64
+ PendingSession,
65
+ } from './load-client'
66
+ import type { ServerLoadResult } from './load-server'
54
67
  import type {
55
68
  HistoryAction,
56
69
  HistoryLocation,
@@ -62,7 +75,6 @@ import type {
62
75
  import type {
63
76
  Awaitable,
64
77
  Constrain,
65
- ControlledPromise,
66
78
  NoInfer,
67
79
  NonNullableUpdater,
68
80
  PickAsRequired,
@@ -75,8 +87,6 @@ import type {
75
87
  AnyRouteWithContext,
76
88
  LoaderStaleReloadMode,
77
89
  MakeRemountDepsOptionsUnion,
78
- RouteContextOptions,
79
- RouteLike,
80
90
  RouteMask,
81
91
  SearchMiddleware,
82
92
  SearchMiddlewareMeta,
@@ -106,7 +116,6 @@ import type {
106
116
  } from './manifest'
107
117
  import type { AnySchema, AnyValidator } from './validators'
108
118
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
109
- import type { NotFoundError } from './not-found'
110
119
  import type {
111
120
  AnySerializationAdapter,
112
121
  ValidateSerializableInput,
@@ -263,9 +272,9 @@ export interface RouterOptions<
263
272
  */
264
273
  defaultPreloadStaleTime?: number
265
274
  /**
266
- * The default `defaultPreloadGcTime` a route should use if no preloadGcTime is provided.
275
+ * The default `preloadGcTime` a route should use if none is provided.
267
276
  *
268
- * @default 1_800_000 `(30 minutes)`
277
+ * @default 300_000 `(5 minutes)`
269
278
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadgctime-property)
270
279
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
271
280
  */
@@ -297,7 +306,7 @@ export interface RouterOptions<
297
306
  /**
298
307
  * The default `gcTime` a route should use if no gcTime is provided.
299
308
  *
300
- * @default 1_800_000 `(30 minutes)`
309
+ * @default 300_000 `(5 minutes)`
301
310
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultgctime-property)
302
311
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options)
303
312
  */
@@ -544,14 +553,10 @@ export interface RouterState<
544
553
  in out TRouteMatch = MakeRouteMatchUnion,
545
554
  > {
546
555
  status: 'pending' | 'idle'
547
- loadedAt: number
548
556
  isLoading: boolean
549
- isTransitioning: boolean
550
557
  matches: Array<TRouteMatch>
551
558
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
552
559
  resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
553
- statusCode: number
554
- redirect?: AnyRedirect
555
560
  }
556
561
 
557
562
  export interface BuildNextOptions {
@@ -619,9 +624,21 @@ export type SubscribeFn = <TType extends keyof RouterEvents>(
619
624
  ) => () => void
620
625
 
621
626
  export interface MatchRoutesOpts {
622
- preload?: boolean
623
627
  throwOnError?: boolean
624
- dest?: BuildNextOptions
628
+ /** @internal */
629
+ _controller?: AbortController
630
+ /** @internal */
631
+ _rematerialize?: boolean
632
+ }
633
+
634
+ function routeNeedsLoad(route: AnyRoute): unknown {
635
+ return (
636
+ route.options.loader ||
637
+ route.options.beforeLoad ||
638
+ route.lazyFn ||
639
+ (route.options.component as any)?.preload ||
640
+ (route.options.pendingComponent as any)?.preload
641
+ )
625
642
  }
626
643
 
627
644
  export type InferRouterContext<TRouteTree extends AnyRoute> =
@@ -748,6 +765,8 @@ export type EmitFn = (routerEvent: RouterEvent) => void
748
765
  export type LoadFn = (opts?: {
749
766
  sync?: boolean
750
767
  action?: { type: HistoryAction }
768
+ _signal?: AbortSignal
769
+ _dedupe?: boolean
751
770
  }) => Promise<void>
752
771
 
753
772
  export type CommitLocationFn = ({
@@ -756,7 +775,11 @@ export type CommitLocationFn = ({
756
775
  ...next
757
776
  }: ParsedLocation & CommitLocationOptions) => Promise<void>
758
777
 
759
- export type StartTransitionFn = (fn: () => void) => void
778
+ export type StartTransitionFn = (
779
+ fn: () => void,
780
+ expected: Array<AnyRouteMatch>,
781
+ urgent?: boolean,
782
+ ) => Promise<boolean>
760
783
 
761
784
  export interface MatchRoutesFn {
762
785
  (
@@ -775,13 +798,6 @@ export interface MatchRoutesFn {
775
798
  ): Array<AnyRouteMatch>
776
799
  }
777
800
 
778
- export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined
779
-
780
- export type UpdateMatchFn = (
781
- id: string,
782
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
783
- ) => void
784
-
785
801
  export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>>
786
802
 
787
803
  export type ResolveRedirect = (err: AnyRedirect) => ResolvedRedirect
@@ -896,18 +912,61 @@ export function getLocationChangeInfo(
896
912
  location: ParsedLocation,
897
913
  resolvedLocation?: ParsedLocation,
898
914
  ) {
899
- const fromLocation = resolvedLocation
900
- const toLocation = location
901
- const pathChanged = fromLocation?.pathname !== toLocation.pathname
902
- const hrefChanged = fromLocation?.href !== toLocation.href
903
- const hashChanged = fromLocation?.hash !== toLocation.hash
904
- return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
915
+ return {
916
+ fromLocation: resolvedLocation,
917
+ toLocation: location,
918
+ pathChanged: resolvedLocation?.pathname !== location.pathname,
919
+ hrefChanged: resolvedLocation?.href !== location.href,
920
+ hashChanged: resolvedLocation?.hash !== location.hash,
921
+ }
905
922
  }
906
923
 
907
- export const locationHistoryActions = new WeakMap<
908
- ParsedLocation,
909
- HistoryAction
910
- >()
924
+ /**
925
+ * Return only state owned by the application, excluding volatile history
926
+ * bookkeeping. Mask payloads (`__tempLocation`/`__tempKey`) are kept: they
927
+ * distinguish otherwise-identical locations.
928
+ */
929
+ export function _getUserHistoryState({
930
+ key: _key,
931
+ __TSR_key: _tsrKey,
932
+ __TSR_index: _tsrIndex,
933
+ __hashScrollIntoViewOptions: _hashScroll,
934
+ ...state
935
+ }: ParsedHistoryState): HistoryState {
936
+ return state
937
+ }
938
+
939
+ /** Run route lifecycle callbacks in leave/enter/stay phases. */
940
+ export function runRouteLifecycle(
941
+ router: AnyRouter,
942
+ previous: Array<AnyRouteMatch>,
943
+ matches: Array<AnyRouteMatch>,
944
+ isCurrent?: () => boolean,
945
+ ): void {
946
+ for (const match of previous) {
947
+ if (isCurrent?.() === false) {
948
+ return
949
+ }
950
+ if (!matches.some((candidate) => candidate.routeId === match.routeId)) {
951
+ ;(router.routesById as Record<string, AnyRoute>)[
952
+ match.routeId
953
+ ]!.options.onLeave?.(match)
954
+ }
955
+ }
956
+ for (const match of matches) {
957
+ if (isCurrent?.() === false) {
958
+ return
959
+ }
960
+ const route = (router.routesById as Record<string, AnyRoute>)[
961
+ match.routeId
962
+ ]!
963
+ route.options[
964
+ previous.some((candidate) => candidate.routeId === match.routeId)
965
+ ? 'onStay'
966
+ : 'onEnter'
967
+ ]?.(match)
968
+ }
969
+ }
911
970
 
912
971
  type LightweightRouteMatchResult = {
913
972
  matchedRoutes: ReadonlyArray<AnyRoute>
@@ -956,6 +1015,43 @@ declare global {
956
1015
  | undefined
957
1016
  }
958
1017
 
1018
+ export interface RouterCore<
1019
+ in out TRouteTree extends AnyRoute,
1020
+ in out TTrailingSlashOption extends TrailingSlashOption,
1021
+ in out TDefaultStructuralSharingOption extends boolean,
1022
+ in out TRouterHistory extends RouterHistory = RouterHistory,
1023
+ in out TDehydrated extends Record<string, any> = Record<string, any>,
1024
+ > {
1025
+ shouldViewTransition?: boolean | ViewTransitionOptions
1026
+ /** Current client load transaction and owner of navigation writes. */
1027
+ _tx?: LoadTransaction
1028
+ /** Joinable in-flight loader generations keyed by match ID. */
1029
+ _flights?: Map<string, LoaderFlight>
1030
+ /** Whole speculative lanes that an identical navigation may adopt. */
1031
+ _preloads?: Map<string, ActivePreload>
1032
+ /** Owns cancellable work before a client transaction publishes. */
1033
+ _preflight?: AbortController
1034
+ /** Transfers one reconstructed SSR prefix into its initial client load. */
1035
+ _handoff?: HydrationHandoff
1036
+ /** Pending-boundary reveal and minimum-visible timing state. */
1037
+ _pending?: PendingSession
1038
+ /** Result of the latest server load, used to render or redirect. */
1039
+ _serverResult?: ServerLoadResult
1040
+ /** Framework callback that acknowledges an exact matches publication. */
1041
+ _rendered?: (matches: Array<AnyRouteMatch>) => void
1042
+ /** Development-only HMR reload for a route and its descendants. */
1043
+ _refreshRoute: (() => Promise<void>) | undefined
1044
+ /** Development-only replacement for a route's lazy chunk owner. */
1045
+ _replaceRouteChunk:
1046
+ | ((route: AnyRoute, lazyFn: AnyRoute['lazyFn']) => void)
1047
+ | undefined
1048
+ }
1049
+
1050
+ export type HydrationHandoff = [
1051
+ claim: () => AbortController | undefined,
1052
+ finish: (matches?: Array<AnyRouteMatch>) => number | undefined,
1053
+ ]
1054
+
959
1055
  /**
960
1056
  * Core, framework-agnostic router engine that powers TanStack Router.
961
1057
  *
@@ -978,14 +1074,17 @@ export class RouterCore<
978
1074
  )}`
979
1075
  _scroll: {
980
1076
  next: boolean
1077
+ // True until the current PUSH/REPLACE renders, so its hash owns window scroll.
1078
+ hash?: boolean
981
1079
  restoring?: boolean
982
1080
  restoration?: boolean
983
1081
  reset?: boolean
984
1082
  } = { next: true }
985
- shouldViewTransition?: boolean | ViewTransitionOptions = undefined
986
- isViewTransitionTypesSupported?: boolean = undefined
987
1083
  subscribers = new Set<RouterListener<RouterEvent>>()
988
- viewTransitionPromise?: ControlledPromise<true>
1084
+ /** Accepted off-screen loader generations keyed by match ID. */
1085
+ _cache = new Map<string, AnyRouteMatch>()
1086
+ /** Accepted semantic lane, excluding temporary pending presentation. */
1087
+ _committed: Array<AnyRouteMatch> = []
989
1088
 
990
1089
  // Must build in constructor
991
1090
  stores!: RouterStores<TRouteTree>
@@ -1006,7 +1105,7 @@ export class RouterCore<
1006
1105
  rewrite?: LocationRewrite
1007
1106
  origin?: string
1008
1107
  latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
1009
- pendingBuiltLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
1108
+ _pendingLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
1010
1109
  basepath!: string
1011
1110
  routeTree!: TRouteTree
1012
1111
  routesById!: RoutesById<TRouteTree>
@@ -1051,24 +1150,19 @@ export class RouterCore<
1051
1150
  options.protocolAllowlist ?? DEFAULT_PROTOCOL_ALLOWLIST,
1052
1151
  })
1053
1152
 
1054
- if (typeof document !== 'undefined') {
1153
+ if (!(isServer ?? typeof document === 'undefined')) {
1055
1154
  self.__TSR_ROUTER__ = this
1056
1155
  }
1057
1156
  }
1058
1157
 
1059
- // This is a default implementation that can optionally be overridden
1060
- // by the router provider once rendered. We provide this so that the
1061
- // router can be used in a non-react environment if necessary
1062
- startTransition: StartTransitionFn = (fn) => fn()
1063
-
1158
+ startTransition: StartTransitionFn = async (fn) => {
1159
+ fn()
1160
+ return false
1161
+ }
1064
1162
  isShell() {
1065
1163
  return !!this.options.isShell
1066
1164
  }
1067
1165
 
1068
- isPrerendering() {
1069
- return !!this.options.isPrerendering
1070
- }
1071
-
1072
1166
  update: UpdateFn<
1073
1167
  TRouteTree,
1074
1168
  TTrailingSlashOption,
@@ -1094,7 +1188,8 @@ export class RouterCore<
1094
1188
  ...newOptions,
1095
1189
  }
1096
1190
 
1097
- this.isServer = this.options.isServer ?? typeof document === 'undefined'
1191
+ this.isServer =
1192
+ this.options.isServer ?? isServer ?? typeof document === 'undefined'
1098
1193
 
1099
1194
  this.protocolAllowlist = new Set(this.options.protocolAllowlist)
1100
1195
 
@@ -1168,17 +1263,13 @@ export class RouterCore<
1168
1263
  if (!this.stores && this.latestLocation) {
1169
1264
  const config = this.getStoreConfig(this)
1170
1265
  this.batch = config.batch
1171
- this.stores = createRouterStores(
1172
- getInitialRouterState(this.latestLocation),
1173
- config,
1174
- )
1266
+ this.stores = createRouterStores(this.latestLocation, config)
1175
1267
 
1176
1268
  if (!(isServer ?? this.isServer)) {
1177
1269
  setupScrollRestoration(this)
1178
1270
  }
1179
1271
  }
1180
1272
 
1181
- let needsLocationUpdate = false
1182
1273
  const nextBasepath = this.options.basepath ?? '/'
1183
1274
  const nextRewriteOption = this.options.rewrite
1184
1275
  const basepathChanged = basepathWasUnset || prevBasepath !== nextBasepath
@@ -1211,21 +1302,9 @@ export class RouterCore<
1211
1302
  this.updateLatestLocation()
1212
1303
  }
1213
1304
 
1214
- needsLocationUpdate = true
1215
- }
1216
-
1217
- if (needsLocationUpdate && this.stores) {
1218
- this.stores.location.set(this.latestLocation)
1219
- }
1220
-
1221
- if (
1222
- typeof window !== 'undefined' &&
1223
- 'CSS' in window &&
1224
- typeof window.CSS?.supports === 'function'
1225
- ) {
1226
- this.isViewTransitionTypesSupported = window.CSS.supports(
1227
- 'selector(:active-view-transition-type(a))',
1228
- )
1305
+ if (this.stores) {
1306
+ this.stores.location.set(this.latestLocation)
1307
+ }
1229
1308
  }
1230
1309
  }
1231
1310
 
@@ -1296,11 +1375,15 @@ export class RouterCore<
1296
1375
  }
1297
1376
 
1298
1377
  emit: EmitFn = (routerEvent) => {
1299
- this.subscribers.forEach((listener) => {
1378
+ for (const listener of this.subscribers) {
1300
1379
  if (listener.eventType === routerEvent.type) {
1301
- listener.fn(routerEvent)
1380
+ try {
1381
+ listener.fn(routerEvent)
1382
+ } catch (e) {
1383
+ console.error(e)
1384
+ }
1302
1385
  }
1303
- })
1386
+ }
1304
1387
  }
1305
1388
 
1306
1389
  /**
@@ -1408,10 +1491,6 @@ export class RouterCore<
1408
1491
  return branch
1409
1492
  }
1410
1493
 
1411
- get looseRoutesById() {
1412
- return this.routesById as Record<string, AnyRoute>
1413
- }
1414
-
1415
1494
  matchRoutes: MatchRoutesFn = (
1416
1495
  pathnameOrNext: string | ParsedLocation,
1417
1496
  locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
@@ -1430,16 +1509,6 @@ export class RouterCore<
1430
1509
  return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1431
1510
  }
1432
1511
 
1433
- private getParentContext(parentMatch?: AnyRouteMatch) {
1434
- const parentMatchId = parentMatch?.id
1435
-
1436
- const parentContext = !parentMatchId
1437
- ? ((this.options.context as any) ?? undefined)
1438
- : (parentMatch.context ?? this.options.context ?? undefined)
1439
-
1440
- return parentContext
1441
- }
1442
-
1443
1512
  private matchRoutesInternal(
1444
1513
  next: ParsedLocation,
1445
1514
  opts?: MatchRoutesOpts,
@@ -1466,18 +1535,19 @@ export class RouterCore<
1466
1535
  }
1467
1536
  }
1468
1537
 
1469
- const globalNotFoundRouteId = isGlobalNotFound
1538
+ const _notFoundRouteId = isGlobalNotFound
1470
1539
  ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes)
1471
1540
  : undefined
1472
1541
 
1473
1542
  const matches = new Array<AnyRouteMatch>(matchedRoutes.length)
1474
- // Snapshot of active match state keyed by routeId, used to stabilise
1475
- // params/search across navigations.
1476
- const previousActiveMatchesByRouteId = new Map<string, AnyRouteMatch>()
1477
- for (const store of this.stores.matchStores.values()) {
1478
- if (store.routeId) {
1479
- previousActiveMatchesByRouteId.set(store.routeId, store.get())
1480
- }
1543
+ const committed = this._committed
1544
+ const previousAt = (route: AnyRoute, index: number) => {
1545
+ const match = committed[index]
1546
+ return match?.routeId === route.id
1547
+ ? match
1548
+ : route === this.options.notFoundRoute
1549
+ ? committed.find((candidate) => candidate.routeId === route.id)
1550
+ : undefined
1481
1551
  }
1482
1552
 
1483
1553
  for (let index = 0; index < matchedRoutes.length; index++) {
@@ -1509,7 +1579,6 @@ export class RouterCore<
1509
1579
  ...strictSearch,
1510
1580
  }
1511
1581
  strictMatchSearch = { ...parentStrictSearch, ...strictSearch }
1512
- searchError = undefined
1513
1582
  } catch (err: any) {
1514
1583
  let searchParamError = err
1515
1584
  if (!(err instanceof SearchParamError)) {
@@ -1527,19 +1596,25 @@ export class RouterCore<
1527
1596
  searchError = searchParamError
1528
1597
  }
1529
1598
  }
1530
-
1531
1599
  // This is where we need to call route.options.loaderDeps() to get any additional
1532
1600
  // deps that the route's loader function might need to run. We need to do this
1533
1601
  // before we create the match so that we can pass the deps to the route's
1534
1602
  // potential key function which is used to uniquely identify the route match in state
1535
1603
 
1536
- const loaderDeps =
1537
- route.options.loaderDeps?.({
1538
- search: preMatchSearch,
1539
- }) ?? ''
1540
-
1541
- const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1542
-
1604
+ let loaderDeps: any = ''
1605
+ let loaderDepsHash = ''
1606
+ try {
1607
+ loaderDeps =
1608
+ route.options.loaderDeps?.({
1609
+ search: preMatchSearch,
1610
+ }) ?? ''
1611
+ loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) || '' : ''
1612
+ } catch (cause) {
1613
+ if (opts?.throwOnError) {
1614
+ throw cause
1615
+ }
1616
+ searchError ??= cause
1617
+ }
1543
1618
  const { interpolatedPath, usedParams } = interpolatePath({
1544
1619
  path: route.fullPath,
1545
1620
  params: routeParams,
@@ -1547,12 +1622,9 @@ export class RouterCore<
1547
1622
  server: this.isServer,
1548
1623
  })
1549
1624
 
1550
- // Waste not, want not. If we already have a match for this route,
1551
- // reuse it. This is important for layout routes, which might stick
1552
- // around between navigation actions that only change leaf routes.
1553
-
1554
- // Existing matches are matches that are already loaded along with
1555
- // pending matches that are still loading
1625
+ // Seed planning from the accepted same-ID cache generation first, then
1626
+ // from the committed generation for this route. Presentation stores are
1627
+ // deliberately not a semantic reuse authority.
1556
1628
  const matchId =
1557
1629
  // route.id for disambiguation
1558
1630
  route.id +
@@ -1561,13 +1633,16 @@ export class RouterCore<
1561
1633
  // explicit deps
1562
1634
  loaderDepsHash
1563
1635
 
1564
- const existingMatch = this.getMatch(matchId)
1565
-
1566
- const previousMatch = previousActiveMatchesByRouteId.get(route.id)
1636
+ const previousMatch = previousAt(route, index)
1637
+ const existingMatch =
1638
+ process.env.NODE_ENV !== 'production' && opts?._rematerialize
1639
+ ? undefined
1640
+ : (this._cache.get(matchId) ??
1641
+ (previousMatch?.id === matchId ? previousMatch : undefined))
1567
1642
 
1568
1643
  const strictParams = existingMatch?._strictParams ?? usedParams
1569
1644
 
1570
- let paramsError: unknown = undefined
1645
+ let paramsError: unknown
1571
1646
 
1572
1647
  if (!existingMatch) {
1573
1648
  try {
@@ -1603,15 +1678,10 @@ export class RouterCore<
1603
1678
  ? nullReplaceEqualDeep(previousMatch.search, preMatchSearch)
1604
1679
  : nullReplaceEqualDeep(existingMatch.search, preMatchSearch),
1605
1680
  _strictSearch: strictMatchSearch,
1681
+ searchError,
1606
1682
  }
1607
1683
  } else {
1608
- const status =
1609
- route.options.loader ||
1610
- route.options.beforeLoad ||
1611
- route.lazyFn ||
1612
- routeNeedsPreload(route)
1613
- ? 'pending'
1614
- : 'success'
1684
+ const status = routeNeedsLoad(route) ? 'pending' : 'success'
1615
1685
 
1616
1686
  match = {
1617
1687
  id: matchId,
@@ -1626,96 +1696,42 @@ export class RouterCore<
1626
1696
  ? nullReplaceEqualDeep(previousMatch.search, preMatchSearch)
1627
1697
  : preMatchSearch,
1628
1698
  _strictSearch: strictMatchSearch,
1629
- searchError: undefined,
1699
+ searchError,
1630
1700
  status,
1631
1701
  isFetching: false,
1632
1702
  error: undefined,
1633
1703
  paramsError,
1634
- __routeContext: undefined,
1635
- _nonReactive: {
1636
- loadPromise: createControlledPromise(),
1637
- },
1638
- __beforeLoadContext: undefined,
1639
1704
  context: {},
1640
- abortController: new AbortController(),
1641
- fetchCount: 0,
1705
+ abortController: opts?._controller ?? new AbortController(),
1642
1706
  cause,
1643
1707
  loaderDeps: previousMatch
1644
1708
  ? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps)
1645
1709
  : loaderDeps,
1646
1710
  invalid: false,
1647
1711
  preload: false,
1648
- links: undefined,
1649
- scripts: undefined,
1650
- headScripts: undefined,
1651
- meta: undefined,
1652
1712
  staticData: route.options.staticData || {},
1653
1713
  fullPath: route.fullPath,
1654
1714
  }
1655
1715
  }
1656
1716
 
1657
- if (!opts?.preload) {
1658
- // If we have a global not found, mark the right match as global not found
1659
- match.globalNotFound = globalNotFoundRouteId === route.id
1660
- }
1661
-
1662
- // update the searchError if there is one
1663
- match.searchError = searchError
1664
-
1665
- const parentContext = this.getParentContext(parentMatch)
1666
-
1667
- match.context = {
1668
- ...parentContext,
1669
- ...match.__routeContext,
1670
- ...match.__beforeLoadContext,
1717
+ // If we have a global not found, mark the right match as global not found
1718
+ const _notFound = _notFoundRouteId === route.id
1719
+ if (match._notFound && !_notFound) {
1720
+ match.error = undefined
1671
1721
  }
1722
+ match._notFound = _notFound
1672
1723
 
1673
1724
  matches[index] = match
1674
1725
  }
1675
1726
 
1676
1727
  for (let index = 0; index < matches.length; index++) {
1677
1728
  const match = matches[index]!
1678
- const route = this.looseRoutesById[match.routeId]!
1679
- const existingMatch = this.getMatch(match.id)
1680
-
1681
- // Update the match's params
1682
- const previousMatch = previousActiveMatchesByRouteId.get(match.routeId)
1683
- match.params = previousMatch
1684
- ? nullReplaceEqualDeep(previousMatch.params, routeParams)
1685
- : routeParams
1686
-
1687
- if (!existingMatch) {
1688
- const parentMatch = matches[index - 1]
1689
- const parentContext = this.getParentContext(parentMatch)
1690
-
1691
- // Update the match's context
1692
-
1693
- if (route.options.context) {
1694
- const contextFnContext: RouteContextOptions<any, any, any, any, any> =
1695
- {
1696
- deps: match.loaderDeps,
1697
- params: match.params,
1698
- context: parentContext ?? {},
1699
- location: next,
1700
- navigate: (opts: any) =>
1701
- this.navigate({ ...opts, _fromLocation: next }),
1702
- buildLocation: this.buildLocation,
1703
- cause: match.cause,
1704
- abortController: match.abortController,
1705
- preload: !!match.preload,
1706
- matches,
1707
- routeId: route.id,
1708
- }
1709
- // Get the route context
1710
- match.__routeContext =
1711
- route.options.context(contextFnContext) ?? undefined
1712
- }
1713
-
1714
- match.context = {
1715
- ...parentContext,
1716
- ...match.__routeContext,
1717
- ...match.__beforeLoadContext,
1718
- }
1729
+ match.params =
1730
+ match.cause === 'stay'
1731
+ ? nullReplaceEqualDeep(match.params, routeParams)
1732
+ : routeParams
1733
+ if (opts?._controller) {
1734
+ match.context = {}
1719
1735
  }
1720
1736
  }
1721
1737
 
@@ -1723,22 +1739,35 @@ export class RouterCore<
1723
1739
  }
1724
1740
 
1725
1741
  getMatchedRoutes: GetMatchRoutesFn = (pathname) => {
1726
- return getMatchedRoutes({
1727
- pathname,
1728
- routesById: this.routesById,
1729
- processedTree: this.processedTree,
1730
- })
1742
+ const routeParams: Record<string, string> = Object.create(null)
1743
+ const match = findRouteMatch(
1744
+ trimPathRight(pathname),
1745
+ this.processedTree,
1746
+ true,
1747
+ )
1748
+ if (match) {
1749
+ Object.assign(routeParams, match.rawParams)
1750
+ }
1751
+ return {
1752
+ matchedRoutes: match?.branch || [this.routesById[rootRouteId]!],
1753
+ routeParams,
1754
+ foundRoute: match?.route,
1755
+ }
1731
1756
  }
1732
1757
 
1733
1758
  /**
1734
1759
  * Lightweight route matching for buildLocation.
1735
1760
  * Only computes fullPath, accumulated search, and params - skipping expensive
1736
- * operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
1761
+ * operations like AbortController, loaderDeps, and full match objects.
1737
1762
  */
1738
1763
  private matchRoutesLightweight(
1739
1764
  location: ParsedLocation,
1740
1765
  ): LightweightRouteMatchResult {
1741
- const lastStateMatchId = last(this.stores.matchesId.get())
1766
+ const lastRouteId = last(this.stores.ids.get())
1767
+ const lastStateMatch = lastRouteId
1768
+ ? this.stores.byRoute.get(lastRouteId)!.get()
1769
+ : undefined
1770
+ const lastStateMatchId = lastStateMatch?.id
1742
1771
  const cached = this.lightweightCache.get(location)
1743
1772
  if (cached && cached[0] === lastStateMatchId) {
1744
1773
  return cached[1]
@@ -1772,8 +1801,6 @@ export class RouterCore<
1772
1801
  }
1773
1802
 
1774
1803
  // Determine params: reuse from state if possible, otherwise parse
1775
- const lastStateMatch =
1776
- lastStateMatchId && this.stores.matchStores.get(lastStateMatchId)?.get()
1777
1804
  const canReuseParams =
1778
1805
  lastStateMatch &&
1779
1806
  lastStateMatch.routeId === lastRoute.id &&
@@ -1808,37 +1835,6 @@ export class RouterCore<
1808
1835
  return result
1809
1836
  }
1810
1837
 
1811
- cancelMatch = (id: string) => {
1812
- const match = this.getMatch(id)
1813
-
1814
- if (!match) return
1815
-
1816
- match.abortController.abort()
1817
- clearTimeout(match._nonReactive.pendingTimeout)
1818
- match._nonReactive.pendingTimeout = undefined
1819
- }
1820
-
1821
- cancelMatches = () => {
1822
- this.stores.pendingIds.get().forEach((matchId) => {
1823
- this.cancelMatch(matchId)
1824
- })
1825
-
1826
- this.stores.matchesId.get().forEach((matchId) => {
1827
- if (this.stores.pendingMatchStores.has(matchId)) {
1828
- return
1829
- }
1830
-
1831
- const match = this.stores.matchStores.get(matchId)?.get()
1832
- if (!match) {
1833
- return
1834
- }
1835
-
1836
- if (match.status === 'pending' || match.isFetching === 'loader') {
1837
- this.cancelMatch(matchId)
1838
- }
1839
- })
1840
- }
1841
-
1842
1838
  /**
1843
1839
  * Build the next ParsedLocation from navigation options without committing.
1844
1840
  * Resolves `to`/`from`, params/search/hash/state, applies search validation
@@ -1854,7 +1850,7 @@ export class RouterCore<
1854
1850
  ): ParsedLocation => {
1855
1851
  // We allow the caller to override the current location
1856
1852
  const currentLocation =
1857
- dest._fromLocation || this.pendingBuiltLocation || this.latestLocation
1853
+ dest._fromLocation || this._pendingLocation || this.latestLocation
1858
1854
 
1859
1855
  // Use lightweight matching - only computes what buildLocation needs
1860
1856
  // (fullPath, search, params) without creating full match objects
@@ -1909,15 +1905,7 @@ export class RouterCore<
1909
1905
  : sourcePath
1910
1906
 
1911
1907
  // Resolve the next params
1912
- const nextParams =
1913
- dest.params === false || dest.params === null
1914
- ? Object.create(null)
1915
- : (dest.params ?? true) === true
1916
- ? fromParams
1917
- : Object.assign(
1918
- fromParams,
1919
- functionalUpdate(dest.params as any, fromParams),
1920
- )
1908
+ const nextParams = resolveNextParams(dest.params, fromParams)
1921
1909
 
1922
1910
  const destRoute = this.routesByPath[
1923
1911
  trimPathRight(nextTo) as keyof typeof this.routesByPath
@@ -2015,12 +2003,12 @@ export class RouterCore<
2015
2003
  nextSearch = validatedSearch
2016
2004
  }
2017
2005
 
2018
- nextSearch = applySearchMiddleware({
2019
- search: nextSearch,
2006
+ nextSearch = applySearchMiddleware(
2007
+ nextSearch,
2020
2008
  dest,
2021
2009
  destRoutes,
2022
- _includeValidateSearch: opts._includeValidateSearch,
2023
- })
2010
+ opts._includeValidateSearch,
2011
+ )
2024
2012
 
2025
2013
  // Replace the equal deep
2026
2014
  nextSearch = nullReplaceEqualDeep(fromSearch, nextSearch)
@@ -2120,12 +2108,7 @@ export class RouterCore<
2120
2108
 
2121
2109
  // If mask has a params function, call it with the matched params as context
2122
2110
  // Otherwise, use the matched params or the provided params value
2123
- const nextParams =
2124
- maskParams === false || maskParams === null
2125
- ? Object.create(null)
2126
- : (maskParams ?? true) === true
2127
- ? params
2128
- : Object.assign(params, functionalUpdate(maskParams, params))
2111
+ const nextParams = resolveNextParams(maskParams, params)
2129
2112
 
2130
2113
  maskedDest = {
2131
2114
  from: opts.from,
@@ -2154,7 +2137,7 @@ export class RouterCore<
2154
2137
  return buildWithMatches(opts)
2155
2138
  }
2156
2139
 
2157
- commitLocationPromise: undefined | ControlledPromise<void>
2140
+ _commitPromise: (Promise<void> & { resolve: () => void }) | undefined
2158
2141
 
2159
2142
  /**
2160
2143
  * Commit a previously built location to history (push/replace), optionally
@@ -2166,38 +2149,27 @@ export class RouterCore<
2166
2149
  ...next
2167
2150
  }) => {
2168
2151
  let historyAction: HistoryAction | undefined
2169
- const isSameState = () => {
2170
- // the following props are ignored but may still be provided when navigating,
2171
- // temporarily add the previous values to the next state so they don't affect
2172
- // the comparison
2173
- const ignoredProps = [
2174
- 'key', // TODO: Remove in v2 - use __TSR_key instead
2175
- '__TSR_key',
2176
- '__TSR_index',
2177
- '__hashScrollIntoViewOptions',
2178
- ] as const
2179
- ignoredProps.forEach((prop) => {
2180
- ;(next.state as any)[prop] = this.latestLocation.state[prop]
2181
- })
2182
- const isEqual = deepEqual(next.state, this.latestLocation.state)
2183
- ignoredProps.forEach((prop) => {
2184
- delete next.state[prop]
2185
- })
2186
- return isEqual
2187
- }
2188
-
2189
- const isSameUrl =
2190
- trimPathRight(this.latestLocation.href) === trimPathRight(next.href)
2152
+ const isSameLocation =
2153
+ trimPathRight(this.latestLocation.href) === trimPathRight(next.href) &&
2154
+ deepEqual(
2155
+ _getUserHistoryState(next.state),
2156
+ _getUserHistoryState(this.latestLocation.state),
2157
+ )
2191
2158
 
2192
- let previousCommitPromise = this.commitLocationPromise
2193
- this.commitLocationPromise = createControlledPromise<void>(() => {
2159
+ const previousCommitPromise = this._commitPromise
2160
+ let resolve!: () => void
2161
+ const commitPromise = new Promise<void>((done) => {
2162
+ resolve = done
2163
+ }) as Promise<void> & { resolve: () => void }
2164
+ commitPromise.resolve = () => {
2165
+ resolve()
2194
2166
  previousCommitPromise?.resolve()
2195
- previousCommitPromise = undefined
2196
- })
2167
+ }
2168
+ this._commitPromise = commitPromise
2197
2169
 
2198
2170
  // Don't commit to history if nothing changed
2199
- if (isSameUrl && isSameState()) {
2200
- this.load()
2171
+ if (isSameLocation) {
2172
+ this.load({ _dedupe: true })
2201
2173
  } else {
2202
2174
  let {
2203
2175
  // eslint-disable-next-line prefer-const
@@ -2248,21 +2220,17 @@ export class RouterCore<
2248
2220
  nextHistory.state,
2249
2221
  { ignoreBlocker },
2250
2222
  )
2223
+
2224
+ // Without a subscribed adapter the history commit cannot trigger the
2225
+ // load itself. The same-location branch already loads directly.
2226
+ if (!this.history.subscribers.size) {
2227
+ this.load({ action: { type: historyAction } })
2228
+ }
2251
2229
  }
2252
2230
 
2253
2231
  this._scroll.next = next.resetScroll ?? true
2254
2232
 
2255
- if (!this.history.subscribers.size) {
2256
- this.load(
2257
- historyAction
2258
- ? {
2259
- action: { type: historyAction },
2260
- }
2261
- : undefined,
2262
- )
2263
- }
2264
-
2265
- return this.commitLocationPromise
2233
+ return this._commitPromise
2266
2234
  }
2267
2235
 
2268
2236
  /** Convenience helper: build a location from options, then commit it. */
@@ -2272,9 +2240,11 @@ export class RouterCore<
2272
2240
  hashScrollIntoView,
2273
2241
  viewTransition,
2274
2242
  ignoreBlocker,
2243
+ _redirects,
2275
2244
  href,
2276
2245
  ...rest
2277
- }: BuildNextOptions & CommitLocationOptions = {}) => {
2246
+ }: BuildNextOptions &
2247
+ CommitLocationOptions & { _redirects?: number } = {}) => {
2278
2248
  if (href) {
2279
2249
  const currentIndex = this.history.location.state.__TSR_index
2280
2250
 
@@ -2298,8 +2268,12 @@ export class RouterCore<
2298
2268
  ...(rest as any),
2299
2269
  _includeValidateSearch: true,
2300
2270
  })
2271
+ if (_redirects) {
2272
+ ;(location as typeof location & { _redirects?: number })._redirects =
2273
+ _redirects
2274
+ }
2301
2275
 
2302
- this.pendingBuiltLocation = location as ParsedLocation<
2276
+ this._pendingLocation = location as ParsedLocation<
2303
2277
  FullSearchSchema<TRouteTree>
2304
2278
  >
2305
2279
 
@@ -2315,8 +2289,8 @@ export class RouterCore<
2315
2289
  // Clear pending location after commit starts
2316
2290
  // We do this on next microtask to allow synchronous navigate calls to chain
2317
2291
  queueMicrotask(() => {
2318
- if (this.pendingBuiltLocation === location) {
2319
- this.pendingBuiltLocation = undefined
2292
+ if (this._pendingLocation === location) {
2293
+ this._pendingLocation = undefined
2320
2294
  }
2321
2295
  })
2322
2296
 
@@ -2412,254 +2386,17 @@ export class RouterCore<
2412
2386
  })
2413
2387
  }
2414
2388
 
2415
- latestLoadPromise: undefined | Promise<void>
2416
-
2417
- beforeLoad = () => {
2418
- // Cancel any pending matches
2419
- this.cancelMatches()
2420
- this.updateLatestLocation()
2421
-
2422
- if (isServer ?? this.isServer) {
2423
- // for SPAs on the initial load, this is handled by the Transitioner
2424
- const nextLocation = this.buildLocation({
2425
- to: this.latestLocation.pathname,
2426
- search: true,
2427
- params: true,
2428
- hash: true,
2429
- state: true,
2430
- _includeValidateSearch: true,
2431
- })
2432
-
2433
- // Check if location changed - origin check is unnecessary since buildLocation
2434
- // always uses this.origin when constructing URLs
2435
- if (this.latestLocation.publicHref !== nextLocation.publicHref) {
2436
- const href = this.getParsedLocationHref(nextLocation)
2437
- if (nextLocation.external) {
2438
- throw redirect({ href })
2439
- } else {
2440
- throw redirect({ href, _builtLocation: nextLocation })
2441
- }
2442
- }
2443
- }
2444
-
2445
- // Match the routes
2446
- const pendingMatches = this.matchRoutes(this.latestLocation)
2447
-
2448
- const nextCachedMatches = this.stores.cachedMatches
2449
- .get()
2450
- .filter((d) => !pendingMatches.some((e) => e.id === d.id))
2451
-
2452
- // Ingest the new matches
2453
- this.batch(() => {
2454
- this.stores.status.set('pending')
2455
- this.stores.statusCode.set(200)
2456
- this.stores.isLoading.set(true)
2457
- this.stores.location.set(this.latestLocation)
2458
- this.stores.setPending(pendingMatches)
2459
- // If a cached match moved to pending matches, remove it from cached matches
2460
- this.stores.setCached(nextCachedMatches)
2461
- })
2462
- }
2463
-
2464
2389
  load: LoadFn = async (opts): Promise<void> => {
2465
- const historyAction = opts?.action?.type
2466
- let redirect: AnyRedirect | undefined
2467
- let notFound: NotFoundError | undefined
2468
- let loadPromise: Promise<void>
2469
- const previousLocation =
2470
- this.stores.resolvedLocation.get() ?? this.stores.location.get()
2471
-
2472
- // eslint-disable-next-line prefer-const
2473
- loadPromise = new Promise<void>((resolve) => {
2474
- this.startTransition(async () => {
2475
- try {
2476
- this.beforeLoad()
2477
- if (historyAction) {
2478
- locationHistoryActions.set(this.latestLocation, historyAction)
2479
- } else {
2480
- locationHistoryActions.delete(this.latestLocation)
2481
- }
2482
- const next = this.latestLocation
2483
- const prevLocation = this.stores.resolvedLocation.get()
2484
- const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
2485
-
2486
- if (!this.stores.redirect.get()) {
2487
- this.emit({
2488
- type: 'onBeforeNavigate',
2489
- ...locationChangeInfo,
2490
- })
2491
- }
2492
-
2493
- this.emit({
2494
- type: 'onBeforeLoad',
2495
- ...locationChangeInfo,
2496
- })
2497
-
2498
- await loadMatches({
2499
- router: this,
2500
- sync: opts?.sync,
2501
- forceStaleReload: previousLocation.href === next.href,
2502
- matches: this.stores.pendingMatches.get(),
2503
- location: next,
2504
- updateMatch: this.updateMatch,
2505
- // eslint-disable-next-line @typescript-eslint/require-await
2506
- onReady: async () => {
2507
- // Wrap batch in framework-specific transition wrapper (e.g., Solid's startTransition)
2508
- this.startTransition(() => {
2509
- this.startViewTransition(async () => {
2510
- // this.viewTransitionPromise = createControlledPromise<true>()
2511
-
2512
- // Commit the pending matches. If a previous match was
2513
- // removed, place it in the cachedMatches
2514
- //
2515
- // exitingMatches uses match.id (routeId + params + loaderDeps) so
2516
- // navigating /foo?page=1 → /foo?page=2 correctly caches the page=1 entry.
2517
- let exitingMatches: Array<AnyRouteMatch> | null = null
2518
-
2519
- // Lifecycle-hook identity uses routeId only so that navigating between
2520
- // different params/deps of the same route fires onStay (not onLeave+onEnter).
2521
- let hookExitingMatches: Array<AnyRouteMatch> | null = null
2522
- let hookEnteringMatches: Array<AnyRouteMatch> | null = null
2523
- let hookStayingMatches: Array<AnyRouteMatch> | null = null
2524
-
2525
- this.batch(() => {
2526
- const pendingMatches = this.stores.pendingMatches.get()
2527
- const mountPending = pendingMatches.length
2528
- const currentMatches = this.stores.matches.get()
2529
-
2530
- exitingMatches = mountPending
2531
- ? currentMatches.filter(
2532
- (match) =>
2533
- !this.stores.pendingMatchStores.has(match.id),
2534
- )
2535
- : null
2536
-
2537
- // Lifecycle-hook identity: routeId only (route presence in tree)
2538
- // Build routeId sets from pools to avoid derived stores.
2539
- const pendingRouteIds = new Set<string>()
2540
- for (const s of this.stores.pendingMatchStores.values()) {
2541
- if (s.routeId) pendingRouteIds.add(s.routeId)
2542
- }
2543
- const activeRouteIds = new Set<string>()
2544
- for (const s of this.stores.matchStores.values()) {
2545
- if (s.routeId) activeRouteIds.add(s.routeId)
2546
- }
2547
-
2548
- hookExitingMatches = mountPending
2549
- ? currentMatches.filter(
2550
- (match) => !pendingRouteIds.has(match.routeId),
2551
- )
2552
- : null
2553
- hookEnteringMatches = mountPending
2554
- ? pendingMatches.filter(
2555
- (match) => !activeRouteIds.has(match.routeId),
2556
- )
2557
- : null
2558
- hookStayingMatches = mountPending
2559
- ? pendingMatches.filter((match) =>
2560
- activeRouteIds.has(match.routeId),
2561
- )
2562
- : currentMatches
2563
-
2564
- this.stores.isLoading.set(false)
2565
- this.stores.loadedAt.set(Date.now())
2566
- /**
2567
- * When committing new matches, cache any exiting matches that are still usable.
2568
- * Routes that resolved with `status: 'error'` or `status: 'notFound'` are
2569
- * deliberately excluded from `cachedMatches` so that subsequent invalidations
2570
- * or reloads re-run their loaders instead of reusing the failed/not-found data.
2571
- */
2572
- if (mountPending) {
2573
- this.stores.setMatches(pendingMatches)
2574
- this.stores.setPending([])
2575
- this.stores.setCached([
2576
- ...this.stores.cachedMatches.get(),
2577
- ...exitingMatches!.filter(
2578
- (d) =>
2579
- d.status !== 'error' &&
2580
- d.status !== 'notFound' &&
2581
- d.status !== 'redirected',
2582
- ),
2583
- ])
2584
- this.clearExpiredCache()
2585
- }
2586
- })
2587
-
2588
- //
2589
- for (const [matches, hook] of [
2590
- [hookExitingMatches, 'onLeave'],
2591
- [hookEnteringMatches, 'onEnter'],
2592
- [hookStayingMatches, 'onStay'],
2593
- ] as const) {
2594
- if (!matches) continue
2595
- for (const match of matches as Array<AnyRouteMatch>) {
2596
- this.looseRoutesById[match.routeId]!.options[hook]?.(
2597
- match,
2598
- )
2599
- }
2600
- }
2601
- })
2602
- })
2603
- },
2604
- })
2605
- } catch (err) {
2606
- if (isRedirect(err)) {
2607
- redirect = err
2608
- if (!(isServer ?? this.isServer)) {
2609
- this.navigate({
2610
- ...redirect.options,
2611
- replace: true,
2612
- ignoreBlocker: true,
2613
- })
2614
- }
2615
- } else if (isNotFound(err)) {
2616
- notFound = err
2617
- }
2618
-
2619
- const nextStatusCode = redirect
2620
- ? redirect.status
2621
- : notFound
2622
- ? 404
2623
- : this.stores.matches.get().some((d) => d.status === 'error')
2624
- ? 500
2625
- : 200
2626
-
2627
- this.batch(() => {
2628
- this.stores.statusCode.set(nextStatusCode)
2629
- this.stores.redirect.set(redirect)
2630
- })
2631
- }
2632
-
2633
- if (this.latestLoadPromise === loadPromise) {
2634
- this.commitLocationPromise?.resolve()
2635
- this.latestLoadPromise = undefined
2636
- this.commitLocationPromise = undefined
2637
- }
2638
-
2639
- resolve()
2640
- })
2641
- })
2642
-
2643
- this.latestLoadPromise = loadPromise
2644
-
2645
- await loadPromise
2646
-
2647
- while (
2648
- (this.latestLoadPromise as any) &&
2649
- loadPromise !== this.latestLoadPromise
2650
- ) {
2651
- await this.latestLoadPromise
2390
+ if (isServer ?? this.isServer) {
2391
+ return loadServerRoute(this, opts)
2652
2392
  }
2653
2393
 
2654
- let newStatusCode: number | undefined = undefined
2655
- if (this.hasNotFoundMatch()) {
2656
- newStatusCode = 404
2657
- } else if (this.stores.matches.get().some((d) => d.status === 'error')) {
2658
- newStatusCode = 500
2659
- }
2660
- if (newStatusCode !== undefined) {
2661
- this.stores.statusCode.set(newStatusCode)
2394
+ this.updateLatestLocation()
2395
+ if (opts?.action) {
2396
+ this._scroll.hash =
2397
+ opts.action.type === 'PUSH' || opts.action.type === 'REPLACE'
2662
2398
  }
2399
+ await loadClientRoute(this, opts)
2663
2400
  }
2664
2401
 
2665
2402
  startViewTransition = (fn: () => Promise<void>) => {
@@ -2674,9 +2411,8 @@ export class RouterCore<
2674
2411
  // Attempt to start a view transition (or just apply the changes if we can't)
2675
2412
  if (
2676
2413
  shouldViewTransition &&
2677
- typeof document !== 'undefined' &&
2678
- 'startViewTransition' in document &&
2679
- typeof document.startViewTransition === 'function'
2414
+ !(isServer ?? typeof document === 'undefined') &&
2415
+ typeof (document as any).startViewTransition === 'function'
2680
2416
  ) {
2681
2417
  // lib.dom.ts doesn't support viewTransition types variant yet.
2682
2418
  // TODO: Fix this when dom types are updated
@@ -2684,7 +2420,7 @@ export class RouterCore<
2684
2420
 
2685
2421
  if (
2686
2422
  typeof shouldViewTransition === 'object' &&
2687
- this.isViewTransitionTypesSupported
2423
+ window.CSS?.supports?.('selector(:active-view-transition-type(a))')
2688
2424
  ) {
2689
2425
  const next = this.latestLocation
2690
2426
  const prevLocation = this.stores.resolvedLocation.get()
@@ -2697,8 +2433,7 @@ export class RouterCore<
2697
2433
  : shouldViewTransition.types
2698
2434
 
2699
2435
  if (resolvedViewTransitionTypes === false) {
2700
- fn()
2701
- return
2436
+ return fn()
2702
2437
  }
2703
2438
 
2704
2439
  startViewTransitionParams = {
@@ -2709,58 +2444,19 @@ export class RouterCore<
2709
2444
  startViewTransitionParams = fn
2710
2445
  }
2711
2446
 
2712
- document.startViewTransition(startViewTransitionParams)
2713
- } else {
2714
- fn()
2447
+ return (document as any).startViewTransition(startViewTransitionParams)
2448
+ .updateCallbackDone
2715
2449
  }
2716
- }
2717
-
2718
- updateMatch: UpdateMatchFn = (id, updater) => {
2719
- this.startTransition(() => {
2720
- const pendingMatch = this.stores.pendingMatchStores.get(id)
2721
- if (pendingMatch) {
2722
- pendingMatch.set(updater)
2723
- return
2724
- }
2725
-
2726
- const activeMatch = this.stores.matchStores.get(id)
2727
- if (activeMatch) {
2728
- activeMatch.set(updater)
2729
- return
2730
- }
2731
-
2732
- const cachedMatch = this.stores.cachedMatchStores.get(id)
2733
- if (cachedMatch) {
2734
- const next = updater(cachedMatch.get())
2735
- if (next.status === 'redirected') {
2736
- const deleted = this.stores.cachedMatchStores.delete(id)
2737
- if (deleted) {
2738
- this.stores.cachedIds.set((prev) =>
2739
- prev.filter((matchId) => matchId !== id),
2740
- )
2741
- }
2742
- } else {
2743
- cachedMatch.set(next)
2744
- }
2745
- }
2746
- })
2747
- }
2748
-
2749
- getMatch: GetMatchFn = (matchId: string): AnyRouteMatch | undefined => {
2750
- return (
2751
- this.stores.cachedMatchStores.get(matchId)?.get() ??
2752
- this.stores.pendingMatchStores.get(matchId)?.get() ??
2753
- this.stores.matchStores.get(matchId)?.get()
2754
- )
2450
+ return fn()
2755
2451
  }
2756
2452
 
2757
2453
  /**
2758
2454
  * Invalidate the current matches and optionally force them back into a pending state.
2759
2455
  *
2760
2456
  * - Marks all matches that pass the optional `filter` as `invalid: true`.
2761
- * - If `forcePending` is true, or a match is currently in `'error'` or `'notFound'` status,
2762
- * its status is reset to `'pending'` and its `error` cleared so that the loader is re-run
2763
- * on the next `load()` call (eg. after HMR or a manual invalidation).
2457
+ *
2458
+ * The next load decides when to publish pending UI, so invalidation does not
2459
+ * mutate the currently rendered status.
2764
2460
  */
2765
2461
  invalidate: InvalidateFn<
2766
2462
  RouterCore<
@@ -2771,44 +2467,54 @@ export class RouterCore<
2771
2467
  TDehydrated
2772
2468
  >
2773
2469
  > = (opts) => {
2470
+ const committedMatches = this._committed
2471
+ const filter = opts?.filter
2472
+ const invalidIds = filter
2473
+ ? new Set(
2474
+ [...committedMatches, ...this._cache.values()]
2475
+ .filter((match) => filter(match as MakeRouteMatchUnion<this>))
2476
+ .map((match) => match.id),
2477
+ )
2478
+ : undefined
2774
2479
  const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
2775
- if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
2776
- return {
2480
+ if (!invalidIds || invalidIds.has(d.id)) {
2481
+ const route = this.routesById[d.routeId] as AnyRoute
2482
+ const next = {
2777
2483
  ...d,
2778
2484
  invalid: true,
2779
- ...(opts?.forcePending ||
2780
- d.status === 'error' ||
2781
- d.status === 'notFound'
2485
+ ...((opts?.forcePending ||
2486
+ d.status === 'error' ||
2487
+ d.status === 'notFound') &&
2488
+ routeNeedsLoad(route)
2782
2489
  ? ({ status: 'pending', error: undefined } as const)
2783
2490
  : undefined),
2784
2491
  }
2492
+ // Invalidation replaces this owner; it does not create another lease.
2493
+ ;(d as AnyRouteMatch & { _flight?: LoaderFlight })._flight = undefined
2494
+ return next
2785
2495
  }
2786
2496
  return d
2787
2497
  }
2788
2498
 
2789
- this.batch(() => {
2790
- this.stores.setMatches(this.stores.matches.get().map(invalidate))
2791
- this.stores.setCached(this.stores.cachedMatches.get().map(invalidate))
2792
- this.stores.setPending(this.stores.pendingMatches.get().map(invalidate))
2793
- })
2499
+ const committed = committedMatches.map(invalidate)
2500
+ this._committed = committed
2501
+ const cache = new Map<string, AnyRouteMatch>()
2502
+ for (const [id, match] of this._cache) {
2503
+ cache.set(id, invalidate(match))
2504
+ }
2505
+ this._cache = cache
2794
2506
 
2795
2507
  this.shouldViewTransition = false
2796
2508
  return this.load({ sync: opts?.sync })
2797
2509
  }
2798
2510
 
2799
- getParsedLocationHref = (location: ParsedLocation) => {
2800
- // For redirects and external use, we need publicHref (with rewrite output applied)
2801
- // href is the internal path after rewrite input, publicHref is user-facing
2802
- return location.publicHref || '/'
2803
- }
2804
-
2805
2511
  resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2806
2512
  const locationHeader = redirect.headers.get('Location')
2807
2513
 
2808
2514
  if (!redirect.options.href || redirect.options._builtLocation) {
2809
2515
  const location =
2810
2516
  redirect.options._builtLocation ?? this.buildLocation(redirect.options)
2811
- const href = this.getParsedLocationHref(location)
2517
+ const href = location.publicHref || '/'
2812
2518
  redirect.options.href = href
2813
2519
  redirect.headers.set('Location', href)
2814
2520
  } else if (locationHeader) {
@@ -2845,43 +2551,37 @@ export class RouterCore<
2845
2551
  }
2846
2552
 
2847
2553
  clearCache: ClearCacheFn<this> = (opts) => {
2554
+ const cached = this._cache
2555
+ const preloads = this._preloads
2848
2556
  const filter = opts?.filter
2849
- if (filter !== undefined) {
2850
- this.stores.setCached(
2851
- this.stores.cachedMatches
2852
- .get()
2853
- .filter((m) => !filter(m as MakeRouteMatchUnion<this>)),
2854
- )
2855
- } else {
2856
- this.stores.setCached([])
2557
+ const retained = new Map<string, AnyRouteMatch>()
2558
+ const discarded: Array<AnyRouteMatch> = []
2559
+ for (const [id, match] of cached) {
2560
+ if (filter && !filter(match as MakeRouteMatchUnion<this>)) {
2561
+ retained.set(id, match)
2562
+ } else {
2563
+ discarded.push(match)
2564
+ }
2857
2565
  }
2858
- }
2859
-
2860
- clearExpiredCache = () => {
2861
- const now = Date.now()
2862
- // This is where all of the garbage collection magic happens
2863
- const filter = (d: MakeRouteMatch<TRouteTree>) => {
2864
- const route = this.looseRoutesById[d.routeId]!
2865
-
2866
- if (!route.options.loader) {
2867
- return true
2566
+ const retainedPreloads = new Map<string, ActivePreload>()
2567
+ const discardedPreloads: Array<ActivePreload> = []
2568
+ for (const [href, preload] of preloads ?? []) {
2569
+ if (!filter || preload[0].some(filter as any)) {
2570
+ discardedPreloads.push(preload)
2571
+ discarded.push(...preload[0])
2572
+ } else {
2573
+ retainedPreloads.set(href, preload)
2868
2574
  }
2575
+ }
2869
2576
 
2870
- // If the route was preloaded, use the preloadGcTime
2871
- // otherwise, use the gcTime
2872
- const gcTime =
2873
- (d.preload
2874
- ? (route.options.preloadGcTime ?? this.options.defaultPreloadGcTime)
2875
- : (route.options.gcTime ?? this.options.defaultGcTime)) ??
2876
- 5 * 60 * 1000
2877
-
2878
- const isError = d.status === 'error'
2879
- if (isError) return true
2880
-
2881
- const gcEligible = now - d.updatedAt >= gcTime
2882
- return gcEligible
2577
+ // Install both replacement authorities before releasing a public loader
2578
+ // signal, whose abort listeners can synchronously reenter the router.
2579
+ this._cache = retained
2580
+ this._preloads = retainedPreloads
2581
+ transferMatchResources(this, discarded)
2582
+ for (const preload of discardedPreloads) {
2583
+ preload[1].abort()
2883
2584
  }
2884
- this.clearCache({ filter })
2885
2585
  }
2886
2586
 
2887
2587
  loadRouteChunk = loadRouteChunk
@@ -2891,69 +2591,7 @@ export class RouterCore<
2891
2591
  TTrailingSlashOption,
2892
2592
  TDefaultStructuralSharingOption,
2893
2593
  TRouterHistory
2894
- > = async (opts) => {
2895
- const next = opts._builtLocation ?? this.buildLocation(opts as any)
2896
-
2897
- let matches = this.matchRoutes(next, {
2898
- throwOnError: true,
2899
- preload: true,
2900
- dest: opts,
2901
- })
2902
-
2903
- const activeMatchIds = new Set([
2904
- ...this.stores.matchesId.get(),
2905
- ...this.stores.pendingIds.get(),
2906
- ])
2907
-
2908
- const loadedMatchIds = new Set([
2909
- ...activeMatchIds,
2910
- ...this.stores.cachedIds.get(),
2911
- ])
2912
-
2913
- // If the matches are already loaded, we need to add them to the cached matches.
2914
- const matchesToCache = matches.filter(
2915
- (match) => !loadedMatchIds.has(match.id),
2916
- )
2917
- if (matchesToCache.length) {
2918
- const cachedMatches = this.stores.cachedMatches.get()
2919
- this.stores.setCached([...cachedMatches, ...matchesToCache])
2920
- }
2921
-
2922
- try {
2923
- matches = await loadMatches({
2924
- router: this,
2925
- matches,
2926
- location: next,
2927
- preload: true,
2928
- updateMatch: (id, updater) => {
2929
- // Don't update the match if it's currently loaded
2930
- if (activeMatchIds.has(id)) {
2931
- matches = matches.map((d) => (d.id === id ? updater(d) : d))
2932
- } else {
2933
- this.updateMatch(id, updater)
2934
- }
2935
- },
2936
- })
2937
-
2938
- return matches
2939
- } catch (err) {
2940
- if (isRedirect(err)) {
2941
- if (err.options.reloadDocument) {
2942
- return undefined
2943
- }
2944
-
2945
- return await this.preloadRoute({
2946
- ...err.options,
2947
- _fromLocation: next,
2948
- })
2949
- }
2950
- if (!isNotFound(err)) {
2951
- // Preload errors are not fatal, but we should still log them
2952
- console.error(err)
2953
- }
2954
- return undefined
2955
- }
2956
- }
2594
+ > = (opts) => preloadClientRoute(this, opts)
2957
2595
 
2958
2596
  matchRoute: MatchRouteFn<
2959
2597
  TRouteTree,
@@ -2971,12 +2609,12 @@ export class RouterCore<
2971
2609
  }
2972
2610
  const next = this.buildLocation(matchLocation as any)
2973
2611
 
2974
- if (opts?.pending && this.stores.status.get() !== 'pending') {
2612
+ const isPending = this.stores.status.get() === 'pending'
2613
+ if (opts?.pending && !isPending) {
2975
2614
  return false
2976
2615
  }
2977
2616
 
2978
- const pending =
2979
- opts?.pending === undefined ? !this.stores.isLoading.get() : opts.pending
2617
+ const pending = opts?.pending ?? !isPending
2980
2618
 
2981
2619
  const baseLocation = pending
2982
2620
  ? this.latestLocation
@@ -3016,11 +2654,19 @@ export class RouterCore<
3016
2654
  serverSsr?: ServerSsr
3017
2655
 
3018
2656
  serverSsrLifecycle?: RouterSsrLifecycle
2657
+ }
3019
2658
 
3020
- hasNotFoundMatch = () => {
3021
- return this.stores.matches
3022
- .get()
3023
- .some((d) => d.status === 'notFound' || d.globalNotFound)
2659
+ /**
2660
+ * In non-production environments,
2661
+ * augment the RouterCore class with a `_refreshRoute` method
2662
+ * dedicated to HMR.
2663
+ */
2664
+ if (process.env.NODE_ENV !== 'production') {
2665
+ RouterCore.prototype._replaceRouteChunk = replaceRouteChunk
2666
+ RouterCore.prototype._refreshRoute = async function () {
2667
+ this._serverResult = undefined
2668
+ this.updateLatestLocation()
2669
+ await refreshClientRoute(this)
3024
2670
  }
3025
2671
  }
3026
2672
 
@@ -3057,14 +2703,11 @@ export function getInitialRouterState(
3057
2703
  location: ParsedLocation,
3058
2704
  ): RouterState<any> {
3059
2705
  return {
3060
- loadedAt: 0,
3061
2706
  isLoading: false,
3062
- isTransitioning: false,
3063
2707
  status: 'idle',
3064
2708
  resolvedLocation: undefined,
3065
2709
  location,
3066
2710
  matches: [],
3067
- statusCode: 200,
3068
2711
  }
3069
2712
  }
3070
2713
 
@@ -3096,56 +2739,12 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3096
2739
  return {}
3097
2740
  }
3098
2741
 
3099
- /**
3100
- * Build the matched route chain and extract params for a pathname.
3101
- * Falls back to the root route if no specific route is found.
3102
- */
3103
- export function getMatchedRoutes<TRouteLike extends RouteLike>({
3104
- pathname,
3105
- routesById,
3106
- processedTree,
3107
- }: {
3108
- pathname: string
3109
- routesById: Record<string, TRouteLike>
3110
- processedTree: ProcessedTree<any, any, any>
3111
- }) {
3112
- const routeParams: Record<string, string> = Object.create(null)
3113
- const trimmedPath = trimPathRight(pathname)
3114
-
3115
- let foundRoute: TRouteLike | undefined = undefined
3116
- const match = findRouteMatch<TRouteLike>(trimmedPath, processedTree, true)
3117
- if (match) {
3118
- foundRoute = match.route
3119
- Object.assign(routeParams, match.rawParams) // Copy params, because they're cached
3120
- }
3121
-
3122
- const matchedRoutes = match?.branch || [routesById[rootRouteId]!]
3123
-
3124
- return { matchedRoutes, routeParams, foundRoute }
3125
- }
3126
-
3127
- /**
3128
- * TODO: once caches are persisted across requests on the server,
3129
- * we can cache the built middleware chain using `last(destRoutes)` as the key
3130
- */
3131
- function applySearchMiddleware({
3132
- search,
3133
- dest,
3134
- destRoutes,
3135
- _includeValidateSearch,
3136
- }: {
3137
- search: any
3138
- dest: { search?: unknown }
3139
- destRoutes: ReadonlyArray<AnyRoute>
3140
- _includeValidateSearch: boolean | undefined
3141
- }) {
3142
- const middleware = buildMiddlewareChain(destRoutes)
3143
- return middleware(search, dest, _includeValidateSearch ?? false)
3144
- }
3145
-
3146
- function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3147
- let dest: BuildNextOptions
3148
- let includeValidateSearch: boolean | undefined
2742
+ function applySearchMiddleware(
2743
+ search: any,
2744
+ dest: BuildNextOptions,
2745
+ destRoutes: ReadonlyArray<AnyRoute>,
2746
+ includeValidateSearch: boolean | undefined,
2747
+ ) {
3149
2748
  const middlewares = [] as Array<SearchMiddleware<any>>
3150
2749
 
3151
2750
  for (const route of destRoutes) {
@@ -3238,15 +2837,7 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3238
2837
  return (middlewares[index]! as any)({ search: currentSearch, next, meta })
3239
2838
  }
3240
2839
 
3241
- return function middleware(
3242
- search: any,
3243
- nextDest: BuildNextOptions,
3244
- _includeValidateSearch: boolean,
3245
- ) {
3246
- dest = nextDest
3247
- includeValidateSearch = _includeValidateSearch
3248
- return applyNext(0, search)
3249
- }
2840
+ return applyNext(0, search)
3250
2841
  }
3251
2842
 
3252
2843
  function findGlobalNotFoundRouteId(
@@ -3254,26 +2845,41 @@ function findGlobalNotFoundRouteId(
3254
2845
  routes: ReadonlyArray<AnyRoute>,
3255
2846
  ) {
3256
2847
  if (notFoundMode !== 'root') {
2848
+ let fallback
3257
2849
  for (let i = routes.length - 1; i >= 0; i--) {
3258
2850
  const route = routes[i]!
3259
- if (route.children) {
2851
+ if (route.options.notFoundComponent) {
3260
2852
  return route.id
3261
2853
  }
2854
+ fallback ||= route.children && route.id
2855
+ }
2856
+ if (fallback) {
2857
+ return fallback
3262
2858
  }
3263
2859
  }
3264
2860
  return rootRouteId
3265
2861
  }
3266
2862
 
2863
+ function resolveNextParams(
2864
+ spec: unknown,
2865
+ base: Record<string, unknown>,
2866
+ ): Record<string, unknown> {
2867
+ return spec === false || spec === null
2868
+ ? Object.create(null)
2869
+ : (spec ?? true) === true
2870
+ ? base
2871
+ : Object.assign(base, functionalUpdate(spec as any, base))
2872
+ }
2873
+
3267
2874
  function extractStrictParams(
3268
2875
  route: AnyRoute,
3269
2876
  accumulatedParams: Record<string, unknown>,
3270
2877
  ) {
3271
2878
  const parseParams = route.options.params?.parse ?? route.options.parseParams
3272
2879
  if (parseParams) {
3273
- const result = parseParams(accumulatedParams as Record<string, string>)
3274
- if (result === false) {
3275
- throw new Error('Route params.parse returned false for a matched route')
3276
- }
3277
- Object.assign(accumulatedParams, result)
2880
+ Object.assign(
2881
+ accumulatedParams,
2882
+ parseParams(accumulatedParams as Record<string, string>),
2883
+ )
3278
2884
  }
3279
2885
  }