@tanstack/start-client-core 1.132.0-alpha.2 → 1.132.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.
Files changed (43) hide show
  1. package/dist/esm/constants.d.ts +2 -0
  2. package/dist/esm/constants.js +5 -0
  3. package/dist/esm/constants.js.map +1 -0
  4. package/dist/esm/createClientRpc.d.ts +4 -0
  5. package/dist/esm/createClientRpc.js +24 -0
  6. package/dist/esm/createClientRpc.js.map +1 -0
  7. package/dist/esm/createServerFn.d.ts +0 -7
  8. package/dist/esm/createServerFn.js +2 -28
  9. package/dist/esm/createServerFn.js.map +1 -1
  10. package/dist/esm/getRouterInstance.d.ts +1 -0
  11. package/dist/esm/getRouterInstance.js +7 -0
  12. package/dist/esm/getRouterInstance.js.map +1 -0
  13. package/dist/esm/index.d.ts +5 -3
  14. package/dist/esm/index.js +8 -5
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/serializer/ServerFunctionSerializationAdapter.d.ts +5 -0
  17. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +12 -0
  18. package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -0
  19. package/dist/esm/serializer/getClientSerovalPlugins.d.ts +3 -0
  20. package/dist/esm/serializer/getClientSerovalPlugins.js +13 -0
  21. package/dist/esm/serializer/getClientSerovalPlugins.js.map +1 -0
  22. package/dist/esm/serializer/getDefaultSerovalPlugins.d.ts +3 -0
  23. package/dist/esm/serializer/getDefaultSerovalPlugins.js +19 -0
  24. package/dist/esm/serializer/getDefaultSerovalPlugins.js.map +1 -0
  25. package/dist/esm/serializer.d.ts +0 -7
  26. package/dist/esm/serverFnFetcher.d.ts +1 -0
  27. package/dist/esm/serverFnFetcher.js +217 -0
  28. package/dist/esm/serverFnFetcher.js.map +1 -0
  29. package/package.json +5 -3
  30. package/src/constants.ts +2 -0
  31. package/src/createClientRpc.ts +24 -0
  32. package/src/createServerFn.ts +2 -36
  33. package/src/getRouterInstance.ts +7 -0
  34. package/src/index.tsx +6 -4
  35. package/src/serializer/ServerFunctionSerializationAdapter.ts +10 -0
  36. package/src/serializer/getClientSerovalPlugins.ts +10 -0
  37. package/src/serializer/getDefaultSerovalPlugins.ts +24 -0
  38. package/src/serializer.ts +0 -194
  39. package/src/serverFnFetcher.ts +299 -0
  40. package/dist/esm/serializer.js +0 -162
  41. package/dist/esm/serializer.js.map +0 -1
  42. package/dist/esm/tests/serializer.test.d.ts +0 -1
  43. package/src/tests/serializer.test.tsx +0 -151
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverFnFetcher.js","sources":["../../src/serverFnFetcher.ts"],"sourcesContent":["import {\n encode,\n isNotFound,\n isPlainObject,\n parseRedirect,\n} from '@tanstack/router-core'\nimport { fromCrossJSON, fromJSON, toJSONAsync } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { getClientSerovalPlugins } from './serializer/getClientSerovalPlugins'\nimport { TSR_FORMDATA_CONTEXT } from './constants'\nimport type { FunctionMiddlewareClientFnOptions } from './createMiddleware'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | null = null\n\nexport async function serverFnFetcher(\n url: string,\n args: Array<any>,\n handler: (url: string, requestInit: RequestInit) => Promise<Response>,\n) {\n if (!serovalPlugins) {\n serovalPlugins = getClientSerovalPlugins()\n }\n const _first = args[0]\n\n // If createServerFn was used to wrap the fetcher,\n // We need to handle the arguments differently\n if (isPlainObject(_first) && _first.method) {\n const first = _first as FunctionMiddlewareClientFnOptions<\n any,\n any,\n any,\n any\n > & {\n headers: HeadersInit\n }\n const type = first.data instanceof FormData ? 'formData' : 'payload'\n\n // Arrange the headers\n const headers = new Headers({\n 'x-tsr-redirect': 'manual',\n ...(type === 'payload'\n ? {\n 'content-type': 'application/json',\n accept: 'application/x-ndjson, application/json',\n }\n : {}),\n ...(first.headers instanceof Headers\n ? Object.fromEntries(first.headers.entries())\n : first.headers),\n })\n\n // If the method is GET, we need to move the payload to the query string\n if (first.method === 'GET') {\n const encodedPayload = encode({\n payload: await serializePayload(first),\n })\n\n if (encodedPayload) {\n if (url.includes('?')) {\n url += `&${encodedPayload}`\n } else {\n url += `?${encodedPayload}`\n }\n }\n }\n\n if (url.includes('?')) {\n url += `&createServerFn`\n } else {\n url += `?createServerFn`\n }\n if (first.response === 'raw') {\n url += `&raw`\n }\n\n return await getResponse(async () =>\n handler(url, {\n method: first.method,\n headers,\n signal: first.signal,\n ...(await getFetcherRequestOptions(first)),\n }),\n )\n }\n\n // If not a custom fetcher, it was probably\n // a `use server` function, so just proxy the arguments\n // through as a POST request\n return await getResponse(() =>\n handler(url, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(args),\n }),\n )\n}\n\nasync function serializePayload(\n opts: FunctionMiddlewareClientFnOptions<any, any, any, any>,\n) {\n const payloadToSerialize: any = {}\n if (opts.data) {\n payloadToSerialize['data'] = opts.data\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (opts.context && Object.keys(opts.context).length > 0) {\n payloadToSerialize['context'] = opts.context\n }\n\n return serialize(payloadToSerialize)\n}\n\nasync function serialize(data: any) {\n return JSON.stringify(\n await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins! })),\n )\n}\n\nasync function getFetcherRequestOptions(\n opts: FunctionMiddlewareClientFnOptions<any, any, any, any>,\n) {\n if (opts.method === 'POST') {\n if (opts.data instanceof FormData) {\n opts.data.set(TSR_FORMDATA_CONTEXT, await serialize(opts.context))\n return {\n body: opts.data,\n }\n }\n\n return {\n body: await serializePayload(opts),\n }\n }\n\n return {}\n}\n\n/**\n * Retrieves a response from a given function and manages potential errors\n * and special response types including redirects and not found errors.\n *\n * @param fn - The function to execute for obtaining the response.\n * @returns The processed response from the function.\n * @throws If the response is invalid or an error occurs during processing.\n */\nasync function getResponse(fn: () => Promise<Response>) {\n const response = await (async () => {\n try {\n return await fn()\n } catch (error) {\n if (error instanceof Response) {\n return error\n }\n\n throw error\n }\n })()\n\n const contentType = response.headers.get('content-type')\n invariant(contentType, 'expected content-type header to be set')\n const serializedByStart = !!response.headers.get('x-tss-serialized')\n // If the response is not ok, throw an error\n if (!response.ok) {\n if (serializedByStart && contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n const result = fromJSON(jsonPayload, { plugins: serovalPlugins! })\n throw result\n }\n\n throw new Error(await response.text())\n }\n\n if (serializedByStart) {\n let result\n if (contentType.includes('application/x-ndjson')) {\n const refs = new Map()\n result = await processServerFnResponse({\n response,\n onMessage: (msg) =>\n fromCrossJSON(msg, { refs, plugins: serovalPlugins! }),\n onError(msg, error) {\n // TODO how could we notify consumer that an error occured?\n console.error(msg, error)\n },\n })\n }\n if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n result = fromJSON(jsonPayload, { plugins: serovalPlugins! })\n }\n invariant(result, 'expected result to be resolved')\n if (result instanceof Error) {\n throw result\n }\n return result\n }\n\n if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n const redirect = parseRedirect(jsonPayload)\n if (redirect) {\n throw redirect\n }\n if (isNotFound(jsonPayload)) {\n throw jsonPayload\n }\n return jsonPayload\n }\n\n return response\n}\n\nasync function processServerFnResponse({\n response,\n onMessage,\n onError,\n}: {\n response: Response\n onMessage: (msg: any) => any\n onError?: (msg: string, error?: any) => void\n}) {\n if (!response.body) {\n throw new Error('No response body')\n }\n\n const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()\n\n let buffer = ''\n let firstRead = false\n let firstObject\n\n while (!firstRead) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n if (buffer.length === 0 && done) {\n throw new Error('Stream ended before first object')\n }\n\n // common case: buffer ends with newline\n if (buffer.endsWith('\\n')) {\n const lines = buffer.split('\\n').filter(Boolean)\n const firstLine = lines[0]\n if (!firstLine) throw new Error('No JSON line in the first chunk')\n firstObject = JSON.parse(firstLine)\n firstRead = true\n buffer = lines.slice(1).join('\\n')\n } else {\n // fallback: wait for a newline to parse first object safely\n const newlineIndex = buffer.indexOf('\\n')\n if (newlineIndex >= 0) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.length > 0) {\n firstObject = JSON.parse(line)\n firstRead = true\n }\n }\n }\n }\n\n // process rest of the stream asynchronously\n ;(async () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n const lastNewline = buffer.lastIndexOf('\\n')\n if (lastNewline >= 0) {\n const chunk = buffer.slice(0, lastNewline)\n buffer = buffer.slice(lastNewline + 1)\n const lines = chunk.split('\\n').filter(Boolean)\n\n for (const line of lines) {\n try {\n onMessage(JSON.parse(line))\n } catch (e) {\n onError?.(`Invalid JSON line: ${line}`, e)\n }\n }\n }\n\n if (done) {\n break\n }\n }\n } catch (err) {\n onError?.('Stream processing error:', err)\n }\n })()\n\n return onMessage(firstObject)\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/start-client-core",
3
- "version": "1.132.0-alpha.2",
3
+ "version": "1.132.0-alpha.3",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -44,10 +44,12 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "cookie-es": "^1.2.2",
47
+ "seroval": "^1.3.2",
48
+ "seroval-plugins": "^1.3.2",
47
49
  "tiny-invariant": "^1.3.3",
48
50
  "tiny-warning": "^1.0.3",
49
- "@tanstack/router-core": "1.132.0-alpha.2",
50
- "@tanstack/start-storage-context": "1.132.0-alpha.2"
51
+ "@tanstack/router-core": "1.132.0-alpha.3",
52
+ "@tanstack/start-storage-context": "1.132.0-alpha.3"
51
53
  },
