@tanstack/react-router 1.12.16 → 1.14.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.
- package/dist/cjs/Matches.cjs +49 -16
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -0
- package/dist/cjs/fileRoute.cjs.map +1 -1
- package/dist/cjs/fileRoute.d.cts +3 -1
- package/dist/cjs/index.cjs +5 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/not-found.cjs +46 -0
- package/dist/cjs/not-found.cjs.map +1 -0
- package/dist/cjs/not-found.d.cts +16 -0
- package/dist/cjs/route.cjs +6 -0
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +7 -0
- package/dist/cjs/router.cjs +114 -55
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +23 -5
- package/dist/esm/Matches.d.ts +2 -0
- package/dist/esm/Matches.js +44 -11
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/fileRoute.d.ts +3 -1
- package/dist/esm/fileRoute.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/not-found.d.ts +16 -0
- package/dist/esm/not-found.js +46 -0
- package/dist/esm/not-found.js.map +1 -0
- package/dist/esm/route.d.ts +7 -0
- package/dist/esm/route.js +6 -0
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +23 -5
- package/dist/esm/router.js +75 -16
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/Matches.tsx +52 -5
- package/src/fileRoute.ts +5 -6
- package/src/index.tsx +1 -0
- package/src/not-found.tsx +54 -0
- package/src/route.ts +18 -6
- package/src/router.ts +120 -17
package/src/route.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from './utils'
|
|
20
20
|
import { BuildLocationFn, NavigateFn } from './RouterProvider'
|
|
21
21
|
import { LazyRoute } from '.'
|
|
22
|
+
import warning from 'tiny-warning'
|
|
23
|
+
import { NotFoundError, notFound } from '.'
|
|
22
24
|
|
|
23
25
|
export const rootRouteId = '__root__' as const
|
|
24
26
|
export type RootRouteId = typeof rootRouteId
|
|
@@ -191,6 +193,7 @@ export type UpdatableRouteOptions<
|
|
|
191
193
|
// The content to be rendered when the route is matched. If no component is provided, defaults to `<Outlet />`
|
|
192
194
|
component?: RouteComponent
|
|
193
195
|
errorComponent?: false | null | ErrorRouteComponent
|
|
196
|
+
notFoundComponent?: NotFoundRouteComponent
|
|
194
197
|
pendingComponent?: RouteComponent
|
|
195
198
|
pendingMs?: number
|
|
196
199
|
pendingMinMs?: number
|
|
@@ -368,12 +371,11 @@ export type MergeFromFromParent<T, U> = IsAny<T, U, T & U>
|
|
|
368
371
|
export type ResolveAllParams<
|
|
369
372
|
TParentRoute extends AnyRoute,
|
|
370
373
|
TParams extends AnyPathParams,
|
|
371
|
-
> =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
>
|
|
374
|
+
> = Record<never, string> extends TParentRoute['types']['allParams']
|
|
375
|
+
? TParams
|
|
376
|
+
: Expand<
|
|
377
|
+
UnionToIntersection<TParentRoute['types']['allParams'] & TParams> & {}
|
|
378
|
+
>
|
|
377
379
|
|
|
378
380
|
export type RouteConstraints = {
|
|
379
381
|
TParentRoute: AnyRoute
|
|
@@ -573,6 +575,10 @@ export class RouteApi<
|
|
|
573
575
|
}): TSelected => {
|
|
574
576
|
return useLoaderData({ ...opts, from: this.id } as any)
|
|
575
577
|
}
|
|
578
|
+
|
|
579
|
+
notFound = (opts?: NotFoundError) => {
|
|
580
|
+
return notFound({ route: this.id as string, ...opts })
|
|
581
|
+
}
|
|
576
582
|
}
|
|
577
583
|
|
|
578
584
|
/**
|
|
@@ -1249,6 +1255,10 @@ export type ErrorComponentProps = {
|
|
|
1249
1255
|
error: unknown
|
|
1250
1256
|
info: { componentStack: string }
|
|
1251
1257
|
}
|
|
1258
|
+
export type NotFoundRouteProps = {
|
|
1259
|
+
// TODO: Make sure this is `| null | undefined` (this is for global not-founds)
|
|
1260
|
+
data: unknown
|
|
1261
|
+
}
|
|
1252
1262
|
//
|
|
1253
1263
|
|
|
1254
1264
|
export type ReactNode = any
|
|
@@ -1266,6 +1276,8 @@ export type RouteComponent<TProps = any> = SyncRouteComponent<TProps> &
|
|
|
1266
1276
|
|
|
1267
1277
|
export type ErrorRouteComponent = RouteComponent<ErrorComponentProps>
|
|
1268
1278
|
|
|
1279
|
+
export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps>
|
|
1280
|
+
|
|
1269
1281
|
export class NotFoundRoute<
|
|
1270
1282
|
TParentRoute extends AnyRootRoute,
|
|
1271
1283
|
TSearchSchemaInput extends Record<string, any> = {},
|
package/src/router.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
RouteMask,
|
|
18
18
|
Route,
|
|
19
19
|
LoaderFnContext,
|
|
20
|
+
rootRouteId,
|
|
20
21
|
} from './route'
|
|
21
22
|
import {
|
|
22
23
|
FullSearchSchema,
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
} from './path'
|
|
64
65
|
import invariant from 'tiny-invariant'
|
|
65
66
|
import { isRedirect } from './redirects'
|
|
67
|
+
import { NotFoundError, isNotFound } from './not-found'
|
|
66
68
|
import { ResolveRelativePath, ToOptions } from './link'
|
|
67
69
|
import { NoInfer } from '@tanstack/react-store'
|
|
68
70
|
// import warning from 'tiny-warning'
|
|
@@ -71,7 +73,7 @@ import { NoInfer } from '@tanstack/react-store'
|
|
|
71
73
|
|
|
72
74
|
declare global {
|
|
73
75
|
interface Window {
|
|
74
|
-
__TSR_DEHYDRATED__?:
|
|
76
|
+
__TSR_DEHYDRATED__?: { data: string }
|
|
75
77
|
__TSR_ROUTER_CONTEXT__?: React.Context<Router<any>>
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -131,8 +133,20 @@ export interface RouterOptions<
|
|
|
131
133
|
unmaskOnReload?: boolean
|
|
132
134
|
Wrap?: (props: { children: any }) => JSX.Element
|
|
133
135
|
InnerWrap?: (props: { children: any }) => JSX.Element
|
|
136
|
+
/**
|
|
137
|
+
* @deprecated
|
|
138
|
+
* Use `notFoundComponent` instead.
|
|
139
|
+
* See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
|
|
140
|
+
*/
|
|
134
141
|
notFoundRoute?: AnyRoute
|
|
142
|
+
transformer?: RouterTransformer
|
|
135
143
|
errorSerializer?: RouterErrorSerializer<TSerializedError>
|
|
144
|
+
globalNotFound?: RouteComponent
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface RouterTransformer {
|
|
148
|
+
stringify: (obj: unknown) => string
|
|
149
|
+
parse: (str: string) => unknown
|
|
136
150
|
}
|
|
137
151
|
export interface RouterErrorSerializer<TSerializedError> {
|
|
138
152
|
serialize: (err: unknown) => TSerializedError
|
|
@@ -176,7 +190,7 @@ export interface DehydratedRouterState {
|
|
|
176
190
|
|
|
177
191
|
export type DehydratedRouteMatch = Pick<
|
|
178
192
|
RouteMatch,
|
|
179
|
-
'id' | 'status' | 'updatedAt' | 'loaderData'
|
|
193
|
+
'id' | 'status' | 'updatedAt' | 'notFoundError' | 'loaderData'
|
|
180
194
|
>
|
|
181
195
|
|
|
182
196
|
export interface DehydratedRouter {
|
|
@@ -194,6 +208,7 @@ export const componentTypes = [
|
|
|
194
208
|
'component',
|
|
195
209
|
'errorComponent',
|
|
196
210
|
'pendingComponent',
|
|
211
|
+
'notFoundComponent',
|
|
197
212
|
] as const
|
|
198
213
|
|
|
199
214
|
export type RouterEvents = {
|
|
@@ -256,7 +271,12 @@ export class Router<
|
|
|
256
271
|
// Must build in constructor
|
|
257
272
|
__store!: Store<RouterState<TRouteTree>>
|
|
258
273
|
options!: PickAsRequired<
|
|
259
|
-
|
|
274
|
+
Omit<
|
|
275
|
+
RouterOptions<TRouteTree, TDehydrated, TSerializedError>,
|
|
276
|
+
'transformer'
|
|
277
|
+
> & {
|
|
278
|
+
transformer: RouterTransformer
|
|
279
|
+
},
|
|
260
280
|
'stringifySearch' | 'parseSearch' | 'context'
|
|
261
281
|
>
|
|
262
282
|
history!: RouterHistory
|
|
@@ -282,6 +302,7 @@ export class Router<
|
|
|
282
302
|
...options,
|
|
283
303
|
stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
|
|
284
304
|
parseSearch: options?.parseSearch ?? defaultParseSearch,
|
|
305
|
+
transformer: options?.transformer ?? JSON,
|
|
285
306
|
})
|
|
286
307
|
}
|
|
287
308
|
|
|
@@ -297,6 +318,12 @@ export class Router<
|
|
|
297
318
|
TSerializedError
|
|
298
319
|
>,
|
|
299
320
|
) => {
|
|
321
|
+
if (newOptions.notFoundRoute) {
|
|
322
|
+
console.warn(
|
|
323
|
+
'The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.',
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
300
327
|
const previousOptions = this.options
|
|
301
328
|
this.options = {
|
|
302
329
|
...this.options,
|
|
@@ -581,17 +608,23 @@ export class Router<
|
|
|
581
608
|
|
|
582
609
|
let matchedRoutes: AnyRoute[] = [routeCursor]
|
|
583
610
|
|
|
611
|
+
let isGlobalNotFound = false
|
|
612
|
+
|
|
584
613
|
// Check to see if the route needs a 404 entry
|
|
585
614
|
if (
|
|
586
615
|
// If we found a route, and it's not an index route and we have left over path
|
|
587
|
-
|
|
616
|
+
foundRoute
|
|
588
617
|
? foundRoute.path !== '/' && routeParams['**']
|
|
589
618
|
: // Or if we didn't find a route and we have left over path
|
|
590
|
-
trimPathRight(pathname)
|
|
591
|
-
// And we have a 404 route configured
|
|
592
|
-
this.options.notFoundRoute
|
|
619
|
+
trimPathRight(pathname)
|
|
593
620
|
) {
|
|
594
|
-
|
|
621
|
+
// If the user has defined an (old) 404 route, use it
|
|
622
|
+
if (this.options.notFoundRoute) {
|
|
623
|
+
matchedRoutes.push(this.options.notFoundRoute)
|
|
624
|
+
} else {
|
|
625
|
+
// If there is no routes found during path matching
|
|
626
|
+
isGlobalNotFound = true
|
|
627
|
+
}
|
|
595
628
|
}
|
|
596
629
|
|
|
597
630
|
while (routeCursor?.parentRoute) {
|
|
@@ -710,7 +743,14 @@ export class Router<
|
|
|
710
743
|
)
|
|
711
744
|
|
|
712
745
|
const match: AnyRouteMatch = existingMatch
|
|
713
|
-
? {
|
|
746
|
+
? {
|
|
747
|
+
...existingMatch,
|
|
748
|
+
cause,
|
|
749
|
+
notFoundError:
|
|
750
|
+
isGlobalNotFound && route.id === rootRouteId
|
|
751
|
+
? { global: true }
|
|
752
|
+
: undefined,
|
|
753
|
+
}
|
|
714
754
|
: {
|
|
715
755
|
id: matchId,
|
|
716
756
|
routeId: route.id,
|
|
@@ -733,6 +773,10 @@ export class Router<
|
|
|
733
773
|
loaderDeps,
|
|
734
774
|
invalid: false,
|
|
735
775
|
preload: false,
|
|
776
|
+
notFoundError:
|
|
777
|
+
isGlobalNotFound && route.id === rootRouteId
|
|
778
|
+
? { global: true }
|
|
779
|
+
: undefined,
|
|
736
780
|
links: route.options.links?.(),
|
|
737
781
|
scripts: route.options.scripts?.(),
|
|
738
782
|
staticData: route.options.staticData || {},
|
|
@@ -781,8 +825,8 @@ export class Router<
|
|
|
781
825
|
this.latestLocation.pathname,
|
|
782
826
|
fromSearch,
|
|
783
827
|
)
|
|
784
|
-
const stayingMatches = matches?.filter(
|
|
785
|
-
fromMatches?.find((e) => e.routeId === d.routeId),
|
|
828
|
+
const stayingMatches = matches?.filter(
|
|
829
|
+
(d) => fromMatches?.find((e) => e.routeId === d.routeId),
|
|
786
830
|
)
|
|
787
831
|
|
|
788
832
|
const prevParams = { ...last(fromMatches)?.params }
|
|
@@ -1098,6 +1142,10 @@ export class Router<
|
|
|
1098
1142
|
throw err
|
|
1099
1143
|
}
|
|
1100
1144
|
|
|
1145
|
+
if (isNotFound(err)) {
|
|
1146
|
+
this.updateMatchesWithNotFound(matches, match, err)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1101
1149
|
try {
|
|
1102
1150
|
route.options.onError?.(err)
|
|
1103
1151
|
} catch (errorHandlerErr) {
|
|
@@ -1200,6 +1248,11 @@ export class Router<
|
|
|
1200
1248
|
}
|
|
1201
1249
|
return true
|
|
1202
1250
|
}
|
|
1251
|
+
|
|
1252
|
+
if (isNotFound(err)) {
|
|
1253
|
+
this.updateMatchesWithNotFound(matches, match, err)
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1203
1256
|
return false
|
|
1204
1257
|
}
|
|
1205
1258
|
|
|
@@ -1663,7 +1716,7 @@ export class Router<
|
|
|
1663
1716
|
typeof getData === 'function' ? await (getData as any)() : getData
|
|
1664
1717
|
return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
|
|
1665
1718
|
strKey,
|
|
1666
|
-
)}"] = ${JSON.stringify(data)}
|
|
1719
|
+
)}"] = ${JSON.stringify(this.options.transformer.stringify(data))}
|
|
1667
1720
|
;(() => {
|
|
1668
1721
|
var el = document.getElementById('${id}')
|
|
1669
1722
|
el.parentElement.removeChild(el)
|
|
@@ -1681,7 +1734,9 @@ export class Router<
|
|
|
1681
1734
|
if (typeof document !== 'undefined') {
|
|
1682
1735
|
const strKey = typeof key === 'string' ? key : JSON.stringify(key)
|
|
1683
1736
|
|
|
1684
|
-
return
|
|
1737
|
+
return this.options.transformer.parse(
|
|
1738
|
+
window[`__TSR_DEHYDRATED__${strKey}` as any] as unknown as string,
|
|
1739
|
+
) as T
|
|
1685
1740
|
}
|
|
1686
1741
|
|
|
1687
1742
|
return undefined
|
|
@@ -1694,7 +1749,15 @@ export class Router<
|
|
|
1694
1749
|
return {
|
|
1695
1750
|
state: {
|
|
1696
1751
|
dehydratedMatches: this.state.matches.map((d) => ({
|
|
1697
|
-
...pick(d, [
|
|
1752
|
+
...pick(d, [
|
|
1753
|
+
'id',
|
|
1754
|
+
'status',
|
|
1755
|
+
'updatedAt',
|
|
1756
|
+
'loaderData',
|
|
1757
|
+
// Not-founds that occur during SSR don't require the client to load data before
|
|
1758
|
+
// triggering in order to prevent the flicker of the loading component
|
|
1759
|
+
'notFoundError',
|
|
1760
|
+
]),
|
|
1698
1761
|
// If an error occurs server-side during SSRing,
|
|
1699
1762
|
// send a small subset of the error to the client
|
|
1700
1763
|
error: d.error
|
|
@@ -1708,11 +1771,11 @@ export class Router<
|
|
|
1708
1771
|
}
|
|
1709
1772
|
}
|
|
1710
1773
|
|
|
1711
|
-
hydrate = async (__do_not_use_server_ctx?:
|
|
1774
|
+
hydrate = async (__do_not_use_server_ctx?: string) => {
|
|
1712
1775
|
let _ctx = __do_not_use_server_ctx
|
|
1713
1776
|
// Client hydrates from window
|
|
1714
1777
|
if (typeof document !== 'undefined') {
|
|
1715
|
-
_ctx = window.__TSR_DEHYDRATED__
|
|
1778
|
+
_ctx = window.__TSR_DEHYDRATED__?.data
|
|
1716
1779
|
}
|
|
1717
1780
|
|
|
1718
1781
|
invariant(
|
|
@@ -1720,7 +1783,7 @@ export class Router<
|
|
|
1720
1783
|
'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
|
|
1721
1784
|
)
|
|
1722
1785
|
|
|
1723
|
-
const ctx = _ctx
|
|
1786
|
+
const ctx = this.options.transformer.parse(_ctx) as HydrationCtx
|
|
1724
1787
|
this.dehydratedData = ctx.payload as any
|
|
1725
1788
|
this.options.hydrate?.(ctx.payload as any)
|
|
1726
1789
|
const dehydratedState = ctx.router.state
|
|
@@ -1763,6 +1826,46 @@ export class Router<
|
|
|
1763
1826
|
})
|
|
1764
1827
|
}
|
|
1765
1828
|
|
|
1829
|
+
// Finds a match that has a notFoundComponent
|
|
1830
|
+
updateMatchesWithNotFound = (
|
|
1831
|
+
matches: AnyRouteMatch[],
|
|
1832
|
+
currentMatch: AnyRouteMatch,
|
|
1833
|
+
err: NotFoundError,
|
|
1834
|
+
) => {
|
|
1835
|
+
const matchesByRouteId = Object.fromEntries(
|
|
1836
|
+
matches.map((match) => [match.routeId, match]),
|
|
1837
|
+
) as Record<string, AnyRouteMatch>
|
|
1838
|
+
|
|
1839
|
+
if (err.global) {
|
|
1840
|
+
matchesByRouteId[rootRouteId]!.notFoundError = err
|
|
1841
|
+
} else {
|
|
1842
|
+
// If the err contains a routeId, start searching up from that route
|
|
1843
|
+
let currentRoute = (this.routesById as any)[
|
|
1844
|
+
err.route ?? currentMatch.routeId
|
|
1845
|
+
] as AnyRoute
|
|
1846
|
+
|
|
1847
|
+
// Go up the tree until we find a route with a notFoundComponent
|
|
1848
|
+
while (!currentRoute.options.notFoundComponent) {
|
|
1849
|
+
currentRoute = currentRoute?.parentRoute
|
|
1850
|
+
|
|
1851
|
+
invariant(
|
|
1852
|
+
currentRoute,
|
|
1853
|
+
'Found invalid route tree while trying to find not-found handler.',
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
if (currentRoute.id === rootRouteId) break
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const match = matchesByRouteId[currentRoute.id]
|
|
1860
|
+
invariant(match, 'Could not find match for route: ' + currentRoute.id)
|
|
1861
|
+
match.notFoundError = err
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
hasNotFoundMatch = () => {
|
|
1866
|
+
return this.__store.state.matches.some((d) => d.notFoundError)
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1766
1869
|
// resolveMatchPromise = (matchId: string, key: string, value: any) => {
|
|
1767
1870
|
// state.matches
|
|
1768
1871
|
// .find((d) => d.id === matchId)
|