@tanstack/router-core 1.132.0-alpha.0 → 1.132.0-alpha.12

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 (128) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +9 -11
  3. package/dist/cjs/config.cjs +10 -0
  4. package/dist/cjs/config.cjs.map +1 -0
  5. package/dist/cjs/config.d.cts +17 -0
  6. package/dist/cjs/fileRoute.d.cts +3 -2
  7. package/dist/cjs/index.cjs +10 -2
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +8 -3
  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/qss.cjs +19 -19
  14. package/dist/cjs/qss.cjs.map +1 -1
  15. package/dist/cjs/qss.d.cts +6 -4
  16. package/dist/cjs/redirect.cjs +3 -3
  17. package/dist/cjs/redirect.cjs.map +1 -1
  18. package/dist/cjs/route.cjs.map +1 -1
  19. package/dist/cjs/route.d.cts +42 -41
  20. package/dist/cjs/router.cjs +85 -654
  21. package/dist/cjs/router.cjs.map +1 -1
  22. package/dist/cjs/router.d.cts +19 -23
  23. package/dist/cjs/scroll-restoration.cjs +32 -29
  24. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  25. package/dist/cjs/scroll-restoration.d.cts +1 -10
  26. package/dist/cjs/searchParams.cjs +7 -15
  27. package/dist/cjs/searchParams.cjs.map +1 -1
  28. package/dist/cjs/ssr/constants.cjs +5 -0
  29. package/dist/cjs/ssr/constants.cjs.map +1 -0
  30. package/dist/cjs/ssr/constants.d.cts +1 -0
  31. package/dist/cjs/ssr/{seroval-plugins.cjs → serializer/ShallowErrorPlugin.cjs} +2 -2
  32. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -0
  33. package/dist/cjs/ssr/{seroval-plugins.d.cts → serializer/ShallowErrorPlugin.d.cts} +1 -2
  34. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +11 -0
  35. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -0
  36. package/dist/cjs/ssr/serializer/seroval-plugins.d.cts +2 -0
  37. package/dist/cjs/ssr/serializer/transformer.cjs +52 -0
  38. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -0
  39. package/dist/cjs/ssr/serializer/transformer.d.cts +56 -0
  40. package/dist/cjs/ssr/server.d.cts +5 -0
  41. package/dist/cjs/ssr/ssr-client.cjs +53 -40
  42. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  43. package/dist/cjs/ssr/ssr-client.d.cts +5 -1
  44. package/dist/cjs/ssr/ssr-server.cjs +12 -10
  45. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  46. package/dist/cjs/ssr/ssr-server.d.cts +0 -1
  47. package/dist/cjs/ssr/tsrScript.cjs +1 -1
  48. package/dist/cjs/ssr/tsrScript.cjs.map +1 -1
  49. package/dist/cjs/typePrimitives.d.cts +6 -6
  50. package/dist/cjs/utils.cjs +14 -7
  51. package/dist/cjs/utils.cjs.map +1 -1
  52. package/dist/cjs/utils.d.cts +2 -1
  53. package/dist/esm/Matches.d.ts +9 -11
  54. package/dist/esm/Matches.js.map +1 -1
  55. package/dist/esm/config.d.ts +17 -0
  56. package/dist/esm/config.js +10 -0
  57. package/dist/esm/config.js.map +1 -0
  58. package/dist/esm/fileRoute.d.ts +3 -2
  59. package/dist/esm/index.d.ts +8 -3
  60. package/dist/esm/index.js +11 -3
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/load-matches.d.ts +16 -0
  63. package/dist/esm/load-matches.js +636 -0
  64. package/dist/esm/load-matches.js.map +1 -0
  65. package/dist/esm/qss.d.ts +6 -4
  66. package/dist/esm/qss.js +19 -19
  67. package/dist/esm/qss.js.map +1 -1
  68. package/dist/esm/redirect.js +3 -3
  69. package/dist/esm/redirect.js.map +1 -1
  70. package/dist/esm/route.d.ts +42 -41
  71. package/dist/esm/route.js.map +1 -1
  72. package/dist/esm/router.d.ts +19 -23
  73. package/dist/esm/router.js +85 -654
  74. package/dist/esm/router.js.map +1 -1
  75. package/dist/esm/scroll-restoration.d.ts +1 -10
  76. package/dist/esm/scroll-restoration.js +32 -29
  77. package/dist/esm/scroll-restoration.js.map +1 -1
  78. package/dist/esm/searchParams.js +7 -15
  79. package/dist/esm/searchParams.js.map +1 -1
  80. package/dist/esm/ssr/constants.d.ts +1 -0
  81. package/dist/esm/ssr/constants.js +5 -0
  82. package/dist/esm/ssr/constants.js.map +1 -0
  83. package/dist/esm/ssr/{seroval-plugins.d.ts → serializer/ShallowErrorPlugin.d.ts} +1 -2
  84. package/dist/esm/ssr/{seroval-plugins.js → serializer/ShallowErrorPlugin.js} +2 -2
  85. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -0
  86. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -0
  87. package/dist/esm/ssr/serializer/seroval-plugins.js +11 -0
  88. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -0
  89. package/dist/esm/ssr/serializer/transformer.d.ts +56 -0
  90. package/dist/esm/ssr/serializer/transformer.js +52 -0
  91. package/dist/esm/ssr/serializer/transformer.js.map +1 -0
  92. package/dist/esm/ssr/server.d.ts +5 -0
  93. package/dist/esm/ssr/ssr-client.d.ts +5 -1
  94. package/dist/esm/ssr/ssr-client.js +53 -40
  95. package/dist/esm/ssr/ssr-client.js.map +1 -1
  96. package/dist/esm/ssr/ssr-server.d.ts +0 -1
  97. package/dist/esm/ssr/ssr-server.js +12 -10
  98. package/dist/esm/ssr/ssr-server.js.map +1 -1
  99. package/dist/esm/ssr/tsrScript.js +1 -1
  100. package/dist/esm/ssr/tsrScript.js.map +1 -1
  101. package/dist/esm/typePrimitives.d.ts +6 -6
  102. package/dist/esm/utils.d.ts +2 -1
  103. package/dist/esm/utils.js +14 -7
  104. package/dist/esm/utils.js.map +1 -1
  105. package/package.json +2 -2
  106. package/src/Matches.ts +18 -10
  107. package/src/config.ts +42 -0
  108. package/src/fileRoute.ts +15 -3
  109. package/src/index.ts +24 -2
  110. package/src/load-matches.ts +955 -0
  111. package/src/qss.ts +27 -24
  112. package/src/redirect.ts +3 -3
  113. package/src/route.ts +146 -35
  114. package/src/router.ts +135 -925
  115. package/src/scroll-restoration.ts +42 -37
  116. package/src/searchParams.ts +8 -19
  117. package/src/ssr/constants.ts +1 -0
  118. package/src/ssr/{seroval-plugins.ts → serializer/ShallowErrorPlugin.ts} +2 -2
  119. package/src/ssr/serializer/seroval-plugins.ts +9 -0
  120. package/src/ssr/serializer/transformer.ts +215 -0
  121. package/src/ssr/server.ts +6 -0
  122. package/src/ssr/ssr-client.ts +72 -44
  123. package/src/ssr/ssr-server.ts +18 -10
  124. package/src/ssr/tsrScript.ts +5 -1
  125. package/src/typePrimitives.ts +6 -6
  126. package/src/utils.ts +21 -10
  127. package/dist/cjs/ssr/seroval-plugins.cjs.map +0 -1
  128. package/dist/esm/ssr/seroval-plugins.js.map +0 -1
