@kubb/plugin-client 5.0.0-beta.42 → 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.
@@ -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
+ })
@@ -1,14 +1,15 @@
1
1
  import path from 'node:path'
2
- import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
2
+ import { getOperationParameters, operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
3
  import { camelCase } from '@internals/utils'
4
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, jsxRendererSync } from '@kubb/renderer-jsx'
9
+ import { File, jsxRenderer } from '@kubb/renderer-jsx'
10
10
  import { StaticClassClient } from '../components/StaticClassClient'
11
11
  import type { PluginClient } from '../types'
12
+ import { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from '../utils.ts'
12
13
 
13
14
  type OperationData = {
14
15
  node: ast.OperationNode
@@ -29,10 +30,12 @@ function resolveTypeImportNames(node: ast.OperationNode, tsResolver: ResolverTs)
29
30
  return resolveOperationTypeNames(node, tsResolver, { order: 'body-response-first' })
30
31
  }
31
32
 
32
- function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
33
+ function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod, parser: PluginClient['resolvedOptions']['parser']): Array<string> {
34
+ const { query: queryParams } = getOperationParameters(node)
33
35
  const names: Array<string | null | undefined> = [
34
- zodResolver.resolveResponseName?.(node),
35
- node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
36
+ resolveResponseParser(parser) === 'zod' ? zodResolver.resolveResponseName?.(node) : null,
37
+ resolveRequestParser(parser) === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
38
+ resolveQueryParamsParser(parser) === 'zod' && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]!) : null,
36
39
  ]
37
40
  return names.filter((n): n is string => Boolean(n))
38
41
  }
@@ -45,7 +48,7 @@ function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod
45
48
  */
