@kubb/plugin-fetch 5.0.0-beta.98 → 5.0.0

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.
package/dist/index.js CHANGED
@@ -1,11 +1,217 @@
1
1
  import "./rolldown-runtime-C0LytTxp.js";
2
+ import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin, macroSimplifyUnion } from "kubb/kit";
2
3
  import path from "node:path";
3
- import { Resolver, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
4
4
  import { createFunctionParameter, createFunctionParameters, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
5
5
  import { File, Function, jsxRenderer } from "kubb/jsx";
6
6
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
7
7
  import { pluginZodName } from "@kubb/plugin-zod";
8
8
  import { fileURLToPath } from "node:url";
9
+ //#region ../../internals/shared/src/params.ts
10
+ /**
11
+ * Drops parameters that share the same name, keeping the first.
12
+ *
13
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
14
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
15
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
16
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
17
+ */
18
+ function dedupeParams(params) {
19
+ const seen = /* @__PURE__ */ new Set();
20
+ return params.filter((param) => {
21
+ if (seen.has(param.name)) return false;
22
+ seen.add(param.name);
23
+ return true;
24
+ });
25
+ }
26
+ //#endregion
27
+ //#region ../../internals/shared/src/operation.ts
28
+ /**
29
+ * Builds the `ResolverFileParams` every operation generator passes to
30
+ * `resolver.file`: a file named `name`, tagged by the operation's first
31
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
32
+ * that was repeated at dozens of call sites across the client and query plugins.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
37
+ * ```
38
+ */
39
+ function operationFileEntry(node, name, extname = ".ts") {
40
+ return {
41
+ name,
42
+ extname,
43
+ tag: node.tags[0] ?? "default",
44
+ path: node.path
45
+ };
46
+ }
47
+ /**
48
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
49
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
50
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
51
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
52
+ *
53
+ * @example Cache `plugin-ts`'s file for the current operation
54
+ * ```ts
55
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
56
+ * ```
57
+ */
58
+ function resolveDependencyOperationFile(options) {
59
+ const { cache, node, resolver, root, output, group } = options;
60
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
61
+ ...operationFileEntry(node, node.operationId),
62
+ root,
63
+ output,
64
+ group: group ?? void 0
65
+ }));
66
+ }
67
+ function getOperationLink(node, link) {
68
+ if (!link) return null;
69
+ if (typeof link === "function") return link(node) ?? null;
70
+ return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
71
+ }
72
+ /**
73
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
74
+ * are present and the union, default, and form-data flags the client uses to pick one.
75
+ */
76
+ function buildContentTypeInfo(contentTypes) {
77
+ const isMultipleContentTypes = contentTypes.length > 1;
78
+ return {
79
+ contentTypes,
80
+ isMultipleContentTypes,
81
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
82
+ defaultContentType: contentTypes[0] ?? "application/json",
83
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
84
+ };
85
+ }
86
+ function getContentTypeInfo(node) {
87
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
88
+ }
89
+ /**
90
+ * The request-body counterpart for the primary success response: the content types it documents and
91
+ * whether several are present, so the client can let a caller pick which one to accept.
92
+ */
93
+ function getResponseContentTypeInfo(node) {
94
+ return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
95
+ }
96
+ /**
97
+ * Reads the single base content type of an operation's primary success response, lowercased and
98
+ * stripped of any `; charset=...` suffix. Returns `undefined` when the response declares zero or
99
+ * more than one content type, since neither case has a single type to act on.
100
+ */
101
+ function getPrimarySuccessContentType(node) {
102
+ const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
103
+ if (contentTypes.length !== 1) return void 0;
104
+ return contentTypes[0].split(";")[0].trim().toLowerCase();
105
+ }
106
+ /**
107
+ * Whether an operation streams its primary success response as Server-Sent Events
108
+ * (`text/event-stream`). The client generator uses this to return a typed event stream instead of a
109
+ * one-shot `RequestResult`.
110
+ */
111
+ function isEventStream(node) {
112
+ return getPrimarySuccessContentType(node) === "text/event-stream";
113
+ }
114
+ /**
115
+ * Derives the default `responseType` for an operation from its primary success response.
116
+ *
117
+ * Returns a value only when that response declares a single non-JSON content type. `text/event-stream`
118
+ * and other binary types (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`,
119
+ * `video/*`) map to a stream or `'blob'`, and other `text/*` maps to `'text'`. Otherwise `undefined`,
120
+ * leaving the runtime client's `Content-Type` auto-detection in charge.
121
+ */
122
+ function getResponseType(node) {
123
+ const baseType = getPrimarySuccessContentType(node);
124
+ if (!baseType) return void 0;
125
+ if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
126
+ if (baseType === "text/event-stream") return "stream";
127
+ if (baseType.startsWith("text/")) return "text";
128
+ if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
129
+ }
130
+ /**
131
+ * Which of the grouped request options an operation carries.
132
+ */
133
+ function getRequestGroups(node) {
134
+ const { path, query, header } = getOperationParameters(node);
135
+ return {
136
+ path: path.length > 0,
137
+ query: query.length > 0,
138
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
139
+ headers: header.length > 0
140
+ };
141
+ }
142
+ /**
143
+ * Resolves which grouped request options an operation carries together with whether each group
144
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
145
+ * required, matching the generated `RequestConfig` type.
146
+ */
147
+ function getRequestGroupOptionality(node) {
148
+ const groups = getRequestGroups(node);
149
+ const { path, query, header } = getOperationParameters(node);
150
+ const hasRequiredPath = path.some((param) => param.required);
151
+ const hasRequiredQuery = query.some((param) => param.required);
152
+ const hasRequiredHeader = header.some((param) => param.required);
153
+ return {
154
+ groups,
155
+ hasRequiredPath,
156
+ hasRequiredQuery,
157
+ hasRequiredHeader,
158
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
159
+ };
160
+ }
161
+ function buildOperationComments(node, options = {}) {
162
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
163
+ const linkComment = getOperationLink(node, link);
164
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
165
+ node.description && `@description ${node.description}`,
166
+ node.summary && `@summary ${node.summary}`,
167
+ linkComment,
168
+ node.deprecated && "@deprecated"
169
+ ] : [
170
+ node.description && `@description ${node.description}`,
171
+ node.summary && `@summary ${node.summary}`,
172
+ node.deprecated && "@deprecated",
173
+ linkComment
174
+ ]).filter((comment) => Boolean(comment));
175
+ if (!splitLines) return filteredComments;
176
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
177
+ }
178
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
179
+ /**
180
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
181
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
182
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
183
+ * same parameters once per plugin.
184
+ */
185
+ function getOperationParameters(node) {
186
+ const cached = operationParameterGroupsByNode.get(node);
187
+ if (cached) return cached;
188
+ const groups = {
189
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
190
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
191
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
192
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
193
+ };
194
+ operationParameterGroupsByNode.set(node, groups);
195
+ return groups;
196
+ }
197
+ function getStatusCodeNumber(statusCode) {
198
+ const code = Number(statusCode);
199
+ return Number.isNaN(code) ? null : code;
200
+ }
201
+ function isSuccessStatusCode(statusCode) {
202
+ const code = getStatusCodeNumber(statusCode);
203
+ return code !== null && code >= 200 && code < 300;
204
+ }
205
+ function getSuccessResponses(responses) {
206
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
207
+ }
208
+ function getOperationSuccessResponses(node) {
209
+ return getSuccessResponses(node.responses);
210
+ }
211
+ function getPrimarySuccessResponse(node) {
212
+ return getOperationSuccessResponses(node)[0] ?? null;
213
+ }
214
+ //#endregion
9
215
  //#region ../../internals/utils/src/casing.ts
