@tanstack/router-core 1.131.19 → 1.131.22

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.
package/src/router.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  createControlledPromise,
10
10
  deepEqual,
11
11
  functionalUpdate,
12
- isPromise,
13
12
  last,
14
13
  pick,
15
14
  replaceEqualDeep,
@@ -35,6 +34,7 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
35
34
  import { rootRouteId } from './root'
36
35
  import { isRedirect, redirect } from './redirect'
37
36
  import { createLRUCache } from './lru-cache'
37
+ import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches'
38
38
  import type { ParsePathnameCache, Segment } from './path'
39
39
  import type { SearchParser, SearchSerializer } from './searchParams'
40
40
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
@@ -57,13 +57,10 @@ import type {
57
57
  AnyContext,
58
58
  AnyRoute,
59
59
  AnyRouteWithContext,
60
- BeforeLoadContextOptions,
61
- LoaderFnContext,
62
60
  MakeRemountDepsOptionsUnion,
63
61
  RouteContextOptions,
64
62
  RouteMask,
65
63
  SearchMiddleware,
66
- SsrContextOptions,
67
64
  } from './route'
68
65
  import type {
69
66
  FullSearchSchema,
@@ -763,18 +760,6 @@ export type CreateRouterFn = <
763
760
  TDehydrated
764
761
  >
765
762
 
766
- type InnerLoadContext = {
767
- location: ParsedLocation
768
- firstBadMatchIndex?: number
769
- rendered?: boolean
770
- updateMatch: UpdateMatchFn
771
- matches: Array<AnyRouteMatch>
772
- preload?: boolean
773
- onReady?: () => Promise<void>
774
- sync?: boolean
775
- matchPromises: Array<Promise<AnyRouteMatch>>
776
- }
777
-
778
763
  export class RouterCore<
779
764
  in out TRouteTree extends AnyRoute,
780
765
  in out TTrailingSlashOption extends TrailingSlashOption,
@@ -1902,10 +1887,12 @@ export class RouterCore<
1902
1887
  }),
1903
1888
  })
1904
1889
 
