@tanstack/router-core 1.154.13 → 1.155.0

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.
@@ -27,10 +27,19 @@ interface ResolvePathOptions {
27
27
  * and supporting relative segments (`.`/`..`) and absolute `to` values.
28
28
  */
29
29
  export declare function resolvePath({ base, to, trailingSlash, cache, }: ResolvePathOptions): string;
30
+ /**
31
+ * Create a pre-compiled decode config from allowed characters.
32
+ * This should be called once at router initialization.
33
+ */
34
+ export declare function compileDecodeCharMap(pathParamsAllowedCharacters: ReadonlyArray<string>): (encoded: string) => string;
30
35
  interface InterpolatePathOptions {
31
36
  path?: string;
32
37
  params: Record<string, unknown>;
33
- decodeCharMap?: Map<string, string>;
38
+ /**
39
+ * A function that decodes a path parameter value.
40
+ * Obtained from `compileDecodeCharMap(pathParamsAllowedCharacters)`.
41
+ */
42
+ decoder?: (encoded: string) => string;
34
43
  }
35
44
  type InterPolatePathResult = {
36
45
  interpolatedPath: string;
@@ -43,5 +52,5 @@ type InterPolatePathResult = {
43
52
  * - Encodes params safely (configurable allowed characters)
44
53
  * - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards
45
54
  */
46
- export declare function interpolatePath({ path, params, decodeCharMap, }: InterpolatePathOptions): InterPolatePathResult;
55
+ export declare function interpolatePath({ path, params, decoder, }: InterpolatePathOptions): InterPolatePathResult;
47
56
  export {};
@@ -49,6 +49,7 @@ class RouterCore {
49
49
  this.tempLocationKey = `${Math.round(
50
50
  Math.random() * 1e7
51
51
  )}`;
52
+ this.resetNextScroll = true;
52
53
  this.shouldViewTransition = void 0;
53
54
  this.isViewTransitionTypesSupported = void 0;
54
55
  this.subscribers = /* @__PURE__ */ new Set();
@@ -70,12 +71,10 @@ class RouterCore {
70
71
  ...newOptions
71
72
  };
72
73
  this.isServer = this.options.isServer ?? typeof document === "undefined";
73
- this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters ? new Map(
74
- this.options.pathParamsAllowedCharacters.map((char) => [
75
- encodeURIComponent(char),
76
- char
77
- ])
78
- ) : void 0;
74
+ if (this.options.pathParamsAllowedCharacters)
75
+ this.pathParamsDecoder = path.compileDecodeCharMap(
76
+ this.options.pathParamsAllowedCharacters
77
+ );
79
78
  if (!this.history || this.options.history && this.options.history !== this.history) {
80
79
  if (!this.options.history) {
81
80
  if (!this.isServer) {
@@ -250,9 +249,9 @@ class RouterCore {
250
249
  search: locationSearchOrOpts
251
250
  },
252
251
  opts
253
- ).matches;
252
+ );
254
253
  }
255
- return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts).matches;
254
+ return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts);
256
255
  };
257
256
  this.getMatchedRoutes = (pathname) => {
258
257
  return getMatchedRoutes({
@@ -318,14 +317,12 @@ class RouterCore {
318
317
  }).interpolatedPath;
319
318
  const destMatchResult = this.getMatchedRoutes(interpolatedNextTo);
320
319
  let destRoutes = destMatchResult.matchedRoutes;
321
- const rawParams = destMatchResult.routeParams;
322
320
  const isGlobalNotFound = destMatchResult.foundRoute ? destMatchResult.foundRoute.path !== "/" && destMatchResult.routeParams["**"] : path.trimPathRight(interpolatedNextTo);
323
- let globalNotFoundRouteId;
324
321
  if (isGlobalNotFound) {
325
322
  if (this.options.notFoundRoute) {
326
323
  destRoutes = [...destRoutes, this.options.notFoundRoute];
327
324
  } else {
328
- globalNotFoundRouteId = findGlobalNotFoundRouteId(
325
+ findGlobalNotFoundRouteId(
329
326
  this.options.notFoundMode,
330
327
  destRoutes
331
328
  );
@@ -347,7 +344,7 @@ class RouterCore {
347
344
  path.interpolatePath({
348
345
  path: nextTo,
349
346
  params: nextParams,
350
- decodeCharMap: this.pathParamsDecodeCharMap
347
+ decoder: this.pathParamsDecoder
351
348
  }).interpolatedPath
352
349
  );
353
350
  let nextSearch = fromSearch;
@@ -381,31 +378,19 @@ class RouterCore {
381
378
  const hashStr = hash ? `#${hash}` : "";
382
379
  let nextState = dest.state === true ? currentLocation.state : dest.state ? utils.functionalUpdate(dest.state, currentLocation.state) : {};
383
380
  nextState = utils.replaceEqualDeep(currentLocation.state, nextState);
384
- const snapshotParams = {
385
- ...rawParams,
386
- ...nextParams
387
- };
388
- const matchSnapshot = buildMatchSnapshotFromRoutes({
389
- routes: destRoutes,
390
- params: snapshotParams,
391
- searchStr,
392
- globalNotFoundRouteId
393
- });
394
381
  const fullPath = `${nextPathname}${searchStr}${hashStr}`;
395
382
  const url = new URL(fullPath, this.origin);
396
383
  const rewrittenUrl = rewrite.executeRewriteOutput(this.rewrite, url);
397
- const encodedHref = url.href.replace(url.origin, "");
398
384
  return {
399
385
  publicHref: rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
400
- href: encodedHref,
386
+ href: fullPath,
401
387
  url: rewrittenUrl,
402
388
  pathname: nextPathname,
403
389
  search: nextSearch,
404
390
  searchStr,
405
391
  state: nextState,
406
392
  hash: hash ?? "",
407
- unmaskOnReload: dest.unmaskOnReload,
408
- _matchSnapshot: matchSnapshot
393
+ unmaskOnReload: dest.unmaskOnReload
409
394
  };
410
395
  };
411
396
  const buildWithMatches = (dest = {}, maskedDest) => {
@@ -477,73 +462,53 @@ class RouterCore {
477
462
  });
478
463
  if (isSameUrl && isSameState()) {
479
464
  this.load();
480
- return this.commitLocationPromise;
481
- }
482
- let {
483
- // eslint-disable-next-line prefer-const
484
- maskedLocation,
485
- // eslint-disable-next-line prefer-const
486
- hashScrollIntoView,
487
- // don't pass url into history since it is a URL instance that cannot be serialized
488
- // eslint-disable-next-line prefer-const
489
- url: _url,
490
- ...nextHistory
491
- } = next;
492
- if (maskedLocation) {
493
- nextHistory = {
494
- ...maskedLocation,
495
- state: {
496
- ...maskedLocation.state,
497
- __tempKey: void 0,
498
- __tempLocation: {
499
- ...nextHistory,
500
- search: nextHistory.searchStr,
501
- state: {
502
- ...nextHistory.state,
503
- __tempKey: void 0,
504
- __tempLocation: void 0,
505
- __TSR_key: void 0,
506
- key: void 0
507
- // TODO: Remove in v2 - use __TSR_key instead
465
+ } else {
466
+ let {
467
+ // eslint-disable-next-line prefer-const
468
+ maskedLocation,
469
+ // eslint-disable-next-line prefer-const
470
+ hashScrollIntoView,
471
+ // don't pass url into history since it is a URL instance that cannot be serialized
472
+ // eslint-disable-next-line prefer-const
473
+ url: _url,
474
+ ...nextHistory
475
+ } = next;
476
+ if (maskedLocation) {
477
+ nextHistory = {
478
+ ...maskedLocation,
479
+ state: {
480
+ ...maskedLocation.state,
481
+ __tempKey: void 0,
482
+ __tempLocation: {
483
+ ...nextHistory,
484
+ search: nextHistory.searchStr,
485
+ state: {
486
+ ...nextHistory.state,
487
+ __tempKey: void 0,
488
+ __tempLocation: void 0,
489
+ __TSR_key: void 0,
490
+ key: void 0
491
+ // TODO: Remove in v2 - use __TSR_key instead
492
+ }
508
493
  }
509
494
  }
495
+ };
496
+ if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
497
+ nextHistory.state.__tempKey = this.tempLocationKey;
510
498
  }
511
- };
512
- if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
513
- nextHistory.state.__tempKey = this.tempLocationKey;
514
499
  }
500
+ nextHistory.state.__hashScrollIntoViewOptions = hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true;
501
+ this.shouldViewTransition = viewTransition;
502
+ this.history[next.replace ? "replace" : "push"](
503
+ nextHistory.publicHref,
504
+ nextHistory.state,
505
+ { ignoreBlocker }
506
+ );
515
507
  }
516
- nextHistory.state.__hashScrollIntoViewOptions = hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true;
517
- nextHistory.state.__TSR_resetScroll = next.resetScroll ?? true;
518
- this.shouldViewTransition = viewTransition;
519
- nextHistory.state.__TSR_sessionId = this.sessionId;
520
- nextHistory.state.__TSR_matches = next._matchSnapshot ?? buildMatchSnapshot({
521
- matchResult: this.getMatchedRoutes(next.pathname),
522
- pathname: next.pathname,
523
- searchStr: next.searchStr,
524
- notFoundRoute: this.options.notFoundRoute,
525
- notFoundMode: this.options.notFoundMode
526
- });
527
- const precomputedLocation = {
528
- ...next,
529
- publicHref: nextHistory.publicHref,
530
- state: nextHistory.state,
531
- maskedLocation
532
- };
533
- const result = await this.history[next.replace ? "replace" : "push"](
534
- nextHistory.publicHref,
535
- nextHistory.state,
536
- { ignoreBlocker, skipTransitionerLoad: true }
537
- );
538
- if (result.type === "BLOCKED") {
539
- this.commitLocationPromise?.resolve();
540
- return this.commitLocationPromise;
541
- }
542
- if (this.history.location.href !== nextHistory.publicHref) {
543
- return this.commitLocationPromise;
508
+ this.resetNextScroll = next.resetScroll ?? true;
509
+ if (!this.history.subscribers.size) {
510
+ this.load();
544
511
  }
545
- this.latestLocation = precomputedLocation;
546
- this.load({ _skipUpdateLatestLocation: true });
547
512
  return this.commitLocationPromise;
548
513
  };
549
514
  this.buildAndCommitLocation = ({
@@ -650,11 +615,9 @@ class RouterCore {
650
615
  _isNavigate: true
651
616
  });
652
617
  };
653
- this.beforeLoad = (opts) => {
618
+ this.beforeLoad = () => {
654
619
  this.cancelMatches();
655
- if (!opts?._skipUpdateLatestLocation) {
656
- this.updateLatestLocation();
657
- }
620
+ this.updateLatestLocation();
658
621
  if (this.isServer) {
659
622
  const nextLocation = this.buildLocation({
660
623
  to: this.latestLocation.pathname,
@@ -669,8 +632,7 @@ class RouterCore {
669
632
  throw redirect.redirect({ href });
670
633
  }
671
634
  }
672
- const snapshot = this.latestLocation.state.__TSR_sessionId === this.sessionId ? this.latestLocation.state.__TSR_matches : void 0;
673
- const pendingMatches = this.matchRoutes(this.latestLocation, { snapshot });
635
+ const pendingMatches = this.matchRoutes(this.latestLocation);
674
636
  this.__store.setState((s) => ({
675
637
  ...s,
676
638
  status: "pending",
@@ -691,9 +653,7 @@ class RouterCore {
691
653
  loadPromise = new Promise((resolve) => {
692
654
  this.startTransition(async () => {
693
655
  try {
694
- this.beforeLoad({
695
- _skipUpdateLatestLocation: opts?._skipUpdateLatestLocation
696
- });
656
+ this.beforeLoad();
697
657
  const next = this.latestLocation;
698
658
  const prevLocation = this.state.resolvedLocation;
699
659
  if (!this.state.redirect) {
@@ -1050,7 +1010,6 @@ class RouterCore {
1050
1010
  (d) => d.status === "notFound" || d.globalNotFound
1051
1011
  );
1052
1012
  };
1053
- this.sessionId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1054
1013
  this.update({
1055
1014
  defaultPreloadDelay: 50,
1056
1015
  defaultPendingMs: 1e3,
@@ -1079,57 +1038,33 @@ class RouterCore {
1079
1038
  return this.routesById;
1080
1039
  }
1081
1040
  matchRoutesInternal(next, opts) {
1082
- const snapshot = opts?.snapshot;
1083
- const snapshotValid = snapshot && snapshot.routeIds.length > 0 && snapshot.routeIds.every((id) => this.routesById[id]);
1084
- let matchedRoutes;
1085
- let routeParams;
1086
- let rawParams;
1087
- let globalNotFoundRouteId;
1088
- let parsedParams;
1089
- if (snapshotValid) {
1090
- matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]);
1091
- routeParams = { ...snapshot.params };
1092
- rawParams = { ...snapshot.params };
1093
- globalNotFoundRouteId = snapshot.globalNotFoundRouteId;
1094
- parsedParams = snapshot.parsedParams;
1095
- } else {
1096
- const matchedRoutesResult = this.getMatchedRoutes(next.pathname);
1097
- const { foundRoute, routeParams: rp } = matchedRoutesResult;
1098
- routeParams = rp;
1099
- rawParams = { ...rp };
1100
- matchedRoutes = matchedRoutesResult.matchedRoutes;
1101
- parsedParams = matchedRoutesResult.parsedParams;
1102
- let isGlobalNotFound = false;
1103
- if (
1104
- // If we found a route, and it's not an index route and we have left over path
1105
- foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
1106
- // Or if we didn't find a route and we have left over path
1107
- path.trimPathRight(next.pathname)
1108
- )
1109
- ) {
1110
- if (this.options.notFoundRoute) {
1111
- matchedRoutes = [...matchedRoutes, this.options.notFoundRoute];
1112
- } else {
1113
- isGlobalNotFound = true;
1114
- }
1041
+ const matchedRoutesResult = this.getMatchedRoutes(next.pathname);
1042
+ const { foundRoute, routeParams, parsedParams } = matchedRoutesResult;
1043
+ let { matchedRoutes } = matchedRoutesResult;
1044
+ let isGlobalNotFound = false;
1045
+ if (
1046
+ // If we found a route, and it's not an index route and we have left over path
1047
+ foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
1048
+ // Or if we didn't find a route and we have left over path
1049
+ path.trimPathRight(next.pathname)
1050
+ )
1051
+ ) {
1052
+ if (this.options.notFoundRoute) {
1053
+ matchedRoutes = [...matchedRoutes, this.options.notFoundRoute];
1054
+ } else {
1055
+ isGlobalNotFound = true;
1115
1056
  }
1116
- globalNotFoundRouteId = isGlobalNotFound ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes) : void 0;
1117
1057
  }
1058
+ const globalNotFoundRouteId = isGlobalNotFound ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes) : void 0;
1118
1059
  const matches = [];
1119
1060
  const getParentContext = (parentMatch) => {
1120
1061
  const parentMatchId = parentMatch?.id;
1121
1062
  const parentContext = !parentMatchId ? this.options.context ?? void 0 : parentMatch.context ?? this.options.context ?? void 0;
1122
1063
  return parentContext;
1123
1064
  };
1124
- const canUseCachedSearch = snapshotValid && snapshot.searchStr === next.searchStr && snapshot.validatedSearches?.length === matchedRoutes.length;
1125
- const validatedSearchesToCache = [];
1126
1065
  matchedRoutes.forEach((route, index) => {
1127
1066
  const parentMatch = matches[index - 1];
1128
1067
  const [preMatchSearch, strictMatchSearch, searchError] = (() => {
1129
- if (canUseCachedSearch) {
1130
- const cached = snapshot.validatedSearches[index];
1131
- return [cached.search, cached.strictSearch, void 0];
1132
- }
1133
1068
  const parentSearch = parentMatch?.search ?? next.search;
1134
1069
  const parentStrictSearch = parentMatch?._strictSearch ?? void 0;
1135
1070
  try {
@@ -1155,12 +1090,6 @@ class RouterCore {
1155
1090
  return [parentSearch, {}, searchParamError];
1156
1091
  }
1157
1092
  })();
1158
- if (!canUseCachedSearch) {
1159
- validatedSearchesToCache.push({
1160
- search: preMatchSearch,
1161
- strictSearch: strictMatchSearch
1162
- });
1163
- }
1164
1093
  const loaderDeps = route.options.loaderDeps?.({
1165
1094
  search: preMatchSearch
1166
1095
  }) ?? "";
@@ -1168,7 +1097,7 @@ class RouterCore {
1168
1097
  const { interpolatedPath, usedParams } = path.interpolatePath({
1169
1098
  path: route.fullPath,
1170
1099
  params: routeParams,
1171
- decodeCharMap: this.pathParamsDecodeCharMap
1100
+ decoder: this.pathParamsDecoder
1172
1101
  });
1173
1102
  const matchId = (
1174
1103
  // route.id for disambiguation
@@ -1274,13 +1203,6 @@ class RouterCore {
1274
1203
  };
1275
1204
  matches.push(match);
1276
1205
  });
1277
- if (!canUseCachedSearch && validatedSearchesToCache.length > 0) {
1278
- const existingSnapshot = next.state?.__TSR_matches;
1279
- if (existingSnapshot) {
1280
- existingSnapshot.searchStr = next.searchStr;
1281
- existingSnapshot.validatedSearches = validatedSearchesToCache;
1282
- }
1283
- }
1284
1206
  matches.forEach((match, index) => {
1285
1207
  const route = this.looseRoutesById[match.routeId];
1286
1208
  const existingMatch = this.getMatch(match.id);
@@ -1309,7 +1231,7 @@ class RouterCore {
1309
1231
  };
1310
1232
  }
1311
1233
  });
1312
- return { matches, rawParams };
1234
+ return matches;
1313
1235
  }
1314
1236
  }
1315
1237
  class SearchParamError extends Error {
@@ -1368,7 +1290,7 @@ function getMatchedRoutes({
1368
1290
  const routeParams = {};
1369
1291
  const trimmedPath = path.trimPathRight(pathname);
1370
1292
  let foundRoute = void 0;
1371
- let parsedParams = {};
1293
+ let parsedParams = void 0;
1372
1294
  const match = newProcessRouteTree.findRouteMatch(trimmedPath, processedTree, true);
1373
1295
  if (match) {
1374
1296
  foundRoute = match.route;
@@ -1378,64 +1300,6 @@ function getMatchedRoutes({
1378
1300
  const matchedRoutes = match?.branch || [routesById[root.rootRouteId]];
1379
1301
  return { matchedRoutes, routeParams, foundRoute, parsedParams };
1380
1302
  }
1381
- function buildMatchSnapshot({
1382
- matchResult,
1383
- pathname,
1384
- searchStr,
1385
- notFoundRoute,
1386
- notFoundMode
1387
- }) {
1388
- const snapshot = {
1389
- routeIds: matchResult.matchedRoutes.map((r) => r.id),
1390
- params: matchResult.routeParams,
1391
- parsedParams: matchResult.parsedParams,
1392
- searchStr
1393
- };
1394
- const isGlobalNotFound = matchResult.foundRoute ? matchResult.foundRoute.path !== "/" && matchResult.routeParams["**"] : path.trimPathRight(pathname);
1395
- if (isGlobalNotFound) {
1396
- if (notFoundRoute) {
1397
- snapshot.globalNotFoundRouteId = notFoundRoute.id;
1398
- } else {
1399
- if (notFoundMode !== "root") {
1400
- for (let i = matchResult.matchedRoutes.length - 1; i >= 0; i--) {
1401
- const route = matchResult.matchedRoutes[i];
1402
- if (route.children) {
1403
- snapshot.globalNotFoundRouteId = route.id;
1404
- break;
1405
- }
1406
- }
1407
- }
1408
- if (!snapshot.globalNotFoundRouteId) {
1409
- snapshot.globalNotFoundRouteId = root.rootRouteId;
1410
- }
1411
- }
1412
- }
1413
- return snapshot;
1414
- }
1415
- function buildMatchSnapshotFromRoutes({
1416
- routes,
1417
- params,
1418
- searchStr,
1419
- globalNotFoundRouteId
1420
- }) {
1421
- const stringParams = {};
1422
- for (const key in params) {
1423
- const value = params[key];
1424
- if (value != null) {
1425
- stringParams[key] = String(value);
1426
- }
1427
- }
1428
- const snapshot = {
1429
- routeIds: routes.map((r) => r.id),
1430
- params: stringParams,
1431
- parsedParams: params,
1432
- searchStr
1433
- };
1434
- if (globalNotFoundRouteId) {
1435
- snapshot.globalNotFoundRouteId = globalNotFoundRouteId;
1436
- }
1437
- return snapshot;
1438
- }
1439
1303
  function applySearchMiddleware({
1440
1304
  search,
1441
1305
  dest,
@@ -1534,8 +1398,6 @@ function findGlobalNotFoundRouteId(notFoundMode, routes) {
1534
1398
  exports.PathParamError = PathParamError;
1535
1399
  exports.RouterCore = RouterCore;
1536
1400
  exports.SearchParamError = SearchParamError;
1537
- exports.buildMatchSnapshot = buildMatchSnapshot;
1538
- exports.buildMatchSnapshotFromRoutes = buildMatchSnapshotFromRoutes;
1539
1401
  exports.defaultSerializeError = defaultSerializeError;
1540
1402
  exports.getInitialRouterState = getInitialRouterState;
1541
1403
  exports.getLocationChangeInfo = getLocationChangeInfo;