10
216
  /**
11
217
  * Shared implementation for camelCase and PascalCase conversion.
@@ -201,342 +407,6 @@ function buildJSDoc(comments, options = {}) {
201
407
  return `/**\n${comments.map((c) => `${indent}${c}`).join("\n")}\n */${suffix}`;
202
408
  }
203
409
  //#endregion
204
- //#region ../../internals/utils/src/url.ts
205
- function transformParam(raw, casing) {
206
- const param = isValidVarName(raw) ? raw : camelCase(raw);
207
- return casing === "camelcase" ? camelCase(param) : param;
208
- }
209
- function toParamsObject(path, { replacer, casing } = {}) {
210
- const params = {};
211
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
212
- const param = transformParam(match[1], casing);
213
- const key = replacer ? replacer(param) : param;
214
- params[key] = key;
215
- }
216
- return Object.keys(params).length > 0 ? params : null;
217
- }
218
- /**
219
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
220
- */
221
- var Url = class Url {
222
- /**
223
- * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
224
- *
225
- * @example
226
- * Url.canParse('https://petstore.swagger.io/v2') // true
227
- * Url.canParse('/pet/{petId}') // false
228
- */
229
- static canParse(url, base) {
230
- return URL.canParse(url, base);
231
- }
232
- /**
233
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
234
- *
235
- * @example
236
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
237
- */
238
- static toPath(path) {
239
- return path.replace(/\{([^}]+)\}/g, ":$1");
240
- }
241
- /**
242
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
243
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
244
- * key.
245
- *
246
- * @example
247
- * Url.toCasedTemplate('/projects/{project_id}', { casing: 'camelcase' }) // '/projects/{projectId}'
248
- */
249
- static toCasedTemplate(path, { casing } = {}) {
250
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name, casing)}}`);
251
- }
252
- /**
253
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
254
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
255
- * and `casing` controls parameter identifier casing.
256
- *
257
- * @example
258
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
259
- *
260
- * @example
261
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
262
- */
263
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
264
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
265
- if (i % 2 === 0) return part;
266
- const param = transformParam(part, casing);
267
- return `\${${replacer ? replacer(param) : param}}`;
268
- }).join("");
269
- return `\`${prefix ?? ""}${result}\``;
270
- }
271
- /**
272
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
273
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
274
- * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
275
- * literal. Shared by the client and cypress generators that pass a grouped `path` object.
276
- *
277
- * @example
278
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
279
- */
280
- static toGroupedTemplateString(path, { prefix } = {}) {
281
- return Url.toTemplateString(path, {
282
- prefix,
283
- casing: "camelcase",
284
- replacer: (name) => `path.${name}`
285
- });
286
- }
287
- /**
288
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
289
- * expression when `stringify` is set.
290
- *
291
- * @example
292
- * Url.toObject('/pet/{petId}')
293
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
294
- */
295
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
296
- const object = {
297
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
298
- replacer,
299
- casing
300
- }),
301
- params: toParamsObject(path, {
302
- replacer,
303
- casing
304
- })
305
- };
306
- if (stringify) {
307
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
308
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
309
- return `{ url: '${object.url}' }`;
310
- }
311
- return object;
312
- }
313
- };
314
- //#endregion
315
- //#region ../../internals/shared/src/params.ts
316
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
317
- /**
318
- * Applies camelCase to parameter names and returns a new array without mutating the input.
319
- *
320
- * Run it before handing parameters to schema builders so output property keys get the right casing
321
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
322
- * original array is returned unchanged. Results are cached per input array.
323
- */
324
- function caseParams(params, casing) {
325
- if (!casing) return params;
326
- const cached = caseParamsCache.get(params);
327
- if (cached) return cached;
328
- const result = params.map((param) => ({
329
- ...param,
330
- name: camelCase(param.name)
331
- }));
332
- caseParamsCache.set(params, result);
333
- return result;
334
- }
335
- /**
336
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
337
- *
338
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
339
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
340
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
341
- * identity so the resulting group is collision-free regardless of the names each caller carries.
342
- */
343
- function dedupeByCasedName(params) {
344
- const seen = /* @__PURE__ */ new Set();
345
- return params.filter((param) => {
346
- const key = camelCase(param.name);
347
- if (seen.has(key)) return false;
348
- seen.add(key);
349
- return true;
350
- });
351
- }
352
- function buildParamsMapping(originalParams, mappedParams) {
353
- const mapping = {};
354
- let hasChanged = false;
355
- originalParams.forEach((param, i) => {
356
- const mappedName = mappedParams[i]?.name ?? param.name;
357
- mapping[param.name] = mappedName;
358
- if (param.name !== mappedName) hasChanged = true;
359
- });
360
- return hasChanged ? mapping : null;
361
- }
362
- function toAccess(object, name) {
363
- return isValidVarName(name) ? `${object}.${name}` : `${object}[${JSON.stringify(name)}]`;
364
- }
365
- /**
366
- * Renders the object-literal expression that renames the camelCased keys of a grouped request
367
- * option back to the names the OpenAPI document declares, guarded so an omitted optional group
368
- * stays omitted. Shared by the client and cypress generators, which pass a `buildParamsMapping`
369
- * result and the source expression to read the keys from.
370
- *
371
- * @example
372
- * ```ts
373
- * buildParamsRemapExpression({ source: 'config.query', mapping: { include_deleted: 'includeDeleted' } })
374
- * // 'config.query ? { "include_deleted": config.query.includeDeleted } : config.query'
375
- * ```
376
- */
377
- function buildParamsRemapExpression({ source, mapping }) {
378
- return `${source} ? { ${Object.entries(mapping).map(([originalName, casedName]) => `${JSON.stringify(originalName)}: ${toAccess(source, casedName)}`).join(", ")} } : ${source}`;
379
- }
380
- //#endregion
381
- //#region ../../internals/shared/src/operation.ts
382
- /**
383
- * Builds the `ResolverFileParams` every operation generator passes to
384
- * `resolver.file`: a file named `name`, tagged by the operation's first
385
- * tag (or `'default'`), at the operation's path. Centralizes the entry object
386
- * that was repeated at dozens of call sites across the client and query plugins.
387
- *
388
- * @example
389
- * ```ts
390
- * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
391
- * ```
392
- */
393
- function operationFileEntry(node, name, extname = ".ts") {
394
- return {
395
- name,
396
- extname,
397
- tag: node.tags[0] ?? "default",
398
- path: node.path
399
- };
400
- }
401
- function getOperationLink(node, link) {
402
- if (!link) return null;
403
- if (typeof link === "function") return link(node) ?? null;
404
- if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
405
- return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
406
- }
407
- /**
408
- * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
409
- * are present and the union, default, and form-data flags the client uses to pick one.
410
- */
411
- function buildContentTypeInfo(contentTypes) {
412
- const isMultipleContentTypes = contentTypes.length > 1;
413
- return {
414
- contentTypes,
415
- isMultipleContentTypes,
416
- contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
417
- defaultContentType: contentTypes[0] ?? "application/json",
418
- hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
419
- };
420
- }
421
- function getContentTypeInfo(node) {
422
- return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
423
- }
424
- /**
425
- * The request-body counterpart for the primary success response: the content types it documents and
426
- * whether several are present, so the client can let a caller pick which one to accept.
427
- */
428
- function getResponseContentTypeInfo(node) {
429
- return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
430
- }
431
- /**
432
- * Reads the single base content type of an operation's primary success response, lowercased and
433
- * stripped of any `; charset=...` suffix. Returns `undefined` when the response declares zero or
434
- * more than one content type, since neither case has a single type to act on.
435
- */
436
- function getPrimarySuccessContentType(node) {
437
- const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
438
- if (contentTypes.length !== 1) return void 0;
439
- return contentTypes[0].split(";")[0].trim().toLowerCase();
440
- }
441
- /**
442
- * Whether an operation streams its primary success response as Server-Sent Events
443
- * (`text/event-stream`). The client generator uses this to return a typed event stream instead of a
444
- * one-shot `RequestResult`.
445
- */
446
- function isEventStream(node) {
447
- return getPrimarySuccessContentType(node) === "text/event-stream";
448
- }
449
- /**
450
- * Derives the default `responseType` for an operation from its primary success response.
451
- *
452
- * Returns a value only when that response declares a single non-JSON content type. `text/event-stream`
453
- * and other binary types (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`,
454
- * `video/*`) map to a stream or `'blob'`, and other `text/*` maps to `'text'`. Otherwise `undefined`,
455
- * leaving the runtime client's `Content-Type` auto-detection in charge.
456
- */
457
- function getResponseType(node) {
458
- const baseType = getPrimarySuccessContentType(node);
459
- if (!baseType) return void 0;
460
- if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
461
- if (baseType === "text/event-stream") return "stream";
462
- if (baseType.startsWith("text/")) return "text";
463
- if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
464
- }
465
- /**
466
- * Which of the grouped request options an operation carries.
467
- */
468
- function getRequestGroups(node) {
469
- const { path, query, header } = getOperationParameters(node);
470
- return {
471
- path: path.length > 0,
472
- query: query.length > 0,
473
- body: Boolean(node.requestBody?.content?.[0]?.schema),
474
- headers: header.length > 0
475
- };
476
- }
477
- /**
478
- * Resolves which grouped request options an operation carries together with whether each group
479
- * holds a required member. The grouped parameter stays optional only when nothing inside it is
480
- * required, matching the generated `RequestConfig` type.
481
- */
482
- function getRequestGroupOptionality(node) {
483
- const groups = getRequestGroups(node);
484
- const { path, query, header } = getOperationParameters(node);
485
- const hasRequiredPath = path.some((param) => param.required);
486
- const hasRequiredQuery = query.some((param) => param.required);
487
- const hasRequiredHeader = header.some((param) => param.required);
488
- return {
489
- groups,
490
- hasRequiredPath,
491
- hasRequiredQuery,
492
- hasRequiredHeader,
493
- isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
494
- };
495
- }
496
- function buildOperationComments(node, options = {}) {
497
- const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
498
- const linkComment = getOperationLink(node, link);
499
- const filteredComments = (linkPosition === "beforeDeprecated" ? [
500
- node.description && `@description ${node.description}`,
501
- node.summary && `@summary ${node.summary}`,
502
- linkComment,
503
- node.deprecated && "@deprecated"
504
- ] : [
505
- node.description && `@description ${node.description}`,
506
- node.summary && `@summary ${node.summary}`,
507
- node.deprecated && "@deprecated",
508
- linkComment
509
- ]).filter((comment) => Boolean(comment));
510
- if (!splitLines) return filteredComments;
511
- return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
512
- }
513
- function getOperationParameters(node, options = {}) {
514
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
515
- return {
516
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
517
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
518
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
519
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
520
- };
521
- }
522
- function getStatusCodeNumber(statusCode) {
523
- const code = Number(statusCode);
524
- return Number.isNaN(code) ? null : code;
525
- }
526
- function isSuccessStatusCode(statusCode) {
527
- const code = getStatusCodeNumber(statusCode);
528
- return code !== null && code >= 200 && code < 300;
529
- }
530
- function getSuccessResponses(responses) {
531
- return responses.filter((response) => isSuccessStatusCode(response.statusCode));
532
- }
533
- function getOperationSuccessResponses(node) {
534
- return getSuccessResponses(node.responses);
535
- }
536
- function getPrimarySuccessResponse(node) {
537
- return getOperationSuccessResponses(node)[0] ?? null;
538
- }
539
- //#endregion
540
410
  //#region ../../internals/shared/src/group.ts
