@kubb/plugin-fetch 5.0.0-beta.80 → 5.0.0-beta.84
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/index.cjs +43 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- package/src/generators/clientGenerator.tsx +2 -2
- package/src/plugin.ts +4 -3
- package/src/types.ts +1 -1
- package/templates/fetch.ts +189 -128
- package/templates/serializers.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-fetch",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.84",
|
|
4
4
|
"description": "Generate a slim, type-safe HTTP client with Kubb based on the native Fetch api.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -49,20 +49,18 @@
|
|
|
49
49
|
"registry": "https://registry.npmjs.org/"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@kubb/
|
|
53
|
-
"@kubb/
|
|
54
|
-
"@kubb/renderer-jsx": "5.0.0-beta.79",
|
|
55
|
-
"@kubb/plugin-ts": "5.0.0-beta.80",
|
|
56
|
-
"@kubb/plugin-zod": "5.0.0-beta.80"
|
|
52
|
+
"@kubb/plugin-ts": "5.0.0-beta.84",
|
|
53
|
+
"@kubb/plugin-zod": "5.0.0-beta.84"
|
|
57
54
|
},
|
|
58
55
|
"devDependencies": {
|
|
56
|
+
"kubb": "5.0.0-beta.84",
|
|
59
57
|
"typescript": "^6.0.3",
|
|
60
58
|
"@internals/client": "0.0.0",
|
|
61
59
|
"@internals/shared": "0.0.0",
|
|
62
60
|
"@internals/utils": "0.0.0"
|
|
63
61
|
},
|
|
64
62
|
"peerDependencies": {
|
|
65
|
-
"
|
|
63
|
+
"kubb": "5.0.0-beta.84"
|
|
66
64
|
},
|
|
67
65
|
"engines": {
|
|
68
66
|
"node": ">=22"
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
type SecurityDocument,
|
|
9
9
|
} from '@internals/client'
|
|
10
10
|
import { isEventStream, operationFileEntry } from '@internals/shared'
|
|
11
|
-
import { ast, defineGenerator } from '
|
|
11
|
+
import { ast, defineGenerator } from 'kubb/kit'
|
|
12
12
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
13
13
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
14
|
-
import { File, jsxRenderer } from '
|
|
14
|
+
import { File, jsxRenderer } from 'kubb/jsx'
|
|
15
15
|
import type { PluginFetch } from '../types.ts'
|
|
16
16
|
|
|
17
17
|
/**
|
package/src/plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { createSdkGenerator, defaultMacros, isValidatorEnabled, resolverClient } from '@internals/client'
|
|
3
3
|
import { createGroupConfig } from '@internals/shared'
|
|
4
|
-
import { definePlugin } from '
|
|
4
|
+
import { definePlugin } from 'kubb/kit'
|
|
5
5
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
6
6
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
7
7
|
import { clientGenerator } from './generators/clientGenerator.tsx'
|
|
@@ -22,7 +22,7 @@ export const pluginFetchName = 'plugin-fetch' satisfies PluginFetch['name']
|
|
|
22
22
|
*
|
|
23
23
|
* @example
|
|
24
24
|
* ```ts
|
|
25
|
-
* import { defineConfig } from 'kubb'
|
|
25
|
+
* import { defineConfig } from 'kubb/config'
|
|
26
26
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
27
27
|
* import { pluginFetch } from '@kubb/plugin-fetch'
|
|
28
28
|
*
|
|
@@ -80,6 +80,7 @@ export const pluginFetch = definePlugin<PluginFetch>((options) => {
|
|
|
80
80
|
ctx.addGenerator(...selectedGenerators)
|
|
81
81
|
|
|
82
82
|
const root = path.resolve(ctx.config.root, ctx.config.output.path)
|
|
83
|
+
const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
|
|
83
84
|
|
|
84
85
|
ctx.injectFile({
|
|
85
86
|
baseName: 'serializers.ts',
|
|
@@ -91,7 +92,7 @@ export const pluginFetch = definePlugin<PluginFetch>((options) => {
|
|
|
91
92
|
baseName: 'client.ts',
|
|
92
93
|
path: path.resolve(root, '.kubb/client.ts'),
|
|
93
94
|
copy: fetchClientTemplatePath,
|
|
94
|
-
footer:
|
|
95
|
+
footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
|
|
95
96
|
})
|
|
96
97
|
|
|
97
98
|
ctx.injectFile({
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PluginFactoryOptions } from '
|
|
1
|
+
import type { PluginFactoryOptions } from 'kubb/kit'
|
|
2
2
|
import type { Options, ResolvedOptions, ResolverClient } from '@internals/client'
|
|
3
3
|
|
|
4
4
|
export type { Options, ResolvedOptions, ResolverClient } from '@internals/client'
|
package/templates/fetch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers'
|
|
1
|
+
import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
|
|
2
2
|
import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
|
|
3
3
|
import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
|
|
4
4
|
|
|
@@ -323,6 +323,15 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
|
|
|
323
323
|
return Object.assign({}, ...sources.map(serializeHeaders))
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
function getHeader(headers: Record<string, string>, name: string): string | undefined {
|
|
327
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
|
|
328
|
+
return key ? headers[key] : undefined
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
332
|
+
return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase())
|
|
333
|
+
}
|
|
334
|
+
|
|
326
335
|
/**
|
|
327
336
|
* Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, shared by the send path and `getUrl`.
|
|
328
337
|
*/
|
|
@@ -393,10 +402,14 @@ export async function resolveAuth(params: {
|
|
|
393
402
|
|
|
394
403
|
if (scheme.type === 'apiKey') {
|
|
395
404
|
const name = scheme.name ?? 'Authorization'
|
|
396
|
-
if (scheme.in === 'query')
|
|
397
|
-
|
|
398
|
-
else
|
|
399
|
-
|
|
405
|
+
if (scheme.in === 'query') {
|
|
406
|
+
if (query[name] === undefined) query[name] = token
|
|
407
|
+
} else if (scheme.in === 'cookie') {
|
|
408
|
+
headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
|
|
409
|
+
} else if (!hasHeader(headers, name)) {
|
|
410
|
+
headers[name] = token
|
|
411
|
+
}
|
|
412
|
+
} else if (!hasHeader(headers, 'Authorization')) {
|
|
400
413
|
headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
|
|
401
414
|
}
|
|
402
415
|
return
|
|
@@ -434,73 +447,84 @@ function resolveContentType(contentType: ContentType | undefined): { request?: s
|
|
|
434
447
|
}
|
|
435
448
|
|
|
436
449
|
/**
|
|
437
|
-
*
|
|
450
|
+
* The per-concern serializers for a call, the per-call serializer winning over the client's and
|
|
451
|
+
* falling back to the defaults.
|
|
438
452
|
*/
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
453
|
+
function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
|
|
454
|
+
return {
|
|
455
|
+
querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
|
|
456
|
+
bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
|
|
457
|
+
pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
444
460
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Resolves everything a call needs before it touches the transport: merged headers with the
|
|
463
|
+
* negotiated content type, auth on headers or query, serialized cookies, the validated and
|
|
464
|
+
* serialized body, and the full URL.
|
|
465
|
+
*/
|
|
466
|
+
async function resolveRequest<TBody, TRequest, TResponse>({
|
|
467
|
+
config,
|
|
468
|
+
requestConfig,
|
|
469
|
+
}: {
|
|
470
|
+
config: ClientConfig<TRequest, TResponse>
|
|
471
|
+
requestConfig: RequestConfig<TBody, TRequest, TResponse>
|
|
472
|
+
}): Promise<{ request: ResolvedRequest; codecs: Record<string, Codec> }> {
|
|
473
|
+
const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
474
|
+
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
475
|
+
|
|
476
|
+
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
477
|
+
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
478
|
+
const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
|
|
479
|
+
if (responseContentType && !hasHeader(headers, 'accept')) {
|
|
480
|
+
headers['Accept'] = responseContentType
|
|
449
481
|
}
|
|
450
482
|
|
|
451
|
-
const
|
|
452
|
-
const transport = requestConfig.transport ?? config.transport ?? defaultTransport
|
|
453
|
-
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
454
|
-
const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
|
|
455
|
-
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
456
|
-
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
457
|
-
|
|
458
|
-
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
459
|
-
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
460
|
-
const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
|
|
461
|
-
if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
|
|
462
|
-
headers['Accept'] = responseContentType
|
|
463
|
-
}
|
|
483
|
+
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
464
484
|
|
|
465
|
-
|
|
485
|
+
await resolveAuth({
|
|
486
|
+
security: requestConfig.security,
|
|
487
|
+
auth: requestConfig.auth ?? config.auth,
|
|
488
|
+
headers,
|
|
489
|
+
query,
|
|
490
|
+
})
|
|
466
491
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
query,
|
|
472
|
-
})
|
|
492
|
+
if (requestConfig.cookies) {
|
|
493
|
+
const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
|
|
494
|
+
if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
|
|
495
|
+
}
|
|
473
496
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
497
|
+
const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
|
|
498
|
+
const requestContentTypeBase = baseContentType(requestContentType)
|
|
499
|
+
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
500
|
+
const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
|
|
501
|
+
const body = contentCodec?.serialize
|
|
502
|
+
? contentCodec.serialize(validatedBody, requestContentType)
|
|
503
|
+
: bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
|
|
504
|
+
// A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
|
|
505
|
+
if (body instanceof FormData) {
|
|
506
|
+
for (const key of Object.keys(headers)) {
|
|
507
|
+
if (key.toLowerCase() === 'content-type') delete headers[key]
|
|
477
508
|
}
|
|
509
|
+
} else if (requestContentTypeOption) {
|
|
510
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
511
|
+
} else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) {
|
|
512
|
+
headers['Content-Type'] = 'application/json'
|
|
513
|
+
}
|
|
478
514
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
|
|
487
|
-
if (body instanceof FormData) {
|
|
488
|
-
delete headers['Content-Type']
|
|
489
|
-
delete headers['content-type']
|
|
490
|
-
} else if (requestContentTypeOption) {
|
|
491
|
-
headers['Content-Type'] = requestContentTypeOption
|
|
492
|
-
}
|
|
493
|
-
const url = serializeUrl({
|
|
494
|
-
parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
|
|
495
|
-
pathParams: requestConfig.path ?? {},
|
|
496
|
-
search: querySerializer(query, requestConfig.styles?.query),
|
|
497
|
-
pathSerializer,
|
|
498
|
-
pathStyles: requestConfig.styles?.path,
|
|
499
|
-
})
|
|
515
|
+
const url = serializeUrl({
|
|
516
|
+
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
517
|
+
pathParams: requestConfig.path ?? {},
|
|
518
|
+
search: querySerializer(query, requestConfig.styles?.query),
|
|
519
|
+
pathSerializer,
|
|
520
|
+
pathStyles: requestConfig.styles?.path,
|
|
521
|
+
})
|
|
500
522
|
|
|
501
|
-
|
|
523
|
+
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
502
524
|
|
|
503
|
-
|
|
525
|
+
return {
|
|
526
|
+
codecs,
|
|
527
|
+
request: {
|
|
504
528
|
url,
|
|
505
529
|
method: (requestConfig.method ?? 'GET').toUpperCase(),
|
|
506
530
|
headers,
|
|
@@ -509,48 +533,86 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
509
533
|
credentials: requestConfig.credentials,
|
|
510
534
|
options,
|
|
511
535
|
responseType: requestConfig.responseType,
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
let result = await transport(resolvedRequest)
|
|
516
|
-
result = await interceptors.response.run(result)
|
|
517
|
-
|
|
518
|
-
const isSuccess = result.status >= 200 && result.status < 300
|
|
519
|
-
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
520
|
-
|
|
521
|
-
const contentType = result.contentType ?? getResponseContentType(result.headers)
|
|
522
|
-
let decoded = result.data
|
|
523
|
-
if (contentType) {
|
|
524
|
-
const codec = codecs[contentType]
|
|
525
|
-
if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType)
|
|
526
|
-
}
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
}
|
|
527
539
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Turns a transport result into the call result: decodes the body through the matching codec,
|
|
542
|
+
* validates it, and throws a `ResponseError` (after running the error interceptors) for a non-2xx
|
|
543
|
+
* response under `throwOnError`.
|
|
544
|
+
*/
|
|
545
|
+
async function settleResult<TRequest, TResponse>({
|
|
546
|
+
result,
|
|
547
|
+
codecs,
|
|
548
|
+
throwOnError,
|
|
549
|
+
validator,
|
|
550
|
+
errorInterceptors,
|
|
551
|
+
}: {
|
|
552
|
+
result: TransportResult<unknown, TRequest, TResponse>
|
|
553
|
+
codecs: Record<string, Codec>
|
|
554
|
+
throwOnError: boolean
|
|
555
|
+
validator: { response?: Validator; error?: Validator } | undefined
|
|
556
|
+
errorInterceptors: InterceptorStack<ResponseError<unknown, TRequest, TResponse>>
|
|
557
|
+
}): Promise<CallResult<TRequest, TResponse>> {
|
|
558
|
+
const isSuccess = result.status >= 200 && result.status < 300
|
|
559
|
+
const contentType = result.contentType ?? getResponseContentType(result.headers)
|
|
560
|
+
let decoded = result.data
|
|
561
|
+
if (contentType) {
|
|
562
|
+
const codec = codecs[contentType]
|
|
563
|
+
if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType)
|
|
564
|
+
}
|
|
542
565
|
|
|
543
|
-
|
|
544
|
-
const
|
|
566
|
+
if (isSuccess) {
|
|
567
|
+
const data = await runValidator(validator?.response, decoded)
|
|
568
|
+
return { status: result.status, data, error: undefined, contentType, request: result.request, response: result.response }
|
|
569
|
+
}
|
|
545
570
|
|
|
546
|
-
|
|
571
|
+
const error = await runValidator(validator?.error, decoded)
|
|
572
|
+
if (throwOnError) {
|
|
573
|
+
const responseError = new ResponseError({
|
|
574
|
+
data: error,
|
|
547
575
|
status: result.status,
|
|
548
|
-
|
|
549
|
-
error,
|
|
576
|
+
statusText: result.statusText,
|
|
550
577
|
contentType,
|
|
551
578
|
request: result.request,
|
|
552
579
|
response: result.response,
|
|
553
|
-
}
|
|
580
|
+
})
|
|
581
|
+
await errorInterceptors.run(responseError)
|
|
582
|
+
throw responseError
|
|
583
|
+
}
|
|
584
|
+
return { status: result.status, data: undefined, error, contentType, request: result.request, response: result.response }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Builds the shared client core bound to a transport, exported by each plugin as `client` plus a `createClient` factory.
|
|
589
|
+
*/
|
|
590
|
+
export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
591
|
+
options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
|
|
592
|
+
): ClientInstance<TRequest, TResponse> {
|
|
593
|
+
const { defaultTransport, ...initialConfig } = options
|
|
594
|
+
let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
|
|
595
|
+
|
|
596
|
+
const interceptors: Interceptors<TRequest, TResponse> = {
|
|
597
|
+
request: createInterceptorStack<ResolvedRequest>(),
|
|
598
|
+
response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>>(),
|
|
599
|
+
error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>>(),
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
603
|
+
const transport = requestConfig.transport ?? config.transport ?? defaultTransport
|
|
604
|
+
const { request, codecs } = await resolveRequest({ config, requestConfig })
|
|
605
|
+
|
|
606
|
+
const resolvedRequest = await interceptors.request.run(request)
|
|
607
|
+
const result = await interceptors.response.run(await transport(resolvedRequest))
|
|
608
|
+
|
|
609
|
+
return settleResult({
|
|
610
|
+
result,
|
|
611
|
+
codecs,
|
|
612
|
+
throwOnError: requestConfig.throwOnError ?? config.throwOnError ?? true,
|
|
613
|
+
validator: requestConfig.validator,
|
|
614
|
+
errorInterceptors: interceptors.error,
|
|
615
|
+
})
|
|
554
616
|
}) as ClientInstance<TRequest, TResponse>
|
|
555
617
|
|
|
556
618
|
client.getConfig = () => config
|
|
@@ -559,11 +621,10 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
559
621
|
return config
|
|
560
622
|
}
|
|
561
623
|
client.getUrl = (requestConfig) => {
|
|
562
|
-
const querySerializer =
|
|
563
|
-
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
624
|
+
const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
564
625
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
565
626
|
return serializeUrl({
|
|
566
|
-
parts: [
|
|
627
|
+
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
567
628
|
pathParams: requestConfig.path ?? {},
|
|
568
629
|
search: querySerializer(query, requestConfig.styles?.query),
|
|
569
630
|
pathSerializer,
|
|
@@ -607,7 +668,7 @@ async function parseResponse(response: Response, responseType?: ResponseType): P
|
|
|
607
668
|
case 'stream':
|
|
608
669
|
return response.body ?? undefined
|
|
609
670
|
case 'json': {
|
|
610
|
-
// An empty body with a JSON content-type would make response.json() throw
|
|
671
|
+
// An empty body with a JSON content-type would make response.json() throw, so treat it as no data.
|
|
611
672
|
const body = await response.text()
|
|
612
673
|
return body ? JSON.parse(body) : undefined
|
|
613
674
|
}
|
|
@@ -622,6 +683,34 @@ async function parseResponse(response: Response, responseType?: ResponseType): P
|
|
|
622
683
|
}
|
|
623
684
|
}
|
|
624
685
|
|
|
686
|
+
/**
|
|
687
|
+
* The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
|
|
688
|
+
*/
|
|
689
|
+
const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
|
|
690
|
+
const init: RequestInit = {
|
|
691
|
+
...request.options, // cache, mode, redirect, keepalive, duplex, next, …
|
|
692
|
+
method: request.method,
|
|
693
|
+
headers: request.headers,
|
|
694
|
+
body: request.body,
|
|
695
|
+
signal: request.signal,
|
|
696
|
+
}
|
|
697
|
+
if (request.credentials) init.credentials = request.credentials
|
|
698
|
+
|
|
699
|
+
const nativeRequest = new Request(request.url, init)
|
|
700
|
+
const response = await globalThis.fetch(nativeRequest)
|
|
701
|
+
const data = await parseResponse(response, request.responseType)
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
data,
|
|
705
|
+
status: response.status,
|
|
706
|
+
statusText: response.statusText,
|
|
707
|
+
headers: response.headers,
|
|
708
|
+
contentType: getResponseContentType(response.headers),
|
|
709
|
+
request: nativeRequest,
|
|
710
|
+
response,
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
625
714
|
/**
|
|
626
715
|
* One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
|
|
627
716
|
*/
|
|
@@ -724,34 +813,6 @@ export async function toEventStream<TData = unknown>(result: Promise<{ data: unk
|
|
|
724
813
|
}
|
|
725
814
|
}
|
|
726
815
|
|
|
727
|
-
/**
|
|
728
|
-
* The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
|
|
729
|
-
*/
|
|
730
|
-
const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
|
|
731
|
-
const init: RequestInit = {
|
|
732
|
-
...request.options, // cache, mode, redirect, keepalive, duplex, next, …
|
|
733
|
-
method: request.method,
|
|
734
|
-
headers: request.headers,
|
|
735
|
-
body: request.body,
|
|
736
|
-
signal: request.signal,
|
|
737
|
-
}
|
|
738
|
-
if (request.credentials) init.credentials = request.credentials
|
|
739
|
-
|
|
740
|
-
const nativeRequest = new Request(request.url, init)
|
|
741
|
-
const response = await globalThis.fetch(nativeRequest)
|
|
742
|
-
const data = await parseResponse(response, request.responseType)
|
|
743
|
-
|
|
744
|
-
return {
|
|
745
|
-
data,
|
|
746
|
-
status: response.status,
|
|
747
|
-
statusText: response.statusText,
|
|
748
|
-
headers: response.headers,
|
|
749
|
-
contentType: getResponseContentType(response.headers),
|
|
750
|
-
request: nativeRequest,
|
|
751
|
-
response,
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
816
|
export const client = createClientCore({ defaultTransport })
|
|
756
817
|
|
|
757
818
|
export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)
|
package/templates/serializers.ts
CHANGED
|
@@ -122,6 +122,10 @@ function isFormBody(body: unknown): body is BodyInit {
|
|
|
122
122
|
)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export function isDefaultJsonBody(body: unknown): boolean {
|
|
126
|
+
return body !== undefined && body !== null && !isFormBody(body)
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void {
|
|
126
130
|
if (value === undefined || value === null) return
|
|
127
131
|
if (value instanceof Blob) formData.append(key, value)
|