@kubb/plugin-fetch 5.0.0-beta.73

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.
@@ -0,0 +1,578 @@
1
+ /**
2
+ * HTTP status codes treated as a success. A resolved call only ever carries a body from one of
3
+ * these; everything else is an error (thrown by default, or surfaced on `error`).
4
+ */
5
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
6
+
7
+ /**
8
+ * The success members of a per-status responses record (`{ '200': ...; '404': ... }`).
9
+ */
10
+ export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
11
+
12
+ /**
13
+ * The error members of a per-status responses record — every documented status that is not a 2xx.
14
+ */
15
+ export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
16
+
17
+ /**
18
+ * Converts a response record's string status key to its numeric literal (`'200'` becomes `200`).
19
+ * Non-numeric keys such as the OpenAPI `default` response stay `number`.
20
+ */
21
+ export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
22
+
23
+ /**
24
+ * One result variant for a single documented status. `status` is the numeric literal at the top
25
+ * level, so a `switch (result.status)` narrows `data` (a 2xx status) or `error` (everything else) to
26
+ * that status's payload.
27
+ */
28
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
29
+ ? { status: ToStatusNumber<TStatus>; data: TResponses[TStatus]; error: undefined; request: TRequest; response: TResponse }
30
+ : { status: ToStatusNumber<TStatus>; data: undefined; error: TResponses[TStatus]; request: TRequest; response: TResponse }
31
+
32
+ /**
33
+ * The union of every documented status' result variant.
34
+ */
35
+ export type ResultUnion<TResponses, TRequest, TResponse> = {
36
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
37
+ }[keyof TResponses]
38
+
39
+ /**
40
+ * The union of just the success (2xx) status variants. Selected by status code, not by `error`, so an
41
+ * untyped (`any`) error payload can never widen a success result's `data`.
42
+ */
43
+ export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
44
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
45
+ }[Extract<keyof TResponses, SuccessStatusCode>]
46
+
47
+ /**
48
+ * The shape every generated function returns, discriminated by the top-level `status`. With
49
+ * `throwOnError` (the default) a resolved call always means success, so the result is the union of the
50
+ * 2xx variants and `error` is `undefined`; without it every documented status is a variant, so a
51
+ * `switch (result.status)` (or narrowing on `error`) narrows `data` and `error` to that status's
52
+ * payload. Operations with no typed responses fall back to a `status`/`request`/`response`-only result.
53
+ */
54
+ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
55
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
56
+ ? { status: number; data: SuccessOf<TResponses>; error: undefined; request: TRequest; response: TResponse }
57
+ : SuccessResultUnion<TResponses, TRequest, TResponse>
58
+ : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
59
+ ? { status: number; data: undefined; error: undefined; request: TRequest; response: TResponse }
60
+ : ResultUnion<TResponses, TRequest, TResponse>
61
+
62
+ /**
63
+ * The data-shaped keys of the grouped options object. `Options` subtracts these from the runtime
64
+ * `RequestConfig` and adds them back, typed per operation, from the generated `<Name>Request` type.
65
+ */
66
+ export type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
67
+
68
+ export type HeaderValue = string | number | boolean | null | undefined | object
69
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
70
+ export type RequestCredentials = 'omit' | 'same-origin' | 'include'
71
+ export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
72
+
73
+ /**
74
+ * Serializes the query object into a search string. Array and object members follow the configured
75
+ * style (`form` with `explode` by default; `deepObject` for nested objects).
76
+ */
77
+ export type QuerySerializer = (params: Record<string, unknown>) => string
78
+
79
+ /**
80
+ * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
81
+ * `ArrayBuffer`, and string bodies pass through untouched.
82
+ */
83
+ export type BodySerializer = (body: unknown, contentType?: string) => BodyInit | undefined
84
+
85
+ /**
86
+ * Parses a value before it is sent or after it is received, returning the parsed (and optionally
87
+ * transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` hooks.
88
+ */
89
+ export type Parser<T = unknown> = (value: T) => T | Promise<T>
90
+
91
+ /**
92
+ * A resolved security scheme carried on each generated call's `security` array. The runtime passes it
93
+ * to the configured `auth` resolver and places the returned token accordingly.
94
+ */
95
+ export type Auth = {
96
+ type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
97
+ scheme?: 'bearer' | 'basic'
98
+ name?: string
99
+ in?: 'header' | 'query' | 'cookie'
100
+ }
101
+
102
+ /**
103
+ * The token a consumer returns for a scheme, or `undefined` to skip it. Bearer and basic schemes are
104
+ * prefixed by the runtime (basic is base64-encoded), so return the raw token or `user:password`.
105
+ */
106
+ export type AuthToken = string | undefined
107
+
108
+ /**
109
+ * Resolves the token for a security scheme: either a static token used for every scheme, or a
110
+ * callback called once per scheme on a guarded operation until one returns a token.
111
+ */
112
+ export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
113
+
114
+ /**
115
+ * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
116
+ * from the grouped options; everything else is plain request configuration.
117
+ */
118
+ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Response> = {
119
+ baseURL?: string
120
+ url?: string
121
+ method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
122
+ path?: Record<string, unknown>
123
+ query?: unknown
124
+ params?: unknown
125
+ body?: TBody
126
+ headers?: HeadersInit
127
+ signal?: AbortSignal
128
+ credentials?: RequestCredentials
129
+ contentType?: string
130
+ responseType?: ResponseType
131
+ throwOnError?: boolean
132
+ client?: ClientInstance<TRequest, TResponse>
133
+ transport?: Transport<TRequest, TResponse>
134
+ querySerializer?: QuerySerializer
135
+ bodySerializer?: BodySerializer
136
+ parser?: { request?: Parser; response?: Parser }
137
+ security?: Array<Auth>
138
+ auth?: AuthResolver
139
+ }
140
+
141
+ /**
142
+ * The grouped options object passed to every generated function: the request config minus the
143
+ * data-shaped keys and the literal `url`, plus the per-operation `<Name>Request`.
144
+ */
145
+ export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = Omit<
146
+ RequestConfig<unknown, TRequest, TResponse>,
147
+ keyof DataShape | 'url'
148
+ > &
149
+ TData & {
150
+ client?: ClientInstance<TRequest, TResponse>
151
+ throwOnError?: ThrowOnError
152
+ }
153
+
154
+ /**
155
+ * Client-level configuration shared by every call an instance makes. Per-call `RequestConfig`
156
+ * overrides these.
157
+ */
158
+ export type ClientConfig<TRequest = Request, TResponse = Response> = {
159
+ baseURL?: string
160
+ headers?: HeadersInit
161
+ credentials?: RequestCredentials
162
+ throwOnError?: boolean
163
+ transport?: Transport<TRequest, TResponse>
164
+ querySerializer?: QuerySerializer
165
+ bodySerializer?: BodySerializer
166
+ auth?: AuthResolver
167
+ }
168
+
169
+ /**
170
+ * The normalized request the transport receives. The shared core does all serialization, auth, and
171
+ * header work; the transport only performs the send.
172
+ */
173
+ export type ResolvedRequest = {
174
+ url: string
175
+ method: string
176
+ headers: Record<string, string>
177
+ body?: BodyInit
178
+ signal?: AbortSignal
179
+ credentials?: RequestCredentials
180
+ responseType?: ResponseType
181
+ }
182
+
183
+ /**
184
+ * What a transport returns: the parsed body plus the native request/response objects, kept reachable
185
+ * so status, headers, and the raw body never have to grow the result type.
186
+ */
187
+ export type TransportResult<TData = unknown, TRequest = Request, TResponse = Response> = {
188
+ data: TData
189
+ status: number
190
+ statusText: string
191
+ headers: Headers
192
+ request: TRequest
193
+ response: TResponse
194
+ }
195
+
196
+ /**
197
+ * The per-plugin send. plugin-fetch wraps `globalThis.fetch`, plugin-axios an axios instance, and
198
+ * plugin-ky a ky instance. Supplied to `createClientCore` as `defaultTransport`.
199
+ */
200
+ export type Transport<TRequest = Request, TResponse = Response> = (request: ResolvedRequest) => Promise<TransportResult<unknown, TRequest, TResponse>>
201
+
202
+ /**
203
+ * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper.
204
+ */
205
+ export type CallResult<TRequest = Request, TResponse = Response> = {
206
+ status: number
207
+ data: unknown
208
+ error: unknown
209
+ request: TRequest
210
+ response: TResponse
211
+ }
212
+
213
+ /**
214
+ * A registered interceptor with its ejection id.
215
+ */
216
+ export type InterceptorFn<T> = (value: T) => T | Promise<T>
217
+
218
+ /**
219
+ * A single interceptor channel — request, response, or error — with a transport-agnostic
220
+ * `use` / `eject` / `update` API.
221
+ */
222
+ export type InterceptorStack<T> = {
223
+ use: (fn: InterceptorFn<T>) => number
224
+ eject: (id: number) => void
225
+ update: (id: number, fn: InterceptorFn<T>) => void
226
+ run: (value: T) => Promise<T>
227
+ }
228
+
229
+ /**
230
+ * The three interceptor channels every client instance exposes.
231
+ */
232
+ export type Interceptors<TRequest = Request, TResponse = Response> = {
233
+ request: InterceptorStack<ResolvedRequest>
234
+ response: InterceptorStack<TransportResult<unknown, TRequest, TResponse>>
235
+ error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>>
236
+ }
237
+
238
+ /**
239
+ * A client instance: the callable send plus configuration, interceptors, and an isolated
240
+ * `createClient` factory bound to the same transport.
241
+ */
242
+ export type ClientInstance<TRequest = Request, TResponse = Response> = {
243
+ <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
244
+ getConfig: () => ClientConfig<TRequest, TResponse>
245
+ setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
246
+ interceptors: Interceptors<TRequest, TResponse>
247
+ createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
248
+ }
249
+
250
+ /**
251
+ * Thrown for responses outside the 2xx range, so a resolved call always means success. The parsed
252
+ * error body and the native request/response stay reachable on the error.
253
+ */
254
+ export class ResponseError<TError = unknown, TRequest = Request, TResponse = Response> extends Error {
255
+ data: TError
256
+ status: number
257
+ statusText: string
258
+ request: TRequest
259
+ response: TResponse
260
+
261
+ constructor(config: { data: TError; status: number; statusText: string; request: TRequest; response: TResponse }) {
262
+ super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
263
+ this.name = 'ResponseError'
264
+ this.data = config.data
265
+ this.status = config.status
266
+ this.statusText = config.statusText
267
+ this.request = config.request
268
+ this.response = config.response
269
+ }
270
+ }
271
+
272
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
273
+
274
+ function isFormBody(body: unknown): body is BodyInit {
275
+ return (
276
+ body instanceof FormData ||
277
+ body instanceof URLSearchParams ||
278
+ body instanceof Blob ||
279
+ body instanceof ArrayBuffer ||
280
+ ArrayBuffer.isView(body) ||
281
+ typeof body === 'string'
282
+ )
283
+ }
284
+
285
+ /**
286
+ * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
287
+ * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
288
+ */
289
+ export const defaultBodySerializer: BodySerializer = (body, contentType) => {
290
+ if (body === undefined || body === null) return undefined
291
+ if (isFormBody(body)) return body as BodyInit
292
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
293
+ return new URLSearchParams(body as Record<string, string>)
294
+ }
295
+ return JSON.stringify(body)
296
+ }
297
+
298
+ function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
299
+ if (value === undefined || value === null) return
300
+ if (Array.isArray(value)) {
301
+ for (const item of value) appendQueryValue(search, key, item)
302
+ return
303
+ }
304
+ if (typeof value === 'object') {
305
+ for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
306
+ appendQueryValue(search, `${key}[${prop}]`, propValue)
307
+ }
308
+ return
309
+ }
310
+ search.append(key, String(value))
311
+ }
312
+
313
+ /**
314
+ * Default query serializer: arrays explode into repeated keys and nested objects use the
315
+ * `deepObject` style (`key[prop]=value`).
316
+ */
317
+ export const defaultQuerySerializer: QuerySerializer = (params) => {
318
+ const search = new URLSearchParams()
319
+ for (const [key, value] of Object.entries(params)) {
320
+ appendQueryValue(search, key, value)
321
+ }
322
+ return search.toString()
323
+ }
324
+
325
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
326
+ if (!headers) return {}
327
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
328
+ const result: Record<string, string> = {}
329
+ for (const [key, value] of entries) {
330
+ if (value === undefined || value === null) continue
331
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
332
+ }
333
+ return result
334
+ }
335
+
336
+ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string, string> {
337
+ return Object.assign({}, ...sources.map(serializeHeaders))
338
+ }
339
+
340
+ /**
341
+ * Creates a transport-agnostic interceptor channel. Interceptors run in registration order; `eject`
342
+ * removes one by id and `update` swaps its function in place without reordering.
343
+ */
344
+ export function createInterceptorStack<T>(): InterceptorStack<T> {
345
+ let entries: Array<{ id: number; fn: InterceptorFn<T> }> = []
346
+ let counter = 0
347
+ return {
348
+ use(fn) {
349
+ const id = ++counter
350
+ entries.push({ id, fn })
351
+ return id
352
+ },
353
+ eject(id) {
354
+ entries = entries.filter((entry) => entry.id !== id)
355
+ },
356
+ update(id, fn) {
357
+ const entry = entries.find((item) => item.id === id)
358
+ if (entry) entry.fn = fn
359
+ },
360
+ async run(value) {
361
+ let result = value
362
+ for (const entry of entries) {
363
+ result = await entry.fn(result)
364
+ }
365
+ return result
366
+ },
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Walks the per-operation security in order and places the first resolved token on the request,
372
+ * mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
373
+ * Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
374
+ * `name` in the header, query, or cookie.
375
+ */
376
+ export async function resolveAuth(params: {
377
+ security: Array<Auth> | undefined
378
+ auth: AuthResolver | undefined
379
+ headers: Record<string, string>
380
+ query: Record<string, unknown>
381
+ }): Promise<void> {
382
+ const { security, auth, headers, query } = params
383
+ if (!security?.length || auth === undefined) return
384
+
385
+ for (const scheme of security) {
386
+ const token = typeof auth === 'function' ? await auth(scheme) : auth
387
+ if (token === undefined) continue
388
+
389
+ if (scheme.type === 'apiKey') {
390
+ const name = scheme.name ?? 'Authorization'
391
+ if (scheme.in === 'query') query[name] = token
392
+ else if (scheme.in === 'cookie') headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
393
+ else headers[name] = token
394
+ } else {
395
+ headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
396
+ }
397
+ return
398
+ }
399
+ }
400
+
401
+ async function runParser<T>(parser: Parser | undefined, value: T): Promise<T> {
402
+ if (!parser) return value
403
+ return (await parser(value)) as T
404
+ }
405
+
406
+ /**
407
+ * Builds the shared client core bound to a transport. Each plugin calls this with its
408
+ * `defaultTransport` and exports the resulting instance as `client`, plus a `createClient` factory.
409
+ */
410
+ export function createClientCore<TRequest = Request, TResponse = Response>(
411
+ options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
412
+ ): ClientInstance<TRequest, TResponse> {
413
+ const { defaultTransport, ...initialConfig } = options
414
+ let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
415
+
416
+ const interceptors: Interceptors<TRequest, TResponse> = {
417
+ request: createInterceptorStack<ResolvedRequest>(),
418
+ response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>>(),
419
+ error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>>(),
420
+ }
421
+
422
+ const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
423
+ const transport = requestConfig.transport ?? config.transport ?? defaultTransport
424
+ const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
425
+ const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
426
+
427
+ const headers = mergeHeaders(config.headers, requestConfig.headers)
428
+ if (requestConfig.contentType && requestConfig.contentType !== 'multipart/form-data') {
429
+ headers['Content-Type'] = requestConfig.contentType
430
+ }
431
+
432
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
433
+
434
+ await resolveAuth({
435
+ security: requestConfig.security,
436
+ auth: requestConfig.auth ?? config.auth,
437
+ headers,
438
+ query,
439
+ })
440
+
441
+ const rawBody = requestConfig.body
442
+ const validatedBody = await runParser(requestConfig.parser?.request, rawBody)
443
+ const search = querySerializer(query)
444
+ const pathParams = requestConfig.path ?? {}
445
+ const interpolatedUrl = [config.baseURL, requestConfig.baseURL, requestConfig.url]
446
+ .filter(Boolean)
447
+ .join('')
448
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
449
+ const url = interpolatedUrl + (search ? `?${search}` : '')
450
+
451
+ let resolvedRequest: ResolvedRequest = {
452
+ url,
453
+ method: (requestConfig.method ?? 'GET').toUpperCase(),
454
+ headers,
455
+ body: bodySerializer(validatedBody, headers['Content-Type'] ?? headers['content-type']),
456
+ signal: requestConfig.signal,
457
+ credentials: requestConfig.credentials,
458
+ responseType: requestConfig.responseType,
459
+ }
460
+ resolvedRequest = await interceptors.request.run(resolvedRequest)
461
+
462
+ let result = await transport(resolvedRequest)
463
+ result = await interceptors.response.run(result)
464
+
465
+ const isSuccess = result.status >= 200 && result.status < 300
466
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
467
+
468
+ if (!isSuccess && throwOnError) {
469
+ const error = new ResponseError({
470
+ data: result.data,
471
+ status: result.status,
472
+ statusText: result.statusText,
473
+ request: result.request,
474
+ response: result.response,
475
+ })
476
+ await interceptors.error.run(error)
477
+ throw error
478
+ }
479
+
480
+ const data = isSuccess ? await runParser(requestConfig.parser?.response, result.data) : undefined
481
+
482
+ return {
483
+ status: result.status,
484
+ data,
485
+ error: isSuccess ? undefined : result.data,
486
+ request: result.request,
487
+ response: result.response,
488
+ }
489
+ }) as ClientInstance<TRequest, TResponse>
490
+
491
+ client.getConfig = () => config
492
+ client.setConfig = (next) => {
493
+ config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
494
+ return config
495
+ }
496
+ client.interceptors = interceptors
497
+ client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
498
+
499
+ return client
500
+ }
501
+
502
+ /**
503
+ * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
504
+ */
505
+ function detectResponseType(contentType: string | null): ResponseType | undefined {
506
+ if (!contentType) return undefined
507
+ if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
508
+ if (contentType.includes('text/')) return 'text'
509
+ if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
510
+ return undefined
511
+ }
512
+
513
+ /**
514
+ * Parses a `fetch` response body. Empty responses (204/205/304 or no body) resolve to `undefined`.
515
+ * An explicit `responseType`, or one detected from the `Content-Type` header, forces the matching
516
+ * `Response` method; otherwise the body is read as text and `JSON.parse`d, falling back to raw text.
517
+ */
518
+ async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
519
+ if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
520
+ return undefined
521
+ }
522
+
523
+ switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {
524
+ case 'text':
525
+ case 'document':
526
+ return response.text()
527
+ case 'blob':
528
+ return response.blob()
529
+ case 'arraybuffer':
530
+ return response.arrayBuffer()
531
+ case 'stream':
532
+ return response.body ?? undefined
533
+ case 'json': {
534
+ // An empty body with a JSON content-type would make response.json() throw; treat it as no data.
535
+ const body = await response.text()
536
+ return body ? JSON.parse(body) : undefined
537
+ }
538
+ }
539
+
540
+ const text = await response.text()
541
+ if (!text) return undefined
542
+ try {
543
+ return JSON.parse(text)
544
+ } catch {
545
+ return text
546
+ }
547
+ }
548
+
549
+ /**
550
+ * The default transport: builds a native `Request` from the resolved request, sends it through
551
+ * `globalThis.fetch`, and returns the parsed body alongside the native request/response objects.
552
+ */
553
+ const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
554
+ const init: RequestInit = {
555
+ method: request.method,
556
+ headers: request.headers,
557
+ body: request.body,
558
+ signal: request.signal,
559
+ }
560
+ if (request.credentials) init.credentials = request.credentials
561
+
562
+ const nativeRequest = new Request(request.url, init)
563
+ const response = await globalThis.fetch(nativeRequest)
564
+ const data = await parseResponse(response, request.responseType)
565
+
566
+ return {
567
+ data,
568
+ status: response.status,
569
+ statusText: response.statusText,
570
+ headers: response.headers,
571
+ request: nativeRequest,
572
+ response,
573
+ }
574
+ }
575
+
576
+ export const client = createClientCore({ defaultTransport })
577
+
578
+ export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)