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