@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.js CHANGED
@@ -1,327 +1,23 @@
1
1
  import { t as __name } from "./rolldown-runtime-C0LytTxp.js";
2
+ import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin } 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, createObjectBindingPattern, createTypeLiteral, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
5
5
  import { File, Function, Type, jsxRenderer } from "kubb/jsx";
6
6
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
7
- //#region ../../internals/utils/src/casing.ts
8
- /**
9
- * Shared implementation for camelCase and PascalCase conversion.
10
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
11
- * and capitalizes each word according to `pascal`.
12
- *
13
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
14
- */
15
- function toCamelOrPascal(text, pascal) {
16
- 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) => {
17
- if (word.length > 1 && word === word.toUpperCase()) return word;
18
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
19
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
20
- }
21
- /**
22
- * Converts `text` to camelCase.
23
- *
24
- * @example Word boundaries
25
- * `camelCase('hello-world') // 'helloWorld'`
26
- *
27
- * @example With a prefix
28
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
29
- */
30
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
31
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
32
- }
33
- /**
34
- * Uppercases only the first character of `text`, leaving the rest untouched.
35
- * Unlike {@link pascalCase} it never re-splits word boundaries or strips characters.
36
- *
37
- * @example
38
- * `capitalize('getPetById') // 'GetPetById'`
39
- */
40
- function capitalize(text) {
41
- return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
42
- }
43
- //#endregion
44
- //#region ../../internals/utils/src/reserved.ts
45
- /**
46
- * JavaScript and Java reserved words.
47
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
48
- */
49
- const reservedWords = /* @__PURE__ */ new Set([
50
- "abstract",
51
- "arguments",
52
- "boolean",
53
- "break",
54
- "byte",
55
- "case",
56
- "catch",
57
- "char",
58
- "class",
59
- "const",
60
- "continue",
61
- "debugger",
62
- "default",
63
- "delete",
64
- "do",
65
- "double",
66
- "else",
67
- "enum",
68
- "eval",
69
- "export",
70
- "extends",
71
- "false",
72
- "final",
73
- "finally",
74
- "float",
75
- "for",
76
- "function",
77
- "goto",
78
- "if",
79
- "implements",
80
- "import",
81
- "in",
82
- "instanceof",
83
- "int",
84
- "interface",
85
- "let",
86
- "long",
87
- "native",
88
- "new",
89
- "null",
90
- "package",
91
- "private",
92
- "protected",
93
- "public",
94
- "return",
95
- "short",
96
- "static",
97
- "super",
98
- "switch",
99
- "synchronized",
100
- "this",
101
- "throw",
102
- "throws",
103
- "transient",
104
- "true",
105
- "try",
106
- "typeof",
107
- "var",
108
- "void",
109
- "volatile",
110
- "while",
111
- "with",
112
- "yield",
113
- "Array",
114
- "Date",
115
- "hasOwnProperty",
116
- "Infinity",
117
- "isFinite",
118
- "isNaN",
119
- "isPrototypeOf",
120
- "length",
121
- "Math",
122
- "name",
123
- "NaN",
124
- "Number",
125
- "Object",
126
- "prototype",
127
- "String",
128
- "toString",
129
- "undefined",
130
- "valueOf"
131
- ]);
132
- /**
133
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
134
- *
135
- * @example
136
- * ```ts
137
- * isValidVarName('status') // true
138
- * isValidVarName('class') // false (reserved word)
139
- * isValidVarName('42foo') // false (starts with digit)
140
- * ```
141
- */
142
- function isValidVarName(name) {
143
- if (!name || reservedWords.has(name)) return false;
144
- return isIdentifier(name);
145
- }
146
- /**
147
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
148
- *
149
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
150
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
151
- * deciding whether an object key needs quoting.
152
- *
153
- * @example
154
- * ```ts
155
- * isIdentifier('name') // true
156
- * isIdentifier('x-total')// false
157
- * ```
158
- */
159
- function isIdentifier(name) {
160
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
161
- }
162
- //#endregion
163
- //#region ../../internals/utils/src/strings.ts
164
- /**
165
- * Renders a dotted path or string array as an optional-chaining accessor expression rooted at
166
- * `accessor`. Returns `null` for an empty path.
167
- *
168
- * @example
169
- * ```ts
170
- * getNestedAccessor('pagination.next.id', 'lastPage')
171
- * // "lastPage?.['pagination']?.['next']?.['id']"
172
- * ```
173
- */
174
- function getNestedAccessor(param, accessor) {
175
- const parts = Array.isArray(param) ? param : param.split(".");
176
- if (parts.length === 0 || parts.length === 1 && parts[0] === "") return null;
177
- return `${accessor}?.['${`${parts.join("']?.['")}']`}`;
178
- }
179
- //#endregion
180
- //#region ../../internals/utils/src/url.ts
181
- function transformParam(raw, casing) {
182
- const param = isValidVarName(raw) ? raw : camelCase(raw);
183
- return casing === "camelcase" ? camelCase(param) : param;
184
- }
185
- function toParamsObject(path, { replacer, casing } = {}) {
186
- const params = {};
187
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
188
- const param = transformParam(match[1], casing);
189
- const key = replacer ? replacer(param) : param;
190
- params[key] = key;
191
- }
192
- return Object.keys(params).length > 0 ? params : null;
193
- }
194
- /**
195
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
196
- */
197
- var Url = class Url {
198
- /**
199
- * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
200
- *
201
- * @example
202
- * Url.canParse('https://petstore.swagger.io/v2') // true
203
- * Url.canParse('/pet/{petId}') // false
204
- */
205
- static canParse(url, base) {
206
- return URL.canParse(url, base);
207
- }
208
- /**
209
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
210
- *
211
- * @example
212
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
213
- */
214
- static toPath(path) {
215
- return path.replace(/\{([^}]+)\}/g, ":$1");
216
- }
217
- /**
218
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
219
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
220
- * key.
221
- *
222
- * @example
223
- * Url.toCasedTemplate('/projects/{project_id}', { casing: 'camelcase' }) // '/projects/{projectId}'
224
- */
225
- static toCasedTemplate(path, { casing } = {}) {
226
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name, casing)}}`);
227
- }
228
- /**
229
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
230
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
231
- * and `casing` controls parameter identifier casing.
232
- *
233
- * @example
234
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
235
- *
236
- * @example
237
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
238
- */
239
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
240
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
241
- if (i % 2 === 0) return part;
242
- const param = transformParam(part, casing);
243
- return `\${${replacer ? replacer(param) : param}}`;
244
- }).join("");
245
- return `\`${prefix ?? ""}${result}\``;
246
- }
247
- /**
248
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
249
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
250
- * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
251
- * literal. Shared by the client and cypress generators that pass a grouped `path` object.
252
- *
253
- * @example
254
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
255
- */
256
- static toGroupedTemplateString(path, { prefix } = {}) {
257
- return Url.toTemplateString(path, {
258
- prefix,
259
- casing: "camelcase",
260
- replacer: (name) => `path.${name}`
261
- });
262
- }
263
- /**
264
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
265
- * expression when `stringify` is set.
266
- *
267
- * @example
268
- * Url.toObject('/pet/{petId}')
269
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
270
- */
271
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
272
- const object = {
273
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
274
- replacer,
275
- casing
276
- }),
277
- params: toParamsObject(path, {
278
- replacer,
279
- casing
280
- })
281
- };
282
- if (stringify) {
283
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
284
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
285
- return `{ url: '${object.url}' }`;
286
- }
287
- return object;
288
- }
289
- };
290
- //#endregion
291
7
  //#region ../../internals/shared/src/params.ts