package/src/router.ts CHANGED
@@ -8,9 +8,9 @@ import invariant from 'tiny-invariant'
8
8
  import {
9
9
  createControlledPromise,
10
10
  deepEqual,
11
+ findLast,
11
12
  functionalUpdate,
12
13
  last,
13
- pick,
14
14
  replaceEqualDeep,
15
15
  } from './utils'
16
16
  import {
@@ -34,6 +34,7 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
34
34
  import { rootRouteId } from './root'
35
35
  import { isRedirect, redirect } from './redirect'
36
36
  import { createLRUCache } from './lru-cache'
37
+ import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches'
37
38
  import type { ParsePathnameCache, Segment } from './path'
38
39
  import type { SearchParser, SearchSerializer } from './searchParams'
39
40
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
@@ -45,6 +46,7 @@ import type {
45
46
  } from '@tanstack/history'
46
47
  import type {
47
48
  Awaitable,
49
+ Constrain,
48
50
  ControlledPromise,
49
51
  NoInfer,
50
52
  NonNullableUpdater,
@@ -56,13 +58,10 @@ import type {
56
58
  AnyContext,
57
59
  AnyRoute,
58
60
  AnyRouteWithContext,
59
- BeforeLoadContextOptions,
60
- LoaderFnContext,
61
61
  MakeRemountDepsOptionsUnion,
62
62
  RouteContextOptions,
63
63
  RouteMask,
64
64
  SearchMiddleware,
65
- SsrContextOptions,
66
65
  } from './route'
67
66
  import type {
68
67
  FullSearchSchema,
@@ -86,6 +85,11 @@ import type { Manifest } from './manifest'
86
85
  import type { AnySchema, AnyValidator } from './validators'
87
86
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
88
87
  import type { NotFoundError } from './not-found'
88
+ import type {
89
+ AnySerializationAdapter,
90
+ ValidateSerializableInput,
91
+ } from './ssr/serializer/transformer'
92
+ import type { AnyRouterConfig } from './config'
89
93
 
90
94
  export type ControllablePromise<T = any> = Promise<T> & {
91
95
  resolve: (value: T) => void
@@ -96,6 +100,8 @@ export type InjectedHtmlEntry = Promise<string>
96
100
 
97
101
  export interface DefaultRegister {
98
102
  router: AnyRouter
103
+ config: AnyRouterConfig
104
+ ssr: SSROption
99
105
  }
100
106
 
101
107
  export interface Register extends DefaultRegister {
@@ -113,12 +119,14 @@ export interface DefaultRouterOptionsExtensions {}
113
119
  export interface RouterOptionsExtensions
114
120
  extends DefaultRouterOptionsExtensions {}
115
121
 
122
+ export type SSROption = boolean | 'data-only'
123
+
116
124
  export interface RouterOptions<
117
125
  TRouteTree extends AnyRoute,
118
126
  TTrailingSlashOption extends TrailingSlashOption,
119
127
  TDefaultStructuralSharingOption extends boolean = false,
120
128
  TRouterHistory extends RouterHistory = RouterHistory,
121
- TDehydrated extends Record<string, any> = Record<string, any>,
129
+ TDehydrated = undefined,
122
130
  > extends RouterOptionsExtensions {
123
131
  /**
124
132
  * The history object that will be used to manage the browser history.
@@ -286,7 +294,10 @@ export interface RouterOptions<
286
294
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
287
295
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
288
296
  */
289
- dehydrate?: () => Awaitable<TDehydrated>
297
+ dehydrate?: () => Constrain<
298
+ TDehydrated,
299
+ ValidateSerializableInput<Register, TDehydrated>
300
+ >
290
301
  /**
291
302
  * A function that will be called when the router is hydrated.
292
303
  *
@@ -356,7 +367,7 @@ export interface RouterOptions<
356
367
  *
357
368
  * @default true
358
369
  */
359
- defaultSsr?: boolean | 'data-only'
370
+ defaultSsr?: SSROption
360
371
 
361
372
  search?: {
362
373
  /**
@@ -392,7 +403,9 @@ export interface RouterOptions<
392
403
  *
393
404
  * @default false
394
405
  */
395
- scrollRestoration?: boolean
406
+ scrollRestoration?:
407
+ | boolean
408
+ | ((opts: { location: ParsedLocation }) => boolean)
396
409
 
397
410
  /**
398
411
  * A function that will be called to get the key for the scroll restoration cache.
@@ -423,6 +436,8 @@ export interface RouterOptions<
423
436
  * @default false
424
437
  */
425
438
  disableGlobalCatchBoundary?: boolean
439
+
440
+ serializationAdapters?: ReadonlyArray<AnySerializationAdapter>
426
441
  }
427
442
 
428
443
  export interface RouterState<
@@ -614,8 +629,8 @@ export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
614
629
  }) => Promise<void>
615
630
 
616
631
  export type ParseLocationFn<TRouteTree extends AnyRoute> = (
632
+ locationToParse: HistoryLocation,
617
633
  previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
618
- locationToParse?: HistoryLocation,
619
634
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
620
635
 
621
636
  export type GetMatchRoutesFn = (
@@ -705,6 +720,7 @@ export interface ViewTransitionOptions {
705
720
  }) => Array<string>)
706
721
  }
707
722
 
723
+ // TODO where is this used? can we remove this?
708
724
  export function defaultSerializeError(err: unknown) {
709
725
  if (err instanceof Error) {
710
726
  const obj = {
@@ -901,7 +917,7 @@ export class RouterCore<
901
917
  initialEntries: [this.basepath || '/'],
902
918
  })
903
919
  : createBrowserHistory()) as TRouterHistory)
904
- this.latestLocation = this.parseLocation()
920
+ this.updateLatestLocation()
905
921
  }
906
922
 
907
923
  if (this.options.routeTree !== this.routeTree) {
@@ -935,10 +951,17 @@ export class RouterCore<
935
951
  }
936
952
  }
937
953
 
