@kubb/plugin-vue-query 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.cjs CHANGED
@@ -23,335 +23,31 @@ var __copyProps = (to, from, except, desc) => {
23
23
  }
24
24
  return to;
25
25
  };
26
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
26
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
27
27
  value: mod,
28
28
  enumerable: true
29
29
  }) : target, mod));
30
30
  //#endregion
31
+ let kubb_kit = require("kubb/kit");
31
32
  let node_path = require("node:path");
32
33
  node_path = __toESM(node_path, 1);
33
- let kubb_kit = require("kubb/kit");
34
34
  let _kubb_plugin_ts = require("@kubb/plugin-ts");
35
35
  let kubb_jsx = require("kubb/jsx");
36
36
  let kubb_jsx_jsx_runtime = require("kubb/jsx/jsx-runtime");
37
- //#region ../../internals/utils/src/casing.ts
38
- /**
39
- * Shared implementation for camelCase and PascalCase conversion.
40
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
41
- * and capitalizes each word according to `pascal`.
42
- *
43
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
44
- */
45
- function toCamelOrPascal(text, pascal) {
46
- return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
47
- if (word.length > 1 && word === word.toUpperCase()) return word;
48
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
49
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
50
- }
51
- /**
52
- * Converts `text` to camelCase.
53
- *
54
- * @example Word boundaries
55
- * `camelCase('hello-world') // 'helloWorld'`
56
- *
57
- * @example With a prefix
58
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
59
- */
60
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
61
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
62
- }
63
- /**
64
- * Uppercases only the first character of `text`, leaving the rest untouched.
65
- * Unlike {@link pascalCase} it never re-splits word boundaries or strips characters.
66
- *
67
- * @example
68
- * `capitalize('getPetById') // 'GetPetById'`
69
- */
70
- function capitalize(text) {
71
- return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
72
- }
73
- //#endregion
74
- //#region ../../internals/utils/src/reserved.ts
75
- /**
76
- * JavaScript and Java reserved words.
77
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
78
- */
79
- const reservedWords = /* @__PURE__ */ new Set([
80
- "abstract",
81
- "arguments",
82
- "boolean",
83
- "break",
84
- "byte",
85
- "case",
86
- "catch",
87
- "char",
88
- "class",
89
- "const",
90
- "continue",
91
- "debugger",
92
- "default",
93
- "delete",
94
- "do",
95
- "double",
96
- "else",
97
- "enum",
98
- "eval",
99
- "export",
100
- "extends",
101
- "false",
102
- "final",
103
- "finally",
104
- "float",
105
- "for",
106
- "function",
107
- "goto",
108
- "if",
109
- "implements",
110
- "import",
111
- "in",
112
- "instanceof",
113
- "int",
114
- "interface",
115
- "let",
116
- "long",
117
- "native",
118
- "new",
119
- "null",
120
- "package",
121
- "private",
122
- "protected",
123
- "public",
124
- "return",
125
- "short",
126
- "static",
127
- "super",
128
- "switch",
129
- "synchronized",
130
- "this",
131
- "throw",
132
- "throws",
133
- "transient",
134
- "true",
135
- "try",
136
- "typeof",
137
- "var",
138
- "void",
139
- "volatile",
140
- "while",
141
- "with",
142
- "yield",
143
- "Array",
144
- "Date",
145
- "hasOwnProperty",
146
- "Infinity",
147
- "isFinite",
148
- "isNaN",
149
- "isPrototypeOf",
150
- "length",
151
- "Math",
152
- "name",
153
- "NaN",
154
- "Number",
155
- "Object",
156
- "prototype",
157
- "String",
158
- "toString",
159
- "undefined",
160
- "valueOf"
161
- ]);
162
- /**
163
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
164
- *
165
- * @example
166
- * ```ts
167
- * isValidVarName('status') // true
168
- * isValidVarName('class') // false (reserved word)
169
- * isValidVarName('42foo') // false (starts with digit)
170
- * ```
171
- */
172
- function isValidVarName(name) {
173
- if (!name || reservedWords.has(name)) return false;
174
- return isIdentifier(name);
175
- }
176
- /**
177
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
178
- *
179
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
180
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
181
- * deciding whether an object key needs quoting.
182
- *
183
- * @example
184
- * ```ts
185
- * isIdentifier('name') // true
186
- * isIdentifier('x-total')// false
187
- * ```
188
- */
189
- function isIdentifier(name) {
190
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
191
- }
192
- //#endregion
193
- //#region ../../internals/utils/src/strings.ts
194
- /**
195
- * Renders a dotted path or string array as an optional-chaining accessor expression rooted at
196
- * `accessor`. Returns `null` for an empty path.
197
- *
198
- * @example
199
- * ```ts
200
- * getNestedAccessor('pagination.next.id', 'lastPage')
201
- * // "lastPage?.['pagination']?.['next']?.['id']"
202
- * ```
203
- */
204
- function getNestedAccessor(param, accessor) {
205
- const parts = Array.isArray(param) ? param : param.split(".");
206
- if (parts.length === 0 || parts.length === 1 && parts[0] === "") return null;
207
- return `${accessor}?.['${`${parts.join("']?.['")}']`}`;
208
- }
209
- //#endregion
210
- //#region ../../internals/utils/src/url.ts
211
- function transformParam(raw, casing) {
212
- const param = isValidVarName(raw) ? raw : camelCase(raw);
213
- return casing === "camelcase" ? camelCase(param) : param;
214
- }
215
- function toParamsObject(path, { replacer, casing } = {}) {
216
- const params = {};
217
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
218
- const param = transformParam(match[1], casing);
219
- const key = replacer ? replacer(param) : param;
220
- params[key] = key;
221
- }
222
- return Object.keys(params).length > 0 ? params : null;
223
- }
224
- /**
225
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
226
- */
227
- var Url = class Url {
228
- /**
229
- * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
230
- *
231
- * @example
232
- * Url.canParse('https://petstore.swagger.io/v2') // true
233
- * Url.canParse('/pet/{petId}') // false
234
- */
235
- static canParse(url, base) {
236
- return URL.canParse(url, base);
237
- }
238
- /**
239
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
240
- *
241
- * @example
242
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
243
- */
244
- static toPath(path) {
245
- return path.replace(/\{([^}]+)\}/g, ":$1");
246
- }
247
- /**
248
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
249
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
250
- * key.
251
- *
252
- * @example
253
- * Url.toCasedTemplate('/projects/{project_id}', { casing: 'camelcase' }) // '/projects/{projectId}'
254
- */
255
- static toCasedTemplate(path, { casing } = {}) {
256
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name, casing)}}`);
257
- }
258
- /**
259
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
260
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
261
- * and `casing` controls parameter identifier casing.
262
- *
263
- * @example
264
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
265
- *
266
- * @example
267
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
268
- */
269
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
270
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
271
- if (i % 2 === 0) return part;
272
- const param = transformParam(part, casing);
273
- return `\${${replacer ? replacer(param) : param}}`;
274
- }).join("");
275
- return `\`${prefix ?? ""}${result}\``;
276
- }
277
- /**
278
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
279
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
280
- * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
281
- * literal. Shared by the client and cypress generators that pass a grouped `path` object.
282
- *
283
- * @example
284
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
285
- */
286
- static toGroupedTemplateString(path, { prefix } = {}) {
287
- return Url.toTemplateString(path, {
288
- prefix,
289
- casing: "camelcase",
290
- replacer: (name) => `path.${name}`
291
- });
292
- }
293
- /**
294
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
295
- * expression when `stringify` is set.
296
- *
297
- * @example
298
- * Url.toObject('/pet/{petId}')
299
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
300
- */
301
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
302
- const object = {
303
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
304
- replacer,
305
- casing
306
- }),
307
- params: toParamsObject(path, {
308
- replacer,
309
- casing
310
- })
311
- };
312
- if (stringify) {
313
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
314
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
315
- return `{ url: '${object.url}' }`;
316
- }
317
- return object;
318
- }
319
- };
320
- //#endregion
321
37
  //#region ../../internals/shared/src/params.ts
