@tanstack/start-server-core 1.132.0-alpha.9 → 1.132.1
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.d.ts +3 -6
- package/dist/esm/createStartHandler.js +261 -220
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/index.d.ts +1 -3
- package/dist/esm/index.js +0 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/loadVirtualModule.js +0 -2
- package/dist/esm/loadVirtualModule.js.map +1 -1
- package/dist/esm/request-handler.d.ts +19 -0
- package/dist/esm/request-response.d.ts +2 -2
- package/dist/esm/request-response.js +5 -2
- package/dist/esm/request-response.js.map +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -1
- package/dist/esm/server-functions-handler.d.ts +2 -1
- package/dist/esm/server-functions-handler.js +16 -13
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/dist/esm/virtual-modules.d.ts +0 -2
- package/dist/esm/virtual-modules.js +0 -1
- package/dist/esm/virtual-modules.js.map +1 -1
- package/package.json +7 -7
- package/src/createStartHandler.ts +353 -326
- package/src/index.tsx +2 -42
- package/src/loadVirtualModule.ts +0 -2
- package/src/request-handler.ts +31 -0
- package/src/request-response.ts +8 -5
- package/src/serializer/ServerFunctionSerializationAdapter.ts +1 -1
- package/src/server-functions-handler.ts +21 -16
- package/src/tanstack-start.d.ts +0 -2
- package/src/virtual-modules.ts +0 -2
- package/dist/esm/serializer/getSerovalPlugins.d.ts +0 -3
- package/dist/esm/serializer/getSerovalPlugins.js +0 -13
- package/dist/esm/serializer/getSerovalPlugins.js.map +0 -1
- package/dist/esm/serverRoute.d.ts +0 -124
- package/dist/esm/serverRoute.js +0 -103
- package/dist/esm/serverRoute.js.map +0 -1
- package/src/serializer/getSerovalPlugins.ts +0 -10
- package/src/serverRoute.ts +0 -736
|
@@ -5,11 +5,10 @@ import {
|
|
|
5
5
|
mergeHeaders,
|
|
6
6
|
} from '@tanstack/start-client-core'
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
executeRewriteInput,
|
|
9
9
|
isRedirect,
|
|
10
10
|
isResolvedRedirect,
|
|
11
11
|
joinPaths,
|
|
12
|
-
processRouteTree,
|
|
13
12
|
trimPath,
|
|
14
13
|
} from '@tanstack/router-core'
|
|
15
14
|
import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server'
|
|
@@ -17,31 +16,28 @@ import { runWithStartContext } from '@tanstack/start-storage-context'
|
|
|
17
16
|
import { getResponseHeaders, requestHandler } from './request-response'
|
|
18
17
|
import { getStartManifest } from './router-manifest'
|
|
19
18
|
import { handleServerAction } from './server-functions-handler'
|
|
20
|
-
import { VIRTUAL_MODULES } from './virtual-modules'
|
|
21
|
-
import { loadVirtualModule } from './loadVirtualModule'
|
|
22
19
|
|
|
23
20
|
import { HEADERS } from './constants'
|
|
24
21
|
import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
|
|
25
22
|
import type {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
AnyStartInstanceOptions,
|
|
24
|
+
RouteMethod,
|
|
25
|
+
RouteMethodHandlerFn,
|
|
26
|
+
RouterEntry,
|
|
27
|
+
StartEntry,
|
|
28
|
+
} from '@tanstack/start-client-core'
|
|
29
|
+
import type { RequestHandler } from './request-handler'
|
|
30
30
|
import type {
|
|
31
31
|
AnyRoute,
|
|
32
32
|
AnyRouter,
|
|
33
33
|
Awaitable,
|
|
34
34
|
Manifest,
|
|
35
|
-
|
|
35
|
+
Register,
|
|
36
36
|
} from '@tanstack/router-core'
|
|
37
37
|
import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
|
|
38
38
|
|
|
39
39
|
type TODO = any
|
|
40
40
|
|
|
41
|
-
export type CustomizeStartHandler<TRouter extends AnyRouter> = (
|
|
42
|
-
cb: HandlerCallback<TRouter>,
|
|
43
|
-
) => RequestHandler
|
|
44
|
-
|
|
45
41
|
function getStartResponseHeaders(opts: { router: AnyRouter }) {
|
|
46
42
|
const headers = mergeHeaders(
|
|
47
43
|
getResponseHeaders() as Headers,
|
|
@@ -55,74 +51,98 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
|
|
|
55
51
|
return headers
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
export function createStartHandler<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
export function createStartHandler<TRegister = Register>(
|
|
55
|
+
cb: HandlerCallback<AnyRouter>,
|
|
56
|
+
): RequestHandler<TRegister> {
|
|
57
|
+
if (!process.env.TSS_SERVER_FN_BASE) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'tanstack/start-server-core: TSS_SERVER_FN_BASE must be defined in your environment for createStartHandler()',
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
// TODO do we remove this?
|
|
63
|
+
const APP_BASE = process.env.TSS_APP_BASE || '/'
|
|
64
|
+
// Add trailing slash to sanitise user defined TSS_SERVER_FN_BASE
|
|
65
|
+
const serverFnBase = joinPaths([
|
|
66
|
+
APP_BASE,
|
|
67
|
+
trimPath(process.env.TSS_SERVER_FN_BASE),
|
|
68
|
+
'/',
|
|
69
|
+
])
|
|
67
70
|
let startRoutesManifest: Manifest | null = null
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
let startEntry: StartEntry | null = null
|
|
72
|
+
let routerEntry: RouterEntry | null = null
|
|
73
|
+
const getEntries = async (): Promise<{
|
|
74
|
+
startEntry: StartEntry
|
|
75
|
+
routerEntry: RouterEntry
|
|
76
|
+
}> => {
|
|
77
|
+
if (routerEntry === null) {
|
|
78
|
+
// @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
|
|
79
|
+
routerEntry = await import('#tanstack-router-entry')
|
|
80
|
+
}
|
|
81
|
+
if (startEntry === null) {
|
|
82
|
+
// @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
|
|
83
|
+
startEntry = await import('#tanstack-start-entry')
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
startEntry: startEntry as unknown as StartEntry,
|
|
87
|
+
routerEntry: routerEntry as unknown as RouterEntry,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
request.headers.get('Origin') ||
|
|
90
|
-
request.headers.get('Referer') ||
|
|
91
|
-
'http://localhost'
|
|
92
|
-
)
|
|
93
|
-
}
|
|
91
|
+
const originalFetch = globalThis.fetch
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
93
|
+
const startRequestResolver: RequestHandler<Register> = async (
|
|
94
|
+
request,
|
|
95
|
+
requestOpts,
|
|
96
|
+
) => {
|
|
97
|
+
function getOrigin() {
|
|
98
|
+
const originHeader = request.headers.get('Origin')
|
|
99
|
+
if (originHeader) {
|
|
100
|
+
return originHeader
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return new URL(request.url).origin
|
|
104
|
+
} catch {}
|
|
105
|
+
return 'http://localhost'
|
|
106
|
+
}
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
// Patching fetch function to use our request resolver
|
|
109
|
+
// if the input starts with `/` which is a common pattern for
|
|
110
|
+
// client-side routing.
|
|
111
|
+
// When we encounter similar requests, we can assume that the
|
|
112
|
+
// user wants to use the same origin as the current request.
|
|
113
|
+
globalThis.fetch = async function (input, init) {
|
|
114
|
+
function resolve(url: URL, requestOptions: RequestInit | undefined) {
|
|
115
|
+
const fetchRequest = new Request(url, requestOptions)
|
|
116
|
+
return startRequestResolver(fetchRequest, requestOpts)
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
if (typeof input === 'string' && input.startsWith('/')) {
|
|
120
|
+
// e.g: fetch('/api/data')
|
|
121
|
+
const url = new URL(input, getOrigin())
|
|
122
|
+
return resolve(url, init)
|
|
123
|
+
} else if (
|
|
124
|
+
typeof input === 'object' &&
|
|
125
|
+
'url' in input &&
|
|
126
|
+
typeof input.url === 'string' &&
|
|
127
|
+
input.url.startsWith('/')
|
|
128
|
+
) {
|
|
129
|
+
// e.g: fetch(new Request('/api/data'))
|
|
130
|
+
const url = new URL(input.url, getOrigin())
|
|
131
|
+
return resolve(url, init)
|
|
132
|
+
}
|
|
116
133
|
|
|
117
|
-
|
|
134
|
+
// If not, it should just use the original fetch
|
|
135
|
+
return originalFetch(input, init)
|
|
136
|
+
}
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
const url = new URL(request.url)
|
|
139
|
+
const href = url.href.replace(url.origin, '')
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
let router: AnyRouter | null = null
|
|
142
|
+
const getRouter = async () => {
|
|
143
|
+
if (router) return router
|
|
144
|
+
// TODO how does this work with base path? does the router need to be configured the same as APP_BASE?
|
|
145
|
+
router = await (await getEntries()).routerEntry.getRouter()
|
|
126
146
|
|
|
127
147
|
// Update the client-side router with the history
|
|
128
148
|
const isPrerendering = process.env.TSS_PRERENDERING === 'true'
|
|
@@ -134,178 +154,149 @@ export function createStartHandler<TRouter extends AnyRouter>({
|
|
|
134
154
|
// the header is set by the prerender plugin
|
|
135
155
|
isShell = request.headers.get(HEADERS.TSS_SHELL) === 'true'
|
|
136
156
|
}
|
|
137
|
-
// insert start specific default serialization adapters
|
|
138
|
-
const serializationAdapters = (
|
|
139
|
-
router.options.serializationAdapters ?? []
|
|
140
|
-
).concat(ServerFunctionSerializationAdapter)
|
|
141
157
|
|
|
158
|
+
// Create a history for the client-side router
|
|
159
|
+
const history = createMemoryHistory({
|
|
160
|
+
initialEntries: [href],
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const origin = router.options.origin ?? getOrigin()
|
|
142
164
|
router.update({
|
|
143
165
|
history,
|
|
144
166
|
isShell,
|
|
145
167
|
isPrerendering,
|
|
146
|
-
|
|
168
|
+
origin,
|
|
169
|
+
...{
|
|
170
|
+
defaultSsr: startOptions.defaultSsr,
|
|
171
|
+
serializationAdapters: startOptions.serializationAdapters,
|
|
172
|
+
},
|
|
147
173
|
})
|
|
174
|
+
return router
|
|
175
|
+
}
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (routeTreeModule === null) {
|
|
177
|
+
const startOptions: AnyStartInstanceOptions =
|
|
178
|
+
(await (await getEntries()).startEntry.startInstance?.getOptions()) ||
|
|
179
|
+
({} as AnyStartInstanceOptions)
|
|
180
|
+
startOptions.serializationAdapters =
|
|
181
|
+
startOptions.serializationAdapters || []
|
|
182
|
+
// insert start specific default serialization adapters
|
|
183
|
+
startOptions.serializationAdapters.push(ServerFunctionSerializationAdapter)
|
|
184
|
+
|
|
185
|
+
const requestHandlerMiddleware = handlerToMiddleware(
|
|
186
|
+
async ({ context }) => {
|
|
187
|
+
const response = await runWithStartContext(
|
|
188
|
+
{
|
|
189
|
+
getRouter,
|
|
190
|
+
startOptions,
|
|
191
|
+
contextAfterGlobalMiddlewares: context,
|
|
192
|
+
},
|
|
193
|
+
async () => {
|
|
169
194
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
// First, let's attempt to handle server functions
|
|
196
|
+
if (href.startsWith(serverFnBase)) {
|
|
197
|
+
return await handleServerAction({
|
|
198
|
+
request,
|
|
199
|
+
context: requestOpts?.context,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const executeRouter = async ({
|
|
204
|
+
serverContext,
|
|
205
|
+
}: {
|
|
206
|
+
serverContext: any
|
|
207
|
+
}) => {
|
|
208
|
+
const requestAcceptHeader =
|
|
209
|
+
request.headers.get('Accept') || '*/*'
|
|
210
|
+
const splitRequestAcceptHeader = requestAcceptHeader.split(',')
|
|
211
|
+
|
|
212
|
+
const supportedMimeTypes = ['*/*', 'text/html']
|
|
213
|
+
const isRouterAcceptSupported = supportedMimeTypes.some(
|
|
214
|
+
(mimeType) =>
|
|
215
|
+
splitRequestAcceptHeader.some((acceptedMimeType) =>
|
|
216
|
+
acceptedMimeType.trim().startsWith(mimeType),
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (!isRouterAcceptSupported) {
|
|
221
|
+
return json(
|
|
222
|
+
{
|
|
223
|
+
error: 'Only HTML requests are supported here',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
status: 500,
|
|
181
227
|
},
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// if the startRoutesManifest is not loaded yet, load it once
|
|
232
|
+
if (startRoutesManifest === null) {
|
|
233
|
+
startRoutesManifest = await getStartManifest({
|
|
234
|
+
basePath: APP_BASE,
|
|
182
235
|
})
|
|
236
|
+
}
|
|
237
|
+
const router = await getRouter()
|
|
238
|
+
attachRouterServerSsrUtils({
|
|
239
|
+
router,
|
|
240
|
+
manifest: startRoutesManifest,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
router.update({ additionalContext: { serverContext } })
|
|
244
|
+
await router.load()
|
|
245
|
+
|
|
246
|
+
// If there was a redirect, skip rendering the page at all
|
|
247
|
+
if (router.state.redirect) {
|
|
248
|
+
return router.state.redirect
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await router.serverSsr!.dehydrate()
|
|
252
|
+
|
|
253
|
+
const responseHeaders = getStartResponseHeaders({ router })
|
|
254
|
+
const response = await cb({
|
|
255
|
+
request,
|
|
256
|
+
router,
|
|
257
|
+
responseHeaders,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return response
|
|
183
261
|
}
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.log(e)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
262
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const supportedMimeTypes = ['*/*', 'text/html']
|
|
194
|
-
const isRouterAcceptSupported = supportedMimeTypes.some(
|
|
195
|
-
(mimeType) =>
|
|
196
|
-
splitRequestAcceptHeader.some((acceptedMimeType) =>
|
|
197
|
-
acceptedMimeType.trim().startsWith(mimeType),
|
|
198
|
-
),
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
if (!isRouterAcceptSupported) {
|
|
202
|
-
return json(
|
|
203
|
-
{
|
|
204
|
-
error: 'Only HTML requests are supported here',
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
status: 500,
|
|
208
|
-
},
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// if the startRoutesManifest is not loaded yet, load it once
|
|
213
|
-
if (startRoutesManifest === null) {
|
|
214
|
-
startRoutesManifest = await getStartManifest({
|
|
215
|
-
basePath: APP_BASE,
|
|
263
|
+
const response = await handleServerRoutes({
|
|
264
|
+
getRouter,
|
|
265
|
+
request,
|
|
266
|
+
executeRouter,
|
|
216
267
|
})
|
|
217
|
-
}
|
|
218
268
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
269
|
+
return response
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err instanceof Response) {
|
|
272
|
+
return err
|
|
273
|
+
}
|
|
223
274
|
|
|
224
|
-
|
|
225
|
-
if (router.state.redirect) {
|
|
226
|
-
return router.state.redirect
|
|
275
|
+
throw err
|
|
227
276
|
}
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
return response
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const flattenedMiddlewares = startOptions.requestMiddleware
|
|
284
|
+
? flattenMiddlewares(startOptions.requestMiddleware)
|
|
285
|
+
: []
|
|
286
|
+
const middlewares = flattenedMiddlewares.map((d) => d.options.server)
|
|
287
|
+
const ctx = await executeMiddleware(
|
|
288
|
+
[...middlewares, requestHandlerMiddleware],
|
|
289
|
+
{
|
|
290
|
+
request,
|
|
291
|
+
|
|
292
|
+
context: requestOpts?.context || {},
|
|
293
|
+
},
|
|
294
|
+
)
|
|
228
295
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const responseHeaders = getStartResponseHeaders({ router })
|
|
232
|
-
const response = await cb({
|
|
233
|
-
request,
|
|
234
|
-
router,
|
|
235
|
-
responseHeaders,
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
return response
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// If we have a server route tree, then we try matching to see if we have a
|
|
242
|
-
// server route that matches the request.
|
|
243
|
-
if (processedServerRouteTree) {
|
|
244
|
-
const [_matchedRoutes, response] = await handleServerRoutes({
|
|
245
|
-
processedServerRouteTree,
|
|
246
|
-
router,
|
|
247
|
-
request,
|
|
248
|
-
basePath: APP_BASE,
|
|
249
|
-
executeRouter,
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
if (response) return response
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Server Routes did not produce a response, so fallback to normal SSR matching using the router
|
|
256
|
-
const routerResponse = await executeRouter()
|
|
257
|
-
return routerResponse
|
|
258
|
-
} catch (err) {
|
|
259
|
-
if (err instanceof Response) {
|
|
260
|
-
return err
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
throw err
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
if (isRedirect(response)) {
|
|
268
|
-
if (isResolvedRedirect(response)) {
|
|
269
|
-
if (request.headers.get('x-tsr-redirect') === 'manual') {
|
|
270
|
-
return json(
|
|
271
|
-
{
|
|
272
|
-
...response.options,
|
|
273
|
-
isSerializedRedirect: true,
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
headers: response.headers,
|
|
277
|
-
},
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
return response
|
|
281
|
-
}
|
|
282
|
-
if (
|
|
283
|
-
response.options.to &&
|
|
284
|
-
typeof response.options.to === 'string' &&
|
|
285
|
-
!response.options.to.startsWith('/')
|
|
286
|
-
) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
`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)}`,
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
['params', 'search', 'hash'].some(
|
|
294
|
-
(d) => typeof (response.options as any)[d] === 'function',
|
|
295
|
-
)
|
|
296
|
-
) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(
|
|
299
|
-
response.options,
|
|
300
|
-
)
|
|
301
|
-
.filter((d) => typeof (response.options as any)[d] === 'function')
|
|
302
|
-
.map((d) => `"${d}"`)
|
|
303
|
-
.join(', ')}`,
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const redirect = router.resolveRedirect(response)
|
|
296
|
+
const response: Response = ctx.response
|
|
308
297
|
|
|
298
|
+
if (isRedirect(response)) {
|
|
299
|
+
if (isResolvedRedirect(response)) {
|
|
309
300
|
if (request.headers.get('x-tsr-redirect') === 'manual') {
|
|
310
301
|
return json(
|
|
311
302
|
{
|
|
@@ -317,146 +308,182 @@ export function createStartHandler<TRouter extends AnyRouter>({
|
|
|
317
308
|
},
|
|
318
309
|
)
|
|
319
310
|
}
|
|
311
|
+
return response
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
response.options.to &&
|
|
315
|
+
typeof response.options.to === 'string' &&
|
|
316
|
+
!response.options.to.startsWith('/')
|
|
317
|
+
) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`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)}`,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
320
322
|
|
|
321
|
-
|
|
323
|
+
if (
|
|
324
|
+
['params', 'search', 'hash'].some(
|
|
325
|
+
(d) => typeof (response.options as any)[d] === 'function',
|
|
326
|
+
)
|
|
327
|
+
) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(
|
|
330
|
+
response.options,
|
|
331
|
+
)
|
|
332
|
+
.filter((d) => typeof (response.options as any)[d] === 'function')
|
|
333
|
+
.map((d) => `"${d}"`)
|
|
334
|
+
.join(', ')}`,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const router = await getRouter()
|
|
339
|
+
const redirect = router.resolveRedirect(response)
|
|
340
|
+
|
|
341
|
+
if (request.headers.get('x-tsr-redirect') === 'manual') {
|
|
342
|
+
return json(
|
|
343
|
+
{
|
|
344
|
+
...response.options,
|
|
345
|
+
isSerializedRedirect: true,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
headers: response.headers,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
322
351
|
}
|
|
323
352
|
|
|
324
|
-
return
|
|
353
|
+
return redirect
|
|
325
354
|
}
|
|
326
355
|
|
|
327
|
-
return
|
|
356
|
+
return response
|
|
328
357
|
}
|
|
358
|
+
|
|
359
|
+
return requestHandler(startRequestResolver)
|
|
329
360
|
}
|
|
330
361
|
|
|
331
|
-
async function handleServerRoutes(
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
async function handleServerRoutes({
|
|
363
|
+
getRouter,
|
|
364
|
+
request,
|
|
365
|
+
executeRouter,
|
|
366
|
+
}: {
|
|
367
|
+
getRouter: () => Awaitable<AnyRouter>
|
|
334
368
|
request: Request
|
|
335
|
-
|
|
336
|
-
|
|
369
|
+
executeRouter: ({
|
|
370
|
+
serverContext,
|
|
371
|
+
}: {
|
|
372
|
+
serverContext: any
|
|
373
|
+
}) => Promise<Response>
|
|
337
374
|
}) {
|
|
338
|
-
const
|
|
375
|
+
const router = await getRouter()
|
|
376
|
+
let url = new URL(request.url)
|
|
377
|
+
url = executeRewriteInput(router.rewrite, url)
|
|
339
378
|
const pathname = url.pathname
|
|
340
|
-
|
|
341
|
-
const serverTreeResult = getMatchedRoutes<AnyServerRouteWithTypes>({
|
|
379
|
+
const { matchedRoutes, foundRoute, routeParams } = router.getMatchedRoutes(
|
|
342
380
|
pathname,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
routesByPath: opts.processedServerRouteTree.routesByPath,
|
|
346
|
-
routesById: opts.processedServerRouteTree.routesById,
|
|
347
|
-
flatRoutes: opts.processedServerRouteTree.flatRoutes,
|
|
348
|
-
})
|
|
381
|
+
undefined,
|
|
382
|
+
)
|
|
349
383
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let response: Response | undefined
|
|
353
|
-
let matchedRoutes: Array<AnyServerRouteWithTypes> = []
|
|
354
|
-
matchedRoutes = serverTreeResult.matchedRoutes
|
|
355
|
-
// check if the app route tree found a match that is deeper than the server route tree
|
|
356
|
-
if (routeTreeResult.foundRoute) {
|
|
357
|
-
if (
|
|
358
|
-
serverTreeResult.matchedRoutes.length <
|
|
359
|
-
routeTreeResult.matchedRoutes.length
|
|
360
|
-
) {
|
|
361
|
-
const closestCommon = [...routeTreeResult.matchedRoutes]
|
|
362
|
-
.reverse()
|
|
363
|
-
.find((r) => {
|
|
364
|
-
return opts.processedServerRouteTree.routesById[r.id] !== undefined
|
|
365
|
-
})
|
|
366
|
-
if (closestCommon) {
|
|
367
|
-
// walk up the tree and collect all parents
|
|
368
|
-
let routeId = closestCommon.id
|
|
369
|
-
matchedRoutes = []
|
|
370
|
-
do {
|
|
371
|
-
const route = opts.processedServerRouteTree.routesById[routeId]
|
|
372
|
-
if (!route) {
|
|
373
|
-
break
|
|
374
|
-
}
|
|
375
|
-
matchedRoutes.push(route)
|
|
376
|
-
routeId = route.parentRoute?.id
|
|
377
|
-
} while (routeId)
|
|
384
|
+
// TODO: Error handling? What happens when its `throw redirect()` vs `throw new Error()`?
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
386
|
+
const middlewares = flattenMiddlewares(
|
|
387
|
+
matchedRoutes.flatMap((r) => r.options.server?.middleware).filter(Boolean),
|
|
388
|
+
).map((d) => d.options.server)
|
|
383
389
|
|
|
384
|
-
|
|
385
|
-
// We've found a server route that (partially) matches the request, so we can call it.
|
|
386
|
-
// TODO: Error handling? What happens when its `throw redirect()` vs `throw new Error()`?
|
|
390
|
+
const server = foundRoute?.options.server
|
|
387
391
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
if (server) {
|
|
393
|
+
if (server.handlers) {
|
|
394
|
+
const handlers =
|
|
395
|
+
typeof server.handlers === 'function'
|
|
396
|
+
? server.handlers({
|
|
397
|
+
createHandlers: (d: any) => d,
|
|
398
|
+
})
|
|
399
|
+
: server.handlers
|
|
391
400
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
).find(
|
|
396
|
-
(method) => method.toLowerCase() ===
|
|
401
|
+
const requestMethod = request.method.toLowerCase()
|
|
402
|
+
|
|
403
|
+
// Attempt to find the method in the handlers
|
|
404
|
+
let method = Object.keys(handlers).find(
|
|
405
|
+
(method) => method.toLowerCase() === requestMethod,
|
|
397
406
|
)
|
|
398
407
|
|
|
408
|
+
// If no method is found, attempt to find the 'all' method
|
|
409
|
+
if (!method) {
|
|
410
|
+
method = Object.keys(handlers).find(
|
|
411
|
+
(method) => method.toLowerCase() === 'all',
|
|
412
|
+
)
|
|
413
|
+
? 'all'
|
|
414
|
+
: undefined
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// If a method is found, execute the handler
|
|
399
418
|
if (method) {
|
|
400
|
-
const handler =
|
|
419
|
+
const handler = handlers[method as RouteMethod]
|
|
401
420
|
if (handler) {
|
|
421
|
+
const mayDefer = !!foundRoute.options.component
|
|
402
422
|
if (typeof handler === 'function') {
|
|
403
|
-
middlewares.push(handlerToMiddleware(handler
|
|
423
|
+
middlewares.push(handlerToMiddleware(handler, mayDefer))
|
|
404
424
|
} else {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
handler._options.middlewares.length
|
|
408
|
-
) {
|
|
425
|
+
const { middleware } = handler
|
|
426
|
+
if (middleware && middleware.length) {
|
|
409
427
|
middlewares.push(
|
|
410
|
-
...flattenMiddlewares(
|
|
411
|
-
(d) => d.options.server,
|
|
412
|
-
),
|
|
428
|
+
...flattenMiddlewares(middleware).map((d) => d.options.server),
|
|
413
429
|
)
|
|
414
430
|
}
|
|
415
|
-
if (handler.
|
|
416
|
-
middlewares.push(handlerToMiddleware(handler.
|
|
431
|
+
if (handler.handler) {
|
|
432
|
+
middlewares.push(handlerToMiddleware(handler.handler, mayDefer))
|
|
417
433
|
}
|
|
418
434
|
}
|
|
419
435
|
}
|
|
420
436
|
}
|
|
421
437
|
}
|
|
438
|
+
}
|
|
422
439
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Do generalize the existing middleware execution? Or do we need to
|
|
428
|
-
// build a new middleware execution system for server routes?
|
|
429
|
-
const ctx = await executeMiddleware(middlewares, {
|
|
430
|
-
request: opts.request,
|
|
431
|
-
context: {},
|
|
432
|
-
params: serverTreeResult.routeParams,
|
|
433
|
-
pathname,
|
|
434
|
-
})
|
|
440
|
+
// eventually, execute the router
|
|
441
|
+
middlewares.push(
|
|
442
|
+
handlerToMiddleware((ctx) => executeRouter({ serverContext: ctx.context })),
|
|
443
|
+
)
|
|
435
444
|
|
|
436
|
-
|
|
437
|
-
|
|
445
|
+
const ctx = await executeMiddleware(middlewares, {
|
|
446
|
+
request,
|
|
447
|
+
context: {},
|
|
448
|
+
params: routeParams,
|
|
449
|
+
pathname,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const response: Response = ctx.response
|
|
438
453
|
|
|
439
|
-
|
|
440
|
-
// the app router happens to match the same path,
|
|
441
|
-
// it can use any request middleware from server routes
|
|
442
|
-
return [matchedRoutes, response] as const
|
|
454
|
+
return response
|
|
443
455
|
}
|
|
444
456
|
|
|
457
|
+
function throwRouteHandlerError() {
|
|
458
|
+
if (process.env.NODE_ENV === 'development') {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`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.`,
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
throw new Error('Internal Server Error')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function throwIfMayNotDefer() {
|
|
467
|
+
if (process.env.NODE_ENV === 'development') {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`You cannot defer to the app router if there is no component defined on this route.`,
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
throw new Error('Internal Server Error')
|
|
473
|
+
}
|
|
445
474
|
function handlerToMiddleware(
|
|
446
|
-
handler:
|
|
447
|
-
|
|
448
|
-
any,
|
|
449
|
-
any,
|
|
450
|
-
any,
|
|
451
|
-
any
|
|
452
|
-
>,
|
|
475
|
+
handler: RouteMethodHandlerFn<any, AnyRoute, any, any, any, any>,
|
|
476
|
+
mayDefer: boolean = false,
|
|
453
477
|
) {
|
|
478
|
+
if (mayDefer) {
|
|
479
|
+
return handler as TODO
|
|
480
|
+
}
|
|
454
481
|
return async ({ next: _next, ...rest }: TODO) => {
|
|
455
|
-
const response = await handler(rest)
|
|
456
|
-
if (response) {
|
|
457
|
-
|
|
482
|
+
const response = await handler({ ...rest, next: throwIfMayNotDefer })
|
|
483
|
+
if (!response) {
|
|
484
|
+
throwRouteHandlerError()
|
|
458
485
|
}
|
|
459
|
-
return
|
|
486
|
+
return response
|
|
460
487
|
}
|
|
461
488
|
}
|
|
462
489
|
|