@tanstack/start-client-core 1.132.0-alpha.0 → 1.132.0-alpha.10

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 (53) hide show
  1. package/dist/esm/constants.d.ts +5 -0
  2. package/dist/esm/constants.js +13 -0
  3. package/dist/esm/constants.js.map +1 -0
  4. package/dist/esm/createClientRpc.d.ts +6 -0
  5. package/dist/esm/createClientRpc.js +26 -0
  6. package/dist/esm/createClientRpc.js.map +1 -0
  7. package/dist/esm/createMiddleware.d.ts +35 -40
  8. package/dist/esm/createMiddleware.js.map +1 -1
  9. package/dist/esm/createServerFn.d.ts +46 -58
  10. package/dist/esm/createServerFn.js +39 -45
  11. package/dist/esm/createServerFn.js.map +1 -1
  12. package/dist/esm/envOnly.d.ts +2 -2
  13. package/dist/esm/envOnly.js +4 -4
  14. package/dist/esm/envOnly.js.map +1 -1
  15. package/dist/esm/getRouterInstance.d.ts +1 -0
  16. package/dist/esm/getRouterInstance.js +7 -0
  17. package/dist/esm/getRouterInstance.js.map +1 -0
  18. package/dist/esm/index.d.ts +6 -5
  19. package/dist/esm/index.js +13 -8
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/serializer/ServerFunctionSerializationAdapter.d.ts +5 -0
  22. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +17 -0
  23. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -0
  24. package/dist/esm/serializer/getClientSerovalPlugins.d.ts +3 -0
  25. package/dist/esm/serializer/getClientSerovalPlugins.js +13 -0
  26. package/dist/esm/serializer/getClientSerovalPlugins.js.map +1 -0
  27. package/dist/esm/serializer/getDefaultSerovalPlugins.d.ts +1 -0
  28. package/dist/esm/serializer/getDefaultSerovalPlugins.js +16 -0
  29. package/dist/esm/serializer/getDefaultSerovalPlugins.js.map +1 -0
  30. package/dist/esm/serverFnFetcher.d.ts +1 -0
  31. package/dist/esm/serverFnFetcher.js +217 -0
  32. package/dist/esm/serverFnFetcher.js.map +1 -0
  33. package/package.json +5 -4
  34. package/src/constants.ts +8 -0
  35. package/src/createClientRpc.ts +26 -0
  36. package/src/createMiddleware.ts +93 -91
  37. package/src/createServerFn.ts +158 -206
  38. package/src/envOnly.ts +2 -2
  39. package/src/getRouterInstance.ts +7 -0
  40. package/src/index.tsx +11 -15
  41. package/src/serializer/ServerFunctionSerializationAdapter.ts +16 -0
  42. package/src/serializer/getClientSerovalPlugins.ts +10 -0
  43. package/src/serializer/getDefaultSerovalPlugins.ts +19 -0
  44. package/src/serverFnFetcher.ts +299 -0
  45. package/src/tests/createServerFn.test-d.ts +124 -105
  46. package/src/tests/createServerMiddleware.test-d.ts +11 -9
  47. package/src/tests/envOnly.test-d.ts +9 -9
  48. package/dist/esm/serializer.d.ts +0 -23
  49. package/dist/esm/serializer.js +0 -152
  50. package/dist/esm/serializer.js.map +0 -1
  51. package/dist/esm/tests/serializer.test.d.ts +0 -1
  52. package/src/serializer.ts +0 -206
  53. package/src/tests/serializer.test.tsx +0 -151
