@tanstack/router-core 1.142.11 → 1.143.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.
@@ -683,8 +683,6 @@ const runLoader = async (
683
683
  // so we need to wait for it to resolve before
684
684
  // we can use the options
685
685
  if (route._lazyPromise) await route._lazyPromise
686
- const headResult = executeHead(inner, matchId, route)
687
- const head = headResult ? await headResult : undefined
688
686
  const pendingPromise = match._nonReactive.minPendingPromise
689
687
  if (pendingPromise) await pendingPromise
690
688
 
@@ -697,7 +695,6 @@ const runLoader = async (
697
695
  status: 'success',
698
696
  isFetching: false,
699
697
  updatedAt: Date.now(),
700
- ...head,
701
698
  }))
702
699
  } catch (e) {
703
700
  let error = e
@@ -721,28 +718,17 @@ const runLoader = async (
721
718
  onErrorError,
722
719
  )
723
720
  }
724
- const headResult = executeHead(inner, matchId, route)
725
- const head = headResult ? await headResult : undefined
726
721
  inner.updateMatch(matchId, (prev) => ({
727
722
  ...prev,
728
723
  error,
729
724
  status: 'error',
730
725
  isFetching: false,
731
- ...head,
732
726
  }))
733
727
  }
734
728
  } catch (err) {
735
729
  const match = inner.router.getMatch(matchId)
736
730
  // in case of a redirecting match during preload, the match does not exist
737
731
  if (match) {
738
- const headResult = executeHead(inner, matchId, route)
739
- if (headResult) {
740
- const head = await headResult
741
- inner.updateMatch(matchId, (prev) => ({
742
- ...prev,
743
- ...head,
744
- }))
745
- }
746
732
  match._nonReactive.loaderPromise = undefined
747
733
  }
748
734
  handleRedirectAndNotFound(inner, match, err)
@@ -767,14 +753,6 @@ const loadRouteMatch = async (
767
753
 
768
754
  if (shouldSkipLoader(inner, matchId)) {
769
755
  if (inner.router.isServer) {
770
- const headResult = executeHead(inner, matchId, route)
771
- if (headResult) {
772
- const head = await headResult
773
- inner.updateMatch(matchId, (prev) => ({
774
- ...prev,
775
- ...head,
776
- }))
777
- }
778
756
  return inner.router.getMatch(matchId)!
779
757
  }
780
758
  } else {
@@ -852,18 +830,6 @@ const loadRouteMatch = async (
852
830
  })()
853
831
  } else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
854
832
  await runLoader(inner, matchId, index, route)
855
- } else {
856
- // if the loader did not run, still update head.
857
- // reason: parent's beforeLoad may have changed the route context
858
- // and only now do we know the route context (and that the loader would not run)
859
- const headResult = executeHead(inner, matchId, route)
860
- if (headResult) {
861
- const head = await headResult
862
- inner.updateMatch(matchId, (prev) => ({
863
- ...prev,
864
- ...head,
865
- }))
866
- }
867
833
  }
868
834
  }
869
835
  }
@@ -931,7 +897,52 @@ export async function loadMatches(arg: {
931
897
  for (let i = 0; i < max; i++) {
932
898
  inner.matchPromises.push(loadRouteMatch(inner, i))
933
899
  }
934
- await Promise.all(inner.matchPromises)
900
+ // Use allSettled to ensure all loaders complete regardless of success/failure
901
+ const results = await Promise.allSettled(inner.matchPromises)
902
+
903
+ const failures = results
904
+ // TODO when we drop support for TS 5.4, we can use the built-in type guard for PromiseRejectedResult
905
+ .filter(
906
+ (result): result is PromiseRejectedResult =>
907
+ result.status === 'rejected',
908
+ )
909
+ .map((result) => result.reason)
910
+
911
+ // Find first redirect (throw immediately) or notFound (throw after head execution)
912
+ let firstNotFound: unknown
913
+ for (const err of failures) {
914
+ if (isRedirect(err)) {
915
+ throw err
916
+ }
917
+ if (!firstNotFound && isNotFound(err)) {
918
+ firstNotFound = err
919
+ }
920
+ }
921
+
922
+ // serially execute head functions after all loaders have completed (successfully or not)
923
+ // Each head execution is wrapped in try-catch to ensure all heads run even if one fails
924
+ for (const match of inner.matches) {
925
+ const { id: matchId, routeId } = match
926
+ const route = inner.router.looseRoutesById[routeId]!
927
+ try {
928
+ const headResult = executeHead(inner, matchId, route)
929
+ if (headResult) {
930
+ const head = await headResult
931
+ inner.updateMatch(matchId, (prev) => ({
932
+ ...prev,
933
+ ...head,
934
+ }))
935
+ }
936
+ } catch (err) {
937
+ // Log error but continue executing other head functions
938
+ console.error(`Error executing head for route ${routeId}:`, err)
939
+ }
940
+ }
941
+
942
+ // Throw notFound after head execution
943
+ if (firstNotFound) {
944
+ throw firstNotFound
945
+ }
935
946
 
936
947
  const readyPromise = triggerOnReady(inner)
937
948
  if (isPromise(readyPromise)) await readyPromise
@@ -945,7 +956,6 @@ export async function loadMatches(arg: {
945
956
  throw err
946
957
  }
947
958
  }
948
-
949
959
  return inner.matches
950
960
  }
951
961
 
package/src/router.ts CHANGED
@@ -1177,6 +1177,7 @@ export class RouterCore<
1177
1177
  // Before we do any processing, we need to allow rewrites to modify the URL
1178
1178
  // build up the full URL by combining the href from history with the router's origin
1179
1179
  const fullUrl = new URL(href, this.origin)
1180
+
1180
1181
  const url = executeRewriteInput(this.rewrite, fullUrl)
1181
1182
 
1182
1183
  const parsedSearch = this.options.parseSearch(url.search)
@@ -1187,11 +1188,33 @@ export class RouterCore<
1187
1188
 
1188
1189
  const fullPath = url.href.replace(url.origin, '')
1189
1190
 
1191
+ // Save the internal pathname for route matching (before output rewrite)
1192
+ const internalPathname = url.pathname
1193
+
1194
+ // Compute publicHref by applying the output rewrite.
1195
+ //
1196
+ // The publicHref represents the URL as it should appear in the browser.
1197
+ // This must match what buildLocation computes for the same logical route,
1198
+ // otherwise the server-side redirect check will see a mismatch and trigger
1199
+ // an infinite redirect loop.
1200
+ //
1201
+ // We always apply the output rewrite (not conditionally) because the
1202
+ // incoming URL may have already been transformed by external middleware
1203
+ // before reaching the router. In that case, the input rewrite has nothing
1204
+ // to do, but we still need the output rewrite to reconstruct the correct
1205
+ // public-facing URL.
1206
+ //
1207
+ // Clone the URL to avoid mutating the one used for route matching.
1208
+ const urlForOutput = new URL(url.href)
1209
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, urlForOutput)
1210
+ const publicHref =
1211
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash
1212
+
1190
1213
  return {
1191
1214
  href: fullPath,
1192
- publicHref: href,
1215
+ publicHref,
1193
1216
  url: url,
1194
- pathname: decodePath(url.pathname),
1217
+ pathname: decodePath(internalPathname),
1195
1218
  searchStr,
1196
1219
  search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1197
1220
  hash: url.hash.split('#').reverse()[0] ?? '',