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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +39 -24
  2. package/dist/clients/axios.cjs +26 -4
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +11 -5
  5. package/dist/clients/axios.js +26 -4
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +77 -9
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +10 -3
  10. package/dist/clients/fetch.js +77 -9
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +836 -514
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +206 -105
  15. package/dist/index.js +833 -515
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.cjs.map +1 -1
  19. package/dist/templates/clients/axios.source.d.ts +1 -1
  20. package/dist/templates/clients/axios.source.js +1 -1
  21. package/dist/templates/clients/axios.source.js.map +1 -1
  22. package/dist/templates/clients/fetch.source.cjs +1 -1
  23. package/dist/templates/clients/fetch.source.cjs.map +1 -1
  24. package/dist/templates/clients/fetch.source.d.ts +1 -1
  25. package/dist/templates/clients/fetch.source.js +1 -1
  26. package/dist/templates/clients/fetch.source.js.map +1 -1
  27. package/dist/templates/config.source.cjs.map +1 -1
  28. package/dist/templates/config.source.d.ts +1 -1
  29. package/dist/templates/config.source.js.map +1 -1
  30. package/package.json +14 -26
  31. package/src/clients/axios.ts +41 -7
  32. package/src/clients/fetch.ts +106 -6
  33. package/src/components/ClassClient.tsx +47 -24
  34. package/src/components/Client.tsx +100 -71
  35. package/src/components/StaticClassClient.tsx +47 -24
  36. package/src/components/Url.tsx +10 -12
  37. package/src/components/WrapperClient.tsx +9 -5
  38. package/src/functionParams.ts +8 -8
  39. package/src/generators/classClientGenerator.tsx +63 -51
  40. package/src/generators/clientGenerator.tsx +45 -48
  41. package/src/generators/groupedClientGenerator.tsx +12 -6
  42. package/src/generators/operationsGenerator.ts +47 -0
  43. package/src/generators/staticClassClientGenerator.tsx +57 -45
  44. package/src/index.ts +2 -1
  45. package/src/plugin.ts +30 -28
  46. package/src/resolvers/resolverClient.ts +32 -8
  47. package/src/types.ts +111 -65
  48. package/src/utils.ts +142 -63
  49. package/extension.yaml +0 -776
  50. package/src/components/Operations.tsx +0 -28
  51. package/src/generators/operationsGenerator.tsx +0 -28
  52. package/templates/clients/axios.ts +0 -73
  53. package/templates/clients/fetch.ts +0 -96
  54. package/templates/config.ts +0 -43
  55. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  56. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
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`.
@@ -7,10 +7,39 @@ import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginF
7
7
  export type ResolverClient = Resolver & {
8
8
  /**
9
9
  * Resolves the function name for a given raw operation name.
10
+ *
10
11
  * @example Resolving operation names
11
12
  * `resolver.resolveName('show pet by id') // -> 'showPetById'`
12
13
  */
13
14
  resolveName(this: ResolverClient, name: string): string
15
+ /**
16
+ * Resolves the output file name for a client module.
17
+ */
18
+ resolvePathName(this: ResolverClient, name: string, type?: 'file' | 'function' | 'type' | 'const'): string
19
+ /**
20
+ * Resolves the generated class name for class-based clients.
21
+ */
22
+ resolveClassName(this: ResolverClient, name: string): string
23
+ /**
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'`
30
+ */
31
+ resolveGroupName(this: ResolverClient, name: string): string
32
+ /**
33
+ * Resolves the generated SDK facade property name for a client class.
34
+ */
35
+ resolveClientPropertyName(this: ResolverClient, name: string): string
36
+ /**
37
+ * Resolves the URL helper function name for an operation.
38
+ *
39
+ * @example Resolving URL helper names
40
+ * `resolver.resolveUrlName(node) // -> 'getShowPetByIdUrl'`
41
+ */
42
+ resolveUrlName(this: ResolverClient, node: ast.OperationNode): string
14
43
  }