938
- get state() {
954
+ get state(): RouterState<TRouteTree> {
939
955
  return this.__store.state
940
956
  }
941
957
 
958
+ updateLatestLocation = () => {
959
+ this.latestLocation = this.parseLocation(
960
+ this.history.location,
961
+ this.latestLocation,
962
+ )
963
+ }
964
+
942
965
  buildRouteTree = () => {
943
966
  const { routesById, routesByPath, flatRoutes } = processRouteTree({
944
967
  routeTree: this.routeTree,
@@ -985,8 +1008,8 @@ export class RouterCore<
985
1008
  }
986
1009
 
987
1010
  parseLocation: ParseLocationFn<TRouteTree> = (
988
- previousLocation,
989
1011
  locationToParse,
1012
+ previousLocation,
990
1013
  ) => {
991
1014
  const parse = ({
992
1015
  pathname,
@@ -1007,7 +1030,7 @@ export class RouterCore<
1007
1030
  }
1008
1031
  }
1009
1032
 
1010
- const location = parse(locationToParse ?? this.history.location)
1033
+ const location = parse(locationToParse)
1011
1034
 
1012
1035
  const { __tempLocation, __tempKey } = location.state
1013
1036
 
@@ -1139,8 +1162,8 @@ export class RouterCore<
1139
1162
  const parentMatchId = parentMatch?.id
1140
1163
 
1141
1164
  const parentContext = !parentMatchId
1142
- ? ((this.options.context as any) ?? {})
1143
- : (parentMatch.context ?? this.options.context ?? {})
1165
+ ? ((this.options.context as any) ?? undefined)
1166
+ : (parentMatch.context ?? this.options.context ?? undefined)
1144
1167
 
1145
1168
  return parentContext
1146
1169
  }
@@ -1162,12 +1185,12 @@ export class RouterCore<
1162
1185
  ] = (() => {
1163
1186
  // Validate the search params and stabilize them
1164
1187
  const parentSearch = parentMatch?.search ?? next.search
1165
- const parentStrictSearch = parentMatch?._strictSearch ?? {}
1188
+ const parentStrictSearch = parentMatch?._strictSearch ?? undefined
1166
1189
 
1167
1190
  try {
1168
1191
  const strictSearch =
1169
1192
  validateSearch(route.options.validateSearch, { ...parentSearch }) ??
1170
- {}
1193
+ undefined
1171
1194
 
1172
1195
  return [
1173
1196
  {
@@ -1277,7 +1300,10 @@ export class RouterCore<
1277
1300
  isFetching: false,
1278
1301
  error: undefined,
1279
1302
  paramsError: parseErrors[index],
1280
- __routeContext: {},
1303
+ __routeContext: undefined,
1304
+ _nonReactive: {
1305
+ loadPromise: createControlledPromise(),
1306
+ },
1281
1307
  __beforeLoadContext: undefined,
1282
1308
  context: {},
1283
1309
  abortController: new AbortController(),
@@ -1293,7 +1319,6 @@ export class RouterCore<
1293
1319
  headScripts: undefined,
1294
1320
  meta: undefined,
1295
1321
  staticData: route.options.staticData || {},
1296
- loadPromise: createControlledPromise(),
1297
1322
  fullPath: route.fullPath,
1298
1323
  }
1299
1324
  }
@@ -1328,22 +1353,25 @@ export class RouterCore<
1328
1353
  const parentContext = getParentContext(parentMatch)
1329
1354
 
1330
1355
  // Update the match's context
1331
- const contextFnContext: RouteContextOptions<any, any, any, any> = {
1332
- deps: match.loaderDeps,
1333
- params: match.params,
1334
- context: parentContext,
1335
- location: next,
1336
- navigate: (opts: any) =>
1337
- this.navigate({ ...opts, _fromLocation: next }),
1338
- buildLocation: this.buildLocation,
1339
- cause: match.cause,
1340
- abortController: match.abortController,
1341
- preload: !!match.preload,
1342
- matches,
1343
- }
1344
1356
 
1345
- // Get the route context
1346
- match.__routeContext = route.options.context?.(contextFnContext) ?? {}
1357
+ if (route.options.context) {
1358
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
1359
+ deps: match.loaderDeps,
1360
+ params: match.params,
1361
+ context: parentContext ?? {},
1362
+ location: next,
1363
+ navigate: (opts: any) =>
1364
+ this.navigate({ ...opts, _fromLocation: next }),
1365
+ buildLocation: this.buildLocation,
1366
+ cause: match.cause,
1367
+ abortController: match.abortController,
1368
+ preload: !!match.preload,
1369
+ matches,
1370
+ }
1371
+ // Get the route context
1372
+ match.__routeContext =
1373
+ route.options.context(contextFnContext) ?? undefined
1374
+ }
1347
1375
 
1348
1376
  match.context = {
1349
1377
  ...parentContext,
@@ -1381,13 +1409,8 @@ export class RouterCore<
1381
1409
  if (!match) return
1382
1410
 
1383
1411
  match.abortController.abort()
1384
- this.updateMatch(id, (prev) => {
1385
- clearTimeout(prev.pendingTimeout)
1386
- return {
1387
- ...prev,
1388
- pendingTimeout: undefined,
1389
- }
1390
- })
1412
+ clearTimeout(match._nonReactive.pendingTimeout)
1413
+ match._nonReactive.pendingTimeout = undefined
1391
1414
  }
1392
1415
 
1393
1416
  cancelMatches = () => {
@@ -1409,106 +1432,94 @@ export class RouterCore<
1409
1432
  _buildLocation: true,
1410
1433
  })
1411
1434
 
1435
+ // Now let's find the starting pathname
1436
+ // This should default to the current location if no from is provided
1412
1437
  const lastMatch = last(allCurrentLocationMatches)!
1413
1438
 
1414
- // First let's find the starting pathname
1415
- // By default, start with the current location
1416
- let fromPath = lastMatch.fullPath
1417
- const toPath = dest.to
1418
- ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1419
- : this.resolvePathWithBase(fromPath, '.')
1420
-
1421
- const routeIsChanging =
1422
- !!dest.to &&
1423
- !comparePaths(dest.to.toString(), fromPath) &&
1424
- !comparePaths(toPath, fromPath)
1425
-
1426
- // If the route is changing we need to find the relative fromPath
1427
- if (dest.unsafeRelative === 'path') {
1428
- fromPath = currentLocation.pathname
1429
- } else if (routeIsChanging && dest.from) {
1430
- fromPath = dest.from
1431
-
1432
- // do this check only on navigations during test or development
1433
- if (process.env.NODE_ENV !== 'production' && dest._isNavigate) {
1434
- const allFromMatches = this.getMatchedRoutes(
1435
- dest.from,
1436
- undefined,
1437
- ).matchedRoutes
1439
+ // check that from path exists in the current route tree
1440
+ // do this check only on navigations during test or development
1441
+ if (
1442
+ dest.from &&
1443
+ process.env.NODE_ENV !== 'production' &&
1444
+ dest._isNavigate
1445
+ ) {
1446
+ const allFromMatches = this.getMatchedRoutes(
1447
+ dest.from,
1448
+ undefined,
1449
+ ).matchedRoutes
1438
1450
 
1439
- const matchedFrom = [...allCurrentLocationMatches]
1440
- .reverse()
1441
- .find((d) => {
1442
- return comparePaths(d.fullPath, fromPath)
1443
- })
1451
+ const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1452
+ return comparePaths(d.fullPath, dest.from!)
1453
+ })
1444
1454
 
1445
- const matchedCurrent = [...allFromMatches].reverse().find((d) => {
1446
- return comparePaths(d.fullPath, currentLocation.pathname)
1447
- })
1455
+ const matchedCurrent = findLast(allFromMatches, (d) => {
1456
+ return comparePaths(d.fullPath, lastMatch.fullPath)
1457
+ })
1448
1458
 
1449
- // for from to be invalid it shouldn't just be unmatched to currentLocation
1450
- // but the currentLocation should also be unmatched to from
1451
- if (!matchedFrom && !matchedCurrent) {
1452
- console.warn(`Could not find match for from: ${fromPath}`)
1453
- }
1459
+ // for from to be invalid it shouldn't just be unmatched to currentLocation
1460
+ // but the currentLocation should also be unmatched to from
1461
+ if (!matchedFrom && !matchedCurrent) {
1462
+ console.warn(`Could not find match for from: ${dest.from}`)
1454
1463
  }
1455
1464
  }
1456
1465
 
1466
+ const defaultedFromPath =
1467
+ dest.unsafeRelative === 'path'
1468
+ ? currentLocation.pathname
1469
+ : (dest.from ?? lastMatch.fullPath)
1470
+
1471
+ // ensure this includes the basePath if set
1472
+ const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1473
+
1457
1474
  // From search should always use the current location
1458
1475
  const fromSearch = lastMatch.search
1459
1476
  // Same with params. It can't hurt to provide as many as possible
1460
1477
  const fromParams = { ...lastMatch.params }
1461
1478
 
1462
1479
  // Resolve the next to
1480
+ // ensure this includes the basePath if set
1463
1481
  const nextTo = dest.to
1464
1482
  ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1465
1483
  : this.resolvePathWithBase(fromPath, '.')
1466
1484
 
1467
1485
  // Resolve the next params
1468
- let nextParams =
1486
+ const nextParams =
1469
1487
  dest.params === false || dest.params === null
1470
1488
  ? {}
1471
1489
  : (dest.params ?? true) === true
1472
1490
  ? fromParams
1473
- : {
1474
- ...fromParams,
1475
- ...functionalUpdate(dest.params as any, fromParams),
1476
- }
1491
+ : Object.assign(
1492
+ fromParams,
1493
+ functionalUpdate(dest.params as any, fromParams),
1494
+ )
1477
1495
 
1478
1496
  // Interpolate the path first to get the actual resolved path, then match against that
1479
1497
  const interpolatedNextTo = interpolatePath({
1480
1498
  path: nextTo,
1481
- params: nextParams ?? {},
1499
+ params: nextParams,
1482
1500
  parseCache: this.parsePathnameCache,
1483
1501
  }).interpolatedPath
1484
1502
 
1485
- const destRoutes = this.matchRoutes(
1486
- interpolatedNextTo,
1487
- {},
1488
- {
1489
- _buildLocation: true,
1490
- },
1491
- ).map((d) => this.looseRoutesById[d.routeId]!)
1503
+ const destRoutes = this.matchRoutes(interpolatedNextTo, undefined, {
1504
+ _buildLocation: true,
1505
+ }).map((d) => this.looseRoutesById[d.routeId]!)
1492
1506
 
1493
1507
  // If there are any params, we need to stringify them
1494
1508
  if (Object.keys(nextParams).length > 0) {
1495
- destRoutes
1496
- .map((route) => {
1497
- return (
1498
- route.options.params?.stringify ?? route.options.stringifyParams
1499
- )
1500
- })
1501
- .filter(Boolean)
1502
- .forEach((fn) => {
1503
- nextParams = { ...nextParams!, ...fn!(nextParams) }
1504
- })
1509
+ for (const route of destRoutes) {
1510
+ const fn =
1511
+ route.options.params?.stringify ?? route.options.stringifyParams
1512
+ if (fn) {
1513
+ Object.assign(nextParams, fn(nextParams))
1514
+ }
1515
+ }
1505
1516
  }
1506
1517
 
1507
1518
  const nextPathname = interpolatePath({
1508
1519
  // Use the original template path for interpolation
1509
1520
  // This preserves the original parameter syntax including optional parameters
1510
1521
  path: nextTo,
1511
- params: nextParams ?? {},
1522
+ params: nextParams,
1512
1523
  leaveWildcards: false,
1513
1524
  leaveParams: opts.leaveParams,
1514
1525
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1518,20 +1529,20 @@ export class RouterCore<
1518
1529
  // Resolve the next search
1519
1530
  let nextSearch = fromSearch
1520
1531
  if (opts._includeValidateSearch && this.options.search?.strict) {
1521
- let validatedSearch = {}
1532
+ const validatedSearch = {}
1522
1533
  destRoutes.forEach((route) => {
1523
- try {
1524
- if (route.options.validateSearch) {
1525
- validatedSearch = {
1526
- ...validatedSearch,
1527
- ...(validateSearch(route.options.validateSearch, {
1534
+ if (route.options.validateSearch) {
1535
+ try {
1536
+ Object.assign(
1537
+ validatedSearch,
1538
+ validateSearch(route.options.validateSearch, {
1528
1539
  ...validatedSearch,
1529
1540
  ...nextSearch,
1530
- }) ?? {}),
1531
- }
1541
+ }),
1542
+ )
1543
+ } catch {
1544
+ // ignore errors here because they are already handled in matchRoutes
1532
1545
  }
1533
- } catch {
1534
- // ignore errors here because they are already handled in matchRoutes
1535
1546
  }
1536
1547
  })
1537
1548
  nextSearch = validatedSearch
@@ -1618,7 +1629,7 @@ export class RouterCore<
1618
1629
  if (foundMask) {
1619
1630
  const { from: _from, ...maskProps } = foundMask
1620
1631
  maskedDest = {
1621
- ...pick(opts, ['from']),
1632
+ from: opts.from,
1622
1633
  ...maskProps,
1623
1634
  params,
1624
1635
  }
@@ -1636,7 +1647,7 @@ export class RouterCore<
1636
1647
 
1637
1648
  if (opts.mask) {
1638
1649
  return buildWithMatches(opts, {
1639
- ...pick(opts, ['from']),
1650
+ from: opts.from,
1640
1651
  ...opts.mask,
1641
1652
  })
1642
1653
  }
@@ -1805,7 +1816,7 @@ export class RouterCore<
1805
1816
  beforeLoad = () => {
1806
1817
  // Cancel any pending matches
1807
1818
  this.cancelMatches()
1808
- this.latestLocation = this.parseLocation(this.latestLocation)
1819
+ this.updateLatestLocation()
1809
1820
 
1810
1821
  if (this.isServer) {
1811
1822
  // for SPAs on the initial load, this is handled by the Transitioner
@@ -1884,10 +1895,12 @@ export class RouterCore<
1884
1895
  }),
1885
1896
  })
1886
1897
 
1887
- await this.loadMatches({
1898
+ await loadMatches({
1899
+ router: this,
1888
1900
  sync: opts?.sync,
1889
1901
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1890
1902
  location: next,
1903
+ updateMatch: this.updateMatch,
1891
1904
  // eslint-disable-next-line @typescript-eslint/require-await
1892
1905
  onReady: async () => {
1893
1906
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -2077,704 +2090,6 @@ export class RouterCore<
2077
2090
  )
2078
2091
  }
2079
2092
 
2080
- loadMatches = async ({
2081
- location,
2082
- matches,
2083
- preload: allPreload,
2084
- onReady,
2085
- updateMatch = this.updateMatch,
2086
- sync,
2087
- }: {
2088
- location: ParsedLocation
2089
- matches: Array<AnyRouteMatch>
2090
- preload?: boolean
2091
- onReady?: () => Promise<void>
2092
- updateMatch?: (
2093
- id: string,
2094
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
2095
- ) => void
2096
- getMatch?: (matchId: string) => AnyRouteMatch | undefined
2097
- sync?: boolean
2098
- }): Promise<Array<MakeRouteMatch>> => {
2099
- let firstBadMatchIndex: number | undefined
2100
- let rendered = false
2101
-
2102
- const triggerOnReady = async () => {
2103
- if (!rendered) {
2104
- rendered = true
2105
- await onReady?.()
2106
- }
2107
- }
2108
-
2109
- const resolvePreload = (matchId: string) => {
2110
- return !!(allPreload && !this.state.matches.some((d) => d.id === matchId))
2111
- }
2112
-
2113
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2114
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2115
- if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2116
- triggerOnReady()
2117
- }
2118
-
2119
- const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2120
- if (isRedirect(err) || isNotFound(err)) {
2121
- if (isRedirect(err)) {
2122
- if (err.redirectHandled) {
2123
- if (!err.options.reloadDocument) {
2124
- throw err
2125
- }
2126
- }
2127
- }
2128
-
2129
- match.beforeLoadPromise?.resolve()
2130
- match.loaderPromise?.resolve()
2131
-
2132
- updateMatch(match.id, (prev) => ({
2133
- ...prev,
2134
- status: isRedirect(err)
2135
- ? 'redirected'
2136
- : isNotFound(err)
2137
- ? 'notFound'
2138
- : 'error',
2139
- isFetching: false,
2140
- error: err,
2141
- beforeLoadPromise: undefined,
2142
- loaderPromise: undefined,
2143
- }))
2144
-
2145
- if (!(err as any).routeId) {
2146
- ;(err as any).routeId = match.routeId
2147
- }
2148
-
2149
- match.loadPromise?.resolve()
2150
-
2151
- if (isRedirect(err)) {
2152
- rendered = true
2153
- err.options._fromLocation = location
2154
- err.redirectHandled = true
2155
- err = this.resolveRedirect(err)
2156
- throw err
2157
- } else if (isNotFound(err)) {
2158
- this._handleNotFound(matches, err, {
2159
- updateMatch,
2160
- })
2161
- throw err
2162
- }
2163
- }
2164
- }
2165
-
2166
- const shouldSkipLoader = (matchId: string) => {
2167
- const match = this.getMatch(matchId)!
2168
- // upon hydration, we skip the loader if the match has been dehydrated on the server
2169
- if (!this.isServer && match._dehydrated) {
2170
- return true
2171
- }
2172
-
2173
- if (this.isServer) {
2174
- if (match.ssr === false) {
2175
- return true
2176
- }
2177
- }
2178
- return false
2179
- }
2180
-
2181
- try {
2182
- await new Promise<void>((resolveAll, rejectAll) => {
2183
- ;(async () => {
2184
- try {
2185
- const handleSerialError = (
2186
- index: number,
2187
- err: any,
2188
- routerCode: string,
2189
- ) => {
2190
- const { id: matchId, routeId } = matches[index]!
2191
- const route = this.looseRoutesById[routeId]!
2192
-
2193
- // Much like suspense, we use a promise here to know if
2194
- // we've been outdated by a new loadMatches call and
2195
- // should abort the current async operation
2196
- if (err instanceof Promise) {
2197
- throw err
2198
- }
2199
-
2200
- err.routerCode = routerCode
2201
- firstBadMatchIndex = firstBadMatchIndex ?? index
2202
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2203
-
2204
- try {
2205
- route.options.onError?.(err)
2206
- } catch (errorHandlerErr) {
2207
- err = errorHandlerErr
2208
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2209
- }
2210
-
2211
- updateMatch(matchId, (prev) => {
2212
- prev.beforeLoadPromise?.resolve()
2213
- prev.loadPromise?.resolve()
2214
-
2215
- return {
2216
- ...prev,
2217
- error: err,
2218
- status: 'error',
2219
- isFetching: false,
2220
- updatedAt: Date.now(),
2221
- abortController: new AbortController(),
2222
- beforeLoadPromise: undefined,
2223
- }
2224
- })
2225
- }
2226
-
2227
- for (const [index, { id: matchId, routeId }] of matches.entries()) {
2228
- const existingMatch = this.getMatch(matchId)!
2229
- const parentMatchId = matches[index - 1]?.id
2230
- const parentMatch = parentMatchId
2231
- ? this.getMatch(parentMatchId)!
2232
- : undefined
2233
-
2234
- const route = this.looseRoutesById[routeId]!
2235
-
2236
- const pendingMs =
2237
- route.options.pendingMs ?? this.options.defaultPendingMs
2238
-
2239
- // on the server, determine whether SSR the current match or not
2240
- if (this.isServer) {
2241
- let ssr: boolean | 'data-only'
2242
- // in SPA mode, only SSR the root route
2243
- if (this.isShell()) {
2244
- ssr = matchId === rootRouteId
2245
- } else {
2246
- const defaultSsr = this.options.defaultSsr ?? true
2247
- if (parentMatch?.ssr === false) {
2248
- ssr = false
2249
- } else {
2250
- let tempSsr: boolean | 'data-only'
2251
- if (route.options.ssr === undefined) {
2252
- tempSsr = defaultSsr
2253
- } else if (typeof route.options.ssr === 'function') {
2254
- const { search, params } = this.getMatch(matchId)!
2255
-
2256
- function makeMaybe(value: any, error: any) {
2257
- if (error) {
2258
- return { status: 'error' as const, error }
2259
- }
2260
- return { status: 'success' as const, value }
2261
- }
2262
-
2263
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2264
- search: makeMaybe(search, existingMatch.searchError),
2265
- params: makeMaybe(params, existingMatch.paramsError),
2266
- location,
2267
- matches: matches.map((match) => ({
2268
- index: match.index,
2269
- pathname: match.pathname,
2270
- fullPath: match.fullPath,
2271
- staticData: match.staticData,
2272
- id: match.id,
2273
- routeId: match.routeId,
2274
- search: makeMaybe(match.search, match.searchError),
2275
- params: makeMaybe(match.params, match.paramsError),
2276
- ssr: match.ssr,
2277
- })),
2278
- }
2279
- tempSsr =
2280
- (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2281
- } else {
2282
- tempSsr = route.options.ssr
2283
- }
2284
-
2285
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2286
- ssr = 'data-only'
2287
- } else {
2288
- ssr = tempSsr
2289
- }
2290
- }
2291
- }
2292
- updateMatch(matchId, (prev) => ({
2293
- ...prev,
2294
- ssr,
2295
- }))
2296
- }
2297
-
2298
- if (shouldSkipLoader(matchId)) {
2299
- continue
2300
- }
2301
-
2302
- const shouldPending = !!(
2303
- onReady &&
2304
- !this.isServer &&
2305
- !resolvePreload(matchId) &&
2306
- (route.options.loader ||
2307
- route.options.beforeLoad ||
2308
- routeNeedsPreload(route)) &&
2309
- typeof pendingMs === 'number' &&
2310
- pendingMs !== Infinity &&
2311
- (route.options.pendingComponent ??
2312
- (this.options as any)?.defaultPendingComponent)
2313
- )
2314
-
2315
- let executeBeforeLoad = true
2316
- const setupPendingTimeout = () => {
2317
- if (
2318
- shouldPending &&
2319
- this.getMatch(matchId)!.pendingTimeout === undefined
2320
- ) {
2321
- const pendingTimeout = setTimeout(() => {
2322
- try {
2323
- // Update the match and prematurely resolve the loadMatches promise so that
2324
- // the pending component can start rendering
2325
- triggerOnReady()
2326
- } catch {}
2327
- }, pendingMs)
2328
- updateMatch(matchId, (prev) => ({
2329
- ...prev,
2330
- pendingTimeout,
2331
- }))
2332
- }
2333
- }
2334
- if (
2335
- // If we are in the middle of a load, either of these will be present
2336
- // (not to be confused with `loadPromise`, which is always defined)
2337
- existingMatch.beforeLoadPromise ||
2338
- existingMatch.loaderPromise
2339
- ) {
2340
- setupPendingTimeout()
2341
-
2342
- // Wait for the beforeLoad to resolve before we continue
2343
- await existingMatch.beforeLoadPromise
2344
- const match = this.getMatch(matchId)!
2345
- if (match.status === 'error') {
2346
- executeBeforeLoad = true
2347
- } else if (
2348
- match.preload &&
2349
- (match.status === 'redirected' || match.status === 'notFound')
2350
- ) {
2351
- handleRedirectAndNotFound(match, match.error)
2352
- }
2353
- }
2354
- if (executeBeforeLoad) {
2355
- // If we are not in the middle of a load OR the previous load failed, start it
2356
- try {
2357
- updateMatch(matchId, (prev) => {
2358
- // explicitly capture the previous loadPromise
2359
- const prevLoadPromise = prev.loadPromise
2360
- return {
2361
- ...prev,
2362
- loadPromise: createControlledPromise<void>(() => {
2363
- prevLoadPromise?.resolve()
2364
- }),
2365
- beforeLoadPromise: createControlledPromise<void>(),
2366
- }
2367
- })
2368
-
2369
- const { paramsError, searchError } = this.getMatch(matchId)!
2370
-
2371
- if (paramsError) {
2372
- handleSerialError(index, paramsError, 'PARSE_PARAMS')
2373
- }
2374
-
2375
- if (searchError) {
2376
- handleSerialError(index, searchError, 'VALIDATE_SEARCH')
2377
- }
2378
-
2379
- setupPendingTimeout()
2380
-
2381
- const abortController = new AbortController()
2382
-
2383
- const parentMatchContext =
2384
- parentMatch?.context ?? this.options.context ?? {}
2385
-
2386
- updateMatch(matchId, (prev) => ({
2387
- ...prev,
2388
- isFetching: 'beforeLoad',
2389
- fetchCount: prev.fetchCount + 1,
2390
- abortController,
2391
- context: {
2392
- ...parentMatchContext,
2393
- ...prev.__routeContext,
2394
- },
2395
- }))
2396
-
2397
- const { search, params, context, cause } =
2398
- this.getMatch(matchId)!
2399
-
2400
- const preload = resolvePreload(matchId)
2401
-
2402
- const beforeLoadFnContext: BeforeLoadContextOptions<
2403
- any,
2404
- any,
2405
- any,
2406
- any,
2407
- any
2408
- > = {
2409
- search,
2410
- abortController,
2411
- params,
2412
- preload,
2413
- context,
2414
- location,
2415
- navigate: (opts: any) =>
2416
- this.navigate({ ...opts, _fromLocation: location }),
2417
- buildLocation: this.buildLocation,
2418
- cause: preload ? 'preload' : cause,
2419
- matches,
2420
- }
2421
-
2422
- const beforeLoadContext =
2423
- await route.options.beforeLoad?.(beforeLoadFnContext)
2424
-
2425
- if (
2426
- isRedirect(beforeLoadContext) ||
2427
- isNotFound(beforeLoadContext)
2428
- ) {
2429
- handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
2430
- }
2431
-
2432
- updateMatch(matchId, (prev) => {
2433
- return {
2434
- ...prev,
2435
- __beforeLoadContext: beforeLoadContext,
2436
- context: {
2437
- ...parentMatchContext,
2438
- ...prev.__routeContext,
2439
- ...beforeLoadContext,
2440
- },
2441
- abortController,
2442
- }
2443
- })
2444
- } catch (err) {
2445
- handleSerialError(index, err, 'BEFORE_LOAD')
2446
- }
2447
-
2448
- updateMatch(matchId, (prev) => {
2449
- prev.beforeLoadPromise?.resolve()
2450
-
2451
- return {
2452
- ...prev,
2453
- beforeLoadPromise: undefined,
2454
- isFetching: false,
2455
- }
2456
- })
2457
- }
2458
- }
2459
-
2460
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
2461
- const matchPromises: Array<Promise<AnyRouteMatch>> = []
2462
-
2463
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2464
- matchPromises.push(
2465
- (async () => {
2466
- let loaderShouldRunAsync = false
2467
- let loaderIsRunningAsync = false
2468
- const route = this.looseRoutesById[routeId]!
2469
-
2470
- const executeHead = async () => {
2471
- const match = this.getMatch(matchId)
2472
- // in case of a redirecting match during preload, the match does not exist
2473
- if (!match) {
2474
- return
2475
- }
2476
- const assetContext = {
2477
- matches,
2478
- match,
2479
- params: match.params,
2480
- loaderData: match.loaderData,
2481
- }
2482
- const headFnContent =
2483
- await route.options.head?.(assetContext)
2484
- const meta = headFnContent?.meta
2485
- const links = headFnContent?.links
2486
- const headScripts = headFnContent?.scripts
2487
- const styles = headFnContent?.styles
2488
-
2489
- const scripts = await route.options.scripts?.(assetContext)
2490
- const headers = await route.options.headers?.(assetContext)
2491
- return {
2492
- meta,
2493
- links,
2494
- headScripts,
2495
- headers,
2496
- scripts,
2497
- styles,
2498
- }
2499
- }
2500
-
2501
- const potentialPendingMinPromise = async () => {
2502
- const latestMatch = this.getMatch(matchId)!
2503
- if (latestMatch.minPendingPromise) {
2504
- await latestMatch.minPendingPromise
2505
- }
2506
- }
2507
-
2508
- const prevMatch = this.getMatch(matchId)!
2509
- if (shouldSkipLoader(matchId)) {
2510
- if (this.isServer) {
2511
- const head = await executeHead()
2512
- updateMatch(matchId, (prev) => ({
2513
- ...prev,
2514
- ...head,
2515
- }))
2516
- return this.getMatch(matchId)!
2517
- }
2518
- }
2519
- // there is a loaderPromise, so we are in the middle of a load
2520
- else if (prevMatch.loaderPromise) {
2521
- // do not block if we already have stale data we can show
2522
- // but only if the ongoing load is not a preload since error handling is different for preloads
2523
- // and we don't want to swallow errors
2524
- if (
2525
- prevMatch.status === 'success' &&
2526
- !sync &&
2527
- !prevMatch.preload
2528
- ) {
2529
- return this.getMatch(matchId)!
2530
- }
2531
- await prevMatch.loaderPromise
2532
- const match = this.getMatch(matchId)!
2533
- if (match.error) {
2534
- handleRedirectAndNotFound(match, match.error)
2535
- }
2536
- } else {
2537
- const parentMatchPromise = matchPromises[index - 1] as any
2538
-
2539
- const getLoaderContext = (): LoaderFnContext => {
2540
- const {
2541
- params,
2542
- loaderDeps,
2543
- abortController,
2544
- context,
2545
- cause,
2546
- } = this.getMatch(matchId)!
2547
-
2548
- const preload = resolvePreload(matchId)
2549
-
2550
- return {
2551
- params,
2552
- deps: loaderDeps,
2553
- preload: !!preload,
2554
- parentMatchPromise,
2555
- abortController: abortController,
2556
- context,
2557
- location,
2558
- navigate: (opts) =>
2559
- this.navigate({ ...opts, _fromLocation: location }),
2560
- cause: preload ? 'preload' : cause,
2561
- route,
2562
- }
2563
- }
2564
-
2565
- // This is where all of the stale-while-revalidate magic happens
2566
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2567
-
2568
- const preload = resolvePreload(matchId)
2569
-
2570
- const staleAge = preload
2571
- ? (route.options.preloadStaleTime ??
2572
- this.options.defaultPreloadStaleTime ??
2573
- 30_000) // 30 seconds for preloads by default
2574
- : (route.options.staleTime ??
2575
- this.options.defaultStaleTime ??
2576
- 0)
2577
-
2578
- const shouldReloadOption = route.options.shouldReload
2579
-
2580
- // Default to reloading the route all the time
2581
- // Allow shouldReload to get the last say,
2582
- // if provided.
2583
- const shouldReload =
2584
- typeof shouldReloadOption === 'function'
2585
- ? shouldReloadOption(getLoaderContext())
2586
- : shouldReloadOption
2587
-
2588
- updateMatch(matchId, (prev) => ({
2589
- ...prev,
2590
- loaderPromise: createControlledPromise<void>(),
2591
- preload:
2592
- !!preload &&
2593
- !this.state.matches.some((d) => d.id === matchId),
2594
- }))
2595
-
2596
- const runLoader = async () => {
2597
- try {
2598
- // If the Matches component rendered
2599
- // the pending component and needs to show it for
2600
- // a minimum duration, we''ll wait for it to resolve
2601
- // before committing to the match and resolving
2602
- // the loadPromise
2603
-
2604
- // Actually run the loader and handle the result
2605
- try {
2606
- if (
2607
- !this.isServer ||
2608
- (this.isServer &&
2609
- this.getMatch(matchId)!.ssr === true)
2610
- ) {
2611
- this.loadRouteChunk(route)
2612
- }
2613
-
2614
- updateMatch(matchId, (prev) => ({
2615
- ...prev,
2616
- isFetching: 'loader',
2617
- }))
2618
-
2619
- // Kick off the loader!
2620
- const loaderData =
2621
- await route.options.loader?.(getLoaderContext())
2622
-
2623
- handleRedirectAndNotFound(
2624
- this.getMatch(matchId)!,
2625
- loaderData,
2626
- )
2627
- updateMatch(matchId, (prev) => ({
2628
- ...prev,
2629
- loaderData,
2630
- }))
2631
-
2632
- // Lazy option can modify the route options,
2633
- // so we need to wait for it to resolve before
2634
- // we can use the options
2635
- await route._lazyPromise
2636
- const head = await executeHead()
2637
- await potentialPendingMinPromise()
2638
-
2639
- // Last but not least, wait for the the components
2640
- // to be preloaded before we resolve the match
2641
- await route._componentsPromise
2642
- updateMatch(matchId, (prev) => ({
2643
- ...prev,
2644
- error: undefined,
2645
- status: 'success',
2646
- isFetching: false,
2647
- updatedAt: Date.now(),
2648
- ...head,
2649
- }))
2650
- } catch (e) {
2651
- let error = e
2652
-
2653
- await potentialPendingMinPromise()
2654
-
2655
- handleRedirectAndNotFound(this.getMatch(matchId)!, e)
2656
-
2657
- try {
2658
- route.options.onError?.(e)
2659
- } catch (onErrorError) {
2660
- error = onErrorError
2661
- handleRedirectAndNotFound(
2662
- this.getMatch(matchId)!,
2663
- onErrorError,
2664
- )
2665
- }
2666
- const head = await executeHead()
2667
- updateMatch(matchId, (prev) => ({
2668
- ...prev,
2669
- error,
2670
- status: 'error',
2671
- isFetching: false,
2672
- ...head,
2673
- }))
2674
- }
2675
- } catch (err) {
2676
- const head = await executeHead()
2677
-
2678
- updateMatch(matchId, (prev) => ({
2679
- ...prev,
2680
- loaderPromise: undefined,
2681
- ...head,
2682
- }))
2683
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2684
- }
2685
- }
2686
-
2687
- // If the route is successful and still fresh, just resolve
2688
- const { status, invalid } = this.getMatch(matchId)!
2689
- loaderShouldRunAsync =
2690
- status === 'success' &&
2691
- (invalid || (shouldReload ?? age > staleAge))
2692
- if (preload && route.options.preload === false) {
2693
- // Do nothing
2694
- } else if (loaderShouldRunAsync && !sync) {
2695
- loaderIsRunningAsync = true
2696
- ;(async () => {
2697
- try {
2698
- await runLoader()
2699
- const { loaderPromise, loadPromise } =
2700
- this.getMatch(matchId)!
2701
- loaderPromise?.resolve()
2702
- loadPromise?.resolve()
2703
- updateMatch(matchId, (prev) => ({
2704
- ...prev,
2705
- loaderPromise: undefined,
2706
- }))
2707
- } catch (err) {
2708
- if (isRedirect(err)) {
2709
- await this.navigate(err.options)
2710
- }
2711
- }
2712
- })()
2713
- } else if (
2714
- status !== 'success' ||
2715
- (loaderShouldRunAsync && sync)
2716
- ) {
2717
- await runLoader()
2718
- } else {
2719
- // if the loader did not run, still update head.
2720
- // reason: parent's beforeLoad may have changed the route context
2721
- // and only now do we know the route context (and that the loader would not run)
2722
- const head = await executeHead()
2723
- updateMatch(matchId, (prev) => ({
2724
- ...prev,
2725
- ...head,
2726
- }))
2727
- }
2728
- }
2729
- if (!loaderIsRunningAsync) {
2730
- const { loaderPromise, loadPromise } =
2731
- this.getMatch(matchId)!
2732
- loaderPromise?.resolve()
2733
- loadPromise?.resolve()
2734
- }
2735
-
2736
- updateMatch(matchId, (prev) => {
2737
- clearTimeout(prev.pendingTimeout)
2738
- return {
2739
- ...prev,
2740
- isFetching: loaderIsRunningAsync
2741
- ? prev.isFetching
2742
- : false,
2743
- loaderPromise: loaderIsRunningAsync
2744
- ? prev.loaderPromise
2745
- : undefined,
2746
- invalid: false,
2747
- pendingTimeout: undefined,
2748
- _dehydrated: undefined,
2749
- }
2750
- })
2751
- return this.getMatch(matchId)!
2752
- })(),
2753
- )
2754
- })
2755
-
2756
- await Promise.all(matchPromises)
2757
-
2758
- resolveAll()
2759
- } catch (err) {
2760
- rejectAll(err)
2761
- }
2762
- })()
2763
- })
2764
- await triggerOnReady()
2765
- } catch (err) {
2766
- if (isRedirect(err) || isNotFound(err)) {
2767
- if (isNotFound(err) && !allPreload) {
2768
- await triggerOnReady()
2769
- }
2770
-
2771
- throw err
2772
- }
2773
- }
2774
-
2775
- return matches
2776
- }
2777
-
2778
2093
  invalidate: InvalidateFn<
2779
2094
  RouterCore<
2780
2095
  TRouteTree,
@@ -2791,7 +2106,7 @@ export class RouterCore<
2791
2106
  invalid: true,
2792
2107
  ...(opts?.forcePending || d.status === 'error'
2793
2108
  ? ({ status: 'pending', error: undefined } as const)
2794
- : {}),
2109
+ : undefined),
2795
2110
  }
2796
2111
  }
2797
2112
  return d
@@ -2868,36 +2183,7 @@ export class RouterCore<
2868
2183
  this.clearCache({ filter })
2869
2184
  }
2870
2185
 
2871
- loadRouteChunk = (route: AnyRoute) => {
2872
- if (route._lazyPromise === undefined) {
2873
- if (route.lazyFn) {
2874
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
2875
- // explicitly don't copy over the lazy route's id
2876
- const { id: _id, ...options } = lazyRoute.options
2877
- Object.assign(route.options, options)
2878
- })
2879
- } else {
2880
- route._lazyPromise = Promise.resolve()
2881
- }
2882
- }
2883
-
2884
- // If for some reason lazy resolves more lazy components...
2885
- // We'll wait for that before pre attempt to preload any
2886
- // components themselves.
2887
- if (route._componentsPromise === undefined) {
2888
- route._componentsPromise = route._lazyPromise.then(() =>
2889
- Promise.all(
2890
- componentTypes.map(async (type) => {
2891
- const component = route.options[type]
2892
- if ((component as any)?.preload) {
2893
- await (component as any).preload()
2894
- }
2895
- }),
2896
- ),
2897
- )
2898
- }
2899
- return route._componentsPromise
2900
- }
2186
+ loadRouteChunk = loadRouteChunk
2901
2187
 