322
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
323
38
  /**
324
- * Applies camelCase to parameter names and returns a new array without mutating the input.
39
+ * Drops parameters that share the same name, keeping the first.
325
40
  *
326
- * Run it before handing parameters to schema builders so output property keys get the right casing
327
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
328
- * original array is returned unchanged. Results are cached per input array.
41
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
42
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
43
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
44
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
329
45
  */
330
- function caseParams(params, casing) {
331
- if (!casing) return params;
332
- const cached = caseParamsCache.get(params);
333
- if (cached) return cached;
334
- const result = params.map((param) => ({
335
- ...param,
336
- name: camelCase(param.name)
337
- }));
338
- caseParamsCache.set(params, result);
339
- return result;
340
- }
341
- /**
342
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
343
- *
344
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
345
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
346
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
347
- * identity so the resulting group is collision-free regardless of the names each caller carries.
348
- */
349
- function dedupeByCasedName(params) {
46
+ function dedupeParams(params) {
350
47
  const seen = /* @__PURE__ */ new Set();
351
48
  return params.filter((param) => {
352
- const key = camelCase(param.name);
353
- if (seen.has(key)) return false;
354
- seen.add(key);
49
+ if (seen.has(param.name)) return false;
50
+ seen.add(param.name);
355
51
  return true;
356
52
  });
357
53
  }
@@ -376,11 +72,30 @@ function operationFileEntry(node, name, extname = ".ts") {
376
72
  path: node.path
377
73
  };
378
74
  }