15
44
 
16
45
  /**
@@ -22,9 +51,10 @@ export type ResolverClient = Resolver & {
22
51
  export type ClientImportPath =
23
52
  | {
24
53
  /**
25
- * Which client should be used to do the HTTP calls.
26
- * - 'axios' uses axios client for HTTP requests.
27
- * - 'fetch' uses native fetch API for HTTP requests.
54
+ * HTTP client used by the generated code.
55
+ * - `'axios'` imports from `@kubb/plugin-client/clients/axios`. Requires `axios` at runtime.
56
+ * - `'fetch'` imports from `@kubb/plugin-client/clients/fetch`. Uses the global `fetch`.
57
+ *
28
58
  * @default 'axios'
29
59
  */
30
60
  client?: 'axios' | 'fetch'
@@ -33,9 +63,12 @@ export type ClientImportPath =
33
63
  | {
34
64
  client?: never
35
65
  /**
36
- * Client import path for API calls.
37
- * Used as `import client from '${importPath}'`.
38
- * Accepts relative and absolute paths; path changes are not performed.
66
+ * Path to a custom client module. Generated files import their HTTP runtime from here
67
+ * instead of `@kubb/plugin-client/clients/{client}`. Accepts both relative paths and
68
+ * bare module specifiers; the value is used as-is.
69
+ *
70
+ * @note When combined with a query plugin, the module must export `Client`,
71
+ * `RequestConfig`, and `ResponseErrorConfig` types.
39
72
  */
40
73
  importPath: string
41
74
  /**
@@ -57,142 +90,155 @@ export type ClientImportPath =
57
90
  type ParamsTypeOptions =
58
91
  | {
59
92
  /**
60
- * All parameters path, query, headers, and body are merged into a single
93
+ * Every operation parameter (path, query, headers, body) is wrapped in a single
61
94
  * destructured object argument.
62
- * - 'object' returns the params and pathParams as an object.
63
- * @default 'inline'
64
95
  */
65
96
  paramsType: 'object'
66
97
  /**
67
98
  * `pathParamsType` has no effect when `paramsType` is `'object'`.
68
- * Path params are already inside the single destructured object.
99
+ * Path params already live inside the single destructured object.
69
100
  */
70
101
  pathParamsType?: never
71
102
  }
72
103
  | {
73
104
  /**
74
- * Each parameter group is emitted as a separate function argument.
75
- * - 'inline' returns the params as comma separated params.
105
+ * Each parameter group is emitted as a separate positional function argument.
106
+ *
76
107
  * @default 'inline'
77
108
  */
78
109
  paramsType?: 'inline'
79
110
  /**
80
- * Controls how path parameters are arranged within the inline argument list.
81
- * - 'object' groups path params into a destructured object: `{ petId }: PathParams`.
82
- * - 'inline' emits each path param as its own argument: `petId: string`.
111
+ * How URL path parameters are arranged inside the inline argument list.
112
+ * - `'object'` groups them into one destructured object: `{ petId }: PathParams`.
113
+ * - `'inline'` emits each path param as its own argument: `petId: string`.
114
+ *
83
115
  * @default 'inline'
84
116
  */
85
117
  pathParamsType?: 'object' | 'inline'
86
118
  }
87
119
 
