@kubb/plugin-client 5.0.0-beta.4 → 5.0.0-beta.42

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 (48) hide show
  1. package/README.md +39 -24
  2. package/dist/clients/axios.cjs +26 -4
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +11 -5
  5. package/dist/clients/axios.js +26 -4
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +77 -9
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +10 -3
  10. package/dist/clients/fetch.js +77 -9
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +625 -354
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +160 -94
  15. package/dist/index.js +626 -355
  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.d.ts +1 -1
  19. package/dist/templates/clients/axios.source.js +1 -1
  20. package/dist/templates/clients/fetch.source.cjs +1 -1
  21. package/dist/templates/clients/fetch.source.d.ts +1 -1
  22. package/dist/templates/clients/fetch.source.js +1 -1
  23. package/dist/templates/config.source.d.ts +1 -1
  24. package/extension.yaml +792 -301
  25. package/package.json +11 -16
  26. package/src/clients/axios.ts +41 -7
  27. package/src/clients/fetch.ts +106 -6
  28. package/src/components/ClassClient.tsx +19 -20
  29. package/src/components/Client.tsx +74 -53
  30. package/src/components/Operations.tsx +2 -1
  31. package/src/components/StaticClassClient.tsx +19 -20
  32. package/src/components/Url.tsx +8 -9
  33. package/src/components/WrapperClient.tsx +9 -5
  34. package/src/functionParams.ts +8 -8
  35. package/src/generators/classClientGenerator.tsx +51 -47
  36. package/src/generators/clientGenerator.tsx +37 -48
  37. package/src/generators/groupedClientGenerator.tsx +14 -8
  38. package/src/generators/operationsGenerator.tsx +14 -8
  39. package/src/generators/staticClassClientGenerator.tsx +45 -41
  40. package/src/plugin.ts +27 -26
  41. package/src/resolvers/resolverClient.ts +31 -8
  42. package/src/types.ts +93 -55
  43. package/src/utils.ts +35 -56
  44. package/templates/clients/axios.ts +0 -73
  45. package/templates/clients/fetch.ts +0 -96
  46. package/templates/config.ts +0 -43
  47. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  48. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/package.json CHANGED
@@ -1,23 +1,17 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.4",
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.42",
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,7 +24,6 @@
30
24
  "files": [
31
25
  "src",
32
26
  "dist",
33
- "templates",
34
27
  "extension.yaml",
35
28
  "*.d.ts",
36
29
  "*.d.cts",
@@ -94,17 +87,18 @@
94
87
  "registry": "https://registry.npmjs.org/"
95
88
  },
96
89
  "dependencies": {
97
- "@kubb/core": "5.0.0-beta.4",
98
- "@kubb/renderer-jsx": "5.0.0-beta.4",
99
- "@kubb/plugin-ts": "5.0.0-beta.4",
100
- "@kubb/plugin-zod": "5.0.0-beta.4"
90
+ "@kubb/core": "5.0.0-beta.42",
91
+ "@kubb/renderer-jsx": "5.0.0-beta.42",
92
+ "@kubb/plugin-ts": "5.0.0-beta.42",
93
+ "@kubb/plugin-zod": "5.0.0-beta.42"
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.4",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.42",
108
102
  "axios": "^1.7.2"
109
103
  },
