@tanstack/start-server-core 1.121.10 → 1.121.12

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.
@@ -10,7 +10,6 @@ import {
10
10
  isResolvedRedirect,
11
11
  joinPaths,
12
12
  processRouteTree,
13
- rootRouteId,
14
13
  trimPath,
15
14
  } from '@tanstack/router-core'
16
15
  import { getResponseHeaders, requestHandler } from './h3'
@@ -19,9 +18,18 @@ import { getStartManifest } from './router-manifest'
19
18
  import { handleServerAction } from './server-functions-handler'
20
19
  import { VIRTUAL_MODULES } from './virtual-modules'
21
20
  import { loadVirtualModule } from './loadVirtualModule'
22
- import type { AnyServerRoute, AnyServerRouteWithTypes } from './serverRoute'
21
+ import type {
22
+ AnyServerRoute,
23
+ AnyServerRouteWithTypes,
24
+ ServerRouteMethodHandlerFn,
25
+ } from './serverRoute'
23
26
  import type { RequestHandler } from './h3'
24
- import type { AnyRouter } from '@tanstack/router-core'
27
+ import type {
28
+ AnyRoute,
29
+ AnyRouter,
30
+ Manifest,
31
+ ProcessRouteTreeResult,
32
+ } from '@tanstack/router-core'
25
33
  import type { HandlerCallback } from './handlerCallback'
26
34
 
27
35
  type TODO = any
@@ -30,8 +38,8 @@ export type CustomizeStartHandler<TRouter extends AnyRouter> = (
30
38
  cb: HandlerCallback<TRouter>,
31
39
  ) => RequestHandler
32
40
 
