@kubb/plugin-cypress 5.0.0-beta.10 → 5.0.0-beta.101

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,335 +1,165 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
3
- import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
4
- import { File, Function, jsxRenderer } from "@kubb/renderer-jsx";
5
- import { jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
6
- //#region ../../internals/utils/src/casing.ts
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
+ import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
3
+ import { File, Function, jsxRenderer } from "kubb/jsx";
4
+ import { jsx, jsxs } from "kubb/jsx/jsx-runtime";
5
+ import { pluginTsName } from "@kubb/plugin-ts";
6
+ //#region ../../internals/shared/src/params.ts
7
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`.
8
+ * Drops parameters that share the same name, keeping the first.
11
9
  *
12
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
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.
13
14
  */
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
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
18
- return word.charAt(0).toUpperCase() + word.slice(1);
19
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
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
+ });
20
22
  }
23
+ //#endregion
24
+ //#region ../../internals/shared/src/operation.ts
21
25
  /**
22
- * Splits `text` on `.` and applies `transformPart` to each segment.
23
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
24
- * Segments are joined with `/` to form a file path.
25
- *
26
- * Only splits on dots followed by a letter so that version numbers
27
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
26
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
27
+ * are present and the union, default, and form-data flags the client uses to pick one.
28
28
  */
29
- function applyToFileParts(text, transformPart) {
30
- const parts = text.split(/\.(?=[a-zA-Z])/);
31
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
29
+ function buildContentTypeInfo(contentTypes) {
30
+ const isMultipleContentTypes = contentTypes.length > 1;
31
+ return {
32
+ contentTypes,
33
+ isMultipleContentTypes,
34
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
35
+ defaultContentType: contentTypes[0] ?? "application/json",
36
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
37
+ };
38
+ }
39
+ function getContentTypeInfo(node) {
40
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
32
41
  }
33
42
  /**
34
- * Converts `text` to camelCase.
35
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
36
- *
37
- * @example
38
- * camelCase('hello-world') // 'helloWorld'
39
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
43
+ * The request-body counterpart for the primary success response: the content types it documents and
44
+ * whether several are present, so the client can let a caller pick which one to accept.
40
45
  */
41
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
42
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
43
- prefix,
44
- suffix
45
- } : {}));
46
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
46
+ function getResponseContentTypeInfo(node) {
47
+ return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
48
+ }
49
+ function buildRequestConfigType(node) {
50
+ const request = getContentTypeInfo(node);
51
+ const response = getResponseContentTypeInfo(node);
52
+ const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
53
+ const members = [request.isMultipleContentTypes ? `request?: ${request.contentTypeUnion}` : null, response.isMultipleContentTypes ? `response?: ${response.contentTypeUnion}` : null].filter(Boolean);
54
+ return members.length ? `${configType} & { contentType?: { ${members.join("; ")} } }` : configType;
47
55
  }
48
- //#endregion
49
- //#region ../../internals/utils/src/reserved.ts
50
56
  /**
51
- * JavaScript and Java reserved words.
52
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
57
+ * Which of the grouped request options an operation carries.
53
58
  */
54
- const reservedWords = new Set([
55
- "abstract",
56
- "arguments",
57
- "boolean",
58
- "break",
59
- "byte",
60
- "case",
61
- "catch",
62
- "char",
63
- "class",
64
- "const",
65
- "continue",
66
- "debugger",
67
- "default",
68
- "delete",
69
- "do",
70
- "double",
71
- "else",
72
- "enum",
73
- "eval",
74
- "export",
75
- "extends",
76
- "false",
77
- "final",
78
- "finally",
79
- "float",
80
- "for",
81
- "function",
82
- "goto",
83
- "if",
84
- "implements",
85
- "import",
86
- "in",
87
- "instanceof",
88
- "int",
89
- "interface",
90
- "let",
91
- "long",
92
- "native",
93
- "new",
94
- "null",
95
- "package",
96
- "private",
97
- "protected",
98
- "public",
99
- "return",
100
- "short",
101
- "static",
102
- "super",
103
- "switch",
104
- "synchronized",
105
- "this",
106
- "throw",
107
- "throws",
108
- "transient",
109
- "true",
110
- "try",
111
- "typeof",
112
- "var",
113
- "void",
114
- "volatile",
115
- "while",
116
- "with",
117
- "yield",
118
- "Array",
119
- "Date",
120
- "hasOwnProperty",
121
- "Infinity",
122
- "isFinite",
123
- "isNaN",
124
- "isPrototypeOf",
125
- "length",
126
- "Math",
127
- "name",
128
- "NaN",
129
- "Number",
130
- "Object",
131
- "prototype",
132
- "String",
133
- "toString",
134
- "undefined",
135
- "valueOf"
136
- ]);
59
+ function getRequestGroups(node) {
60
+ const { path, query, header } = getOperationParameters(node);
61
+ return {
62
+ path: path.length > 0,
63
+ query: query.length > 0,
64
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
65
+ headers: header.length > 0
66
+ };
67
+ }
137
68
  /**
138
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
139
- *
140
- * @example
141
- * ```ts
142
- * isValidVarName('status') // true
143
- * isValidVarName('class') // false (reserved word)
144
- * isValidVarName('42foo') // false (starts with digit)
145
- * ```
69
+ * Resolves which grouped request options an operation carries together with whether each group
70
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
71
+ * required, matching the generated `RequestConfig` type.
146
72
  */
147
- function isValidVarName(name) {
148
- if (!name || reservedWords.has(name)) return false;
149
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
73
+ function getRequestGroupOptionality(node) {
74
+ const groups = getRequestGroups(node);
75
+ const { path, query, header } = getOperationParameters(node);
76
+ const hasRequiredPath = path.some((param) => param.required);
77
+ const hasRequiredQuery = query.some((param) => param.required);
78
+ const hasRequiredHeader = header.some((param) => param.required);
79
+ return {
80
+ groups,
81
+ hasRequiredPath,
82
+ hasRequiredQuery,
83
+ hasRequiredHeader,
84
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
85
+ };
150
86
  }
151
- //#endregion
152
- //#region ../../internals/utils/src/urlPath.ts
153
87
  /**
154
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
155
- *
156
- * @example
157
- * const p = new URLPath('/pet/{petId}')
158
- * p.URL // '/pet/:petId'
159
- * p.template // '`/pet/${petId}`'
88
+ * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
89
+ * function, typed from the operation's `Options` (minus `url`). Only the groups the
90
+ * operation actually has are destructured. The trailing `config` parameter carries the
91
+ * runtime `RequestConfig` overrides plus `client`.
160
92
  */
161
- var URLPath = class {
162
- /**
163
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
164
- */
165
- path;
166
- #options;
167
- constructor(path, options = {}) {
168
- this.path = path;
169
- this.#options = options;
170
- }
171
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` `/pet/:petId`.
172
- *
173
- * @example
174
- * ```ts
175
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
176
- * ```
177
- */
178
- get URL() {
179
- return this.toURLPath();
180
- }
181
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
182
- *
183
- * @example
184
- * ```ts
185
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
186
- * new URLPath('/pet/{petId}').isURL // false
187
- * ```
188
- */
189
- get isURL() {
190
- try {
191
- return !!new URL(this.path).href;
192
- } catch {
193
- return false;
194
- }
195
- }
196
- /**
197
- * Converts the OpenAPI path to a TypeScript template literal string.
198
- *
199
- * @example
200
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
201
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
202
- */
203
- get template() {
204
- return this.toTemplateString();
205
- }
206
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
207
- *
208
- * @example
209
- * ```ts
210
- * new URLPath('/pet/{petId}').object
211
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
212
- * ```
213
- */
214
- get object() {
215
- return this.toObject();
216
- }
217
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
218
- *
219
- * @example
220
- * ```ts
221
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
222
- * new URLPath('/pet').params // undefined
223
- * ```
224
- */
225
- get params() {
226
- return this.toParamsObject();
227
- }
228
- #transformParam(raw) {
229
- const param = isValidVarName(raw) ? raw : camelCase(raw);
230
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
231
- }
232
- /**
233
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
234
- */
235
- #eachParam(fn) {
236
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
237
- const raw = match[1];
238
- fn(raw, this.#transformParam(raw));
239
- }
240
- }
241
- toObject({ type = "path", replacer, stringify } = {}) {
242
- const object = {
243
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
244
- params: this.toParamsObject()
245
- };
246
- if (stringify) {
247
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
248
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
249
- return `{ url: '${object.url}' }`;
250
- }
251
- return object;
252
- }
253
- /**
254
- * Converts the OpenAPI path to a TypeScript template literal string.
255
- * An optional `replacer` can transform each extracted parameter name before interpolation.
256
- *
257
- * @example
258
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
259
- */
260
- toTemplateString({ prefix = "", replacer } = {}) {
261
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
262
- if (i % 2 === 0) return part;
263
- const param = this.#transformParam(part);
264
- return `\${${replacer ? replacer(param) : param}}`;
265
- }).join("")}\``;
266
- }
267
- /**
268
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
269
- * An optional `replacer` transforms each parameter name in both key and value positions.
270
- * Returns `undefined` when no path parameters are found.
271
- *
272
- * @example
273
- * ```ts
274
- * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
275
- * // { petId: 'petId', tagId: 'tagId' }
276
- * ```
277
- */
278
- toParamsObject(replacer) {
279
- const params = {};
280
- this.#eachParam((_raw, param) => {
281
- const key = replacer ? replacer(param) : param;
282
- params[key] = key;
283
- });
284
- return Object.keys(params).length > 0 ? params : void 0;
285
- }
286
- /** Converts the OpenAPI path to Express-style colon syntax.
287
- *
288
- * @example
289
- * ```ts
290
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
291
- * ```
292
- */
293
- toURLPath() {
294
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
295
- }
296
- };
297
- //#endregion
298
- //#region ../../internals/shared/src/operation.ts
299
- function getOperationParameters(node, options = {}) {
300
- const params = ast.caseParams(node.parameters, options.paramsCasing);
93
+ function buildRequestParamsSignature(node, resolver, options = {}) {
94
+ const { isConfigurable = true } = options;
95
+ const { groups, isOptional } = getRequestGroupOptionality(node);
96
+ const names = [
97
+ "path",
98
+ "query",
99
+ "body",
100
+ "headers"
101
+ ].filter((key) => groups[key]);
102
+ return {
103
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.response.options(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
104
+ groups
105
+ };
106
+ }
107
+ function getOperationParameters(node) {
301
108
  return {
302
- path: params.filter((param) => param.in === "path"),
303
- query: params.filter((param) => param.in === "query"),
304
- header: params.filter((param) => param.in === "header"),
305
- cookie: params.filter((param) => param.in === "cookie")
109
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
110
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
111
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
112
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
306
113
  };
307
114
  }
308
115
  function getStatusCodeNumber(statusCode) {
309
116
  const code = Number(statusCode);
310
- return Number.isNaN(code) ? void 0 : code;
117
+ return Number.isNaN(code) ? null : code;
118
+ }
119
+ function isSuccessStatusCode(statusCode) {
120
+ const code = getStatusCodeNumber(statusCode);
121
+ return code !== null && code >= 200 && code < 300;
311
122
  }
312
123
  function isErrorStatusCode(statusCode) {
313
124
  const code = getStatusCodeNumber(statusCode);
314
- return code !== void 0 && code >= 400;
125
+ return code !== null && code >= 400;
126
+ }
127
+ function getSuccessResponses(responses) {
128
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
129
+ }
130
+ function getOperationSuccessResponses(node) {
131
+ return getSuccessResponses(node.responses);
132
+ }
133
+ function getPrimarySuccessResponse(node) {
134
+ return getOperationSuccessResponses(node)[0] ?? null;
315
135
  }
316
136
  function resolveErrorNames(node, resolver) {
317
- return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
137
+ return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.response.status(node, response.statusCode));
318
138
  }
319
139
  function resolveStatusCodeNames(node, resolver) {
320
- return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
140
+ return node.responses.map((response) => resolver.response.status(node, response.statusCode));
321
141
  }
142
+ const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
322
143
  function resolveOperationTypeNames(node, resolver, options = {}) {
323
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
144
+ const cacheKey = `${node.operationId}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${options.includeParams === false ? "noparams" : ""}\0${(options.exclude ?? []).join(",")}`;
145
+ let byResolver = typeNamesByResolver.get(resolver);
146
+ if (byResolver) {
147
+ const cached = byResolver.get(cacheKey);
148
+ if (cached) return cached;
149
+ } else {
150
+ byResolver = /* @__PURE__ */ new Map();
151
+ typeNamesByResolver.set(resolver, byResolver);
152
+ }
153
+ const { path, query, header } = getOperationParameters(node);
324
154
  const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
325
155
  const exclude = new Set(options.exclude ?? []);
326
- const paramNames = [
327
- ...path.map((param) => resolver.resolvePathParamsName(node, param)),
328
- ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
329
- ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
156
+ const paramNames = options.includeParams === false ? [] : [
157
+ ...path.map((param) => resolver.param.path(node, param)),
158
+ ...query.map((param) => resolver.param.query(node, param)),
159
+ ...header.map((param) => resolver.param.headers(node, param))
330
160
  ];
331
- const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0, resolver.resolveResponseName(node)];
332
- return (options.order === "body-response-first" ? [
161
+ const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.response.body(node) : null, resolver.response.response(node)];
162
+ const result = (options.order === "body-response-first" ? [
333
163
  ...bodyAndResponseNames,
334
164
  ...paramNames,
335
165
  ...responseStatusNames
@@ -338,53 +168,84 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
338
168
  ...bodyAndResponseNames,
339
169
  ...responseStatusNames
340
170
  ]).filter((name) => Boolean(name) && !exclude.has(name));
171
+ byResolver.set(cacheKey, result);
172
+ return result;
173
+ }
174
+ //#endregion
175
+ //#region ../../internals/utils/src/casing.ts
176
+ /**
177
+ * Shared implementation for camelCase and PascalCase conversion.
178
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
179
+ * and capitalizes each word according to `pascal`.
180
+ *
181
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
182
+ */
183
+ function toCamelOrPascal(text, pascal) {
184
+ 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) => {
185
+ if (word.length > 1 && word === word.toUpperCase()) return word;
186
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
187
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
188
+ }
189
+ /**
190
+ * Converts `text` to camelCase.
191
+ *
192
+ * @example Word boundaries
193
+ * `camelCase('hello-world') // 'helloWorld'`
194
+ *
195
+ * @example With a prefix
196
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
197
+ */
198
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
199
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
200
+ }
201
+ //#endregion
202
+ //#region ../../internals/shared/src/group.ts
203
+ /**
204
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
205
+ * shared default naming so every plugin groups output consistently:
206
+ *
207
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
208
+ * - other groups use the camelCased group (`pet store` → `petStore`).
209
+ *
210
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
211
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
212
+ * per-plugin convention.
213
+ *
214
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * createGroupConfig(group) // shared across every plugin
219
+ * ```
220
+ */
221
+ function createGroupConfig(group) {
222
+ if (!group) return null;
223
+ const defaultName = (ctx) => {
224
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
225
+ return camelCase(ctx.group);
226
+ };
227
+ return {
228
+ ...group,
229
+ name: group.name ? group.name : defaultName
230
+ };
341
231
  }
342
232
  //#endregion
343
233
  //#region src/components/Request.tsx
344
- const declarationPrinter = functionPrinter({ mode: "declaration" });
345
- function Request({ baseURL = "", name, dataReturnType, resolver, node, paramsType, pathParamsType, paramsCasing }) {
346
- const paramsNode = ast.createOperationParams(node, {
347
- paramsType,
348
- pathParamsType,
349
- paramsCasing,
350
- resolver,
351
- extraParams: [ast.createFunctionParameter({
352
- name: "options",
353
- type: ast.createParamsType({
354
- variant: "reference",
355
- name: "Partial<Cypress.RequestOptions>"
356
- }),
357
- default: "{}"
358
- })]
359
- });
360
- const paramsSignature = declarationPrinter.print(paramsNode) ?? "";
361
- const responseType = resolver.resolveResponseName(node);
362
- const returnType = dataReturnType === "data" ? `Cypress.Chainable<${responseType}>` : `Cypress.Chainable<Cypress.Response<${responseType}>>`;
363
- const casedPathParams = getOperationParameters(node, { paramsCasing }).path;
364
- const pathParamNameMap = new Map(casedPathParams.map((p) => [camelCase(p.name), p.name]));
365
- const urlTemplate = new URLPath(node.path, { casing: paramsCasing }).toTemplateString({
366
- prefix: baseURL,
367
- replacer: (param) => pathParamNameMap.get(camelCase(param)) ?? param
368
- });
234
+ function Request({ baseURL = "", name, resolver, node }) {
235
+ if (!ast.isHttpOperationNode(node)) return null;
236
+ const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
237
+ const paramsSignature = [signature, "options: Partial<Cypress.RequestOptions> = {}"].filter(Boolean).join(", ");
238
+ const responseType = resolver.response.response(node);
239
+ const returnType = `Cypress.Chainable<${responseType}>`;
240
+ const urlTemplate = Url.toGroupedTemplateString(node.path, { prefix: baseURL });
369
241
  const requestOptions = [`method: '${node.method}'`, `url: ${urlTemplate}`];
370
- const queryParams = getOperationParameters(node).query;
371
- if (queryParams.length > 0) {
372
- const casedQueryParams = getOperationParameters(node, { paramsCasing }).query;
373
- if (casedQueryParams.some((p, i) => p.name !== queryParams[i].name)) {
374
- const pairs = queryParams.map((orig, i) => `${orig.name}: params.${casedQueryParams[i].name}`).join(", ");
375
- requestOptions.push(`qs: params ? { ${pairs} } : undefined`);
376
- } else requestOptions.push("qs: params");
377
- }
378
- const headerParams = getOperationParameters(node).header;
379
- if (headerParams.length > 0) {
380
- const casedHeaderParams = getOperationParameters(node, { paramsCasing }).header;
381
- if (casedHeaderParams.some((p, i) => p.name !== headerParams[i].name)) {
382
- const pairs = headerParams.map((orig, i) => `'${orig.name}': headers.${casedHeaderParams[i].name}`).join(", ");
383
- requestOptions.push(`headers: headers ? { ${pairs} } : undefined`);
384
- } else requestOptions.push("headers");
385
- }
386
- if (node.requestBody?.content?.[0]?.schema) requestOptions.push("body: data");
242
+ if (groups.query) requestOptions.push("qs: query");
243
+ if (groups.headers) requestOptions.push("headers");
244
+ if (groups.body) requestOptions.push("body");
387
245
  requestOptions.push("...options");
246
+ const requestCall = `return cy.request<${responseType}>({
247
+ ${requestOptions.join(",\n ")}
248
+ }).then((res) => res.body)`;
388
249
  return /* @__PURE__ */ jsx(File.Source, {
389
250
  name,
390
251
  isIndexable: true,
@@ -394,62 +255,70 @@ function Request({ baseURL = "", name, dataReturnType, resolver, node, paramsTyp
394
255
  export: true,
395
256
  params: paramsSignature,
396
257
  returnType,
397
- children: dataReturnType === "data" ? `return cy.request<${responseType}>({
398
- ${requestOptions.join(",\n ")}
399
- }).then((res) => res.body)` : `return cy.request<${responseType}>({
400
- ${requestOptions.join(",\n ")}
401
- })`
258
+ children: requestCall
402
259
  })
403
260
  });
404
261
  }
405
262
  //#endregion
406
263
  //#region src/generators/cypressGenerator.tsx
264
+ /**
265
+ * Built-in generator for `@kubb/plugin-cypress`. Emits one typed
266
+ * `cy.request()` wrapper per OpenAPI operation, ready to call inside Cypress
267
+ * test specs and custom commands.
268
+ */
407
269
  const cypressGenerator = defineGenerator({
408
270
  name: "cypress",
409
271
  renderer: jsxRenderer,
410
272
  operation(node, ctx) {
411
- const { adapter, config, resolver, driver, root } = ctx;
412
- const { output, baseURL, dataReturnType, paramsCasing, paramsType, pathParamsType, group } = ctx.options;
273
+ if (!ast.isHttpOperationNode(node)) return null;
274
+ const { config, resolver, driver, root } = ctx;
275
+ const { output, baseURL, group } = ctx.options;
413
276
  const pluginTs = driver.getPlugin(pluginTsName);
414
277
  if (!pluginTs) return null;
415
278
  const tsResolver = driver.getResolver(pluginTsName);
416
- const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing });
279
+ const importedTypeNames = [tsResolver.response.options(node), ...resolveOperationTypeNames(node, tsResolver, { includeParams: false })];
417
280
  const meta = {
418
- name: resolver.resolveName(node.operationId),
419
- file: resolver.resolveFile({
281
+ name: resolver.name(node.operationId),
282
+ file: resolver.file({
420
283
  name: node.operationId,
421
284
  extname: ".ts",
422
285
  tag: node.tags[0] ?? "default",
423
- path: node.path
424
- }, {
286
+ path: node.path,
425
287
  root,
426
288
  output,
427
- group
289
+ group: group ?? void 0
428
290
  }),
429
- fileTs: tsResolver.resolveFile({
291
+ fileTs: tsResolver.file({
430
292
  name: node.operationId,
431
293
  extname: ".ts",
432
294
  tag: node.tags[0] ?? "default",
433
- path: node.path
434
- }, {
295
+ path: node.path,
435
296
  root,
436
297
  output: pluginTs.options?.output ?? output,
437
- group: pluginTs.options?.group
298
+ group: pluginTs.options?.group ?? void 0
438
299
  })
439
300
  };
440
301
  return /* @__PURE__ */ jsxs(File, {
441
302
  baseName: meta.file.baseName,
442
303
  path: meta.file.path,
443
304
  meta: meta.file.meta,
444
- banner: resolver.resolveBanner(adapter.inputNode, {
305
+ banner: resolver.default.banner(ctx.meta, {
445
306
  output,
446
- config
307
+ config,
308
+ file: {
309
+ path: meta.file.path,
310
+ baseName: meta.file.baseName
311
+ }
447
312
  }),
448
- footer: resolver.resolveFooter(adapter.inputNode, {
313
+ footer: resolver.default.footer(ctx.meta, {
449
314
  output,
450
- config
315
+ config,
316
+ file: {
317
+ path: meta.file.path,
318
+ baseName: meta.file.baseName
319
+ }
451
320
  }),
452
- children: [meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
321
+ children: [importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
453
322
  name: importedTypeNames,
454
323
  root: meta.file.path,
455
324
  path: meta.fileTs.path,
@@ -458,10 +327,6 @@ const cypressGenerator = defineGenerator({
458
327
  name: meta.name,
459
328
  node,
460
329
  resolver: tsResolver,
461
- dataReturnType,
462
- paramsCasing,
463
- paramsType,
464
- pathParamsType,
465
330
  baseURL
466
331
  })]
467
332
  });
@@ -470,87 +335,72 @@ const cypressGenerator = defineGenerator({
470
335
  //#endregion
471
336
  //#region src/resolvers/resolverCypress.ts
472
337
  /**
473
- * Naming convention resolver for Cypress plugin.
338
+ * Default resolver used by `@kubb/plugin-cypress`. Decides the names and file
339
+ * paths for every generated `cy.request()` wrapper. Functions and files use
340
+ * camelCase, matching the convention from `@kubb/plugin-axios` and `@kubb/plugin-fetch`.
474
341
  *
475
- * Provides default naming helpers using camelCase for functions and file paths.
342
+ * @example Resolve a helper name
343
+ * ```ts
344
+ * import { resolverCypress } from '@kubb/plugin-cypress'
476
345
  *
477
- * @example
478
- * `resolverCypress.default('list pets', 'function') // → 'listPets'`
346
+ * resolverCypress.name('list pets') // 'listPets'
347
+ * ```
479
348
  */
480
- const resolverCypress = defineResolver(() => ({
481
- name: "default",
482
- pluginName: "plugin-cypress",
483
- default(name, type) {
484
- return camelCase(name, { isFile: type === "file" });
485
- },
486
- resolveName(name) {
487
- return this.default(name, "function");
488
- },
489
- resolvePathName(name, type) {
490
- return this.default(name, type);
491
- }
492
- }));
349
+ const resolverCypress = createResolver({ pluginName: "plugin-cypress" });
493
350
  //#endregion
494
351
  //#region src/plugin.ts
495
352
  /**
496
- * Canonical plugin name for `@kubb/plugin-cypress`, used to identify the plugin
497
- * in driver lookups and warnings.
353
+ * Canonical plugin name for `@kubb/plugin-cypress`. Used for driver lookups and
354
+ * cross-plugin dependency references.
498
355
  */
499
356
  const pluginCypressName = "plugin-cypress";
500
357
  /**
501
- * The `@kubb/plugin-cypress` plugin factory.
502
- *
503
- * Generates Cypress `cy.request()` test functions from an OpenAPI/AST `RootNode`.
504
- * Walks operations, delegates rendering to the active generators,
505
- * and writes barrel files based on `output.barrelType`.
358
+ * Generates one typed `cy.request()` wrapper per OpenAPI operation. Each helper
359
+ * has typed path params, body, query, and a typed response, so failing API
360
+ * calls in Cypress show up at compile time instead of inside the test runner.
506
361
  *
507
362
  * @example
508
363
  * ```ts
509
- * import pluginCypress from '@kubb/plugin-cypress'
364
+ * import { defineConfig } from 'kubb/config'
365
+ * import { pluginTs } from '@kubb/plugin-ts'
366
+ * import { pluginCypress } from '@kubb/plugin-cypress'
510
367
  *
511
368
  * export default defineConfig({
512
- * plugins: [pluginCypress({ output: { path: 'cypress' } })],
369
+ * input: './petStore.yaml',
370
+ * output: { path: './src/gen' },
371
+ * plugins: [
372
+ * pluginTs(),
373
+ * pluginCypress({
374
+ * output: { path: './cypress' },
375
+ * }),
376
+ * ],
513
377
  * })
514
378
  * ```
515
379
  */
516
380
  const pluginCypress = definePlugin((options) => {
517
381
  const { output = {
518
382
  path: "cypress",
519
- barrelType: "named"
520
- }, group, exclude = [], include, override = [], dataReturnType = "data", baseURL, paramsCasing, paramsType = "inline", pathParamsType = paramsType === "object" ? "object" : options.pathParamsType || "inline", resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
521
- const groupConfig = group ? {
522
- ...group,
523
- name: group.name ? group.name : (ctx) => {
524
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
525
- return `${camelCase(ctx.group)}Requests`;
526
- }
527
- } : void 0;
383
+ barrel: { type: "named" }
384
+ }, group, exclude = [], include, override = [], baseURL, resolver: userResolver, macros: userMacros } = options;
385
+ const groupConfig = createGroupConfig(group);
528
386
  return {
529
387
  name: pluginCypressName,
530
388
  options,
531
389
  dependencies: [pluginTsName],
532
390
  hooks: { "kubb:plugin:setup"(ctx) {
533
- const resolver = userResolver ? {
534
- ...resolverCypress,
535
- ...userResolver
536
- } : resolverCypress;
391
+ const resolver = userResolver ? Resolver.merge(resolverCypress, userResolver) : resolverCypress;
537
392
  ctx.setOptions({
538
393
  output,
539
394
  exclude,
540
395
  include,
541
396
  override,
542
- dataReturnType,
543
397
  group: groupConfig,
544
398
  baseURL,
545
- paramsCasing,
546
- paramsType,
547
- pathParamsType,
548
399
  resolver
549
400
  });
550
401
  ctx.setResolver(resolver);
551
- if (userTransformer) ctx.setTransformer(userTransformer);
402
+ if (userMacros?.length) ctx.setMacros(userMacros);
552
403
  ctx.addGenerator(cypressGenerator);
553
- for (const gen of userGenerators) ctx.addGenerator(gen);
554
404
  } }
555
405
  };
556
406
  });