541
411
  /**
542
412
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -568,38 +438,6 @@ function createGroupConfig(group) {
568
438
  };
569
439
  }
570
440
  //#endregion
571
- //#region ../../internals/client/src/builders/paramsRemap.ts
572
- /**
573
- * Builds the call-config entries that rename the camelCased `query` and `headers` keys back to the
574
- * names the OpenAPI document declares, so the wire format follows the spec while the generated
575
- * types keep camelCase keys. Returns an empty array when no name changes. Path parameters need no
576
- * remap because the URL template placeholders are renamed in sync with the `path` keys. Emit the
577
- * entries after the `...config` spread so they override the camelCased groups the caller passes in.
578
- *
579
- * @example
580
- * ```ts
581
- * // a query param named include_deleted in the spec
582
- * buildParamsRemap({ node }) // ['query: config.query ? { "include_deleted": config.query.includeDeleted } : config.query']
583
- * ```
584
- */
585
- function buildParamsRemap({ node }) {
586
- if (!ast.isHttpOperationNode(node)) return [];
587
- const original = getOperationParameters(node, { paramsCasing: "original" });
588
- const cased = getOperationParameters(node);
589
- const queryMapping = buildParamsMapping(original.query, cased.query);
590
- const headerMapping = buildParamsMapping(original.header, cased.header);
591
- const entries = [];
592
- if (queryMapping) entries.push(`query: ${buildParamsRemapExpression({
593
- source: "config.query",
594
- mapping: queryMapping
595
- })}`);
596
- if (headerMapping) entries.push(`headers: ${buildParamsRemapExpression({
597
- source: "config.headers",
598
- mapping: headerMapping
599
- })}`);
600
- return entries;
601
- }
602
- //#endregion
603
441
  //#region ../../internals/client/src/builders/generics.ts
