Skip to content

Commit 28325f8

Browse files
feat: add buildServerFnUrl
1 parent 2c9f5c0 commit 28325f8

7 files changed

Lines changed: 244 additions & 85 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { buildServerFnUrlFromBase } from './client-rpc/serverFnUrl'
2+
import type { ServerFnFetcherTypes } from './createServerFn'
3+
import type { IntersectAllValidatorInputs } from './createMiddleware'
4+
5+
type BuildServerFnUrlData<TServerFn> =
6+
TServerFn extends ServerFnFetcherTypes<
7+
'GET',
8+
infer TMiddlewares,
9+
infer TInputValidator
10+
>
11+
? IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
12+
: never
13+
14+
type GetServerFn = {
15+
url: string
16+
} & ServerFnFetcherTypes<'GET', any, any>
17+
18+
export function buildServerFnUrl<TServerFn extends GetServerFn>(
19+
serverFn: TServerFn,
20+
...args: undefined extends BuildServerFnUrlData<TServerFn>
21+
? [data?: BuildServerFnUrlData<TServerFn>]
22+
: [data: BuildServerFnUrlData<TServerFn>]
23+
): Promise<string> {
24+
return buildServerFnUrlFromBase(
25+
serverFn.url,
26+
args.length ? { data: args[0] } : undefined,
27+
)
28+
}

