@tanstack/router-core 1.132.0-alpha.1 → 1.132.0-alpha.15

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 (145) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +9 -11
  3. package/dist/cjs/config.cjs +10 -0
  4. package/dist/cjs/config.cjs.map +1 -0
  5. package/dist/cjs/config.d.cts +17 -0
  6. package/dist/cjs/fileRoute.d.cts +3 -2
  7. package/dist/cjs/index.cjs +15 -3
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +11 -4
  10. package/dist/cjs/load-matches.cjs +636 -0
  11. package/dist/cjs/load-matches.cjs.map +1 -0
  12. package/dist/cjs/load-matches.d.cts +16 -0
  13. package/dist/cjs/location.d.cts +38 -0
  14. package/dist/cjs/path.cjs +6 -49
  15. package/dist/cjs/path.cjs.map +1 -1
  16. package/dist/cjs/path.d.cts +3 -6
  17. package/dist/cjs/qss.cjs +19 -19
  18. package/dist/cjs/qss.cjs.map +1 -1
  19. package/dist/cjs/qss.d.cts +6 -4
  20. package/dist/cjs/redirect.cjs +3 -3
  21. package/dist/cjs/redirect.cjs.map +1 -1
  22. package/dist/cjs/rewrite.cjs +63 -0
  23. package/dist/cjs/rewrite.cjs.map +1 -0
  24. package/dist/cjs/rewrite.d.cts +22 -0
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/route.d.cts +42 -41
  27. package/dist/cjs/router.cjs +134 -681
  28. package/dist/cjs/router.cjs.map +1 -1
  29. package/dist/cjs/router.d.cts +68 -25
  30. package/dist/cjs/scroll-restoration.cjs +32 -29
  31. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  32. package/dist/cjs/scroll-restoration.d.cts +1 -10
  33. package/dist/cjs/searchParams.cjs +7 -15
  34. package/dist/cjs/searchParams.cjs.map +1 -1
  35. package/dist/cjs/ssr/constants.cjs +5 -0
  36. package/dist/cjs/ssr/constants.cjs.map +1 -0
  37. package/dist/cjs/ssr/constants.d.cts +1 -0
  38. package/dist/cjs/ssr/{seroval-plugins.cjs → serializer/ShallowErrorPlugin.cjs} +2 -2
  39. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -0
  40. package/dist/cjs/ssr/{seroval-plugins.d.cts → serializer/ShallowErrorPlugin.d.cts} +1 -2
  41. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +11 -0
  42. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -0
  43. package/dist/cjs/ssr/serializer/seroval-plugins.d.cts +2 -0
  44. package/dist/cjs/ssr/serializer/transformer.cjs +52 -0
  45. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -0
  46. package/dist/cjs/ssr/serializer/transformer.d.cts +56 -0
  47. package/dist/cjs/ssr/server.d.cts +5 -0
  48. package/dist/cjs/ssr/ssr-client.cjs +53 -40
  49. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  50. package/dist/cjs/ssr/ssr-client.d.cts +5 -1
  51. package/dist/cjs/ssr/ssr-server.cjs +12 -10
  52. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  53. package/dist/cjs/ssr/ssr-server.d.cts +0 -1
  54. package/dist/cjs/ssr/tsrScript.cjs +1 -1
  55. package/dist/cjs/ssr/tsrScript.cjs.map +1 -1
  56. package/dist/cjs/typePrimitives.d.cts +6 -6
  57. package/dist/cjs/utils.cjs +14 -7
  58. package/dist/cjs/utils.cjs.map +1 -1
  59. package/dist/cjs/utils.d.cts +2 -1
  60. package/dist/esm/Matches.d.ts +9 -11
  61. package/dist/esm/Matches.js.map +1 -1
  62. package/dist/esm/config.d.ts +17 -0
  63. package/dist/esm/config.js +10 -0
  64. package/dist/esm/config.js.map +1 -0
  65. package/dist/esm/fileRoute.d.ts +3 -2
  66. package/dist/esm/index.d.ts +11 -4
  67. package/dist/esm/index.js +17 -5
  68. package/dist/esm/index.js.map +1 -1
  69. package/dist/esm/load-matches.d.ts +16 -0
  70. package/dist/esm/load-matches.js +636 -0
  71. package/dist/esm/load-matches.js.map +1 -0
  72. package/dist/esm/location.d.ts +38 -0
  73. package/dist/esm/path.d.ts +3 -6
  74. package/dist/esm/path.js +6 -49
  75. package/dist/esm/path.js.map +1 -1
  76. package/dist/esm/qss.d.ts +6 -4
  77. package/dist/esm/qss.js +19 -19
  78. package/dist/esm/qss.js.map +1 -1
  79. package/dist/esm/redirect.js +3 -3
  80. package/dist/esm/redirect.js.map +1 -1
  81. package/dist/esm/rewrite.d.ts +22 -0
  82. package/dist/esm/rewrite.js +63 -0
  83. package/dist/esm/rewrite.js.map +1 -0
  84. package/dist/esm/route.d.ts +42 -41
  85. package/dist/esm/route.js.map +1 -1
  86. package/dist/esm/router.d.ts +68 -25
  87. package/dist/esm/router.js +136 -683
  88. package/dist/esm/router.js.map +1 -1
  89. package/dist/esm/scroll-restoration.d.ts +1 -10
  90. package/dist/esm/scroll-restoration.js +32 -29
  91. package/dist/esm/scroll-restoration.js.map +1 -1
  92. package/dist/esm/searchParams.js +7 -15
  93. package/dist/esm/searchParams.js.map +1 -1
  94. package/dist/esm/ssr/constants.d.ts +1 -0
  95. package/dist/esm/ssr/constants.js +5 -0
  96. package/dist/esm/ssr/constants.js.map +1 -0
  97. package/dist/esm/ssr/{seroval-plugins.d.ts → serializer/ShallowErrorPlugin.d.ts} +1 -2
  98. package/dist/esm/ssr/{seroval-plugins.js → serializer/ShallowErrorPlugin.js} +2 -2
  99. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -0
  100. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -0
  101. package/dist/esm/ssr/serializer/seroval-plugins.js +11 -0
  102. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -0
  103. package/dist/esm/ssr/serializer/transformer.d.ts +56 -0
  104. package/dist/esm/ssr/serializer/transformer.js +52 -0
  105. package/dist/esm/ssr/serializer/transformer.js.map +1 -0
  106. package/dist/esm/ssr/server.d.ts +5 -0
  107. package/dist/esm/ssr/ssr-client.d.ts +5 -1
  108. package/dist/esm/ssr/ssr-client.js +53 -40
  109. package/dist/esm/ssr/ssr-client.js.map +1 -1
  110. package/dist/esm/ssr/ssr-server.d.ts +0 -1
  111. package/dist/esm/ssr/ssr-server.js +12 -10
  112. package/dist/esm/ssr/ssr-server.js.map +1 -1
  113. package/dist/esm/ssr/tsrScript.js +1 -1
  114. package/dist/esm/ssr/tsrScript.js.map +1 -1
  115. package/dist/esm/typePrimitives.d.ts +6 -6
  116. package/dist/esm/utils.d.ts +2 -1
  117. package/dist/esm/utils.js +14 -7
  118. package/dist/esm/utils.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/Matches.ts +18 -10
  121. package/src/config.ts +42 -0
  122. package/src/fileRoute.ts +15 -3
  123. package/src/index.ts +32 -3
  124. package/src/load-matches.ts +955 -0
  125. package/src/location.ts +38 -0
  126. package/src/path.ts +5 -66
  127. package/src/qss.ts +27 -24
  128. package/src/redirect.ts +3 -3
  129. package/src/rewrite.ts +70 -0
  130. package/src/route.ts +146 -35
  131. package/src/router.ts +263 -972
  132. package/src/scroll-restoration.ts +42 -37
  133. package/src/searchParams.ts +8 -19
  134. package/src/ssr/constants.ts +1 -0
  135. package/src/ssr/{seroval-plugins.ts → serializer/ShallowErrorPlugin.ts} +2 -2
  136. package/src/ssr/serializer/seroval-plugins.ts +9 -0
  137. package/src/ssr/serializer/transformer.ts +215 -0
  138. package/src/ssr/server.ts +6 -0
  139. package/src/ssr/ssr-client.ts +72 -44
  140. package/src/ssr/ssr-server.ts +18 -10
  141. package/src/ssr/tsrScript.ts +5 -1
  142. package/src/typePrimitives.ts +6 -6
  143. package/src/utils.ts +21 -10
  144. package/dist/cjs/ssr/seroval-plugins.cjs.map +0 -1
  145. package/dist/esm/ssr/seroval-plugins.js.map +0 -1
package/src/router.ts CHANGED
@@ -1,16 +1,12 @@
1
1
  import { Store, batch } from '@tanstack/store'