292
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
293
- /**
294
- * Applies camelCase to parameter names and returns a new array without mutating the input.
295
- *
296
- * Run it before handing parameters to schema builders so output property keys get the right casing
297
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
298
- * original array is returned unchanged. Results are cached per input array.
299
- */
300
- function caseParams(params, casing) {
301
- if (!casing) return params;
302
- const cached = caseParamsCache.get(params);
303
- if (cached) return cached;
304
- const result = params.map((param) => ({
305
- ...param,
306
- name: camelCase(param.name)
307
- }));
308
- caseParamsCache.set(params, result);
309
- return result;
310
- }
311
8
  /**
312
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
9
+ * Drops parameters that share the same name, keeping the first.
313
10
  *
314
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
315
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
316
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
317
- * identity so the resulting group is collision-free regardless of the names each caller carries.
11
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
12
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
13
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
14
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
318
15
  */
319
- function dedupeByCasedName(params) {
16
+ function dedupeParams(params) {
320
17
  const seen = /* @__PURE__ */ new Set();
321
18
  return params.filter((param) => {
322
- const key = camelCase(param.name);
323
- if (seen.has(key)) return false;
324
- seen.add(key);
19
+ if (seen.has(param.name)) return false;
20
+ seen.add(param.name);
325
21
  return true;
326
22
  });
327
23
  }
@@ -346,11 +42,30 @@ function operationFileEntry(node, name, extname = ".ts") {
346
42
  path: node.path
347
43
  };
348
44
  }
