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

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 (43) 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 +76 -8
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +9 -2
  10. package/dist/clients/fetch.js +76 -8
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +627 -353
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +153 -86
  15. package/dist/index.js +628 -354
  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 +106 -6
  25. package/src/components/ClassClient.tsx +19 -20
  26. package/src/components/Client.tsx +74 -53
  27. package/src/components/Operations.tsx +2 -1
  28. package/src/components/StaticClassClient.tsx +19 -20
  29. package/src/components/Url.tsx +8 -9
  30. package/src/components/WrapperClient.tsx +9 -5
  31. package/src/functionParams.ts +8 -8
  32. package/src/generators/classClientGenerator.tsx +51 -47
  33. package/src/generators/clientGenerator.tsx +37 -48
  34. package/src/generators/groupedClientGenerator.tsx +14 -8
  35. package/src/generators/operationsGenerator.tsx +14 -8
  36. package/src/generators/staticClassClientGenerator.tsx +45 -41
  37. package/src/plugin.ts +27 -26
  38. package/src/resolvers/resolverClient.ts +31 -8
  39. package/src/types.ts +93 -55
  40. package/src/utils.ts +35 -56
  41. package/templates/clients/axios.ts +0 -73
  42. package/templates/clients/fetch.ts +0 -96
  43. 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,198 @@ var URLPath = class {
332
353
  }
333
354
  };
334
355
  //#endregion
356
+ //#region ../../internals/shared/src/operation.ts
357
+ /**
358
+ * Builds the `ResolverFileParams` every operation generator passes to
359
+ * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
360
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
361
+ * that was repeated at dozens of call sites across the client and query plugins.
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
366
+ * ```
367
+ */
368
+ function operationFileEntry(node, name, extname = ".ts") {
369
+ return {
370
+ name,
371
+ extname,
372
+ tag: node.tags[0] ?? "default",
373
+ path: node.path
374
+ };
375
+ }
376
+ function getOperationLink(node, link) {
377
+ if (!link) return null;
378
+ if (typeof link === "function") return link(node) ?? null;
379
+ if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
380
+ return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
381
+ }
382
+ function getContentTypeInfo(node) {
383
+ const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
384
+ const isMultipleContentTypes = contentTypes.length > 1;
385
+ return {
386
+ contentTypes,
387
+ isMultipleContentTypes,
388
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
389
+ defaultContentType: contentTypes[0] ?? "application/json",
390
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
391
+ };
392
+ }
393
+ /**
394
+ * Derives the default `responseType` for an operation from its primary success response.
395
+ *
396
+ * Returns a value only when that response declares a single non-JSON content type — a binary type
397
+ * (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`, `video/*`) maps to `'blob'`
398
+ * and other `text/*` maps to `'text'`. Otherwise `undefined`, leaving the runtime client's
399
+ * `Content-Type` auto-detection in charge.
400
+ */
401
+ function getResponseType(node) {
402
+ const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
403
+ if (contentTypes.length !== 1) return void 0;
404
+ const baseType = contentTypes[0].split(";")[0].trim().toLowerCase();
405
+ if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
406
+ if (baseType.startsWith("text/")) return "text";
407
+ if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
408
+ }
409
+ function buildRequestConfigType(node, resolver) {
410
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
411
+ const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
412
+ return `${requestName ? `Partial<RequestConfig<${requestName}>>` : "Partial<RequestConfig>"} & { ${["client?: Client", isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null].filter(Boolean).join("; ")} }`;
413
+ }
414
+ function buildOperationComments(node, options = {}) {
415
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
416
+ const linkComment = getOperationLink(node, link);
417
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
418
+ node.description && `@description ${node.description}`,
419
+ node.summary && `@summary ${node.summary}`,
420
+ linkComment,
421
+ node.deprecated && "@deprecated"
422
+ ] : [
423
+ node.description && `@description ${node.description}`,
424
+ node.summary && `@summary ${node.summary}`,
425
+ node.deprecated && "@deprecated",
426
+ linkComment
427
+ ]).filter((comment) => Boolean(comment));
428
+ if (!splitLines) return filteredComments;
429
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
430
+ }
431
+ function getOperationParameters(node, options = {}) {
432
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
433
+ return {
434
+ path: params.filter((param) => param.in === "path"),
435
+ query: params.filter((param) => param.in === "query"),
436
+ header: params.filter((param) => param.in === "header"),
437
+ cookie: params.filter((param) => param.in === "cookie")
438
+ };
439
+ }
440
+ function getStatusCodeNumber(statusCode) {
441
+ const code = Number(statusCode);
442
+ return Number.isNaN(code) ? null : code;
443
+ }
444
+ function isSuccessStatusCode(statusCode) {
445
+ const code = getStatusCodeNumber(statusCode);
446
+ return code !== null && code >= 200 && code < 300;
447
+ }
448
+ function isErrorStatusCode(statusCode) {
449
+ const code = getStatusCodeNumber(statusCode);
450
+ return code !== null && code >= 400;
451
+ }
452
+ function getSuccessResponses(responses) {
453
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
454
+ }
455
+ function getOperationSuccessResponses(node) {
456
+ return getSuccessResponses(node.responses);
457
+ }
458
+ function getPrimarySuccessResponse(node) {
459
+ return getOperationSuccessResponses(node)[0] ?? null;
460
+ }
461
+ function resolveErrorNames(node, resolver) {
462
+ return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
463
+ }
464
+ function resolveSuccessNames(node, resolver) {
465
+ return node.responses.filter((response) => isSuccessStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
466
+ }
467
+ function resolveStatusCodeNames(node, resolver) {
468
+ return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
469
+ }
470
+ const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
471
+ function resolveOperationTypeNames(node, resolver, options = {}) {
472
+ const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
473
+ let byResolver = typeNamesByResolver.get(resolver);
474
+ if (byResolver) {
475
+ const cached = byResolver.get(cacheKey);
476
+ if (cached) return cached;
477
+ } else {
478
+ byResolver = /* @__PURE__ */ new Map();
479
+ typeNamesByResolver.set(resolver, byResolver);
480
+ }
481
+ const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
482
+ const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
483
+ const exclude = new Set(options.exclude ?? []);
484
+ const paramNames = [
485
+ ...path.map((param) => resolver.resolvePathParamsName(node, param)),
486
+ ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
487
+ ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
488
+ ];
489
+ const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
490
+ const result = (options.order === "body-response-first" ? [
491
+ ...bodyAndResponseNames,
492
+ ...paramNames,
493
+ ...responseStatusNames
494
+ ] : [
495
+ ...paramNames,
496
+ ...bodyAndResponseNames,
497
+ ...responseStatusNames
498
+ ]).filter((name) => Boolean(name) && !exclude.has(name));
499
+ byResolver.set(cacheKey, result);
500
+ return result;
501
+ }
502
+ //#endregion
503
+ //#region ../../internals/shared/src/group.ts
504
+ /**
505
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
506
+ * shared default naming so every plugin groups output consistently:
507
+ *
508
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
509
+ * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
510
+ *
511
+ * Returns `null` when grouping is disabled, matching the per-plugin convention.
512
+ *
513
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
514
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
515
+ * @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
520
+ * createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
521
+ * createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
522
+ * ```
523
+ */
524
+ function createGroupConfig(group, options) {
525
+ if (!group) return null;
526
+ const defaultName = (ctx) => {
527
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
528
+ return `${camelCase(ctx.group)}${options.suffix}`;
529
+ };
530
+ return {
531
+ ...group,
532
+ name: options.honorName && group.name ? group.name : defaultName
533
+ };
534
+ }
535
+ //#endregion
536
+ //#region ../../internals/shared/src/params.ts
537
+ function buildParamsMapping(originalParams, mappedParams) {
538
+ const mapping = {};
539
+ let hasChanged = false;
540
+ originalParams.forEach((param, i) => {
541
+ const mappedName = mappedParams[i]?.name ?? param.name;
542
+ mapping[param.name] = mappedName;
543
+ if (param.name !== mappedName) hasChanged = true;
544
+ });
545
+ return hasChanged ? mapping : null;
546
+ }
547
+ //#endregion
335
548
  //#region src/functionParams.ts
336
549
  const declarationPrinter$4 = functionPrinter({ mode: "declaration" });
337
550
  const callPrinter = functionPrinter({ mode: "call" });
@@ -342,18 +555,18 @@ function createType(type) {
342
555
  return type ? ast.createParamsType({
343
556
  variant: "reference",
344
557
  name: type
345
- }) : void 0;
558
+ }) : null;
346
559
  }