2
- import {
3
- createBrowserHistory,
4
- createMemoryHistory,
5
- parseHref,
6
- } from '@tanstack/history'
2
+ import { createBrowserHistory, parseHref } from '@tanstack/history'
7
3
  import invariant from 'tiny-invariant'
8
4
  import {
9
5
  createControlledPromise,
10
6
  deepEqual,
7
+ findLast,
11
8
  functionalUpdate,
12
9
  last,
13
- pick,
14
10
  replaceEqualDeep,
15
11
  } from './utils'
16
12
  import {
@@ -20,7 +16,6 @@ import {
20
16
  SEGMENT_TYPE_WILDCARD,
21
17
  cleanPath,
22
18
  interpolatePath,
23
- joinPaths,
24
19
  matchPathname,
25
20
  parsePathname,
26
21
  resolvePath,
@@ -34,6 +29,13 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
34
29
  import { rootRouteId } from './root'
35
30
  import { isRedirect, redirect } from './redirect'
36
31
  import { createLRUCache } from './lru-cache'
32
+ import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches'
33
+ import {
34
+ composeRewrites,
35
+ executeRewriteInput,
36
+ executeRewriteOutput,
37
+ rewriteBasepath,
38
+ } from './rewrite'
37
39
  import type { ParsePathnameCache, Segment } from './path'
38
40
  import type { SearchParser, SearchSerializer } from './searchParams'
39
41
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
@@ -45,6 +47,7 @@ import type {
45
47
  } from '@tanstack/history'
46
48
  import type {
47
49
  Awaitable,
50
+ Constrain,
48
51
  ControlledPromise,
49
52
  NoInfer,
50
53
  NonNullableUpdater,
@@ -56,13 +59,10 @@ import type {
56
59
  AnyContext,
57
60
  AnyRoute,
58
61
  AnyRouteWithContext,
59
- BeforeLoadContextOptions,
60
- LoaderFnContext,
61
62
  MakeRemountDepsOptionsUnion,
62
63
  RouteContextOptions,
63
64
  RouteMask,
64
65
  SearchMiddleware,
65
- SsrContextOptions,
66
66
  } from './route'
67
67
  import type {
68
68
  FullSearchSchema,
@@ -86,6 +86,11 @@ import type { Manifest } from './manifest'
86
86
  import type { AnySchema, AnyValidator } from './validators'
87
87
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
88
88
  import type { NotFoundError } from './not-found'
89
+ import type {
90
+ AnySerializationAdapter,
91
+ ValidateSerializableInput,
92
+ } from './ssr/serializer/transformer'
93
+ import type { AnyRouterConfig } from './config'
89
94
 
90
95
  export type ControllablePromise<T = any> = Promise<T> & {
91
96
  resolve: (value: T) => void
@@ -96,6 +101,8 @@ export type InjectedHtmlEntry = Promise<string>
96
101
 
97
102
  export interface DefaultRegister {
98
103
  router: AnyRouter
104
+ config: AnyRouterConfig
105
+ ssr: SSROption
99
106
  }
100
107
 
101
108
  export interface Register extends DefaultRegister {
@@ -113,12 +120,14 @@ export interface DefaultRouterOptionsExtensions {}
113
120
  export interface RouterOptionsExtensions
114
121
  extends DefaultRouterOptionsExtensions {}
115
122
 
123
+ export type SSROption = boolean | 'data-only'
124
+
116
125
  export interface RouterOptions<
117
126
  TRouteTree extends AnyRoute,
118
127
  TTrailingSlashOption extends TrailingSlashOption,
119
128
  TDefaultStructuralSharingOption extends boolean = false,
120
129
  TRouterHistory extends RouterHistory = RouterHistory,
121
- TDehydrated extends Record<string, any> = Record<string, any>,
130
+ TDehydrated = undefined,
122
131
  > extends RouterOptionsExtensions {
123
132
  /**
124
133
  * The history object that will be used to manage the browser history.
@@ -263,6 +272,18 @@ export interface RouterOptions<
263
272
  /**
264
273
  * The basepath for then entire router. This is useful for mounting a router instance at a subpath.
265
274
  *
275
+ * @deprecated - use `rewrite.input` with the new `rewriteBasepath` utility instead:
276
+ * ```ts
277
+ * const router = createRouter({
278
+ * routeTree,
279
+ * rewrite: rewriteBasepath('/basepath')
280
+ * // Or wrap existing rewrite functionality
281
+ * rewrite: rewriteBasepath('/basepath', {
282
+ * output: ({ url }) => {...},
283
+ * input: ({ url }) => {...},
284
+ * })
285
+ * })
286
+ * ```
266
287
  * @default '/'
267
288
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#basepath-property)
268
289
  */
@@ -286,7 +307,10 @@ export interface RouterOptions<
286
307
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
287
308
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
288
309
  */
289
- dehydrate?: () => Awaitable<TDehydrated>
310
+ dehydrate?: () => Constrain<
311
+ TDehydrated,
312
+ ValidateSerializableInput<Register, TDehydrated>
313
+ >
290
314
  /**
291
315
  * A function that will be called when the router is hydrated.
292
316
  *
@@ -356,7 +380,7 @@ export interface RouterOptions<
356
380
  *
357
381
  * @default true
358
382
  */
359
- defaultSsr?: boolean | 'data-only'
383
+ defaultSsr?: SSROption
360
384
 
361
385
  search?: {
362
386
  /**
@@ -392,7 +416,9 @@ export interface RouterOptions<
392
416
  *
393
417
  * @default false
394
418
  */
395
- scrollRestoration?: boolean
419
+ scrollRestoration?:
420
+ | boolean
421
+ | ((opts: { location: ParsedLocation }) => boolean)
396
422
 
397
423
  /**
398
424
  * A function that will be called to get the key for the scroll restoration cache.
@@ -423,8 +449,48 @@ export interface RouterOptions<
423
449
  * @default false
424
450
  */
425
451
  disableGlobalCatchBoundary?: boolean
452
+
453
+ serializationAdapters?: ReadonlyArray<AnySerializationAdapter>
454
+ /**
455
+ * Configures how the router will rewrite the location between the actual href and the internal href of the router.
456
+ *
457
+ * @default undefined
458
+ * @description You can provide a custom rewrite pair (in/out) or use the utilities like `rewriteBasepath` as a convenience for common use cases, or even do both!
459
+ * This is useful for basepath rewriting, shifting data from the origin to the path (for things like )
460
+ */
461
+ rewrite?: LocationRewrite
462
+ origin?: string
426
463
  }
427
464
 
465
+ export type LocationRewrite = {
466
+ /**
467
+ * A function that will be called to rewrite the URL before it is interpreted by the router from the history instance.
468
+ * Utilities like `rewriteBasepath` are provided as a convenience for common use cases.
469
+ *
470
+ * @default undefined
471
+ */
472
+ input?: LocationRewriteFunction
473
+ /**
474
+ * A function that will be called to rewrite the URL before it is committed to the actual history instance from the router.
475
+ * Utilities like `rewriteBasepath` are provided as a convenience for common use cases.
476
+ *
477
+ * @default undefined
478
+ */
479
+ output?: LocationRewriteFunction
480
+ }
481
+
482
+ /**
483
+ * A function that will be called to rewrite the URL.
484
+ *
485
+ * @param url The URL to rewrite.
486
+ * @returns The rewritten URL (as a URL instance or full href string) or undefined if no rewrite is needed.
487
+ */
488
+ export type LocationRewriteFunction = ({
489
+ url,
490
+ }: {
491
+ url: URL
492
+ }) => undefined | string | URL
493
+
428
494
  export interface RouterState<
429
495
  in out TRouteTree extends AnyRoute = AnyRoute,
430
496
  in out TRouteMatch = MakeRouteMatchUnion,
@@ -614,8 +680,8 @@ export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
614
680
  }) => Promise<void>
615
681
 
616
682
  export type ParseLocationFn<TRouteTree extends AnyRoute> = (
683
+ locationToParse: HistoryLocation,
617
684
  previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
618
- locationToParse?: HistoryLocation,
619
685
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
620
686
 
621
687
  export type GetMatchRoutesFn = (
@@ -705,6 +771,7 @@ export interface ViewTransitionOptions {
705
771
  }) => Array<string>)
706
772
  }
707
773
 
774
+ // TODO where is this used? can we remove this?
708
775
  export function defaultSerializeError(err: unknown) {
709
776
  if (err instanceof Error) {
710
777
  const obj = {
@@ -794,7 +861,10 @@ export class RouterCore<
794
861
  'stringifySearch' | 'parseSearch' | 'context'
795
862
  >
796
863
  history!: TRouterHistory
864
+ rewrite?: LocationRewrite
865
+ origin?: string
797
866
  latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
867
+ // @deprecated - basepath functionality is now implemented via the `rewrite` option
798
868
  basepath!: string
799
869
  routeTree!: TRouteTree
800
870
  routesById!: RoutesById<TRouteTree>
@@ -858,7 +928,6 @@ export class RouterCore<
858
928
  )
859
929
  }
860
930
 
861
- const previousOptions = this.options
862
931
  this.options = {
863
932
  ...this.options,
864
933
  ...newOptions,
@@ -876,32 +945,42 @@ export class RouterCore<
876
945
  : undefined
877
946
 
878
947
  if (
879
- !this.basepath ||
880
- (newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
948
+ !this.history ||
949
+ (this.options.history && this.options.history !== this.history)
881
950
  ) {
882
- if (
883
- newOptions.basepath === undefined ||
884
- newOptions.basepath === '' ||
885
- newOptions.basepath === '/'
886
- ) {
887
- this.basepath = '/'
951
+ if (!this.options.history) {
952
+ if (!this.isServer) {
953
+ this.history = createBrowserHistory() as TRouterHistory
954
+ }
955
+ } else {
956
+ this.history = this.options.history
957
+ }
958
+ }
959
+ // For backwards compatibility, we support a basepath option, which we now implement as a rewrite
960
+ if (this.options.basepath) {
961
+ const basepathRewrite = rewriteBasepath({
962
+ basepath: this.options.basepath,
963
+ })
964
+ if (this.options.rewrite) {
965
+ this.rewrite = composeRewrites([basepathRewrite, this.options.rewrite])
888
966
  } else {
889
- this.basepath = `/${trimPath(newOptions.basepath)}`
967
+ this.rewrite = basepathRewrite
890
968
  }
969
+ } else {
970
+ this.rewrite = this.options.rewrite
891
971
  }
892
972
 
893
- if (
894
- !this.history ||
895
- (this.options.history && this.options.history !== this.history)
896
- ) {
897
- this.history =
898
- this.options.history ??
899
- ((this.isServer
900
- ? createMemoryHistory({
901
- initialEntries: [this.basepath || '/'],
902
- })
903
- : createBrowserHistory()) as TRouterHistory)
904
- this.latestLocation = this.parseLocation()
973
+ this.origin = this.options.origin
974
+ if (!this.origin) {
975
+ if (!this.isServer) {
976
+ this.origin = window.origin
977
+ } else {
978
+ // fallback for the server, can be overridden by calling router.update({origin}) on the server
979
+ this.origin = 'http://localhost'
980
+ }
981
+ }
982
+ if (this.history) {
983
+ this.updateLatestLocation()
905
984
  }
906
985
 
907
986
  if (this.options.routeTree !== this.routeTree) {
@@ -909,7 +988,7 @@ export class RouterCore<
909
988
  this.buildRouteTree()
910
989
  }
911
990
 
912
- if (!this.__store) {
991
+ if (!this.__store && this.latestLocation) {
913
992
  this.__store = new Store(getInitialRouterState(this.latestLocation), {
914
993
  onUpdate: () => {
915
994
  this.__store.state = {
@@ -935,10 +1014,17 @@ export class RouterCore<
935
1014
  }
936
1015
  }
937
1016
 
938
- get state() {
1017
+ get state(): RouterState<TRouteTree> {
939
1018
  return this.__store.state
940
1019
  }
941
1020
 
1021
+ updateLatestLocation = () => {
1022
+ this.latestLocation = this.parseLocation(
1023
+ this.history.location,
1024
+ this.latestLocation,
1025
+ )
1026
+ }
1027
+
942
1028
  buildRouteTree = () => {
943
1029
  const { routesById, routesByPath, flatRoutes } = processRouteTree({
944
1030
  routeTree: this.routeTree,
@@ -985,29 +1071,41 @@ export class RouterCore<
985
1071
  }
986
1072
 
987
1073
  parseLocation: ParseLocationFn<TRouteTree> = (
988
- previousLocation,
989
1074
  locationToParse,
1075
+ previousLocation,
990
1076
  ) => {
991
1077
  const parse = ({
992
- pathname,
993
- search,
994
- hash,
1078
+ href,
995
1079
  state,
996
1080
  }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
997
- const parsedSearch = this.options.parseSearch(search)
1081
+ // Before we do any processing, we need to allow rewrites to modify the URL
1082
+ // build up the full URL by combining the href from history with the router's origin
1083
+ const fullUrl = new URL(href, this.origin)
1084
+ const url = executeRewriteInput(this.rewrite, fullUrl)
1085
+
1086
+ const parsedSearch = this.options.parseSearch(url.search)
998
1087
  const searchStr = this.options.stringifySearch(parsedSearch)
1088
+ // Make sure our final url uses the re-stringified pathname, search, and has for consistency
1089
+ // (We were already doing this, so just keeping it for now)
1090
+ url.search = searchStr
1091
+
1092
+ const fullPath = url.href.replace(url.origin, '')
1093
+
1094
+ const { pathname, hash } = url
999
1095
 
1000
1096
  return {
1097
+ href: fullPath,
1098
+ publicHref: href,
1099
+ url: url.href,
1001
1100
  pathname,
1002
1101
  searchStr,
1003
1102
  search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1004
1103
  hash: hash.split('#').reverse()[0] ?? '',
1005
- href: `${pathname}${searchStr}${hash}`,
1006
1104
  state: replaceEqualDeep(previousLocation?.state, state),
1007
1105
  }
1008
1106
  }
1009
1107
 
1010
- const location = parse(locationToParse ?? this.history.location)
1108
+ const location = parse(locationToParse)
1011
1109
 
1012
1110
  const { __tempLocation, __tempKey } = location.state
1013
1111
 
@@ -1030,11 +1128,9 @@ export class RouterCore<
1030
1128
 
1031
1129
  resolvePathWithBase = (from: string, path: string) => {
1032
1130
  const resolvedPath = resolvePath({
1033
- basepath: this.basepath,
1034
1131
  base: from,
1035
1132
  to: cleanPath(path),
1036
1133
  trailingSlash: this.options.trailingSlash,
1037
- caseSensitive: this.options.caseSensitive,
1038
1134
  parseCache: this.parsePathnameCache,
1039
1135
  })
1040
1136
  return resolvedPath
@@ -1139,8 +1235,8 @@ export class RouterCore<
1139
1235
  const parentMatchId = parentMatch?.id
1140
1236
 
1141
1237
  const parentContext = !parentMatchId
1142
- ? ((this.options.context as any) ?? {})
1143
- : (parentMatch.context ?? this.options.context ?? {})
1238
+ ? ((this.options.context as any) ?? undefined)
1239
+ : (parentMatch.context ?? this.options.context ?? undefined)
1144
1240
 
1145
1241
  return parentContext
1146
1242
  }
@@ -1162,12 +1258,12 @@ export class RouterCore<
1162
1258
  ] = (() => {
1163
1259
  // Validate the search params and stabilize them
1164
1260
  const parentSearch = parentMatch?.search ?? next.search
1165
- const parentStrictSearch = parentMatch?._strictSearch ?? {}
1261
+ const parentStrictSearch = parentMatch?._strictSearch ?? undefined
1166
1262
 
1167
1263
  try {
1168
1264
  const strictSearch =
1169
1265
  validateSearch(route.options.validateSearch, { ...parentSearch }) ??
1170
- {}
1266
+ undefined
1171
1267
 
1172
1268
  return [
1173
1269
  {
@@ -1266,7 +1362,7 @@ export class RouterCore<
1266
1362
  ? replaceEqualDeep(previousMatch.params, routeParams)
1267
1363
  : routeParams,
1268
1364
  _strictParams: usedParams,
1269
- pathname: joinPaths([this.basepath, interpolatedPath]),
1365
+ pathname: interpolatedPath,
1270
1366
  updatedAt: Date.now(),
1271
1367
  search: previousMatch
1272
1368
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
@@ -1277,7 +1373,10 @@ export class RouterCore<
1277
1373
  isFetching: false,
1278
1374
  error: undefined,
1279
1375
  paramsError: parseErrors[index],
1280
- __routeContext: {},
1376
+ __routeContext: undefined,
1377
+ _nonReactive: {
1378
+ loadPromise: createControlledPromise(),
1379
+ },
1281
1380
  __beforeLoadContext: undefined,
1282
1381
  context: {},
1283
1382
  abortController: new AbortController(),
@@ -1293,7 +1392,6 @@ export class RouterCore<
1293
1392
  headScripts: undefined,
1294
1393
  meta: undefined,
1295
1394
  staticData: route.options.staticData || {},
1296
- loadPromise: createControlledPromise(),
1297
1395
  fullPath: route.fullPath,
1298
1396
  }
1299
1397
  }
@@ -1328,22 +1426,25 @@ export class RouterCore<
1328
1426
  const parentContext = getParentContext(parentMatch)
1329
1427
 
1330
1428
  // Update the match's context
1331
- const contextFnContext: RouteContextOptions<any, any, any, any> = {
1332
- deps: match.loaderDeps,
1333
- params: match.params,
1334
- context: parentContext,
1335
- location: next,
1336
- navigate: (opts: any) =>
1337
- this.navigate({ ...opts, _fromLocation: next }),
1338
- buildLocation: this.buildLocation,
1339
- cause: match.cause,
1340
- abortController: match.abortController,
1341
- preload: !!match.preload,
1342
- matches,
1343
- }
1344
1429
 
1345
- // Get the route context
1346
- match.__routeContext = route.options.context?.(contextFnContext) ?? {}
1430
+ if (route.options.context) {
1431
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
1432
+ deps: match.loaderDeps,
1433
+ params: match.params,
1434
+ context: parentContext ?? {},
1435
+ location: next,
1436
+ navigate: (opts: any) =>
1437
+ this.navigate({ ...opts, _fromLocation: next }),
1438
+ buildLocation: this.buildLocation,
1439
+ cause: match.cause,
1440
+ abortController: match.abortController,
1441
+ preload: !!match.preload,
1442
+ matches,
1443
+ }
1444
+ // Get the route context
1445
+ match.__routeContext =
1446
+ route.options.context(contextFnContext) ?? undefined
1447
+ }
1347
1448
 
1348
1449
  match.context = {
1349
1450
  ...parentContext,
@@ -1366,7 +1467,6 @@ export class RouterCore<
1366
1467
  return getMatchedRoutes({
1367
1468
  pathname,
1368
1469
  routePathname,
1369
- basepath: this.basepath,
1370
1470
  caseSensitive: this.options.caseSensitive,
1371
1471
  routesByPath: this.routesByPath,
1372
1472
  routesById: this.routesById,
@@ -1381,13 +1481,8 @@ export class RouterCore<
1381
1481
  if (!match) return
1382
1482
 
1383
1483
  match.abortController.abort()
1384
- this.updateMatch(id, (prev) => {
1385
- clearTimeout(prev.pendingTimeout)
1386
- return {
1387
- ...prev,
1388
- pendingTimeout: undefined,
1389
- }
1390
- })
1484
+ clearTimeout(match._nonReactive.pendingTimeout)
1485
+ match._nonReactive.pendingTimeout = undefined
1391
1486
  }
1392
1487
 
1393
1488
  cancelMatches = () => {
@@ -1409,106 +1504,94 @@ export class RouterCore<
1409
1504
  _buildLocation: true,
1410
1505
  })
1411
1506
 
1507
+ // Now let's find the starting pathname
1508
+ // This should default to the current location if no from is provided
1412
1509
  const lastMatch = last(allCurrentLocationMatches)!
1413
1510
 
1414
- // First let's find the starting pathname
1415
- // By default, start with the current location
1416
- let fromPath = lastMatch.fullPath
1417
- const toPath = dest.to
1418
- ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1419
- : this.resolvePathWithBase(fromPath, '.')
1420
-
1421
- const routeIsChanging =
1422
- !!dest.to &&
1423
- !comparePaths(dest.to.toString(), fromPath) &&
1424
- !comparePaths(toPath, fromPath)
1425
-
1426
- // If the route is changing we need to find the relative fromPath
1427
- if (dest.unsafeRelative === 'path') {
1428
- fromPath = currentLocation.pathname
1429
- } else if (routeIsChanging && dest.from) {
1430
- fromPath = dest.from
1431
-
1432
- // do this check only on navigations during test or development
1433
- if (process.env.NODE_ENV !== 'production' && dest._isNavigate) {
1434
- const allFromMatches = this.getMatchedRoutes(
1435
- dest.from,
1436
- undefined,
1437
- ).matchedRoutes
1511
+ // check that from path exists in the current route tree
1512
+ // do this check only on navigations during test or development
1513
+ if (
1514
+ dest.from &&
1515
+ process.env.NODE_ENV !== 'production' &&
1516
+ dest._isNavigate
1517
+ ) {
1518
+ const allFromMatches = this.getMatchedRoutes(
1519
+ dest.from,
1520
+ undefined,
1521
+ ).matchedRoutes
1438
1522
 
1439
- const matchedFrom = [...allCurrentLocationMatches]
1440
- .reverse()
1441
- .find((d) => {
1442
- return comparePaths(d.fullPath, fromPath)
1443
- })
1523
+ const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1524
+ return comparePaths(d.fullPath, dest.from!)
1525
+ })
1444
1526
 
1445
- const matchedCurrent = [...allFromMatches].reverse().find((d) => {
1446
- return comparePaths(d.fullPath, currentLocation.pathname)
1447
- })
1527
+ const matchedCurrent = findLast(allFromMatches, (d) => {
1528
+ return comparePaths(d.fullPath, lastMatch.fullPath)
1529
+ })
1448
1530
 
1449
- // for from to be invalid it shouldn't just be unmatched to currentLocation
1450
- // but the currentLocation should also be unmatched to from
1451
- if (!matchedFrom && !matchedCurrent) {
1452
- console.warn(`Could not find match for from: ${fromPath}`)
1453
- }
1531
+ // for from to be invalid it shouldn't just be unmatched to currentLocation
1532
+ // but the currentLocation should also be unmatched to from
1533
+ if (!matchedFrom && !matchedCurrent) {
1534
+ console.warn(`Could not find match for from: ${dest.from}`)
1454
1535
  }
1455
1536
  }
1456
1537
 
1538
+ const defaultedFromPath =
1539
+ dest.unsafeRelative === 'path'
1540
+ ? currentLocation.pathname
1541
+ : (dest.from ?? lastMatch.fullPath)
1542
+
1543
+ // ensure this includes the basePath if set
1544
+ const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1545
+
1457
1546
  // From search should always use the current location
1458
1547
  const fromSearch = lastMatch.search
1459
1548
  // Same with params. It can't hurt to provide as many as possible
1460
1549
  const fromParams = { ...lastMatch.params }
1461
1550
 
1462
1551
  // Resolve the next to
1552
+ // ensure this includes the basePath if set
1463
1553
  const nextTo = dest.to
1464
1554
  ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1465
1555
  : this.resolvePathWithBase(fromPath, '.')
1466
1556
 
1467
1557
  // Resolve the next params
1468
- let nextParams =
1558
+ const nextParams =
1469
1559
  dest.params === false || dest.params === null
1470
1560
  ? {}
1471
1561
  : (dest.params ?? true) === true
1472
1562
  ? fromParams
1473
- : {
1474
- ...fromParams,
1475
- ...functionalUpdate(dest.params as any, fromParams),
1476
- }
1563
+ : Object.assign(
1564
+ fromParams,
1565
+ functionalUpdate(dest.params as any, fromParams),
1566
+ )
1477
1567
 
1478
1568
  // Interpolate the path first to get the actual resolved path, then match against that
1479
1569
  const interpolatedNextTo = interpolatePath({
1480
1570
  path: nextTo,
1481
- params: nextParams ?? {},
1571
+ params: nextParams,
1482
1572
  parseCache: this.parsePathnameCache,
1483
1573
  }).interpolatedPath
1484
1574
 
1485
- const destRoutes = this.matchRoutes(
1486
- interpolatedNextTo,
1487
- {},
1488
- {
1489
- _buildLocation: true,
1490
- },
1491
- ).map((d) => this.looseRoutesById[d.routeId]!)
1575
+ const destRoutes = this.matchRoutes(interpolatedNextTo, undefined, {
1576
+ _buildLocation: true,
1577
+ }).map((d) => this.looseRoutesById[d.routeId]!)
1492
1578
 
1493
1579
  // If there are any params, we need to stringify them
1494
1580
  if (Object.keys(nextParams).length > 0) {
1495
- destRoutes
1496
- .map((route) => {
1497
- return (
1498
- route.options.params?.stringify ?? route.options.stringifyParams
1499
- )
1500
- })
1501
- .filter(Boolean)
1502
- .forEach((fn) => {
1503
- nextParams = { ...nextParams!, ...fn!(nextParams) }
1504
- })
1581
+ for (const route of destRoutes) {
1582
+ const fn =
1583
+ route.options.params?.stringify ?? route.options.stringifyParams
1584
+ if (fn) {
1585
+ Object.assign(nextParams, fn(nextParams))
1586
+ }
1587
+ }
1505
1588
  }
1506
1589
 
1507
1590
  const nextPathname = interpolatePath({
1508
1591
  // Use the original template path for interpolation
1509
1592
  // This preserves the original parameter syntax including optional parameters
1510
1593
  path: nextTo,
1511
- params: nextParams ?? {},
1594
+ params: nextParams,
1512
1595
  leaveWildcards: false,
1513
1596
  leaveParams: opts.leaveParams,
1514
1597
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1518,20 +1601,20 @@ export class RouterCore<
1518
1601
  // Resolve the next search
1519
1602
  let nextSearch = fromSearch
1520
1603
  if (opts._includeValidateSearch && this.options.search?.strict) {
1521
- let validatedSearch = {}
1604
+ const validatedSearch = {}
1522
1605
  destRoutes.forEach((route) => {
1523
- try {
1524
- if (route.options.validateSearch) {
1525
- validatedSearch = {
1526
- ...validatedSearch,
1527
- ...(validateSearch(route.options.validateSearch, {
1606
+ if (route.options.validateSearch) {
1607
+ try {
1608
+ Object.assign(
1609
+ validatedSearch,
1610
+ validateSearch(route.options.validateSearch, {
1528
1611
  ...validatedSearch,
1529
1612
  ...nextSearch,
1530
- }) ?? {}),
1531
- }
1613
+ }),
1614
+ )
1615
+ } catch {
1616
+ // ignore errors here because they are already handled in matchRoutes
1532
1617
  }
1533
- } catch {
1534
- // ignore errors here because they are already handled in matchRoutes
1535
1618
  }
1536
1619
  })
1537
1620
  nextSearch = validatedSearch
@@ -1572,14 +1655,25 @@ export class RouterCore<
1572
1655
  // Replace the equal deep
1573
1656
  nextState = replaceEqualDeep(currentLocation.state, nextState)
1574
1657
 
1575
- // Return the next location
1658
+ // Create the full path of the location
1659
+ const fullPath = `${nextPathname}${searchStr}${hashStr}`
1660
+
1661
+ // Create the new href with full origin
1662
+ const url = new URL(fullPath, this.origin)
1663
+
1664
+ // If a rewrite function is provided, use it to rewrite the URL
1665
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1666
+
1576
1667
  return {
1668
+ publicHref:
1669
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
1670
+ href: fullPath,
1671
+ url: rewrittenUrl.href,
1577
1672
  pathname: nextPathname,
1578
1673
  search: nextSearch,
1579
1674
  searchStr,
1580
1675
  state: nextState as any,
1581
1676
  hash: hash ?? '',
1582
- href: `${nextPathname}${searchStr}${hashStr}`,
1583
1677
  unmaskOnReload: dest.unmaskOnReload,
1584
1678
  }
1585
1679
  }
@@ -1597,7 +1691,6 @@ export class RouterCore<
1597
1691
 
1598
1692
  const foundMask = this.options.routeMasks?.find((d) => {
1599
1693
  const match = matchPathname(
1600
- this.basepath,
1601
1694
  next.pathname,
1602
1695
  {
1603
1696
  to: d.from,
@@ -1618,7 +1711,7 @@ export class RouterCore<
1618
1711
  if (foundMask) {
1619
1712
  const { from: _from, ...maskProps } = foundMask
1620
1713
  maskedDest = {
1621
- ...pick(opts, ['from']),
1714
+ from: opts.from,
1622
1715
  ...maskProps,
1623
1716
  params,
1624
1717
  }
@@ -1636,7 +1729,7 @@ export class RouterCore<
1636
1729
 
1637
1730
  if (opts.mask) {
1638
1731
  return buildWithMatches(opts, {
1639
- ...pick(opts, ['from']),
1732
+ from: opts.from,
1640
1733
  ...opts.mask,
1641
1734
  })
1642
1735
  }
@@ -1671,7 +1764,8 @@ export class RouterCore<
1671
1764
  return isEqual
1672
1765
  }
1673
1766
 
1674
- const isSameUrl = this.latestLocation.href === next.href
1767
+ const isSameUrl =
1768
+ trimPathRight(this.latestLocation.href) === trimPathRight(next.href)
1675
1769
 
1676
1770
  const previousCommitPromise = this.commitLocationPromise
1677
1771
  this.commitLocationPromise = createControlledPromise<void>(() => {
@@ -1720,7 +1814,7 @@ export class RouterCore<
1720
1814
  this.shouldViewTransition = viewTransition
1721
1815
 
1722
1816
  this.history[next.replace ? 'replace' : 'push'](
1723
- nextHistory.href,
1817
+ nextHistory.publicHref,
1724
1818
  nextHistory.state,
1725
1819
  { ignoreBlocker },
1726
1820
  )
@@ -1782,7 +1876,7 @@ export class RouterCore<
1782
1876
  if (reloadDocument) {
1783
1877
  if (!href) {
1784
1878
  const location = this.buildLocation({ to, ...rest } as any)
1785
- href = this.history.createHref(location.href)
1879
+ href = location.href
1786
1880
  }
1787
1881
  if (rest.replace) {
1788
1882
  window.location.replace(href)
@@ -1805,7 +1899,7 @@ export class RouterCore<
1805
1899
  beforeLoad = () => {
1806
1900
  // Cancel any pending matches
1807
1901
  this.cancelMatches()
1808
- this.latestLocation = this.parseLocation(this.latestLocation)
1902
+ this.updateLatestLocation()
1809
1903
 
1810
1904
  if (this.isServer) {
1811
1905
  // for SPAs on the initial load, this is handled by the Transitioner
@@ -1835,6 +1929,7 @@ export class RouterCore<
1835
1929
  throw redirect({ href: nextLocation.href })
1836
1930
  }
1837
1931
  }
1932
+
1838
1933
  // Match the routes
1839
1934
  const pendingMatches = this.matchRoutes(this.latestLocation)
1840
1935
 
@@ -1884,10 +1979,12 @@ export class RouterCore<
1884
1979
  }),
1885
1980
  })
1886
1981
 
1887
- await this.loadMatches({
1982
+ await loadMatches({
1983
+ router: this,
1888
1984
  sync: opts?.sync,
1889
1985
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1890
1986
  location: next,
1987
+ updateMatch: this.updateMatch,
1891
1988
  // eslint-disable-next-line @typescript-eslint/require-await
1892
1989
  onReady: async () => {
1893
1990
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -1978,6 +2075,7 @@ export class RouterCore<
1978
2075
  this.latestLoadPromise = undefined
1979
2076
  this.commitLocationPromise = undefined
1980
2077
  }
2078
+
1981
2079
  resolve()
1982
2080
  })
1983
2081
  })
@@ -2077,704 +2175,6 @@ export class RouterCore<
2077
2175
  )
2078
2176
  }
2079
2177
 
2080
- loadMatches = async ({
2081
- location,
2082
- matches,
2083
- preload: allPreload,
2084
- onReady,
2085
- updateMatch = this.updateMatch,
2086
- sync,
2087
- }: {
2088
- location: ParsedLocation
2089
- matches: Array<AnyRouteMatch>
2090
- preload?: boolean
2091
- onReady?: () => Promise<void>
2092
- updateMatch?: (
2093
- id: string,
2094
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
2095
- ) => void
2096
- getMatch?: (matchId: string) => AnyRouteMatch | undefined
2097
- sync?: boolean
2098
- }): Promise<Array<MakeRouteMatch>> => {
2099
- let firstBadMatchIndex: number | undefined
2100
- let rendered = false
2101
-
2102
- const triggerOnReady = async () => {
2103
- if (!rendered) {
2104
- rendered = true
2105
- await onReady?.()
2106
- }
2107
- }
2108
-
2109
- const resolvePreload = (matchId: string) => {
2110
- return !!(allPreload && !this.state.matches.some((d) => d.id === matchId))
2111
- }
2112
-
2113
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2114
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2115
- if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2116
- triggerOnReady()
2117
- }
2118
-
2119
- const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2120
- if (isRedirect(err) || isNotFound(err)) {
2121
- if (isRedirect(err)) {
2122
- if (err.redirectHandled) {
2123
- if (!err.options.reloadDocument) {
2124
- throw err
2125
- }
2126
- }
2127
- }
2128
-
2129
- match.beforeLoadPromise?.resolve()
2130
- match.loaderPromise?.resolve()
2131
-
2132
- updateMatch(match.id, (prev) => ({
2133
- ...prev,
2134
- status: isRedirect(err)
2135
- ? 'redirected'
2136
- : isNotFound(err)
2137
- ? 'notFound'
2138
- : 'error',
2139
- isFetching: false,
2140
- error: err,
2141
- beforeLoadPromise: undefined,
2142
- loaderPromise: undefined,
2143
- }))
2144
-
2145
- if (!(err as any).routeId) {
2146
- ;(err as any).routeId = match.routeId
2147
- }
2148
-
2149
- match.loadPromise?.resolve()
2150
-
2151
- if (isRedirect(err)) {
2152
- rendered = true
2153
- err.options._fromLocation = location
2154
- err.redirectHandled = true
2155
- err = this.resolveRedirect(err)
2156
- throw err
2157
- } else if (isNotFound(err)) {
2158
- this._handleNotFound(matches, err, {
2159
- updateMatch,
2160
- })
2161
- throw err
2162
- }
2163
- }
2164
- }
2165
-
2166
- const shouldSkipLoader = (matchId: string) => {
2167
- const match = this.getMatch(matchId)!
2168
- // upon hydration, we skip the loader if the match has been dehydrated on the server
2169
- if (!this.isServer && match._dehydrated) {
2170
- return true
2171
- }
2172
-
2173
- if (this.isServer) {
2174
- if (match.ssr === false) {
2175
- return true
2176
- }
2177
- }
2178
- return false
2179
- }
2180
-
2181
- try {
2182
- await new Promise<void>((resolveAll, rejectAll) => {
2183
- ;(async () => {
2184
- try {
2185
- const handleSerialError = (
2186
- index: number,
2187
- err: any,
2188
- routerCode: string,
2189
- ) => {
2190
- const { id: matchId, routeId } = matches[index]!
2191
- const route = this.looseRoutesById[routeId]!
2192
-
2193
- // Much like suspense, we use a promise here to know if
2194
- // we've been outdated by a new loadMatches call and
2195
- // should abort the current async operation
2196
- if (err instanceof Promise) {
2197
- throw err
2198
- }
2199
-
2200
- err.routerCode = routerCode
2201
- firstBadMatchIndex = firstBadMatchIndex ?? index
2202
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2203
-
2204
- try {
2205
- route.options.onError?.(err)
2206
- } catch (errorHandlerErr) {
2207
- err = errorHandlerErr
2208
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2209
- }
2210
-
2211
- updateMatch(matchId, (prev) => {
2212
- prev.beforeLoadPromise?.resolve()
2213
- prev.loadPromise?.resolve()
2214
-
2215
- return {
2216
- ...prev,
2217
- error: err,
2218
- status: 'error',
2219
- isFetching: false,
2220
- updatedAt: Date.now(),
2221
- abortController: new AbortController(),
2222
- beforeLoadPromise: undefined,
2223
- }
2224
- })
2225
- }
2226
-
2227
- for (const [index, { id: matchId, routeId }] of matches.entries()) {
2228
- const existingMatch = this.getMatch(matchId)!
2229
- const parentMatchId = matches[index - 1]?.id
2230
- const parentMatch = parentMatchId
2231
- ? this.getMatch(parentMatchId)!
2232
- : undefined
2233
-
2234
- const route = this.looseRoutesById[routeId]!
2235
-
2236
- const pendingMs =
2237
- route.options.pendingMs ?? this.options.defaultPendingMs
2238
-
2239
- // on the server, determine whether SSR the current match or not
2240
- if (this.isServer) {
2241
- let ssr: boolean | 'data-only'
2242
- // in SPA mode, only SSR the root route
2243
- if (this.isShell()) {
2244
- ssr = matchId === rootRouteId
2245
- } else {
2246
- const defaultSsr = this.options.defaultSsr ?? true
2247
- if (parentMatch?.ssr === false) {
2248
- ssr = false
2249
- } else {
2250
- let tempSsr: boolean | 'data-only'
2251
- if (route.options.ssr === undefined) {
2252
- tempSsr = defaultSsr
2253
- } else if (typeof route.options.ssr === 'function') {
2254
- const { search, params } = this.getMatch(matchId)!
2255
-
2256
- function makeMaybe(value: any, error: any) {
2257
- if (error) {
2258
- return { status: 'error' as const, error }
2259
- }
2260
- return { status: 'success' as const, value }
2261
- }
2262
-
2263
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2264
- search: makeMaybe(search, existingMatch.searchError),
2265
- params: makeMaybe(params, existingMatch.paramsError),
2266
- location,
2267
- matches: matches.map((match) => ({
2268
- index: match.index,
2269
- pathname: match.pathname,
2270
- fullPath: match.fullPath,
2271
- staticData: match.staticData,
2272
- id: match.id,
2273
- routeId: match.routeId,
2274
- search: makeMaybe(match.search, match.searchError),
2275
- params: makeMaybe(match.params, match.paramsError),
2276
- ssr: match.ssr,
2277
- })),
2278
- }
2279
- tempSsr =
2280
- (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2281
- } else {
2282
- tempSsr = route.options.ssr
2283
- }
2284
-
2285
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2286
- ssr = 'data-only'
2287
- } else {
2288
- ssr = tempSsr
2289
- }
2290
- }
2291
- }
2292
- updateMatch(matchId, (prev) => ({
2293
- ...prev,
2294
- ssr,
2295
- }))
2296
- }
2297
-
2298
- if (shouldSkipLoader(matchId)) {
2299
- continue
2300
- }
2301
-
2302
- const shouldPending = !!(
2303
- onReady &&
2304
- !this.isServer &&
2305
- !resolvePreload(matchId) &&
2306
- (route.options.loader ||
2307
- route.options.beforeLoad ||
2308
- routeNeedsPreload(route)) &&
2309
- typeof pendingMs === 'number' &&
2310
- pendingMs !== Infinity &&
2311
- (route.options.pendingComponent ??
2312
- (this.options as any)?.defaultPendingComponent)
2313
- )
2314
-
2315
- let executeBeforeLoad = true
2316
- const setupPendingTimeout = () => {
2317
- if (
2318
- shouldPending &&
2319
- this.getMatch(matchId)!.pendingTimeout === undefined
2320
- ) {
2321
- const pendingTimeout = setTimeout(() => {
2322
- try {
2323
- // Update the match and prematurely resolve the loadMatches promise so that
2324
- // the pending component can start rendering
2325
- triggerOnReady()
2326
- } catch {}
2327
- }, pendingMs)
2328
- updateMatch(matchId, (prev) => ({
2329
- ...prev,
2330
- pendingTimeout,
2331
- }))
2332
- }
2333
- }
2334
- if (
2335
- // If we are in the middle of a load, either of these will be present
2336
- // (not to be confused with `loadPromise`, which is always defined)
2337
- existingMatch.beforeLoadPromise ||
2338
- existingMatch.loaderPromise
2339
- ) {
2340
- setupPendingTimeout()
2341
-
2342
- // Wait for the beforeLoad to resolve before we continue
2343
- await existingMatch.beforeLoadPromise
2344
- const match = this.getMatch(matchId)!
2345
- if (match.status === 'error') {
2346
- executeBeforeLoad = true
2347
- } else if (
2348
- match.preload &&
2349
- (match.status === 'redirected' || match.status === 'notFound')
2350
- ) {
2351
- handleRedirectAndNotFound(match, match.error)
2352
- }
2353
- }
2354
- if (executeBeforeLoad) {
2355
- // If we are not in the middle of a load OR the previous load failed, start it
2356
- try {
2357
- updateMatch(matchId, (prev) => {
2358
- // explicitly capture the previous loadPromise
2359
- const prevLoadPromise = prev.loadPromise
2360
- return {
2361
- ...prev,
2362
- loadPromise: createControlledPromise<void>(() => {
2363
- prevLoadPromise?.resolve()
2364
- }),
2365
- beforeLoadPromise: createControlledPromise<void>(),
2366
- }
2367
- })
2368
-
2369
- const { paramsError, searchError } = this.getMatch(matchId)!
2370
-
2371
- if (paramsError) {
2372
- handleSerialError(index, paramsError, 'PARSE_PARAMS')
2373
- }
2374
-
2375
- if (searchError) {
2376
- handleSerialError(index, searchError, 'VALIDATE_SEARCH')
2377
- }
2378
-
2379
- setupPendingTimeout()
2380
-
2381
- const abortController = new AbortController()
2382
-
2383
- const parentMatchContext =
2384
- parentMatch?.context ?? this.options.context ?? {}
2385
-
2386
- updateMatch(matchId, (prev) => ({
2387
- ...prev,
2388
- isFetching: 'beforeLoad',
2389
- fetchCount: prev.fetchCount + 1,
2390
- abortController,
2391
- context: {
2392
- ...parentMatchContext,
2393
- ...prev.__routeContext,
2394
- },
2395
- }))
2396
-
2397
- const { search, params, context, cause } =
2398
- this.getMatch(matchId)!
2399
-
2400
- const preload = resolvePreload(matchId)
2401
-
2402
- const beforeLoadFnContext: BeforeLoadContextOptions<
2403
- any,
2404
- any,
2405
- any,
2406
- any,
2407
- any
2408
- > = {
2409
- search,
2410
- abortController,
2411
- params,
2412
- preload,
2413
- context,
2414
- location,
2415
- navigate: (opts: any) =>
2416
- this.navigate({ ...opts, _fromLocation: location }),
2417
- buildLocation: this.buildLocation,
2418
- cause: preload ? 'preload' : cause,
2419
- matches,
2420
- }
2421
-
2422
- const beforeLoadContext =
2423
- await route.options.beforeLoad?.(beforeLoadFnContext)
2424
-
2425
- if (
2426
- isRedirect(beforeLoadContext) ||
2427
- isNotFound(beforeLoadContext)
2428
- ) {
2429
- handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
2430
- }
2431
-
2432
- updateMatch(matchId, (prev) => {
2433
- return {
2434
- ...prev,
2435
- __beforeLoadContext: beforeLoadContext,
2436
- context: {
2437
- ...parentMatchContext,
2438
- ...prev.__routeContext,
2439
- ...beforeLoadContext,
2440
- },
2441
- abortController,
2442
- }
2443
- })
2444
- } catch (err) {
2445
- handleSerialError(index, err, 'BEFORE_LOAD')
2446
- }
2447
-
2448
- updateMatch(matchId, (prev) => {
2449
- prev.beforeLoadPromise?.resolve()
2450
-
2451
- return {
2452
- ...prev,
2453
- beforeLoadPromise: undefined,
2454
- isFetching: false,
2455
- }
2456
- })
2457
- }
2458
- }
2459
-
2460
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
2461
- const matchPromises: Array<Promise<AnyRouteMatch>> = []
2462
-
2463
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2464
- matchPromises.push(
2465
- (async () => {
2466
- let loaderShouldRunAsync = false
2467
- let loaderIsRunningAsync = false
2468
- const route = this.looseRoutesById[routeId]!
2469
-
2470
- const executeHead = async () => {
2471
- const match = this.getMatch(matchId)
2472
- // in case of a redirecting match during preload, the match does not exist
2473
- if (!match) {
2474
- return
2475
- }
2476
- const assetContext = {
2477
- matches,
2478
- match,
2479
- params: match.params,
2480
- loaderData: match.loaderData,
2481
- }
2482
- const headFnContent =
2483
- await route.options.head?.(assetContext)
2484
- const meta = headFnContent?.meta
2485
- const links = headFnContent?.links
2486
- const headScripts = headFnContent?.scripts
2487
- const styles = headFnContent?.styles
2488
-
2489
- const scripts = await route.options.scripts?.(assetContext)
2490
- const headers = await route.options.headers?.(assetContext)
2491
- return {
2492
- meta,
2493
- links,
2494
- headScripts,
2495
- headers,
2496
- scripts,
2497
- styles,
2498
- }
2499
- }
2500
-
2501
- const potentialPendingMinPromise = async () => {
2502
- const latestMatch = this.getMatch(matchId)!
2503
- if (latestMatch.minPendingPromise) {
2504
- await latestMatch.minPendingPromise
2505
- }
2506
- }
2507
-
2508
- const prevMatch = this.getMatch(matchId)!
2509
- if (shouldSkipLoader(matchId)) {
2510
- if (this.isServer) {
2511
- const head = await executeHead()
2512
- updateMatch(matchId, (prev) => ({
2513
- ...prev,
2514
- ...head,
2515
- }))
2516
- return this.getMatch(matchId)!
2517
- }
2518
- }
2519
- // there is a loaderPromise, so we are in the middle of a load
2520
- else if (prevMatch.loaderPromise) {
2521
- // do not block if we already have stale data we can show
2522
- // but only if the ongoing load is not a preload since error handling is different for preloads
2523
- // and we don't want to swallow errors
2524
- if (
2525
- prevMatch.status === 'success' &&
2526
- !sync &&
2527
- !prevMatch.preload
2528
- ) {
2529
- return this.getMatch(matchId)!
2530
- }
2531
- await prevMatch.loaderPromise
2532
- const match = this.getMatch(matchId)!
2533
- if (match.error) {
2534
- handleRedirectAndNotFound(match, match.error)
2535
- }
2536
- } else {
2537
- const parentMatchPromise = matchPromises[index - 1] as any
2538
-
2539
- const getLoaderContext = (): LoaderFnContext => {
2540
- const {
2541
- params,
2542
- loaderDeps,
2543
- abortController,
2544
- context,
2545
- cause,
2546
- } = this.getMatch(matchId)!
2547
-
2548
- const preload = resolvePreload(matchId)
2549
-
2550
- return {
2551
- params,
2552
- deps: loaderDeps,
2553
- preload: !!preload,
2554
- parentMatchPromise,
2555
- abortController: abortController,
2556
- context,
2557
- location,
2558
- navigate: (opts) =>
2559
- this.navigate({ ...opts, _fromLocation: location }),
2560
- cause: preload ? 'preload' : cause,
2561
- route,
2562
- }
2563
- }
2564
-
2565
- // This is where all of the stale-while-revalidate magic happens
2566
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2567
-
2568
- const preload = resolvePreload(matchId)
2569
-
2570
- const staleAge = preload
2571
- ? (route.options.preloadStaleTime ??
2572
- this.options.defaultPreloadStaleTime ??
2573
- 30_000) // 30 seconds for preloads by default
2574
- : (route.options.staleTime ??
2575
- this.options.defaultStaleTime ??
2576
- 0)
2577
-
2578
- const shouldReloadOption = route.options.shouldReload
2579
-
2580
- // Default to reloading the route all the time
2581
- // Allow shouldReload to get the last say,
2582
- // if provided.
2583
- const shouldReload =
2584
- typeof shouldReloadOption === 'function'
2585
- ? shouldReloadOption(getLoaderContext())
2586
- : shouldReloadOption
2587
-
2588
- updateMatch(matchId, (prev) => ({
2589
- ...prev,
2590
- loaderPromise: createControlledPromise<void>(),
2591
- preload:
2592
- !!preload &&
2593
- !this.state.matches.some((d) => d.id === matchId),
2594
- }))
2595
-
2596
- const runLoader = async () => {
2597
- try {
2598
- // If the Matches component rendered
2599
- // the pending component and needs to show it for
2600
- // a minimum duration, we''ll wait for it to resolve
2601
- // before committing to the match and resolving
2602
- // the loadPromise
2603
-
2604
- // Actually run the loader and handle the result
2605
- try {
2606
- if (
2607
- !this.isServer ||
2608
- (this.isServer &&
2609
- this.getMatch(matchId)!.ssr === true)
2610
- ) {
2611
- this.loadRouteChunk(route)
2612
- }
2613
-
2614
- updateMatch(matchId, (prev) => ({
2615
- ...prev,
2616
- isFetching: 'loader',
2617
- }))
2618
-
2619
- // Kick off the loader!
2620
- const loaderData =
2621
- await route.options.loader?.(getLoaderContext())
2622
-
2623
- handleRedirectAndNotFound(
2624
- this.getMatch(matchId)!,
2625
- loaderData,
2626
- )
2627
- updateMatch(matchId, (prev) => ({
2628
- ...prev,
2629
- loaderData,
2630
- }))
2631
-
2632
- // Lazy option can modify the route options,
2633
- // so we need to wait for it to resolve before
2634
- // we can use the options
2635
- await route._lazyPromise
2636
- const head = await executeHead()
2637
- await potentialPendingMinPromise()
2638
-
2639
- // Last but not least, wait for the the components
2640
- // to be preloaded before we resolve the match
2641
- await route._componentsPromise
2642
- updateMatch(matchId, (prev) => ({
2643
- ...prev,
2644
- error: undefined,
2645
- status: 'success',
2646
- isFetching: false,
2647
- updatedAt: Date.now(),
2648
- ...head,
2649
- }))
2650
- } catch (e) {
2651
- let error = e
2652
-
2653
- await potentialPendingMinPromise()
2654
-
2655
- handleRedirectAndNotFound(this.getMatch(matchId)!, e)
2656
-
2657
- try {
2658
- route.options.onError?.(e)
2659
- } catch (onErrorError) {
2660
- error = onErrorError
2661
- handleRedirectAndNotFound(
2662
- this.getMatch(matchId)!,
2663
- onErrorError,
2664
- )
2665
- }
2666
- const head = await executeHead()
2667
- updateMatch(matchId, (prev) => ({
2668
- ...prev,
2669
- error,
2670
- status: 'error',
2671
- isFetching: false,
2672
- ...head,
2673
- }))
2674
- }
2675
- } catch (err) {
2676
- const head = await executeHead()
2677
-
2678
- updateMatch(matchId, (prev) => ({
2679
- ...prev,
2680
- loaderPromise: undefined,
2681
- ...head,
2682
- }))
2683
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2684
- }
2685
- }
2686
-
2687
- // If the route is successful and still fresh, just resolve
2688
- const { status, invalid } = this.getMatch(matchId)!
2689
- loaderShouldRunAsync =
2690
- status === 'success' &&
2691
- (invalid || (shouldReload ?? age > staleAge))
2692
- if (preload && route.options.preload === false) {
2693
- // Do nothing
2694
- } else if (loaderShouldRunAsync && !sync) {
2695
- loaderIsRunningAsync = true
2696
- ;(async () => {
2697
- try {
2698
- await runLoader()
2699
- const { loaderPromise, loadPromise } =
2700
- this.getMatch(matchId)!
2701
- loaderPromise?.resolve()
2702
- loadPromise?.resolve()
2703
- updateMatch(matchId, (prev) => ({
2704
- ...prev,
2705
- loaderPromise: undefined,
2706
- }))
2707
- } catch (err) {
2708
- if (isRedirect(err)) {
2709
- await this.navigate(err.options)
2710
- }
2711
- }
2712
- })()
2713
- } else if (
2714
- status !== 'success' ||
2715
- (loaderShouldRunAsync && sync)
2716
- ) {
2717
- await runLoader()
2718
- } else {
2719
- // if the loader did not run, still update head.
2720
- // reason: parent's beforeLoad may have changed the route context
2721
- // and only now do we know the route context (and that the loader would not run)
2722
- const head = await executeHead()
2723
- updateMatch(matchId, (prev) => ({
2724
- ...prev,
2725
- ...head,
2726
- }))
2727
- }
2728
- }
2729
- if (!loaderIsRunningAsync) {
2730
- const { loaderPromise, loadPromise } =
2731
- this.getMatch(matchId)!
2732
- loaderPromise?.resolve()
2733
- loadPromise?.resolve()
2734
- }
2735
-
2736
- updateMatch(matchId, (prev) => {
2737
- clearTimeout(prev.pendingTimeout)
2738
- return {
2739
- ...prev,
2740
- isFetching: loaderIsRunningAsync
2741
- ? prev.isFetching
2742
- : false,
2743
- loaderPromise: loaderIsRunningAsync
2744
- ? prev.loaderPromise
2745
- : undefined,
2746
- invalid: false,
2747
- pendingTimeout: undefined,
2748
- _dehydrated: undefined,
2749
- }
2750
- })
2751
- return this.getMatch(matchId)!
2752
- })(),
2753
- )
2754
- })
2755
-
2756
- await Promise.all(matchPromises)
2757
-
2758
- resolveAll()
2759
- } catch (err) {
2760
- rejectAll(err)
2761
- }
2762
- })()
2763
- })
2764
- await triggerOnReady()
2765
- } catch (err) {
2766
- if (isRedirect(err) || isNotFound(err)) {
2767
- if (isNotFound(err) && !allPreload) {
2768
- await triggerOnReady()
2769
- }
2770
-
2771
- throw err
2772
- }
2773
- }
2774
-
2775
- return matches
2776
- }
2777
-
2778
2178
  invalidate: InvalidateFn<
2779
2179
  RouterCore<
2780
2180
  TRouteTree,
@@ -2791,7 +2191,7 @@ export class RouterCore<
2791
2191
  invalid: true,
2792
2192
  ...(opts?.forcePending || d.status === 'error'
2793
2193
  ? ({ status: 'pending', error: undefined } as const)
2794
- : {}),
2194
+ : undefined),
2795
2195
  }
2796
2196
  }
2797
2197
  return d
@@ -2868,36 +2268,7 @@ export class RouterCore<
2868
2268
  this.clearCache({ filter })
2869
2269
  }
2870
2270
 
2871
- loadRouteChunk = (route: AnyRoute) => {
2872
- if (route._lazyPromise === undefined) {
2873
- if (route.lazyFn) {
2874
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
2875
- // explicitly don't copy over the lazy route's id
2876
- const { id: _id, ...options } = lazyRoute.options
2877
- Object.assign(route.options, options)
2878
- })
2879
- } else {
2880
- route._lazyPromise = Promise.resolve()
2881
- }
2882
- }
2883
-
2884
- // If for some reason lazy resolves more lazy components...
2885
- // We'll wait for that before pre attempt to preload any
2886
- // components themselves.
2887
- if (route._componentsPromise === undefined) {
2888
- route._componentsPromise = route._lazyPromise.then(() =>
2889
- Promise.all(
2890
- componentTypes.map(async (type) => {
2891
- const component = route.options[type]
2892
- if ((component as any)?.preload) {
2893
- await (component as any).preload()
2894
- }
2895
- }),
2896
- ),
2897
- )
2898
- }
2899
- return route._componentsPromise
2900
- }
2271
+ loadRouteChunk = loadRouteChunk
2901
2272
 
2902
2273
  preloadRoute: PreloadRouteFn<
2903
2274
  TRouteTree,
@@ -2937,7 +2308,8 @@ export class RouterCore<
2937
2308
  })
2938
2309
 
2939
2310
  try {
2940
- matches = await this.loadMatches({
2311
+ matches = await loadMatches({
2312
+ router: this,
2941
2313
  matches,
2942
2314
  location: next,
2943
2315
  preload: true,
@@ -3002,7 +2374,6 @@ export class RouterCore<
3002
2374
  : this.state.resolvedLocation || this.state.location
3003
2375
 
3004
2376
  const match = matchPathname(
3005
- this.basepath,
3006
2377
  baseLocation.pathname,
3007
2378
  {
3008
2379
  ...opts,
@@ -3035,68 +2406,6 @@ export class RouterCore<
3035
2406
 
3036
2407
  serverSsr?: ServerSsr
3037
2408
 
3038
- _handleNotFound = (
3039
- matches: Array<AnyRouteMatch>,
3040
- err: NotFoundError,
3041
- {
3042
- updateMatch = this.updateMatch,
3043
- }: {
3044
- updateMatch?: (
3045
- id: string,
3046
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
3047
- ) => void
3048
- } = {},
3049
- ) => {
3050
- // Find the route that should handle the not found error
3051
- // First check if a specific route is requested to show the error
3052
- const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
3053
- const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3054
-
3055
- // Setup routesByRouteId object for quick access
3056
- for (const match of matches) {
3057
- matchesByRouteId[match.routeId] = match
3058
- }
3059
-
3060
- // Ensure a NotFoundComponent exists on the route
3061
- if (
3062
- !routeCursor.options.notFoundComponent &&
3063
- (this.options as any)?.defaultNotFoundComponent
3064
- ) {
3065
- routeCursor.options.notFoundComponent = (
3066
- this.options as any
3067
- ).defaultNotFoundComponent
3068
- }
3069
-
3070
- // Ensure we have a notFoundComponent
3071
- invariant(
3072
- routeCursor.options.notFoundComponent,
3073
- 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
3074
- )
3075
-
3076
- // Find the match for this route
3077
- const matchForRoute = matchesByRouteId[routeCursor.id]
3078
-
3079
- invariant(
3080
- matchForRoute,
3081
- 'Could not find match for route: ' + routeCursor.id,
3082
- )
3083
-
3084
- // Assign the error to the match - using non-null assertion since we've checked with invariant
3085
- updateMatch(matchForRoute.id, (prev) => ({
3086
- ...prev,
3087
- status: 'notFound',
3088
- error: err,
3089
- isFetching: false,
3090
- }))
3091
-
3092
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3093
- err.routeId = routeCursor.parentRoute.id
3094
- this._handleNotFound(matches, err, {
3095
- updateMatch,
3096
- })
3097
- }
3098
- }
3099
-
3100
2409
  hasNotFoundMatch = () => {
3101
2410
  return this.__store.state.matches.some(
3102
2411
  (d) => d.status === 'notFound' || d.globalNotFound,
@@ -3174,22 +2483,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3174
2483
  return {}
3175
2484
  }
3176
2485
 
3177
- export const componentTypes = [
3178
- 'component',
3179
- 'errorComponent',
3180
- 'pendingComponent',
3181
- 'notFoundComponent',
3182
- ] as const
3183
-
3184
- function routeNeedsPreload(route: AnyRoute) {
3185
- for (const componentType of componentTypes) {
3186
- if ((route.options[componentType] as any)?.preload) {
3187
- return true
3188
- }
3189
- }
3190
- return false
3191
- }
3192
-
3193
2486
  interface RouteLike {
3194
2487
  id: string
3195
2488
  isRoot?: boolean
@@ -3416,7 +2709,6 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3416
2709
  export function getMatchedRoutes<TRouteLike extends RouteLike>({
3417
2710
  pathname,
3418
2711
  routePathname,
3419
- basepath,
3420
2712
  caseSensitive,
3421
2713
  routesByPath,
3422
2714
  routesById,
@@ -3425,7 +2717,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3425
2717
  }: {
3426
2718
  pathname: string
3427
2719
  routePathname?: string
3428
- basepath: string
3429
2720
  caseSensitive?: boolean
3430
2721
  routesByPath: Record<string, TRouteLike>
3431
2722
  routesById: Record<string, TRouteLike>
@@ -3436,7 +2727,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3436
2727
  const trimmedPath = trimPathRight(pathname)
3437
2728
  const getMatchedParams = (route: TRouteLike) => {
3438
2729
  const result = matchPathname(
3439
- basepath,
3440
2730
  trimmedPath,
3441
2731
  {
3442
2732
  to: route.fullPath,
@@ -3562,7 +2852,8 @@ function applySearchMiddleware({
3562
2852
  try {
3563
2853
  const validatedSearch = {
3564
2854
  ...result,
3565
- ...(validateSearch(route.options.validateSearch, result) ?? {}),
2855
+ ...(validateSearch(route.options.validateSearch, result) ??
2856
+ undefined),
3566
2857
  }
3567
2858
  return validatedSearch
3568
2859
  } catch {