52
54
  "scripts": {}
53
55
  }
@@ -0,0 +1,2 @@
1
+ export const TSR_FORMDATA_CONTEXT = '__TSR_CONTEXT'
2
+ export {}
@@ -0,0 +1,24 @@
1
+ import { serverFnFetcher } from './serverFnFetcher'
2
+
3
+ let baseUrl: string
4
+ function sanitizeBase(base: string) {
5
+ return base.replace(/^\/|\/$/g, '')
6
+ }
7
+
8
+ export const createClientRpc = (functionId: string) => {
9
+ if (!baseUrl) {
10
+ const sanitizedAppBase = sanitizeBase(process.env.TSS_APP_BASE || '/')
11
+ const sanitizedServerBase = sanitizeBase(process.env.TSS_SERVER_FN_BASE!)
12
+ baseUrl = `${sanitizedAppBase ? `/${sanitizedAppBase}` : ''}/${sanitizedServerBase}/`
13
+ }
14
+ const url = baseUrl + functionId
15
+
16
+ const clientFn = (...args: Array<any>) => {
17
+ return serverFnFetcher(url, args, fetch)
18
+ }
19
+
20
+ return Object.assign(clientFn, {
21
+ url,
22
+ functionId,
23
+ })
24
+ }
@@ -1,11 +1,8 @@
1
1
  import { isNotFound, isRedirect } from '@tanstack/router-core'