604
442
  /**
605
443
  * Builds the `RequestResult` generic arguments for one operation: the plugin-ts per-status responses
@@ -831,11 +669,10 @@ function buildCallConfig({ node, validator, zodResolver, security }) {
831
669
  const securityLiteral = buildSecurityMetadata({ security });
832
670
  return `{ ${[
833
671
  `method: '${node.method.toUpperCase()}'`,
834
- `url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
672
+ `url: '${node.path}'`,
835
673
  securityLiteral ? `security: ${securityLiteral}` : null,
836
674
  validatorLiteral,
837
- "...config",
838
- ...buildParamsRemap({ node })
675
+ "...config"
839
676
  ].filter(Boolean).join(", ")} }`;
840
677
  }
841
678
  /**
@@ -961,14 +798,13 @@ function Operation({ name, node, tsResolver, zodResolver, validator, security, i
961
798
  const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
962
799
  const callConfig = `{ ${[
963
800
  `method: '${node.method.toUpperCase()}'`,
964
- `url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
801
+ `url: '${node.path}'`,
965
802
  securityLiteral ? `security: ${securityLiteral}` : null,
966
803
  stylesLiteral ? `styles: ${stylesLiteral}` : null,
967
804
  validatorLiteral,
968
805
  contentTypeLiteral,
969
806
  responseTypeLiteral,
970
- "...config",
971
- ...buildParamsRemap({ node })
807
+ "...config"
972
808
  ].filter(Boolean).join(", ")} }`;
973
809
  const eventType = `SuccessOf<${tsResolver.response.responses(node)}>`;
974
810
  const returnType = eventStream ? `Promise<EventStreamResult<${eventType}>>` : signature.returnType;
@@ -1032,6 +868,125 @@ function SdkClient({ name, isExportable = true, isIndexable = true, operations,
1032
868
  });
1033
869
  }
1034
870
  //#endregion
871
+ //#region ../../internals/client/src/generators/clientGenerator.tsx
872
+ /**
873
+ * Builds the built-in per-operation generator shared by the client plugins (`@kubb/plugin-fetch`,
874
+ * `@kubb/plugin-axios`). Emits one async function per OpenAPI operation using the shared
875
+ * `Operation` component: a grouped `<Name>Request` type and a function that forwards a single
876
+ * `options` object to the bundled `client` and returns the `RequestResult`. Only the generator
877
+ * `name` differs between plugins; every other resolution, import, and rendering step is identical.
878
+ */
879
+ function createClientGenerator(name) {
880
+ return defineGenerator({
881
+ name,
882
+ renderer: jsxRenderer,
883
+ operation(node, ctx) {
884
+ if (!ast.isHttpOperationNode(node)) return null;
885
+ const { config, driver, resolver, root } = ctx;
886
+ const { output, validator, group } = ctx.options;
887
+ const pluginTs = driver.getPlugin(pluginTsName);
888
+ if (!pluginTs) return null;
889
+ const tsResolver = driver.getResolver(pluginTsName);
890
+ const pluginZod = resolveResponseValidator(validator) === "zod" || resolveRequestValidator(validator) === "zod" ? driver.getPlugin(pluginZodName) : null;
891
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
892
+ const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
893
+ const importedTypeNames = [tsResolver.response.options(node), tsResolver.response.responses(node)];
894
+ const importedZodNames = zodResolver ? [
895
+ resolveResponseValidator(validator) === "zod" ? zodResolver.response.response?.(node) : null,
896
+ resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
897
+ resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body?.(node) : null
898
+ ].filter((name) => Boolean(name)) : [];
899
+ const meta = {
900
+ name: resolver.name(node.operationId),
901
+ file: resolver.file({
902
+ ...operationFileEntry(node, node.operationId),
903
+ root,
904
+ output,
905
+ group: group ?? void 0
906
+ }),
907
+ fileTs: resolveDependencyOperationFile({
908
+ cache: ctx.cache,
909
+ node,
910
+ resolver: tsResolver,
911
+ root,
912
+ output: pluginTs.options?.output ?? output,
913
+ group: pluginTs.options?.group
914
+ }),
915
+ fileZod: zodResolver && pluginZod?.options ? zodResolver.file({
916
+ ...operationFileEntry(node, node.operationId),
917
+ root,
918
+ output: pluginZod.options.output ?? output,
919
+ group: pluginZod.options?.group ?? void 0
920
+ }) : null
921
+ };
922
+ const security = getOperationSecurity({
923
+ document: ctx.adapter.document,
924
+ method: node.method,
925
+ path: node.path
926
+ });
927
+ const clientPath = path.resolve(root, ".kubb/client.ts");
928
+ const eventStream = isEventStream(node);
929
+ return /* @__PURE__ */ jsxs(File, {
930
+ baseName: meta.file.baseName,
931
+ path: meta.file.path,
932
+ meta: meta.file.meta,
933
+ banner: resolver.default.banner(ctx.meta, {
934
+ output,
935
+ config,
936
+ file: {
937
+ path: meta.file.path,
938
+ baseName: meta.file.baseName
939
+ }
940
+ }),
941
+ footer: resolver.default.footer(ctx.meta, {
942
+ output,
943
+ config,
944
+ file: {
945
+ path: meta.file.path,
946
+ baseName: meta.file.baseName
947
+ }
948
+ }),
949
+ children: [
950
+ /* @__PURE__ */ jsx(File.Import, {
951
+ name: eventStream ? ["client", "toEventStream"] : ["client"],
952
+ root: meta.file.path,
953
+ path: clientPath
954
+ }),
955
+ /* @__PURE__ */ jsx(File.Import, {
956
+ name: eventStream ? [
957
+ "Options",
958
+ "EventStreamResult",
959
+ "SuccessOf"
960
+ ] : ["Options", "RequestResult"],
961
+ root: meta.file.path,
962
+ path: clientPath,
963
+ isTypeOnly: true
964
+ }),
965
+ meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
966
+ name: Array.from(new Set(importedTypeNames)),
967
+ root: meta.file.path,
968
+ path: meta.fileTs.path,
969
+ isTypeOnly: true
970
+ }),
971
+ meta.fileZod && importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
972
+ name: importedZodNames,
973
+ root: meta.file.path,
974
+ path: meta.fileZod.path
975
+ }),
976
+ /* @__PURE__ */ jsx(Operation, {
977
+ name: meta.name,
978
+ node,
979
+ tsResolver,
980
+ zodResolver,
981
+ validator,
982
+ security
983
+ })
984
+ ]
985
+ });
986
+ }
987
+ });
988
+ }
989
+ //#endregion
1035
990
  //#region ../../internals/client/src/components/SdkFacade.tsx
