@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/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { t as __name } from "./chunk--u3MIqq1.js";
1
+ import { t as __name } from "./chunk-C0LytTxp.js";
2
2
  import { source } from "./templates/clients/axios.source.js";
3
3
  import { source as source$1 } from "./templates/clients/fetch.source.js";
4
4
  import { source as source$2 } from "./templates/config.source.js";
5
5
  import path from "node:path";
6
6
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
7
+ import { buildJSDoc, stringify } from "@kubb/ast/utils";
7
8
  import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
8
9
  import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
9
10
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
@@ -19,68 +20,57 @@ import { pluginZodName } from "@kubb/plugin-zod";
19
20
  function toCamelOrPascal(text, pascal) {
20
21
  return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
21
22
  if (word.length > 1 && word === word.toUpperCase()) return word;
22
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
23
- return word.charAt(0).toUpperCase() + word.slice(1);
23
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
24
24
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
25
25
  }
26
26
  /**
27
- * Splits `text` on `.` and applies `transformPart` to each segment.
28
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
29
- * Segments are joined with `/` to form a file path.
30
- *
31
- * Only splits on dots followed by a letter so that version numbers
32
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
33
- */
34
- function applyToFileParts(text, transformPart) {
35
- const parts = text.split(/\.(?=[a-zA-Z])/);
36
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
37
- }
38
- /**
39
27
  * Converts `text` to camelCase.
40
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
41
28
  *
42
- * @example
43
- * camelCase('hello-world') // 'helloWorld'
44
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
29
+ * @example Word boundaries
30
+ * `camelCase('hello-world') // 'helloWorld'`
31
+ *
32
+ * @example With a prefix
33
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
45
34
  */
46
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
47
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
48
- prefix,
49
- suffix
50
- } : {}));
35
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
51
36
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
52
37
  }
53
38
  /**
54
39
  * Converts `text` to PascalCase.
55
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
56
40
  *
57
- * @example
58
- * pascalCase('hello-world') // 'HelloWorld'
59
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
41
+ * @example Word boundaries
42
+ * `pascalCase('hello-world') // 'HelloWorld'`
43
+ *
44
+ * @example With a suffix
45
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
60
46
  */
61
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
62
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
63
- prefix,
64
- suffix
65
- }) : camelCase(part));
47
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
66
48
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
67
49
  }
68
50
  //#endregion
69
- //#region ../../internals/utils/src/jsdoc.ts
51
+ //#region ../../internals/utils/src/fs.ts
70
52
  /**
71
- * Builds a JSDoc comment block from an array of lines.
72
- * Returns `fallback` when `comments` is empty so callers always get a usable string.
53
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
54
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
55
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
73
56
  *
74
- * @example
75
- * ```ts
76
- * buildJSDoc(['@type string', '@example hello'])
77
- * // '/**\n * @type string\n * @example hello\n *\/\n '
78
- * ```
57
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
58
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
59
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
60
+ * absolute path, letting generated files escape the configured output directory.
61
+ *
62
+ * @example Nested path from a dotted name
63
+ * `toFilePath('pet.petId') // 'pet/petId'`
64
+ *
65
+ * @example PascalCase the final segment
66
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
67
+ *
68
+ * @example Suffix applied to the final segment only
69
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
79
70
  */
80
- function buildJSDoc(comments, options = {}) {
81
- const { indent = " * ", suffix = "\n ", fallback = " " } = options;
82
- if (comments.length === 0) return fallback;
83
- return `/**\n${comments.map((c) => `${indent}${c}`).join("\n")}\n */${suffix}`;
71
+ function toFilePath(name, caseLast = camelCase) {
72
+ const parts = name.split(/\.(?=[a-zA-Z])/);
73
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
84
74
  }
85
75
  //#endregion
86
76
  //#region ../../internals/utils/src/reserved.ts
@@ -185,100 +175,104 @@ function isValidVarName(name) {
185
175
  if (!name || reservedWords.has(name)) return false;
186
176
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
187
177
  }
188
- //#endregion
189
- //#region ../../internals/utils/src/urlPath.ts
190
178
  /**
191
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
179
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
180
+ * otherwise prefixes it with `_` so the result is a valid identifier.
181
+ *
182
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
183
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
184
+ * variable, type, or function names.
192
185
  *
193
186
  * @example
194
- * const p = new URLPath('/pet/{petId}')
195
- * p.URL // '/pet/:petId'
196
- * p.template // '`/pet/${petId}`'
187
+ * ```ts
188
+ * ensureValidVarName('409') // '_409'
189
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
190
+ * ensureValidVarName('Pet') // 'Pet'
191
+ * ensureValidVarName('class') // '_class'
192
+ * ```
197
193
  */
198
- var URLPath = class {
199
- /**
200
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
201
- */
202
- path;
203
- #options;
204
- constructor(path, options = {}) {
205
- this.path = path;
206
- this.#options = options;
194
+ function ensureValidVarName(name) {
195
+ if (!name || isValidVarName(name)) return name;
196
+ return `_${name}`;
197
+ }
198
+ //#endregion
199
+ //#region ../../internals/utils/src/url.ts
200
+ function transformParam(raw, casing) {
201
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
202
+ return casing === "camelcase" ? camelCase(param) : param;
203
+ }
204
+ function toParamsObject(path, { replacer, casing } = {}) {
205
+ const params = {};
206
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
207
+ const param = transformParam(match[1], casing);
208
+ const key = replacer ? replacer(param) : param;
209
+ params[key] = key;
207
210
  }
208
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
209
- *
210
- * @example
211
- * ```ts
212
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
213
- * ```
214
- */
215
- get URL() {
216
- return this.toURLPath();
211
+ return Object.keys(params).length > 0 ? params : null;
212
+ }
213
+ /**
214
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
215
+ */
216
+ var Url$1 = class Url$1 {
217
+ static {
218
+ __name(this, "Url");
217
219
  }
218
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
220
+ /**
221
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
219
222
  *
220
223
  * @example
221
- * ```ts
222
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
223
- * new URLPath('/pet/{petId}').isURL // false
224
- * ```
224
+ * Url.canParse('https://petstore.swagger.io/v2') // true
225
+ * Url.canParse('/pet/{petId}') // false
225
226
  */
226
- get isURL() {
227
- try {
228
- return !!new URL(this.path).href;
229
- } catch {
230
- return false;
231
- }
227
+ static canParse(url, base) {
228
+ return URL.canParse(url, base);
232
229
  }
233
230
  /**
234
- * Converts the OpenAPI path to a TypeScript template literal string.
231
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
235
232
  *
236
233
  * @example
237
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
238
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
234
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
239
235
  */
240
- get template() {
241
- return this.toTemplateString();
236
+ static toPath(path) {
237
+ return path.replace(/\{([^}]+)\}/g, ":$1");
242
238
  }
243
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
239
+ /**
240
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
241
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
242
+ * and `casing` controls parameter identifier casing.
244
243
  *
245
244
  * @example
246
- * ```ts
247
- * new URLPath('/pet/{petId}').object
248
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
249
- * ```
250
- */
251
- get object() {
252
- return this.toObject();
253
- }
254
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
245
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
255
246
  *
256
247
  * @example
257
- * ```ts
258
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
259
- * new URLPath('/pet').params // undefined
260
- * ```
248
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
261
249
  */
262
- get params() {
263
- return this.getParams();
264
- }
265
- #transformParam(raw) {
266
- const param = isValidVarName(raw) ? raw : camelCase(raw);
267
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
250
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
251
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
252
+ if (i % 2 === 0) return part;
253
+ const param = transformParam(part, casing);
254
+ return `\${${replacer ? replacer(param) : param}}`;
255
+ }).join("");
256
+ return `\`${prefix ?? ""}${result}\``;
268
257
  }
269
258
  /**
270
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
259
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
260
+ * expression when `stringify` is set.
261
+ *
262
+ * @example
263
+ * Url.toObject('/pet/{petId}')
264
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
271
265
  */
272
- #eachParam(fn) {
273
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
274
- const raw = match[1];
275
- fn(raw, this.#transformParam(raw));
276
- }
277
- }
278
- toObject({ type = "path", replacer, stringify } = {}) {
266
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
279
267
  const object = {
280
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
281
- params: this.getParams()
268
+ url: type === "path" ? Url$1.toPath(path) : Url$1.toTemplateString(path, {
269
+ replacer,
270
+ casing
271
+ }),
272
+ params: toParamsObject(path, {
273
+ replacer,
274
+ casing
275
+ })
282
276
  };
283
277
  if (stringify) {
284
278
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -287,51 +281,211 @@ var URLPath = class {
287
281
  }
288
282
  return object;
289
283
  }
290
- /**
291
- * Converts the OpenAPI path to a TypeScript template literal string.
292
- * An optional `replacer` can transform each extracted parameter name before interpolation.
293
- *
294
- * @example
295
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
296
- */
297
- toTemplateString({ prefix = "", replacer } = {}) {
298
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
299
- if (i % 2 === 0) return part;
300
- const param = this.#transformParam(part);
301
- return `\${${replacer ? replacer(param) : param}}`;
302
- }).join("")}\``;
303
- }
304
- /**
305
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
306
- * An optional `replacer` transforms each parameter name in both key and value positions.
307
- * Returns `undefined` when no path parameters are found.
308
- *
309
- * @example
310
- * ```ts
311
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
312
- * // { petId: 'petId', tagId: 'tagId' }
313
- * ```
314
- */
315
- getParams(replacer) {
316
- const params = {};
317
- this.#eachParam((_raw, param) => {
318
- const key = replacer ? replacer(param) : param;
319
- params[key] = key;
320
- });
321
- return Object.keys(params).length > 0 ? params : void 0;
322
- }
323
- /** Converts the OpenAPI path to Express-style colon syntax.
324
- *
325
- * @example
326
- * ```ts
327
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
328
- * ```
329
- */
330
- toURLPath() {
331
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
332
- }
333
284
  };
334
285
  //#endregion