46
49
  export const staticClassClientGenerator = defineGenerator<PluginClient>({
47
50
  name: 'staticClassClient',
48
- renderer: jsxRendererSync,
51
+ renderer: jsxRenderer,
49
52
  operations(nodes, ctx) {
50
53
  const { config, driver, resolver, root } = ctx
51
54
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath } = ctx.options
@@ -56,7 +59,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
56
59
 
57
60
  const tsResolver = driver.getResolver(pluginTsName)
58
61
  const tsPluginOptions = pluginTs.options
59
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
62
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null
60
63
  const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
61
64
 
62
65
  function buildOperationData(node: ast.OperationNode): OperationData {
@@ -87,7 +90,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
87
90
  const controllers = nodes.reduce((acc, operationNode) => {
88
91
  if (!ast.isHttpOperationNode(operationNode)) return acc
89
92
  const tag = operationNode.tags[0]
90
- const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveGroupName('Client')
93
+ const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveClassName('Client')
91
94
 
92
95
  if (!tag && !group) {
93
96
  const name = resolver.resolveClassName('ApiClient')
@@ -144,7 +147,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
144
147
 
145
148
  ops.forEach((op) => {
146
149
  if (!op.zodFile || !zodResolver) return
147
- const names = resolveZodImportNames(op.node, zodResolver)
150
+ const names = resolveZodImportNames(op.node, zodResolver, parser)
148
151
  if (!zodImportsByFile.has(op.zodFile.path)) {
149
152
  zodImportsByFile.set(op.zodFile.path, new Set())
150
153
  }
@@ -162,8 +165,9 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
162
165
  <>
163
166
  {controllers.map(({ name, file, operations: ops }) => {
164
167
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops)
165
- const { zodImportsByFile, zodFilesByPath } =
166
- parser === 'zod' ? collectZodImports(ops) : { zodImportsByFile: new Map<string, Set<string>>(), zodFilesByPath: new Map<string, ast.FileNode>() }
168
+ const { zodImportsByFile, zodFilesByPath } = isParserEnabled(parser)
169
+ ? collectZodImports(ops)
170
+ : { zodImportsByFile: new Map<string, Set<string>>(), zodFilesByPath: new Map<string, ast.FileNode>() }
167
171
  const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false)
168
172
 
169
173
  return (
@@ -196,6 +200,10 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
196
200
 
197
201
  {hasFormData && <File.Import name={['buildFormData']} root={file.path} path={path.resolve(root, '.kubb/config.ts')} />}
198
202
 
203
+ {parser === 'zod' && zodResolver && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && (
204
+ <File.Import name={['z']} path="zod" isTypeOnly />
205
+ )}
206
+
199
207
  {Array.from(typeImportsByFile.entries()).map(([filePath, importSet]) => {
200
208
  const typeFile = typeFilesByPath.get(filePath)
201
209
  if (!typeFile) return null
@@ -204,7 +212,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
204
212
  return <File.Import key={filePath} name={importNames} root={file.path} path={typeFile.path} isTypeOnly />
205
213
  })}
206
214
 
207
- {parser === 'zod' &&
215
+ {isParserEnabled(parser) &&
208
216
  Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
209
217
  const zodFile = zodFilesByPath.get(filePath)
210
218
  if (!zodFile) return null
package/src/index.ts CHANGED
@@ -2,8 +2,9 @@ export { Client } from './components/Client.tsx'
2
2
  export { classClientGenerator } from './generators/classClientGenerator.tsx'
3
3
  export { clientGenerator } from './generators/clientGenerator.tsx'
4
4
  export { groupedClientGenerator } from './generators/groupedClientGenerator.tsx'
5
- export { operationsGenerator } from './generators/operationsGenerator.tsx'
5
+ export { operationsGenerator } from './generators/operationsGenerator.ts'
6
6
  export { staticClassClientGenerator } from './generators/staticClassClientGenerator.tsx'
7
7
  export { default, pluginClient, pluginClientName } from './plugin.ts'
8
8
  export { resolverClient } from './resolvers/resolverClient.ts'
9
+ export { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from './utils.ts'
9
10
  export type { ClientImportPath, PluginClient, ResolverClient } from './types.ts'
package/src/plugin.ts CHANGED
@@ -7,13 +7,14 @@ import { pluginZodName } from '@kubb/plugin-zod'
7
7
  import { classClientGenerator } from './generators/classClientGenerator.tsx'
8
8
  import { clientGenerator } from './generators/clientGenerator.tsx'
9
9
  import { groupedClientGenerator } from './generators/groupedClientGenerator.tsx'
10
- import { operationsGenerator } from './generators/operationsGenerator.tsx'
10
+ import { operationsGenerator } from './generators/operationsGenerator.ts'
11
11
  import { staticClassClientGenerator } from './generators/staticClassClientGenerator.tsx'
12
12
  import { resolverClient } from './resolvers/resolverClient.ts'
13
13
  import { source as axiosClientSource } from './templates/clients/axios.source.ts'
14
14
  import { source as fetchClientSource } from './templates/clients/fetch.source.ts'
15
15
  import { source as configSource } from './templates/config.source.ts'
16
16
  import type { PluginClient } from './types.ts'
17
+ import { isParserEnabled } from './utils.ts'
17
18
 
18
19
  /**
19
20
  * Canonical plugin name for `@kubb/plugin-client`. Used for driver lookups and
@@ -48,7 +49,7 @@ export const pluginClientName = 'plugin-client' satisfies PluginClient['name']
48
49
  */
49
50
  export const pluginClient = definePlugin<PluginClient>((options) => {
50
51
  const {
51
- output = { path: 'clients', barrelType: 'named' },
52
+ output = { path: 'clients', barrel: { type: 'named' } },
52
53
  group,
53
54
  exclude = [],
54
55
  include,
@@ -80,12 +81,12 @@ export const pluginClient = definePlugin<PluginClient>((options) => {
80
81
  operations ? operationsGenerator : null,
81
82
  ].filter((x): x is NonNullable<typeof x> => Boolean(x))
82
83
 
83
- const groupConfig = createGroupConfig(group, { suffix: 'Controller' })
84
+ const groupConfig = createGroupConfig(group)
84
85
 
85
86
  return {
86
87
  name: pluginClientName,
87
88
  options,
88
- dependencies: [pluginTsName, parser === 'zod' ? pluginZodName : null].filter((dependency): dependency is string => Boolean(dependency)),
89
+ dependencies: [pluginTsName, isParserEnabled(parser) ? pluginZodName : null].filter((dependency): dependency is string => Boolean(dependency)),
89
90
  hooks: {
90
91
  'kubb:plugin:setup'(ctx) {
91
92
  const resolver = userResolver ? { ...resolverClient, ...userResolver } : resolverClient
@@ -1,4 +1,4 @@
1
- import { camelCase, ensureValidVarName, pascalCase } from '@internals/utils'
1
+ import { camelCase, ensureValidVarName, pascalCase, toFilePath } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginClient } from '../types.ts'
4
4
 
@@ -13,6 +13,7 @@ import type { PluginClient } from '../types.ts'
13
13
  *
14
14
  * resolverClient.default('list pets', 'function') // 'listPets'
15
15
  * resolverClient.resolveClassName('pet') // 'Pet'
16
+ * resolverClient.resolveGroupName('pet') // 'PetClient'
16
17
  * resolverClient.resolveUrlName(operationNode) // 'getShowPetByIdUrl'
17
18
  * ```
18
19
  */
@@ -20,8 +21,8 @@ export const resolverClient = defineResolver<PluginClient>(() => ({
20
21
  name: 'default',
21
22
  pluginName: 'plugin-client',
22
23
  default(name, type) {
23
- const resolved = camelCase(name, { isFile: type === 'file' })
24
- return type === 'file' ? resolved : ensureValidVarName(resolved)
24
+ if (type === 'file') return toFilePath(name)
25
+ return ensureValidVarName(camelCase(name))
25
26
  },
26
27
  resolveName(name) {
27
28
  return this.default(name, 'function')
@@ -33,7 +34,7 @@ export const resolverClient = defineResolver<PluginClient>(() => ({
33
34
  return ensureValidVarName(pascalCase(name))
34
35
  },
35
36
  resolveGroupName(name) {
36
- return ensureValidVarName(pascalCase(name))
37
+ return ensureValidVarName(pascalCase(`${name} Client`))
37
38
  },
38
39
  resolveClientPropertyName(name) {
39
40
  return ensureValidVarName(camelCase(name))
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, OutputOptions, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
2
2
 
3
3
  /**
4
4
  * The concrete resolver type for `@kubb/plugin-client`.
@@ -21,7 +21,12 @@ export type ResolverClient = Resolver & {
21
21
  */
22
22
  resolveClassName(this: ResolverClient, name: string): string
23
23
  /**
24
- * Resolves the generated class name for tag-based client groups.
24
+ * Resolves the generated class name for tag-based client groups. The default
25
+ * appends a `Client` suffix (tag `pet` becomes `PetClient`) so the class never
26
+ * collides with the schema model of the same name in the barrel.
27
+ *
28
+ * @example Resolving tag-group class names
29
+ * `resolver.resolveGroupName('pet') // -> 'PetClient'`
25
30
  */
26
31
  resolveGroupName(this: ResolverClient, name: string): string
27
32
  /**
@@ -112,17 +117,13 @@ type ParamsTypeOptions =
112
117
  pathParamsType?: 'object' | 'inline'
113
118
  }
114
119
 
115
- export type Options = {
116
- /**
117
- * Where the generated client files are written and how they are exported.
118
- *
119
- * @default { path: 'clients', barrel: { type: 'named' } }
120
- */
121
- output?: Output
122
- /**
123
- * Split generated files into subfolders based on the operation's tag.
124
- */
125
- group?: Group
120
+ /**
121
+ * Where the generated client files are written and how they are exported, plus the optional
122
+ * `group` strategy. The `group` option organizes `output.mode: 'directory'` output into per-tag or per-path subdirectories.
123
+ *
124
+ * @default { path: 'clients', barrel: { type: 'named' } }
125
+ */
126
+ export type Options = OutputOptions & {
126
127
  /**
127
128
  * Skip operations matching at least one entry in the list.
128
129
  */
@@ -157,7 +158,9 @@ export type Options = {
157
158
  /**
158
159
  * Shape of the value returned by each generated client function.
159
160
  * - `'data'` — only the response body.
160
- * - `'full'` — the full response config (body, status, headers, request).
161
+ * - `'full'` — the full response as a discriminated union keyed by HTTP status code.
162
+ * Each member is `{ status: N; data: StatusNType; statusText: string }`,
163
+ * so narrowing on `res.status` also narrows `res.data` to the matching response type.
161
164
  *
162
165
  * @default 'data'
163
166
  */
@@ -170,14 +173,19 @@ export type Options = {
170
173
  */
171
174
  paramsCasing?: 'camelcase'
172
175
  /**
173
- * Validator applied to response bodies before they are returned to the caller.
174
- * - `false` (default) no validation. The client has no runtime parser; the response is
175
- * returned as-is, cast to the generated TypeScript type.
176
- * - `'zod'` pipes responses through schemas from `@kubb/plugin-zod`.
176
+ * Validator applied to request and response bodies using schemas from `@kubb/plugin-zod`.
177
+ * - `false` (default): no validation. The response is returned as-is.
178
+ * - `'zod'`: validates response bodies only (backward-compatible shorthand).
179
+ * - `{ request?: 'zod'; response?: 'zod' }`: opt in per direction. `request` validates the
180
+ * request body and query parameters before the call. `response` validates the response body.
177
181
  *
178
182
  * @default false
183
+ * @example Response only (shorthand)
184
+ * `parser: 'zod'`
185
+ * @example Both directions
186
+ * `parser: { request: 'zod', response: 'zod' }`
179
187
  */
180
- parser?: false | 'zod'
188
+ parser?: false | 'zod' | { request?: 'zod'; response?: 'zod' }
181
189
  /**
182
190
  * Shape of the generated client.
183
191
  * - `'function'` — one standalone async function per operation.
@@ -206,8 +214,8 @@ export type Options = {
206
214
  * sdk: { className: 'PetStoreSDK' },
207
215
  * })
208
216
  * // class PetStoreSDK {
209
- * // readonly petController: petController
210
- * // readonly storeController: storeController
217
+ * // readonly petClient: PetClient
218
+ * // readonly storeClient: StoreClient
211
219
  * // constructor(config = {}) { ... }
212
220
  * // }
213
221
  * ```
package/src/utils.ts CHANGED
@@ -1,11 +1,53 @@
1
- import { getOperationParameters, getResponseType, resolveSuccessNames } from '@internals/shared'
2
- import type { URLPath } from '@internals/utils'
1
+ import { buildStatusUnionType, getOperationParameters, getResponseType, resolveSuccessNames } from '@internals/shared'
3
2
  import { ast } from '@kubb/core'
4
3
  import type { ResolverTs } from '@kubb/plugin-ts'
5
4
  import type { ResolverZod } from '@kubb/plugin-zod'
6
5
  import { createFunctionParams } from './functionParams.ts'
7
6
  import type { PluginClient } from './types.ts'
8
7
 
8
+ type ParserOption = PluginClient['resolvedOptions']['parser']
9
+
10
+ /**
11
+ * Returns `true` when any direction of the parser uses Zod (used for dependency checks).
12
+ */
13
+ export function isParserEnabled(parser: ParserOption | undefined | false): boolean {
14
+ if (!parser) return false
15
+ if (parser === 'zod') return true
16
+ return !!(parser.request || parser.response)
17
+ }
18
+
19
+ /**
20
+ * Returns `'zod'` when request body parsing is enabled, `null` otherwise.
21
+ * The string shorthand `'zod'` also enables request body parsing (existing behavior).
22
+ */
23
+ export function resolveRequestParser(parser: ParserOption | undefined | false): 'zod' | null {
24
+ if (!parser) return null
25
+ if (parser === 'zod') return 'zod'
26
+ return parser.request ?? null
27
+ }
28
+
29
+ /**
30
+ * Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise.
31
+ * Only the object form `{ request: 'zod' }` enables query-params parsing.
32
+ * The string shorthand `'zod'` does not, preserving its existing behavior.
33
+ */
34
+ export function resolveQueryParamsParser(parser: ParserOption | undefined | false): 'zod' | null {
35
+ if (!parser || parser === 'zod') return null
36
+ return parser.request ?? null
37
+ }
38
+
39
+ /**
40
+ * Returns `'zod'` when response-direction parsing is enabled, `null` otherwise.
41
+ * `parser: 'zod'` (string shorthand) maps to response parsing and returns `'zod'`.
42
+ */
43
+ export function resolveResponseParser(parser: ParserOption | undefined | false): 'zod' | null {
44
+ if (!parser) return null
45
+ if (parser === 'zod') return 'zod'
46
+ return parser.response ?? null
47
+ }
48
+
49
+ export { buildStatusUnionType }
50
+
9
51
  /**
10
52
  * Builds HTTP headers array for a client request.
11
53
  * Includes Content-Type (if not default) and spreads header parameters if present.
@@ -18,16 +60,42 @@ export function buildHeaders(contentType: string, hasHeaderParams: boolean): Arr
18
60
  }
19
61
 
20
62
  /**
21
- * Builds TypeScript generic parameters for a client method.
22
- * Includes response type, error type, and optional request type.
63
+ * Returns the generic type arguments — response, error, and request body — for a generated
64
+ * client call.
65
+ *
66
+ * When `dataReturnType` is `'full'`, the response generic widens to a union of all documented
67
+ * status types. When `parser` is `'zod'` and a request body schema exists, the request type
68
+ * uses `z.output<typeof schema>` to reflect post-transform types (e.g. date coercion).
23
69
  */
24
- export function buildGenerics(node: ast.OperationNode, tsResolver: ResolverTs): Array<string> {
70
+ export function buildGenerics(
71
+ node: ast.OperationNode,
72
+ tsResolver: ResolverTs,
73
+ options: {
74
+ dataReturnType?: PluginClient['resolvedOptions']['dataReturnType']
75
+ zodResolver?: ResolverZod | null
76
+ parser?: PluginClient['resolvedOptions']['parser']
77
+ } = {},
78
+ ): Array<string> {
79
+ const allStatusNames = node.responses.map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
25
80
  const successNames = resolveSuccessNames(node, tsResolver)
26
- const responseName = successNames.length > 0 ? successNames.join(' | ') : tsResolver.resolveResponseName(node)
81
+ const responseName =
82
+ options.dataReturnType === 'full'
83
+ ? allStatusNames.length > 0
84
+ ? allStatusNames.join(' | ')
85
+ : tsResolver.resolveResponseName(node)
86
+ : successNames.length > 0
87
+ ? successNames.join(' | ')
88
+ : tsResolver.resolveResponseName(node)
27
89
  const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
28
90
  const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
29
91
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
30
- return [responseName, TError, requestName || 'unknown'].filter(Boolean)
92
+
93
+ const zodRequestName =
94
+ options.parser === 'zod' && options.zodResolver && node.requestBody?.content?.[0]?.schema ? (options.zodResolver.resolveDataName?.(node) ?? null) : null
95
+
96
+ const requestGenericType = zodRequestName ? `z.output<typeof ${zodRequestName}>` : requestName || 'unknown'
97
+
98
+ return [responseName, TError, requestGenericType].filter(Boolean)
31
99
  }
32
100
 
33
101
  /**
@@ -36,22 +104,24 @@ export function buildGenerics(node: ast.OperationNode, tsResolver: ResolverTs):
36
104
  */
37
105
  export function buildClassClientParams({
38
106
  node,
39
- path,
107
+ url,
40
108
  baseURL,
41
109
  tsResolver,
42
110
  isFormData,
43
111
  isMultipleContentTypes,
44
112
  hasFormData,
45
113
  headers,
114
+ zodQueryParamsName,
46
115
  }: {
47
116
  node: ast.OperationNode
48
- path: URLPath
117
+ url: string
49
118
  baseURL: string | null | undefined
50
119
  tsResolver: ResolverTs
51
120
  isFormData: boolean
52
121
  isMultipleContentTypes: boolean
53
122
  hasFormData: boolean
54
123
  headers: Array<string>
124
+ zodQueryParamsName?: string | null
55
125
  }) {
56
126
  const { query: queryParams } = getOperationParameters(node)
57
127
  const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]!) : null
@@ -69,14 +139,14 @@ export function buildClassClientParams({
69
139
  value: JSON.stringify(ast.isHttpOperationNode(node) ? node.method.toUpperCase() : ''),
70
140
  },
71
141
  url: {
72
- value: path.template,
142
+ value: url,
73
143
  },
74
144
  baseURL: baseURL
75
145
  ? {
76
146
  value: JSON.stringify(baseURL),
77
147
  }
78
148
  : null,
79
- params: queryParamsName ? {} : null,
149
+ params: queryParamsName ? (zodQueryParamsName ? { value: 'requestParams' } : {}) : null,
80
150
  data: requestName
81
151
  ? {
82
152
  value:
@@ -101,7 +171,7 @@ export function buildClassClientParams({
101
171
 
102
172
  /**
103
173
  * Builds the request data parsing line for client methods.
104
- * Applies Zod validation if configured, otherwise uses data directly.
174
+ * Applies Zod validation when `parser.request === 'zod'`, otherwise assigns data directly.
105
175
  */
106
176
  export function buildRequestDataLine({
107
177
  parser,
@@ -112,8 +182,9 @@ export function buildRequestDataLine({
112
182
  node: ast.OperationNode
113
183
  zodResolver?: ResolverZod | null
114
184
  }): string {
115
- const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
116
- if (parser === 'zod' && zodRequestName) {
185
+ const requestParser = resolveRequestParser(parser)
186
+ const zodRequestName = zodResolver && requestParser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
187
+ if (requestParser === 'zod' && zodRequestName) {
117
188
  return `const requestData = ${zodRequestName}.parse(data)`
118
189
  }
119
190
  if (node.requestBody?.content?.[0]?.schema) {
@@ -122,6 +193,28 @@ export function buildRequestDataLine({
122
193
  return ''
123
194
  }
124
195
 
196
+ /**
197
+ * Builds the query parameters parsing line for client methods.
198
+ * Returns an empty string when no query params exist or query-params parsing is not enabled.
199
+ * Only the object form `parser: { request: 'zod' }` triggers this. `parser: 'zod'` does not.
200
+ */
201
+ export function buildQueryParamsLine({
202
+ parser,
203
+ node,
204
+ zodResolver,
205
+ }: {
206
+ parser: PluginClient['resolvedOptions']['parser'] | undefined
207
+ node: ast.OperationNode
208
+ zodResolver?: ResolverZod | null
209
+ }): string {
210
+ if (resolveQueryParamsParser(parser) !== 'zod' || !zodResolver) return ''
211
+ const { query: queryParams } = getOperationParameters(node)
212
+ if (queryParams.length === 0) return ''
213
+ const zodQueryParamsName = zodResolver.resolveQueryParamsName?.(node, queryParams[0]!)
214
+ if (!zodQueryParamsName) return ''
215
+ return `const requestParams = ${zodQueryParamsName}.parse(params)`
216
+ }
217
+
125
218
  /**
126
219
  * Builds the form data conversion line for file upload requests.
127
220
  * Returns empty string if not applicable.
@@ -132,28 +225,35 @@ export function buildFormDataLine(isFormData: boolean, hasRequest: boolean): str
132
225
 
133
226
  /**
134
227
  * Builds the return statement for a client method.
135
- * Applies Zod validation to response data if configured, otherwise returns raw response.
228
+ * When `dataReturnType` is `'full'`, casts the response to the status-discriminated union type.
229
+ * When `parser.response` is `'zod'`, pipes the response body through the Zod schema before returning.
136
230
  */
137
231
  export function buildReturnStatement({
138
232
  dataReturnType,
139
233
  parser,
140
234
  node,
141
235
  zodResolver,
236
+ tsResolver,
142
237
  }: {
143
238
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
144
239
  parser: PluginClient['resolvedOptions']['parser'] | undefined
145
240
  node: ast.OperationNode
146
241
  zodResolver?: ResolverZod | null
242
+ tsResolver?: ResolverTs | null
147
243
  }): string {
148
- const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
149
- if (dataReturnType === 'full' && parser === 'zod' && zodResponseName) {
150
- return `return {...res, data: ${zodResponseName}.parse(res.data)}`
244
+ const responseParser = resolveResponseParser(parser)
245
+ const zodResponseName = zodResolver && responseParser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
246
+
247
+ if (dataReturnType === 'full' && tsResolver) {
248
+ const unionType = buildStatusUnionType(node, tsResolver)
249
+ if (responseParser === 'zod' && zodResponseName) {
250
+ return `return {...res, data: ${zodResponseName}.parse(res.data)} as ${unionType}`
251
+ }
252
+ return `return res as ${unionType}`
151
253
  }
152
- if (dataReturnType === 'data' && parser === 'zod' && zodResponseName) {
254
+
255
+ if (dataReturnType === 'data' && responseParser === 'zod' && zodResponseName) {
153
256
  return `return ${zodResponseName}.parse(res.data)`
154
257
  }
155
- if (dataReturnType === 'full' && parser !== 'zod') {
156
- return 'return res'
157
- }
158
258
  return 'return res.data'
159
259
  }