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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +24 -4
  2. package/dist/clients/axios.cjs +25 -3
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +9 -2
  5. package/dist/clients/axios.js +25 -3
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +20 -2
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +9 -2
  10. package/dist/clients/fetch.js +20 -2
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +524 -301
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +150 -84
  15. package/dist/index.js +525 -302
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.js +1 -1
  19. package/dist/templates/clients/fetch.source.cjs +1 -1
  20. package/dist/templates/clients/fetch.source.js +1 -1
  21. package/extension.yaml +1293 -0
  22. package/package.json +11 -17
  23. package/src/clients/axios.ts +41 -7
  24. package/src/clients/fetch.ts +30 -3
  25. package/src/components/ClassClient.tsx +17 -19
  26. package/src/components/Client.tsx +68 -51
  27. package/src/components/StaticClassClient.tsx +17 -19
  28. package/src/components/Url.tsx +7 -9
  29. package/src/components/WrapperClient.tsx +9 -5
  30. package/src/functionParams.ts +8 -8
  31. package/src/generators/classClientGenerator.tsx +40 -38
  32. package/src/generators/clientGenerator.tsx +32 -35
  33. package/src/generators/groupedClientGenerator.tsx +14 -8
  34. package/src/generators/operationsGenerator.tsx +12 -6
  35. package/src/generators/staticClassClientGenerator.tsx +34 -32
  36. package/src/plugin.ts +24 -11
  37. package/src/resolvers/resolverClient.ts +31 -8
  38. package/src/types.ts +90 -53
  39. package/src/utils.ts +30 -53
  40. package/templates/clients/axios.ts +0 -73
  41. package/templates/clients/fetch.ts +0 -96
  42. package/templates/config.ts +0 -43
@@ -1,17 +1,21 @@
1
- import { camelCase } from '@internals/utils'
2
1
  import { File } from '@kubb/renderer-jsx'
3
2
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
4
3
 
4
+ type ClientController = {
5
+ className: string
6
+ propertyName: string
7
+ }
8
+
5
9
  type Props = {
6
10
  name: string
7
- classNames: Array<string>
11
+ controllers: Array<ClientController>
8
12
  isExportable?: boolean
9
13
  isIndexable?: boolean
10
14
  }
11
15
 