347
560
  function createDeclarationLeaf(name, spec) {
348
561
  if (spec.default !== void 0) return ast.createFunctionParameter({
349
562
  name,
350
- type: createType(spec.type),
563
+ type: createType(spec.type) ?? void 0,
351
564
  default: spec.default,
352
565
  rest: spec.mode === "inlineSpread"
353
566
  });
354
567
  return ast.createFunctionParameter({
355
568
  name,
356
- type: createType(spec.type),
569
+ type: createType(spec.type) ?? void 0,
357
570
  optional: !!spec.optional,
358
571
  rest: spec.mode === "inlineSpread"
359
572
  });
@@ -362,14 +575,14 @@ function createDeclarationParam(name, spec) {
362
575
  if (isGroup(spec)) return ast.createParameterGroup({
363
576
  inline: spec.mode === "inlineSpread",
364
577
  default: spec.default,
365
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => createDeclarationLeaf(childName, child))
578
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => createDeclarationLeaf(childName, child))
366
579
  });
367
580
  return createDeclarationLeaf(name, spec);
368
581
  }
369
582
  function createCallParam(name, spec) {
370
583
  if (isGroup(spec)) return ast.createParameterGroup({
371
584
  inline: spec.mode === "inlineSpread",
372
- properties: Object.entries(spec.children).filter(([, child]) => child !== void 0).map(([childName, child]) => ast.createFunctionParameter({
585
+ properties: Object.entries(spec.children).filter(([, child]) => child != null).map(([childName, child]) => ast.createFunctionParameter({
373
586
  name: child?.mode === "inlineSpread" ? spec.mode === "inlineSpread" ? child.value ?? childName : `...${child.value ?? childName}` : child?.value ? `${childName}: ${child.value}` : childName,
374
587
  rest: spec.mode === "inlineSpread" && child?.mode === "inlineSpread"
375
588
  }))
@@ -384,7 +597,7 @@ function createCallParam(name, spec) {
384
597
  * Returns utilities to output constructor signatures (`toConstructor()`) or call expressions (`toCall()`).
385
598
  */
386
599
  function createFunctionParams(params) {
387
- const entries = Object.entries(params).filter(([, spec]) => spec !== void 0);
600
+ const entries = Object.entries(params).filter(([, spec]) => spec != null);
388
601
  return {
389
602
  toConstructor() {
390
603
  return declarationPrinter$4.print(ast.createFunctionParameters({ params: entries.map(([name, spec]) => createDeclarationParam(name, spec)) })) ?? "";
@@ -395,106 +608,9 @@ function createFunctionParams(params) {
395
608
  };
396
609
  }
397
610
  //#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
611
  //#region src/components/Url.tsx
496
612
  const declarationPrinter$3 = functionPrinter({ mode: "declaration" });
497
- function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
613
+ function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
498
614
  const urlNode = {
499
615
  ...node,
500
616
  parameters: node.parameters.filter((p) => p.in === "path"),
@@ -507,10 +623,10 @@ function getParams$1({ paramsType, paramsCasing, pathParamsType, node, tsResolve
507
623
  resolver: tsResolver
508
624
  });
509
625
  }
510
- __name(getParams$1, "getParams");
511
626
  function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
627
+ if (!ast.isHttpOperationNode(node)) return null;
512
628
  const path = new URLPath(node.path);
513
- const paramsNode = getParams$1({
629
+ const paramsNode = buildUrlParamsNode({
514
630
  paramsType,
515
631
  paramsCasing,
516
632
  pathParamsType,
@@ -518,9 +634,9 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
518
634
  tsResolver
519
635
  });
520
636
  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;
637
+ const { path: originalPathParams } = getOperationParameters(node);
638
+ const { path: casedPathParams } = getOperationParameters(node, { paramsCasing });
639
+ const pathParamsMapping = paramsCasing ? buildParamsMapping(originalPathParams, casedPathParams) : null;
524
640
  return /* @__PURE__ */ jsx(File.Source, {
525
641
  name,
526
642
  isExportable,
@@ -542,56 +658,53 @@ function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsTyp
542
658
  })
543
659
  });
544
660
  }
545
- Url.getParams = getParams$1;
546
661
  //#endregion
547
662
  //#region src/components/Client.tsx
548
663
  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;
664
+ function buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable }) {
551
665
  return ast.createOperationParams(node, {
552
666
  paramsType,
553
667
  pathParamsType: paramsType === "object" ? "object" : pathParamsType === "object" ? "object" : "inline",
554
668
  paramsCasing,
555
669
  resolver: tsResolver,
556
- extraParams: isConfigurable ? [ast.createFunctionParameter({
670
+ extraParams: [...isConfigurable ? [ast.createFunctionParameter({
557
671
  name: "config",
558
672
  type: ast.createParamsType({
559
673
  variant: "reference",
560
- name: requestName ? `Partial<RequestConfig<${requestName}>> & { client?: Client }` : "Partial<RequestConfig> & { client?: Client }"
674
+ name: buildRequestConfigType(node, tsResolver)
561
675
  }),
562
676
  default: "{}"
563
- })] : []
677
+ })] : []]
564
678
  });
565
679
  }
566
680
  function Client({ name, isExportable = true, isIndexable = true, returnType, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType, node, tsResolver, zodResolver, urlName, children, isConfigurable = true }) {
681
+ if (!ast.isHttpOperationNode(node)) return null;
567
682
  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;
683
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
684
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
685
+ const responseType = getResponseType(node);
686
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
687
+ const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing });
688
+ const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null;
689
+ const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null;
690
+ const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null;
691
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
692
+ const successNames = resolveSuccessNames(node, tsResolver);
693
+ const responseName = successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
694
+ const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]) : null;
695
+ const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]) : null;
696
+ const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
697
+ const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
585
698
  const errorNames = node.responses.filter((r) => {
586
699
  return Number.parseInt(r.statusCode, 10) >= 400;
587
700
  }).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);