88
- export type Options = {
89
- /**
90
- * Specify the export location for the files and define the behavior of the output.
91
- * @default { path: 'clients', barrelType: 'named' }
92
- */
93
- output?: Output
94
- /**
95
- * Group the clients based on the provided name.
96
- */
97
- 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 & {
98
127
  /**
99
- * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
128
+ * Skip operations matching at least one entry in the list.
100
129
  */
101
130
  exclude?: Array<Exclude>
102
131
  /**
103
- * Array containing include parameters to include tags/operations/methods/paths.
132
+ * Restrict generation to operations matching at least one entry in the list.
104
133
  */
105
134
  include?: Array<Include>
106
135
  /**
107
- * Array containing override parameters to override `options` based on tags/operations/methods/paths.
136
+ * Apply a different options object to operations matching a pattern.
108
137
  */
109
138
  override?: Array<Override<ResolvedOptions>>
110
139
  /**
111
- * Create `operations.ts` file with all operations grouped by methods.
140
+ * Emit an `operations.ts` file that re-exports every generated function grouped by HTTP method.
141
+ *
112
142
  * @default false
113
143
  */
114
144
  operations?: boolean
115
145
  /**
116
- * Export urls that are used by operation x.
117
- * - 'export' makes them part of your barrel file.
118
- * - false does not make them exportable.
146
+ * Whether to also export the URL builder helpers (`get<Operation>Url`).
147
+ * - `'export'` exposes them via the barrel.
148
+ * - `false` keeps them private.
149
+ *
119
150
  * @default false
120
- * @example getGetPetByIdUrl
121
151
  */
122
152
  urlType?: 'export' | false
123
153
  /**
124
- * Allows you to set a custom base url for all generated calls.
154
+ * Base URL prepended to every request. When omitted, falls back to the adapter's
155
+ * server URL (typically `servers[0].url`).
125
156
  */
126
157
  baseURL?: string
127
158
  /**
128
- * ReturnType that is used when calling the client.
129
- * - 'data' returns ResponseConfig[data].
130
- * - 'full' returns ResponseConfig.
159
+ * Shape of the value returned by each generated client function.
160
+ * - `'data'` only the response body.
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.
164
+ *
131
165
  * @default 'data'
132
166
  */
133
167
  dataReturnType?: 'data' | 'full'
134
168
  /**
135
- * How to style your params, by default no casing is applied.
136
- * - 'camelcase' uses camelCase for pathParams, queryParams and headerParams names
137
- * @note response types (data/body) are not affected by this option
169
+ * Rename parameter properties in the generated client (path, query, headers).
170
+ * The HTTP request still uses the original spec names; Kubb writes the mapping for you.
171
+ *
172
+ * @note Use the same value on `@kubb/plugin-ts` so types stay compatible.
138
173
  */
139
174
  paramsCasing?: 'camelcase'
140
175
  /**
141
- * Which parser can be used before returning the data.
142
- * - 'client' returns the data as-is from the client.
143
- * - 'zod' uses @kubb/plugin-zod to parse the data.
144
- * @default 'client'
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.
181
+ *
182
+ * @default false
183
+ * @example Response only (shorthand)
184
+ * `parser: 'zod'`
185
+ * @example Both directions
186
+ * `parser: { request: 'zod', response: 'zod' }`
145
187
  */
146
- parser?: 'client' | 'zod'
188
+ parser?: false | 'zod' | { request?: 'zod'; response?: 'zod' }
147
189
  /**
148
- * How to generate the client code.
149
- * - 'function' generates standalone functions for each operation.
150
- * - 'class' generates a class with methods for each operation.
151
- * - 'staticClass' generates a class with static methods for each operation.
190
+ * Shape of the generated client.
191
+ * - `'function'` one standalone async function per operation.
192
+ * - `'class'` one class per tag with instance methods.
193
+ * - `'staticClass'` one class per tag with static methods.
194
+ *
152
195
  * @default 'function'
196
+ * @note Only `'function'` is compatible with query plugins.
153
197
  */
154
198
  clientType?: 'function' | 'class' | 'staticClass'
155
199
  /**
156
- * Bundle the selected client into the generated `.kubb` directory.
157
- * When disabled the generated clients will import the shared runtime from `@kubb/plugin-client/clients/*`.
200
+ * Copy the HTTP client runtime into the generated output so consumers do not need
201
+ * `@kubb/plugin-client` at runtime. When `false`, generated files import from
202
+ * `@kubb/plugin-client/clients/{client}`.
203
+ *
158
204
  * @default false
159
- * In version 5 of Kubb this is by default true
160
205
  */
161
206
  bundle?: boolean
162
207
  /**
163
- * Generate an SDK facade class that composes all tag-based client classes into a single entry point.
164
- * Setting this option automatically enables `clientType: 'class'`.
208
+ * Generate a single SDK class composing every tag-based client into one entry point.
209
+ * Automatically enables `clientType: 'class'`.
210
+ *
165
211
  * @example
166
212
  * ```ts
167
213
  * pluginClient({
168
214
  * sdk: { className: 'PetStoreSDK' },
169
215
  * })
170
- * // Generates a class with a shared constructor config and one property per tag:
171
216
  * // class PetStoreSDK {
172
- * // readonly petController: petController
173
- * // readonly storeController: storeController
217
+ * // readonly petClient: PetClient
218
+ * // readonly storeClient: StoreClient
174
219
  * // constructor(config = {}) { ... }
175
220
  * // }
176
221
  * ```
177
222
  */
178
223
  sdk?: {
179
224
  /**
180
- * Name of the generated SDK facade class.
225
+ * Name of the generated SDK facade class. Also the file name.
181
226
  */
182
227
  className: string
183
228
  }
184
229
  /**
185
- * Override individual resolver methods. Any method you omit falls back to the
186
- * preset resolver's implementation. Use `this.default(...)` to call it.
230
+ * Override how names and file paths are built for the generated client.
231
+ * Methods you omit fall back to the default resolver. `this` is bound to the
232
+ * full resolver, so `this.default(name)` delegates to the built-in implementation.
187
233
  */
188
234
  resolver?: Partial<ResolverClient> & ThisType<ResolverClient>
189
235
  /**
190
- * Single AST visitor applied to each node before printing.
191
- * Return `null` or `undefined` from a method to leave the node unchanged.
236
+ * AST visitor applied to each operation node before code is printed.
237
+ * Return `null` or `undefined` to leave the node unchanged.
192
238
  */
193
239
  transformer?: ast.Visitor
194
240
  /**
195
- * Define some generators next to the client generators.
241
+ * Custom generators that run alongside the built-in client generators.
196
242
  */
197
243
  generators?: Array<Generator<PluginClient>>
198
244
  } & ClientImportPath &
@@ -203,7 +249,7 @@ type ResolvedOptions = {
203
249
  exclude: Array<Exclude>
204
250
  include: Array<Include> | undefined
205
251
  override: Array<Override<ResolvedOptions>>
206
- group: Group | undefined
252
+ group: Group | null
207
253
  client: Options['client']
208
254
  clientType: NonNullable<Options['clientType']>
209
255
  bundle: NonNullable<Options['bundle']>
package/src/utils.ts CHANGED
@@ -1,64 +1,101 @@
1
- import { URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
1
+ import { buildStatusUnionType, getOperationParameters, getResponseType, resolveSuccessNames } from '@internals/shared'
2
+ import { ast } from '@kubb/core'
3
3
  import type { ResolverTs } from '@kubb/plugin-ts'
4
4
  import type { ResolverZod } from '@kubb/plugin-zod'
5
5
  import { createFunctionParams } from './functionParams.ts'
6
6
  import type { PluginClient } from './types.ts'
7
7
 
8
+ type ParserOption = PluginClient['resolvedOptions']['parser']
9
+
8
10
  /**
9
- * Extracts documentation comments from an operation node.
10
- * Includes description, summary, link, and deprecation information.
11
+ * Returns `true` when any direction of the parser uses Zod (used for dependency checks).
11
12
  */
12
- export function getComments(node: ast.OperationNode): Array<string> {
13
- return [
14
- node.description && `@description ${node.description}`,
15
- node.summary && `@summary ${node.summary}`,
16
- node.path && `{@link ${new URLPath(node.path).URL}}`,
17
- node.deprecated && '@deprecated',
18
- ]
19
- .filter((x): x is string => Boolean(x))
20
- .flatMap((text) => text.split(/\r?\n/).map((line) => line.trim()))
21
- .filter((x): x is string => Boolean(x))
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)
22
17
  }
23
18
 
24
19
  /**
25
- * Builds a mapping of original parameter names to their transformed (cased) names.
26
- * Returns undefined if no names have changed.
20
+ * Returns `'zod'` when request body parsing is enabled, `null` otherwise.
21
+ * The string shorthand `'zod'` also enables request body parsing (existing behavior).
27
22
  */
28
- export function buildParamsMapping(originalParams: Array<ast.ParameterNode>, casedParams: Array<ast.ParameterNode>): Record<string, string> | undefined {
29
- const mapping: Record<string, string> = {}
30
- let hasChanged = false
31
- originalParams.forEach((param, i) => {
32
- const casedName = casedParams[i]?.name ?? param.name
33
- mapping[param.name] = casedName
34
- if (param.name !== casedName) {
35
- hasChanged = true
36
- }
37
- })
38
- return hasChanged ? mapping : undefined
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
39
37
  }
40
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
+
41
51
  /**
42
52
  * Builds HTTP headers array for a client request.
43
53
  * Includes Content-Type (if not default) and spreads header parameters if present.
44
54
  */
45
55
  export function buildHeaders(contentType: string, hasHeaderParams: boolean): Array<string> {
46
56
  return [
47
- contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined,
48
- hasHeaderParams ? '...headers' : undefined,
57
+ contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
58
+ hasHeaderParams ? '...headers' : null,
49
59
  ].filter(Boolean) as Array<string>
50
60
  }
51
61
 
52
62
  /**
53
- * Builds TypeScript generic parameters for a client method.
54
- * 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).
55
69
  */
56
- export function buildGenerics(node: ast.OperationNode, tsResolver: ResolverTs): Array<string> {
57
- const responseName = tsResolver.resolveResponseName(node)
58
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
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))
80
+ const successNames = resolveSuccessNames(node, tsResolver)
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)
89
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
59
90
  const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
