@kubb/plugin-axios 5.4.0 → 5.4.2

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-axios",
3
- "version": "5.4.0",
3
+ "version": "5.4.2",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -66,18 +66,18 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@kubb/plugin-ts": "^5.0.2",
69
- "@kubb/plugin-zod": "^5.1.5"
69
+ "@kubb/plugin-zod": "^5.2.1"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@internals/client": "0.0.0",
73
73
  "@internals/shared": "0.0.0",
74
74
  "axios": "^1.20.0",
75
- "kubb": "^5.2.0",
75
+ "kubb": "^5.3.13",
76
76
  "typescript": "^6.0.3"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "axios": "^1.20.0",
80
- "kubb": "^5.2.0"
80
+ "kubb": "^5.3.13"
81
81
  },
82
82
  "peerDependenciesMeta": {
83
83
  "axios": {
package/src/plugin.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { readFileSync } from 'node:fs'
1
2
  import path from 'node:path'
2
3
  import { createSdkGenerator, defaultMacros, resolverClient } from '@internals/client'
3
4
  import { createGroupConfig } from '@internals/shared'
4
- import { definePlugin, Resolver } from 'kubb/kit'
5
+ import { ast, definePlugin, Resolver } from 'kubb/kit'
5
6
  import { pluginTsName } from '@kubb/plugin-ts'
6
7
  import { pluginZodName } from '@kubb/plugin-zod'
7
8
  import { clientGenerator } from './generators/clientGenerator.tsx'
@@ -90,10 +91,45 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
90
91
  copy: axiosSerializersTemplatePath,
91
92
  })
92
93
 
94
+ const clientPath = path.resolve(root, '.kubb/client.ts')
95
+ const clientSource = readFileSync(axiosClientTemplatePath, 'utf8')
96
+ const clientBody = clientSource.slice(clientSource.indexOf('\n\n') + 2)
97
+ const runtimeRoot = path.dirname(clientPath)
98
+
93
99
  ctx.injectFile({
94
100
  baseName: 'client.ts',
95
- path: path.resolve(root, '.kubb/client.ts'),
96
- copy: axiosClientTemplatePath,
101
+ path: clientPath,
102
+ imports: [
103
+ ast.factory.createImport({ name: 'axios', path: 'axios' }),
104
+ ast.factory.createImport({
105
+ name: ['AxiosError', 'AxiosInstance', 'AxiosRequestConfig', 'AxiosResponse', 'InternalAxiosRequestConfig'],
106
+ path: 'axios',
107
+ isTypeOnly: true,
108
+ }),
109
+ ast.factory.createImport({
110
+ name: ['applyHeaderStyles', 'defaultBodySerializer', 'defaultPathSerializer', 'defaultQuerySerializer', 'isDefaultJsonBody', 'serializeCookies'],
111
+ path: path.resolve(root, '.kubb/serializers.ts'),
112
+ root: runtimeRoot,
113
+ }),
114
+ ast.factory.createImport({
115
+ name: ['HeadersInit', 'PathParamStyle', 'PathSerializer', 'Serializers', 'Styles'],
116
+ path: path.resolve(root, '.kubb/serializers.ts'),
117
+ root: runtimeRoot,
118
+ isTypeOnly: true,
119
+ }),
120
+ ast.factory.createImport({
121
+ name: ['ParseError', 'validateStandardSchema'],
122
+ path: path.resolve(root, '.kubb/standardSchema.ts'),
123
+ root: runtimeRoot,
124
+ }),
125
+ ast.factory.createImport({
126
+ name: ['StandardSchemaValidator'],
127
+ path: path.resolve(root, '.kubb/standardSchema.ts'),
128
+ root: runtimeRoot,
129
+ isTypeOnly: true,
130
+ }),
131
+ ],
132
+ sources: [ast.factory.createSource({ nodes: [ast.factory.createText(clientBody)] })],
97
133
  footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
98
134
  })
99
135
 
@@ -1,8 +1,8 @@
1
1
  import axios from 'axios'
2
2
  import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
