@tanstack/router-core 1.171.15 → 1.171.16-pre.0

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 (134) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +3 -12
  3. package/dist/cjs/await-signal.cjs +21 -0
  4. package/dist/cjs/await-signal.cjs.map +1 -0
  5. package/dist/cjs/await-signal.d.cts +1 -0
  6. package/dist/cjs/index.cjs +3 -1
  7. package/dist/cjs/index.d.cts +3 -2
  8. package/dist/cjs/isServer/client.cjs +2 -0
  9. package/dist/cjs/isServer/client.cjs.map +1 -1
  10. package/dist/cjs/isServer/client.d.cts +1 -0
  11. package/dist/cjs/isServer/development.cjs +2 -0
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.d.cts +1 -0
  14. package/dist/cjs/isServer/server.cjs +2 -0
  15. package/dist/cjs/isServer/server.cjs.map +1 -1
  16. package/dist/cjs/isServer/server.d.cts +1 -0
  17. package/dist/cjs/load-client.cjs +1470 -0
  18. package/dist/cjs/load-client.cjs.map +1 -0
  19. package/dist/cjs/load-client.d.cts +147 -0
  20. package/dist/cjs/load-server.cjs +565 -0
  21. package/dist/cjs/load-server.cjs.map +1 -0
  22. package/dist/cjs/load-server.d.cts +16 -0
  23. package/dist/cjs/redirect.cjs.map +1 -1
  24. package/dist/cjs/redirect.d.cts +0 -1
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/router.cjs +187 -442
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +61 -51
  29. package/dist/cjs/ssr/client.cjs +2 -2
  30. package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
  31. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  32. package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
  33. package/dist/cjs/ssr/handlerCallback.cjs +49 -4
  34. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  35. package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
  36. package/dist/cjs/ssr/server.cjs +4 -0
  37. package/dist/cjs/ssr/server.d.cts +2 -2
  38. package/dist/cjs/ssr/ssr-client.d.cts +1 -10
  39. package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
  40. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  41. package/dist/cjs/ssr/ssr-server.cjs +6 -6
  42. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  43. package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
  44. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  45. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
  46. package/dist/cjs/ssr/types.d.cts +0 -1
  47. package/dist/cjs/stores.cjs +30 -104
  48. package/dist/cjs/stores.cjs.map +1 -1
  49. package/dist/cjs/stores.d.cts +7 -32
  50. package/dist/cjs/utils.cjs +0 -4
  51. package/dist/cjs/utils.cjs.map +1 -1
  52. package/dist/esm/Matches.d.ts +3 -12
  53. package/dist/esm/Matches.js.map +1 -1
  54. package/dist/esm/await-signal.d.ts +1 -0
  55. package/dist/esm/await-signal.js +21 -0
  56. package/dist/esm/await-signal.js.map +1 -0
  57. package/dist/esm/index.d.ts +3 -2
  58. package/dist/esm/index.js +3 -2
  59. package/dist/esm/isServer/client.d.ts +1 -0
  60. package/dist/esm/isServer/client.js +2 -1
  61. package/dist/esm/isServer/client.js.map +1 -1
  62. package/dist/esm/isServer/development.d.ts +1 -0
  63. package/dist/esm/isServer/development.js +2 -1
  64. package/dist/esm/isServer/development.js.map +1 -1
  65. package/dist/esm/isServer/server.d.ts +1 -0
  66. package/dist/esm/isServer/server.js +2 -1
  67. package/dist/esm/isServer/server.js.map +1 -1
  68. package/dist/esm/load-client.d.ts +147 -0
  69. package/dist/esm/load-client.js +1462 -0
  70. package/dist/esm/load-client.js.map +1 -0
  71. package/dist/esm/load-server.d.ts +16 -0
  72. package/dist/esm/load-server.js +565 -0
  73. package/dist/esm/load-server.js.map +1 -0
  74. package/dist/esm/redirect.d.ts +0 -1
  75. package/dist/esm/redirect.js.map +1 -1
  76. package/dist/esm/route.js.map +1 -1
  77. package/dist/esm/router.d.ts +61 -51
  78. package/dist/esm/router.js +188 -444
  79. package/dist/esm/router.js.map +1 -1
  80. package/dist/esm/ssr/client.js +1 -1
  81. package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
  82. package/dist/esm/ssr/createRequestHandler.js +61 -11
  83. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  84. package/dist/esm/ssr/handlerCallback.d.ts +3 -0
  85. package/dist/esm/ssr/handlerCallback.js +47 -5
  86. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  87. package/dist/esm/ssr/server.d.ts +2 -2
  88. package/dist/esm/ssr/server.js +3 -3
  89. package/dist/esm/ssr/ssr-client.d.ts +1 -10
  90. package/dist/esm/ssr/ssr-match-id.js +2 -2
  91. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  92. package/dist/esm/ssr/ssr-server.js +6 -6
  93. package/dist/esm/ssr/ssr-server.js.map +1 -1
  94. package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
  95. package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
  96. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  97. package/dist/esm/ssr/types.d.ts +0 -1
  98. package/dist/esm/stores.d.ts +7 -32
  99. package/dist/esm/stores.js +30 -104
  100. package/dist/esm/stores.js.map +1 -1
  101. package/dist/esm/utils.js +1 -4
  102. package/dist/esm/utils.js.map +1 -1
  103. package/package.json +3 -3
  104. package/src/Matches.ts +6 -21
  105. package/src/await-signal.ts +27 -0
  106. package/src/index.ts +1 -3
  107. package/src/isServer/client.ts +1 -0
  108. package/src/isServer/development.ts +1 -0
  109. package/src/isServer/server.ts +1 -0
  110. package/src/load-client.ts +2701 -0
  111. package/src/load-server.ts +849 -0
  112. package/src/redirect.ts +0 -1
  113. package/src/route.ts +2 -11
  114. package/src/router.ts +379 -769
  115. package/src/ssr/createRequestHandler.ts +109 -16
  116. package/src/ssr/handlerCallback.ts +76 -10
  117. package/src/ssr/server.ts +4 -1
  118. package/src/ssr/ssr-client.ts +1 -310
  119. package/src/ssr/ssr-match-id.ts +11 -2
  120. package/src/ssr/ssr-server.ts +9 -8
  121. package/src/ssr/transformStreamWithRouter.ts +37 -1
  122. package/src/ssr/types.ts +0 -1
  123. package/src/stores.ts +46 -208
  124. package/dist/cjs/load-matches.cjs +0 -658
  125. package/dist/cjs/load-matches.cjs.map +0 -1
  126. package/dist/cjs/load-matches.d.cts +0 -18
  127. package/dist/cjs/ssr/ssr-client.cjs +0 -183
  128. package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
  129. package/dist/esm/load-matches.d.ts +0 -18
  130. package/dist/esm/load-matches.js +0 -656
  131. package/dist/esm/load-matches.js.map +0 -1
  132. package/dist/esm/ssr/ssr-client.js +0 -183
  133. package/dist/esm/ssr/ssr-client.js.map +0 -1
  134. package/src/load-matches.ts +0 -1278