286
+ //#region ../../internals/shared/src/operation.ts
287
+ /**
288
+ * Builds the `ResolverFileParams` every operation generator passes to
289
+ * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
290
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
291
+ * that was repeated at dozens of call sites across the client and query plugins.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
296
+ * ```
297
+ */
298
+ function operationFileEntry(node, name, extname = ".ts") {
299
+ return {
300
+ name,
301
+ extname,
302
+ tag: node.tags[0] ?? "default",
303
+ path: node.path
304
+ };
305
+ }
306
+ function getOperationLink(node, link) {
307
+ if (!link) return null;
308
+ if (typeof link === "function") return link(node) ?? null;
309
+ if (link === "urlPath") return node.path ? `{@link ${Url$1.toPath(node.path)}}` : null;
310
+ return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
311
+ }
312
+ function getContentTypeInfo(node) {
313
+ const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
314
+ const isMultipleContentTypes = contentTypes.length > 1;
315
+ return {
316
+ contentTypes,
317
+ isMultipleContentTypes,
318
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
319
+ defaultContentType: contentTypes[0] ?? "application/json",
320
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
321
+ };
322
+ }
323
+ /**
324
+ * Derives the default `responseType` for an operation from its primary success response.
325
+ *
326
+ * Returns a value only when that response declares a single non-JSON content type — a binary type
327
+ * (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`, `video/*`) maps to `'blob'`
328
+ * and other `text/*` maps to `'text'`. Otherwise `undefined`, leaving the runtime client's
329
+ * `Content-Type` auto-detection in charge.
330
+ */
331
+ function getResponseType(node) {
332
+ const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
333
+ if (contentTypes.length !== 1) return void 0;
334
+ const baseType = contentTypes[0].split(";")[0].trim().toLowerCase();
335
+ if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
336
+ if (baseType.startsWith("text/")) return "text";
337
+ if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
338
+ }
339
+ function buildRequestConfigType(node, resolver) {
340
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
341
+ const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
342
+ return `${requestName ? `Partial<RequestConfig<${requestName}>>` : "Partial<RequestConfig>"} & { ${["client?: Client", isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null].filter(Boolean).join("; ")} }`;
343
+ }
344
+ function buildOperationComments(node, options = {}) {
345
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
346
+ const linkComment = getOperationLink(node, link);
347
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
348
+ node.description && `@description ${node.description}`,
349
+ node.summary && `@summary ${node.summary}`,
350
+ linkComment,
351
+ node.deprecated && "@deprecated"
352
+ ] : [
353
+ node.description && `@description ${node.description}`,
354
+ node.summary && `@summary ${node.summary}`,
355
+ node.deprecated && "@deprecated",
356
+ linkComment
357
+ ]).filter((comment) => Boolean(comment));
358
+ if (!splitLines) return filteredComments;
359
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
360
+ }
361
+ function getOperationParameters(node, options = {}) {
362
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
363
+ return {
364
+ path: params.filter((param) => param.in === "path"),
365
+ query: params.filter((param) => param.in === "query"),
366
+ header: params.filter((param) => param.in === "header"),
367
+ cookie: params.filter((param) => param.in === "cookie")
368
+ };
369
+ }
370
+ function getStatusCodeNumber(statusCode) {
371
+ const code = Number(statusCode);
372
+ return Number.isNaN(code) ? null : code;
373
+ }
374
+ function isSuccessStatusCode(statusCode) {
375
+ const code = getStatusCodeNumber(statusCode);
376
+ return code !== null && code >= 200 && code < 300;
377
+ }
378
+ function isErrorStatusCode(statusCode) {
379
+ const code = getStatusCodeNumber(statusCode);
380
+ return code !== null && code >= 400;
381
+ }
382
+ function getSuccessResponses(responses) {
383
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
384
+ }
385
+ function getOperationSuccessResponses(node) {
386
+ return getSuccessResponses(node.responses);
387
+ }
388
+ function getPrimarySuccessResponse(node) {
389
+ return getOperationSuccessResponses(node)[0] ?? null;
390
+ }
391
+ function resolveErrorNames(node, resolver) {
392
+ return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
393
+ }
394
+ function resolveSuccessNames(node, resolver) {
395
+ return node.responses.filter((response) => isSuccessStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
396
+ }
397
+ function resolveStatusCodeNames(node, resolver) {
398
+ return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
399
+ }
400
+ /**
401
+ * Builds the discriminated union type string for `dataReturnType: 'full'` return shapes.
402
+ * Each member is `{ status: N; data: StatusNType; statusText: string }`.
403
+ */
404
+ function buildStatusUnionType(node, resolver) {
405
+ const members = node.responses.map((r) => {
406
+ const typeName = resolver.resolveResponseStatusName(node, r.statusCode);
407
+ const statusCode = Number.parseInt(r.statusCode, 10);
408
+ return `{ status: ${Number.isNaN(statusCode) ? "number" : String(statusCode)}; data: ${typeName}; statusText: string }`;
409
+ });
410
+ if (members.length === 1) return members[0];
411
+ return `(${members.join(" | ")})`;
412
+ }
413
+ const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
414
+ function resolveOperationTypeNames(node, resolver, options = {}) {
415
+ const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
416
+ let byResolver = typeNamesByResolver.get(resolver);
417
+ if (byResolver) {
418
+ const cached = byResolver.get(cacheKey);
419
+ if (cached) return cached;
420
+ } else {
421
+ byResolver = /* @__PURE__ */ new Map();
422
+ typeNamesByResolver.set(resolver, byResolver);
423
+ }
424
+ const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
425
+ const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
426
+ const exclude = new Set(options.exclude ?? []);
427
+ const paramNames = [
428
+ ...path.map((param) => resolver.resolvePathParamsName(node, param)),
429
+ ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
430
+ ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
431
+ ];
432
+ const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
433
+ const result = (options.order === "body-response-first" ? [
434
+ ...bodyAndResponseNames,
435
+ ...paramNames,
436
+ ...responseStatusNames
437
+ ] : [
438
+ ...paramNames,
439
+ ...bodyAndResponseNames,
440
+ ...responseStatusNames
441
+ ]).filter((name) => Boolean(name) && !exclude.has(name));
442
+ byResolver.set(cacheKey, result);
443
+ return result;
444
+ }
445
+ //#endregion
446
+ //#region ../../internals/shared/src/group.ts
447
+ /**
448
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
449
+ * shared default naming so every plugin groups output consistently:
450
+ *
451
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
452
+ * - other groups use the camelCased group (`pet store` → `petStore`).
453
+ *
454
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
455
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
456
+ * per-plugin convention.
457
+ *
458
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * createGroupConfig(group) // shared across every plugin
463
+ * ```
464
+ */
465
+ function createGroupConfig(group) {
466
+ if (!group) return null;
467
+ const defaultName = (ctx) => {
468
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
469
+ return camelCase(ctx.group);
470
+ };
471
+ return {
472
+ ...group,
473
+ name: group.name ? group.name : defaultName
474
+ };
475
+ }
476
+ //#endregion
477
+ //#region ../../internals/shared/src/params.ts
478
+ function buildParamsMapping(originalParams, mappedParams) {
479
+ const mapping = {};
480
+ let hasChanged = false;
481
+ originalParams.forEach((param, i) => {
482
+ const mappedName = mappedParams[i]?.name ?? param.name;
483
+ mapping[param.name] = mappedName;
484
+ if (param.name !== mappedName) hasChanged = true;
485
+ });
486
+ return hasChanged ? mapping : null;
487
+ }
488
+ //#endregion
335
489
  //#region src/functionParams.ts
336
490
  const declarationPrinter$4 = functionPrinter({ mode: "declaration" });
337
491
  const callPrinter = functionPrinter({ mode: "call" });
@@ -342,18 +496,18 @@ function createType(type) {
342
496
  return type ? ast.createParamsType({
343
497
  variant: "reference",
344
498
  name: type
345
- }) : void 0;
499
+ }) : null;
346
500
  }
347
501
  function createDeclarationLeaf(name, spec) {
348
502
  if (spec.default !== void 0) return ast.createFunctionParameter({
349
503
  name,
350
- type: createType(spec.type),
504
+ type: createType(spec.type) ?? void 0,
351
505
  default: spec.default,
352
506
  rest: spec.mode === "inlineSpread"
353
507
  });
354
508
  return ast.createFunctionParameter({
355
509
  name,
356
- type: createType(spec.type),
510
+ type: createType(spec.type) ?? void 0,
357
511
  optional: !!spec.optional,
358
512
  rest: spec.mode === "inlineSpread"
359
513
  });
@@ -362,14 +516,14 @@ function createDeclarationParam(name, spec) {
362
516
  if (isGroup(spec)) return ast.createParameterGroup({
363
517
  inline: spec.mode === "inlineSpread",
364
518
  default: spec.default,
365
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => createDeclarationLeaf(childName, child))
519
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => createDeclarationLeaf(childName, child))
366
520
  });
367
521
  return createDeclarationLeaf(name, spec);
368
522
  }
