@tanstack/react-router 1.48.1 → 1.49.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.
Files changed (51) hide show
  1. package/dist/cjs/Matches.cjs +3 -1
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/Matches.d.cts +5 -4
  4. package/dist/cjs/Transitioner.cjs +2 -2
  5. package/dist/cjs/Transitioner.cjs.map +1 -1
  6. package/dist/cjs/fileRoute.cjs.map +1 -1
  7. package/dist/cjs/fileRoute.d.cts +4 -4
  8. package/dist/cjs/index.cjs +2 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +3 -1
  11. package/dist/cjs/route.cjs +1 -1
  12. package/dist/cjs/route.cjs.map +1 -1
  13. package/dist/cjs/route.d.cts +73 -59
  14. package/dist/cjs/router.cjs +90 -57
  15. package/dist/cjs/router.cjs.map +1 -1
  16. package/dist/cjs/router.d.cts +22 -9
  17. package/dist/cjs/transformer.cjs +39 -0
  18. package/dist/cjs/transformer.cjs.map +1 -0
  19. package/dist/cjs/transformer.d.cts +5 -0
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +1 -0
  22. package/dist/esm/Matches.d.ts +5 -4
  23. package/dist/esm/Matches.js +3 -1
  24. package/dist/esm/Matches.js.map +1 -1
  25. package/dist/esm/Transitioner.js +2 -2
  26. package/dist/esm/Transitioner.js.map +1 -1
  27. package/dist/esm/fileRoute.d.ts +4 -4
  28. package/dist/esm/fileRoute.js.map +1 -1
  29. package/dist/esm/index.d.ts +3 -1
  30. package/dist/esm/index.js +2 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/route.d.ts +73 -59
  33. package/dist/esm/route.js +1 -1
  34. package/dist/esm/route.js.map +1 -1
  35. package/dist/esm/router.d.ts +22 -9
  36. package/dist/esm/router.js +91 -58
  37. package/dist/esm/router.js.map +1 -1
  38. package/dist/esm/transformer.d.ts +5 -0
  39. package/dist/esm/transformer.js +39 -0
  40. package/dist/esm/transformer.js.map +1 -0
  41. package/dist/esm/utils.d.ts +1 -0
  42. package/dist/esm/utils.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/Matches.tsx +11 -7
  45. package/src/Transitioner.tsx +2 -2
  46. package/src/fileRoute.ts +26 -21
  47. package/src/index.tsx +5 -1
  48. package/src/route.ts +356 -186
  49. package/src/router.ts +144 -75
  50. package/src/transformer.ts +49 -0
  51. package/src/utils.ts +5 -0
package/src/router.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { createBrowserHistory, createMemoryHistory } from '@tanstack/history'
1
+ import {
2
+ createBrowserHistory,
3
+ createMemoryHistory,
4
+ parseHref,
5
+ } from '@tanstack/history'
2
6
  import { Store } from '@tanstack/react-store'
3
7
  import invariant from 'tiny-invariant'
4
8
  import warning from 'tiny-warning'
@@ -25,6 +29,7 @@ import {
25
29
  } from './path'
26
30
  import { isRedirect, isResolvedRedirect } from './redirects'
27
31
  import { isNotFound } from './not-found'
32
+ import { defaultTransformer } from './transformer'
28
33
  import type * as React from 'react'