1036
991
  /**
1037
992
  * Renders a composed root SDK class that instantiates every tag client from one shared config, so
@@ -1061,7 +1016,7 @@ function resolveTypeImportNames(node, tsResolver) {
1061
1016
  return [tsResolver.response.options(node), tsResolver.response.responses(node)];
1062
1017
  }
1063
1018
  function resolveZodImportNames(node, zodResolver, validator) {
1064
- const { query: queryParams } = getOperationParameters(node, { paramsCasing: "original" });
1019
+ const { query: queryParams } = getOperationParameters(node);
1065
1020
  return [
1066
1021
  resolveResponseValidator(validator) === "zod" ? zodResolver.response.response(node) : null,
1067
1022
  resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
@@ -1301,7 +1256,7 @@ function createSdkGenerator() {
1301
1256
  * drops union members a broader scalar already covers, keeping the generated response and error
1302
1257
  * unions tidy. A plugin wires them with `ctx.setMacros([...defaultMacros, ...userMacros])`.
1303
1258
  */
1304
- const defaultMacros = [ast.macroSimplifyUnion];
1259
+ const defaultMacros = [macroSimplifyUnion];
1305
1260
  //#endregion
1306
1261
  //#region ../../internals/client/src/resolver.ts
1307
1262
  /**
@@ -1330,115 +1285,10 @@ const resolverClient = createResolver({
1330
1285
  //#region src/generators/clientGenerator.tsx
1331
1286
  /**
1332
1287
  * Built-in operation generator for `@kubb/plugin-fetch`. Emits one async function per OpenAPI
1333
- * operation using the shared `Operation` component: a grouped `<Name>Request` type and a function that
1334
- * forwards a single `options` object to the bundled `client` and returns the `RequestResult`.
1288
+ * operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
1289
+ * that forwards a single `options` object to the bundled `client` and returns the `RequestResult`.
1335
1290
  */