2
2
  import { mergeHeaders } from '@tanstack/router-core/ssr/client'
3
- import { getStartContext } from '@tanstack/start-storage-context'
4
3
  import { globalMiddleware } from './registerGlobalMiddleware'
5
4
 
6
- import { startSerializer } from './serializer'
7
-
8
- import { createIsomorphicFn } from './createIsomorphicFn'
5
+ import { getRouterInstance } from './getRouterInstance'
9
6
  import type {
10
7
  SerializerParse,
11
8
  SerializerStringify,
@@ -33,10 +30,6 @@ import type {
33
30
 
34
31
  type TODO = any
35
32
 
36
- const getRouterInstance = createIsomorphicFn()
37
- .client(() => window.__TSR_ROUTER__!)
38
- .server(() => getStartContext({ throwIfNotFound: false })?.router)
39
-
40
33
  export function createServerFn<
41
34
  TMethod extends Method,
42
35
  TServerFnResponseType extends ServerFnResponseType = 'data',
@@ -143,10 +136,7 @@ export function createServerFn<
143
136
  ...extractedFn,
144
137
  // The extracted function on the server-side calls
145
138
  // this function
146
- __executeServer: async (opts_: any, signal: AbortSignal) => {
147
- const opts =
148
- opts_ instanceof FormData ? extractFormDataContext(opts_) : opts_
149
-
139
+ __executeServer: async (opts: any, signal: AbortSignal) => {
150
140
  const ctx = {
151
141
  ...extractedFn,
152
142
  ...opts,
@@ -521,30 +511,6 @@ export interface ServerFnBuilder<
521
511
  >
522
512
  }
523
513
 
524
- export function extractFormDataContext(formData: FormData) {
525
- const serializedContext = formData.get('__TSR_CONTEXT')
526
- formData.delete('__TSR_CONTEXT')
527
-
528
- if (typeof serializedContext !== 'string') {
529
- return {
530
- context: {},
531
- data: formData,
532
- }
533
- }
534
-
535
- try {
536
- const context = startSerializer.parse(serializedContext)
537
- return {
538
- context,
539
- data: formData,
540
- }
541
- } catch {
542
- return {
543
- data: formData,
544
- }
545
- }
546
- }
547
-
548
514
  export function flattenMiddlewares(
549
515
  middlewares: Array<AnyFunctionMiddleware>,
550
516
  ): Array<AnyFunctionMiddleware> {
@@ -0,0 +1,7 @@
1
+ import { getStartContext } from '@tanstack/start-storage-context'
2
+ import { createIsomorphicFn } from './createIsomorphicFn'
3
+
4
+ // TODO should this be a public API
5
+ export const getRouterInstance = createIsomorphicFn()
6
+ .client(() => window.__TSR_ROUTER__!)
7
+ .server(() => getStartContext().router)
package/src/index.tsx CHANGED
@@ -5,10 +5,7 @@ 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
8
  export type {
11
- StartSerializer,
12
9
  Serializable,
13
10
  SerializerParse,
14
11
  SerializerParseBy,
@@ -83,7 +80,12 @@ export {
83
80
  applyMiddleware,
84
81
  execValidator,
85
82
  serverFnBaseToMiddleware,
86
- extractFormDataContext,
87
83
  flattenMiddlewares,
88
84
  executeMiddleware,
89
85
  } from './createServerFn'
86
+
87
+ export { createClientRpc } from './createClientRpc'
88
+
89
+ export { getDefaultSerovalPlugins } from './serializer/getDefaultSerovalPlugins'
90
+
91
+ export { TSR_FORMDATA_CONTEXT } from './constants'
@@ -0,0 +1,10 @@
1
+ import { createSerializationAdapter } from '@tanstack/router-core'
2
+ import { createClientRpc } from '../createClientRpc'
3
+
4
+ export const ServerFunctionSerializationAdapter = createSerializationAdapter({
5
+ key: '$TSS/serverfn',
6
+ test: (v): v is { functionId: string } =>
7
+ typeof v == 'function' && 'functionId' in v,
8
+ toSerializable: ({ functionId }) => ({ functionId }),
9
+ fromSerializable: ({ functionId }) => createClientRpc(functionId),
10
+ })
@@ -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,24 @@
1
+ import invariant from 'tiny-invariant'
2
+ import {
3
+ makeSerovalPlugin,
4
+ defaultSerovalPlugins as routerDefaultSerovalPlugins,
5
+ } from '@tanstack/router-core'
6
+ import { FormDataPlugin } from 'seroval-plugins/web'
7
+ import { getRouterInstance } from '../getRouterInstance'
8
+ import type { AnyTransformer } from '@tanstack/router-core'
9
+
10
+ import type { Plugin } from 'seroval'
11
+
12
+ export const defaultSerovalPlugins = [
13
+ ...routerDefaultSerovalPlugins,
14
+ FormDataPlugin as Plugin<FormData, any>,
15
+ ]
16
+
17
+ export function getDefaultSerovalPlugins() {
18
+ const router = getRouterInstance()
19
+ invariant(router, 'Expected router instance to be available')
20
+ const adapters = router.options.serializationAdapters as
21
+ | Array<AnyTransformer>
22
+ | undefined
23
+ return [...defaultSerovalPlugins, ...(adapters?.map(makeSerovalPlugin) ?? [])]
24
+ }
package/src/serializer.ts CHANGED
@@ -1,12 +1,3 @@
1
- import { isPlainObject } from '@tanstack/router-core'
2
-
3
- export interface StartSerializer {
4
- stringify: (obj: unknown) => string
5
- parse: (str: string) => unknown
6
- encode: <T>(value: T) => T
7
- decode: <T>(value: T) => T
8
- }
9
-
10
1
  export type SerializerStringifyBy<T, TSerializable> = T extends TSerializable
11
2
  ? T
12
3
  : T extends (...args: Array<any>) => any
@@ -32,188 +23,3 @@ export type Serializable = Date | undefined | Error | FormData | bigint
32
23
  export type SerializerStringify<T> = SerializerStringifyBy<T, Serializable>
33
24
 
34
25
  export type SerializerParse<T> = SerializerParseBy<T, Serializable>
35
- export const startSerializer: StartSerializer = {
36
- stringify: (value: any) =>
37
- JSON.stringify(value, function replacer(key, val) {
38
- const ogVal = this[key]
39
- const serializer = serializers.find((t) => t.stringifyCondition(ogVal))
40
-
41
- if (serializer) {
42
- return serializer.stringify(ogVal)
43
- }
44
-
45
- return val
46
- }),
47
- parse: (value: string) =>
48
- JSON.parse(value, function parser(key, val) {
49
- const ogVal = this[key]
50
- if (isPlainObject(ogVal)) {
51
- const serializer = serializers.find((t) => t.parseCondition(ogVal))
52
-
53
- if (serializer) {
54
- return serializer.parse(ogVal)
55
- }
56
- }
57
-
58
- return val
59
- }),
60
- encode: (value: any) => {
61
- // When encoding, dive first
62
- if (Array.isArray(value)) {
63
- return value.map((v) => startSerializer.encode(v))
64
- }
65
-
66
- if (isPlainObject(value)) {
67
- return Object.fromEntries(
68
- Object.entries(value).map(([key, v]) => [
69
- key,
70
- startSerializer.encode(v),
71
- ]),
72
- )
73
- }
74
-
75
- const serializer = serializers.find((t) => t.stringifyCondition(value))
76
- if (serializer) {
77
- return serializer.stringify(value)
78
- }
79
-
80
- return value
81
- },
82
- decode: (value: any) => {
83
- // Attempt transform first
84
- if (isPlainObject(value)) {
85
- const serializer = serializers.find((t) => t.parseCondition(value))
86
- if (serializer) {
87
- return serializer.parse(value)
88
- }
89
- }
90
-
91
- if (Array.isArray(value)) {
92
- return value.map((v) => startSerializer.decode(v))
93
- }
94
-
95
- if (isPlainObject(value)) {
96
- return Object.fromEntries(
97
- Object.entries(value).map(([key, v]) => [
98
- key,
99
- startSerializer.decode(v),
100
- ]),
101
- )
102
- }
103
-
104
- return value
105
- },
106
- }
107
- const createSerializer = <TKey extends string, TInput, TSerialized>(
108
- key: TKey,
109
- check: (value: any) => value is TInput,
110
- toValue: (value: TInput) => TSerialized,
111
- fromValue: (value: TSerialized) => TInput,
112
- ) => ({
113
- key,
114
- stringifyCondition: check,
115
- stringify: (value: any) => ({ [`$${key}`]: toValue(value) }),
116
- parseCondition: (value: any) => Object.hasOwn(value, `$${key}`),
117
- parse: (value: any) => fromValue(value[`$${key}`]),
118
- })
119
- // Keep these ordered by predicted frequency
120
- // Make sure to keep DefaultSerializable in sync with these serializers
121
- // Also, make sure that they are unit tested in serializer.test.tsx
122
- const serializers = [
123
- createSerializer(
124
- // Key
125
- 'undefined',
126
- // Check
127
- (v): v is undefined => v === undefined,
128
- // To
129
- () => 0,
130
- // From
131
- () => undefined,
132
- ),
133
- createSerializer(
134
- // Key
135
- 'date',
136
- // Check
137
- (v): v is Date => v instanceof Date,
138
- // To
139
- (v) => v.toISOString(),
140
- // From
141
- (v) => new Date(v),
142
- ),
143
- createSerializer(
144
- // Key
145
- 'error',
146
- // Check
147
- (v): v is Error => v instanceof Error,
148
- // To
149
- (v) => ({
150
- ...v,
151
- message: v.message,
152
- stack: process.env.NODE_ENV === 'development' ? v.stack : undefined,
153
- cause: v.cause,
154
- }),
155
- // From
156
- (v) => Object.assign(new Error(v.message), v),
157
- ),
158
- createSerializer(
159
- // Key
160
- 'formData',
161
- // Check
162
- (v): v is FormData => v instanceof FormData,
163
- // To
164
- (v) => {
165
- const entries: Record<
166
- string,
167
- Array<FormDataEntryValue> | FormDataEntryValue
168
- > = {}
169
- v.forEach((value, key) => {
170
- const entry = entries[key]
171
- if (entry !== undefined) {
172
- if (Array.isArray(entry)) {
173
- entry.push(value)
174
- } else {
175
- entries[key] = [entry, value]
176
- }
177
- } else {
178
- entries[key] = value
179
- }
180
- })
181
- return entries
182
- },
183
- // From
184
- (v) => {
185
- const formData = new FormData()
186
- Object.entries(v).forEach(([key, value]) => {
187
- if (Array.isArray(value)) {
188
- value.forEach((val) => formData.append(key, val))
189
- } else {
190
- formData.append(key, value)
191
- }
192
- })
193
- return formData
194
- },
195
- ),
196
- createSerializer(
197
- // Key
198
- 'bigint',
199
- // Check
200
- (v): v is bigint => typeof v === 'bigint',
201
- // To
202
- (v) => v.toString(),
203
- // From
204
- (v) => BigInt(v),
205
- ),
206
- createSerializer(
207
- // Key
208
- 'server-function',
209
- // Check
210
- (v): v is { functionId: string } =>
211
- typeof v === 'function' &&
212
- 'functionId' in v &&
213
- typeof v.functionId === 'string',
214
- // To
215
- ({ functionId }) => ({ functionId, __serverFn: true }),
216
- // From, dummy impl. the actual server function lookup is done on the server in packages/start-server-core/src/server-functions-handler.ts
217
- (v) => v,
218
- ),
219
- ] as const