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

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 (115) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +7 -9
  3. package/dist/cjs/index.cjs +8 -2
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/index.d.cts +6 -2
  6. package/dist/cjs/load-matches.cjs +636 -0
  7. package/dist/cjs/load-matches.cjs.map +1 -0
  8. package/dist/cjs/load-matches.d.cts +16 -0
  9. package/dist/cjs/qss.cjs +19 -19
  10. package/dist/cjs/qss.cjs.map +1 -1
  11. package/dist/cjs/qss.d.cts +6 -4
  12. package/dist/cjs/redirect.cjs +3 -3
  13. package/dist/cjs/redirect.cjs.map +1 -1
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/route.d.cts +0 -4
  16. package/dist/cjs/router.cjs +64 -632
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +14 -26
  19. package/dist/cjs/scroll-restoration.cjs +20 -25
  20. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  21. package/dist/cjs/scroll-restoration.d.cts +0 -9
  22. package/dist/cjs/searchParams.cjs +7 -15
  23. package/dist/cjs/searchParams.cjs.map +1 -1
  24. package/dist/cjs/ssr/constants.cjs +5 -0
  25. package/dist/cjs/ssr/constants.cjs.map +1 -0
  26. package/dist/cjs/ssr/constants.d.cts +1 -0
  27. package/dist/cjs/ssr/{seroval-plugins.cjs → serializer/ShallowErrorPlugin.cjs} +2 -2
  28. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -0
  29. package/dist/cjs/ssr/{seroval-plugins.d.cts → serializer/ShallowErrorPlugin.d.cts} +1 -2
  30. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +11 -0
  31. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -0
  32. package/dist/cjs/ssr/serializer/seroval-plugins.d.cts +2 -0
  33. package/dist/cjs/ssr/serializer/transformer.cjs +50 -0
  34. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -0
  35. package/dist/cjs/ssr/serializer/transformer.d.cts +18 -0
  36. package/dist/cjs/ssr/ssr-client.cjs +53 -40
  37. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  38. package/dist/cjs/ssr/ssr-client.d.cts +5 -1
  39. package/dist/cjs/ssr/ssr-server.cjs +12 -10
  40. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  41. package/dist/cjs/ssr/ssr-server.d.cts +0 -1
  42. package/dist/cjs/ssr/tsrScript.cjs +1 -1
  43. package/dist/cjs/ssr/tsrScript.cjs.map +1 -1
  44. package/dist/cjs/typePrimitives.d.cts +6 -6
  45. package/dist/cjs/utils.cjs +14 -7
  46. package/dist/cjs/utils.cjs.map +1 -1
  47. package/dist/cjs/utils.d.cts +2 -1
  48. package/dist/esm/Matches.d.ts +7 -9
  49. package/dist/esm/Matches.js.map +1 -1
  50. package/dist/esm/index.d.ts +6 -2
  51. package/dist/esm/index.js +9 -3
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/load-matches.d.ts +16 -0
  54. package/dist/esm/load-matches.js +636 -0
  55. package/dist/esm/load-matches.js.map +1 -0
  56. package/dist/esm/qss.d.ts +6 -4
  57. package/dist/esm/qss.js +19 -19
  58. package/dist/esm/qss.js.map +1 -1
  59. package/dist/esm/redirect.js +3 -3
  60. package/dist/esm/redirect.js.map +1 -1
  61. package/dist/esm/route.d.ts +0 -4
  62. package/dist/esm/route.js.map +1 -1
  63. package/dist/esm/router.d.ts +14 -26
  64. package/dist/esm/router.js +64 -632
  65. package/dist/esm/router.js.map +1 -1
  66. package/dist/esm/scroll-restoration.d.ts +0 -9
  67. package/dist/esm/scroll-restoration.js +20 -25
  68. package/dist/esm/scroll-restoration.js.map +1 -1
  69. package/dist/esm/searchParams.js +7 -15
  70. package/dist/esm/searchParams.js.map +1 -1
  71. package/dist/esm/ssr/constants.d.ts +1 -0
  72. package/dist/esm/ssr/constants.js +5 -0
  73. package/dist/esm/ssr/constants.js.map +1 -0
  74. package/dist/esm/ssr/{seroval-plugins.d.ts → serializer/ShallowErrorPlugin.d.ts} +1 -2
  75. package/dist/esm/ssr/{seroval-plugins.js → serializer/ShallowErrorPlugin.js} +2 -2
  76. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -0
  77. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -0
  78. package/dist/esm/ssr/serializer/seroval-plugins.js +11 -0
  79. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -0
  80. package/dist/esm/ssr/serializer/transformer.d.ts +18 -0
  81. package/dist/esm/ssr/serializer/transformer.js +50 -0
  82. package/dist/esm/ssr/serializer/transformer.js.map +1 -0
  83. package/dist/esm/ssr/ssr-client.d.ts +5 -1
  84. package/dist/esm/ssr/ssr-client.js +53 -40
  85. package/dist/esm/ssr/ssr-client.js.map +1 -1
  86. package/dist/esm/ssr/ssr-server.d.ts +0 -1
  87. package/dist/esm/ssr/ssr-server.js +12 -10
  88. package/dist/esm/ssr/ssr-server.js.map +1 -1
  89. package/dist/esm/ssr/tsrScript.js +1 -1
  90. package/dist/esm/ssr/tsrScript.js.map +1 -1
  91. package/dist/esm/typePrimitives.d.ts +6 -6
  92. package/dist/esm/utils.d.ts +2 -1
  93. package/dist/esm/utils.js +14 -7
  94. package/dist/esm/utils.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/Matches.ts +16 -8
  97. package/src/index.ts +12 -2
  98. package/src/load-matches.ts +955 -0
  99. package/src/qss.ts +27 -24
  100. package/src/redirect.ts +3 -3
  101. package/src/route.ts +10 -2
  102. package/src/router.ts +99 -893
  103. package/src/scroll-restoration.ts +25 -32
  104. package/src/searchParams.ts +8 -19
  105. package/src/ssr/constants.ts +1 -0
  106. package/src/ssr/{seroval-plugins.ts → serializer/ShallowErrorPlugin.ts} +2 -2
  107. package/src/ssr/serializer/seroval-plugins.ts +9 -0
  108. package/src/ssr/serializer/transformer.ts +78 -0
  109. package/src/ssr/ssr-client.ts +72 -44
  110. package/src/ssr/ssr-server.ts +18 -10
  111. package/src/ssr/tsrScript.ts +5 -1
  112. package/src/typePrimitives.ts +6 -6
  113. package/src/utils.ts +21 -10
  114. package/dist/cjs/ssr/seroval-plugins.cjs.map +0 -1
  115. package/dist/esm/ssr/seroval-plugins.js.map +0 -1
