@tanstack/router-core 1.132.0-alpha.2 → 1.132.0-alpha.20

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 (142) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +2 -2
  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 +7 -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 -37
  27. package/dist/cjs/router.cjs +134 -780
  28. package/dist/cjs/router.cjs.map +1 -1
  29. package/dist/cjs/router.d.cts +68 -36
  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 -1
  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 +15 -1
  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/utils.cjs +8 -7
  57. package/dist/cjs/utils.cjs.map +1 -1
  58. package/dist/cjs/utils.d.cts +1 -1
  59. package/dist/esm/Matches.d.ts +2 -2
  60. package/dist/esm/Matches.js.map +1 -1
  61. package/dist/esm/config.d.ts +17 -0
  62. package/dist/esm/config.js +10 -0
  63. package/dist/esm/config.js.map +1 -0
  64. package/dist/esm/fileRoute.d.ts +3 -2
  65. package/dist/esm/index.d.ts +11 -4
  66. package/dist/esm/index.js +17 -5
  67. package/dist/esm/index.js.map +1 -1
  68. package/dist/esm/load-matches.d.ts +16 -0
  69. package/dist/esm/load-matches.js +636 -0
  70. package/dist/esm/load-matches.js.map +1 -0
  71. package/dist/esm/location.d.ts +38 -0
  72. package/dist/esm/path.d.ts +3 -6
  73. package/dist/esm/path.js +7 -49
  74. package/dist/esm/path.js.map +1 -1
  75. package/dist/esm/qss.d.ts +6 -4
  76. package/dist/esm/qss.js +19 -19
  77. package/dist/esm/qss.js.map +1 -1
  78. package/dist/esm/redirect.js +3 -3
  79. package/dist/esm/redirect.js.map +1 -1
  80. package/dist/esm/rewrite.d.ts +22 -0
  81. package/dist/esm/rewrite.js +63 -0
  82. package/dist/esm/rewrite.js.map +1 -0
  83. package/dist/esm/route.d.ts +42 -37
  84. package/dist/esm/route.js.map +1 -1
  85. package/dist/esm/router.d.ts +68 -36
  86. package/dist/esm/router.js +136 -782
  87. package/dist/esm/router.js.map +1 -1
  88. package/dist/esm/scroll-restoration.d.ts +1 -1
  89. package/dist/esm/scroll-restoration.js +32 -29
  90. package/dist/esm/scroll-restoration.js.map +1 -1
  91. package/dist/esm/searchParams.js +7 -15
  92. package/dist/esm/searchParams.js.map +1 -1
  93. package/dist/esm/ssr/constants.d.ts +1 -0
  94. package/dist/esm/ssr/constants.js +5 -0
  95. package/dist/esm/ssr/constants.js.map +1 -0
  96. package/dist/esm/ssr/{seroval-plugins.d.ts → serializer/ShallowErrorPlugin.d.ts} +1 -2
  97. package/dist/esm/ssr/{seroval-plugins.js → serializer/ShallowErrorPlugin.js} +2 -2
  98. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -0
  99. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -0
  100. package/dist/esm/ssr/serializer/seroval-plugins.js +11 -0
  101. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -0
  102. package/dist/esm/ssr/serializer/transformer.d.ts +56 -0
  103. package/dist/esm/ssr/serializer/transformer.js +52 -0
  104. package/dist/esm/ssr/serializer/transformer.js.map +1 -0
  105. package/dist/esm/ssr/server.d.ts +5 -0
  106. package/dist/esm/ssr/ssr-client.d.ts +5 -1
  107. package/dist/esm/ssr/ssr-client.js +15 -1
  108. package/dist/esm/ssr/ssr-client.js.map +1 -1
  109. package/dist/esm/ssr/ssr-server.d.ts +0 -1
  110. package/dist/esm/ssr/ssr-server.js +12 -10
  111. package/dist/esm/ssr/ssr-server.js.map +1 -1
  112. package/dist/esm/ssr/tsrScript.js +1 -1
  113. package/dist/esm/ssr/tsrScript.js.map +1 -1
  114. package/dist/esm/utils.d.ts +1 -1
  115. package/dist/esm/utils.js +8 -7
  116. package/dist/esm/utils.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/Matches.ts +2 -2
  119. package/src/config.ts +42 -0
  120. package/src/fileRoute.ts +15 -3
  121. package/src/index.ts +32 -3
  122. package/src/load-matches.ts +955 -0
  123. package/src/location.ts +38 -0
  124. package/src/path.ts +9 -66
  125. package/src/qss.ts +27 -24
  126. package/src/redirect.ts +3 -3
  127. package/src/rewrite.ts +70 -0
  128. package/src/route.ts +136 -33
  129. package/src/router.ts +271 -1170
  130. package/src/scroll-restoration.ts +42 -37
  131. package/src/searchParams.ts +8 -19
  132. package/src/ssr/constants.ts +1 -0
  133. package/src/ssr/{seroval-plugins.ts → serializer/ShallowErrorPlugin.ts} +2 -2
  134. package/src/ssr/serializer/seroval-plugins.ts +9 -0
  135. package/src/ssr/serializer/transformer.ts +215 -0
  136. package/src/ssr/server.ts +6 -0
  137. package/src/ssr/ssr-client.ts +30 -3
  138. package/src/ssr/ssr-server.ts +18 -10
  139. package/src/ssr/tsrScript.ts +5 -1
  140. package/src/utils.ts +11 -10
  141. package/dist/cjs/ssr/seroval-plugins.cjs.map +0 -1
  142. package/dist/esm/ssr/seroval-plugins.js.map +0 -1
package/src/router.ts CHANGED
@@ -1,17 +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
- isPromise,
13
9
  last,
14
- pick,
15
10
  replaceEqualDeep,
16
11
  } from './utils'
