@tanstack/start-server-core 1.143.9 → 1.144.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/esm/createStartHandler.js +268 -257
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/frame-protocol.d.ts +32 -0
- package/dist/esm/frame-protocol.js +139 -0
- package/dist/esm/frame-protocol.js.map +1 -0
- package/dist/esm/server-functions-handler.d.ts +2 -1
- package/dist/esm/server-functions-handler.js +147 -93
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/package.json +4 -4
- package/src/createStartHandler.ts +386 -343
- package/src/frame-protocol.ts +216 -0
- package/src/server-functions-handler.ts +182 -103
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createMemoryHistory } from '@tanstack/history'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createNullProtoObject,
|
|
4
|
+
flattenMiddlewares,
|
|
5
|
+
mergeHeaders,
|
|
6
|
+
safeObjectMerge,
|
|
7
|
+
} from '@tanstack/start-client-core'
|
|
3
8
|
import {
|
|
4
9
|
executeRewriteInput,
|
|
5
10
|
isRedirect,
|
|
@@ -17,6 +22,8 @@ import { handleServerAction } from './server-functions-handler'
|
|
|
17
22
|
import { HEADERS } from './constants'
|
|
18
23
|
import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
|
|
19
24
|
import type {
|
|
25
|
+
AnyFunctionMiddleware,
|
|
26
|
+
AnyRequestMiddleware,
|
|
20
27
|
AnyStartInstanceOptions,
|
|
21
28
|
RouteMethod,
|
|
22
29
|
RouteMethodHandlerFn,
|
|
@@ -27,7 +34,6 @@ import type { RequestHandler } from './request-handler'
|
|
|
27
34
|
import type {
|
|
28
35
|
AnyRoute,
|
|
29
36
|
AnyRouter,
|
|
30
|
-
Awaitable,
|
|
31
37
|
Manifest,
|
|
32
38
|
Register,
|
|
33
39
|
} from '@tanstack/router-core'
|
|
@@ -35,6 +41,10 @@ import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
|
|
|
35
41
|
|
|
36
42
|
type TODO = any
|
|
37
43
|
|
|
44
|
+
type AnyMiddlewareServerFn =
|
|
45
|
+
| AnyRequestMiddleware['options']['server']
|
|
46
|
+
| AnyFunctionMiddleware['options']['server']
|
|
47
|
+
|
|
38
48
|
function getStartResponseHeaders(opts: { router: AnyRouter }) {
|
|
39
49
|
const headers = mergeHeaders(
|
|
40
50
|
{
|
|
@@ -47,46 +57,165 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
|
|
|
47
57
|
return headers
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// Cached entries - loaded once per process
|
|
61
|
+
let cachedStartEntry: StartEntry | null = null
|
|
62
|
+
let cachedRouterEntry: RouterEntry | null = null
|
|
63
|
+
let cachedManifest: Manifest | null = null
|
|
64
|
+
|
|
65
|
+
async function getEntries(): Promise<{
|
|
66
|
+
startEntry: StartEntry
|
|
67
|
+
routerEntry: RouterEntry
|
|
68
|
+
}> {
|
|
69
|
+
if (cachedRouterEntry === null) {
|
|
70
|
+
// @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
|
|
71
|
+
cachedRouterEntry = await import('#tanstack-router-entry')
|
|
72
|
+
}
|
|
73
|
+
if (cachedStartEntry === null) {
|
|
74
|
+
// @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
|
|
75
|
+
cachedStartEntry = await import('#tanstack-start-entry')
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
startEntry: cachedStartEntry as unknown as StartEntry,
|
|
79
|
+
routerEntry: cachedRouterEntry as unknown as RouterEntry,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getManifest(): Promise<Manifest> {
|
|
84
|
+
if (cachedManifest === null) {
|
|
85
|
+
cachedManifest = await getStartManifest()
|
|
86
|
+
}
|
|
87
|
+
return cachedManifest
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Pre-computed constants
|
|
91
|
+
const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
|
|
92
|
+
const SERVER_FN_BASE = process.env.TSS_SERVER_FN_BASE
|
|
93
|
+
const IS_PRERENDERING = process.env.TSS_PRERENDERING === 'true'
|
|
94
|
+
const IS_SHELL_ENV = process.env.TSS_SHELL === 'true'
|
|
95
|
+
const IS_DEV = process.env.NODE_ENV === 'development'
|
|
96
|
+
|
|
97
|
+
// Reusable error messages
|
|
98
|
+
const ERR_NO_RESPONSE = IS_DEV
|
|
99
|
+
? `It looks like you forgot to return a response from your server route handler. If you want to defer to the app router, make sure to have a component set in this route.`
|
|
100
|
+
: 'Internal Server Error'
|
|
101
|
+
|
|
102
|
+
const ERR_NO_DEFER = IS_DEV
|
|
103
|
+
? `You cannot defer to the app router if there is no component defined on this route.`
|
|
104
|
+
: 'Internal Server Error'
|
|
105
|
+
|
|
106
|
+
function throwRouteHandlerError(): never {
|
|
107
|
+
throw new Error(ERR_NO_RESPONSE)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function throwIfMayNotDefer(): never {
|
|
111
|
+
throw new Error(ERR_NO_DEFER)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a value is a special response (Response or Redirect)
|
|
116
|
+
*/
|
|
117
|
+
function isSpecialResponse(value: unknown): value is Response {
|
|
118
|
+
return value instanceof Response || isRedirect(value)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize middleware result to context shape
|
|
123
|
+
*/
|
|
124
|
+
function handleCtxResult(result: TODO) {
|
|
125
|
+
if (isSpecialResponse(result)) {
|
|
126
|
+
return { response: result }
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute a middleware chain
|
|
133
|
+
*/
|
|
134
|
+
function executeMiddleware(middlewares: Array<TODO>, ctx: TODO): Promise<TODO> {
|
|
135
|
+
let index = -1
|
|
136
|
+
|
|
137
|
+
const next = async (nextCtx?: TODO): Promise<TODO> => {
|
|
138
|
+
// Merge context if provided using safeObjectMerge for prototype pollution prevention
|
|
139
|
+
if (nextCtx) {
|
|
140
|
+
if (nextCtx.context) {
|
|
141
|
+
ctx.context = safeObjectMerge(ctx.context, nextCtx.context)
|
|
142
|
+
}
|
|
143
|
+
// Copy own properties except context (Object.keys returns only own enumerable properties)
|
|
144
|
+
for (const key of Object.keys(nextCtx)) {
|
|
145
|
+
if (key !== 'context') {
|
|
146
|
+
ctx[key] = nextCtx[key]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
64
149
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
150
|
+
|
|
151
|
+
index++
|
|
152
|
+
const middleware = middlewares[index]
|
|
153
|
+
if (!middleware) return ctx
|
|
154
|
+
|
|
155
|
+
let result: TODO
|
|
156
|
+
try {
|
|
157
|
+
result = await middleware({ ...ctx, next })
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (isSpecialResponse(err)) {
|
|
160
|
+
ctx.response = err
|
|
161
|
+
return ctx
|
|
162
|
+
}
|
|
163
|
+
throw err
|
|
68
164
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
165
|
+
|
|
166
|
+
const normalized = handleCtxResult(result)
|
|
167
|
+
if (normalized) {
|
|
168
|
+
if (normalized.response !== undefined) {
|
|
169
|
+
ctx.response = normalized.response
|
|
170
|
+
}
|
|
171
|
+
if (normalized.context) {
|
|
172
|
+
ctx.context = safeObjectMerge(ctx.context, normalized.context)
|
|
173
|
+
}
|
|
72
174
|
}
|
|
175
|
+
|
|
176
|
+
return ctx
|
|
73
177
|
}
|
|
74
178
|
|
|
179
|
+
return next()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Wrap a route handler as middleware
|
|
184
|
+
*/
|
|
185
|
+
function handlerToMiddleware(
|
|
186
|
+
handler: RouteMethodHandlerFn<any, AnyRoute, any, any, any, any, any>,
|
|
187
|
+
mayDefer: boolean = false,
|
|
188
|
+
): TODO {
|
|
189
|
+
if (mayDefer) {
|
|
190
|
+
return handler
|
|
191
|
+
}
|
|
192
|
+
return async (ctx: TODO) => {
|
|
193
|
+
const response = await handler({ ...ctx, next: throwIfMayNotDefer })
|
|
194
|
+
if (!response) {
|
|
195
|
+
throwRouteHandlerError()
|
|
196
|
+
}
|
|
197
|
+
return response
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createStartHandler<TRegister = Register>(
|
|
202
|
+
cb: HandlerCallback<AnyRouter>,
|
|
203
|
+
): RequestHandler<TRegister> {
|
|
75
204
|
const startRequestResolver: RequestHandler<Register> = async (
|
|
76
205
|
request,
|
|
77
206
|
requestOpts,
|
|
78
207
|
) => {
|
|
79
208
|
let router: AnyRouter | null = null as AnyRouter | null
|
|
80
|
-
// Track whether the callback will handle cleanup
|
|
81
209
|
let cbWillCleanup = false as boolean
|
|
82
|
-
try {
|
|
83
|
-
const origin = getOrigin(request)
|
|
84
210
|
|
|
211
|
+
try {
|
|
85
212
|
const url = new URL(request.url)
|
|
86
213
|
const href = url.href.replace(url.origin, '')
|
|
214
|
+
const origin = getOrigin(request)
|
|
87
215
|
|
|
216
|
+
const entries = await getEntries()
|
|
88
217
|
const startOptions: AnyStartInstanceOptions =
|
|
89
|
-
(await
|
|
218
|
+
(await entries.startEntry.startInstance?.getOptions()) ||
|
|
90
219
|
({} as AnyStartInstanceOptions)
|
|
91
220
|
|
|
92
221
|
const serializationAdapters = [
|
|
@@ -99,22 +228,27 @@ export function createStartHandler<TRegister = Register>(
|
|
|
99
228
|
serializationAdapters,
|
|
100
229
|
}
|
|
101
230
|
|
|
102
|
-
|
|
231
|
+
// Flatten request middlewares once
|
|
232
|
+
const flattenedRequestMiddlewares = startOptions.requestMiddleware
|
|
233
|
+
? flattenMiddlewares(startOptions.requestMiddleware)
|
|
234
|
+
: []
|
|
235
|
+
|
|
236
|
+
// Create set for deduplication
|
|
237
|
+
const executedRequestMiddlewares = new Set<TODO>(
|
|
238
|
+
flattenedRequestMiddlewares,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
// Memoized router getter
|
|
242
|
+
const getRouter = async (): Promise<AnyRouter> => {
|
|
103
243
|
if (router) return router
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let isShell = process.env.TSS_SHELL === 'true'
|
|
110
|
-
if (isPrerendering && !isShell) {
|
|
111
|
-
// only read the shell header if we are prerendering
|
|
112
|
-
// to avoid runtime behavior changes by injecting this header
|
|
113
|
-
// the header is set by the prerender plugin
|
|
244
|
+
|
|
245
|
+
router = await entries.routerEntry.getRouter()
|
|
246
|
+
|
|
247
|
+
let isShell = IS_SHELL_ENV
|
|
248
|
+
if (IS_PRERENDERING && !isShell) {
|
|
114
249
|
isShell = request.headers.get(HEADERS.TSS_SHELL) === 'true'
|
|
115
250
|
}
|
|
116
251
|
|
|
117
|
-
// Create a history for the client-side router
|
|
118
252
|
const history = createMemoryHistory({
|
|
119
253
|
initialEntries: [href],
|
|
120
254
|
})
|
|
@@ -122,7 +256,7 @@ export function createStartHandler<TRegister = Register>(
|
|
|
122
256
|
router.update({
|
|
123
257
|
history,
|
|
124
258
|
isShell,
|
|
125
|
-
isPrerendering,
|
|
259
|
+
isPrerendering: IS_PRERENDERING,
|
|
126
260
|
origin: router.options.origin ?? origin,
|
|
127
261
|
...{
|
|
128
262
|
defaultSsr: requestStartOptions.defaultSsr,
|
|
@@ -133,184 +267,134 @@ export function createStartHandler<TRegister = Register>(
|
|
|
133
267
|
},
|
|
134
268
|
basepath: ROUTER_BASEPATH,
|
|
135
269
|
})
|
|
270
|
+
|
|
136
271
|
return router
|
|
137
272
|
}
|
|
138
273
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
274
|
+
// Check for server function requests first (early exit)
|
|
275
|
+
if (SERVER_FN_BASE && url.pathname.startsWith(SERVER_FN_BASE)) {
|
|
276
|
+
const serverFnId = url.pathname
|
|
277
|
+
.slice(SERVER_FN_BASE.length)
|
|
278
|
+
.split('/')[0]
|
|
279
|
+
|
|
280
|
+
if (!serverFnId) {
|
|
281
|
+
throw new Error('Invalid server action param for serverFnId')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const serverFnHandler = async ({ context }: TODO) => {
|
|
285
|
+
return runWithStartContext(
|
|
142
286
|
{
|
|
143
287
|
getRouter,
|
|
144
288
|
startOptions: requestStartOptions,
|
|
145
289
|
contextAfterGlobalMiddlewares: context,
|
|
146
290
|
request,
|
|
291
|
+
executedRequestMiddlewares,
|
|
147
292
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
context: requestOpts?.context,
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const executeRouter = async ({
|
|
159
|
-
serverContext,
|
|
160
|
-
}: {
|
|
161
|
-
serverContext: any
|
|
162
|
-
}) => {
|
|
163
|
-
const requestAcceptHeader =
|
|
164
|
-
request.headers.get('Accept') || '*/*'
|
|
165
|
-
const splitRequestAcceptHeader =
|
|
166
|
-
requestAcceptHeader.split(',')
|
|
167
|
-
|
|
168
|
-
const supportedMimeTypes = ['*/*', 'text/html']
|
|
169
|
-
const isRouterAcceptSupported = supportedMimeTypes.some(
|
|
170
|
-
(mimeType) =>
|
|
171
|
-
splitRequestAcceptHeader.some((acceptedMimeType) =>
|
|
172
|
-
acceptedMimeType.trim().startsWith(mimeType),
|
|
173
|
-
),
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
if (!isRouterAcceptSupported) {
|
|
177
|
-
return Response.json(
|
|
178
|
-
{
|
|
179
|
-
error: 'Only HTML requests are supported here',
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
status: 500,
|
|
183
|
-
},
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// if the startRoutesManifest is not loaded yet, load it once
|
|
188
|
-
if (startRoutesManifest === null) {
|
|
189
|
-
startRoutesManifest = await getStartManifest()
|
|
190
|
-
}
|
|
191
|
-
const router = await getRouter()
|
|
192
|
-
attachRouterServerSsrUtils({
|
|
193
|
-
router,
|
|
194
|
-
manifest: startRoutesManifest,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
router.update({ additionalContext: { serverContext } })
|
|
198
|
-
await router.load()
|
|
199
|
-
|
|
200
|
-
// If there was a redirect, skip rendering the page at all
|
|
201
|
-
if (router.state.redirect) {
|
|
202
|
-
return router.state.redirect
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await router.serverSsr!.dehydrate()
|
|
206
|
-
|
|
207
|
-
const responseHeaders = getStartResponseHeaders({ router })
|
|
208
|
-
// Mark that the callback will handle cleanup
|
|
209
|
-
cbWillCleanup = true
|
|
210
|
-
const response = await cb({
|
|
211
|
-
request,
|
|
212
|
-
router,
|
|
213
|
-
responseHeaders,
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
return response
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const response = await handleServerRoutes({
|
|
220
|
-
getRouter,
|
|
221
|
-
request,
|
|
222
|
-
executeRouter,
|
|
223
|
-
context,
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
return response
|
|
227
|
-
} catch (err) {
|
|
228
|
-
if (err instanceof Response) {
|
|
229
|
-
return err
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
throw err
|
|
233
|
-
}
|
|
234
|
-
},
|
|
293
|
+
() =>
|
|
294
|
+
handleServerAction({
|
|
295
|
+
request,
|
|
296
|
+
context: requestOpts?.context,
|
|
297
|
+
serverFnId,
|
|
298
|
+
}),
|
|
235
299
|
)
|
|
236
|
-
|
|
237
|
-
},
|
|
238
|
-
)
|
|
300
|
+
}
|
|
239
301
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const ctx = await executeMiddleware(
|
|
245
|
-
[...middlewares, requestHandlerMiddleware],
|
|
246
|
-
{
|
|
302
|
+
const middlewares = flattenedRequestMiddlewares.map(
|
|
303
|
+
(d) => d.options.server,
|
|
304
|
+
)
|
|
305
|
+
const ctx = await executeMiddleware([...middlewares, serverFnHandler], {
|
|
247
306
|
request,
|
|
307
|
+
context: createNullProtoObject(requestOpts?.context),
|
|
308
|
+
})
|
|
248
309
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
310
|
+
return handleRedirectResponse(ctx.response, request, getRouter)
|
|
311
|
+
}
|
|
252
312
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return Response.json(
|
|
259
|
-
{
|
|
260
|
-
...response.options,
|
|
261
|
-
isSerializedRedirect: true,
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
headers: response.headers,
|
|
265
|
-
},
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
return response
|
|
269
|
-
}
|
|
270
|
-
if (
|
|
271
|
-
response.options.to &&
|
|
272
|
-
typeof response.options.to === 'string' &&
|
|
273
|
-
!response.options.to.startsWith('/')
|
|
274
|
-
) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
`Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(response.options)}`,
|
|
277
|
-
)
|
|
278
|
-
}
|
|
313
|
+
// Router execution function
|
|
314
|
+
const executeRouter = async (serverContext: TODO): Promise<Response> => {
|
|
315
|
+
const acceptHeader = request.headers.get('Accept') || '*/*'
|
|
316
|
+
const acceptParts = acceptHeader.split(',')
|
|
317
|
+
const supportedMimeTypes = ['*/*', 'text/html']
|
|
279
318
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
.filter((d) => typeof (response.options as any)[d] === 'function')
|
|
290
|
-
.map((d) => `"${d}"`)
|
|
291
|
-
.join(', ')}`,
|
|
319
|
+
const isSupported = supportedMimeTypes.some((mimeType) =>
|
|
320
|
+
acceptParts.some((part) => part.trim().startsWith(mimeType)),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if (!isSupported) {
|
|
324
|
+
return Response.json(
|
|
325
|
+
{ error: 'Only HTML requests are supported here' },
|
|
326
|
+
{ status: 500 },
|
|
292
327
|
)
|
|
293
328
|
}
|
|
294
329
|
|
|
295
|
-
const
|
|
296
|
-
const
|
|
330
|
+
const manifest = await getManifest()
|
|
331
|
+
const routerInstance = await getRouter()
|
|
297
332
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
333
|
+
attachRouterServerSsrUtils({
|
|
334
|
+
router: routerInstance,
|
|
335
|
+
manifest,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
routerInstance.update({ additionalContext: { serverContext } })
|
|
339
|
+
await routerInstance.load()
|
|
340
|
+
|
|
341
|
+
if (routerInstance.state.redirect) {
|
|
342
|
+
return routerInstance.state.redirect
|
|
308
343
|
}
|
|
309
344
|
|
|
310
|
-
|
|
345
|
+
await routerInstance.serverSsr!.dehydrate()
|
|
346
|
+
|
|
347
|
+
const responseHeaders = getStartResponseHeaders({
|
|
348
|
+
router: routerInstance,
|
|
349
|
+
})
|
|
350
|
+
cbWillCleanup = true
|
|
351
|
+
|
|
352
|
+
return cb({
|
|
353
|
+
request,
|
|
354
|
+
router: routerInstance,
|
|
355
|
+
responseHeaders,
|
|
356
|
+
})
|
|
311
357
|
}
|
|
312
358
|
|
|
313
|
-
|
|
359
|
+
// Main request handler
|
|
360
|
+
const requestHandlerMiddleware = async ({ context }: TODO) => {
|
|
361
|
+
return runWithStartContext(
|
|
362
|
+
{
|
|
363
|
+
getRouter,
|
|
364
|
+
startOptions: requestStartOptions,
|
|
365
|
+
contextAfterGlobalMiddlewares: context,
|
|
366
|
+
request,
|
|
367
|
+
executedRequestMiddlewares,
|
|
368
|
+
},
|
|
369
|
+
async () => {
|
|
370
|
+
try {
|
|
371
|
+
return await handleServerRoutes({
|
|
372
|
+
getRouter,
|
|
373
|
+
request,
|
|
374
|
+
url,
|
|
375
|
+
executeRouter,
|
|
376
|
+
context,
|
|
377
|
+
executedRequestMiddlewares,
|
|
378
|
+
})
|
|
379
|
+
} catch (err) {
|
|
380
|
+
if (err instanceof Response) {
|
|
381
|
+
return err
|
|
382
|
+
}
|
|
383
|
+
throw err
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const middlewares = flattenedRequestMiddlewares.map(
|
|
390
|
+
(d) => d.options.server,
|
|
391
|
+
)
|
|
392
|
+
const ctx = await executeMiddleware(
|
|
393
|
+
[...middlewares, requestHandlerMiddleware],
|
|
394
|
+
{ request, context: createNullProtoObject(requestOpts?.context) },
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return handleRedirectResponse(ctx.response, request, getRouter)
|
|
314
398
|
} finally {
|
|
315
399
|
if (router && !cbWillCleanup) {
|
|
316
400
|
// Clean up router SSR state if it was set up but won't be cleaned up by the callback
|
|
@@ -326,25 +410,78 @@ export function createStartHandler<TRegister = Register>(
|
|
|
326
410
|
return requestHandler(startRequestResolver)
|
|
327
411
|
}
|
|
328
412
|
|
|
413
|
+
async function handleRedirectResponse(
|
|
414
|
+
response: Response,
|
|
415
|
+
request: Request,
|
|
416
|
+
getRouter: () => Promise<AnyRouter>,
|
|
417
|
+
): Promise<Response> {
|
|
418
|
+
if (!isRedirect(response)) {
|
|
419
|
+
return response
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (isResolvedRedirect(response)) {
|
|
423
|
+
if (request.headers.get('x-tsr-serverFn') === 'true') {
|
|
424
|
+
return Response.json(
|
|
425
|
+
{ ...response.options, isSerializedRedirect: true },
|
|
426
|
+
{ headers: response.headers },
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
return response
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const opts = response.options
|
|
433
|
+
if (opts.to && typeof opts.to === 'string' && !opts.to.startsWith('/')) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
`Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(opts)}`,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
['params', 'search', 'hash'].some(
|
|
441
|
+
(d) => typeof (opts as TODO)[d] === 'function',
|
|
442
|
+
)
|
|
443
|
+
) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(
|
|
446
|
+
opts,
|
|
447
|
+
)
|
|
448
|
+
.filter((d) => typeof (opts as TODO)[d] === 'function')
|
|
449
|
+
.map((d) => `"${d}"`)
|
|
450
|
+
.join(', ')}`,
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const router = await getRouter()
|
|
455
|
+
const redirect = router.resolveRedirect(response)
|
|
456
|
+
|
|
457
|
+
if (request.headers.get('x-tsr-serverFn') === 'true') {
|
|
458
|
+
return Response.json(
|
|
459
|
+
{ ...response.options, isSerializedRedirect: true },
|
|
460
|
+
{ headers: response.headers },
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return redirect
|
|
465
|
+
}
|
|
466
|
+
|
|
329
467
|
async function handleServerRoutes({
|
|
330
468
|
getRouter,
|
|
331
469
|
request,
|
|
470
|
+
url,
|
|
332
471
|
executeRouter,
|
|
333
472
|
context,
|
|
473
|
+
executedRequestMiddlewares,
|
|
334
474
|
}: {
|
|
335
|
-
getRouter: () =>
|
|
475
|
+
getRouter: () => Promise<AnyRouter>
|
|
336
476
|
request: Request
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}: {
|
|
340
|
-
serverContext: any
|
|
341
|
-
}) => Promise<Response>
|
|
477
|
+
url: URL
|
|
478
|
+
executeRouter: (serverContext: any) => Promise<Response>
|
|
342
479
|
context: any
|
|
343
|
-
|
|
480
|
+
executedRequestMiddlewares: Set<AnyRequestMiddleware>
|
|
481
|
+
}): Promise<Response> {
|
|
344
482
|
const router = await getRouter()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const pathname = url.pathname
|
|
483
|
+
const rewrittenUrl = executeRewriteInput(router.rewrite, url)
|
|
484
|
+
const pathname = rewrittenUrl.pathname
|
|
348
485
|
// this will perform a fuzzy match, however for server routes we need an exact match
|
|
349
486
|
// if the route is not an exact match, executeRouter will handle rendering the app router
|
|
350
487
|
// the match will be cached internally, so no extra work is done during the app router render
|
|
@@ -353,158 +490,64 @@ async function handleServerRoutes({
|
|
|
353
490
|
|
|
354
491
|
const isExactMatch = foundRoute && routeParams['**'] === undefined
|
|
355
492
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
: server.handlers
|
|
371
|
-
|
|
372
|
-
const requestMethod = request.method.toUpperCase() as RouteMethod
|
|
373
|
-
|
|
374
|
-
// Attempt to find the method in the handlers
|
|
375
|
-
const handler = handlers[requestMethod] ?? handlers['ANY']
|
|
376
|
-
|
|
377
|
-
// If a method is found, execute the handler
|
|
378
|
-
if (handler) {
|
|
379
|
-
const mayDefer = !!foundRoute.options.component
|
|
380
|
-
if (typeof handler === 'function') {
|
|
381
|
-
middlewares.push(handlerToMiddleware(handler, mayDefer))
|
|
382
|
-
} else {
|
|
383
|
-
const { middleware } = handler
|
|
384
|
-
if (middleware && middleware.length) {
|
|
385
|
-
middlewares.push(
|
|
386
|
-
...flattenMiddlewares(middleware).map((d) => d.options.server),
|
|
387
|
-
)
|
|
388
|
-
}
|
|
389
|
-
if (handler.handler) {
|
|
390
|
-
middlewares.push(handlerToMiddleware(handler.handler, mayDefer))
|
|
391
|
-
}
|
|
493
|
+
// Collect and dedupe route middlewares
|
|
494
|
+
const routeMiddlewares: Array<AnyMiddlewareServerFn> = []
|
|
495
|
+
|
|
496
|
+
// Collect middleware from matched routes, filtering out those already executed
|
|
497
|
+
// in the request phase
|
|
498
|
+
for (const route of matchedRoutes) {
|
|
499
|
+
const serverMiddleware = route.options.server?.middleware as
|
|
500
|
+
| Array<AnyRequestMiddleware>
|
|
501
|
+
| undefined
|
|
502
|
+
if (serverMiddleware) {
|
|
503
|
+
const flattened = flattenMiddlewares(serverMiddleware)
|
|
504
|
+
for (const m of flattened) {
|
|
505
|
+
if (!executedRequestMiddlewares.has(m)) {
|
|
506
|
+
routeMiddlewares.push(m.options.server)
|
|
392
507
|
}
|
|
393
508
|
}
|
|
394
509
|
}
|
|
395
510
|
}
|
|
396
511
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
context,
|
|
405
|
-
params: routeParams,
|
|
406
|
-
pathname,
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
const response: Response = ctx.response
|
|
410
|
-
|
|
411
|
-
return response
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function throwRouteHandlerError() {
|
|
415
|
-
if (process.env.NODE_ENV === 'development') {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`It looks like you forgot to return a response from your server route handler. If you want to defer to the app router, make sure to have a component set in this route.`,
|
|
418
|
-
)
|
|
419
|
-
}
|
|
420
|
-
throw new Error('Internal Server Error')
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function throwIfMayNotDefer() {
|
|
424
|
-
if (process.env.NODE_ENV === 'development') {
|
|
425
|
-
throw new Error(
|
|
426
|
-
`You cannot defer to the app router if there is no component defined on this route.`,
|
|
427
|
-
)
|
|
428
|
-
}
|
|
429
|
-
throw new Error('Internal Server Error')
|
|
430
|
-
}
|
|
431
|
-
function handlerToMiddleware(
|
|
432
|
-
handler: RouteMethodHandlerFn<any, AnyRoute, any, any, any, any, any>,
|
|
433
|
-
mayDefer: boolean = false,
|
|
434
|
-
) {
|
|
435
|
-
if (mayDefer) {
|
|
436
|
-
return handler as TODO
|
|
437
|
-
}
|
|
438
|
-
return async ({ next: _next, ...rest }: TODO) => {
|
|
439
|
-
const response = await handler({ ...rest, next: throwIfMayNotDefer })
|
|
440
|
-
if (!response) {
|
|
441
|
-
throwRouteHandlerError()
|
|
442
|
-
}
|
|
443
|
-
return response
|
|
444
|
-
}
|
|
445
|
-
}
|
|
512
|
+
// Add handler middleware if exact match
|
|
513
|
+
const server = foundRoute?.options.server
|
|
514
|
+
if (server?.handlers && isExactMatch) {
|
|
515
|
+
const handlers =
|
|
516
|
+
typeof server.handlers === 'function'
|
|
517
|
+
? server.handlers({ createHandlers: (d: any) => d })
|
|
518
|
+
: server.handlers
|
|
446
519
|
|
|
447
|
-
|
|
448
|
-
|
|
520
|
+
const requestMethod = request.method.toUpperCase() as RouteMethod
|
|
521
|
+
const handler = handlers[requestMethod] ?? handlers['ANY']
|
|
449
522
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const middleware = middlewares[index]
|
|
453
|
-
if (!middleware) return ctx
|
|
523
|
+
if (handler) {
|
|
524
|
+
const mayDefer = !!foundRoute.options.component
|
|
454
525
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
result = await middleware({
|
|
458
|
-
...ctx,
|
|
459
|
-
// Allow the middleware to call the next middleware in the chain
|
|
460
|
-
next: async (nextCtx: TODO) => {
|
|
461
|
-
// Allow the caller to extend the context for the next middleware
|
|
462
|
-
const nextResult = await next({
|
|
463
|
-
...ctx,
|
|
464
|
-
...nextCtx,
|
|
465
|
-
context: {
|
|
466
|
-
...ctx.context,
|
|
467
|
-
...(nextCtx?.context || {}),
|
|
468
|
-
},
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
// Merge the result into the context\
|
|
472
|
-
return Object.assign(ctx, handleCtxResult(nextResult))
|
|
473
|
-
},
|
|
474
|
-
// Allow the middleware result to extend the return context
|
|
475
|
-
})
|
|
476
|
-
} catch (err: TODO) {
|
|
477
|
-
if (isSpecialResponse(err)) {
|
|
478
|
-
result = {
|
|
479
|
-
response: err,
|
|
480
|
-
}
|
|
526
|
+
if (typeof handler === 'function') {
|
|
527
|
+
routeMiddlewares.push(handlerToMiddleware(handler, mayDefer))
|
|
481
528
|
} else {
|
|
482
|
-
|
|
529
|
+
if (handler.middleware?.length) {
|
|
530
|
+
const handlerMiddlewares = flattenMiddlewares(handler.middleware)
|
|
531
|
+
for (const m of handlerMiddlewares) {
|
|
532
|
+
routeMiddlewares.push(m.options.server)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (handler.handler) {
|
|
536
|
+
routeMiddlewares.push(handlerToMiddleware(handler.handler, mayDefer))
|
|
537
|
+
}
|
|
483
538
|
}
|
|
484
539
|
}
|
|
485
|
-
|
|
486
|
-
// Merge the middleware result into the context, just in case it
|
|
487
|
-
// returns a partial context
|
|
488
|
-
return Object.assign(ctx, handleCtxResult(result))
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return handleCtxResult(next(ctx))
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function handleCtxResult(result: TODO) {
|
|
495
|
-
if (isSpecialResponse(result)) {
|
|
496
|
-
return {
|
|
497
|
-
response: result,
|
|
498
|
-
}
|
|
499
540
|
}
|
|
500
541
|
|
|
501
|
-
|
|
502
|
-
|
|
542
|
+
// Final middleware: execute router
|
|
543
|
+
routeMiddlewares.push((ctx: TODO) => executeRouter(ctx.context))
|
|
503
544
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
545
|
+
const ctx = await executeMiddleware(routeMiddlewares, {
|
|
546
|
+
request,
|
|
547
|
+
context,
|
|
548
|
+
params: routeParams,
|
|
549
|
+
pathname,
|
|
550
|
+
})
|
|
507
551
|
|
|
508
|
-
|
|
509
|
-
return response instanceof Response
|
|
552
|
+
return ctx.response
|
|
510
553
|
}
|