@@ -0,0 +1,955 @@
1
+ import { batch } from '@tanstack/store'
2
+ import invariant from 'tiny-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, 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
+ updateMatch: UpdateMatchFn
32
+ matches: Array<AnyRouteMatch>
33
+ preload?: boolean
34
+ onReady?: () => Promise<void>
35
+ sync?: boolean
36
+ /** mutable state, scoped to a `loadMatches` call */
37
+ matchPromises: Array<Promise<AnyRouteMatch>>
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 resolvePreload = (inner: InnerLoadContext, matchId: string): boolean => {
48
+ return !!(
49
+ inner.preload && !inner.router.state.matches.some((d) => d.id === matchId)
50
+ )
51
+ }
52
+
53
+ const _handleNotFound = (inner: InnerLoadContext, err: NotFoundError) => {
54
+ // Find the route that should handle the not found error
55
+ // First check if a specific route is requested to show the error
56
+ const routeCursor =
57
+ inner.router.routesById[err.routeId ?? ''] ?? inner.router.routeTree
58
+
59
+ // Ensure a NotFoundComponent exists on the route
60
+ if (
61
+ !routeCursor.options.notFoundComponent &&
62
+ (inner.router.options as any)?.defaultNotFoundComponent
63
+ ) {
64
+ routeCursor.options.notFoundComponent = (
65
+ inner.router.options as any
66
+ ).defaultNotFoundComponent
67
+ }
68
+
69
+ // Ensure we have a notFoundComponent
70
+ invariant(
71
+ routeCursor.options.notFoundComponent,
72
+ 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
73
+ )
74
+
75
+ // Find the match for this route
76
+ const matchForRoute = inner.matches.find((m) => m.routeId === routeCursor.id)
77
+
78
+ invariant(matchForRoute, 'Could not find match for route: ' + routeCursor.id)
79
+
80
+ // Assign the error to the match - using non-null assertion since we've checked with invariant
81
+ inner.updateMatch(matchForRoute.id, (prev) => ({
82
+ ...prev,
83
+ status: 'notFound',
84
+ error: err,
85
+ isFetching: false,
86
+ }))
87
+
88
+ if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
89
+ err.routeId = routeCursor.parentRoute.id
90
+ _handleNotFound(inner, err)
91
+ }
92
+ }
93
+
94
+ const handleRedirectAndNotFound = (
95
+ inner: InnerLoadContext,
96
+ match: AnyRouteMatch | undefined,
97
+ err: unknown,
98
+ ): void => {
99
+ if (!isRedirect(err) && !isNotFound(err)) return
100
+
101
+ if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
102
+ throw err
103
+ }
104
+
105
+ // in case of a redirecting match during preload, the match does not exist
106
+ if (match) {
107
+ match._nonReactive.beforeLoadPromise?.resolve()
108
+ match._nonReactive.loaderPromise?.resolve()
109
+ match._nonReactive.beforeLoadPromise = undefined
110
+ match._nonReactive.loaderPromise = undefined
111
+
112
+ const status = isRedirect(err) ? 'redirected' : 'notFound'
113
+
114
+ inner.updateMatch(match.id, (prev) => ({
115
+ ...prev,
116
+ status,
117
+ isFetching: false,
118
+ error: err,
119
+ }))
120
+
121
+ if (isNotFound(err) && !err.routeId) {
122
+ err.routeId = match.routeId
123
+ }
124
+
125
+ match._nonReactive.loadPromise?.resolve()
126
+ }
127
+
128
+ if (isRedirect(err)) {
129
+ inner.rendered = true
130
+ err.options._fromLocation = inner.location
131
+ err.redirectHandled = true
132
+ err = inner.router.resolveRedirect(err)
133
+ throw err
134
+ } else {
135
+ _handleNotFound(inner, err)
136
+ throw err
137
+ }
138
+ }
139
+
140
+ const shouldSkipLoader = (
141
+ inner: InnerLoadContext,
142
+ matchId: string,
143
+ ): boolean => {
144
+ const match = inner.router.getMatch(matchId)!
145
+ // upon hydration, we skip the loader if the match has been dehydrated on the server
146
+ if (!inner.router.isServer && match._nonReactive.dehydrated) {
147
+ return true
148
+ }
149
+
150
+ if (inner.router.isServer && match.ssr === false) {
151
+ return true
152
+ }
153
+
154
+ return false
155
+ }
156
+
157
+ const handleSerialError = (
158
+ inner: InnerLoadContext,
159
+ index: number,
160
+ err: any,
161
+ routerCode: string,
162
+ ): void => {
163
+ const { id: matchId, routeId } = inner.matches[index]!
164
+ const route = inner.router.looseRoutesById[routeId]!
165
+
166
+ // Much like suspense, we use a promise here to know if
167
+ // we've been outdated by a new loadMatches call and
168
+ // should abort the current async operation
169
+ if (err instanceof Promise) {
170
+ throw err
171
+ }
172
+
173
+ err.routerCode = routerCode
174
+ inner.firstBadMatchIndex ??= index
175
+ handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
176
+
177
+ try {
178
+ route.options.onError?.(err)
179
+ } catch (errorHandlerErr) {
180
+ err = errorHandlerErr
181
+ handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
182
+ }
183
+
184
+ inner.updateMatch(matchId, (prev) => {
185
+ prev._nonReactive.beforeLoadPromise?.resolve()
186
+ prev._nonReactive.beforeLoadPromise = undefined
187
+ prev._nonReactive.loadPromise?.resolve()
188
+
189
+ return {
190
+ ...prev,
191
+ error: err,
192
+ status: 'error',
193
+ isFetching: false,
194
+ updatedAt: Date.now(),
195
+ abortController: new AbortController(),
196
+ }
197
+ })
198
+ }
199
+
200
+ const isBeforeLoadSsr = (
201
+ inner: InnerLoadContext,
202
+ matchId: string,
203
+ index: number,
204
+ route: AnyRoute,
205
+ ): void | Promise<void> => {
206
+ const existingMatch = inner.router.getMatch(matchId)!
207
+ const parentMatchId = inner.matches[index - 1]?.id
208
+ const parentMatch = parentMatchId
209
+ ? inner.router.getMatch(parentMatchId)!
210
+ : undefined
211
+
212
+ // in SPA mode, only SSR the root route
213
+ if (inner.router.isShell()) {
214
+ existingMatch.ssr = matchId === rootRouteId
215
+ return
216
+ }
217
+
218
+ if (parentMatch?.ssr === false) {
219
+ existingMatch.ssr = false
220
+ return
221
+ }
222
+
223
+ const parentOverride = (tempSsr: boolean | 'data-only') => {
224
+ if (tempSsr === true && parentMatch?.ssr === 'data-only') {
225
+ return 'data-only'
226
+ }
227
+ return tempSsr
228
+ }
229
+
230
+ const defaultSsr = inner.router.options.defaultSsr ?? true
231
+
232
+ if (route.options.ssr === undefined) {
233
+ existingMatch.ssr = parentOverride(defaultSsr)
234
+ return
235
+ }
236
+
237
+ if (typeof route.options.ssr !== 'function') {
238
+ existingMatch.ssr = parentOverride(route.options.ssr)
239
+ return
240
+ }
241
+ const { search, params } = existingMatch
242
+
243
+ const ssrFnContext: SsrContextOptions<any, any, any> = {
244
+ search: makeMaybe(search, existingMatch.searchError),
245
+ params: makeMaybe(params, existingMatch.paramsError),
246
+ location: inner.location,
247
+ matches: inner.matches.map((match) => ({
248
+ index: match.index,
249
+ pathname: match.pathname,
250
+ fullPath: match.fullPath,
251
+ staticData: match.staticData,
252
+ id: match.id,
253
+ routeId: match.routeId,
254
+ search: makeMaybe(match.search, match.searchError),
255
+ params: makeMaybe(match.params, match.paramsError),
256
+ ssr: match.ssr,
257
+ })),
258
+ }
259
+
260
+ const tempSsr = route.options.ssr(ssrFnContext)
261
+ if (isPromise(tempSsr)) {
262
+ return tempSsr.then((ssr) => {
263
+ existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
264
+ })
265
+ }
266
+
267
+ existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
268
+ return
269
+ }
270
+
271
+ const setupPendingTimeout = (
272
+ inner: InnerLoadContext,
273
+ matchId: string,
274
+ route: AnyRoute,
275
+ match: AnyRouteMatch,
276
+ ): void => {
277
+ if (match._nonReactive.pendingTimeout !== undefined) return
278
+
279
+ const pendingMs =
280
+ route.options.pendingMs ?? inner.router.options.defaultPendingMs
281
+ const shouldPending = !!(
282
+ inner.onReady &&
283
+ !inner.router.isServer &&
284
+ !resolvePreload(inner, matchId) &&
285
+ (route.options.loader ||
286
+ route.options.beforeLoad ||
287
+ routeNeedsPreload(route)) &&
288
+ typeof pendingMs === 'number' &&
289
+ pendingMs !== Infinity &&
290
+ (route.options.pendingComponent ??
291
+ (inner.router.options as any)?.defaultPendingComponent)
292
+ )
293
+
294
+ if (shouldPending) {
295
+ const pendingTimeout = setTimeout(() => {
296
+ // Update the match and prematurely resolve the loadMatches promise so that
297
+ // the pending component can start rendering
298
+ triggerOnReady(inner)
299
+ }, pendingMs)
300
+ match._nonReactive.pendingTimeout = pendingTimeout
301
+ }
302
+ }
303
+
304
+ const preBeforeLoadSetup = (
305
+ inner: InnerLoadContext,
306
+ matchId: string,
307
+ route: AnyRoute,
308
+ ): void | Promise<void> => {
309
+ const existingMatch = inner.router.getMatch(matchId)!
310
+
311
+ // If we are in the middle of a load, either of these will be present
312
+ // (not to be confused with `loadPromise`, which is always defined)
313
+ if (
314
+ !existingMatch._nonReactive.beforeLoadPromise &&
315
+ !existingMatch._nonReactive.loaderPromise
316
+ )
317
+ return
318
+
319
+ setupPendingTimeout(inner, matchId, route, existingMatch)
320
+
321
+ const then = () => {
322
+ const match = inner.router.getMatch(matchId)!
323
+ if (
324
+ match.preload &&
325
+ (match.status === 'redirected' || match.status === 'notFound')
326
+ ) {
327
+ handleRedirectAndNotFound(inner, match, match.error)
328
+ }
329
+ }
330
+
331
+ // Wait for the previous beforeLoad to resolve before we continue
332
+ return existingMatch._nonReactive.beforeLoadPromise
333
+ ? existingMatch._nonReactive.beforeLoadPromise.then(then)
334
+ : then()
335
+ }
336
+
337
+ const executeBeforeLoad = (
338
+ inner: InnerLoadContext,
339
+ matchId: string,
340
+ index: number,
341
+ route: AnyRoute,
342
+ ): void | Promise<void> => {
343
+ const match = inner.router.getMatch(matchId)!
344
+
345
+ // explicitly capture the previous loadPromise
346
+ const prevLoadPromise = match._nonReactive.loadPromise
347
+ match._nonReactive.loadPromise = createControlledPromise<void>(() => {
348
+ prevLoadPromise?.resolve()
349
+ })
350
+
351
+ const { paramsError, searchError } = match
352
+
353
+ if (paramsError) {
354
+ handleSerialError(inner, index, paramsError, 'PARSE_PARAMS')
355
+ }
356
+
357
+ if (searchError) {
358
+ handleSerialError(inner, index, searchError, 'VALIDATE_SEARCH')
359
+ }
360
+
361
+ setupPendingTimeout(inner, matchId, route, match)
362
+
363
+ const abortController = new AbortController()
364
+
365
+ const parentMatchId = inner.matches[index - 1]?.id
366
+ const parentMatch = parentMatchId
367
+ ? inner.router.getMatch(parentMatchId)!
368
+ : undefined
369
+ const parentMatchContext =
370
+ parentMatch?.context ?? inner.router.options.context ?? undefined
371
+
372
+ const context = { ...parentMatchContext, ...match.__routeContext }
373
+
374
+ let isPending = false
375
+ const pending = () => {
376
+ if (isPending) return
377
+ isPending = true
378
+ inner.updateMatch(matchId, (prev) => ({
379
+ ...prev,
380
+ isFetching: 'beforeLoad',
381
+ fetchCount: prev.fetchCount + 1,
382
+ abortController,
383
+ context,
384
+ }))
385
+ }
386
+
387
+ const resolve = () => {
388
+ match._nonReactive.beforeLoadPromise?.resolve()
389
+ match._nonReactive.beforeLoadPromise = undefined
390
+ inner.updateMatch(matchId, (prev) => ({
391
+ ...prev,
392
+ isFetching: false,
393
+ }))
394
+ }
395
+
396
+ // if there is no `beforeLoad` option, skip everything, batch update the store, return early
397
+ if (!route.options.beforeLoad) {
398
+ batch(() => {
399
+ pending()
400
+ resolve()
401
+ })
402
+ return
403
+ }
404
+
405
+ match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
406
+
407
+ const { search, params, cause } = match
408
+ const preload = resolvePreload(inner, matchId)
409
+ const beforeLoadFnContext: BeforeLoadContextOptions<any, any, any, any, any> =
410
+ {
411
+ search,
412
+ abortController,
413
+ params,
414
+ preload,
415
+ context,
416
+ location: inner.location,
417
+ navigate: (opts: any) =>
418
+ inner.router.navigate({
419
+ ...opts,
420
+ _fromLocation: inner.location,
421
+ }),
422
+ buildLocation: inner.router.buildLocation,
423
+ cause: preload ? 'preload' : cause,
424
+ matches: inner.matches,
425
+ }
426
+
427
+ const updateContext = (beforeLoadContext: any) => {
428
+ if (beforeLoadContext === undefined) {
429
+ batch(() => {
430
+ pending()
431
+ resolve()
432
+ })
433
+ return
434
+ }
435
+ if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
436
+ pending()
437
+ handleSerialError(inner, index, beforeLoadContext, 'BEFORE_LOAD')
438
+ }
439
+
440
+ batch(() => {
441
+ pending()
442
+ inner.updateMatch(matchId, (prev) => ({
443
+ ...prev,
444
+ __beforeLoadContext: beforeLoadContext,
445
+ context: {
446
+ ...prev.context,
447
+ ...beforeLoadContext,
448
+ },
449
+ }))
450
+ resolve()
451
+ })
452
+ }
453
+
454
+ let beforeLoadContext
455
+ try {
456
+ beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
457
+ if (isPromise(beforeLoadContext)) {
458
+ pending()
459
+ return beforeLoadContext
460
+ .catch((err) => {
461
+ handleSerialError(inner, index, err, 'BEFORE_LOAD')
462
+ })
463
+ .then(updateContext)
464
+ }
465
+ } catch (err) {
466
+ pending()
467
+ handleSerialError(inner, index, err, 'BEFORE_LOAD')
468
+ }
469
+
470
+ updateContext(beforeLoadContext)
471
+ return
472
+ }
473
+
474
+ const handleBeforeLoad = (
475
+ inner: InnerLoadContext,
476
+ index: number,
477
+ ): void | Promise<void> => {
478
+ const { id: matchId, routeId } = inner.matches[index]!
479
+ const route = inner.router.looseRoutesById[routeId]!
480
+
481
+ const serverSsr = () => {
482
+ // on the server, determine whether SSR the current match or not
483
+ if (inner.router.isServer) {
484
+ const maybePromise = isBeforeLoadSsr(inner, matchId, index, route)
485
+ if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
486
+ }
487
+ return queueExecution()
488
+ }
489
+
490
+ const queueExecution = () => {
491
+ if (shouldSkipLoader(inner, matchId)) return
492
+ const result = preBeforeLoadSetup(inner, matchId, route)
493
+ return isPromise(result) ? result.then(execute) : execute()
494
+ }
495
+
496
+ const execute = () => executeBeforeLoad(inner, matchId, index, route)
497
+
498
+ return serverSsr()
499
+ }
500
+
501
+ const executeHead = (
502
+ inner: InnerLoadContext,
503
+ matchId: string,
504
+ route: AnyRoute,
505
+ ): void | Promise<
506
+ Pick<
507
+ AnyRouteMatch,
508
+ 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
509
+ >
510
+ > => {
511
+ const match = inner.router.getMatch(matchId)
512
+ // in case of a redirecting match during preload, the match does not exist
513
+ if (!match) {
514
+ return
515
+ }
516
+ if (!route.options.head && !route.options.scripts && !route.options.headers) {
517
+ return
518
+ }
519
+ const assetContext = {
520
+ matches: inner.matches,
521
+ match,
522
+ params: match.params,
523
+ loaderData: match.loaderData,
524
+ }
525
+
526
+ return Promise.all([
527
+ route.options.head?.(assetContext),
528
+ route.options.scripts?.(assetContext),
529
+ route.options.headers?.(assetContext),
530
+ ]).then(([headFnContent, scripts, headers]) => {
531
+ const meta = headFnContent?.meta
532
+ const links = headFnContent?.links
533
+ const headScripts = headFnContent?.scripts
534
+ const styles = headFnContent?.styles
535
+
536
+ return {
537
+ meta,
538
+ links,
539
+ headScripts,
540
+ headers,
541
+ scripts,
542
+ styles,
543
+ }
544
+ })
545
+ }
546
+
547
+ const getLoaderContext = (
548
+ inner: InnerLoadContext,
549
+ matchId: string,
550
+ index: number,
551
+ route: AnyRoute,
552
+ ): LoaderFnContext => {
553
+ const parentMatchPromise = inner.matchPromises[index - 1] as any
554
+ const { params, loaderDeps, abortController, context, cause } =
555
+ inner.router.getMatch(matchId)!
556
+
557
+ const preload = resolvePreload(inner, matchId)
558
+
559
+ return {
560
+ params,
561
+ deps: loaderDeps,
562
+ preload: !!preload,
563
+ parentMatchPromise,
564
+ abortController,
565
+ context,
566
+ location: inner.location,
567
+ navigate: (opts) =>
568
+ inner.router.navigate({
569
+ ...opts,
570
+ _fromLocation: inner.location,
571
+ }),
572
+ cause: preload ? 'preload' : cause,
573
+ route,
574
+ }
575
+ }
576
+
577
+ const runLoader = async (
578
+ inner: InnerLoadContext,
579
+ matchId: string,
580
+ index: number,
581
+ route: AnyRoute,
582
+ ): Promise<void> => {
583
+ try {
584
+ // If the Matches component rendered
585
+ // the pending component and needs to show it for
586
+ // a minimum duration, we''ll wait for it to resolve
587
+ // before committing to the match and resolving
588
+ // the loadPromise
589
+
590
+ const match = inner.router.getMatch(matchId)!
591
+
592
+ // Actually run the loader and handle the result
593
+ try {
594
+ if (!inner.router.isServer || match.ssr === true) {
595
+ loadRouteChunk(route)
596
+ }
597
+
598
+ // Kick off the loader!
599
+ const loaderResult = route.options.loader?.(
600
+ getLoaderContext(inner, matchId, index, route),
601
+ )
602
+ const loaderResultIsPromise =
603
+ route.options.loader && isPromise(loaderResult)
604
+
605
+ const willLoadSomething = !!(
606
+ loaderResultIsPromise ||
607
+ route._lazyPromise ||
608
+ route._componentsPromise ||
609
+ route.options.head ||
610
+ route.options.scripts ||
611
+ route.options.headers ||
612
+ match._nonReactive.minPendingPromise
613
+ )
614
+
615
+ if (willLoadSomething) {
616
+ inner.updateMatch(matchId, (prev) => ({
617
+ ...prev,
618
+ isFetching: 'loader',
619
+ }))
620
+ }
621
+
622
+ if (route.options.loader) {
623
+ const loaderData = loaderResultIsPromise
624
+ ? await loaderResult
625
+ : loaderResult
626
+
627
+ handleRedirectAndNotFound(
628
+ inner,
629
+ inner.router.getMatch(matchId),
630
+ loaderData,
631
+ )
632
+ if (loaderData !== undefined) {
633
+ inner.updateMatch(matchId, (prev) => ({
634
+ ...prev,
635
+ loaderData,
636
+ }))
637
+ }
638
+ }
639
+
640
+ // Lazy option can modify the route options,
641
+ // so we need to wait for it to resolve before
642
+ // we can use the options
643
+ if (route._lazyPromise) await route._lazyPromise
644
+ const headResult = executeHead(inner, matchId, route)
645
+ const head = headResult ? await headResult : undefined
646
+ const pendingPromise = match._nonReactive.minPendingPromise
647
+ if (pendingPromise) await pendingPromise
648
+
649
+ // Last but not least, wait for the the components
650
+ // to be preloaded before we resolve the match
651
+ if (route._componentsPromise) await route._componentsPromise
652
+ inner.updateMatch(matchId, (prev) => ({
653
+ ...prev,
654
+ error: undefined,
655
+ status: 'success',
656
+ isFetching: false,
657
+ updatedAt: Date.now(),
658
+ ...head,
659
+ }))
660
+ } catch (e) {
661
+ let error = e
662
+
663
+ const pendingPromise = match._nonReactive.minPendingPromise
664
+ if (pendingPromise) await pendingPromise
665
+
666
+ handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), e)
667
+
668
+ try {
669
+ route.options.onError?.(e)
670
+ } catch (onErrorError) {
671
+ error = onErrorError
672
+ handleRedirectAndNotFound(
673
+ inner,
674
+ inner.router.getMatch(matchId),
675
+ onErrorError,
676
+ )
677
+ }
678
+ const headResult = executeHead(inner, matchId, route)
679
+ const head = headResult ? await headResult : undefined
680
+ inner.updateMatch(matchId, (prev) => ({
681
+ ...prev,
682
+ error,
683
+ status: 'error',
684
+ isFetching: false,
685
+ ...head,
686
+ }))
687
+ }
688
+ } catch (err) {
689
+ const match = inner.router.getMatch(matchId)
690
+ // in case of a redirecting match during preload, the match does not exist
691
+ if (match) {
692
+ const headResult = executeHead(inner, matchId, route)
693
+ if (headResult) {
694
+ const head = await headResult
695
+ inner.updateMatch(matchId, (prev) => ({
696
+ ...prev,
697
+ ...head,
698
+ }))
699
+ }
700
+ match._nonReactive.loaderPromise = undefined
701
+ }
702
+ handleRedirectAndNotFound(inner, match, err)
703
+ }
704
+ }
705
+
706
+ const loadRouteMatch = async (
707
+ inner: InnerLoadContext,
708
+ index: number,
709
+ ): Promise<AnyRouteMatch> => {
710
+ const { id: matchId, routeId } = inner.matches[index]!
711
+ let loaderShouldRunAsync = false
712
+ let loaderIsRunningAsync = false
713
+ const route = inner.router.looseRoutesById[routeId]!
714
+
715
+ if (shouldSkipLoader(inner, matchId)) {
716
+ if (inner.router.isServer) {
717
+ const headResult = executeHead(inner, matchId, route)
718
+ if (headResult) {
719
+ const head = await headResult
720
+ inner.updateMatch(matchId, (prev) => ({
721
+ ...prev,
722
+ ...head,
723
+ }))
724
+ }
725
+ return inner.router.getMatch(matchId)!
726
+ }
727
+ } else {
728
+ const prevMatch = inner.router.getMatch(matchId)!
729
+ // there is a loaderPromise, so we are in the middle of a load
730
+ if (prevMatch._nonReactive.loaderPromise) {
731
+ // do not block if we already have stale data we can show
732
+ // but only if the ongoing load is not a preload since error handling is different for preloads
733
+ // and we don't want to swallow errors
734
+ if (prevMatch.status === 'success' && !inner.sync && !prevMatch.preload) {
735
+ return prevMatch
736
+ }
737
+ await prevMatch._nonReactive.loaderPromise
738
+ const match = inner.router.getMatch(matchId)!
739
+ if (match.error) {
740
+ handleRedirectAndNotFound(inner, match, match.error)
741
+ }
742
+ } else {
743
+ // This is where all of the stale-while-revalidate magic happens
744
+ const age = Date.now() - prevMatch.updatedAt
745
+
746
+ const preload = resolvePreload(inner, matchId)
747
+
748
+ const staleAge = preload
749
+ ? (route.options.preloadStaleTime ??
750
+ inner.router.options.defaultPreloadStaleTime ??
751
+ 30_000) // 30 seconds for preloads by default
752
+ : (route.options.staleTime ??
753
+ inner.router.options.defaultStaleTime ??
754
+ 0)
755
+
756
+ const shouldReloadOption = route.options.shouldReload
757
+
758
+ // Default to reloading the route all the time
759
+ // Allow shouldReload to get the last say,
760
+ // if provided.
761
+ const shouldReload =
762
+ typeof shouldReloadOption === 'function'
763
+ ? shouldReloadOption(getLoaderContext(inner, matchId, index, route))
764
+ : shouldReloadOption
765
+
766
+ const nextPreload =
767
+ !!preload && !inner.router.state.matches.some((d) => d.id === matchId)
768
+ const match = inner.router.getMatch(matchId)!
769
+ match._nonReactive.loaderPromise = createControlledPromise<void>()
770
+ if (nextPreload !== match.preload) {
771
+ inner.updateMatch(matchId, (prev) => ({
772
+ ...prev,
773
+ preload: nextPreload,
774
+ }))
775
+ }
776
+
777
+ // If the route is successful and still fresh, just resolve
778
+ const { status, invalid } = match
779
+ loaderShouldRunAsync =
780
+ status === 'success' && (invalid || (shouldReload ?? age > staleAge))
781
+ if (preload && route.options.preload === false) {
782
+ // Do nothing
783
+ } else if (loaderShouldRunAsync && !inner.sync) {
784
+ loaderIsRunningAsync = true
785
+ ;(async () => {
786
+ try {
787
+ await runLoader(inner, matchId, index, route)
788
+ const match = inner.router.getMatch(matchId)!
789
+ match._nonReactive.loaderPromise?.resolve()
790
+ match._nonReactive.loadPromise?.resolve()
791
+ match._nonReactive.loaderPromise = undefined
792
+ } catch (err) {
793
+ if (isRedirect(err)) {
794
+ await inner.router.navigate(err.options)
795
+ }
796
+ }
797
+ })()
798
+ } else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
799
+ await runLoader(inner, matchId, index, route)
800
+ } else {
801
+ // if the loader did not run, still update head.
802
+ // reason: parent's beforeLoad may have changed the route context
803
+ // and only now do we know the route context (and that the loader would not run)
804
+ const headResult = executeHead(inner, matchId, route)
805
+ if (headResult) {
806
+ const head = await headResult
807
+ inner.updateMatch(matchId, (prev) => ({
808
+ ...prev,
809
+ ...head,
810
+ }))
811
+ }
812
+ }
813
+ }
814
+ }
815
+ const match = inner.router.getMatch(matchId)!
816
+ if (!loaderIsRunningAsync) {
817
+ match._nonReactive.loaderPromise?.resolve()
818
+ match._nonReactive.loadPromise?.resolve()
819
+ }
820
+
821
+ clearTimeout(match._nonReactive.pendingTimeout)
822
+ match._nonReactive.pendingTimeout = undefined
823
+ if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
824
+ match._nonReactive.dehydrated = undefined
825
+ const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
826
+ if (nextIsFetching !== match.isFetching || match.invalid !== false) {
827
+ inner.updateMatch(matchId, (prev) => ({
828
+ ...prev,
829
+ isFetching: nextIsFetching,
830
+ invalid: false,
831
+ }))
832
+ return inner.router.getMatch(matchId)!
833
+ } else {
834
+ return match
835
+ }
836
+ }
837
+
838
+ export async function loadMatches(arg: {
839
+ router: AnyRouter
840
+ location: ParsedLocation
841
+ matches: Array<AnyRouteMatch>
842
+ preload?: boolean
843
+ onReady?: () => Promise<void>
844
+ updateMatch: UpdateMatchFn
845
+ sync?: boolean
846
+ }): Promise<Array<MakeRouteMatch>> {
847
+ const inner: InnerLoadContext = Object.assign(arg, {
848
+ matchPromises: [],
849
+ })
850
+
851
+ // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
852
+ // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
853
+ if (
854
+ !inner.router.isServer &&
855
+ inner.router.state.matches.some((d) => d._forcePending)
856
+ ) {
857
+ triggerOnReady(inner)
858
+ }
859
+
860
+ try {
861
+ // Execute all beforeLoads one by one
862
+ for (let i = 0; i < inner.matches.length; i++) {
863
+ const beforeLoad = handleBeforeLoad(inner, i)
864
+ if (isPromise(beforeLoad)) await beforeLoad
865
+ }
866
+
867
+ // Execute all loaders in parallel
868
+ const max = inner.firstBadMatchIndex ?? inner.matches.length
869
+ for (let i = 0; i < max; i++) {
870
+ inner.matchPromises.push(loadRouteMatch(inner, i))
871
+ }
872
+ await Promise.all(inner.matchPromises)
873
+
874
+ const readyPromise = triggerOnReady(inner)
875
+ if (isPromise(readyPromise)) await readyPromise
876
+ } catch (err) {
877
+ if (isNotFound(err) && !inner.preload) {
878
+ const readyPromise = triggerOnReady(inner)
879
+ if (isPromise(readyPromise)) await readyPromise
880
+ throw err
881
+ }
882
+ if (isRedirect(err)) {
883
+ throw err
884
+ }
885
+ }
886
+
887
+ return inner.matches
888
+ }
889
+
890
+ export async function loadRouteChunk(route: AnyRoute) {
891
+ if (!route._lazyLoaded && route._lazyPromise === undefined) {
892
+ if (route.lazyFn) {
893
+ route._lazyPromise = route.lazyFn().then((lazyRoute) => {
894
+ // explicitly don't copy over the lazy route's id
895
+ const { id: _id, ...options } = lazyRoute.options
896
+ Object.assign(route.options, options)
897
+ route._lazyLoaded = true
898
+ route._lazyPromise = undefined // gc promise, we won't need it anymore
899
+ })
900
+ } else {
901
+ route._lazyLoaded = true
902
+ }
903
+ }
904
+
905
+ // If for some reason lazy resolves more lazy components...
906
+ // We'll wait for that before we attempt to preload the
907
+ // components themselves.
908
+ if (!route._componentsLoaded && route._componentsPromise === undefined) {
909
+ const loadComponents = () => {
910
+ const preloads = []
911
+ for (const type of componentTypes) {
912
+ const preload = (route.options[type] as any)?.preload
913
+ if (preload) preloads.push(preload())
914
+ }
915
+ if (preloads.length)
916
+ return Promise.all(preloads).then(() => {
917
+ route._componentsLoaded = true
918
+ route._componentsPromise = undefined // gc promise, we won't need it anymore
919
+ })
920
+ route._componentsLoaded = true
921
+ route._componentsPromise = undefined // gc promise, we won't need it anymore
922
+ return
923
+ }
924
+ route._componentsPromise = route._lazyPromise
925
+ ? route._lazyPromise.then(loadComponents)
926
+ : loadComponents()
927
+ }
928
+ return route._componentsPromise
929
+ }
930
+
931
+ function makeMaybe<TValue, TError>(
932
+ value: TValue,
933
+ error: TError,
934
+ ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
935
+ if (error) {
936
+ return { status: 'error' as const, error }
937
+ }
938
+ return { status: 'success' as const, value }
939
+ }
940
+
941
+ export function routeNeedsPreload(route: AnyRoute) {
942
+ for (const componentType of componentTypes) {
943
+ if ((route.options[componentType] as any)?.preload) {
944
+ return true
945
+ }
946
+ }
947
+ return false
948
+ }
949
+
950
+ export const componentTypes = [
951
+ 'component',
952
+ 'errorComponent',
953
+ 'pendingComponent',
954
+ 'notFoundComponent',
955
+ ] as const