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