@tanstack/router-core 1.121.0-alpha.27 → 1.121.0-alpha.28
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.map +1 -1
- package/dist/cjs/Matches.d.cts +31 -1
- package/dist/cjs/RouterProvider.d.cts +2 -1
- package/dist/cjs/defer.cjs +1 -1
- package/dist/cjs/defer.cjs.map +1 -1
- package/dist/cjs/global.d.cts +7 -0
- package/dist/cjs/index.cjs +1 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +6 -6
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/link.d.cts +12 -0
- package/dist/cjs/lru-cache.cjs +62 -0
- package/dist/cjs/lru-cache.cjs.map +1 -0
- package/dist/cjs/lru-cache.d.cts +5 -0
- package/dist/cjs/not-found.cjs +1 -1
- package/dist/cjs/not-found.cjs.map +1 -1
- package/dist/cjs/path.cjs +316 -148
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +18 -24
- package/dist/cjs/qss.cjs.map +1 -1
- package/dist/cjs/redirect.cjs +3 -0
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/route.cjs +6 -12
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +29 -9
- package/dist/cjs/router.cjs +453 -272
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +55 -85
- package/dist/cjs/scroll-restoration.cjs +20 -13
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/scroll-restoration.d.cts +9 -1
- package/dist/cjs/searchMiddleware.cjs.map +1 -1
- package/dist/cjs/searchParams.cjs.map +1 -1
- package/dist/cjs/ssr/client.cjs +10 -0
- package/dist/cjs/ssr/client.cjs.map +1 -0
- package/dist/cjs/ssr/client.d.cts +5 -0
- package/dist/cjs/ssr/createRequestHandler.cjs +50 -0
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -0
- package/dist/cjs/ssr/createRequestHandler.d.cts +9 -0
- package/dist/cjs/ssr/handlerCallback.cjs +7 -0
- package/dist/cjs/ssr/handlerCallback.cjs.map +1 -0
- package/dist/cjs/ssr/handlerCallback.d.cts +9 -0
- package/dist/cjs/ssr/headers.cjs +39 -0
- package/dist/cjs/ssr/headers.cjs.map +1 -0
- package/dist/cjs/ssr/headers.d.cts +5 -0
- package/dist/cjs/ssr/json.cjs +14 -0
- package/dist/cjs/ssr/json.cjs.map +1 -0
- package/dist/cjs/ssr/json.d.cts +4 -0
- package/dist/cjs/ssr/seroval-plugins.cjs +34 -0
- package/dist/cjs/ssr/seroval-plugins.cjs.map +1 -0
- package/dist/cjs/ssr/seroval-plugins.d.cts +10 -0
- package/dist/cjs/ssr/server.cjs +13 -0
- package/dist/cjs/ssr/server.cjs.map +1 -0
- package/dist/cjs/ssr/server.d.cts +6 -0
- package/dist/cjs/ssr/ssr-client.cjs +159 -0
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -0
- package/dist/cjs/ssr/ssr-client.d.cts +29 -0
- package/dist/cjs/ssr/ssr-server.cjs +107 -0
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -0
- package/dist/cjs/ssr/ssr-server.d.cts +18 -0
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +183 -0
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -0
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +6 -0
- package/dist/cjs/ssr/tsrScript.cjs +4 -0
- package/dist/cjs/ssr/tsrScript.cjs.map +1 -0
- package/dist/cjs/ssr/tsrScript.d.cts +0 -0
- package/dist/cjs/utils.cjs +7 -25
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -6
- package/dist/esm/Matches.d.ts +31 -1
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/RouterProvider.d.ts +2 -1
- package/dist/esm/defer.js +1 -1
- package/dist/esm/defer.js.map +1 -1
- package/dist/esm/global.d.ts +7 -0
- package/dist/esm/index.d.ts +6 -6
- package/dist/esm/index.js +2 -3
- package/dist/esm/link.d.ts +12 -0
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/lru-cache.d.ts +5 -0
- package/dist/esm/lru-cache.js +62 -0
- package/dist/esm/lru-cache.js.map +1 -0
- package/dist/esm/not-found.js +1 -1
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/path.d.ts +18 -24
- package/dist/esm/path.js +316 -148
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/qss.js.map +1 -1
- package/dist/esm/redirect.js +3 -0
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/route.d.ts +29 -9
- package/dist/esm/route.js +6 -12
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +55 -85
- package/dist/esm/router.js +462 -281
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.d.ts +9 -1
- package/dist/esm/scroll-restoration.js +20 -13
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/searchMiddleware.js.map +1 -1
- package/dist/esm/searchParams.js.map +1 -1
- package/dist/esm/ssr/client.d.ts +5 -0
- package/dist/esm/ssr/client.js +10 -0
- package/dist/esm/ssr/client.js.map +1 -0
- package/dist/esm/ssr/createRequestHandler.d.ts +9 -0
- package/dist/esm/ssr/createRequestHandler.js +50 -0
- package/dist/esm/ssr/createRequestHandler.js.map +1 -0
- package/dist/esm/ssr/handlerCallback.d.ts +9 -0
- package/dist/esm/ssr/handlerCallback.js +7 -0
- package/dist/esm/ssr/handlerCallback.js.map +1 -0
- package/dist/esm/ssr/headers.d.ts +5 -0
- package/dist/esm/ssr/headers.js +39 -0
- package/dist/esm/ssr/headers.js.map +1 -0
- package/dist/esm/ssr/json.d.ts +4 -0
- package/dist/esm/ssr/json.js +14 -0
- package/dist/esm/ssr/json.js.map +1 -0
- package/dist/esm/ssr/seroval-plugins.d.ts +10 -0
- package/dist/esm/ssr/seroval-plugins.js +34 -0
- package/dist/esm/ssr/seroval-plugins.js.map +1 -0
- package/dist/esm/ssr/server.d.ts +6 -0
- package/dist/esm/ssr/server.js +13 -0
- package/dist/esm/ssr/server.js.map +1 -0
- package/dist/esm/ssr/ssr-client.d.ts +29 -0
- package/dist/esm/ssr/ssr-client.js +159 -0
- package/dist/esm/ssr/ssr-client.js.map +1 -0
- package/dist/esm/ssr/ssr-server.d.ts +18 -0
- package/dist/esm/ssr/ssr-server.js +107 -0
- package/dist/esm/ssr/ssr-server.js.map +1 -0
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +6 -0
- package/dist/esm/ssr/transformStreamWithRouter.js +183 -0
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -0
- package/dist/esm/ssr/tsrScript.d.ts +0 -0
- package/dist/esm/ssr/tsrScript.js +5 -0
- package/dist/esm/ssr/tsrScript.js.map +1 -0
- package/dist/esm/utils.d.ts +1 -6
- package/dist/esm/utils.js +8 -26
- package/dist/esm/utils.js.map +1 -1
- package/package.json +29 -2
- package/src/Matches.ts +40 -1
- package/src/RouterProvider.ts +2 -1
- package/src/global.ts +9 -0
- package/src/index.ts +12 -20
- package/src/link.ts +12 -0
- package/src/lru-cache.ts +68 -0
- package/src/path.ts +424 -174
- package/src/redirect.ts +3 -0
- package/src/route.ts +44 -13
- package/src/router.ts +580 -312
- package/src/scroll-restoration.ts +30 -18
- package/src/ssr/client.ts +5 -0
- package/src/ssr/createRequestHandler.ts +74 -0
- package/src/ssr/handlerCallback.ts +15 -0
- package/src/ssr/headers.ts +51 -0
- package/src/ssr/json.ts +18 -0
- package/src/ssr/seroval-plugins.ts +43 -0
- package/src/ssr/server.ts +10 -0
- package/src/ssr/ssr-client.ts +242 -0
- package/src/ssr/ssr-server.ts +132 -0
- package/src/ssr/transformStreamWithRouter.ts +259 -0
- package/src/ssr/tsrScript.ts +7 -0
- package/src/utils.ts +10 -39
- package/src/vite-env.d.ts +4 -0
- package/dist/cjs/serializer.d.cts +0 -22
- package/dist/esm/serializer.d.ts +0 -22
- package/src/serializer.ts +0 -32
|
@@ -2,6 +2,7 @@ import { functionalUpdate } from './utils'
|
|
|
2
2
|
import type { AnyRouter } from './router'
|
|
3
3
|
import type { ParsedLocation } from './location'
|
|
4
4
|
import type { NonNullableUpdater } from './utils'
|
|
5
|
+
import type { HistoryLocation } from '@tanstack/history'
|
|
5
6
|
|
|
6
7
|
export type ScrollRestorationEntry = { scrollX: number; scrollY: number }
|
|
7
8
|
|
|
@@ -79,7 +80,7 @@ export const scrollRestorationCache = createScrollRestorationCache()
|
|
|
79
80
|
*/
|
|
80
81
|
|
|
81
82
|
export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
|
|
82
|
-
return location.state.
|
|
83
|
+
return location.state.__TSR_key! || location.href
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function getCssSelector(el: any): string {
|
|
@@ -100,15 +101,21 @@ let ignoreScroll = false
|
|
|
100
101
|
// unless they are passed in as arguments. Why? Because we need to be able to
|
|
101
102
|
// toString() it into a script tag to execute as early as possible in the browser
|
|
102
103
|
// during SSR. Additionally, we also call it from within the router lifecycle
|
|
103
|
-
export function restoreScroll(
|
|
104
|
-
storageKey
|
|
105
|
-
key
|
|
106
|
-
behavior
|
|
107
|
-
shouldScrollRestoration
|
|
108
|
-
scrollToTopSelectors
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
export function restoreScroll({
|
|
105
|
+
storageKey,
|
|
106
|
+
key,
|
|
107
|
+
behavior,
|
|
108
|
+
shouldScrollRestoration,
|
|
109
|
+
scrollToTopSelectors,
|
|
110
|
+
location,
|
|
111
|
+
}: {
|
|
112
|
+
storageKey: string
|
|
113
|
+
key?: string
|
|
114
|
+
behavior?: ScrollToOptions['behavior']
|
|
115
|
+
shouldScrollRestoration?: boolean
|
|
116
|
+
scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
|
|
117
|
+
location?: HistoryLocation
|
|
118
|
+
}) {
|
|
112
119
|
let byKey: ScrollRestorationByKey
|
|
113
120
|
|
|
114
121
|
try {
|
|
@@ -128,7 +135,11 @@ export function restoreScroll(
|
|
|
128
135
|
;(() => {
|
|
129
136
|
// If we have a cached entry for this location state,
|
|
130
137
|
// we always need to prefer that over the hash scroll.
|
|
131
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
shouldScrollRestoration &&
|
|
140
|
+
elementEntries &&
|
|
141
|
+
Object.keys(elementEntries).length > 0
|
|
142
|
+
) {
|
|
132
143
|
for (const elementSelector in elementEntries) {
|
|
133
144
|
const entry = elementEntries[elementSelector]!
|
|
134
145
|
if (elementSelector === 'window') {
|
|
@@ -153,7 +164,7 @@ export function restoreScroll(
|
|
|
153
164
|
// Which means we've never seen this location before,
|
|
154
165
|
// we need to check if there is a hash in the URL.
|
|
155
166
|
// If there is, we need to scroll it's ID into view.
|
|
156
|
-
const hash = window.location.hash.split('#')[1]
|
|
167
|
+
const hash = (location ?? window.location).hash.split('#')[1]
|
|
157
168
|
|
|
158
169
|
if (hash) {
|
|
159
170
|
const hashScrollIntoViewOptions =
|
|
@@ -321,13 +332,14 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
321
332
|
return
|
|
322
333
|
}
|
|
323
334
|
|
|
324
|
-
restoreScroll(
|
|
335
|
+
restoreScroll({
|
|
325
336
|
storageKey,
|
|
326
|
-
cacheKey,
|
|
327
|
-
router.options.scrollRestorationBehavior
|
|
328
|
-
router.isScrollRestoring
|
|
329
|
-
router.options.scrollToTopSelectors
|
|
330
|
-
|
|
337
|
+
key: cacheKey,
|
|
338
|
+
behavior: router.options.scrollRestorationBehavior,
|
|
339
|
+
shouldScrollRestoration: router.isScrollRestoring,
|
|
340
|
+
scrollToTopSelectors: router.options.scrollToTopSelectors,
|
|
341
|
+
location: router.history.location,
|
|
342
|
+
})
|
|
331
343
|
|
|
332
344
|
if (router.isScrollRestoring) {
|
|
333
345
|
// Mark the location as having been seen
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createMemoryHistory } from '@tanstack/history'
|
|
2
|
+
import { mergeHeaders } from './headers'
|
|
3
|
+
import { attachRouterServerSsrUtils } from './ssr-server'
|
|
4
|
+
import type { HandlerCallback } from './handlerCallback'
|
|
5
|
+
import type { AnyRouter } from '../router'
|
|
6
|
+
import type { Manifest } from '../manifest'
|
|
7
|
+
|
|
8
|
+
export type RequestHandler<TRouter extends AnyRouter> = (
|
|
9
|
+
cb: HandlerCallback<TRouter>,
|
|
10
|
+
) => Promise<Response>
|
|
11
|
+
|
|
12
|
+
export function createRequestHandler<TRouter extends AnyRouter>({
|
|
13
|
+
createRouter,
|
|
14
|
+
request,
|
|
15
|
+
getRouterManifest,
|
|
16
|
+
}: {
|
|
17
|
+
createRouter: () => TRouter
|
|
18
|
+
request: Request
|
|
19
|
+
getRouterManifest?: () => Manifest | Promise<Manifest>
|
|
20
|
+
}): RequestHandler<TRouter> {
|
|
21
|
+
return async (cb) => {
|
|
22
|
+
const router = createRouter()
|
|
23
|
+
|
|
24
|
+
attachRouterServerSsrUtils(router, await getRouterManifest?.())
|
|
25
|
+
|
|
26
|
+
const url = new URL(request.url, 'http://localhost')
|
|
27
|
+
|
|
28
|
+
const href = url.href.replace(url.origin, '')
|
|
29
|
+
|
|
30
|
+
// Create a history for the router
|
|
31
|
+
const history = createMemoryHistory({
|
|
32
|
+
initialEntries: [href],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Update the router with the history and context
|
|
36
|
+
router.update({
|
|
37
|
+
history,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await router.load()
|
|
41
|
+
|
|
42
|
+
await router.serverSsr?.dehydrate()
|
|
43
|
+
|
|
44
|
+
const responseHeaders = getRequestHeaders({
|
|
45
|
+
router,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return cb({
|
|
49
|
+
request,
|
|
50
|
+
router,
|
|
51
|
+
responseHeaders,
|
|
52
|
+
} as any)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getRequestHeaders(opts: { router: AnyRouter }): Headers {
|
|
57
|
+
let headers = mergeHeaders(
|
|
58
|
+
{
|
|
59
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
60
|
+
},
|
|
61
|
+
...opts.router.state.matches.map((match) => {
|
|
62
|
+
return match.headers
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Handle Redirects
|
|
67
|
+
const { redirect } = opts.router.state
|
|
68
|
+
|
|
69
|
+
if (redirect) {
|
|
70
|
+
headers = mergeHeaders(headers, redirect.headers)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return headers
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyRouter } from '../router'
|
|
2
|
+
|
|
3
|
+
export interface HandlerCallback<TRouter extends AnyRouter> {
|
|
4
|
+
(ctx: {
|
|
5
|
+
request: Request
|
|
6
|
+
router: TRouter
|
|
7
|
+
responseHeaders: Headers
|
|
8
|
+
}): Response | Promise<Response>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function defineHandlerCallback<TRouter extends AnyRouter>(
|
|
12
|
+
handler: HandlerCallback<TRouter>,
|
|
13
|
+
): HandlerCallback<TRouter> {
|
|
14
|
+
return handler
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { splitSetCookieString } from 'cookie-es'
|
|
2
|
+
import type { OutgoingHttpHeaders } from 'node:http2'
|
|
3
|
+
|
|
4
|
+
// A utility function to turn HeadersInit into an object
|
|
5
|
+
export function headersInitToObject(
|
|
6
|
+
headers: HeadersInit,
|
|
7
|
+
): Record<keyof OutgoingHttpHeaders, string> {
|
|
8
|
+
const obj: Record<keyof OutgoingHttpHeaders, string> = {}
|
|
9
|
+
const headersInstance = new Headers(headers)
|
|
10
|
+
for (const [key, value] of headersInstance.entries()) {
|
|
11
|
+
obj[key] = value
|
|
12
|
+
}
|
|
13
|
+
return obj
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type AnyHeaders =
|
|
17
|
+
| Headers
|
|
18
|
+
| HeadersInit
|
|
19
|
+
| Record<string, string>
|
|
20
|
+
| Array<[string, string]>
|
|
21
|
+
| OutgoingHttpHeaders
|
|
22
|
+
| undefined
|
|
23
|
+
|
|
24
|
+
// Helper function to convert various HeaderInit types to a Headers instance
|
|
25
|
+
function toHeadersInstance(init: AnyHeaders) {
|
|
26
|
+
if (init instanceof Headers) {
|
|
27
|
+
return new Headers(init)
|
|
28
|
+
} else if (Array.isArray(init)) {
|
|
29
|
+
return new Headers(init)
|
|
30
|
+
} else if (typeof init === 'object') {
|
|
31
|
+
return new Headers(init as HeadersInit)
|
|
32
|
+
} else {
|
|
33
|
+
return new Headers()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Function to merge headers with proper overrides
|
|
38
|
+
export function mergeHeaders(...headers: Array<AnyHeaders>) {
|
|
39
|
+
return headers.reduce((acc: Headers, header) => {
|
|
40
|
+
const headersInstance = toHeadersInstance(header)
|
|
41
|
+
for (const [key, value] of headersInstance.entries()) {
|
|
42
|
+
if (key === 'set-cookie') {
|
|
43
|
+
const splitCookies = splitSetCookieString(value)
|
|
44
|
+
splitCookies.forEach((cookie) => acc.append('set-cookie', cookie))
|
|
45
|
+
} else {
|
|
46
|
+
acc.set(key, value)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return acc
|
|
50
|
+
}, new Headers())
|
|
51
|
+
}
|
package/src/ssr/json.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mergeHeaders } from './headers'
|
|
2
|
+
|
|
3
|
+
export interface JsonResponse<TData> extends Response {
|
|
4
|
+
json: () => Promise<TData>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function json<TData>(
|
|
8
|
+
payload: TData,
|
|
9
|
+
init?: ResponseInit,
|
|
10
|
+
): JsonResponse<TData> {
|
|
11
|
+
return new Response(JSON.stringify(payload), {
|
|
12
|
+
...init,
|
|
13
|
+
headers: mergeHeaders(
|
|
14
|
+
{ 'content-type': 'application/json' },
|
|
15
|
+
init?.headers,
|
|
16
|
+
),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createPlugin } from 'seroval'
|
|
2
|
+
import type { SerovalNode } from 'seroval'
|
|
3
|
+
|
|
4
|
+
interface ErrorNode {
|
|
5
|
+
message: SerovalNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* this plugin serializes only the `message` part of an Error
|
|
10
|
+
* this helps with serializing e.g. a ZodError which has functions attached that cannot be serialized
|
|
11
|
+
*/
|
|
12
|
+
export const ShallowErrorPlugin = /* @__PURE__ */ createPlugin<
|
|
13
|
+
Error,
|
|
14
|
+
ErrorNode
|
|
15
|
+
>({
|
|
16
|
+
tag: 'tanstack-start:seroval-plugins/Error',
|
|
17
|
+
test(value) {
|
|
18
|
+
return value instanceof Error
|
|
19
|
+
},
|
|
20
|
+
parse: {
|
|
21
|
+
sync(value, ctx) {
|
|
22
|
+
return {
|
|
23
|
+
message: ctx.parse(value.message),
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async async(value, ctx) {
|
|
27
|
+
return {
|
|
28
|
+
message: await ctx.parse(value.message),
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
stream(value, ctx) {
|
|
32
|
+
return {
|
|
33
|
+
message: ctx.parse(value.message),
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
serialize(node, ctx) {
|
|
38
|
+
return 'new Error(' + ctx.serialize(node.message) + ')'
|
|
39
|
+
},
|
|
40
|
+
deserialize(node, ctx) {
|
|
41
|
+
return new Error(ctx.deserialize(node.message) as string)
|
|
42
|
+
},
|
|
43
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createRequestHandler } from './createRequestHandler'
|
|
2
|
+
export type { RequestHandler } from './createRequestHandler'
|
|
3
|
+
export { defineHandlerCallback } from './handlerCallback'
|
|
4
|
+
export type { HandlerCallback } from './handlerCallback'
|
|
5
|
+
export {
|
|
6
|
+
transformPipeableStreamWithRouter,
|
|
7
|
+
transformStreamWithRouter,
|
|
8
|
+
transformReadableStreamWithRouter,
|
|
9
|
+
} from './transformStreamWithRouter'
|
|
10
|
+
export { attachRouterServerSsrUtils } from './ssr-server'
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import invariant from 'tiny-invariant'
|
|
2
|
+
import { batch } from '@tanstack/store'
|
|
3
|
+
import { createControlledPromise } from '../utils'
|
|
4
|
+
import type { AnyRouteMatch, MakeRouteMatch } from '../Matches'
|
|
5
|
+
import type { AnyRouter } from '../router'
|
|
6
|
+
import type { Manifest } from '../manifest'
|
|
7
|
+
import type { RouteContextOptions } from '../route'
|
|
8
|
+
import type { GLOBAL_TSR } from './ssr-server'
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
[GLOBAL_TSR]?: TsrSsrGlobal
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TsrSsrGlobal {
|
|
17
|
+
router?: DehydratedRouter
|
|
18
|
+
// clean scripts, shortened since this is sent for each streamed script
|
|
19
|
+
c: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hydrateMatch(
|
|
23
|
+
deyhydratedMatch: DehydratedMatch,
|
|
24
|
+
): Partial<MakeRouteMatch> {
|
|
25
|
+
return {
|
|
26
|
+
id: deyhydratedMatch.i,
|
|
27
|
+
__beforeLoadContext: deyhydratedMatch.b,
|
|
28
|
+
loaderData: deyhydratedMatch.l,
|
|
29
|
+
status: deyhydratedMatch.s,
|
|
30
|
+
ssr: deyhydratedMatch.ssr,
|
|
31
|
+
updatedAt: deyhydratedMatch.u,
|
|
32
|
+
error: deyhydratedMatch.e,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export interface DehydratedMatch {
|
|
36
|
+
i: MakeRouteMatch['id']
|
|
37
|
+
b?: MakeRouteMatch['__beforeLoadContext']
|
|
38
|
+
l?: MakeRouteMatch['loaderData']
|
|
39
|
+
e?: MakeRouteMatch['error']
|
|
40
|
+
u: MakeRouteMatch['updatedAt']
|
|
41
|
+
s: MakeRouteMatch['status']
|
|
42
|
+
ssr?: MakeRouteMatch['ssr']
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DehydratedRouter {
|
|
46
|
+
manifest: Manifest | undefined
|
|
47
|
+
dehydratedData?: any
|
|
48
|
+
lastMatchId?: string
|
|
49
|
+
matches: Array<DehydratedMatch>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function hydrate(router: AnyRouter): Promise<any> {
|
|
53
|
+
invariant(
|
|
54
|
+
window.$_TSR?.router,
|
|
55
|
+
'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router
|
|
59
|
+
|
|
60
|
+
router.ssr = {
|
|
61
|
+
manifest,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Hydrate the router state
|
|
65
|
+
const matches = router.matchRoutes(router.state.location)
|
|
66
|
+
|
|
67
|
+
// kick off loading the route chunks
|
|
68
|
+
const routeChunkPromise = Promise.all(
|
|
69
|
+
matches.map((match) => {
|
|
70
|
+
const route = router.looseRoutesById[match.routeId]!
|
|
71
|
+
return router.loadRouteChunk(route)
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
function setMatchForcePending(match: AnyRouteMatch) {
|
|
76
|
+
// usually the minPendingPromise is created in the Match component if a pending match is rendered
|
|
77
|
+
// however, this might be too late if the match synchronously resolves
|
|
78
|
+
const route = router.looseRoutesById[match.routeId]!
|
|
79
|
+
const pendingMinMs =
|
|
80
|
+
route.options.pendingMinMs ?? router.options.defaultPendingMinMs
|
|
81
|
+
if (pendingMinMs) {
|
|
82
|
+
const minPendingPromise = createControlledPromise<void>()
|
|
83
|
+
match.minPendingPromise = minPendingPromise
|
|
84
|
+
match._forcePending = true
|
|
85
|
+
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
minPendingPromise.resolve()
|
|
88
|
+
// We've handled the minPendingPromise, so we can delete it
|
|
89
|
+
router.updateMatch(match.id, (prev) => ({
|
|
90
|
+
...prev,
|
|
91
|
+
minPendingPromise: undefined,
|
|
92
|
+
_forcePending: undefined,
|
|
93
|
+
}))
|
|
94
|
+
}, pendingMinMs)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Right after hydration and before the first render, we need to rehydrate each match
|
|
99
|
+
// First step is to reyhdrate loaderData and __beforeLoadContext
|
|
100
|
+
let firstNonSsrMatchIndex: number | undefined = undefined
|
|
101
|
+
matches.forEach((match) => {
|
|
102
|
+
const dehydratedMatch = window.$_TSR!.router!.matches.find(
|
|
103
|
+
(d) => d.i === match.id,
|
|
104
|
+
)
|
|
105
|
+
if (!dehydratedMatch) {
|
|
106
|
+
Object.assign(match, { dehydrated: false, ssr: false })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Object.assign(match, hydrateMatch(dehydratedMatch))
|
|
111
|
+
|
|
112
|
+
if (match.ssr === false) {
|
|
113
|
+
match._dehydrated = false
|
|
114
|
+
} else {
|
|
115
|
+
match._dehydrated = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (match.ssr === 'data-only' || match.ssr === false) {
|
|
119
|
+
if (firstNonSsrMatchIndex === undefined) {
|
|
120
|
+
firstNonSsrMatchIndex = match.index
|
|
121
|
+
setMatchForcePending(match)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
router.__store.setState((s) => {
|
|
127
|
+
return {
|
|
128
|
+
...s,
|
|
129
|
+
matches,
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Allow the user to handle custom hydration data
|
|
134
|
+
await router.options.hydrate?.(dehydratedData)
|
|
135
|
+
|
|
136
|
+
// now that all necessary data is hydrated:
|
|
137
|
+
// 1) fully reconstruct the route context
|
|
138
|
+
// 2) execute `head()` and `scripts()` for each match
|
|
139
|
+
await Promise.all(
|
|
140
|
+
router.state.matches.map(async (match) => {
|
|
141
|
+
const route = router.looseRoutesById[match.routeId]!
|
|
142
|
+
|
|
143
|
+
const parentMatch = router.state.matches[match.index - 1]
|
|
144
|
+
const parentContext = parentMatch?.context ?? router.options.context ?? {}
|
|
145
|
+
|
|
146
|
+
// `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
|
|
147
|
+
// so run it again and merge route context
|
|
148
|
+
const contextFnContext: RouteContextOptions<any, any, any, any> = {
|
|
149
|
+
deps: match.loaderDeps,
|
|
150
|
+
params: match.params,
|
|
151
|
+
context: parentContext,
|
|
152
|
+
location: router.state.location,
|
|
153
|
+
navigate: (opts: any) =>
|
|
154
|
+
router.navigate({ ...opts, _fromLocation: router.state.location }),
|
|
155
|
+
buildLocation: router.buildLocation,
|
|
156
|
+
cause: match.cause,
|
|
157
|
+
abortController: match.abortController,
|
|
158
|
+
preload: false,
|
|
159
|
+
matches,
|
|
160
|
+
}
|
|
161
|
+
match.__routeContext = route.options.context?.(contextFnContext) ?? {}
|
|
162
|
+
|
|
163
|
+
match.context = {
|
|
164
|
+
...parentContext,
|
|
165
|
+
...match.__routeContext,
|
|
166
|
+
...match.__beforeLoadContext,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const assetContext = {
|
|
170
|
+
matches: router.state.matches,
|
|
171
|
+
match,
|
|
172
|
+
params: match.params,
|
|
173
|
+
loaderData: match.loaderData,
|
|
174
|
+
}
|
|
175
|
+
const headFnContent = await route.options.head?.(assetContext)
|
|
176
|
+
|
|
177
|
+
const scripts = await route.options.scripts?.(assetContext)
|
|
178
|
+
|
|
179
|
+
match.meta = headFnContent?.meta
|
|
180
|
+
match.links = headFnContent?.links
|
|
181
|
+
match.headScripts = headFnContent?.scripts
|
|
182
|
+
match.styles = headFnContent?.styles
|
|
183
|
+
match.scripts = scripts
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
|
|
188
|
+
const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
|
|
189
|
+
// all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
|
|
190
|
+
if (!hasSsrFalseMatches && !isSpaMode) {
|
|
191
|
+
matches.forEach((match) => {
|
|
192
|
+
// remove the _dehydrate flag since we won't run router.load() which would remove it
|
|
193
|
+
match._dehydrated = undefined
|
|
194
|
+
})
|
|
195
|
+
return routeChunkPromise
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
|
|
199
|
+
const loadPromise = Promise.resolve()
|
|
200
|
+
.then(() => router.load())
|
|
201
|
+
.catch((err) => {
|
|
202
|
+
console.error('Error during router hydration:', err)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// in SPA mode we need to keep the first match below the root route pending until router.load() is finished
|
|
206
|
+
// this will prevent that other pending components are rendered but hydration is not blocked
|
|
207
|
+
if (isSpaMode) {
|
|
208
|
+
const match = matches[1]
|
|
209
|
+
invariant(
|
|
210
|
+
match,
|
|
211
|
+
'Expected to find a match below the root match in SPA mode.',
|
|
212
|
+
)
|
|
213
|
+
setMatchForcePending(match)
|
|
214
|
+
|
|
215
|
+
match._displayPending = true
|
|
216
|
+
match.displayPendingPromise = loadPromise
|
|
217
|
+
|
|
218
|
+
loadPromise.then(() => {
|
|
219
|
+
batch(() => {
|
|
220
|
+
// ensure router is not in status 'pending' anymore
|
|
221
|
+
// this usually happens in Transitioner but if loading synchronously resolves,
|
|
222
|
+
// Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
|
|
223
|
+
if (router.__store.state.status === 'pending') {
|
|
224
|
+
router.__store.setState((s) => ({
|
|
225
|
+
...s,
|
|
226
|
+
status: 'idle',
|
|
227
|
+
resolvedLocation: s.location,
|
|
228
|
+
}))
|
|
229
|
+
}
|
|
230
|
+
// hide the pending component once the load is finished
|
|
231
|
+
router.updateMatch(match.id, (prev) => {
|
|
232
|
+
return {
|
|
233
|
+
...prev,
|
|
234
|
+
_displayPending: undefined,
|
|
235
|
+
displayPendingPromise: undefined,
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
return routeChunkPromise
|
|
242
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
|
|
2
|
+
import { ReadableStreamPlugin } from 'seroval-plugins/web'
|
|
3
|
+
import invariant from 'tiny-invariant'
|
|
4
|
+
import { createControlledPromise } from '../utils'
|
|
5
|
+
import minifiedTsrBootStrapScript from './tsrScript?script-string'
|
|
6
|
+
import { ShallowErrorPlugin } from './seroval-plugins'
|
|
7
|
+
import type { AnyRouter } from '../router'
|
|
8
|
+
import type { DehydratedMatch } from './ssr-client'
|
|
9
|
+
import type { DehydratedRouter } from './client'
|
|
10
|
+
import type { AnyRouteMatch } from '../Matches'
|
|
11
|
+
import type { Manifest } from '../manifest'
|
|
12
|
+
|
|
13
|
+
declare module '../router' {
|
|
14
|
+
interface ServerSsr {
|
|
15
|
+
setRenderFinished: () => void
|
|
16
|
+
}
|
|
17
|
+
interface RouterEvents {
|
|
18
|
+
onInjectedHtml: {
|
|
19
|
+
type: 'onInjectedHtml'
|
|
20
|
+
promise: Promise<string>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GLOBAL_TSR = '$_TSR'
|
|
26
|
+
const SCOPE_ID = 'tsr'
|
|
27
|
+
|
|
28
|
+
export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
|
|
29
|
+
const dehydratedMatch: DehydratedMatch = {
|
|
30
|
+
i: match.id,
|
|
31
|
+
u: match.updatedAt,
|
|
32
|
+
s: match.status,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const properties = [
|
|
36
|
+
['__beforeLoadContext', 'b'],
|
|
37
|
+
['loaderData', 'l'],
|
|
38
|
+
['error', 'e'],
|
|
39
|
+
['ssr', 'ssr'],
|
|
40
|
+
] as const
|
|
41
|
+
|
|
42
|
+
for (const [key, shorthand] of properties) {
|
|
43
|
+
if (match[key] !== undefined) {
|
|
44
|
+
dehydratedMatch[shorthand] = match[key]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return dehydratedMatch
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function attachRouterServerSsrUtils(
|
|
51
|
+
router: AnyRouter,
|
|
52
|
+
manifest: Manifest | undefined,
|
|
53
|
+
) {
|
|
54
|
+
router.ssr = {
|
|
55
|
+
manifest,
|
|
56
|
+
}
|
|
57
|
+
const serializationRefs = new Map<unknown, number>()
|
|
58
|
+
|
|
59
|
+
let initialScriptSent = false
|
|
60
|
+
const getInitialScript = () => {
|
|
61
|
+
if (initialScriptSent) {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
initialScriptSent = true
|
|
65
|
+
return `${getCrossReferenceHeader(SCOPE_ID)};${minifiedTsrBootStrapScript};`
|
|
66
|
+
}
|
|
67
|
+
let _dehydrated = false
|
|
68
|
+
const listeners: Array<() => void> = []
|
|
69
|
+
|
|
70
|
+
router.serverSsr = {
|
|
71
|
+
injectedHtml: [],
|
|
72
|
+
injectHtml: (getHtml) => {
|
|
73
|
+
const promise = Promise.resolve().then(getHtml)
|
|
74
|
+
router.serverSsr!.injectedHtml.push(promise)
|
|
75
|
+
router.emit({
|
|
76
|
+
type: 'onInjectedHtml',
|
|
77
|
+
promise,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return promise.then(() => {})
|
|
81
|
+
},
|
|
82
|
+
injectScript: (getScript) => {
|
|
83
|
+
return router.serverSsr!.injectHtml(async () => {
|
|
84
|
+
const script = await getScript()
|
|
85
|
+
return `<script class='$tsr'>${getInitialScript()}${script};if (typeof $_TSR !== 'undefined') $_TSR.c()</script>`
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
dehydrate: async () => {
|
|
89
|
+
invariant(!_dehydrated, 'router is already dehydrated!')
|
|
90
|
+
let matchesToDehydrate = router.state.matches
|
|
91
|
+
if (router.isShell()) {
|
|
92
|
+
// In SPA mode we only want to dehydrate the root match
|
|
93
|
+
matchesToDehydrate = matchesToDehydrate.slice(0, 1)
|
|
94
|
+
}
|
|
95
|
+
const matches = matchesToDehydrate.map(dehydrateMatch)
|
|
96
|
+
|
|
97
|
+
const dehydratedRouter: DehydratedRouter = {
|
|
98
|
+
manifest: router.ssr!.manifest,
|
|
99
|
+
matches,
|
|
100
|
+
}
|
|
101
|
+
const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
|
|
102
|
+
if (lastMatchId) {
|
|
103
|
+
dehydratedRouter.lastMatchId = lastMatchId
|
|
104
|
+
}
|
|
105
|
+
dehydratedRouter.dehydratedData = await router.options.dehydrate?.()
|
|
106
|
+
_dehydrated = true
|
|
107
|
+
|
|
108
|
+
const p = createControlledPromise<string>()
|
|
109
|
+
crossSerializeStream(dehydratedRouter, {
|
|
110
|
+
refs: serializationRefs,
|
|
111
|
+
// TODO make plugins configurable
|
|
112
|
+
plugins: [ReadableStreamPlugin, ShallowErrorPlugin],
|
|
113
|
+
onSerialize: (data, initial) => {
|
|
114
|
+
const serialized = initial ? `${GLOBAL_TSR}["router"]=` + data : data
|
|
115
|
+
router.serverSsr!.injectScript(() => serialized)
|
|
116
|
+
},
|
|
117
|
+
scopeId: SCOPE_ID,
|
|
118
|
+
onDone: () => p.resolve(''),
|
|
119
|
+
onError: (err) => p.reject(err),
|
|
120
|
+
})
|
|
121
|
+
// make sure the stream is kept open until the promise is resolved
|
|
122
|
+
router.serverSsr!.injectHtml(() => p)
|
|
123
|
+
},
|
|
124
|
+
isDehydrated() {
|
|
125
|
+
return _dehydrated
|
|
126
|
+
},
|
|
127
|
+
onRenderFinished: (listener) => listeners.push(listener),
|
|
128
|
+
setRenderFinished: () => {
|
|
129
|
+
listeners.forEach((l) => l())
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|