@kubb/plugin-client 5.0.0-beta.3 → 5.0.0-beta.30

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.
Files changed (42) hide show
  1. package/README.md +24 -4
  2. package/dist/clients/axios.cjs +25 -3
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +9 -2
  5. package/dist/clients/axios.js +25 -3
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +20 -2
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +9 -2
  10. package/dist/clients/fetch.js +20 -2
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +524 -301
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +150 -84
  15. package/dist/index.js +525 -302
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.js +1 -1
  19. package/dist/templates/clients/fetch.source.cjs +1 -1
  20. package/dist/templates/clients/fetch.source.js +1 -1
  21. package/extension.yaml +1293 -0
  22. package/package.json +11 -17
  23. package/src/clients/axios.ts +41 -7
  24. package/src/clients/fetch.ts +30 -3
  25. package/src/components/ClassClient.tsx +17 -19
  26. package/src/components/Client.tsx +68 -51
  27. package/src/components/StaticClassClient.tsx +17 -19
  28. package/src/components/Url.tsx +7 -9
  29. package/src/components/WrapperClient.tsx +9 -5
  30. package/src/functionParams.ts +8 -8
  31. package/src/generators/classClientGenerator.tsx +40 -38
  32. package/src/generators/clientGenerator.tsx +32 -35
  33. package/src/generators/groupedClientGenerator.tsx +14 -8
  34. package/src/generators/operationsGenerator.tsx +12 -6
  35. package/src/generators/staticClassClientGenerator.tsx +34 -32
  36. package/src/plugin.ts +24 -11
  37. package/src/resolvers/resolverClient.ts +31 -8
  38. package/src/types.ts +90 -53
  39. package/src/utils.ts +30 -53
  40. package/templates/clients/axios.ts +0 -73
  41. package/templates/clients/fetch.ts +0 -96
  42. package/templates/config.ts +0 -43
package/package.json CHANGED
@@ -1,23 +1,17 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.3",
4
- "description": "API client generator plugin for Kubb, creating type-safe HTTP clients (Axios, Fetch) from OpenAPI specifications for making API requests.",
3
+ "version": "5.0.0-beta.30",
4
+ "description": "Generate type-safe HTTP clients from your OpenAPI specification. Supports Axios, Fetch, and custom client adapters with full TypeScript inference and zero boilerplate.",
5
5
  "keywords": [
6
6
  "api-client",
7
7
  "axios",
8
- "code-generator",
8
+ "code-generation",
9
9
  "codegen",
10
10
  "fetch",
11
11
  "http-client",
12
12
  "kubb",
13
- "oas",
14
13
  "openapi",
15
- "plugins",
16
- "rest-api",
17
- "sdk-generator",
18
14
  "swagger",
19
- "type-safe",
20
- "type-safety",
21
15
  "typescript"
22
16
  ],
23
17
  "license": "MIT",
@@ -30,8 +24,7 @@
30
24
  "files": [
31
25
  "src",
32
26
  "dist",
33
- "templates",
34
- "plugin.json",
27
+ "extension.yaml",
35
28
  "*.d.ts",
36
29
  "*.d.cts",
37
30
  "!/**/**.test.**",
@@ -94,17 +87,18 @@
94
87
  "registry": "https://registry.npmjs.org/"
95
88
  },
96
89
  "dependencies": {
97
- "@kubb/core": "5.0.0-beta.3",
98
- "@kubb/renderer-jsx": "5.0.0-beta.3",
99
- "@kubb/plugin-ts": "5.0.0-beta.3",
100
- "@kubb/plugin-zod": "5.0.0-beta.3"
90
+ "@kubb/core": "5.0.0-beta.29",
91
+ "@kubb/renderer-jsx": "5.0.0-beta.29",
92
+ "@kubb/plugin-ts": "5.0.0-beta.30",
93
+ "@kubb/plugin-zod": "5.0.0-beta.30"
101
94
  },
102
95
  "devDependencies": {
103
- "axios": "^1.15.2",
96
+ "axios": "^1.16.1",
97
+ "@internals/shared": "0.0.0",
104
98
  "@internals/utils": "0.0.0"
105
99
  },
106
100
  "peerDependencies": {
107
- "@kubb/renderer-jsx": "5.0.0-beta.3",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.29",
108
102
  "axios": "^1.7.2"
109
103
  },
