@kubb/plugin-mcp 5.0.0-beta.99 → 5.0.1

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,223 +1,10 @@
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
- /**
156
- * Keeps the OpenAPI parameter name as-is when it is already a valid JS identifier, and
157
- * camelCases it only enough to become one otherwise (for example a hyphenated path segment).
158
- */
159
- function transformParam(raw) {
160
- return isValidVarName(raw) ? raw : camelCase(raw);
161
- }
162
- /**
163
- * Helpers for OpenAPI/Swagger paths.
164
- */
165
- var Url = class Url {
166
- /**
167
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
168
- *
169
- * @example
170
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
171
- */
172
- static toPath(path) {
173
- return path.replace(/\{([^}]+)\}/g, ":$1");
174
- }
175
- /**
176
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
177
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
178
- * key.
179
- *
180
- * @example
181
- * Url.toSafeTemplate('/user/{monetary-account-id}') // '/user/{monetaryAccountId}'
182
- */
183
- static toSafeTemplate(path) {
184
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name)}}`);
185
- }
186
- /**
187
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
188
- * `prefix` is prepended inside the literal, and `replacer` transforms each parameter name.
189
- *
190
- * @example
191
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
192
- *
193
- * @example
194
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
195
- */
196
- static toTemplateString(path, { prefix, replacer } = {}) {
197
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
198
- if (i % 2 === 0) return part;
199
- const param = transformParam(part);
200
- return `\${${replacer ? replacer(param) : param}}`;
201
- }).join("");
202
- return `\`${prefix ?? ""}${result}\``;
203
- }
204
- /**
205
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
206
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
207
- * names match the generated `path` type, and `prefix` is prepended inside the literal. Shared by
208
- * the client and cypress generators that pass a grouped `path` object.
209
- *
210
- * @example
211
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
212
- */
213
- static toGroupedTemplateString(path, { prefix } = {}) {
214
- return Url.toTemplateString(path, {
215
- prefix,
216
- replacer: (name) => `path.${name}`
217
- });
218
- }
219
- };
220
- //#endregion
221
8
  //#region ../../internals/shared/src/params.ts
222
9
  /**
223
10
  * Drops parameters that share the same name, keeping the first.
@@ -256,11 +43,30 @@ function operationFileEntry(node, name, extname = ".ts") {
256
43
  path: node.path
257
44
  };
258
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
+ }
259
66
  function getOperationLink(node, link) {
260
67
  if (!link) return null;
261
68
  if (typeof link === "function") return link(node) ?? null;
262
- if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
263
- return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
69
+ return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
264
70
  }
265
71
  /**
266
72
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
@@ -361,16 +167,24 @@ function buildOperationComments(node, options = {}) {
361
167
  if (!splitLines) return filteredComments;
362
168
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
363
169
  }
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
+ */
364
177
  function getOperationParameters(node) {
365
- return {
366
- path: dedupeParams(node.parameters.filter((param) => param.in === "path").map((param) => isValidVarName(param.name) ? param : {
367
- ...param,
368
- name: camelCase(param.name)
369
- })),
178
+ const cached = operationParameterGroupsByNode.get(node);
179
+ if (cached) return cached;
180
+ const groups = {
181
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
370
182
  query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
371
183
  header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
372
184
  cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
373
185
  };
186
+ operationParameterGroupsByNode.set(node, groups);
187
+ return groups;
374
188
  }
375
189
  function getStatusCodeNumber(statusCode) {
376
190
  const code = Number(statusCode);
@@ -394,6 +208,33 @@ function findSuccessStatusCode(responses) {
394
208
  return null;
395
209
  }
396
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
397
238
  //#region ../../internals/shared/src/group.ts
398
239
  /**
399
240
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -680,23 +521,30 @@ function resolveContractClient(options) {
680
521
  * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
681
522
  * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
682
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.
683
529
  */
684
530
  function resolveClientOperation(options) {
685
- const { clientPlugin, driver, node, root, output } = options;
531
+ const { clientPlugin, driver, node, root, output, cache } = options;
686
532
  if (!clientPlugin) return null;
687
- const resolver = driver.getResolver(clientPlugin.pluginName);
688
- const plugin = driver.getPlugin(clientPlugin.pluginName);
689
- const file = resolver.file({
690
- ...operationFileEntry(node, node.operationId),
691
- root,
692
- output: plugin?.options?.output ?? output,
693
- 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
+ };
694
547
  });
695
- return {
696
- name: resolver.name(node.operationId),
697
- path: file.path,
698
- clientPath: path.resolve(root, ".kubb/client.ts")
699
- };
700
548
  }
701
549
  //#endregion
702
550
  //#region src/generators/mcpGenerator.tsx
@@ -721,7 +569,8 @@ const mcpGenerator = defineGenerator({
721
569
  driver,
722
570
  node,
723
571
  root,
724
- output
572
+ output,
573
+ cache: ctx.cache
725
574
  });
726
575
  if (!contractOp) return null;
727
576
  const optionsName = tsResolver.response.options(node);
@@ -736,14 +585,13 @@ const mcpGenerator = defineGenerator({
736
585
  output,
737
586
  group: group ?? void 0
738
587
  }),
739
- fileTs: tsResolver.file({
740
- name: node.operationId,
741
- extname: ".ts",
742
- tag: node.tags[0] ?? "default",
743
- path: node.path,
588
+ fileTs: resolveDependencyOperationFile({
589
+ cache: ctx.cache,
590
+ node,
591
+ resolver: tsResolver,
744
592
  root,
745
593
  output: pluginTs.options?.output ?? output,
746
- group: pluginTs.options?.group ?? void 0
594
+ group: pluginTs.options?.group
747
595
  })
748
596
  };
749
597
  return /* @__PURE__ */ jsxs(File, {