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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +39 -24
  2. package/dist/clients/axios.cjs +26 -4
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +11 -5
  5. package/dist/clients/axios.js +26 -4
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +77 -9
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +10 -3
  10. package/dist/clients/fetch.js +77 -9
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +625 -354
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +160 -94
  15. package/dist/index.js +626 -355
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.d.ts +1 -1
  19. package/dist/templates/clients/axios.source.js +1 -1
  20. package/dist/templates/clients/fetch.source.cjs +1 -1
  21. package/dist/templates/clients/fetch.source.d.ts +1 -1
  22. package/dist/templates/clients/fetch.source.js +1 -1
  23. package/dist/templates/config.source.d.ts +1 -1
  24. package/extension.yaml +792 -301
  25. package/package.json +11 -16
  26. package/src/clients/axios.ts +41 -7
  27. package/src/clients/fetch.ts +106 -6
  28. package/src/components/ClassClient.tsx +19 -20
  29. package/src/components/Client.tsx +74 -53
  30. package/src/components/Operations.tsx +2 -1
  31. package/src/components/StaticClassClient.tsx +19 -20
  32. package/src/components/Url.tsx +8 -9
  33. package/src/components/WrapperClient.tsx +9 -5
  34. package/src/functionParams.ts +8 -8
  35. package/src/generators/classClientGenerator.tsx +51 -47
  36. package/src/generators/clientGenerator.tsx +37 -48
  37. package/src/generators/groupedClientGenerator.tsx +14 -8
  38. package/src/generators/operationsGenerator.tsx +14 -8
  39. package/src/generators/staticClassClientGenerator.tsx +45 -41
  40. package/src/plugin.ts +27 -26
  41. package/src/resolvers/resolverClient.ts +31 -8
  42. package/src/types.ts +93 -55
  43. package/src/utils.ts +35 -56
  44. package/templates/clients/axios.ts +0 -73
  45. package/templates/clients/fetch.ts +0 -96
  46. package/templates/config.ts +0 -43
  47. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  48. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -1,19 +1,20 @@
1
+ import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
1
2
  import { buildJSDoc, URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
3
+ import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
4
5
  import { functionPrinter } from '@kubb/plugin-ts'
5
6
  import type { ResolverZod } from '@kubb/plugin-zod'
6
7
  import { File } from '@kubb/renderer-jsx'
7
8
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
9
  import type { PluginClient } from '../types.ts'
9
- import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement, getComments } from '../utils.ts'
10
- import { Client } from './Client.tsx'
10
+ import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement } from '../utils.ts'
11
+ import { buildClientParamsNode } from './Client.tsx'
11
12
 
12
13
  type OperationData = {
13
14
  node: ast.OperationNode
14
15
  name: string
15
16
  tsResolver: ResolverTs
16
- zodResolver?: ResolverZod
17
+ zodResolver?: ResolverZod | null
17
18
  }
18
19
 
19
20
  type Props = {
@@ -21,7 +22,7 @@ type Props = {
21
22
  isExportable?: boolean
22
23
  isIndexable?: boolean
23
24
  operations: Array<OperationData>
24
- baseURL: string | undefined
25
+ baseURL: string | null | undefined
25
26
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
26
27
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
27
28
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -34,8 +35,8 @@ type GenerateMethodProps = {
34
35
  node: ast.OperationNode
35
36
  name: string
36
37
  tsResolver: ResolverTs
37
- zodResolver?: ResolverZod
38
- baseURL: string | undefined
38
+ zodResolver?: ResolverZod | null
39
+ baseURL: string | null | undefined
39
40
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
40
41
  parser: PluginClient['resolvedOptions']['parser'] | undefined
41
42
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -57,26 +58,25 @@ function generateMethod({
57
58
  paramsCasing,
58
59
  pathParamsType,
59
60
  }: GenerateMethodProps): string {
61
+ if (!ast.isHttpOperationNode(node)) return ''
60
62
  const path = new URLPath(node.path, { casing: paramsCasing })
61
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
62
- const isFormData = contentType === 'multipart/form-data'
63
- const headerParamsName =
64
- node.parameters.filter((p) => p.in === 'header').length > 0
65
- ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === 'header')[0]!)
66
- : undefined
67
- const headers = buildHeaders(contentType, !!headerParamsName)
63
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
64
+ const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
65
+ const { header: headerParams } = getOperationParameters(node)
66
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
67
+ const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
68
68
  const generics = buildGenerics(node, tsResolver)
69
- const paramsNode = Client.getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
69
+ const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
70
70
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
71
- const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
72
- const jsdoc = buildJSDoc(getComments(node))
71
+ const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers })
72
+ const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
73
73
 