60
91
  const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
61
- 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)
62
99
  }
63
100
 
64
101
  /**
@@ -67,24 +104,29 @@ export function buildGenerics(node: ast.OperationNode, tsResolver: ResolverTs):
67
104
  */
68
105
  export function buildClassClientParams({
69
106
  node,
70
- path,
107
+ url,
71
108
  baseURL,
72
109
  tsResolver,
73
110
  isFormData,
111
+ isMultipleContentTypes,
112
+ hasFormData,
74
113
  headers,
114
+ zodQueryParamsName,
75
115
  }: {
76
116
  node: ast.OperationNode
77
- path: URLPath
78
- baseURL: string | undefined
117
+ url: string
118
+ baseURL: string | null | undefined
79
119
  tsResolver: ResolverTs
80
120
  isFormData: boolean
121
+ isMultipleContentTypes: boolean
122
+ hasFormData: boolean
81
123
  headers: Array<string>
124
+ zodQueryParamsName?: string | null
82
125
  }) {
83
- const queryParamsName =
84
- node.parameters.filter((p) => p.in === 'query').length > 0
85
- ? tsResolver.resolveQueryParamsName(node, node.parameters.filter((p) => p.in === 'query')[0]!)
86
- : undefined
87
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
126
+ const { query: queryParams } = getOperationParameters(node)
127
+ const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]!) : null
128
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
129
+ const responseType = getResponseType(node)
88
130
 