1905
- await this.loadMatches({
1890
+ await loadMatches({
1891
+ router: this,
1906
1892
  sync: opts?.sync,
1907
1893
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1908
1894
  location: next,
1895
+ updateMatch: this.updateMatch,
1909
1896
  // eslint-disable-next-line @typescript-eslint/require-await
1910
1897
  onReady: async () => {
1911
1898
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -2095,873 +2082,6 @@ export class RouterCore<
2095
2082
  )
2096
2083
  }
2097
2084
 
2098
- private triggerOnReady = (
2099
- innerLoadContext: InnerLoadContext,
2100
- ): void | Promise<void> => {
2101
- if (!innerLoadContext.rendered) {
2102
- innerLoadContext.rendered = true
2103
- return innerLoadContext.onReady?.()
2104
- }
2105
- }
2106
-
2107
- private resolvePreload = (
2108
- innerLoadContext: InnerLoadContext,
2109
- matchId: string,
2110
- ): boolean => {
2111
- return !!(
2112
- innerLoadContext.preload &&
2113
- !this.state.matches.some((d) => d.id === matchId)
2114
- )
2115
- }
2116
-
2117
- private handleRedirectAndNotFound = (
2118
- innerLoadContext: InnerLoadContext,
2119
- match: AnyRouteMatch | undefined,
2120
- err: unknown,
2121
- ): void => {
2122
- if (!isRedirect(err) && !isNotFound(err)) return
2123
-
2124
- if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
2125
- throw err
2126
- }
2127
-
2128
- // in case of a redirecting match during preload, the match does not exist
2129
- if (match) {
2130
- match._nonReactive.beforeLoadPromise?.resolve()
2131
- match._nonReactive.loaderPromise?.resolve()
2132
- match._nonReactive.beforeLoadPromise = undefined
2133
- match._nonReactive.loaderPromise = undefined
2134
-
2135
- const status = isRedirect(err) ? 'redirected' : 'notFound'
2136
-
2137
- innerLoadContext.updateMatch(match.id, (prev) => ({
2138
- ...prev,
2139
- status,
2140
- isFetching: false,
2141
- error: err,
2142
- }))
2143
-
2144
- if (isNotFound(err) && !err.routeId) {
2145
- err.routeId = match.routeId
2146
- }
2147
-
2148
- match._nonReactive.loadPromise?.resolve()
2149
- }
2150
-
2151
- if (isRedirect(err)) {
2152
- innerLoadContext.rendered = true
2153
- err.options._fromLocation = innerLoadContext.location
2154
- err.redirectHandled = true
2155
- err = this.resolveRedirect(err)
2156
- throw err
2157
- } else {
2158
- this._handleNotFound(innerLoadContext, err)
2159
- throw err
2160
- }
2161
- }
2162
-
2163
- private shouldSkipLoader = (matchId: string): boolean => {
2164
- const match = this.getMatch(matchId)!
2165
- // upon hydration, we skip the loader if the match has been dehydrated on the server
2166
- if (!this.isServer && match._nonReactive.dehydrated) {
2167
- return true
2168
- }
2169
-
2170
- if (this.isServer) {
2171
- if (match.ssr === false) {
2172
- return true
2173
- }
2174
- }
2175
- return false
2176
- }
2177
-
2178
- private handleSerialError = (
2179
- innerLoadContext: InnerLoadContext,
2180
- index: number,
2181
- err: any,
2182
- routerCode: string,
2183
- ): void => {
2184
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2185
- const route = this.looseRoutesById[routeId]!
2186
-
2187
- // Much like suspense, we use a promise here to know if
2188
- // we've been outdated by a new loadMatches call and
2189
- // should abort the current async operation
2190
- if (err instanceof Promise) {
2191
- throw err
2192
- }
2193
-
2194
- err.routerCode = routerCode
2195
- innerLoadContext.firstBadMatchIndex ??= index
2196
- this.handleRedirectAndNotFound(
2197
- innerLoadContext,
2198
- this.getMatch(matchId),
2199
- err,
2200
- )
2201
-
2202
- try {
2203
- route.options.onError?.(err)
2204
- } catch (errorHandlerErr) {
2205
- err = errorHandlerErr
2206
- this.handleRedirectAndNotFound(
2207
- innerLoadContext,
2208
- this.getMatch(matchId),
2209
- err,
2210
- )
2211
- }
2212
-
2213
- innerLoadContext.updateMatch(matchId, (prev) => {
2214
- prev._nonReactive.beforeLoadPromise?.resolve()
2215
- prev._nonReactive.beforeLoadPromise = undefined
2216
- prev._nonReactive.loadPromise?.resolve()
2217
-
2218
- return {
2219
- ...prev,
2220
- error: err,
2221
- status: 'error',
2222
- isFetching: false,
2223
- updatedAt: Date.now(),
2224
- abortController: new AbortController(),
2225
- }
2226
- })
2227
- }
2228
-
2229
- private isBeforeLoadSsr = (
2230
- innerLoadContext: InnerLoadContext,
2231
- matchId: string,
2232
- index: number,
2233
- route: AnyRoute,
2234
- ): void | Promise<void> => {
2235
- const existingMatch = this.getMatch(matchId)!
2236
- const parentMatchId = innerLoadContext.matches[index - 1]?.id
2237
- const parentMatch = parentMatchId
2238
- ? this.getMatch(parentMatchId)!
2239
- : undefined
2240
-
2241
- // in SPA mode, only SSR the root route
2242
- if (this.isShell()) {
2243
- existingMatch.ssr = matchId === rootRouteId
2244
- return
2245
- }
2246
-
2247
- if (parentMatch?.ssr === false) {
2248
- existingMatch.ssr = false
2249
- return
2250
- }
2251
-
2252
- const parentOverride = (tempSsr: boolean | 'data-only') => {
2253
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2254
- return 'data-only'
2255
- }
2256
- return tempSsr
2257
- }
2258
-
2259
- const defaultSsr = this.options.defaultSsr ?? true
2260
-
2261
- if (route.options.ssr === undefined) {
2262
- existingMatch.ssr = parentOverride(defaultSsr)
2263
- return
2264
- }
2265
-
2266
- if (typeof route.options.ssr !== 'function') {
2267
- existingMatch.ssr = parentOverride(route.options.ssr)
2268
- return
2269
- }
2270
- const { search, params } = this.getMatch(matchId)!
2271
-
2272
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2273
- search: makeMaybe(search, existingMatch.searchError),
2274
- params: makeMaybe(params, existingMatch.paramsError),
2275
- location: innerLoadContext.location,
2276
- matches: innerLoadContext.matches.map((match) => ({
2277
- index: match.index,
2278
- pathname: match.pathname,
2279
- fullPath: match.fullPath,
2280
- staticData: match.staticData,
2281
- id: match.id,
2282
- routeId: match.routeId,
2283
- search: makeMaybe(match.search, match.searchError),
2284
- params: makeMaybe(match.params, match.paramsError),
2285
- ssr: match.ssr,
2286
- })),
2287
- }
2288
-
2289
- const tempSsr = route.options.ssr(ssrFnContext)
2290
- if (isPromise(tempSsr)) {
2291
- return tempSsr.then((ssr) => {
2292
- existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
2293
- })
2294
- }
2295
-
2296
- existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
2297
- return
2298
- }
2299
-
2300
- private setupPendingTimeout = (
2301
- innerLoadContext: InnerLoadContext,
2302
- matchId: string,
2303
- route: AnyRoute,
2304
- ): void => {
2305
- const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs
2306
- const shouldPending = !!(
2307
- innerLoadContext.onReady &&
2308
- !this.isServer &&
2309
- !this.resolvePreload(innerLoadContext, matchId) &&
2310
- (route.options.loader ||
2311
- route.options.beforeLoad ||
2312
- routeNeedsPreload(route)) &&
2313
- typeof pendingMs === 'number' &&
2314
- pendingMs !== Infinity &&
2315
- (route.options.pendingComponent ??
2316
- (this.options as any)?.defaultPendingComponent)
2317
- )
2318
- const match = this.getMatch(matchId)!
2319
- if (shouldPending && match._nonReactive.pendingTimeout === undefined) {
2320
- const pendingTimeout = setTimeout(() => {
2321
- // Update the match and prematurely resolve the loadMatches promise so that
2322
- // the pending component can start rendering
2323
- this.triggerOnReady(innerLoadContext)
2324
- }, pendingMs)
2325
- match._nonReactive.pendingTimeout = pendingTimeout
2326
- }
2327
- }
2328
-
2329
- private shouldExecuteBeforeLoad = (
2330
- innerLoadContext: InnerLoadContext,
2331
- matchId: string,
2332
- route: AnyRoute,
2333
- ): boolean | Promise<boolean> => {
2334
- const existingMatch = this.getMatch(matchId)!
2335
-
2336
- // If we are in the middle of a load, either of these will be present
2337
- // (not to be confused with `loadPromise`, which is always defined)
2338
- if (
2339
- !existingMatch._nonReactive.beforeLoadPromise &&
2340
- !existingMatch._nonReactive.loaderPromise
2341
- )
2342
- return true
2343
-
2344
- this.setupPendingTimeout(innerLoadContext, matchId, route)
2345
-
2346
- const then = () => {
2347
- let shouldExecuteBeforeLoad = true
2348
- const match = this.getMatch(matchId)!
2349
- if (match.status === 'error') {
2350
- shouldExecuteBeforeLoad = true
2351
- } else if (
2352
- match.preload &&
2353
- (match.status === 'redirected' || match.status === 'notFound')
2354
- ) {
2355
- this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2356
- }
2357
- return shouldExecuteBeforeLoad
2358
- }
2359
-
2360
- // Wait for the beforeLoad to resolve before we continue
2361
- return existingMatch._nonReactive.beforeLoadPromise
2362
- ? existingMatch._nonReactive.beforeLoadPromise.then(then)
2363
- : then()
2364
- }
2365
-
2366
- private executeBeforeLoad = (
2367
- innerLoadContext: InnerLoadContext,
2368
- matchId: string,
2369
- index: number,
2370
- route: AnyRoute,
2371
- ): void | Promise<void> => {
2372
- const match = this.getMatch(matchId)!
2373
-
2374
- match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
2375
- // explicitly capture the previous loadPromise
2376
- const prevLoadPromise = match._nonReactive.loadPromise
2377
- match._nonReactive.loadPromise = createControlledPromise<void>(() => {
2378
- prevLoadPromise?.resolve()
2379
- })
2380
-
2381
- const { paramsError, searchError } = match
2382
-
2383
- if (paramsError) {
2384
- this.handleSerialError(
2385
- innerLoadContext,
2386
- index,
2387
- paramsError,
2388
- 'PARSE_PARAMS',
2389
- )
2390
- }
2391
-
2392
- if (searchError) {
2393
- this.handleSerialError(
2394
- innerLoadContext,
2395
- index,
2396
- searchError,
2397
- 'VALIDATE_SEARCH',
2398
- )
2399
- }
2400
-
2401
- this.setupPendingTimeout(innerLoadContext, matchId, route)
2402
-
2403
- const abortController = new AbortController()
2404
-
2405
- const parentMatchId = innerLoadContext.matches[index - 1]?.id
2406
- const parentMatch = parentMatchId
2407
- ? this.getMatch(parentMatchId)!
2408
- : undefined
2409
- const parentMatchContext =
2410
- parentMatch?.context ?? this.options.context ?? undefined
2411
-
2412
- const context = { ...parentMatchContext, ...match.__routeContext }
2413
-
2414
- let isPending = false
2415
- const pending = () => {
2416
- if (isPending) return
2417
- isPending = true
2418
- innerLoadContext.updateMatch(matchId, (prev) => ({
2419
- ...prev,
2420
- isFetching: 'beforeLoad',
2421
- fetchCount: prev.fetchCount + 1,
2422
- abortController,
2423
- context,
2424
- }))
2425
- }
2426
-
2427
- const resolve = () => {
2428
- match._nonReactive.beforeLoadPromise?.resolve()
2429
- match._nonReactive.beforeLoadPromise = undefined
2430
- innerLoadContext.updateMatch(matchId, (prev) => ({
2431
- ...prev,
2432
- isFetching: false,
2433
- }))
2434
- }
2435
-
2436
- // if there is no `beforeLoad` option, skip everything, batch update the store, return early
2437
- if (!route.options.beforeLoad) {
2438
- batch(() => {
2439
- pending()
2440
- resolve()
2441
- })
2442
- return
2443
- }
2444
-
2445
- const { search, params, cause } = match
2446
- const preload = this.resolvePreload(innerLoadContext, matchId)
2447
- const beforeLoadFnContext: BeforeLoadContextOptions<
2448
- any,
2449
- any,
2450
- any,
2451
- any,
2452
- any
2453
- > = {
2454
- search,
2455
- abortController,
2456
- params,
2457
- preload,
2458
- context,
2459
- location: innerLoadContext.location,
2460
- navigate: (opts: any) =>
2461
- this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2462
- buildLocation: this.buildLocation,
2463
- cause: preload ? 'preload' : cause,
2464
- matches: innerLoadContext.matches,
2465
- }
2466
-
2467
- const updateContext = (beforeLoadContext: any) => {
2468
- if (beforeLoadContext === undefined) {
2469
- batch(() => {
2470
- pending()
2471
- resolve()
2472
- })
2473
- return
2474
- }
2475
- if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
2476
- pending()
2477
- this.handleSerialError(
2478
- innerLoadContext,
2479
- index,
2480
- beforeLoadContext,
2481
- 'BEFORE_LOAD',
2482
- )
2483
- }
2484
-
2485
- batch(() => {
2486
- pending()
2487
- innerLoadContext.updateMatch(matchId, (prev) => ({
2488
- ...prev,
2489
- __beforeLoadContext: beforeLoadContext,
2490
- context: {
2491
- ...prev.context,
2492
- ...beforeLoadContext,
2493
- },
2494
- }))
2495
- resolve()
2496
- })
2497
- }
2498
-
2499
- let beforeLoadContext
2500
- try {
2501
- beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
2502
- if (isPromise(beforeLoadContext)) {
2503
- pending()
2504
- return beforeLoadContext
2505
- .catch((err) => {
2506
- this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2507
- })
2508
- .then(updateContext)
2509
- }
2510
- } catch (err) {
2511
- pending()
2512
- this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2513
- }
2514
-
2515
- updateContext(beforeLoadContext)
2516
- return
2517
- }
2518
-
2519
- private handleBeforeLoad = (
2520
- innerLoadContext: InnerLoadContext,
2521
- index: number,
2522
- ): void | Promise<void> => {
2523
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2524
- const route = this.looseRoutesById[routeId]!
2525
-
2526
- const serverSsr = () => {
2527
- // on the server, determine whether SSR the current match or not
2528
- if (this.isServer) {
2529
- const maybePromise = this.isBeforeLoadSsr(
2530
- innerLoadContext,
2531
- matchId,
2532
- index,
2533
- route,
2534
- )
2535
- if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
2536
- }
2537
- return queueExecution()
2538
- }
2539
-
2540
- const queueExecution = () => {
2541
- if (this.shouldSkipLoader(matchId)) return
2542
- const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
2543
- innerLoadContext,
2544
- matchId,
2545
- route,
2546
- )
2547
- return isPromise(shouldExecuteBeforeLoadResult)
2548
- ? shouldExecuteBeforeLoadResult.then(execute)
2549
- : execute(shouldExecuteBeforeLoadResult)
2550
- }
2551
-
2552
- const execute = (shouldExecuteBeforeLoad: boolean) => {
2553
- if (shouldExecuteBeforeLoad) {
2554
- // If we are not in the middle of a load OR the previous load failed, start it
2555
- return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
2556
- }
2557
- return
2558
- }
2559
-
2560
- return serverSsr()
2561
- }
2562
-
2563
- private executeHead = (
2564
- innerLoadContext: InnerLoadContext,
2565
- matchId: string,
2566
- route: AnyRoute,
2567
- ): void | Promise<
2568
- Pick<
2569
- AnyRouteMatch,
2570
- 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
2571
- >
2572
- > => {
2573
- const match = this.getMatch(matchId)
2574
- // in case of a redirecting match during preload, the match does not exist
2575
- if (!match) {
2576
- return
2577
- }
2578
- if (
2579
- !route.options.head &&
2580
- !route.options.scripts &&
2581
- !route.options.headers
2582
- ) {
2583
- return
2584
- }
2585
- const assetContext = {
2586
- matches: innerLoadContext.matches,
2587
- match,
2588
- params: match.params,
2589
- loaderData: match.loaderData,
2590
- }
2591
-
2592
- return Promise.all([
2593
- route.options.head?.(assetContext),
2594
- route.options.scripts?.(assetContext),
2595
- route.options.headers?.(assetContext),
2596
- ]).then(([headFnContent, scripts, headers]) => {
2597
- const meta = headFnContent?.meta
2598
- const links = headFnContent?.links
2599
- const headScripts = headFnContent?.scripts
2600
- const styles = headFnContent?.styles
2601
-
2602
- return {
2603
- meta,
2604
- links,
2605
- headScripts,
2606
- headers,
2607
- scripts,
2608
- styles,
2609
- }
2610
- })
2611
- }
2612
-
2613
- private potentialPendingMinPromise = (
2614
- matchId: string,
2615
- ): void | ControlledPromise<void> => {
2616
- const latestMatch = this.getMatch(matchId)!
2617
- return latestMatch._nonReactive.minPendingPromise
2618
- }
2619
-
2620
- private getLoaderContext = (
2621
- innerLoadContext: InnerLoadContext,
2622
- matchId: string,
2623
- index: number,
2624
- route: AnyRoute,
2625
- ): LoaderFnContext => {
2626
- const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
2627
- const { params, loaderDeps, abortController, context, cause } =
2628
- this.getMatch(matchId)!
2629
-
2630
- const preload = this.resolvePreload(innerLoadContext, matchId)
2631
-
2632
- return {
2633
- params,
2634
- deps: loaderDeps,
2635
- preload: !!preload,
2636
- parentMatchPromise,
2637
- abortController: abortController,
2638
- context,
2639
- location: innerLoadContext.location,
2640
- navigate: (opts) =>
2641
- this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2642
- cause: preload ? 'preload' : cause,
2643
- route,
2644
- }
2645
- }
2646
-
2647
- private runLoader = async (
2648
- innerLoadContext: InnerLoadContext,
2649
- matchId: string,
2650
- index: number,
2651
- route: AnyRoute,
2652
- ): Promise<void> => {
2653
- try {
2654
- // If the Matches component rendered
2655
- // the pending component and needs to show it for
2656
- // a minimum duration, we''ll wait for it to resolve
2657
- // before committing to the match and resolving
2658
- // the loadPromise
2659
-
2660
- // Actually run the loader and handle the result
2661
- try {
2662
- if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
2663
- this.loadRouteChunk(route)
2664
- }
2665
-
2666
- // Kick off the loader!
2667
- const loaderResult = route.options.loader?.(
2668
- this.getLoaderContext(innerLoadContext, matchId, index, route),
2669
- )
2670
- const loaderResultIsPromise =
2671
- route.options.loader && isPromise(loaderResult)
2672
-
2673
- const willLoadSomething = !!(
2674
- loaderResultIsPromise ||
2675
- route._lazyPromise ||
2676
- route._componentsPromise ||
2677
- route.options.head ||
2678
- route.options.scripts ||
2679
- route.options.headers ||
2680
- this.getMatch(matchId)!._nonReactive.minPendingPromise
2681
- )
2682
-
2683
- if (willLoadSomething) {
2684
- innerLoadContext.updateMatch(matchId, (prev) => ({
2685
- ...prev,
2686
- isFetching: 'loader',
2687
- }))
2688
- }
2689
-
2690
- if (route.options.loader) {
2691
- const loaderData = loaderResultIsPromise
2692
- ? await loaderResult
2693
- : loaderResult
2694
-
2695
- this.handleRedirectAndNotFound(
2696
- innerLoadContext,
2697
- this.getMatch(matchId),
2698
- loaderData,
2699
- )
2700
- if (loaderData !== undefined) {
2701
- innerLoadContext.updateMatch(matchId, (prev) => ({
2702
- ...prev,
2703
- loaderData,
2704
- }))
2705
- }
2706
- }
2707
-
2708
- // Lazy option can modify the route options,
2709
- // so we need to wait for it to resolve before
2710
- // we can use the options
2711
- if (route._lazyPromise) await route._lazyPromise
2712
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2713
- const head = headResult ? await headResult : undefined
2714
- const pendingPromise = this.potentialPendingMinPromise(matchId)
2715
- if (pendingPromise) await pendingPromise
2716
-
2717
- // Last but not least, wait for the the components
2718
- // to be preloaded before we resolve the match
2719
- if (route._componentsPromise) await route._componentsPromise
2720
- innerLoadContext.updateMatch(matchId, (prev) => ({
2721
- ...prev,
2722
- error: undefined,
2723
- status: 'success',
2724
- isFetching: false,
2725
- updatedAt: Date.now(),
2726
- ...head,
2727
- }))
2728
- } catch (e) {
2729
- let error = e
2730
-
2731
- const pendingPromise = this.potentialPendingMinPromise(matchId)
2732
- if (pendingPromise) await pendingPromise
2733
-
2734
- this.handleRedirectAndNotFound(
2735
- innerLoadContext,
2736
- this.getMatch(matchId),
2737
- e,
2738
- )
2739
-
2740
- try {
2741
- route.options.onError?.(e)
2742
- } catch (onErrorError) {
2743
- error = onErrorError
2744
- this.handleRedirectAndNotFound(
2745
- innerLoadContext,
2746
- this.getMatch(matchId),
2747
- onErrorError,
2748
- )
2749
- }
2750
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2751
- const head = headResult ? await headResult : undefined
2752
- innerLoadContext.updateMatch(matchId, (prev) => ({
2753
- ...prev,
2754
- error,
2755
- status: 'error',
2756
- isFetching: false,
2757
- ...head,
2758
- }))
2759
- }
2760
- } catch (err) {
2761
- const match = this.getMatch(matchId)
2762
- // in case of a redirecting match during preload, the match does not exist
2763
- if (match) {
2764
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2765
- if (headResult) {
2766
- const head = await headResult
2767
- innerLoadContext.updateMatch(matchId, (prev) => ({
2768
- ...prev,
2769
- ...head,
2770
- }))
2771
- }
2772
- match._nonReactive.loaderPromise = undefined
2773
- }
2774
- this.handleRedirectAndNotFound(innerLoadContext, match, err)
2775
- }
2776
- }
2777
-
2778
- private loadRouteMatch = async (
2779
- innerLoadContext: InnerLoadContext,
2780
- index: number,
2781
- ): Promise<AnyRouteMatch> => {
2782
- const { id: matchId, routeId } = innerLoadContext.matches[index]!
2783
- let loaderShouldRunAsync = false
2784
- let loaderIsRunningAsync = false
2785
- const route = this.looseRoutesById[routeId]!
2786
-
2787
- const prevMatch = this.getMatch(matchId)!
2788
- if (this.shouldSkipLoader(matchId)) {
2789
- if (this.isServer) {
2790
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2791
- if (headResult) {
2792
- const head = await headResult
2793
- innerLoadContext.updateMatch(matchId, (prev) => ({
2794
- ...prev,
2795
- ...head,
2796
- }))
2797
- }
2798
- return this.getMatch(matchId)!
2799
- }
2800
- }
2801
- // there is a loaderPromise, so we are in the middle of a load
2802
- else if (prevMatch._nonReactive.loaderPromise) {
2803
- // do not block if we already have stale data we can show
2804
- // but only if the ongoing load is not a preload since error handling is different for preloads
2805
- // and we don't want to swallow errors
2806
- if (
2807
- prevMatch.status === 'success' &&
2808
- !innerLoadContext.sync &&
2809
- !prevMatch.preload
2810
- ) {
2811
- return this.getMatch(matchId)!
2812
- }
2813
- await prevMatch._nonReactive.loaderPromise
2814
- const match = this.getMatch(matchId)!
2815
- if (match.error) {
2816
- this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2817
- }
2818
- } else {
2819
- // This is where all of the stale-while-revalidate magic happens
2820
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2821
-
2822
- const preload = this.resolvePreload(innerLoadContext, matchId)
2823
-
2824
- const staleAge = preload
2825
- ? (route.options.preloadStaleTime ??
2826
- this.options.defaultPreloadStaleTime ??
2827
- 30_000) // 30 seconds for preloads by default
2828
- : (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
2829
-
2830
- const shouldReloadOption = route.options.shouldReload
2831
-
2832
- // Default to reloading the route all the time
2833
- // Allow shouldReload to get the last say,
2834
- // if provided.
2835
- const shouldReload =
2836
- typeof shouldReloadOption === 'function'
2837
- ? shouldReloadOption(
2838
- this.getLoaderContext(innerLoadContext, matchId, index, route),
2839
- )
2840
- : shouldReloadOption
2841
-
2842
- const nextPreload =
2843
- !!preload && !this.state.matches.some((d) => d.id === matchId)
2844
- const match = this.getMatch(matchId)!
2845
- match._nonReactive.loaderPromise = createControlledPromise<void>()
2846
- if (nextPreload !== match.preload) {
2847
- innerLoadContext.updateMatch(matchId, (prev) => ({
2848
- ...prev,
2849
- preload: nextPreload,
2850
- }))
2851
- }
2852
-
2853
- // If the route is successful and still fresh, just resolve
2854
- const { status, invalid } = this.getMatch(matchId)!
2855
- loaderShouldRunAsync =
2856
- status === 'success' && (invalid || (shouldReload ?? age > staleAge))
2857
- if (preload && route.options.preload === false) {
2858
- // Do nothing
2859
- } else if (loaderShouldRunAsync && !innerLoadContext.sync) {
2860
- loaderIsRunningAsync = true
2861
- ;(async () => {
2862
- try {
2863
- await this.runLoader(innerLoadContext, matchId, index, route)
2864
- const match = this.getMatch(matchId)!
2865
- match._nonReactive.loaderPromise?.resolve()
2866
- match._nonReactive.loadPromise?.resolve()
2867
- match._nonReactive.loaderPromise = undefined
2868
- } catch (err) {
2869
- if (isRedirect(err)) {
2870
- await this.navigate(err.options)
2871
- }
2872
- }
2873
- })()
2874
- } else if (
2875
- status !== 'success' ||
2876
- (loaderShouldRunAsync && innerLoadContext.sync)
2877
- ) {
2878
- await this.runLoader(innerLoadContext, matchId, index, route)
2879
- } else {
2880
- // if the loader did not run, still update head.
2881
- // reason: parent's beforeLoad may have changed the route context
2882
- // and only now do we know the route context (and that the loader would not run)
2883
- const headResult = this.executeHead(innerLoadContext, matchId, route)
2884
- if (headResult) {
2885
- const head = await headResult
2886
- innerLoadContext.updateMatch(matchId, (prev) => ({
2887
- ...prev,
2888
- ...head,
2889
- }))
2890
- }
2891
- }
2892
- }
2893
- const match = this.getMatch(matchId)!
2894
- if (!loaderIsRunningAsync) {
2895
- match._nonReactive.loaderPromise?.resolve()
2896
- match._nonReactive.loadPromise?.resolve()
2897
- }
2898
-
2899
- clearTimeout(match._nonReactive.pendingTimeout)
2900
- match._nonReactive.pendingTimeout = undefined
2901
- if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
2902
- match._nonReactive.dehydrated = undefined
2903
- const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
2904
- if (nextIsFetching !== match.isFetching || match.invalid !== false) {
2905
- innerLoadContext.updateMatch(matchId, (prev) => ({
2906
- ...prev,
2907
- isFetching: nextIsFetching,
2908
- invalid: false,
2909
- }))
2910
- }
2911
- return this.getMatch(matchId)!
2912
- }
2913
-
2914
- loadMatches = async (baseContext: {
2915
- location: ParsedLocation
2916
- matches: Array<AnyRouteMatch>
2917
- preload?: boolean
2918
- onReady?: () => Promise<void>
2919
- updateMatch?: UpdateMatchFn
2920
- sync?: boolean
2921
- }): Promise<Array<MakeRouteMatch>> => {
2922
- const innerLoadContext = baseContext as InnerLoadContext
2923
- innerLoadContext.updateMatch ??= this.updateMatch
2924
- innerLoadContext.matchPromises = []
2925
-
2926
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2927
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2928
- if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2929
- this.triggerOnReady(innerLoadContext)
2930
- }
2931
-
2932
- try {
2933
- // Execute all beforeLoads one by one
2934
- for (let i = 0; i < innerLoadContext.matches.length; i++) {
2935
- const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
2936
- if (isPromise(beforeLoad)) await beforeLoad
2937
- }
2938
-
2939
- // Execute all loaders in parallel
2940
- const max =
2941
- innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
2942
- for (let i = 0; i < max; i++) {
2943
- innerLoadContext.matchPromises.push(
2944
- this.loadRouteMatch(innerLoadContext, i),
2945
- )
2946
- }
2947
- await Promise.all(innerLoadContext.matchPromises)
2948
-
2949
- const readyPromise = this.triggerOnReady(innerLoadContext)
2950
- if (isPromise(readyPromise)) await readyPromise
2951
- } catch (err) {
2952
- if (isNotFound(err) && !innerLoadContext.preload) {
2953
- const readyPromise = this.triggerOnReady(innerLoadContext)
2954
- if (isPromise(readyPromise)) await readyPromise
2955
- throw err
2956
- }
2957
- if (isRedirect(err)) {
2958
- throw err
2959
- }
2960
- }
2961
-
2962
- return innerLoadContext.matches
2963
- }
2964
-
2965
2085
  invalidate: InvalidateFn<