1336
- const clientGenerator = defineGenerator({
1337
- name: "fetch",
1338
- renderer: jsxRenderer,
1339
- operation(node, ctx) {
1340
- if (!ast.isHttpOperationNode(node)) return null;
1341
- const { config, driver, resolver, root } = ctx;
1342
- const { output, validator, group } = ctx.options;
1343
- const pluginTs = driver.getPlugin(pluginTsName);
1344
- if (!pluginTs) return null;
1345
- const tsResolver = driver.getResolver(pluginTsName);
1346
- const pluginZod = resolveResponseValidator(validator) === "zod" || resolveRequestValidator(validator) === "zod" ? driver.getPlugin(pluginZodName) : null;
1347
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1348
- const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
1349
- const importedTypeNames = [tsResolver.response.options(node), tsResolver.response.responses(node)];
1350
- const importedZodNames = zodResolver ? [
1351
- resolveResponseValidator(validator) === "zod" ? zodResolver.response.response?.(node) : null,
1352
- resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
1353
- resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body?.(node) : null
1354
- ].filter((name) => Boolean(name)) : [];
1355
- const meta = {
1356
- name: resolver.name(node.operationId),
1357
- file: resolver.file({
1358
- ...operationFileEntry(node, node.operationId),
1359
- root,
1360
- output,
1361
- group: group ?? void 0
1362
- }),
1363
- fileTs: tsResolver.file({
1364
- ...operationFileEntry(node, node.operationId),
1365
- root,
1366
- output: pluginTs.options?.output ?? output,
1367
- group: pluginTs.options?.group ?? void 0
1368
- }),
1369
- fileZod: zodResolver && pluginZod?.options ? zodResolver.file({
1370
- ...operationFileEntry(node, node.operationId),
1371
- root,
1372
- output: pluginZod.options.output ?? output,
1373
- group: pluginZod.options?.group ?? void 0
1374
- }) : null
1375
- };
1376
- const security = getOperationSecurity({
1377
- document: ctx.adapter.document,
1378
- method: node.method,
1379
- path: node.path
1380
- });
1381
- const clientPath = path.resolve(root, ".kubb/client.ts");
1382
- const eventStream = isEventStream(node);
1383
- return /* @__PURE__ */ jsxs(File, {
1384
- baseName: meta.file.baseName,
1385
- path: meta.file.path,
1386
- meta: meta.file.meta,
1387
- banner: resolver.default.banner(ctx.meta, {
1388
- output,
1389
- config,
1390
- file: {
1391
- path: meta.file.path,
1392
- baseName: meta.file.baseName
1393
- }
1394
- }),
1395
- footer: resolver.default.footer(ctx.meta, {
1396
- output,
1397
- config,
1398
- file: {
1399
- path: meta.file.path,
1400
- baseName: meta.file.baseName
1401
- }
1402
- }),
1403
- children: [
1404
- /* @__PURE__ */ jsx(File.Import, {
1405
- name: eventStream ? ["client", "toEventStream"] : ["client"],
1406
- root: meta.file.path,
1407
- path: clientPath
1408
- }),
1409
- /* @__PURE__ */ jsx(File.Import, {
1410
- name: eventStream ? [
1411
- "Options",
1412
- "EventStreamResult",
1413
- "SuccessOf"
1414
- ] : ["Options", "RequestResult"],
1415
- root: meta.file.path,
1416
- path: clientPath,
1417
- isTypeOnly: true
1418
- }),
1419
- meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1420
- name: Array.from(new Set(importedTypeNames)),
1421
- root: meta.file.path,
1422
- path: meta.fileTs.path,
1423
- isTypeOnly: true
1424
- }),
1425
- meta.fileZod && importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1426
- name: importedZodNames,
1427
- root: meta.file.path,
1428
- path: meta.fileZod.path
1429
- }),
1430
- /* @__PURE__ */ jsx(Operation, {
1431
- name: meta.name,
1432
- node,
1433
- tsResolver,
1434
- zodResolver,
1435
- validator,
1436
- security
1437
- })
1438
- ]
1439
- });
1440
- }
1441
- });
1291
+ const clientGenerator = createClientGenerator("fetch");
1442
1292
  //#endregion
1443
1293
  //#region src/templates.ts
1444
1294
  /** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */