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

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 (56) 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 +836 -514
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +206 -105
  15. package/dist/index.js +833 -515
  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.cjs.map +1 -1
  19. package/dist/templates/clients/axios.source.d.ts +1 -1
  20. package/dist/templates/clients/axios.source.js +1 -1
  21. package/dist/templates/clients/axios.source.js.map +1 -1
  22. package/dist/templates/clients/fetch.source.cjs +1 -1
  23. package/dist/templates/clients/fetch.source.cjs.map +1 -1
  24. package/dist/templates/clients/fetch.source.d.ts +1 -1
  25. package/dist/templates/clients/fetch.source.js +1 -1
  26. package/dist/templates/clients/fetch.source.js.map +1 -1
  27. package/dist/templates/config.source.cjs.map +1 -1
  28. package/dist/templates/config.source.d.ts +1 -1
  29. package/dist/templates/config.source.js.map +1 -1
  30. package/package.json +14 -26
  31. package/src/clients/axios.ts +41 -7
  32. package/src/clients/fetch.ts +106 -6
  33. package/src/components/ClassClient.tsx +47 -24
  34. package/src/components/Client.tsx +100 -71
  35. package/src/components/StaticClassClient.tsx +47 -24
  36. package/src/components/Url.tsx +10 -12
  37. package/src/components/WrapperClient.tsx +9 -5
  38. package/src/functionParams.ts +8 -8
  39. package/src/generators/classClientGenerator.tsx +63 -51
  40. package/src/generators/clientGenerator.tsx +45 -48
  41. package/src/generators/groupedClientGenerator.tsx +12 -6
  42. package/src/generators/operationsGenerator.ts +47 -0
  43. package/src/generators/staticClassClientGenerator.tsx +57 -45
  44. package/src/index.ts +2 -1
  45. package/src/plugin.ts +30 -28
  46. package/src/resolvers/resolverClient.ts +32 -8
  47. package/src/types.ts +111 -65
  48. package/src/utils.ts +142 -63
  49. package/extension.yaml +0 -776
  50. package/src/components/Operations.tsx +0 -28
  51. package/src/generators/operationsGenerator.tsx +0 -28
  52. package/templates/clients/axios.ts +0 -73
  53. package/templates/clients/fetch.ts +0 -96
  54. package/templates/config.ts +0 -43
  55. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  56. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
- import { camelCase, pascalCase } from '@internals/utils'
3
- import type { ast } from '@kubb/core'
4
- import { defineGenerator } from '@kubb/core'
2
+ import { getOperationParameters, operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
+ import { camelCase } from '@internals/utils'
4
+ import { ast, defineGenerator } from '@kubb/core'
5
5
  import type { ResolverTs } from '@kubb/plugin-ts'
6
6
  import { pluginTsName } from '@kubb/plugin-ts'
7
7
  import type { ResolverZod } from '@kubb/plugin-zod'
@@ -10,70 +10,73 @@ import { File, jsxRenderer } from '@kubb/renderer-jsx'
10
10
  import { ClassClient } from '../components/ClassClient'
11
11
  import { WrapperClient } from '../components/WrapperClient'
12
12
  import type { PluginClient } from '../types'
13
+ import { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from '../utils.ts'
13
14
 
14
15
  type OperationData = {
15
16
  node: ast.OperationNode
16
17
  name: string
17
18
  tsResolver: ResolverTs
18
- zodResolver: ResolverZod | undefined
19
+ zodResolver: ResolverZod | null
19
20
  typeFile: ast.FileNode
20
- zodFile: ast.FileNode | undefined
21
+ zodFile: ast.FileNode | null
21
22
  }
22
23
 
23
24
  type Controller = {
24
25
  name: string
26
+ propertyName: string
25
27
  file: ast.FileNode
26
28
  operations: Array<OperationData>
27
29
  }
28
30
 
29
31
  function resolveTypeImportNames(node: ast.OperationNode, tsResolver: ResolverTs): Array<string> {
30
- const names: Array<string | undefined> = [
31
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
32
- tsResolver.resolveResponseName(node),
33
- ...node.parameters.filter((p) => p.in === 'path').map((p) => tsResolver.resolvePathParamsName(node, p)),
34
- ...node.parameters.filter((p) => p.in === 'query').map((p) => tsResolver.resolveQueryParamsName(node, p)),
35
- ...node.parameters.filter((p) => p.in === 'header').map((p) => tsResolver.resolveHeaderParamsName(node, p)),
36
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode)),
37
- ]
38
- return names.filter((n): n is string => Boolean(n))
32
+ return resolveOperationTypeNames(node, tsResolver, { order: 'body-response-first' })
39
33
  }