2966
2086
  RouterCore<
2967
2087
  TRouteTree,
@@ -3055,47 +2175,7 @@ export class RouterCore<
3055
2175
  this.clearCache({ filter })
3056
2176
  }
3057
2177
 
3058
- loadRouteChunk = (route: AnyRoute) => {
3059
- if (!route._lazyLoaded && route._lazyPromise === undefined) {
3060
- if (route.lazyFn) {
3061
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
3062
- // explicitly don't copy over the lazy route's id
3063
- const { id: _id, ...options } = lazyRoute.options
3064
- Object.assign(route.options, options)
3065
- route._lazyLoaded = true
3066
- route._lazyPromise = undefined // gc promise, we won't need it anymore
3067
- })
3068
- } else {
3069
- route._lazyLoaded = true
3070
- }
3071
- }
3072
-
3073
- // If for some reason lazy resolves more lazy components...
3074
- // We'll wait for that before we attempt to preload the
3075
- // components themselves.
3076
- if (!route._componentsLoaded && route._componentsPromise === undefined) {
3077
- const loadComponents = () => {
3078
- const preloads = []
3079
- for (const type of componentTypes) {
3080
- const preload = (route.options[type] as any)?.preload
3081
- if (preload) preloads.push(preload())
3082
- }
3083
- if (preloads.length)
3084
- return Promise.all(preloads).then(() => {
3085
- route._componentsLoaded = true
3086
- route._componentsPromise = undefined // gc promise, we won't need it anymore
3087
- })
3088
- route._componentsLoaded = true
3089
- route._componentsPromise = undefined // gc promise, we won't need it anymore
3090
- return
3091
- }
3092
- route._componentsPromise = route._lazyPromise
3093
- ? route._lazyPromise.then(loadComponents)
3094
- : loadComponents()
3095
- }
3096
-
3097
- return route._componentsPromise
3098
- }
2178
+ loadRouteChunk = loadRouteChunk
3099
2179
 
