@tanstack/router-core 1.163.3 → 1.166.4

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.
@@ -28,32 +28,29 @@ const buildMatchContext = (inner, index, includeCurrentMatch = true) => {
28
28
  }
29
29
  return context;
30
30
  };
31
- const _handleNotFound = (inner, err, routerCode) => {
32
- const routeCursor = inner.router.routesById[err.routeId ?? ""] ?? inner.router.routeTree;
33
- if (!routeCursor.options.notFoundComponent && inner.router.options?.defaultNotFoundComponent) {
34
- routeCursor.options.notFoundComponent = inner.router.options.defaultNotFoundComponent;
31
+ const getNotFoundBoundaryIndex = (inner, err) => {
32
+ if (!inner.matches.length) {
33
+ return void 0;
35
34
  }
36
- const willWalkUp = routerCode === "BEFORE_LOAD" && routeCursor.parentRoute;
37
- if (!willWalkUp) {
38
- invariant(
39
- routeCursor.options.notFoundComponent,
40
- "No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router."
41
- );
42
- }
43
- const matchForRoute = inner.matches.find((m) => m.routeId === routeCursor.id);
44
- invariant(matchForRoute, "Could not find match for route: " + routeCursor.id);
45
- inner.updateMatch(matchForRoute.id, (prev) => ({
46
- ...prev,
47
- status: "notFound",
48
- error: err,
49
- isFetching: false
50
- }));
51
- if (willWalkUp) {
52
- err.routeId = routeCursor.parentRoute.id;
53
- _handleNotFound(inner, err, routerCode);
35
+ const requestedRouteId = err.routeId;
36
+ const matchedRootIndex = inner.matches.findIndex(
37
+ (m) => m.routeId === inner.router.routeTree.id
38
+ );
39
+ const rootIndex = matchedRootIndex >= 0 ? matchedRootIndex : 0;
40
+ let startIndex = requestedRouteId ? inner.matches.findIndex((match) => match.routeId === requestedRouteId) : inner.firstBadMatchIndex ?? inner.matches.length - 1;
41
+ if (startIndex < 0) {
42
+ startIndex = rootIndex;
43
+ }
44
+ for (let i = startIndex; i >= 0; i--) {
45
+ const match = inner.matches[i];
46
+ const route = inner.router.looseRoutesById[match.routeId];
47
+ if (route.options.notFoundComponent) {
48
+ return i;
49
+ }
54
50
  }
51
+ return requestedRouteId ? startIndex : rootIndex;
55
52
  };
56
- const handleRedirectAndNotFound = (inner, match, err, routerCode) => {
53
+ const handleRedirectAndNotFound = (inner, match, err) => {
57
54
  if (!isRedirect(err) && !isNotFound(err)) return;
58
55
  if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
59
56
  throw err;
@@ -63,11 +60,10 @@ const handleRedirectAndNotFound = (inner, match, err, routerCode) => {
63
60
  match._nonReactive.loaderPromise?.resolve();
64
61
  match._nonReactive.beforeLoadPromise = void 0;
65
62
  match._nonReactive.loaderPromise = void 0;
66
- const status = isRedirect(err) ? "redirected" : "notFound";
67
63
  match._nonReactive.error = err;
68
64
  inner.updateMatch(match.id, (prev) => ({
69
65
  ...prev,
70
- status,
66
+ status: isRedirect(err) ? "redirected" : prev.status === "pending" ? "success" : prev.status,
71
67
  context: buildMatchContext(inner, match.index),
72
68
  isFetching: false,
73
69
  error: err
@@ -82,14 +78,14 @@ const handleRedirectAndNotFound = (inner, match, err, routerCode) => {
82
78
  err.options._fromLocation = inner.location;
83
79
  err.redirectHandled = true;
84
80
  err = inner.router.resolveRedirect(err);
85
- throw err;
86
- } else {
87
- _handleNotFound(inner, err, routerCode);
88
- throw err;
89
81
  }
82
+ throw err;
90
83
  };
91
84
  const shouldSkipLoader = (inner, matchId) => {
92
85
  const match = inner.router.getMatch(matchId);
86
+ if (!match) {
87
+ return true;
88
+ }
93
89
  if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) {
94
90
  return true;
95
91
  }
@@ -98,6 +94,15 @@ const shouldSkipLoader = (inner, matchId) => {
98
94
  }
99
95
  return false;
100
96
  };
97
+ const syncMatchContext = (inner, matchId, index) => {
98
+ const nextContext = buildMatchContext(inner, index);
99
+ inner.updateMatch(matchId, (prev) => {
100
+ return {
101
+ ...prev,
102
+ context: nextContext
103
+ };
104
+ });
105
+ };
101
106
  const handleSerialError = (inner, index, err, routerCode) => {
102
107
  const { id: matchId, routeId } = inner.matches[index];
103
108
  const route = inner.router.looseRoutesById[routeId];
@@ -106,22 +111,12 @@ const handleSerialError = (inner, index, err, routerCode) => {
106
111
  }
107
112
  err.routerCode = routerCode;
108
113
  inner.firstBadMatchIndex ??= index;
109
- handleRedirectAndNotFound(
110
- inner,
111
- inner.router.getMatch(matchId),
112
- err,
113
- routerCode
114
- );
114
+ handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err);
115
115
  try {
116
116
  route.options.onError?.(err);
117
117
  } catch (errorHandlerErr) {
118
118
  err = errorHandlerErr;
119
- handleRedirectAndNotFound(
120
- inner,
121
- inner.router.getMatch(matchId),
122
- err,
123
- routerCode
124
- );
119
+ handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err);
125
120
  }
126
121
  inner.updateMatch(matchId, (prev) => {
127
122
  prev._nonReactive.beforeLoadPromise?.resolve();
@@ -136,6 +131,9 @@ const handleSerialError = (inner, index, err, routerCode) => {
136
131
  abortController: new AbortController()
137
132
  };
138
133
  });
134
+ if (!inner.preload && !isRedirect(err) && !isNotFound(err)) {
135
+ inner.serialError ??= err;
136
+ }
139
137
  };
140
138
  const isBeforeLoadSsr = (inner, matchId, index, route) => {
141
139
  const existingMatch = inner.router.getMatch(matchId);
@@ -371,8 +369,8 @@ const executeHead = (inner, matchId, route) => {
371
369
  };
372
370
  });
373
371
  };
374
- const getLoaderContext = (inner, matchId, index, route) => {
375
- const parentMatchPromise = inner.matchPromises[index - 1];
372
+ const getLoaderContext = (inner, matchPromises, matchId, index, route) => {
373
+ const parentMatchPromise = matchPromises[index - 1];
376
374
  const { params, loaderDeps, abortController, cause } = inner.router.getMatch(matchId);
377
375
  const context = buildMatchContext(inner, index);
378
376
  const preload = resolvePreload(inner, matchId);
@@ -393,7 +391,7 @@ const getLoaderContext = (inner, matchId, index, route) => {
393
391
  ...inner.router.options.additionalContext
394
392
  };
395
393
  };
396
- const runLoader = async (inner, matchId, index, route) => {
394
+ const runLoader = async (inner, matchPromises, matchId, index, route) => {
397
395
  try {
398
396
  const match = inner.router.getMatch(matchId);
399
397
  try {
@@ -401,7 +399,7 @@ const runLoader = async (inner, matchId, index, route) => {
401
399
  loadRouteChunk(route);
402
400
  }
403
401
  const loaderResult = route.options.loader?.(
404
- getLoaderContext(inner, matchId, index, route)
402
+ getLoaderContext(inner, matchPromises, matchId, index, route)
405
403
  );
406
404
  const loaderResultIsPromise = route.options.loader && isPromise(loaderResult);
407
405
  const willLoadSomething = !!(loaderResultIsPromise || route._lazyPromise || route._componentsPromise || route.options.head || route.options.scripts || route.options.headers || match._nonReactive.minPendingPromise);
@@ -485,20 +483,23 @@ const runLoader = async (inner, matchId, index, route) => {
485
483
  handleRedirectAndNotFound(inner, match, err);
486
484
  }
487
485
  };
488
- const loadRouteMatch = async (inner, index) => {
489
- async function handleLoader(preload, prevMatch, match2, route2) {
486
+ const loadRouteMatch = async (inner, matchPromises, index) => {
487
+ async function handleLoader(preload, prevMatch, previousRouteMatchId, match2, route2) {
490
488
  const age = Date.now() - prevMatch.updatedAt;
491
489
  const staleAge = preload ? route2.options.preloadStaleTime ?? inner.router.options.defaultPreloadStaleTime ?? 3e4 : route2.options.staleTime ?? inner.router.options.defaultStaleTime ?? 0;
492
490
  const shouldReloadOption = route2.options.shouldReload;
493
- const shouldReload = typeof shouldReloadOption === "function" ? shouldReloadOption(getLoaderContext(inner, matchId, index, route2)) : shouldReloadOption;
491
+ const shouldReload = typeof shouldReloadOption === "function" ? shouldReloadOption(
492
+ getLoaderContext(inner, matchPromises, matchId, index, route2)
493
+ ) : shouldReloadOption;
494
494
  const { status, invalid } = match2;
495
- loaderShouldRunAsync = status === "success" && (invalid || (shouldReload ?? age > staleAge));
495
+ const staleMatchShouldReload = age > staleAge && (!!inner.forceStaleReload || match2.cause === "enter" || previousRouteMatchId !== void 0 && previousRouteMatchId !== match2.id);
496
+ loaderShouldRunAsync = status === "success" && (invalid || (shouldReload ?? staleMatchShouldReload));
496
497
  if (preload && route2.options.preload === false) ;
497
498
  else if (loaderShouldRunAsync && !inner.sync) {
498
499
  loaderIsRunningAsync = true;
499
500
  (async () => {
500
501
  try {
501
- await runLoader(inner, matchId, index, route2);
502
+ await runLoader(inner, matchPromises, matchId, index, route2);
502
503
  const match3 = inner.router.getMatch(matchId);
503
504
  match3._nonReactive.loaderPromise?.resolve();
504
505
  match3._nonReactive.loadPromise?.resolve();
@@ -510,7 +511,9 @@ const loadRouteMatch = async (inner, index) => {
510
511
  }
511
512
  })();
512
513
  } else if (status !== "success" || loaderShouldRunAsync && inner.sync) {
513
- await runLoader(inner, matchId, index, route2);
514
+ await runLoader(inner, matchPromises, matchId, index, route2);
515
+ } else {
516
+ syncMatchContext(inner, matchId, index);
514
517
  }
515
518
  }
516
519
  const { id: matchId, routeId } = inner.matches[index];
@@ -518,11 +521,17 @@ const loadRouteMatch = async (inner, index) => {
518
521
  let loaderIsRunningAsync = false;
519
522
  const route = inner.router.looseRoutesById[routeId];
520
523
  if (shouldSkipLoader(inner, matchId)) {
524
+ const match2 = inner.router.getMatch(matchId);
525
+ if (!match2) {
526
+ return inner.matches[index];
527
+ }
528
+ syncMatchContext(inner, matchId, index);
521
529
  if (isServer ?? inner.router.isServer) {
522
530
  return inner.router.getMatch(matchId);
523
531
  }
524
532
  } else {
525
533
  const prevMatch = inner.router.getMatch(matchId);
534
+ const previousRouteMatchId = inner.router.state.matches[index]?.routeId === routeId ? inner.router.state.matches[index].id : inner.router.state.matches.find((d) => d.routeId === routeId)?.id;
526
535
  const preload = resolvePreload(inner, matchId);
527
536
  if (prevMatch._nonReactive.loaderPromise) {
528
537
  if (prevMatch.status === "success" && !inner.sync && !prevMatch.preload) {
@@ -535,7 +544,13 @@ const loadRouteMatch = async (inner, index) => {
535
544
  handleRedirectAndNotFound(inner, match2, error);
536
545
  }
537
546
  if (match2.status === "pending") {
538
- await handleLoader(preload, prevMatch, match2, route);
547
+ await handleLoader(
548
+ preload,
549
+ prevMatch,
550
+ previousRouteMatchId,
551
+ match2,
552
+ route
553
+ );
539
554
  }
540
555
  } else {
541
556
  const nextPreload = preload && !inner.router.state.matches.some((d) => d.id === matchId);
@@ -547,7 +562,7 @@ const loadRouteMatch = async (inner, index) => {
547
562
  preload: nextPreload
548
563
  }));
549
564
  }
550
- await handleLoader(preload, prevMatch, match2, route);
565
+ await handleLoader(preload, prevMatch, previousRouteMatchId, match2, route);
551
566
  }
552
567
  }
553
568
  const match = inner.router.getMatch(matchId);
@@ -572,65 +587,141 @@ const loadRouteMatch = async (inner, index) => {
572
587
  }
573
588
  };
574
589
  async function loadMatches(arg) {
575
- const inner = Object.assign(arg, {
576
- matchPromises: []
577
- });
590
+ const inner = arg;
591
+ const matchPromises = [];
578
592
  if (!(isServer ?? inner.router.isServer) && inner.router.state.matches.some((d) => d._forcePending)) {
579
593
  triggerOnReady(inner);
580
594
  }
581
- try {
582
- for (let i = 0; i < inner.matches.length; i++) {
595
+ let beforeLoadNotFound;
596
+ for (let i = 0; i < inner.matches.length; i++) {
597
+ try {
583
598
  const beforeLoad = handleBeforeLoad(inner, i);
584
599
  if (isPromise(beforeLoad)) await beforeLoad;
585
- }
586
- const max = inner.firstBadMatchIndex ?? inner.matches.length;
587
- for (let i = 0; i < max; i++) {
588
- inner.matchPromises.push(loadRouteMatch(inner, i));
589
- }
590
- const results = await Promise.allSettled(inner.matchPromises);
591
- const failures = results.filter(
592
- (result) => result.status === "rejected"
593
- ).map((result) => result.reason);
594
- let firstNotFound;
595
- for (const err of failures) {
600
+ } catch (err) {
596
601
  if (isRedirect(err)) {
597
602
  throw err;
598
603
  }
599
- if (!firstNotFound && isNotFound(err)) {
600
- firstNotFound = err;
604
+ if (isNotFound(err)) {
605
+ beforeLoadNotFound = err;
606
+ } else {
607
+ if (!inner.preload) throw err;
601
608
  }
609
+ break;
602
610
  }
603
- for (const match of inner.matches) {
604
- const { id: matchId, routeId } = match;
605
- const route = inner.router.looseRoutesById[routeId];
606
- try {
607
- const headResult = executeHead(inner, matchId, route);
608
- if (headResult) {
609
- const head = await headResult;
610
- inner.updateMatch(matchId, (prev) => ({
611
- ...prev,
612
- ...head
613
- }));
614
- }
615
- } catch (err) {
616
- console.error(`Error executing head for route ${routeId}:`, err);
611
+ if (inner.serialError) {
612
+ break;
613
+ }
614
+ }
615
+ const baseMaxIndexExclusive = inner.firstBadMatchIndex ?? inner.matches.length;
616
+ const boundaryIndex = beforeLoadNotFound && !inner.preload ? getNotFoundBoundaryIndex(inner, beforeLoadNotFound) : void 0;
617
+ const maxIndexExclusive = beforeLoadNotFound && inner.preload ? 0 : boundaryIndex !== void 0 ? Math.min(boundaryIndex + 1, baseMaxIndexExclusive) : baseMaxIndexExclusive;
618
+ let firstNotFound;
619
+ let firstUnhandledRejection;
620
+ for (let i = 0; i < maxIndexExclusive; i++) {
621
+ matchPromises.push(loadRouteMatch(inner, matchPromises, i));
622
+ }
623
+ try {
624
+ await Promise.all(matchPromises);
625
+ } catch {
626
+ const settled = await Promise.allSettled(matchPromises);
627
+ for (const result of settled) {
628
+ if (result.status !== "rejected") continue;
629
+ const reason = result.reason;
630
+ if (isRedirect(reason)) {
631
+ throw reason;
632
+ }
633
+ if (isNotFound(reason)) {
634
+ firstNotFound ??= reason;
635
+ } else {
636
+ firstUnhandledRejection ??= reason;
617
637
  }
618
638
  }
619
- if (firstNotFound) {
620
- throw firstNotFound;
639
+ if (firstUnhandledRejection !== void 0) {
640
+ throw firstUnhandledRejection;
621
641
  }
622
- const readyPromise = triggerOnReady(inner);
623
- if (isPromise(readyPromise)) await readyPromise;
624
- } catch (err) {
625
- if (isNotFound(err) && !inner.preload) {
626
- const readyPromise = triggerOnReady(inner);
627
- if (isPromise(readyPromise)) await readyPromise;
628
- throw err;
642
+ }
643
+ const notFoundToThrow = firstNotFound ?? (beforeLoadNotFound && !inner.preload ? beforeLoadNotFound : void 0);
644
+ let headMaxIndex = inner.serialError ? inner.firstBadMatchIndex ?? 0 : inner.matches.length - 1;
645
+ if (!notFoundToThrow && beforeLoadNotFound && inner.preload) {
646
+ return inner.matches;
647
+ }
648
+ if (notFoundToThrow) {
649
+ const renderedBoundaryIndex = getNotFoundBoundaryIndex(
650
+ inner,
651
+ notFoundToThrow
652
+ );
653
+ invariant(
654
+ renderedBoundaryIndex !== void 0,
655
+ "Could not find match for notFound boundary"
656
+ );
657
+ const boundaryMatch = inner.matches[renderedBoundaryIndex];
658
+ const boundaryRoute = inner.router.looseRoutesById[boundaryMatch.routeId];
659
+ const defaultNotFoundComponent = inner.router.options?.defaultNotFoundComponent;
660
+ if (!boundaryRoute.options.notFoundComponent && defaultNotFoundComponent) {
661
+ boundaryRoute.options.notFoundComponent = defaultNotFoundComponent;
629
662
  }
630
- if (isRedirect(err)) {
631
- throw err;
663
+ notFoundToThrow.routeId = boundaryMatch.routeId;
664
+ const boundaryIsRoot = boundaryMatch.routeId === inner.router.routeTree.id;
665
+ inner.updateMatch(boundaryMatch.id, (prev) => ({
666
+ ...prev,
667
+ ...boundaryIsRoot ? (
668
+ // For root boundary, use globalNotFound so the root component's
669
+ // shell still renders and <Outlet> handles the not-found display,
670
+ // instead of replacing the entire root shell via status='notFound'.
671
+ { status: "success", globalNotFound: true, error: void 0 }
672
+ ) : (
673
+ // For non-root boundaries, set status:'notFound' so MatchInner
674
+ // renders the notFoundComponent directly.
675
+ { status: "notFound", error: notFoundToThrow }
676
+ ),
677
+ isFetching: false
678
+ }));
679
+ headMaxIndex = renderedBoundaryIndex;
680
+ await loadRouteChunk(boundaryRoute);
681
+ } else if (!inner.preload) {
682
+ const rootMatch = inner.matches[0];
683
+ if (!rootMatch.globalNotFound) {
684
+ const currentRootMatch = inner.router.getMatch(rootMatch.id);
685
+ if (currentRootMatch?.globalNotFound) {
686
+ inner.updateMatch(rootMatch.id, (prev) => ({
687
+ ...prev,
688
+ globalNotFound: false,
689
+ error: void 0
690
+ }));
691
+ }
632
692
  }
633
693
  }
694
+ if (inner.serialError && inner.firstBadMatchIndex !== void 0) {
695
+ const errorRoute = inner.router.looseRoutesById[inner.matches[inner.firstBadMatchIndex].routeId];
696
+ await loadRouteChunk(errorRoute);
697
+ }
698
+ for (let i = 0; i <= headMaxIndex; i++) {
699
+ const match = inner.matches[i];
700
+ const { id: matchId, routeId } = match;
701
+ const route = inner.router.looseRoutesById[routeId];
702
+ try {
703
+ const headResult = executeHead(inner, matchId, route);
704
+ if (headResult) {
705
+ const head = await headResult;
706
+ inner.updateMatch(matchId, (prev) => ({
707
+ ...prev,
708
+ ...head
709
+ }));
710
+ }
711
+ } catch (err) {
712
+ console.error(`Error executing head for route ${routeId}:`, err);
713
+ }
714
+ }
715
+ const readyPromise = triggerOnReady(inner);
716
+ if (isPromise(readyPromise)) {
717
+ await readyPromise;
718
+ }
719
+ if (notFoundToThrow) {
720
+ throw notFoundToThrow;
721
+ }
722
+ if (inner.serialError && !inner.preload && !inner.onReady) {
723
+ throw inner.serialError;
724
+ }
634
725
  return inner.matches;
635
726
  }
636
727
  async function loadRouteChunk(route) {