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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +24 -4
  2. package/dist/clients/axios.cjs +25 -3
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +9 -2
  5. package/dist/clients/axios.js +25 -3
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +20 -2
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +9 -2
  10. package/dist/clients/fetch.js +20 -2
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +524 -301
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +150 -84
  15. package/dist/index.js +525 -302
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.js +1 -1
  19. package/dist/templates/clients/fetch.source.cjs +1 -1
  20. package/dist/templates/clients/fetch.source.js +1 -1
  21. package/extension.yaml +1293 -0
  22. package/package.json +11 -17
  23. package/src/clients/axios.ts +41 -7
  24. package/src/clients/fetch.ts +30 -3
  25. package/src/components/ClassClient.tsx +17 -19
  26. package/src/components/Client.tsx +68 -51
  27. package/src/components/StaticClassClient.tsx +17 -19
  28. package/src/components/Url.tsx +7 -9
  29. package/src/components/WrapperClient.tsx +9 -5
  30. package/src/functionParams.ts +8 -8
  31. package/src/generators/classClientGenerator.tsx +40 -38
  32. package/src/generators/clientGenerator.tsx +32 -35
  33. package/src/generators/groupedClientGenerator.tsx +14 -8
  34. package/src/generators/operationsGenerator.tsx +12 -6
  35. package/src/generators/staticClassClientGenerator.tsx +34 -32
  36. package/src/plugin.ts +24 -11
  37. package/src/resolvers/resolverClient.ts +31 -8
  38. package/src/types.ts +90 -53
  39. package/src/utils.ts +30 -53
  40. package/templates/clients/axios.ts +0 -73
  41. package/templates/clients/fetch.ts +0 -96
  42. package/templates/config.ts +0 -43
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ 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
7
  import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
8
- import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
8
+ import { Const, File, Function, jsxRendererSync } from "@kubb/renderer-jsx";
9
9
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
10
10
  import { pluginZodName } from "@kubb/plugin-zod";
11
11
  //#region ../../internals/utils/src/casing.ts
@@ -185,6 +185,26 @@ function isValidVarName(name) {
185
185
  if (!name || reservedWords.has(name)) return false;
186
186
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
187
187
  }
188
+ /**
189
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
190
+ * otherwise prefixes it with `_` so the result is a valid identifier.
191
+ *
192
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
193
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
194
+ * variable, type, or function names.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * ensureValidVarName('409') // '_409'
199
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
200
+ * ensureValidVarName('Pet') // 'Pet'
201
+ * ensureValidVarName('class') // '_class'
202
+ * ```
203
+ */
204
+ function ensureValidVarName(name) {
205
+ if (!name || isValidVarName(name)) return name;
206
+ return `_${name}`;
207
+ }
188
208
  //#endregion
189
209
  //#region ../../internals/utils/src/urlPath.ts
190
210
  /**
@@ -251,16 +271,16 @@ var URLPath = class {
251
271
  get object() {
252
272
  return this.toObject();
253
273
  }
254
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
274
+ /** Returns a map of path parameter names, or `null` when the path has no parameters.
255
275
  *
256
276
  * @example
257
277
  * ```ts
258
278
  * new URLPath('/pet/{petId}').params // { petId: 'petId' }
259
- * new URLPath('/pet').params // undefined
279
+ * new URLPath('/pet').params // null
260
280
  * ```
261
281
  */
262
282
  get params() {
263
- return this.getParams();
283
+ return this.toParamsObject();
264
284
  }
265
285
  #transformParam(raw) {
266
286
  const param = isValidVarName(raw) ? raw : camelCase(raw);
@@ -278,7 +298,7 @@ var URLPath = class {
278
298
  toObject({ type = "path", replacer, stringify } = {}) {
279
299
  const object = {
280
300
  url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
281
- params: this.getParams()
301
+ params: this.toParamsObject()
282
302
  };
283
303
  if (stringify) {
284
304
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -294,12 +314,13 @@ var URLPath = class {
294
314
  * @example
295
315
  * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
296
316
  */
297
- toTemplateString({ prefix = "", replacer } = {}) {
298
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
317
+ toTemplateString({ prefix, replacer } = {}) {
318
+ const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
299
319
  if (i % 2 === 0) return part;
300
320
  const param = this.#transformParam(part);
301
321
  return `\${${replacer ? replacer(param) : param}}`;
302
- }).join("")}\``;
322
+ }).join("");
323
+ return `\`${prefix ?? ""}${result}\``;
303
324
  }
304
325
  /**
305
326
  * Extracts all `{param}` segments from the path and returns them as a key-value map.
@@ -308,17 +329,17 @@ var URLPath = class {
308
329
  *
309
330
  * @example
310
331
  * ```ts
311
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
332
+ * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
312
333
  * // { petId: 'petId', tagId: 'tagId' }
313
334
  * ```
314
335
  */
315
- getParams(replacer) {
336
+ toParamsObject(replacer) {
316
337
  const params = {};
317
338
  this.#eachParam((_raw, param) => {
318
339
  const key = replacer ? replacer(param) : param;
319
340
  params[key] = key;
320
341
  });
321
- return Object.keys(params).length > 0 ? params : void 0;
342
+ return Object.keys(params).length > 0 ? params : null;
322
343
  }