3100
2180
  preloadRoute: PreloadRouteFn<
3101
2181
  TRouteTree,
@@ -3135,7 +2215,8 @@ export class RouterCore<
3135
2215
  })
3136
2216
 
3137
2217
  try {
3138
- matches = await this.loadMatches({
2218
+ matches = await loadMatches({
2219
+ router: this,
3139
2220
  matches,
3140
2221
  location: next,
3141
2222
  preload: true,
@@ -3233,58 +2314,6 @@ export class RouterCore<
3233
2314
 
3234
2315
  serverSsr?: ServerSsr
3235
2316
 
3236
- private _handleNotFound = (
3237
- innerLoadContext: InnerLoadContext,
3238
- err: NotFoundError,
3239
- ) => {
3240
- // Find the route that should handle the not found error
3241
- // First check if a specific route is requested to show the error
3242
- const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
3243
- const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3244
-
3245
- // Setup routesByRouteId object for quick access
3246
- for (const match of innerLoadContext.matches) {
3247
- matchesByRouteId[match.routeId] = match
3248
- }
3249
-
3250
- // Ensure a NotFoundComponent exists on the route
3251
- if (
3252
- !routeCursor.options.notFoundComponent &&
3253
- (this.options as any)?.defaultNotFoundComponent
3254
- ) {
3255
- routeCursor.options.notFoundComponent = (
3256
- this.options as any
3257
- ).defaultNotFoundComponent
3258
- }
3259
-
3260
- // Ensure we have a notFoundComponent
3261
- invariant(
3262
- routeCursor.options.notFoundComponent,
3263
- 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
3264
- )
3265
-
3266
- // Find the match for this route
3267
- const matchForRoute = matchesByRouteId[routeCursor.id]
3268
-
3269
- invariant(
3270
- matchForRoute,
3271
- 'Could not find match for route: ' + routeCursor.id,
3272
- )
3273
-
3274
- // Assign the error to the match - using non-null assertion since we've checked with invariant
3275
- innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
3276
- ...prev,
3277
- status: 'notFound',
3278
- error: err,
3279
- isFetching: false,
3280
- }))
3281
-
3282
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3283
- err.routeId = routeCursor.parentRoute.id
3284
- this._handleNotFound(innerLoadContext, err)
3285
- }
3286
- }
3287
-
3288
2317
  hasNotFoundMatch = () => {
3289
2318
  return this.__store.state.matches.some(
3290
2319
  (d) => d.status === 'notFound' || d.globalNotFound,
@@ -3296,16 +2325,6 @@ export class SearchParamError extends Error {}
3296
2325
 
3297
2326
  export class PathParamError extends Error {}
3298
2327
 
3299
- function makeMaybe<TValue, TError>(
3300
- value: TValue,
3301
- error: TError,
3302
- ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
3303
- if (error) {
3304
- return { status: 'error' as const, error }
3305
- }
3306
- return { status: 'success' as const, value }
3307
- }
3308
-
3309
2328
  const normalize = (str: string) =>
3310
2329
  str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3311
2330
  function comparePaths(a: string, b: string) {
@@ -3372,22 +2391,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3372
2391
  return {}
3373
2392
  }
3374
2393
 
3375
- export const componentTypes = [
3376
- 'component',
3377
- 'errorComponent',
3378
- 'pendingComponent',
3379
- 'notFoundComponent',
3380
- ] as const
3381
-
3382
- function routeNeedsPreload(route: AnyRoute) {
3383
- for (const componentType of componentTypes) {
3384
- if ((route.options[componentType] as any)?.preload) {
3385
- return true
3386
- }
3387
- }
3388
- return false
3389
- }
3390
-
3391
2394
  interface RouteLike {
3392
2395
  id: string
3393
2396
  isRoot?: boolean