17
12
  import {
@@ -21,7 +16,6 @@ import {
21
16
  SEGMENT_TYPE_WILDCARD,
22
17
  cleanPath,
23
18
  interpolatePath,
24
- joinPaths,
25
19
  matchPathname,
26
20
  parsePathname,
27
21
  resolvePath,
@@ -35,6 +29,13 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
35
29
  import { rootRouteId } from './root'
36
30
  import { isRedirect, redirect } from './redirect'
37
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'
38
39
  import type { ParsePathnameCache, Segment } from './path'
39
40
  import type { SearchParser, SearchSerializer } from './searchParams'
40
41
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
@@ -46,6 +47,7 @@ import type {
46
47
  } from '@tanstack/history'
47
48
  import type {
48
49
  Awaitable,
50
+ Constrain,
49
51
  ControlledPromise,
50
52
  NoInfer,
51
53
  NonNullableUpdater,
@@ -57,13 +59,10 @@ import type {
57
59
  AnyContext,
58
60
  AnyRoute,
59
61
  AnyRouteWithContext,
60
- BeforeLoadContextOptions,
61
- LoaderFnContext,
62
62
  MakeRemountDepsOptionsUnion,
63
63
  RouteContextOptions,
64
64
  RouteMask,
65
65
  SearchMiddleware,
66
- SsrContextOptions,
67
66
  } from './route'
68
67
  import type {
69
68
  FullSearchSchema,
@@ -87,6 +86,11 @@ import type { Manifest } from './manifest'
87
86
  import type { AnySchema, AnyValidator } from './validators'
88
87
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
89
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'
90
94
 
91
95
  export type ControllablePromise<T = any> = Promise<T> & {
92
96
  resolve: (value: T) => void
@@ -97,6 +101,8 @@ export type InjectedHtmlEntry = Promise<string>
97
101
 
98
102
  export interface DefaultRegister {
99
103
  router: AnyRouter
104
+ config: AnyRouterConfig
105
+ ssr: SSROption
100
106
  }
101
107
 
102
108
  export interface Register extends DefaultRegister {
@@ -114,12 +120,14 @@ export interface DefaultRouterOptionsExtensions {}
114
120
  export interface RouterOptionsExtensions
115
121
  extends DefaultRouterOptionsExtensions {}
116
122
 
123
+ export type SSROption = boolean | 'data-only'
124
+
117
125
  export interface RouterOptions<
118
126
  TRouteTree extends AnyRoute,
119
127
  TTrailingSlashOption extends TrailingSlashOption,
120
128
  TDefaultStructuralSharingOption extends boolean = false,
121
129
  TRouterHistory extends RouterHistory = RouterHistory,
122
- TDehydrated extends Record<string, any> = Record<string, any>,
130
+ TDehydrated = undefined,
123
131
  > extends RouterOptionsExtensions {
124
132
  /**
125
133
  * The history object that will be used to manage the browser history.
@@ -264,6 +272,18 @@ export interface RouterOptions<
264
272
  /**
265
273
  * The basepath for then entire router. This is useful for mounting a router instance at a subpath.
266
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
+ * ```
267
287
  * @default '/'
268
288
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#basepath-property)
269
289
  */
@@ -287,7 +307,10 @@ export interface RouterOptions<
287
307
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
288
308
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
289
309
  */
290
- dehydrate?: () => Awaitable<TDehydrated>
310
+ dehydrate?: () => Constrain<
311
+ TDehydrated,
312
+ ValidateSerializableInput<Register, TDehydrated>
313
+ >
291
314
  /**
292
315
  * A function that will be called when the router is hydrated.
293
316
  *
@@ -357,7 +380,7 @@ export interface RouterOptions<
357
380
  *
358
381
  * @default true
359
382
  */
360
- defaultSsr?: boolean | 'data-only'
383
+ defaultSsr?: SSROption
361
384
 
362
385
  search?: {
363
386
  /**
@@ -393,7 +416,9 @@ export interface RouterOptions<
393
416
  *
394
417
  * @default false
395
418
  */
396
- scrollRestoration?: boolean
419
+ scrollRestoration?:
420
+ | boolean
421
+ | ((opts: { location: ParsedLocation }) => boolean)
397
422
 
398
423
  /**
399
424
  * A function that will be called to get the key for the scroll restoration cache.
@@ -424,8 +449,48 @@ export interface RouterOptions<
424
449
  * @default false
425
450
  */
426
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
427
463
  }
428
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
+
429
494
  export interface RouterState<
430
495
  in out TRouteTree extends AnyRoute = AnyRoute,
431
496
  in out TRouteMatch = MakeRouteMatchUnion,
@@ -615,8 +680,8 @@ export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
615
680
  }) => Promise<void>
616
681
 
617
682
  export type ParseLocationFn<TRouteTree extends AnyRoute> = (
683
+ locationToParse: HistoryLocation,
618
684
  previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
619
- locationToParse?: HistoryLocation,
620
685
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
621
686
 
622
687
  export type GetMatchRoutesFn = (
@@ -706,6 +771,7 @@ export interface ViewTransitionOptions {
706
771
  }) => Array<string>)
707
772
  }
708
773
 
774
+ // TODO where is this used? can we remove this?
709
775
  export function defaultSerializeError(err: unknown) {
710
776
  if (err instanceof Error) {
711
777
  const obj = {
@@ -763,18 +829,6 @@ export type CreateRouterFn = <
763
829
  TDehydrated
764
830
  >
765
831
 
766
- type InnerLoadContext = {
767
- location: ParsedLocation
768
- firstBadMatchIndex?: number
769
- rendered?: boolean
770
- updateMatch: UpdateMatchFn
771
- matches: Array<AnyRouteMatch>
772
- preload?: boolean
773
- onReady?: () => Promise<void>
774
- sync?: boolean
775
- matchPromises: Array<Promise<AnyRouteMatch>>
776
- }
777
-
778
832
  export class RouterCore<
779
833
  in out TRouteTree extends AnyRoute,
780
834
  in out TTrailingSlashOption extends TrailingSlashOption,
@@ -807,7 +861,10 @@ export class RouterCore<
807
861
  'stringifySearch' | 'parseSearch' | 'context'
808
862
  >
809
863
  history!: TRouterHistory
864
+ rewrite?: LocationRewrite
865
+ origin?: string
810
866
  latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
867
+ // @deprecated - basepath functionality is now implemented via the `rewrite` option
811
868
  basepath!: string
812
869
  routeTree!: TRouteTree
813
870
  routesById!: RoutesById<TRouteTree>
@@ -871,7 +928,6 @@ export class RouterCore<
871
928
  )
872
929
  }
873
930
 
874
- const previousOptions = this.options
875
931
  this.options = {
876
932
  ...this.options,
877
933
  ...newOptions,
@@ -889,32 +945,42 @@ export class RouterCore<
889
945
  : undefined
890
946
 
891
947
  if (
892
- !this.basepath ||
893
- (newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
948
+ !this.history ||
949
+ (this.options.history && this.options.history !== this.history)
894
950
  ) {
895
- if (
896
- newOptions.basepath === undefined ||
897
- newOptions.basepath === '' ||
898
- newOptions.basepath === '/'
899
- ) {
900
- this.basepath = '/'
951
+ if (!this.options.history) {
952
+ if (!this.isServer) {
953
+ this.history = createBrowserHistory() as TRouterHistory
954
+ }
901
955
  } else {
902
- this.basepath = `/${trimPath(newOptions.basepath)}`
956
+ this.history = this.options.history
903
957
  }
904
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])
966
+ } else {
967
+ this.rewrite = basepathRewrite
968
+ }
969
+ } else {
970
+ this.rewrite = this.options.rewrite
971
+ }
905
972
 
906
- if (
907
- !this.history ||
908
- (this.options.history && this.options.history !== this.history)
909
- ) {
910
- this.history =
911
- this.options.history ??
912
- ((this.isServer
913
- ? createMemoryHistory({
914
- initialEntries: [this.basepath || '/'],
915
- })
916
- : createBrowserHistory()) as TRouterHistory)
917
- 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()
918
984
  }
919
985
 
920
986
  if (this.options.routeTree !== this.routeTree) {
@@ -922,7 +988,7 @@ export class RouterCore<
922
988
  this.buildRouteTree()
923
989
  }
924
990
 
925
- if (!this.__store) {
991
+ if (!this.__store && this.latestLocation) {
926
992
  this.__store = new Store(getInitialRouterState(this.latestLocation), {
927
993
  onUpdate: () => {
928
994
  this.__store.state = {
@@ -948,10 +1014,17 @@ export class RouterCore<
948
1014
  }
949
1015
  }
950
1016
 
951
- get state() {
1017
+ get state(): RouterState<TRouteTree> {
952
1018
  return this.__store.state
953
1019
  }
954
1020
 
1021
+ updateLatestLocation = () => {
1022
+ this.latestLocation = this.parseLocation(
1023
+ this.history.location,
1024
+ this.latestLocation,
1025
+ )
1026
+ }
1027
+
955
1028
  buildRouteTree = () => {
956
1029
  const { routesById, routesByPath, flatRoutes } = processRouteTree({
957
1030
  routeTree: this.routeTree,
@@ -998,29 +1071,41 @@ export class RouterCore<
998
1071
  }
999
1072
 
1000
1073
  parseLocation: ParseLocationFn<TRouteTree> = (
1001
- previousLocation,
1002
1074
  locationToParse,
1075
+ previousLocation,
1003
1076
  ) => {
1004
1077
  const parse = ({
1005
- pathname,
1006
- search,
1007
- hash,
1078
+ href,
1008
1079
  state,
1009
1080
  }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
1010
- 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)
1011
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
1012
1095
 
1013
1096
  return {
1097
+ href: fullPath,
1098
+ publicHref: href,
1099
+ url: url.href,
1014
1100
  pathname,
1015
1101
  searchStr,
1016
1102
  search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1017
1103
  hash: hash.split('#').reverse()[0] ?? '',
1018
- href: `${pathname}${searchStr}${hash}`,
1019
1104
  state: replaceEqualDeep(previousLocation?.state, state),
1020
1105
  }
1021
1106
  }
1022
1107
 
1023
- const location = parse(locationToParse ?? this.history.location)
1108
+ const location = parse(locationToParse)
1024
1109
 
1025
1110
  const { __tempLocation, __tempKey } = location.state
1026
1111
 
@@ -1043,11 +1128,9 @@ export class RouterCore<
1043
1128
 
1044
1129
  resolvePathWithBase = (from: string, path: string) => {
1045
1130
  const resolvedPath = resolvePath({
1046
- basepath: this.basepath,
1047
1131
  base: from,
1048
1132
  to: cleanPath(path),
1049
1133
  trailingSlash: this.options.trailingSlash,
1050
- caseSensitive: this.options.caseSensitive,
1051
1134
  parseCache: this.parsePathnameCache,
1052
1135
  })
1053
1136
  return resolvedPath
@@ -1119,33 +1202,6 @@ export class RouterCore<
1119
1202
  return rootRouteId
1120
1203
  })()
1121
1204
 
1122
- const parseErrors = matchedRoutes.map((route) => {
1123
- let parsedParamsError
1124
-
1125
- const parseParams =
1126
- route.options.params?.parse ?? route.options.parseParams
1127
-
1128
- if (parseParams) {
1129
- try {
1130
- const parsedParams = parseParams(routeParams)
1131
- // Add the parsed params to the accumulated params bag
1132
- Object.assign(routeParams, parsedParams)
1133
- } catch (err: any) {
1134
- parsedParamsError = new PathParamError(err.message, {
1135
- cause: err,
1136
- })
1137
-
1138
- if (opts?.throwOnError) {
1139
- throw parsedParamsError
1140
- }
1141
-
1142
- return parsedParamsError
1143
- }
1144
- }
1145
-
1146
- return
1147
- })
1148
-
1149
1205
  const matches: Array<AnyRouteMatch> = []
1150
1206
 
1151
1207
  const getParentContext = (parentMatch?: AnyRouteMatch) => {
@@ -1218,20 +1274,19 @@ export class RouterCore<
1218
1274
 
1219
1275
  const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1220
1276
 
1221
- const { usedParams, interpolatedPath } = interpolatePath({
1277
+ const { interpolatedPath } = interpolatePath({
1222
1278
  path: route.fullPath,
1223
1279
  params: routeParams,
1224
1280
  decodeCharMap: this.pathParamsDecodeCharMap,
1225
1281
  })
1226
1282
 
1227
- const matchId =
1228
- interpolatePath({
1229
- path: route.id,
1230
- params: routeParams,
1231
- leaveWildcards: true,
1232
- decodeCharMap: this.pathParamsDecodeCharMap,
1233
- parseCache: this.parsePathnameCache,
1234
- }).interpolatedPath + loaderDepsHash
1283
+ const interpolatePathResult = interpolatePath({
1284
+ path: route.id,
1285
+ params: routeParams,
1286
+ leaveWildcards: true,
1287
+ decodeCharMap: this.pathParamsDecodeCharMap,
1288
+ parseCache: this.parsePathnameCache,
1289
+ })
1235
1290
 
1236
1291
  // Waste not, want not. If we already have a match for this route,
1237
1292
  // reuse it. This is important for layout routes, which might stick
@@ -1239,12 +1294,43 @@ export class RouterCore<
1239
1294
 
1240
1295
  // Existing matches are matches that are already loaded along with
1241
1296
  // pending matches that are still loading
1297
+ const matchId = interpolatePathResult.interpolatedPath + loaderDepsHash
1298
+
1242
1299
  const existingMatch = this.getMatch(matchId)
1243
1300
 
1244
1301
  const previousMatch = this.state.matches.find(
1245
1302
  (d) => d.routeId === route.id,
1246
1303
  )
1247
1304
 
1305
+ const strictParams =
1306
+ existingMatch?._strictParams ?? interpolatePathResult.usedParams
1307
+
1308
+ let paramsError: PathParamError | undefined = undefined
1309
+
1310
+ if (!existingMatch) {
1311
+ const strictParseParams =
1312
+ route.options.params?.parse ?? route.options.parseParams
1313
+
1314
+ if (strictParseParams) {
1315
+ try {
1316
+ Object.assign(
1317
+ strictParams,
1318
+ strictParseParams(strictParams as Record<string, string>),
1319
+ )
1320
+ } catch (err: any) {
1321
+ paramsError = new PathParamError(err.message, {
1322
+ cause: err,
1323
+ })
1324
+
1325
+ if (opts?.throwOnError) {
1326
+ throw paramsError
1327
+ }
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ Object.assign(routeParams, strictParams)
1333
+
1248
1334
  const cause = previousMatch ? 'stay' : 'enter'
1249
1335
 
1250
1336
  let match: AnyRouteMatch
@@ -1256,7 +1342,7 @@ export class RouterCore<
1256
1342
  params: previousMatch
1257
1343
  ? replaceEqualDeep(previousMatch.params, routeParams)
1258
1344
  : routeParams,
1259
- _strictParams: usedParams,
1345
+ _strictParams: strictParams,
1260
1346
  search: previousMatch
1261
1347
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1262
1348
  : replaceEqualDeep(existingMatch.search, preMatchSearch),
@@ -1278,8 +1364,8 @@ export class RouterCore<
1278
1364
  params: previousMatch
1279
1365
  ? replaceEqualDeep(previousMatch.params, routeParams)
1280
1366
  : routeParams,
1281
- _strictParams: usedParams,
1282
- pathname: joinPaths([this.basepath, interpolatedPath]),
1367
+ _strictParams: strictParams,
1368
+ pathname: interpolatedPath,
1283
1369
  updatedAt: Date.now(),
1284
1370
  search: previousMatch
1285
1371
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
@@ -1289,7 +1375,7 @@ export class RouterCore<
1289
1375
  status,
1290
1376
  isFetching: false,
1291
1377
  error: undefined,
1292
- paramsError: parseErrors[index],
1378
+ paramsError,
1293
1379
  __routeContext: undefined,
1294
1380
  _nonReactive: {
1295
1381
  loadPromise: createControlledPromise(),
@@ -1384,7 +1470,6 @@ export class RouterCore<
1384
1470
  return getMatchedRoutes({
1385
1471
  pathname,
1386
1472
  routePathname,
1387
- basepath: this.basepath,
1388
1473
  caseSensitive: this.options.caseSensitive,
1389
1474
  routesByPath: this.routesByPath,
1390
1475
  routesById: this.routesById,
@@ -1422,52 +1507,44 @@ export class RouterCore<
1422
1507
  _buildLocation: true,
1423
1508
  })
1424
1509
 
1510
+ // Now let's find the starting pathname
1511
+ // This should default to the current location if no from is provided
1425
1512
  const lastMatch = last(allCurrentLocationMatches)!
1426
1513
 
1427
- // First let's find the starting pathname
1428
- // By default, start with the current location
1429
- let fromPath = this.resolvePathWithBase(lastMatch.fullPath, '.')
1430
- const toPath = dest.to
1431
- ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1432
- : this.resolvePathWithBase(fromPath, '.')
1433
-
1434
- const routeIsChanging =
1435
- !!dest.to &&
1436
- !comparePaths(dest.to.toString(), fromPath) &&
1437
- !comparePaths(toPath, fromPath)
1438
-
1439
- // If the route is changing we need to find the relative fromPath
1440
- if (dest.unsafeRelative === 'path') {
1441
- fromPath = currentLocation.pathname
1442
- } else if (routeIsChanging && dest.from) {
1443
- fromPath = dest.from
1444
-
1445
- // do this check only on navigations during test or development
1446
- if (process.env.NODE_ENV !== 'production' && dest._isNavigate) {
1447
- const allFromMatches = this.getMatchedRoutes(
1448
- dest.from,
1449
- undefined,
1450
- ).matchedRoutes
1514
+ // check that from path exists in the current route tree
1515
+ // do this check only on navigations during test or development
1516
+ if (
1517
+ dest.from &&
1518
+ process.env.NODE_ENV !== 'production' &&
1519
+ dest._isNavigate
1520
+ ) {
1521
+ const allFromMatches = this.getMatchedRoutes(
1522
+ dest.from,
1523
+ undefined,
1524
+ ).matchedRoutes
1451
1525
 
1452
- const matchedFrom = [...allCurrentLocationMatches]
1453
- .reverse()
1454
- .find((d) => {
1455
- return comparePaths(d.fullPath, fromPath)
1456
- })
1526
+ const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1527
+ return comparePaths(d.fullPath, dest.from!)
1528
+ })
1457
1529
 
1458
- const matchedCurrent = [...allFromMatches].reverse().find((d) => {
1459
- return comparePaths(d.fullPath, currentLocation.pathname)
1460
- })
1530
+ const matchedCurrent = findLast(allFromMatches, (d) => {
1531
+ return comparePaths(d.fullPath, lastMatch.fullPath)
1532
+ })
1461
1533
 
1462
- // for from to be invalid it shouldn't just be unmatched to currentLocation
1463
- // but the currentLocation should also be unmatched to from
1464
- if (!matchedFrom && !matchedCurrent) {
1465
- console.warn(`Could not find match for from: ${fromPath}`)
1466
- }
1534
+ // for from to be invalid it shouldn't just be unmatched to currentLocation
1535
+ // but the currentLocation should also be unmatched to from
1536
+ if (!matchedFrom && !matchedCurrent) {
1537
+ console.warn(`Could not find match for from: ${dest.from}`)
1467
1538
  }
1468
1539
  }
1469
1540
 
1470
- fromPath = this.resolvePathWithBase(fromPath, '.')
1541
+ const defaultedFromPath =
1542
+ dest.unsafeRelative === 'path'
1543
+ ? currentLocation.pathname
1544
+ : (dest.from ?? lastMatch.fullPath)
1545
+
1546
+ // ensure this includes the basePath if set
1547
+ const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1471
1548
 
1472
1549
  // From search should always use the current location
1473
1550
  const fromSearch = lastMatch.search
@@ -1475,25 +1552,26 @@ export class RouterCore<
1475
1552
  const fromParams = { ...lastMatch.params }
1476
1553
 
1477
1554
  // Resolve the next to
1555
+ // ensure this includes the basePath if set
1478
1556
  const nextTo = dest.to
1479
1557
  ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1480
1558
  : this.resolvePathWithBase(fromPath, '.')
1481
1559
 
1482
1560
  // Resolve the next params
1483
- let nextParams =
1561
+ const nextParams =
1484
1562
  dest.params === false || dest.params === null
1485
1563
  ? {}
1486
1564
  : (dest.params ?? true) === true
1487
1565
  ? fromParams
1488
- : {
1489
- ...fromParams,
1490
- ...functionalUpdate(dest.params as any, fromParams),
1491
- }
1566
+ : Object.assign(
1567
+ fromParams,
1568
+ functionalUpdate(dest.params as any, fromParams),
1569
+ )
1492
1570
 
1493
1571
  // Interpolate the path first to get the actual resolved path, then match against that
1494
1572
  const interpolatedNextTo = interpolatePath({
1495
1573
  path: nextTo,
1496
- params: nextParams ?? {},
1574
+ params: nextParams,
1497
1575
  parseCache: this.parsePathnameCache,
1498
1576
  }).interpolatedPath
1499
1577
 
@@ -1503,23 +1581,20 @@ export class RouterCore<
1503
1581
 
1504
1582
  // If there are any params, we need to stringify them
1505
1583
  if (Object.keys(nextParams).length > 0) {
1506
- destRoutes
1507
- .map((route) => {
1508
- return (
1509
- route.options.params?.stringify ?? route.options.stringifyParams
1510
- )
1511
- })
1512
- .filter(Boolean)
1513
- .forEach((fn) => {
1514
- nextParams = { ...nextParams!, ...fn!(nextParams) }
1515
- })
1584
+ for (const route of destRoutes) {
1585
+ const fn =
1586
+ route.options.params?.stringify ?? route.options.stringifyParams
1587
+ if (fn) {
1588
+ Object.assign(nextParams, fn(nextParams))
1589
+ }
1590
+ }
1516
1591
  }
1517
1592
 
1518
1593
  const nextPathname = interpolatePath({
1519
1594
  // Use the original template path for interpolation
1520
1595
  // This preserves the original parameter syntax including optional parameters
1521
1596
  path: nextTo,
1522
- params: nextParams ?? {},
1597
+ params: nextParams,
1523
1598
  leaveWildcards: false,
1524
1599
  leaveParams: opts.leaveParams,
1525
1600
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1529,20 +1604,20 @@ export class RouterCore<
1529
1604
  // Resolve the next search
1530
1605
  let nextSearch = fromSearch
1531
1606
  if (opts._includeValidateSearch && this.options.search?.strict) {
1532
- let validatedSearch = {}
1607
+ const validatedSearch = {}
1533
1608
  destRoutes.forEach((route) => {
1534
- try {
1535
- if (route.options.validateSearch) {
1536
- validatedSearch = {
1537
- ...validatedSearch,
1538
- ...(validateSearch(route.options.validateSearch, {
1609
+ if (route.options.validateSearch) {
1610
+ try {
1611
+ Object.assign(
1612
+ validatedSearch,
1613
+ validateSearch(route.options.validateSearch, {
1539
1614
  ...validatedSearch,
1540
1615
  ...nextSearch,
1541
- }) ?? {}),
1542
- }
1616
+ }),
1617
+ )
1618
+ } catch {
1619
+ // ignore errors here because they are already handled in matchRoutes
1543
1620
  }
1544
- } catch {
1545
- // ignore errors here because they are already handled in matchRoutes
1546
1621
  }
1547
1622
  })
1548
1623
  nextSearch = validatedSearch
@@ -1583,14 +1658,25 @@ export class RouterCore<
1583
1658
  // Replace the equal deep
1584
1659
  nextState = replaceEqualDeep(currentLocation.state, nextState)
1585
1660
 
1586
- // Return the next location
1661
+ // Create the full path of the location
1662
+ const fullPath = `${nextPathname}${searchStr}${hashStr}`
1663
+
1664
+ // Create the new href with full origin
1665
+ const url = new URL(fullPath, this.origin)
1666
+
1667
+ // If a rewrite function is provided, use it to rewrite the URL
1668
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1669
+
1587
1670
  return {
1671
+ publicHref:
1672
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
1673
+ href: fullPath,
1674
+ url: rewrittenUrl.href,
1588
1675
  pathname: nextPathname,
1589
1676
  search: nextSearch,
1590
1677
  searchStr,
1591
1678
  state: nextState as any,
1592
1679
  hash: hash ?? '',
1593
- href: `${nextPathname}${searchStr}${hashStr}`,
1594
1680
  unmaskOnReload: dest.unmaskOnReload,
1595
1681
  }
1596
1682
  }
@@ -1608,7 +1694,6 @@ export class RouterCore<
1608
1694
 
1609
1695
  const foundMask = this.options.routeMasks?.find((d) => {
1610
1696
  const match = matchPathname(
1611
- this.basepath,
1612
1697
  next.pathname,
1613
1698
  {
1614
1699
  to: d.from,
@@ -1629,7 +1714,7 @@ export class RouterCore<
1629
1714
  if (foundMask) {
1630
1715
  const { from: _from, ...maskProps } = foundMask
1631
1716
  maskedDest = {
1632
- ...pick(opts, ['from']),
1717
+ from: opts.from,
1633
1718
  ...maskProps,
1634
1719
  params,
1635
1720
  }
@@ -1638,8 +1723,7 @@ export class RouterCore<
1638
1723
  }
1639
1724
 
1640
1725
  if (maskedNext) {
1641
- const maskedFinal = build(maskedDest)
1642
- next.maskedLocation = maskedFinal
1726
+ next.maskedLocation = maskedNext
1643
1727
  }
1644
1728
 
1645
1729
  return next
@@ -1647,7 +1731,7 @@ export class RouterCore<
1647
1731
 
1648
1732
  if (opts.mask) {
1649
1733
  return buildWithMatches(opts, {
1650
- ...pick(opts, ['from']),
1734
+ from: opts.from,
1651
1735
  ...opts.mask,
1652
1736
  })
1653
1737
  }
@@ -1682,7 +1766,8 @@ export class RouterCore<
1682
1766
  return isEqual
1683
1767
  }
1684
1768
 
1685
- const isSameUrl = this.latestLocation.href === next.href
1769
+ const isSameUrl =
1770
+ trimPathRight(this.latestLocation.href) === trimPathRight(next.href)
1686
1771
 
1687
1772
  const previousCommitPromise = this.commitLocationPromise
1688
1773
  this.commitLocationPromise = createControlledPromise<void>(() => {
@@ -1731,7 +1816,7 @@ export class RouterCore<
1731
1816
  this.shouldViewTransition = viewTransition
1732
1817
 
1733
1818
  this.history[next.replace ? 'replace' : 'push'](
1734
- nextHistory.href,
1819
+ nextHistory.publicHref,
1735
1820
  nextHistory.state,
1736
1821
  { ignoreBlocker },
1737
1822
  )
@@ -1793,7 +1878,7 @@ export class RouterCore<
1793
1878
  if (reloadDocument) {
1794
1879
  if (!href) {
1795
1880
  const location = this.buildLocation({ to, ...rest } as any)
1796
- href = this.history.createHref(location.href)
1881
+ href = location.href
1797
1882
  }
1798
1883
  if (rest.replace) {
1799
1884
  window.location.replace(href)
@@ -1816,7 +1901,7 @@ export class RouterCore<
1816
1901
  beforeLoad = () => {
1817
1902
  // Cancel any pending matches
1818
1903
  this.cancelMatches()
1819
- this.latestLocation = this.parseLocation(this.latestLocation)
1904
+ this.updateLatestLocation()
1820
1905
 
1821
1906
  if (this.isServer) {
1822
1907
  // for SPAs on the initial load, this is handled by the Transitioner
@@ -1846,6 +1931,7 @@ export class RouterCore<
1846
1931
  throw redirect({ href: nextLocation.href })
1847
1932
  }
1848
1933
  }
1934
+
1849
1935
  // Match the routes
1850
1936
  const pendingMatches = this.matchRoutes(this.latestLocation)
1851
1937
 
@@ -1895,10 +1981,12 @@ export class RouterCore<
1895
1981
  }),
1896
1982
  })
1897
1983
 
1898
- await this.loadMatches({
1984
+ await loadMatches({
1985
+ router: this,
1899
1986
  sync: opts?.sync,
1900
1987
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1901
1988
  location: next,
1989
+ updateMatch: this.updateMatch,
1902
1990
  // eslint-disable-next-line @typescript-eslint/require-await
1903
1991
  onReady: async () => {
1904
1992
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -1989,6 +2077,7 @@ export class RouterCore<
1989
2077
  this.latestLoadPromise = undefined
1990
2078
  this.commitLocationPromise = undefined
1991
2079
  }
2080
+
1992
2081
  resolve()
1993
2082
  })
1994
2083
  })
@@ -2088,873 +2177,6 @@ export class RouterCore<
2088
2177
  )
2089
2178
  }
2090
2179
 
2091
- private triggerOnReady = (
2092
- innerLoadContext: InnerLoadContext,
2093
- ): void | Promise<void> => {
2094
- if (!innerLoadContext.rendered) {
2095
- innerLoadContext.rendered = true
2096
- return innerLoadContext.onReady?.()
2097
- }
2098
- }
2099
-
2100
- private resolvePreload = (
2101
- innerLoadContext: InnerLoadContext,
2102
- matchId: string,
2103
- ): boolean => {
2104
- return !!(
2105
- innerLoadContext.preload &&
2106
- !this.state.matches.some((d) => d.id === matchId)
2107
- )
2108
- }
2109
-
2110
- private handleRedirectAndNotFound = (
2111
- innerLoadContext: InnerLoadContext,
2112
- match: AnyRouteMatch | undefined,
2113
- err: unknown,
2114
- ): void => {
2115
- if (!isRedirect(err) && !isNotFound(err)) return
2116
-
2117
- if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
2118
- throw err
2119
- }
2120
-
2121
- // in case of a redirecting match during preload, the match does not exist
2122
- if (match) {
2123
- match._nonReactive.beforeLoadPromise?.resolve()
2124
- match._nonReactive.loaderPromise?.resolve()
2125
- match._nonReactive.beforeLoadPromise = undefined
2126
- match._nonReactive.loaderPromise = undefined
2127
-
2128
- const status = isRedirect(err) ? 'redirected' : 'notFound'
2129
-
2130
- innerLoadContext.updateMatch(match.id, (prev) => ({
2131
- ...prev,
2132
- status,
2133
- isFetching: false,
2134
- error: err,
2135
- }))
2136
-
2137
- if (isNotFound(err) && !err.routeId) {
2138
- err.routeId = match.routeId
2139
- }
2140
-
2141
- match._nonReactive.loadPromise?.resolve()
2142
- }
2143
-
2144
- if (isRedirect(err)) {
2145
- innerLoadContext.rendered = true
2146
- err.options._fromLocation = innerLoadContext.location
2147
- err.redirectHandled = true
2148
- err = this.resolveRedirect(err)
2149
- throw err
2150
- } else {
2151
- this._handleNotFound(innerLoadContext, err)
2152
- throw err
2153
- }
2154
- }
2155
-
2156
- private shouldSkipLoader = (matchId: string): boolean => {
2157
- const match = this.getMatch(matchId)!
2158
- // upon hydration, we skip the loader if the match has been dehydrated on the server
2159
- if (!this.isServer && match._nonReactive.dehydrated) {
2160
- return true
2161
- }
2162
-
2163
- if (this.isServer) {
2164
- if (match.ssr === false) {
2165
- return true
2166
- }
2167
- }
2168
- return false
2169
- }
2170
-
2171
- private handleSerialError = (
2172
- innerLoadContext: InnerLoadContext,
2173
- index: number,
2174
- err: any,
2175
- routerCode: string,
2176
- ): void => {
2177
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2178
- const route = this.looseRoutesById[routeId]!
2179
-
2180
- // Much like suspense, we use a promise here to know if
2181
- // we've been outdated by a new loadMatches call and
2182
- // should abort the current async operation
2183
- if (err instanceof Promise) {
2184
- throw err
2185
- }
2186
-
2187
- err.routerCode = routerCode
2188
- innerLoadContext.firstBadMatchIndex ??= index
2189
- this.handleRedirectAndNotFound(
2190
- innerLoadContext,
2191
- this.getMatch(matchId),
2192
- err,
2193
- )
2194
-
2195
- try {
2196
- route.options.onError?.(err)
2197
- } catch (errorHandlerErr) {
2198
- err = errorHandlerErr
2199
- this.handleRedirectAndNotFound(
2200
- innerLoadContext,
2201
- this.getMatch(matchId),
2202
- err,
2203
- )
2204
- }
2205
-
2206
- innerLoadContext.updateMatch(matchId, (prev) => {
2207
- prev._nonReactive.beforeLoadPromise?.resolve()
2208
- prev._nonReactive.beforeLoadPromise = undefined
2209
- prev._nonReactive.loadPromise?.resolve()
2210
-
2211
- return {
2212
- ...prev,
2213
- error: err,
2214
- status: 'error',
2215
- isFetching: false,
2216
- updatedAt: Date.now(),
2217
- abortController: new AbortController(),
2218
- }
2219
- })
2220
- }
2221
-
2222
- private isBeforeLoadSsr = (
2223
- innerLoadContext: InnerLoadContext,
2224
- matchId: string,
2225
- index: number,
2226
- route: AnyRoute,
2227
- ): void | Promise<void> => {
2228
- const existingMatch = this.getMatch(matchId)!
2229
- const parentMatchId = innerLoadContext.matches[index - 1]?.id
2230
- const parentMatch = parentMatchId
2231
- ? this.getMatch(parentMatchId)!
2232
- : undefined
2233
-
2234
- // in SPA mode, only SSR the root route
2235
- if (this.isShell()) {
2236
- existingMatch.ssr = matchId === rootRouteId
2237
- return
2238
- }
2239
-
2240
- if (parentMatch?.ssr === false) {
2241
- existingMatch.ssr = false
2242
- return
2243
- }
2244
-
2245
- const parentOverride = (tempSsr: boolean | 'data-only') => {
2246
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2247
- return 'data-only'
2248
- }
2249
- return tempSsr
2250
- }
2251
-
2252
- const defaultSsr = this.options.defaultSsr ?? true
2253
-
2254
- if (route.options.ssr === undefined) {
2255
- existingMatch.ssr = parentOverride(defaultSsr)
2256
- return
2257
- }
2258
-
2259
- if (typeof route.options.ssr !== 'function') {
2260
- existingMatch.ssr = parentOverride(route.options.ssr)
2261
- return
2262
- }
2263
- const { search, params } = this.getMatch(matchId)!
2264
-
2265
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2266
- search: makeMaybe(search, existingMatch.searchError),
2267
- params: makeMaybe(params, existingMatch.paramsError),
2268
- location: innerLoadContext.location,
2269
- matches: innerLoadContext.matches.map((match) => ({
2270
- index: match.index,
2271
- pathname: match.pathname,
2272
- fullPath: match.fullPath,
2273
- staticData: match.staticData,
2274
- id: match.id,
2275
- routeId: match.routeId,
2276
- search: makeMaybe(match.search, match.searchError),
2277
- params: makeMaybe(match.params, match.paramsError),
2278
- ssr: match.ssr,
2279
- })),
2280
- }
2281
-
2282
- const tempSsr = route.options.ssr(ssrFnContext)
2283
- if (isPromise(tempSsr)) {
2284
- return tempSsr.then((ssr) => {
2285
- existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
2286
- })
2287
- }
2288
-
2289
- existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
2290
- return
2291
- }
2292
-
2293
- private setupPendingTimeout = (
2294
- innerLoadContext: InnerLoadContext,
2295
- matchId: string,
2296
- route: AnyRoute,
2297
- ): void => {
2298
- const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs
2299
- const shouldPending = !!(
2300
- innerLoadContext.onReady &&
2301
- !this.isServer &&
2302
- !this.resolvePreload(innerLoadContext, matchId) &&
2303
- (route.options.loader ||
2304
- route.options.beforeLoad ||
2305
- routeNeedsPreload(route)) &&
2306
- typeof pendingMs === 'number' &&
2307
- pendingMs !== Infinity &&
2308
- (route.options.pendingComponent ??
2309
- (this.options as any)?.defaultPendingComponent)
2310
- )
2311
- const match = this.getMatch(matchId)!
2312
- if (shouldPending && match._nonReactive.pendingTimeout === undefined) {
2313
- const pendingTimeout = setTimeout(() => {
2314
- // Update the match and prematurely resolve the loadMatches promise so that
2315
- // the pending component can start rendering
2316
- this.triggerOnReady(innerLoadContext)
2317
- }, pendingMs)
2318
- match._nonReactive.pendingTimeout = pendingTimeout
2319
- }
2320
- }
2321
-
2322
- private shouldExecuteBeforeLoad = (
2323
- innerLoadContext: InnerLoadContext,
2324
- matchId: string,
2325
- route: AnyRoute,
2326
- ): boolean | Promise<boolean> => {
2327
- const existingMatch = this.getMatch(matchId)!
2328
-
2329
- // If we are in the middle of a load, either of these will be present
2330
- // (not to be confused with `loadPromise`, which is always defined)
2331
- if (
2332
- !existingMatch._nonReactive.beforeLoadPromise &&
2333
- !existingMatch._nonReactive.loaderPromise
2334
- )
2335
- return true
2336
-
2337
- this.setupPendingTimeout(innerLoadContext, matchId, route)
2338
-
2339
- const then = () => {
2340
- let shouldExecuteBeforeLoad = true
2341
- const match = this.getMatch(matchId)!
2342
- if (match.status === 'error') {
2343
- shouldExecuteBeforeLoad = true
2344
- } else if (
2345
- match.preload &&
2346
- (match.status === 'redirected' || match.status === 'notFound')
2347
- ) {
2348
- this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2349
- }
2350
- return shouldExecuteBeforeLoad
2351
- }
2352
-
2353
- // Wait for the beforeLoad to resolve before we continue
2354
- return existingMatch._nonReactive.beforeLoadPromise
2355
- ? existingMatch._nonReactive.beforeLoadPromise.then(then)
2356
- : then()
2357
- }
2358
-
2359
- private executeBeforeLoad = (
2360
- innerLoadContext: InnerLoadContext,
2361
- matchId: string,
2362
- index: number,
2363
- route: AnyRoute,
2364
- ): void | Promise<void> => {
2365
- const match = this.getMatch(matchId)!
2366
-
2367
- match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
2368
- // explicitly capture the previous loadPromise
2369
- const prevLoadPromise = match._nonReactive.loadPromise
2370
- match._nonReactive.loadPromise = createControlledPromise<void>(() => {
2371
- prevLoadPromise?.resolve()
2372
- })
2373
-
2374
- const { paramsError, searchError } = match
2375
-
2376
- if (paramsError) {
2377
- this.handleSerialError(
2378
- innerLoadContext,
2379
- index,
2380
- paramsError,
2381
- 'PARSE_PARAMS',
2382
- )
2383
- }
2384
-
2385
- if (searchError) {
2386
- this.handleSerialError(
2387
- innerLoadContext,
2388
- index,
2389
- searchError,
2390
- 'VALIDATE_SEARCH',
2391
- )
2392
- }
2393
-
2394
- this.setupPendingTimeout(innerLoadContext, matchId, route)
2395
-
2396
- const abortController = new AbortController()
2397
-
2398
- const parentMatchId = innerLoadContext.matches[index - 1]?.id
2399
- const parentMatch = parentMatchId
2400
- ? this.getMatch(parentMatchId)!
2401
- : undefined
2402
- const parentMatchContext =
2403
- parentMatch?.context ?? this.options.context ?? undefined
2404
-
2405
- const context = { ...parentMatchContext, ...match.__routeContext }
2406
-
2407
- let isPending = false
2408
- const pending = () => {
2409
- if (isPending) return
2410
- isPending = true
2411
- innerLoadContext.updateMatch(matchId, (prev) => ({
2412
- ...prev,
2413
- isFetching: 'beforeLoad',
2414
- fetchCount: prev.fetchCount + 1,
2415
- abortController,
2416
- context,
2417
- }))
2418
- }
2419
-
2420
- const resolve = () => {
2421
- match._nonReactive.beforeLoadPromise?.resolve()
2422
- match._nonReactive.beforeLoadPromise = undefined
2423
- innerLoadContext.updateMatch(matchId, (prev) => ({
2424
- ...prev,
2425
- isFetching: false,
2426
- }))
2427
- }
2428
-
2429
- // if there is no `beforeLoad` option, skip everything, batch update the store, return early
2430
- if (!route.options.beforeLoad) {
2431
- batch(() => {
2432
- pending()
2433
- resolve()
2434
- })
2435
- return
2436
- }
2437
-
2438
- const { search, params, cause } = match
2439
- const preload = this.resolvePreload(innerLoadContext, matchId)
2440
- const beforeLoadFnContext: BeforeLoadContextOptions<
2441
- any,
2442
- any,
2443
- any,
2444
- any,
2445
- any
2446
- > = {
2447
- search,
2448
- abortController,
2449
- params,
2450
- preload,
2451
- context,
2452
- location: innerLoadContext.location,
2453
- navigate: (opts: any) =>
2454
- this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2455
- buildLocation: this.buildLocation,
2456
- cause: preload ? 'preload' : cause,
2457
- matches: innerLoadContext.matches,
2458
- }
2459
-
2460
- const updateContext = (beforeLoadContext: any) => {
2461
- if (beforeLoadContext === undefined) {
2462
- batch(() => {
2463
- pending()
2464
- resolve()
2465
- })
2466
- return
2467
- }
2468
- if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
2469
- pending()
2470
- this.handleSerialError(
2471
- innerLoadContext,
2472
- index,
2473
- beforeLoadContext,
2474
- 'BEFORE_LOAD',
2475
- )
2476
- }
2477
-
2478
- batch(() => {
2479
- pending()
2480
- innerLoadContext.updateMatch(matchId, (prev) => ({
2481
- ...prev,
2482
- __beforeLoadContext: beforeLoadContext,
2483
- context: {
2484
- ...prev.context,
2485
- ...beforeLoadContext,
2486
- },
2487
- }))
2488
- resolve()
2489
- })
2490
- }
2491
-
2492
- let beforeLoadContext
2493
- try {
2494
- beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
2495
- if (isPromise(beforeLoadContext)) {
2496
- pending()
2497
- return beforeLoadContext
2498
- .catch((err) => {
2499
- this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2500
- })
2501
- .then(updateContext)
2502
- }
2503
- } catch (err) {
2504
- pending()
2505
- this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2506
- }
2507
-
2508
- updateContext(beforeLoadContext)
2509
- return
2510
- }
2511
-
2512
- private handleBeforeLoad = (
2513
- innerLoadContext: InnerLoadContext,
2514
- index: number,
2515
- ): void | Promise<void> => {
2516
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2517
- const route = this.looseRoutesById[routeId]!
2518
-
2519
- const serverSsr = () => {
2520
- // on the server, determine whether SSR the current match or not
2521
- if (this.isServer) {
2522
- const maybePromise = this.isBeforeLoadSsr(
2523
- innerLoadContext,
2524
- matchId,
2525
- index,
2526
- route,
2527
- )
2528
- if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
2529
- }
2530
- return queueExecution()
2531
- }
2532
-
2533
- const queueExecution = () => {
2534
- if (this.shouldSkipLoader(matchId)) return
2535
- const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
2536
- innerLoadContext,
2537
- matchId,
2538
- route,
2539
- )
2540
- return isPromise(shouldExecuteBeforeLoadResult)
2541
- ? shouldExecuteBeforeLoadResult.then(execute)
2542
- : execute(shouldExecuteBeforeLoadResult)
2543
- }
2544
-
2545
- const execute = (shouldExecuteBeforeLoad: boolean) => {
2546
- if (shouldExecuteBeforeLoad) {
2547
- // If we are not in the middle of a load OR the previous load failed, start it
2548
- return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
2549
- }
2550
- return
2551
- }
2552
-
2553
- return serverSsr()
2554
- }
2555
-
2556
- private executeHead = (
2557
- innerLoadContext: InnerLoadContext,
2558
- matchId: string,
2559
- route: AnyRoute,
2560
- ): void | Promise<
2561
- Pick<
2562
- AnyRouteMatch,
2563
- 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
2564
- >
2565
- > => {
2566
- const match = this.getMatch(matchId)
2567
- // in case of a redirecting match during preload, the match does not exist
2568
- if (!match) {
2569
- return
2570
- }
2571
- if (
2572
- !route.options.head &&
2573
- !route.options.scripts &&
2574
- !route.options.headers
2575
- ) {
2576
- return
2577
- }
2578
- const assetContext = {
2579
- matches: innerLoadContext.matches,
2580
- match,
2581
- params: match.params,
2582
- loaderData: match.loaderData,
2583
- }
2584
-
2585
- return Promise.all([
2586
- route.options.head?.(assetContext),
2587
- route.options.scripts?.(assetContext),
2588
- route.options.headers?.(assetContext),
2589
- ]).then(([headFnContent, scripts, headers]) => {
2590
- const meta = headFnContent?.meta
2591
- const links = headFnContent?.links
2592
- const headScripts = headFnContent?.scripts
2593
- const styles = headFnContent?.styles
2594
-
2595
- return {
2596
- meta,
2597
- links,
2598
- headScripts,
2599
- headers,
2600
- scripts,
2601
- styles,
2602
- }
2603
- })
2604
- }
2605
-
2606
- private potentialPendingMinPromise = (
2607
- matchId: string,
2608
- ): void | ControlledPromise<void> => {
2609
- const latestMatch = this.getMatch(matchId)!
2610
- return latestMatch._nonReactive.minPendingPromise
2611
- }
2612
-
2613
- private getLoaderContext = (
2614
- innerLoadContext: InnerLoadContext,
2615
- matchId: string,
2616
- index: number,
2617
- route: AnyRoute,
2618
- ): LoaderFnContext => {
2619
- const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
2620
- const { params, loaderDeps, abortController, context, cause } =
2621
- this.getMatch(matchId)!
2622
-
2623
- const preload = this.resolvePreload(innerLoadContext, matchId)
2624
-
2625
- return {
2626
- params,
2627
- deps: loaderDeps,
2628
- preload: !!preload,
2629
- parentMatchPromise,
2630
- abortController: abortController,
2631
- context,
2632
- location: innerLoadContext.location,
2633
- navigate: (opts) =>
2634
- this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2635
- cause: preload ? 'preload' : cause,
2636
- route,
2637
- }
2638
- }
2639
-
2640
- private runLoader = async (
2641
- innerLoadContext: InnerLoadContext,
2642
- matchId: string,
2643
- index: number,
2644
- route: AnyRoute,
2645
- ): Promise<void> => {
2646
- try {
2647
- // If the Matches component rendered
2648
- // the pending component and needs to show it for
2649
- // a minimum duration, we''ll wait for it to resolve
2650
- // before committing to the match and resolving
2651
- // the loadPromise
2652
-
2653
- // Actually run the loader and handle the result
2654
- try {
2655
- if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
2656
- this.loadRouteChunk(route)
2657
- }
2658
-
2659
- // Kick off the loader!
2660
- const loaderResult = route.options.loader?.(
2661
- this.getLoaderContext(innerLoadContext, matchId, index, route),
2662
- )
2663
- const loaderResultIsPromise =
2664
- route.options.loader && isPromise(loaderResult)
2665
-
2666
- const willLoadSomething = !!(
2667
- loaderResultIsPromise ||
2668
- route._lazyPromise ||
2669
- route._componentsPromise ||
2670
- route.options.head ||
2671
- route.options.scripts ||
2672
- route.options.headers ||
2673
- this.getMatch(matchId)!._nonReactive.minPendingPromise
2674
- )
2675
-
2676
- if (willLoadSomething) {
2677
- innerLoadContext.updateMatch(matchId, (prev) => ({
2678
- ...prev,
2679
- isFetching: 'loader',
2680
- }))
2681
- }
2682
-
2683
- if (route.options.loader) {
2684
- const loaderData = loaderResultIsPromise
2685
- ? await loaderResult
2686
- : loaderResult
2687
-
2688
- this.handleRedirectAndNotFound(
2689
- innerLoadContext,
2690
- this.getMatch(matchId),
2691
- loaderData,
2692
- )
2693
- if (loaderData !== undefined) {
2694
- innerLoadContext.updateMatch(matchId, (prev) => ({
2695
- ...prev,
2696
- loaderData,
2697
- }))
2698
- }
2699
- }
2700
-
2701
- // Lazy option can modify the route options,
2702
- // so we need to wait for it to resolve before
2703
- // we can use the options
2704
- if (route._lazyPromise) await route._lazyPromise
2705
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2706
- const head = headResult ? await headResult : undefined
2707
- const pendingPromise = this.potentialPendingMinPromise(matchId)
2708
- if (pendingPromise) await pendingPromise
2709
-
2710
- // Last but not least, wait for the the components
2711
- // to be preloaded before we resolve the match
2712
- if (route._componentsPromise) await route._componentsPromise
2713
- innerLoadContext.updateMatch(matchId, (prev) => ({
2714
- ...prev,
2715
- error: undefined,
2716
- status: 'success',
2717
- isFetching: false,
2718
- updatedAt: Date.now(),
2719
- ...head,
2720
- }))
2721
- } catch (e) {
2722
- let error = e
2723
-
2724
- const pendingPromise = this.potentialPendingMinPromise(matchId)
2725
- if (pendingPromise) await pendingPromise
2726
-
2727
- this.handleRedirectAndNotFound(
2728
- innerLoadContext,
2729
- this.getMatch(matchId),
2730
- e,
2731
- )
2732
-
2733
- try {
2734
- route.options.onError?.(e)
2735
- } catch (onErrorError) {
2736
- error = onErrorError
2737
- this.handleRedirectAndNotFound(
2738
- innerLoadContext,
2739
- this.getMatch(matchId),
2740
- onErrorError,
2741
- )
2742
- }
2743
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2744
- const head = headResult ? await headResult : undefined
2745
- innerLoadContext.updateMatch(matchId, (prev) => ({
2746
- ...prev,
2747
- error,
2748
- status: 'error',
2749
- isFetching: false,
2750
- ...head,
2751
- }))
2752
- }
2753
- } catch (err) {
2754
- const match = this.getMatch(matchId)
2755
- // in case of a redirecting match during preload, the match does not exist
2756
- if (match) {
2757
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2758
- if (headResult) {
2759
- const head = await headResult
2760
- innerLoadContext.updateMatch(matchId, (prev) => ({
2761
- ...prev,
2762
- ...head,
2763
- }))
2764
- }
2765
- match._nonReactive.loaderPromise = undefined
2766
- }
2767
- this.handleRedirectAndNotFound(innerLoadContext, match, err)
2768
- }
2769
- }
2770
-
2771
- private loadRouteMatch = async (
2772
- innerLoadContext: InnerLoadContext,
2773
- index: number,
2774
- ): Promise<AnyRouteMatch> => {
2775
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2776
- let loaderShouldRunAsync = false
2777
- let loaderIsRunningAsync = false
2778
- const route = this.looseRoutesById[routeId]!
2779
-
2780
- const prevMatch = this.getMatch(matchId)!
2781
- if (this.shouldSkipLoader(matchId)) {
2782
- if (this.isServer) {
2783
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2784
- if (headResult) {
2785
- const head = await headResult
2786
- innerLoadContext.updateMatch(matchId, (prev) => ({
2787
- ...prev,
2788
- ...head,
2789
- }))
2790
- }
2791
- return this.getMatch(matchId)!
2792
- }
2793
- }
2794
- // there is a loaderPromise, so we are in the middle of a load
2795
- else if (prevMatch._nonReactive.loaderPromise) {
2796
- // do not block if we already have stale data we can show
2797
- // but only if the ongoing load is not a preload since error handling is different for preloads
2798
- // and we don't want to swallow errors
2799
- if (
2800
- prevMatch.status === 'success' &&
2801
- !innerLoadContext.sync &&
2802
- !prevMatch.preload
2803
- ) {
2804
- return this.getMatch(matchId)!
2805
- }
2806
- await prevMatch._nonReactive.loaderPromise
2807
- const match = this.getMatch(matchId)!
2808
- if (match.error) {
2809
- this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2810
- }
2811
- } else {
2812
- // This is where all of the stale-while-revalidate magic happens
2813
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2814
-
2815
- const preload = this.resolvePreload(innerLoadContext, matchId)
2816
-
2817
- const staleAge = preload
2818
- ? (route.options.preloadStaleTime ??
2819
- this.options.defaultPreloadStaleTime ??
2820
- 30_000) // 30 seconds for preloads by default
2821
- : (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
2822
-
2823
- const shouldReloadOption = route.options.shouldReload
2824
-
2825
- // Default to reloading the route all the time
2826
- // Allow shouldReload to get the last say,
2827
- // if provided.
2828
- const shouldReload =
2829
- typeof shouldReloadOption === 'function'
2830
- ? shouldReloadOption(
2831
- this.getLoaderContext(innerLoadContext, matchId, index, route),
2832
- )
2833
- : shouldReloadOption
2834
-
2835
- const nextPreload =
2836
- !!preload && !this.state.matches.some((d) => d.id === matchId)
2837
- const match = this.getMatch(matchId)!
2838
- match._nonReactive.loaderPromise = createControlledPromise<void>()
2839
- if (nextPreload !== match.preload) {
2840
- innerLoadContext.updateMatch(matchId, (prev) => ({
2841
- ...prev,
2842
- preload: nextPreload,
2843
- }))
2844
- }
2845
-
2846
- // If the route is successful and still fresh, just resolve
2847
- const { status, invalid } = this.getMatch(matchId)!
2848
- loaderShouldRunAsync =
2849
- status === 'success' && (invalid || (shouldReload ?? age > staleAge))
2850
- if (preload && route.options.preload === false) {
2851
- // Do nothing
2852
- } else if (loaderShouldRunAsync && !innerLoadContext.sync) {
2853
- loaderIsRunningAsync = true
2854
- ;(async () => {
2855
- try {
2856
- await this.runLoader(innerLoadContext, matchId, index, route)
2857
- const match = this.getMatch(matchId)!
2858
- match._nonReactive.loaderPromise?.resolve()
2859
- match._nonReactive.loadPromise?.resolve()
2860
- match._nonReactive.loaderPromise = undefined
2861
- } catch (err) {
2862
- if (isRedirect(err)) {
2863
- await this.navigate(err.options)
2864
- }
2865
- }
2866
- })()
2867
- } else if (
2868
- status !== 'success' ||
2869
- (loaderShouldRunAsync && innerLoadContext.sync)
2870
- ) {
2871
- await this.runLoader(innerLoadContext, matchId, index, route)
2872
- } else {
2873
- // if the loader did not run, still update head.
2874
- // reason: parent's beforeLoad may have changed the route context
2875
- // and only now do we know the route context (and that the loader would not run)
2876
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2877
- if (headResult) {
2878
- const head = await headResult
2879
- innerLoadContext.updateMatch(matchId, (prev) => ({
2880
- ...prev,
2881
- ...head,
2882
- }))
2883
- }
2884
- }
2885
- }
2886
- const match = this.getMatch(matchId)!
2887
- if (!loaderIsRunningAsync) {
2888
- match._nonReactive.loaderPromise?.resolve()
2889
- match._nonReactive.loadPromise?.resolve()
2890
- }
2891
-
2892
- clearTimeout(match._nonReactive.pendingTimeout)
2893
- match._nonReactive.pendingTimeout = undefined
2894
- if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
2895
- match._nonReactive.dehydrated = undefined
2896
- const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
2897
- if (nextIsFetching !== match.isFetching || match.invalid !== false) {
2898
- innerLoadContext.updateMatch(matchId, (prev) => ({
2899
- ...prev,
2900
- isFetching: nextIsFetching,
2901
- invalid: false,
2902
- }))
2903
- }
2904
- return this.getMatch(matchId)!
2905
- }
2906
-
2907
- loadMatches = async (baseContext: {
2908
- location: ParsedLocation
2909
- matches: Array<AnyRouteMatch>
2910
- preload?: boolean
2911
- onReady?: () => Promise<void>
2912
- updateMatch?: UpdateMatchFn
2913
- sync?: boolean
2914
- }): Promise<Array<MakeRouteMatch>> => {
2915
- const innerLoadContext = baseContext as InnerLoadContext
2916
- innerLoadContext.updateMatch ??= this.updateMatch
2917
- innerLoadContext.matchPromises = []
2918
-
2919
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2920
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2921
- if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2922
- this.triggerOnReady(innerLoadContext)
2923
- }
2924
-
2925
- try {
2926
- // Execute all beforeLoads one by one
2927
- for (let i = 0; i < innerLoadContext.matches.length; i++) {
2928
- const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
2929
- if (isPromise(beforeLoad)) await beforeLoad
2930
- }
2931
-
2932
- // Execute all loaders in parallel
2933
- const max =
2934
- innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
2935
- for (let i = 0; i < max; i++) {
2936
- innerLoadContext.matchPromises.push(
2937
- this.loadRouteMatch(innerLoadContext, i),
2938
- )
2939
- }
2940
- await Promise.all(innerLoadContext.matchPromises)
2941
-
2942
- const readyPromise = this.triggerOnReady(innerLoadContext)
2943
- if (isPromise(readyPromise)) await readyPromise
2944
- } catch (err) {
2945
- if (isNotFound(err) && !innerLoadContext.preload) {
2946
- const readyPromise = this.triggerOnReady(innerLoadContext)
2947
- if (isPromise(readyPromise)) await readyPromise
2948
- throw err
2949
- }
2950
- if (isRedirect(err)) {
2951
- throw err
2952
- }
2953
- }
2954
-
2955
- return innerLoadContext.matches
2956
- }
2957
-
2958
2180
  invalidate: InvalidateFn<
2959
2181
  RouterCore<
2960
2182
  TRouteTree,
@@ -3048,47 +2270,7 @@ export class RouterCore<
3048
2270
  this.clearCache({ filter })
3049
2271
  }
3050
2272
 
3051
- loadRouteChunk = (route: AnyRoute) => {
3052
- if (!route._lazyLoaded && route._lazyPromise === undefined) {
3053
- if (route.lazyFn) {
3054
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
3055
- // explicitly don't copy over the lazy route's id
3056
- const { id: _id, ...options } = lazyRoute.options
3057
- Object.assign(route.options, options)
3058
- route._lazyLoaded = true
3059
- route._lazyPromise = undefined // gc promise, we won't need it anymore
3060
- })
3061
- } else {
3062
- route._lazyLoaded = true
3063
- }
3064
- }
3065
-
3066
- // If for some reason lazy resolves more lazy components...
3067
- // We'll wait for that before we attempt to preload the
3068
- // components themselves.
3069
- if (!route._componentsLoaded && route._componentsPromise === undefined) {
3070
- const loadComponents = () => {
3071
- const preloads = []
3072
- for (const type of componentTypes) {
3073
- const preload = (route.options[type] as any)?.preload
3074
- if (preload) preloads.push(preload())
3075
- }
3076
- if (preloads.length)
3077
- return Promise.all(preloads).then(() => {
3078
- route._componentsLoaded = true
3079
- route._componentsPromise = undefined // gc promise, we won't need it anymore
3080
- })
3081
- route._componentsLoaded = true
3082
- route._componentsPromise = undefined // gc promise, we won't need it anymore
3083
- return
3084
- }
3085
- route._componentsPromise = route._lazyPromise
3086
- ? route._lazyPromise.then(loadComponents)
3087
- : loadComponents()
3088
- }
3089
-
3090
- return route._componentsPromise
3091
- }
2273
+ loadRouteChunk = loadRouteChunk
3092
2274
 
3093
2275
  preloadRoute: PreloadRouteFn<
3094
2276
  TRouteTree,
@@ -3128,7 +2310,8 @@ export class RouterCore<
3128
2310
  })
3129
2311
 
3130
2312
  try {
3131
- matches = await this.loadMatches({
2313
+ matches = await loadMatches({
2314
+ router: this,
3132
2315
  matches,
3133
2316
  location: next,
3134
2317
  preload: true,
@@ -3193,7 +2376,6 @@ export class RouterCore<
3193
2376
  : this.state.resolvedLocation || this.state.location
3194
2377
 
3195
2378
  const match = matchPathname(
3196
- this.basepath,
3197
2379
  baseLocation.pathname,
3198
2380
  {
3199
2381
  ...opts,
@@ -3226,58 +2408,6 @@ export class RouterCore<
3226
2408
 
3227
2409
  serverSsr?: ServerSsr
3228
2410
 
3229
- private _handleNotFound = (
3230
- innerLoadContext: InnerLoadContext,
3231
- err: NotFoundError,
3232
- ) => {
3233
- // Find the route that should handle the not found error
3234
- // First check if a specific route is requested to show the error
3235
- const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
3236
- const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3237
-
3238
- // Setup routesByRouteId object for quick access
3239
- for (const match of innerLoadContext.matches) {
3240
- matchesByRouteId[match.routeId] = match
3241
- }
3242
-
3243
- // Ensure a NotFoundComponent exists on the route
3244
- if (
3245
- !routeCursor.options.notFoundComponent &&
3246
- (this.options as any)?.defaultNotFoundComponent
3247
- ) {
3248
- routeCursor.options.notFoundComponent = (
3249
- this.options as any
3250
- ).defaultNotFoundComponent
3251
- }
3252
-
3253
- // Ensure we have a notFoundComponent
3254
- invariant(
3255
- routeCursor.options.notFoundComponent,
3256
- 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
3257
- )
3258
-
3259
- // Find the match for this route
3260
- const matchForRoute = matchesByRouteId[routeCursor.id]
3261
-
3262
- invariant(
3263
- matchForRoute,
3264
- 'Could not find match for route: ' + routeCursor.id,
3265
- )
3266
-
3267
- // Assign the error to the match - using non-null assertion since we've checked with invariant
3268
- innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
3269
- ...prev,
3270
- status: 'notFound',
3271
- error: err,
3272
- isFetching: false,
3273
- }))
3274
-
3275
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3276
- err.routeId = routeCursor.parentRoute.id
3277
- this._handleNotFound(innerLoadContext, err)
3278
- }
3279
- }
3280
-
3281
2411
  hasNotFoundMatch = () => {
3282
2412
  return this.__store.state.matches.some(
3283
2413
  (d) => d.status === 'notFound' || d.globalNotFound,
@@ -3289,16 +2419,6 @@ export class SearchParamError extends Error {}
3289
2419
 
3290
2420
  export class PathParamError extends Error {}
3291
2421
 
3292
- function makeMaybe<TValue, TError>(
3293
- value: TValue,
3294
- error: TError,
3295
- ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
3296
- if (error) {
3297
- return { status: 'error' as const, error }
3298
- }
3299
- return { status: 'success' as const, value }
3300
- }
3301
-
3302
2422
  const normalize = (str: string) =>
3303
2423
  str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3304
2424
  function comparePaths(a: string, b: string) {
@@ -3365,22 +2485,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3365
2485
  return {}
3366
2486
  }
3367
2487
 
3368
- export const componentTypes = [
3369
- 'component',
3370
- 'errorComponent',
3371
- 'pendingComponent',
3372
- 'notFoundComponent',
3373
- ] as const
3374
-
3375
- function routeNeedsPreload(route: AnyRoute) {
3376
- for (const componentType of componentTypes) {
3377
- if ((route.options[componentType] as any)?.preload) {
3378
- return true
3379
- }
3380
- }
3381
- return false
3382
- }
3383
-
3384
2488
  interface RouteLike {
3385
2489
  id: string
3386
2490
  isRoot?: boolean
@@ -3607,7 +2711,6 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3607
2711
  export function getMatchedRoutes<TRouteLike extends RouteLike>({
3608
2712
  pathname,
3609
2713
  routePathname,
3610
- basepath,
3611
2714
  caseSensitive,
3612
2715
  routesByPath,
3613
2716
  routesById,
@@ -3616,7 +2719,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3616
2719
  }: {
3617
2720
  pathname: string
3618
2721
  routePathname?: string
3619
- basepath: string
3620
2722
  caseSensitive?: boolean
3621
2723
  routesByPath: Record<string, TRouteLike>
3622
2724
  routesById: Record<string, TRouteLike>
@@ -3627,7 +2729,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3627
2729
  const trimmedPath = trimPathRight(pathname)
3628
2730
  const getMatchedParams = (route: TRouteLike) => {
3629
2731
  const result = matchPathname(
3630
- basepath,
3631
2732
  trimmedPath,
3632
2733
  {
3633
2734
  to: route.fullPath,