323
344
  /** Converts the OpenAPI path to Express-style colon syntax.
324
345
  *
@@ -332,6 +353,121 @@ var URLPath = class {
332
353
  }
333
354
  };
334
355
  //#endregion
356
+ //#region ../../internals/shared/src/operation.ts
357
+ function getOperationLink(node, link) {
358
+ if (!link) return null;
359
+ if (typeof link === "function") return link(node) ?? null;
360
+ if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
361
+ return `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`;
362
+ }
363
+ function getContentTypeInfo(node) {
364
+ const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
365
+ const isMultipleContentTypes = contentTypes.length > 1;
366
+ return {
367
+ contentTypes,
368
+ isMultipleContentTypes,
369
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
370
+ defaultContentType: contentTypes[0] ?? "application/json",
371
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
372
+ };
373
+ }
374
+ function buildRequestConfigType(node, resolver) {
375
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
376
+ const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
377
+ return `${requestName ? `Partial<RequestConfig<${requestName}>>` : "Partial<RequestConfig>"} & { ${["client?: Client", isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null].filter(Boolean).join("; ")} }`;
378
+ }
379
+ function buildOperationComments(node, options = {}) {
380
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
381
+ const linkComment = getOperationLink(node, link);
382
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
383
+ node.description && `@description ${node.description}`,
384
+ node.summary && `@summary ${node.summary}`,
385
+ linkComment,
386
+ node.deprecated && "@deprecated"
387
+ ] : [
388
+ node.description && `@description ${node.description}`,
389
+ node.summary && `@summary ${node.summary}`,
390
+ node.deprecated && "@deprecated",
391
+ linkComment
392
+ ]).filter((comment) => Boolean(comment));
393
+ if (!splitLines) return filteredComments;
394
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
395
+ }
396
+ function getOperationParameters(node, options = {}) {
397
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
398
+ return {
399
+ path: params.filter((param) => param.in === "path"),
400
+ query: params.filter((param) => param.in === "query"),
401
+ header: params.filter((param) => param.in === "header"),
402
+ cookie: params.filter((param) => param.in === "cookie")
403
+ };
404
+ }
405
+ function getStatusCodeNumber(statusCode) {
406
+ const code = Number(statusCode);
407
+ return Number.isNaN(code) ? null : code;
408
+ }
409
+ function isSuccessStatusCode(statusCode) {
410
+ const code = getStatusCodeNumber(statusCode);
411
+ return code !== null && code >= 200 && code < 300;
412
+ }
413
+ function isErrorStatusCode(statusCode) {
414
+ const code = getStatusCodeNumber(statusCode);
415
+ return code !== null && code >= 400;
416
+ }
417
+ function resolveErrorNames(node, resolver) {
418
+ return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
419
+ }
420
+ function resolveSuccessNames(node, resolver) {
421
+ return node.responses.filter((response) => isSuccessStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
422
+ }
423
+ function resolveStatusCodeNames(node, resolver) {
424
+ return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
425
+ }
426
+ const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
427
+ function resolveOperationTypeNames(node, resolver, options = {}) {
428
+ const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
429
+ let byResolver = typeNamesByResolver.get(resolver);
430
+ if (byResolver) {
431
+ const cached = byResolver.get(cacheKey);
432
+ if (cached) return cached;
433
+ } else {
434
+ byResolver = /* @__PURE__ */ new Map();
435
+ typeNamesByResolver.set(resolver, byResolver);
436
+ }
437
+ const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
438
+ const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
439
+ const exclude = new Set(options.exclude ?? []);
440
+ const paramNames = [
441
+ ...path.map((param) => resolver.resolvePathParamsName(node, param)),
442
+ ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
443
+ ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
444
+ ];
445
+ const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
446
+ const result = (options.order === "body-response-first" ? [
447
+ ...bodyAndResponseNames,
448
+ ...paramNames,
449
+ ...responseStatusNames
450
+ ] : [
451
+ ...paramNames,
452
+ ...bodyAndResponseNames,
453
+ ...responseStatusNames
454
+ ]).filter((name) => Boolean(name) && !exclude.has(name));
455
+ byResolver.set(cacheKey, result);
456
+ return result;
457
+ }
458
+ //#endregion
459
+ //#region ../../internals/shared/src/params.ts
460
+ function buildParamsMapping(originalParams, mappedParams) {
461
+ const mapping = {};
462
+ let hasChanged = false;
463
+ originalParams.forEach((param, i) => {
464
+ const mappedName = mappedParams[i]?.name ?? param.name;
465
+ mapping[param.name] = mappedName;
466
+ if (param.name !== mappedName) hasChanged = true;
467
+ });
468
+ return hasChanged ? mapping : null;
469
+ }
470
+ //#endregion
335
471
  //#region src/functionParams.ts
336
472
  const declarationPrinter$4 = functionPrinter({ mode: "declaration" });
337
473
  const callPrinter = functionPrinter({ mode: "call" });
@@ -342,18 +478,18 @@ function createType(type) {
342
478
  return type ? ast.createParamsType({
343
479
  variant: "reference",
344
480
  name: type
345
- }) : void 0;
481
+ }) : null;
346
482
  }
347
483
  function createDeclarationLeaf(name, spec) {
348
484
  if (spec.default !== void 0) return ast.createFunctionParameter({
349
485
  name,
350
- type: createType(spec.type),
486
+ type: createType(spec.type) ?? void 0,
351
487
  default: spec.default,
352
488
  rest: spec.mode === "inlineSpread"
353
489
  });
354
490
  return ast.createFunctionParameter({
355
491
  name,
356
- type: createType(spec.type),
492
+ type: createType(spec.type) ?? void 0,
357
493
  optional: !!spec.optional,
358
494
  rest: spec.mode === "inlineSpread"
359
495
  });
@@ -362,14 +498,14 @@ function createDeclarationParam(name, spec) {
362
498
  if (isGroup(spec)) return ast.createParameterGroup({
363
499
  inline: spec.mode === "inlineSpread",
364
500
  default: spec.default,
365
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => createDeclarationLeaf(childName, child))
501
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => createDeclarationLeaf(childName, child))
366
502
  });
367
503
  return createDeclarationLeaf(name, spec);
368
504
  }
369
505
  function createCallParam(name, spec) {
370
506
  if (isGroup(spec)) return ast.createParameterGroup({
371
507
  inline: spec.mode === "inlineSpread",
372
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => ast.createFunctionParameter({
508
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => ast.createFunctionParameter({
373
509
  name: child?.mode === "inlineSpread" ? spec.mode === "inlineSpread" ? child.value ?? childName : `...${child.value ?? childName}` : child?.value ? `${childName}: ${child.value}` : childName,
374
510
  rest: spec.mode === "inlineSpread" && child?.mode === "inlineSpread"
375
511
  }))
@@ -384,7 +520,7 @@ function createCallParam(name, spec) {
384
520
  * Returns utilities to output constructor signatures (`toConstructor()`) or call expressions (`toCall()`).
385
521
  */
386
522
  function createFunctionParams(params) {
387
- const entries = Object.entries(params).filter(([, spec]) => spec !== void 0);
523
+ const entries = Object.entries(params).filter(([, spec]) => spec != null);
388
524
  return {
389
525
  toConstructor() {
390
526
  return declarationPrinter$4.print(ast.createFunctionParameters({ params: entries.map(([name, spec]) => createDeclarationParam(name, spec)) })) ?? "";
@@ -395,106 +531,9 @@ function createFunctionParams(params) {
395
531
  };
396
532
  }
397
533
  //#endregion
398
- //#region src/utils.ts
399
- /**
400
- * Extracts documentation comments from an operation node.
401
- * Includes description, summary, link, and deprecation information.
402
- */
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));
410
- }
411
- /**
412
- * Builds a mapping of original parameter names to their transformed (cased) names.
413
- * Returns undefined if no names have changed.
414
- */
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;
424
- }
425
- /**
426
- * Builds HTTP headers array for a client request.
427
- * Includes Content-Type (if not default) and spreads header parameters if present.
428
- */
429
- function buildHeaders(contentType, hasHeaderParams) {
430
- return [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0, hasHeaderParams ? "...headers" : void 0].filter(Boolean);
431
- }
432
- /**
433
- * Builds TypeScript generic parameters for a client method.
434
- * Includes response type, error type, and optional request type.
435
- */
436
- function buildGenerics(node, tsResolver) {
437
- const responseName = tsResolver.resolveResponseName(node);
438
- const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0;
439
- const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
440
- return [
441
- responseName,
442
- `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
443
- requestName || "unknown"
444
- ].filter(Boolean);
445
- }
446
- /**
447
- * Builds the parameters object for a class-based client method.
448
- * Includes URL, method, base URL, headers, and request/response data.
449
- */
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;
453
- return createFunctionParams({ config: {
454
- mode: "object",
455
- children: {
456
- 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
463
- }
464
- } });
465
- }
466
- /**
467
- * Builds the request data parsing line for client methods.
468
- * Applies Zod validation if configured, otherwise uses data directly.
469
- */
470
- 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)`;
473
- if (node.requestBody?.content?.[0]?.schema) return "const requestData = data";
474
- return "";
475
- }
476
- /**
477
- * Builds the form data conversion line for file upload requests.
478
- * Returns empty string if not applicable.
479
- */
480
- function buildFormDataLine(isFormData, hasRequest) {
481
- return isFormData && hasRequest ? "const formData = buildFormData(requestData)" : "";
482
- }
483
- /**
484
- * Builds the return statement for a client method.
485
- * Applies Zod validation to response data if configured, otherwise returns raw response.
486
- */
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";
492
- return "return res.data";
493
- }
494
- //#endregion
495
534
  //#region src/components/Url.tsx
496
535
  const declarationPrinter$3 = functionPrinter({ mode: "declaration" });
497
- function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
536
+ function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
498
537
  const urlNode = {
499
538
  ...node,
500
539
  parameters: node.parameters.filter((p) => p.in === "path"),
@@ -507,10 +546,9 @@ function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolve
507
546
  resolver: tsResolver
508
547
  });
509
548
  }
510
- __name(getParams$1, "getParams");
511
549
  function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
512
550
  const path = new URLPath(node.path);
513
- const paramsNode = getParams$1({
551
+ const paramsNode = buildUrlParamsNode({
514
552
  paramsType,
515
553
  paramsCasing,
516
554
  pathParamsType,
@@ -518,9 +556,9 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
518
556
  tsResolver
519
557
  });
520
558
  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;
559
+ const { path: originalPathParams } = getOperationParameters(node);
560
+ const { path: casedPathParams } = getOperationParameters(node, { paramsCasing });
561
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null;
524
562
  return /* @__PURE__ */ jsx(File.Source, {
525
563
  name,
526
564
  isExportable,
@@ -542,56 +580,51 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
542
580
  })
543
581
  });
544
582
  }
