@tanstack/router-core 1.153.1 → 1.153.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.
@@ -6,7 +6,7 @@ export type { IsRequiredParams, AddTrailingSlash, RemoveTrailingSlashes, AddLead
6
6
  export { componentTypes } from './load-matches.cjs';
7
7
  export type { RouteToPath, TrailingSlashOptionByRouter, ParseRoute, CodeRouteToPath, RouteIds, FullSearchSchema, FullSearchSchemaInput, AllParams, RouteById, AllContext, RoutePaths, RoutesById, RoutesByPath, AllLoaderData, RouteByPath, } from './routeInfo.cjs';
8
8
  export type { InferFileRouteTypes, FileRouteTypes, FileRoutesByPath, CreateFileRoute, LazyRoute, LazyRouteOptions, CreateLazyFileRoute, } from './fileRoute.cjs';
9
- export type { ParsedLocation } from './location.cjs';
9
+ export type { MatchSnapshot, ParsedLocation, ValidatedSearchEntry, } from './location.cjs';
10
10
  export type { Manifest, RouterManagedTag } from './manifest.cjs';
11
11
  export { isMatch } from './Matches.cjs';
12
12
  export type { AnyMatchAndValue, FindValueByIndex, FindValueByKey, CreateMatchAndValue, NextMatchAndValue, IsMatchKeyOf, IsMatchPath, IsMatchResult, IsMatchParse, IsMatch, RouteMatch, RouteMatchExtensions, MakeRouteMatchUnion, MakeRouteMatch, AnyRouteMatch, MakeRouteMatchFromRoute, MatchRouteOptions, } from './Matches.cjs';
@@ -1,5 +1,32 @@
1
1
  import { ParsedHistoryState } from '@tanstack/history';
2
2
  import { AnySchema } from './validators.cjs';
3
+ /**
4
+ * Per-route validated search result cached in snapshot.
5
+ */
6
+ export interface ValidatedSearchEntry {
7
+ /** Merged search (parent + this route's validated) */
8
+ search: Record<string, unknown>;
9
+ /** Strict search (only this route's validated fields) */
10
+ strictSearch: Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Match snapshot stored in history state for fast-path on back/forward navigation.
14
+ * Allows skipping path matching by storing route IDs and params.
15
+ */
16
+ export interface MatchSnapshot {
17
+ /** Ordered route IDs that matched */
18
+ routeIds: Array<string>;
19
+ /** Raw path params extracted from the URL */
20
+ params: Record<string, string>;
21
+ /** Parsed/validated path params */
22
+ parsedParams: Record<string, unknown>;
23
+ /** Route ID that should show global not found, if any */
24
+ globalNotFoundRouteId?: string;
25
+ /** Search string when snapshot was created (for cache invalidation) */
26
+ searchStr?: string;
27
+ /** Per-route validated search results (parallel to routeIds) */
28
+ validatedSearches?: Array<ValidatedSearchEntry>;
29
+ }
3
30
  export interface ParsedLocation<TSearchObj extends AnySchema = {}> {
4
31
  /**
5
32
  * The full path of the location, including pathname, search, and hash.
@@ -49,7 +49,6 @@ class RouterCore {
49
49
  this.tempLocationKey = `${Math.round(
50
50
  Math.random() * 1e7
51
51
  )}`;
52
- this.resetNextScroll = true;
53
52
  this.shouldViewTransition = void 0;
54
53
  this.isViewTransitionTypesSupported = void 0;
55
54
  this.subscribers = /* @__PURE__ */ new Set();
@@ -317,9 +316,13 @@ class RouterCore {
317
316
  path: nextTo,
318
317
  params: nextParams
319
318
  }).interpolatedPath;
320
- const destRoutes = this.matchRoutes(interpolatedNextTo, void 0, {
319
+ const destMatches = this.matchRoutes(interpolatedNextTo, void 0, {
321
320
  _buildLocation: true
322
- }).map((d) => this.looseRoutesById[d.routeId]);
321
+ });
322
+ const destRoutes = destMatches.map(
323
+ (d) => this.looseRoutesById[d.routeId]
324
+ );
325
+ const globalNotFoundMatch = destMatches.find((m) => m.globalNotFound);
323
326
  if (Object.keys(nextParams).length > 0) {
324
327
  for (const route of destRoutes) {
325
328
  const fn = route.options.params?.stringify ?? route.options.stringifyParams;
@@ -370,19 +373,27 @@ class RouterCore {
370
373
  const hashStr = hash ? `#${hash}` : "";
371
374
  let nextState = dest.state === true ? currentLocation.state : dest.state ? utils.functionalUpdate(dest.state, currentLocation.state) : {};
372
375
  nextState = utils.replaceEqualDeep(currentLocation.state, nextState);
376
+ const matchSnapshot = buildMatchSnapshotFromRoutes({
377
+ routes: destRoutes,
378
+ params: nextParams,
379
+ searchStr,
380
+ globalNotFoundRouteId: globalNotFoundMatch?.routeId
381
+ });
373
382
  const fullPath = `${nextPathname}${searchStr}${hashStr}`;
374
383
  const url = new URL(fullPath, this.origin);
375
384
  const rewrittenUrl = rewrite.executeRewriteOutput(this.rewrite, url);
385
+ const encodedHref = url.href.replace(url.origin, "");
376
386
  return {
377
387
  publicHref: rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
378
- href: fullPath,
388
+ href: encodedHref,
379
389
  url: rewrittenUrl,
380
390
  pathname: nextPathname,
381
391
  search: nextSearch,
382
392
  searchStr,
383
393
  state: nextState,
384
394
  hash: hash ?? "",
385
- unmaskOnReload: dest.unmaskOnReload
395
+ unmaskOnReload: dest.unmaskOnReload,
396
+ _matchSnapshot: matchSnapshot
386
397
  };
387
398
  };
388
399
  const buildWithMatches = (dest = {}, maskedDest) => {
@@ -425,7 +436,7 @@ class RouterCore {
425
436
  }
426
437
  return buildWithMatches(opts);
427
438
  };
428
- this.commitLocation = ({
439
+ this.commitLocation = async ({
429
440
  viewTransition,
430
441
  ignoreBlocker,
431
442
  ...next
@@ -454,53 +465,73 @@ class RouterCore {
454
465
  });
455
466
  if (isSameUrl && isSameState()) {
456
467
  this.load();
457
- } else {
458
- let {
459
- // eslint-disable-next-line prefer-const
460
- maskedLocation,
461
- // eslint-disable-next-line prefer-const
462
- hashScrollIntoView,
463
- // don't pass url into history since it is a URL instance that cannot be serialized
464
- // eslint-disable-next-line prefer-const
465
- url: _url,
466
- ...nextHistory
467
- } = next;
468
- if (maskedLocation) {
469
- nextHistory = {
470
- ...maskedLocation,
471
- state: {
472
- ...maskedLocation.state,
473
- __tempKey: void 0,
474
- __tempLocation: {
475
- ...nextHistory,
476
- search: nextHistory.searchStr,
477
- state: {
478
- ...nextHistory.state,
479
- __tempKey: void 0,
480
- __tempLocation: void 0,
481
- __TSR_key: void 0,
482
- key: void 0
483
- // TODO: Remove in v2 - use __TSR_key instead
484
- }
468
+ return this.commitLocationPromise;
469
+ }
470
+ let {
471
+ // eslint-disable-next-line prefer-const
472
+ maskedLocation,
473
+ // eslint-disable-next-line prefer-const
474
+ hashScrollIntoView,
475
+ // don't pass url into history since it is a URL instance that cannot be serialized
476
+ // eslint-disable-next-line prefer-const
477
+ url: _url,
478
+ ...nextHistory
479
+ } = next;
480
+ if (maskedLocation) {
481
+ nextHistory = {
482
+ ...maskedLocation,
483
+ state: {
484
+ ...maskedLocation.state,
485
+ __tempKey: void 0,
486
+ __tempLocation: {
487
+ ...nextHistory,
488
+ search: nextHistory.searchStr,
489
+ state: {
490
+ ...nextHistory.state,
491
+ __tempKey: void 0,
492
+ __tempLocation: void 0,
493
+ __TSR_key: void 0,
494
+ key: void 0
495
+ // TODO: Remove in v2 - use __TSR_key instead
485
496
  }
486
497
  }
487
- };
488
- if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
489
- nextHistory.state.__tempKey = this.tempLocationKey;
490
498
  }
499
+ };
500
+ if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
501
+ nextHistory.state.__tempKey = this.tempLocationKey;
491
502
  }
492
- nextHistory.state.__hashScrollIntoViewOptions = hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true;
493
- this.shouldViewTransition = viewTransition;
494
- this.history[next.replace ? "replace" : "push"](
495
- nextHistory.publicHref,
496
- nextHistory.state,
497
- { ignoreBlocker }
498
- );
499
503
  }
500
- this.resetNextScroll = next.resetScroll ?? true;
501
- if (!this.history.subscribers.size) {
502
- this.load();
504
+ nextHistory.state.__hashScrollIntoViewOptions = hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true;
505
+ nextHistory.state.__TSR_resetScroll = next.resetScroll ?? true;
506
+ this.shouldViewTransition = viewTransition;
507
+ nextHistory.state.__TSR_sessionId = this.sessionId;
508
+ nextHistory.state.__TSR_matches = next._matchSnapshot ?? buildMatchSnapshot({
509
+ matchResult: this.getMatchedRoutes(next.pathname),
510
+ pathname: next.pathname,
511
+ searchStr: next.searchStr,
512
+ notFoundRoute: this.options.notFoundRoute,
513
+ notFoundMode: this.options.notFoundMode
514
+ });
515
+ const precomputedLocation = {
516
+ ...next,
517
+ publicHref: nextHistory.publicHref,
518
+ state: nextHistory.state,
519
+ maskedLocation
520
+ };
521
+ const result = await this.history[next.replace ? "replace" : "push"](
522
+ nextHistory.publicHref,
523
+ nextHistory.state,
524
+ { ignoreBlocker, skipTransitionerLoad: true }
525
+ );
526
+ if (result.type === "BLOCKED") {
527
+ this.commitLocationPromise?.resolve();
528
+ return this.commitLocationPromise;
529
+ }
530
+ if (this.history.location.href !== nextHistory.publicHref) {
531
+ return this.commitLocationPromise;
503
532
  }
533
+ this.latestLocation = precomputedLocation;
534
+ this.load({ _skipUpdateLatestLocation: true });
504
535
  return this.commitLocationPromise;
505
536
  };
506
537
  this.buildAndCommitLocation = ({
@@ -607,9 +638,11 @@ class RouterCore {
607
638
  _isNavigate: true
608
639
  });
609
640
  };
610
- this.beforeLoad = () => {
641
+ this.beforeLoad = (opts) => {
611
642
  this.cancelMatches();
612
- this.updateLatestLocation();
643
+ if (!opts?._skipUpdateLatestLocation) {
644
+ this.updateLatestLocation();
645
+ }
613
646
  if (this.isServer) {
614
647
  const nextLocation = this.buildLocation({
615
648
  to: this.latestLocation.pathname,
@@ -624,7 +657,8 @@ class RouterCore {
624
657
  throw redirect.redirect({ href });
625
658
  }
626
659
  }
627
- const pendingMatches = this.matchRoutes(this.latestLocation);
660
+ const snapshot = this.latestLocation.state.__TSR_sessionId === this.sessionId ? this.latestLocation.state.__TSR_matches : void 0;
661
+ const pendingMatches = this.matchRoutes(this.latestLocation, { snapshot });
628
662
  this.__store.setState((s) => ({
629
663
  ...s,
630
664
  status: "pending",
@@ -645,7 +679,9 @@ class RouterCore {
645
679
  loadPromise = new Promise((resolve) => {
646
680
  this.startTransition(async () => {
647
681
  try {
648
- this.beforeLoad();
682
+ this.beforeLoad({
683
+ _skipUpdateLatestLocation: opts?._skipUpdateLatestLocation
684
+ });
649
685
  const next = this.latestLocation;
650
686
  const prevLocation = this.state.resolvedLocation;
651
687
  if (!this.state.redirect) {
@@ -1002,6 +1038,7 @@ class RouterCore {
1002
1038
  (d) => d.status === "notFound" || d.globalNotFound
1003
1039
  );
1004
1040
  };
1041
+ this.sessionId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1005
1042
  this.update({
1006
1043
  defaultPreloadDelay: 50,
1007
1044
  defaultPendingMs: 1e3,
@@ -1030,46 +1067,67 @@ class RouterCore {
1030
1067
  return this.routesById;
1031
1068
  }
1032
1069
  matchRoutesInternal(next, opts) {
1033
- const matchedRoutesResult = this.getMatchedRoutes(next.pathname);
1034
- const { foundRoute, routeParams, parsedParams } = matchedRoutesResult;
1035
- let { matchedRoutes } = matchedRoutesResult;
1036
- let isGlobalNotFound = false;
1037
- if (
1038
- // If we found a route, and it's not an index route and we have left over path
1039
- foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
1040
- // Or if we didn't find a route and we have left over path
1041
- path.trimPathRight(next.pathname)
1042
- )
1043
- ) {
1044
- if (this.options.notFoundRoute) {
1045
- matchedRoutes = [...matchedRoutes, this.options.notFoundRoute];
1046
- } else {
1047
- isGlobalNotFound = true;
1048
- }
1049
- }
1050
- const globalNotFoundRouteId = (() => {
1051
- if (!isGlobalNotFound) {
1052
- return void 0;
1070
+ const snapshot = opts?.snapshot;
1071
+ const snapshotValid = snapshot && snapshot.routeIds.length > 0 && snapshot.routeIds.every((id) => this.routesById[id]);
1072
+ let matchedRoutes;
1073
+ let routeParams;
1074
+ let globalNotFoundRouteId;
1075
+ let parsedParams;
1076
+ if (snapshotValid) {
1077
+ matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]);
1078
+ routeParams = { ...snapshot.params };
1079
+ globalNotFoundRouteId = snapshot.globalNotFoundRouteId;
1080
+ parsedParams = snapshot.parsedParams;
1081
+ } else {
1082
+ const matchedRoutesResult = this.getMatchedRoutes(next.pathname);
1083
+ const { foundRoute, routeParams: rp } = matchedRoutesResult;
1084
+ routeParams = rp;
1085
+ matchedRoutes = matchedRoutesResult.matchedRoutes;
1086
+ parsedParams = matchedRoutesResult.parsedParams;
1087
+ let isGlobalNotFound = false;
1088
+ if (
1089
+ // If we found a route, and it's not an index route and we have left over path
1090
+ foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
1091
+ // Or if we didn't find a route and we have left over path
1092
+ path.trimPathRight(next.pathname)
1093
+ )
1094
+ ) {
1095
+ if (this.options.notFoundRoute) {
1096
+ matchedRoutes = [...matchedRoutes, this.options.notFoundRoute];
1097
+ } else {
1098
+ isGlobalNotFound = true;
1099
+ }
1053
1100
  }
1054
- if (this.options.notFoundMode !== "root") {
1055
- for (let i = matchedRoutes.length - 1; i >= 0; i--) {
1056
- const route = matchedRoutes[i];
1057
- if (route.children) {
1058
- return route.id;
1101
+ globalNotFoundRouteId = (() => {
1102
+ if (!isGlobalNotFound) {
1103
+ return void 0;
1104
+ }
1105
+ if (this.options.notFoundMode !== "root") {
1106
+ for (let i = matchedRoutes.length - 1; i >= 0; i--) {
1107
+ const route = matchedRoutes[i];
1108
+ if (route.children) {
1109
+ return route.id;
1110
+ }
1059
1111
  }
1060
1112
  }
1061
- }
1062
- return root.rootRouteId;
1063
- })();
1113
+ return root.rootRouteId;
1114
+ })();
1115
+ }
1064
1116
  const matches = [];
1065
1117
  const getParentContext = (parentMatch) => {
1066
1118
  const parentMatchId = parentMatch?.id;
1067
1119
  const parentContext = !parentMatchId ? this.options.context ?? void 0 : parentMatch.context ?? this.options.context ?? void 0;
1068
1120
  return parentContext;
1069
1121
  };
1122
+ const canUseCachedSearch = snapshotValid && snapshot.searchStr === next.searchStr && snapshot.validatedSearches?.length === matchedRoutes.length;
1123
+ const validatedSearchesToCache = [];
1070
1124
  matchedRoutes.forEach((route, index) => {
1071
1125
  const parentMatch = matches[index - 1];
1072
1126
  const [preMatchSearch, strictMatchSearch, searchError] = (() => {
1127
+ if (canUseCachedSearch) {
1128
+ const cached = snapshot.validatedSearches[index];
1129
+ return [cached.search, cached.strictSearch, void 0];
1130
+ }
1073
1131
  const parentSearch = parentMatch?.search ?? next.search;
1074
1132
  const parentStrictSearch = parentMatch?._strictSearch ?? void 0;
1075
1133
  try {
@@ -1095,6 +1153,12 @@ class RouterCore {
1095
1153
  return [parentSearch, {}, searchParamError];
1096
1154
  }
1097
1155
  })();
1156
+ if (!canUseCachedSearch) {
1157
+ validatedSearchesToCache.push({
1158
+ search: preMatchSearch,
1159
+ strictSearch: strictMatchSearch
1160
+ });
1161
+ }
1098
1162
  const loaderDeps = route.options.loaderDeps?.({
1099
1163
  search: preMatchSearch
1100
1164
  }) ?? "";
@@ -1208,6 +1272,13 @@ class RouterCore {
1208
1272
  };
1209
1273
  matches.push(match);
1210
1274
  });
1275
+ if (!canUseCachedSearch && validatedSearchesToCache.length > 0) {
1276
+ const existingSnapshot = next.state?.__TSR_matches;
1277
+ if (existingSnapshot) {
1278
+ existingSnapshot.searchStr = next.searchStr;
1279
+ existingSnapshot.validatedSearches = validatedSearchesToCache;
1280
+ }
1281
+ }
1211
1282
  matches.forEach((match, index) => {
1212
1283
  const route = this.looseRoutesById[match.routeId];
1213
1284
  const existingMatch = this.getMatch(match.id);
@@ -1295,7 +1366,7 @@ function getMatchedRoutes({
1295
1366
  const routeParams = {};
1296
1367
  const trimmedPath = path.trimPathRight(pathname);
1297
1368
  let foundRoute = void 0;
1298
- let parsedParams = void 0;
1369
+ let parsedParams = {};
1299
1370
  const match = newProcessRouteTree.findRouteMatch(trimmedPath, processedTree, true);
1300
1371
  if (match) {
1301
1372
  foundRoute = match.route;
@@ -1305,6 +1376,64 @@ function getMatchedRoutes({
1305
1376
  const matchedRoutes = match?.branch || [routesById[root.rootRouteId]];
1306
1377
  return { matchedRoutes, routeParams, foundRoute, parsedParams };
1307
1378
  }
1379
+ function buildMatchSnapshot({
1380
+ matchResult,
1381
+ pathname,
1382
+ searchStr,
1383
+ notFoundRoute,
1384
+ notFoundMode
1385
+ }) {
1386
+ const snapshot = {
1387
+ routeIds: matchResult.matchedRoutes.map((r) => r.id),
1388
+ params: matchResult.routeParams,
1389
+ parsedParams: matchResult.parsedParams,
1390
+ searchStr
1391
+ };
1392
+ const isGlobalNotFound = matchResult.foundRoute ? matchResult.foundRoute.path !== "/" && matchResult.routeParams["**"] : path.trimPathRight(pathname);
1393
+ if (isGlobalNotFound) {
1394
+ if (notFoundRoute) {
1395
+ snapshot.globalNotFoundRouteId = notFoundRoute.id;
1396
+ } else {
1397
+ if (notFoundMode !== "root") {
1398
+ for (let i = matchResult.matchedRoutes.length - 1; i >= 0; i--) {
1399
+ const route = matchResult.matchedRoutes[i];
1400
+ if (route.children) {
1401
+ snapshot.globalNotFoundRouteId = route.id;
1402
+ break;
1403
+ }
1404
+ }
1405
+ }
1406
+ if (!snapshot.globalNotFoundRouteId) {
1407
+ snapshot.globalNotFoundRouteId = root.rootRouteId;
1408
+ }
1409
+ }
1410
+ }
1411
+ return snapshot;
1412
+ }
1413
+ function buildMatchSnapshotFromRoutes({
1414
+ routes,
1415
+ params,
1416
+ searchStr,
1417
+ globalNotFoundRouteId
1418
+ }) {
1419
+ const stringParams = {};
1420
+ for (const key in params) {
1421
+ const value = params[key];
1422
+ if (value != null) {
1423
+ stringParams[key] = String(value);
1424
+ }
1425
+ }
1426
+ const snapshot = {
1427
+ routeIds: routes.map((r) => r.id),
1428
+ params: stringParams,
1429
+ parsedParams: params,
1430
+ searchStr
1431
+ };
1432
+ if (globalNotFoundRouteId) {
1433
+ snapshot.globalNotFoundRouteId = globalNotFoundRouteId;
1434
+ }
1435
+ return snapshot;
1436
+ }
1308
1437
  function applySearchMiddleware({
1309
1438
  search,
1310
1439
  dest,
@@ -1385,6 +1514,8 @@ function applySearchMiddleware({
1385
1514
  exports.PathParamError = PathParamError;
1386
1515
  exports.RouterCore = RouterCore;
1387
1516
  exports.SearchParamError = SearchParamError;
1517
+ exports.buildMatchSnapshot = buildMatchSnapshot;
1518
+ exports.buildMatchSnapshotFromRoutes = buildMatchSnapshotFromRoutes;
1388
1519
  exports.defaultSerializeError = defaultSerializeError;
1389
1520
  exports.getInitialRouterState = getInitialRouterState;
1390
1521
  exports.getLocationChangeInfo = getLocationChangeInfo;