@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.js CHANGED
@@ -1,301 +1,24 @@
1
1
  import "./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 { Const, File, Function, jsxRenderer } from "kubb/jsx";
5
5
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
6
6
  import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
7
7
  import { pluginZodName } from "@kubb/plugin-zod";
8
- //#region ../../internals/utils/src/casing.ts
9
- /**
10
- * Shared implementation for camelCase and PascalCase conversion.
11
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
12
- * and capitalizes each word according to `pascal`.
13
- *
14
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
15
- */
16
- function toCamelOrPascal(text, pascal) {
17
- 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) => {
18
- if (word.length > 1 && word === word.toUpperCase()) return word;
19
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
20
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
21
- }
22
- /**
23
- * Converts `text` to camelCase.
24
- *
25
- * @example Word boundaries
26
- * `camelCase('hello-world') // 'helloWorld'`
27
- *
28
- * @example With a prefix
29
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
30
- */
31
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
32
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
33
- }
34
- //#endregion
35
- //#region ../../internals/utils/src/reserved.ts
36
- /**
37
- * JavaScript and Java reserved words.
38
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
39
- */
40
- const reservedWords = /* @__PURE__ */ new Set([
41
- "abstract",
42
- "arguments",
43
- "boolean",
44
- "break",
45
- "byte",
46
- "case",
47
- "catch",
48
- "char",
49
- "class",
50
- "const",
51
- "continue",
52
- "debugger",
53
- "default",
54
- "delete",
55
- "do",
56
- "double",
57
- "else",
58
- "enum",
59
- "eval",
60
- "export",
61
- "extends",
62
- "false",
63
- "final",
64
- "finally",
65
- "float",
66
- "for",
67
- "function",
68
- "goto",
69
- "if",
70
- "implements",
71
- "import",
72
- "in",
73
- "instanceof",
74
- "int",
75
- "interface",
76
- "let",
77
- "long",
78
- "native",
79
- "new",
80
- "null",
81
- "package",
82
- "private",
83
- "protected",
84
- "public",
85
- "return",
86
- "short",
87
- "static",
88
- "super",
89
- "switch",
90
- "synchronized",
91
- "this",
92
- "throw",
93
- "throws",
94
- "transient",
95
- "true",
96
- "try",
97
- "typeof",
98
- "var",
99
- "void",
100
- "volatile",
101
- "while",
102
- "with",
103
- "yield",
104
- "Array",
105
- "Date",
106
- "hasOwnProperty",
107
- "Infinity",
108
- "isFinite",
109
- "isNaN",
110
- "isPrototypeOf",
111
- "length",
112
- "Math",
113
- "name",
114
- "NaN",
115
- "Number",
116
- "Object",
117
- "prototype",
118
- "String",
119
- "toString",
120
- "undefined",
121
- "valueOf"
122
- ]);
123
- /**
124
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
125
- *
126
- * @example
127
- * ```ts
128
- * isValidVarName('status') // true
129
- * isValidVarName('class') // false (reserved word)
130
- * isValidVarName('42foo') // false (starts with digit)
131
- * ```
132
- */
133
- function isValidVarName(name) {
134
- if (!name || reservedWords.has(name)) return false;
135
- return isIdentifier(name);
136
- }
137
- /**
138
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
139
- *
140
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
141
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
142
- * deciding whether an object key needs quoting.
143
- *
144
- * @example
145
- * ```ts
146
- * isIdentifier('name') // true
147
- * isIdentifier('x-total')// false
148
- * ```
149
- */
150
- function isIdentifier(name) {
151
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
152
- }
153
- //#endregion
154
- //#region ../../internals/utils/src/url.ts
155
- function transformParam(raw, casing) {
156
- const param = isValidVarName(raw) ? raw : camelCase(raw);
157
- return casing === "camelcase" ? camelCase(param) : param;
158
- }
159
- function toParamsObject(path, { replacer, casing } = {}) {
160
- const params = {};
161
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
162
- const param = transformParam(match[1], casing);
163
- const key = replacer ? replacer(param) : param;
164
- params[key] = key;
165
- }
166
- return Object.keys(params).length > 0 ? params : null;
167
- }
168
- /**
169
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
170
- */
171
- var Url = class Url {
172
- /**
173
- * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
174
- *
175
- * @example
176
- * Url.canParse('https://petstore.swagger.io/v2') // true
177
- * Url.canParse('/pet/{petId}') // false
178
- */
179
- static canParse(url, base) {
180
- return URL.canParse(url, base);
181
- }
182
- /**
183
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
184
- *
185
- * @example
186
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
187
- */
188
- static toPath(path) {
189
- return path.replace(/\{([^}]+)\}/g, ":$1");
190
- }
191
- /**
192
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
193
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
194
- * key.
195
- *
196
- * @example
197
- * Url.toCasedTemplate('/projects/{project_id}', { casing: 'camelcase' }) // '/projects/{projectId}'
198
- */
199
- static toCasedTemplate(path, { casing } = {}) {
200
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name, casing)}}`);
201
- }
202
- /**
203
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
204
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
205
- * and `casing` controls parameter identifier casing.
206
- *
207
- * @example
208
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
209
- *
210
- * @example
211
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
212
- */
213
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
214
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
215
- if (i % 2 === 0) return part;
216
- const param = transformParam(part, casing);
217
- return `\${${replacer ? replacer(param) : param}}`;
218
- }).join("");
219
- return `\`${prefix ?? ""}${result}\``;
220
- }
221
- /**
222
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
223
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
224
- * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
225
- * literal. Shared by the client and cypress generators that pass a grouped `path` object.
226
- *
227
- * @example
228
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
229
- */
230
- static toGroupedTemplateString(path, { prefix } = {}) {
231
- return Url.toTemplateString(path, {
232
- prefix,
233
- casing: "camelcase",
234
- replacer: (name) => `path.${name}`
235
- });
236
- }
237
- /**
238
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
239
- * expression when `stringify` is set.
240
- *
241
- * @example
242
- * Url.toObject('/pet/{petId}')
243
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
244
- */
245
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
246
- const object = {
247
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
248
- replacer,
249
- casing
250
- }),
251
- params: toParamsObject(path, {
252
- replacer,
253
- casing
254
- })
255
- };
256
- if (stringify) {
257
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
258
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
259
- return `{ url: '${object.url}' }`;
260
- }
261
- return object;
262
- }
263
- };
264
- //#endregion
265
8
  //#region ../../internals/shared/src/params.ts
