@kubb/plugin-client 5.0.0-beta.22 → 5.0.0-beta.27

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 (37) hide show
  1. package/dist/clients/axios.cjs +18 -3
  2. package/dist/clients/axios.cjs.map +1 -1
  3. package/dist/clients/axios.d.ts +8 -2
  4. package/dist/clients/axios.js +18 -3
  5. package/dist/clients/axios.js.map +1 -1
  6. package/dist/clients/fetch.cjs +16 -1
  7. package/dist/clients/fetch.cjs.map +1 -1
  8. package/dist/clients/fetch.d.ts +8 -2
  9. package/dist/clients/fetch.js +16 -1
  10. package/dist/clients/fetch.js.map +1 -1
  11. package/dist/index.cjs +171 -94
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +125 -65
  14. package/dist/index.js +171 -94
  15. package/dist/index.js.map +1 -1
  16. package/dist/templates/clients/axios.source.cjs +1 -1
  17. package/dist/templates/clients/axios.source.js +1 -1
  18. package/dist/templates/clients/fetch.source.cjs +1 -1
  19. package/dist/templates/clients/fetch.source.js +1 -1
  20. package/extension.yaml +774 -260
  21. package/package.json +6 -6
  22. package/src/clients/axios.ts +28 -5
  23. package/src/clients/fetch.ts +25 -2
  24. package/src/components/ClassClient.tsx +3 -3
  25. package/src/components/Client.tsx +27 -19
  26. package/src/components/StaticClassClient.tsx +3 -3
  27. package/src/components/Url.tsx +1 -1
  28. package/src/functionParams.ts +8 -8
  29. package/src/generators/classClientGenerator.tsx +16 -11
  30. package/src/generators/clientGenerator.tsx +16 -8
  31. package/src/generators/groupedClientGenerator.tsx +9 -3
  32. package/src/generators/operationsGenerator.tsx +7 -1
  33. package/src/generators/staticClassClientGenerator.tsx +16 -10
  34. package/src/plugin.ts +24 -11
  35. package/src/resolvers/resolverClient.ts +16 -9
  36. package/src/types.ts +66 -53
  37. package/src/utils.ts +17 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.22",
3
+ "version": "5.0.0-beta.27",
4
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",
@@ -87,10 +87,10 @@
87
87
  "registry": "https://registry.npmjs.org/"
88
88
  },
89
89
  "dependencies": {
90
- "@kubb/core": "5.0.0-beta.22",
91
- "@kubb/renderer-jsx": "5.0.0-beta.22",
92
- "@kubb/plugin-ts": "5.0.0-beta.22",
93
- "@kubb/plugin-zod": "5.0.0-beta.22"
90
+ "@kubb/core": "5.0.0-beta.27",
91
+ "@kubb/renderer-jsx": "5.0.0-beta.27",
92
+ "@kubb/plugin-ts": "5.0.0-beta.27",
93
+ "@kubb/plugin-zod": "5.0.0-beta.27"
94
94
  },
95
95
  "devDependencies": {
96
96
  "axios": "^1.16.1",
@@ -98,7 +98,7 @@
98
98
  "@internals/utils": "0.0.0"
99
99
  },
100
100
  "peerDependencies": {
101
- "@kubb/renderer-jsx": "5.0.0-beta.22",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.27",
102
102
  "axios": "^1.7.2"
103
103
  },
104
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,7 +23,7 @@ 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']
21
28
  contentType?: string
22
29
  }
@@ -56,14 +63,30 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
56
63
  ...merged,
57
64
  ...config,
58
65
  headers: {
59
- ...merged.headers,
60
- ...config.headers,
66
+ ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),
67
+ ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
61
68
  },
62
69
  }
63
70
  }, {})
64
71
  }
65
72
 
66
- 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)
67
90
 