110
104
  "peerDependenciesMeta": {
@@ -4,6 +4,13 @@ import axios from 'axios'
4
4
  declare const AXIOS_BASE: string
5
5
  declare const AXIOS_HEADERS: string
6
6
 
7
+ /**
8
+ * Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).
9
+ * Non-string values are JSON-serialized before the request is sent.
10
+ */
11
+ export type HeaderValue = string | number | boolean | null | undefined | object
12
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
13
+
7
14
  /**
8
15
  * Subset of AxiosRequestConfig
9
16
  */
@@ -16,8 +23,9 @@ export type RequestConfig<TData = unknown> = {
16
23
  responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
17
24
  signal?: AbortSignal
18
25
  validateStatus?: (status: number) => boolean
19
- headers?: AxiosRequestConfig['headers']
26
+ headers?: HeadersInit
20
27
  paramsSerializer?: AxiosRequestConfig['paramsSerializer']
28
+ contentType?: string
21
29
  }
22
30
 
23
31
  /**
@@ -55,22 +63,48 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
55
63
  ...merged,
56
64
  ...config,
57
65
  headers: {
58
- ...merged.headers,
59
- ...config.headers,
66
+ ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),
67
+ ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
60
68
  },
61
69
  }
62
70
  }, {})
63
71
  }
64
72
 
65
- export const axiosInstance = axios.create(getConfig())
73
+ /**
74
+ * Serializes header values into the string form axios ultimately puts on the wire.
75
+ * Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`
76
+ * are sent in their canonical JSON-string form rather than `[object Object]`.
77
+ */
78
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
79
+ if (!headers) return {}
80
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
81
+ const result: Record<string, string> = {}
82
+ for (const [key, value] of entries) {
83
+ if (value === undefined || value === null) continue
84
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
85
+ }
86
+ return result
87
+ }
88
+
89
+ export const axiosInstance = axios.create(getConfig() as AxiosRequestConfig)
66
90
 
67
91
  export const client = async <TResponseData, TError = unknown, TRequestData = unknown>(
68
92
  config: RequestConfig<TRequestData>,
69
93
  _request?: unknown,
70
94
  ): Promise<ResponseConfig<TResponseData>> => {
71
- return axiosInstance.request<TResponseData, ResponseConfig<TResponseData>>(mergeConfig(getConfig(), config)).catch((e: AxiosError<TError>) => {
72
- throw e
73
- })
95
+ const requestConfig = mergeConfig(getConfig(), config)
96
+ const { contentType, headers, ...axiosConfig } = requestConfig
97
+ return axiosInstance
98
+ .request<TResponseData, ResponseConfig<TResponseData>>({
99
+ ...axiosConfig,
100
+ headers: {
101
+ ...(contentType && contentType !== 'multipart/form-data' ? { 'Content-Type': contentType } : {}),
102
+ ...serializeHeaders(headers),
103
+ },
104
+ })
105
+ .catch((e: AxiosError<TError>) => {
106
+ throw e
107
+ })
74
108
  }
75
109
 
76
110
  client.getConfig = getConfig
@@ -3,6 +3,13 @@
3
3
  */
4
4
  export type RequestCredentials = 'omit' | 'same-origin' | 'include'
5
5
 
6
+ /**
7
+ * Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).
8
+ * Non-string values are JSON-serialized before the request is sent.
9
+ */
10
+ export type HeaderValue = string | number | boolean | null | undefined | object
11
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
12
+
6
13
  /**
7
14
  * Subset of FetchRequestConfig
8
15
  */
@@ -14,8 +21,9 @@ export type RequestConfig<TData = unknown> = {
14
21
  data?: TData | FormData
15
22
  responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
16
23
  signal?: AbortSignal
17
- headers?: [string, string][] | Record<string, string>
24
+ headers?: HeadersInit
18
25
  credentials?: RequestCredentials
26
+ contentType?: string
19
27
  }
20
28
 
21
29
  /**
@@ -50,6 +58,22 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
50
58
  }, {})
51
59
  }
52
60
 
61
+ /**
62
+ * Serializes header values into the string form `fetch` expects.
63
+ * Objects (including arrays) are JSON-stringified so spec-defined object
64
+ * headers like `X-Filter` are sent in their canonical JSON-string form.
65
+ */
66
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
67
+ if (!headers) return {}
68
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
69
+ const result: Record<string, string> = {}
70
+ for (const [key, value] of entries) {
71
+ if (value === undefined || value === null) continue
72
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
73
+ }
74
+ return result
75
+ }
76
+
53
77
  export type ResponseErrorConfig<TError = unknown> = TError
