@tanstack/start-server-core 1.120.7 → 1.121.0-alpha.11

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 (60) hide show
  1. package/README.md +6 -27
  2. package/dist/cjs/createRequestHandler.cjs +1 -3
  3. package/dist/cjs/createRequestHandler.cjs.map +1 -1
  4. package/dist/cjs/createStartHandler.cjs +268 -31
  5. package/dist/cjs/createStartHandler.cjs.map +1 -1
  6. package/dist/cjs/createStartHandler.d.cts +8 -6
  7. package/dist/cjs/h3.cjs +30 -73
  8. package/dist/cjs/h3.cjs.map +1 -1
  9. package/dist/cjs/h3.d.cts +11 -7
  10. package/dist/cjs/handlerCallback.cjs.map +1 -1
  11. package/dist/cjs/handlerCallback.d.cts +3 -4
  12. package/dist/cjs/index.cjs +17 -14
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/index.d.cts +6 -1
  15. package/dist/cjs/router-manifest.cjs +44 -0
  16. package/dist/cjs/router-manifest.cjs.map +1 -0
  17. package/dist/cjs/router-manifest.d.cts +17 -0
  18. package/dist/cjs/server-functions-handler.cjs +145 -0
  19. package/dist/cjs/server-functions-handler.cjs.map +1 -0
  20. package/dist/cjs/server-functions-handler.d.cts +3 -0
  21. package/dist/cjs/serverRoute.cjs +100 -0
  22. package/dist/cjs/serverRoute.cjs.map +1 -0
  23. package/dist/cjs/serverRoute.d.cts +115 -0
  24. package/dist/cjs/ssr-server.cjs +3 -2
  25. package/dist/cjs/ssr-server.cjs.map +1 -1
  26. package/dist/esm/createRequestHandler.js +1 -3
  27. package/dist/esm/createRequestHandler.js.map +1 -1
  28. package/dist/esm/createStartHandler.d.ts +8 -6
  29. package/dist/esm/createStartHandler.js +248 -33
  30. package/dist/esm/createStartHandler.js.map +1 -1
  31. package/dist/esm/h3.d.ts +11 -7
  32. package/dist/esm/h3.js +26 -63
  33. package/dist/esm/h3.js.map +1 -1
  34. package/dist/esm/handlerCallback.d.ts +3 -4
  35. package/dist/esm/handlerCallback.js.map +1 -1
  36. package/dist/esm/index.d.ts +6 -1
  37. package/dist/esm/index.js +14 -5
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/router-manifest.d.ts +17 -0
  40. package/dist/esm/router-manifest.js +44 -0
  41. package/dist/esm/router-manifest.js.map +1 -0
  42. package/dist/esm/server-functions-handler.d.ts +3 -0
  43. package/dist/esm/server-functions-handler.js +145 -0
  44. package/dist/esm/server-functions-handler.js.map +1 -0
  45. package/dist/esm/serverRoute.d.ts +115 -0
  46. package/dist/esm/serverRoute.js +100 -0
  47. package/dist/esm/serverRoute.js.map +1 -0
  48. package/dist/esm/ssr-server.js +3 -2
  49. package/dist/esm/ssr-server.js.map +1 -1
  50. package/package.json +8 -6
  51. package/src/createRequestHandler.ts +1 -3
  52. package/src/createStartHandler.ts +373 -47
  53. package/src/h3.ts +71 -78
  54. package/src/handlerCallback.ts +5 -12
  55. package/src/index.tsx +11 -1
  56. package/src/router-manifest.ts +79 -0
  57. package/src/server-functions-handler.ts +260 -0
  58. package/src/serverRoute.ts +661 -0
  59. package/src/ssr-server.ts +2 -0
  60. package/src/tanstack-start.d.ts +5 -0
@@ -1,22 +1,15 @@
1
- import type { EventHandlerResponse } from 'h3'
2
1
  import type { AnyRouter } from '@tanstack/router-core'
3
2
 
4
- export interface HandlerCallback<
5
- TRouter extends AnyRouter,
6
- TResponse extends EventHandlerResponse = EventHandlerResponse,
7
- > {
3
+ export interface HandlerCallback<TRouter extends AnyRouter> {
8
4
  (ctx: {
9
5
  request: Request
10
6
  router: TRouter
11
7
  responseHeaders: Headers
12
- }): TResponse
8
+ }): Response | Promise<Response>
13
9
  }