68
91
  export const client = async <TResponseData, TError = unknown, TRequestData = unknown>(
69
92
  config: RequestConfig<TRequestData>,
@@ -76,7 +99,7 @@ export const client = async <TResponseData, TError = unknown, TRequestData = unk
76
99
  ...axiosConfig,
77
100
  headers: {
78
101
  ...(contentType && contentType !== 'multipart/form-data' ? { 'Content-Type': contentType } : {}),
79
- ...headers,
102
+ ...serializeHeaders(headers),
80
103
  },
81
104
  })
82
105
  .catch((e: AxiosError<TError>) => {
@@ -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,7 +21,7 @@ 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
19
26
  contentType?: string
20
27
  }
@@ -51,6 +58,22 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
51
58
  }, {})
52
59
  }
53
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
+
54
77
  export type ResponseErrorConfig<TError = unknown> = TError
55
78
 
56
79
  export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
@@ -85,7 +108,7 @@ export const client = async <TResponseData, _TError = unknown, RequestData = unk
85
108
  signal: config.signal,
86
109
  headers: {
87
110
  ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),
88
- ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
111
+ ...serializeHeaders(config.headers),
89
112
  },
90
113
  })
91
114
 
@@ -14,7 +14,7 @@ type OperationData = {
14
14
  node: ast.OperationNode
15
15
  name: string
16
16
  tsResolver: ResolverTs
17
- zodResolver?: ResolverZod
17
+ zodResolver?: ResolverZod | null
18
18
  }
19
19
 