12
- export function WrapperClient({ name, classNames, isExportable = true, isIndexable = true }: Props): KubbReactNode {
13
- const properties = classNames.map((className) => ` readonly ${camelCase(className)}: ${className}`).join('\n')
14
- const assignments = classNames.map((className) => ` this.${camelCase(className)} = new ${className}(config)`).join('\n')
16
+ export function WrapperClient({ name, controllers, isExportable = true, isIndexable = true }: Props): KubbReactNode {
17
+ const properties = controllers.map(({ className, propertyName }) => ` readonly ${propertyName}: ${className}`).join('\n')
18
+ const assignments = controllers.map(({ className, propertyName }) => ` this.${propertyName} = new ${className}(config)`).join('\n')
15
19
 
16
20
  const classCode = `export class ${name} {
17
21
  ${properties}
@@ -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() {
@@ -1,12 +1,13 @@
1
1
  import path from 'node:path'
2
- import { camelCase, pascalCase } from '@internals/utils'
2
+ import { resolveOperationTypeNames } from '@internals/shared'
3
+ import { camelCase } from '@internals/utils'
3
4
  import type { ast } from '@kubb/core'
4
5
  import { defineGenerator } from '@kubb/core'
5
6
  import type { ResolverTs } from '@kubb/plugin-ts'
6
7
  import { pluginTsName } from '@kubb/plugin-ts'
7
8
  import type { ResolverZod } from '@kubb/plugin-zod'
8
9
  import { pluginZodName } from '@kubb/plugin-zod'
9
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
10
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
10
11
  import { ClassClient } from '../components/ClassClient'
11
12
  import { WrapperClient } from '../components/WrapperClient'
12
13
  import type { PluginClient } from '../types'
@@ -15,52 +16,50 @@ 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
35
  function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
42
- const names: Array<string | undefined> = [
36
+ const names: Array<string | null | undefined> = [
43
37
  zodResolver.resolveResponseName?.(node),
44
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined,
38
+ node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
45
39
  ]
46
40
  return names.filter((n): n is string => Boolean(n))
47
41
  }
48
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
+ */
49
48
  export const classClientGenerator = defineGenerator<PluginClient>({
50
49
  name: 'classClient',
51
- renderer: jsxRenderer,
50
+ renderer: jsxRendererSync,
52
51
  operations(nodes, ctx) {
53
- const { adapter, config, driver, resolver, root } = ctx
52
+ const { config, driver, resolver, root } = ctx
54
53
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options
55
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
54
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
56
55
 
57
56
  const pluginTs = driver.getPlugin(pluginTsName)
58
57
  if (!pluginTs) return null
59
58
 
60
59
  const tsResolver = driver.getResolver(pluginTsName)
61
60
  const tsPluginOptions = pluginTs.options
62
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
63
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
61
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
62
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
64
63
 
65
64
  function buildOperationData(node: ast.OperationNode): OperationData {
66
65
  const typeFile = tsResolver.resolveFile(
@@ -71,9 +70,9 @@ export const classClientGenerator = defineGenerator<PluginClient>({
71
70
  zodResolver && pluginZod?.options
72
71
  ? zodResolver.resolveFile(
73
72
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
74
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group },
73
+ { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group ?? undefined },
75
74
  )
76
- : undefined
75
+ : null
77
76
 
78
77
  return {
79
78
  node: node,
@@ -87,29 +86,32 @@ export const classClientGenerator = defineGenerator<PluginClient>({
87
86
 
88
87
  const controllers = nodes.reduce((acc, operationNode) => {
89
88
  const tag = operationNode.tags[0]
90
- const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag)) : 'Client'
89
+ const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveGroupName('Client')
91
90
 
92
91
  if (!tag && !group) {
93
- const name = 'ApiClient'
94
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
92
+ const name = resolver.resolveClassName('ApiClient')
93
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
95
94
  const operationData = buildOperationData(operationNode)
96
95
  const previous = acc.find((item) => item.file.path === file.path)
97
96
 
98
97
  if (previous) {
99
98
  previous.operations.push(operationData)
100
99
  } else {
101
- acc.push({ name, file, operations: [operationData] })
100
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
102
101
  }
103
- } else if (tag) {
102
+ return acc
103
+ }
104
+
105
+ if (tag) {
104
106
  const name = groupName
105
- 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 })
106
108
  const operationData = buildOperationData(operationNode)
107
109
  const previous = acc.find((item) => item.file.path === file.path)
108
110
 
109
111
  if (previous) {
110
112
  previous.operations.push(operationData)
111
113
  } else {
112
- acc.push({ name, file, operations: [operationData] })
114
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
113
115
  }
114
116
  }
115
117
 
@@ -159,7 +161,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
159
161
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops)
160
162
  const { zodImportsByFile, zodFilesByPath } =
161
163
  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')
164
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false)
163
165
 
164
166
  return (
165
167
  <File
@@ -167,18 +169,18 @@ export const classClientGenerator = defineGenerator<PluginClient>({
167
169
  baseName={file.baseName}
168
170
  path={file.path}
169
171
  meta={file.meta}
170
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
171
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
172
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
173
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
172
174
  >
173
175
  {importPath ? (
174
176
  <>
175
- <File.Import name={'fetch'} path={importPath} />
177
+ <File.Import name={'client'} path={importPath} />
176
178
  <File.Import name={['mergeConfig']} path={importPath} />
177
179
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
178
180
  </>
179
181
  ) : (
180
182
  <>
181
- <File.Import name={['fetch']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
183
+ <File.Import name={['client']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
182
184
  <File.Import name={['mergeConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
183
185
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
184
186
  </>
@@ -218,7 +220,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
218
220
  })
219
221
 
220
222
  if (sdk) {
221
- 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 })
222
224
 
223
225
  files.push(
224
226
  <File
@@ -226,8 +228,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
226
228
  baseName={sdkFile.baseName}
227
229
  path={sdkFile.path}
228
230
  meta={sdkFile.meta}
229
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
230
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
231
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
232
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
231
233
  >
232
234
  {importPath ? (
233
235
  <File.Import name={['Client', 'RequestConfig']} path={importPath} isTypeOnly />
@@ -239,7 +241,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
239
241
  <File.Import key={name} name={[name]} root={sdkFile.path} path={file.path} />
240
242
  ))}
241
243
 
242
- <WrapperClient name={sdk.className} classNames={controllers.map(({ name }) => name)} />
244
+ <WrapperClient name={sdk.className} controllers={controllers.map(({ name, propertyName }) => ({ className: name, propertyName }))} />
243
245
  </File>,
244
246
  )
245
247
  }
@@ -1,19 +1,25 @@
1
1
  import path from 'node:path'
2
- import { ast, defineGenerator } from '@kubb/core'
2
+ import { resolveOperationTypeNames } from '@internals/shared'
3
+ import { defineGenerator } from '@kubb/core'
3
4
  import { pluginTsName } from '@kubb/plugin-ts'
4
5
  import { pluginZodName } from '@kubb/plugin-zod'
5
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
6
+ import { File, jsxRendererSync } 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'
9
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
+ */
10
16
  export const clientGenerator = defineGenerator<PluginClient>({
11
17
  name: 'client',
12
- renderer: jsxRenderer,
18
+ renderer: jsxRendererSync,
13
19
  operation(node, ctx) {
14
- const { adapter, config, driver, resolver, root } = ctx
20
+ const { config, driver, resolver, root } = ctx
15
21
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options
16
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
22
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
17
23
 
18
24
  const pluginTs = driver.getPlugin(pluginTsName)
19
25
 
@@ -23,38 +29,31 @@ export const clientGenerator = defineGenerator<PluginClient>({
23
29
 
24
30
  const tsResolver = driver.getResolver(pluginTsName)
25
31
 
26
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
27
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
32
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
33
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
28
34
 
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')
33
-
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)
35
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing })
42
36
 
43
37
  const importedZodNames =
44
38
  zodResolver && parser === 'zod'
45
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(Boolean)
39
+ ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter(
40
+ (name): name is string => Boolean(name),
41
+ )
46
42
  : []
47
43
 
48
44
  const meta = {
49
45
  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 }),
46
+ urlName: resolver.resolveUrlName(node),
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
+ ),
52
51
  fileTs: tsResolver.resolveFile(
53
52
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
54
53
  {
55
54
  root,
56
55
  output: pluginTs.options?.output ?? output,
57
- group: pluginTs.options?.group,
56
+ group: pluginTs.options?.group ?? undefined,
58
57
  },
59
58
  ),
60
59
  fileZod:
@@ -64,30 +63,30 @@ export const clientGenerator = defineGenerator<PluginClient>({
64
63
  {
65
64
  root,
66
65
  output: pluginZod.options.output ?? output,
67
- group: pluginZod.options?.group,
66
+ group: pluginZod.options?.group ?? undefined,
68
67
  },
69
68
  )
70
- : undefined,
69
+ : null,
71
70
  } as const
72
71
 
73
- const isFormData = node.requestBody?.content?.[0]?.contentType === 'multipart/form-data'
72
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false
74
73
 
75
74
  return (
76
75
  <File
77
76
  baseName={meta.file.baseName}
78
77
  path={meta.file.path}
79
78
  meta={meta.file.meta}
80
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
81
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
79
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
80
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
82
81
  >
83
82
  {importPath ? (
84
83
  <>
85
- <File.Import name={'fetch'} path={importPath} />
84
+ <File.Import name={'client'} path={importPath} />
86
85
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
87
86
  </>
88
87
  ) : (
89
88
  <>
90
- <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
89
+ <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
91
90
  <File.Import
92
91
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
93
92
  root={meta.file.path}
@@ -97,11 +96,9 @@ export const clientGenerator = defineGenerator<PluginClient>({
97
96
  </>
98
97
  )}
99
98
 
100
- {isFormData && node.requestBody?.content?.[0]?.schema && (
101
- <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
102
- )}
99
+ {hasFormData && <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />}
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 />
@@ -1,30 +1,36 @@
1
1
  import { camelCase } from '@internals/utils'
2
2
  import type { ast } from '@kubb/core'
3
3
  import { defineGenerator } from '@kubb/core'
4
- import { File, Function, jsxRenderer } from '@kubb/renderer-jsx'
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
- renderer: jsxRenderer,
15
+ renderer: jsxRendererSync,
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} />
@@ -1,25 +1,31 @@
1
1
  import { defineGenerator } from '@kubb/core'
2
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
2
+ 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
- renderer: jsxRenderer,
14
+ renderer: jsxRendererSync,
9
15
  operations(nodes, ctx) {
10
- const { config, resolver, adapter, root } = 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(adapter.inputNode, { output, config })}
22
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
27
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
28
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
23
29
  >
24
30
  <Operations name={name} nodes={nodes} />
25
31
  </File>