@tanstack/react-router 1.40.0 → 1.42.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/CatchBoundary.d.cts +2 -2
- package/dist/cjs/Match.cjs +238 -0
- package/dist/cjs/Match.cjs.map +1 -0
- package/dist/cjs/Match.d.cts +5 -0
- package/dist/cjs/Matches.cjs +8 -249
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -11
- package/dist/cjs/RouterProvider.cjs.map +1 -1
- package/dist/cjs/RouterProvider.d.cts +2 -2
- package/dist/cjs/SafeFragment.cjs +8 -0
- package/dist/cjs/SafeFragment.cjs.map +1 -0
- package/dist/cjs/SafeFragment.d.cts +1 -0
- package/dist/cjs/ScriptOnce.cjs +28 -0
- package/dist/cjs/ScriptOnce.cjs.map +1 -0
- package/dist/cjs/ScriptOnce.d.cts +5 -0
- package/dist/cjs/Transitioner.cjs +2 -1
- package/dist/cjs/Transitioner.cjs.map +1 -1
- package/dist/cjs/awaited.cjs +14 -71
- package/dist/cjs/awaited.cjs.map +1 -1
- package/dist/cjs/awaited.d.cts +3 -6
- package/dist/cjs/defer.cjs +7 -13
- package/dist/cjs/defer.cjs.map +1 -1
- package/dist/cjs/defer.d.cts +2 -6
- package/dist/cjs/fileRoute.d.cts +9 -1
- package/dist/cjs/index.cjs +11 -7
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +9 -4
- package/dist/cjs/isServerSideError.cjs +22 -0
- package/dist/cjs/isServerSideError.cjs.map +1 -0
- package/dist/cjs/isServerSideError.d.cts +5 -0
- package/dist/cjs/link.cjs +1 -0
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/matchContext.cjs +23 -0
- package/dist/cjs/matchContext.cjs.map +1 -0
- package/dist/cjs/matchContext.d.cts +2 -0
- package/dist/cjs/not-found.cjs.map +1 -1
- package/dist/cjs/not-found.d.cts +2 -2
- package/dist/cjs/qss.cjs.map +1 -1
- package/dist/cjs/qss.d.cts +1 -1
- package/dist/cjs/redirects.cjs.map +1 -1
- package/dist/cjs/renderRouteNotFound.cjs +22 -0
- package/dist/cjs/renderRouteNotFound.cjs.map +1 -0
- package/dist/cjs/renderRouteNotFound.d.cts +4 -0
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +37 -31
- package/dist/cjs/router.cjs +35 -25
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +14 -9
- package/dist/cjs/useMatch.cjs +2 -2
- package/dist/cjs/useMatch.cjs.map +1 -1
- package/dist/cjs/utils.cjs +4 -3
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -2
- package/dist/esm/CatchBoundary.d.ts +2 -2
- package/dist/esm/Match.d.ts +5 -0
- package/dist/esm/Match.js +221 -0
- package/dist/esm/Match.js.map +1 -0
- package/dist/esm/Matches.d.ts +2 -11
- package/dist/esm/Matches.js +5 -246
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/RouterProvider.d.ts +2 -2
- package/dist/esm/RouterProvider.js.map +1 -1
- package/dist/esm/SafeFragment.d.ts +1 -0
- package/dist/esm/SafeFragment.js +8 -0
- package/dist/esm/SafeFragment.js.map +1 -0
- package/dist/esm/ScriptOnce.d.ts +5 -0
- package/dist/esm/ScriptOnce.js +28 -0
- package/dist/esm/ScriptOnce.js.map +1 -0
- package/dist/esm/Transitioner.js +2 -1
- package/dist/esm/Transitioner.js.map +1 -1
- package/dist/esm/awaited.d.ts +3 -6
- package/dist/esm/awaited.js +16 -73
- package/dist/esm/awaited.js.map +1 -1
- package/dist/esm/defer.d.ts +2 -6
- package/dist/esm/defer.js +8 -14
- package/dist/esm/defer.js.map +1 -1
- package/dist/esm/fileRoute.d.ts +9 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +9 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/isServerSideError.d.ts +5 -0
- package/dist/esm/isServerSideError.js +22 -0
- package/dist/esm/isServerSideError.js.map +1 -0
- package/dist/esm/link.js +1 -0
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/matchContext.d.ts +2 -0
- package/dist/esm/matchContext.js +6 -0
- package/dist/esm/matchContext.js.map +1 -0
- package/dist/esm/not-found.d.ts +2 -2
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/qss.d.ts +1 -1
- package/dist/esm/qss.js.map +1 -1
- package/dist/esm/redirects.js.map +1 -1
- package/dist/esm/renderRouteNotFound.d.ts +4 -0
- package/dist/esm/renderRouteNotFound.js +22 -0
- package/dist/esm/renderRouteNotFound.js.map +1 -0
- package/dist/esm/route.d.ts +37 -31
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +14 -9
- package/dist/esm/router.js +35 -25
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/useMatch.js +1 -1
- package/dist/esm/useMatch.js.map +1 -1
- package/dist/esm/utils.d.ts +3 -2
- package/dist/esm/utils.js +4 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/Match.tsx +296 -0
- package/src/Matches.tsx +4 -333
- package/src/RouterProvider.tsx +1 -1
- package/src/SafeFragment.tsx +5 -0
- package/src/ScriptOnce.tsx +27 -0
- package/src/Transitioner.tsx +1 -1
- package/src/awaited.tsx +17 -89
- package/src/defer.ts +9 -26
- package/src/index.tsx +7 -16
- package/src/isServerSideError.tsx +23 -0
- package/src/link.tsx +2 -0
- package/src/matchContext.tsx +3 -0
- package/src/not-found.tsx +1 -1
- package/src/qss.ts +5 -6
- package/src/redirects.ts +0 -1
- package/src/renderRouteNotFound.tsx +28 -0
- package/src/route.ts +61 -65
- package/src/router.ts +61 -42
- package/src/useMatch.tsx +1 -1
- package/src/utils.ts +11 -9
package/src/Matches.tsx
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import invariant from 'tiny-invariant'
|
|
3
2
|
import warning from 'tiny-warning'
|
|
4
3
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary'
|
|
5
4
|
import { useRouterState } from './useRouterState'
|
|
6
5
|
import { useRouter } from './useRouter'
|
|
7
|
-
import { createControlledPromise, pick } from './utils'
|
|
8
|
-
import { CatchNotFound, DefaultGlobalNotFound, isNotFound } from './not-found'
|
|
9
|
-
import { isRedirect } from './redirects'
|
|
10
6
|
import { type AnyRouter, type RegisteredRouter } from './router'
|
|
11
7
|
import { Transitioner } from './Transitioner'
|
|
12
8
|
import {
|
|
@@ -14,7 +10,9 @@ import {
|
|
|
14
10
|
type ReactNode,
|
|
15
11
|
type StaticDataRouteOption,
|
|
16
12
|
} from './route'
|
|
17
|
-
import {
|
|
13
|
+
import { matchContext } from './matchContext'
|
|
14
|
+
import { Match } from './Match'
|
|
15
|
+
import { SafeFragment } from './SafeFragment'
|
|
18
16
|
import type { ResolveRelativePath, ToOptions } from './link'
|
|
19
17
|
import type {
|
|
20
18
|
AllContext,
|
|
@@ -29,8 +27,6 @@ import type {
|
|
|
29
27
|
} from './routeInfo'
|
|
30
28
|
import type { ControlledPromise, DeepPartial, NoInfer } from './utils'
|
|
31
29
|
|
|
32
|
-
export const matchContext = React.createContext<string | undefined>(undefined)
|
|
33
|
-
|
|
34
30
|
export interface RouteMatch<
|
|
35
31
|
TRouteId,
|
|
36
32
|
TAllParams,
|
|
@@ -42,6 +38,7 @@ export interface RouteMatch<
|
|
|
42
38
|
> {
|
|
43
39
|
id: string
|
|
44
40
|
routeId: TRouteId
|
|
41
|
+
index: number
|
|
45
42
|
pathname: string
|
|
46
43
|
params: TAllParams
|
|
47
44
|
status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
|
|
@@ -157,308 +154,6 @@ function MatchesInner() {
|
|
|
157
154
|
)
|
|
158
155
|
}
|
|
159
156
|
|
|
160
|
-
function SafeFragment(props: any) {
|
|
161
|
-
return <>{props.children}</>
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function Match({ matchId }: { matchId: string }) {
|
|
165
|
-
const router = useRouter()
|
|
166
|
-
const routeId = useRouterState({
|
|
167
|
-
select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
invariant(
|
|
171
|
-
routeId,
|
|
172
|
-
`Could not find routeId for matchId "${matchId}". Please file an issue!`,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
const route: AnyRoute = router.routesById[routeId]
|
|
176
|
-
|
|
177
|
-
const PendingComponent =
|
|
178
|
-
route.options.pendingComponent ?? router.options.defaultPendingComponent
|
|
179
|
-
|
|
180
|
-
const pendingElement = PendingComponent ? <PendingComponent /> : null
|
|
181
|
-
|
|
182
|
-
const routeErrorComponent =
|
|
183
|
-
route.options.errorComponent ?? router.options.defaultErrorComponent
|
|
184
|
-
|
|
185
|
-
const routeOnCatch = route.options.onCatch ?? router.options.defaultOnCatch
|
|
186
|
-
|
|
187
|
-
const routeNotFoundComponent = route.isRoot
|
|
188
|
-
? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
|
|
189
|
-
route.options.notFoundComponent ??
|
|
190
|
-
router.options.notFoundRoute?.options.component
|
|
191
|
-
: route.options.notFoundComponent
|
|
192
|
-
|
|
193
|
-
const ResolvedSuspenseBoundary =
|
|
194
|
-
// If we're on the root route, allow forcefully wrapping in suspense
|
|
195
|
-
(!route.isRoot || route.options.wrapInSuspense) &&
|
|
196
|
-
(route.options.wrapInSuspense ??
|
|
197
|
-
PendingComponent ??
|
|
198
|
-
(route.options.errorComponent as any)?.preload)
|
|
199
|
-
? React.Suspense
|
|
200
|
-
: SafeFragment
|
|
201
|
-
|
|
202
|
-
const ResolvedCatchBoundary = routeErrorComponent
|
|
203
|
-
? CatchBoundary
|
|
204
|
-
: SafeFragment
|
|
205
|
-
|
|
206
|
-
const ResolvedNotFoundBoundary = routeNotFoundComponent
|
|
207
|
-
? CatchNotFound
|
|
208
|
-
: SafeFragment
|
|
209
|
-
|
|
210
|
-
const resetKey = useRouterState({
|
|
211
|
-
select: (s) => s.resolvedLocation.state.key!,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
return (
|
|
215
|
-
<matchContext.Provider value={matchId}>
|
|
216
|
-
<ResolvedSuspenseBoundary fallback={pendingElement}>
|
|
217
|
-
<ResolvedCatchBoundary
|
|
218
|
-
getResetKey={() => resetKey}
|
|
219
|
-
errorComponent={routeErrorComponent || ErrorComponent}
|
|
220
|
-
onCatch={(error, errorInfo) => {
|
|
221
|
-
// Forward not found errors (we don't want to show the error component for these)
|
|
222
|
-
if (isNotFound(error)) throw error
|
|
223
|
-
warning(false, `Error in route match: ${matchId}`)
|
|
224
|
-
routeOnCatch?.(error, errorInfo)
|
|
225
|
-
}}
|
|
226
|
-
>
|
|
227
|
-
<ResolvedNotFoundBoundary
|
|
228
|
-
fallback={(error) => {
|
|
229
|
-
// If the current not found handler doesn't exist or it has a
|
|
230
|
-
// route ID which doesn't match the current route, rethrow the error
|
|
231
|
-
if (
|
|
232
|
-
!routeNotFoundComponent ||
|
|
233
|
-
(error.routeId && error.routeId !== routeId) ||
|
|
234
|
-
(!error.routeId && !route.isRoot)
|
|
235
|
-
)
|
|
236
|
-
throw error
|
|
237
|
-
|
|
238
|
-
return React.createElement(routeNotFoundComponent, error as any)
|
|
239
|
-
}}
|
|
240
|
-
>
|
|
241
|
-
<MatchInner matchId={matchId} />
|
|
242
|
-
</ResolvedNotFoundBoundary>
|
|
243
|
-
</ResolvedCatchBoundary>
|
|
244
|
-
</ResolvedSuspenseBoundary>
|
|
245
|
-
</matchContext.Provider>
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function MatchInner({ matchId }: { matchId: string }): any {
|
|
250
|
-
const router = useRouter()
|
|
251
|
-
const routeId = useRouterState({
|
|
252
|
-
select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const route = router.routesById[routeId]!
|
|
256
|
-
|
|
257
|
-
const match = useRouterState({
|
|
258
|
-
select: (s) =>
|
|
259
|
-
pick(s.matches.find((d) => d.id === matchId)!, [
|
|
260
|
-
'id',
|
|
261
|
-
'status',
|
|
262
|
-
'error',
|
|
263
|
-
'loadPromise',
|
|
264
|
-
'minPendingPromise',
|
|
265
|
-
]),
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
const RouteErrorComponent =
|
|
269
|
-
(route.options.errorComponent ?? router.options.defaultErrorComponent) ||
|
|
270
|
-
ErrorComponent
|
|
271
|
-
|
|
272
|
-
if (match.status === 'notFound') {
|
|
273
|
-
let error: unknown
|
|
274
|
-
if (isServerSideError(match.error)) {
|
|
275
|
-
const deserializeError =
|
|
276
|
-
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
|
|
277
|
-
|
|
278
|
-
error = deserializeError(match.error.data)
|
|
279
|
-
} else {
|
|
280
|
-
error = match.error
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
invariant(isNotFound(error), 'Expected a notFound error')
|
|
284
|
-
|
|
285
|
-
return renderRouteNotFound(router, route, error)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (match.status === 'redirected') {
|
|
289
|
-
// Redirects should be handled by the router transition. If we happen to
|
|
290
|
-
// encounter a redirect here, it's a bug. Let's warn, but render nothing.
|
|
291
|
-
invariant(isRedirect(match.error), 'Expected a redirect error')
|
|
292
|
-
|
|
293
|
-
// warning(
|
|
294
|
-
// false,
|
|
295
|
-
// 'Tried to render a redirected route match! This is a weird circumstance, please file an issue!',
|
|
296
|
-
// )
|
|
297
|
-
|
|
298
|
-
throw match.loadPromise
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (match.status === 'error') {
|
|
302
|
-
// If we're on the server, we need to use React's new and super
|
|
303
|
-
// wonky api for throwing errors from a server side render inside
|
|
304
|
-
// of a suspense boundary. This is the only way to get
|
|
305
|
-
// renderToPipeableStream to not hang indefinitely.
|
|
306
|
-
// We'll serialize the error and rethrow it on the client.
|
|
307
|
-
if (router.isServer) {
|
|
308
|
-
return (
|
|
309
|
-
<RouteErrorComponent
|
|
310
|
-
error={match.error}
|
|
311
|
-
info={{
|
|
312
|
-
componentStack: '',
|
|
313
|
-
}}
|
|
314
|
-
/>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (isServerSideError(match.error)) {
|
|
319
|
-
const deserializeError =
|
|
320
|
-
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
|
|
321
|
-
throw deserializeError(match.error.data)
|
|
322
|
-
} else {
|
|
323
|
-
throw match.error
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (match.status === 'pending') {
|
|
328
|
-
// We're pending, and if we have a minPendingMs, we need to wait for it
|
|
329
|
-
const pendingMinMs =
|
|
330
|
-
route.options.pendingMinMs ?? router.options.defaultPendingMinMs
|
|
331
|
-
|
|
332
|
-
if (pendingMinMs && !match.minPendingPromise) {
|
|
333
|
-
// Create a promise that will resolve after the minPendingMs
|
|
334
|
-
|
|
335
|
-
match.minPendingPromise = createControlledPromise()
|
|
336
|
-
|
|
337
|
-
if (!router.isServer) {
|
|
338
|
-
Promise.resolve().then(() => {
|
|
339
|
-
router.__store.setState((s) => ({
|
|
340
|
-
...s,
|
|
341
|
-
matches: s.matches.map((d) =>
|
|
342
|
-
d.id === match.id
|
|
343
|
-
? { ...d, minPendingPromise: createControlledPromise() }
|
|
344
|
-
: d,
|
|
345
|
-
),
|
|
346
|
-
}))
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
setTimeout(() => {
|
|
350
|
-
// We've handled the minPendingPromise, so we can delete it
|
|
351
|
-
router.__store.setState((s) => {
|
|
352
|
-
return {
|
|
353
|
-
...s,
|
|
354
|
-
matches: s.matches.map((d) =>
|
|
355
|
-
d.id === match.id
|
|
356
|
-
? {
|
|
357
|
-
...d,
|
|
358
|
-
minPendingPromise:
|
|
359
|
-
(d.minPendingPromise?.resolve(), undefined),
|
|
360
|
-
}
|
|
361
|
-
: d,
|
|
362
|
-
),
|
|
363
|
-
}
|
|
364
|
-
})
|
|
365
|
-
}, pendingMinMs)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
throw match.loadPromise
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
373
|
-
if (match.status === 'success') {
|
|
374
|
-
const Comp = route.options.component ?? router.options.defaultComponent
|
|
375
|
-
|
|
376
|
-
if (Comp) {
|
|
377
|
-
return <Comp />
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return <Outlet />
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
invariant(
|
|
384
|
-
false,
|
|
385
|
-
'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
|
|
386
|
-
)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export const Outlet = React.memo(function Outlet() {
|
|
390
|
-
const router = useRouter()
|
|
391
|
-
const matchId = React.useContext(matchContext)
|
|
392
|
-
const routeId = useRouterState({
|
|
393
|
-
select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
const route = router.routesById[routeId]!
|
|
397
|
-
|
|
398
|
-
const { parentGlobalNotFound } = useRouterState({
|
|
399
|
-
select: (s) => {
|
|
400
|
-
const matches = s.matches
|
|
401
|
-
const parentMatch = matches.find((d) => d.id === matchId)
|
|
402
|
-
invariant(
|
|
403
|
-
parentMatch,
|
|
404
|
-
`Could not find parent match for matchId "${matchId}"`,
|
|
405
|
-
)
|
|
406
|
-
return {
|
|
407
|
-
parentGlobalNotFound: parentMatch.globalNotFound,
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
const childMatchId = useRouterState({
|
|
413
|
-
select: (s) => {
|
|
414
|
-
const matches = s.matches
|
|
415
|
-
const index = matches.findIndex((d) => d.id === matchId)
|
|
416
|
-
return matches[index + 1]?.id
|
|
417
|
-
},
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
if (parentGlobalNotFound) {
|
|
421
|
-
return renderRouteNotFound(router, route, undefined)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (!childMatchId) {
|
|
425
|
-
return null
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const nextMatch = <Match matchId={childMatchId} />
|
|
429
|
-
|
|
430
|
-
const pendingElement = router.options.defaultPendingComponent ? (
|
|
431
|
-
<router.options.defaultPendingComponent />
|
|
432
|
-
) : null
|
|
433
|
-
|
|
434
|
-
if (matchId === rootRouteId) {
|
|
435
|
-
return (
|
|
436
|
-
<React.Suspense fallback={pendingElement}>{nextMatch}</React.Suspense>
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return nextMatch
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
function renderRouteNotFound(router: AnyRouter, route: AnyRoute, data: any) {
|
|
444
|
-
if (!route.options.notFoundComponent) {
|
|
445
|
-
if (router.options.defaultNotFoundComponent) {
|
|
446
|
-
return <router.options.defaultNotFoundComponent data={data} />
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (process.env.NODE_ENV === 'development') {
|
|
450
|
-
warning(
|
|
451
|
-
route.options.notFoundComponent,
|
|
452
|
-
`A notFoundError was encountered on the route with ID "${route.id}", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (<div>Not Found<div>)`,
|
|
453
|
-
)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return <DefaultGlobalNotFound />
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return <route.options.notFoundComponent data={data} />
|
|
460
|
-
}
|
|
461
|
-
|
|
462
157
|
export interface MatchRouteOptions {
|
|
463
158
|
pending?: boolean
|
|
464
159
|
caseSensitive?: boolean
|
|
@@ -608,27 +303,3 @@ export function useChildMatches<
|
|
|
608
303
|
},
|
|
609
304
|
})
|
|
610
305
|
}
|
|
611
|
-
|
|
612
|
-
export function isServerSideError(error: unknown): error is {
|
|
613
|
-
__isServerError: true
|
|
614
|
-
data: Record<string, any>
|
|
615
|
-
} {
|
|
616
|
-
if (!(typeof error === 'object' && error && 'data' in error)) return false
|
|
617
|
-
if (!('__isServerError' in error && error.__isServerError)) return false
|
|
618
|
-
if (!(typeof error.data === 'object' && error.data)) return false
|
|
619
|
-
|
|
620
|
-
return error.__isServerError === true
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
export function defaultDeserializeError(serializedData: Record<string, any>) {
|
|
624
|
-
if ('name' in serializedData && 'message' in serializedData) {
|
|
625
|
-
const error = new Error(serializedData.message)
|
|
626
|
-
error.name = serializedData.name
|
|
627
|
-
if (process.env.NODE_ENV === 'development') {
|
|
628
|
-
error.stack = serializedData.stack
|
|
629
|
-
}
|
|
630
|
-
return error
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return serializedData.data
|
|
634
|
-
}
|
package/src/RouterProvider.tsx
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function ScriptOnce({
|
|
2
|
+
className,
|
|
3
|
+
children,
|
|
4
|
+
log,
|
|
5
|
+
...rest
|
|
6
|
+
}: { children: string; log?: boolean } & React.HTMLProps<HTMLScriptElement>) {
|
|
7
|
+
if (typeof document !== 'undefined') {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<script
|
|
13
|
+
{...rest}
|
|
14
|
+
className={`tsr-once ${className || ''}`}
|
|
15
|
+
dangerouslySetInnerHTML={{
|
|
16
|
+
__html: [
|
|
17
|
+
children,
|
|
18
|
+
(log ?? true) && process.env.NODE_ENV === 'development'
|
|
19
|
+
? `console.info('ScriptOnce', ${JSON.stringify(children)})`
|
|
20
|
+
: '',
|
|
21
|
+
]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join('\n'),
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
package/src/Transitioner.tsx
CHANGED
|
@@ -52,7 +52,7 @@ export function Transitioner() {
|
|
|
52
52
|
// Try to load the initial location
|
|
53
53
|
useLayoutEffect(() => {
|
|
54
54
|
if (
|
|
55
|
-
window.
|
|
55
|
+
window.__TSR__?.dehydrated ||
|
|
56
56
|
(mountLoadForRouter.current.router === router &&
|
|
57
57
|
mountLoadForRouter.current.mounted)
|
|
58
58
|
) {
|
package/src/awaited.tsx
CHANGED
|
@@ -2,86 +2,50 @@ import * as React from 'react'
|
|
|
2
2
|
import warning from 'tiny-warning'
|
|
3
3
|
import { useRouter } from './useRouter'
|
|
4
4
|
import { defaultSerializeError } from './router'
|
|
5
|
-
import {
|
|
6
|
-
import { defaultDeserializeError, isServerSideError } from './
|
|
5
|
+
import { defer } from './defer'
|
|
6
|
+
import { defaultDeserializeError, isServerSideError } from './isServerSideError'
|
|
7
7
|
import type { DeferredPromise } from './defer'
|
|
8
8
|
|
|
9
9
|
export type AwaitOptions<T> = {
|
|
10
|
-
promise:
|
|
10
|
+
promise: Promise<T>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useAwaited<T>({
|
|
14
|
-
promise,
|
|
14
|
+
promise: _promise,
|
|
15
15
|
}: AwaitOptions<T>): [T, DeferredPromise<T>] {
|
|
16
16
|
const router = useRouter()
|
|
17
|
-
|
|
17
|
+
const promise = _promise as DeferredPromise<T>
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
defer(promise)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// were resolved on the server and no further action is needed
|
|
24
|
-
if (isDehydratedDeferred(promise) && state.status === 'pending') {
|
|
25
|
-
const streamedData = (window as any)[`__TSR__DEFERRED__${state.uid}`]
|
|
26
|
-
|
|
27
|
-
if (streamedData) {
|
|
28
|
-
Object.assign(state, router.options.transformer.parse(streamedData))
|
|
29
|
-
} else {
|
|
30
|
-
let token = router.registeredDeferredsIds.get(state.uid)
|
|
31
|
-
|
|
32
|
-
// If we haven't yet, create a promise and resolver that our streamed HTML can use
|
|
33
|
-
// when the client-side data is streamed in and ready.
|
|
34
|
-
if (!token) {
|
|
35
|
-
token = {}
|
|
36
|
-
router.registeredDeferredsIds.set(state.uid, token)
|
|
37
|
-
router.registeredDeferreds.set(token, state)
|
|
38
|
-
|
|
39
|
-
Object.assign(state, {
|
|
40
|
-
resolve: () => {
|
|
41
|
-
state.__resolvePromise?.()
|
|
42
|
-
// rerender()
|
|
43
|
-
},
|
|
44
|
-
promise: new Promise((r) => {
|
|
45
|
-
state.__resolvePromise = r as any
|
|
46
|
-
}),
|
|
47
|
-
__resolvePromise: () => {},
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
}
|
|
21
|
+
if (promise.status === 'pending') {
|
|
22
|
+
throw promise
|
|
51
23
|
}
|
|
52
24
|
|
|
53
|
-
|
|
54
|
-
// For originating promises, this will be the original promise
|
|
55
|
-
// For dehydrated promises, this will be the placeholder promise
|
|
56
|
-
// that will be resolved when the server sends the real data
|
|
57
|
-
if (state.status === 'pending') {
|
|
58
|
-
throw isDehydratedDeferred(promise) ? state.promise : promise
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (state.status === 'error') {
|
|
25
|
+
if (promise.status === 'error') {
|
|
62
26
|
if (typeof document !== 'undefined') {
|
|
63
|
-
if (isServerSideError(
|
|
27
|
+
if (isServerSideError(promise.error)) {
|
|
64
28
|
throw (
|
|
65
29
|
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
|
|
66
|
-
)(
|
|
30
|
+
)(promise.error.data as any)
|
|
67
31
|
} else {
|
|
68
32
|
warning(
|
|
69
33
|
false,
|
|
70
34
|
"Encountered a server-side error that doesn't fit the expected shape",
|
|
71
35
|
)
|
|
72
|
-
throw
|
|
36
|
+
throw promise.error
|
|
73
37
|
}
|
|
74
38
|
} else {
|
|
75
39
|
throw {
|
|
76
40
|
data: (
|
|
77
41
|
router.options.errorSerializer?.serialize ?? defaultSerializeError
|
|
78
|
-
)(
|
|
42
|
+
)(promise.error),
|
|
79
43
|
__isServerError: true,
|
|
80
44
|
}
|
|
81
45
|
}
|
|
82
46
|
}
|
|
83
47
|
|
|
84
|
-
return [promise.
|
|
48
|
+
return [promise.data as any, promise]
|
|
85
49
|
}
|
|
86
50
|
|
|
87
51
|
export function Await<T>(
|
|
@@ -102,43 +66,7 @@ function AwaitInner<T>(
|
|
|
102
66
|
fallback?: React.ReactNode
|
|
103
67
|
children: (result: T) => React.ReactNode
|
|
104
68
|
},
|
|
105
|
-
) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const state = promise.__deferredState
|
|
109
|
-
// If we are the originator of the promise,
|
|
110
|
-
// inject the state into the HTML stream
|
|
111
|
-
return (
|
|
112
|
-
<>
|
|
113
|
-
{!isDehydratedDeferred(promise) ? (
|
|
114
|
-
<ScriptOnce
|
|
115
|
-
children={`window.__TSR__DEFERRED__${state.uid} = ${JSON.stringify(router.options.transformer.stringify(state))}
|
|
116
|
-
if (window.__TSR__ROUTER__) {
|
|
117
|
-
let deferred = window.__TSR__ROUTER__.getDeferred('${state.uid}');
|
|
118
|
-
if (deferred) deferred.resolve(window.__TSR__DEFERRED__${state.uid});
|
|
119
|
-
}`}
|
|
120
|
-
/>
|
|
121
|
-
) : null}
|
|
122
|
-
{props.children(data)}
|
|
123
|
-
</>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function ScriptOnce({
|
|
128
|
-
className,
|
|
129
|
-
children,
|
|
130
|
-
...rest
|
|
131
|
-
}: { children: string } & React.HTMLProps<HTMLScriptElement>) {
|
|
132
|
-
return (
|
|
133
|
-
<script
|
|
134
|
-
{...rest}
|
|
135
|
-
className={`tsr-script-once ${className || ''}`}
|
|
136
|
-
dangerouslySetInnerHTML={{
|
|
137
|
-
__html: [
|
|
138
|
-
children,
|
|
139
|
-
`document.querySelectorAll('.tsr-script-once').forEach((el) => el.parentElement.removeChild(el));`,
|
|
140
|
-
].join('\n'),
|
|
141
|
-
}}
|
|
142
|
-
/>
|
|
143
|
-
)
|
|
69
|
+
): React.JSX.Element {
|
|
70
|
+
const [data] = useAwaited(props)
|
|
71
|
+
return props.children(data) as React.JSX.Element
|
|
144
72
|
}
|
package/src/defer.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { defaultSerializeError } from './router'
|
|
|
3
3
|
export type DeferredPromiseState<T> = {
|
|
4
4
|
uid: string
|
|
5
5
|
resolve?: () => void
|
|
6
|
-
|
|
7
|
-
__resolvePromise?: () => void
|
|
6
|
+
reject?: () => void
|
|
8
7
|
} & (
|
|
9
8
|
| {
|
|
10
9
|
status: 'pending'
|
|
@@ -22,9 +21,7 @@ export type DeferredPromiseState<T> = {
|
|
|
22
21
|
}
|
|
23
22
|
)
|
|
24
23
|
|
|
25
|
-
export type DeferredPromise<T> = Promise<T> &
|
|
26
|
-
__deferredState: DeferredPromiseState<T>
|
|
27
|
-
}
|
|
24
|
+
export type DeferredPromise<T> = Promise<T> & DeferredPromiseState<T>
|
|
28
25
|
|
|
29
26
|
export function defer<T>(
|
|
30
27
|
_promise: Promise<T>,
|
|
@@ -34,23 +31,19 @@ export function defer<T>(
|
|
|
34
31
|
) {
|
|
35
32
|
const promise = _promise as DeferredPromise<T>
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
promise.__deferredState = {
|
|
40
|
-
uid: Math.random().toString(36).slice(2),
|
|
34
|
+
if (!(promise as any).status) {
|
|
35
|
+
Object.assign(promise, {
|
|
41
36
|
status: 'pending',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const state = promise.__deferredState
|
|
37
|
+
})
|
|
45
38
|
|
|
46
39
|
promise
|
|
47
40
|
.then((data) => {
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
promise.status = 'success' as any
|
|
42
|
+
promise.data = data
|
|
50
43
|
})
|
|
51
44
|
.catch((error) => {
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
promise.status = 'error' as any
|
|
46
|
+
;(promise as any).error = {
|
|
54
47
|
data: (options?.serializeError ?? defaultSerializeError)(error),
|
|
55
48
|
__isServerError: true,
|
|
56
49
|
}
|
|
@@ -59,13 +52,3 @@ export function defer<T>(
|
|
|
59
52
|
|
|
60
53
|
return promise
|
|
61
54
|
}
|
|
62
|
-
|
|
63
|
-
export function isDehydratedDeferred(obj: any): boolean {
|
|
64
|
-
return (
|
|
65
|
-
typeof obj === 'object' &&
|
|
66
|
-
obj !== null &&
|
|
67
|
-
!(obj instanceof Promise) &&
|
|
68
|
-
!obj.then &&
|
|
69
|
-
'__deferredState' in obj
|
|
70
|
-
)
|
|
71
|
-
}
|