20
20
  type Props = {
@@ -35,7 +35,7 @@ type GenerateMethodProps = {
35
35
  node: ast.OperationNode
36
36
  name: string
37
37
  tsResolver: ResolverTs
38
- zodResolver?: ResolverZod
38
+ zodResolver?: ResolverZod | null
39
39
  baseURL: string | null | undefined
40
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
41
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
@@ -62,7 +62,7 @@ function generateMethod({
62
62
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
63
  const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
64
64
  const { header: headerParams } = getOperationParameters(node)
65
- const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : undefined
65
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
66
66
  const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
67
67
  const generics = buildGenerics(node, tsResolver)
68
68
  const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
@@ -1,4 +1,11 @@
1
- import { buildOperationComments, buildParamsMapping, buildRequestConfigType, getContentTypeInfo, getOperationParameters } from '@internals/shared'
1
+ import {
2
+ buildOperationComments,
3
+ buildParamsMapping,
4
+ buildRequestConfigType,
5
+ getContentTypeInfo,
6
+ getOperationParameters,
7
+ resolveSuccessNames,
8
+ } from '@internals/shared'
2
9
  import { isValidVarName, URLPath } from '@internals/utils'
3
10
  import { ast } from '@kubb/core'
4
11
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -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
 
@@ -96,17 +103,18 @@ export function Client({
96
103
  const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
97
104
  const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
98
105
 
99
- const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : undefined
100
- const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : undefined
101
- 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
102
109
 
103
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
104
- const responseName = tsResolver.resolveResponseName(node)
105
- const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : undefined
106
- 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
107
115
 
108
- const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : undefined
109
- 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
110
118
 
111
119
  const errorNames = node.responses
112
120
  .filter((r) => {
@@ -116,8 +124,8 @@ export function Client({
116
124
  .map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
117
125
 
118
126
  const headers = [
119
- !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined,
120
- headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined,
127
+ !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
128
+ headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null,
121
129
  ].filter(Boolean)
122
130
 
123
131
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
@@ -158,8 +166,8 @@ export function Client({
158
166
  ? {
159
167
  value: `\`${baseURL}\``,
160
168
  }
161
- : undefined,
162
- params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : undefined,
169
+ : null,
170
+ params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : null,
163
171
  data: requestName
164
172
  ? {
165
173
  value:
@@ -169,18 +177,18 @@ export function Client({
169
177
  ? 'formData as FormData'
170
178
  : 'requestData',
171
179
  }
172
- : undefined,
173
- contentType: isConfigurable && isMultipleContentTypes ? {} : undefined,
180
+ : null,
181
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
174
182
  requestConfig: isConfigurable
175
183
  ? {
176
184
  mode: 'inlineSpread',
177
185
  }
178
- : undefined,
186
+ : null,
179
187
  headers: headers.length
180
188
  ? {
181
189
  value: isConfigurable ? `{ ${headers.join(', ')}, ...requestConfig.headers }` : `{ ${headers.join(', ')} }`,
182
190
  }
183
- : undefined,
191
+ : null,
184
192
  },
185
193
  },
186
194
  })
@@ -14,7 +14,7 @@ type OperationData = {
14
14
  node: ast.OperationNode
15
15
  name: string
16
16
  tsResolver: ResolverTs
17
- zodResolver?: ResolverZod
17
+ zodResolver?: ResolverZod | null
18
18
  }
19
19
 
20
20
  type Props = {
@@ -35,7 +35,7 @@ type GenerateMethodProps = {
35
35
  node: ast.OperationNode
36
36
  name: string
37
37
  tsResolver: ResolverTs
38
- zodResolver?: ResolverZod
38
+ zodResolver?: ResolverZod | null
39
39
  baseURL: string | null | undefined
40
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
41
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
@@ -62,7 +62,7 @@ function generateMethod({
62
62
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
63
  const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
64
64
  const { header: headerParams } = getOperationParameters(node)
65
- const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : undefined
65
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
66
66
  const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
67
67
  const generics = buildGenerics(node, tsResolver)
68
68
  const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
@@ -70,7 +70,7 @@ export function Url({
70
70
 
71
71
  const { path: originalPathParams } = getOperationParameters(node)
72
72
  const { path: casedPathParams } = getOperationParameters(node, { paramsCasing })
73
- const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : undefined
73
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null
74
74
 
75
75
  return (
76
76
  <File.Source name={name} isExportable={isExportable} isIndexable={isIndexable}>
@@ -11,7 +11,7 @@ type ParamLeaf = {
11
11
 
12
12
  type ParamGroup = {
13
13
  mode: 'object' | 'inlineSpread'
14
- children: Record<string, ParamLeaf | undefined>
14
+ children: Record<string, ParamLeaf | null | undefined>
15
15
  default?: string
16
16
  }
17
17
 
@@ -25,14 +25,14 @@ function isGroup(spec: ParamSpec): spec is ParamGroup {
25
25
  }
26
26
 
27
27
  function createType(type?: string) {
28
- return type ? ast.createParamsType({ variant: 'reference', name: type }) : undefined
28
+ return type ? ast.createParamsType({ variant: 'reference', name: type }) : null
29
29
  }
30
30
 
31
31
  function createDeclarationLeaf(name: string, spec: ParamLeaf): ast.FunctionParameterNode {
32
32
  if (spec.default !== undefined) {
33
33
  return ast.createFunctionParameter({
34
34
  name,
35
- type: createType(spec.type),
35
+ type: createType(spec.type) ?? undefined,
36
36
  default: spec.default,
37
37
  rest: spec.mode === 'inlineSpread',
38
38
  })
@@ -40,7 +40,7 @@ function createDeclarationLeaf(name: string, spec: ParamLeaf): ast.FunctionParam
40
40
 
41
41
  return ast.createFunctionParameter({
42
42
  name,
43
- type: createType(spec.type),
43
+ type: createType(spec.type) ?? undefined,
44
44
  optional: !!spec.optional,
45
45
  rest: spec.mode === 'inlineSpread',
46
46
  })
@@ -52,7 +52,7 @@ function createDeclarationParam(name: string, spec: ParamSpec): ast.FunctionPara
52
52
  inline: spec.mode === 'inlineSpread',
53
53
  default: spec.default,
54
54
  properties: Object.entries(spec.children)
55
- .filter(([, child]) => child !== undefined)
55
+ .filter(([, child]) => child != null)
56
56
  .map(([childName, child]) => createDeclarationLeaf(childName, child!)),
57
57
  })
58
58
  }
@@ -65,7 +65,7 @@ function createCallParam(name: string, spec: ParamSpec): ast.FunctionParameterNo
65
65
  return ast.createParameterGroup({
66
66
  inline: spec.mode === 'inlineSpread',
67
67
  properties: Object.entries(spec.children)
68
- .filter(([, child]) => child !== undefined)
68
+ .filter(([, child]) => child != null)
69
69
  .map(([childName, child]) =>
70
70
  ast.createFunctionParameter({
71
71
  name:
@@ -92,8 +92,8 @@ function createCallParam(name: string, spec: ParamSpec): ast.FunctionParameterNo
92
92
  * Creates function parameter builders for generating function signatures and calls.
93
93
  * Returns utilities to output constructor signatures (`toConstructor()`) or call expressions (`toCall()`).
94
94
  */
95
- export function createFunctionParams(params: Record<string, ParamSpec | undefined>) {
96
- const entries = Object.entries(params).filter(([, spec]) => spec !== undefined) as Array<[string, ParamSpec]>
95
+ export function createFunctionParams(params: Record<string, ParamSpec | null | undefined>) {
96
+ const entries = Object.entries(params).filter(([, spec]) => spec != null) as Array<[string, ParamSpec]>
97
97
 
98
98
  return {
99
99
  toConstructor() {
@@ -16,9 +16,9 @@ type OperationData = {
16
16
  node: ast.OperationNode
17
17
  name: string
18
18
  tsResolver: ResolverTs
19
- zodResolver: ResolverZod | undefined
19
+ zodResolver: ResolverZod | null
20
20
  typeFile: ast.FileNode
21
- zodFile: ast.FileNode | undefined
21
+ zodFile: ast.FileNode | null
22
22
  }
23
23
 
24
24
  type Controller = {
@@ -33,13 +33,18 @@ function resolveTypeImportNames(node: ast.OperationNode, tsResolver: ResolverTs)
33
33
  }
34
34
 
35
35
  function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
36
- const names: Array<string | undefined> = [
36
+ const names: Array<string | null | undefined> = [
37
37
  zodResolver.resolveResponseName?.(node),
38
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined,
38
+ node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
39
39
  ]
40
40
  return names.filter((n): n is string => Boolean(n))
41
41
  }
42
42
 
43
+ /**
44
+ * Built-in `operations` generator for `@kubb/plugin-client` when
45
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
46
+ * per operation and a shared constructor for request configuration.
47
+ */
43
48
  export const classClientGenerator = defineGenerator<PluginClient>({
44
49
  name: 'classClient',
45
50
  renderer: jsxRendererSync,
@@ -53,8 +58,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
53
58
 
54
59
  const tsResolver = driver.getResolver(pluginTsName)
55
60
  const tsPluginOptions = pluginTs.options
56
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
57
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
61
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
62
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
58
63
 
59
64
  function buildOperationData(node: ast.OperationNode): OperationData {
60
65
  const typeFile = tsResolver.resolveFile(
@@ -65,9 +70,9 @@ export const classClientGenerator = defineGenerator<PluginClient>({
65
70
  zodResolver && pluginZod?.options
66
71
  ? zodResolver.resolveFile(
67
72
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
68
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group },
73
+ { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group ?? undefined },
69
74
  )
70
- : undefined
75
+ : null
71
76
 
72
77
  return {
73
78
  node: node,
@@ -85,7 +90,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
85
90
 
86
91
  if (!tag && !group) {
87
92
  const name = resolver.resolveClassName('ApiClient')
88
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
93
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
89
94
  const operationData = buildOperationData(operationNode)
90
95
  const previous = acc.find((item) => item.file.path === file.path)
91
96
 
@@ -99,7 +104,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
99
104
 
100
105
  if (tag) {
101
106
  const name = groupName
102
- const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group })
107
+ const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group: group ?? undefined })
103
108
  const operationData = buildOperationData(operationNode)
104
109
  const previous = acc.find((item) => item.file.path === file.path)
105
110
 
@@ -215,7 +220,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
215
220
  })
216
221
 
217
222
  if (sdk) {
218
- const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group })
223
+ const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group: group ?? undefined })
219
224
 
220
225
  files.push(
221
226
  <File
@@ -8,6 +8,11 @@ import { Client } from '../components/Client'
8
8
  import { Url } from '../components/Url.tsx'
9
9
  import type { PluginClient } from '../types'
10
10
 
11
+ /**
12
+ * Built-in operation generator for `@kubb/plugin-client`. Emits one async
13
+ * function per OpenAPI operation, plus the matching URL helper. Used when
14
+ * `clientType: 'function'` (the default).
15
+ */
11
16
  export const clientGenerator = defineGenerator<PluginClient>({
12
17
  name: 'client',
13
18
  renderer: jsxRendererSync,
@@ -24,14 +29,14 @@ export const clientGenerator = defineGenerator<PluginClient>({
24
29
 
25
30
  const tsResolver = driver.getResolver(pluginTsName)
26
31
 
27
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
28
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
32
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
33
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
29
34
 
30
35
  const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing })
31
36
 
32
37
  const importedZodNames =
33
38
  zodResolver && parser === 'zod'
34
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(
39
+ ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter(
35
40
  (name): name is string => Boolean(name),
36
41
  )
37
42
  : []
@@ -39,13 +44,16 @@ export const clientGenerator = defineGenerator<PluginClient>({
39
44
  const meta = {
40
45
  name: resolver.resolveName(node.operationId),
41
46
  urlName: resolver.resolveUrlName(node),
42
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
47
+ file: resolver.resolveFile(
48
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
49
+ { root, output, group: group ?? undefined },
50
+ ),
43
51
  fileTs: tsResolver.resolveFile(
44
52
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
45
53
  {
46
54
  root,
47
55
  output: pluginTs.options?.output ?? output,
48
- group: pluginTs.options?.group,
56
+ group: pluginTs.options?.group ?? undefined,
49
57
  },
50
58
  ),
51
59
  fileZod:
@@ -55,10 +63,10 @@ export const clientGenerator = defineGenerator<PluginClient>({
55
63
  {
56
64
  root,
57
65
  output: pluginZod.options.output ?? output,
58
- group: pluginZod.options?.group,
66
+ group: pluginZod.options?.group ?? undefined,
59
67
  },
60
68
  )
61
- : undefined,
69
+ : null,
62
70
  } as const
63
71
 
64
72
  const hasFormData = node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false
@@ -90,7 +98,7 @@ export const clientGenerator = defineGenerator<PluginClient>({
90
98
 
91
99
  {hasFormData && <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />}
92
100
 
93
- {meta.fileZod && importedZodNames.length > 0 && <File.Import name={importedZodNames as string[]} root={meta.file.path} path={meta.fileZod.path} />}
101
+ {meta.fileZod && importedZodNames.length > 0 && <File.Import name={importedZodNames as Array<string>} root={meta.file.path} path={meta.fileZod.path} />}
94
102
 
95
103
  {meta.fileTs && importedTypeNames.length > 0 && (
96
104
  <File.Import name={Array.from(new Set(importedTypeNames))} root={meta.file.path} path={meta.fileTs.path} isTypeOnly />
@@ -4,6 +4,12 @@ import { defineGenerator } from '@kubb/core'
4
4
  import { File, Function, jsxRendererSync } from '@kubb/renderer-jsx'
5
5
  import type { PluginClient } from '../types'
6
6
 
7
+ /**
8
+ * Emits one aggregate file per tag/group when `group` is configured. Each
9
+ * file re-exports every client function for that group, so callers can
10
+ * `import { petController } from './gen/clients'` instead of importing
11
+ * each operation individually.
12
+ */
7
13
  export const groupedClientGenerator = defineGenerator<PluginClient>({
8
14
  name: 'groupedClient',
9
15
  renderer: jsxRendererSync,
@@ -15,16 +21,16 @@ export const groupedClientGenerator = defineGenerator<PluginClient>({
15
21
  (acc, operationNode) => {
16
22
  if (group?.type === 'tag') {
17
23
  const tag = operationNode.tags[0]
18
- const name = tag ? group?.name?.({ group: camelCase(tag) }) : undefined
24
+ const name = tag ? group?.name?.({ group: camelCase(tag) }) : null
19
25
 
20
26
  if (!tag || !name) {
21
27
  return acc
22
28
  }
23
29
 
24
- const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group })
30
+ const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group: group ?? undefined })
25
31
  const clientFile = resolver.resolveFile(
26
32
  { name: operationNode.operationId, extname: '.ts', tag: operationNode.tags[0] ?? 'default', path: operationNode.path },
27
- { root, output, group },
33
+ { root, output, group: group ?? undefined },
28
34
  )
29
35
 
30
36
  const client = {
@@ -3,6 +3,12 @@ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
3
3
  import { Operations } from '../components/Operations'
4
4
  import type { PluginClient } from '../types'
5
5
 
6
+ /**
7
+ * Generates an `operations.ts` file that re-exports every operation grouped
8
+ * by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
9
+ * for building meta-tooling on top of the generated client (route
10
+ * registries, API explorers).
11
+ */
6
12
  export const operationsGenerator = defineGenerator<PluginClient>({
7
13
  name: 'client',
8
14
  renderer: jsxRendererSync,
@@ -11,7 +17,7 @@ export const operationsGenerator = defineGenerator<PluginClient>({
11
17
  const { output, group } = ctx.options
12
18
 
13
19
  const name = 'operations'
14
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
20
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
15
21
 
16
22
  return (
17
23
  <File
@@ -15,9 +15,9 @@ type OperationData = {
15
15
  node: ast.OperationNode
16
16
  name: string
17
17
  tsResolver: ResolverTs
18
- zodResolver: ResolverZod | undefined
18
+ zodResolver: ResolverZod | null
19
19
  typeFile: ast.FileNode
20
- zodFile: ast.FileNode | undefined
20
+ zodFile: ast.FileNode | null
21
21
  }
22
22
 
23
23
  type Controller = {
@@ -31,13 +31,19 @@ function resolveTypeImportNames(node: ast.OperationNode, tsResolver: ResolverTs)
31
31
  }
32
32
 
33
33
  function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
34
- const names: Array<string | undefined> = [
34
+ const names: Array<string | null | undefined> = [
35
35
  zodResolver.resolveResponseName?.(node),
36
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined,
36
+ node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
37
37
  ]
38
38
  return names.filter((n): n is string => Boolean(n))
39
39
  }
40
40
 
41
+ /**
42
+ * Built-in `operations` generator for `@kubb/plugin-client` when
43
+ * `clientType: 'staticClass'`. Emits one class per tag, with a static method
44
+ * per operation so callers can use `Pet.getPetById(...)` without
45
+ * instantiating the class.
46
+ */
41
47
  export const staticClassClientGenerator = defineGenerator<PluginClient>({
42
48
  name: 'staticClassClient',
43
49
  renderer: jsxRendererSync,
@@ -51,8 +57,8 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
51
57
 
52
58
  const tsResolver = driver.getResolver(pluginTsName)
53
59
  const tsPluginOptions = pluginTs.options
54
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
55
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
60
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
61
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
56
62
 
57
63
  function buildOperationData(node: ast.OperationNode): OperationData {
58
64
  const typeFile = tsResolver.resolveFile(
@@ -63,9 +69,9 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
63
69
  zodResolver && pluginZod?.options
64
70
  ? zodResolver.resolveFile(
65
71
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
66
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group },
72
+ { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group ?? undefined },
67
73
  )
68
- : undefined
74
+ : null
69
75
 
70
76
  return {
71
77
  node: node,
@@ -83,7 +89,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
83
89
 
84
90
  if (!tag && !group) {
85
91
  const name = resolver.resolveClassName('ApiClient')
86
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
92
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
87
93
  const operationData = buildOperationData(operationNode)
88
94
  const previous = acc.find((item) => item.file.path === file.path)
89
95
 
@@ -97,7 +103,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
97
103
 
98
104
  if (tag) {
99
105
  const name = groupName
100
- const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group })
106
+ const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group: group ?? undefined })
101
107
  const operationData = buildOperationData(operationNode)
102
108
  const previous = acc.find((item) => item.file.path === file.path)
103
109