74
74
  const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
75
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
75
+ const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
76
76
  const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver })
77
77
 
78
78
  const methodBody = [
79
- 'const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)',
79
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
80
80
  '',
81
81
  requestDataLine,
82
82
  formDataLine,
@@ -127,4 +127,3 @@ export function StaticClassClient({
127
127
  </File.Source>
128
128
  )
129
129
  }
130
- StaticClassClient.getParams = Client.getParams
@@ -1,3 +1,4 @@
1
+ import { buildParamsMapping, getOperationParameters } from '@internals/shared'
1
2
  import { isValidVarName, URLPath } from '@internals/utils'
2
3
  import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
@@ -5,14 +6,13 @@ import { functionPrinter } from '@kubb/plugin-ts'
5
6
  import { Const, File, Function } from '@kubb/renderer-jsx'
6
7
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
7
8
  import type { PluginClient } from '../types.ts'
8
- import { buildParamsMapping } from '../utils.ts'
9
9
 
10
10
  type Props = {
11
11
  name: string
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']
@@ -30,7 +30,7 @@ type GetParamsProps = {
30
30
 
31
31
  const declarationPrinter = functionPrinter({ mode: 'declaration' })
32
32
 
33
- function getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver }: GetParamsProps): ast.FunctionParametersNode {
33
+ export function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver }: GetParamsProps): ast.FunctionParametersNode {
34
34
  // Build a URL-only node with only path params (no body, query, header)
35
35
  const urlNode: ast.OperationNode = {
36
36
  ...node,
@@ -57,9 +57,10 @@ export function Url({
57
57
  node,
58
58
  tsResolver,
59
59
  }: Props): KubbReactNode {
60
+ if (!ast.isHttpOperationNode(node)) return null
60
61
  const path = new URLPath(node.path)
61
62
 
62
- const paramsNode = getParams({
63
+ const paramsNode = buildUrlParamsNode({
63
64
  paramsType,
64
65
  paramsCasing,
65
66
  pathParamsType,
@@ -68,9 +69,9 @@ export function Url({
68
69
  })
69
70
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
70
71
 
71
- const originalPathParams = node.parameters.filter((p) => p.in === 'path')
72
- const casedPathParams = ast.caseParams(originalPathParams, paramsCasing)
73
- const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : undefined
72
+ const { path: originalPathParams } = getOperationParameters(node)
73
+ const { path: casedPathParams } = getOperationParameters(node, { paramsCasing })
74
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null
74
75
 
75
76
  return (
76
77
  <File.Source name={name} isExportable={isExportable} isIndexable={isIndexable}>
@@ -88,5 +89,3 @@ export function Url({
88
89
  </File.Source>
89
90
  )
90
91
  }
91
-
92
- Url.getParams = getParams
@@ -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,12 @@
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 { 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'
8
8
  import { pluginZodName } from '@kubb/plugin-zod'
9
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
9
+ import { File, jsxRendererSync } 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'
@@ -15,65 +15,65 @@ 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 = {
24
24
  name: string
25
+ propertyName: string
25
26
  file: ast.FileNode
26
27
  operations: Array<OperationData>
27
28
  }
28
29
 
29
30
  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))
31
+ return resolveOperationTypeNames(node, tsResolver, { order: 'body-response-first' })
39
32
  }
40
33
 
41
34
  function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
42
- const names: Array<string | undefined> = [
35
+ const names: Array<string | null | undefined> = [
43
36
  zodResolver.resolveResponseName?.(node),
44
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined,
37
+ node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
45
38
  ]
46
39
  return names.filter((n): n is string => Boolean(n))
47
40
  }
48
41
 
42
+ /**
43
+ * Built-in `operations` generator for `@kubb/plugin-client` when
44
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
45
+ * per operation and a shared constructor for request configuration.
46
+ */
49
47
  export const classClientGenerator = defineGenerator<PluginClient>({
50
48
  name: 'classClient',
51
- renderer: jsxRenderer,
49
+ renderer: jsxRendererSync,
52
50
  operations(nodes, ctx) {
53
- const { adapter, config, driver, resolver, root } = ctx
51
+ const { config, driver, resolver, root } = ctx
54
52
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options
55
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
53
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
56
54
 
57
55
  const pluginTs = driver.getPlugin(pluginTsName)
58
56
  if (!pluginTs) return null
59
57
 
60
58
  const tsResolver = driver.getResolver(pluginTsName)
61
59
  const tsPluginOptions = pluginTs.options
62
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
63
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
60
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
61
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
64
62
 
65
63
  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
- )
64
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
65
+ root,
66
+ output: tsPluginOptions?.output ?? output,
67
+ group: tsPluginOptions?.group,
68
+ })
70
69
  const zodFile =
71
70
  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
71
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
72
+ root,
73
+ output: pluginZod.options?.output ?? output,
74
+ group: pluginZod.options?.group ?? undefined,
75
+ })
76
+ : null
77
77
 
78
78
  return {
79
79
  node: node,
@@ -86,30 +86,34 @@ export const classClientGenerator = defineGenerator<PluginClient>({
86
86
  }
87
87
 
88
88
  const controllers = nodes.reduce((acc, operationNode) => {
89
+ if (!ast.isHttpOperationNode(operationNode)) return acc
89
90
  const tag = operationNode.tags[0]
90
- const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag)) : 'Client'
91
+ const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveGroupName('Client')
91
92
 
92
93
  if (!tag && !group) {
93
- const name = 'ApiClient'
94
- const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group })
94
+ const name = resolver.resolveClassName('ApiClient')
95
+ const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
95
96
  const operationData = buildOperationData(operationNode)
96
97
  const previous = acc.find((item) => item.file.path === file.path)
97
98
 
98
99
  if (previous) {
99
100
  previous.operations.push(operationData)
100
101
  } else {
101
- acc.push({ name, file, operations: [operationData] })
102
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
102
103
  }
103
- } else if (tag) {
104
+ return acc
105
+ }
106
+
107
+ if (tag) {
104
108
  const name = groupName
105
- const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group })
109
+ const file = resolver.resolveFile({ name, extname: '.ts', tag }, { root, output, group: group ?? undefined })
106
110
  const operationData = buildOperationData(operationNode)
107
111
  const previous = acc.find((item) => item.file.path === file.path)
108
112
 
109
113
  if (previous) {
110
114
  previous.operations.push(operationData)
111
115
  } else {
112
- acc.push({ name, file, operations: [operationData] })
116
+ acc.push({ name, propertyName: resolver.resolveClientPropertyName(name), file, operations: [operationData] })
113
117
  }
114
118
  }
115
119
 
@@ -159,7 +163,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
159
163
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops)
160
164
  const { zodImportsByFile, zodFilesByPath } =
161
165
  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')
166
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false)
163
167
 
