@kubb/plugin-mcp 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
@@ -19,309 +19,32 @@ var __copyProps = (to, from, except, desc) => {
19
19
  }
20
20
  return to;
21
21
  };
22
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
23
23
  value: mod,
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
+ let kubb_kit = require("kubb/kit");
27
28
  let node_path = require("node:path");
28
29
  node_path = __toESM(node_path, 1);
29
- let kubb_kit = require("kubb/kit");
30
30
  let kubb_jsx = require("kubb/jsx");
31
31
  let kubb_jsx_jsx_runtime = require("kubb/jsx/jsx-runtime");
32
32
  let _kubb_plugin_ts = require("@kubb/plugin-ts");
33
33
  let _kubb_plugin_zod = require("@kubb/plugin-zod");
34
- //#region ../../internals/utils/src/casing.ts
35
- /**
36
- * Shared implementation for camelCase and PascalCase conversion.
37
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
38
- * and capitalizes each word according to `pascal`.
39
- *
40
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
41
- */
42
- function toCamelOrPascal(text, pascal) {
43
- 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) => {
44
- if (word.length > 1 && word === word.toUpperCase()) return word;
45
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
46
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
47
- }
48
- /**
49
- * Converts `text` to camelCase.
50
- *
51
- * @example Word boundaries
52
- * `camelCase('hello-world') // 'helloWorld'`
53
- *
54
- * @example With a prefix
55
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
56
- */
57
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
58
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
59
- }
60
- //#endregion
61
- //#region ../../internals/utils/src/reserved.ts
62
- /**
63
- * JavaScript and Java reserved words.
64
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
65
- */
66
- const reservedWords = /* @__PURE__ */ new Set([
67
- "abstract",
68
- "arguments",
69
- "boolean",
70
- "break",
71
- "byte",
72
- "case",
73
- "catch",
74
- "char",
75
- "class",
76
- "const",
77
- "continue",
78
- "debugger",
79
- "default",
80
- "delete",
81
- "do",
82
- "double",
83
- "else",
84
- "enum",
85
- "eval",
86
- "export",
87
- "extends",
88
- "false",
89
- "final",
90
- "finally",
91
- "float",
92
- "for",
93
- "function",
94
- "goto",
95
- "if",
96
- "implements",
97
- "import",
98
- "in",
99
- "instanceof",
100
- "int",
101
- "interface",
102
- "let",
103
- "long",
104
- "native",
105
- "new",
106
- "null",
107
- "package",
108
- "private",
109
- "protected",
110
- "public",
111
- "return",
112
- "short",
113
- "static",
114
- "super",
115
- "switch",
116
- "synchronized",
117
- "this",
118
- "throw",
119
- "throws",
120
- "transient",
121
- "true",
122
- "try",
123
- "typeof",
124
- "var",
125
- "void",
126
- "volatile",
127
- "while",
128
- "with",
129
- "yield",
130
- "Array",
131
- "Date",
132
- "hasOwnProperty",
133
- "Infinity",
134
- "isFinite",
135
- "isNaN",
136
- "isPrototypeOf",
137
- "length",
138
- "Math",
139
- "name",
140
- "NaN",
141
- "Number",
142
- "Object",
143
- "prototype",
144
- "String",
145
- "toString",
146
- "undefined",
147
- "valueOf"
148
- ]);
149
- /**
150
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
151
- *
152
- * @example
153
- * ```ts
154
- * isValidVarName('status') // true
155
- * isValidVarName('class') // false (reserved word)
156
- * isValidVarName('42foo') // false (starts with digit)
157
- * ```
158
- */
159
- function isValidVarName(name) {
160
- if (!name || reservedWords.has(name)) return false;
161
- return isIdentifier(name);
162
- }
163
- /**
164
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
165
- *
166
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
167
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
168
- * deciding whether an object key needs quoting.
169
- *
170
- * @example
171
- * ```ts
172
- * isIdentifier('name') // true
173
- * isIdentifier('x-total')// false
174
- * ```
175
- */
176
- function isIdentifier(name) {
177
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
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
34
  //#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
35
  /**
312
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
36
+ * Drops parameters that share the same name, keeping the first.
313
37
  *
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.
38
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
39
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
40
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
41
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
318
42
  */
319
- function dedupeByCasedName(params) {
43
+ function dedupeParams(params) {
320
44
  const seen = /* @__PURE__ */ new Set();
321
45
  return params.filter((param) => {
322
- const key = camelCase(param.name);
323
- if (seen.has(key)) return false;
324
- seen.add(key);
46
+ if (seen.has(param.name)) return false;
47
+ seen.add(param.name);
325
48
  return true;
326
49
  });
327
50
  }
@@ -346,11 +69,30 @@ function operationFileEntry(node, name, extname = ".ts") {
346
69
  path: node.path
347
70
  };
348
71
  }
72
+ /**
73
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
74
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
75
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
76
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
77
+ *
78
+ * @example Cache `plugin-ts`'s file for the current operation
79
+ * ```ts
80
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
81
+ * ```
82
+ */
83
+ function resolveDependencyOperationFile(options) {
84
+ const { cache, node, resolver, root, output, group } = options;
85
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
86
+ ...operationFileEntry(node, node.operationId),
87
+ root,
88
+ output,
89
+ group: group ?? void 0
90
+ }));
91
+ }
349
92
  function getOperationLink(node, link) {
350
93
  if (!link) return null;
351
94
  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;
95
+ return node.path ? `{@link ${kubb_kit.Url.toPath(node.path)}}` : null;
354
96
  }
