@tanstack/start-server-core 1.132.0-alpha.2 → 1.132.0-alpha.21
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/createServerRpc.d.ts +6 -0
- package/dist/esm/createServerRpc.js +27 -0
- package/dist/esm/createServerRpc.js.map +1 -0
- package/dist/esm/createStartHandler.js +30 -15
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/getServerFnById.d.ts +1 -0
- package/dist/esm/getServerFnById.js +30 -0
- package/dist/esm/getServerFnById.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.d.ts +5 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +25 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -0
- package/dist/esm/serializer/getSerovalPlugins.d.ts +3 -0
- package/dist/esm/serializer/getSerovalPlugins.js +13 -0
- package/dist/esm/serializer/getSerovalPlugins.js.map +1 -0
- package/dist/esm/server-functions-handler.js +121 -81
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/package.json +7 -6
- package/src/createServerRpc.ts +30 -0
- package/src/createStartHandler.ts +76 -61
- package/src/getServerFnById.ts +33 -0
- package/src/global.d.ts +1 -1
- package/src/index.tsx +2 -0
- package/src/serializer/ServerFunctionSerializationAdapter.ts +24 -0
- package/src/serializer/getSerovalPlugins.ts +10 -0
- package/src/server-functions-handler.ts +137 -114
package/src/index.tsx
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSerializationAdapter } from '@tanstack/router-core'
|
|
2
|
+
import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'
|
|
3
|
+
import { createServerRpc } from '../createServerRpc'
|
|
4
|
+
import { getServerFnById } from '../getServerFnById'
|
|
5
|
+
|
|
6
|
+
export const ServerFunctionSerializationAdapter = createSerializationAdapter({
|
|
7
|
+
key: '$TSS/serverfn',
|
|
8
|
+
test: (v): v is { functionId: string } => {
|
|
9
|
+
if (typeof v !== 'object' || v === null) return false
|
|
10
|
+
|
|
11
|
+
if (!(TSS_SERVER_FUNCTION in v)) return false
|
|
12
|
+
|
|
13
|
+
return !!v[TSS_SERVER_FUNCTION]
|
|
14
|
+
},
|
|
15
|
+
toSerializable: ({ functionId }) => ({ functionId }),
|
|
16
|
+
fromSerializable: ({ functionId }) => {
|
|
17
|
+
const fn = async (opts: any, signal: any): Promise<any> => {
|
|
18
|
+
const serverFn = await getServerFnById(functionId)
|
|
19
|
+
const result = await serverFn(opts ?? {}, signal)
|
|
20
|
+
return result.result
|
|
21
|
+
}
|
|
22
|
+
return createServerRpc(functionId, fn)
|
|
23
|
+
},
|
|
24
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { makeSerovalPlugin } from '@tanstack/router-core'
|
|
2
|
+
import { getDefaultSerovalPlugins } from '@tanstack/start-client-core'
|
|
3
|
+
import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
|
|
4
|
+
|
|
5
|
+
export function getSerovalPlugins() {
|
|
6
|
+
return [
|
|
7
|
+
...getDefaultSerovalPlugins(),
|
|
8
|
+
makeSerovalPlugin(ServerFunctionSerializationAdapter),
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { isNotFound } from '@tanstack/router-core'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
TSS_FORMDATA_CONTEXT,
|
|
5
|
+
X_TSS_SERIALIZED,
|
|
6
|
+
} from '@tanstack/start-client-core'
|
|
7
|
+
import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
|
|
6
8
|
import { getResponse } from './request-response'
|
|
9
|
+
import { getServerFnById } from './getServerFnById'
|
|
10
|
+
import { getSerovalPlugins } from './serializer/getSerovalPlugins'
|
|
7
11
|
|
|
8
12
|
function sanitizeBase(base: string | undefined) {
|
|
9
13
|
if (!base) {
|
|
@@ -15,72 +19,6 @@ function sanitizeBase(base: string | undefined) {
|
|
|
15
19
|
return base.replace(/^\/|\/$/g, '')
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
async function revive(root: any, reviver?: (key: string, value: any) => any) {
|
|
19
|
-
async function reviveNode(holder: any, key: string) {
|
|
20
|
-
const value = holder[key]
|
|
21
|
-
|
|
22
|
-
if (value && typeof value === 'object') {
|
|
23
|
-
await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (reviver) {
|
|
27
|
-
holder[key] = await reviver(key, holder[key])
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const holder = { '': root }
|
|
32
|
-
await reviveNode(holder, '')
|
|
33
|
-
return holder['']
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function reviveServerFns(key: string, value: any) {
|
|
37
|
-
if (value && value.__serverFn === true && value.functionId) {
|
|
38
|
-
const serverFn = await getServerFnById(value.functionId)
|
|
39
|
-
return async (opts: any, signal: any): Promise<any> => {
|
|
40
|
-
const result = await serverFn(opts ?? {}, signal)
|
|
41
|
-
return result.result
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return value
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function getServerFnById(serverFnId: string) {
|
|
48
|
-
const { default: serverFnManifest } = await loadVirtualModule(
|
|
49
|
-
VIRTUAL_MODULES.serverFnManifest,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const serverFnInfo = serverFnManifest[serverFnId]
|
|
53
|
-
|
|
54
|
-
if (!serverFnInfo) {
|
|
55
|
-
console.info('serverFnManifest', serverFnManifest)
|
|
56
|
-
throw new Error('Server function info not found for ' + serverFnId)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const fnModule = await serverFnInfo.importer()
|
|
60
|
-
|
|
61
|
-
if (!fnModule) {
|
|
62
|
-
console.info('serverFnInfo', serverFnInfo)
|
|
63
|
-
throw new Error('Server function module not resolved for ' + serverFnId)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const action = fnModule[serverFnInfo.functionName]
|
|
67
|
-
|
|
68
|
-
if (!action) {
|
|
69
|
-
console.info('serverFnInfo', serverFnInfo)
|
|
70
|
-
console.info('fnModule', fnModule)
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Server function module export not resolved for serverFn ID: ${serverFnId}`,
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
return action
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function parsePayload(payload: any) {
|
|
79
|
-
const parsedPayload = startSerializer.parse(payload)
|
|
80
|
-
await revive(parsedPayload, reviveServerFns)
|
|
81
|
-
return parsedPayload
|
|
82
|
-
}
|
|
83
|
-
|
|
84
22
|
export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
85
23
|
const controller = new AbortController()
|
|
86
24
|
const signal = controller.signal
|
|
@@ -104,7 +42,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
104
42
|
}
|
|
105
43
|
|
|
106
44
|
const isCreateServerFn = 'createServerFn' in search
|
|
107
|
-
const isRaw = 'raw' in search
|
|
108
45
|
|
|
109
46
|
if (typeof serverFnId !== 'string') {
|
|
110
47
|
throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
|
|
@@ -118,14 +55,21 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
118
55
|
'application/x-www-form-urlencoded',
|
|
119
56
|
]
|
|
120
57
|
|
|
58
|
+
const contentType = request.headers.get('Content-Type')
|
|
59
|
+
const serovalPlugins = getSerovalPlugins()
|
|
60
|
+
|
|
61
|
+
function parsePayload(payload: any) {
|
|
62
|
+
const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })
|
|
63
|
+
return parsedPayload
|
|
64
|
+
}
|
|
65
|
+
|
|
121
66
|
const response = await (async () => {
|
|
122
67
|
try {
|
|
123
68
|
let result = await (async () => {
|
|
124
69
|
// FormData
|
|
125
70
|
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
request.headers.get('Content-Type')?.includes(type),
|
|
71
|
+
formDataContentTypes.some(
|
|
72
|
+
(type) => contentType && contentType.includes(type),
|
|
129
73
|
)
|
|
130
74
|
) {
|
|
131
75
|
// We don't support GET requests with FormData payloads... that seems impossible
|
|
@@ -133,45 +77,59 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
133
77
|
method.toLowerCase() !== 'get',
|
|
134
78
|
'GET requests with FormData payloads are not supported',
|
|
135
79
|
)
|
|
80
|
+
const formData = await request.formData()
|
|
81
|
+
const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)
|
|
82
|
+
formData.delete(TSS_FORMDATA_CONTEXT)
|
|
83
|
+
|
|
84
|
+
const params = {
|
|
85
|
+
context: {} as any,
|
|
86
|
+
data: formData,
|
|
87
|
+
}
|
|
88
|
+
if (typeof serializedContext === 'string') {
|
|
89
|
+
try {
|
|
90
|
+
params.context = parsePayload(JSON.parse(serializedContext))
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
136
93
|
|
|
137
|
-
return await action(
|
|
94
|
+
return await action(params, signal)
|
|
138
95
|
}
|
|
139
96
|
|
|
140
97
|
// Get requests use the query string
|
|
141
98
|
if (method.toLowerCase() === 'get') {
|
|
99
|
+
invariant(
|
|
100
|
+
isCreateServerFn,
|
|
101
|
+
'expected GET request to originate from createServerFn',
|
|
102
|
+
)
|
|
142
103
|
// By default the payload is the search params
|
|
143
|
-
let payload: any = search
|
|
144
|
-
|
|
145
|
-
// If this GET request was created by createServerFn,
|
|
146
|
-
// then the payload will be on the payload param
|
|
147
|
-
if (isCreateServerFn) {
|
|
148
|
-
payload = search.payload
|
|
149
|
-
}
|
|
150
|
-
|
|
104
|
+
let payload: any = search.payload
|
|
151
105
|
// If there's a payload, we should try to parse it
|
|
152
|
-
payload = payload ? await parsePayload(payload) : payload
|
|
106
|
+
payload = payload ? await parsePayload(JSON.parse(payload)) : payload
|
|
153
107
|
|
|
154
108
|
// Send it through!
|
|
155
109
|
return await action(payload, signal)
|
|
156
110
|
}
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
|
|
112
|
+
if (method.toLowerCase() !== 'post') {
|
|
113
|
+
throw new Error('expected POST method')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
117
|
+
throw new Error('expected application/json content type')
|
|
118
|
+
}
|
|
160
119
|
|
|
161
|
-
|
|
162
|
-
// as JSON, but we'll just pass it through for now.
|
|
163
|
-
const payload = await parsePayload(jsonPayloadAsString)
|
|
120
|
+
const jsonPayload = await request.json()
|
|
164
121
|
|
|
165
122
|
// If this POST request was created by createServerFn,
|
|
166
|
-
//
|
|
123
|
+
// its payload will be the only argument
|
|
167
124
|
if (isCreateServerFn) {
|
|
125
|
+
const payload = await parsePayload(jsonPayload)
|
|
168
126
|
return await action(payload, signal)
|
|
169
127
|
}
|
|
170
128
|
|
|
171
129
|
// Otherwise, we'll spread the payload. Need to
|
|
172
130
|
// support `use server` functions that take multiple
|
|
173
131
|
// arguments.
|
|
174
|
-
return await action(...
|
|
132
|
+
return await action(...jsonPayload)
|
|
175
133
|
})()
|
|
176
134
|
|
|
177
135
|
// Any time we get a Response back, we should just
|
|
@@ -192,18 +150,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
192
150
|
}
|
|
193
151
|
}
|
|
194
152
|
|
|
195
|
-
// if (!search.createServerFn) {
|
|
196
|
-
// result = result.result
|
|
197
|
-
// }
|
|
198
|
-
|
|
199
|
-
// else if (
|
|
200
|
-
// isPlainObject(result) &&
|
|
201
|
-
// 'result' in result &&
|
|
202
|
-
// result.result instanceof Response
|
|
203
|
-
// ) {
|
|
204
|
-
// return result.result
|
|
205
|
-
// }
|
|
206
|
-
|
|
207
153
|
// TODO: RSCs Where are we getting this package?
|
|
208
154
|
// if (isValidElement(result)) {
|
|
209
155
|
// const { renderToPipeableStream } = await import(
|
|
@@ -228,16 +174,86 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
228
174
|
}
|
|
229
175
|
|
|
230
176
|
const response = getResponse()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
177
|
+
let nonStreamingBody: any = undefined
|
|
178
|
+
|
|
179
|
+
if (result !== undefined) {
|
|
180
|
+
// first run without the stream in case `result` does not need streaming
|
|
181
|
+
let done = false as boolean
|
|
182
|
+
const callbacks: {
|
|
183
|
+
onParse: (value: any) => void
|
|
184
|
+
onDone: () => void
|
|
185
|
+
onError: (error: any) => void
|
|
186
|
+
} = {
|
|
187
|
+
onParse: (value) => {
|
|
188
|
+
nonStreamingBody = value
|
|
189
|
+
},
|
|
190
|
+
onDone: () => {
|
|
191
|
+
done = true
|
|
192
|
+
},
|
|
193
|
+
onError: (error) => {
|
|
194
|
+
throw error
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
toCrossJSONStream(result, {
|
|
198
|
+
refs: new Map(),
|
|
199
|
+
plugins: serovalPlugins,
|
|
200
|
+
onParse(value) {
|
|
201
|
+
callbacks.onParse(value)
|
|
202
|
+
},
|
|
203
|
+
onDone() {
|
|
204
|
+
callbacks.onDone()
|
|
205
|
+
},
|
|
206
|
+
onError: (error) => {
|
|
207
|
+
callbacks.onError(error)
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
if (done) {
|
|
211
|
+
return new Response(
|
|
212
|
+
nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
|
|
213
|
+
{
|
|
214
|
+
status: response?.status,
|
|
215
|
+
statusText: response?.statusText,
|
|
216
|
+
headers: {
|
|
217
|
+
'Content-Type': 'application/json',
|
|
218
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// not done yet, we need to stream
|
|
225
|
+
const stream = new ReadableStream({
|
|
226
|
+
start(controller) {
|
|
227
|
+
callbacks.onParse = (value) =>
|
|
228
|
+
controller.enqueue(JSON.stringify(value) + '\n')
|
|
229
|
+
callbacks.onDone = () => {
|
|
230
|
+
try {
|
|
231
|
+
controller.close()
|
|
232
|
+
} catch (error) {
|
|
233
|
+
controller.error(error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
callbacks.onError = (error) => controller.error(error)
|
|
237
|
+
// stream the initial body
|
|
238
|
+
if (nonStreamingBody !== undefined) {
|
|
239
|
+
callbacks.onParse(nonStreamingBody)
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
return new Response(stream, {
|
|
234
244
|
status: response?.status,
|
|
235
245
|
statusText: response?.statusText,
|
|
236
246
|
headers: {
|
|
237
|
-
'Content-Type': 'application/
|
|
247
|
+
'Content-Type': 'application/x-ndjson',
|
|
248
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
238
249
|
},
|
|
239
|
-
}
|
|
240
|
-
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return new Response(undefined, {
|
|
254
|
+
status: response?.status,
|
|
255
|
+
statusText: response?.statusText,
|
|
256
|
+
})
|
|
241
257
|
} catch (error: any) {
|
|
242
258
|
if (error instanceof Response) {
|
|
243
259
|
return error
|
|
@@ -265,10 +281,21 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
265
281
|
console.error(error)
|
|
266
282
|
console.info()
|
|
267
283
|
|
|
268
|
-
|
|
269
|
-
|
|
284
|
+
const serializedError = JSON.stringify(
|
|
285
|
+
await Promise.resolve(
|
|
286
|
+
toCrossJSONAsync(error, {
|
|
287
|
+
refs: new Map(),
|
|
288
|
+
plugins: serovalPlugins,
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
const response = getResponse()
|
|
293
|
+
return new Response(serializedError, {
|
|
294
|
+
status: response?.status ?? 500,
|
|
295
|
+
statusText: response?.statusText,
|
|
270
296
|
headers: {
|
|
271
297
|
'Content-Type': 'application/json',
|
|
298
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
272
299
|
},
|
|
273
300
|
})
|
|
274
301
|
}
|
|
@@ -276,10 +303,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
276
303
|
|
|
277
304
|
request.signal.removeEventListener('abort', abort)
|
|
278
305
|
|
|
279
|
-
if (isRaw) {
|
|
280
|
-
return response
|
|
281
|
-
}
|
|
282
|
-
|
|
283
306
|
return response
|
|
284
307
|
}
|
|
285
308
|
|