45
+ /**
46
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
47
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
48
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
49
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
50
+ *
51
+ * @example Cache `plugin-ts`'s file for the current operation
52
+ * ```ts
53
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
54
+ * ```
55
+ */
56
+ function resolveDependencyOperationFile(options) {
57
+ const { cache, node, resolver, root, output, group } = options;
58
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
59
+ ...operationFileEntry(node, node.operationId),
60
+ root,
61
+ output,
62
+ group: group ?? void 0
63
+ }));
64
+ }
349
65
  function getOperationLink(node, link) {
350
66
  if (!link) return null;
351
67
  if (typeof link === "function") return link(node) ?? null;
352
- if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
353
- return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
68
+ return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
354
69
  }
355
70
  /**
356
71
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
@@ -440,14 +155,24 @@ function buildOperationComments(node, options = {}) {
440
155
  if (!splitLines) return filteredComments;
441
156
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
442
157
  }
443
- function getOperationParameters(node, options = {}) {
444
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
445
- return {
446
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
447
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
448
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
449
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
158
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
159
+ /**
160
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
161
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
162
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
163
+ * same parameters once per plugin.
164
+ */
165
+ function getOperationParameters(node) {
166
+ const cached = operationParameterGroupsByNode.get(node);
167
+ if (cached) return cached;
168
+ const groups = {
169
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
170
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
171
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
172
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
450
173
  };
174
+ operationParameterGroupsByNode.set(node, groups);
175
+ return groups;
451
176
  }