355
97
  /**
356
98
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
@@ -451,14 +193,24 @@ function buildOperationComments(node, options = {}) {
451
193
  if (!splitLines) return filteredComments;
452
194
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
453
195
  }
454
- function getOperationParameters(node, options = {}) {
455
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
456
- return {
457
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
458
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
459
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
460
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
196
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
197
+ /**
198
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
199
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
200
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
201
+ * same parameters once per plugin.
202
+ */
203
+ function getOperationParameters(node) {
204
+ const cached = operationParameterGroupsByNode.get(node);
205
+ if (cached) return cached;
206
+ const groups = {
207
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
208
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
209
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
210
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
461
211
  };
212
+ operationParameterGroupsByNode.set(node, groups);
213
+ return groups;
462
214
  }
463
215
  function getStatusCodeNumber(statusCode) {
464
216
  const code = Number(statusCode);
@@ -482,6 +234,33 @@ function findSuccessStatusCode(responses) {
482
234
  return null;
483
235
  }
484
236
  //#endregion
237
+ //#region ../../internals/utils/src/casing.ts
238
+ /**
239
+ * Shared implementation for camelCase and PascalCase conversion.
240
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
241
+ * and capitalizes each word according to `pascal`.
242
+ *
243
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
244
+ */
245
+ function toCamelOrPascal(text, pascal) {
246
+ 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) => {
247
+ if (word.length > 1 && word === word.toUpperCase()) return word;
248
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
249
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
250
+ }
251
+ /**
252
+ * Converts `text` to camelCase.
253
+ *
254
+ * @example Word boundaries
255
+ * `camelCase('hello-world') // 'helloWorld'`
256
+ *
257
+ * @example With a prefix
258
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
259
+ */
260
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
261
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
262
+ }
263
+ //#endregion
485
264
  //#region ../../internals/shared/src/group.ts
486
265
  /**
487
266
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -768,23 +547,30 @@ function resolveContractClient(options) {
768
547
  * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
769
548
  * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
770
549
  * read `RequestConfig` / `ResponseErrorConfig` from.
550
+ *
551
+ * The result is cached in the current node's `cache` (`ctx.cache`) under the client plugin's name,
552
+ * so several dependents reading the same client plugin for one operation in a single pass
553
+ * (react-query's query/mutation/infinite generators, vue-query, swr, the MCP handler, ...) share
554
+ * one computed result instead of each re-deriving the name and path.
771
555
  */
772
556
  function resolveClientOperation(options) {
773
- const { clientPlugin, driver, node, root, output } = options;
557
+ const { clientPlugin, driver, node, root, output, cache } = options;
774
558
  if (!clientPlugin) return null;
775
- const resolver = driver.getResolver(clientPlugin.pluginName);
776
- const plugin = driver.getPlugin(clientPlugin.pluginName);
777
- const file = resolver.file({
778
- ...operationFileEntry(node, node.operationId),
779
- root,
780
- output: plugin?.options?.output ?? output,
781
- group: plugin?.options?.group ?? void 0
559
+ return cache.ensureItem(`${clientPlugin.pluginName}:clientOperation`, () => {
560
+ const resolver = driver.getResolver(clientPlugin.pluginName);
561
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
562
+ const file = resolver.file({
563
+ ...operationFileEntry(node, node.operationId),
564
+ root,
565
+ output: plugin?.options?.output ?? output,
566
+ group: plugin?.options?.group ?? void 0
567
+ });
568
+ return {
569
+ name: resolver.name(node.operationId),
570
+ path: file.path,
571
+ clientPath: node_path.default.resolve(root, ".kubb/client.ts")
572
+ };
782
573
  });
783
- return {
784
- name: resolver.name(node.operationId),
785
- path: file.path,
786
- clientPath: node_path.default.resolve(root, ".kubb/client.ts")
787
- };
788
574
  }
789
575
  //#endregion
790
576
  //#region src/generators/mcpGenerator.tsx
@@ -809,7 +595,8 @@ const mcpGenerator = (0, kubb_kit.defineGenerator)({
809
595
  driver,
810
596
  node,
811
597
  root,
812
- output
598
+ output,
599
+ cache: ctx.cache
813
600
  });
814
601
  if (!contractOp) return null;
815
602
  const optionsName = tsResolver.response.options(node);
@@ -824,14 +611,13 @@ const mcpGenerator = (0, kubb_kit.defineGenerator)({
824
611
  output,
825
612
  group: group ?? void 0
826
613
  }),
827
- fileTs: tsResolver.file({
828
- name: node.operationId,
829
- extname: ".ts",
830
- tag: node.tags[0] ?? "default",
831
- path: node.path,
614
+ fileTs: resolveDependencyOperationFile({
615
+ cache: ctx.cache,
616
+ node,
617
+ resolver: tsResolver,
832
618
  root,
833
619
  output: pluginTs.options?.output ?? output,
834
- group: pluginTs.options?.group ?? void 0
620
+ group: pluginTs.options?.group
835
621
  })
836
622
  };
837
623
  return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File, {