54
78
 
55
79
  export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
@@ -77,12 +101,15 @@ export const client = async <TResponseData, _TError = unknown, RequestData = unk
77
101
  targetUrl += `?${normalizedParams}`
78
102
  }
79
103
 
80
- const response = await fetch(targetUrl, {
104
+ const response = await globalThis.fetch(targetUrl, {
81
105
  credentials: config.credentials || 'same-origin',
82
106
  method: config.method?.toUpperCase(),
83
107
  body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),
84
108
  signal: config.signal,
85
- headers: config.headers,
109
+ headers: {
110
+ ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),
111
+ ...serializeHeaders(config.headers),
112
+ },
86
113
  })
87
114
 
88
115
  const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()
@@ -1,3 +1,4 @@
1
+ import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
1
2
  import { buildJSDoc, URLPath } from '@internals/utils'
2
3
  import type { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -6,14 +7,14 @@ import type { ResolverZod } from '@kubb/plugin-zod'
6
7
  import { File } from '@kubb/renderer-jsx'
7
8
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
9
  import type { PluginClient } from '../types.ts'
9
- import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement, getComments } from '../utils.ts'
10
- import { Client } from './Client.tsx'
10
+ import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement } from '../utils.ts'
11
+ import { buildClientParamsNode } from './Client.tsx'
11
12
 
12
13
  type OperationData = {
13
14
  node: ast.OperationNode
14
15
  name: string
15
16
  tsResolver: ResolverTs
16
- zodResolver?: ResolverZod
17
+ zodResolver?: ResolverZod | null
17
18
  }
18
19
 
19
20
  type Props = {
@@ -21,7 +22,7 @@ type Props = {
21
22
  isExportable?: boolean
22
23
  isIndexable?: boolean
23
24
  operations: Array<OperationData>
24
- baseURL: string | undefined
25
+ baseURL: string | null | undefined
25
26
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
26
27
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
27
28
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -34,8 +35,8 @@ type GenerateMethodProps = {
34
35
  node: ast.OperationNode
35
36
  name: string
36
37
  tsResolver: ResolverTs
37
- zodResolver?: ResolverZod
38
- baseURL: string | undefined
38
+ zodResolver?: ResolverZod | null
39
+ baseURL: string | null | undefined
39
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
40
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
41
42
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -58,25 +59,23 @@ function generateMethod({
58
59
  pathParamsType,
59
60
  }: GenerateMethodProps): string {
60
61
  const path = new URLPath(node.path, { casing: paramsCasing })
61
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
62
- const isFormData = contentType === 'multipart/form-data'
63
- const headerParamsName =
64
- node.parameters.filter((p) => p.in === 'header').length > 0
65
- ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === 'header')[0]!)
66
- : undefined
67
- const headers = buildHeaders(contentType, !!headerParamsName)
62
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
64
+ const { header: headerParams } = getOperationParameters(node)
65
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
66
+ const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
68
67
  const generics = buildGenerics(node, tsResolver)
69
- const paramsNode = ClassClient.getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
68
+ const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
70
69
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
71
- const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
72
- const jsdoc = buildJSDoc(getComments(node))
70
+ const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers })
71
+ const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
73
72
 
74
73
  const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
75
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
74
+ const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
76
75
  const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver })
77
76
 
78
77
  const methodBody = [
79
- 'const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)',
78
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
80
79
  '',
81
80
  requestDataLine,
82
81
  formDataLine,
@@ -135,4 +134,3 @@ ${methods.join('\n\n')}
135
134
  </File.Source>
136
135
  )
137
136
  }
138
- ClassClient.getParams = Client.getParams
@@ -1,3 +1,11 @@
1
+ import {
2
+ buildOperationComments,
3
+ buildParamsMapping,
4
+ buildRequestConfigType,
5
+ getContentTypeInfo,
6
+ getOperationParameters,
7
+ resolveSuccessNames,
8
+ } from '@internals/shared'
1
9
  import { isValidVarName, URLPath } from '@internals/utils'
2
10
  import { ast } from '@kubb/core'
3
11
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -7,8 +15,7 @@ import { File, Function } from '@kubb/renderer-jsx'
7
15
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
16
  import { createFunctionParams } from '../functionParams.ts'