369
523
  function createCallParam(name, spec) {
370
524
  if (isGroup(spec)) return ast.createParameterGroup({
371
525
  inline: spec.mode === "inlineSpread",
372
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => ast.createFunctionParameter({
526
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => ast.createFunctionParameter({
373
527
  name: child?.mode === "inlineSpread" ? spec.mode === "inlineSpread" ? child.value ?? childName : `...${child.value ?? childName}` : child?.value ? `${childName}: ${child.value}` : childName,
374
528
  rest: spec.mode === "inlineSpread" && child?.mode === "inlineSpread"
375
529
  }))
@@ -384,7 +538,7 @@ function createCallParam(name, spec) {
384
538
  * Returns utilities to output constructor signatures (`toConstructor()`) or call expressions (`toCall()`).
385
539
  */
386
540
  function createFunctionParams(params) {
387
- const entries = Object.entries(params).filter(([, spec]) => spec !== void 0);
541
+ const entries = Object.entries(params).filter(([, spec]) => spec != null);
388
542
  return {
389
543
  toConstructor() {
390
544
  return declarationPrinter$4.print(ast.createFunctionParameters({ params: entries.map(([name, spec]) => createDeclarationParam(name, spec)) })) ?? "";
@@ -397,83 +551,118 @@ function createFunctionParams(params) {
397
551
  //#endregion
398
552
  //#region src/utils.ts
399
553
  /**
400
- * Extracts documentation comments from an operation node.
401
- * Includes description, summary, link, and deprecation information.
554
+ * Returns `true` when any direction of the parser uses Zod (used for dependency checks).
402
555
  */
403
- function getComments(node) {
404
- return [
405
- node.description && `@description ${node.description}`,
406
- node.summary && `@summary ${node.summary}`,
407
- node.path && `{@link ${new URLPath(node.path).URL}}`,
408
- node.deprecated && "@deprecated"
409
- ].filter((x) => Boolean(x)).flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((x) => Boolean(x));
556
+ function isParserEnabled(parser) {
557
+ if (!parser) return false;
558
+ if (parser === "zod") return true;
559
+ return !!(parser.request || parser.response);
410
560
  }
411
561
  /**
412
- * Builds a mapping of original parameter names to their transformed (cased) names.
413
- * Returns undefined if no names have changed.
562
+ * Returns `'zod'` when request body parsing is enabled, `null` otherwise.
563
+ * The string shorthand `'zod'` also enables request body parsing (existing behavior).
414
564
  */
415
- function buildParamsMapping(originalParams, casedParams) {
416
- const mapping = {};
417
- let hasChanged = false;
418
- originalParams.forEach((param, i) => {
419
- const casedName = casedParams[i]?.name ?? param.name;
420
- mapping[param.name] = casedName;
421
- if (param.name !== casedName) hasChanged = true;
422
- });
423
- return hasChanged ? mapping : void 0;
565
+ function resolveRequestParser(parser) {
566
+ if (!parser) return null;
567
+ if (parser === "zod") return "zod";
568
+ return parser.request ?? null;
569
+ }
570
+ /**
571
+ * Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise.
572
+ * Only the object form `{ request: 'zod' }` enables query-params parsing.
573
+ * The string shorthand `'zod'` does not, preserving its existing behavior.
574
+ */
575
+ function resolveQueryParamsParser(parser) {
576
+ if (!parser || parser === "zod") return null;
577
+ return parser.request ?? null;
578
+ }
579
+ /**
580
+ * Returns `'zod'` when response-direction parsing is enabled, `null` otherwise.
581
+ * `parser: 'zod'` (string shorthand) maps to response parsing and returns `'zod'`.
582
+ */
583
+ function resolveResponseParser(parser) {
584
+ if (!parser) return null;
585
+ if (parser === "zod") return "zod";
586
+ return parser.response ?? null;
424
587
  }
425
588
  /**
426
589
  * Builds HTTP headers array for a client request.
427
590
  * Includes Content-Type (if not default) and spreads header parameters if present.
428
591
  */
429
592
  function buildHeaders(contentType, hasHeaderParams) {
430
- return [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0, hasHeaderParams ? "...headers" : void 0].filter(Boolean);
593
+ return [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, hasHeaderParams ? "...headers" : null].filter(Boolean);
431
594
  }
432
595
  /**
433
- * Builds TypeScript generic parameters for a client method.
434
- * Includes response type, error type, and optional request type.
596
+ * Returns the generic type arguments — response, error, and request body — for a generated
597
+ * client call.
598
+ *
599
+ * When `dataReturnType` is `'full'`, the response generic widens to a union of all documented
600
+ * status types. When `parser` is `'zod'` and a request body schema exists, the request type
601
+ * uses `z.output<typeof schema>` to reflect post-transform types (e.g. date coercion).
435
602
  */
436
- function buildGenerics(node, tsResolver) {
437
- const responseName = tsResolver.resolveResponseName(node);
438
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0;
603
+ function buildGenerics(node, tsResolver, options = {}) {
604
+ const allStatusNames = node.responses.map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
605
+ const successNames = resolveSuccessNames(node, tsResolver);
606
+ const responseName = options.dataReturnType === "full" ? allStatusNames.length > 0 ? allStatusNames.join(" | ") : tsResolver.resolveResponseName(node) : successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
607
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
439
608
  const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
609
+ const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`;
610
+ const zodRequestName = options.parser === "zod" && options.zodResolver && node.requestBody?.content?.[0]?.schema ? options.zodResolver.resolveDataName?.(node) ?? null : null;
440
611
  return [
441
612
  responseName,
442
- `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
443
- requestName || "unknown"
613
+ TError,
614
+ zodRequestName ? `z.output<typeof ${zodRequestName}>` : requestName || "unknown"
444
615
  ].filter(Boolean);
445
616
  }
446
617
  /**
447
618
  * Builds the parameters object for a class-based client method.
448
619
  * Includes URL, method, base URL, headers, and request/response data.
449
620
  */
450
- function buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers }) {
451
- const queryParamsName = node.parameters.filter((p) => p.in === "query").length > 0 ? tsResolver.resolveQueryParamsName(node, node.parameters.filter((p) => p.in === "query")[0]) : void 0;
452
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0;
621
+ function buildClassClientParams({ node, url, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers, zodQueryParamsName }) {
622
+ const { query: queryParams } = getOperationParameters(node);
623
+ const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]) : null;
624
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
625
+ const responseType = getResponseType(node);
453
626
  return createFunctionParams({ config: {
454
627
  mode: "object",
455
628
  children: {
456
629
  requestConfig: { mode: "inlineSpread" },
457
- method: { value: JSON.stringify(node.method.toUpperCase()) },
458
- url: { value: path.template },
459
- baseURL: baseURL ? { value: JSON.stringify(baseURL) } : void 0,
460
- params: queryParamsName ? {} : void 0,
461
- data: requestName ? { value: isFormData ? "formData as FormData" : "requestData" } : void 0,
462
- headers: headers.length ? { value: `{ ${headers.join(", ")}, ...requestConfig.headers }` } : void 0
630
+ method: { value: JSON.stringify(ast.isHttpOperationNode(node) ? node.method.toUpperCase() : "") },
631
+ url: { value: url },
632
+ baseURL: baseURL ? { value: JSON.stringify(baseURL) } : null,
633
+ params: queryParamsName ? zodQueryParamsName ? { value: "requestParams" } : {} : null,
634
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
635
+ contentType: isMultipleContentTypes ? {} : null,
636
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
637
+ headers: headers.length ? { value: `{ ${headers.join(", ")}, ...requestConfig.headers }` } : null
463
638
  }
464
639
  } });
465
640
  }
466
641
  /**
467
642
  * Builds the request data parsing line for client methods.
468
- * Applies Zod validation if configured, otherwise uses data directly.
643
+ * Applies Zod validation when `parser.request === 'zod'`, otherwise assigns data directly.
469
644
  */
470
645
  function buildRequestDataLine({ parser, node, zodResolver }) {
471
- const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0;
472
- if (parser === "zod" && zodRequestName) return `const requestData = ${zodRequestName}.parse(data)`;
646
+ const requestParser = resolveRequestParser(parser);
647
+ const zodRequestName = zodResolver && requestParser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
648
+ if (requestParser === "zod" && zodRequestName) return `const requestData = ${zodRequestName}.parse(data)`;
473
649
  if (node.requestBody?.content?.[0]?.schema) return "const requestData = data";
474
650
  return "";
475
651
  }
476
652
  /**
653
+ * Builds the query parameters parsing line for client methods.
654
+ * Returns an empty string when no query params exist or query-params parsing is not enabled.
655
+ * Only the object form `parser: { request: 'zod' }` triggers this. `parser: 'zod'` does not.
656
+ */
657
+ function buildQueryParamsLine({ parser, node, zodResolver }) {
658
+ if (resolveQueryParamsParser(parser) !== "zod" || !zodResolver) return "";
659
+ const { query: queryParams } = getOperationParameters(node);
660
+ if (queryParams.length === 0) return "";
661
+ const zodQueryParamsName = zodResolver.resolveQueryParamsName?.(node, queryParams[0]);
662
+ if (!zodQueryParamsName) return "";
663
+ return `const requestParams = ${zodQueryParamsName}.parse(params)`;
664
+ }
665
+ /**
477
666
  * Builds the form data conversion line for file upload requests.
478
667
  * Returns empty string if not applicable.
479
668
  */
@@ -482,19 +671,24 @@ function buildFormDataLine(isFormData, hasRequest) {
482
671
  }
483
672
  /**
484
673
  * Builds the return statement for a client method.
485
- * Applies Zod validation to response data if configured, otherwise returns raw response.
674
+ * When `dataReturnType` is `'full'`, casts the response to the status-discriminated union type.
675
+ * When `parser.response` is `'zod'`, pipes the response body through the Zod schema before returning.
486
676
  */
487
- function buildReturnStatement({ dataReturnType, parser, node, zodResolver }) {
488
- const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : void 0;
489
- if (dataReturnType === "full" && parser === "zod" && zodResponseName) return `return {...res, data: ${zodResponseName}.parse(res.data)}`;
490
- if (dataReturnType === "data" && parser === "zod" && zodResponseName) return `return ${zodResponseName}.parse(res.data)`;
491
- if (dataReturnType === "full" && parser === "client") return "return res";
677
+ function buildReturnStatement({ dataReturnType, parser, node, zodResolver, tsResolver }) {
678
+ const responseParser = resolveResponseParser(parser);
679
+ const zodResponseName = zodResolver && responseParser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
680
+ if (dataReturnType === "full" && tsResolver) {
681
+ const unionType = buildStatusUnionType(node, tsResolver);
682
+ if (responseParser === "zod" && zodResponseName) return `return {...res, data: ${zodResponseName}.parse(res.data)} as ${unionType}`;
683
+ return `return res as ${unionType}`;
684
+ }
685
+ if (dataReturnType === "data" && responseParser === "zod" && zodResponseName) return `return ${zodResponseName}.parse(res.data)`;
492
686
  return "return res.data";
493
687
  }
494
688
  //#endregion
495
689
  //#region src/components/Url.tsx
496
690
  const declarationPrinter$3 = functionPrinter({ mode: "declaration" });
497
- function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
691
+ function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
498
692
  const urlNode = {
499
693
  ...node,
500
694
  parameters: node.parameters.filter((p) => p.in === "path"),
@@ -507,10 +701,9 @@ function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolve
507
701
  resolver: tsResolver
508
702
  });
509
703
  }
510
- __name(getParams$1, "getParams");
511
704
  function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
512
- const path = new URLPath(node.path);
513
- const paramsNode = getParams$1({
705
+ if (!ast.isHttpOperationNode(node)) return null;
706
+ const paramsNode = buildUrlParamsNode({
514
707
  paramsType,
515
708
  paramsCasing,
516
709
  pathParamsType,
@@ -518,9 +711,9 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
518
711
  tsResolver
519
712
  });
520
713
  const paramsSignature = declarationPrinter$3.print(paramsNode) ?? "";
521
- const originalPathParams = node.parameters.filter((p) => p.in === "path");
522
- const casedPathParams = ast.caseParams(originalPathParams, paramsCasing);
523
- const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : void 0;
714
+ const { path: originalPathParams } = getOperationParameters(node);
715
+ const { path: casedPathParams } = getOperationParameters(node, { paramsCasing });
716
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null;
524
717
  return /* @__PURE__ */ jsx(File.Source, {
525
718
  name,
526
719
  isExportable,
@@ -534,7 +727,7 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
534
727
  pathParamsMapping && Object.keys(pathParamsMapping).length > 0 && /* @__PURE__ */ jsx("br", {}),
535
728
  /* @__PURE__ */ jsx(Const, {
536
729
  name: "res",
537
- children: `{ method: '${node.method.toUpperCase()}', url: ${path.toTemplateString({ prefix: baseURL })} as const }`
730
+ children: `{ method: '${node.method.toUpperCase()}', url: ${Url$1.toTemplateString(node.path, { prefix: baseURL })} as const }`
538
731
  }),
539
732
  /* @__PURE__ */ jsx("br", {}),
540
733
  "return res"
@@ -542,56 +735,58 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
542
735
  })
543
736
  });
544
737
  }
545
- Url.getParams = getParams$1;
546
738
  //#endregion
547
739
  //#region src/components/Client.tsx
548
740
  const declarationPrinter$2 = functionPrinter({ mode: "declaration" });
549
- function getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable }) {
550
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0;
741
+ function buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable }) {
551
742
  return ast.createOperationParams(node, {
552
743
  paramsType,
553
744
  pathParamsType: paramsType === "object" ? "object" : pathParamsType === "object" ? "object" : "inline",
554
745
  paramsCasing,
555
746
  resolver: tsResolver,
556
- extraParams: isConfigurable ? [ast.createFunctionParameter({
747
+ extraParams: [...isConfigurable ? [ast.createFunctionParameter({
557
748
  name: "config",
558
749
  type: ast.createParamsType({
559
750
  variant: "reference",
560
- name: requestName ? `Partial<RequestConfig<${requestName}>> & { client?: Client }` : "Partial<RequestConfig> & { client?: Client }"
751
+ name: buildRequestConfigType(node, tsResolver)
561
752
  }),
562
753
  default: "{}"
563
- })] : []
754
+ })] : []]
564
755
  });
565
756
  }
566
757
  function Client({ name, isExportable = true, isIndexable = true, returnType, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType, node, tsResolver, zodResolver, urlName, children, isConfigurable = true }) {
567
- const path = new URLPath(node.path);
568
- const contentType = node.requestBody?.content?.[0]?.contentType ?? "application/json";
569
- const isFormData = contentType === "multipart/form-data";
570
- const originalPathParams = node.parameters.filter((p) => p.in === "path");
571
- const casedPathParams = ast.caseParams(originalPathParams, paramsCasing);
572
- const originalQueryParams = node.parameters.filter((p) => p.in === "query");
573
- const casedQueryParams = ast.caseParams(originalQueryParams, paramsCasing);
574
- const originalHeaderParams = node.parameters.filter((p) => p.in === "header");
575
- const casedHeaderParams = ast.caseParams(originalHeaderParams, paramsCasing);
576
- const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : void 0;
577
- const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : void 0;
578
- const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : void 0;
579
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0;
580
- const responseName = tsResolver.resolveResponseName(node);
581
- const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]) : void 0;
582
- const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]) : void 0;
583
- const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : void 0;
584
- const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0;
758
+ if (!ast.isHttpOperationNode(node)) return null;
759
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
760
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
761
+ const responseType = getResponseType(node);
762
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
763
+ const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing });
764
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null;
765
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null;
766
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null;
767
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
768
+ const successNames = resolveSuccessNames(node, tsResolver);
769
+ const responseName = successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
770
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]) : null;
771
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]) : null;
772
+ const requestParser = resolveRequestParser(parser);
773
+ const responseParser = resolveResponseParser(parser);
774
+ const zodResponseName = zodResolver && responseParser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
775
+ const zodRequestName = zodResolver && requestParser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
776
+ const queryParamsParser = resolveQueryParamsParser(parser);
777
+ const zodQueryParamsName = zodResolver && queryParamsParser === "zod" && originalQueryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, originalQueryParams[0]) : null;
585
778
  const errorNames = node.responses.filter((r) => {
586
779
  return Number.parseInt(r.statusCode, 10) >= 400;
587
780
  }).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