701
+ const headers = [!isMultipleContentTypes && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, headerParamsName ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null].filter(Boolean);
589
702
  const generics = [
590
703
  responseName,
591
704
  `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
592
705
  requestName || "unknown"
593
706
  ].filter(Boolean);
594
- const paramsNode = getParams({
707
+ const paramsNode = buildClientParamsNode({
595
708
  paramsType,
596
709
  paramsCasing,
597
710
  pathParamsType,
@@ -600,7 +713,7 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
600
713
  isConfigurable
601
714
  });
602
715
  const paramsSignature = declarationPrinter$2.print(paramsNode) ?? "";
603
- const urlParamsNode = Url.getParams({
716
+ const urlParamsNode = buildUrlParamsNode({
604
717
  paramsType,
605
718
  paramsCasing,
606
719
  pathParamsType,
@@ -613,18 +726,20 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
613
726
  children: {
614
727
  method: { value: JSON.stringify(node.method.toUpperCase()) },
615
728
  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
729
+ baseURL: baseURL && !urlName ? { value: `\`${baseURL}\`` } : null,
730
+ params: queryParamsName ? queryParamsMapping ? { value: "mappedParams" } : {} : null,
731
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
732
+ contentType: isConfigurable && isMultipleContentTypes ? {} : null,
733
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
734
+ requestConfig: isConfigurable ? { mode: "inlineSpread" } : null,
735
+ headers: headers.length ? { value: isConfigurable ? `{ ${headers.join(", ")}, ...requestConfig.headers }` : `{ ${headers.join(", ")} }` } : null
621
736
  }
622
737
  } });
623
738
  const childrenElement = children ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
624
739
  dataReturnType === "full" && parser === "zod" && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`,
625
740
  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"
741
+ dataReturnType === "full" && parser !== "zod" && "return res",
742
+ dataReturnType === "data" && parser !== "zod" && "return res.data"
628
743
  ] });
629
744
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx(File.Source, {
630
745
  name,
@@ -635,10 +750,14 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
635
750
  async: true,
636
751
  export: isExportable,
637
752
  params: paramsSignature,
638
- JSDoc: { comments: getComments(node) },
753
+ JSDoc: { comments: buildOperationComments(node, {
754
+ link: "urlPath",
755
+ linkPosition: "beforeDeprecated",
756
+ splitLines: true
757
+ }) },
639
758
  returnType,
640
759
  children: [
641
- isConfigurable ? "const { client: request = fetch, ...requestConfig } = config" : "",
760
+ isConfigurable ? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = config` : "",
642
761
  /* @__PURE__ */ jsx("br", {}),
643
762
  /* @__PURE__ */ jsx("br", {}),
644
763
  pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => isValidVarName(originalName) && originalName !== camelCaseName).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
@@ -655,26 +774,104 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
655
774
  ] }),
656
775
  parser === "zod" && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && "const requestData = data",
657
776
  /* @__PURE__ */ jsx("br", {}),
658
- isFormData && requestName && "const formData = buildFormData(requestData)",
777
+ (isFormData || isMultipleContentTypes && hasFormData) && requestName && "const formData = buildFormData(requestData)",
659
778
  /* @__PURE__ */ jsx("br", {}),