452
177
  function getStatusCodeNumber(statusCode) {
453
178
  const code = Number(statusCode);
@@ -481,7 +206,7 @@ function resolveStatusCodeNames(node, resolver) {
481
206
  }
482
207
  const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
483
208
  function resolveOperationTypeNames(node, resolver, options = {}) {
484
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
209
+ const cacheKey = `${node.operationId}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
485
210
  let byResolver = typeNamesByResolver.get(resolver);
486
211
  if (byResolver) {
487
212
  const cached = byResolver.get(cacheKey);
@@ -490,7 +215,7 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
490
215
  byResolver = /* @__PURE__ */ new Map();
491
216
  typeNamesByResolver.set(resolver, byResolver);
492
217
  }
493
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
218
+ const { path, query, header } = getOperationParameters(node);
494
219
  const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
495
220
  const exclude = new Set(options.exclude ?? []);
496
221
  const paramNames = options.includeParams === false ? [] : [
@@ -512,6 +237,60 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
512
237
  return result;
513
238
  }
514
239
  //#endregion
240
+ //#region ../../internals/utils/src/casing.ts
241
+ /**
242
+ * Shared implementation for camelCase and PascalCase conversion.
243
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
244
+ * and capitalizes each word according to `pascal`.
245
+ *
246
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
247
+ */
248
+ function toCamelOrPascal(text, pascal) {
249
+ 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) => {
250
+ if (word.length > 1 && word === word.toUpperCase()) return word;
251
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
252
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
253
+ }
254
+ /**
255
+ * Converts `text` to camelCase.
256
+ *
257
+ * @example Word boundaries
258
+ * `camelCase('hello-world') // 'helloWorld'`
259
+ *
260
+ * @example With a prefix
261
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
262
+ */
263
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
264
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
265
+ }
266
+ /**
267
+ * Uppercases only the first character of `text`, leaving the rest untouched.
268
+ * Unlike {@link pascalCase} it never re-splits word boundaries or strips characters.
269
+ *
270
+ * @example
271
+ * `capitalize('getPetById') // 'GetPetById'`
272
+ */
273
+ function capitalize(text) {
274
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
275
+ }
276
+ //#endregion
277
+ //#region ../../internals/utils/src/strings.ts
278
+ /**
279
+ * Renders a dotted path or string array as an optional-chaining accessor expression rooted at
280
+ * `accessor`. Returns `null` for an empty path.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * getNestedAccessor('pagination.next.id', 'lastPage')
285
+ * // "lastPage?.['pagination']?.['next']?.['id']"
286
+ * ```
287
+ */
288
+ function getNestedAccessor(param, accessor) {
289
+ const parts = Array.isArray(param) ? param : param.split(".");
290
+ if (parts.length === 0 || parts.length === 1 && parts[0] === "") return null;
291
+ return `${accessor}?.['${`${parts.join("']?.['")}']`}`;
292
+ }
293
+ //#endregion
515
294
  //#region ../../internals/shared/src/group.ts
516
295
  /**
517
296
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -621,14 +400,16 @@ function buildGroupedRequestParam(node, options) {
621
400
  */
622
401
  function buildQueryOptionsParams(node, options) {
623
402
  const { resolver, memberTypeWrapper } = options;
624
- return createFunctionParameters({ params: [buildGroupedRequestParam(node, {
403
+ const groupedParam = buildGroupedRequestParam(node, {
625
404
  resolver,
626
405
  memberTypeWrapper
627
- }), createFunctionParameter({
406
+ });
407
+ const configParam = createFunctionParameter({
628
408
  name: "config",
629
409
  type: `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`,
630
410
  default: "{}"
631
- })].filter((param) => param !== null) });
411
+ });
412
+ return createFunctionParameters({ params: [groupedParam, configParam].filter((param) => param !== null) });
632
413
  }
633
414
  /**
634
415
  * Builds the call to a client `<op>` function inside a query/mutation hook body. The function takes
@@ -682,7 +463,7 @@ function resolveFallbackPageParamType(initialPageParam) {
682
463
  * so callers can reuse it when rewriting the paginated request.
683
464
  */
684
465
  function resolvePageParamType(node, { resolver, initialPageParam, queryParam }) {
685
- const firstQueryParam = getOperationParameters(node, { paramsCasing: "original" }).query[0];
466
+ const firstQueryParam = getOperationParameters(node).query[0];
686
467
  const groupName = firstQueryParam ? resolver.param.query(node, firstQueryParam) : null;
687
468
  const queryParamsTypeName = groupName !== (firstQueryParam ? resolver.param.name(node, firstQueryParam) : null) ? groupName : null;
688
469
  const queryParamType = queryParam && queryParamsTypeName ? `${queryParamsTypeName}['${queryParam}']` : null;
@@ -726,6 +507,19 @@ function resolveMutationConfig(mutation, options) {
726
507
  };
727
508
  }
728
509
  /**
510
+ * Classifies an operation as a query or a mutation from the resolved `query` / `mutation` method lists.
511
+ * `query: false` still marks the operation as a query so the query-family generators keep matching it,
512
+ * and a method already claimed by `query` never counts as a mutation.
513
+ */
514
+ function classifyOperation(node, { query, mutation }) {
515
+ const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
516
+ const queryMethods = new Set(query ? query.methods : []);
517
+ return {
518
+ isQuery,
519
+ isMutation: mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase())
520
+ };
521
+ }
522
+ /**
729
523
  * Applies the shared infinite-query defaults during plugin setup: a falsy value disables infinite
730
524
  * queries, and an object merges over `queryParam: 'id'` and `initialPageParam: 0` with the cursor
731
525
  * paths cleared.
@@ -998,23 +792,30 @@ function resolveContractClient(options) {
998
792
  * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
999
793
  * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
1000
794
  * read `RequestConfig` / `ResponseErrorConfig` from.
795
+ *
796
+ * The result is cached in the current node's `cache` (`ctx.cache`) under the client plugin's name,
797
+ * so several dependents reading the same client plugin for one operation in a single pass
798
+ * (react-query's query/mutation/infinite generators, vue-query, swr, the MCP handler, ...) share
799
+ * one computed result instead of each re-deriving the name and path.
1001
800
  */