packages/start-client-core/src/client-rpc/serverFnFetcher.ts

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import {
22
createRawStreamDeserializePlugin,
3-
encode,
3+
hasKeys,
44
invariant,
55
isNotFound,
66
parseRedirect,
77
} from '@tanstack/router-core'
8-
import { fromCrossJSON, toJSONAsync } from 'seroval'
9-
import { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins'
8+
import { fromCrossJSON } from 'seroval'
9+
import { getDefaultSerovalPlugins as getSerovalPlugins } from '../getDefaultSerovalPlugins'
1010
import {
1111
TSS_CONTENT_TYPE_FRAMED,
1212
TSS_FORMDATA_CONTEXT,
1313
X_TSS_RAW_RESPONSE,
1414
X_TSS_SERIALIZED,
1515
validateFramedProtocolVersion,
1616
} from '../constants'
17+
import {
18+
buildServerFnUrlFromBase,
19+
serializeServerFnPayload,
20+
serializeServerFnPayloadValue,
21+
} from './serverFnUrl'
1722
import { createFrameDecoder } from './frame-decoder'
1823
import type { FunctionMiddlewareClientFnOptions } from '../createMiddleware'
19-
import type { Plugin as SerovalPlugin } from 'seroval'
20-
21-
let serovalPlugins: Array<SerovalPlugin<any, any>> | null = null
2224

2325
/**
2426
* Current async post-processing context for deserialization.
@@ -84,19 +86,6 @@ async function awaitPostProcessPromises(
8486
}
8587
}
8688

87-
/**
88-
* Checks if an object has at least one own enumerable property.
89-
* More efficient than Object.keys(obj).length > 0 as it short-circuits on first property.
90-
*/
91-
const hop = Object.prototype.hasOwnProperty
92-
function hasOwnProperties(obj: object): boolean {
93-
for (const _ in obj) {
94-
if (hop.call(obj, _)) {
95-
return true
96-
}
97-
}
98-
return false
99-
}
10089
// caller =>
10190
// serverFnFetcher =>
10291
// client =>
@@ -112,9 +101,6 @@ export async function serverFnFetcher(
112101
args: Array<any>,
113102
handler: (url: string, requestInit: RequestInit) => Promise<Response>,
114103
) {
115-
if (!serovalPlugins) {
116-
serovalPlugins = getDefaultSerovalPlugins()
117-
}
118104
const _first = args[0]
119105

120106
const first = _first as FunctionMiddlewareClientFnOptions<any, any, any> & {
@@ -139,20 +125,7 @@ export async function serverFnFetcher(
139125

140126
// If the method is GET, we need to move the payload to the query string
141127
if (first.method === 'GET') {
142-
if (type === 'formData') {
143-
throw new Error('FormData is not supported with GET requests')
144-
}
145-
const serializedPayload = await serializePayload(first)
146-
if (serializedPayload !== undefined) {
147-
const encodedPayload = encode({
148-
payload: serializedPayload,
149-
})
150-
if (url.includes('?')) {
151-
url += `&${encodedPayload}`
152-
} else {
153-
url += `?${encodedPayload}`
154-
}
155-
}
128+
url = await buildServerFnUrlFromBase(url, first)
156129
}
157130

158131
let body = undefined
@@ -174,49 +147,21 @@ export async function serverFnFetcher(
174147
)
175148
}
176149

177-
async function serializePayload(
178-
opts: FunctionMiddlewareClientFnOptions<any, any, any>,
179-
): Promise<string | undefined> {
180-
let payloadAvailable = false
181-
const payloadToSerialize: any = {}
182-
if (opts.data !== undefined) {
183-
payloadAvailable = true
184-
payloadToSerialize['data'] = opts.data
185-
}
186-
187-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
188-
if (opts.context && hasOwnProperties(opts.context)) {
189-
payloadAvailable = true
190-
payloadToSerialize['context'] = opts.context
191-
}
192-
193-
if (payloadAvailable) {
194-
return serialize(payloadToSerialize)
195-
}
196-
return undefined
197-
}
198-
199-
async function serialize(data: any) {
200-
return JSON.stringify(
201-
await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins! })),
202-
)
203-
}
204-
205150
async function getFetchBody(
206151
opts: FunctionMiddlewareClientFnOptions<any, any, any>,
207152
): Promise<{ body: FormData | string; contentType?: string } | undefined> {
208153
if (opts.data instanceof FormData) {
209154
let serializedContext = undefined
210155
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
211-
if (opts.context && hasOwnProperties(opts.context)) {
212-
serializedContext = await serialize(opts.context)
156+
if (opts.context && hasKeys(opts.context)) {
157+
serializedContext = await serializeServerFnPayloadValue(opts.context)
213158
}
214159
if (serializedContext !== undefined) {
215160
opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext)
216161
}
217162
return { body: opts.data }
218163
}
219-
const serializedBody = await serializePayload(opts)
164+
const serializedBody = await serializeServerFnPayload(opts)
220165
if (serializedBody) {
221166
return { body: serializedBody, contentType: 'application/json' }
222167
}
@@ -281,7 +226,7 @@ async function getResponse(fn: () => Promise<Response>) {
281226
// Create deserialize plugin that wires up the raw streams
282227
const rawStreamPlugin =
283228
createRawStreamDeserializePlugin(getOrCreateStream)
284-
const plugins = [rawStreamPlugin, ...(serovalPlugins || [])]
229+
const plugins = [rawStreamPlugin, ...getSerovalPlugins()]
285230

286231
const refs = new Map()
287232
result = await processFramedResponse({
@@ -299,7 +244,7 @@ async function getResponse(fn: () => Promise<Response>) {
299244
const postProcessPromises: Array<Promise<unknown>> = []
300245
setPostProcessContext(postProcessPromises)
301246
try {
302-
result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
247+
result = fromCrossJSON(jsonPayload, { plugins: getSerovalPlugins() })
303248
} finally {
304249
setPostProcessContext(null)
305250
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { encode, hasKeys } from '@tanstack/router-core'
2+
import { toJSONAsync } from 'seroval'
3+
import { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins'
4+
5+
type ServerFnUrlPayloadOptions = {
6+
data?: any
7+
context?: any
8+
}
9+
10+
export async function buildServerFnUrlFromBase(
11+
url: string,
12+
opts?: ServerFnUrlPayloadOptions,
13+
): Promise<string> {
14+
if (typeof FormData !== 'undefined' && opts?.data instanceof FormData) {
15+
throw new Error('FormData is not supported with GET requests')
16+
}
17+
18+
const serializedPayload = await serializeServerFnPayload(opts)
19+
if (serializedPayload === undefined) {
20+
return url
21+
}
22+
23+
const encodedPayload = encode({
24+
payload: serializedPayload,
25+
})
26+
27+
return url.includes('?')
28+
? `${url}&${encodedPayload}`
29+
: `${url}?${encodedPayload}`
30+
}
31+
32+
export async function serializeServerFnPayload(
33+
opts?: ServerFnUrlPayloadOptions,
34+
): Promise<string | undefined> {
35+
let payloadAvailable = false
36+
const payloadToSerialize: any = {}
37+
if (opts?.data !== undefined) {
38+
payloadAvailable = true
39+
payloadToSerialize['data'] = opts.data
40+
}
41+
42+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
43+
if (opts?.context && hasKeys(opts.context)) {
44+
payloadAvailable = true
45+
payloadToSerialize['context'] = opts.context
46+
}
47+
48+
if (payloadAvailable) {
49+
return serializeServerFnPayloadValue(payloadToSerialize)
50+
}
51+
return undefined
52+
}
53+
54+
export async function serializeServerFnPayloadValue(data: any) {
55+
return JSON.stringify(
56+
await Promise.resolve(
57+
toJSONAsync(data, { plugins: getDefaultSerovalPlugins() }),
58+
),
59+
)
60+
}

packages/start-client-core/src/createServerFn.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export type ServerFnStrictOutput<TStrict extends ServerFnStrict> =
5757
: true
5858

5959
export type CreateServerFn<TRegister> = <
60-
TMethod extends Method,
60+
TMethod extends Method = 'GET',
6161
TStrict extends ServerFnStrict = true,
6262
TResponse = unknown,
6363
TMiddlewares = undefined,
@@ -368,10 +368,15 @@ export type CompiledFetcherFnOptions = {
368368
context?: any
369369
}
370370

371-
export type Fetcher<TMiddlewares, TInputValidator, TResponse> =
371+
export type Fetcher<
372+
TMiddlewares,
373+
TInputValidator,
374+
TResponse,
375+
TMethod extends Method = Method,
376+
> =
372377
undefined extends IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
373-
? OptionalFetcher<TMiddlewares, TInputValidator, TResponse>
374-
: RequiredFetcher<TMiddlewares, TInputValidator, TResponse>
378+
? OptionalFetcher<TMiddlewares, TInputValidator, TResponse, TMethod>
379+
: RequiredFetcher<TMiddlewares, TInputValidator, TResponse, TMethod>
375380

376381
export interface FetcherBase {
377382
[TSS_SERVER_FUNCTION]: true
@@ -385,24 +390,41 @@ export interface FetcherBase {
385390
}) => Promise<unknown>
386391
}
387392

388-
export interface OptionalFetcher<
393+
export type OptionalFetcher<
389394
TMiddlewares,
390395
TInputValidator,
391396
TResponse,
392-
> extends FetcherBase {
393-
(
394-
options?: OptionalFetcherDataOptions<TMiddlewares, TInputValidator>,
395-
): Promise<Awaited<TResponse>>
396-
}
397+
TMethod extends Method = Method,
398+
> = FetcherBase &
399+
ServerFnFetcherTypes<TMethod, TMiddlewares, TInputValidator> & {
400+
(
401+
options?: OptionalFetcherDataOptions<TMiddlewares, TInputValidator>,
402+
): Promise<Awaited<TResponse>>
403+
}
397404

398-
export interface RequiredFetcher<
405+
export type RequiredFetcher<
399406
TMiddlewares,
400407
TInputValidator,
401408
TResponse,
402-
> extends FetcherBase {
403-
(
404-
opts: RequiredFetcherDataOptions<TMiddlewares, TInputValidator>,
405-
): Promise<Awaited<TResponse>>
409+
TMethod extends Method = Method,
410+
> = FetcherBase &
411+
ServerFnFetcherTypes<TMethod, TMiddlewares, TInputValidator> & {
412+
(
413+
opts: RequiredFetcherDataOptions<TMiddlewares, TInputValidator>,
414+
): Promise<Awaited<TResponse>>
415+
}
416+
417+
export interface ServerFnFetcherTypes<
418+
in out TMethod extends Method,
419+
in out TMiddlewares,
420+
in out TInputValidator,
421+
> {
422+
'~serverFnTypes': {
423+
method: TMethod
424+
middlewares: TMiddlewares
425+
inputValidator: TInputValidator
426+
allInput: IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
427+
}
406428
}
407429

408430
// Ideally, this type should just be `export type CustomFetch = typeof globalThis.fetch`, but that conflicts with the type overrides the `bun-types` package - a dependency of unplugin.
@@ -735,7 +757,7 @@ export interface ServerFnHandler<
735757
TNewResponse,
736758
TStrict
737759
>,
738-
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse>
760+
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse, TMethod>
739761
}
740762

741763
export interface ServerFnBuilder<

packages/start-client-core/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
type IsomorphicFnBase,
1616
} from '@tanstack/start-fn-stubs'
1717
export { createServerFn } from './createServerFn'
18+
export { buildServerFnUrl } from './buildServerFnUrl'
1819
export {
1920
createMiddleware,
2021
type IntersectAllValidatorInputs,
@@ -65,6 +66,7 @@ export type {
6566
FetcherBaseOptions,
6667
ServerFn,
6768
ServerFnCtx,
69+
ServerFnFetcherTypes,
6870
ServerFnOptions,
6971
ServerFnStrict,
7072
ServerFnStrictInput,

0 commit comments

Comments
 (0)