660
- isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await fetch<${generics.join(", ")}>(${clientParams.toCall()})`,
779
+ isConfigurable ? `const res = await request<${generics.join(", ")}>(${clientParams.toCall()})` : `const res = await client<${generics.join(", ")}>(${clientParams.toCall()})`,
661
780
  /* @__PURE__ */ jsx("br", {}),
662
781
  childrenElement
663
782
  ]
664
783
  })
665
784
  })] });
666
785
  }
667
- Client.getParams = getParams;
786
+ //#endregion
787
+ //#region src/utils.ts
788
+ /**
789
+ * Builds HTTP headers array for a client request.
790
+ * Includes Content-Type (if not default) and spreads header parameters if present.
791
+ */
792
+ function buildHeaders(contentType, hasHeaderParams) {
793
+ return [contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null, hasHeaderParams ? "...headers" : null].filter(Boolean);
794
+ }
795
+ /**
796
+ * Builds TypeScript generic parameters for a client method.
797
+ * Includes response type, error type, and optional request type.
798
+ */
799
+ function buildGenerics(node, tsResolver) {
800
+ const successNames = resolveSuccessNames(node, tsResolver);
801
+ const responseName = successNames.length > 0 ? successNames.join(" | ") : tsResolver.resolveResponseName(node);
802
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
803
+ const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode));
804
+ return [
805
+ responseName,
806
+ `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(" | ") : "Error"}>`,
807
+ requestName || "unknown"
808
+ ].filter(Boolean);
809
+ }
810
+ /**
811
+ * Builds the parameters object for a class-based client method.
812
+ * Includes URL, method, base URL, headers, and request/response data.
813
+ */
814
+ function buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers }) {
815
+ const { query: queryParams } = getOperationParameters(node);
816
+ const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]) : null;
817
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
818
+ const responseType = getResponseType(node);
819
+ return createFunctionParams({ config: {
820
+ mode: "object",
821
+ children: {
822
+ requestConfig: { mode: "inlineSpread" },
823
+ method: { value: JSON.stringify(ast.isHttpOperationNode(node) ? node.method.toUpperCase() : "") },
824
+ url: { value: path.template },
825
+ baseURL: baseURL ? { value: JSON.stringify(baseURL) } : null,
826
+ params: queryParamsName ? {} : null,
827
+ data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
828
+ contentType: isMultipleContentTypes ? {} : null,
829
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
830
+ headers: headers.length ? { value: `{ ${headers.join(", ")}, ...requestConfig.headers }` } : null
831
+ }
832
+ } });
833
+ }
834
+ /**
835
+ * Builds the request data parsing line for client methods.
836
+ * Applies Zod validation if configured, otherwise uses data directly.
837
+ */
838
+ function buildRequestDataLine({ parser, node, zodResolver }) {
839
+ const zodRequestName = zodResolver && parser === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null;
840
+ if (parser === "zod" && zodRequestName) return `const requestData = ${zodRequestName}.parse(data)`;
841
+ if (node.requestBody?.content?.[0]?.schema) return "const requestData = data";
842
+ return "";
843
+ }
844
+ /**
845
+ * Builds the form data conversion line for file upload requests.
846
+ * Returns empty string if not applicable.
847
+ */
848
+ function buildFormDataLine(isFormData, hasRequest) {
849
+ return isFormData && hasRequest ? "const formData = buildFormData(requestData)" : "";
850
+ }
851
+ /**
852
+ * Builds the return statement for a client method.
853
+ * Applies Zod validation to response data if configured, otherwise returns raw response.
854
+ */
855
+ function buildReturnStatement({ dataReturnType, parser, node, zodResolver }) {
856
+ const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
857
+ if (dataReturnType === "full" && parser === "zod" && zodResponseName) return `return {...res, data: ${zodResponseName}.parse(res.data)}`;
858
+ if (dataReturnType === "data" && parser === "zod" && zodResponseName) return `return ${zodResponseName}.parse(res.data)`;
859
+ if (dataReturnType === "full" && parser !== "zod") return "return res";
860
+ return "return res.data";
861
+ }
668
862
  //#endregion
669
863
  //#region src/components/ClassClient.tsx
670
864
  const declarationPrinter$1 = functionPrinter({ mode: "declaration" });
671
865
  function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
866
+ if (!ast.isHttpOperationNode(node)) return "";
672
867
  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));
868
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
869
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
870
+ const { header: headerParams } = getOperationParameters(node);
871
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
872
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
676
873
  const generics = buildGenerics(node, tsResolver);
677
- const paramsNode = ClassClient.getParams({
874
+ const paramsNode = buildClientParamsNode({
678
875
  paramsType,
679
876
  paramsCasing,
680
877
  pathParamsType,
@@ -689,15 +886,21 @@ function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataRe
689
886
  baseURL,
690
887
  tsResolver,
691
888
  isFormData,
889
+ isMultipleContentTypes,
890
+ hasFormData,
692
891
  headers
693
892
  });
694
- const jsdoc = buildJSDoc(getComments(node));
893
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
894
+ link: "urlPath",
895
+ linkPosition: "beforeDeprecated",
896
+ splitLines: true
897
+ }));
695
898
  const requestDataLine = buildRequestDataLine({
696
899
  parser,
697
900
  node,
698
901
  zodResolver
699
902
  });
700
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
903
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
701
904
  const returnStatement = buildReturnStatement({
702
905
  dataReturnType,
703
906
  parser,
@@ -705,7 +908,7 @@ function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataRe
705
908
  zodResolver
706
909
  });
707
910
  return `${jsdoc}async ${name}(${paramsSignature}) {\n${[
708
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
911
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
709
912
  "",
710
913
  requestDataLine,
711
914
  formDataLine,
@@ -742,15 +945,14 @@ ${operations.map(({ node, name: methodName, tsResolver, zodResolver }) => genera
742
945
  children: [classCode, children]
743
946
  });
744
947
  }
745
- ClassClient.getParams = Client.getParams;
746
948
  //#endregion
747
949
  //#region src/components/WrapperClient.tsx
748
- function WrapperClient({ name, classNames, isExportable = true, isIndexable = true }) {
950
+ function WrapperClient({ name, controllers, isExportable = true, isIndexable = true }) {
749
951
  const classCode = `export class ${name} {
750
- ${classNames.map((className) => ` readonly ${camelCase(className)}: ${className}`).join("\n")}
952
+ ${controllers.map(({ className, propertyName }) => ` readonly ${propertyName}: ${className}`).join("\n")}
751
953
 
752
954
  constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
753
- ${classNames.map((className) => ` this.${camelCase(className)} = new ${className}(config)`).join("\n")}
955
+ ${controllers.map(({ className, propertyName }) => ` this.${propertyName} = new ${className}(config)`).join("\n")}
754
956
  }
755
957
  }`;
756
958
  return /* @__PURE__ */ jsx(File.Source, {
@@ -763,54 +965,42 @@ ${classNames.map((className) => ` this.${camelCase(className)} = new ${classN
763
965
  //#endregion
764
966
  //#region src/generators/classClientGenerator.tsx
765
967
  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));
968
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
774
969
  }
775
970
  __name(resolveTypeImportNames$1, "resolveTypeImportNames");
776
971
  function resolveZodImportNames$1(node, zodResolver) {
777
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
972
+ return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((n) => Boolean(n));
778
973
  }
779
974
  __name(resolveZodImportNames$1, "resolveZodImportNames");
975
+ /**
976
+ * Built-in `operations` generator for `@kubb/plugin-client` when
977
+ * `clientType: 'class'`. Emits one class per tag, with one instance method
978
+ * per operation and a shared constructor for request configuration.
979
+ */
780
980
  const classClientGenerator = defineGenerator({
781
981
  name: "classClient",
782
- renderer: jsxRenderer,
982
+ renderer: jsxRendererSync,
783
983
  operations(nodes, ctx) {
784
- const { adapter, config, driver, resolver, root } = ctx;
984
+ const { config, driver, resolver, root } = ctx;
785
985
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, sdk } = ctx.options;
786
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
986
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
787
987
  const pluginTs = driver.getPlugin(pluginTsName);
788
988
  if (!pluginTs) return null;
789
989
  const tsResolver = driver.getResolver(pluginTsName);
790
990
  const tsPluginOptions = pluginTs.options;
791
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
792
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
991
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
992
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
793
993
  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
- }, {
994
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
800
995
  root,
801
996
  output: tsPluginOptions?.output ?? output,
802
997
  group: tsPluginOptions?.group
803
998
  });
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
- }, {
999
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
810
1000
  root,
811
1001
  output: pluginZod.options?.output ?? output,
812
- group: pluginZod.options?.group
813
- }) : void 0;
1002
+ group: pluginZod.options?.group ?? void 0
1003
+ }) : null;
814
1004
  return {
815
1005
  node,
816
1006
  name: resolver.resolveName(node.operationId),
@@ -821,27 +1011,31 @@ const classClientGenerator = defineGenerator({
821
1011
  };
822
1012
  }
823
1013
  const controllers = nodes.reduce((acc, operationNode) => {
1014
+ if (!ast.isHttpOperationNode(operationNode)) return acc;
824
1015
  const tag = operationNode.tags[0];
825
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
1016
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
826
1017
  if (!tag && !group) {
827
- const name = "ApiClient";
1018
+ const name = resolver.resolveClassName("ApiClient");
828
1019
  const file = resolver.resolveFile({
829
1020
  name,
830
1021
  extname: ".ts"
831
1022
  }, {
832
1023
  root,
833
1024
  output,
834
- group
1025
+ group: group ?? void 0
835
1026
  });
836
1027
  const operationData = buildOperationData(operationNode);
837
1028
  const previous = acc.find((item) => item.file.path === file.path);
838
1029
  if (previous) previous.operations.push(operationData);
839
1030
  else acc.push({
840
1031
  name,
1032
+ propertyName: resolver.resolveClientPropertyName(name),
841
1033
  file,
842
1034
  operations: [operationData]
843
1035
  });
844
- } else if (tag) {
1036
+ return acc;
1037
+ }
1038
+ if (tag) {
845
1039
  const name = groupName;
846
1040
  const file = resolver.resolveFile({
847
1041
  name,
@@ -850,13 +1044,14 @@ const classClientGenerator = defineGenerator({
850
1044
  }, {
851
1045
  root,
852
1046
  output,
853
- group
1047
+ group: group ?? void 0
854
1048
  });
855
1049
  const operationData = buildOperationData(operationNode);
856
1050
  const previous = acc.find((item) => item.file.path === file.path);
857
1051
  if (previous) previous.operations.push(operationData);
858
1052
  else acc.push({
859
1053
  name,
1054
+ propertyName: resolver.resolveClientPropertyName(name),
860
1055
  file,
861
1056
  operations: [operationData]
862
1057
  });
@@ -904,23 +1099,31 @@ const classClientGenerator = defineGenerator({
904
1099
  zodImportsByFile: /* @__PURE__ */ new Map(),
905
1100
  zodFilesByPath: /* @__PURE__ */ new Map()
906
1101
  };
907
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1102
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
908
1103
  return /* @__PURE__ */ jsxs(File, {
909
1104
  baseName: file.baseName,
910
1105
  path: file.path,
911
1106
  meta: file.meta,
912
- banner: resolver.resolveBanner(adapter.inputNode, {
1107
+ banner: resolver.resolveBanner(ctx.meta, {
913
1108
  output,
914
- config
1109
+ config,
1110
+ file: {
1111
+ path: file.path,
1112
+ baseName: file.baseName
1113
+ }
915
1114
  }),
916
- footer: resolver.resolveFooter(adapter.inputNode, {
1115
+ footer: resolver.resolveFooter(ctx.meta, {
917
1116
  output,
918
- config
1117
+ config,
1118
+ file: {
1119
+ path: file.path,
1120
+ baseName: file.baseName
1121
+ }
919
1122
  }),
920
1123
  children: [
921
1124
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
922
1125
  /* @__PURE__ */ jsx(File.Import, {
923
- name: "fetch",
1126
+ name: "client",
924
1127
  path: importPath
925
1128
  }),
926
1129
  /* @__PURE__ */ jsx(File.Import, {
@@ -938,7 +1141,7 @@ const classClientGenerator = defineGenerator({
938
1141
  })
939
1142
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
940
1143
  /* @__PURE__ */ jsx(File.Import, {
941
- name: ["fetch"],
1144
+ name: ["client"],
942
1145
  root: file.path,
943
1146
  path: path.resolve(root, ".kubb/client.ts")
944
1147
  }),
@@ -1006,19 +1209,27 @@ const classClientGenerator = defineGenerator({
1006
1209
  }, {
1007
1210
  root,
1008
1211
  output,
1009
- group
1212
+ group: group ?? void 0
1010
1213
  });
1011
1214
  files.push(/* @__PURE__ */ jsxs(File, {
1012
1215
  baseName: sdkFile.baseName,
1013
1216
  path: sdkFile.path,
1014
1217
  meta: sdkFile.meta,
1015
- banner: resolver.resolveBanner(adapter.inputNode, {
1218
+ banner: resolver.resolveBanner(ctx.meta, {
1016
1219
  output,
1017
- config
1220
+ config,
1221
+ file: {
1222
+ path: sdkFile.path,
1223
+ baseName: sdkFile.baseName
1224
+ }
1018
1225
  }),
1019
- footer: resolver.resolveFooter(adapter.inputNode, {
1226
+ footer: resolver.resolveFooter(ctx.meta, {
1020
1227
  output,
1021
- config
1228
+ config,
1229
+ file: {
1230
+ path: sdkFile.path,
1231
+ baseName: sdkFile.baseName
1232
+ }
1022
1233
  }),
1023
1234
  children: [
1024
1235
  importPath ? /* @__PURE__ */ jsx(File.Import, {
@@ -1038,7 +1249,10 @@ const classClientGenerator = defineGenerator({
1038
1249
  }, name)),
1039
1250
  /* @__PURE__ */ jsx(WrapperClient, {
1040
1251
  name: sdk.className,
1041
- classNames: controllers.map(({ name }) => name)
1252
+ controllers: controllers.map(({ name, propertyName }) => ({
1253
+ className: name,
1254
+ propertyName
1255
+ }))
1042
1256
  })
1043
1257
  ]
1044
1258
  }, sdkFile.path));
@@ -1048,81 +1262,69 @@ const classClientGenerator = defineGenerator({
1048
1262
  });
1049
1263
  //#endregion
1050
1264
  //#region src/generators/clientGenerator.tsx
1265
+ /**
1266
+ * Built-in operation generator for `@kubb/plugin-client`. Emits one async
1267
+ * function per OpenAPI operation, plus the matching URL helper. Used when
1268
+ * `clientType: 'function'` (the default).
1269
+ */
1051
1270
  const clientGenerator = defineGenerator({
1052
1271
  name: "client",
1053
- renderer: jsxRenderer,
1272
+ renderer: jsxRendererSync,
1054
1273
  operation(node, ctx) {
1055
- const { adapter, config, driver, resolver, root } = ctx;
1274
+ if (!ast.isHttpOperationNode(node)) return null;
1275
+ const { config, driver, resolver, root } = ctx;
1056
1276
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options;
1057
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1277
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1058
1278
  const pluginTs = driver.getPlugin(pluginTsName);
1059
1279
  if (!pluginTs) return null;
1060
1280
  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) : [];
1281
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
1282
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1283
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing });
1284
+ const importedZodNames = zodResolver && parser === "zod" ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((name) => Boolean(name)) : [];
1076
1285
  const meta = {
1077
1286
  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
- }, {
1287
+ urlName: resolver.resolveUrlName(node),
1288
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), {
1085
1289
  root,
1086
1290
  output,
1087
- group
1291
+ group: group ?? void 0
1088
1292
  }),
1089
- fileTs: tsResolver.resolveFile({
1090
- name: node.operationId,
1091
- extname: ".ts",
1092
- tag: node.tags[0] ?? "default",
1093
- path: node.path
1094
- }, {
1293
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1095
1294
  root,
1096
1295
  output: pluginTs.options?.output ?? output,
1097
- group: pluginTs.options?.group
1296
+ group: pluginTs.options?.group ?? void 0
1098
1297
  }),
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
- }, {
1298
+ fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1105
1299
  root,
1106
1300
  output: pluginZod.options.output ?? output,
1107
- group: pluginZod.options?.group
1108
- }) : void 0
1301
+ group: pluginZod.options?.group ?? void 0
1302
+ }) : null
1109
1303
  };
1110
- const isFormData = node.requestBody?.content?.[0]?.contentType === "multipart/form-data";
1304
+ const hasFormData = node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false;
1111
1305
  return /* @__PURE__ */ jsxs(File, {
1112
1306
  baseName: meta.file.baseName,
1113
1307
  path: meta.file.path,
1114
1308
  meta: meta.file.meta,
1115
- banner: resolver.resolveBanner(adapter.inputNode, {
1309
+ banner: resolver.resolveBanner(ctx.meta, {
1116
1310
  output,
1117
- config
1311
+ config,
1312
+ file: {
1313
+ path: meta.file.path,
1314
+ baseName: meta.file.baseName
1315
+ }
1118
1316
  }),
1119
- footer: resolver.resolveFooter(adapter.inputNode, {
1317
+ footer: resolver.resolveFooter(ctx.meta, {
1120
1318
  output,
1121
- config
1319
+ config,
1320
+ file: {
1321
+ path: meta.file.path,
1322
+ baseName: meta.file.baseName
1323
+ }
1122
1324
  }),
1123
1325
  children: [
1124
1326
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1125
- name: "fetch",
1327
+ name: "client",
1126
1328
  path: importPath
1127
1329
  }), /* @__PURE__ */ jsx(File.Import, {
1128
1330
  name: [
@@ -1133,7 +1335,7 @@ const clientGenerator = defineGenerator({
1133
1335
  path: importPath,
1134
1336
  isTypeOnly: true
1135
1337
  })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Import, {
1136
- name: ["fetch"],
1338
+ name: ["client"],
1137
1339
  root: meta.file.path,
1138
1340
  path: path.resolve(root, ".kubb/client.ts")
1139
1341
  }), /* @__PURE__ */ jsx(File.Import, {
@@ -1146,7 +1348,7 @@ const clientGenerator = defineGenerator({
1146
1348
  path: path.resolve(root, ".kubb/client.ts"),
1147
1349
  isTypeOnly: true
1148
1350
  })] }),
1149
- isFormData && node.requestBody?.content?.[0]?.schema && /* @__PURE__ */ jsx(File.Import, {
1351
+ hasFormData && /* @__PURE__ */ jsx(File.Import, {
1150
1352
  name: ["buildFormData"],
1151
1353
  root: meta.file.path,
1152
1354
  path: path.resolve(root, ".kubb/config.ts")
@@ -1192,16 +1394,22 @@ const clientGenerator = defineGenerator({
1192
1394
  });
1193
1395
  //#endregion
1194
1396
  //#region src/generators/groupedClientGenerator.tsx
1397
+ /**
1398
+ * Emits one aggregate file per tag/group when `group` is configured. Each
1399
+ * file re-exports every client function for that group, so callers can
1400
+ * `import { petController } from './gen/clients'` instead of importing
1401
+ * each operation individually.
1402
+ */
1195
1403
  const groupedClientGenerator = defineGenerator({
1196
1404
  name: "groupedClient",
1197
- renderer: jsxRenderer,
1405
+ renderer: jsxRendererSync,
1198
1406
  operations(nodes, ctx) {
1199
- const { config, resolver, adapter, root } = ctx;
1407
+ const { config, resolver, root } = ctx;
1200
1408
  const { output, group } = ctx.options;
1201
1409
  return /* @__PURE__ */ jsx(Fragment, { children: nodes.reduce((acc, operationNode) => {
1202
1410
  if (group?.type === "tag") {
1203
1411
  const tag = operationNode.tags[0];
1204
- const name = tag ? group?.name?.({ group: camelCase(tag) }) : void 0;
1412
+ const name = tag ? group?.name?.({ group: camelCase(tag) }) : null;
1205
1413
  if (!tag || !name) return acc;
1206
1414
  const file = resolver.resolveFile({
1207
1415
  name,
@@ -1210,7 +1418,7 @@ const groupedClientGenerator = defineGenerator({
1210
1418
  }, {
1211
1419
  root,
1212
1420
  output,
1213
- group
1421
+ group: group ?? void 0
1214
1422
  });
1215
1423
  const clientFile = resolver.resolveFile({
1216
1424
  name: operationNode.operationId,
@@ -1220,7 +1428,7 @@ const groupedClientGenerator = defineGenerator({
1220
1428
  }, {
1221
1429
  root,
1222
1430
  output,
1223
- group
1431
+ group: group ?? void 0
1224
1432
  });
1225
1433
  const client = {
1226
1434
  name: resolver.resolveName(operationNode.operationId),
@@ -1240,13 +1448,23 @@ const groupedClientGenerator = defineGenerator({
1240
1448
  baseName: file.baseName,
1241
1449
  path: file.path,
1242
1450
  meta: file.meta,
1243
- banner: resolver.resolveBanner(adapter.inputNode, {
1451
+ banner: resolver.resolveBanner(ctx.meta, {
1244
1452
  output,
1245
- config
1453
+ config,
1454
+ file: {
1455
+ path: file.path,
1456
+ baseName: file.baseName,
1457
+ isAggregation: true
1458
+ }
1246
1459
  }),
1247
- footer: resolver.resolveFooter(adapter.inputNode, {
1460
+ footer: resolver.resolveFooter(ctx.meta, {
1248
1461
  output,
1249
- config
1462
+ config,
1463
+ file: {
1464
+ path: file.path,
1465
+ baseName: file.baseName,
1466
+ isAggregation: true
1467
+ }
1250
1468
  }),
1251
1469
  children: [clients.map((client) => /* @__PURE__ */ jsx(File.Import, {
1252
1470
  name: [client.name],
@@ -1271,6 +1489,7 @@ const groupedClientGenerator = defineGenerator({
1271
1489
  function Operations({ name, nodes }) {
1272
1490
  const operationsObject = {};
1273
1491
  nodes.forEach((node) => {
1492
+ if (!ast.isHttpOperationNode(node)) return;
1274
1493
  operationsObject[node.operationId] = {
1275
1494
  path: new URLPath(node.path).URL,
1276
1495
  method: node.method.toLowerCase()
@@ -1289,11 +1508,17 @@ function Operations({ name, nodes }) {
1289
1508
  }
1290
1509
  //#endregion
1291
1510
  //#region src/generators/operationsGenerator.tsx
1511
+ /**
1512
+ * Generates an `operations.ts` file that re-exports every operation grouped
1513
+ * by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
1514
+ * for building meta-tooling on top of the generated client (route
1515
+ * registries, API explorers).
1516
+ */
1292
1517
  const operationsGenerator = defineGenerator({
1293
1518
  name: "client",
1294
- renderer: jsxRenderer,
1519
+ renderer: jsxRendererSync,
1295
1520
  operations(nodes, ctx) {
1296
- const { config, resolver, adapter, root } = ctx;
1521
+ const { config, resolver, root } = ctx;
1297
1522
  const { output, group } = ctx.options;
1298
1523
  const name = "operations";
1299
1524
  const file = resolver.resolveFile({
@@ -1302,23 +1527,31 @@ const operationsGenerator = defineGenerator({
1302
1527
  }, {
1303
1528
  root,
1304
1529
  output,
1305
- group
1530
+ group: group ?? void 0
1306
1531
  });
1307
1532
  return /* @__PURE__ */ jsx(File, {
1308
1533
  baseName: file.baseName,
1309
1534
  path: file.path,
1310
1535
  meta: file.meta,
1311
- banner: resolver.resolveBanner(adapter.inputNode, {
1536
+ banner: resolver.resolveBanner(ctx.meta, {
1312
1537
  output,
1313
- config
1538
+ config,
1539
+ file: {
1540
+ path: file.path,
1541
+ baseName: file.baseName
1542
+ }
1314
1543
  }),
1315
- footer: resolver.resolveFooter(adapter.inputNode, {
1544
+ footer: resolver.resolveFooter(ctx.meta, {
1316
1545
  output,
1317
- config
1546
+ config,
1547
+ file: {
1548
+ path: file.path,
1549
+ baseName: file.baseName
1550
+ }
1318
1551
  }),
1319
1552
  children: /* @__PURE__ */ jsx(Operations, {
1320
1553
  name,
1321
- nodes
1554
+ nodes: nodes.filter(ast.isHttpOperationNode)
1322
1555
  })
1323
1556
  });
1324
1557
  }
@@ -1327,12 +1560,15 @@ const operationsGenerator = defineGenerator({
1327
1560
  //#region src/components/StaticClassClient.tsx
1328
1561
  const declarationPrinter = functionPrinter({ mode: "declaration" });
1329
1562
  function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
1563
+ if (!ast.isHttpOperationNode(node)) return "";
1330
1564
  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));
1565
+ const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
1566
+ const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
1567
+ const { header: headerParams } = getOperationParameters(node);
1568
+ const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]) : null;
1569
+ const headers = isMultipleContentTypes ? headerParamsName ? ["...headers"] : [] : buildHeaders(contentType, !!headerParamsName);
1334
1570
  const generics = buildGenerics(node, tsResolver);
1335
- const paramsNode = Client.getParams({
1571
+ const paramsNode = buildClientParamsNode({
1336
1572
  paramsType,
1337
1573
  paramsCasing,
1338
1574
  pathParamsType,
@@ -1347,15 +1583,21 @@ function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataRetu
1347
1583
  baseURL,
1348
1584
  tsResolver,
1349
1585
  isFormData,
1586
+ isMultipleContentTypes,
1587
+ hasFormData,
1350
1588
  headers
1351
1589
  });
1352
- const jsdoc = buildJSDoc(getComments(node));
1590
+ const jsdoc = buildJSDoc(buildOperationComments(node, {
1591
+ link: "urlPath",
1592
+ linkPosition: "beforeDeprecated",
1593
+ splitLines: true
1594
+ }));
1353
1595
  const requestDataLine = buildRequestDataLine({
1354
1596
  parser,
1355
1597
  node,
1356
1598
  zodResolver
1357
1599
  });
1358
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema);
1600
+ const formDataLine = buildFormDataLine(isFormData || isMultipleContentTypes && hasFormData, !!node.requestBody?.content?.[0]?.schema);
1359
1601
  const returnStatement = buildReturnStatement({
1360
1602
  dataReturnType,
1361
1603
  parser,
@@ -1363,7 +1605,7 @@ function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataRetu
1363
1605
  zodResolver
1364
1606
  });
1365
1607
  return `${jsdoc} static async ${name}(${paramsSignature}) {\n${[
1366
- "const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)",
1608
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ""}...requestConfig } = mergeConfig(this.#config, config)`,
1367
1609
  "",
1368
1610
  requestDataLine,
1369
1611
  formDataLine,
@@ -1391,56 +1633,44 @@ function StaticClassClient({ name, isExportable = true, isIndexable = true, oper
1391
1633
  children: [classCode, children]
1392
1634
  });
1393
1635
  }
1394
- StaticClassClient.getParams = Client.getParams;
1395
1636
  //#endregion
1396
1637
  //#region src/generators/staticClassClientGenerator.tsx
1397
1638
  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));
1639
+ return resolveOperationTypeNames(node, tsResolver, { order: "body-response-first" });
1406
1640
  }
1407
1641
  function resolveZodImportNames(node, zodResolver) {
1408
- return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : void 0].filter((n) => Boolean(n));
1642
+ return [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null].filter((n) => Boolean(n));
1409
1643
  }
1644
+ /**
1645
+ * Built-in `operations` generator for `@kubb/plugin-client` when
1646
+ * `clientType: 'staticClass'`. Emits one class per tag, with a static method
1647
+ * per operation so callers can use `Pet.getPetById(...)` without
1648
+ * instantiating the class.
1649
+ */
1410
1650
  const staticClassClientGenerator = defineGenerator({
1411
1651
  name: "staticClassClient",
1412
- renderer: jsxRenderer,
1652
+ renderer: jsxRendererSync,
1413
1653
  operations(nodes, ctx) {
1414
- const { adapter, config, driver, resolver, root } = ctx;
1654
+ const { config, driver, resolver, root } = ctx;
1415
1655
  const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath } = ctx.options;
1416
- const baseURL = ctx.options.baseURL ?? adapter.inputNode?.meta?.baseURL;
1656
+ const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
1417
1657
  const pluginTs = driver.getPlugin(pluginTsName);
1418
1658
  if (!pluginTs) return null;
1419
1659
  const tsResolver = driver.getResolver(pluginTsName);
1420
1660
  const tsPluginOptions = pluginTs.options;
1421
- const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : void 0;
1422
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : void 0;
1661
+ const pluginZod = parser === "zod" ? driver.getPlugin(pluginZodName) : null;
1662
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1423
1663
  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
- }, {
1664
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1430
1665
  root,
1431
1666
  output: tsPluginOptions?.output ?? output,
1432
1667
  group: tsPluginOptions?.group
1433
1668
  });
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
- }, {
1669
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1440
1670
  root,
1441
1671
  output: pluginZod.options?.output ?? output,
1442
- group: pluginZod.options?.group
1443
- }) : void 0;
1672
+ group: pluginZod.options?.group ?? void 0
1673
+ }) : null;
1444
1674
  return {
1445
1675
  node,
1446
1676
  name: resolver.resolveName(node.operationId),
@@ -1451,17 +1681,18 @@ const staticClassClientGenerator = defineGenerator({
1451
1681
  };
1452
1682
  }
1453
1683
  const controllers = nodes.reduce((acc, operationNode) => {
1684
+ if (!ast.isHttpOperationNode(operationNode)) return acc;
1454
1685
  const tag = operationNode.tags[0];
1455
- const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? pascalCase(tag) : "Client";
1686
+ const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
1456
1687
  if (!tag && !group) {
1457
- const name = "ApiClient";
1688
+ const name = resolver.resolveClassName("ApiClient");
1458
1689
  const file = resolver.resolveFile({
1459
1690
  name,
1460
1691
  extname: ".ts"
1461
1692
  }, {
1462
1693
  root,
1463
1694
  output,
1464
- group
1695
+ group: group ?? void 0
1465
1696
  });
1466
1697
  const operationData = buildOperationData(operationNode);
1467
1698
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1471,7 +1702,9 @@ const staticClassClientGenerator = defineGenerator({
1471
1702
  file,
1472
1703
  operations: [operationData]
1473
1704
  });
1474
- } else if (tag) {
1705
+ return acc;
1706
+ }
1707
+ if (tag) {
1475
1708
  const name = groupName;
1476
1709
  const file = resolver.resolveFile({
1477
1710
  name,
@@ -1480,7 +1713,7 @@ const staticClassClientGenerator = defineGenerator({
1480
1713
  }, {
1481
1714
  root,
1482
1715
  output,
1483
- group
1716
+ group: group ?? void 0
1484
1717
  });
1485
1718
  const operationData = buildOperationData(operationNode);
1486
1719
  const previous = acc.find((item) => item.file.path === file.path);
@@ -1534,23 +1767,31 @@ const staticClassClientGenerator = defineGenerator({
1534
1767
  zodImportsByFile: /* @__PURE__ */ new Map(),
1535
1768
  zodFilesByPath: /* @__PURE__ */ new Map()
1536
1769
  };
1537
- const hasFormData = ops.some((op) => op.node.requestBody?.content?.[0]?.contentType === "multipart/form-data");
1770
+ const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === "multipart/form-data") ?? false);
1538
1771
  return /* @__PURE__ */ jsxs(File, {
1539
1772
  baseName: file.baseName,
1540
1773
  path: file.path,
1541
1774
  meta: file.meta,
1542
- banner: resolver.resolveBanner(adapter.inputNode, {
1775
+ banner: resolver.resolveBanner(ctx.meta, {
1543
1776
  output,
1544
- config
1777
+ config,
1778
+ file: {
1779
+ path: file.path,
1780
+ baseName: file.baseName
1781
+ }
1545
1782
  }),
1546
- footer: resolver.resolveFooter(adapter.inputNode, {
1783
+ footer: resolver.resolveFooter(ctx.meta, {
1547
1784
  output,
1548
- config
1785
+ config,
1786
+ file: {
1787
+ path: file.path,
1788
+ baseName: file.baseName
1789
+ }
1549
1790
  }),
1550
1791
  children: [
1551
1792
  importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
1552
1793
  /* @__PURE__ */ jsx(File.Import, {
1553
- name: "fetch",
1794
+ name: "client",
1554
1795
  path: importPath
1555
1796
  }),
1556
1797
  /* @__PURE__ */ jsx(File.Import, {
@@ -1568,7 +1809,7 @@ const staticClassClientGenerator = defineGenerator({
1568
1809
  })
1569
1810
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1570
1811
  /* @__PURE__ */ jsx(File.Import, {
1571
- name: ["fetch"],
1812
+ name: ["client"],
1572
1813
  root: file.path,
1573
1814
  path: path.resolve(root, ".kubb/client.ts")
1574
1815
  }),
@@ -1634,39 +1875,75 @@ const staticClassClientGenerator = defineGenerator({
1634
1875
  //#endregion
1635
1876
  //#region src/resolvers/resolverClient.ts
1636
1877
  /**
1637
- * Naming convention resolver for client plugin.
1878
+ * Default resolver used by `@kubb/plugin-client`. Decides the names and file
1879
+ * paths for every generated client function or class. Functions and files use
1880
+ * camelCase; classes and tag groups use PascalCase.
1638
1881
  *
1639
- * Provides default naming helpers using camelCase for functions and file paths.
1882
+ * @example Resolve client function and class names
1883
+ * ```ts
1884
+ * import { resolverClient } from '@kubb/plugin-client'
1640
1885
  *
1641
- * @example
1642
- * `resolverClient.default('list pets', 'function') // 'listPets'`
1886
+ * resolverClient.default('list pets', 'function') // 'listPets'
1887
+ * resolverClient.resolveClassName('pet') // 'Pet'
1888
+ * resolverClient.resolveUrlName(operationNode) // 'getShowPetByIdUrl'
1889
+ * ```
1643
1890
  */
1644
- const resolverClient = defineResolver((ctx) => ({
1891
+ const resolverClient = defineResolver(() => ({
1645
1892
  name: "default",
1646
1893
  pluginName: "plugin-client",
1647
1894
  default(name, type) {
1648
- return camelCase(name, { isFile: type === "file" });
1895
+ const resolved = camelCase(name, { isFile: type === "file" });
1896
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1649
1897
  },
1650
1898
  resolveName(name) {
1651
- return ctx.default(name, "function");
1899
+ return this.default(name, "function");
1900
+ },
1901
+ resolvePathName(name, type) {
1902
+ return this.default(name, type);
1903
+ },
1904
+ resolveClassName(name) {
1905
+ return ensureValidVarName(pascalCase(name));
1906
+ },
1907
+ resolveGroupName(name) {
1908
+ return ensureValidVarName(pascalCase(name));
1909
+ },
1910
+ resolveClientPropertyName(name) {
1911
+ return ensureValidVarName(camelCase(name));
1912
+ },
1913
+ resolveUrlName(node) {
1914
+ const name = this.resolveName(node.operationId);
1915
+ return `get${name.charAt(0).toUpperCase()}${name.slice(1)}Url`;
1652
1916
  }
1653
1917
  }));
1654
1918
  //#endregion
1655
1919
  //#region src/plugin.ts
1656
1920
  /**
1657
- * Canonical plugin name for `@kubb/plugin-client`, used in driver lookups and warnings.
1921
+ * Canonical plugin name for `@kubb/plugin-client`. Used for driver lookups and
1922
+ * cross-plugin dependency references.
1658
1923
  */
1659
1924
  const pluginClientName = "plugin-client";
1660
1925
  /**
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`.
1926
+ * Generates one HTTP client function per OpenAPI operation. Each function has
1927
+ * typed path params, query params, body, and response, so callers use the API
1928
+ * like any other typed function. Ships with `axios` and `fetch` runtimes; bring
1929
+ * your own by setting `importPath`.
1664
1930
  *
1665
- * @example Client generator
1931
+ * @example
1666
1932
  * ```ts
1667
- * import pluginClient from '@kubb/plugin-client'
1933
+ * import { defineConfig } from 'kubb'
1934
+ * import { pluginTs } from '@kubb/plugin-ts'
1935
+ * import { pluginClient } from '@kubb/plugin-client'
1936
+ *
1668
1937
  * export default defineConfig({
1669
- * plugins: [pluginClient({ output: { path: 'clients' } })]
1938
+ * input: { path: './petStore.yaml' },
1939
+ * output: { path: './src/gen' },
1940
+ * plugins: [
1941
+ * pluginTs(),
1942
+ * pluginClient({
1943
+ * output: { path: './clients' },
1944
+ * client: 'fetch',
1945
+ * }),
1946
+ * ],
1670
1947
  * })
1671
1948
  * ```
1672
1949
  */
@@ -1674,24 +1951,21 @@ const pluginClient = definePlugin((options) => {
1674
1951
  const { output = {
1675
1952
  path: "clients",
1676
1953
  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;
1954
+ }, 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
1955
  const resolvedImportPath = importPath ?? (!bundle ? `@kubb/plugin-client/clients/${client}` : void 0);
1679
1956
  const selectedGenerators = options.generators ?? [
1680
1957
  clientType === "staticClass" ? staticClassClientGenerator : clientType === "class" ? classClientGenerator : clientGenerator,
1681
- group && clientType === "function" ? groupedClientGenerator : void 0,
1682
- operations ? operationsGenerator : void 0
1958
+ group && clientType === "function" ? groupedClientGenerator : null,
1959
+ operations ? operationsGenerator : null
1683
1960
  ].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;
1961
+ const groupConfig = createGroupConfig(group, {
1962
+ suffix: "Controller",
1963
+ honorName: true
1964
+ });
1691
1965
  return {
1692
1966
  name: pluginClientName,
1693
1967
  options,
1694
- dependencies: [pluginTsName, parser === "zod" ? pluginZodName : void 0].filter(Boolean),
1968
+ dependencies: [pluginTsName, parser === "zod" ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
1695
1969
  hooks: { "kubb:plugin:setup"(ctx) {
1696
1970
  const resolver = userResolver ? {
1697
1971
  ...resolverClient,