1002
801
  function resolveClientOperation(options) {
1003
- const { clientPlugin, driver, node, root, output } = options;
802
+ const { clientPlugin, driver, node, root, output, cache } = options;
1004
803
  if (!clientPlugin) return null;
1005
- const resolver = driver.getResolver(clientPlugin.pluginName);
1006
- const plugin = driver.getPlugin(clientPlugin.pluginName);
1007
- const file = resolver.file({
1008
- ...operationFileEntry(node, node.operationId),
1009
- root,
1010
- output: plugin?.options?.output ?? output,
1011
- group: plugin?.options?.group ?? void 0
804
+ return cache.ensureItem(`${clientPlugin.pluginName}:clientOperation`, () => {
805
+ const resolver = driver.getResolver(clientPlugin.pluginName);
806
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
807
+ const file = resolver.file({
808
+ ...operationFileEntry(node, node.operationId),
809
+ root,
810
+ output: plugin?.options?.output ?? output,
811
+ group: plugin?.options?.group ?? void 0
812
+ });
813
+ return {
814
+ name: resolver.name(node.operationId),
815
+ path: file.path,
816
+ clientPath: path.resolve(root, ".kubb/client.ts")
817
+ };
1012
818
  });
1013
- return {
1014
- name: resolver.name(node.operationId),
1015
- path: file.path,
1016
- clientPath: path.resolve(root, ".kubb/client.ts")
1017
- };
1018
819
  }
1019
820
  //#endregion
1020
821
  //#region src/utils.ts
@@ -1138,10 +939,11 @@ function buildInfiniteQueryParamsNode(node, options) {
1138
939
  }`,
1139
940
  default: "{}"
1140
941
  });
1141
- return createFunctionParameters({ params: [buildGroupedRequestParam(node, {
942
+ const groupedParam = buildGroupedRequestParam(node, {
1142
943
  resolver,
1143
944
  memberTypeWrapper: maybeRefOrGetter
1144
- }), optionsParam].filter((param) => param !== null) });
945
+ });
946
+ return createFunctionParameters({ params: [groupedParam, optionsParam].filter((param) => param !== null) });
1145
947
  }
1146
948
  function InfiniteQuery({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, tsResolver }) {
1147
949
  const { TData, TError } = buildResponseTypes(node, tsResolver);
@@ -1210,13 +1012,14 @@ function resolveMutationRequestType(node, resolver) {
1210
1012
  function buildMutationParamsNode(node, options) {
1211
1013
  const { resolver } = options;
1212
1014
  const { TData, TError } = buildResponseTypes(node, resolver);
1015
+ const TRequest = resolveMutationRequestType(node, resolver);
1213
1016
  return createFunctionParameters({ params: [createFunctionParameter({
1214
1017
  name: "options",
1215
1018
  type: `{
1216
1019
  mutation?: MutationObserverOptions<${[
1217
1020
  TData,
1218
1021
  TError,
1219
- resolveMutationRequestType(node, resolver),
1022
+ TRequest,
1220
1023
  "TContext"
1221
1024
  ].join(", ")}> & { client?: QueryClient },
1222
1025
  client?: ${buildRequestConfigType(node)},
@@ -1290,10 +1093,11 @@ function buildQueryParamsNode(node, options) {
1290
1093
  }`,
1291
1094
  default: "{}"
1292
1095
  });
1293
- return createFunctionParameters({ params: [buildGroupedRequestParam(node, {
1096
+ const groupedParam = buildGroupedRequestParam(node, {
1294
1097
  resolver,
1295
1098
  memberTypeWrapper: maybeRefOrGetter
1296
- }), optionsParam].filter((param) => param !== null) });
1099
+ });
1100
+ return createFunctionParameters({ params: [groupedParam, optionsParam].filter((param) => param !== null) });
1297
1101
  }
1298
1102
  function Query({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, tsResolver }) {
1299
1103
  const { TData, TError } = buildResponseTypes(node, tsResolver);
@@ -1341,9 +1145,9 @@ function Query({ name, queryKeyTypeName, queryOptionsName, queryKeyName, node, t
1341
1145
  //#region src/generators/infiniteQueryGenerator.tsx
1342
1146
  /**
1343
1147
  * Built-in generator for `useInfiniteQuery` composables. Enabled when
1344
- * `pluginVueQuery({ infinite: { ... } })`. Emits one `useFooInfiniteQuery`
1345
- * composable per query operation, wiring the configured cursor path into
1346
- * TanStack Query's cursor-based pagination.
1148
+ * `pluginVueQuery({ infinite: { ... }, hooks: true })`. Emits one
1149
+ * `useFooInfiniteQuery` composable per query operation, wiring the
1150
+ * configured cursor path into TanStack Query's cursor-based pagination.
1347
1151
  */
1348
1152
  const infiniteQueryGenerator = defineGenerator({
1349
1153
  name: "vue-query-infinite",
@@ -1355,13 +1159,14 @@ const infiniteQueryGenerator = defineGenerator({
1355
1159
  const pluginTs = driver.getPlugin(pluginTsName);
1356
1160
  if (!pluginTs) return null;
1357
1161
  const tsResolver = driver.getResolver(pluginTsName);
1358
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1359
- const queryMethods = new Set(query ? query.methods : []);
1360
- const isMutation = mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase());
1162
+ const { isQuery, isMutation } = classifyOperation(node, {
1163
+ query,
1164
+ mutation
1165
+ });
1361
1166
  const infiniteOptions = infinite && typeof infinite === "object" ? infinite : null;
1362
- if (!isQuery || isMutation || !infiniteOptions) return null;
1167
+ if (!isQuery || isMutation || !infiniteOptions || !hooks) return null;
1363
1168
  const normalizeKey = (key) => key.replace(/\?$/, "");
1364
- const queryParamKeys = getOperationParameters(node, { paramsCasing: "original" }).query.map((p) => p.name);
1169
+ const queryParamKeys = getOperationParameters(node).query.map((p) => p.name);
1365
1170
  if (!(infiniteOptions.queryParam ? queryParamKeys.some((k) => normalizeKey(k) === infiniteOptions.queryParam) : false)) return null;
1366
1171
  const importPath = query ? query.importPath : "@tanstack/vue-query";
1367
1172
  const contractOp = resolveClientOperation({
@@ -1369,7 +1174,8 @@ const infiniteQueryGenerator = defineGenerator({
1369
1174
  driver,
1370
1175
  node,
1371
1176
  root,
1372
- output
1177
+ output,
1178
+ cache: ctx.cache
1373
1179
  });
1374
1180
  if (!contractOp) return null;
1375
1181
  const queryName = resolver.infiniteQuery.name(node);
@@ -1383,15 +1189,20 @@ const infiniteQueryGenerator = defineGenerator({
1383
1189
  output,
1384
1190
  group: group ?? void 0
1385
1191
  }),
1386
- fileTs: tsResolver.file({
1387
- ...operationFileEntry(node, node.operationId),
1192
+ fileTs: resolveDependencyOperationFile({
1193
+ cache: ctx.cache,
1194
+ node,
1195
+ resolver: tsResolver,
1388
1196
  root,
1389
1197
  output: pluginTs.options?.output ?? output,
1390
- group: pluginTs.options?.group ?? void 0
1198
+ group: pluginTs.options?.group
1391
1199
  })
1392
1200
  };
1393
- const rawQueryParams = getOperationParameters(node, { paramsCasing: "original" }).query;
1394
- const queryParamsTypeName = rawQueryParams.length > 0 && tsResolver.param.query(node, rawQueryParams[0]) !== tsResolver.param.name(node, rawQueryParams[0]) ? tsResolver.param.query(node, rawQueryParams[0]) : null;
1201
+ const { queryParamsTypeName } = resolvePageParamType(node, {
1202
+ resolver: tsResolver,
1203
+ initialPageParam: infiniteOptions.initialPageParam,
1204
+ queryParam: infiniteOptions.queryParam
1205
+ });
1395
1206
  const importedTypeNames = [
1396
1207
  tsResolver.response.options(node),
1397
1208
  queryParamsTypeName,
@@ -1477,32 +1288,30 @@ const infiniteQueryGenerator = defineGenerator({
1477
1288
  initialPageParam: infiniteOptions.initialPageParam,
1478
1289
  queryParam: infiniteOptions.queryParam
1479
1290
  }),
1480
- hooks && /* @__PURE__ */ jsxs(Fragment, { children: [
1481
- /* @__PURE__ */ jsx(File.Import, {
1482
- name: ["useInfiniteQuery"],
1483
- path: importPath
1484
- }),
1485
- /* @__PURE__ */ jsx(File.Import, {
1486
- name: [
1487
- "QueryKey",
1488
- "QueryClient",
1489
- "UseInfiniteQueryOptions",
1490
- "UseInfiniteQueryReturnType"
1491
- ],
1492
- path: importPath,
1493
- isTypeOnly: true
1494
- }),
1495
- /* @__PURE__ */ jsx(InfiniteQuery, {
1496
- name: queryName,
1497
- queryOptionsName,
1498
- queryKeyName,
1499
- queryKeyTypeName,
1500
- node,
1501
- tsResolver,
1502
- initialPageParam: infiniteOptions.initialPageParam,
1503
- queryParam: infiniteOptions.queryParam
1504
- })
1505
- ] })
1291
+ /* @__PURE__ */ jsx(File.Import, {
1292
+ name: ["useInfiniteQuery"],
1293
+ path: importPath
1294
+ }),
1295
+ /* @__PURE__ */ jsx(File.Import, {
1296
+ name: [
1297
+ "QueryKey",
1298
+ "QueryClient",
1299
+ "UseInfiniteQueryOptions",
1300
+ "UseInfiniteQueryReturnType"
1301
+ ],
1302
+ path: importPath,
1303
+ isTypeOnly: true
1304
+ }),
1305
+ /* @__PURE__ */ jsx(InfiniteQuery, {
1306
+ name: queryName,
1307
+ queryOptionsName,
1308
+ queryKeyName,
1309
+ queryKeyTypeName,
1310
+ node,
1311
+ tsResolver,
1312
+ initialPageParam: infiniteOptions.initialPageParam,
1313
+ queryParam: infiniteOptions.queryParam
1314
+ })
1506
1315
  ]
1507
1316
  });
1508
1317
  }
@@ -1524,16 +1333,19 @@ const mutationGenerator = defineGenerator({
1524
1333
  const pluginTs = driver.getPlugin(pluginTsName);
1525
1334
  if (!pluginTs) return null;
1526
1335
  const tsResolver = driver.getResolver(pluginTsName);
1527
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1528
- const queryMethods = new Set(query ? query.methods : []);
1529
- if (!(mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase()))) return null;
1336
+ const { isMutation } = classifyOperation(node, {
1337
+ query,
1338
+ mutation
1339
+ });
1340
+ if (!isMutation) return null;
1530
1341
  const importPath = mutation ? mutation.importPath : "@tanstack/vue-query";
1531
1342
  const contractOp = resolveClientOperation({
1532
1343
  clientPlugin: { pluginName: client.pluginName },
1533
1344
  driver,
1534
1345
  node,
1535
1346
  root,
1536
- output
1347
+ output,
1348
+ cache: ctx.cache
1537
1349
  });
1538
1350
  if (!contractOp) return null;
1539
1351
  const mutationHookName = resolver.mutation.name(node);
@@ -1546,11 +1358,13 @@ const mutationGenerator = defineGenerator({
1546
1358
  output,
1547
1359
  group: group ?? void 0
1548
1360
  }),
1549
- fileTs: tsResolver.file({
1550
- ...operationFileEntry(node, node.operationId),
1361
+ fileTs: resolveDependencyOperationFile({
1362
+ cache: ctx.cache,
1363
+ node,
1364
+ resolver: tsResolver,
1551
1365
  root,
1552
1366
  output: pluginTs.options?.output ?? output,
1553
- group: pluginTs.options?.group ?? void 0
1367
+ group: pluginTs.options?.group
1554
1368
  })
1555
1369
  };
1556
1370
  const importedTypeNames = [tsResolver.response.options(node), ...resolveOperationTypeNames(node, tsResolver, {
@@ -1652,9 +1466,10 @@ const queryGenerator = defineGenerator({
1652
1466
  const pluginTs = driver.getPlugin(pluginTsName);
1653
1467
  if (!pluginTs) return null;
1654
1468
  const tsResolver = driver.getResolver(pluginTsName);
1655
- const isQuery = query === false || !!query && query.methods.some((method) => node.method.toLowerCase() === method.toLowerCase());
1656
- const queryMethods = new Set(query ? query.methods : []);
1657
- const isMutation = mutation !== false && !isQuery && (mutation ? mutation.methods : []).some((method) => !queryMethods.has(method) && node.method.toLowerCase() === method.toLowerCase());
1469
+ const { isQuery, isMutation } = classifyOperation(node, {
1470
+ query,
1471
+ mutation
1472
+ });
1658
1473
  if (!isQuery || isMutation) return null;
1659
1474
  const importPath = query ? query.importPath : "@tanstack/vue-query";
1660
1475
  const contractOp = resolveClientOperation({
@@ -1662,7 +1477,8 @@ const queryGenerator = defineGenerator({
1662
1477
  driver,
1663
1478
  node,
1664
1479
  root,
1665
- output
1480
+ output,
1481
+ cache: ctx.cache
1666
1482
  });
1667
1483
  if (!contractOp) return null;
1668
1484
  const queryName = resolver.query.name(node);
@@ -1676,11 +1492,13 @@ const queryGenerator = defineGenerator({
1676
1492
  output,
1677
1493
  group: group ?? void 0
1678
1494
  }),
1679
- fileTs: tsResolver.file({
1680
- ...operationFileEntry(node, node.operationId),
1495
+ fileTs: resolveDependencyOperationFile({
1496
+ cache: ctx.cache,
1497
+ node,
1498
+ resolver: tsResolver,
1681
1499
  root,
1682
1500
  output: pluginTs.options?.output ?? output,
1683
- group: pluginTs.options?.group ?? void 0
1501
+ group: pluginTs.options?.group
1684
1502
  })
1685
1503
  };
1686
1504
  const importedTypeNames = [tsResolver.response.options(node), ...resolveOperationTypeNames(node, tsResolver, {