@tanstack/router-core 1.171.15 → 1.171.16

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