33
- export function getStartResponseHeaders(opts: { router: AnyRouter }) {
34
- let headers = mergeHeaders(
41
+ function getStartResponseHeaders(opts: { router: AnyRouter }) {
42
+ const headers = mergeHeaders(
35
43
  getResponseHeaders(),
36
44
  {
37
45
  'Content-Type': 'text/html; charset=UTF-8',
@@ -40,12 +48,6 @@ export function getStartResponseHeaders(opts: { router: AnyRouter }) {
40
48
  return match.headers
41
49
  }),
42
50
  )
43
- // Handle Redirects
44
- const { redirect } = opts.router.state
45
-
46
- if (redirect) {
47
- headers = mergeHeaders(headers, redirect.headers)
48
- }
49
51
  return headers
50
52
  }
51
53
 
@@ -54,7 +56,14 @@ export function createStartHandler<TRouter extends AnyRouter>({
54
56
  }: {
55
57
  createRouter: () => TRouter
56
58
  }): CustomizeStartHandler<TRouter> {
57
- let serverRouteTree: AnyServerRoute | undefined | null = null
59
+ let routeTreeModule: {
60
+ serverRouteTree: AnyServerRoute | undefined
61
+ routeTree: AnyRoute | undefined
62
+ } | null = null
63
+ let startRoutesManifest: Manifest | null = null
64
+ let processedServerRouteTree:
65
+ | ProcessRouteTreeResult<AnyServerRouteWithTypes>
66
+ | undefined = undefined
58
67
 
59
68
  return (cb) => {
60
69
  const originalFetch = globalThis.fetch
@@ -101,23 +110,16 @@ export function createStartHandler<TRouter extends AnyRouter>({
101
110
  const url = new URL(request.url)
102
111
  const href = url.href.replace(url.origin, '')
103
112
 
113
+ const APP_BASE = process.env.TSS_APP_BASE || '/'
114
+
115
+ // TODO how does this work with base path? does the router need to be configured the same as APP_BASE?
116
+ const router = createRouter()
104
117
  // Create a history for the client-side router
105
118
  const history = createMemoryHistory({
106
119
  initialEntries: [href],
107
120
  })
108
121
 
109
- const APP_BASE = process.env.TSS_APP_BASE || '/'
110
-
111
- // TODO do not create a router instance before we need it
112
- // Create the client-side router
113
- const router = createRouter()
114
-
115
- // TODO only build startRoutesManifest once, not per request
116
- // Attach the server-side SSR utils to the client-side router
117
- const startRoutesManifest = await getStartManifest({ basePath: APP_BASE })
118
- attachRouterServerSsrUtils(router, startRoutesManifest)
119
-
120
- // Update the client-side router with the history and context
122
+ // Update the client-side router with the history
121
123
  router.update({
122
124
  history,
123
125
  })
@@ -141,67 +143,96 @@ export function createStartHandler<TRouter extends AnyRouter>({
141
143
  return await handleServerAction({ request })
142
144
  }
143
145
 
144
- if (serverRouteTree === null) {
146
+ if (routeTreeModule === null) {
145
147
  try {
146
- serverRouteTree = (
147
- await loadVirtualModule(VIRTUAL_MODULES.routeTree)
148
- ).serverRouteTree
148
+ routeTreeModule = await loadVirtualModule(
149
+ VIRTUAL_MODULES.routeTree,
150
+ )
151
+ if (routeTreeModule.serverRouteTree) {
152
+ processedServerRouteTree =
153
+ processRouteTree<AnyServerRouteWithTypes>({
154
+ routeTree: routeTreeModule.serverRouteTree,
155
+ initRoute: (route, i) => {
156
+ route.init({
157
+ originalIndex: i,
158
+ })
159
+ },
160
+ })
161
+ }
149
162
  } catch (e) {
150
163
  console.log(e)
151
164
  }
152
165
  }
153
166
 
154
- // If we have a server route tree, then we try matching to see if we have a
155
- // server route that matches the request.
156
- if (serverRouteTree) {
157
- const [_matchedRoutes, response] = await handleServerRoutes({
158
- routeTree: serverRouteTree,
159
- request,
160
- basePath: APP_BASE,
161
- })
167
+ async function executeRouter() {
168
+ const requestAcceptHeader = request.headers.get('Accept') || '*/*'
169
+ const splitRequestAcceptHeader = requestAcceptHeader.split(',')
162
170
 
163
- if (response) return response
164
- }
171
+ const supportedMimeTypes = ['*/*', 'text/html']
172
+ const isRouterAcceptSupported = supportedMimeTypes.some(
173
+ (mimeType) =>
174
+ splitRequestAcceptHeader.some((acceptedMimeType) =>
175
+ acceptedMimeType.trim().startsWith(mimeType),
176
+ ),
177
+ )
165
178
 
166
- const requestAcceptHeader = request.headers.get('Accept') || '*/*'
167
- const splitRequestAcceptHeader = requestAcceptHeader.split(',')
179
+ if (!isRouterAcceptSupported) {
180
+ return json(
181
+ {
182
+ error: 'Only HTML requests are supported here',
183
+ },
184
+ {
185
+ status: 500,
186
+ },
187
+ )
188
+ }
168
189
 
169
- const supportedMimeTypes = ['*/*', 'text/html']
170
- const isRouterAcceptSupported = supportedMimeTypes.some((mimeType) =>
171
- splitRequestAcceptHeader.some((acceptedMimeType) =>
172
- acceptedMimeType.trim().startsWith(mimeType),
173
- ),
174
- )
190
+ // if the startRoutesManifest is not loaded yet, load it once
191
+ if (startRoutesManifest === null) {
192
+ startRoutesManifest = await getStartManifest({
193
+ basePath: APP_BASE,
194
+ })
195
+ }
175
196
 
176
- if (!isRouterAcceptSupported) {
177
- return json(
178
- {
179
- error: 'Only HTML requests are supported here',
180
- },
181
- {
182
- status: 500,
183
- },
184
- )
185
- }
197
+ // Attach the server-side SSR utils to the client-side router
198
+ attachRouterServerSsrUtils(router, startRoutesManifest)
186
199
 
187
- // If no Server Routes were found, so fallback to normal SSR matching using
188
- // the router
200
+ await router.load()
189
201
 
190
- await router.load()
202
+ // If there was a redirect, skip rendering the page at all
203
+ if (router.state.redirect) {
204
+ return router.state.redirect
205
+ }
191
206
 
192
- // If there was a redirect, skip rendering the page at all
193
- if (router.state.redirect) return router.state.redirect
207
+ dehydrateRouter(router)
194
208
 
195
- dehydrateRouter(router)
209
+ const responseHeaders = getStartResponseHeaders({ router })
210
+ const response = await cb({
211
+ request,
212
+ router,
213
+ responseHeaders,
214
+ })
196
215
 
197
- const responseHeaders = getStartResponseHeaders({ router })
198
- const response = await cb({
199
- request,
200
- router,
201
- responseHeaders,
202
- })
216
+ return response
217
+ }
203
218
 
204
- return response
219
+ // If we have a server route tree, then we try matching to see if we have a
220
+ // server route that matches the request.
221
+ if (processedServerRouteTree) {
222
+ const [_matchedRoutes, response] = await handleServerRoutes({
223
+ processedServerRouteTree,
224
+ router,
225
+ request,
226
+ basePath: APP_BASE,
227
+ executeRouter,
228
+ })
229
+
230
+ if (response) return response
231
+ }
232
+
233
+ // Server Routes did not produce a response, so fallback to normal SSR matching using the router
234
+ const routerResponse = await executeRouter()
235
+ return routerResponse
205
236
  } catch (err) {
206
237
  if (err instanceof Response) {
207
238
  return err
@@ -275,78 +306,97 @@ export function createStartHandler<TRouter extends AnyRouter>({
275
306
  }
276
307
  }
277
308
 
278
- async function handleServerRoutes({
279
- routeTree,
280
- request,
281
- basePath,
282
- }: {
283
- routeTree: AnyServerRouteWithTypes
309
+ async function handleServerRoutes(opts: {
310
+ router: AnyRouter
311
+ processedServerRouteTree: ProcessRouteTreeResult<AnyServerRouteWithTypes>
284
312
  request: Request
285
313
  basePath: string
314
+ executeRouter: () => Promise<Response>
286
315
  }) {
287
- // TODO only process server route tree once, not per request
288
- const { flatRoutes, routesById, routesByPath } = processRouteTree({
289
- routeTree,
290
- initRoute: (route, i) => {
291
- route.init({
292
- originalIndex: i,
293
- })
294
- },
295
- })
296
-
297
- const url = new URL(request.url)
316
+ const url = new URL(opts.request.url)
298
317
  const pathname = url.pathname
299
318
 
300
- // TODO history seems not to be needed, we can just use the pathname
301
- const history = createMemoryHistory({
302
- initialEntries: [pathname],
319
+ const serverTreeResult = getMatchedRoutes<AnyServerRouteWithTypes>({
320
+ pathname,
321
+ basepath: opts.basePath,
322
+ caseSensitive: true,
323
+ routesByPath: opts.processedServerRouteTree.routesByPath,
324
+ routesById: opts.processedServerRouteTree.routesById,
325
+ flatRoutes: opts.processedServerRouteTree.flatRoutes,
303
326
  })
304
327
 
305
- const { matchedRoutes, foundRoute, routeParams } =
306
- getMatchedRoutes<AnyServerRouteWithTypes>({
307
- pathname: history.location.pathname,
308
- basepath: basePath,
309
- caseSensitive: true,
310
- routesByPath,
311
- routesById,
312
- flatRoutes,
313
- })
328
+ const routeTreeResult = opts.router.getMatchedRoutes(pathname, undefined)
314
329
 
315
330
  let response: Response | undefined
331
+ let matchedRoutes: Array<AnyServerRouteWithTypes> = []
332
+ matchedRoutes = serverTreeResult.matchedRoutes
333
+ // check if the app route tree found a match that is deeper than the server route tree
334
+ if (routeTreeResult.foundRoute) {
335
+ if (
336
+ serverTreeResult.matchedRoutes.length <
337
+ routeTreeResult.matchedRoutes.length
338
+ ) {
339
+ const closestCommon = [...routeTreeResult.matchedRoutes]
340
+ .reverse()
341
+ .find((r) => {
342
+ return opts.processedServerRouteTree.routesById[r.id] !== undefined
343
+ })
344
+ if (closestCommon) {
345
+ // walk up the tree and collect all parents
346
+ let routeId = closestCommon.id
347
+ matchedRoutes = []
348
+ do {
349
+ const route = opts.processedServerRouteTree.routesById[routeId]
350
+ if (!route) {
351
+ break
352
+ }
353
+ matchedRoutes.push(route)
354
+ routeId = route.parentRoute?.id
355
+ } while (routeId)
316
356
 
317
- if (foundRoute && foundRoute.id !== rootRouteId) {
318
- // We've found a server route that matches the request, so we can call it.
319
- // TODO: Get the input type-signature correct
320
- // TODO: Perform the middlewares?
321
- // TODO: Error handling? What happens when its `throw redirect()` vs `throw new Error()`?
322
-
323
- const method = Object.keys(foundRoute.options.methods).find(
324
- (method) => method.toLowerCase() === request.method.toLowerCase(),
325
- )
357
+ matchedRoutes.reverse()
358
+ }
359
+ }
360
+ }
326
361
 
327
- if (method) {
328
- const handler = foundRoute.options.methods[method]
362
+ if (matchedRoutes.length) {
363
+ // We've found a server route that (partially) matches the request, so we can call it.
364
+ // TODO: Error handling? What happens when its `throw redirect()` vs `throw new Error()`?
329
365
 
330
- if (handler) {
331
- const middlewares = flattenMiddlewares(
332
- matchedRoutes.flatMap((r) => r.options.middleware).filter(Boolean),
333
- ).map((d) => d.options.server)
366
+ const middlewares = flattenMiddlewares(
367
+ matchedRoutes.flatMap((r) => r.options.middleware).filter(Boolean),
368
+ ).map((d) => d.options.server)
334
369
 
335
- middlewares.push(handlerToMiddleware(handler) as TODO)
370
+ if (serverTreeResult.foundRoute?.options.methods) {
371
+ const method = Object.keys(
372
+ serverTreeResult.foundRoute.options.methods,
373
+ ).find(
374
+ (method) => method.toLowerCase() === opts.request.method.toLowerCase(),
375
+ )
336
376
 
337
- // TODO: This is starting to feel too much like a server function
338
- // Do generalize the existing middleware execution? Or do we need to
339
- // build a new middleware execution system for server routes?
340
- const ctx = await executeMiddleware(middlewares, {
341
- request,
342
- context: {},
343
- params: routeParams,
344
- pathname: history.location.pathname,
345
- })
377
+ if (method) {
378
+ const handler = serverTreeResult.foundRoute.options.methods[method]
346
379
 
347
- response = ctx.response
380
+ if (handler) {
381
+ middlewares.push(handlerToMiddleware(handler) as TODO)
382
+ }
348
383
  }
349
384
  }
385
+
386
+ // eventually, execute the router
387
+ middlewares.push(handlerToMiddleware(opts.executeRouter))
388
+
389
+ // TODO: This is starting to feel too much like a server function
390
+ // Do generalize the existing middleware execution? Or do we need to
391
+ // build a new middleware execution system for server routes?
392
+ const ctx = await executeMiddleware(middlewares, {
393
+ request: opts.request,
394
+ context: {},
395
+ params: serverTreeResult.routeParams,
396
+ pathname,
397
+ })
398
+
399
+ response = ctx.response
350
400
  }
351
401
 
352
402
  // We return the matched routes too so if
@@ -356,11 +406,21 @@ async function handleServerRoutes({
356
406
  }
357
407
 
358
408
  function handlerToMiddleware(
359
- handler: AnyServerRouteWithTypes['options']['methods'][string],
409
+ handler: ServerRouteMethodHandlerFn<
410
+ AnyServerRouteWithTypes,
411
+ any,
412
+ any,
413
+ any,
414
+ any
415
+ >,
360
416
  ) {
361
- return async ({ next: _next, ...rest }: TODO) => ({
362
- response: await handler(rest),
363
- })
417
+ return async ({ next: _next, ...rest }: TODO) => {
418
+ const response = await handler(rest)
419
+ if (response) {
420
+ return { response }
421
+ }
422
+ return _next(rest)
423
+ }
364
424
  }
365
425
 
366
426
  function executeMiddleware(middlewares: TODO, ctx: TODO) {
@@ -42,8 +42,8 @@ export interface ServerRouteOptions<
42
42
  pathname: TFullPath
43
43
  originalIndex: number
44
44
  getParentRoute?: () => TParentRoute
45
- middleware: Constrain<TMiddlewares, ReadonlyArray<AnyRequestMiddleware>>
46
- methods: Record<
45
+ middleware?: Constrain<TMiddlewares, ReadonlyArray<AnyRequestMiddleware>>
46
+ methods?: Record<
47
47
  string,
48
48
  ServerRouteMethodHandlerFn<TParentRoute, TFullPath, TMiddlewares, any, any>
49
49
  >