@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.
- package/dist/esm/constants.d.ts +2 -0
- package/dist/esm/constants.js +5 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/createClientRpc.d.ts +4 -0
- package/dist/esm/createClientRpc.js +24 -0
- package/dist/esm/createClientRpc.js.map +1 -0
- package/dist/esm/createServerFn.d.ts +0 -7
- package/dist/esm/createServerFn.js +2 -28
- package/dist/esm/createServerFn.js.map +1 -1
- package/dist/esm/getRouterInstance.d.ts +1 -0
- package/dist/esm/getRouterInstance.js +7 -0
- package/dist/esm/getRouterInstance.js.map +1 -0
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.js +8 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.d.ts +5 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +12 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -0
- package/dist/esm/serializer/getClientSerovalPlugins.d.ts +3 -0
- package/dist/esm/serializer/getClientSerovalPlugins.js +13 -0
- package/dist/esm/serializer/getClientSerovalPlugins.js.map +1 -0
- package/dist/esm/serializer/getDefaultSerovalPlugins.d.ts +3 -0
- package/dist/esm/serializer/getDefaultSerovalPlugins.js +19 -0
- package/dist/esm/serializer/getDefaultSerovalPlugins.js.map +1 -0
- package/dist/esm/serializer.d.ts +0 -7
- package/dist/esm/serverFnFetcher.d.ts +1 -0
- package/dist/esm/serverFnFetcher.js +217 -0
- package/dist/esm/serverFnFetcher.js.map +1 -0
- package/package.json +5 -3
- package/src/constants.ts +2 -0
- package/src/createClientRpc.ts +24 -0
- package/src/createServerFn.ts +2 -36
- package/src/getRouterInstance.ts +7 -0
- package/src/index.tsx +6 -4
- package/src/serializer/ServerFunctionSerializationAdapter.ts +10 -0
- package/src/serializer/getClientSerovalPlugins.ts +10 -0
- package/src/serializer/getDefaultSerovalPlugins.ts +24 -0
- package/src/serializer.ts +0 -194
- package/src/serverFnFetcher.ts +299 -0
- package/dist/esm/serializer.js +0 -162
- package/dist/esm/serializer.js.map +0 -1
- package/dist/esm/tests/serializer.test.d.ts +0 -1
- package/src/tests/serializer.test.tsx +0 -151
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encode,
|
|
3
|
+
isNotFound,
|
|
4
|
+
isPlainObject,
|
|
5
|
+
parseRedirect,
|
|
6
|
+
} from '@tanstack/router-core'
|
|
7
|
+
import { fromCrossJSON, fromJSON, toJSONAsync } from 'seroval'
|
|
8
|
+
import invariant from 'tiny-invariant'
|
|
9
|
+
import { getClientSerovalPlugins } from './serializer/getClientSerovalPlugins'
|
|
10
|
+
import { TSR_FORMDATA_CONTEXT } 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
|
+
const encodedPayload = encode({
|
|
56
|
+
payload: await serializePayload(first),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (encodedPayload) {
|
|
60
|
+
if (url.includes('?')) {
|
|
61
|
+
url += `&${encodedPayload}`
|
|
62
|
+
} else {
|
|
63
|
+
url += `?${encodedPayload}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (url.includes('?')) {
|
|
69
|
+
url += `&createServerFn`
|
|
70
|
+
} else {
|
|
71
|
+
url += `?createServerFn`
|
|
72
|
+
}
|
|
73
|
+
if (first.response === 'raw') {
|
|
74
|
+
url += `&raw`
|
|
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(TSR_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 = fromJSON(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 occured?
|
|
187
|
+
console.error(msg, error)
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
if (contentType.includes('application/json')) {
|
|
192
|
+
const jsonPayload = await response.json()
|
|
193
|
+
result = fromJSON(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
|
+
}
|
package/dist/esm/serializer.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from "@tanstack/router-core";
|
|
2
|
-
const startSerializer = {
|
|
3
|
-
stringify: (value) => JSON.stringify(value, function replacer(key, val) {
|
|
4
|
-
const ogVal = this[key];
|
|
5
|
-
const serializer = serializers.find((t) => t.stringifyCondition(ogVal));
|
|
6
|
-
if (serializer) {
|
|
7
|
-
return serializer.stringify(ogVal);
|
|
8
|
-
}
|
|
9
|
-
return val;
|
|
10
|
-
}),
|
|
11
|
-
parse: (value) => JSON.parse(value, function parser(key, val) {
|
|
12
|
-
const ogVal = this[key];
|
|
13
|
-
if (isPlainObject(ogVal)) {
|
|
14
|
-
const serializer = serializers.find((t) => t.parseCondition(ogVal));
|
|
15
|
-
if (serializer) {
|
|
16
|
-
return serializer.parse(ogVal);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return val;
|
|
20
|
-
}),
|
|
21
|
-
encode: (value) => {
|
|
22
|
-
if (Array.isArray(value)) {
|
|
23
|
-
return value.map((v) => startSerializer.encode(v));
|
|
24
|
-
}
|
|
25
|
-
if (isPlainObject(value)) {
|
|
26
|
-
return Object.fromEntries(
|
|
27
|
-
Object.entries(value).map(([key, v]) => [
|
|
28
|
-
key,
|
|
29
|
-
startSerializer.encode(v)
|
|
30
|
-
])
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
const serializer = serializers.find((t) => t.stringifyCondition(value));
|
|
34
|
-
if (serializer) {
|
|
35
|
-
return serializer.stringify(value);
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
},
|
|
39
|
-
decode: (value) => {
|
|
40
|
-
if (isPlainObject(value)) {
|
|
41
|
-
const serializer = serializers.find((t) => t.parseCondition(value));
|
|
42
|
-
if (serializer) {
|
|
43
|
-
return serializer.parse(value);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (Array.isArray(value)) {
|
|
47
|
-
return value.map((v) => startSerializer.decode(v));
|
|
48
|
-
}
|
|
49
|
-
if (isPlainObject(value)) {
|
|
50
|
-
return Object.fromEntries(
|
|
51
|
-
Object.entries(value).map(([key, v]) => [
|
|
52
|
-
key,
|
|
53
|
-
startSerializer.decode(v)
|
|
54
|
-
])
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const createSerializer = (key, check, toValue, fromValue) => ({
|
|
61
|
-
key,
|
|
62
|
-
stringifyCondition: check,
|
|
63
|
-
stringify: (value) => ({ [`$${key}`]: toValue(value) }),
|
|
64
|
-
parseCondition: (value) => Object.hasOwn(value, `$${key}`),
|
|
65
|
-
parse: (value) => fromValue(value[`$${key}`])
|
|
66
|
-
});
|
|
67
|
-
const serializers = [
|
|
68
|
-
createSerializer(
|
|
69
|
-
// Key
|
|
70
|
-
"undefined",
|
|
71
|
-
// Check
|
|
72
|
-
(v) => v === void 0,
|
|
73
|
-
// To
|
|
74
|
-
() => 0,
|
|
75
|
-
// From
|
|
76
|
-
() => void 0
|
|
77
|
-
),
|
|
78
|
-
createSerializer(
|
|
79
|
-
// Key
|
|
80
|
-
"date",
|
|
81
|
-
// Check
|
|
82
|
-
(v) => v instanceof Date,
|
|
83
|
-
// To
|
|
84
|
-
(v) => v.toISOString(),
|
|
85
|
-
// From
|
|
86
|
-
(v) => new Date(v)
|
|
87
|
-
),
|
|
88
|
-
createSerializer(
|
|
89
|
-
// Key
|
|
90
|
-
"error",
|
|
91
|
-
// Check
|
|
92
|
-
(v) => v instanceof Error,
|
|
93
|
-
// To
|
|
94
|
-
(v) => ({
|
|
95
|
-
...v,
|
|
96
|
-
message: v.message,
|
|
97
|
-
stack: process.env.NODE_ENV === "development" ? v.stack : void 0,
|
|
98
|
-
cause: v.cause
|
|
99
|
-
}),
|
|
100
|
-
// From
|
|
101
|
-
(v) => Object.assign(new Error(v.message), v)
|
|
102
|
-
),
|
|
103
|
-
createSerializer(
|
|
104
|
-
// Key
|
|
105
|
-
"formData",
|
|
106
|
-
// Check
|
|
107
|
-
(v) => v instanceof FormData,
|
|
108
|
-
// To
|
|
109
|
-
(v) => {
|
|
110
|
-
const entries = {};
|
|
111
|
-
v.forEach((value, key) => {
|
|
112
|
-
const entry = entries[key];
|
|
113
|
-
if (entry !== void 0) {
|
|
114
|
-
if (Array.isArray(entry)) {
|
|
115
|
-
entry.push(value);
|
|
116
|
-
} else {
|
|
117
|
-
entries[key] = [entry, value];
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
entries[key] = value;
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
return entries;
|
|
124
|
-
},
|
|
125
|
-
// From
|
|
126
|
-
(v) => {
|
|
127
|
-
const formData = new FormData();
|
|
128
|
-
Object.entries(v).forEach(([key, value]) => {
|
|
129
|
-
if (Array.isArray(value)) {
|
|
130
|
-
value.forEach((val) => formData.append(key, val));
|
|
131
|
-
} else {
|
|
132
|
-
formData.append(key, value);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return formData;
|
|
136
|
-
}
|
|
137
|
-
),
|
|
138
|
-
createSerializer(
|
|
139
|
-
// Key
|
|
140
|
-
"bigint",
|
|
141
|
-
// Check
|
|
142
|
-
(v) => typeof v === "bigint",
|
|
143
|
-
// To
|
|
144
|
-
(v) => v.toString(),
|
|
145
|
-
// From
|
|
146
|
-
(v) => BigInt(v)
|
|
147
|
-
),
|
|
148
|
-
createSerializer(
|
|
149
|
-
// Key
|
|
150
|
-
"server-function",
|
|
151
|
-
// Check
|
|
152
|
-
(v) => typeof v === "function" && "functionId" in v && typeof v.functionId === "string",
|
|
153
|
-
// To
|
|
154
|
-
({ functionId }) => ({ functionId, __serverFn: true }),
|
|
155
|
-
// From, dummy impl. the actual server function lookup is done on the server in packages/start-server-core/src/server-functions-handler.ts
|
|
156
|
-
(v) => v
|
|
157
|
-
)
|
|
158
|
-
];
|
|
159
|
-
export {
|
|
160
|
-
startSerializer
|
|
161
|
-
};
|
|
162
|
-
//# sourceMappingURL=serializer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.js","sources":["../../src/serializer.ts"],"sourcesContent":["import { isPlainObject } from '@tanstack/router-core'\n\nexport interface StartSerializer {\n stringify: (obj: unknown) => string\n parse: (str: string) => unknown\n encode: <T>(value: T) => T\n decode: <T>(value: T) => T\n}\n\nexport type SerializerStringifyBy<T, TSerializable> = T extends TSerializable\n ? T\n : T extends (...args: Array<any>) => any\n ? 'Function is not serializable'\n : { [K in keyof T]: SerializerStringifyBy<T[K], TSerializable> }\n\nexport type SerializerParseBy<T, TSerializable> = T extends TSerializable\n ? T\n : unknown extends SerializerExtensions['ReadableStream']\n ? { [K in keyof T]: SerializerParseBy<T[K], TSerializable> }\n : T extends SerializerExtensions['ReadableStream']\n ? ReadableStream\n : { [K in keyof T]: SerializerParseBy<T[K], TSerializable> }\n\nexport interface DefaultSerializerExtensions {\n ReadableStream: unknown\n}\n\nexport interface SerializerExtensions extends DefaultSerializerExtensions {}\n\nexport type Serializable = Date | undefined | Error | FormData | bigint\n\nexport type SerializerStringify<T> = SerializerStringifyBy<T, Serializable>\n\nexport type SerializerParse<T> = SerializerParseBy<T, Serializable>\nexport const startSerializer: StartSerializer = {\n stringify: (value: any) =>\n JSON.stringify(value, function replacer(key, val) {\n const ogVal = this[key]\n const serializer = serializers.find((t) => t.stringifyCondition(ogVal))\n\n if (serializer) {\n return serializer.stringify(ogVal)\n }\n\n return val\n }),\n parse: (value: string) =>\n JSON.parse(value, function parser(key, val) {\n const ogVal = this[key]\n if (isPlainObject(ogVal)) {\n const serializer = serializers.find((t) => t.parseCondition(ogVal))\n\n if (serializer) {\n return serializer.parse(ogVal)\n }\n }\n\n return val\n }),\n encode: (value: any) => {\n // When encoding, dive first\n if (Array.isArray(value)) {\n return value.map((v) => startSerializer.encode(v))\n }\n\n if (isPlainObject(value)) {\n return Object.fromEntries(\n Object.entries(value).map(([key, v]) => [\n key,\n startSerializer.encode(v),\n ]),\n )\n }\n\n const serializer = serializers.find((t) => t.stringifyCondition(value))\n if (serializer) {\n return serializer.stringify(value)\n }\n\n return value\n },\n decode: (value: any) => {\n // Attempt transform first\n if (isPlainObject(value)) {\n const serializer = serializers.find((t) => t.parseCondition(value))\n if (serializer) {\n return serializer.parse(value)\n }\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => startSerializer.decode(v))\n }\n\n if (isPlainObject(value)) {\n return Object.fromEntries(\n Object.entries(value).map(([key, v]) => [\n key,\n startSerializer.decode(v),\n ]),\n )\n }\n\n return value\n },\n}\nconst createSerializer = <TKey extends string, TInput, TSerialized>(\n key: TKey,\n check: (value: any) => value is TInput,\n toValue: (value: TInput) => TSerialized,\n fromValue: (value: TSerialized) => TInput,\n) => ({\n key,\n stringifyCondition: check,\n stringify: (value: any) => ({ [`$${key}`]: toValue(value) }),\n parseCondition: (value: any) => Object.hasOwn(value, `$${key}`),\n parse: (value: any) => fromValue(value[`$${key}`]),\n})\n// Keep these ordered by predicted frequency\n// Make sure to keep DefaultSerializable in sync with these serializers\n// Also, make sure that they are unit tested in serializer.test.tsx\nconst serializers = [\n createSerializer(\n // Key\n 'undefined',\n // Check\n (v): v is undefined => v === undefined,\n // To\n () => 0,\n // From\n () => undefined,\n ),\n createSerializer(\n // Key\n 'date',\n // Check\n (v): v is Date => v instanceof Date,\n // To\n (v) => v.toISOString(),\n // From\n (v) => new Date(v),\n ),\n createSerializer(\n // Key\n 'error',\n // Check\n (v): v is Error => v instanceof Error,\n // To\n (v) => ({\n ...v,\n message: v.message,\n stack: process.env.NODE_ENV === 'development' ? v.stack : undefined,\n cause: v.cause,\n }),\n // From\n (v) => Object.assign(new Error(v.message), v),\n ),\n createSerializer(\n // Key\n 'formData',\n // Check\n (v): v is FormData => v instanceof FormData,\n // To\n (v) => {\n const entries: Record<\n string,\n Array<FormDataEntryValue> | FormDataEntryValue\n > = {}\n v.forEach((value, key) => {\n const entry = entries[key]\n if (entry !== undefined) {\n if (Array.isArray(entry)) {\n entry.push(value)\n } else {\n entries[key] = [entry, value]\n }\n } else {\n entries[key] = value\n }\n })\n return entries\n },\n // From\n (v) => {\n const formData = new FormData()\n Object.entries(v).forEach(([key, value]) => {\n if (Array.isArray(value)) {\n value.forEach((val) => formData.append(key, val))\n } else {\n formData.append(key, value)\n }\n })\n return formData\n },\n ),\n createSerializer(\n // Key\n 'bigint',\n // Check\n (v): v is bigint => typeof v === 'bigint',\n // To\n (v) => v.toString(),\n // From\n (v) => BigInt(v),\n ),\n createSerializer(\n // Key\n 'server-function',\n // Check\n (v): v is { functionId: string } =>\n typeof v === 'function' &&\n 'functionId' in v &&\n typeof v.functionId === 'string',\n // To\n ({ functionId }) => ({ functionId, __serverFn: true }),\n // From, dummy impl. the actual server function lookup is done on the server in packages/start-server-core/src/server-functions-handler.ts\n (v) => v,\n ),\n] as const\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { startSerializer as serializer } from '../serializer'
|
|
3
|
-
|
|
4
|
-
describe('transformer.stringify', () => {
|
|
5
|
-
it('should stringify dates', () => {
|
|
6
|
-
const date = new Date('2021-08-19T20:00:00.000Z')
|
|
7
|
-
expect(serializer.stringify(date)).toMatchInlineSnapshot(`
|
|
8
|
-
"{"$date":"2021-08-19T20:00:00.000Z"}"
|
|
9
|
-
`)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('should stringify undefined', () => {
|
|
13
|
-
expect(serializer.stringify(undefined)).toMatchInlineSnapshot(
|
|
14
|
-
`"{"$undefined":0}"`,
|
|
15
|
-
)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('should stringify object foo="bar"', () => {
|
|
19
|
-
expect(serializer.stringify({ foo: 'bar' })).toMatchInlineSnapshot(`
|
|
20
|
-
"{"foo":"bar"}"
|
|
21
|
-
`)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should stringify object foo=undefined', () => {
|
|
25
|
-
expect(serializer.stringify({ foo: undefined })).toMatchInlineSnapshot(
|
|
26
|
-
`"{"foo":{"$undefined":0}}"`,
|
|
27
|
-
)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should stringify object foo=Date', () => {
|
|
31
|
-
const date = new Date('2021-08-19T20:00:00.000Z')
|
|
32
|
-
expect(serializer.stringify({ foo: date })).toMatchInlineSnapshot(`
|
|
33
|
-
"{"foo":{"$date":"2021-08-19T20:00:00.000Z"}}"
|
|
34
|
-
`)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('should stringify empty FormData', () => {
|
|
38
|
-
const formData = new FormData()
|
|
39
|
-
expect(serializer.stringify(formData)).toMatchInlineSnapshot(
|
|
40
|
-
`"{"$formData":{}}"`,
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should stringify FormData with key-value pairs of foo="bar",name="Sean"', () => {
|
|
45
|
-
const formData = new FormData()
|
|
46
|
-
formData.append('foo', 'bar')
|
|
47
|
-
formData.append('name', 'Sean')
|
|
48
|
-
expect(serializer.stringify(formData)).toMatchInlineSnapshot(
|
|
49
|
-
`"{"$formData":{"foo":"bar","name":"Sean"}}"`,
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
it('should stringify FormData with multiple values for the same key', () => {
|
|
53
|
-
const formData = new FormData()
|
|
54
|
-
formData.append('foo', 'bar')
|
|
55
|
-
formData.append('foo', 'baz')
|
|
56
|
-
expect(serializer.stringify(formData)).toMatchInlineSnapshot(
|
|
57
|
-
`"{"$formData":{"foo":["bar","baz"]}}"`,
|
|
58
|
-
)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('should stringify bigint', () => {
|
|
62
|
-
const bigint = BigInt('9007199254740992')
|
|
63
|
-
expect(serializer.stringify(bigint)).toMatchInlineSnapshot(
|
|
64
|
-
`"{"$bigint":"9007199254740992"}"`,
|
|
65
|
-
)
|
|
66
|
-
})
|
|
67
|
-
it('should stringify object foo=bigint', () => {
|
|
68
|
-
const bigint = BigInt('9007199254740992')
|
|
69
|
-
expect(serializer.stringify({ foo: bigint })).toMatchInlineSnapshot(
|
|
70
|
-
`"{"foo":{"$bigint":"9007199254740992"}}"`,
|
|
71
|
-
)
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
describe('transformer.parse', () => {
|
|
76
|
-
it('should parse dates', () => {
|
|
77
|
-
const date = new Date('2021-08-19T20:00:00.000Z')
|
|
78
|
-
const str = serializer.stringify(date)
|
|
79
|
-
expect(serializer.parse(str)).toEqual(date)
|
|
80
|
-
expect(serializer.parse(str) instanceof Date).toBe(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('should parse undefined', () => {
|
|
84
|
-
const str = serializer.stringify(undefined)
|
|
85
|
-
expect(serializer.parse(str)).toBeUndefined()
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('should parse object foo="bar"', () => {
|
|
89
|
-
const obj = { foo: 'bar' }
|
|
90
|
-
const str = serializer.stringify(obj)
|
|
91
|
-
expect(serializer.parse(str)).toEqual(obj)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('should parse object foo=undefined', () => {
|
|
95
|
-
const obj = { foo: undefined }
|
|
96
|
-
const str = serializer.stringify(obj)
|
|
97
|
-
expect(serializer.parse(str)).toEqual(obj)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('should parse object foo=Date', () => {
|
|
101
|
-
const date = new Date('2021-08-19T20:00:00.000Z')
|
|
102
|
-
const obj = { foo: date }
|
|
103
|
-
const str = serializer.stringify(obj)
|
|
104
|
-
expect(serializer.parse(str)).toEqual(obj)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should parse empty FormData', () => {
|
|
108
|
-
const formData = new FormData()
|
|
109
|
-
const str = serializer.stringify(formData)
|
|
110
|
-
const parsed = serializer.parse(str) as FormData
|
|
111
|
-
expect(parsed).toBeInstanceOf(FormData)
|
|
112
|
-
expect([...parsed.entries()]).toEqual([])
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should parse FormData with key-value pairs of foo="bar",name="Sean"', () => {
|
|
116
|
-
const formData = new FormData()
|
|
117
|
-
formData.append('foo', 'bar')
|
|
118
|
-
formData.append('name', 'Sean')
|
|
119
|
-
const str = serializer.stringify(formData)
|
|
120
|
-
const parsed = serializer.parse(str) as FormData
|
|
121
|
-
expect(parsed).toBeInstanceOf(FormData)
|
|
122
|
-
expect([...parsed.entries()]).toEqual([
|
|
123
|
-
['foo', 'bar'],
|
|
124
|
-
['name', 'Sean'],
|
|
125
|
-
])
|
|
126
|
-
})
|
|
127
|
-
it('should parse FormData with multiple values for the same key', () => {
|
|
128
|
-
const formData = new FormData()
|
|
129
|
-
formData.append('foo', 'bar')
|
|
130
|
-
formData.append('foo', 'baz')
|
|
131
|
-
const str = serializer.stringify(formData)
|
|
132
|
-
const parsed = serializer.parse(str) as FormData
|
|
133
|
-
expect(parsed).toBeInstanceOf(FormData)
|
|
134
|
-
expect([...parsed.entries()]).toEqual([
|
|
135
|
-
['foo', 'bar'],
|
|
136
|
-
['foo', 'baz'],
|
|
137
|
-
])
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should parse bigint', () => {
|
|
141
|
-
const bigint = BigInt('9007199254740992')
|
|
142
|
-
const str = serializer.stringify(bigint)
|
|
143
|
-
expect(serializer.parse(str)).toEqual(bigint)
|
|
144
|
-
})
|
|
145
|
-
it('should parse object foo=bigint', () => {
|
|
146
|
-
const bigint = BigInt('9007199254740992')
|
|
147
|
-
const obj = { foo: bigint }
|
|
148
|
-
const str = serializer.stringify(obj)
|
|
149
|
-
expect(serializer.parse(str)).toEqual(obj)
|
|
150
|
-
})
|
|
151
|
-
})
|