75
+ /**
76
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
77
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
78
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
79
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
80
+ *
81
+ * @example Cache `plugin-ts`'s file for the current operation
82
+ * ```ts
83
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
84
+ * ```
85
+ */
86
+ function resolveDependencyOperationFile(options) {
87
+ const { cache, node, resolver, root, output, group } = options;
88
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
89
+ ...operationFileEntry(node, node.operationId),
90
+ root,
91
+ output,
92
+ group: group ?? void 0
93
+ }));
94
+ }
379
95
  function getOperationLink(node, link) {
380
96
  if (!link) return null;
381
97
  if (typeof link === "function") return link(node) ?? null;
382
- if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
383
- return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
98
+ return node.path ? `{@link ${kubb_kit.Url.toPath(node.path)}}` : null;
384
99
  }
385
100
  /**
386
101
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
@@ -470,14 +185,24 @@ function buildOperationComments(node, options = {}) {
470
185
  if (!splitLines) return filteredComments;
471
186
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
472
187
  }
473
- function getOperationParameters(node, options = {}) {
474
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
475
- return {
476
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
477
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
478
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
479
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
188
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
189
+ /**
190
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
191
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
192
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
193
+ * same parameters once per plugin.
194
+ */
195
+ function getOperationParameters(node) {
196
+ const cached = operationParameterGroupsByNode.get(node);
197
+ if (cached) return cached;
198
+ const groups = {
199
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
200
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
201
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
202
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
480
203
  };
204
+ operationParameterGroupsByNode.set(node, groups);
205
+ return groups;
481
206
  }