110
104
  "peerDependenciesMeta": {
@@ -129,6 +123,7 @@
129
123
  "lint:fix": "oxlint --fix .",
130
124
  "release": "pnpm publish --no-git-check",
131
125
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
126
+ "release:stage": "pnpm stage publish --no-git-check",
132
127
  "start": "tsdown --watch",
133
128
  "test": "vitest --passWithNoTests",
134
129
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
@@ -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,93 @@ 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
+
77
+ /**
78
+ * Serializes the request body into the form `fetch` expects.
79
+ * `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through
80
+ * untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`
81
+ * and JSON-serialized otherwise.
82
+ */
83
+ function serializeBody(data: unknown, contentType?: string): BodyInit | undefined {
84
+ if (data === undefined || data === null) return undefined
85
+ if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
86
+ return data as BodyInit
87
+ }
88
+ if (typeof data === 'string') return data
89
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
90
+ return new URLSearchParams(data as Record<string, string>)
91
+ }
92
+ return JSON.stringify(data)
93
+ }
94
+
95
+ /**
96
+ * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
97
+ */
98
+ function detectResponseType(contentType: string | null): RequestConfig['responseType'] {
99
+ if (!contentType) return undefined
100
+ if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
101
+ if (contentType.includes('text/')) return 'text'
102
+ if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
103
+ return undefined
104
+ }
105
+
106
+ /**
107
+ * Parses a `fetch` response body.
108
+ *
109
+ * - Empty responses (204/205/304 or no body) resolve to `{}`.
110
+ * - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching
111
+ * `Response` method.
112
+ * - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.
113
+ */
114
+ async function parseResponse<TData>(response: Response, responseType?: RequestConfig['responseType']): Promise<TData> {
115
+ if ([204, 205, 304].includes(response.status) || !response.body) {
116
+ return {} as TData
117
+ }
118
+
119
+ switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {
120
+ case 'text':
121
+ case 'document':
122
+ return (await response.text()) as TData
123
+ case 'blob':
124
+ return (await response.blob()) as TData
125
+ case 'arraybuffer':
126
+ return (await response.arrayBuffer()) as TData
127
+ case 'stream':
128
+ return response.body as TData
129
+ case 'json':
130
+ return (await response.json()) as TData
131
+ }
132
+
133
+ // Explicit but unrecognized responseType keeps the JSON default; otherwise read text and parse it.
134
+ if (responseType) {
135
+ return (await response.json()) as TData
136
+ }
137
+ const text = await response.text()
138
+ if (!text) {
139
+ return {} as TData
140
+ }
141
+ try {
142
+ return JSON.parse(text) as TData
143
+ } catch {
144
+ return text as TData
145
+ }
146
+ }
147
+
53
148
  export type ResponseErrorConfig<TError = unknown> = TError
54
149
 
55
150
  export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
@@ -77,18 +172,23 @@ export const client = async <TResponseData, _TError = unknown, RequestData = unk
77
172
  targetUrl += `?${normalizedParams}`
78
173
  }
79
174
 
80
- const response = await fetch(targetUrl, {
175
+ const headers: Record<string, string> = {
176
+ ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),
177
+ ...serializeHeaders(config.headers),
178
+ }
179
+
180
+ const response = await globalThis.fetch(targetUrl, {
81
181
  credentials: config.credentials || 'same-origin',
82
182
  method: config.method?.toUpperCase(),
83
- body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),
183
+ body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),
84
184
  signal: config.signal,
85
- headers: config.headers,
185
+ headers,
86
186
  })
87
187
 
88
- const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()
188
+ const data = await parseResponse<TResponseData>(response, config.responseType)
89
189
 
90
190
  return {
91
- data: data as TResponseData,
191
+ data,
92
192
  status: response.status,
93
193
  statusText: response.statusText,
94
194
  headers: response.headers as Headers,
@@ -1,19 +1,20 @@
1
+ import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
1
2
  import { buildJSDoc, URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
3
+ import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
4
5
  import { functionPrinter } from '@kubb/plugin-ts'
5
6
  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']
@@ -57,26 +58,25 @@ function generateMethod({
57
58
  paramsCasing,
58
59
  pathParamsType,
59
60
  }: GenerateMethodProps): string {
61
+ if (!ast.isHttpOperationNode(node)) return ''
60
62
  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)
63
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
64
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
65
+ const { header: headerParams } = getOperationParameters(node)
66
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
67
+ const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
68
68
  const generics = buildGenerics(node, tsResolver)
69
- const paramsNode = ClassClient.getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
69
+ const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
70
70
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
71
- const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
72
- const jsdoc = buildJSDoc(getComments(node))
71
+ const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers })
72
+ const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
73
73
 
74
74
  const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
75
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
75
+ const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
76
76
  const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver })
77
77
 
78
78
  const methodBody = [
79
- 'const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)',
79
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
80
80
  '',
81
81
  requestDataLine,
82
82
  formDataLine,
@@ -135,4 +135,3 @@ ${methods.join('\n\n')}
135
135
  </File.Source>
136
136
  )
137
137
  }
138
- ClassClient.getParams = Client.getParams
@@ -1,3 +1,12 @@
1
+ import {
2
+ buildOperationComments,
3
+ buildParamsMapping,
4
+ buildRequestConfigType,
5
+ getContentTypeInfo,
6
+ getOperationParameters,
7
+ getResponseType,
8
+ resolveSuccessNames,
9
+ } from '@internals/shared'
1
10
  import { isValidVarName, URLPath } from '@internals/utils'