@@ -1,1278 +0,0 @@
1
- import { isServer } from '@tanstack/router-core/isServer'
2
- import { invariant } from './invariant'
3
- import { createControlledPromise, isPromise } from './utils'
4
- import { isNotFound } from './not-found'
5
- import { rootRouteId } from './root'
6
- import { isRedirect } from './redirect'
7
- import type { NotFoundError } from './not-found'
8
- import type { ParsedLocation } from './location'
9
- import type {
10
- AnyRoute,
11
- BeforeLoadContextOptions,
12
- LoaderFnContext,
13
- SsrContextOptions,
14
- } from './route'
15
- import type { AnyRouteMatch, MakeRouteMatch } from './Matches'
16
- import type { AnyRouter, SSROption, UpdateMatchFn } from './router'
17
-
18
- /**
19
- * An object of this shape is created when calling `loadMatches`.
20
- * It contains everything we need for all other functions in this file
21
- * to work. (It's basically the function's argument, plus a few mutable states)
22
- */
23
- type InnerLoadContext = {
24
- /** the calling router instance */
25
- router: AnyRouter
26
- location: ParsedLocation
27
- /** mutable state, scoped to a `loadMatches` call */
28
- firstBadMatchIndex?: number
29
- /** mutable state, scoped to a `loadMatches` call */
30
- rendered?: boolean
31
- serialError?: unknown
32
- updateMatch: UpdateMatchFn
33
- matches: Array<AnyRouteMatch>
34
- preload?: boolean
35
- forceStaleReload?: boolean
36
- onReady?: () => Promise<void>
37
- sync?: boolean
38
- }
39
-
40
- const triggerOnReady = (inner: InnerLoadContext): void | Promise<void> => {
41
- if (!inner.rendered) {
42
- inner.rendered = true
43
- return inner.onReady?.()
44
- }
45
- }
46
-
47
- const hasForcePendingActiveMatch = (router: AnyRouter): boolean => {
48
- return router.stores.matchesId.get().some((matchId) => {
49
- return router.stores.matchStores.get(matchId)?.get()._forcePending
50
- })
51
- }
52
-
53
- const resolvePreload = (inner: InnerLoadContext, matchId: string): boolean => {
54
- return !!(inner.preload && !inner.router.stores.matchStores.has(matchId))
55
- }
56
-
57
- /**
58
- * Builds the accumulated context from router options and all matches up to (and optionally including) the given index.
59
- * Merges __routeContext and __beforeLoadContext from each match.
60
- */
61
- const buildMatchContext = (
62
- inner: InnerLoadContext,
63
- index: number,
64
- includeCurrentMatch: boolean = true,
65
- ): Record<string, unknown> => {
66
- const context: Record<string, unknown> = {
67
- ...(inner.router.options.context ?? {}),
68
- }
69
- const end = includeCurrentMatch ? index : index - 1
70
- for (let i = 0; i <= end; i++) {
71
- const innerMatch = inner.matches[i]
72
- if (!innerMatch) continue
73
- const m = inner.router.getMatch(innerMatch.id)
74
- if (!m) continue
75
- Object.assign(context, m.__routeContext, m.__beforeLoadContext)
76
- }
77
- return context
78
- }
79
-
80
- const getNotFoundBoundaryIndex = (
81
- inner: InnerLoadContext,
82
- err: NotFoundError,
83
- ): number | undefined => {
84
- if (!inner.matches.length) {
85
- return undefined
86
- }
87
-
88
- const requestedRouteId = err.routeId
89
- const matchedRootIndex = inner.matches.findIndex(
90
- (m) => m.routeId === inner.router.routeTree.id,
91
- )
92
- const rootIndex = matchedRootIndex >= 0 ? matchedRootIndex : 0
93
-
94
- let startIndex = requestedRouteId
95
- ? inner.matches.findIndex((match) => match.routeId === requestedRouteId)
96
- : (inner.firstBadMatchIndex ?? inner.matches.length - 1)
97
-
98
- if (startIndex < 0) {
99
- startIndex = rootIndex
100
- }
101
-
102
- for (let i = startIndex; i >= 0; i--) {
103
- const match = inner.matches[i]!
104
- const route = inner.router.looseRoutesById[match.routeId]!
105
- if (route.options.notFoundComponent) {
106
- return i
107
- }
108
- }
109
-
110
- // If no boundary component is found, preserve explicit routeId targeting behavior,
111
- // otherwise default to root for untargeted notFounds.
112
- return requestedRouteId ? startIndex : rootIndex
113
- }
114
-
115
- const handleRedirectAndNotFound = (
116
- inner: InnerLoadContext,
117
- match: AnyRouteMatch | undefined,
118
- err: unknown,
119
- ): void => {
120
- if (!isRedirect(err) && !isNotFound(err)) return
121
-
122
- if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
123
- throw err
124
- }
125
-
126
- // in case of a redirecting match during preload, the match does not exist
127
- if (match) {
128
- match._nonReactive.beforeLoadPromise?.resolve()
129
- match._nonReactive.loaderPromise?.resolve()
130
- match._nonReactive.beforeLoadPromise = undefined
131
- match._nonReactive.loaderPromise = undefined
132
-
133
- match._nonReactive.error = err
134
-
135
- inner.updateMatch(match.id, (prev) => ({
136
- ...prev,
137
- status: isRedirect(err)
138
- ? 'redirected'
139
- : isNotFound(err)
140
- ? 'notFound'
141
- : prev.status === 'pending'
142
- ? 'success'
143
- : prev.status,
144
- context: buildMatchContext(inner, match.index),
145
- isFetching: false,
146
- error: err,
147
- }))
148
-
149
- if (isNotFound(err) && !err.routeId) {
150
- // Stamp the throwing match's routeId so that the finalization step in
151
- // loadMatches knows where the notFound originated. The actual boundary
152
- // resolution (walking up to the nearest notFoundComponent) is deferred to
153
- // the finalization step, where firstBadMatchIndex is stable and
154
- // headMaxIndex can be capped correctly.
155
- err.routeId = match.routeId
156
- }
157
-
158
- match._nonReactive.loadPromise?.resolve()
159
- }
160
-
161
- if (isRedirect(err)) {
162
- inner.rendered = true
163
- err.options._fromLocation = inner.location
164
- err.redirectHandled = true
165
- err = inner.router.resolveRedirect(err)
166
- }
167
-
168
- throw err
169
- }
170
-
171
- const shouldSkipLoader = (
172
- inner: InnerLoadContext,
173
- matchId: string,
174
- ): boolean => {
175
- const match = inner.router.getMatch(matchId)
176
- if (!match) {
177
- return true
178
- }
179
- // upon hydration, we skip the loader if the match has been dehydrated on the server
180
- if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) {
181
- return true
182
- }
183
-
184
- if ((isServer ?? inner.router.isServer) && match.ssr === false) {
185
- return true
186
- }
187
-
188
- return false
189
- }
190
-
191
- const syncMatchContext = (
192
- inner: InnerLoadContext,
193
- matchId: string,
194
- index: number,
195
- ): void => {
196
- const nextContext = buildMatchContext(inner, index)
197
-
198
- inner.updateMatch(matchId, (prev) => {
199
- return {
200
- ...prev,
201
- context: nextContext,
202
- }
203
- })
204
- }
205
-
206
- const handleSerialError = (
207
- inner: InnerLoadContext,
208
- index: number,
209
- err: any,
210
- ): void => {
211
- const { id: matchId, routeId } = inner.matches[index]!
212
- const route = inner.router.looseRoutesById[routeId]!
213
-
214
- // Much like suspense, we use a promise here to know if
215
- // we've been outdated by a new loadMatches call and
216
- // should abort the current async operation
217
- if (err instanceof Promise) {
218
- throw err
219
- }
220
-
221
- inner.firstBadMatchIndex ??= index
222
- handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
223
-
224
- try {
225
- route.options.onError?.(err)
226
- } catch (errorHandlerErr) {
227
- err = errorHandlerErr
228
- handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
229
- }
230
-
231
- inner.updateMatch(matchId, (prev) => {
232
- prev._nonReactive.beforeLoadPromise?.resolve()
233
- prev._nonReactive.beforeLoadPromise = undefined
234
- prev._nonReactive.loadPromise?.resolve()
235
-
236
- return {
237
- ...prev,
238
- error: err,
239
- status: 'error',
240
- isFetching: false,
241
- updatedAt: Date.now(),
242
- abortController: new AbortController(),
243
- }
244
- })
245
-
246
- if (!inner.preload && !isRedirect(err) && !isNotFound(err)) {
247
- inner.serialError ??= err
248
- }
249
- }
250
-
251
- const isBeforeLoadSsr = (
252
- inner: InnerLoadContext,
253
- matchId: string,
254
- index: number,
255
- route: AnyRoute,
256
- ): void | Promise<void> => {
257
- const existingMatch = inner.router.getMatch(matchId)!
258
- const parentMatchId = inner.matches[index - 1]?.id
259
- const parentMatch = parentMatchId
260
- ? inner.router.getMatch(parentMatchId)!
261
- : undefined
262
-
263
- // in SPA mode, only SSR the root route
264
- if (inner.router.isShell()) {
265
- existingMatch.ssr = route.id === rootRouteId
266
- return
267
- }
268
-
269
- if (parentMatch?.ssr === false) {
270
- existingMatch.ssr = false
271
- return
272
- }
273
-
274
- const parentOverride = (tempSsr: SSROption) => {
275
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
276
- return 'data-only'
277
- }
278
- return tempSsr
279
- }
280
-
281
- const defaultSsr = inner.router.options.defaultSsr ?? true
282
-
283
- if (route.options.ssr === undefined) {
284
- existingMatch.ssr = parentOverride(defaultSsr)
285
- return
286
- }
287
-
288
- if (typeof route.options.ssr !== 'function') {
289
- existingMatch.ssr = parentOverride(route.options.ssr)
290
- return
291
- }
292
- const { search, params } = existingMatch
293
-
294
- const ssrFnContext: SsrContextOptions<any, any, any> = {
295
- search: makeMaybe(search, existingMatch.searchError),
296
- params: makeMaybe(params, existingMatch.paramsError),
297
- location: inner.location,
298
- matches: inner.matches.map((match) => ({
299
- index: match.index,
300
- pathname: match.pathname,
301
- fullPath: match.fullPath,
302
- staticData: match.staticData,
303
- id: match.id,
304
- routeId: match.routeId,
305
- search: makeMaybe(match.search, match.searchError),
306
- params: makeMaybe(match.params, match.paramsError),
307
- ssr: match.ssr,
308
- })),
309
- }
310
-
311
- const tempSsr = route.options.ssr(ssrFnContext)
312
- if (isPromise(tempSsr)) {
313
- return tempSsr.then((ssr) => {
314
- existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
315
- })
316
- }
317
-
318
- existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
319
- return
320
- }
321
-
322
- const setupPendingTimeout = (
323
- inner: InnerLoadContext,
324
- matchId: string,
325
- route: AnyRoute,
326
- match: AnyRouteMatch,
327
- ): void => {
328
- if (match._nonReactive.pendingTimeout !== undefined) return
329
-
330
- const pendingMs =
331
- route.options.pendingMs ?? inner.router.options.defaultPendingMs
332
- const shouldPending = !!(
333
- inner.onReady &&
334
- !(isServer ?? inner.router.isServer) &&
335
- !resolvePreload(inner, matchId) &&
336
- (route.options.loader ||
337
- route.options.beforeLoad ||
338
- routeNeedsPreload(route)) &&
339
- typeof pendingMs === 'number' &&
340
- pendingMs !== Infinity &&
341
- (route.options.pendingComponent ??
342
- (inner.router.options as any)?.defaultPendingComponent)
343
- )
344
-
345
- if (shouldPending) {
346
- const pendingTimeout = setTimeout(() => {
347
- // Update the match and prematurely resolve the loadMatches promise so that
348
- // the pending component can start rendering
349
- triggerOnReady(inner)
350
- }, pendingMs)
351
- match._nonReactive.pendingTimeout = pendingTimeout
352
- }
353
- }
354
-
355
- const preBeforeLoadSetup = (
356
- inner: InnerLoadContext,
357
- matchId: string,
358
- route: AnyRoute,
359
- ): void | Promise<void> => {
360
- const existingMatch = inner.router.getMatch(matchId)!
361
-
362
- // If we are in the middle of a load, either of these will be present
363
- // (not to be confused with `loadPromise`, which is always defined)
364
- if (
365
- !existingMatch._nonReactive.beforeLoadPromise &&
366
- !existingMatch._nonReactive.loaderPromise
367
- )
368
- return
369
-
370
- setupPendingTimeout(inner, matchId, route, existingMatch)
371
-
372
- const then = () => {
373
- const match = inner.router.getMatch(matchId)!
374
- if (
375
- match.preload &&
376
- (match.status === 'redirected' || match.status === 'notFound')
377
- ) {
378
- handleRedirectAndNotFound(inner, match, match.error)
379
- }
380
- }
381
-
382
- // Wait for the previous beforeLoad to resolve before we continue
383
- return existingMatch._nonReactive.beforeLoadPromise
384
- ? existingMatch._nonReactive.beforeLoadPromise.then(then)
385
- : then()
386
- }
387
-
388
- const executeBeforeLoad = (
389
- inner: InnerLoadContext,
390
- matchId: string,
391
- index: number,
392
- route: AnyRoute,
393
- ): void | Promise<void> => {
394
- const match = inner.router.getMatch(matchId)!
395
-
396
- // explicitly capture the previous loadPromise
397
- let prevLoadPromise = match._nonReactive.loadPromise
398
- match._nonReactive.loadPromise = createControlledPromise<void>(() => {
399
- prevLoadPromise?.resolve()
400
- prevLoadPromise = undefined
401
- })
402
-
403
- const { paramsError, searchError } = match
404
-
405
- if (paramsError) {
406
- handleSerialError(inner, index, paramsError)
407
- }
408
-
409
- if (searchError) {
410
- handleSerialError(inner, index, searchError)
411
- }
412
-
413
- setupPendingTimeout(inner, matchId, route, match)
414
-
415
- const abortController = new AbortController()
416
-
417
- let isPending = false
418
- const pending = () => {
419
- if (isPending) return
420
- isPending = true
421
- inner.updateMatch(matchId, (prev) => ({
422
- ...prev,
423
- isFetching: 'beforeLoad',
424
- fetchCount: prev.fetchCount + 1,
425
- abortController,
426
- // Note: We intentionally don't update context here.
427
- // Context should only be updated after beforeLoad resolves to avoid
428
- // components seeing incomplete context during async beforeLoad execution.
429
- }))
430
- }
431
-
432
- const resolve = () => {
433
- match._nonReactive.beforeLoadPromise?.resolve()
434
- match._nonReactive.beforeLoadPromise = undefined
435
- inner.updateMatch(matchId, (prev) => ({
436
- ...prev,
437
- isFetching: false,
438
- }))
439
- }
440
-
441
- // if there is no `beforeLoad` option, just mark as pending and resolve
442
- // Context will be updated later in loadRouteMatch after loader completes
443
- if (!route.options.beforeLoad) {
444
- inner.router.batch(() => {
445
- pending()
446
- resolve()
447
- })
448
- return
449
- }
450
-
451
- match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
452
-
453
- // Build context from all parent matches, excluding current match's __beforeLoadContext
454
- // (since we're about to execute beforeLoad for this match)
455
- const context = {
456
- ...buildMatchContext(inner, index, false),
457
- ...match.__routeContext,
458
- }
459
- const { search, params, cause } = match
460
- const preload = resolvePreload(inner, matchId)
461
- const beforeLoadFnContext: BeforeLoadContextOptions<
462
- any,
463
- any,
464
- any,
465
- any,
466
- any,
467
- any,
468
- any,
469
- any,
470
- any
471
- > = {
472
- search,
473
- abortController,
474
- params,
475
- preload,
476
- context,
477
- location: inner.location,
478
- navigate: (opts: any) =>
479
- inner.router.navigate({
480
- ...opts,
481
- _fromLocation: inner.location,
482
- }),
483
- buildLocation: inner.router.buildLocation,
484
- cause: preload ? 'preload' : cause,
485
- matches: inner.matches,
486
- routeId: route.id,
487
- ...inner.router.options.additionalContext,
488
- }
489
-
490
- const updateContext = (beforeLoadContext: any) => {
491
- if (beforeLoadContext === undefined) {
492
- inner.router.batch(() => {
493
- pending()
494
- resolve()
495
- })
496
- return
497
- }
498
- if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
499
- pending()
500
- handleSerialError(inner, index, beforeLoadContext)
501
- }
502
-
503
- inner.router.batch(() => {
504
- pending()
505
- inner.updateMatch(matchId, (prev) => ({
506
- ...prev,
507
- __beforeLoadContext: beforeLoadContext,
508
- }))
509
- resolve()
510
- })
511
- }
512
-
513
- let beforeLoadContext
514
- try {
515
- beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
516
- if (isPromise(beforeLoadContext)) {
517
- pending()
518
- return beforeLoadContext
519
- .catch((err) => {
520
- handleSerialError(inner, index, err)
521
- })
522
- .then(updateContext)
523
- }
524
- } catch (err) {
525
- pending()
526
- handleSerialError(inner, index, err)
527
- }
528
-
529
- updateContext(beforeLoadContext)
530
- return
531
- }
532
-
533
- const handleBeforeLoad = (
534
- inner: InnerLoadContext,
535
- index: number,
536
- ): void | Promise<void> => {
537
- const { id: matchId, routeId } = inner.matches[index]!
538
- const route = inner.router.looseRoutesById[routeId]!
539
-
540
- const serverSsr = () => {
541
- // on the server, determine whether SSR the current match or not
542
- if (isServer ?? inner.router.isServer) {
543
- const maybePromise = isBeforeLoadSsr(inner, matchId, index, route)
544
- if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
545
- }
546
- return queueExecution()
547
- }
548
-
549
- const execute = () => executeBeforeLoad(inner, matchId, index, route)
550
-
551
- const queueExecution = () => {
552
- if (shouldSkipLoader(inner, matchId)) return
553
- const result = preBeforeLoadSetup(inner, matchId, route)
554
- return isPromise(result) ? result.then(execute) : execute()
555
- }
556
-
557
- return serverSsr()
558
- }
559
-
560
- const executeHead = (
561
- inner: InnerLoadContext,
562
- matchId: string,
563
- route: AnyRoute,
564
- ): void | Promise<
565
- Pick<
566
- AnyRouteMatch,
567
- 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
568
- >
569
- > => {
570
- const match = inner.router.getMatch(matchId)
571
- // in case of a redirecting match during preload, the match does not exist
572
- if (!match) {
573
- return
574
- }
575
- if (!route.options.head && !route.options.scripts && !route.options.headers) {
576
- return
577
- }
578
- const assetContext = {
579
- ssr: inner.router.options.ssr,
580
- matches: inner.matches,
581
- match,
582
- params: match.params,
583
- loaderData: match.loaderData,
584
- }
585
-
586
- return Promise.all([
587
- route.options.head?.(assetContext),
588
- route.options.scripts?.(assetContext),
589
- route.options.headers?.(assetContext),
590
- ]).then(([headFnContent, scripts, headers]) => {
591
- const meta = headFnContent?.meta
592
- const links = headFnContent?.links
593
- const headScripts = headFnContent?.scripts
594
- const styles = headFnContent?.styles
595
-
596
- return {
597
- meta,
598
- links,
599
- headScripts,
600
- headers,
601
- scripts,
602
- styles,
603
- }
604
- })
605
- }
606
-
607
- const getLoaderContext = (
608
- inner: InnerLoadContext,
609
- matchPromises: Array<Promise<AnyRouteMatch>>,
610
- matchId: string,
611
- index: number,
612
- route: AnyRoute,
613
- ): LoaderFnContext => {
614
- const parentMatchPromise = matchPromises[index - 1] as any
615
- const { params, loaderDeps, abortController, cause } =
616
- inner.router.getMatch(matchId)!
617
-
618
- const context = buildMatchContext(inner, index)
619
-
620
- const preload = resolvePreload(inner, matchId)
621
-
622
- return {
623
- params,
624
- deps: loaderDeps,
625
- preload: !!preload,
626
- parentMatchPromise,
627
- abortController,
628
- context,
629
- location: inner.location,
630
- navigate: (opts) =>
631
- inner.router.navigate({
632
- ...opts,
633
- _fromLocation: inner.location,
634
- }),
635
- cause: preload ? 'preload' : cause,
636
- route,
637
- ...inner.router.options.additionalContext,
638
- }
639
- }
640
-
641
- const runLoader = async (
642
- inner: InnerLoadContext,
643
- matchPromises: Array<Promise<AnyRouteMatch>>,
644
- matchId: string,
645
- index: number,
646
- route: AnyRoute,
647
- ): Promise<void> => {
648
- try {
649
- // If the Matches component rendered
650
- // the pending component and needs to show it for
651
- // a minimum duration, we''ll wait for it to resolve
652
- // before committing to the match and resolving
653
- // the loadPromise
654
-
655
- const match = inner.router.getMatch(matchId)!
656
-
657
- // Actually run the loader and handle the result
658
- try {
659
- if (!(isServer ?? inner.router.isServer) || match.ssr === true) {
660
- loadRouteChunk(route)
661
- }
662
-
663
- // Kick off the loader!
664
- const routeLoader = route.options.loader
665
- const loader =
666
- typeof routeLoader === 'function' ? routeLoader : routeLoader?.handler
667
- const loaderResult = loader?.(
668
- getLoaderContext(inner, matchPromises, matchId, index, route),
669
- )
670
- const loaderResultIsPromise = !!loader && isPromise(loaderResult)
671
-
672
- const willLoadSomething = !!(
673
- loaderResultIsPromise ||
674
- route._lazyPromise ||
675
- route._componentsPromise ||
676
- route.options.head ||
677
- route.options.scripts ||
678
- route.options.headers ||
679
- match._nonReactive.minPendingPromise
680
- )
681
-
682
- if (willLoadSomething) {
683
- inner.updateMatch(matchId, (prev) => ({
684
- ...prev,
685
- isFetching: 'loader',
686
- }))
687
- }
688
-
689
- if (loader) {
690
- const loaderData = loaderResultIsPromise
691
- ? await loaderResult
692
- : loaderResult
693
-
694
- handleRedirectAndNotFound(
695
- inner,
696
- inner.router.getMatch(matchId),
697
- loaderData,
698
- )
699
- if (loaderData !== undefined) {
700
- inner.updateMatch(matchId, (prev) => ({
701
- ...prev,
702
- loaderData,
703
- }))
704
- }
705
- }
706
-
707
- // Lazy option can modify the route options,
708
- // so we need to wait for it to resolve before
709
- // we can use the options
710
- if (route._lazyPromise) await route._lazyPromise
711
- const pendingPromise = match._nonReactive.minPendingPromise
712
- if (pendingPromise) await pendingPromise
713
-
714
- // Last but not least, wait for the components
715
- // to be preloaded before we resolve the match
716
- if (route._componentsPromise) await route._componentsPromise
717
- inner.updateMatch(matchId, (prev) => ({
718
- ...prev,
719
- error: undefined,
720
- context: buildMatchContext(inner, index),
721
- status: 'success',
722
- isFetching: false,
723
- updatedAt: Date.now(),
724
- }))
725
- } catch (e) {
726
- let error = e
727
-
728
- if ((error as any)?.name === 'AbortError') {
729
- if (match.abortController.signal.aborted) {
730
- match._nonReactive.loaderPromise?.resolve()
731
- match._nonReactive.loaderPromise = undefined
732
- return
733
- }
734
- inner.updateMatch(matchId, (prev) => ({
735
- ...prev,
736
- status: prev.status === 'pending' ? 'success' : prev.status,
737
- isFetching: false,
738
- context: buildMatchContext(inner, index),
739
- }))
740
- return
741
- }
742
-
743
- const pendingPromise = match._nonReactive.minPendingPromise
744
- if (pendingPromise) await pendingPromise
745
-
746
- if (isNotFound(e)) {
747
- await (route.options.notFoundComponent as any)?.preload?.()
748
- }
749
-
750
- handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), e)
751
-
752
- try {
753
- route.options.onError?.(e)
754
- } catch (onErrorError) {
755
- error = onErrorError
756
- handleRedirectAndNotFound(
757
- inner,
758
- inner.router.getMatch(matchId),
759
- onErrorError,
760
- )
761
- }
762
- if (!isRedirect(error) && !isNotFound(error)) {
763
- await loadRouteChunk(route, ['errorComponent'])
764
- }
765
-
766
- inner.updateMatch(matchId, (prev) => ({
767
- ...prev,
768
- error,
769
- context: buildMatchContext(inner, index),
770
- status: 'error',
771
- isFetching: false,
772
- }))
773
- }
774
- } catch (err) {
775
- const match = inner.router.getMatch(matchId)
776
- // in case of a redirecting match during preload, the match does not exist
777
- if (match) {
778
- match._nonReactive.loaderPromise = undefined
779
- }
780
- handleRedirectAndNotFound(inner, match, err)
781
- }
782
- }
783
-
784
- const loadRouteMatch = async (
785
- inner: InnerLoadContext,
786
- matchPromises: Array<Promise<AnyRouteMatch>>,
787
- index: number,
788
- ): Promise<AnyRouteMatch> => {
789
- async function handleLoader(
790
- preload: boolean,
791
- prevMatch: AnyRouteMatch,
792
- previousRouteMatchId: string | undefined,
793
- match: AnyRouteMatch,
794
- route: AnyRoute,
795
- ) {
796
- const age = Date.now() - prevMatch.updatedAt
797
-
798
- const staleAge = preload
799
- ? (route.options.preloadStaleTime ??
800
- inner.router.options.defaultPreloadStaleTime ??
801
- 30_000) // 30 seconds for preloads by default
802
- : (route.options.staleTime ?? inner.router.options.defaultStaleTime ?? 0)
803
-
804
- const shouldReloadOption = route.options.shouldReload
805
-
806
- // Default to reloading the route all the time
807
- // Allow shouldReload to get the last say,
808
- // if provided.
809
- const shouldReload =
810
- typeof shouldReloadOption === 'function'
811
- ? shouldReloadOption(
812
- getLoaderContext(inner, matchPromises, matchId, index, route),
813
- )
814
- : shouldReloadOption
815
-
816
- // If the route is successful and still fresh, just resolve
817
- const { status, invalid } = match
818
- const staleMatchShouldReload =
819
- age >= staleAge &&
820
- (!!inner.forceStaleReload ||
821
- match.cause === 'enter' ||
822
- (previousRouteMatchId !== undefined &&
823
- previousRouteMatchId !== match.id))
824
- loaderShouldRunAsync =
825
- status === 'success' &&
826
- (invalid || (shouldReload ?? staleMatchShouldReload))
827
- if (preload && route.options.preload === false) {
828
- // Do nothing
829
- } else if (
830
- loaderShouldRunAsync &&
831
- !inner.sync &&
832
- shouldReloadInBackground
833
- ) {
834
- loaderIsRunningAsync = true
835
- ;(async () => {
836
- try {
837
- await runLoader(inner, matchPromises, matchId, index, route)
838
- const match = inner.router.getMatch(matchId)!
839
- match._nonReactive.loaderPromise?.resolve()
840
- match._nonReactive.loadPromise?.resolve()
841
- match._nonReactive.loaderPromise = undefined
842
- match._nonReactive.loadPromise = undefined
843
- } catch (err) {
844
- if (isRedirect(err)) {
845
- await inner.router.navigate(err.options)
846
- }
847
- }
848
- })()
849
- } else if (status !== 'success' || loaderShouldRunAsync) {
850
- await runLoader(inner, matchPromises, matchId, index, route)
851
- } else {
852
- syncMatchContext(inner, matchId, index)
853
- }
854
- }
855
-
856
- const { id: matchId, routeId } = inner.matches[index]!
857
- let loaderShouldRunAsync = false
858
- let loaderIsRunningAsync = false
859
- const route = inner.router.looseRoutesById[routeId]!
860
- const routeLoader = route.options.loader
861
- const shouldReloadInBackground =
862
- ((typeof routeLoader === 'function'
863
- ? undefined
864
- : routeLoader?.staleReloadMode) ??
865
- inner.router.options.defaultStaleReloadMode) !== 'blocking'
866
-
867
- if (shouldSkipLoader(inner, matchId)) {
868
- const match = inner.router.getMatch(matchId)
869
- if (!match) {
870
- return inner.matches[index]!
871
- }
872
-
873
- syncMatchContext(inner, matchId, index)
874
-
875
- if (isServer ?? inner.router.isServer) {
876
- return inner.router.getMatch(matchId)!
877
- }
878
- } else {
879
- const prevMatch = inner.router.getMatch(matchId)! // This is where all of the stale-while-revalidate magic happens
880
- const activeIdAtIndex = inner.router.stores.matchesId.get()[index]
881
- const activeAtIndex =
882
- (activeIdAtIndex &&
883
- inner.router.stores.matchStores.get(activeIdAtIndex)) ||
884
- null
885
- const previousRouteMatchId =
886
- activeAtIndex?.routeId === routeId
887
- ? activeIdAtIndex
888
- : inner.router.stores.matches.get().find((d) => d.routeId === routeId)
889
- ?.id
890
- const preload = resolvePreload(inner, matchId)
891
-
892
- // there is a loaderPromise, so we are in the middle of a load
893
- if (prevMatch._nonReactive.loaderPromise) {
894
- // do not block if we already have stale data we can show
895
- // but only if the ongoing load is not a preload since error handling is different for preloads
896
- // and we don't want to swallow errors
897
- if (
898
- prevMatch.status === 'success' &&
899
- !inner.sync &&
900
- !prevMatch.preload &&
901
- shouldReloadInBackground
902
- ) {
903
- return prevMatch
904
- }
905
- await prevMatch._nonReactive.loaderPromise
906
- const match = inner.router.getMatch(matchId)!
907
- const error = match._nonReactive.error || match.error
908
- if (error) {
909
- handleRedirectAndNotFound(inner, match, error)
910
- }
911
-
912
- if (match.status === 'pending') {
913
- await handleLoader(
914
- preload,
915
- prevMatch,
916
- previousRouteMatchId,
917
- match,
918
- route,
919
- )
920
- }
921
- } else {
922
- const nextPreload =
923
- preload && !inner.router.stores.matchStores.has(matchId)
924
- const match = inner.router.getMatch(matchId)!
925
- match._nonReactive.loaderPromise = createControlledPromise<void>()
926
- if (nextPreload !== match.preload) {
927
- inner.updateMatch(matchId, (prev) => ({
928
- ...prev,
929
- preload: nextPreload,
930
- }))
931
- }
932
-
933
- await handleLoader(preload, prevMatch, previousRouteMatchId, match, route)
934
- }
935
- }
936
- const match = inner.router.getMatch(matchId)!
937
- if (!loaderIsRunningAsync) {
938
- match._nonReactive.loaderPromise?.resolve()
939
- match._nonReactive.loadPromise?.resolve()
940
- match._nonReactive.loadPromise = undefined
941
- }
942
-
943
- clearTimeout(match._nonReactive.pendingTimeout)
944
- match._nonReactive.pendingTimeout = undefined
945
- if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
946
- match._nonReactive.dehydrated = undefined
947
-
948
- const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
949
- if (nextIsFetching !== match.isFetching || match.invalid !== false) {
950
- inner.updateMatch(matchId, (prev) => ({
951
- ...prev,
952
- isFetching: nextIsFetching,
953
- invalid: false,
954
- }))
955
- return inner.router.getMatch(matchId)!
956
- } else {
957
- return match
958
- }
959
- }
960
-
961
- export async function loadMatches(arg: {
962
- router: AnyRouter
963
- location: ParsedLocation
964
- matches: Array<AnyRouteMatch>
965
- preload?: boolean
966
- forceStaleReload?: boolean
967
- onReady?: () => Promise<void>
968
- updateMatch: UpdateMatchFn
969
- sync?: boolean
970
- }): Promise<Array<MakeRouteMatch>> {
971
- const inner: InnerLoadContext = arg
972
- const matchPromises: Array<Promise<AnyRouteMatch>> = []
973
-
974
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
975
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
976
- if (
977
- !(isServer ?? inner.router.isServer) &&
978
- hasForcePendingActiveMatch(inner.router)
979
- ) {
980
- triggerOnReady(inner)
981
- }
982
-
983
- let beforeLoadNotFound: NotFoundError | undefined
984
-
985
- // Execute all beforeLoads one by one
986
- for (let i = 0; i < inner.matches.length; i++) {
987
- try {
988
- const beforeLoad = handleBeforeLoad(inner, i)
989
- if (isPromise(beforeLoad)) await beforeLoad
990
- } catch (err) {
991
- if (isRedirect(err)) {
992
- throw err
993
- }
994
- if (isNotFound(err)) {
995
- beforeLoadNotFound = err
996
- } else {
997
- if (!inner.preload) throw err
998
- }
999
- break
1000
- }
1001
-
1002
- if (inner.serialError || inner.firstBadMatchIndex != null) {
1003
- break
1004
- }
1005
- }
1006
-
1007
- // Execute loaders once, with max index adapted for beforeLoad notFound handling.
1008
- const baseMaxIndexExclusive = inner.firstBadMatchIndex ?? inner.matches.length
1009
-
1010
- const boundaryIndex =
1011
- beforeLoadNotFound && !inner.preload
1012
- ? getNotFoundBoundaryIndex(inner, beforeLoadNotFound)
1013
- : undefined
1014
-
1015
- const maxIndexExclusive =
1016
- beforeLoadNotFound && inner.preload
1017
- ? 0
1018
- : boundaryIndex !== undefined
1019
- ? Math.min(boundaryIndex + 1, baseMaxIndexExclusive)
1020
- : baseMaxIndexExclusive
1021
-
1022
- let firstNotFound: NotFoundError | undefined
1023
- let firstUnhandledRejection: unknown
1024
-
1025
- for (let i = 0; i < maxIndexExclusive; i++) {
1026
- matchPromises.push(loadRouteMatch(inner, matchPromises, i))
1027
- }
1028
-
1029
- try {
1030
- await Promise.all(matchPromises)
1031
- } catch {
1032
- const settled = await Promise.allSettled(matchPromises)
1033
-
1034
- for (const result of settled) {
1035
- if (result.status !== 'rejected') continue
1036
-
1037
- const reason = result.reason
1038
- if (isRedirect(reason)) {
1039
- throw reason
1040
- }
1041
- if (isNotFound(reason)) {
1042
- firstNotFound ??= reason
1043
- } else {
1044
- firstUnhandledRejection ??= reason
1045
- }
1046
- }
1047
-
1048
- if (firstUnhandledRejection !== undefined) {
1049
- throw firstUnhandledRejection
1050
- }
1051
- }
1052
-
1053
- const notFoundToThrow =
1054
- firstNotFound ??
1055
- (beforeLoadNotFound && !inner.preload ? beforeLoadNotFound : undefined)
1056
-
1057
- let headMaxIndex =
1058
- inner.firstBadMatchIndex !== undefined
1059
- ? inner.firstBadMatchIndex
1060
- : inner.matches.length - 1
1061
-
1062
- if (!notFoundToThrow && beforeLoadNotFound && inner.preload) {
1063
- return inner.matches
1064
- }
1065
-
1066
- if (notFoundToThrow) {
1067
- // Determine once which matched route will actually render the
1068
- // notFoundComponent, then pass this precomputed index through the remaining
1069
- // finalization steps.
1070
- // This can differ from the throwing route when routeId targets an ancestor
1071
- // boundary (or when bubbling resolves to a parent/root boundary).
1072
- const renderedBoundaryIndex = getNotFoundBoundaryIndex(
1073
- inner,
1074
- notFoundToThrow,
1075
- )
1076
-
1077
- if (renderedBoundaryIndex === undefined) {
1078
- if (process.env.NODE_ENV !== 'production') {
1079
- throw new Error(
1080
- 'Invariant failed: Could not find match for notFound boundary',
1081
- )
1082
- }
1083
-
1084
- invariant()
1085
- }
1086
- const boundaryMatch = inner.matches[renderedBoundaryIndex]!
1087
-
1088
- const boundaryRoute = inner.router.looseRoutesById[boundaryMatch.routeId]!
1089
- const defaultNotFoundComponent = (inner.router.options as any)
1090
- ?.defaultNotFoundComponent
1091
-
1092
- // Ensure a notFoundComponent exists on the boundary route
1093
- if (!boundaryRoute.options.notFoundComponent && defaultNotFoundComponent) {
1094
- boundaryRoute.options.notFoundComponent = defaultNotFoundComponent
1095
- }
1096
-
1097
- notFoundToThrow.routeId = boundaryMatch.routeId
1098
-
1099
- const boundaryIsRoot = boundaryMatch.routeId === inner.router.routeTree.id
1100
-
1101
- inner.updateMatch(boundaryMatch.id, (prev) => ({
1102
- ...prev,
1103
- ...(boundaryIsRoot
1104
- ? // For root boundary, use globalNotFound so the root component's
1105
- // shell still renders and <Outlet> handles the not-found display,
1106
- // instead of replacing the entire root shell via status='notFound'.
1107
- { status: 'success' as const, globalNotFound: true, error: undefined }
1108
- : // For non-root boundaries, set status:'notFound' so MatchInner
1109
- // renders the notFoundComponent directly.
1110
- { status: 'notFound' as const, error: notFoundToThrow }),
1111
- isFetching: false,
1112
- }))
1113
-
1114
- headMaxIndex = renderedBoundaryIndex
1115
-
1116
- // Ensure the rendering boundary route chunk (and its lazy components, including
1117
- // lazy notFoundComponent) is loaded before we continue to head execution/render.
1118
- await loadRouteChunk(boundaryRoute, ['notFoundComponent'])
1119
- } else if (!inner.preload) {
1120
- // Clear stale root global-not-found state on normal navigations that do not
1121
- // throw notFound. This must live here (instead of only in runLoader success)
1122
- // because the root loader may be skipped when data is still fresh.
1123
- const rootMatch = inner.matches[0]!
1124
- // `rootMatch` is the next match for this navigation. If it is not global
1125
- // not-found, then any currently stored root global-not-found is stale.
1126
- if (!rootMatch.globalNotFound) {
1127
- // `currentRootMatch` is the current store state (from the previous
1128
- // navigation/load). Update only when a stale flag is actually present.
1129
- const currentRootMatch = inner.router.getMatch(rootMatch.id)
1130
- if (currentRootMatch?.globalNotFound) {
1131
- inner.updateMatch(rootMatch.id, (prev) => ({
1132
- ...prev,
1133
- globalNotFound: false,
1134
- error: undefined,
1135
- }))
1136
- }
1137
- }
1138
- }
1139
-
1140
- // When a serial error occurred (e.g. beforeLoad threw a regular Error),
1141
- // the erroring route's lazy chunk wasn't loaded because loaders were skipped.
1142
- // We need to load it so the code-split errorComponent is available for rendering.
1143
- if (inner.serialError && inner.firstBadMatchIndex !== undefined) {
1144
- const errorRoute =
1145
- inner.router.looseRoutesById[
1146
- inner.matches[inner.firstBadMatchIndex]!.routeId
1147
- ]!
1148
- await loadRouteChunk(errorRoute, ['errorComponent'])
1149
- }
1150
-
1151
- // serially execute heads once after loaders/notFound handling, ensuring
1152
- // all head functions get a chance even if one throws.
1153
- for (let i = 0; i <= headMaxIndex; i++) {
1154
- const match = inner.matches[i]!
1155
- const { id: matchId, routeId } = match
1156
- const route = inner.router.looseRoutesById[routeId]!
1157
- try {
1158
- const headResult = executeHead(inner, matchId, route)
1159
- if (headResult) {
1160
- const head = await headResult
1161
- inner.updateMatch(matchId, (prev) => ({
1162
- ...prev,
1163
- ...head,
1164
- }))
1165
- }
1166
- } catch (err) {
1167
- console.error(`Error executing head for route ${routeId}:`, err)
1168
- }
1169
- }
1170
-
1171
- const readyPromise = triggerOnReady(inner)
1172
- if (isPromise(readyPromise)) {
1173
- await readyPromise
1174
- }
1175
-
1176
- if (notFoundToThrow) {
1177
- throw notFoundToThrow
1178
- }
1179
-
1180
- if (inner.serialError && !inner.preload && !inner.onReady) {
1181
- throw inner.serialError
1182
- }
1183
-
1184
- return inner.matches
1185
- }
1186
-
1187
- export type RouteComponentType =
1188
- | 'component'
1189
- | 'errorComponent'
1190
- | 'pendingComponent'
1191
- | 'notFoundComponent'
1192
-
1193
- function preloadRouteComponents(
1194
- route: AnyRoute,
1195
- componentTypesToLoad: Array<RouteComponentType>,
1196
- ): Promise<void> | undefined {
1197
- const preloads = componentTypesToLoad
1198
- .map((type) => (route.options[type] as any)?.preload?.())
1199
- .filter(Boolean)
1200
-
1201
- if (preloads.length === 0) return undefined
1202
-
1203
- return Promise.all(preloads) as any as Promise<void>
1204
- }
1205
-
1206
- export function loadRouteChunk(
1207
- route: AnyRoute,
1208
- componentTypesToLoad: Array<RouteComponentType> = componentTypes,
1209
- ) {
1210
- if (!route._lazyLoaded && route._lazyPromise === undefined) {
1211
- if (route.lazyFn) {
1212
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
1213
- // explicitly don't copy over the lazy route's id
1214
- const { id: _id, ...options } = lazyRoute.options
1215
- Object.assign(route.options, options)
1216
- route._lazyLoaded = true
1217
- route._lazyPromise = undefined // gc promise, we won't need it anymore
1218
- })
1219
- } else {
1220
- route._lazyLoaded = true
1221
- }
1222
- }
1223
-
1224
- const runAfterLazy = () =>
1225
- route._componentsLoaded
1226
- ? undefined
1227
- : componentTypesToLoad === componentTypes
1228
- ? (() => {
1229
- if (route._componentsPromise === undefined) {
1230
- const componentsPromise = preloadRouteComponents(
1231
- route,
1232
- componentTypes,
1233
- )
1234
-
1235
- if (componentsPromise) {
1236
- route._componentsPromise = componentsPromise.then(() => {
1237
- route._componentsLoaded = true
1238
- route._componentsPromise = undefined // gc promise, we won't need it anymore
1239
- })
1240
- } else {
1241
- route._componentsLoaded = true
1242
- }
1243
- }
1244
-
1245
- return route._componentsPromise
1246
- })()
1247
- : preloadRouteComponents(route, componentTypesToLoad)
1248
-
1249
- return route._lazyPromise
1250
- ? route._lazyPromise.then(runAfterLazy)
1251
- : runAfterLazy()
1252
- }
1253
-
1254
- function makeMaybe<TValue, TError>(
1255
- value: TValue,
1256
- error: TError,
1257
- ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
1258
- if (error) {
1259
- return { status: 'error' as const, error }
1260
- }
1261
- return { status: 'success' as const, value }
1262
- }
1263
-
1264
- export function routeNeedsPreload(route: AnyRoute) {
1265
- for (const componentType of componentTypes) {
1266
- if ((route.options[componentType] as any)?.preload) {
1267
- return true
1268
- }
1269
- }
1270
- return false
1271
- }
1272
-
1273
- export const componentTypes: Array<RouteComponentType> = [
1274
- 'component',
1275
- 'errorComponent',
1276
- 'pendingComponent',
1277
- 'notFoundComponent',
1278
- ] as const