482
207
  function getStatusCodeNumber(statusCode) {
483
208
  const code = Number(statusCode);
@@ -511,7 +236,7 @@ function resolveStatusCodeNames(node, resolver) {
511
236
  }
512
237
  const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
513
238
  function resolveOperationTypeNames(node, resolver, options = {}) {
514
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
239
+ const cacheKey = `${node.operationId}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
515
240
  let byResolver = typeNamesByResolver.get(resolver);
516
241
  if (byResolver) {
517
242
  const cached = byResolver.get(cacheKey);
@@ -520,7 +245,7 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
520
245
  byResolver = /* @__PURE__ */ new Map();
521
246
  typeNamesByResolver.set(resolver, byResolver);
522
247
  }
523
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
248
+ const { path, query, header } = getOperationParameters(node);
524
249
  const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
525
250
  const exclude = new Set(options.exclude ?? []);
526
251
  const paramNames = options.includeParams === false ? [] : [
@@ -542,6 +267,60 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
542
267
  return result;
543
268
  }
544
269
  //#endregion
270
+ //#region ../../internals/utils/src/casing.ts
271
+ /**
272
+ * Shared implementation for camelCase and PascalCase conversion.
273
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
274
+ * and capitalizes each word according to `pascal`.
275
+ *
276
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
277
+ */
278
+ function toCamelOrPascal(text, pascal) {
279
+ return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
280
+ if (word.length > 1 && word === word.toUpperCase()) return word;
281
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
282
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
283
+ }
284
+ /**
285
+ * Converts `text` to camelCase.
286
+ *
287
+ * @example Word boundaries
288
+ * `camelCase('hello-world') // 'helloWorld'`
289
+ *
290
+ * @example With a prefix
291
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
292
+ */
293
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
294
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
295
+ }
296
+ /**
297
+ * Uppercases only the first character of `text`, leaving the rest untouched.
298
+ * Unlike {@link pascalCase} it never re-splits word boundaries or strips characters.
299
+ *
300
+ * @example
301
+ * `capitalize('getPetById') // 'GetPetById'`
302
+ */
303
+ function capitalize(text) {
304
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
305
+ }
306
+ //#endregion
307
+ //#region ../../internals/utils/src/strings.ts
308
+ /**
309
+ * Renders a dotted path or string array as an optional-chaining accessor expression rooted at
310
+ * `accessor`. Returns `null` for an empty path.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * getNestedAccessor('pagination.next.id', 'lastPage')
315
+ * // "lastPage?.['pagination']?.['next']?.['id']"
316
+ * ```
317
+ */
318
+ function getNestedAccessor(param, accessor) {
319
+ const parts = Array.isArray(param) ? param : param.split(".");
320
+ if (parts.length === 0 || parts.length === 1 && parts[0] === "") return null;
321
+ return `${accessor}?.['${`${parts.join("']?.['")}']`}`;
322
+ }
323
+ //#endregion
545
324
  //#region ../../internals/shared/src/group.ts
546
325
  /**
547
326
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -651,14 +430,16 @@ function buildGroupedRequestParam(node, options) {
651
430
  */
652
431
  function buildQueryOptionsParams(node, options) {
653
432
  const { resolver, memberTypeWrapper } = options;
654
- return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [buildGroupedRequestParam(node, {
433
+ const groupedParam = buildGroupedRequestParam(node, {
655
434
  resolver,
656
435
  memberTypeWrapper
657
- }), (0, _kubb_plugin_ts.createFunctionParameter)({
436
+ });
437
+ const configParam = (0, _kubb_plugin_ts.createFunctionParameter)({
658
438
  name: "config",
659
439
  type: `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`,
660
440
  default: "{}"
661
- })].filter((param) => param !== null) });
441
+ });
442
+ return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [groupedParam, configParam].filter((param) => param !== null) });
662
443
  }
663
444
  /**
664
445
  * Builds the call to a client `<op>` function inside a query/mutation hook body. The function takes
@@ -712,7 +493,7 @@ function resolveFallbackPageParamType(initialPageParam) {
712
493
  * so callers can reuse it when rewriting the paginated request.
713
494
  */
714
495
  function resolvePageParamType(node, { resolver, initialPageParam, queryParam }) {
715
- const firstQueryParam = getOperationParameters(node, { paramsCasing: "original" }).query[0];
496
+ const firstQueryParam = getOperationParameters(node).query[0];
716
497
  const groupName = firstQueryParam ? resolver.param.query(node, firstQueryParam) : null;
717
498
  const queryParamsTypeName = groupName !== (firstQueryParam ? resolver.param.name(node, firstQueryParam) : null) ? groupName : null;
718
499
  const queryParamType = queryParam && queryParamsTypeName ? `${queryParamsTypeName}['${queryParam}']` : null;
@@ -756,6 +537,19 @@ function resolveMutationConfig(mutation, options) {
756
537
  };
757
538
  }