14
10
 
15
- export function defineHandlerCallback<
16
- TRouter extends AnyRouter,
17
- TResponse = EventHandlerResponse,
18
- >(
19
- handler: HandlerCallback<TRouter, TResponse>,
20
- ): HandlerCallback<TRouter, TResponse> {
11
+ export function defineHandlerCallback<TRouter extends AnyRouter>(
12
+ handler: HandlerCallback<TRouter>,
13
+ ): HandlerCallback<TRouter> {
21
14
  return handler
22
15
  }
package/src/index.tsx CHANGED
@@ -3,10 +3,20 @@ export {
3
3
  transformPipeableStreamWithRouter,
4
4
  } from './transformStreamWithRouter'
5
5
 
6
- export { createStartHandler } from './createStartHandler'
6
+ export {
7
+ getStartResponseHeaders,
8
+ createStartHandler,
9
+ } from './createStartHandler'
10
+ export type { CustomizeStartHandler } from './createStartHandler'
7
11
  export { createRequestHandler } from './createRequestHandler'
8
12
 
9
13
  export { defineHandlerCallback } from './handlerCallback'
10
14
  export type { HandlerCallback } from './handlerCallback'
11
15
 
16
+ export { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server'
17
+ export { handleServerAction } from './server-functions-handler'
18
+
12
19
  export * from './h3'
20
+
21
+ export { createServerRoute, createServerFileRoute } from './serverRoute'
22
+ export type { CreateServerFileRoute } from './serverRoute'
@@ -0,0 +1,79 @@
1
+ import { tsrStartManifest } from 'tanstack-start-router-manifest:v'
2
+ import { rootRouteId } from '@tanstack/router-core'
3
+
4
+ declare global {
5
+ // eslint-disable-next-line no-var
6
+ var TSS_INJECTED_HEAD_SCRIPTS: string | undefined
7
+ }
8
+
9
+ /**
10
+ * @description Returns the router manifest that should be sent to the client.
11
+ * This includes only the assets and preloads for the current route and any
12
+ * special assets that are needed for the client. It does not include relationships
13
+ * between routes or any other data that is not needed for the client.
14
+ */
15
+ export function getStartManifest() {
16
+ const startManifest = tsrStartManifest()
17
+
18
+ const rootRoute = (startManifest.routes[rootRouteId] =
19
+ startManifest.routes[rootRouteId] || {})
20
+
21
+ rootRoute.assets = rootRoute.assets || []
22
+
23
+ // Get the entry for the client
24
+ // const ClientManifest = getManifest('client')
25
+
26
+ // const importPath =
27
+ // ClientManifest.inputs[ClientManifest.handler]?.output.path
28
+ // if (!importPath) {
29
+ // invariant(importPath, 'Could not find client entry in manifest')
30
+ // }
31
+
32
+ if (process.env.NODE_ENV === 'development' && !process.env.TSS_CLIENT_ENTRY) {
33
+ throw new Error(
34
+ 'tanstack/start-server-core: TSS_CLIENT_ENTRY must be defined in your environment for getStartManifest()',
35
+ )
36
+ }
37
+
38
+ if (process.env.NODE_ENV === 'development') {
39
+ // Always fake that HMR is ready
40
+ // const CLIENT_BASE = sanitizeBase(process.env.TSS_CLIENT_BASE || '')
41
+
42
+ // if (!CLIENT_BASE) {
43
+ // throw new Error(
44
+ // 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()',
45
+ // )
46
+ // }
47
+
48
+ const script = `${globalThis.TSS_INJECTED_HEAD_SCRIPTS ? globalThis.TSS_INJECTED_HEAD_SCRIPTS + '; ' : ''}import(${JSON.stringify(process.env.TSS_CLIENT_ENTRY)})`
49
+
50
+ rootRoute.assets.push({
51
+ tag: 'script',
52
+ attrs: {
53
+ type: 'module',
54
+ suppressHydrationWarning: true,
55
+ async: true,
56
+ },
57
+ children: script,
58
+ })
59
+ }
60
+
61
+ const manifest = {
62
+ ...startManifest,
63
+ routes: Object.fromEntries(
64
+ Object.entries(startManifest.routes).map(([k, v]) => {
65
+ const { preloads, assets } = v
66
+ return [
67
+ k,
68
+ {
69
+ preloads,
70
+ assets,
71
+ },
72
+ ]
73
+ }),
74
+ ),
75
+ }
76
+
77
+ // Strip out anything that isn't needed for the client
78
+ return manifest
79
+ }
@@ -0,0 +1,260 @@
1
+ import { isNotFound } from '@tanstack/router-core'
2
+ import invariant from 'tiny-invariant'
3
+ import { startSerializer } from '@tanstack/start-client-core'
4
+ // @ts-expect-error
5
+ import _serverFnManifest from 'tanstack-start-server-fn-manifest:v'
6
+ import { getEvent, getResponseStatus } from './h3'
7
+
8
+ const serverFnManifest = _serverFnManifest as Record<
9
+ string,
10
+ {
11
+ functionName: string
12
+ extractedFilename: string
13
+ importer: () => Promise<any>
14
+ }
15
+ >
16
+
17
+ function sanitizeBase(base: string | undefined) {
18
+ if (!base) {
19
+ throw new Error(
20
+ '🚨 process.env.TSS_SERVER_FN_BASE is required in start/server-handler/index',
21
+ )
22
+ }
23
+
24
+ return base.replace(/^\/|\/$/g, '')
25
+ }
26
+
27
+ export const handleServerAction = async ({ request }: { request: Request }) => {
28
+ const controller = new AbortController()
29
+ const signal = controller.signal
30
+ const abort = () => controller.abort()
31
+ request.signal.addEventListener('abort', abort)
32
+
33
+ const method = request.method
34
+ const url = new URL(request.url, 'http://localhost:3000')
35
+ // extract the serverFnId from the url as host/_serverFn/:serverFnId
36
+ // Define a regex to match the path and extract the :thing part
37
+ const regex = new RegExp(
38
+ `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,
39
+ )
40
+
41
+ // Execute the regex
42
+ const match = url.pathname.match(regex)
43
+ const serverFnId = match ? match[1] : null
44
+ const search = Object.fromEntries(url.searchParams.entries()) as {
45
+ payload?: any
46
+ createServerFn?: boolean
47
+ }
48
+
49
+ const isCreateServerFn = 'createServerFn' in search
50
+ const isRaw = 'raw' in search
51
+
52
+ if (typeof serverFnId !== 'string') {
53
+ throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
54
+ }
55
+
56
+ const serverFnInfo = serverFnManifest[serverFnId]
57
+
58
+ if (!serverFnInfo) {
59
+ console.info('serverFnManifest', serverFnManifest)
60
+ throw new Error('Server function info not found for ' + serverFnId)
61
+ }
62
+
63
+ const fnModule: undefined | { [key: string]: any } =
64
+ await serverFnInfo.importer()
65
+
66
+ if (!fnModule) {
67
+ console.info('serverFnInfo', serverFnInfo)
68
+ throw new Error('Server function module not resolved for ' + serverFnId)
69
+ }
70
+
71
+ const action = fnModule[serverFnInfo.functionName]
72
+
73
+ if (!action) {
74
+ console.info('serverFnInfo', serverFnInfo)
75
+ console.info('fnModule', fnModule)
76
+ throw new Error(
77
+ `Server function module export not resolved for serverFn ID: ${serverFnId}`,
78
+ )
79
+ }
80
+
81
+ // Known FormData 'Content-Type' header values
82
+ const formDataContentTypes = [
83
+ 'multipart/form-data',
84
+ 'application/x-www-form-urlencoded',
85
+ ]
86
+
87
+ const response = await (async () => {
88
+ try {
89
+ let result = await (async () => {
90
+ // FormData
91
+ if (
92
+ request.headers.get('Content-Type') &&
93
+ formDataContentTypes.some((type) =>
94
+ request.headers.get('Content-Type')?.includes(type),
95
+ )
96
+ ) {
97
+ // We don't support GET requests with FormData payloads... that seems impossible
98
+ invariant(
99
+ method.toLowerCase() !== 'get',
100
+ 'GET requests with FormData payloads are not supported',
101
+ )
102
+
103
+ return await action(await request.formData(), signal)
104
+ }
105
+
106
+ // Get requests use the query string
107
+ if (method.toLowerCase() === 'get') {
108
+ // By default the payload is the search params
109
+ let payload: any = search
110
+
111
+ // If this GET request was created by createServerFn,
112
+ // then the payload will be on the payload param
113
+ if (isCreateServerFn) {
114
+ payload = search.payload
115
+ }
116
+
117
+ // If there's a payload, we should try to parse it
118
+ payload = payload ? startSerializer.parse(payload) : payload
119
+
120
+ // Send it through!
121
+ return await action(payload, signal)
122
+ }
123
+
124
+ // This must be a POST request, likely JSON???
125
+ const jsonPayloadAsString = await request.text()
126
+
127
+ // We should probably try to deserialize the payload
128
+ // as JSON, but we'll just pass it through for now.
129
+ const payload = startSerializer.parse(jsonPayloadAsString)
130
+
131
+ // If this POST request was created by createServerFn,
132
+ // it's payload will be the only argument
133
+ if (isCreateServerFn) {
134
+ return await action(payload, signal)
135
+ }
136
+
137
+ // Otherwise, we'll spread the payload. Need to
138
+ // support `use server` functions that take multiple
139
+ // arguments.
140
+ return await action(...(payload as any), signal)
141
+ })()
142
+
143
+ // Any time we get a Response back, we should just
144
+ // return it immediately.
145
+ if (result.result instanceof Response) {
146
+ return result.result
147
+ }
148
+
149
+ // If this is a non createServerFn request, we need to
150
+ // pull out the result from the result object
151
+ if (!isCreateServerFn) {
152
+ result = result.result
153
+
154
+ // The result might again be a response,
155
+ // and if it is, return it.
156
+ if (result instanceof Response) {
157
+ return result
158
+ }
159
+ }
160
+
161
+ // if (!search.createServerFn) {
162
+ // result = result.result
163
+ // }
164
+
165
+ // else if (
166
+ // isPlainObject(result) &&
167
+ // 'result' in result &&
168
+ // result.result instanceof Response
169
+ // ) {
170
+ // return result.result
171
+ // }
172
+
173
+ // TODO: RSCs Where are we getting this package?
174
+ // if (isValidElement(result)) {
175
+ // const { renderToPipeableStream } = await import(
176
+ // // @ts-expect-error
177
+ // 'react-server-dom/server'
178
+ // )
179
+
180
+ // const pipeableStream = renderToPipeableStream(result)
181
+
182
+ // setHeaders(event, {
183
+ // 'Content-Type': 'text/x-component',
184
+ // } as any)
185
+
186
+ // sendStream(event, response)
187
+ // event._handled = true
188
+
189
+ // return new Response(null, { status: 200 })
190
+ // }
191
+
192
+ if (isNotFound(result)) {
193
+ return isNotFoundResponse(result)
194
+ }
195
+
196
+ return new Response(
197
+ result !== undefined ? startSerializer.stringify(result) : undefined,
198
+ {
199
+ status: getResponseStatus(getEvent()),
200
+ headers: {
201
+ 'Content-Type': 'application/json',
202
+ },
203
+ },
204
+ )
205
+ } catch (error: any) {
206
+ if (error instanceof Response) {
207
+ return error
208
+ }
209
+ // else if (
210
+ // isPlainObject(error) &&
211
+ // 'result' in error &&
212
+ // error.result instanceof Response
213
+ // ) {
214
+ // return error.result
215
+ // }
216
+
217
+ // Currently this server-side context has no idea how to
218
+ // build final URLs, so we need to defer that to the client.
219
+ // The client will check for __redirect and __notFound keys,
220
+ // and if they exist, it will handle them appropriately.
221
+
222
+ if (isNotFound(error)) {
223
+ return isNotFoundResponse(error)
224
+ }
225
+
226
+ console.info()
227
+ console.info('Server Fn Error!')
228
+ console.info()
229
+ console.error(error)
230
+ console.info()
231
+
232
+ return new Response(startSerializer.stringify(error), {
233
+ status: 500,
234
+ headers: {
235
+ 'Content-Type': 'application/json',
236
+ },
237
+ })
238
+ }
239
+ })()
240
+
241
+ request.signal.removeEventListener('abort', abort)
242
+
243
+ if (isRaw) {
244
+ return response
245
+ }
246
+
247
+ return response
248
+ }
249
+
250
+ function isNotFoundResponse(error: any) {
251
+ const { headers, ...rest } = error
252
+
253
+ return new Response(JSON.stringify(rest), {
254
+ status: 200,
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ ...(headers || {}),
258
+ },
259
+ })
260
+ }