- import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
4
- import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
5
- import { ParseError, type StandardSchemaValidator, validateStandardSchema } from './standardSchema'
3
+ import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers.ts'
4
+ import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers.ts'
5
+ import { ParseError, type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
6
6
 
7
7
  /**
8
8
  * HTTP status codes treated as a success, everything else is an error.
@@ -225,12 +225,12 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
225
225
  baseURL?: string
226
226
  url?: string
227
227
  method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
228
- path?: Record<string, unknown>
228
+ path?: unknown
229
229
  query?: unknown
230
230
  params?: unknown
231
- cookies?: Record<string, unknown>
231
+ cookies?: unknown
232
232
  body?: TBody
233
- headers?: HeadersInit
233
+ headers?: unknown
234
234
  styles?: Styles
235
235
  signal?: AbortSignal
236
236
  options?: AxiosOptions
@@ -289,24 +289,27 @@ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>
289
289
  response: TResponse
290
290
  }
291
291
 
292
- export type InterceptorFn<T> = (value: T) => T | Promise<T>
292
+ export type InterceptorFn<T, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = (
293
+ value: T,
294
+ requestConfig?: RequestConfig<unknown, TRequest, TResponse>,
295
+ ) => T | Promise<T>
293
296
 
294
297
  /**
295
298
  * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API, backed by axios's native interceptor managers.
296
299
  */
297
- export type InterceptorChannel<T> = {
298
- use: (fn: InterceptorFn<T>) => number
300
+ export type InterceptorChannel<T, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
301
+ use: (fn: InterceptorFn<T, TRequest, TResponse>) => number
299
302
  eject: (id: number) => void
300
- update: (id: number, fn: InterceptorFn<T>) => void
303
+ update: (id: number, fn: InterceptorFn<T, TRequest, TResponse>) => void
301
304
  }
302
305
 
303
306
  /**
304
307
  * The three interceptor channels every client instance exposes, wrapping axios's native managers with `error` mapped onto the response rejection handler.
305
308
  */
306
- export type Interceptors = {
307
- request: InterceptorChannel<InternalAxiosRequestConfig>
308
- response: InterceptorChannel<AxiosResponse>
309
- error: InterceptorChannel<AxiosError>
309
+ export type Interceptors<TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
310
+ request: InterceptorChannel<InternalAxiosRequestConfig, TRequest, TResponse>
311
+ response: InterceptorChannel<AxiosResponse, TRequest, TResponse>
312
+ error: InterceptorChannel<AxiosError, TRequest, TResponse>
310
313
  }
311
314
 
312
315
  /**
@@ -318,7 +321,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
318
321
  getConfig: () => ClientConfig
319
322
  setConfig: (config: ClientConfig) => ClientConfig
320
323
  getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
321
- interceptors: Interceptors
324
+ interceptors: Interceptors<TRequest, TResponse>
322
325
  createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
323
326
  }
324
327
 
@@ -397,13 +400,27 @@ function serializeUrl({
397
400
  /**
398
401
  * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
399
402
  */
400
- function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
403
+ function createInterceptorChannel<T, TRequest, TResponse>(
404
+ register: (fn: InterceptorFn<T, TRequest, TResponse>) => number,
405
+ ejectNative: (id: number) => void,
406
+ getRequestConfig: (value: T) => RequestConfig<unknown, TRequest, TResponse> | undefined,
407
+ getContextId: (value: T) => number | undefined,
408
+ setContextId: (value: T, id: number | undefined) => void,
409
+ ): InterceptorChannel<T, TRequest, TResponse> {
401
410
  const ids = new Map<number, number>()
402
411
  let counter = 0
412
+ const registerWithContext = (fn: InterceptorFn<T, TRequest, TResponse>) =>
413
+ register(async (value) => {
414
+ const contextId = getContextId(value)
415
+ const result = await fn(value, getRequestConfig(value))
416
+ if (contextId !== undefined) setContextId(result, contextId)
417
+ return result
418
+ })
419
+
403
420
  return {
404
421
  use(fn) {
405
422
  const id = ++counter
406
- ids.set(id, register(fn))
423
+ ids.set(id, registerWithContext(fn))
407
424
  return id
408
425
  },
409
426
  eject(id) {
@@ -415,7 +432,7 @@ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number,
415
432
  update(id, fn) {
416
433
  const nativeId = ids.get(id)
417
434
  if (nativeId !== undefined) ejectNative(nativeId)
418
- ids.set(id, register(fn))
435
+ ids.set(id, registerWithContext(fn))
419
436
  },
420
437
  }
421
438
  }
@@ -526,7 +543,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
526
543
  const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
527
544
  const codecs = { ...config.codecs, ...requestConfig.codecs }
528
545
 
529
- const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
546
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers as HeadersInit | undefined, requestConfig.styles?.header))
530
547
  const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
531
548
  const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
532
549
  if (responseContentType && !hasHeader(headers, 'accept')) {
@@ -543,7 +560,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
543
560
  })
544
561
 
545
562
  if (requestConfig.cookies) {
546
- const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
563
+ const cookie = serializeCookies(requestConfig.cookies as Record<string, unknown>, requestConfig.styles?.cookie)
547
564
  if (cookie) headers['Cookie'] = [headers['Cookie'], cookie].filter(Boolean).join('; ')
548
565
  }
549
566
 
@@ -570,7 +587,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
570
587
  headers['Content-Type'] = 'application/json'
571
588
  }
572
589
 
573
- const pathParams = requestConfig.path ?? {}
590
+ const pathParams = (requestConfig.path ?? {}) as Record<string, unknown>
574
591
  const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
575
592
  pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
576
593
  )