758
539
  /**
540
+ * Classifies an operation as a query or a mutation from the resolved `query` / `mutation` method lists.
541
+ * `query: false` still marks the operation as a query so the query-family generators keep matching it,
542
+ * and a method already claimed by `query` never counts as a mutation.
543
+ */
544
+ function classifyOperation(node, { query, mutation }) {
545
+ const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
546
+ const queryMethods = new Set(query ? query.methods : []);
547
+ return {
548
+ isQuery,
549
+ isMutation: mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase())
550
+ };
551
+ }
552
+ /**
759
553
  * Applies the shared infinite-query defaults during plugin setup: a falsy value disables infinite
760
554
  * queries, and an object merges over `queryParam: 'id'` and `initialPageParam: 0` with the cursor
761
555
  * paths cleared.
@@ -852,7 +646,7 @@ __name(InfiniteQueryOptions$1, "InfiniteQueryOptions");
852
646
  const declarationPrinter$6 = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
853
647
  const mutationKeyTransformer = ({ node }) => {
854
648
  if (!node.path) return [];
855
- return [`{ url: '${Url.toPath(node.path)}' }`];
649
+ return [`{ url: '${kubb_kit.Url.toPath(node.path)}' }`];
856
650
  };
857
651
  function MutationKey({ name, node, transformer }) {
858
652
  const paramsNode = (0, _kubb_plugin_ts.createFunctionParameters)({ params: [] });
@@ -934,7 +728,7 @@ const queryKeyTransformer = ({ node }) => {
934
728
  const hasQueryParams = getOperationParameters(node).query.length > 0;
935
729
  const hasRequestBody = !!node.requestBody?.content?.[0]?.schema;
936
730
  return [
937
- hasPathParams ? `{ url: '${Url.toPath(node.path)}', params: path }` : `{ url: '${Url.toPath(node.path)}' }`,
731
+ hasPathParams ? `{ url: '${kubb_kit.Url.toPath(node.path)}', params: path }` : `{ url: '${kubb_kit.Url.toPath(node.path)}' }`,
938
732
  hasQueryParams ? "...(query ? [query] : [])" : null,
939
733
  hasRequestBody ? "...(body ? [body] : [])" : null
940
734
  ].filter(Boolean);
@@ -1028,23 +822,30 @@ function resolveContractClient(options) {
1028
822
  * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
1029
823
  * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
1030
824
  * read `RequestConfig` / `ResponseErrorConfig` from.
825
+ *
826
+ * The result is cached in the current node's `cache` (`ctx.cache`) under the client plugin's name,
827
+ * so several dependents reading the same client plugin for one operation in a single pass
828
+ * (react-query's query/mutation/infinite generators, vue-query, swr, the MCP handler, ...) share
829
+ * one computed result instead of each re-deriving the name and path.
1031
830
  */