164
168
  return (
165
169
  <File
@@ -167,18 +171,18 @@ export const classClientGenerator = defineGenerator<PluginClient>({
167
171
  baseName={file.baseName}
168
172
  path={file.path}
169
173
  meta={file.meta}
170
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
171
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
174
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
175
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
172
176
  >
173
177
  {importPath ? (
174
178
  <>
175
- <File.Import name={'fetch'} path={importPath} />
179
+ <File.Import name={'client'} path={importPath} />
176
180
  <File.Import name={['mergeConfig']} path={importPath} />
177
181
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
178
182
  </>
179
183
  ) : (
180
184
  <>
181
- <File.Import name={['fetch']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
185
+ <File.Import name={['client']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
182
186
  <File.Import name={['mergeConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} />
183
187
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} root={file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
184
188
  </>
@@ -218,7 +222,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
218
222
  })
219
223
 
220
224
  if (sdk) {
221
- const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group })
225
+ const sdkFile = resolver.resolveFile({ name: sdk.className, extname: '.ts' }, { root, output, group: group ?? undefined })
222
226
 
223
227
  files.push(
224
228
  <File
@@ -226,8 +230,8 @@ export const classClientGenerator = defineGenerator<PluginClient>({
226
230
  baseName={sdkFile.baseName}
227
231
  path={sdkFile.path}
228
232
  meta={sdkFile.meta}
229
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
230
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
233
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
234
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: sdkFile.path, baseName: sdkFile.baseName } })}
231
235
  >
232
236
  {importPath ? (
233
237
  <File.Import name={['Client', 'RequestConfig']} path={importPath} isTypeOnly />
@@ -239,7 +243,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
239
243
  <File.Import key={name} name={[name]} root={sdkFile.path} path={file.path} />
240
244
  ))}
241
245
 
242
- <WrapperClient name={sdk.className} classNames={controllers.map(({ name }) => name)} />
246
+ <WrapperClient name={sdk.className} controllers={controllers.map(({ name, propertyName }) => ({ className: name, propertyName }))} />
243
247
  </File>,
244
248
  )
245
249
  }
@@ -1,19 +1,26 @@
1
1
  import path from 'node:path'
2
+ import { 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'
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
+ if (!ast.isHttpOperationNode(node)) return null
21
+ const { config, driver, resolver, root } = ctx
15
22
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options
16
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL
23
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
17
24
 
18
25
  const pluginTs = driver.getPlugin(pluginTsName)
19
26
 
@@ -23,71 +30,55 @@ export const clientGenerator = defineGenerator<PluginClient>({
23
30
 
24
31
  const tsResolver = driver.getResolver(pluginTsName)
25
32
 
26
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
27
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
33
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
34
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
28
35
 
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)
36
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing })
42
37
 
43
38
  const importedZodNames =
44
39
  zodResolver && parser === 'zod'
45
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(Boolean)
40
+ ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter(
41
+ (name): name is string => Boolean(name),
42
+ )
46
43
  : []
47
44
 
48
45
  const meta = {
49
46
  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
- ),
47
+ urlName: resolver.resolveUrlName(node),
48
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group: group ?? undefined }),
49
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
50
+ root,
51
+ output: pluginTs.options?.output ?? output,
52
+ group: pluginTs.options?.group ?? undefined,
53
+ }),
60
54
  fileZod:
61
55
  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,
56
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
57
+ root,
58
+ output: pluginZod.options.output ?? output,
59
+ group: pluginZod.options?.group ?? undefined,
60
+ })
61
+ : null,
71
62
  } as const
72
63
 
73
- const isFormData = node.requestBody?.content?.[0]?.contentType === 'multipart/form-data'
64
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false
74
65
 
75
66
  return (
76
67
  <File
77
68
  baseName={meta.file.baseName}
78
69
  path={meta.file.path}
79
70
  meta={meta.file.meta}
80
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
81
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
71
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
72
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
82
73
  >
83
74
  {importPath ? (
84
75
  <>
85
- <File.Import name={'fetch'} path={importPath} />
76
+ <File.Import name={'client'} path={importPath} />
86
77
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={importPath} isTypeOnly />
87
78
  </>
88
79
  ) : (
89
80
  <>
90
- <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
81
+ <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
91
82
  <File.Import
92
83
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
93
84
  root={meta.file.path}
@@ -97,11 +88,9 @@ export const clientGenerator = defineGenerator<PluginClient>({
97
88
  </>
98
89
  )}
99
90
 
100
- {isFormData && node.requestBody?.content?.[0]?.schema && (
101
- <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
102
- )}
91
+ {hasFormData && <File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />}
103
92
 
104
- {meta.fileZod && importedZodNames.length > 0 && <File.Import name={importedZodNames as string[]} root={meta.file.path} path={meta.fileZod.path} />}
93
+ {meta.fileZod && importedZodNames.length > 0 && <File.Import name={importedZodNames as Array<string>} root={meta.file.path} path={meta.fileZod.path} />}
105
94
 
106
95
  {meta.fileTs && importedTypeNames.length > 0 && (
107
96
  <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} />