89
131
  return createFunctionParams({
90
132
  config: {
@@ -94,27 +136,34 @@ export function buildClassClientParams({
94
136
  mode: 'inlineSpread',
95
137
  },
96
138
  method: {
97
- value: JSON.stringify(node.method.toUpperCase()),
139
+ value: JSON.stringify(ast.isHttpOperationNode(node) ? node.method.toUpperCase() : ''),
98
140
  },
99
141
  url: {
100
- value: path.template,
142
+ value: url,
101
143
  },
102
144
  baseURL: baseURL
103
145
  ? {
104
146
  value: JSON.stringify(baseURL),
105
147
  }
106
- : undefined,
107
- params: queryParamsName ? {} : undefined,
148
+ : null,
149
+ params: queryParamsName ? (zodQueryParamsName ? { value: 'requestParams' } : {}) : null,
108
150
  data: requestName
109
151
  ? {
110
- value: isFormData ? 'formData as FormData' : 'requestData',
152
+ value:
153
+ isMultipleContentTypes && hasFormData
154
+ ? "contentType === 'multipart/form-data' ? formData as FormData : requestData"
155
+ : isFormData
156
+ ? 'formData as FormData'
157
+ : 'requestData',
111
158
  }
112
- : undefined,
159
+ : null,
160
+ contentType: isMultipleContentTypes ? {} : null,
161
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
113
162
  headers: headers.length
114
163
  ? {
115
164
  value: `{ ${headers.join(', ')}, ...requestConfig.headers }`,
116
165
  }
117
- : undefined,
166
+ : null,
118
167
  },
119
168
  },
120
169
  })