588
- const headers = [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0, headerParamsName ? headerParamsMapping ? "...mappedHeaders" : "...headers" : void 0].filter(Boolean);
781
+ const headers = [!isMultipleContentTypes && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, headerParamsName ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null].filter(Boolean);
782
+ const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`;
783
+ const allStatusNames = node.responses.map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
589
784
  const generics = [
590
- responseName,
591
- `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
592
- requestName || "unknown"
785
+ dataReturnType === "full" ? allStatusNames.length > 0 ? allStatusNames.join(" | ") : responseName : responseName,
786
+ TError,
787
+ parser === "zod" && zodRequestName ? `z.output<typeof ${zodRequestName}>` : requestName || "unknown"
593
788
  ].filter(Boolean);
594
- const paramsNode = getParams({
789
+ const paramsNode = buildClientParamsNode({
595
790
  paramsType,
596
791
  paramsCasing,
597
792
  pathParamsType,
@@ -600,7 +795,7 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
600
795
  isConfigurable
601
796
  });
602
797
  const paramsSignature = declarationPrinter$2.print(paramsNode) ?? "";
603
- const urlParamsNode = Url.getParams({
798
+ const urlParamsNode = buildUrlParamsNode({
604
799
  paramsType,
605
800
  paramsCasing,
606
801
  pathParamsType,
@@ -611,20 +806,23 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
611
806
  const clientParams = createFunctionParams({ config: {
612
807
  mode: "object",
613
808
  children: {
614
- method: { value: JSON.stringify(node.method.toUpperCase()) },
615
- url: { value: urlName ? `${urlName}(${urlParamsCall}).url.toString()` : path.template },
616
- baseURL: baseURL && !urlName ? { value: `\`${baseURL}\`` } : void 0,
617
- params: queryParamsName ? queryParamsMapping ? { value: "mappedParams" } : {} : void 0,
618
- data: requestName ? { value: isFormData ? "formData as FormData" : "requestData" } : void 0,
619
- requestConfig: isConfigurable ? { mode: "inlineSpread" } : void 0,
620
- headers: headers.length ? { value: isConfigurable ? `{ ${headers.join(", ")}, ...requestConfig.headers }` : `{ ${headers.join(", ")} }` } : void 0
809
+ method: { value: stringify(node.method.toUpperCase()) },
810
+ url: { value: urlName ? `${urlName}(${urlParamsCall}).url.toString()` : Url$1.toTemplateString(node.path) },
811
+ baseURL: baseURL && !urlName ? { value: `\`${baseURL}\`` } : null,
812
+ params: queryParamsName ? zodQueryParamsName ? { value: "requestParams" } : queryParamsMapping ? { value: "mappedParams" } : {} : null,
813
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
814
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
815
+ responseType: responseType ? { value: stringify(responseType) } : null,
816
+ requestConfig: isConfigurable ? { mode: "inlineSpread" } : null,
817
+ headers: headers.length ? { value: isConfigurable ? `{ ${headers.join(", ")}, ...requestConfig.headers }` : `{ ${headers.join(", ")} }` } : null
621
818
  }
622
819
  } });
820
+ const statusUnionType = dataReturnType === "full" ? buildStatusUnionType(node, tsResolver) : null;
623
821
  const childrenElement = children ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
624
- dataReturnType === "full" && parser === "zod" && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`,
625
- dataReturnType === "data" && parser === "zod" && zodResponseName && `return ${zodResponseName}.parse(res.data)`,
626
- dataReturnType === "full" && parser === "client" && "return res",
627
- dataReturnType === "data" && parser === "client" && "return res.data"
822
+ dataReturnType === "full" && responseParser === "zod" && zodResponseName && statusUnionType && `return {...res, data: ${zodResponseName}.parse(res.data)} as ${statusUnionType}`,
823
+ dataReturnType === "full" && responseParser !== "zod" && statusUnionType && `return res as ${statusUnionType}`,
824
+ dataReturnType === "data" && responseParser === "zod" && zodResponseName && `return ${zodResponseName}.parse(res.data)`,
825
+ dataReturnType === "data" && responseParser !== "zod" && "return res.data"
628
826
  ] });
629
827
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx(File.Source, {
630
828
  name,
@@ -635,46 +833,46 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
635
833
  async: true,
636
834
  export: isExportable,
637
835
  params: paramsSignature,
638
- JSDoc: { comments: getComments(node) },
836
+ JSDoc: { comments: buildOperationComments(node, {
837
+ link: "urlPath",
838
+ linkPosition: "beforeDeprecated",
839
+ splitLines: true
840
+ }) },
639
841
  returnType,
640
842
  children: [
641
- isConfigurable ? "const { client: request = fetch, ...requestConfig } = config" : "",
642
- /* @__PURE__ */ jsx("br", {}),
843
+ isConfigurable ? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${stringify(contentType)}, ` : ""}...requestConfig } = config` : "",
643
844
  /* @__PURE__ */ jsx("br", {}),
644
845
  pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => isValidVarName(originalName) && originalName !== camelCaseName).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
645
- pathParamsMapping && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx("br", {})] }),
646
- queryParamsMapping && queryParamsName && /* @__PURE__ */ jsxs(Fragment, { children: [
647
- `const mappedParams = params ? { ${Object.entries(queryParamsMapping).map(([originalName, camelCaseName]) => `"${originalName}": params.${camelCaseName}`).join(", ")} } : undefined`,
648
- /* @__PURE__ */ jsx("br", {}),
649
- /* @__PURE__ */ jsx("br", {})
650
- ] }),
651
- headerParamsMapping && headerParamsName && /* @__PURE__ */ jsxs(Fragment, { children: [
652
- `const mappedHeaders = headers ? { ${Object.entries(headerParamsMapping).map(([originalName, camelCaseName]) => `"${originalName}": headers.${camelCaseName}`).join(", ")} } : undefined`,
653
- /* @__PURE__ */ jsx("br", {}),
654
- /* @__PURE__ */ jsx("br", {})
655
- ] }),
656
- parser === "zod" && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && "const requestData = data",
657
- /* @__PURE__ */ jsx("br", {}),
658
- isFormData && requestName && "const formData = buildFormData(requestData)",
846
+ pathParamsMapping && /* @__PURE__ */ jsx("br", {}),
847
+ queryParamsMapping && queryParamsName && /* @__PURE__ */ jsxs(Fragment, { children: [`const mappedParams = params ? { ${Object.entries(queryParamsMapping).map(([originalName, camelCaseName]) => `"${originalName}": params.${camelCaseName}`).join(", ")} } : undefined`, /* @__PURE__ */ jsx("br", {})] }),
848
+ headerParamsMapping && headerParamsName && /* @__PURE__ */ jsxs(Fragment, { children: [`const mappedHeaders = headers ? { ${Object.entries(headerParamsMapping).map(([originalName, camelCaseName]) => `"${originalName}": headers.${camelCaseName}`).join(", ")} } : undefined`, /* @__PURE__ */ jsx("br", {})] }),
849
+ requestParser === "zod" && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && "const requestData = data",
850
+ zodQueryParamsName && `const requestParams = ${zodQueryParamsName}.parse(params)`,
851
+ (isFormData || isMultipleContentTypes && hasFormData) && requestName && "const formData = buildFormData(requestData)",
659
852
  /* @__PURE__ */ jsx("br", {}),
660
- isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await fetch<${generics.join(", ")}>(${clientParams.toCall()})`,
853
+ isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await client<${generics.join(", ")}>(${clientParams.toCall()})`,
661
854
  /* @__PURE__ */ jsx("br", {}),
662
855
  childrenElement
663
856
  ]
664
857
  })
665
858
  })] });
666
859
  }
667
- Client.getParams = getParams;
668
860
  //#endregion
669
861
  //#region src/components/ClassClient.tsx
670
862
  const declarationPrinter$1 = functionPrinter({ mode: "declaration" });
671
863
  function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
672
- const path = new URLPath(node.path, { casing: paramsCasing });
673
- const contentType = node.requestBody?.content?.[0]?.contentType ?? "application/json";
674
- const isFormData = contentType === "multipart/form-data";
675
- const headers = buildHeaders(contentType, !!(node.parameters.filter((p) => p.in === "header").length > 0 ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === "header")[0]) : void 0));
676
- const generics = buildGenerics(node, tsResolver);
677
- const paramsNode = ClassClient.getParams({
864
+ if (!ast.isHttpOperationNode(node)) return "";
865
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
866
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
867
+ const { header: headerParams } = getOperationParameters(node);
868
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
869
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
870
+ const generics = buildGenerics(node, tsResolver, {
871
+ dataReturnType,
872
+ zodResolver,
873
+ parser
874
+ });
875
+ const paramsNode = buildClientParamsNode({
678
876
  paramsType,
679
877
  paramsCasing,
680
878
  pathParamsType,
@@ -683,31 +881,47 @@ function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataRe
683
881
  isConfigurable: true
684
882
  });
685
883
  const paramsSignature = declarationPrinter$1.print(paramsNode) ?? "";
884
+ const { query: queryParams } = getOperationParameters(node);
885
+ const zodQueryParamsName = zodResolver && resolveQueryParamsParser(parser) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null;
686
886
  const clientParams = buildClassClientParams({
687
887
  node,
688
- path,
888
+ url: Url$1.toTemplateString(node.path, { casing: paramsCasing }),
689
889
  baseURL,
690
890
  tsResolver,
691
891
  isFormData,
692
- headers
892
+ isMultipleContentTypes,
893
+ hasFormData,
894
+ headers,
895
+ zodQueryParamsName
693
896
  });
694
- const jsdoc = buildJSDoc(getComments(node));
897
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
898
+ link: "urlPath",
899
+ linkPosition: "beforeDeprecated",
900
+ splitLines: true
901
+ }));
695
902
  const requestDataLine = buildRequestDataLine({
696
903
  parser,
697
904
  node,
698
905
  zodResolver
699
906
  });
700
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
907
+ const queryParamsLine = buildQueryParamsLine({
908
+ parser,
909
+ node,
910
+ zodResolver
911
+ });
912
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
701
913
  const returnStatement = buildReturnStatement({
702
914
  dataReturnType,
703
915
  parser,
704
916
  node,
705
- zodResolver
917
+ zodResolver,
918
+ tsResolver
706
919
  });
707
920
  return `${jsdoc}async ${name}(${paramsSignature}) {\n${[
708
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
921
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
709
922
  "",
710
923
  requestDataLine,
924
+ queryParamsLine,
711
925
  formDataLine,
712
926
  `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})`,
713
927
  returnStatement
@@ -742,15 +956,14 @@ ${operations.map(({ node, name: methodName, tsResolver, zodResolver }) => genera
742
956
  children: [classCode, children]
743
957
  });
744
958
  }
745
- ClassClient.getParams = Client.getParams;
746
959
  //#endregion
747
960
  //#region src/components/WrapperClient.tsx
748
- function WrapperClient({ name, classNames, isExportable = true, isIndexable = true }) {
961
+ function WrapperClient({ name, controllers, isExportable = true, isIndexable = true }) {
749
962
  const classCode = `export class ${name} {
750
- ${classNames.map((className) => ` readonly ${camelCase(className)}: ${className}`).join("\n")}
963
+ ${controllers.map(({ className, propertyName }) => ` readonly ${propertyName}: ${className}`).join("\n")}
751
964
 
752
965
  constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
753
- ${classNames.map((className) => ` this.${camelCase(className)} = new ${className}(config)`).join("\n")}
966
+ ${controllers.map(({ className, propertyName }) => ` this.${propertyName} = new ${className}(config)`).join("\n")}
754
967
  }
755
968
  }`;
756
969
  return /* @__PURE__ */ jsx(File.Source, {
@@ -763,54 +976,47 @@ ${classNames.map((className) => ` this.${camelCase(className)} = new ${classN
763
976
  //#endregion
764
977
  //#region src/generators/classClientGenerator.tsx
765
978
  function resolveTypeImportNames$1(node, tsResolver) {
766
- return [
767
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
768
- tsResolver.resolveResponseName(node),
769
- ...node.parameters.filter((p) => p.in === "path").map((p) => tsResolver.resolvePathParamsName(node, p)),
770
- ...node.parameters.filter((p) => p.in === "query").map((p) => tsResolver.resolveQueryParamsName(node, p)),
771
- ...node.parameters.filter((p) => p.in === "header").map((p) => tsResolver.resolveHeaderParamsName(node, p)),
772
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode))
773
- ].filter((n) => Boolean(n));
979
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
774
980
  }
775
981
  __name(resolveTypeImportNames$1, "resolveTypeImportNames");
776
- function resolveZodImportNames$1(node, zodResolver) {
777
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
982
+ function resolveZodImportNames$1(node, zodResolver, parser) {
983
+ const { query: queryParams } = getOperationParameters(node);
984
+ return [
985
+ resolveResponseParser(parser) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
986
+ resolveRequestParser(parser) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
987
+ resolveQueryParamsParser(parser) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null
988
+ ].filter((n) => Boolean(n));
778
989
  }
779
990
  __name(resolveZodImportNames$1, "resolveZodImportNames");
991
+ /**
992
+ * Built-in `operations` generator for `@kubb/plugin-client` when
993
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
994
+ * per operation and a shared constructor for request configuration.
995
+ */
780
996
  const classClientGenerator = defineGenerator({
781
997
  name: "classClient",
782
998
  renderer: jsxRenderer,
783
999
  operations(nodes, ctx) {
784
- const { adapter, config, driver, resolver, root } = ctx;
1000
+ const { config, driver, resolver, root } = ctx;
785
1001
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options;
786
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1002
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
787
1003
  const pluginTs = driver.getPlugin(pluginTsName);
788
1004
  if (!pluginTs) return null;
789
1005
  const tsResolver = driver.getResolver(pluginTsName);
790
1006
  const tsPluginOptions = pluginTs.options;
791
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
792
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
1007
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null;
1008
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
793
1009
  function buildOperationData(node) {
794
- const typeFile = tsResolver.resolveFile({
795
- name: node.operationId,
796
- extname: ".ts",
797
- tag: node.tags[0] ?? "default",
798
- path: node.path
799
- }, {
1010
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
800
1011
  root,
801
1012
  output: tsPluginOptions?.output ?? output,
802
1013
  group: tsPluginOptions?.group
803
1014
  });
804
- const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile({
805
- name: node.operationId,
806
- extname: ".ts",
807
- tag: node.tags[0] ?? "default",
808
- path: node.path
809
- }, {
1015
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
810
1016
  root,
811
1017
  output: pluginZod.options?.output ?? output,
812
- group: pluginZod.options?.group
813
- }) : void 0;
1018
+ group: pluginZod.options?.group ?? void 0
1019
+ }) : null;
814
1020
  return {
815
1021
  node,
816
1022
  name: resolver.resolveName(node.operationId),
@@ -821,27 +1027,31 @@ const classClientGenerator = defineGenerator({
821
1027
  };
822
1028
  }
823
1029
  const controllers = nodes.reduce((acc, operationNode) => {
1030
+ if (!ast.isHttpOperationNode(operationNode)) return acc;
824
1031
  const tag = operationNode.tags[0];
825
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
1032
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveClassName("Client");
826
1033
  if (!tag && !group) {
827
- const name = "ApiClient";
1034
+ const name = resolver.resolveClassName("ApiClient");
828
1035
  const file = resolver.resolveFile({
829
1036
  name,
830
1037
  extname: ".ts"
831
1038
  }, {
832
1039
  root,
833
1040
  output,
834
- group
1041
+ group: group ?? void 0
835
1042
  });
836
1043
  const operationData = buildOperationData(operationNode);
837
1044
  const previous = acc.find((item) => item.file.path === file.path);
838
1045
  if (previous) previous.operations.push(operationData);
839
1046
  else acc.push({
840
1047
  name,
1048
+ propertyName: resolver.resolveClientPropertyName(name),
841
1049
  file,
842
1050
  operations: [operationData]
843
1051
  });
844
- } else if (tag) {
1052
+ return acc;
1053
+ }
1054
+ if (tag) {
845
1055
  const name = groupName;
846
1056
  const file = resolver.resolveFile({
847
1057
  name,
@@ -850,13 +1060,14 @@ const classClientGenerator = defineGenerator({
850
1060
  }, {
851
1061
  root,
852
1062
  output,
853
- group
1063
+ group: group ?? void 0
854
1064
  });
855
1065
  const operationData = buildOperationData(operationNode);
856
1066
  const previous = acc.find((item) => item.file.path === file.path);
857
1067
  if (previous) previous.operations.push(operationData);
858
1068
  else acc.push({
859
1069
  name,
1070
+ propertyName: resolver.resolveClientPropertyName(name),
860
1071
  file,
861
1072
  operations: [operationData]
862
1073
  });
@@ -885,7 +1096,7 @@ const classClientGenerator = defineGenerator({
885
1096
  const zodFilesByPath = /* @__PURE__ */ new Map();
886
1097
  ops.forEach((op) => {
887
1098
  if (!op.zodFile || !zodResolver) return;
888
- const names = resolveZodImportNames$1(op.node, zodResolver);
1099
+ const names = resolveZodImportNames$1(op.node, zodResolver, parser);
889
1100
  if (!zodImportsByFile.has(op.zodFile.path)) zodImportsByFile.set(op.zodFile.path, /* @__PURE__ */ new Set());
890
1101
  const imports = zodImportsByFile.get(op.zodFile.path);
891
1102
  names.forEach((n) => {
@@ -900,27 +1111,35 @@ const classClientGenerator = defineGenerator({
900
1111
  }
901
1112
  const files = controllers.map(({ name, file, operations: ops }) => {
902
1113
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops);
903
- const { zodImportsByFile, zodFilesByPath } = parser === "zod" ? collectZodImports(ops) : {
1114
+ const { zodImportsByFile, zodFilesByPath } = isParserEnabled(parser) ? collectZodImports(ops) : {
904
1115
  zodImportsByFile: /* @__PURE__ */ new Map(),
905
1116
  zodFilesByPath: /* @__PURE__ */ new Map()
906
1117
  };
907
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1118
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
908
1119
  return /* @__PURE__ */ jsxs(File, {
909
1120
  baseName: file.baseName,
910
1121
  path: file.path,
911
1122
  meta: file.meta,
912
- banner: resolver.resolveBanner(adapter.inputNode, {
1123
+ banner: resolver.resolveBanner(ctx.meta, {
913
1124
  output,
914
- config
1125
+ config,
1126
+ file: {
1127
+ path: file.path,
1128
+ baseName: file.baseName
1129
+ }
915
1130
  }),
916
- footer: resolver.resolveFooter(adapter.inputNode, {
1131
+ footer: resolver.resolveFooter(ctx.meta, {
917
1132
  output,
918
- config
1133
+ config,
1134
+ file: {
1135
+ path: file.path,
1136
+ baseName: file.baseName
1137
+ }
919
1138
  }),
920
1139
  children: [
921
1140
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
922
1141
  /* @__PURE__ */ jsx(File.Import, {
923
- name: "fetch",
1142
+ name: "client",
924
1143
  path: importPath
925
1144
  }),
926
1145
  /* @__PURE__ */ jsx(File.Import, {
@@ -938,7 +1157,7 @@ const classClientGenerator = defineGenerator({
938
1157
  })
939
1158
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
940
1159
  /* @__PURE__ */ jsx(File.Import, {
941
- name: ["fetch"],
1160
+ name: ["client"],
942
1161
  root: file.path,
943
1162
  path: path.resolve(root, ".kubb/client.ts")
944
1163
  }),
@@ -963,6 +1182,11 @@ const classClientGenerator = defineGenerator({
963
1182
  root: file.path,
964
1183
  path: path.resolve(root, ".kubb/config.ts")
965
1184
  }),
1185
+ parser === "zod" && zodResolver && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && /* @__PURE__ */ jsx(File.Import, {
1186
+ name: ["z"],
1187
+ path: "zod",
1188
+ isTypeOnly: true
1189
+ }),
966
1190
  Array.from(typeImportsByFile.entries()).map(([filePath, importSet]) => {
967
1191
  const typeFile = typeFilesByPath.get(filePath);
968
1192
  if (!typeFile) return null;
@@ -975,7 +1199,7 @@ const classClientGenerator = defineGenerator({
975
1199
  isTypeOnly: true
976
1200
  }, filePath);
977
1201
  }),
978
- parser === "zod" && Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
1202
+ isParserEnabled(parser) && Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
979
1203
  const zodFile = zodFilesByPath.get(filePath);
980
1204
  if (!zodFile) return null;
981
1205
  const importNames = Array.from(importSet).filter(Boolean);
@@ -1006,19 +1230,27 @@ const classClientGenerator = defineGenerator({
1006
1230
  }, {
1007
1231
  root,
1008
1232
  output,
1009
- group
1233
+ group: group ?? void 0
1010
1234
  });
1011
1235
  files.push(/* @__PURE__ */ jsxs(File, {
1012
1236
  baseName: sdkFile.baseName,
1013
1237
  path: sdkFile.path,
1014
1238
  meta: sdkFile.meta,
1015
- banner: resolver.resolveBanner(adapter.inputNode, {
1239
+ banner: resolver.resolveBanner(ctx.meta, {
1016
1240
  output,
1017
- config
1241
+ config,
1242
+ file: {
1243
+ path: sdkFile.path,
1244
+ baseName: sdkFile.baseName
1245
+ }
1018
1246
  }),
1019
- footer: resolver.resolveFooter(adapter.inputNode, {
1247
+ footer: resolver.resolveFooter(ctx.meta, {
1020
1248
  output,
1021
- config
1249
+ config,
1250
+ file: {
1251
+ path: sdkFile.path,
1252
+ baseName: sdkFile.baseName
1253
+ }
1022
1254
  }),
1023
1255
  children: [
1024
1256
  importPath ? /* @__PURE__ */ jsx(File.Import, {
@@ -1038,7 +1270,10 @@ const classClientGenerator = defineGenerator({
1038
1270
  }, name)),
1039
1271
  /* @__PURE__ */ jsx(WrapperClient, {
1040
1272
  name: sdk.className,
1041
- classNames: controllers.map(({ name }) => name)
1273
+ controllers: controllers.map(({ name, propertyName }) => ({
1274
+ className: name,
1275
+ propertyName
1276
+ }))
1042
1277
  })
1043
1278
  ]
1044
1279
  }, sdkFile.path));
@@ -1048,81 +1283,75 @@ const classClientGenerator = defineGenerator({
1048
1283
  });
1049
1284
  //#endregion
1050
1285
  //#region src/generators/clientGenerator.tsx
1286
+ /**
1287
+ * Built-in operation generator for `@kubb/plugin-client`. Emits one async
1288
+ * function per OpenAPI operation, plus the matching URL helper. Used when
1289
+ * `clientType: 'function'` (the default).
1290
+ */
1051
1291
  const clientGenerator = defineGenerator({
1052
1292
  name: "client",
1053
1293
  renderer: jsxRenderer,
1054
1294
  operation(node, ctx) {
1055
- const { adapter, config, driver, resolver, root } = ctx;
1295
+ if (!ast.isHttpOperationNode(node)) return null;
1296
+ const { config, driver, resolver, root } = ctx;
1056
1297
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options;
1057
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1298
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1058
1299
  const pluginTs = driver.getPlugin(pluginTsName);
1059
1300
  if (!pluginTs) return null;
1060
1301
  const tsResolver = driver.getResolver(pluginTsName);
1061
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
1062
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
1063
- const casedParams = ast.caseParams(node.parameters, paramsCasing);
1064
- const pathParams = casedParams.filter((p) => p.in === "path");
1065
- const queryParams = casedParams.filter((p) => p.in === "query");
1066
- const headerParams = casedParams.filter((p) => p.in === "header");
1067
- const importedTypeNames = [
1068
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
1069
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
1070
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
1071
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
1072
- tsResolver.resolveResponseName(node),
1073
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode))
1074
- ].filter(Boolean);
1075
- const importedZodNames = zodResolver && parser === "zod" ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter(Boolean) : [];
1302
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null;
1303
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1304
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing });
1305
+ const { query: queryParams } = getOperationParameters(node);
1306
+ const importedZodNames = zodResolver ? [
1307
+ resolveResponseParser(parser) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
1308
+ resolveRequestParser(parser) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
1309
+ resolveQueryParamsParser(parser) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null
1310
+ ].filter((name) => Boolean(name)) : [];
1311
+ const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) ?? null : null;
1076
1312
  const meta = {
1077
1313
  name: resolver.resolveName(node.operationId),
1078
- urlName: `get${resolver.resolveName(node.operationId).charAt(0).toUpperCase()}${resolver.resolveName(node.operationId).slice(1)}Url`,
1079
- file: resolver.resolveFile({
1080
- name: node.operationId,
1081
- extname: ".ts",
1082
- tag: node.tags[0] ?? "default",
1083
- path: node.path
1084
- }, {
1314
+ urlName: resolver.resolveUrlName(node),
1315
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), {
1085
1316
  root,
1086
1317
  output,
1087
- group
1318
+ group: group ?? void 0
1088
1319
  }),
1089
- fileTs: tsResolver.resolveFile({
1090
- name: node.operationId,
1091
- extname: ".ts",
1092
- tag: node.tags[0] ?? "default",
1093
- path: node.path
1094
- }, {
1320
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1095
1321
  root,
1096
1322
  output: pluginTs.options?.output ?? output,
1097
- group: pluginTs.options?.group
1323
+ group: pluginTs.options?.group ?? void 0
1098
1324
  }),
1099
- fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile({
1100
- name: node.operationId,
1101
- extname: ".ts",
1102
- tag: node.tags[0] ?? "default",
1103
- path: node.path
1104
- }, {
1325
+ fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1105
1326
  root,
1106
1327
  output: pluginZod.options.output ?? output,
1107
- group: pluginZod.options?.group
1108
- }) : void 0
1328
+ group: pluginZod.options?.group ?? void 0
1329
+ }) : null
1109
1330
  };
1110
- const isFormData = node.requestBody?.content?.[0]?.contentType === "multipart/form-data";
1331
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false;
1111
1332
  return /* @__PURE__ */ jsxs(File, {
1112
1333
  baseName: meta.file.baseName,
1113
1334
  path: meta.file.path,
1114
1335
  meta: meta.file.meta,
1115
- banner: resolver.resolveBanner(adapter.inputNode, {
1336
+ banner: resolver.resolveBanner(ctx.meta, {
1116
1337
  output,
1117
- config
1338
+ config,
1339
+ file: {
1340
+ path: meta.file.path,
1341
+ baseName: meta.file.baseName
1342
+ }
1118
1343
  }),
1119
- footer: resolver.resolveFooter(adapter.inputNode, {
1344
+ footer: resolver.resolveFooter(ctx.meta, {
1120
1345
  output,
1121
- config
1346
+ config,
1347
+ file: {
1348
+ path: meta.file.path,
1349
+ baseName: meta.file.baseName
1350
+ }
1122
1351
  }),
1123
1352
  children: [
1124
1353
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1125
- name: "fetch",
1354
+ name: "client",
1126
1355
  path: importPath
1127
1356
  }), /* @__PURE__ */ jsx(File.Import, {
1128
1357
  name: [
@@ -1133,7 +1362,7 @@ const clientGenerator = defineGenerator({
1133
1362
  path: importPath,
1134
1363
  isTypeOnly: true
1135
1364
  })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1136
- name: ["fetch"],
1365
+ name: ["client"],
1137
1366
  root: meta.file.path,
1138
1367
  path: path.resolve(root, ".kubb/client.ts")
1139
1368
  }), /* @__PURE__ */ jsx(File.Import, {
@@ -1146,11 +1375,16 @@ const clientGenerator = defineGenerator({
1146
1375
  path: path.resolve(root, ".kubb/client.ts"),
1147
1376
  isTypeOnly: true
1148
1377
  })] }),
1149
- isFormData && node.requestBody?.content?.[0]?.schema && /* @__PURE__ */ jsx(File.Import, {
1378
+ hasFormData && /* @__PURE__ */ jsx(File.Import, {
1150
1379
  name: ["buildFormData"],
1151
1380
  root: meta.file.path,
1152
1381
  path: path.resolve(root, ".kubb/config.ts")
1153
1382
  }),
1383
+ zodRequestName && /* @__PURE__ */ jsx(File.Import, {
1384
+ name: ["z"],
1385
+ path: "zod",
1386
+ isTypeOnly: true
1387
+ }),
1154
1388
  meta.fileZod && importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1155
1389
  name: importedZodNames,
1156
1390
  root: meta.file.path,
@@ -1192,16 +1426,22 @@ const clientGenerator = defineGenerator({
1192
1426
  });
1193
1427
  //#endregion
1194
1428
  //#region src/generators/groupedClientGenerator.tsx
1429
+ /**
1430
+ * Emits one aggregate file per tag/group when `group` is configured. Each
1431
+ * file re-exports every client function for that group, so callers can
1432
+ * `import { petController } from './gen/clients'` instead of importing
1433
+ * each operation individually.
1434
+ */
1195
1435
  const groupedClientGenerator = defineGenerator({
1196
1436
  name: "groupedClient",
1197
1437
  renderer: jsxRenderer,
1198
1438
  operations(nodes, ctx) {
1199
- const { config, resolver, adapter, root } = ctx;
1439
+ const { config, resolver, root } = ctx;
1200
1440
  const { output, group } = ctx.options;
1201
1441
  return /* @__PURE__ */ jsx(Fragment, { children: nodes.reduce((acc, operationNode) => {
1202
1442
  if (group?.type === "tag") {
1203
1443
  const tag = operationNode.tags[0];
1204
- const name = tag ? group?.name?.({ group: camelCase(tag) }) : void 0;
1444
+ const name = tag ? group?.name?.({ group: camelCase(tag) }) : null;
1205
1445
  if (!tag || !name) return acc;
1206
1446
  const file = resolver.resolveFile({
1207
1447
  name,
@@ -1210,7 +1450,7 @@ const groupedClientGenerator = defineGenerator({
1210
1450
  }, {
1211
1451
  root,
1212
1452
  output,
1213
- group
1453
+ group: group ?? void 0
1214
1454
  });
1215
1455
  const clientFile = resolver.resolveFile({
1216
1456
  name: operationNode.operationId,
@@ -1220,7 +1460,7 @@ const groupedClientGenerator = defineGenerator({
1220
1460
  }, {
1221
1461
  root,
1222
1462
  output,
1223
- group
1463
+ group: group ?? void 0
1224
1464
  });
1225
1465
  const client = {
1226
1466
  name: resolver.resolveName(operationNode.operationId),
@@ -1240,13 +1480,23 @@ const groupedClientGenerator = defineGenerator({
1240
1480
  baseName: file.baseName,
1241
1481
  path: file.path,
1242
1482
  meta: file.meta,
1243
- banner: resolver.resolveBanner(adapter.inputNode, {
1483
+ banner: resolver.resolveBanner(ctx.meta, {
1244
1484
  output,
1245
- config
1485
+ config,
1486
+ file: {
1487
+ path: file.path,
1488
+ baseName: file.baseName,
1489
+ isAggregation: true
1490
+ }
1246
1491
  }),
1247
- footer: resolver.resolveFooter(adapter.inputNode, {
1492
+ footer: resolver.resolveFooter(ctx.meta, {
1248
1493
  output,
1249
- config
1494
+ config,
1495
+ file: {
1496
+ path: file.path,
1497
+ baseName: file.baseName,
1498
+ isAggregation: true
1499
+ }
1250
1500
  }),
1251
1501
  children: [clients.map((client) => /* @__PURE__ */ jsx(File.Import, {
1252
1502
  name: [client.name],
@@ -1267,33 +1517,17 @@ const groupedClientGenerator = defineGenerator({
1267
1517
  }
1268
1518
  });
1269
1519
  //#endregion
1270
- //#region src/components/Operations.tsx
1271
- function Operations({ name, nodes }) {
1272
- const operationsObject = {};
1273
- nodes.forEach((node) => {
1274
- operationsObject[node.operationId] = {
1275
- path: new URLPath(node.path).URL,
1276
- method: node.method.toLowerCase()
1277
- };
1278
- });
1279
- return /* @__PURE__ */ jsx(File.Source, {
1280
- name,
1281
- isExportable: true,
1282
- isIndexable: true,
1283
- children: /* @__PURE__ */ jsx(Const, {
1284
- name,
1285
- export: true,
1286
- children: JSON.stringify(operationsObject, void 0, 2)
1287
- })
1288
- });
1289
- }
1290
- //#endregion
1291
- //#region src/generators/operationsGenerator.tsx
1520
+ //#region src/generators/operationsGenerator.ts
1521
+ /**
1522
+ * Generates an `operations.ts` file that re-exports every operation grouped
1523
+ * by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
1524
+ * for building meta-tooling on top of the generated client (route
1525
+ * registries, API explorers).
1526
+ */
1292
1527
  const operationsGenerator = defineGenerator({
1293
1528
  name: "client",
1294
- renderer: jsxRenderer,
1295
1529
  operations(nodes, ctx) {
1296
- const { config, resolver, adapter, root } = ctx;
1530
+ const { config, resolver, root } = ctx;
1297
1531
  const { output, group } = ctx.options;
1298
1532
  const name = "operations";
1299
1533
  const file = resolver.resolveFile({
@@ -1302,37 +1536,65 @@ const operationsGenerator = defineGenerator({
1302
1536
  }, {
1303
1537
  root,
1304
1538
  output,
1305
- group
1539
+ group: group ?? void 0
1306
1540
  });
1307
- return /* @__PURE__ */ jsx(File, {
1541
+ const operationsObject = {};
1542
+ for (const node of nodes) {
1543
+ if (!ast.isHttpOperationNode(node)) continue;
1544
+ operationsObject[node.operationId] = {
1545
+ path: Url$1.toPath(node.path),
1546
+ method: node.method.toLowerCase()
1547
+ };
1548
+ }
1549
+ return [ast.createFile({
1308
1550
  baseName: file.baseName,
1309
1551
  path: file.path,
1310
1552
  meta: file.meta,
1311
- banner: resolver.resolveBanner(adapter.inputNode, {
1553
+ banner: resolver.resolveBanner(ctx.meta, {
1312
1554
  output,
1313
- config
1555
+ config,
1556
+ file: {
1557
+ path: file.path,
1558
+ baseName: file.baseName
1559
+ }
1314
1560
  }),
1315
- footer: resolver.resolveFooter(adapter.inputNode, {
1561
+ footer: resolver.resolveFooter(ctx.meta, {
1316
1562
  output,
1317
- config
1563
+ config,
1564
+ file: {
1565
+ path: file.path,
1566
+ baseName: file.baseName
1567
+ }
1318
1568
  }),
1319
- children: /* @__PURE__ */ jsx(Operations, {
1569
+ sources: [ast.createSource({
1320
1570
  name,
1321
- nodes
1322
- })
1323
- });
1571
+ isExportable: true,
1572
+ isIndexable: true,
1573
+ nodes: [ast.createConst({
1574
+ name,
1575
+ export: true,
1576
+ nodes: [ast.createText(JSON.stringify(operationsObject, void 0, 2))]
1577
+ })]
1578
+ })]
1579
+ })];
1324
1580
  }
1325
1581
  });
1326
1582
  //#endregion
1327
1583
  //#region src/components/StaticClassClient.tsx
1328
1584
  const declarationPrinter = functionPrinter({ mode: "declaration" });
1329
1585
  function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
1330
- const path = new URLPath(node.path, { casing: paramsCasing });
1331
- const contentType = node.requestBody?.content?.[0]?.contentType ?? "application/json";
1332
- const isFormData = contentType === "multipart/form-data";
1333
- const headers = buildHeaders(contentType, !!(node.parameters.filter((p) => p.in === "header").length > 0 ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === "header")[0]) : void 0));
1334
- const generics = buildGenerics(node, tsResolver);
1335
- const paramsNode = Client.getParams({
1586
+ if (!ast.isHttpOperationNode(node)) return "";
1587
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
1588
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
1589
+ const { header: headerParams } = getOperationParameters(node);
1590
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
1591
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
1592
+ const generics = buildGenerics(node, tsResolver, {
1593
+ dataReturnType,
1594
+ zodResolver,
1595
+ parser
1596
+ });
1597
+ const paramsNode = buildClientParamsNode({
1336
1598
  paramsType,
1337
1599
  paramsCasing,
1338
1600
  pathParamsType,
@@ -1341,31 +1603,47 @@ function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataRetu
1341
1603
  isConfigurable: true
1342
1604
  });
1343
1605
  const paramsSignature = declarationPrinter.print(paramsNode) ?? "";
1606
+ const { query: queryParams } = getOperationParameters(node);
1607
+ const zodQueryParamsName = zodResolver && resolveQueryParamsParser(parser) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null;
1344
1608
  const clientParams = buildClassClientParams({
1345
1609
  node,
1346
- path,
1610
+ url: Url$1.toTemplateString(node.path, { casing: paramsCasing }),
1347
1611
  baseURL,
1348
1612
  tsResolver,
1349
1613
  isFormData,
1350
- headers
1614
+ isMultipleContentTypes,
1615
+ hasFormData,
1616
+ headers,
1617
+ zodQueryParamsName
1351
1618
  });
1352
- const jsdoc = buildJSDoc(getComments(node));
1619
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
1620
+ link: "urlPath",
1621
+ linkPosition: "beforeDeprecated",
1622
+ splitLines: true
1623
+ }));
1353
1624
  const requestDataLine = buildRequestDataLine({
1354
1625
  parser,
1355
1626
  node,
1356
1627
  zodResolver
1357
1628
  });
1358
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
1629
+ const queryParamsLine = buildQueryParamsLine({
1630
+ parser,
1631
+ node,
1632
+ zodResolver
1633
+ });
1634
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
1359
1635
  const returnStatement = buildReturnStatement({
1360
1636
  dataReturnType,
1361
1637
  parser,
1362
1638
  node,
1363
- zodResolver
1639
+ zodResolver,
1640
+ tsResolver
1364
1641
  });
1365
1642
  return `${jsdoc} static async ${name}(${paramsSignature}) {\n${[
1366
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
1643
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
1367
1644
  "",
1368
1645
  requestDataLine,
1646
+ queryParamsLine,
1369
1647
  formDataLine,
1370
1648
  `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})`,
1371
1649
  returnStatement
@@ -1391,56 +1669,49 @@ function StaticClassClient({ name, isExportable = true, isIndexable = true, oper
1391
1669
  children: [classCode, children]
1392
1670
  });
1393
1671
  }
1394
- StaticClassClient.getParams = Client.getParams;
1395
1672
  //#endregion
1396
1673
  //#region src/generators/staticClassClientGenerator.tsx
1397
1674
  function resolveTypeImportNames(node, tsResolver) {
1675
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
1676
+ }
1677
+ function resolveZodImportNames(node, zodResolver, parser) {
1678
+ const { query: queryParams } = getOperationParameters(node);
1398
1679
  return [
1399
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
1400
- tsResolver.resolveResponseName(node),
1401
- ...node.parameters.filter((p) => p.in === "path").map((p) => tsResolver.resolvePathParamsName(node, p)),
1402
- ...node.parameters.filter((p) => p.in === "query").map((p) => tsResolver.resolveQueryParamsName(node, p)),
1403
- ...node.parameters.filter((p) => p.in === "header").map((p) => tsResolver.resolveHeaderParamsName(node, p)),
1404
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode))
1680
+ resolveResponseParser(parser) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
1681
+ resolveRequestParser(parser) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
1682
+ resolveQueryParamsParser(parser) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null
1405
1683
  ].filter((n) => Boolean(n));
1406
1684
  }
1407
- function resolveZodImportNames(node, zodResolver) {
1408
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
1409
- }
1685
+ /**
1686
+ * Built-in `operations` generator for `@kubb/plugin-client` when
1687
+ * `clientType: 'staticClass'`. Emits one class per tag, with a static method
1688
+ * per operation so callers can use `Pet.getPetById(...)` without
1689
+ * instantiating the class.
1690
+ */
1410
1691
  const staticClassClientGenerator = defineGenerator({
1411
1692
  name: "staticClassClient",
1412
1693
  renderer: jsxRenderer,
1413
1694
  operations(nodes, ctx) {
1414
- const { adapter, config, driver, resolver, root } = ctx;
1695
+ const { config, driver, resolver, root } = ctx;
1415
1696
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath } = ctx.options;
1416
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1697
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1417
1698
  const pluginTs = driver.getPlugin(pluginTsName);
1418
1699
  if (!pluginTs) return null;
1419
1700
  const tsResolver = driver.getResolver(pluginTsName);
1420
1701
  const tsPluginOptions = pluginTs.options;
1421
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
1422
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
1702
+ const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null;
1703
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1423
1704
  function buildOperationData(node) {
1424
- const typeFile = tsResolver.resolveFile({
1425
- name: node.operationId,
1426
- extname: ".ts",
1427
- tag: node.tags[0] ?? "default",
1428
- path: node.path
1429
- }, {
1705
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1430
1706
  root,
1431
1707
  output: tsPluginOptions?.output ?? output,
1432
1708
  group: tsPluginOptions?.group
1433
1709
  });
1434
- const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile({
1435
- name: node.operationId,
1436
- extname: ".ts",
1437
- tag: node.tags[0] ?? "default",
1438
- path: node.path
1439
- }, {
1710
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1440
1711
  root,
1441
1712
  output: pluginZod.options?.output ?? output,
1442
- group: pluginZod.options?.group
1443
- }) : void 0;
1713
+ group: pluginZod.options?.group ?? void 0
1714
+ }) : null;
1444
1715
  return {
1445
1716
  node,
1446
1717
  name: resolver.resolveName(node.operationId),
@@ -1451,17 +1722,18 @@ const staticClassClientGenerator = defineGenerator({
1451
1722
  };
1452
1723
  }
1453
1724
  const controllers = nodes.reduce((acc, operationNode) => {
1725
+ if (!ast.isHttpOperationNode(operationNode)) return acc;
1454
1726
  const tag = operationNode.tags[0];
1455
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
1727
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveClassName("Client");
1456
1728
  if (!tag && !group) {
1457
- const name = "ApiClient";
1729
+ const name = resolver.resolveClassName("ApiClient");
1458
1730
  const file = resolver.resolveFile({
1459
1731
  name,
1460
1732
  extname: ".ts"
1461
1733
  }, {
1462
1734
  root,
1463
1735
  output,
1464
- group
1736
+ group: group ?? void 0
1465
1737
  });
1466
1738
  const operationData = buildOperationData(operationNode);
1467
1739
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1471,7 +1743,9 @@ const staticClassClientGenerator = defineGenerator({
1471
1743
  file,
1472
1744
  operations: [operationData]
1473
1745
  });
1474
- } else if (tag) {
1746
+ return acc;
1747
+ }
1748
+ if (tag) {
1475
1749
  const name = groupName;
1476
1750
  const file = resolver.resolveFile({
1477
1751
  name,
@@ -1480,7 +1754,7 @@ const staticClassClientGenerator = defineGenerator({
1480
1754
  }, {
1481
1755
  root,
1482
1756
  output,
1483
- group
1757
+ group: group ?? void 0
1484
1758
  });
1485
1759
  const operationData = buildOperationData(operationNode);
1486
1760
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1515,7 +1789,7 @@ const staticClassClientGenerator = defineGenerator({
1515
1789
  const zodFilesByPath = /* @__PURE__ */ new Map();
1516
1790
  ops.forEach((op) => {
1517
1791
  if (!op.zodFile || !zodResolver) return;
1518
- const names = resolveZodImportNames(op.node, zodResolver);
1792
+ const names = resolveZodImportNames(op.node, zodResolver, parser);
1519
1793
  if (!zodImportsByFile.has(op.zodFile.path)) zodImportsByFile.set(op.zodFile.path, /* @__PURE__ */ new Set());
1520
1794
  const imports = zodImportsByFile.get(op.zodFile.path);
1521
1795
  names.forEach((n) => {
@@ -1530,27 +1804,35 @@ const staticClassClientGenerator = defineGenerator({
1530
1804
  }
1531
1805
  return /* @__PURE__ */ jsx(Fragment, { children: controllers.map(({ name, file, operations: ops }) => {
1532
1806
  const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops);
1533
- const { zodImportsByFile, zodFilesByPath } = parser === "zod" ? collectZodImports(ops) : {
1807
+ const { zodImportsByFile, zodFilesByPath } = isParserEnabled(parser) ? collectZodImports(ops) : {
1534
1808
  zodImportsByFile: /* @__PURE__ */ new Map(),
1535
1809
  zodFilesByPath: /* @__PURE__ */ new Map()
1536
1810
  };
1537
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1811
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
1538
1812
  return /* @__PURE__ */ jsxs(File, {
1539
1813
  baseName: file.baseName,
1540
1814
  path: file.path,
1541
1815
  meta: file.meta,
1542
- banner: resolver.resolveBanner(adapter.inputNode, {
1816
+ banner: resolver.resolveBanner(ctx.meta, {
1543
1817
  output,
1544
- config
1818
+ config,
1819
+ file: {
1820
+ path: file.path,
1821
+ baseName: file.baseName
1822
+ }
1545
1823
  }),
1546
- footer: resolver.resolveFooter(adapter.inputNode, {
1824
+ footer: resolver.resolveFooter(ctx.meta, {
1547
1825
  output,
1548
- config
1826
+ config,
1827
+ file: {
1828
+ path: file.path,
1829
+ baseName: file.baseName
1830
+ }
1549
1831
  }),
1550
1832
  children: [
1551
1833
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
1552
1834
  /* @__PURE__ */ jsx(File.Import, {
1553
- name: "fetch",
1835
+ name: "client",
1554
1836
  path: importPath
1555
1837
  }),
1556
1838
  /* @__PURE__ */ jsx(File.Import, {
@@ -1568,7 +1850,7 @@ const staticClassClientGenerator = defineGenerator({
1568
1850
  })
1569
1851
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1570
1852
  /* @__PURE__ */ jsx(File.Import, {
1571
- name: ["fetch"],
1853
+ name: ["client"],
1572
1854
  root: file.path,
1573
1855
  path: path.resolve(root, ".kubb/client.ts")
1574
1856
  }),
@@ -1593,6 +1875,11 @@ const staticClassClientGenerator = defineGenerator({
1593
1875
  root: file.path,
1594
1876
  path: path.resolve(root, ".kubb/config.ts")
1595
1877
  }),
1878
+ parser === "zod" && zodResolver && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && /* @__PURE__ */ jsx(File.Import, {
1879
+ name: ["z"],
1880
+ path: "zod",
1881
+ isTypeOnly: true
1882
+ }),
1596
1883
  Array.from(typeImportsByFile.entries()).map(([filePath, importSet]) => {
1597
1884
  const typeFile = typeFilesByPath.get(filePath);
1598
1885
  if (!typeFile) return null;
@@ -1605,7 +1892,7 @@ const staticClassClientGenerator = defineGenerator({
1605
1892
  isTypeOnly: true
1606
1893
  }, filePath);
1607
1894
  }),
1608
- parser === "zod" && Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
1895
+ isParserEnabled(parser) && Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
1609
1896
  const zodFile = zodFilesByPath.get(filePath);
1610
1897
  if (!zodFile) return null;
1611
1898
  const importNames = Array.from(importSet).filter(Boolean);
@@ -1634,64 +1921,95 @@ const staticClassClientGenerator = defineGenerator({
1634
1921
  //#endregion
1635
1922
  //#region src/resolvers/resolverClient.ts
1636
1923
  /**
1637
- * Naming convention resolver for client plugin.
1924
+ * Default resolver used by `@kubb/plugin-client`. Decides the names and file
1925
+ * paths for every generated client function or class. Functions and files use
1926
+ * camelCase; classes and tag groups use PascalCase.
1638
1927
  *
1639
- * Provides default naming helpers using camelCase for functions and file paths.
1928
+ * @example Resolve client function and class names
1929
+ * ```ts
1930
+ * import { resolverClient } from '@kubb/plugin-client'
1640
1931
  *
1641
- * @example
1642
- * `resolverClient.default('list pets', 'function') // 'listPets'`
1932
+ * resolverClient.default('list pets', 'function') // 'listPets'
1933
+ * resolverClient.resolveClassName('pet') // 'Pet'
1934
+ * resolverClient.resolveGroupName('pet') // 'PetClient'
1935
+ * resolverClient.resolveUrlName(operationNode) // 'getShowPetByIdUrl'
1936
+ * ```
1643
1937
  */
1644
- const resolverClient = defineResolver((ctx) => ({
1938
+ const resolverClient = defineResolver(() => ({
1645
1939
  name: "default",
1646
1940
  pluginName: "plugin-client",
1647
1941
  default(name, type) {
1648
- return camelCase(name, { isFile: type === "file" });
1942
+ if (type === "file") return toFilePath(name);
1943
+ return ensureValidVarName(camelCase(name));
1649
1944
  },
1650
1945
  resolveName(name) {
1651
- return ctx.default(name, "function");
1946
+ return this.default(name, "function");
1947
+ },
1948
+ resolvePathName(name, type) {
1949
+ return this.default(name, type);
1950
+ },
1951
+ resolveClassName(name) {
1952
+ return ensureValidVarName(pascalCase(name));
1953
+ },
1954
+ resolveGroupName(name) {
1955
+ return ensureValidVarName(pascalCase(`${name} Client`));
1956
+ },
1957
+ resolveClientPropertyName(name) {
1958
+ return ensureValidVarName(camelCase(name));
1959
+ },
1960
+ resolveUrlName(node) {
1961
+ const name = this.resolveName(node.operationId);
1962
+ return `get${name.charAt(0).toUpperCase()}${name.slice(1)}Url`;
1652
1963
  }
1653
1964
  }));
1654
1965
  //#endregion
1655
1966
  //#region src/plugin.ts
1656
1967
  /**
1657
- * Canonical plugin name for `@kubb/plugin-client`, used in driver lookups and warnings.
1968
+ * Canonical plugin name for `@kubb/plugin-client`. Used for driver lookups and
1969
+ * cross-plugin dependency references.
1658
1970
  */
1659
1971
  const pluginClientName = "plugin-client";
1660
1972
  /**
1661
- * Generates type-safe HTTP client functions or classes from an OpenAPI specification.
1662
- * Creates client APIs by walking operations and delegating to generators.
1663
- * Writes barrel files based on the configured `barrelType`.
1973
+ * Generates one HTTP client function per OpenAPI operation. Each function has
1974
+ * typed path params, query params, body, and response, so callers use the API
1975
+ * like any other typed function. Ships with `axios` and `fetch` runtimes; bring
1976
+ * your own by setting `importPath`.
1664
1977
  *
1665
- * @example Client generator
1978
+ * @example
1666
1979
  * ```ts
1667
- * import pluginClient from '@kubb/plugin-client'
1980
+ * import { defineConfig } from 'kubb'
1981
+ * import { pluginTs } from '@kubb/plugin-ts'
1982
+ * import { pluginClient } from '@kubb/plugin-client'
1983
+ *
1668
1984
  * export default defineConfig({
1669
- * plugins: [pluginClient({ output: { path: 'clients' } })]
1985
+ * input: { path: './petStore.yaml' },
1986
+ * output: { path: './src/gen' },
1987
+ * plugins: [
1988
+ * pluginTs(),
1989
+ * pluginClient({
1990
+ * output: { path: './clients' },
1991
+ * client: 'fetch',
1992
+ * }),
1993
+ * ],
1670
1994
  * })
1671
1995
  * ```
1672
1996
  */
1673
1997
  const pluginClient = definePlugin((options) => {
1674
1998
  const { output = {
1675
1999
  path: "clients",
1676
- barrelType: "named"
1677
- }, group, exclude = [], include, override = [], urlType = false, dataReturnType = "data", paramsType = "inline", pathParamsType = paramsType === "object" ? "object" : options.pathParamsType || "inline", operations = false, paramsCasing, clientType = options.sdk ? "class" : "function", parser = "client", client = "axios", importPath, bundle = false, sdk, baseURL, resolver: userResolver, transformer: userTransformer } = options;
2000
+ barrel: { type: "named" }
2001
+ }, group, exclude = [], include, override = [], urlType = false, dataReturnType = "data", paramsType = "inline", pathParamsType = paramsType === "object" ? "object" : options.pathParamsType || "inline", operations = false, paramsCasing, clientType = options.sdk ? "class" : "function", parser = false, client = "axios", importPath, bundle = false, sdk, baseURL, resolver: userResolver, transformer: userTransformer } = options;
1678
2002
  const resolvedImportPath = importPath ?? (!bundle ? `@kubb/plugin-client/clients/${client}` : void 0);
1679
2003
  const selectedGenerators = options.generators ?? [
1680
2004
  clientType === "staticClass" ? staticClassClientGenerator : clientType === "class" ? classClientGenerator : clientGenerator,
1681
- group && clientType === "function" ? groupedClientGenerator : void 0,
1682
- operations ? operationsGenerator : void 0
2005
+ group && clientType === "function" ? groupedClientGenerator : null,
2006
+ operations ? operationsGenerator : null
1683
2007
  ].filter((x) => Boolean(x));
1684
- const groupConfig = group ? {
1685
- ...group,
1686
- name: group.name ? group.name : (ctx) => {
1687
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1688
- return `${camelCase(ctx.group)}Controller`;
1689
- }
1690
- } : void 0;
2008
+ const groupConfig = createGroupConfig(group);
1691
2009
  return {
1692
2010
  name: pluginClientName,
1693
2011
  options,
1694
- dependencies: [pluginTsName, parser === "zod" ? pluginZodName : void 0].filter(Boolean),
2012
+ dependencies: [pluginTsName, isParserEnabled(parser) ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
1695
2013
  hooks: { "kubb:plugin:setup"(ctx) {
1696
2014
  const resolver = userResolver ? {
1697
2015
  ...resolverClient,
@@ -1749,6 +2067,6 @@ const pluginClient = definePlugin((options) => {
1749
2067
  };
1750
2068
  });
1751
2069
  //#endregion
1752
- export { Client, classClientGenerator, clientGenerator, pluginClient as default, pluginClient, groupedClientGenerator, operationsGenerator, pluginClientName, resolverClient, staticClassClientGenerator };
2070
+ export { Client, classClientGenerator, clientGenerator, pluginClient as default, pluginClient, groupedClientGenerator, isParserEnabled, operationsGenerator, pluginClientName, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser, resolverClient, staticClassClientGenerator };
1753
2071
 
1754
2072
  //# sourceMappingURL=index.js.map