@tanstack/start-server-core 1.120.5 → 1.121.0-alpha.3
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/README.md +6 -27
- package/dist/cjs/createRequestHandler.cjs +1 -3
- package/dist/cjs/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/createStartHandler.cjs +265 -31
- package/dist/cjs/createStartHandler.cjs.map +1 -1
- package/dist/cjs/createStartHandler.d.cts +8 -6
- package/dist/cjs/h3.cjs +30 -73
- package/dist/cjs/h3.cjs.map +1 -1
- package/dist/cjs/h3.d.cts +11 -7
- package/dist/cjs/handlerCallback.cjs.map +1 -1
- package/dist/cjs/handlerCallback.d.cts +3 -4
- package/dist/cjs/index.cjs +17 -14
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +6 -1
- package/dist/cjs/router-manifest.cjs +44 -0
- package/dist/cjs/router-manifest.cjs.map +1 -0
- package/dist/cjs/router-manifest.d.cts +17 -0
- package/dist/cjs/server-functions-handler.cjs +145 -0
- package/dist/cjs/server-functions-handler.cjs.map +1 -0
- package/dist/cjs/server-functions-handler.d.cts +3 -0
- package/dist/cjs/serverRoute.cjs +100 -0
- package/dist/cjs/serverRoute.cjs.map +1 -0
- package/dist/cjs/serverRoute.d.cts +115 -0
- package/dist/cjs/ssr-server.cjs +3 -2
- package/dist/cjs/ssr-server.cjs.map +1 -1
- package/dist/esm/createRequestHandler.js +1 -3
- package/dist/esm/createRequestHandler.js.map +1 -1
- package/dist/esm/createStartHandler.d.ts +8 -6
- package/dist/esm/createStartHandler.js +245 -33
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/h3.d.ts +11 -7
- package/dist/esm/h3.js +26 -63
- package/dist/esm/h3.js.map +1 -1
- package/dist/esm/handlerCallback.d.ts +3 -4
- package/dist/esm/handlerCallback.js.map +1 -1
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.js +14 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/router-manifest.d.ts +17 -0
- package/dist/esm/router-manifest.js +44 -0
- package/dist/esm/router-manifest.js.map +1 -0
- package/dist/esm/server-functions-handler.d.ts +3 -0
- package/dist/esm/server-functions-handler.js +145 -0
- package/dist/esm/server-functions-handler.js.map +1 -0
- package/dist/esm/serverRoute.d.ts +115 -0
- package/dist/esm/serverRoute.js +100 -0
- package/dist/esm/serverRoute.js.map +1 -0
- package/dist/esm/ssr-server.js +3 -2
- package/dist/esm/ssr-server.js.map +1 -1
- package/package.json +8 -6
- package/src/createRequestHandler.ts +1 -3
- package/src/createStartHandler.ts +373 -47
- package/src/h3.ts +71 -78
- package/src/handlerCallback.ts +5 -12
- package/src/index.tsx +11 -1
- package/src/router-manifest.ts +79 -0
- package/src/server-functions-handler.ts +260 -0
- package/src/serverRoute.ts +661 -0
- package/src/ssr-server.ts +2 -0
- package/src/tanstack-start.d.ts +5 -0
package/src/handlerCallback.ts
CHANGED
|
@@ -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
|
-
}):
|
|
8
|
+
}): Response | Promise<Response>
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
export function defineHandlerCallback<
|
|
16
|
-
TRouter
|
|
17
|
-
|
|
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 {
|
|
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-manifest'
|
|
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:server-fn-manifest'
|
|
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
|
+
}
|