2
11
  import { ast } from '@kubb/core'
3
12
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -7,8 +16,7 @@ import { File, Function } from '@kubb/renderer-jsx'
7
16
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
17
  import { createFunctionParams } from '../functionParams.ts'
9
18
  import type { PluginClient } from '../types.ts'
10
- import { buildParamsMapping, getComments } from '../utils.ts'
11
- import { Url } from './Url.tsx'
19
+ import { buildUrlParamsNode } from './Url.tsx'
12
20
 
13
21
  type Props = {
14
22
  name: string
@@ -18,7 +26,7 @@ type Props = {
18
26
  isConfigurable?: boolean
19
27
  returnType?: string
20
28
 
21
- baseURL: string | undefined
29
+ baseURL: string | null | undefined
22
30
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
23
31
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
24
32
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -26,7 +34,7 @@ type Props = {
26
34
  parser: PluginClient['resolvedOptions']['parser'] | undefined
27
35
  node: ast.OperationNode
28
36
  tsResolver: ResolverTs
29
- zodResolver?: ResolverZod
37
+ zodResolver?: ResolverZod | null
30
38
  children?: KubbReactNode
31
39
  }
32
40
 
@@ -41,26 +49,33 @@ type GetParamsProps = {
41
49
 
42
50
  const declarationPrinter = functionPrinter({ mode: 'declaration' })
43
51
 
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
-
52
+ export function buildClientParamsNode({
53
+ paramsType,
54
+ paramsCasing,
55
+ pathParamsType,
56
+ node,
57
+ tsResolver,
58
+ isConfigurable,
59
+ }: GetParamsProps): ast.FunctionParametersNode {
47
60
  return ast.createOperationParams(node, {
48
61
  paramsType,
49
62
  pathParamsType: paramsType === 'object' ? 'object' : pathParamsType === 'object' ? 'object' : 'inline',
50
63
  paramsCasing,
51
64
  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 }',
65
+ extraParams: [
66
+ ...(isConfigurable
67
+ ? [
68
+ ast.createFunctionParameter({
69
+ name: 'config',
70
+ type: ast.createParamsType({
71
+ variant: 'reference',
72
+ name: buildRequestConfigType(node, tsResolver),
73
+ }),
74
+ default: '{}',
59
75
  }),
60
- default: '{}',
61
- }),
62
- ]
63
- : [],
76
+ ]
77
+ : []),
78
+ ],
64
79
  })
65
80
  }
66
81
 
@@ -82,28 +97,27 @@ export function Client({
82
97
  children,
83
98
  isConfigurable = true,
84
99
  }: Props): KubbReactNode {
100
+ if (!ast.isHttpOperationNode(node)) return null
85
101
  const path = new URLPath(node.path)
86
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
87
- const isFormData = contentType === 'multipart/form-data'
102
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
103
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
104
+ const responseType = getResponseType(node)
88
105
 
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)
106
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
107
+ const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
95
108
 
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
109
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null
110
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null
111
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null
99
112
 
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
113
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
114
+ const successNames = resolveSuccessNames(node, tsResolver)
115
+ const responseName = successNames.length > 0 ? successNames.join(' | ') : tsResolver.resolveResponseName(node)
116
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : null
117
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]!) : null
104
118
 
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
119
+ const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
120
+ const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
107
121
 
108
122
  const errorNames = node.responses