package/src/index.tsx CHANGED
@@ -5,18 +5,6 @@ export type {
5
5
 
6
6
  export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client'
7
7
 
8
- export { startSerializer } from './serializer'
9
-
10
- export type {
11
- StartSerializer,
12
- Serializable,
13
- SerializerParse,
14
- SerializerParseBy,
15
- SerializerStringify,
16
- SerializerStringifyBy,
17
- SerializerExtensions,
18
- } from './serializer'
19
-
20
8
  export {
21
9
  createIsomorphicFn,
22
10
  type IsomorphicFn,
@@ -24,7 +12,7 @@ export {
24
12
  type ClientOnlyFn,
25
13
  type IsomorphicFnBase,
26
14
  } from './createIsomorphicFn'
27
- export { serverOnly, clientOnly } from './envOnly'
15
+ export { createServerOnlyFn, createClientOnlyFn } from './envOnly'
28
16
  export { createServerFn } from './createServerFn'
29
17
  export {
30
18
  createMiddleware,
@@ -68,7 +56,6 @@ export type {
68
56
  FetcherBaseOptions,
69
57
  ServerFn,
70
58
  ServerFnCtx,
71
- ServerFnResponseType,
72
59
  MiddlewareFn,
73
60
  ServerFnMiddlewareOptions,
74
61
  ServerFnMiddlewareResult,
@@ -83,7 +70,16 @@ export {
83
70
  applyMiddleware,
84
71
  execValidator,
85
72
  serverFnBaseToMiddleware,
86
- extractFormDataContext,
87
73
  flattenMiddlewares,
88
74
  executeMiddleware,
89
75
  } from './createServerFn'
76
+
77
+ export { createClientRpc } from './createClientRpc'
78
+
79
+ export { getDefaultSerovalPlugins } from './serializer/getDefaultSerovalPlugins'
80
+
81
+ export {
82
+ TSS_FORMDATA_CONTEXT,
83
+ TSS_SERVER_FUNCTION,
84
+ X_TSS_SERIALIZED,
85
+ } from './constants'
@@ -0,0 +1,16 @@
1
+ import { createSerializationAdapter } from '@tanstack/router-core'
2
+ import { createClientRpc } from '../createClientRpc'
3
+ import { TSS_SERVER_FUNCTION } from '../constants'
4
+
5
+ export const ServerFunctionSerializationAdapter = createSerializationAdapter({
6
+ key: '$TSS/serverfn',
7
+ test: (v): v is { functionId: string } => {
8
+ if (typeof v !== 'object' || v === null) return false
9
+
10
+ if (!(TSS_SERVER_FUNCTION in v)) return false
11
+
12
+ return !!v[TSS_SERVER_FUNCTION]
13
+ },
14
+ toSerializable: ({ functionId }) => ({ functionId }),
15
+ fromSerializable: ({ functionId }) => createClientRpc(functionId),
16
+ })
@@ -0,0 +1,10 @@
1
+ import { makeSerovalPlugin } from '@tanstack/router-core'
2
+ import { getDefaultSerovalPlugins } from './getDefaultSerovalPlugins'
3
+ import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
4
+
5
+ export function getClientSerovalPlugins() {
6
+ return [
7
+ ...getDefaultSerovalPlugins(),
8
+ makeSerovalPlugin(ServerFunctionSerializationAdapter),
9
+ ]
10
+ }
@@ -0,0 +1,19 @@
1
+ import invariant from 'tiny-invariant'
2
+ import {
3
+ makeSerovalPlugin,
4
+ defaultSerovalPlugins as routerDefaultSerovalPlugins,
5
+ } from '@tanstack/router-core'
6
+ import { getRouterInstance } from '../getRouterInstance'
7
+ import type { AnySerializationAdapter } from '@tanstack/router-core'
8
+
9
+ export function getDefaultSerovalPlugins() {
10
+ const router = getRouterInstance()
11
+ invariant(router, 'Expected router instance to be available')
12
+ const adapters = router.options.serializationAdapters as
13
+ | Array<AnySerializationAdapter>
14
+ | undefined
15
+ return [
16
+ ...(adapters?.map(makeSerovalPlugin) ?? []),
17
+ ...routerDefaultSerovalPlugins,
18
+ ]
19
+ }
@@ -0,0 +1,299 @@
1
+ import {
2
+ encode,
3
+ isNotFound,
4
+ isPlainObject,
5
+ parseRedirect,
6
+ } from '@tanstack/router-core'
7
+ import { fromCrossJSON, toJSONAsync } from 'seroval'
8
+ import invariant from 'tiny-invariant'
9
+ import { getClientSerovalPlugins } from './serializer/getClientSerovalPlugins'
10
+ import { TSS_FORMDATA_CONTEXT, X_TSS_SERIALIZED } from './constants'
11
+ import type { FunctionMiddlewareClientFnOptions } from './createMiddleware'
12
+ import type { Plugin as SerovalPlugin } from 'seroval'
13
+
14
+ let serovalPlugins: Array<SerovalPlugin<any, any>> | null = null
15
+
16
+ export async function serverFnFetcher(
17
+ url: string,
18
+ args: Array<any>,
19
+ handler: (url: string, requestInit: RequestInit) => Promise<Response>,
20
+ ) {
21
+ if (!serovalPlugins) {
22
+ serovalPlugins = getClientSerovalPlugins()
23
+ }
24
+ const _first = args[0]
25
+
26
+ // If createServerFn was used to wrap the fetcher,
27
+ // We need to handle the arguments differently
28
+ if (isPlainObject(_first) && _first.method) {
29
+ const first = _first as FunctionMiddlewareClientFnOptions<
30
+ any,
31
+ any,
32
+ any,
33
+ any
34
+ > & {
35
+ headers: HeadersInit
36
+ }
37
+ const type = first.data instanceof FormData ? 'formData' : 'payload'
38
+
39
+ // Arrange the headers
40
+ const headers = new Headers({
41
+ 'x-tsr-redirect': 'manual',
42
+ ...(type === 'payload'
43
+ ? {
44
+ 'content-type': 'application/json',
45
+ accept: 'application/x-ndjson, application/json',
46
+ }
47
+ : {}),
48
+ ...(first.headers instanceof Headers
49
+ ? Object.fromEntries(first.headers.entries())
50
+ : first.headers),
51
+ })
52
+
53
+ // If the method is GET, we need to move the payload to the query string
54
+ if (first.method === 'GET') {
55
+ if (type === 'formData') {
56
+ throw new Error('FormData is not supported with GET requests')
57
+ }
58
+ const encodedPayload = encode({
59
+ payload: await serializePayload(first),
60
+ })
61
+
62
+ if (encodedPayload) {
63
+ if (url.includes('?')) {
64
+ url += `&${encodedPayload}`
65
+ } else {
66
+ url += `?${encodedPayload}`
67
+ }
68
+ }
69
+ }
70
+
71
+ if (url.includes('?')) {
72
+ url += `&createServerFn`
73
+ } else {
74
+ url += `?createServerFn`
75
+ }
76
+
77
+ return await getResponse(async () =>
78
+ handler(url, {
79
+ method: first.method,
80
+ headers,
81
+ signal: first.signal,
82
+ ...(await getFetcherRequestOptions(first)),
83
+ }),
84
+ )
85
+ }
86
+
87
+ // If not a custom fetcher, it was probably
88
+ // a `use server` function, so just proxy the arguments
89
+ // through as a POST request
90
+ return await getResponse(() =>
91
+ handler(url, {
92
+ method: 'POST',
93
+ headers: {
94
+ Accept: 'application/json',
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify(args),
98
+ }),
99
+ )
100
+ }
101
+
102
+ async function serializePayload(
103
+ opts: FunctionMiddlewareClientFnOptions<any, any, any, any>,
104
+ ) {
105
+ const payloadToSerialize: any = {}
106
+ if (opts.data) {
107
+ payloadToSerialize['data'] = opts.data
108
+ }
109
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
110
+ if (opts.context && Object.keys(opts.context).length > 0) {
111
+ payloadToSerialize['context'] = opts.context
112
+ }
113
+
114
+ return serialize(payloadToSerialize)
115
+ }
116
+
117
+ async function serialize(data: any) {
118
+ return JSON.stringify(
119
+ await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins! })),
120
+ )
121
+ }
122
+
123
+ async function getFetcherRequestOptions(
124
+ opts: FunctionMiddlewareClientFnOptions<any, any, any, any>,
125
+ ) {
126
+ if (opts.method === 'POST') {
127
+ if (opts.data instanceof FormData) {
128
+ opts.data.set(TSS_FORMDATA_CONTEXT, await serialize(opts.context))
129
+ return {
130
+ body: opts.data,
131
+ }
132
+ }
133
+
134
+ return {
135
+ body: await serializePayload(opts),
136
+ }
137
+ }
138
+
139
+ return {}
140
+ }
141
+
142
+ /**
143
+ * Retrieves a response from a given function and manages potential errors
144
+ * and special response types including redirects and not found errors.
145
+ *
146
+ * @param fn - The function to execute for obtaining the response.
147
+ * @returns The processed response from the function.
148
+ * @throws If the response is invalid or an error occurs during processing.
149
+ */
150
+ async function getResponse(fn: () => Promise<Response>) {
151
+ const response = await (async () => {
152
+ try {
153
+ return await fn()
154
+ } catch (error) {
155
+ if (error instanceof Response) {
156
+ return error
157
+ }
158
+
159
+ throw error
160
+ }
161
+ })()
162
+
163
+ const contentType = response.headers.get('content-type')
164
+ invariant(contentType, 'expected content-type header to be set')
165
+ const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)
166
+ // If the response is not ok, throw an error
167
+ if (!response.ok) {
168
+ if (serializedByStart && contentType.includes('application/json')) {
169
+ const jsonPayload = await response.json()
170
+ const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
171
+ throw result
172
+ }
173
+
174
+ throw new Error(await response.text())
175
+ }
176
+
177
+ if (serializedByStart) {
178
+ let result
179
+ if (contentType.includes('application/x-ndjson')) {
180
+ const refs = new Map()
181
+ result = await processServerFnResponse({
182
+ response,
183
+ onMessage: (msg) =>
184
+ fromCrossJSON(msg, { refs, plugins: serovalPlugins! }),
185
+ onError(msg, error) {
186
+ // TODO how could we notify consumer that an error occurred?
187
+ console.error(msg, error)
188
+ },
189
+ })
190
+ }
191
+ if (contentType.includes('application/json')) {
192
+ const jsonPayload = await response.json()
193
+ result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
194
+ }
195
+ invariant(result, 'expected result to be resolved')
196
+ if (result instanceof Error) {
197
+ throw result
198
+ }
199
+ return result
200
+ }
201
+
202
+ if (contentType.includes('application/json')) {
203
+ const jsonPayload = await response.json()
204
+ const redirect = parseRedirect(jsonPayload)
205
+ if (redirect) {
206
+ throw redirect
207
+ }
208
+ if (isNotFound(jsonPayload)) {
209
+ throw jsonPayload
210
+ }
211
+ return jsonPayload
212
+ }
213
+
214
+ return response
215
+ }
216
+
217
+ async function processServerFnResponse({
218
+ response,
219
+ onMessage,
220
+ onError,
221
+ }: {
222
+ response: Response
223
+ onMessage: (msg: any) => any
224
+ onError?: (msg: string, error?: any) => void
225
+ }) {
226
+ if (!response.body) {
227
+ throw new Error('No response body')
228
+ }
229
+
230
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
231
+
232
+ let buffer = ''
233
+ let firstRead = false
234
+ let firstObject
235
+
236
+ while (!firstRead) {
237
+ const { value, done } = await reader.read()
238
+ if (value) buffer += value
239
+
240
+ if (buffer.length === 0 && done) {
241
+ throw new Error('Stream ended before first object')
242
+ }
243
+
244
+ // common case: buffer ends with newline
245
+ if (buffer.endsWith('\n')) {
246
+ const lines = buffer.split('\n').filter(Boolean)
247
+ const firstLine = lines[0]
248
+ if (!firstLine) throw new Error('No JSON line in the first chunk')
249
+ firstObject = JSON.parse(firstLine)
250
+ firstRead = true
251
+ buffer = lines.slice(1).join('\n')
252
+ } else {
253
+ // fallback: wait for a newline to parse first object safely
254
+ const newlineIndex = buffer.indexOf('\n')
255
+ if (newlineIndex >= 0) {
256
+ const line = buffer.slice(0, newlineIndex).trim()
257
+ buffer = buffer.slice(newlineIndex + 1)
258
+ if (line.length > 0) {
259
+ firstObject = JSON.parse(line)
260
+ firstRead = true
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // process rest of the stream asynchronously
267
+ ;(async () => {
268
+ try {
269
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
270
+ while (true) {
271
+ const { value, done } = await reader.read()
272
+ if (value) buffer += value
273
+
274
+ const lastNewline = buffer.lastIndexOf('\n')
275
+ if (lastNewline >= 0) {
276
+ const chunk = buffer.slice(0, lastNewline)
277
+ buffer = buffer.slice(lastNewline + 1)
278
+ const lines = chunk.split('\n').filter(Boolean)
279
+
280
+ for (const line of lines) {
281
+ try {
282
+ onMessage(JSON.parse(line))
283
+ } catch (e) {
284
+ onError?.(`Invalid JSON line: ${line}`, e)
285
+ }
286
+ }
287
+ }
288
+
289
+ if (done) {
290
+ break
291
+ }
292
+ }
293
+ } catch (err) {
294
+ onError?.('Stream processing error:', err)
295
+ }
296
+ })()
297
+
298
+ return onMessage(firstObject)
299
+ }