@kubb/plugin-cypress 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,333 +1,67 @@
1
1
  import "./rolldown-runtime-C0LytTxp.js";
2
- import { Resolver, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
2
+ import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
3
3
  import { File, Function, jsxRenderer } from "kubb/jsx";
4
4
  import { jsx, jsxs } from "kubb/jsx/jsx-runtime";
5
5
  import { pluginTsName } from "@kubb/plugin-ts";
6
- //#region ../../internals/utils/src/casing.ts
7
- /**
8
- * Shared implementation for camelCase and PascalCase conversion.
9
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
10
- * and capitalizes each word according to `pascal`.
11
- *
12
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
13
- */
14
- function toCamelOrPascal(text, pascal) {
15
- 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) => {
16
- if (word.length > 1 && word === word.toUpperCase()) return word;
17
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
18
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
19
- }
6
+ //#region ../../internals/shared/src/params.ts
20
7
  /**
21
- * Converts `text` to camelCase.
22
- *
23
- * @example Word boundaries
24
- * `camelCase('hello-world') // 'helloWorld'`
8
+ * Drops parameters that share the same name, keeping the first.
25
9
  *
26
- * @example With a prefix
27
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
10
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
11
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
12
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
13
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
28
14
  */
29
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
30
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
15
+ function dedupeParams(params) {
16
+ const seen = /* @__PURE__ */ new Set();
17
+ return params.filter((param) => {
18
+ if (seen.has(param.name)) return false;
19
+ seen.add(param.name);
20
+ return true;
21
+ });
31
22
  }
32
23
  //#endregion
33
- //#region ../../internals/utils/src/reserved.ts
34
- /**
35
- * JavaScript and Java reserved words.
36
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
37
- */
38
- const reservedWords = /* @__PURE__ */ new Set([
39
- "abstract",
40
- "arguments",
41
- "boolean",
42
- "break",
43
- "byte",
44
- "case",
45
- "catch",
46
- "char",
47
- "class",
48
- "const",
49
- "continue",
50
- "debugger",
51
- "default",
52
- "delete",
53
- "do",
54
- "double",
55
- "else",
56
- "enum",
57
- "eval",
58
- "export",
59
- "extends",
60
- "false",
61
- "final",
62
- "finally",
63
- "float",
64
- "for",
65
- "function",
66
- "goto",
67
- "if",
68
- "implements",
69
- "import",
70
- "in",
71
- "instanceof",
72
- "int",
73
- "interface",
74
- "let",
75
- "long",
76
- "native",
77
- "new",
78
- "null",
79
- "package",
80
- "private",
81
- "protected",
82
- "public",
83
- "return",
84
- "short",
85
- "static",
86
- "super",
87
- "switch",
88
- "synchronized",
89
- "this",
90
- "throw",
91
- "throws",
92
- "transient",
93
- "true",
94
- "try",
95
- "typeof",
96
- "var",
97
- "void",
98
- "volatile",
99
- "while",
100
- "with",
101
- "yield",
102
- "Array",
103
- "Date",
104
- "hasOwnProperty",
105
- "Infinity",
106
- "isFinite",
107
- "isNaN",
108
- "isPrototypeOf",
109
- "length",
110
- "Math",
111
- "name",
112
- "NaN",
113
- "Number",
114
- "Object",
115
- "prototype",
116
- "String",
117
- "toString",
118
- "undefined",
119
- "valueOf"
120
- ]);
24
+ //#region ../../internals/shared/src/operation.ts
121
25
  /**
122
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
26
+ * Builds the `ResolverFileParams` every operation generator passes to
27
+ * `resolver.file`: a file named `name`, tagged by the operation's first
28
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
29
+ * that was repeated at dozens of call sites across the client and query plugins.
123
30
  *
124
31
  * @example
125
32
  * ```ts
126
- * isValidVarName('status') // true
127
- * isValidVarName('class') // false (reserved word)
128
- * isValidVarName('42foo') // false (starts with digit)
33
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
129
34
  * ```
130
35
  */
131
- function isValidVarName(name) {
132
- if (!name || reservedWords.has(name)) return false;
133
- return isIdentifier(name);
36
+ function operationFileEntry(node, name, extname = ".ts") {
37
+ return {
38
+ name,
39
+ extname,
40
+ tag: node.tags[0] ?? "default",
41
+ path: node.path
42
+ };
134
43
  }
135
44
  /**
136
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
137
- *
138
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
139
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
140
- * deciding whether an object key needs quoting.
45
+ * Resolves a dependency plugin's generated file for `node.operationId`, cached in `cache` (the
46
+ * current node's `ctx.cache`) under the resolver's own plugin name. Several dependents reading the
47
+ * same dependency for the same operation in one pass (a query plugin's several hook generators, the
48
+ * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
141
49
  *
142
- * @example
50
+ * @example Cache `plugin-ts`'s file for the current operation
143
51
  * ```ts
144
- * isIdentifier('name') // true
145
- * isIdentifier('x-total')// false
52
+ * const fileTs = resolveDependencyOperationFile({ cache: ctx.cache, node, resolver: tsResolver, root, output })
146
53
  * ```
147
54
  */
148
- function isIdentifier(name) {
149
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
150
- }
151
- //#endregion
152
- //#region ../../internals/utils/src/url.ts
153
- function transformParam(raw, casing) {
154
- const param = isValidVarName(raw) ? raw : camelCase(raw);
155
- return casing === "camelcase" ? camelCase(param) : param;
156
- }
157
- function toParamsObject(path, { replacer, casing } = {}) {
158
- const params = {};
159
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
160
- const param = transformParam(match[1], casing);
161
- const key = replacer ? replacer(param) : param;
162
- params[key] = key;
163
- }
164
- return Object.keys(params).length > 0 ? params : null;
165
- }
166
- /**
167
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
168
- */
169
- var Url = class Url {
170
- /**
171
- * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
172
- *
173
- * @example
174
- * Url.canParse('https://petstore.swagger.io/v2') // true
175
- * Url.canParse('/pet/{petId}') // false
176
- */
177
- static canParse(url, base) {
178
- return URL.canParse(url, base);
179
- }
180
- /**
181
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
182
- *
183
- * @example
184
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
185
- */
186
- static toPath(path) {
187
- return path.replace(/\{([^}]+)\}/g, ":$1");
188
- }
189
- /**
190
- * Rewrites OpenAPI placeholder names while keeping the `{...}` braces, so the generated `url`
191
- * literal aligns with the grouped `path` request option that the runtime client interpolates by
192
- * key.
193
- *
194
- * @example
195
- * Url.toCasedTemplate('/projects/{project_id}', { casing: 'camelcase' }) // '/projects/{projectId}'
196
- */
197
- static toCasedTemplate(path, { casing } = {}) {
198
- return path.replace(/\{([^}]+)\}/g, (_, name) => `{${transformParam(name, casing)}}`);
199
- }
200
- /**
201
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
202
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
203
- * and `casing` controls parameter identifier casing.
204
- *
205
- * @example
206
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
207
- *
208
- * @example
209
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
210
- */
211
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
212
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
213
- if (i % 2 === 0) return part;
214
- const param = transformParam(part, casing);
215
- return `\${${replacer ? replacer(param) : param}}`;
216
- }).join("");
217
- return `\`${prefix ?? ""}${result}\``;
218
- }
219
- /**
220
- * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
221
- * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
222
- * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
223
- * literal. Shared by the client and cypress generators that pass a grouped `path` object.
224
- *
225
- * @example
226
- * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
227
- */
228
- static toGroupedTemplateString(path, { prefix } = {}) {
229
- return Url.toTemplateString(path, {
230
- prefix,
231
- casing: "camelcase",
232
- replacer: (name) => `path.${name}`
233
- });
234
- }
235
- /**
236
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
237
- * expression when `stringify` is set.
238
- *
239
- * @example
240
- * Url.toObject('/pet/{petId}')
241
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
242
- */
243
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
244
- const object = {
245
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
246
- replacer,
247
- casing
248
- }),
249
- params: toParamsObject(path, {
250
- replacer,
251
- casing
252
- })
253
- };
254
- if (stringify) {
255
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
256
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
257
- return `{ url: '${object.url}' }`;
258
- }
259
- return object;
260
- }
261
- };
262
- //#endregion
263
- //#region ../../internals/shared/src/params.ts
264
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
265
- /**
266
- * Applies camelCase to parameter names and returns a new array without mutating the input.
267
- *
268
- * Run it before handing parameters to schema builders so output property keys get the right casing
269
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
270
- * original array is returned unchanged. Results are cached per input array.
271
- */
272
- function caseParams(params, casing) {
273
- if (!casing) return params;
274
- const cached = caseParamsCache.get(params);
275
- if (cached) return cached;
276
- const result = params.map((param) => ({
277
- ...param,
278
- name: camelCase(param.name)
55
+ function resolveDependencyOperationFile(options) {
56
+ const { cache, node, resolver, root, output, group } = options;
57
+ return cache.ensureItem(`${resolver.pluginName}:operationFile`, () => resolver.file({
58
+ ...operationFileEntry(node, node.operationId),
59
+ root,
60
+ output,
61
+ group: group ?? void 0
279
62
  }));
280
- caseParamsCache.set(params, result);
281
- return result;
282
63
  }
283
64
  /**
284
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
285
- *
286
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
287
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
288
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
289
- * identity so the resulting group is collision-free regardless of the names each caller carries.
290
- */
291
- function dedupeByCasedName(params) {
292
- const seen = /* @__PURE__ */ new Set();
293
- return params.filter((param) => {
294
- const key = camelCase(param.name);
295
- if (seen.has(key)) return false;
296
- seen.add(key);
297
- return true;
298
- });
299
- }
300
- function buildParamsMapping(originalParams, mappedParams) {
301
- const mapping = {};
302
- let hasChanged = false;
303
- originalParams.forEach((param, i) => {
304
- const mappedName = mappedParams[i]?.name ?? param.name;
305
- mapping[param.name] = mappedName;
306
- if (param.name !== mappedName) hasChanged = true;
307
- });
308
- return hasChanged ? mapping : null;
309
- }
310
- function toAccess(object, name) {
311
- return isValidVarName(name) ? `${object}.${name}` : `${object}[${JSON.stringify(name)}]`;
312
- }
313
- /**
314
- * Renders the object-literal expression that renames the camelCased keys of a grouped request
315
- * option back to the names the OpenAPI document declares, guarded so an omitted optional group
316
- * stays omitted. Shared by the client and cypress generators, which pass a `buildParamsMapping`
317
- * result and the source expression to read the keys from.
318
- *
319
- * @example
320
- * ```ts
321
- * buildParamsRemapExpression({ source: 'config.query', mapping: { include_deleted: 'includeDeleted' } })
322
- * // 'config.query ? { "include_deleted": config.query.includeDeleted } : config.query'
323
- * ```
324
- */
325
- function buildParamsRemapExpression({ source, mapping }) {
326
- return `${source} ? { ${Object.entries(mapping).map(([originalName, casedName]) => `${JSON.stringify(originalName)}: ${toAccess(source, casedName)}`).join(", ")} } : ${source}`;
327
- }
328
- //#endregion
329
- //#region ../../internals/shared/src/operation.ts
330
- /**
331
65
  * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
332
66
  * are present and the union, default, and form-data flags the client uses to pick one.
333
67
  */
@@ -409,14 +143,24 @@ function buildRequestParamsSignature(node, resolver, options = {}) {
409
143
  groups
410
144
  };
411
145
  }
412
- function getOperationParameters(node, options = {}) {
413
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
414
- return {
415
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
416
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
417
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
418
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
146
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
147
+ /**
148
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
149
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
150
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
151
+ * same parameters once per plugin.
152
+ */
153
+ function getOperationParameters(node) {
154
+ const cached = operationParameterGroupsByNode.get(node);
155
+ if (cached) return cached;
156
+ const groups = {
157
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
158
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
159
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
160
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
419
161
  };
162
+ operationParameterGroupsByNode.set(node, groups);
163
+ return groups;
420
164
  }
421
165
  function getStatusCodeNumber(statusCode) {
422
166
  const code = Number(statusCode);
@@ -447,7 +191,7 @@ function resolveStatusCodeNames(node, resolver) {
447
191
  }
448
192
  const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
449
193
  function resolveOperationTypeNames(node, resolver, options = {}) {
450
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
194
+ const cacheKey = `${node.operationId}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
451
195
  let byResolver = typeNamesByResolver.get(resolver);
452
196
  if (byResolver) {
453
197
  const cached = byResolver.get(cacheKey);
@@ -456,7 +200,7 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
456
200
  byResolver = /* @__PURE__ */ new Map();
457
201
  typeNamesByResolver.set(resolver, byResolver);
458
202
  }
459
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
203
+ const { path, query, header } = getOperationParameters(node);
460
204
  const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
461
205
  const exclude = new Set(options.exclude ?? []);
462
206
  const paramNames = options.includeParams === false ? [] : [
@@ -478,6 +222,33 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
478
222
  return result;
479
223
  }
480
224
  //#endregion
225
+ //#region ../../internals/utils/src/casing.ts
226
+ /**
227
+ * Shared implementation for camelCase and PascalCase conversion.
228
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
229
+ * and capitalizes each word according to `pascal`.
230
+ *
231
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
232
+ */
233
+ function toCamelOrPascal(text, pascal) {
234
+ 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) => {
235
+ if (word.length > 1 && word === word.toUpperCase()) return word;
236
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
237
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
238
+ }
239
+ /**
240
+ * Converts `text` to camelCase.
241
+ *
242
+ * @example Word boundaries
243
+ * `camelCase('hello-world') // 'helloWorld'`
244
+ *
245
+ * @example With a prefix
246
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
247
+ */
248
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
249
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
250
+ }
251
+ //#endregion
481
252
  //#region ../../internals/shared/src/group.ts
482
253
  /**
483
254
  * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
@@ -512,24 +283,14 @@ function createGroupConfig(group) {
512
283
  //#region src/components/Request.tsx
513
284
  function Request({ baseURL = "", name, resolver, node }) {
514
285
  if (!ast.isHttpOperationNode(node)) return null;
515
- const { query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node, { paramsCasing: "original" });
516
- const { query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node);
517
- const queryParamsMapping = buildParamsMapping(originalQueryParams, casedQueryParams);
518
- const headerParamsMapping = buildParamsMapping(originalHeaderParams, casedHeaderParams);
519
286
  const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
520
287
  const paramsSignature = [signature, "options: Partial<Cypress.RequestOptions> = {}"].filter(Boolean).join(", ");
521
288
  const responseType = resolver.response.response(node);
522
289
  const returnType = `Cypress.Chainable<${responseType}>`;
523
290
  const urlTemplate = Url.toGroupedTemplateString(node.path, { prefix: baseURL });
524
291
  const requestOptions = [`method: '${node.method}'`, `url: ${urlTemplate}`];
525
- if (groups.query) requestOptions.push(queryParamsMapping ? `qs: ${buildParamsRemapExpression({
526
- source: "query",
527
- mapping: queryParamsMapping
528
- })}` : "qs: query");
529
- if (groups.headers) requestOptions.push(headerParamsMapping ? `headers: ${buildParamsRemapExpression({
530
- source: "headers",
531
- mapping: headerParamsMapping
532
- })}` : "headers");
292
+ if (groups.query) requestOptions.push("qs: query");
293
+ if (groups.headers) requestOptions.push("headers");
533
294
  if (groups.body) requestOptions.push("body");
534
295
  requestOptions.push("...options");
535
296
  const requestCall = `return cy.request<${responseType}>({
@@ -577,14 +338,13 @@ const cypressGenerator = defineGenerator({
577
338
  output,
578
339
  group: group ?? void 0
579
340
  }),
580
- fileTs: tsResolver.file({
581
- name: node.operationId,
582
- extname: ".ts",
583
- tag: node.tags[0] ?? "default",
584
- path: node.path,
341
+ fileTs: resolveDependencyOperationFile({
342
+ cache: ctx.cache,
343
+ node,
344
+ resolver: tsResolver,
585
345
  root,
586
346
  output: pluginTs.options?.output ?? output,
587
- group: pluginTs.options?.group ?? void 0
347
+ group: pluginTs.options?.group
588
348
  })
589
349
  };
590
350
  return /* @__PURE__ */ jsxs(File, {