@@ -665,53 +682,89 @@ async function settleResponse<TRequest, TResponse>({
665
682
  export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
666
683
  let config: ClientConfig = { ...options }
667
684
  const instance = config.transport ?? axios.create()
685
+ const requestContexts = new Map<number, RequestConfig<unknown, TRequest, TResponse>>()
686
+ let requestContextId = 0
687
+ const getContextId = (value: AxiosRequestConfig | AxiosResponse | AxiosError) => {
688
+ if (!value || typeof value !== 'object') return undefined
689
+ const axiosConfig = 'config' in value ? value.config : value
690
+ return (axiosConfig as (AxiosRequestConfig & { __kubbRequestContext?: number }) | undefined)?.__kubbRequestContext
691
+ }
692
+ const getRequestConfig = (value: AxiosRequestConfig | AxiosResponse | AxiosError) => {
693
+ const id = getContextId(value)
694
+ return id === undefined ? undefined : requestContexts.get(id)
695
+ }
696
+ const setContextId = (value: AxiosRequestConfig | AxiosResponse | AxiosError, id: number | undefined) => {
697
+ if (!value || typeof value !== 'object') return
698
+ const axiosConfig = 'config' in value ? value.config : value
699
+ if (!axiosConfig) return
700
+ const contextualConfig = axiosConfig as AxiosRequestConfig & { __kubbRequestContext?: number }
701
+ if (id === undefined) delete contextualConfig.__kubbRequestContext
702
+ else contextualConfig.__kubbRequestContext = id
703
+ }
668
704
 
669
705
  const requestManager = instance.interceptors.request
670
706
  const responseManager = instance.interceptors.response
671
- const interceptors: Interceptors = {
672
- request: createInterceptorChannel<InternalAxiosRequestConfig>(
707
+ const interceptors: Interceptors<TRequest, TResponse> = {
708
+ request: createInterceptorChannel<InternalAxiosRequestConfig, TRequest, TResponse>(
673
709
  (fn) => requestManager.use(fn),
674
710
  (id) => requestManager.eject(id),
711
+ getRequestConfig,
712
+ getContextId,
713
+ setContextId,
675
714
  ),
676
- response: createInterceptorChannel<AxiosResponse>(
715
+ response: createInterceptorChannel<AxiosResponse, TRequest, TResponse>(
677
716
  (fn) => responseManager.use(fn),
678
717
  (id) => responseManager.eject(id),
718
+ getRequestConfig,
719
+ getContextId,
720
+ setContextId,
679
721
  ),
680
- error: createInterceptorChannel<AxiosError>(
681
- (fn) =>
682
- responseManager.use(undefined, async (error: unknown) => {
683
- await fn(error as AxiosError)
684
- return Promise.reject(error)
685
- }),
722
+ error: createInterceptorChannel<AxiosError, TRequest, TResponse>(
723
+ (fn) => responseManager.use(undefined, (error: unknown) => Promise.resolve(fn(error as AxiosError)).then(() => Promise.reject(error))),
686
724
  (id) => responseManager.eject(id),
725
+ getRequestConfig,
726
+ getContextId,
727
+ setContextId,
687
728
  ),
688
729
  }
689
730
 
690
731
  const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
691
732
  const activeInstance = requestConfig.transport ?? config.transport ?? instance
692
733
  const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig })
734
+ const contextId = ++requestContextId
735
+ requestContexts.set(contextId, requestConfig)
736
+ ;(axiosConfig as AxiosRequestConfig & { __kubbRequestContext: number }).__kubbRequestContext = contextId
693
737
 
694
738
  try {
695
- const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
696
- return await settleResponse({
697
- response,
698
- codecs,
699
- validator: requestConfig.validator,
700
- onValidationError: requestConfig.onValidationError ?? config.onValidationError,
701
- })
702
- } catch (error) {
703
- const axiosError = error as AxiosError
704
- if (throwOnError && axiosError.response) {
705
- throw new ResponseError({
706
- data: axiosError.response.data,
707
- status: axiosError.response.status,
708
- statusText: axiosError.response.statusText,
709
- contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
710
- request: axiosError.config as TRequest,
711
- response: axiosError.response as TResponse,
739
+ try {
740
+ const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
741
+ const result = await settleResponse<TRequest, TResponse>({
742
+ response,
743
+ codecs,
744
+ validator: requestConfig.validator,
745
+ onValidationError: requestConfig.onValidationError ?? config.onValidationError,
712
746
  })
747
+ setContextId(response, undefined)
748
+ return result
749
+ } catch (error) {
750
+ const axiosError = error as AxiosError
751
+ setContextId(axiosError, undefined)
752
+ if (axiosError.response) setContextId(axiosError.response, undefined)
753
+ if (throwOnError && axiosError.response) {
754
+ throw new ResponseError({
755
+ data: axiosError.response.data,
756
+ status: axiosError.response.status,
757
+ statusText: axiosError.response.statusText,
758
+ contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
759
+ request: axiosError.config as TRequest,
760
+ response: axiosError.response as TResponse,
761
+ })
762
+ }
763
+ throw error
713
764
  }
714
- throw error
765
+ } finally {
766
+ setContextId(axiosConfig, undefined)
767
+ requestContexts.delete(contextId)
715
768
  }
716
769
  }) as ClientInstance<TRequest, TResponse>
717
770
 
@@ -725,7 +778,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
725
778
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
726
779
  return serializeUrl({
727
780
  parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
728
- pathParams: requestConfig.path ?? {},
781
+ pathParams: (requestConfig.path ?? {}) as Record<string, unknown>,
729
782
  search: querySerializer(query, requestConfig.styles?.query),
730
783
  pathSerializer,
731
784
  pathStyles: requestConfig.styles?.path,