@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.
Files changed (38) hide show
  1. package/dist/esm/createStartHandler.d.ts +3 -6
  2. package/dist/esm/createStartHandler.js +261 -220
  3. package/dist/esm/createStartHandler.js.map +1 -1
  4. package/dist/esm/index.d.ts +1 -3
  5. package/dist/esm/index.js +0 -4
  6. package/dist/esm/index.js.map +1 -1
  7. package/dist/esm/loadVirtualModule.js +0 -2
  8. package/dist/esm/loadVirtualModule.js.map +1 -1
  9. package/dist/esm/request-handler.d.ts +19 -0
  10. package/dist/esm/request-response.d.ts +2 -2
  11. package/dist/esm/request-response.js +5 -2
  12. package/dist/esm/request-response.js.map +1 -1
  13. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +1 -1
  14. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -1
  15. package/dist/esm/server-functions-handler.d.ts +2 -1
  16. package/dist/esm/server-functions-handler.js +16 -13
  17. package/dist/esm/server-functions-handler.js.map +1 -1
  18. package/dist/esm/virtual-modules.d.ts +0 -2
  19. package/dist/esm/virtual-modules.js +0 -1
  20. package/dist/esm/virtual-modules.js.map +1 -1
  21. package/package.json +7 -7
  22. package/src/createStartHandler.ts +353 -326
  23. package/src/index.tsx +2 -42
  24. package/src/loadVirtualModule.ts +0 -2
  25. package/src/request-handler.ts +31 -0
  26. package/src/request-response.ts +8 -5
  27. package/src/serializer/ServerFunctionSerializationAdapter.ts +1 -1
  28. package/src/server-functions-handler.ts +21 -16
  29. package/src/tanstack-start.d.ts +0 -2
  30. package/src/virtual-modules.ts +0 -2
  31. package/dist/esm/serializer/getSerovalPlugins.d.ts +0 -3
  32. package/dist/esm/serializer/getSerovalPlugins.js +0 -13
  33. package/dist/esm/serializer/getSerovalPlugins.js.map +0 -1
  34. package/dist/esm/serverRoute.d.ts +0 -124
  35. package/dist/esm/serverRoute.js +0 -103
  36. package/dist/esm/serverRoute.js.map +0 -1
  37. package/src/serializer/getSerovalPlugins.ts +0 -10
  38. 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