9
17
  import type { PluginClient } from '../types.ts'
10
- import { buildParamsMapping, getComments } from '../utils.ts'
11
- import { Url } from './Url.tsx'
18
+ import { buildUrlParamsNode } from './Url.tsx'
12
19
 
13
20
  type Props = {
14
21
  name: string
@@ -18,7 +25,7 @@ type Props = {
18
25
  isConfigurable?: boolean
19
26
  returnType?: string
20
27
 
21
- baseURL: string | undefined
28
+ baseURL: string | null | undefined
22
29
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
23
30
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
24
31
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -26,7 +33,7 @@ type Props = {
26
33
  parser: PluginClient['resolvedOptions']['parser'] | undefined
27
34
  node: ast.OperationNode
28
35
  tsResolver: ResolverTs
29
- zodResolver?: ResolverZod
36
+ zodResolver?: ResolverZod | null
30
37
  children?: KubbReactNode
31
38
  }
32
39
 
@@ -41,26 +48,33 @@ type GetParamsProps = {
41
48
 
42
49
  const declarationPrinter = functionPrinter({ mode: 'declaration' })
43
50
 
44
- function getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable }: GetParamsProps): ast.FunctionParametersNode {
45
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
46
-
51
+ export function buildClientParamsNode({
52
+ paramsType,
53
+ paramsCasing,
54
+ pathParamsType,
55
+ node,
56
+ tsResolver,
57
+ isConfigurable,
58
+ }: GetParamsProps): ast.FunctionParametersNode {
47
59
  return ast.createOperationParams(node, {
48
60
  paramsType,
49
61
  pathParamsType: paramsType === 'object' ? 'object' : pathParamsType === 'object' ? 'object' : 'inline',
50
62
  paramsCasing,
51
63
  resolver: tsResolver,
52
- extraParams: isConfigurable
53
- ? [
54
- ast.createFunctionParameter({
55
- name: 'config',
56
- type: ast.createParamsType({
57
- variant: 'reference',
58
- name: requestName ? `Partial<RequestConfig<${requestName}>> & { client?: Client }` : 'Partial<RequestConfig> & { client?: Client }',
64
+ extraParams: [
65
+ ...(isConfigurable
66
+ ? [
67
+ ast.createFunctionParameter({
68
+ name: 'config',
69
+ type: ast.createParamsType({
70
+ variant: 'reference',
71
+ name: buildRequestConfigType(node, tsResolver),
72
+ }),
73
+ default: '{}',
59
74
  }),
60
- default: '{}',
61
- }),
62
- ]
63
- : [],
75
+ ]
76
+ : []),
77
+ ],
64
78
  })
65
79
  }
66
80
 
@@ -83,27 +97,24 @@ export function Client({
83
97
  isConfigurable = true,
84
98
  }: Props): KubbReactNode {
85
99
  const path = new URLPath(node.path)
86
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
87
- const isFormData = contentType === 'multipart/form-data'
100
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
101
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
88
102
 
89
- const originalPathParams = node.parameters.filter((p) => p.in === 'path')
90
- const casedPathParams = ast.caseParams(originalPathParams, paramsCasing)
91
- const originalQueryParams = node.parameters.filter((p) => p.in === 'query')
92
- const casedQueryParams = ast.caseParams(originalQueryParams, paramsCasing)
93
- const originalHeaderParams = node.parameters.filter((p) => p.in === 'header')
94
- const casedHeaderParams = ast.caseParams(originalHeaderParams, paramsCasing)
103
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
104
+ const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
95
105
 
96
- const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : undefined
97
- const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : undefined
98
- const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : undefined
106
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null
107
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null
108
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null
99
109
 
100
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
101
- const responseName = tsResolver.resolveResponseName(node)
102
- const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : undefined
103
- const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]!) : undefined
110
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
111
+ const successNames = resolveSuccessNames(node, tsResolver)
112
+ const responseName = successNames.length > 0 ? successNames.join(' | ') : tsResolver.resolveResponseName(node)
113
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : null
114
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]!) : null
104
115
 
105
- const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : undefined
106
- const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined
116
+ const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
117
+ const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
107
118
 
108
119
  const errorNames = node.responses
109
120
  .filter((r) => {
@@ -113,14 +124,14 @@ export function Client({
113
124
  .map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
114
125
 
115
126
  const headers = [
116
- contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined,
117
- headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined,
127
+ !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
128
+ headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null,
118
129
  ].filter(Boolean)
119
130
 
120
131
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
121
132
 
122
133
  const generics = [responseName, TError, requestName || 'unknown'].filter(Boolean)
123
- const paramsNode = getParams({
134
+ const paramsNode = buildClientParamsNode({
124
135
  paramsType,
125
136
  paramsCasing,
126
137
  pathParamsType,
@@ -130,7 +141,7 @@ export function Client({
130
141
  })
131
142
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
132
143
 
133
- const urlParamsNode = Url.getParams({
144
+ const urlParamsNode = buildUrlParamsNode({
134
145
  paramsType,
135
146
  paramsCasing,
136
147
  pathParamsType,
@@ -155,23 +166,29 @@ export function Client({
155
166
  ? {
156
167
  value: `\`${baseURL}\``,
157
168
  }
158
- : undefined,
159
- params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : undefined,
169
+ : null,
170
+ params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : null,
160
171
  data: requestName
161
172
  ? {
162
- value: isFormData ? 'formData as FormData' : 'requestData',
173
+ value:
174
+ isMultipleContentTypes && hasFormData
175
+ ? "contentType === 'multipart/form-data' ? formData as FormData : requestData"
176
+ : isFormData
177
+ ? 'formData as FormData'
178
+ : 'requestData',
163
179
  }
164
- : undefined,
180
+ : null,
181
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
165
182
  requestConfig: isConfigurable
166
183
  ? {
167
184
  mode: 'inlineSpread',
168
185
  }
169
- : undefined,
186
+ : null,
170
187
  headers: headers.length
171
188
  ? {
172
189
  value: isConfigurable ? `{ ${headers.join(', ')}, ...requestConfig.headers }` : `{ ${headers.join(', ')} }`,
173
190
  }
174
- : undefined,
191
+ : null,
175
192
  },
176
193
  },
177
194
  })
@@ -198,11 +215,13 @@ export function Client({
198
215
  export={isExportable}
199
216
  params={paramsSignature}
200
217
  JSDoc={{
201
- comments: getComments(node),
218
+ comments: buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }),
202
219
  }}
203
220
  returnType={returnType}
204
221
  >
205
- {isConfigurable ? 'const { client: request = fetch, ...requestConfig } = config' : ''}
222
+ {isConfigurable
223
+ ? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = config`
224
+ : ''}
206
225
  <br />
207
226
  <br />
208
227
  {pathParamsMapping &&
@@ -236,11 +255,11 @@ export function Client({
236
255
  )}
237
256
  {parser === 'zod' && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && 'const requestData = data'}
238
257
  <br />
239
- {isFormData && requestName && 'const formData = buildFormData(requestData)'}
258
+ {(isFormData || (isMultipleContentTypes && hasFormData)) && requestName && 'const formData = buildFormData(requestData)'}
240
259
  <br />
241
260
  {isConfigurable
242
261
  ? `const res = await request<${generics.join(', ')}>(${clientParams.toCall()})`
243
- : `const res = await fetch<${generics.join(', ')}>(${clientParams.toCall()})`}
262
+ : `const res = await client<${generics.join(', ')}>(${clientParams.toCall()})`}
244
263
  <br />
245
264
  {childrenElement}
246
265
  </Function>
@@ -248,5 +267,3 @@ export function Client({
248
267
  </>
249
268
  )
250
269
  }
251
-
252
- Client.getParams = getParams
@@ -1,3 +1,4 @@
1
+ import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
1
2
  import { buildJSDoc, URLPath } from '@internals/utils'
2
3
  import type { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -6,14 +7,14 @@ import type { ResolverZod } from '@kubb/plugin-zod'
6
7
  import { File } from '@kubb/renderer-jsx'
7
8
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
9
  import type { PluginClient } from '../types.ts'
9
- import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement, getComments } from '../utils.ts'
10
- import { Client } from './Client.tsx'
10
+ import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement } from '../utils.ts'
11
+ import { buildClientParamsNode } from './Client.tsx'
11
12
 
12
13
  type OperationData = {
13
14
  node: ast.OperationNode
14
15
  name: string
15
16
  tsResolver: ResolverTs
16
- zodResolver?: ResolverZod
17
+ zodResolver?: ResolverZod | null
17
18
  }
18
19
 
19
20
  type Props = {
@@ -21,7 +22,7 @@ type Props = {
21
22
  isExportable?: boolean
22
23
  isIndexable?: boolean
23
24
  operations: Array<OperationData>
24
- baseURL: string | undefined
25
+ baseURL: string | null | undefined
25
26
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
26
27
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
27
28
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -34,8 +35,8 @@ type GenerateMethodProps = {
34
35
  node: ast.OperationNode
35
36
  name: string
36
37
  tsResolver: ResolverTs
37
- zodResolver?: ResolverZod
38
- baseURL: string | undefined
38
+ zodResolver?: ResolverZod | null
39
+ baseURL: string | null | undefined
39
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
40
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
41
42
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -58,25 +59,23 @@ function generateMethod({
58
59
  pathParamsType,
59
60
  }: GenerateMethodProps): string {
60
61
  const path = new URLPath(node.path, { casing: paramsCasing })
61
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
62
- const isFormData = contentType === 'multipart/form-data'
63
- const headerParamsName =
64
- node.parameters.filter((p) => p.in === 'header').length > 0
65
- ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === 'header')[0]!)
66
- : undefined
67
- const headers = buildHeaders(contentType, !!headerParamsName)
62
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
64
+ const { header: headerParams } = getOperationParameters(node)
65
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
66
+ const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
68
67
  const generics = buildGenerics(node, tsResolver)
69
- const paramsNode = Client.getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
68
+ const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
70
69
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
71
- const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
72
- const jsdoc = buildJSDoc(getComments(node))
70
+ const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers })
71
+ const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
73
72
 
74
73
  const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
75
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
74
+ const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
76
75
  const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver })
77
76
 
78
77
  const methodBody = [
79
- 'const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)',
78
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
80
79
  '',
81
80
  requestDataLine,
82
81
  formDataLine,
@@ -127,4 +126,3 @@ export function StaticClassClient({
127
126
  </File.Source>
128
127
  )
129
128
  }
130
- StaticClassClient.getParams = Client.getParams
@@ -1,3 +1,4 @@
1
+ import { buildParamsMapping, getOperationParameters } from '@internals/shared'
1
2
  import { isValidVarName, URLPath } from '@internals/utils'
2
3
  import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -5,14 +6,13 @@ import { functionPrinter } from '@kubb/plugin-ts'
5
6
  import { Const, File, Function } from '@kubb/renderer-jsx'
6
7
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
7
8
  import type { PluginClient } from '../types.ts'
8
- import { buildParamsMapping } from '../utils.ts'
9
9
 
10
10
  type Props = {
11
11
  name: string
12
12
  isExportable?: boolean
13
13
  isIndexable?: boolean
14
14
 
15
- baseURL: string | undefined
15
+ baseURL: string | null | undefined
16
16
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
17
17
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
18
18
  pathParamsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -30,7 +30,7 @@ type GetParamsProps = {
30
30
 
31
31
  const declarationPrinter = functionPrinter({ mode: 'declaration' })
32
32
 
33
- function getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver }: GetParamsProps): ast.FunctionParametersNode {
33
+ export function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver }: GetParamsProps): ast.FunctionParametersNode {
34
34
  // Build a URL-only node with only path params (no body, query, header)
35
35
  const urlNode: ast.OperationNode = {
36
36
  ...node,
@@ -59,7 +59,7 @@ export function Url({
59
59
  }: Props): KubbReactNode {
60
60
  const path = new URLPath(node.path)
61
61
 
62
- const paramsNode = getParams({
62
+ const paramsNode = buildUrlParamsNode({
63
63
  paramsType,
64
64
  paramsCasing,
65
65
  pathParamsType,
@@ -68,9 +68,9 @@ export function Url({
68
68
  })
69
69
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
70
70
 
71
- const originalPathParams = node.parameters.filter((p) => p.in === 'path')
72
- const casedPathParams = ast.caseParams(originalPathParams, paramsCasing)
73
- const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : undefined
71
+ const { path: originalPathParams } = getOperationParameters(node)
72
+ const { path: casedPathParams } = getOperationParameters(node, { paramsCasing })
73
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null
74
74
 
75
75
  return (
76
76
  <File.Source name={name} isExportable={isExportable} isIndexable={isIndexable}>
@@ -88,5 +88,3 @@ export function Url({
88
88
  </File.Source>
89
89
  )
90
90
  }
91
-
92
- Url.getParams = getParams