@tanstack/router-core 1.171.23 → 1.171.25

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.
@@ -82,17 +82,51 @@ function normalize(value: unknown, rejected: boolean): LoaderOutcome {
82
82
  return rejected ? [ERROR, value] : [SUCCESS, value]
83
83
  }
84
84
 
85
- function normalizeError(route: AnyRoute, cause: unknown): LoaderOutcome {
85
+ function normalizeError(
86
+ router: AnyRouter,
87
+ lane: { location: ParsedLocation },
88
+ route: AnyRoute,
89
+ cause: unknown,
90
+ signal?: AbortSignal,
91
+ notify = true,
92
+ ): LoaderOutcome {
93
+ signal?.throwIfAborted()
86
94
  let outcome = normalize(cause, true)
87
95
  if (outcome[0] !== ERROR) {
88
- return outcome
96
+ return materializeRedirect(router, lane, route, outcome, signal, notify)
89
97
  }
90
98
  try {
91
99
  route.options.onError?.(outcome[1])
92
100
  } catch (onErrorCause) {
93
101
  outcome = normalize(onErrorCause, true)
94
102
  }
95
- return outcome
103
+ signal?.throwIfAborted()
104
+ return materializeRedirect(router, lane, route, outcome, signal, notify)
105
+ }
106
+
107
+ function materializeRedirect(
108
+ router: AnyRouter,
109
+ lane: { location: ParsedLocation },
110
+ route: AnyRoute,
111
+ outcome: LoaderOutcome,
112
+ signal?: AbortSignal,
113
+ notify = true,
114
+ ): LoaderOutcome {
115
+ if (outcome[0] !== REDIRECTED) {
116
+ return outcome
117
+ }
118
+ signal?.throwIfAborted()
119
+ try {
120
+ outcome[1].options._fromLocation = lane.location
121
+ router.resolveRedirect(outcome[1])
122
+ signal?.throwIfAborted()
123
+ return outcome
124
+ } catch (cause) {
125
+ signal?.throwIfAborted()
126
+ return notify
127
+ ? normalizeError(router, lane, route, cause, signal, false)
128
+ : [ERROR, cause]
129
+ }
96
130
  }
97
131
 
98
132
  function maybe<TValue>(
@@ -197,7 +231,13 @@ async function contextualize(
197
231
  match.ssr = await resolveSsr(router, lane, index)
198
232
  } catch (cause) {
199
233
  signal?.throwIfAborted()
200
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
234
+ failure = [
235
+ index,
236
+ stampNotFound(
237
+ match,
238
+ normalizeError(router, lane, route, cause, signal),
239
+ ),
240
+ ]
201
241
  end = index
202
242
  }
203
243
  signal?.throwIfAborted()
@@ -239,7 +279,13 @@ async function contextualize(
239
279
  } catch (cause) {
240
280
  signal?.throwIfAborted()
241
281
  if (!failure) {
242
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
282
+ failure = [
283
+ index,
284
+ stampNotFound(
285
+ match,
286
+ normalizeError(router, lane, route, cause, signal),
287
+ ),
288
+ ]
243
289
  }
244
290
  end = index
245
291
  break
@@ -252,7 +298,10 @@ async function contextualize(
252
298
  if (validationError !== undefined) {
253
299
  failure = [
254
300
  index,
255
- stampNotFound(match, normalizeError(route, validationError)),
301
+ stampNotFound(
302
+ match,
303
+ normalizeError(router, lane, route, validationError, signal),
304
+ ),
256
305
  ]
257
306
  end = index
258
307
  break
@@ -293,7 +342,16 @@ async function contextualize(
293
342
  try {
294
343
  const beforeLoadContext = await route.options.beforeLoad(options)
295
344
  signal?.throwIfAborted()
296
- const outcome = stampNotFound(match, normalize(beforeLoadContext, false))
345
+ const outcome = stampNotFound(
346
+ match,
347
+ materializeRedirect(
348
+ router,
349
+ lane,
350
+ route,
351
+ normalize(beforeLoadContext, false),
352
+ signal,
353
+ ),
354
+ )
297
355
  if (outcome[0] !== SUCCESS) {
298
356
  failure = [index, outcome]
299
357
  end = index
@@ -307,7 +365,13 @@ async function contextualize(
307
365
  parentContext = match.context
308
366
  } catch (cause) {
309
367
  signal?.throwIfAborted()
310
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
368
+ failure = [
369
+ index,
370
+ stampNotFound(
371
+ match,
372
+ normalizeError(router, lane, route, cause, signal),
373
+ ),
374
+ ]
311
375
  end = index
312
376
  break
313
377
  }
@@ -373,14 +437,13 @@ function createLoaderTask(
373
437
  (cause) => normalize(cause, true),
374
438
  )
375
439
  .then((result): LoaderOutcome => {
376
- if (
377
- result[0] !== REDIRECTED &&
378
- (signal?.aborted || match.abortController.signal.reason === lane)
379
- ) {
440
+ if (signal?.aborted || match.abortController.signal.reason === lane) {
380
441
  return [SKIPPED]
381
442
  }
382
443
  if (result[0] === ERROR) {
383
- result = normalizeError(route, result[1])
444
+ result = normalizeError(router, lane, route, result[1], signal)
445
+ } else {
446
+ result = materializeRedirect(router, lane, route, result, signal)
384
447
  }
385
448
  return stampNotFound(match, result)
386
449
  })
@@ -424,13 +487,13 @@ async function getNotFoundBoundary(
424
487
  }
425
488
  for (let candidate = index; candidate >= 0; candidate--) {
426
489
  const route = getRoute(router, matches[candidate]!)
427
- const loading = loadRouteChunk(route, false)
428
- if (loading) {
429
- try {
490
+ try {
491
+ const loading = loadRouteChunk(route, false)
492
+ if (loading) {
430
493
  await loading
431
- } catch {
432
- signal?.throwIfAborted()
433
494
  }
495
+ } catch {
496
+ signal?.throwIfAborted()
434
497
  }
435
498
  signal?.throwIfAborted()
436
499
  if (route.options.notFoundComponent) {
@@ -450,15 +513,6 @@ function abortMatches(
450
513
  }
451
514
  }
452
515
 
453
- function resolveServerRedirect(
454
- router: AnyRouter,
455
- location: ParsedLocation,
456
- value: AnyRedirect,
457
- ): ServerLoadResult {
458
- value.options._fromLocation = location
459
- return { type: 'redirect', redirect: router.resolveRedirect(value) }
460
- }
461
-
462
516
  async function applyFailure(
463
517
  router: AnyRouter,
464
518
  lane: ContextualizedLane,
@@ -530,7 +584,10 @@ async function loadNormalChunks(
530
584
  signal?.throwIfAborted()
531
585
  return [
532
586
  index,
533
- stampNotFound(match, normalizeError(route, cause)),
587
+ stampNotFound(
588
+ match,
589
+ normalizeError(router, lane, route, cause, signal),
590
+ ),
534
591
  ] as IndexedOutcome
535
592
  },
536
593
  )
@@ -540,7 +597,13 @@ async function loadNormalChunks(
540
597
  }
541
598
  } catch (cause) {
542
599
  signal?.throwIfAborted()
543
- chunks.push([index, stampNotFound(match, normalizeError(route, cause))])
600
+ chunks.push([
601
+ index,
602
+ stampNotFound(
603
+ match,
604
+ normalizeError(router, lane, route, cause, signal),
605
+ ),
606
+ ])
544
607
  }
545
608
  }
546
609
  for (const chunk of chunks) {
@@ -698,7 +761,7 @@ async function executeServerLane(
698
761
 
699
762
  if (control?.[1][0] === REDIRECTED) {
700
763
  abortMatches(lane.matches, 0, lane)
701
- return resolveServerRedirect(router, location, control[1][1])
764
+ return { type: 'redirect', redirect: control[1][1] }
702
765
  }
703
766
 
704
767
  let failure = lane.failure ?? loaderFailure
@@ -742,7 +805,7 @@ async function executeServerLane(
742
805
  if (requiredFailure) {
743
806
  if (requiredFailure[1][0] === REDIRECTED) {
744
807
  abortMatches(lane.matches)
745
- return resolveServerRedirect(router, location, requiredFailure[1][1])
808
+ return { type: 'redirect', redirect: requiredFailure[1][1] }
746
809
  }
747
810
  failure = requiredFailure
748
811
  }
@@ -808,9 +871,7 @@ export async function loadServerRoute(
808
871
  })
809
872
  if (next.publicHref !== canonical.publicHref) {
810
873
  const href = canonical.publicHref || '/'
811
- throw canonical.external
812
- ? redirect({ href })
813
- : redirect({ href, _builtLocation: canonical })
874
+ throw redirect({ href })
814
875
  }
815
876
 
816
877
  const fromLocation = router.stores.resolvedLocation.get()
@@ -828,7 +889,8 @@ export async function loadServerRoute(
828
889
  if (!isRedirect(cause)) {
829
890
  throw cause
830
891
  }
831
- result = resolveServerRedirect(router, next, cause)
892
+ cause.options._fromLocation = next
893
+ result = { type: 'redirect', redirect: router.resolveRedirect(cause) }
832
894
  }
833
895
 
834
896
  router._serverResult = result
@@ -27,18 +27,6 @@ type ExtendedSegmentKind =
27
27
  | typeof SEGMENT_TYPE_INDEX
28
28
  | typeof SEGMENT_TYPE_PATHLESS
29
29
 
30
- function getOpenAndCloseBraces(
31
- part: string,
32
- ): [openBrace: number, closeBrace: number] | null {
33
- const openBrace = part.indexOf('{')
34
- if (openBrace === -1) return null
35
- const closeBrace = part.indexOf('}', openBrace)
36
- if (closeBrace === -1) return null
37
- const afterOpen = openBrace + 1
38
- if (afterOpen >= part.length) return null
39
- return [openBrace, closeBrace]
40
- }
41
-
42
30
  type ParsedSegment = Uint16Array & {
43
31
  /** segment type (0 = pathname, 1 = param, 2 = wildcard, 3 = optional param) */
44
32
  0: SegmentKind
@@ -116,9 +104,13 @@ export function parseSegment(
116
104
  return output as ParsedSegment
117
105
  }
118
106
 
119
- const braces = getOpenAndCloseBraces(part)
120
- if (braces) {
121
- const [openBrace, closeBrace] = braces
107
+ const openBrace = part.indexOf('{')
108
+ let closeBrace
109
+ if (
110
+ openBrace !== -1 &&
111
+ openBrace + 1 < part.length &&
112
+ (closeBrace = part.indexOf('}', openBrace)) !== -1
113
+ ) {
122
114
  const firstChar = part.charCodeAt(openBrace + 1)
123
115
 
124
116
  // Check for {-$...} (optional param)
package/src/redirect.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { NavigateOptions } from './link'
2
2
  import type { AnyRouter, RegisteredRouter } from './router'
3
- import type { ParsedLocation } from './location'
4
3
 
5
4
  export type AnyRedirect = Redirect<any, any, any, any, any>
6
5
 
@@ -14,13 +13,7 @@ export type Redirect<
14
13
  TMaskFrom extends string = TFrom,
15
14
  TMaskTo extends string = '.',
16
15
  > = Response & {
17
- options: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
18
- /**
19
- * @internal
20
- * A **trusted** built location that can be used to redirect to.
21
- */
22
- _builtLocation?: ParsedLocation
23
- }
16
+ options: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
24
17
  }
25
18
 
26
19
  export type RedirectOptions<
@@ -50,11 +43,6 @@ export type RedirectOptions<
50
43
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RedirectType#headers-property)
51
44
  */
52
45
  headers?: HeadersInit
53
- /**
54
- * @internal
55
- * A **trusted** built location that can be used to redirect to.
56
- */
57
- _builtLocation?: ParsedLocation
58
46
  } & NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
59
47
 
60
48
  export type ResolvedRedirect<
@@ -122,11 +110,7 @@ export function redirect<
122
110
  ): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> {
123
111
  opts.statusCode = opts.statusCode || opts.code || 307
124
112
 
125
- if (
126
- !opts._builtLocation &&
127
- !opts.reloadDocument &&
128
- typeof opts.href === 'string'
129
- ) {
113
+ if (!opts.reloadDocument && typeof opts.href === 'string') {
130
114
  try {
131
115
  new URL(opts.href)
132
116
  opts.reloadDocument = true
package/src/router.ts CHANGED
@@ -692,13 +692,7 @@ export type PreloadRouteFn<
692
692
  TTo,
693
693
  TMaskFrom,
694
694
  TMaskTo
695
- > & {
696
- /**
697
- * @internal
698
- * A **trusted** built location that can be used to redirect to.
699
- */
700
- _builtLocation?: ParsedLocation
701
- },
695
+ >,
702
696
  ) => Promise<Array<AnyRouteMatch> | undefined>
703
697
 
704
698
  export type MatchRouteFn<
@@ -937,10 +931,10 @@ export function runRouteLifecycle(
937
931
  router: AnyRouter,
938
932
  previous: Array<AnyRouteMatch>,
939
933
  matches: Array<AnyRouteMatch>,
940
- isCurrent?: () => boolean,
934
+ owner?: LoadTransaction,
941
935
  ): void {
942
936
  for (const match of previous) {
943
- if (isCurrent?.() === false) {
937
+ if (owner && router._tx !== owner) {
944
938
  return
945
939
  }
946
940
  if (!matches.some((candidate) => candidate.routeId === match.routeId)) {
@@ -950,7 +944,7 @@ export function runRouteLifecycle(
950
944
  }
951
945
  }
952
946
  for (const match of matches) {
953
- if (isCurrent?.() === false) {
947
+ if (owner && router._tx !== owner) {
954
948
  return
955
949
  }
956
950
  const route = (router.routesById as Record<string, AnyRoute>)[
@@ -1846,6 +1840,19 @@ export class RouterCore<
1846
1840
  unmaskOnReload?: boolean
1847
1841
  } = {},
1848
1842
  ): ParsedLocation => {
1843
+ if (dest.href) {
1844
+ const parsed = parseHref(dest.href, {} as ParsedHistoryState)
1845
+ dest = {
1846
+ ...dest,
1847
+ to: executeRewriteInput(
1848
+ this.rewrite,
1849
+ new URL(parsed.pathname, this.origin),
1850
+ ).pathname,
1851
+ search: this.options.parseSearch(parsed.search),
1852
+ hash: parsed.hash.slice(1),
1853
+ }
1854
+ }
1855
+
1849
1856
  // We allow the caller to override the current location
1850
1857
  const currentLocation =
1851
1858
  dest._fromLocation || this._pendingLocation || this.latestLocation
@@ -2082,59 +2089,35 @@ export class RouterCore<
2082
2089
  }
2083
2090
  }
2084
2091
 
2085
- const buildWithMatches = (
2086
- dest: BuildNextOptions = {},
2087
- maskedDest?: BuildNextOptions,
2088
- ) => {
2089
- const next = build(dest)
2090
-
2091
- let maskedNext = maskedDest ? build(maskedDest) : undefined
2092
-
2093
- if (!maskedNext) {
2094
- const params = Object.create(null)
2095
-
2096
- if (this.options.routeMasks) {
2097
- const match = findFlatMatch<RouteMask<TRouteTree>>(
2098
- next.pathname,
2099
- this.processedTree,
2100
- )
2101
- if (match) {
2102
- Object.assign(params, match.rawParams) // Copy params, because they're cached
2103
- const {
2104
- from: _from,
2105
- params: maskParams,
2106
- ...maskProps
2107
- } = match.route
2108
-
2109
- // If mask has a params function, call it with the matched params as context
2110
- // Otherwise, use the matched params or the provided params value
2111
- const nextParams = resolveNextParams(maskParams, params)
2112
-
2113
- maskedDest = {
2114
- from: opts.from,
2115
- ...maskProps,
2116
- params: nextParams,
2117
- }
2118
- maskedNext = build(maskedDest)
2119
- }
2120
- }
2121
- }
2122
-
2123
- if (maskedNext) {
2124
- next.maskedLocation = maskedNext
2125
- }
2126
-
2127
- return next
2128
- }
2092
+ const next = build(opts)
2129
2093
 
2130
2094
  if (opts.mask) {
2131
- return buildWithMatches(opts, {
2095
+ next.maskedLocation = build({
2132
2096
  from: opts.from,
2133
2097
  ...opts.mask,
2134
2098
  })
2099
+ } else if (this.options.routeMasks) {
2100
+ const match = findFlatMatch<RouteMask<TRouteTree>>(
2101
+ next.pathname,
2102
+ this.processedTree,
2103
+ )
2104
+ if (match) {
2105
+ const params = Object.assign(Object.create(null), match.rawParams)
2106
+ const { from: _from, params: maskParams, ...maskProps } = match.route
2107
+
2108
+ // If mask has a params function, call it with the matched params as context
2109
+ // Otherwise, use the matched params or the provided params value
2110
+ const nextParams = resolveNextParams(maskParams, params)
2111
+
2112
+ next.maskedLocation = build({
2113
+ from: opts.from,
2114
+ ...maskProps,
2115
+ params: nextParams,
2116
+ })
2117
+ }
2135
2118
  }
2136
2119
 
2137
- return buildWithMatches(opts)
2120
+ return next
2138
2121
  }
2139
2122
 
2140
2123
  _commitPromise: (Promise<void> & { resolve: () => void }) | undefined
@@ -2240,38 +2223,12 @@ export class RouterCore<
2240
2223
  hashScrollIntoView,
2241
2224
  viewTransition,
2242
2225
  ignoreBlocker,
2243
- _redirects,
2244
- href,
2245
2226
  ...rest
2246
- }: BuildNextOptions &
2247
- CommitLocationOptions & { _redirects?: number } = {}) => {
2248
- if (href) {
2249
- const currentIndex = this.history.location.state.__TSR_index
2250
-
2251
- const parsed = parseHref(href, {
2252
- __TSR_index: replace ? currentIndex : currentIndex + 1,
2253
- })
2254
-
2255
- // If the href contains the basepath, we need to strip it before setting `to`
2256
- // because `buildLocation` will add the basepath back when creating the final URL.
2257
- // Without this, hrefs like '/app/about' would become '/app/app/about'.
2258
- const hrefUrl = new URL(parsed.pathname, this.origin)
2259
- const rewrittenUrl = executeRewriteInput(this.rewrite, hrefUrl)
2260
-
2261
- rest.to = rewrittenUrl.pathname
2262
- rest.search = this.options.parseSearch(parsed.search)
2263
- // remove the leading `#` from the hash
2264
- rest.hash = parsed.hash.slice(1)
2265
- }
2266
-
2227
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
2267
2228
  const location = this.buildLocation({
2268
2229
  ...(rest as any),
2269
2230
  _includeValidateSearch: true,
2270
2231
  })
2271
- if (_redirects) {
2272
- ;(location as typeof location & { _redirects?: number })._redirects =
2273
- _redirects
2274
- }
2275
2232
 
2276
2233
  this._pendingLocation = location as ParsedLocation<
2277
2234
  FullSearchSchema<TRouteTree>
@@ -2538,9 +2495,8 @@ export class RouterCore<
2538
2495
  resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2539
2496
  const locationHeader = redirect.headers.get('Location')
2540
2497
 
2541
- if (!redirect.options.href || redirect.options._builtLocation) {
2542
- const location =
2543
- redirect.options._builtLocation ?? this.buildLocation(redirect.options)
2498
+ if (!redirect.options.href) {
2499
+ const location = this.buildLocation(redirect.options)
2544
2500
  const href = location.publicHref || '/'
2545
2501
  redirect.options.href = href
2546
2502
  redirect.headers.set('Location', href)
@@ -2559,7 +2515,6 @@ export class RouterCore<
2559
2515
 
2560
2516
  if (
2561
2517
  redirect.options.href &&
2562
- !redirect.options._builtLocation &&
2563
2518
  // Check for dangerous protocols before processing the redirect
2564
2519
  isDangerousProtocol(redirect.options.href, this.protocolAllowlist)
2565
2520
  ) {
@@ -2635,7 +2590,8 @@ export class RouterCore<
2635
2590
  TTrailingSlashOption,
2636
2591
  TDefaultStructuralSharingOption,
2637
2592
  TRouterHistory
2638
- > = (opts) => preloadClientRoute(this, opts)
2593
+ > = (opts: any, builtLocation?: ParsedLocation) =>
2594
+ preloadClientRoute(this, opts, 0, builtLocation)
2639
2595
 
2640
2596
  matchRoute: MatchRouteFn<
2641
2597
  TRouteTree,
package/src/utils.ts CHANGED
@@ -193,10 +193,6 @@ export function last<T>(arr: ReadonlyArray<T>) {
193
193
  return arr[arr.length - 1]
194
194
  }
195
195
 
196
- function isFunction(d: any): d is Function {
197
- return typeof d === 'function'
198
- }
199
-
200
196
  /**
201
197
  * Apply a value-or-updater to a previous value.
202
198
  * Accepts either a literal value or a function of the previous value.
@@ -205,8 +201,8 @@ export function functionalUpdate<TPrevious, TResult = TPrevious>(
205
201
  updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,
206
202
  previous: TPrevious,
207
203
  ): TResult {
208
- if (isFunction(updater)) {
209
- return updater(previous)
204
+ if (typeof updater === 'function') {
205
+ return (updater as Function)(previous)
210
206
  }
211
207
 
212
208
  return updater