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

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 +131 -778
  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 +133 -780
  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 +267 -1168
  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,12 +1274,18 @@ export class RouterCore<
1218
1274
 
1219
1275
  const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1220
1276
 
1221
- const { usedParams, interpolatedPath } = interpolatePath({
1277
+ const { interpolatedPath, usedParams } = interpolatePath({
1222
1278
  path: route.fullPath,
1223
1279
  params: routeParams,
1224
1280
  decodeCharMap: this.pathParamsDecodeCharMap,
1225
1281
  })
1226
1282
 
1283
+ // Waste not, want not. If we already have a match for this route,
1284
+ // reuse it. This is important for layout routes, which might stick
1285
+ // around between navigation actions that only change leaf routes.
1286
+
1287
+ // Existing matches are matches that are already loaded along with
1288
+ // pending matches that are still loading
1227
1289
  const matchId =
1228
1290
  interpolatePath({
1229
1291
  path: route.id,
@@ -1233,18 +1295,40 @@ export class RouterCore<
1233
1295
  parseCache: this.parsePathnameCache,
1234
1296
  }).interpolatedPath + loaderDepsHash
1235
1297
 
1236
- // Waste not, want not. If we already have a match for this route,
1237
- // reuse it. This is important for layout routes, which might stick
1238
- // around between navigation actions that only change leaf routes.
1239
-
1240
- // Existing matches are matches that are already loaded along with
1241
- // pending matches that are still loading
1242
1298
  const existingMatch = this.getMatch(matchId)
1243
1299
 
1244
1300
  const previousMatch = this.state.matches.find(
1245
1301
  (d) => d.routeId === route.id,
1246
1302
  )
1247
1303
 
1304
+ const strictParams = existingMatch?._strictParams ?? usedParams
1305
+
1306
+ let paramsError: PathParamError | undefined = undefined
1307
+
1308
+ if (!existingMatch) {
1309
+ const strictParseParams =
1310
+ route.options.params?.parse ?? route.options.parseParams
1311
+
1312
+ if (strictParseParams) {
1313
+ try {
1314
+ Object.assign(
1315
+ strictParams,
1316
+ strictParseParams(strictParams as Record<string, string>),
1317
+ )
1318
+ } catch (err: any) {
1319
+ paramsError = new PathParamError(err.message, {
1320
+ cause: err,
1321
+ })
1322
+
1323
+ if (opts?.throwOnError) {
1324
+ throw paramsError
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ Object.assign(routeParams, strictParams)
1331
+
1248
1332
  const cause = previousMatch ? 'stay' : 'enter'
1249
1333
 
1250
1334
  let match: AnyRouteMatch
@@ -1256,7 +1340,7 @@ export class RouterCore<
1256
1340
  params: previousMatch
1257
1341
  ? replaceEqualDeep(previousMatch.params, routeParams)
1258
1342
  : routeParams,
1259
- _strictParams: usedParams,
1343
+ _strictParams: strictParams,
1260
1344
  search: previousMatch
1261
1345
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1262
1346
  : replaceEqualDeep(existingMatch.search, preMatchSearch),
@@ -1278,8 +1362,8 @@ export class RouterCore<
1278
1362
  params: previousMatch
1279
1363
  ? replaceEqualDeep(previousMatch.params, routeParams)
1280
1364
  : routeParams,
1281
- _strictParams: usedParams,
1282
- pathname: joinPaths([this.basepath, interpolatedPath]),
1365
+ _strictParams: strictParams,
1366
+ pathname: interpolatedPath,
1283
1367
  updatedAt: Date.now(),
1284
1368
  search: previousMatch
1285
1369
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
@@ -1289,7 +1373,7 @@ export class RouterCore<
1289
1373
  status,
1290
1374
  isFetching: false,
1291
1375
  error: undefined,
1292
- paramsError: parseErrors[index],
1376
+ paramsError,
1293
1377
  __routeContext: undefined,
1294
1378
  _nonReactive: {
1295
1379
  loadPromise: createControlledPromise(),
@@ -1384,7 +1468,6 @@ export class RouterCore<
1384
1468
  return getMatchedRoutes({
1385
1469
  pathname,
1386
1470
  routePathname,
1387
- basepath: this.basepath,
1388
1471
  caseSensitive: this.options.caseSensitive,
1389
1472
  routesByPath: this.routesByPath,
1390
1473
  routesById: this.routesById,
@@ -1422,52 +1505,44 @@ export class RouterCore<
1422
1505
  _buildLocation: true,
1423
1506
  })
1424
1507
 
1508
+ // Now let's find the starting pathname
1509
+ // This should default to the current location if no from is provided
1425
1510
  const lastMatch = last(allCurrentLocationMatches)!
1426
1511
 
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
1512
+ // check that from path exists in the current route tree
1513
+ // do this check only on navigations during test or development
1514
+ if (
1515
+ dest.from &&
1516
+ process.env.NODE_ENV !== 'production' &&
1517
+ dest._isNavigate
1518
+ ) {
1519
+ const allFromMatches = this.getMatchedRoutes(
1520
+ dest.from,
1521
+ undefined,
1522
+ ).matchedRoutes
1451
1523
 
1452
- const matchedFrom = [...allCurrentLocationMatches]
1453
- .reverse()
1454
- .find((d) => {
1455
- return comparePaths(d.fullPath, fromPath)
1456
- })
1524
+ const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1525
+ return comparePaths(d.fullPath, dest.from!)
1526
+ })
1457
1527
 
1458
- const matchedCurrent = [...allFromMatches].reverse().find((d) => {
1459
- return comparePaths(d.fullPath, currentLocation.pathname)
1460
- })
1528
+ const matchedCurrent = findLast(allFromMatches, (d) => {
1529
+ return comparePaths(d.fullPath, lastMatch.fullPath)
1530
+ })
1461
1531
 
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
- }
1532
+ // for from to be invalid it shouldn't just be unmatched to currentLocation
1533
+ // but the currentLocation should also be unmatched to from
1534
+ if (!matchedFrom && !matchedCurrent) {
1535
+ console.warn(`Could not find match for from: ${dest.from}`)
1467
1536
  }
1468
1537
  }
1469
1538
 
1470
- fromPath = this.resolvePathWithBase(fromPath, '.')
1539
+ const defaultedFromPath =
1540
+ dest.unsafeRelative === 'path'
1541
+ ? currentLocation.pathname
1542
+ : (dest.from ?? lastMatch.fullPath)
1543
+
1544
+ // ensure this includes the basePath if set
1545
+ const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1471
1546
 
1472
1547
  // From search should always use the current location
1473
1548
  const fromSearch = lastMatch.search
@@ -1475,25 +1550,26 @@ export class RouterCore<
1475
1550
  const fromParams = { ...lastMatch.params }
1476
1551
 
1477
1552
  // Resolve the next to
1553
+ // ensure this includes the basePath if set
1478
1554
  const nextTo = dest.to
1479
1555
  ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1480
1556
  : this.resolvePathWithBase(fromPath, '.')
1481
1557
 
1482
1558
  // Resolve the next params
1483
- let nextParams =
1559
+ const nextParams =
1484
1560
  dest.params === false || dest.params === null
1485
1561
  ? {}
1486
1562
  : (dest.params ?? true) === true
1487
1563
  ? fromParams
1488
- : {
1489
- ...fromParams,
1490
- ...functionalUpdate(dest.params as any, fromParams),
1491
- }
1564
+ : Object.assign(
1565
+ fromParams,
1566
+ functionalUpdate(dest.params as any, fromParams),
1567
+ )
1492
1568
 
1493
1569
  // Interpolate the path first to get the actual resolved path, then match against that
1494
1570
  const interpolatedNextTo = interpolatePath({
1495
1571
  path: nextTo,
1496
- params: nextParams ?? {},
1572
+ params: nextParams,
1497
1573
  parseCache: this.parsePathnameCache,
1498
1574
  }).interpolatedPath
1499
1575
 
@@ -1503,23 +1579,20 @@ export class RouterCore<
1503
1579
 
1504
1580
  // If there are any params, we need to stringify them
1505
1581
  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
- })
1582
+ for (const route of destRoutes) {
1583
+ const fn =
1584
+ route.options.params?.stringify ?? route.options.stringifyParams
1585
+ if (fn) {
1586
+ Object.assign(nextParams, fn(nextParams))
1587
+ }
1588
+ }
1516
1589
  }
1517
1590
 
1518
1591
  const nextPathname = interpolatePath({
1519
1592
  // Use the original template path for interpolation
1520
1593
  // This preserves the original parameter syntax including optional parameters
1521
1594
  path: nextTo,
1522
- params: nextParams ?? {},
1595
+ params: nextParams,
1523
1596
  leaveWildcards: false,
1524
1597
  leaveParams: opts.leaveParams,
1525
1598
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1529,20 +1602,20 @@ export class RouterCore<
1529
1602
  // Resolve the next search
1530
1603
  let nextSearch = fromSearch
1531
1604
  if (opts._includeValidateSearch && this.options.search?.strict) {
1532
- let validatedSearch = {}
1605
+ const validatedSearch = {}
1533
1606
  destRoutes.forEach((route) => {
1534
- try {
1535
- if (route.options.validateSearch) {
1536
- validatedSearch = {
1537
- ...validatedSearch,
1538
- ...(validateSearch(route.options.validateSearch, {
1607
+ if (route.options.validateSearch) {
1608
+ try {
1609
+ Object.assign(
1610
+ validatedSearch,
1611
+ validateSearch(route.options.validateSearch, {
1539
1612
  ...validatedSearch,
1540
1613
  ...nextSearch,
1541
- }) ?? {}),
1542
- }
1614
+ }),
1615
+ )
1616
+ } catch {
1617
+ // ignore errors here because they are already handled in matchRoutes
1543
1618
  }
1544
- } catch {
1545
- // ignore errors here because they are already handled in matchRoutes
1546
1619
  }
1547
1620
  })
1548
1621
  nextSearch = validatedSearch
@@ -1583,14 +1656,25 @@ export class RouterCore<
1583
1656
  // Replace the equal deep
1584
1657
  nextState = replaceEqualDeep(currentLocation.state, nextState)
1585
1658
 
1586
- // Return the next location
1659
+ // Create the full path of the location
1660
+ const fullPath = `${nextPathname}${searchStr}${hashStr}`
1661
+
1662
+ // Create the new href with full origin
1663
+ const url = new URL(fullPath, this.origin)
1664
+
1665
+ // If a rewrite function is provided, use it to rewrite the URL
1666
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1667
+
1587
1668
  return {
1669
+ publicHref:
1670
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
1671
+ href: fullPath,
1672
+ url: rewrittenUrl.href,
1588
1673
  pathname: nextPathname,
1589
1674
  search: nextSearch,
1590
1675
  searchStr,
1591
1676
  state: nextState as any,
1592
1677
  hash: hash ?? '',
1593
- href: `${nextPathname}${searchStr}${hashStr}`,
1594
1678
  unmaskOnReload: dest.unmaskOnReload,
1595
1679
  }
1596
1680
  }
@@ -1608,7 +1692,6 @@ export class RouterCore<
1608
1692
 
1609
1693
  const foundMask = this.options.routeMasks?.find((d) => {
1610
1694
  const match = matchPathname(
1611
- this.basepath,
1612
1695
  next.pathname,
1613
1696
  {
1614
1697
  to: d.from,
@@ -1629,7 +1712,7 @@ export class RouterCore<
1629
1712
  if (foundMask) {
1630
1713
  const { from: _from, ...maskProps } = foundMask
1631
1714
  maskedDest = {
1632
- ...pick(opts, ['from']),
1715
+ from: opts.from,
1633
1716
  ...maskProps,
1634
1717
  params,
1635
1718
  }
@@ -1638,8 +1721,7 @@ export class RouterCore<
1638
1721
  }
1639
1722
 
1640
1723
  if (maskedNext) {
1641
- const maskedFinal = build(maskedDest)
1642
- next.maskedLocation = maskedFinal
1724
+ next.maskedLocation = maskedNext
1643
1725
  }
1644
1726
 
1645
1727
  return next
@@ -1647,7 +1729,7 @@ export class RouterCore<
1647
1729
 
1648
1730
  if (opts.mask) {
1649
1731
  return buildWithMatches(opts, {
1650
- ...pick(opts, ['from']),
1732
+ from: opts.from,
1651
1733
  ...opts.mask,
1652
1734
  })
1653
1735
  }
@@ -1682,7 +1764,8 @@ export class RouterCore<
1682
1764
  return isEqual
1683
1765
  }
1684
1766
 
1685
- const isSameUrl = this.latestLocation.href === next.href
1767
+ const isSameUrl =
1768
+ trimPathRight(this.latestLocation.href) === trimPathRight(next.href)
1686
1769
 
1687
1770
  const previousCommitPromise = this.commitLocationPromise
1688
1771
  this.commitLocationPromise = createControlledPromise<void>(() => {
@@ -1731,7 +1814,7 @@ export class RouterCore<
1731
1814
  this.shouldViewTransition = viewTransition
1732
1815
 
1733
1816
  this.history[next.replace ? 'replace' : 'push'](
1734
- nextHistory.href,
1817
+ nextHistory.publicHref,
1735
1818
  nextHistory.state,
1736
1819
  { ignoreBlocker },
1737
1820
  )
@@ -1793,7 +1876,7 @@ export class RouterCore<
1793
1876
  if (reloadDocument) {
1794
1877
  if (!href) {
1795
1878
  const location = this.buildLocation({ to, ...rest } as any)
1796
- href = this.history.createHref(location.href)
1879
+ href = location.href
1797
1880
  }
1798
1881
  if (rest.replace) {
1799
1882
  window.location.replace(href)
@@ -1816,7 +1899,7 @@ export class RouterCore<
1816
1899
  beforeLoad = () => {
1817
1900
  // Cancel any pending matches
1818
1901
  this.cancelMatches()
1819
- this.latestLocation = this.parseLocation(this.latestLocation)
1902
+ this.updateLatestLocation()
1820
1903
 
1821
1904
  if (this.isServer) {
1822
1905
  // for SPAs on the initial load, this is handled by the Transitioner
@@ -1846,6 +1929,7 @@ export class RouterCore<
1846
1929
  throw redirect({ href: nextLocation.href })
1847
1930
  }
1848
1931
  }
1932
+
1849
1933
  // Match the routes
1850
1934
  const pendingMatches = this.matchRoutes(this.latestLocation)
1851
1935
 
@@ -1895,10 +1979,12 @@ export class RouterCore<
1895
1979
  }),
1896
1980
  })
1897
1981
 
1898
- await this.loadMatches({
1982
+ await loadMatches({
1983
+ router: this,
1899
1984
  sync: opts?.sync,
1900
1985
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1901
1986
  location: next,
1987
+ updateMatch: this.updateMatch,
1902
1988
  // eslint-disable-next-line @typescript-eslint/require-await
1903
1989
  onReady: async () => {
1904
1990
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -1989,6 +2075,7 @@ export class RouterCore<
1989
2075
  this.latestLoadPromise = undefined
1990
2076
  this.commitLocationPromise = undefined
1991
2077
  }
2078
+
1992
2079
  resolve()
1993
2080
  })
1994
2081
  })
@@ -2088,873 +2175,6 @@ export class RouterCore<
2088
2175
  )
2089
2176
  }
2090
2177
 
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
2178
  invalidate: InvalidateFn<
2959
2179
  RouterCore<
2960
2180
  TRouteTree,
@@ -3048,47 +2268,7 @@ export class RouterCore<
3048
2268
  this.clearCache({ filter })
3049
2269
  }
3050
2270
 
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
- }
2271
+ loadRouteChunk = loadRouteChunk
3092
2272
 
3093
2273
  preloadRoute: PreloadRouteFn<
3094
2274
  TRouteTree,
@@ -3128,7 +2308,8 @@ export class RouterCore<
3128
2308
  })
3129
2309
 
3130
2310
  try {
3131
- matches = await this.loadMatches({
2311
+ matches = await loadMatches({
2312
+ router: this,
3132
2313
  matches,
3133
2314
  location: next,
3134
2315
  preload: true,
@@ -3193,7 +2374,6 @@ export class RouterCore<
3193
2374
  : this.state.resolvedLocation || this.state.location
3194
2375
 
3195
2376
  const match = matchPathname(
3196
- this.basepath,
3197
2377
  baseLocation.pathname,
3198
2378
  {
3199
2379
  ...opts,
@@ -3226,58 +2406,6 @@ export class RouterCore<
3226
2406
 
3227
2407
  serverSsr?: ServerSsr
3228
2408
 
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
2409
  hasNotFoundMatch = () => {
3282
2410
  return this.__store.state.matches.some(
3283
2411
  (d) => d.status === 'notFound' || d.globalNotFound,
@@ -3289,16 +2417,6 @@ export class SearchParamError extends Error {}
3289
2417
 
3290
2418
  export class PathParamError extends Error {}
3291
2419
 
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
2420
  const normalize = (str: string) =>
3303
2421
  str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3304
2422
  function comparePaths(a: string, b: string) {
@@ -3365,22 +2483,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3365
2483
  return {}
3366
2484
  }
3367
2485
 
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
2486
  interface RouteLike {
3385
2487
  id: string
3386
2488
  isRoot?: boolean
@@ -3607,7 +2709,6 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3607
2709
  export function getMatchedRoutes<TRouteLike extends RouteLike>({
3608
2710
  pathname,
3609
2711
  routePathname,
3610
- basepath,
3611
2712
  caseSensitive,
3612
2713
  routesByPath,
3613
2714
  routesById,
@@ -3616,7 +2717,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3616
2717
  }: {
3617
2718
  pathname: string
3618
2719
  routePathname?: string
3619
- basepath: string
3620
2720
  caseSensitive?: boolean
3621
2721
  routesByPath: Record<string, TRouteLike>
3622
2722
  routesById: Record<string, TRouteLike>
@@ -3627,7 +2727,6 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3627
2727
  const trimmedPath = trimPathRight(pathname)
3628
2728
  const getMatchedParams = (route: TRouteLike) => {
3629
2729
  const result = matchPathname(
3630
- basepath,
3631
2730
  trimmedPath,
3632
2731
  {
3633
2732
  to: route.fullPath,