545
- Url.getParams = getParams$1;
546
583
  //#endregion
547
584
  //#region src/components/Client.tsx
548
585
  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;
586
+ function buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable }) {
551
587
  return ast.createOperationParams(node, {
552
588
  paramsType,
553
589
  pathParamsType: paramsType === "object" ? "object" : pathParamsType === "object" ? "object" : "inline",
554
590
  paramsCasing,
555
591
  resolver: tsResolver,
556
- extraParams: isConfigurable ? [ast.createFunctionParameter({
592
+ extraParams: [...isConfigurable ? [ast.createFunctionParameter({
557
593
  name: "config",
558
594
  type: ast.createParamsType({
559
595
  variant: "reference",
560
- name: requestName ? `Partial<RequestConfig<${requestName}>> & { client?: Client }` : "Partial<RequestConfig> & { client?: Client }"
596
+ name: buildRequestConfigType(node, tsResolver)
561
597
  }),
562
598
  default: "{}"
563
- })] : []
599
+ })] : []]
564
600
  });
565
601
  }
566
602
  function Client({ name, isExportable = true, isIndexable = true, returnType, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType, node, tsResolver, zodResolver, urlName, children, isConfigurable = true }) {
567
603
  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;
604
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
605
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
606
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
607
+ const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing });
608
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null;
609
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null;
610
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null;
611
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
612
+ const successNames = resolveSuccessNames(node, tsResolver);
613
+ const responseName = successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
614
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]) : null;
615
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]) : null;
616
+ const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
617
+ const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
585
618
  const errorNames = node.responses.filter((r) => {
586
619
  return Number.parseInt(r.statusCode, 10) >= 400;
587
620
  }).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);
621
+ const headers = [!isMultipleContentTypes && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, headerParamsName ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null].filter(Boolean);
589
622
  const generics = [
590
623
  responseName,
591
624
  `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
592
625
  requestName || "unknown"
593
626
  ].filter(Boolean);
594
- const paramsNode = getParams({
627
+ const paramsNode = buildClientParamsNode({
595
628
  paramsType,
596
629
  paramsCasing,
597
630
  pathParamsType,
@@ -600,7 +633,7 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
600
633
  isConfigurable
601
634
  });
602
635
  const paramsSignature = declarationPrinter$2.print(paramsNode) ?? "";
603
- const urlParamsNode = Url.getParams({
636
+ const urlParamsNode = buildUrlParamsNode({
604
637
  paramsType,
605
638
  paramsCasing,
606
639
  pathParamsType,
@@ -613,11 +646,12 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
613
646
  children: {
614
647
  method: { value: JSON.stringify(node.method.toUpperCase()) },
615
648
  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
649
+ baseURL: baseURL && !urlName ? { value: `\`${baseURL}\`` } : null,
650
+ params: queryParamsName ? queryParamsMapping ? { value: "mappedParams" } : {} : null,
651
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
652
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
653
+ requestConfig: isConfigurable ? { mode: "inlineSpread" } : null,
654
+ headers: headers.length ? { value: isConfigurable ? `{ ${headers.join(", ")}, ...requestConfig.headers }` : `{ ${headers.join(", ")} }` } : null
621
655
  }
622
656
  } });
623
657
  const childrenElement = children ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -635,10 +669,14 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
635
669
  async: true,
636
670
  export: isExportable,
637
671
  params: paramsSignature,
638
- JSDoc: { comments: getComments(node) },
672
+ JSDoc: { comments: buildOperationComments(node, {
673
+ link: "urlPath",
674
+ linkPosition: "beforeDeprecated",
675
+ splitLines: true
676
+ }) },
639
677
  returnType,