40
34
 
41
- function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
42
- const names: Array<string | undefined> = [
43
- zodResolver.resolveResponseName?.(node),
44
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined,
35
+ function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod, parser: PluginClient['resolvedOptions']['parser']): Array<string> {
36
+ const { query: queryParams } = getOperationParameters(node)
37
+ const names: Array<string | null | undefined> = [
38
+ resolveResponseParser(parser) === 'zod' ? zodResolver.resolveResponseName?.(node) : null,
39
+ resolveRequestParser(parser) === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
40
+ resolveQueryParamsParser(parser) === 'zod' && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]!) : null,
45
41
  ]
46
42
  return names.filter((n): n is string => Boolean(n))
47
43
  }
48
44
 
45
+ /**
46
+ * Built-in `operations` generator for `@kubb/plugin-client` when
47
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
48
+ * per operation and a shared constructor for request configuration.
49
+ */
49
50
  export const classClientGenerator = defineGenerator<PluginClient>({
50
51
  name: 'classClient',
51
52
  renderer: jsxRenderer,
52
53
  operations(nodes, ctx) {
53
- const { adapter, config, driver, resolver, root } = ctx
54
+ const { config, driver, resolver, root } = ctx
54
55
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options
55
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
56
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
56
57
 
57
58
  const pluginTs = driver.getPlugin(pluginTsName)
58
59
  if (!pluginTs) return null
59
60
 
60
61
  const tsResolver = driver.getResolver(pluginTsName)
61
62
  const tsPluginOptions = pluginTs.options
62
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
63
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
63
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null
64
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
64
65
 
65
66
  function buildOperationData(node: ast.OperationNode): OperationData {
66
- const typeFile = tsResolver.resolveFile(
67
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
68
- { root, output: tsPluginOptions?.output ?? output, group: tsPluginOptions?.group },
69
- )
67
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
68
+ root,
69
+ output: tsPluginOptions?.output ?? output,
70
+ group: tsPluginOptions?.group,
71
+ })
70
72
  const zodFile =
71
73
  zodResolver && pluginZod?.options
72
- ? zodResolver.resolveFile(
73
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
74
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group },
75
- )
76
- : undefined
74
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
75
+ root,
76
+ output: pluginZod.options?.output ?? output,
77
+ group: pluginZod.options?.group ?? undefined,
78
+ })
79
+ : null
77
80
 
78
81
  return {
79
82
  node: node,
@@ -86,30 +89,34 @@ export const classClientGenerator = defineGenerator<PluginClient>({
86
89
  }
87
90
 
88
91
  const controllers = nodes.reduce((acc, operationNode) => {
92
+ if (!ast.isHttpOperationNode(operationNode)) return acc
89
93
  const tag = operationNode.tags[0]
90
- const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag)) : 'Client'
94
+ const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveClassName('Client')
91
95
 
92
96
  if (!tag && !group) {
93
- const name = 'ApiClient'
94
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
97
+ const name = resolver.resolveClassName('ApiClient')
98
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
95
99
  const operationData = buildOperationData(operationNode)
96
100
  const previous = acc.find((item) => item.file.path === file.path)
97
101
 
98
102
  if (previous) {
99
103
  previous.operations.push(operationData)
100
104
  } else {
101
- acc.push({ name, file, operations: [operationData] })
105
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
102
106
  }
103
- } else if (tag) {
107
+ return acc
108
+ }
109
+
110
+ if (tag) {
104
111
  const name = groupName
105
- const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group })
112
+ const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group: group ?? undefined })
106
113
  const operationData = buildOperationData(operationNode)