29
34
  import type {
30
35
  HistoryLocation,
@@ -37,12 +42,13 @@ import type {
37
42
  AnyContext,
38
43
  AnyRoute,
39
44
  AnyRouteWithContext,
40
- AnySearchSchema,
45
+ BeforeLoadContextOptions,
41
46
  ErrorRouteComponent,
42
47
  LoaderFnContext,
43
48
  NotFoundRouteComponent,
44
49
  RootRoute,
45
50
  RouteComponent,
51
+ RouteContextOptions,
46
52
  RouteMask,
47
53
  } from './route'
48
54
  import type {
@@ -73,13 +79,18 @@ import type {
73
79
  import type { AnyRedirect, ResolvedRedirect } from './redirects'
74
80
  import type { NotFoundError } from './not-found'
75
81
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
82
+ import type { RouterTransformer } from './transformer'
76
83
 
77
84
  //
78
85
 
79
86
  declare global {
80
87
  interface Window {
81
88
  __TSR__?: {
82
- matches: Array<any>
89
+ matches: Array<{
90
+ __beforeLoadContext?: string
91
+ loaderData?: string
92
+ extracted?: Array<ExtractedEntry>
93
+ }>
83
94
  streamedValues: Record<
84
95
  string,
85
96
  {
@@ -121,9 +132,9 @@ export type HydrationCtx = {
121
132
  export type InferRouterContext<TRouteTree extends AnyRoute> =
122
133
  TRouteTree extends RootRoute<
123
134
  any,
135
+ infer TRouterContext extends AnyContext,
124
136
  any,
125
137
  any,
126
- infer TRouterContext extends AnyContext,
127
138
  any,
128
139
  any,
129
140
  any,
@@ -132,6 +143,20 @@ export type InferRouterContext<TRouteTree extends AnyRoute> =
132
143
  ? TRouterContext
133
144
  : AnyContext
134
145
 
146
+ export type ExtractedEntry = {
147
+ dataType: '__beforeLoadContext' | 'loaderData'
148
+ type: 'promise' | 'stream'
149
+ path: Array<string>
150
+ value: any
151
+ id: number
152
+ streamState?: StreamState
153
+ matchIndex: number
154
+ }
155
+
156
+ export type StreamState = {
157
+ promises: Array<ControlledPromise<string | null>>
158
+ }
159
+
135
160
  export type RouterContextOptions<TRouteTree extends AnyRoute> =
136
161
  AnyContext extends InferRouterContext<TRouteTree>
137
162
  ? {
@@ -428,10 +453,6 @@ export interface RouterOptions<
428
453
  isServer?: boolean
429
454
  }
430
455
 
431
- export interface RouterTransformer {
432
- stringify: (obj: unknown) => string
433
- parse: (str: string) => unknown
434
- }
435
456
  export interface RouterErrorSerializer<TSerializedError> {
436
457
  serialize: (err: unknown) => TSerializedError
437
458
  deserialize: (err: TSerializedError) => unknown
@@ -591,7 +612,8 @@ export class Router<
591
612
  matchIndex: number
592
613
  }) => any
593
614
  serializeLoaderData?: (
594
- data: any,
615
+ type: '__beforeLoadContext' | 'loaderData',
616
+ loaderData: any,
595
617
  ctx: {
596
618
  router: AnyRouter
597
619
  match: AnyRouteMatch
@@ -644,10 +666,7 @@ export class Router<
644
666
  notFoundMode: options.notFoundMode ?? 'fuzzy',
645
667
  stringifySearch: options.stringifySearch ?? defaultStringifySearch,
646
668
  parseSearch: options.parseSearch ?? defaultParseSearch,
647
- transformer: options.transformer ?? {
648
- parse: JSON.parse,
649
- stringify: JSON.stringify,
650
- },
669
+ transformer: options.transformer ?? defaultTransformer,
651
670
  })
652
671
 
653
672
  if (typeof document !== 'undefined') {
@@ -932,8 +951,7 @@ export class Router<
932
951
  }
933
952
 
934
953
  matchRoutes = (
935
- pathname: string,
936
- locationSearch: AnySearchSchema,
954
+ next: ParsedLocation,
937
955
  opts?: { preload?: boolean; throwOnError?: boolean },
938
956
  ): Array<AnyRouteMatch> => {
939
957
  let routeParams: Record<string, string> = {}
@@ -941,7 +959,7 @@ export class Router<
941
959
  const foundRoute = this.flatRoutes.find((route) => {
942
960
  const matchedParams = matchPathname(
943
961
  this.basepath,
944
- trimPathRight(pathname),
962
+ trimPathRight(next.pathname),
945
963
  {
946
964
  to: route.fullPath,
947
965
  caseSensitive:
@@ -971,7 +989,7 @@ export class Router<
971
989
  foundRoute
972
990
  ? foundRoute.path !== '/' && routeParams['**']
973
991
  : // Or if we didn't find a route and we have left over path
974
- trimPathRight(pathname)
992
+ trimPathRight(next.pathname)
975
993
  ) {
976
994
  // If the user has defined an (old) 404 route, use it
977
995
  if (this.options.notFoundRoute) {
@@ -1048,7 +1066,7 @@ export class Router<
1048
1066
 
1049
1067
  const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
1050
1068
  // Validate the search params and stabilize them
1051
- const parentSearch = parentMatch?.search ?? locationSearch
1069
+ const parentSearch = parentMatch?.search ?? next.search
1052
1070
 
1053
1071
  try {
1054
1072
  const validator =
@@ -1138,8 +1156,9 @@ export class Router<
1138
1156
  isFetching: false,
1139
1157
  error: undefined,
1140
1158
  paramsError: parseErrors[index],
1141
- routeContext: undefined!,
1142
- context: undefined!,
1159
+ __routeContext: {},
1160
+ __beforeLoadContext: {},
1161
+ context: {},
1143
1162
  abortController: new AbortController(),
1144
1163
  fetchCount: 0,
1145
1164
  cause,
@@ -1180,6 +1199,41 @@ export class Router<
1180
1199
  // And also update the searchError if there is one
1181
1200
  match.searchError = searchError
1182
1201
 
1202
+ const parentMatchId = parentMatch?.id
1203
+
1204
+ const parentContext = !parentMatchId
1205
+ ? ((this.options.context as any) ?? {})
1206
+ : (parentMatch.context ?? this.options.context ?? {})
1207
+
1208
+ match.context = {
1209
+ ...parentContext,
1210
+ ...match.__routeContext,
1211
+ ...match.__beforeLoadContext,
1212
+ }
1213
+
1214
+ // Update the match's context
1215
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
1216
+ search: match.search,
1217
+ params: match.params,
1218
+ context: match.context,
1219
+ location: next,
1220
+ navigate: (opts: any) =>
1221
+ this.navigate({ ...opts, _fromLocation: next }),
1222
+ buildLocation: this.buildLocation,
1223
+ cause: match.cause,
1224
+ abortController: match.abortController,
1225
+ preload: !!match.preload,
1226
+ }
1227
+
1228
+ // Get the route context
1229
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
1230
+
1231
+ match.context = {
1232
+ ...parentContext,
1233
+ ...match.__routeContext,
1234
+ ...match.__beforeLoadContext,
1235
+ }
1236
+
1183
1237
  matches.push(match)
1184
1238
  })
1185
1239
 
@@ -1210,10 +1264,10 @@ export class Router<
1210
1264
  ): ParsedLocation => {
1211
1265
  const fromMatches =
1212
1266
  dest._fromLocation != null
1213
- ? this.matchRoutes(
1214
- dest._fromLocation.pathname,
1215
- dest.fromSearch || dest._fromLocation.search,
1216
- )
1267
+ ? this.matchRoutes({
1268
+ ...dest._fromLocation,
1269
+ search: dest.fromSearch || dest._fromLocation.search,
1270
+ })
1217
1271
  : this.state.matches
1218
1272
 
1219
1273
  const fromMatch =
@@ -1389,9 +1443,9 @@ export class Router<
1389
1443
  }
1390
1444
  }
1391
1445
 
1392
- const nextMatches = this.matchRoutes(next.pathname, next.search)
1446
+ const nextMatches = this.matchRoutes(next)
1393
1447
  const maskedMatches = maskedNext
1394
- ? this.matchRoutes(maskedNext.pathname, maskedNext.search)
1448
+ ? this.matchRoutes(maskedNext)
1395
1449
  : undefined
1396
1450
  const maskedFinal = maskedNext
1397
1451
  ? build(maskedDest, maskedMatches)
@@ -1501,6 +1555,14 @@ export class Router<
1501
1555
  ignoreBlocker,
1502
1556
  ...rest
1503
1557
  }: BuildNextOptions & CommitLocationOptions = {}) => {
1558
+ const href = (rest as any).href
1559
+ if (href) {
1560
+ const parsed = parseHref(href, {})
1561
+ rest.to = parsed.pathname
1562
+ rest.search = this.options.parseSearch(parsed.search)
1563
+ rest.hash = parsed.hash
1564
+ }
1565
+
1504
1566
  const location = this.buildLocation(rest as any)
1505
1567
  return this.commitLocation({
1506
1568
  ...location,
@@ -1511,14 +1573,13 @@ export class Router<
1511
1573
  })
1512
1574
  }
1513
1575
 
1514
- navigate: NavigateFn = ({ from, to, __isRedirect, ...rest }) => {
1576
+ navigate: NavigateFn = ({ to, __isRedirect, ...rest }) => {
1515
1577
  // If this link simply reloads the current route,
1516
1578
  // make sure it has a new key so it will trigger a data refresh
1517
1579
 
1518
1580
  // If this `to` is a valid external URL, return
1519
1581
  // null for LinkUtils
1520
1582
  const toString = String(to)
1521
- // const fromString = from !== undefined ? String(from) : from
1522
1583
  let isExternal
1523
1584
 
1524
1585
  try {
@@ -1533,7 +1594,6 @@ export class Router<
1533
1594
 
1534
1595
  return this.buildAndCommitLocation({
1535
1596
  ...rest,
1536
- from,
1537
1597
  to,
1538
1598
  // to: toString,
1539
1599
  })
@@ -1552,7 +1612,10 @@ export class Router<
1552
1612
  let redirect: ResolvedRedirect | undefined
1553
1613
  let notFound: NotFoundError | undefined
1554
1614
 
1555
- const loadPromise = new Promise<void>((resolve) => {
1615
+ let loadPromise: Promise<void>
1616
+
1617
+ // eslint-disable-next-line prefer-const
1618
+ loadPromise = new Promise<void>((resolve) => {
1556
1619
  this.startReactTransition(async () => {
1557
1620
  try {
1558
1621
  const next = this.latestLocation
@@ -1570,7 +1633,7 @@ export class Router<
1570
1633
  // this.cleanCache()
1571
1634
 
1572
1635
  // Match the routes
1573
- pendingMatches = this.matchRoutes(next.pathname, next.search)
1636
+ pendingMatches = this.matchRoutes(next)
1574
1637
 
1575
1638
  // Ingest the new matches
1576
1639
  this.__store.setState((s) => ({
@@ -1871,6 +1934,7 @@ export class Router<
1871
1934
 
1872
1935
  for (const [index, { id: matchId, routeId }] of matches.entries()) {
1873
1936
  const existingMatch = this.getMatch(matchId)!
1937
+ const parentMatchId = matches[index - 1]?.id
1874
1938
 
1875
1939
  if (
1876
1940
  // If we are in the middle of a load, either of these will be present
@@ -1894,20 +1958,6 @@ export class Router<
1894
1958
  const route = this.looseRoutesById[routeId]!
1895
1959
  const abortController = new AbortController()
1896
1960
 
1897
- const parentMatchId = matches[index - 1]?.id
1898
-
1899
- const getParentContext = () => {
1900
- if (!parentMatchId) {
1901
- return (this.options.context as any) ?? {}
1902
- }
1903
-
1904
- return (
1905
- this.getMatch(parentMatchId)!.context ??
1906
- this.options.context ??
1907
- {}
1908
- )
1909
- }
1910
-
1911
1961
  const pendingMs =
1912
1962
  route.options.pendingMs ?? this.options.defaultPendingMs
1913
1963
 
@@ -1946,30 +1996,39 @@ export class Router<
1946
1996
  handleSerialError(index, searchError, 'VALIDATE_SEARCH')
1947
1997
  }
1948
1998
 
1949
- const parentContext = getParentContext()
1999
+ const getParentMatchContext = () =>
2000
+ parentMatchId
2001
+ ? this.getMatch(parentMatchId)!.context
2002
+ : (this.options.context ?? {})
1950
2003
 
1951
2004
  updateMatch(matchId, (prev) => ({
1952
2005
  ...prev,
1953
2006
  isFetching: 'beforeLoad',
1954
2007
  fetchCount: prev.fetchCount + 1,
1955
- routeContext: replaceEqualDeep(
1956
- prev.routeContext,
1957
- parentContext,
1958
- ),
1959
- context: replaceEqualDeep(prev.context, parentContext),
1960
2008
  abortController,
1961
2009
  pendingTimeout,
2010
+ context: {
2011
+ ...getParentMatchContext(),
2012
+ ...prev.__routeContext,
2013
+ ...prev.__beforeLoadContext,
2014
+ },
1962
2015
  }))
1963
2016
 
1964
- const { search, params, routeContext, cause } =
2017
+ const { search, params, context, cause } =
1965
2018
  this.getMatch(matchId)!
1966
2019
 
1967
- const beforeLoadFnContext = {
2020
+ const beforeLoadFnContext: BeforeLoadContextOptions<
2021
+ any,
2022
+ any,
2023
+ any,
2024
+ any,
2025
+ any
2026
+ > = {
1968
2027
  search,
1969
2028
  abortController,
1970
2029
  params,
1971
2030
  preload: !!preload,
1972
- context: routeContext,
2031
+ context,
1973
2032
  location,
1974
2033
  navigate: (opts: any) =>
1975
2034
  this.navigate({ ...opts, _fromLocation: location }),
@@ -1977,10 +2036,21 @@ export class Router<
1977
2036
  cause: preload ? 'preload' : cause,
1978
2037
  }
1979
2038
 
1980
- const beforeLoadContext =
2039
+ let beforeLoadContext =
1981
2040
  (await route.options.beforeLoad?.(beforeLoadFnContext)) ??
1982
2041
  {}
1983
2042
 
2043
+ if (this.serializeLoaderData) {
2044
+ beforeLoadContext = this.serializeLoaderData(
2045
+ '__beforeLoadContext',
2046
+ beforeLoadContext,
2047
+ {
2048
+ router: this,
2049
+ match: this.getMatch(matchId)!,
2050
+ },
2051
+ )
2052
+ }
2053
+
1984
2054
  if (
1985
2055
  isRedirect(beforeLoadContext) ||
1986
2056
  isNotFound(beforeLoadContext)
@@ -1989,18 +2059,14 @@ export class Router<
1989
2059
  }
1990
2060
 
1991
2061
  updateMatch(matchId, (prev) => {
1992
- const routeContext = {
1993
- ...prev.routeContext,
1994
- ...beforeLoadContext,
1995
- }
1996
-
1997
2062
  return {
1998
2063
  ...prev,
1999
- routeContext: replaceEqualDeep(
2000
- prev.routeContext,
2001
- routeContext,
2002
- ),
2003
- context: replaceEqualDeep(prev.context, routeContext),
2064
+ __beforeLoadContext: beforeLoadContext,
2065
+ context: {
2066
+ ...getParentMatchContext(),
2067
+ ...prev.__routeContext,
2068
+ ...beforeLoadContext,
2069
+ },
2004
2070
  abortController,
2005
2071
  }
2006
2072
  })
@@ -2150,10 +2216,14 @@ export class Router<
2150
2216
  await route.options.loader?.(getLoaderContext())
2151
2217
 
2152
2218
  if (this.serializeLoaderData) {
2153
- loaderData = this.serializeLoaderData(loaderData, {
2154
- router: this,
2155
- match: this.getMatch(matchId)!,
2156
- })
2219
+ loaderData = this.serializeLoaderData(
2220
+ 'loaderData',
2221
+ loaderData,
2222
+ {
2223
+ router: this,
2224
+ match: this.getMatch(matchId)!,
2225
+ },
2226
+ )
2157
2227
  }
2158
2228
 
2159
2229
  handleRedirectAndNotFound(
@@ -2220,7 +2290,9 @@ export class Router<
2220
2290
  // If the route is successful and still fresh, just resolve
2221
2291
  const { status, invalid } = this.getMatch(matchId)!
2222
2292
 
2223
- if (
2293
+ if (preload && route.options.preload === false) {
2294
+ // Do nothing
2295
+ } else if (
2224
2296
  status === 'success' &&
2225
2297
  (invalid || (shouldReload ?? age > staleAge))
2226
2298
  ) {
@@ -2342,7 +2414,7 @@ export class Router<
2342
2414
  ): Promise<Array<AnyRouteMatch> | undefined> => {
2343
2415
  const next = this.buildLocation(opts as any)
2344
2416
 
2345
- let matches = this.matchRoutes(next.pathname, next.search, {
2417
+ let matches = this.matchRoutes(next, {
2346
2418
  throwOnError: true,
2347
2419
  preload: true,
2348
2420
  })
@@ -2499,10 +2571,7 @@ export class Router<
2499
2571
  this.options.hydrate?.(ctx.payload as any)
2500
2572
  const dehydratedState = ctx.router.state
2501
2573
 
2502
- const matches = this.matchRoutes(
2503
- this.state.location.pathname,
2504
- this.state.location.search,
2505
- ).map((match) => {
2574
+ const matches = this.matchRoutes(this.state.location).map((match) => {
2506
2575
  const dehydratedMatch = dehydratedState.dehydratedMatches.find(
2507
2576
  (d) => d.id === match.id,
2508
2577
  )
@@ -0,0 +1,49 @@
1
+ import { isPlainObject } from './utils'
2
+
3
+ export interface RouterTransformer {
4
+ stringify: (obj: unknown) => string
5
+ parse: (str: string) => unknown
6
+ }
7
+
8
+ export const defaultTransformer: RouterTransformer = {
9
+ stringify: (value: any) =>
10
+ JSON.stringify(value, function replacer(key, value) {
11
+ const keyVal = this[key]
12
+ const transformer = transformers.find((t) => t.stringifyCondition(keyVal))
13
+
14
+ if (transformer) {
15
+ return transformer.stringify(keyVal)
16
+ }
17
+
18
+ return value
19
+ }),
20
+ parse: (value: string) =>
21
+ JSON.parse(value, function parser(key, value) {
22
+ const keyVal = this[key]
23
+ const transformer = transformers.find((t) => t.parseCondition(keyVal))
24
+
25
+ if (transformer) {
26
+ return transformer.parse(keyVal)
27
+ }
28
+
29
+ return value
30
+ }),
31
+ }
32
+
33
+ const transformers = [
34
+ {
35
+ // Dates
36
+ stringifyCondition: (value: any) => value instanceof Date,
37
+ stringify: (value: any) => ({ $date: value.toISOString() }),
38
+ parseCondition: (value: any) => isPlainObject(value) && value.$date,
39
+ parse: (value: any) => new Date(value.$date),
40
+ },
41
+ {
42
+ // undefined
43
+ stringifyCondition: (value: any) => value === undefined,
44
+ stringify: () => ({ $undefined: '' }),
45
+ parseCondition: (value: any) =>
46
+ isPlainObject(value) && value.$undefined === '',
47
+ parse: () => undefined,
48
+ },
49
+ ] as const
package/src/utils.ts CHANGED
@@ -4,6 +4,7 @@ export type NoInfer<T> = [T][T extends any ? 0 : never]
4
4
  export type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue
5
5
  ? TYesResult
6
6
  : TNoResult
7
+
7
8
  export type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<
8
9
  TValue,
9
10
  TKey
@@ -98,6 +99,10 @@ export type MergeUnion<TUnion> =
98
99
  | MergeUnionPrimitives<TUnion>
99
100
  | MergeUnionObject<TUnion>
100
101
 
102
+ export type Constrain<T, TConstaint> =
103
+ | (T extends TConstaint ? T : never)
104
+ | TConstaint
105
+
101
106
  export function last<T>(arr: Array<T>) {
102
107
  return arr[arr.length - 1]
103
108
  }