- getMatchedRoutes,
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
- AnyServerRouteWithTypes,
27
- ServerRouteMethodHandlerFn,
28
- } from './serverRoute'
29
- import type { RequestHandler } from './request-response'
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
- ProcessRouteTreeResult,
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<TRouter extends AnyRouter>({
59
- createRouter,
60
- }: {
61
- createRouter: () => Awaitable<TRouter>
62
- }): CustomizeStartHandler<TRouter> {
63
- let routeTreeModule: {
64
- serverRouteTree: AnyServerRouteWithTypes | undefined
65
- routeTree: AnyRoute | undefined
66
- } | null = null
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 processedServerRouteTree:
69
- | ProcessRouteTreeResult<AnyServerRouteWithTypes>
70
- | undefined = undefined
71
-
72
- return (cb) => {
73
- const originalFetch = globalThis.fetch
74
-
75
- const startRequestResolver: RequestHandler = async (request) => {
76
- // Patching fetch function to use our request resolver
77
- // if the input starts with `/` which is a common pattern for
78
- // client-side routing.
79
- // When we encounter similar requests, we can assume that the
80
- // user wants to use the same origin as the current request.
81
- globalThis.fetch = async function (input, init) {
82
- function resolve(url: URL, requestOptions: RequestInit | undefined) {
83
- const fetchRequest = new Request(url, requestOptions)
84
- return startRequestResolver(fetchRequest)
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
- function getOrigin() {
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
- if (typeof input === 'string' && input.startsWith('/')) {
96
- // e.g: fetch('/api/data')
97
- const url = new URL(input, getOrigin())
98
- return resolve(url, init)
99
- } else if (
100
- typeof input === 'object' &&
101
- 'url' in input &&
102
- typeof input.url === 'string' &&
103
- input.url.startsWith('/')
104
- ) {
105
- // e.g: fetch(new Request('/api/data'))
106
- const url = new URL(input.url, getOrigin())
107
- return resolve(url, init)
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
- // If not, it should just use the original fetch
111
- return originalFetch(input, init)
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
- const url = new URL(request.url)
115
- const href = url.href.replace(url.origin, '')
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
- const APP_BASE = process.env.TSS_APP_BASE || '/'
134
+ // If not, it should just use the original fetch
135
+ return originalFetch(input, init)
136
+ }
118
137
 
119
- // TODO how does this work with base path? does the router need to be configured the same as APP_BASE?
120
- const router = await createRouter()
138
+ const url = new URL(request.url)
139
+ const href = url.href.replace(url.origin, '')
121
140
 
122
- // Create a history for the client-side router
123
- const history = createMemoryHistory({
124
- initialEntries: [href],
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
- serializationAdapters,
168
+ origin,
169
+ ...{
170
+ defaultSsr: startOptions.defaultSsr,
171
+ serializationAdapters: startOptions.serializationAdapters,
172
+ },
147
173
  })
174
+ return router
175
+ }
148
176
 
149
- const response = await runWithStartContext({ router }, async () => {
150
- try {
151
- if (!process.env.TSS_SERVER_FN_BASE) {
152
- throw new Error(
153
- 'tanstack/start-server-core: TSS_SERVER_FN_BASE must be defined in your environment for createStartHandler()',
154
- )
155
- }
156
-
157
- // First, let's attempt to handle server functions
158
- // Add trailing slash to sanitise user defined TSS_SERVER_FN_BASE
159
- const serverFnBase = joinPaths([
160
- APP_BASE,
161
- trimPath(process.env.TSS_SERVER_FN_BASE),
162
- '/',
163
- ])
164
- if (href.startsWith(serverFnBase)) {
165
- return await handleServerAction({ request })
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
- routeTreeModule = await loadVirtualModule(
171
- VIRTUAL_MODULES.routeTree,
172
- )
173
- if (routeTreeModule.serverRouteTree) {
174
- processedServerRouteTree =
175
- processRouteTree<AnyServerRouteWithTypes>({
176
- routeTree: routeTreeModule.serverRouteTree,
177
- initRoute: (route, i) => {
178
- route.init({
179
- originalIndex: i,
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
- const executeRouter = async () => {
190
- const requestAcceptHeader = request.headers.get('Accept') || '*/*'
191
- const splitRequestAcceptHeader = requestAcceptHeader.split(',')
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
- // Attach the server-side SSR utils to the client-side router
220
- attachRouterServerSsrUtils(router, startRoutesManifest)
221
-
222
- await router.load()
269
+ return response
270
+ } catch (err) {
271
+ if (err instanceof Response) {
272
+ return err
273
+ }
223
274
 
224
- // If there was a redirect, skip rendering the page at all
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
- await router.serverSsr!.dehydrate()
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
- return redirect
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 response
353
+ return redirect
325
354
  }
326
355
 
327
- return requestHandler(startRequestResolver)
356
+ return response
328
357
  }
358
+
359
+ return requestHandler(startRequestResolver)
329
360
  }
330
361
 
331
- async function handleServerRoutes(opts: {
332
- router: AnyRouter
333
- processedServerRouteTree: ProcessRouteTreeResult<AnyServerRouteWithTypes>
362
+ async function handleServerRoutes({
363
+ getRouter,
364
+ request,
365
+ executeRouter,
366
+ }: {
367
+ getRouter: () => Awaitable<AnyRouter>
334
368
  request: Request
335
- basePath: string
336
- executeRouter: () => Promise<Response>
369
+ executeRouter: ({
370
+ serverContext,
371
+ }: {
372
+ serverContext: any
373
+ }) => Promise<Response>
337
374
  }) {
338
- const url = new URL(opts.request.url)
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
- basepath: opts.basePath,
344
- caseSensitive: true,
345
- routesByPath: opts.processedServerRouteTree.routesByPath,
346
- routesById: opts.processedServerRouteTree.routesById,
347
- flatRoutes: opts.processedServerRouteTree.flatRoutes,
348
- })
381
+ undefined,
382
+ )
349
383
 
350
- const routeTreeResult = opts.router.getMatchedRoutes(pathname, undefined)
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
- matchedRoutes.reverse()
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
- if (matchedRoutes.length) {
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
- const middlewares = flattenMiddlewares(
389
- matchedRoutes.flatMap((r) => r.options.middleware).filter(Boolean),
390
- ).map((d) => d.options.server)
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
- if (serverTreeResult.foundRoute?.options.methods) {
393
- const method = Object.keys(
394
- serverTreeResult.foundRoute.options.methods,
395
- ).find(
396
- (method) => method.toLowerCase() === opts.request.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 = serverTreeResult.foundRoute.options.methods[method]
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) as TODO)
423
+ middlewares.push(handlerToMiddleware(handler, mayDefer))
404
424
  } else {
405
- if (
406
- handler._options.middlewares &&
407
- handler._options.middlewares.length
408
- ) {
425
+ const { middleware } = handler
426
+ if (middleware && middleware.length) {
409
427
  middlewares.push(
410
- ...flattenMiddlewares(handler._options.middlewares as any).map(
411
- (d) => d.options.server,
412
- ),
428
+ ...flattenMiddlewares(middleware).map((d) => d.options.server),
413
429
  )
414
430
  }
415
- if (handler._options.handler) {
416
- middlewares.push(handlerToMiddleware(handler._options.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
- // eventually, execute the router
424
- middlewares.push(handlerToMiddleware(opts.executeRouter))
425
-
426
- // TODO: This is starting to feel too much like a server function
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
- response = ctx.response
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
- // We return the matched routes too so if
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: ServerRouteMethodHandlerFn<
447
- AnyServerRouteWithTypes,
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
- return { response }
482
+ const response = await handler({ ...rest, next: throwIfMayNotDefer })
483
+ if (!response) {
484
+ throwRouteHandlerError()
458
485
  }
459
- return _next(rest)
486
+ return response
460
487
  }
461
488
  }
462
489