107
114
  const previous = acc.find((item) => item.file.path === file.path)
108
115
 
109
116
  if (previous) {
110
117
  previous.operations.push(operationData)
111
118
  } else {
112
- acc.push({ name, file, operations: [operationData] })
119
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
113
120
  }
114
121
  }
115
122
 
@@ -141,7 +148,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
141
148
 
142
149
  ops.forEach((op) => {
143
150
  if (!op.zodFile || !zodResolver) return
144
- const names = resolveZodImportNames(op.node, zodResolver)
151
+ const names = resolveZodImportNames(op.node, zodResolver, parser)
145
152
  if (!zodImportsByFile.has(op.zodFile.path)) {
146
153
  zodImportsByFile.set(op.zodFile.path, new Set())
147
154
  }
@@ -157,9 +164,10 @@ export const classClientGenerator = defineGenerator<PluginClient>({
157
164
 
158
165
  const files = controllers.map(({ name, file, operations: ops }) => {
159
166
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops)
160
- const { zodImportsByFile, zodFilesByPath } =
161
- parser === 'zod' ? collectZodImports(ops) : { zodImportsByFile: new Map<string, Set<string>>(), zodFilesByPath: new Map<string, ast.FileNode>() }
162
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === 'multipart/form-data')
167
+ const { zodImportsByFile, zodFilesByPath } = isParserEnabled(parser)
168
+ ? collectZodImports(ops)
169
+ : { zodImportsByFile: new Map<string, Set<string>>(), zodFilesByPath: new Map<string, ast.FileNode>() }
170
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false)
163
171
 
164
172
  return (
165
173
  <File
@@ -167,18 +175,18 @@ export const classClientGenerator = defineGenerator<PluginClient>({
167
175
  baseName={file.baseName}
168
176
  path={file.path}
169
177
  meta={file.meta}
170
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
171
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
178
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
179
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
172
180
  >
173
181
  {importPath ? (
174
182
  <>
175
- <File.Import name={'fetch'} path={importPath} />
183
+ <File.Import name={'client'} path={importPath} />
176
184
  <File.Import name={['mergeConfig']} path={importPath} />
177
185
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
178
186
  </>
179
187
  ) : (
180
188
  <>
181
- <File.Import name={['fetch']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
189
+ <File.Import name={['client']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
182
190
  <File.Import name={['mergeConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
183
191
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
184
192
  </>
@@ -186,6 +194,10 @@ export const classClientGenerator = defineGenerator<PluginClient>({
186
194
 
187
195
  {hasFormData && <File.Import name={['buildFormData']} root={file.path} path={path.resolve(root, '.kubb/config.ts')} />}
188
196
 
197
+ {parser === 'zod' && zodResolver && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && (
198
+ <File.Import name={['z']} path="zod" isTypeOnly />
199
+ )}
200
+
189
201
  {Array.from(typeImportsByFile.entries()).map(([filePath, importSet]) => {
190
202
  const typeFile = typeFilesByPath.get(filePath)
191
203
  if (!typeFile) return null
@@ -194,7 +206,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
194
206
  return <File.Import key={filePath} name={importNames} root={file.path} path={typeFile.path} isTypeOnly />
195
207
  })}
196
208
 
197
- {parser === 'zod' &&
209
+ {isParserEnabled(parser) &&
198
210
  Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
199
211
  const zodFile = zodFilesByPath.get(filePath)
200
212
  if (!zodFile) return null
@@ -218,7 +230,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
218
230
  })
219
231
 
220
232
  if (sdk) {
221
- const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group })
233
+ const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group: group ?? undefined })
222
234
 
223
235
  files.push(
224
236
  <File
@@ -226,8 +238,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
226
238
  baseName={sdkFile.baseName}
227
239
  path={sdkFile.path}
228
240
  meta={sdkFile.meta}
229
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
230
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
241
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
242
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
231
243
  >
232
244
  {importPath ? (
233
245
  <File.Import name={['Client', 'RequestConfig']} path={importPath} isTypeOnly />
@@ -239,7 +251,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
239
251
  <File.Import key={name} name={[name]} root={sdkFile.path} path={file.path} />
240
252
  ))}
241
253
 
242
- <WrapperClient name={sdk.className} classNames={controllers.map(({ name }) => name)} />
254
+ <WrapperClient name={sdk.className} controllers={controllers.map(({ name, propertyName }) => ({ className: name, propertyName }))} />
243
255
  </File>,
244
256
  )
245
257
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path'
2
+ import { getOperationParameters, operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
2
3
  import { ast, defineGenerator } from '@kubb/core'
3
4
  import { pluginTsName } from '@kubb/plugin-ts'
4
5
  import { pluginZodName } from '@kubb/plugin-zod'
@@ -6,14 +7,21 @@ import { File, jsxRenderer } from '@kubb/renderer-jsx'
6
7
  import { Client } from '../components/Client'
7
8
  import { Url } from '../components/Url.tsx'
8
9
  import type { PluginClient } from '../types'
10
+ import { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from '../utils.ts'
9
11
 
12
+ /**
13
+ * Built-in operation generator for `@kubb/plugin-client`. Emits one async
14
+ * function per OpenAPI operation, plus the matching URL helper. Used when
15
+ * `clientType: 'function'` (the default).
16
+ */
10
17
  export const clientGenerator = defineGenerator<PluginClient>({
11
18
  name: 'client',
12
19
  renderer: jsxRenderer,
13
20
  operation(node, ctx) {
14
- const { adapter, config, driver, resolver, root } = ctx
21
+ if (!ast.isHttpOperationNode(node)) return null
22
+ const { config, driver, resolver, root } = ctx
15
23
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options
16
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
24
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
17
25
 
18
26
  const pluginTs = driver.getPlugin(pluginTsName)
19
27
 
@@ -23,71 +31,60 @@ export const clientGenerator = defineGenerator<PluginClient>({
23
31
 
24
32
  const tsResolver = driver.getResolver(pluginTsName)
25
33
 
26
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
27
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
34
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null
35
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
28
36
 
29
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
30
- const pathParams = casedParams.filter((p) => p.in === 'path')
31
- const queryParams = casedParams.filter((p) => p.in === 'query')
32
- const headerParams = casedParams.filter((p) => p.in === 'header')
37
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing })
33
38
 
34
- const importedTypeNames = [
35
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
36
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
37
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
38
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
39
- tsResolver.resolveResponseName(node),
40
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode)),
41
- ].filter(Boolean)
39
+ const { query: queryParams } = getOperationParameters(node)
42
40
 
43
- const importedZodNames =
44
- zodResolver && parser === 'zod'
45
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(Boolean)
46
- : []
41
+ const importedZodNames = zodResolver
42
+ ? [
43
+ resolveResponseParser(parser) === 'zod' ? zodResolver.resolveResponseName?.(node) : null,
44
+ resolveRequestParser(parser) === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
45
+ resolveQueryParamsParser(parser) === 'zod' && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]!) : null,
46
+ ].filter((name): name is string => Boolean(name))
47
+ : []
48
+
49
+ const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? (zodResolver.resolveDataName?.(node) ?? null) : null
47
50
 
48
51
  const meta = {
49
52
  name: resolver.resolveName(node.operationId),
50
- urlName: `get${resolver.resolveName(node.operationId).charAt(0).toUpperCase()}${resolver.resolveName(node.operationId).slice(1)}Url`,
51
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
52
- fileTs: tsResolver.resolveFile(
53
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
54
- {
55
- root,
56
- output: pluginTs.options?.output ?? output,
57
- group: pluginTs.options?.group,
58
- },
59
- ),
53
+ urlName: resolver.resolveUrlName(node),
54
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group: group ?? undefined }),
55
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
56
+ root,
57
+ output: pluginTs.options?.output ?? output,
58
+ group: pluginTs.options?.group ?? undefined,
59
+ }),
60
60
  fileZod:
61
61
  zodResolver && pluginZod?.options
62
- ? zodResolver.resolveFile(
63
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
64
- {
65
- root,
66
- output: pluginZod.options.output ?? output,
67
- group: pluginZod.options?.group,
68
- },
69
- )
70
- : undefined,
62
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
63
+ root,
64
+ output: pluginZod.options.output ?? output,
65
+ group: pluginZod.options?.group ?? undefined,
66
+ })
67
+ : null,
71
68
  } as const
72
69
 
73
- const isFormData = node.requestBody?.content?.[0]?.contentType === 'multipart/form-data'
70
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false
74
71
 
75
72
  return (
76
73
  <File
77
74
  baseName={meta.file.baseName}
78
75
  path={meta.file.path}
79
76
  meta={meta.file.meta}
80
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
81
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
77
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
78
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
82
79
  >
83
80
  {importPath ? (
84
81
  <>
85
- <File.Import name={'fetch'} path={importPath} />
82
+ <File.Import name={'client'} path={importPath} />
86
83
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
87
84
  </>
88
85
  ) : (
89
86
  <>
90
- <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
87
+ <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
91
88
  <File.Import
92
89
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
93
90
  root={meta.file.path}
@@ -97,11 +94,11 @@ export const clientGenerator = defineGenerator<PluginClient>({
97
94
  </>
98
95
  )}
99
96
 
100
- {isFormData && node.requestBody?.content?.[0]?.schema && (
101
- <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
102
- )}
97
+ {hasFormData && <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />}
98
+
99
+ {zodRequestName && <File.Import name={['z']} path="zod" isTypeOnly />}
103
100
 
104
- {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} />}
105
102
 
106
103
  {meta.fileTs && importedTypeNames.length > 0 && (
107
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, jsxRenderer } 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: jsxRenderer,
10
16
  operations(nodes, ctx) {
11
- const { config, resolver, adapter, root } = 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(adapter.inputNode, { output, config })}
59
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
64
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName, isAggregation: true } })}
65
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName, isAggregation: true } })}
60
66
  >
