@kubb/plugin-client 5.0.0-beta.15 → 5.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.15",
3
+ "version": "5.0.0-beta.25",
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.19",
91
- "@kubb/renderer-jsx": "5.0.0-beta.19",
92
- "@kubb/plugin-ts": "5.0.0-beta.15",
93
- "@kubb/plugin-zod": "5.0.0-beta.15"
90
+ "@kubb/core": "5.0.0-beta.25",
91
+ "@kubb/renderer-jsx": "5.0.0-beta.25",
92
+ "@kubb/plugin-ts": "5.0.0-beta.25",
93
+ "@kubb/plugin-zod": "5.0.0-beta.25"
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.19",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.25",
102
102
  "axios": "^1.7.2"
103
103
  },
104
104
  "peerDependenciesMeta": {
@@ -14,7 +14,7 @@ export type RequestConfig<TData = unknown> = {
14
14
  data?: TData | FormData
15
15
  responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
16
16
  signal?: AbortSignal
17
- headers?: [string, string][] | Record<string, string>
17
+ headers?: Array<[string, string]> | Record<string, string>
18
18
  credentials?: RequestCredentials
19
19
  contentType?: string
20
20
  }
@@ -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 = {
@@ -22,7 +22,7 @@ type Props = {
22
22
  isExportable?: boolean
23
23
  isIndexable?: boolean
24
24
  operations: Array<OperationData>
25
- baseURL: string | undefined
25
+ baseURL: string | null | undefined
26
26
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
27
27
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
28
28
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -35,8 +35,8 @@ type GenerateMethodProps = {
35
35
  node: ast.OperationNode
36
36
  name: string
37
37
  tsResolver: ResolverTs
38
- zodResolver?: ResolverZod
39
- baseURL: string | undefined
38
+ zodResolver?: ResolverZod | null
39
+ baseURL: string | null | undefined
40
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
41
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
42
42
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -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 })
@@ -18,7 +18,7 @@ type Props = {
18
18
  isConfigurable?: boolean
19
19
  returnType?: string
20
20
 
21
- baseURL: string | undefined
21
+ baseURL: string | null | undefined
22
22
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
23
23
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
24
24
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -26,7 +26,7 @@ type Props = {
26
26
  parser: PluginClient['resolvedOptions']['parser'] | undefined
27
27
  node: ast.OperationNode
28
28
  tsResolver: ResolverTs
29
- zodResolver?: ResolverZod
29
+ zodResolver?: ResolverZod | null
30
30
  children?: KubbReactNode
31
31
  }
32
32
 
@@ -96,17 +96,17 @@ export function Client({
96
96
  const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
97
97
  const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
98
98
 
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
99
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null
100
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null
101
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null
102
102
 
103
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
103
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
104
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
105
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : null
106
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]!) : null
107
107
 
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
108
+ const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
109
+ const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
110
110
 
111
111
  const errorNames = node.responses
112
112
  .filter((r) => {
@@ -116,8 +116,8 @@ export function Client({
116
116
  .map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
117
117
 
118
118
  const headers = [
119
- !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined,
120
- headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined,
119
+ !isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
120
+ headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null,
121
121
  ].filter(Boolean)
122
122
 
123
123
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
@@ -158,8 +158,8 @@ export function Client({
158
158
  ? {
159
159
  value: `\`${baseURL}\``,
160
160
  }
161
- : undefined,
162
- params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : undefined,
161
+ : null,
162
+ params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : null,
163
163
  data: requestName
164
164
  ? {
165
165
  value:
@@ -169,18 +169,18 @@ export function Client({
169
169
  ? 'formData as FormData'
170
170
  : 'requestData',
171
171
  }
172
- : undefined,
173
- contentType: isConfigurable && isMultipleContentTypes ? {} : undefined,
172
+ : null,
173
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
174
174
  requestConfig: isConfigurable
175
175
  ? {
176
176
  mode: 'inlineSpread',
177
177
  }
178
- : undefined,
178
+ : null,
179
179
  headers: headers.length
180
180
  ? {
181
181
  value: isConfigurable ? `{ ${headers.join(', ')}, ...requestConfig.headers }` : `{ ${headers.join(', ')} }`,
182
182
  }
183
- : undefined,
183
+ : null,
184
184
  },
185
185
  },
186
186
  })
@@ -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 = {
@@ -22,7 +22,7 @@ type Props = {
22
22
  isExportable?: boolean
23
23
  isIndexable?: boolean
24
24
  operations: Array<OperationData>
25
- baseURL: string | undefined
25
+ baseURL: string | null | undefined
26
26
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
27
27
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
28
28
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -35,8 +35,8 @@ type GenerateMethodProps = {
35
35
  node: ast.OperationNode
36
36
  name: string
37
37
  tsResolver: ResolverTs
38
- zodResolver?: ResolverZod
39
- baseURL: string | undefined
38
+ zodResolver?: ResolverZod | null
39
+ baseURL: string | null | undefined
40
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
41
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
42
42
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -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 })
@@ -12,7 +12,7 @@ type Props = {
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']
@@ -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,28 +33,33 @@ 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,
46
51
  operations(nodes, ctx) {
47
- const { config, driver, resolver, root, inputNode } = ctx
52
+ const { config, driver, resolver, root } = ctx
48
53
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options
49
- const baseURL = ctx.options.baseURL ?? inputNode.meta?.baseURL
54
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
50
55
 
51
56
  const pluginTs = driver.getPlugin(pluginTsName)
52
57
  if (!pluginTs) return null
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
 
@@ -94,9 +99,12 @@ export const classClientGenerator = defineGenerator<PluginClient>({
94
99
  } else {
95
100
  acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
96
101
  }
97
- } else if (tag) {
102
+ return acc
103
+ }
104
+
105
+ if (tag) {
98
106
  const name = groupName
99
- 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 })
100
108
  const operationData = buildOperationData(operationNode)
101
109
  const previous = acc.find((item) => item.file.path === file.path)
102
110
 
@@ -161,8 +169,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
161
169
  baseName={file.baseName}
162
170
  path={file.path}
163
171
  meta={file.meta}
164
- banner={resolver.resolveBanner(inputNode, { output, config })}
165
- footer={resolver.resolveFooter(inputNode, { output, config })}
172
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
173
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
166
174
  >
167
175
  {importPath ? (
168
176
  <>
@@ -212,7 +220,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
212
220
  })
213
221
 
214
222
  if (sdk) {
215
- 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 })
216
224
 
217
225
  files.push(
218
226
  <File
@@ -220,8 +228,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
220
228
  baseName={sdkFile.baseName}
221
229
  path={sdkFile.path}
222
230
  meta={sdkFile.meta}
223
- banner={resolver.resolveBanner(inputNode, { output, config })}
224
- footer={resolver.resolveFooter(inputNode, { output, config })}
231
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
232
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
225
233
  >
226
234
  {importPath ? (
227
235
  <File.Import name={['Client', 'RequestConfig']} path={importPath} isTypeOnly />
@@ -8,13 +8,18 @@ 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,
14
19
  operation(node, ctx) {
15
- const { config, driver, resolver, root, inputNode } = ctx
20
+ const { config, driver, resolver, root } = ctx
16
21
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options
17
- const baseURL = ctx.options.baseURL ?? inputNode.meta?.baseURL
22
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
18
23
 
19
24
  const pluginTs = driver.getPlugin(pluginTsName)
20
25
 
@@ -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
@@ -68,8 +76,8 @@ export const clientGenerator = defineGenerator<PluginClient>({
68
76
  baseName={meta.file.baseName}
69
77
  path={meta.file.path}
70
78
  meta={meta.file.meta}
71
- banner={resolver.resolveBanner(inputNode, { output, config })}
72
- footer={resolver.resolveFooter(inputNode, { output, config })}
79
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
80
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
73
81
  >
74
82
  {importPath ? (
75
83
  <>
@@ -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,27 +4,33 @@ 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,
10
16
  operations(nodes, ctx) {
11
- const { config, resolver, root, inputNode } = ctx
17
+ const { config, resolver, root } = ctx
12
18
  const { output, group } = ctx.options
13
19
 
14
20
  const controllers = nodes.reduce(
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 = {
@@ -55,8 +61,8 @@ export const groupedClientGenerator = defineGenerator<PluginClient>({
55
61
  baseName={file.baseName}
56
62
  path={file.path}
57
63
  meta={file.meta}
58
- banner={resolver.resolveBanner(inputNode, { output, config })}
59
- footer={resolver.resolveFooter(inputNode, { output, config })}
64
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
65
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
60
66
  >
61
67
  {clients.map((client) => (
62
68
  <File.Import key={client.name} name={[client.name]} root={file.path} path={client.file.path} />
@@ -3,23 +3,29 @@ 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,
9
15
  operations(nodes, ctx) {
10
- const { config, resolver, root, inputNode } = ctx
16
+ const { config, resolver, root } = ctx
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
18
24
  baseName={file.baseName}
19
25
  path={file.path}
20
26
  meta={file.meta}
21
- banner={resolver.resolveBanner(inputNode, { output, config })}
22
- footer={resolver.resolveFooter(inputNode, { output, config })}
27
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
28
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
23
29
  >
24
30
  <Operations name={name} nodes={nodes} />
25
31
  </File>