109
123
  .filter((r) => {
@@ -113,14 +127,14 @@ export function Client({
113
127
  .map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
114
128
 
115
129
  const headers = [
116
- contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined,
117
- headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined,
130
+ !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
131
+ headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null,
118
132
  ].filter(Boolean)
119
133
 
120
134
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
121
135
 
122
136
  const generics = [responseName, TError, requestName || 'unknown'].filter(Boolean)
123
- const paramsNode = getParams({
137
+ const paramsNode = buildClientParamsNode({
124
138
  paramsType,
125
139
  paramsCasing,
126
140
  pathParamsType,
@@ -130,7 +144,7 @@ export function Client({
130
144
  })
131
145
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
132
146
 
133
- const urlParamsNode = Url.getParams({
147
+ const urlParamsNode = buildUrlParamsNode({
134
148
  paramsType,
135
149
  paramsCasing,
136
150
  pathParamsType,
@@ -155,23 +169,30 @@ export function Client({
155
169
  ? {
156
170
  value: `\`${baseURL}\``,
157
171
  }
158
- : undefined,
159
- params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : undefined,
172
+ : null,
173
+ params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : null,
160
174
  data: requestName
161
175
  ? {
162
- value: isFormData ? 'formData as FormData' : 'requestData',
176
+ value:
177
+ isMultipleContentTypes && hasFormData
178
+ ? "contentType === 'multipart/form-data' ? formData as FormData : requestData"
179
+ : isFormData
180
+ ? 'formData as FormData'
181
+ : 'requestData',
163
182
  }
164
- : undefined,
183
+ : null,
184
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
185
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
165
186
  requestConfig: isConfigurable
166
187
  ? {
167
188
  mode: 'inlineSpread',
168
189
  }
169
- : undefined,
190
+ : null,
170
191
  headers: headers.length
171
192
  ? {
172
193
  value: isConfigurable ? `{ ${headers.join(', ')}, ...requestConfig.headers }` : `{ ${headers.join(', ')} }`,
173
194
  }
174
- : undefined,
195
+ : null,
175
196
  },
176
197
  },
177
198
  })
@@ -182,8 +203,8 @@ export function Client({
182
203
  <>
183
204
  {dataReturnType === 'full' && parser === 'zod' && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`}
184
205
  {dataReturnType === 'data' && parser === 'zod' && zodResponseName && `return ${zodResponseName}.parse(res.data)`}
185
- {dataReturnType === 'full' && parser === 'client' && 'return res'}
186
- {dataReturnType === 'data' && parser === 'client' && 'return res.data'}
206
+ {dataReturnType === 'full' && parser !== 'zod' && 'return res'}
207
+ {dataReturnType === 'data' && parser !== 'zod' && 'return res.data'}
187
208
  </>
188
209
  )
189
210
 
@@ -198,11 +219,13 @@ export function Client({
198
219
  export={isExportable}
199
220
  params={paramsSignature}
200
221
  JSDoc={{
201
- comments: getComments(node),
222
+ comments: buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }),
202
223
  }}
203
224
  returnType={returnType}
204
225
  >
205
- {isConfigurable ? 'const { client: request = fetch, ...requestConfig } = config' : ''}
226
+ {isConfigurable
227
+ ? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = config`
228
+ : ''}
206
229
  <br />
207
230
  <br />
208
231
  {pathParamsMapping &&
@@ -236,11 +259,11 @@ export function Client({
236
259
  )}
237
260
  {parser === 'zod' && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && 'const requestData = data'}
238
261
  <br />
239
- {isFormData && requestName && 'const formData = buildFormData(requestData)'}
262
+ {(isFormData || (isMultipleContentTypes && hasFormData)) && requestName && 'const formData = buildFormData(requestData)'}
240
263
  <br />
241
264
  {isConfigurable
242
265
  ? `const res = await request<${generics.join(', ')}>(${clientParams.toCall()})`
243
- : `const res = await fetch<${generics.join(', ')}>(${clientParams.toCall()})`}
266
+ : `const res = await client<${generics.join(', ')}>(${clientParams.toCall()})`}
244
267
  <br />
245
268
  {childrenElement}
246
269
  </Function>
@@ -248,5 +271,3 @@ export function Client({
248
271
  </>
249
272
  )
250
273
  }
251
-
252
- Client.getParams = getParams
@@ -1,5 +1,5 @@
1
1
  import { URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
2
+ import { ast } from '@kubb/core'
3
3
  import { Const, File } from '@kubb/renderer-jsx'
4
4
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
5
5
 
@@ -12,6 +12,7 @@ export function Operations({ name, nodes }: OperationsProps): KubbReactNode {
12
12
  const operationsObject: Record<string, { path: string; method: string }> = {}
13
13
 
14
14
  nodes.forEach((node) => {
15
+ if (!ast.isHttpOperationNode(node)) return
15
16
  operationsObject[node.operationId] = {
16
17
  path: new URLPath(node.path).URL,
17
18
  method: node.method.toLowerCase(),