640
678
  children: [
641
- isConfigurable ? "const { client: request = fetch, ...requestConfig } = config" : "",
679
+ isConfigurable ? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = config` : "",
642
680
  /* @__PURE__ */ jsx("br", {}),
643
681
  /* @__PURE__ */ jsx("br", {}),
644
682
  pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => isValidVarName(originalName) && originalName !== camelCaseName).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
@@ -655,26 +693,101 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
655
693
  ] }),
656
694
  parser === "zod" && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && "const requestData = data",
657
695
  /* @__PURE__ */ jsx("br", {}),
658
- isFormData && requestName && "const formData = buildFormData(requestData)",
696
+ (isFormData || isMultipleContentTypes && hasFormData) && requestName && "const formData = buildFormData(requestData)",
659
697
  /* @__PURE__ */ jsx("br", {}),
660
- isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await fetch<${generics.join(", ")}>(${clientParams.toCall()})`,
698
+ isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await client<${generics.join(", ")}>(${clientParams.toCall()})`,
661
699
  /* @__PURE__ */ jsx("br", {}),
662
700
  childrenElement
663
701
  ]
664
702
  })
665
703
  })] });
666
704
  }
667
- Client.getParams = getParams;
705
+ //#endregion
706
+ //#region src/utils.ts
707
+ /**
708
+ * Builds HTTP headers array for a client request.
709
+ * Includes Content-Type (if not default) and spreads header parameters if present.
710
+ */
711
+ function buildHeaders(contentType, hasHeaderParams) {
712
+ return [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, hasHeaderParams ? "...headers" : null].filter(Boolean);
713
+ }
714
+ /**
715
+ * Builds TypeScript generic parameters for a client method.
716
+ * Includes response type, error type, and optional request type.
717
+ */
718
+ function buildGenerics(node, tsResolver) {
719
+ const successNames = resolveSuccessNames(node, tsResolver);
720
+ const responseName = successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
721
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
722
+ const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
723
+ return [
724
+ responseName,
725
+ `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
726
+ requestName || "unknown"
727
+ ].filter(Boolean);
728
+ }
729
+ /**
730
+ * Builds the parameters object for a class-based client method.
731
+ * Includes URL, method, base URL, headers, and request/response data.
732
+ */
733
+ function buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers }) {
734
+ const { query: queryParams } = getOperationParameters(node);
735
+ const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]) : null;
736
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
737
+ return createFunctionParams({ config: {
738
+ mode: "object",
739
+ children: {
740
+ requestConfig: { mode: "inlineSpread" },
741
+ method: { value: JSON.stringify(node.method.toUpperCase()) },
742
+ url: { value: path.template },
743
+ baseURL: baseURL ? { value: JSON.stringify(baseURL) } : null,
744
+ params: queryParamsName ? {} : null,
745
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
746
+ contentType: isMultipleContentTypes ? {} : null,
747
+ headers: headers.length ? { value: `{ ${headers.join(", ")}, ...requestConfig.headers }` } : null
748
+ }
749
+ } });
750
+ }
751
+ /**
752
+ * Builds the request data parsing line for client methods.
753
+ * Applies Zod validation if configured, otherwise uses data directly.
754
+ */
755
+ function buildRequestDataLine({ parser, node, zodResolver }) {
756
+ const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
757
+ if (parser === "zod" && zodRequestName) return `const requestData = ${zodRequestName}.parse(data)`;
758
+ if (node.requestBody?.content?.[0]?.schema) return "const requestData = data";
759
+ return "";
760
+ }
761
+ /**
762
+ * Builds the form data conversion line for file upload requests.
763
+ * Returns empty string if not applicable.
764
+ */
765
+ function buildFormDataLine(isFormData, hasRequest) {
766
+ return isFormData && hasRequest ? "const formData = buildFormData(requestData)" : "";
767
+ }
768
+ /**
769
+ * Builds the return statement for a client method.
770
+ * Applies Zod validation to response data if configured, otherwise returns raw response.
771
+ */
772
+ function buildReturnStatement({ dataReturnType, parser, node, zodResolver }) {
773
+ const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
774
+ if (dataReturnType === "full" && parser === "zod" && zodResponseName) return `return {...res, data: ${zodResponseName}.parse(res.data)}`;
775
+ if (dataReturnType === "data" && parser === "zod" && zodResponseName) return `return ${zodResponseName}.parse(res.data)`;
776
+ if (dataReturnType === "full" && parser === "client") return "return res";
777
+ return "return res.data";
778
+ }
668
779
  //#endregion
669
780
  //#region src/components/ClassClient.tsx
670
781
  const declarationPrinter$1 = functionPrinter({ mode: "declaration" });
671
782
  function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
672
783
  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));
784
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
785
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
786
+ const { header: headerParams } = getOperationParameters(node);
787
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
788
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
676
789
  const generics = buildGenerics(node, tsResolver);
677
- const paramsNode = ClassClient.getParams({
790
+ const paramsNode = buildClientParamsNode({
678
791
  paramsType,
679
792
  paramsCasing,
680
793
  pathParamsType,
@@ -689,15 +802,21 @@ function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataRe
689
802
  baseURL,
690
803
  tsResolver,
691
804
  isFormData,
805
+ isMultipleContentTypes,
806
+ hasFormData,
692
807
  headers
693
808
  });
694
- const jsdoc = buildJSDoc(getComments(node));
809
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
810
+ link: "urlPath",
811
+ linkPosition: "beforeDeprecated",
812
+ splitLines: true
813
+ }));
695
814
  const requestDataLine = buildRequestDataLine({
696
815
  parser,
697
816
  node,
698
817
  zodResolver
699
818
  });
700
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
819
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
701
820
  const returnStatement = buildReturnStatement({
702
821
  dataReturnType,
703
822
  parser,
@@ -705,7 +824,7 @@ function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataRe
705
824
  zodResolver
706
825
  });
707
826
  return `${jsdoc}async ${name}(${paramsSignature}) {\n${[
708
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
827
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
709
828
  "",
710
829
  requestDataLine,
711
830
  formDataLine,
@@ -742,15 +861,14 @@ ${operations.map(({ node, name: methodName, tsResolver, zodResolver }) => genera
742
861
  children: [classCode, children]
743
862
  });
744
863
  }
745
- ClassClient.getParams = Client.getParams;
746
864
  //#endregion
747
865
  //#region src/components/WrapperClient.tsx
748
- function WrapperClient({ name, classNames, isExportable = true, isIndexable = true }) {
866
+ function WrapperClient({ name, controllers, isExportable = true, isIndexable = true }) {
749
867
  const classCode = `export class ${name} {
750
- ${classNames.map((className) => ` readonly ${camelCase(className)}: ${className}`).join("\n")}
868
+ ${controllers.map(({ className, propertyName }) => ` readonly ${propertyName}: ${className}`).join("\n")}
751
869
 
752
870
  constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
753
- ${classNames.map((className) => ` this.${camelCase(className)} = new ${className}(config)`).join("\n")}
871
+ ${controllers.map(({ className, propertyName }) => ` this.${propertyName} = new ${className}(config)`).join("\n")}
754
872
  }
755
873
  }`;
756
874
  return /* @__PURE__ */ jsx(File.Source, {
@@ -763,33 +881,31 @@ ${classNames.map((className) => ` this.${camelCase(className)} = new ${classN
763
881
  //#endregion
764
882
  //#region src/generators/classClientGenerator.tsx
765
883
  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));
884
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
774
885
  }
775
886
  __name(resolveTypeImportNames$1, "resolveTypeImportNames");
776
887
  function resolveZodImportNames$1(node, zodResolver) {
777
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
888
+ return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((n) => Boolean(n));
778
889
  }
779
890
  __name(resolveZodImportNames$1, "resolveZodImportNames");