@@ -122,7 +171,7 @@ export function buildClassClientParams({
122
171
 
123
172
  /**
124
173
  * Builds the request data parsing line for client methods.
125
- * Applies Zod validation if configured, otherwise uses data directly.
174
+ * Applies Zod validation when `parser.request === 'zod'`, otherwise assigns data directly.
126
175
  */
127
176
  export function buildRequestDataLine({
128
177
  parser,
@@ -131,10 +180,11 @@ export function buildRequestDataLine({
131
180
  }: {
132
181
  parser: PluginClient['resolvedOptions']['parser'] | undefined
133
182
  node: ast.OperationNode
134
- zodResolver?: ResolverZod
183
+ zodResolver?: ResolverZod | null
135
184
  }): string {
136
- const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined
137
- 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) {
138
188
  return `const requestData = ${zodRequestName}.parse(data)`
139
189
  }
140
190
  if (node.requestBody?.content?.[0]?.schema) {
@@ -143,6 +193,28 @@ export function buildRequestDataLine({
143
193
  return ''
144
194
  }
145
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
+
146
218
  /**
147
219
  * Builds the form data conversion line for file upload requests.
148
220
  * Returns empty string if not applicable.
@@ -153,28 +225,35 @@ export function buildFormDataLine(isFormData: boolean, hasRequest: boolean): str
153
225
 
154
226
  /**
155
227
  * Builds the return statement for a client method.
156
- * 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.
157
230
  */
158
231
  export function buildReturnStatement({
159
232
  dataReturnType,
160
233
  parser,
161
234
  node,
162
235
  zodResolver,
236
+ tsResolver,
163
237
  }: {
164
238
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
165
239
  parser: PluginClient['resolvedOptions']['parser'] | undefined
166
240
  node: ast.OperationNode
167
- zodResolver?: ResolverZod
241
+ zodResolver?: ResolverZod | null
242
+ tsResolver?: ResolverTs | null
168
243
  }): string {
169
- const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : undefined
170
- if (dataReturnType === 'full' && parser === 'zod' && zodResponseName) {
171
- 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}`
172
253
  }
173
- if (dataReturnType === 'data' && parser === 'zod' && zodResponseName) {
254
+
255
+ if (dataReturnType === 'data' && responseParser === 'zod' && zodResponseName) {
174
256
  return `return ${zodResponseName}.parse(res.data)`
175
257
  }
176
- if (dataReturnType === 'full' && parser === 'client') {
177
- return 'return res'
178
- }
179
258
  return 'return res.data'
180
259
  }