@tanstack/router-core 1.171.15 → 1.171.16-pre.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.map +1 -1
- package/dist/cjs/Matches.d.cts +3 -12
- package/dist/cjs/await-signal.cjs +21 -0
- package/dist/cjs/await-signal.cjs.map +1 -0
- package/dist/cjs/await-signal.d.cts +1 -0
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.d.cts +3 -2
- package/dist/cjs/isServer/client.cjs +2 -0
- package/dist/cjs/isServer/client.cjs.map +1 -1
- package/dist/cjs/isServer/client.d.cts +1 -0
- package/dist/cjs/isServer/development.cjs +2 -0
- package/dist/cjs/isServer/development.cjs.map +1 -1
- package/dist/cjs/isServer/development.d.cts +1 -0
- package/dist/cjs/isServer/server.cjs +2 -0
- package/dist/cjs/isServer/server.cjs.map +1 -1
- package/dist/cjs/isServer/server.d.cts +1 -0
- package/dist/cjs/load-client.cjs +1470 -0
- package/dist/cjs/load-client.cjs.map +1 -0
- package/dist/cjs/load-client.d.cts +147 -0
- package/dist/cjs/load-server.cjs +565 -0
- package/dist/cjs/load-server.cjs.map +1 -0
- package/dist/cjs/load-server.d.cts +16 -0
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/redirect.d.cts +0 -1
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/router.cjs +187 -442
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +61 -51
- package/dist/cjs/ssr/client.cjs +2 -2
- package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
- package/dist/cjs/ssr/handlerCallback.cjs +49 -4
- package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
- package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
- package/dist/cjs/ssr/server.cjs +4 -0
- package/dist/cjs/ssr/server.d.cts +2 -2
- package/dist/cjs/ssr/ssr-client.d.cts +1 -10
- package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
- package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +6 -6
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
- package/dist/cjs/ssr/types.d.cts +0 -1
- package/dist/cjs/stores.cjs +30 -104
- package/dist/cjs/stores.cjs.map +1 -1
- package/dist/cjs/stores.d.cts +7 -32
- package/dist/cjs/utils.cjs +0 -4
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/Matches.d.ts +3 -12
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/await-signal.d.ts +1 -0
- package/dist/esm/await-signal.js +21 -0
- package/dist/esm/await-signal.js.map +1 -0
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +3 -2
- package/dist/esm/isServer/client.d.ts +1 -0
- package/dist/esm/isServer/client.js +2 -1
- package/dist/esm/isServer/client.js.map +1 -1
- package/dist/esm/isServer/development.d.ts +1 -0
- package/dist/esm/isServer/development.js +2 -1
- package/dist/esm/isServer/development.js.map +1 -1
- package/dist/esm/isServer/server.d.ts +1 -0
- package/dist/esm/isServer/server.js +2 -1
- package/dist/esm/isServer/server.js.map +1 -1
- package/dist/esm/load-client.d.ts +147 -0
- package/dist/esm/load-client.js +1462 -0
- package/dist/esm/load-client.js.map +1 -0
- package/dist/esm/load-server.d.ts +16 -0
- package/dist/esm/load-server.js +565 -0
- package/dist/esm/load-server.js.map +1 -0
- package/dist/esm/redirect.d.ts +0 -1
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +61 -51
- package/dist/esm/router.js +188 -444
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/client.js +1 -1
- package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
- package/dist/esm/ssr/createRequestHandler.js +61 -11
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/handlerCallback.d.ts +3 -0
- package/dist/esm/ssr/handlerCallback.js +47 -5
- package/dist/esm/ssr/handlerCallback.js.map +1 -1
- package/dist/esm/ssr/server.d.ts +2 -2
- package/dist/esm/ssr/server.js +3 -3
- package/dist/esm/ssr/ssr-client.d.ts +1 -10
- package/dist/esm/ssr/ssr-match-id.js +2 -2
- package/dist/esm/ssr/ssr-match-id.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +6 -6
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
- package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/ssr/types.d.ts +0 -1
- package/dist/esm/stores.d.ts +7 -32
- package/dist/esm/stores.js +30 -104
- package/dist/esm/stores.js.map +1 -1
- package/dist/esm/utils.js +1 -4
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/Matches.ts +6 -21
- package/src/await-signal.ts +27 -0
- package/src/index.ts +1 -3
- package/src/isServer/client.ts +1 -0
- package/src/isServer/development.ts +1 -0
- package/src/isServer/server.ts +1 -0
- package/src/load-client.ts +2701 -0
- package/src/load-server.ts +849 -0
- package/src/redirect.ts +0 -1
- package/src/route.ts +2 -11
- package/src/router.ts +379 -769
- package/src/ssr/createRequestHandler.ts +109 -16
- package/src/ssr/handlerCallback.ts +76 -10
- package/src/ssr/server.ts +4 -1
- package/src/ssr/ssr-client.ts +1 -310
- package/src/ssr/ssr-match-id.ts +11 -2
- package/src/ssr/ssr-server.ts +9 -8
- package/src/ssr/transformStreamWithRouter.ts +37 -1
- package/src/ssr/types.ts +0 -1
- package/src/stores.ts +46 -208
- package/dist/cjs/load-matches.cjs +0 -658
- package/dist/cjs/load-matches.cjs.map +0 -1
- package/dist/cjs/load-matches.d.cts +0 -18
- package/dist/cjs/ssr/ssr-client.cjs +0 -183
- package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
- package/dist/esm/load-matches.d.ts +0 -18
- package/dist/esm/load-matches.js +0 -656
- package/dist/esm/load-matches.js.map +0 -1
- package/dist/esm/ssr/ssr-client.js +0 -183
- package/dist/esm/ssr/ssr-client.js.map +0 -1
- package/src/load-matches.ts +0 -1278
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createMemoryHistory } from '@tanstack/history'
|
|
2
|
+
import { _getRenderedMatches } from '../load-client'
|
|
2
3
|
import { mergeHeaders } from './headers'
|
|
3
4
|
import {
|
|
4
5
|
attachRouterServerSsrUtils,
|
|
5
6
|
getNormalizedURL,
|
|
6
7
|
getOrigin,
|
|
7
8
|
} from './ssr-server'
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
bindSsrResponseToRequest,
|
|
11
|
+
disposeSsrResponseDetached,
|
|
12
|
+
} from './handlerCallback'
|
|
9
13
|
import type { HandlerCallback } from './handlerCallback'
|
|
10
14
|
import type { AnyHeaders } from './headers'
|
|
11
15
|
import type { AnyRouter } from '../router'
|
|
@@ -15,6 +19,79 @@ export type RequestHandler<TRouter extends AnyRouter> = (
|
|
|
15
19
|
cb: HandlerCallback<TRouter>,
|
|
16
20
|
) => Promise<Response>
|
|
17
21
|
|
|
22
|
+
type RequestWaiter = ((reason: unknown) => void) | undefined
|
|
23
|
+
|
|
24
|
+
const requestWaiters = new WeakMap<AbortSignal, Array<RequestWaiter>>()
|
|
25
|
+
|
|
26
|
+
function removeRequestWaiter(
|
|
27
|
+
waiters: Array<RequestWaiter>,
|
|
28
|
+
index: number,
|
|
29
|
+
reject: (reason: unknown) => void,
|
|
30
|
+
) {
|
|
31
|
+
if (waiters[index] !== reject) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if (index !== waiters.length - 1) {
|
|
35
|
+
waiters[index] = undefined
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
waiters.pop()
|
|
40
|
+
while (waiters.length && waiters[waiters.length - 1] === undefined) {
|
|
41
|
+
waiters.pop()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function waitForRequest<T>(
|
|
46
|
+
value: T | PromiseLike<T>,
|
|
47
|
+
signal: AbortSignal,
|
|
48
|
+
onLate?: (value: T) => void,
|
|
49
|
+
): Promise<T> {
|
|
50
|
+
const promise = Promise.resolve(value)
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
void promise.then(onLate, () => {})
|
|
53
|
+
return Promise.reject(signal.reason)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return new Promise<T>((resolve, reject) => {
|
|
57
|
+
let waiters = requestWaiters.get(signal)
|
|
58
|
+
let index: number
|
|
59
|
+
if (waiters) {
|
|
60
|
+
index = waiters.push(reject) - 1
|
|
61
|
+
} else {
|
|
62
|
+
const newWaiters: Array<RequestWaiter> = [reject]
|
|
63
|
+
waiters = newWaiters
|
|
64
|
+
index = 0
|
|
65
|
+
requestWaiters.set(signal, newWaiters)
|
|
66
|
+
signal.addEventListener(
|
|
67
|
+
'abort',
|
|
68
|
+
() => {
|
|
69
|
+
requestWaiters.delete(signal)
|
|
70
|
+
for (const rejectWaiter of newWaiters) {
|
|
71
|
+
rejectWaiter?.(signal.reason)
|
|
72
|
+
}
|
|
73
|
+
newWaiters.length = 0
|
|
74
|
+
},
|
|
75
|
+
{ once: true },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
void promise.then(
|
|
79
|
+
(result) => {
|
|
80
|
+
removeRequestWaiter(waiters, index, reject)
|
|
81
|
+
if (signal.aborted) {
|
|
82
|
+
onLate?.(result)
|
|
83
|
+
} else {
|
|
84
|
+
resolve(result)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
(error) => {
|
|
88
|
+
removeRequestWaiter(waiters, index, reject)
|
|
89
|
+
reject(error)
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
18
95
|
export function createRequestHandler<TRouter extends AnyRouter>({
|
|
19
96
|
createRouter,
|
|
20
97
|
request,
|
|
@@ -25,13 +102,14 @@ export function createRequestHandler<TRouter extends AnyRouter>({
|
|
|
25
102
|
getRouterManifest?: () => ServerManifest | Promise<ServerManifest>
|
|
26
103
|
}): RequestHandler<TRouter> {
|
|
27
104
|
return async (cb) => {
|
|
105
|
+
request.signal.throwIfAborted()
|
|
28
106
|
const router = createRouter()
|
|
29
107
|
let responseOwnsCleanup = false
|
|
30
108
|
|
|
31
109
|
try {
|
|
32
110
|
attachRouterServerSsrUtils({
|
|
33
111
|
router,
|
|
34
|
-
manifest: await getRouterManifest?.(),
|
|
112
|
+
manifest: await waitForRequest(getRouterManifest?.(), request.signal),
|
|
35
113
|
})
|
|
36
114
|
|
|
37
115
|
// normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
|
|
@@ -50,20 +128,41 @@ export function createRequestHandler<TRouter extends AnyRouter>({
|
|
|
50
128
|
origin: router.options.origin ?? origin,
|
|
51
129
|
})
|
|
52
130
|
|
|
53
|
-
await router.load(
|
|
131
|
+
await router.load({
|
|
132
|
+
_signal: request.signal,
|
|
133
|
+
})
|
|
134
|
+
request.signal.throwIfAborted()
|
|
135
|
+
|
|
136
|
+
const result = router._serverResult
|
|
137
|
+
if (result?.type === 'redirect') {
|
|
138
|
+
return result.redirect
|
|
139
|
+
}
|
|
54
140
|
|
|
55
|
-
await router.serverSsr?.dehydrate()
|
|
141
|
+
await waitForRequest(router.serverSsr?.dehydrate(), request.signal)
|
|
142
|
+
request.signal.throwIfAborted()
|
|
56
143
|
|
|
57
144
|
const responseHeaders = getRequestHeaders({
|
|
58
145
|
router,
|
|
59
146
|
})
|
|
60
147
|
|
|
61
|
-
|
|
62
|
-
|
|
148
|
+
request.signal.throwIfAborted()
|
|
149
|
+
const response = await waitForRequest(
|
|
150
|
+
cb({
|
|
151
|
+
request,
|
|
152
|
+
router,
|
|
153
|
+
responseHeaders,
|
|
154
|
+
}),
|
|
155
|
+
request.signal,
|
|
156
|
+
(late) => {
|
|
157
|
+
disposeSsrResponseDetached(late, request.signal.reason)
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
const ssrResponse = bindSsrResponseToRequest(
|
|
63
161
|
router,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
162
|
+
response,
|
|
163
|
+
request.signal,
|
|
164
|
+
)
|
|
165
|
+
request.signal.throwIfAborted()
|
|
67
166
|
responseOwnsCleanup = ssrResponse.serverSsrCleanup === 'stream'
|
|
68
167
|
return ssrResponse.response
|
|
69
168
|
} finally {
|
|
@@ -79,16 +178,10 @@ export function createRequestHandler<TRouter extends AnyRouter>({
|
|
|
79
178
|
|
|
80
179
|
function getRequestHeaders(opts: { router: AnyRouter }): Headers {
|
|
81
180
|
const matchHeaders: Array<AnyHeaders> = []
|
|
82
|
-
for (const match of opts.router.stores.matches.get()) {
|
|
181
|
+
for (const match of _getRenderedMatches(opts.router.stores.matches.get())) {
|
|
83
182
|
matchHeaders.push(match.headers)
|
|
84
183
|
}
|
|
85
184
|
|
|
86
|
-
// Handle Redirects
|
|
87
|
-
const redirect = opts.router.stores.redirect.get()
|
|
88
|
-
if (redirect) {
|
|
89
|
-
matchHeaders.push(redirect.headers)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
185
|
return mergeHeaders(
|
|
93
186
|
{
|
|
94
187
|
'Content-Type': 'text/html; charset=UTF-8',
|
|
@@ -30,6 +30,40 @@ export function normalizeSsrResponse(
|
|
|
30
30
|
: { response: result, serverSsrCleanup: 'none' }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function disposeSsrResponse(
|
|
34
|
+
response: SsrResponse,
|
|
35
|
+
reason?: unknown,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
if (response.serverSsrCleanup !== 'stream') {
|
|
38
|
+
return Promise.resolve()
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return Promise.resolve(response.dispose(reason))
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return Promise.reject(error)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function disposeSsrResponseDetached(
|
|
48
|
+
result: HandlerCallbackResult,
|
|
49
|
+
reason?: unknown,
|
|
50
|
+
onError: (error: unknown) => void = console.error,
|
|
51
|
+
): void {
|
|
52
|
+
const ssrResponse = normalizeSsrResponse(result)
|
|
53
|
+
if (ssrResponse.serverSsrCleanup === 'stream') {
|
|
54
|
+
void disposeSsrResponse(ssrResponse, reason).catch(onError)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (ssrResponse.response.body) {
|
|
59
|
+
try {
|
|
60
|
+
void ssrResponse.response.body.cancel(reason).catch(onError)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
onError(error)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
export function createSsrStreamResponse<TRouter extends AnyRouter>(
|
|
34
68
|
router: TRouter,
|
|
35
69
|
response: Response,
|
|
@@ -43,29 +77,63 @@ export function createSsrStreamResponse<TRouter extends AnyRouter>(
|
|
|
43
77
|
response,
|
|
44
78
|
serverSsrCleanup: 'stream',
|
|
45
79
|
async dispose(reason?: unknown) {
|
|
46
|
-
if (disposed)
|
|
80
|
+
if (disposed) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
47
83
|
disposed = true
|
|
48
84
|
|
|
85
|
+
// Sever router ownership before asking user/renderer stream machinery to
|
|
86
|
+
// cancel. A custom stream is allowed to ignore cancellation forever.
|
|
87
|
+
router.serverSsr?.cleanup()
|
|
88
|
+
|
|
49
89
|
try {
|
|
50
90
|
await response.body!.cancel(reason)
|
|
51
91
|
} catch {
|
|
52
|
-
//
|
|
92
|
+
// Cleanup above already released router SSR state.
|
|
53
93
|
}
|
|
54
|
-
|
|
55
|
-
router.serverSsr?.cleanup()
|
|
56
94
|
},
|
|
57
95
|
}
|
|
58
96
|
}
|
|
59
97
|
|
|
98
|
+
export function bindSsrResponseToRequest(
|
|
99
|
+
router: AnyRouter | undefined,
|
|
100
|
+
result: HandlerCallbackResult,
|
|
101
|
+
signal: AbortSignal,
|
|
102
|
+
): SsrResponse {
|
|
103
|
+
const ssrResponse = normalizeSsrResponse(result)
|
|
104
|
+
if (ssrResponse.serverSsrCleanup !== 'stream') {
|
|
105
|
+
if (signal.aborted) {
|
|
106
|
+
disposeSsrResponseDetached(result, signal.reason)
|
|
107
|
+
}
|
|
108
|
+
return ssrResponse
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const failed = (error: unknown) => {
|
|
112
|
+
router?.serverSsr?.cleanup()
|
|
113
|
+
console.error(error)
|
|
114
|
+
}
|
|
115
|
+
const abort = () => {
|
|
116
|
+
disposeSsrResponseDetached(ssrResponse, signal.reason, failed)
|
|
117
|
+
}
|
|
118
|
+
if (signal.aborted) {
|
|
119
|
+
abort()
|
|
120
|
+
return ssrResponse
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
124
|
+
router?.serverSsr?.onCleanup(() => {
|
|
125
|
+
signal.removeEventListener('abort', abort)
|
|
126
|
+
})
|
|
127
|
+
return ssrResponse
|
|
128
|
+
}
|
|
129
|
+
|
|
60
130
|
export async function replaceSsrResponse(
|
|
61
131
|
result: HandlerCallbackResult,
|
|
62
132
|
response: Response,
|
|
63
133
|
reason?: unknown,
|
|
64
134
|
): Promise<SsrResponse> {
|
|
65
135
|
const ssrResponse = normalizeSsrResponse(result)
|
|
66
|
-
|
|
67
|
-
await ssrResponse.dispose(reason)
|
|
68
|
-
}
|
|
136
|
+
await disposeSsrResponse(ssrResponse, reason)
|
|
69
137
|
return { response, serverSsrCleanup: 'none' }
|
|
70
138
|
}
|
|
71
139
|
|
|
@@ -74,9 +142,7 @@ export async function stripSsrResponseBody(
|
|
|
74
142
|
reason?: unknown,
|
|
75
143
|
): Promise<SsrResponse> {
|
|
76
144
|
const ssrResponse = normalizeSsrResponse(result)
|
|
77
|
-
|
|
78
|
-
await ssrResponse.dispose(reason)
|
|
79
|
-
}
|
|
145
|
+
await disposeSsrResponse(ssrResponse, reason)
|
|
80
146
|
return {
|
|
81
147
|
response: new Response(null, ssrResponse.response),
|
|
82
148
|
serverSsrCleanup: 'none',
|
package/src/ssr/server.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export { createRequestHandler } from './createRequestHandler'
|
|
1
|
+
export { createRequestHandler, waitForRequest } from './createRequestHandler'
|
|
2
2
|
export type { RequestHandler } from './createRequestHandler'
|
|
3
3
|
export {
|
|
4
|
+
bindSsrResponseToRequest,
|
|
4
5
|
createSsrStreamResponse,
|
|
5
6
|
defineHandlerCallback,
|
|
7
|
+
disposeSsrResponse,
|
|
8
|
+
disposeSsrResponseDetached,
|
|
6
9
|
isSsrResponse,
|
|
7
10
|
normalizeSsrResponse,
|
|
8
11
|
replaceSsrResponse,
|
package/src/ssr/ssr-client.ts
CHANGED
|
@@ -1,310 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isNotFound } from '../not-found'
|
|
3
|
-
import { createControlledPromise } from '../utils'
|
|
4
|
-
import { hydrateSsrMatchId } from './ssr-match-id'
|
|
5
|
-
import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'
|
|
6
|
-
import type { DehydratedMatch, TsrSsrGlobal } from './types'
|
|
7
|
-
import type { AnyRouteMatch } from '../Matches'
|
|
8
|
-
import type { AnyRouter } from '../router'
|
|
9
|
-
import type { RouteContextOptions } from '../route'
|
|
10
|
-
import type { AnySerializationAdapter } from './serializer/transformer'
|
|
11
|
-
|
|
12
|
-
declare global {
|
|
13
|
-
interface Window {
|
|
14
|
-
[GLOBAL_TSR]?: TsrSsrGlobal
|
|
15
|
-
[GLOBAL_SEROVAL]?: any
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function hydrateMatch(
|
|
20
|
-
match: AnyRouteMatch,
|
|
21
|
-
deyhydratedMatch: DehydratedMatch,
|
|
22
|
-
): void {
|
|
23
|
-
match.id = deyhydratedMatch.i
|
|
24
|
-
match.__beforeLoadContext = deyhydratedMatch.b
|
|
25
|
-
match.loaderData = deyhydratedMatch.l
|
|
26
|
-
match.status = deyhydratedMatch.s
|
|
27
|
-
match.ssr = deyhydratedMatch.ssr
|
|
28
|
-
match.updatedAt = deyhydratedMatch.u
|
|
29
|
-
match.error = deyhydratedMatch.e
|
|
30
|
-
// Only hydrate global-not-found when a defined value is present in the
|
|
31
|
-
// dehydrated payload. If omitted, preserve the value computed from the
|
|
32
|
-
// current client location (important for SPA fallback HTML served at unknown
|
|
33
|
-
// URLs, where dehydrated matches may come from `/` but client matching marks
|
|
34
|
-
// root as globalNotFound).
|
|
35
|
-
if (deyhydratedMatch.g !== undefined) {
|
|
36
|
-
match.globalNotFound = deyhydratedMatch.g
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function hydrate(router: AnyRouter): Promise<any> {
|
|
41
|
-
if (!window.$_TSR) {
|
|
42
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
43
|
-
throw new Error(
|
|
44
|
-
'Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
invariant()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const serializationAdapters = router.options.serializationAdapters as
|
|
52
|
-
| Array<AnySerializationAdapter>
|
|
53
|
-
| undefined
|
|
54
|
-
|
|
55
|
-
if (serializationAdapters?.length) {
|
|
56
|
-
const fromSerializableMap = new Map()
|
|
57
|
-
serializationAdapters.forEach((adapter) => {
|
|
58
|
-
fromSerializableMap.set(adapter.key, adapter.fromSerializable)
|
|
59
|
-
})
|
|
60
|
-
window.$_TSR.t = fromSerializableMap
|
|
61
|
-
window.$_TSR.buffer.forEach((script) => script())
|
|
62
|
-
}
|
|
63
|
-
window.$_TSR.initialized = true
|
|
64
|
-
|
|
65
|
-
if (!window.$_TSR.router) {
|
|
66
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
67
|
-
throw new Error(
|
|
68
|
-
'Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
invariant()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const dehydratedRouter = window.$_TSR.router
|
|
76
|
-
dehydratedRouter.matches.forEach((dehydratedMatch) => {
|
|
77
|
-
dehydratedMatch.i = hydrateSsrMatchId(dehydratedMatch.i)
|
|
78
|
-
})
|
|
79
|
-
if (dehydratedRouter.lastMatchId) {
|
|
80
|
-
dehydratedRouter.lastMatchId = hydrateSsrMatchId(
|
|
81
|
-
dehydratedRouter.lastMatchId,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
const { manifest, dehydratedData, lastMatchId } = dehydratedRouter
|
|
85
|
-
|
|
86
|
-
router.ssr = {
|
|
87
|
-
manifest,
|
|
88
|
-
}
|
|
89
|
-
const meta = document.querySelector('meta[property="csp-nonce"]') as
|
|
90
|
-
| HTMLMetaElement
|
|
91
|
-
| undefined
|
|
92
|
-
const nonce = meta?.content
|
|
93
|
-
router.options.ssr = {
|
|
94
|
-
nonce,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Allow the user to handle custom hydration data before matching routes.
|
|
98
|
-
// This lets hydration install router config that affects matching, e.g. rewrites.
|
|
99
|
-
await router.options.hydrate?.(dehydratedData)
|
|
100
|
-
|
|
101
|
-
// Hydrate the router state
|
|
102
|
-
const matches = router.matchRoutes(router.stores.location.get())
|
|
103
|
-
|
|
104
|
-
// kick off loading the route chunks
|
|
105
|
-
const routeChunkPromise = Promise.all(
|
|
106
|
-
matches.map((match) =>
|
|
107
|
-
router.loadRouteChunk(router.looseRoutesById[match.routeId]!),
|
|
108
|
-
),
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
function setMatchForcePending(match: AnyRouteMatch) {
|
|
112
|
-
// usually the minPendingPromise is created in the Match component if a pending match is rendered
|
|
113
|
-
// however, this might be too late if the match synchronously resolves
|
|
114
|
-
const route = router.looseRoutesById[match.routeId]!
|
|
115
|
-
const pendingMinMs =
|
|
116
|
-
route.options.pendingMinMs ?? router.options.defaultPendingMinMs
|
|
117
|
-
if (pendingMinMs) {
|
|
118
|
-
const minPendingPromise = createControlledPromise<void>()
|
|
119
|
-
match._nonReactive.minPendingPromise = minPendingPromise
|
|
120
|
-
match._forcePending = true
|
|
121
|
-
|
|
122
|
-
setTimeout(() => {
|
|
123
|
-
minPendingPromise.resolve()
|
|
124
|
-
// We've handled the minPendingPromise, so we can delete it
|
|
125
|
-
router.updateMatch(match.id, (prev) => {
|
|
126
|
-
prev._nonReactive.minPendingPromise = undefined
|
|
127
|
-
return {
|
|
128
|
-
...prev,
|
|
129
|
-
_forcePending: undefined,
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
}, pendingMinMs)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function setRouteSsr(match: AnyRouteMatch) {
|
|
137
|
-
const route = router.looseRoutesById[match.routeId]
|
|
138
|
-
if (route) {
|
|
139
|
-
route.options.ssr = match.ssr
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Right after hydration and before the first render, we need to rehydrate each match
|
|
143
|
-
// First step is to reyhdrate loaderData and __beforeLoadContext
|
|
144
|
-
let firstNonSsrMatchIndex: number | undefined = undefined
|
|
145
|
-
matches.forEach((match) => {
|
|
146
|
-
const dehydratedMatch = dehydratedRouter.matches.find(
|
|
147
|
-
(d) => d.i === match.id,
|
|
148
|
-
)
|
|
149
|
-
if (!dehydratedMatch) {
|
|
150
|
-
match._nonReactive.dehydrated = false
|
|
151
|
-
match.ssr = false
|
|
152
|
-
setRouteSsr(match)
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
hydrateMatch(match, dehydratedMatch)
|
|
157
|
-
setRouteSsr(match)
|
|
158
|
-
|
|
159
|
-
match._nonReactive.dehydrated = match.ssr !== false
|
|
160
|
-
|
|
161
|
-
if (match.ssr === 'data-only' || match.ssr === false) {
|
|
162
|
-
if (firstNonSsrMatchIndex === undefined) {
|
|
163
|
-
firstNonSsrMatchIndex = match.index
|
|
164
|
-
setMatchForcePending(match)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
router.stores.setMatches(matches)
|
|
170
|
-
|
|
171
|
-
// now that all necessary data is hydrated:
|
|
172
|
-
// 1) fully reconstruct the route context
|
|
173
|
-
// 2) execute `head()` and `scripts()` for each match
|
|
174
|
-
const activeMatches = router.stores.matches.get()
|
|
175
|
-
const location = router.stores.location.get()
|
|
176
|
-
await Promise.all(
|
|
177
|
-
activeMatches.map(async (match) => {
|
|
178
|
-
try {
|
|
179
|
-
const route = router.looseRoutesById[match.routeId]!
|
|
180
|
-
|
|
181
|
-
const parentMatch = activeMatches[match.index - 1]
|
|
182
|
-
const parentContext = parentMatch?.context ?? router.options.context
|
|
183
|
-
|
|
184
|
-
// `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
|
|
185
|
-
// so run it again and merge route context
|
|
186
|
-
if (route.options.context) {
|
|
187
|
-
const contextFnContext: RouteContextOptions<any, any, any, any, any> =
|
|
188
|
-
{
|
|
189
|
-
deps: match.loaderDeps,
|
|
190
|
-
params: match.params,
|
|
191
|
-
context: parentContext ?? {},
|
|
192
|
-
location,
|
|
193
|
-
navigate: (opts: any) =>
|
|
194
|
-
router.navigate({
|
|
195
|
-
...opts,
|
|
196
|
-
_fromLocation: location,
|
|
197
|
-
}),
|
|
198
|
-
buildLocation: router.buildLocation,
|
|
199
|
-
cause: match.cause,
|
|
200
|
-
abortController: match.abortController,
|
|
201
|
-
preload: false,
|
|
202
|
-
matches,
|
|
203
|
-
routeId: route.id,
|
|
204
|
-
}
|
|
205
|
-
match.__routeContext =
|
|
206
|
-
route.options.context(contextFnContext) ?? undefined
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
match.context = {
|
|
210
|
-
...parentContext,
|
|
211
|
-
...match.__routeContext,
|
|
212
|
-
...match.__beforeLoadContext,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const assetContext = {
|
|
216
|
-
ssr: router.options.ssr,
|
|
217
|
-
matches: activeMatches,
|
|
218
|
-
match,
|
|
219
|
-
params: match.params,
|
|
220
|
-
loaderData: match.loaderData,
|
|
221
|
-
}
|
|
222
|
-
const headFnContent = await route.options.head?.(assetContext)
|
|
223
|
-
|
|
224
|
-
const scripts = await route.options.scripts?.(assetContext)
|
|
225
|
-
|
|
226
|
-
match.meta = headFnContent?.meta
|
|
227
|
-
match.links = headFnContent?.links
|
|
228
|
-
match.headScripts = headFnContent?.scripts
|
|
229
|
-
match.styles = headFnContent?.styles
|
|
230
|
-
match.scripts = scripts
|
|
231
|
-
} catch (err) {
|
|
232
|
-
if (isNotFound(err)) {
|
|
233
|
-
match.error = { isNotFound: true }
|
|
234
|
-
console.error(
|
|
235
|
-
`NotFound error during hydration for routeId: ${match.routeId}`,
|
|
236
|
-
err,
|
|
237
|
-
)
|
|
238
|
-
} else {
|
|
239
|
-
match.error = err as any
|
|
240
|
-
console.error(
|
|
241
|
-
`Error during hydration for route ${match.routeId}:`,
|
|
242
|
-
err,
|
|
243
|
-
)
|
|
244
|
-
throw err
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}),
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
|
|
251
|
-
const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
|
|
252
|
-
// all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
|
|
253
|
-
if (!hasSsrFalseMatches && !isSpaMode) {
|
|
254
|
-
matches.forEach((match) => {
|
|
255
|
-
// remove the dehydrated flag since we won't run router.load() which would remove it
|
|
256
|
-
match._nonReactive.dehydrated = undefined
|
|
257
|
-
})
|
|
258
|
-
// Mark the current location as resolved so that later load cycles
|
|
259
|
-
// (e.g. preloads, invalidations) don't mistakenly detect a href change
|
|
260
|
-
// (resolvedLocation defaults to undefined and router.load() is skipped
|
|
261
|
-
// in the normal SSR hydration path).
|
|
262
|
-
router.stores.resolvedLocation.set(router.stores.location.get())
|
|
263
|
-
return routeChunkPromise
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
|
|
267
|
-
const loadPromise = Promise.resolve()
|
|
268
|
-
.then(() => router.load())
|
|
269
|
-
.catch((err) => {
|
|
270
|
-
console.error('Error during router hydration:', err)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// in SPA mode we need to keep the first match below the root route pending until router.load() is finished
|
|
274
|
-
// this will prevent that other pending components are rendered but hydration is not blocked
|
|
275
|
-
if (isSpaMode) {
|
|
276
|
-
const match = matches[1]
|
|
277
|
-
if (!match) {
|
|
278
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
279
|
-
throw new Error(
|
|
280
|
-
'Invariant failed: Expected to find a match below the root match in SPA mode.',
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
invariant()
|
|
285
|
-
}
|
|
286
|
-
setMatchForcePending(match)
|
|
287
|
-
|
|
288
|
-
match._displayPending = true
|
|
289
|
-
match._nonReactive.displayPendingPromise = loadPromise
|
|
290
|
-
|
|
291
|
-
loadPromise.then(() => {
|
|
292
|
-
router.batch(() => {
|
|
293
|
-
// ensure router is not in status 'pending' anymore
|
|
294
|
-
// this usually happens in Transitioner but if loading synchronously resolves,
|
|
295
|
-
// Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
|
|
296
|
-
if (router.stores.status.get() === 'pending') {
|
|
297
|
-
router.stores.status.set('idle')
|
|
298
|
-
router.stores.resolvedLocation.set(router.stores.location.get())
|
|
299
|
-
}
|
|
300
|
-
// hide the pending component once the load is finished
|
|
301
|
-
router.updateMatch(match.id, (prev) => ({
|
|
302
|
-
...prev,
|
|
303
|
-
_displayPending: undefined,
|
|
304
|
-
displayPendingPromise: undefined,
|
|
305
|
-
}))
|
|
306
|
-
})
|
|
307
|
-
})
|
|
308
|
-
}
|
|
309
|
-
return routeChunkPromise
|
|
310
|
-
}
|
|
1
|
+
export { hydrate } from '../load-client'
|
package/src/ssr/ssr-match-id.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export function dehydrateSsrMatchId(id: string): string {
|
|
2
|
-
return id
|
|
2
|
+
return id
|
|
3
|
+
.replaceAll('~', '~~')
|
|
4
|
+
.replaceAll('\0', '~0')
|
|
5
|
+
.replaceAll('\uFFFD', '~r')
|
|
6
|
+
.replaceAll('/', '\0')
|
|
3
7
|
}
|
|
4
8
|
|
|
5
9
|
export function hydrateSsrMatchId(id: string): string {
|
|
6
|
-
return id
|
|
10
|
+
return id
|
|
11
|
+
.replaceAll('\0', '/')
|
|
12
|
+
.replaceAll('\uFFFD', '/')
|
|
13
|
+
.replace(/~([~0r])/g, (_, code) =>
|
|
14
|
+
code === '0' ? '\0' : code === 'r' ? '\uFFFD' : code,
|
|
15
|
+
)
|
|
7
16
|
}
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { decodePath } from '../utils'
|
|
9
9
|
import { createLRUCache } from '../lru-cache'
|
|
10
10
|
import { rootRouteId } from '../root'
|
|
11
|
+
import { _getRenderedMatches } from '../load-client'
|
|
11
12
|
import minifiedTsrBootStrapScript from './tsrScript?script-string'
|
|
12
13
|
import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
|
|
13
14
|
import { dehydrateSsrMatchId } from './ssr-match-id'
|
|
@@ -51,7 +52,7 @@ export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
|
|
|
51
52
|
dehydratedMatch[shorthand] = match[key]
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
if (match.
|
|
55
|
+
if (match._notFound) {
|
|
55
56
|
dehydratedMatch.g = true
|
|
56
57
|
}
|
|
57
58
|
return dehydratedMatch
|
|
@@ -389,7 +390,7 @@ export function attachRouterServerSsrUtils({
|
|
|
389
390
|
if (!manifest) return manifest
|
|
390
391
|
|
|
391
392
|
const requestAssets = getRequestAssets?.()
|
|
392
|
-
const matches = router.stores.matches.get()
|
|
393
|
+
const matches = _getRenderedMatches(router.stores.matches.get())
|
|
393
394
|
const hasAssets = hasRequestAssets(requestAssets)
|
|
394
395
|
|
|
395
396
|
if (!hasAssets && !manifest.inlineCss) {
|
|
@@ -495,8 +496,9 @@ export function attachRouterServerSsrUtils({
|
|
|
495
496
|
|
|
496
497
|
invariant()
|
|
497
498
|
}
|
|
498
|
-
let matchesToDehydrate = router.stores.matches.get()
|
|
499
|
-
|
|
499
|
+
let matchesToDehydrate = _getRenderedMatches(router.stores.matches.get())
|
|
500
|
+
const isShell = router.isShell()
|
|
501
|
+
if (isShell) {
|
|
500
502
|
// In SPA mode we only want to dehydrate the root match
|
|
501
503
|
matchesToDehydrate = matchesToDehydrate.slice(0, 1)
|
|
502
504
|
}
|
|
@@ -540,11 +542,10 @@ export function attachRouterServerSsrUtils({
|
|
|
540
542
|
manifest: manifestToDehydrate,
|
|
541
543
|
matches,
|
|
542
544
|
}
|
|
543
|
-
const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
|
|
544
|
-
if (lastMatchId) {
|
|
545
|
-
dehydratedRouter.lastMatchId = dehydrateSsrMatchId(lastMatchId)
|
|
546
|
-
}
|
|
547
545
|
const dehydratedData = await router.options.dehydrate?.()
|
|
546
|
+
if (cleanupStarted) {
|
|
547
|
+
return
|
|
548
|
+
}
|
|
548
549
|
if (dehydratedData) {
|
|
549
550
|
dehydratedRouter.dehydratedData = dehydratedData
|
|
550
551
|
}
|