891
+ /**
892
+ * Built-in `operations` generator for `@kubb/plugin-client` when
893
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
894
+ * per operation and a shared constructor for request configuration.
895
+ */
780
896
  const classClientGenerator = defineGenerator({
781
897
  name: "classClient",
782
- renderer: jsxRenderer,
898
+ renderer: jsxRendererSync,
783
899
  operations(nodes, ctx) {
784
- const { adapter, config, driver, resolver, root } = ctx;
900
+ const { config, driver, resolver, root } = ctx;
785
901
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options;
786
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
902
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
787
903
  const pluginTs = driver.getPlugin(pluginTsName);
788
904
  if (!pluginTs) return null;
789
905
  const tsResolver = driver.getResolver(pluginTsName);
790
906
  const tsPluginOptions = pluginTs.options;
791
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
792
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
907
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
908
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
793
909
  function buildOperationData(node) {
794
910
  const typeFile = tsResolver.resolveFile({
795
911
  name: node.operationId,
@@ -809,8 +925,8 @@ const classClientGenerator = defineGenerator({
809
925
  }, {
810
926
  root,
811
927
  output: pluginZod.options?.output ?? output,
812
- group: pluginZod.options?.group
813
- }) : void 0;
928
+ group: pluginZod.options?.group ?? void 0
929
+ }) : null;
814
930
  return {
815
931
  node,
816
932
  name: resolver.resolveName(node.operationId),
@@ -822,26 +938,29 @@ const classClientGenerator = defineGenerator({
822
938
  }
823
939
  const controllers = nodes.reduce((acc, operationNode) => {
824
940
  const tag = operationNode.tags[0];
825
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
941
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
826
942
  if (!tag && !group) {
827
- const name = "ApiClient";
943
+ const name = resolver.resolveClassName("ApiClient");
828
944
  const file = resolver.resolveFile({
829
945
  name,
830
946
  extname: ".ts"
831
947
  }, {
832
948
  root,
833
949
  output,
834
- group
950
+ group: group ?? void 0
835
951
  });
836
952
  const operationData = buildOperationData(operationNode);
837
953
  const previous = acc.find((item) => item.file.path === file.path);
838
954
  if (previous) previous.operations.push(operationData);
839
955
  else acc.push({
840
956
  name,
957
+ propertyName: resolver.resolveClientPropertyName(name),
841
958
  file,
842
959
  operations: [operationData]
843
960
  });
844
- } else if (tag) {
961
+ return acc;
962
+ }
963
+ if (tag) {
845
964
  const name = groupName;
846
965
  const file = resolver.resolveFile({
847
966
  name,
@@ -850,13 +969,14 @@ const classClientGenerator = defineGenerator({
850
969
  }, {
851
970
  root,
852
971
  output,
853
- group
972
+ group: group ?? void 0
854
973
  });
855
974
  const operationData = buildOperationData(operationNode);
856
975
  const previous = acc.find((item) => item.file.path === file.path);
857
976
  if (previous) previous.operations.push(operationData);
858
977
  else acc.push({
859
978
  name,
979
+ propertyName: resolver.resolveClientPropertyName(name),
860
980
  file,
861
981
  operations: [operationData]
862
982
  });
@@ -904,23 +1024,31 @@ const classClientGenerator = defineGenerator({
904
1024
  zodImportsByFile: /* @__PURE__ */ new Map(),
905
1025
  zodFilesByPath: /* @__PURE__ */ new Map()
906
1026
  };
907
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1027
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
908
1028
  return /* @__PURE__ */ jsxs(File, {
909
1029
  baseName: file.baseName,
910
1030
  path: file.path,
911
1031
  meta: file.meta,
912
- banner: resolver.resolveBanner(adapter.inputNode, {
1032
+ banner: resolver.resolveBanner(ctx.meta, {
913
1033
  output,
914
- config
1034
+ config,
1035
+ file: {
1036
+ path: file.path,
1037
+ baseName: file.baseName
1038
+ }
915
1039
  }),
916
- footer: resolver.resolveFooter(adapter.inputNode, {
1040
+ footer: resolver.resolveFooter(ctx.meta, {
917
1041
  output,
918
- config
1042
+ config,
1043
+ file: {
1044
+ path: file.path,
1045
+ baseName: file.baseName
1046
+ }
919
1047
  }),
920
1048
  children: [
921
1049
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
922
1050
  /* @__PURE__ */ jsx(File.Import, {
923
- name: "fetch",
1051
+ name: "client",
924
1052
  path: importPath
925
1053
  }),
926
1054
  /* @__PURE__ */ jsx(File.Import, {
@@ -938,7 +1066,7 @@ const classClientGenerator = defineGenerator({
938
1066
  })
939
1067
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
940
1068
  /* @__PURE__ */ jsx(File.Import, {
941
- name: ["fetch"],
1069
+ name: ["client"],
942
1070
  root: file.path,
943
1071
  path: path.resolve(root, ".kubb/client.ts")
944
1072
  }),
@@ -1006,19 +1134,27 @@ const classClientGenerator = defineGenerator({
1006
1134
  }, {
1007
1135
  root,
1008
1136
  output,
1009
- group
1137
+ group: group ?? void 0
1010
1138
  });
1011
1139
  files.push(/* @__PURE__ */ jsxs(File, {
1012
1140
  baseName: sdkFile.baseName,
1013
1141
  path: sdkFile.path,
1014
1142
  meta: sdkFile.meta,
1015
- banner: resolver.resolveBanner(adapter.inputNode, {
1143
+ banner: resolver.resolveBanner(ctx.meta, {
1016
1144
  output,
1017
- config
1145
+ config,
1146
+ file: {
1147
+ path: sdkFile.path,
1148
+ baseName: sdkFile.baseName
1149
+ }
1018
1150
  }),
1019
- footer: resolver.resolveFooter(adapter.inputNode, {
1151
+ footer: resolver.resolveFooter(ctx.meta, {
1020
1152
  output,
1021
- config
1153
+ config,
1154
+ file: {
1155
+ path: sdkFile.path,
1156
+ baseName: sdkFile.baseName
1157
+ }
1022
1158
  }),
1023
1159
  children: [
1024
1160
  importPath ? /* @__PURE__ */ jsx(File.Import, {
@@ -1038,7 +1174,10 @@ const classClientGenerator = defineGenerator({
1038
1174
  }, name)),
1039
1175
  /* @__PURE__ */ jsx(WrapperClient, {
1040
1176
  name: sdk.className,
1041
- classNames: controllers.map(({ name }) => name)
1177
+ controllers: controllers.map(({ name, propertyName }) => ({
1178
+ className: name,
1179
+ propertyName
1180
+ }))
1042
1181
  })
1043
1182
  ]
1044
1183
  }, sdkFile.path));
@@ -1048,34 +1187,28 @@ const classClientGenerator = defineGenerator({
1048
1187
  });
1049
1188
  //#endregion
1050
1189
  //#region src/generators/clientGenerator.tsx
1190
+ /**
1191
+ * Built-in operation generator for `@kubb/plugin-client`. Emits one async
1192
+ * function per OpenAPI operation, plus the matching URL helper. Used when
1193
+ * `clientType: 'function'` (the default).
1194
+ */
1051
1195
  const clientGenerator = defineGenerator({
1052
1196
  name: "client",
1053
- renderer: jsxRenderer,
1197
+ renderer: jsxRendererSync,
1054
1198
  operation(node, ctx) {
1055
- const { adapter, config, driver, resolver, root } = ctx;
1199
+ const { config, driver, resolver, root } = ctx;
1056
1200
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options;
1057
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1201
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1058
1202
  const pluginTs = driver.getPlugin(pluginTsName);
1059
1203
  if (!pluginTs) return null;
1060
1204
  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) : [];
1205
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
1206
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1207
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing });
1208
+ const importedZodNames = zodResolver && parser === "zod" ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((name) => Boolean(name)) : [];
1076
1209
  const meta = {
1077
1210
  name: resolver.resolveName(node.operationId),
1078
- urlName: `get${resolver.resolveName(node.operationId).charAt(0).toUpperCase()}${resolver.resolveName(node.operationId).slice(1)}Url`,
1211
+ urlName: resolver.resolveUrlName(node),
1079
1212
  file: resolver.resolveFile({
1080
1213
  name: node.operationId,
1081
1214
  extname: ".ts",
@@ -1084,7 +1217,7 @@ const clientGenerator = defineGenerator({
1084
1217
  }, {
1085
1218
  root,
1086
1219
  output,
1087
- group
1220
+ group: group ?? void 0
1088
1221
  }),
1089
1222
  fileTs: tsResolver.resolveFile({
1090
1223
  name: node.operationId,
@@ -1094,7 +1227,7 @@ const clientGenerator = defineGenerator({
1094
1227
  }, {
1095
1228
  root,
1096
1229
  output: pluginTs.options?.output ?? output,
1097
- group: pluginTs.options?.group
1230
+ group: pluginTs.options?.group ?? void 0
1098
1231
  }),
1099
1232
  fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile({
1100
1233
  name: node.operationId,
@@ -1104,25 +1237,33 @@ const clientGenerator = defineGenerator({
1104
1237
  }, {
1105
1238
  root,
1106
1239
  output: pluginZod.options.output ?? output,
1107
- group: pluginZod.options?.group
1108
- }) : void 0
1240
+ group: pluginZod.options?.group ?? void 0
1241
+ }) : null
1109
1242
  };
1110
- const isFormData = node.requestBody?.content?.[0]?.contentType === "multipart/form-data";
1243
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false;
1111
1244
  return /* @__PURE__ */ jsxs(File, {
1112
1245
  baseName: meta.file.baseName,
1113
1246
  path: meta.file.path,
1114
1247
  meta: meta.file.meta,
1115
- banner: resolver.resolveBanner(adapter.inputNode, {
1248
+ banner: resolver.resolveBanner(ctx.meta, {
1116
1249
  output,
1117
- config
1250
+ config,
1251
+ file: {
1252
+ path: meta.file.path,
1253
+ baseName: meta.file.baseName
1254
+ }
1118
1255
  }),
1119
- footer: resolver.resolveFooter(adapter.inputNode, {
1256
+ footer: resolver.resolveFooter(ctx.meta, {
1120
1257
  output,
1121
- config
1258
+ config,
1259
+ file: {
1260
+ path: meta.file.path,
1261
+ baseName: meta.file.baseName
1262
+ }
1122
1263
  }),
1123
1264
  children: [
1124
1265
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1125
- name: "fetch",
1266
+ name: "client",
1126
1267
  path: importPath
1127
1268
  }), /* @__PURE__ */ jsx(File.Import, {
1128
1269
  name: [
@@ -1133,7 +1274,7 @@ const clientGenerator = defineGenerator({
1133
1274
  path: importPath,
1134
1275
  isTypeOnly: true
1135
1276
  })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1136
- name: ["fetch"],
1277
+ name: ["client"],
1137
1278
  root: meta.file.path,
1138
1279
  path: path.resolve(root, ".kubb/client.ts")
1139
1280
  }), /* @__PURE__ */ jsx(File.Import, {
@@ -1146,7 +1287,7 @@ const clientGenerator = defineGenerator({
1146
1287
  path: path.resolve(root, ".kubb/client.ts"),
1147
1288
  isTypeOnly: true
1148
1289
  })] }),
1149
- isFormData && node.requestBody?.content?.[0]?.schema && /* @__PURE__ */ jsx(File.Import, {
1290
+ hasFormData && /* @__PURE__ */ jsx(File.Import, {
1150
1291
  name: ["buildFormData"],
1151
1292
  root: meta.file.path,
1152
1293
  path: path.resolve(root, ".kubb/config.ts")
@@ -1192,16 +1333,22 @@ const clientGenerator = defineGenerator({
1192
1333
  });
1193
1334
  //#endregion
1194
1335
  //#region src/generators/groupedClientGenerator.tsx
1336
+ /**
1337
+ * Emits one aggregate file per tag/group when `group` is configured. Each
1338
+ * file re-exports every client function for that group, so callers can
1339
+ * `import { petController } from './gen/clients'` instead of importing
1340
+ * each operation individually.
1341
+ */
1195
1342
  const groupedClientGenerator = defineGenerator({
1196
1343
  name: "groupedClient",
1197
- renderer: jsxRenderer,
1344
+ renderer: jsxRendererSync,
1198
1345
  operations(nodes, ctx) {
1199
- const { config, resolver, adapter, root } = ctx;
1346
+ const { config, resolver, root } = ctx;
1200
1347
  const { output, group } = ctx.options;
1201
1348
  return /* @__PURE__ */ jsx(Fragment, { children: nodes.reduce((acc, operationNode) => {
1202
1349
  if (group?.type === "tag") {
1203
1350
  const tag = operationNode.tags[0];
1204
- const name = tag ? group?.name?.({ group: camelCase(tag) }) : void 0;
1351
+ const name = tag ? group?.name?.({ group: camelCase(tag) }) : null;
1205
1352
  if (!tag || !name) return acc;
1206
1353
  const file = resolver.resolveFile({
1207
1354
  name,
@@ -1210,7 +1357,7 @@ const groupedClientGenerator = defineGenerator({
1210
1357
  }, {
1211
1358
  root,
1212
1359
  output,
1213
- group
1360
+ group: group ?? void 0
1214
1361
  });
1215
1362
  const clientFile = resolver.resolveFile({
1216
1363
  name: operationNode.operationId,
@@ -1220,7 +1367,7 @@ const groupedClientGenerator = defineGenerator({
1220
1367
  }, {
1221
1368
  root,
1222
1369
  output,
1223
- group
1370
+ group: group ?? void 0
1224
1371
  });
1225
1372
  const client = {
1226
1373
  name: resolver.resolveName(operationNode.operationId),
@@ -1240,13 +1387,23 @@ const groupedClientGenerator = defineGenerator({
1240
1387
  baseName: file.baseName,
1241
1388
  path: file.path,
1242
1389
  meta: file.meta,
1243
- banner: resolver.resolveBanner(adapter.inputNode, {
1390
+ banner: resolver.resolveBanner(ctx.meta, {
1244
1391
  output,
1245
- config
1392
+ config,
1393
+ file: {
1394
+ path: file.path,
1395
+ baseName: file.baseName,
1396
+ isAggregation: true
1397
+ }
1246
1398
  }),
1247
- footer: resolver.resolveFooter(adapter.inputNode, {
1399
+ footer: resolver.resolveFooter(ctx.meta, {
1248
1400
  output,
1249
- config
1401
+ config,
1402
+ file: {
1403
+ path: file.path,
1404
+ baseName: file.baseName,
1405
+ isAggregation: true
1406
+ }
1250
1407
  }),
1251
1408
  children: [clients.map((client) => /* @__PURE__ */ jsx(File.Import, {
1252
1409
  name: [client.name],
@@ -1289,11 +1446,17 @@ function Operations({ name, nodes }) {
1289
1446
  }
1290
1447
  //#endregion
1291
1448
  //#region src/generators/operationsGenerator.tsx
1449
+ /**
1450
+ * Generates an `operations.ts` file that re-exports every operation grouped
1451
+ * by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
1452
+ * for building meta-tooling on top of the generated client (route
1453
+ * registries, API explorers).
1454
+ */
1292
1455
  const operationsGenerator = defineGenerator({
1293
1456
  name: "client",
1294
- renderer: jsxRenderer,
1457
+ renderer: jsxRendererSync,
1295
1458
  operations(nodes, ctx) {
1296
- const { config, resolver, adapter, root } = ctx;
1459
+ const { config, resolver, root } = ctx;
1297
1460
  const { output, group } = ctx.options;
1298
1461
  const name = "operations";
1299
1462
  const file = resolver.resolveFile({
@@ -1302,19 +1465,27 @@ const operationsGenerator = defineGenerator({
1302
1465
  }, {
1303
1466
  root,
1304
1467
  output,
1305
- group
1468
+ group: group ?? void 0
1306
1469
  });
1307
1470
  return /* @__PURE__ */ jsx(File, {
1308
1471
  baseName: file.baseName,
1309
1472
  path: file.path,
1310
1473
  meta: file.meta,
1311
- banner: resolver.resolveBanner(adapter.inputNode, {
1474
+ banner: resolver.resolveBanner(ctx.meta, {
1312
1475
  output,
1313
- config
1476
+ config,
1477
+ file: {
1478
+ path: file.path,
1479
+ baseName: file.baseName
1480
+ }
1314
1481
  }),
1315
- footer: resolver.resolveFooter(adapter.inputNode, {
1482
+ footer: resolver.resolveFooter(ctx.meta, {
1316
1483
  output,
1317
- config
1484
+ config,
1485
+ file: {
1486
+ path: file.path,
1487
+ baseName: file.baseName
1488
+ }
1318
1489
  }),
1319
1490
  children: /* @__PURE__ */ jsx(Operations, {
1320
1491
  name,
@@ -1328,11 +1499,13 @@ const operationsGenerator = defineGenerator({
1328
1499
  const declarationPrinter = functionPrinter({ mode: "declaration" });
1329
1500
  function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
1330
1501
  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));
1502
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
1503
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
1504
+ const { header: headerParams } = getOperationParameters(node);
1505
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
1506
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
1334
1507
  const generics = buildGenerics(node, tsResolver);
1335
- const paramsNode = Client.getParams({
1508
+ const paramsNode = buildClientParamsNode({
1336
1509
  paramsType,
1337
1510
  paramsCasing,
1338
1511
  pathParamsType,
@@ -1347,15 +1520,21 @@ function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataRetu
1347
1520
  baseURL,
1348
1521
  tsResolver,
1349
1522
  isFormData,
1523
+ isMultipleContentTypes,
1524
+ hasFormData,
1350
1525
  headers
1351
1526
  });
1352
- const jsdoc = buildJSDoc(getComments(node));
1527
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
1528
+ link: "urlPath",
1529
+ linkPosition: "beforeDeprecated",
1530
+ splitLines: true
1531
+ }));
1353
1532
  const requestDataLine = buildRequestDataLine({
1354
1533
  parser,
1355
1534
  node,
1356
1535
  zodResolver
1357
1536
  });
1358
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
1537
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
1359
1538
  const returnStatement = buildReturnStatement({
1360
1539
  dataReturnType,
1361
1540
  parser,
@@ -1363,7 +1542,7 @@ function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataRetu
1363
1542
  zodResolver
1364
1543
  });
1365
1544
  return `${jsdoc} static async ${name}(${paramsSignature}) {\n${[
1366
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
1545
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
1367
1546
  "",
1368
1547
  requestDataLine,
1369
1548
  formDataLine,
@@ -1391,35 +1570,33 @@ function StaticClassClient({ name, isExportable = true, isIndexable = true, oper
1391
1570
  children: [classCode, children]
1392
1571
  });
1393
1572
  }
1394
- StaticClassClient.getParams = Client.getParams;
1395
1573
  //#endregion
1396
1574
  //#region src/generators/staticClassClientGenerator.tsx
1397
1575
  function resolveTypeImportNames(node, tsResolver) {
1398
- 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))
1405
- ].filter((n) => Boolean(n));
1576
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
1406
1577
  }
1407
1578
  function resolveZodImportNames(node, zodResolver) {
1408
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
1579
+ return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((n) => Boolean(n));
1409
1580
  }
1581
+ /**
1582
+ * Built-in `operations` generator for `@kubb/plugin-client` when
1583
+ * `clientType: 'staticClass'`. Emits one class per tag, with a static method
1584
+ * per operation so callers can use `Pet.getPetById(...)` without
1585
+ * instantiating the class.
1586
+ */
1410
1587
  const staticClassClientGenerator = defineGenerator({
1411
1588
  name: "staticClassClient",
1412
- renderer: jsxRenderer,
1589
+ renderer: jsxRendererSync,
1413
1590
  operations(nodes, ctx) {
1414
- const { adapter, config, driver, resolver, root } = ctx;
1591
+ const { config, driver, resolver, root } = ctx;
1415
1592
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath } = ctx.options;
1416
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1593
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1417
1594
  const pluginTs = driver.getPlugin(pluginTsName);
1418
1595
  if (!pluginTs) return null;
1419
1596
  const tsResolver = driver.getResolver(pluginTsName);
1420
1597
  const tsPluginOptions = pluginTs.options;
1421
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
1422
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
1598
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
1599
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1423
1600
  function buildOperationData(node) {
1424
1601
  const typeFile = tsResolver.resolveFile({
1425
1602
  name: node.operationId,
@@ -1439,8 +1616,8 @@ const staticClassClientGenerator = defineGenerator({
1439
1616
  }, {
1440
1617
  root,
1441
1618
  output: pluginZod.options?.output ?? output,
1442
- group: pluginZod.options?.group
1443
- }) : void 0;
1619
+ group: pluginZod.options?.group ?? void 0
1620
+ }) : null;
1444
1621
  return {
1445
1622
  node,
1446
1623
  name: resolver.resolveName(node.operationId),
@@ -1452,16 +1629,16 @@ const staticClassClientGenerator = defineGenerator({
1452
1629
  }
1453
1630
  const controllers = nodes.reduce((acc, operationNode) => {
1454
1631
  const tag = operationNode.tags[0];
1455
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
1632
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
1456
1633
  if (!tag && !group) {
1457
- const name = "ApiClient";
1634
+ const name = resolver.resolveClassName("ApiClient");
1458
1635
  const file = resolver.resolveFile({
1459
1636
  name,
1460
1637
  extname: ".ts"
1461
1638
  }, {
1462
1639
  root,
1463
1640
  output,
1464
- group
1641
+ group: group ?? void 0
1465
1642
  });
1466
1643
  const operationData = buildOperationData(operationNode);
1467
1644
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1471,7 +1648,9 @@ const staticClassClientGenerator = defineGenerator({
1471
1648
  file,
1472
1649
  operations: [operationData]
1473
1650
  });
1474
- } else if (tag) {
1651
+ return acc;
1652
+ }
1653
+ if (tag) {
1475
1654
  const name = groupName;
1476
1655
  const file = resolver.resolveFile({
1477
1656
  name,
@@ -1480,7 +1659,7 @@ const staticClassClientGenerator = defineGenerator({
1480
1659
  }, {
1481
1660
  root,
1482
1661
  output,
1483
- group
1662
+ group: group ?? void 0
1484
1663
  });
1485
1664
  const operationData = buildOperationData(operationNode);
1486
1665
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1534,23 +1713,31 @@ const staticClassClientGenerator = defineGenerator({
1534
1713
  zodImportsByFile: /* @__PURE__ */ new Map(),
1535
1714
  zodFilesByPath: /* @__PURE__ */ new Map()
1536
1715
  };
1537
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1716
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
1538
1717
  return /* @__PURE__ */ jsxs(File, {
1539
1718
  baseName: file.baseName,
1540
1719
  path: file.path,
1541
1720
  meta: file.meta,
1542
- banner: resolver.resolveBanner(adapter.inputNode, {
1721
+ banner: resolver.resolveBanner(ctx.meta, {
1543
1722
  output,
1544
- config
1723
+ config,
1724
+ file: {
1725
+ path: file.path,
1726
+ baseName: file.baseName
1727
+ }
1545
1728
  }),
1546
- footer: resolver.resolveFooter(adapter.inputNode, {
1729
+ footer: resolver.resolveFooter(ctx.meta, {
1547
1730
  output,
1548
- config
1731
+ config,
1732
+ file: {
1733
+ path: file.path,
1734
+ baseName: file.baseName
1735
+ }
1549
1736
  }),
1550
1737
  children: [
1551
1738
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
1552
1739
  /* @__PURE__ */ jsx(File.Import, {
1553
- name: "fetch",
1740
+ name: "client",
1554
1741
  path: importPath
1555
1742
  }),
1556
1743
  /* @__PURE__ */ jsx(File.Import, {
@@ -1568,7 +1755,7 @@ const staticClassClientGenerator = defineGenerator({
1568
1755
  })
1569
1756
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1570
1757
  /* @__PURE__ */ jsx(File.Import, {
1571
- name: ["fetch"],
1758
+ name: ["client"],
1572
1759
  root: file.path,
1573
1760
  path: path.resolve(root, ".kubb/client.ts")
1574
1761
  }),
@@ -1634,39 +1821,75 @@ const staticClassClientGenerator = defineGenerator({
1634
1821
  //#endregion
1635
1822
  //#region src/resolvers/resolverClient.ts
1636
1823
  /**
1637
- * Naming convention resolver for client plugin.
1824
+ * Default resolver used by `@kubb/plugin-client`. Decides the names and file
1825
+ * paths for every generated client function or class. Functions and files use
1826
+ * camelCase; classes and tag groups use PascalCase.
1638
1827
  *
1639
- * Provides default naming helpers using camelCase for functions and file paths.
1828
+ * @example Resolve client function and class names
1829
+ * ```ts
1830
+ * import { resolverClient } from '@kubb/plugin-client'
1640
1831
  *
1641
- * @example
1642
- * `resolverClient.default('list pets', 'function') // 'listPets'`
1832
+ * resolverClient.default('list pets', 'function') // 'listPets'
1833
+ * resolverClient.resolveClassName('pet') // 'Pet'
1834
+ * resolverClient.resolveUrlName(operationNode) // 'getShowPetByIdUrl'
1835
+ * ```
1643
1836
  */
1644
- const resolverClient = defineResolver((ctx) => ({
1837
+ const resolverClient = defineResolver(() => ({
1645
1838
  name: "default",
1646
1839
  pluginName: "plugin-client",
1647
1840
  default(name, type) {
1648
- return camelCase(name, { isFile: type === "file" });
1841
+ const resolved = camelCase(name, { isFile: type === "file" });
1842
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1649
1843
  },
1650
1844
  resolveName(name) {
1651
- return ctx.default(name, "function");
1845
+ return this.default(name, "function");
1846
+ },
1847
+ resolvePathName(name, type) {
1848
+ return this.default(name, type);
1849
+ },
1850
+ resolveClassName(name) {
1851
+ return ensureValidVarName(pascalCase(name));
1852
+ },
1853
+ resolveGroupName(name) {
1854
+ return ensureValidVarName(pascalCase(name));
1855
+ },
1856
+ resolveClientPropertyName(name) {
1857
+ return ensureValidVarName(camelCase(name));
1858
+ },
1859
+ resolveUrlName(node) {
1860
+ const name = this.resolveName(node.operationId);
1861
+ return `get${name.charAt(0).toUpperCase()}${name.slice(1)}Url`;
1652
1862
  }
1653
1863
  }));
1654
1864
  //#endregion
1655
1865
  //#region src/plugin.ts
1656
1866
  /**
1657
- * Canonical plugin name for `@kubb/plugin-client`, used in driver lookups and warnings.
1867
+ * Canonical plugin name for `@kubb/plugin-client`. Used for driver lookups and
1868
+ * cross-plugin dependency references.
1658
1869
  */
1659
1870
  const pluginClientName = "plugin-client";
1660
1871
  /**
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`.
1872
+ * Generates one HTTP client function per OpenAPI operation. Each function has
1873
+ * typed path params, query params, body, and response, so callers use the API
1874
+ * like any other typed function. Ships with `axios` and `fetch` runtimes; bring
1875
+ * your own by setting `importPath`.
1664
1876
  *
1665
- * @example Client generator
1877
+ * @example
1666
1878
  * ```ts
1667
- * import pluginClient from '@kubb/plugin-client'
1879
+ * import { defineConfig } from 'kubb'
1880
+ * import { pluginTs } from '@kubb/plugin-ts'
1881
+ * import { pluginClient } from '@kubb/plugin-client'
1882
+ *
1668
1883
  * export default defineConfig({
1669
- * plugins: [pluginClient({ output: { path: 'clients' } })]
1884
+ * input: { path: './petStore.yaml' },
1885
+ * output: { path: './src/gen' },
1886
+ * plugins: [
1887
+ * pluginTs(),
1888
+ * pluginClient({
1889
+ * output: { path: './clients' },
1890
+ * client: 'fetch',
1891
+ * }),
1892
+ * ],
1670
1893
  * })
1671
1894
  * ```
1672
1895
  */
@@ -1678,8 +1901,8 @@ const pluginClient = definePlugin((options) => {
1678
1901
  const resolvedImportPath = importPath ?? (!bundle ? `@kubb/plugin-client/clients/${client}` : void 0);
1679
1902
  const selectedGenerators = options.generators ?? [
1680
1903
  clientType === "staticClass" ? staticClassClientGenerator : clientType === "class" ? classClientGenerator : clientGenerator,
1681
- group && clientType === "function" ? groupedClientGenerator : void 0,
1682
- operations ? operationsGenerator : void 0
1904
+ group && clientType === "function" ? groupedClientGenerator : null,
1905
+ operations ? operationsGenerator : null
1683
1906
  ].filter((x) => Boolean(x));
1684
1907
  const groupConfig = group ? {
1685
1908
  ...group,
@@ -1687,11 +1910,11 @@ const pluginClient = definePlugin((options) => {
1687
1910
  if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1688
1911
  return `${camelCase(ctx.group)}Controller`;
1689
1912
  }
1690
- } : void 0;
1913
+ } : null;
1691
1914
  return {
1692
1915
  name: pluginClientName,
1693
1916
  options,
1694
- dependencies: [pluginTsName, parser === "zod" ? pluginZodName : void 0].filter(Boolean),
1917
+ dependencies: [pluginTsName, parser === "zod" ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
1695
1918
  hooks: { "kubb:plugin:setup"(ctx) {
1696
1919
  const resolver = userResolver ? {
1697
1920
  ...resolverClient,