@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-fetch",
3
- "version": "5.0.0-beta.80",
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/ast": "5.0.0-beta.79",
53
- "@kubb/core": "5.0.0-beta.79",
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
- "@kubb/renderer-jsx": "5.0.0-beta.79"
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 '@kubb/core'
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 '@kubb/renderer-jsx'
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 '@kubb/core'
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: baseURL ? `client.setConfig({ baseURL: ${JSON.stringify(baseURL)} })` : undefined,
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 '@kubb/core'
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'
@@ -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') query[name] = token
397
- else if (scheme.in === 'cookie') headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
398
- else headers[name] = token
399
- } else {
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
- * Builds the shared client core bound to a transport, exported by each plugin as `client` plus a `createClient` factory.
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
- export function createClientCore<TRequest = Request, TResponse = Response>(
440
- options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
441
- ): ClientInstance<TRequest, TResponse> {
442
- const { defaultTransport, ...initialConfig } = options
443
- let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
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
- const interceptors: Interceptors<TRequest, TResponse> = {
446
- request: createInterceptorStack<ResolvedRequest>(),
447
- response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>>(),
448
- error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>>(),
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 client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
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
- const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
485
+ await resolveAuth({
486
+ security: requestConfig.security,
487
+ auth: requestConfig.auth ?? config.auth,
488
+ headers,
489
+ query,
490
+ })
466
491
 
467
- await resolveAuth({
468
- security: requestConfig.security,
469
- auth: requestConfig.auth ?? config.auth,
470
- headers,
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
- if (requestConfig.cookies) {
475
- const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
476
- if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
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
- const rawBody = requestConfig.body
480
- const validatedBody = await runValidator(requestConfig.validator?.request, rawBody)
481
- const requestContentTypeBase = baseContentType(requestContentType)
482
- const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
483
- const body = contentCodec?.serialize
484
- ? contentCodec.serialize(validatedBody, requestContentType)
485
- : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
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
- const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
523
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
502
524
 
503
- let resolvedRequest: ResolvedRequest = {
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
- resolvedRequest = await interceptors.request.run(resolvedRequest)
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
- const parsedErrorData = !isSuccess ? await runValidator(requestConfig.validator?.error, decoded) : undefined
529
-
530
- if (!isSuccess && throwOnError) {
531
- const error = new ResponseError({
532
- data: parsedErrorData,
533
- status: result.status,
534
- statusText: result.statusText,
535
- contentType,
536
- request: result.request,
537
- response: result.response,
538
- })
539
- await interceptors.error.run(error)
540
- throw error
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
- const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
544
- const error = isSuccess ? undefined : parsedErrorData
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
- return {
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
- data,
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 = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
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: [config.baseURL, requestConfig.baseURL, requestConfig.url],
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; treat it as no data.
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)
@@ -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)