@tanstack/react-router 1.31.0 → 1.31.2

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.
@@ -7,7 +7,7 @@ import { defaultStringifySearch, defaultParseSearch } from "./searchParams.js";
7
7
  import { createControlledPromise, replaceEqualDeep, pick, last, deepEqual, escapeJSON, functionalUpdate } from "./utils.js";
8
8
  import { getRouteMatch } from "./RouterProvider.js";
9
9
  import { trimPath, trimPathLeft, parsePathname, resolvePath, cleanPath, matchPathname, trimPathRight, interpolatePath, joinPaths } from "./path.js";
10
- import { isRedirect } from "./redirects.js";
10
+ import { isResolvedRedirect, isRedirect } from "./redirects.js";
11
11
  import { isNotFound } from "./not-found.js";
12
12
  const componentTypes = [
13
13
  "component",
@@ -28,7 +28,6 @@ class Router {
28
28
  )}`;
29
29
  this.resetNextScroll = true;
30
30
  this.shouldViewTransition = void 0;
31
- this.navigateTimeout = null;
32
31
  this.latestLoadPromise = Promise.resolve();
33
32
  this.subscribers = /* @__PURE__ */ new Set();
34
33
  this.injectedHtml = [];
@@ -173,7 +172,9 @@ class Router {
173
172
  });
174
173
  };
175
174
  this.checkLatest = (promise) => {
176
- return this.latestLoadPromise !== promise ? this.latestLoadPromise : void 0;
175
+ if (this.latestLoadPromise !== promise) {
176
+ throw this.latestLoadPromise;
177
+ }
177
178
  };
178
179
  this.parseLocation = (previousLocation) => {
179
180
  const parse = ({
@@ -396,20 +397,20 @@ class Router {
396
397
  };
397
398
  this.buildLocation = (opts) => {
398
399
  const build = (dest = {}, matches) => {
399
- var _a, _b, _c, _d;
400
- const fromPath = dest.from || this.latestLocation.pathname;
401
- let fromSearch = ((_a = dest._fromLocation) == null ? void 0 : _a.search) || this.latestLocation.search;
402
- const fromMatches = this.matchRoutes(fromPath, fromSearch);
400
+ var _a, _b, _c;
401
+ let fromPath = this.latestLocation.pathname;
402
+ let fromSearch = dest.fromSearch || this.latestLocation.search;
403
+ const fromMatches = this.matchRoutes(
404
+ this.latestLocation.pathname,
405
+ fromSearch
406
+ );
407
+ fromPath = ((_a = fromMatches.find((d) => d.id === dest.from)) == null ? void 0 : _a.pathname) || fromPath;
403
408
  fromSearch = ((_b = last(fromMatches)) == null ? void 0 : _b.search) || this.latestLocation.search;
404
409
  const stayingMatches = matches == null ? void 0 : matches.filter(
405
410
  (d) => fromMatches.find((e) => e.routeId === d.routeId)
406
411
  );
407
- const fromRoute = this.looseRoutesById[(_c = last(fromMatches)) == null ? void 0 : _c.routeId];
408
- let pathname = dest.to ? this.resolvePathWithBase(
409
- dest.from ?? this.latestLocation.pathname,
410
- `${dest.to}`
411
- ) : this.resolvePathWithBase(fromRoute == null ? void 0 : fromRoute.fullPath, fromRoute == null ? void 0 : fromRoute.fullPath);
412
- const prevParams = { ...(_d = last(fromMatches)) == null ? void 0 : _d.params };
412
+ let pathname = dest.to ? this.resolvePathWithBase(fromPath, `${dest.to}`) : this.resolvePathWithBase(fromPath, fromPath);
413
+ const prevParams = { ...(_c = last(fromMatches)) == null ? void 0 : _c.params };
413
414
  let nextParams = (dest.params ?? true) === true ? prevParams : { ...prevParams, ...functionalUpdate(dest.params, prevParams) };
414
415
  if (Object.keys(nextParams).length > 0) {
415
416
  matches == null ? void 0 : matches.map((d) => this.looseRoutesById[d.routeId].options.stringifyParams).filter(Boolean).forEach((fn) => {
@@ -503,8 +504,6 @@ class Router {
503
504
  viewTransition,
504
505
  ...next
505
506
  }) => {
506
- if (this.navigateTimeout)
507
- clearTimeout(this.navigateTimeout);
508
507
  const isSameUrl = this.latestLocation.href === next.href;
509
508
  if (!isSameUrl) {
510
509
  let { maskedLocation, ...nextHistory } = next;
@@ -574,14 +573,131 @@ class Router {
574
573
  // to: toString,
575
574
  });
576
575
  };
576
+ this.load = async () => {
577
+ const promise = createControlledPromise();
578
+ this.latestLoadPromise = promise;
579
+ let redirect;
580
+ let notFound;
581
+ this.startReactTransition(async () => {
582
+ try {
583
+ const next = this.latestLocation;
584
+ const prevLocation = this.state.resolvedLocation;
585
+ const pathDidChange = prevLocation.href !== next.href;
586
+ this.cancelMatches();
587
+ this.emit({
588
+ type: "onBeforeLoad",
589
+ fromLocation: prevLocation,
590
+ toLocation: next,
591
+ pathChanged: pathDidChange
592
+ });
593
+ let pendingMatches;
594
+ this.__store.batch(() => {
595
+ this.cleanCache();
596
+ pendingMatches = this.matchRoutes(next.pathname, next.search);
597
+ this.__store.setState((s) => ({
598
+ ...s,
599
+ status: "pending",
600
+ isLoading: true,
601
+ location: next,
602
+ pendingMatches,
603
+ // If a cached moved to pendingMatches, remove it from cachedMatches
604
+ cachedMatches: s.cachedMatches.filter((d) => {
605
+ return !pendingMatches.find((e) => e.id === d.id);
606
+ })
607
+ }));
608
+ });
609
+ await this.loadMatches({
610
+ matches: pendingMatches,
611
+ location: next,
612
+ checkLatest: () => this.checkLatest(promise),
613
+ onReady: async () => {
614
+ await this.startViewTransition(async () => {
615
+ let exitingMatches;
616
+ let enteringMatches;
617
+ let stayingMatches;
618
+ this.__store.batch(() => {
619
+ this.__store.setState((s) => {
620
+ const previousMatches = s.matches;
621
+ const newMatches = s.pendingMatches || s.matches;
622
+ exitingMatches = previousMatches.filter(
623
+ (match) => !newMatches.find((d) => d.id === match.id)
624
+ );
625
+ enteringMatches = newMatches.filter(
626
+ (match) => !previousMatches.find((d) => d.id === match.id)
627
+ );
628
+ stayingMatches = previousMatches.filter(
629
+ (match) => newMatches.find((d) => d.id === match.id)
630
+ );
631
+ return {
632
+ ...s,
633
+ isLoading: false,
634
+ matches: newMatches,
635
+ pendingMatches: void 0,
636
+ cachedMatches: [
637
+ ...s.cachedMatches,
638
+ ...exitingMatches.filter((d) => d.status !== "error")
639
+ ]
640
+ };
641
+ });
642
+ this.cleanCache();
643
+ });
644
+ [
645
+ [exitingMatches, "onLeave"],
646
+ [enteringMatches, "onEnter"],
647
+ [stayingMatches, "onStay"]
648
+ ].forEach(([matches, hook]) => {
649
+ matches.forEach((match) => {
650
+ var _a, _b;
651
+ (_b = (_a = this.looseRoutesById[match.routeId].options)[hook]) == null ? void 0 : _b.call(_a, match);
652
+ });
653
+ });
654
+ });
655
+ }
656
+ });
657
+ } catch (err) {
658
+ if (isResolvedRedirect(err)) {
659
+ redirect = err;
660
+ if (!this.isServer) {
661
+ this.navigate({ ...err, replace: true });
662
+ this.load();
663
+ }
664
+ } else if (isNotFound(err)) {
665
+ notFound = err;
666
+ }
667
+ this.__store.setState((s) => ({
668
+ ...s,
669
+ statusCode: (redirect == null ? void 0 : redirect.statusCode) || notFound ? 404 : s.matches.some((d) => d.status === "error") ? 500 : 200,
670
+ redirect
671
+ }));
672
+ }
673
+ promise.resolve();
674
+ });
675
+ return this.latestLoadPromise;
676
+ };
677
+ this.startViewTransition = async (fn) => {
678
+ var _a, _b;
679
+ const shouldViewTransition = this.shouldViewTransition ?? this.options.defaultViewTransition;
680
+ delete this.shouldViewTransition;
681
+ ((_b = (_a = shouldViewTransition && typeof document !== "undefined" ? document : void 0) == null ? void 0 : _a.startViewTransition) == null ? void 0 : _b.call(_a, fn)) || fn();
682
+ };
577
683
  this.loadMatches = async ({
578
684
  checkLatest,
579
685
  location,
580
686
  matches,
581
- preload
687
+ preload,
688
+ onReady
582
689
  }) => {
583
- let latestPromise;
584
690
  let firstBadMatchIndex;
691
+ let rendered = false;
692
+ const triggerOnReady = async () => {
693
+ if (!rendered) {
694
+ rendered = true;
695
+ await (onReady == null ? void 0 : onReady());
696
+ }
697
+ };
698
+ if (!this.isServer && !this.state.matches.length) {
699
+ triggerOnReady();
700
+ }
585
701
  const updateMatch = (id, updater, opts) => {
586
702
  var _a;
587
703
  let updated;
@@ -599,41 +715,64 @@ class Router {
599
715
  });
600
716
  return updated;
601
717
  };
718
+ const handleRedirectAndNotFound = (match, err) => {
719
+ if (isResolvedRedirect(err))
720
+ throw err;
721
+ if (isRedirect(err) || isNotFound(err)) {
722
+ updateMatch(match.id, (prev) => ({
723
+ ...prev,
724
+ status: isRedirect(err) ? "redirected" : isNotFound(err) ? "notFound" : "error",
725
+ isFetching: false,
726
+ error: err
727
+ }));
728
+ rendered = true;
729
+ if (!err.routeId) {
730
+ err.routeId = match.routeId;
731
+ }
732
+ if (isRedirect(err)) {
733
+ err = this.resolveRedirect(err);
734
+ throw err;
735
+ } else if (isNotFound(err)) {
736
+ this.handleNotFound(matches, err);
737
+ throw err;
738
+ }
739
+ }
740
+ };
602
741
  try {
603
742
  await new Promise((resolveAll, rejectAll) => {
604
743
  ;
605
744
  (async () => {
606
745
  var _a, _b;
607
746
  try {
608
- const handleRedirectAndNotFound = (match, err) => {
609
- if (isRedirect(err) || isNotFound(err)) {
610
- updateMatch(match.id, (prev) => ({
611
- ...prev,
612
- status: isRedirect(err) ? "redirected" : isNotFound(err) ? "notFound" : "error",
613
- isFetching: false,
614
- error: err
615
- }));
616
- if (!err.routeId) {
617
- ;
618
- err.routeId = match.routeId;
619
- }
620
- if (isRedirect(err)) {
621
- const redirect = this.resolveRedirect(err);
622
- if (!preload && !this.isServer) {
623
- this.navigate({ ...redirect, replace: true });
624
- }
625
- throw redirect;
626
- } else if (isNotFound(err)) {
627
- if (!preload)
628
- this.handleNotFound(matches, err);
629
- throw err;
630
- }
631
- }
632
- };
633
747
  for (let [index, match] of matches.entries()) {
634
748
  const parentMatch = matches[index - 1];
635
749
  const route = this.looseRoutesById[match.routeId];
636
750
  const abortController = new AbortController();
751
+ let loadPromise = match.loadPromise;
752
+ const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
753
+ const shouldPending = !!(onReady && !this.isServer && !preload && (route.options.loader || route.options.beforeLoad) && typeof pendingMs === "number" && pendingMs !== Infinity && (route.options.pendingComponent ?? this.options.defaultPendingComponent));
754
+ if (shouldPending) {
755
+ setTimeout(() => {
756
+ try {
757
+ checkLatest();
758
+ triggerOnReady();
759
+ } catch {
760
+ }
761
+ }, pendingMs);
762
+ }
763
+ if (match.isFetching) {
764
+ continue;
765
+ }
766
+ const previousResolve = loadPromise.resolve;
767
+ loadPromise = createControlledPromise(
768
+ // Resolve the old when we we resolve the new one
769
+ previousResolve
770
+ );
771
+ matches[index] = match = updateMatch(match.id, (prev) => ({
772
+ ...prev,
773
+ isFetching: "beforeLoad",
774
+ loadPromise
775
+ }));
637
776
  const handleSerialError = (err, routerCode) => {
638
777
  var _a2, _b2;
639
778
  err.routerCode = routerCode;
@@ -661,19 +800,6 @@ class Router {
661
800
  }
662
801
  try {
663
802
  const parentContext = (parentMatch == null ? void 0 : parentMatch.context) ?? this.options.context ?? {};
664
- const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
665
- const pendingPromise = typeof pendingMs !== "number" || pendingMs <= 0 ? Promise.resolve() : new Promise((r) => {
666
- if (pendingMs !== Infinity)
667
- setTimeout(r, pendingMs);
668
- });
669
- const shouldPending = !this.isServer && !preload && (route.options.loader || route.options.beforeLoad) && typeof pendingMs === "number" && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
670
- if (shouldPending) {
671
- pendingPromise.then(async () => {
672
- if (latestPromise = checkLatest())
673
- return latestPromise;
674
- resolveAll();
675
- });
676
- }
677
803
  const beforeLoadContext = await ((_b = (_a = route.options).beforeLoad) == null ? void 0 : _b.call(_a, {
678
804
  search: match.search,
679
805
  abortController,
@@ -685,6 +811,7 @@ class Router {
685
811
  buildLocation: this.buildLocation,
686
812
  cause: preload ? "preload" : match.cause
687
813
  })) ?? {};
814
+ checkLatest();
688
815
  if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
689
816
  handleSerialError(beforeLoadContext, "BEFORE_LOAD");
690
817
  }
@@ -708,6 +835,7 @@ class Router {
708
835
  updateMatch(match.id, () => match);
709
836
  }
710
837
  }
838
+ checkLatest();
711
839
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
712
840
  const matchPromises = [];
713
841
  await Promise.all(
@@ -732,13 +860,11 @@ class Router {
732
860
  let lazyPromise = Promise.resolve();
733
861
  let componentsPromise = Promise.resolve();
734
862
  let loaderPromise = existing.loaderPromise;
735
- let loadPromise = existing.loadPromise;
736
863
  const potentialPendingMinPromise = async () => {
737
864
  const latestMatch = getRouteMatch(this.state, match.id);
738
865
  if (latestMatch == null ? void 0 : latestMatch.minPendingPromise) {
739
866
  await latestMatch.minPendingPromise;
740
- if (latestPromise = checkLatest())
741
- return await latestPromise;
867
+ checkLatest();
742
868
  updateMatch(latestMatch.id, (prev) => ({
743
869
  ...prev,
744
870
  minPendingPromise: void 0
@@ -746,12 +872,15 @@ class Router {
746
872
  }
747
873
  };
748
874
  try {
749
- if (!match.isFetching) {
750
- matches[index] = match = {
751
- ...match,
752
- isFetching: true,
753
- fetchCount: match.fetchCount + 1
754
- };
875
+ if (match.isFetching === "beforeLoad") {
876
+ matches[index] = match = updateMatch(
877
+ match.id,
878
+ (prev) => ({
879
+ ...prev,
880
+ isFetching: "loader",
881
+ fetchCount: match.fetchCount + 1
882
+ })
883
+ );
755
884
  lazyPromise = ((_a2 = route.lazyFn) == null ? void 0 : _a2.call(route).then((lazyRoute) => {
756
885
  Object.assign(route.options, lazyRoute.options);
757
886
  })) || Promise.resolve();
@@ -766,29 +895,21 @@ class Router {
766
895
  )
767
896
  );
768
897
  await lazyPromise;
769
- if (latestPromise = checkLatest())
770
- return await latestPromise;
898
+ checkLatest();
771
899
  loaderPromise = (_c = (_b2 = route.options).loader) == null ? void 0 : _c.call(_b2, loaderContext);
772
- const previousResolve = loadPromise.resolve;
773
- loadPromise = createControlledPromise(
774
- // Resolve the old when we we resolve the new one
775
- previousResolve
900
+ matches[index] = match = updateMatch(
901
+ match.id,
902
+ (prev) => ({
903
+ ...prev,
904
+ loaderPromise
905
+ })
776
906
  );
777
907
  }
778
- matches[index] = match = updateMatch(match.id, (prev) => ({
779
- ...prev,
780
- loaderPromise,
781
- loadPromise
782
- }));
783
908
  const loaderData = await loaderPromise;
784
- if (latestPromise = checkLatest())
785
- return await latestPromise;
909
+ checkLatest();
786
910
  handleRedirectAndNotFound(match, loaderData);
787
- if (latestPromise = checkLatest())
788
- return await latestPromise;
789
911
  await potentialPendingMinPromise();
790
- if (latestPromise = checkLatest())
791
- return await latestPromise;
912
+ checkLatest();
792
913
  const meta = (_e = (_d = route.options).meta) == null ? void 0 : _e.call(_d, {
793
914
  params: match.params,
794
915
  loaderData
@@ -807,12 +928,10 @@ class Router {
807
928
  headers
808
929
  }));
809
930
  } catch (e) {
931
+ checkLatest();
810
932
  let error = e;
811
- if (latestPromise = checkLatest())
812
- return await latestPromise;
813
933
  await potentialPendingMinPromise();
814
- if (latestPromise = checkLatest())
815
- return await latestPromise;
934
+ checkLatest();
816
935
  handleRedirectAndNotFound(match, e);
817
936
  try {
818
937
  (_i = (_h = route.options).onError) == null ? void 0 : _i.call(_h, e);
@@ -828,9 +947,8 @@ class Router {
828
947
  }));
829
948
  }
830
949
  await componentsPromise;
831
- if (latestPromise = checkLatest())
832
- return await latestPromise;
833
- loadPromise.resolve();
950
+ checkLatest();
951
+ match.loadPromise.resolve();
834
952
  };
835
953
  const age = Date.now() - match.updatedAt;
836
954
  const staleAge = preload ? route.options.preloadStaleTime ?? this.options.defaultPreloadStaleTime ?? 3e4 : route.options.staleTime ?? this.options.defaultStaleTime ?? 0;
@@ -844,8 +962,7 @@ class Router {
844
962
  try {
845
963
  await fetch();
846
964
  } catch (err) {
847
- if (latestPromise = checkLatest())
848
- return await latestPromise;
965
+ checkLatest();
849
966
  handleRedirectAndNotFound(match, err);
850
967
  }
851
968
  };
@@ -858,14 +975,14 @@ class Router {
858
975
  }
859
976
  })
860
977
  );
861
- if (latestPromise = checkLatest())
862
- return await latestPromise;
978
+ checkLatest();
863
979
  resolveAll();
864
980
  } catch (err) {
865
981
  rejectAll(err);
866
982
  }
867
983
  })();
868
984
  });
985
+ await triggerOnReady();
869
986
  } catch (err) {
870
987
  if (isRedirect(err) || isNotFound(err)) {
871
988
  throw err;
@@ -890,115 +1007,6 @@ class Router {
890
1007
  });
891
1008
  return this.load();
892
1009
  };
893
- this.load = async () => {
894
- let resolveLoad;
895
- let rejectLoad;
896
- const promise = new Promise((resolve, reject) => {
897
- resolveLoad = resolve;
898
- rejectLoad = reject;
899
- });
900
- this.latestLoadPromise = promise;
901
- let latestPromise;
902
- this.startReactTransition(async () => {
903
- var _a, _b;
904
- try {
905
- const next = this.latestLocation;
906
- const prevLocation = this.state.resolvedLocation;
907
- const pathDidChange = prevLocation.href !== next.href;
908
- this.cancelMatches();
909
- this.emit({
910
- type: "onBeforeLoad",
911
- fromLocation: prevLocation,
912
- toLocation: next,
913
- pathChanged: pathDidChange
914
- });
915
- let pendingMatches;
916
- const previousMatches = this.state.matches;
917
- this.__store.batch(() => {
918
- this.cleanCache();
919
- pendingMatches = this.matchRoutes(next.pathname, next.search);
920
- this.__store.setState((s) => ({
921
- ...s,
922
- status: "pending",
923
- isLoading: true,
924
- location: next,
925
- pendingMatches,
926
- cachedMatches: s.cachedMatches.filter((d) => {
927
- return !pendingMatches.find((e) => e.id === d.id);
928
- })
929
- }));
930
- });
931
- let redirect;
932
- let notFound;
933
- try {
934
- const loadMatchesPromise = this.loadMatches({
935
- matches: pendingMatches,
936
- location: next,
937
- checkLatest: () => this.checkLatest(promise)
938
- });
939
- if (previousMatches.length || this.isServer) {
940
- await loadMatchesPromise;
941
- }
942
- } catch (err) {
943
- if (isRedirect(err)) {
944
- redirect = err;
945
- } else if (isNotFound(err)) {
946
- notFound = err;
947
- }
948
- }
949
- if (latestPromise = this.checkLatest(promise)) {
950
- return latestPromise;
951
- }
952
- const exitingMatches = previousMatches.filter(
953
- (match) => !pendingMatches.find((d) => d.id === match.id)
954
- );
955
- const enteringMatches = pendingMatches.filter(
956
- (match) => !previousMatches.find((d) => d.id === match.id)
957
- );
958
- const stayingMatches = previousMatches.filter(
959
- (match) => pendingMatches.find((d) => d.id === match.id)
960
- );
961
- const shouldViewTransition = this.shouldViewTransition ?? this.options.defaultViewTransition;
962
- delete this.shouldViewTransition;
963
- const apply = () => {
964
- this.__store.batch(() => {
965
- this.__store.setState((s) => ({
966
- ...s,
967
- isLoading: false,
968
- matches: s.pendingMatches,
969
- pendingMatches: void 0,
970
- cachedMatches: [
971
- ...s.cachedMatches,
972
- ...exitingMatches.filter((d) => d.status !== "error")
973
- ],
974
- statusCode: (redirect == null ? void 0 : redirect.statusCode) || notFound ? 404 : s.matches.some((d) => d.status === "error") ? 500 : 200,
975
- redirect
976
- }));
977
- this.cleanCache();
978
- });
979
- [
980
- [exitingMatches, "onLeave"],
981
- [enteringMatches, "onEnter"],
982
- [stayingMatches, "onStay"]
983
- ].forEach(([matches, hook]) => {
984
- matches.forEach((match) => {
985
- var _a2, _b2;
986
- (_b2 = (_a2 = this.looseRoutesById[match.routeId].options)[hook]) == null ? void 0 : _b2.call(_a2, match);
987
- });
988
- });
989
- resolveLoad();
990
- };
991
- ((_b = (_a = shouldViewTransition && typeof document !== "undefined" ? document : void 0) == null ? void 0 : _a.startViewTransition) == null ? void 0 : _b.call(_a, apply)) || apply();
992
- } catch (err) {
993
- if (latestPromise = this.checkLatest(promise)) {
994
- return latestPromise;
995
- }
996
- console.error("Load Error", err);
997
- rejectLoad(err);
998
- }
999
- });
1000
- return this.latestLoadPromise;
1001
- };
1002
1010
  this.resolveRedirect = (err) => {
1003
1011
  const redirect = err;
1004
1012
  if (!redirect.href) {
@@ -1061,7 +1069,7 @@ class Router {
1061
1069
  } catch (err) {
1062
1070
  if (isRedirect(err)) {
1063
1071
  return await this.preloadRoute({
1064
- _fromDest: next,
1072
+ fromSearch: next.search,
1065
1073
  from: next.pathname,
1066
1074
  ...err
1067
1075
  });