2902
2188
  preloadRoute: PreloadRouteFn<
2903
2189
  TRouteTree,
@@ -2937,7 +2223,8 @@ export class RouterCore<
2937
2223
  })
2938
2224
 
2939
2225
  try {
2940
- matches = await this.loadMatches({
2226
+ matches = await loadMatches({
2227
+ router: this,
2941
2228
  matches,
2942
2229
  location: next,
2943
2230
  preload: true,
@@ -3035,68 +2322,6 @@ export class RouterCore<
3035
2322
 
3036
2323
  serverSsr?: ServerSsr
3037
2324
 
3038
- _handleNotFound = (
3039
- matches: Array<AnyRouteMatch>,
3040
- err: NotFoundError,
3041
- {
3042
- updateMatch = this.updateMatch,
3043
- }: {
3044
- updateMatch?: (
3045
- id: string,
3046
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
3047
- ) => void
3048
- } = {},
3049
- ) => {
3050
- // Find the route that should handle the not found error
3051
- // First check if a specific route is requested to show the error
3052
- const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
3053
- const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3054
-
3055
- // Setup routesByRouteId object for quick access
3056
- for (const match of matches) {
3057
- matchesByRouteId[match.routeId] = match
3058
- }
3059
-
3060
- // Ensure a NotFoundComponent exists on the route
3061
- if (
3062
- !routeCursor.options.notFoundComponent &&
3063
- (this.options as any)?.defaultNotFoundComponent
3064
- ) {
3065
- routeCursor.options.notFoundComponent = (
3066
- this.options as any
3067
- ).defaultNotFoundComponent
3068
- }
3069
-
3070
- // Ensure we have a notFoundComponent
3071
- invariant(
3072
- routeCursor.options.notFoundComponent,
3073
- 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
3074
- )
3075
-
3076
- // Find the match for this route
3077
- const matchForRoute = matchesByRouteId[routeCursor.id]
3078
-
3079
- invariant(
3080
- matchForRoute,
3081
- 'Could not find match for route: ' + routeCursor.id,
3082
- )
3083
-
3084
- // Assign the error to the match - using non-null assertion since we've checked with invariant
3085
- updateMatch(matchForRoute.id, (prev) => ({
3086
- ...prev,
3087
- status: 'notFound',
3088
- error: err,
3089
- isFetching: false,
3090
- }))
3091
-
3092
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3093
- err.routeId = routeCursor.parentRoute.id
3094
- this._handleNotFound(matches, err, {
3095
- updateMatch,
3096
- })
3097
- }
3098
- }
3099
-
3100
2325
  hasNotFoundMatch = () => {
3101
2326
  return this.__store.state.matches.some(
3102
2327
  (d) => d.status === 'notFound' || d.globalNotFound,
@@ -3174,22 +2399,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3174
2399
  return {}
3175
2400
  }
3176
2401
 
3177
- export const componentTypes = [
3178
- 'component',
3179
- 'errorComponent',
3180
- 'pendingComponent',
3181
- 'notFoundComponent',
3182
- ] as const
3183
-
3184
- function routeNeedsPreload(route: AnyRoute) {
3185
- for (const componentType of componentTypes) {
3186
- if ((route.options[componentType] as any)?.preload) {
3187
- return true
3188
- }
3189
- }
3190
- return false
3191
- }
3192
-
3193
2402
  interface RouteLike {
3194
2403
  id: string
3195
2404
  isRoot?: boolean
@@ -3562,7 +2771,8 @@ function applySearchMiddleware({
3562
2771
  try {
3563
2772
  const validatedSearch = {
3564
2773
  ...result,
3565
- ...(validateSearch(route.options.validateSearch, result) ?? {}),
2774
+ ...(validateSearch(route.options.validateSearch, result) ??
2775
+ undefined),
3566
2776
  }
3567
2777
  return validatedSearch
3568
2778
  } catch {