266
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
267
- /**
268
- * Applies camelCase to parameter names and returns a new array without mutating the input.
269
- *
270
- * Run it before handing parameters to schema builders so output property keys get the right casing
271
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
272
- * original array is returned unchanged. Results are cached per input array.
273
- */
274
- function caseParams(params, casing) {
275
- if (!casing) return params;
276
- const cached = caseParamsCache.get(params);
277
- if (cached) return cached;
278
- const result = params.map((param) => ({
279
- ...param,
280
- name: camelCase(param.name)
281
- }));
282
- caseParamsCache.set(params, result);
283
- return result;
284
- }
285
9
  /**
286
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
10
+ * Drops parameters that share the same name, keeping the first.
287
11
  *
288
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
289
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
290
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
291
- * identity so the resulting group is collision-free regardless of the names each caller carries.
12
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
13
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
14
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
15
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
292
16
  */
293
- function dedupeByCasedName(params) {
17
+ function dedupeParams(params) {
294
18
  const seen = /* @__PURE__ */ new Set();
295
19
  return params.filter((param) => {
296
- const key = camelCase(param.name);
297
- if (seen.has(key)) return false;
298
- seen.add(key);
20
+ if (seen.has(param.name)) return false;
21
+ seen.add(param.name);
299
22
  return true;
300
23
  });
301
24
  }
@@ -320,11 +43,30 @@ function operationFileEntry(node, name, extname = ".ts") {
320
43
  path: node.path
321
44
  };
322
45
  }
46
+ /**
47
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
48
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
49
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
50
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
51
+ *
52
+ * @example Cache `plugin-ts`'s file for the current operation
53
+ * ```ts
54
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
55
+ * ```
56
+ */
57
+ function resolveDependencyOperationFile(options) {
58
+ const { cache, node, resolver, root, output, group } = options;
59
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
60
+ ...operationFileEntry(node, node.operationId),
61
+ root,
62
+ output,
63
+ group: group ?? void 0
64
+ }));
65
+ }
323
66
  function getOperationLink(node, link) {
324
67
  if (!link) return null;
325
68
  if (typeof link === "function") return link(node) ?? null;
326
- if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
327
- return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
69
+ return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
328
70
  }
329
71
  /**
330
72
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
@@ -425,14 +167,24 @@ function buildOperationComments(node, options = {}) {
425
167
  if (!splitLines) return filteredComments;
426
168
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
427
169
  }
428
- function getOperationParameters(node, options = {}) {
429
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
430
- return {
431
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
432
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
433
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
434
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
170
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
171
+ /**
172
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
173
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
174
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
175
+ * same parameters once per plugin.
176
+ */
177
+ function getOperationParameters(node) {
178
+ const cached = operationParameterGroupsByNode.get(node);
179
+ if (cached) return cached;
180
+ const groups = {
181
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
182
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
183
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
184
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
435
185
  };
186
+ operationParameterGroupsByNode.set(node, groups);
187
+ return groups;
436
188
  }
437
189
  function getStatusCodeNumber(statusCode) {
438
190
  const code = Number(statusCode);
@@ -456,6 +208,33 @@ function findSuccessStatusCode(responses) {
456
208
  return null;
457
209
  }
458
210
  //#endregion
211
+ //#region ../../internals/utils/src/casing.ts
212
+ /**
213
+ * Shared implementation for camelCase and PascalCase conversion.
214
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
215
+ * and capitalizes each word according to `pascal`.
216
+ *
217
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
218
+ */
219
+ function toCamelOrPascal(text, pascal) {
220
+ 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) => {
221
+ if (word.length > 1 && word === word.toUpperCase()) return word;
222
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
223
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
224
+ }
225
+ /**
226
+ * Converts `text` to camelCase.
227
+ *
228
+ * @example Word boundaries
229
+ * `camelCase('hello-world') // 'helloWorld'`
230
+ *
231
+ * @example With a prefix
232
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
233
+ */
234
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
235
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
236
+ }
237
+ //#endregion
459
238
  //#region ../../internals/shared/src/group.ts
460
239
  /**
461
240
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -742,23 +521,30 @@ function resolveContractClient(options) {
742
521
  * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
743
522
  * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
744
523
  * read `RequestConfig` / `ResponseErrorConfig` from.
524
+ *
525
+ * The result is cached in the current node's `cache` (`ctx.cache`) under the client plugin's name,
526
+ * so several dependents reading the same client plugin for one operation in a single pass
527
+ * (react-query's query/mutation/infinite generators, vue-query, swr, the MCP handler, ...) share
528
+ * one computed result instead of each re-deriving the name and path.
745
529
  */
746
530
  function resolveClientOperation(options) {
747
- const { clientPlugin, driver, node, root, output } = options;
531
+ const { clientPlugin, driver, node, root, output, cache } = options;
748
532
  if (!clientPlugin) return null;
749
- const resolver = driver.getResolver(clientPlugin.pluginName);
750
- const plugin = driver.getPlugin(clientPlugin.pluginName);
751
- const file = resolver.file({
752
- ...operationFileEntry(node, node.operationId),
753
- root,
754
- output: plugin?.options?.output ?? output,
755
- group: plugin?.options?.group ?? void 0
533
+ return cache.ensureItem(`${clientPlugin.pluginName}:clientOperation`, () => {
534
+ const resolver = driver.getResolver(clientPlugin.pluginName);
535
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
536
+ const file = resolver.file({
537
+ ...operationFileEntry(node, node.operationId),
538
+ root,
539
+ output: plugin?.options?.output ?? output,
540
+ group: plugin?.options?.group ?? void 0
541
+ });
542
+ return {
543
+ name: resolver.name(node.operationId),
544
+ path: file.path,
545
+ clientPath: path.resolve(root, ".kubb/client.ts")
546
+ };
756
547
  });
757
- return {
758
- name: resolver.name(node.operationId),
759
- path: file.path,
760
- clientPath: path.resolve(root, ".kubb/client.ts")
761
- };
762
548
  }
763
549
  //#endregion
764
550
  //#region src/generators/mcpGenerator.tsx
@@ -783,7 +569,8 @@ const mcpGenerator = defineGenerator({
783
569
  driver,
784
570
  node,
785
571
  root,
786
- output
572
+ output,
573
+ cache: ctx.cache
787
574
  });
788
575
  if (!contractOp) return null;
789
576
  const optionsName = tsResolver.response.options(node);
@@ -798,14 +585,13 @@ const mcpGenerator = defineGenerator({
798
585
  output,
799
586
  group: group ?? void 0
800
587
  }),
801
- fileTs: tsResolver.file({
802
- name: node.operationId,
803
- extname: ".ts",
804
- tag: node.tags[0] ?? "default",
805
- path: node.path,
588
+ fileTs: resolveDependencyOperationFile({
589
+ cache: ctx.cache,
590
+ node,
591
+ resolver: tsResolver,
806
592
  root,
807
593
  output: pluginTs.options?.output ?? output,
808
- group: pluginTs.options?.group ?? void 0
594
+ group: pluginTs.options?.group
809
595
  })
810
596
  };
811
597
  return /* @__PURE__ */ jsxs(File, {