61
67
  {clients.map((client) => (
62
68
  <File.Import key={client.name} name={[client.name]} root={file.path} path={client.file.path} />
@@ -0,0 +1,47 @@
1
+ import { Url } from '@internals/utils'
2
+ import { ast, defineGenerator } from '@kubb/core'
3
+ import type { PluginClient } from '../types'
4
+
5
+ /**
6
+ * Generates an `operations.ts` file that re-exports every operation grouped
7
+ * by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
8
+ * for building meta-tooling on top of the generated client (route
9
+ * registries, API explorers).
10
+ */
11
+ export const operationsGenerator = defineGenerator<PluginClient>({
12
+ name: 'client',
13
+ operations(nodes, ctx) {
14
+ const { config, resolver, root } = ctx
15
+ const { output, group } = ctx.options
16
+
17
+ const name = 'operations'
18
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
19
+
20
+ const operationsObject: Record<string, { path: string; method: string }> = {}
21
+ for (const node of nodes) {
22
+ if (!ast.isHttpOperationNode(node)) continue
23
+ operationsObject[node.operationId] = {
24
+ path: Url.toPath(node.path),
25
+ method: node.method.toLowerCase(),
26
+ }
27
+ }
28
+
29
+ return [
30
+ ast.createFile({
31
+ baseName: file.baseName,
32
+ path: file.path,
33
+ meta: file.meta,
34
+ banner: resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } }),
35
+ footer: resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } }),
36
+ sources: [
37
+ ast.createSource({
38
+ name,
39
+ isExportable: true,
40
+ isIndexable: true,
41
+ nodes: [ast.createConst({ name, export: true, nodes: [ast.createText(JSON.stringify(operationsObject, undefined, 2))] })],
42
+ }),
43
+ ],
44
+ }),
45
+ ]
46
+ },
47
+ })