1032
831
  function resolveClientOperation(options) {
1033
- const { clientPlugin, driver, node, root, output } = options;
832
+ const { clientPlugin, driver, node, root, output, cache } = options;
1034
833
  if (!clientPlugin) return null;
1035
- const resolver = driver.getResolver(clientPlugin.pluginName);
1036
- const plugin = driver.getPlugin(clientPlugin.pluginName);
1037
- const file = resolver.file({
1038
- ...operationFileEntry(node, node.operationId),
1039
- root,
1040
- output: plugin?.options?.output ?? output,
1041
- group: plugin?.options?.group ?? void 0
834
+ return cache.ensureItem(`${clientPlugin.pluginName}:clientOperation`, () => {
835
+ const resolver = driver.getResolver(clientPlugin.pluginName);
836
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
837
+ const file = resolver.file({
838
+ ...operationFileEntry(node, node.operationId),
839
+ root,
840
+ output: plugin?.options?.output ?? output,
841
+ group: plugin?.options?.group ?? void 0
842
+ });
843
+ return {
844
+ name: resolver.name(node.operationId),
845
+ path: file.path,
846
+ clientPath: node_path.default.resolve(root, ".kubb/client.ts")
847
+ };
1042
848
  });
1043
- return {
1044
- name: resolver.name(node.operationId),
1045
- path: file.path,
1046
- clientPath: node_path.default.resolve(root, ".kubb/client.ts")
1047
- };
1048
849
  }
1049
850
  //#endregion
1050
851
  //#region src/utils.ts
@@ -1168,10 +969,11 @@ function buildInfiniteQueryParamsNode(node, options) {
1168
969
  }`,
1169
970
  default: "{}"
1170
971
  });
1171
- return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [buildGroupedRequestParam(node, {
972
+ const groupedParam = buildGroupedRequestParam(node, {
1172
973
  resolver,
1173
974
  memberTypeWrapper: maybeRefOrGetter
1174
- }), optionsParam].filter((param) => param !== null) });
975
+ });
976
+ return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [groupedParam, optionsParam].filter((param) => param !== null) });
1175
977
  }
1176
978
  function InfiniteQuery({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, tsResolver }) {
1177
979
  const { TData, TError } = buildResponseTypes(node, tsResolver);
@@ -1240,13 +1042,14 @@ function resolveMutationRequestType(node, resolver) {
1240
1042
  function buildMutationParamsNode(node, options) {
1241
1043
  const { resolver } = options;
1242
1044
  const { TData, TError } = buildResponseTypes(node, resolver);
1045
+ const TRequest = resolveMutationRequestType(node, resolver);
1243
1046
  return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [(0, _kubb_plugin_ts.createFunctionParameter)({
1244
1047
  name: "options",
1245
1048
  type: `{
1246
1049
  mutation?: MutationObserverOptions<${[
1247
1050
  TData,
1248
1051
  TError,
1249
- resolveMutationRequestType(node, resolver),
1052
+ TRequest,
1250
1053
  "TContext"
1251
1054
  ].join(", ")}> & { client?: QueryClient },
1252
1055
  client?: ${buildRequestConfigType(node)},
@@ -1320,10 +1123,11 @@ function buildQueryParamsNode(node, options) {
1320
1123
  }`,
1321
1124
  default: "{}"
1322
1125
  });
1323
- return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [buildGroupedRequestParam(node, {
1126
+ const groupedParam = buildGroupedRequestParam(node, {
1324
1127
  resolver,
1325
1128
  memberTypeWrapper: maybeRefOrGetter
1326
- }), optionsParam].filter((param) => param !== null) });
1129
+ });
1130
+ return (0, _kubb_plugin_ts.createFunctionParameters)({ params: [groupedParam, optionsParam].filter((param) => param !== null) });
1327
1131
  }
1328
1132
  function Query({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, tsResolver }) {
1329
1133
  const { TData, TError } = buildResponseTypes(node, tsResolver);
@@ -1371,9 +1175,9 @@ function Query({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, t
1371
1175
  //#region src/generators/infiniteQueryGenerator.tsx
1372
1176
  /**
1373
1177
  * Built-in generator for `useInfiniteQuery` composables. Enabled when
1374
- * `pluginVueQuery({ infinite: { ... } })`. Emits one `useFooInfiniteQuery`
1375
- * composable per query operation, wiring the configured cursor path into
1376
- * TanStack Query's cursor-based pagination.
1178
+ * `pluginVueQuery({ infinite: { ... }, hooks: true })`. Emits one
1179
+ * `useFooInfiniteQuery` composable per query operation, wiring the
1180
+ * configured cursor path into TanStack Query's cursor-based pagination.
1377
1181
  */
1378
1182
  const infiniteQueryGenerator = (0, kubb_kit.defineGenerator)({
1379
1183
  name: "vue-query-infinite",
@@ -1385,13 +1189,14 @@ const infiniteQueryGenerator = (0, kubb_kit.defineGenerator)({
1385
1189
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
1386
1190
  if (!pluginTs) return null;
1387
1191
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
1388
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1389
- const queryMethods = new Set(query ? query.methods : []);
1390
- const isMutation = mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase());
1192
+ const { isQuery, isMutation } = classifyOperation(node, {
1193
+ query,
1194
+ mutation
1195
+ });
1391
1196
  const infiniteOptions = infinite && typeof infinite === "object" ? infinite : null;
1392
- if (!isQuery || isMutation || !infiniteOptions) return null;
1197
+ if (!isQuery || isMutation || !infiniteOptions || !hooks) return null;
1393
1198
  const normalizeKey = (key) => key.replace(/\?$/, "");
1394
- const queryParamKeys = getOperationParameters(node, { paramsCasing: "original" }).query.map((p) => p.name);
1199
+ const queryParamKeys = getOperationParameters(node).query.map((p) => p.name);
1395
1200
  if (!(infiniteOptions.queryParam ? queryParamKeys.some((k) => normalizeKey(k) === infiniteOptions.queryParam) : false)) return null;
1396
1201
  const importPath = query ? query.importPath : "@tanstack/vue-query";
1397
1202
  const contractOp = resolveClientOperation({
@@ -1399,7 +1204,8 @@ const infiniteQueryGenerator = (0, kubb_kit.defineGenerator)({
1399
1204
  driver,
1400
1205
  node,
1401
1206
  root,
1402
- output
1207
+ output,
1208
+ cache: ctx.cache
1403
1209
  });
1404
1210
  if (!contractOp) return null;
1405
1211
  const queryName = resolver.infiniteQuery.name(node);
@@ -1413,15 +1219,20 @@ const infiniteQueryGenerator = (0, kubb_kit.defineGenerator)({
1413
1219
  output,
1414
1220
  group: group ?? void 0
1415
1221
  }),
1416
- fileTs: tsResolver.file({
1417
- ...operationFileEntry(node, node.operationId),
1222
+ fileTs: resolveDependencyOperationFile({
1223
+ cache: ctx.cache,
1224
+ node,
1225
+ resolver: tsResolver,
1418
1226
  root,
1419
1227
  output: pluginTs.options?.output ?? output,
1420
- group: pluginTs.options?.group ?? void 0
1228
+ group: pluginTs.options?.group
1421
1229
  })
1422
1230
  };
1423
- const rawQueryParams = getOperationParameters(node, { paramsCasing: "original" }).query;
1424
- const queryParamsTypeName = rawQueryParams.length > 0 && tsResolver.param.query(node, rawQueryParams[0]) !== tsResolver.param.name(node, rawQueryParams[0]) ? tsResolver.param.query(node, rawQueryParams[0]) : null;
1231
+ const { queryParamsTypeName } = resolvePageParamType(node, {
1232
+ resolver: tsResolver,
1233
+ initialPageParam: infiniteOptions.initialPageParam,
1234
+ queryParam: infiniteOptions.queryParam
1235
+ });
1425
1236
  const importedTypeNames = [
1426
1237
  tsResolver.response.options(node),
1427
1238
  queryParamsTypeName,
@@ -1507,32 +1318,30 @@ const infiniteQueryGenerator = (0, kubb_kit.defineGenerator)({
1507
1318
  initialPageParam: infiniteOptions.initialPageParam,
1508
1319
  queryParam: infiniteOptions.queryParam
1509
1320
  }),
1510
- hooks && /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx_jsx_runtime.Fragment, { children: [
1511
- /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
1512
- name: ["useInfiniteQuery"],
1513
- path: importPath
1514
- }),
1515
- /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
1516
- name: [
1517
- "QueryKey",
1518
- "QueryClient",
1519
- "UseInfiniteQueryOptions",
1520
- "UseInfiniteQueryReturnType"
1521
- ],
1522
- path: importPath,
1523
- isTypeOnly: true
1524
- }),
1525
- /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(InfiniteQuery, {
1526
- name: queryName,
1527
- queryOptionsName,
1528
- queryKeyName,
1529
- queryKeyTypeName,
1530
- node,
1531
- tsResolver,
1532
- initialPageParam: infiniteOptions.initialPageParam,
1533
- queryParam: infiniteOptions.queryParam
1534
- })
1535
- ] })
1321
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
1322
+ name: ["useInfiniteQuery"],
1323
+ path: importPath
1324
+ }),
1325
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
1326
+ name: [
1327
+ "QueryKey",
1328
+ "QueryClient",
1329
+ "UseInfiniteQueryOptions",
1330
+ "UseInfiniteQueryReturnType"
1331
+ ],
1332
+ path: importPath,
1333
+ isTypeOnly: true
1334
+ }),
1335
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(InfiniteQuery, {
1336
+ name: queryName,
1337
+ queryOptionsName,
1338
+ queryKeyName,
1339
+ queryKeyTypeName,
1340
+ node,
1341
+ tsResolver,
1342
+ initialPageParam: infiniteOptions.initialPageParam,
1343
+ queryParam: infiniteOptions.queryParam
1344
+ })
1536
1345
  ]
1537
1346
  });
1538
1347
  }
@@ -1554,16 +1363,19 @@ const mutationGenerator = (0, kubb_kit.defineGenerator)({
1554
1363
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
1555
1364
  if (!pluginTs) return null;
1556
1365
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
1557
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1558
- const queryMethods = new Set(query ? query.methods : []);
1559
- if (!(mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase()))) return null;
1366
+ const { isMutation } = classifyOperation(node, {
1367
+ query,
1368
+ mutation
1369
+ });
1370
+ if (!isMutation) return null;
1560
1371
  const importPath = mutation ? mutation.importPath : "@tanstack/vue-query";
1561
1372
  const contractOp = resolveClientOperation({
1562
1373
  clientPlugin: { pluginName: client.pluginName },
1563
1374
  driver,
1564
1375
  node,
1565
1376
  root,
1566
- output
1377
+ output,
1378
+ cache: ctx.cache
1567
1379
  });
1568
1380
  if (!contractOp) return null;
1569
1381
  const mutationHookName = resolver.mutation.name(node);
@@ -1576,11 +1388,13 @@ const mutationGenerator = (0, kubb_kit.defineGenerator)({
1576
1388
  output,
1577
1389
  group: group ?? void 0
1578
1390
  }),
1579
- fileTs: tsResolver.file({
1580
- ...operationFileEntry(node, node.operationId),
1391
+ fileTs: resolveDependencyOperationFile({
1392
+ cache: ctx.cache,
1393
+ node,
1394
+ resolver: tsResolver,
1581
1395
  root,
1582
1396
  output: pluginTs.options?.output ?? output,
1583
- group: pluginTs.options?.group ?? void 0
1397
+ group: pluginTs.options?.group
1584
1398
  })
1585
1399
  };
1586
1400
  const importedTypeNames = [tsResolver.response.options(node), ...resolveOperationTypeNames(node, tsResolver, {
@@ -1682,9 +1496,10 @@ const queryGenerator = (0, kubb_kit.defineGenerator)({
1682
1496
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
1683
1497
  if (!pluginTs) return null;
1684
1498
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
1685
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1686
- const queryMethods = new Set(query ? query.methods : []);
1687
- const isMutation = mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase());
1499
+ const { isQuery, isMutation } = classifyOperation(node, {
1500
+ query,
1501
+ mutation
1502
+ });
1688
1503
  if (!isQuery || isMutation) return null;
1689
1504
  const importPath = query ? query.importPath : "@tanstack/vue-query";
1690
1505
  const contractOp = resolveClientOperation({
@@ -1692,7 +1507,8 @@ const queryGenerator = (0, kubb_kit.defineGenerator)({
1692
1507
  driver,
1693
1508
  node,
1694
1509
  root,
1695
- output
1510
+ output,
1511
+ cache: ctx.cache
1696
1512
  });
1697
1513
  if (!contractOp) return null;
1698
1514
  const queryName = resolver.query.name(node);
@@ -1706,11 +1522,13 @@ const queryGenerator = (0, kubb_kit.defineGenerator)({
1706
1522
  output,
1707
1523
  group: group ?? void 0
1708
1524
  }),
1709
- fileTs: tsResolver.file({
1710
- ...operationFileEntry(node, node.operationId),
1525
+ fileTs: resolveDependencyOperationFile({
1526
+ cache: ctx.cache,
1527
+ node,
1528
+ resolver: tsResolver,
1711
1529
  root,
1712
1530
  output: pluginTs.options?.output ?? output,
1713
- group: pluginTs.options?.group ?? void 0
1531
+ group: pluginTs.options?.group
1714
1532
  })
1715
1533
  };
1716
1534
  const importedTypeNames = [tsResolver.response.options(node), ...resolveOperationTypeNames(node, tsResolver, {