@kubb/plugin-cypress 5.0.0-alpha.8 → 5.0.0-beta.3

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/package.json CHANGED
@@ -1,73 +1,68 @@
1
1
  {
2
2
  "name": "@kubb/plugin-cypress",
3
- "version": "5.0.0-alpha.8",
3
+ "version": "5.0.0-beta.3",
4
4
  "description": "Cypress test generator plugin for Kubb, creating end-to-end tests from OpenAPI specifications for automated API testing.",
5
5
  "keywords": [
6
+ "api-testing",
7
+ "code-generator",
8
+ "codegen",
6
9
  "cypress",
7
10
  "e2e-testing",
8
11
  "end-to-end",
9
- "testing",
10
- "test-generation",
11
- "api-testing",
12
- "test-automation",
13
12
  "integration-testing",
14
- "typescript",
15
- "openapi",
16
- "swagger",
13
+ "kubb",
17
14
  "oas",
18
- "code-generator",
19
- "codegen",
15
+ "openapi",
20
16
  "plugins",
21
- "kubb"
17
+ "swagger",
18
+ "test-automation",
19
+ "test-generation",
20
+ "testing",
21
+ "typescript"
22
22
  ],
23
+ "license": "MIT",
24
+ "author": "stijnvanhulle",
23
25
  "repository": {
24
26
  "type": "git",
25
- "url": "git+https://github.com/kubb-labs/kubb.git",
27
+ "url": "git+https://github.com/kubb-labs/plugins.git",
26
28
  "directory": "packages/plugin-cypress"
27
29
  },
28
- "license": "MIT",
29
- "author": "stijnvanhulle",
30
- "sideEffects": false,
30
+ "files": [
31
+ "src",
32
+ "dist",
33
+ "plugin.json",
34
+ "!/**/**.test.**",
35
+ "!/**/__tests__/**",
36
+ "!/**/__snapshots__/**"
37
+ ],
31
38
  "type": "module",
39
+ "sideEffects": false,
40
+ "main": "./dist/index.cjs",
41
+ "module": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "typesVersions": {},
32
44
  "exports": {
33
45
  ".": {
34
46
  "import": "./dist/index.js",
35
47
  "require": "./dist/index.cjs"
36
48
  },
37
- "./components": {
38
- "import": "./dist/components.js",
39
- "require": "./dist/components.cjs"
40
- },
41
- "./generators": {
42
- "import": "./dist/generators.js",
43
- "require": "./dist/generators.cjs"
44
- },
45
49
  "./package.json": "./package.json"
46
50
  },
47
- "types": "./dist/index.d.ts",
48
- "typesVersions": {
49
- "*": {
50
- "utils": [
51
- "./dist/utils.d.ts"
52
- ],
53
- "hooks": [
54
- "./dist/hooks.d.ts"
55
- ],
56
- "components": [
57
- "./dist/components.d.ts"
58
- ],
59
- "generators": [
60
- "./dist/generators.d.ts"
61
- ]
62
- }
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "dependencies": {
56
+ "@kubb/core": "5.0.0-beta.3",
57
+ "@kubb/renderer-jsx": "5.0.0-beta.3",
58
+ "@kubb/plugin-ts": "5.0.0-beta.3"
59
+ },
60
+ "devDependencies": {
61
+ "@internals/utils": "0.0.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@kubb/renderer-jsx": "5.0.0-beta.3"
63
65
  },
64
- "files": [
65
- "src",
66
- "dist",
67
- "!/**/**.test.**",
68
- "!/**/__tests__/**",
69
- "!/**/__snapshots__/**"
70
- ],
71
66
  "size-limit": [
72
67
  {
73
68
  "path": "./dist/*.js",
@@ -75,30 +70,14 @@
75
70
  "gzip": true
76
71
  }
77
72
  ],
78
- "dependencies": {
79
- "@kubb/react-fabric": "0.14.0",
80
- "@kubb/core": "5.0.0-alpha.8",
81
- "@kubb/oas": "5.0.0-alpha.8",
82
- "@kubb/plugin-oas": "5.0.0-alpha.8",
83
- "@kubb/plugin-ts": "5.0.0-alpha.8"
84
- },
85
73
  "engines": {
86
74
  "node": ">=22"
87
75
  },
88
- "publishConfig": {
89
- "access": "public",
90
- "registry": "https://registry.npmjs.org/"
91
- },
92
- "main": "./dist/index.cjs",
93
- "module": "./dist/index.js",
94
- "devDependencies": {
95
- "@internals/utils": "0.0.0"
96
- },
97
76
  "scripts": {
98
77
  "build": "tsdown && size-limit",
99
78
  "clean": "npx rimraf ./dist",
100
- "lint": "bun biome lint .",
101
- "lint:fix": "bun biome lint --fix --unsafe .",
79
+ "lint": "oxlint .",
80
+ "lint:fix": "oxlint --fix .",
102
81
  "release": "pnpm publish --no-git-check",
103
82
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
104
83
  "start": "tsdown --watch",
@@ -1,9 +1,9 @@
1
- import { URLPath } from '@internals/utils'
2
- import { type HttpMethod, isAllOptional, isOptional } from '@kubb/oas'
3
- import type { OperationSchemas } from '@kubb/plugin-oas'
4
- import { getPathParams } from '@kubb/plugin-oas/utils'
5
- import { File, Function, FunctionParams } from '@kubb/react-fabric'
6
- import type { FabricReactNode } from '@kubb/react-fabric/types'
1
+ import { camelCase, URLPath } from '@internals/utils'
2
+ import { ast } from '@kubb/core'
3
+ import type { ResolverTs } from '@kubb/plugin-ts'
4
+ import { functionPrinter } from '@kubb/plugin-ts'
5
+ import { File, Function } from '@kubb/renderer-jsx'
6
+ import type { KubbReactNode } from '@kubb/renderer-jsx/types'
7
7
  import type { PluginCypress } from '../types.ts'
8
8
 
9
9
  type Props = {
@@ -11,137 +11,104 @@ type Props = {
11
11
  * Name of the function
12
12
  */
13
13
  name: string
14
- typeSchemas: OperationSchemas
15
- url: string
14
+ /**
15
+ * AST operation node
16
+ */
17
+ node: ast.OperationNode
18
+ /**
19
+ * TypeScript resolver for resolving param/data/response type names
20
+ */
21
+ resolver: ResolverTs
16
22
  baseURL: string | undefined
17
23
  dataReturnType: PluginCypress['resolvedOptions']['dataReturnType']
18
24
  paramsCasing: PluginCypress['resolvedOptions']['paramsCasing']
19
25
  paramsType: PluginCypress['resolvedOptions']['paramsType']
20
26
  pathParamsType: PluginCypress['resolvedOptions']['pathParamsType']
21
- method: HttpMethod
22
- }
23
-
24
- type GetParamsProps = {
25
- paramsCasing: PluginCypress['resolvedOptions']['paramsCasing']
26
- paramsType: PluginCypress['resolvedOptions']['paramsType']
27
- pathParamsType: PluginCypress['resolvedOptions']['pathParamsType']
28
- typeSchemas: OperationSchemas
29
27
  }
30
28
 
31
- function getParams({ paramsType, paramsCasing, pathParamsType, typeSchemas }: GetParamsProps) {
32
- if (paramsType === 'object') {
33
- const pathParams = getPathParams(typeSchemas.pathParams, { typed: true, casing: paramsCasing })
34
-
35
- return FunctionParams.factory({
36
- data: {
37
- mode: 'object',
38
- children: {
39
- ...pathParams,
40
- data: typeSchemas.request?.name
41
- ? {
42
- type: typeSchemas.request?.name,
43
- optional: isOptional(typeSchemas.request?.schema),
44
- }
45
- : undefined,
46
- params: typeSchemas.queryParams?.name
47
- ? {
48
- type: typeSchemas.queryParams?.name,
49
- optional: isOptional(typeSchemas.queryParams?.schema),
50
- }
51
- : undefined,
52
- headers: typeSchemas.headerParams?.name
53
- ? {
54
- type: typeSchemas.headerParams?.name,
55
- optional: isOptional(typeSchemas.headerParams?.schema),
56
- }
57
- : undefined,
58
- },
59
- },
60
- options: {
61
- type: 'Partial<Cypress.RequestOptions>',
29
+ const declarationPrinter = functionPrinter({ mode: 'declaration' })
30
+
31
+ export function Request({ baseURL = '', name, dataReturnType, resolver, node, paramsType, pathParamsType, paramsCasing }: Props): KubbReactNode {
32
+ const paramsNode = ast.createOperationParams(node, {
33
+ paramsType,
34
+ pathParamsType,
35
+ paramsCasing,
36
+ resolver,
37
+ extraParams: [
38
+ ast.createFunctionParameter({
39
+ name: 'options',
40
+ type: ast.createParamsType({ variant: 'reference', name: 'Partial<Cypress.RequestOptions>' }),
62
41
  default: '{}',
63
- },
64
- })
65
- }
66
-
67
- return FunctionParams.factory({
68
- pathParams: typeSchemas.pathParams?.name
69
- ? {
70
- mode: pathParamsType === 'object' ? 'object' : 'inlineSpread',
71
- children: getPathParams(typeSchemas.pathParams, { typed: true, casing: paramsCasing }),
72
- default: isAllOptional(typeSchemas.pathParams?.schema) ? '{}' : undefined,
73
- }
74
- : undefined,
75
- data: typeSchemas.request?.name
76
- ? {
77
- type: typeSchemas.request?.name,
78
- optional: isOptional(typeSchemas.request?.schema),
79
- }
80
- : undefined,
81
- params: typeSchemas.queryParams?.name
82
- ? {
83
- type: typeSchemas.queryParams?.name,
84
- optional: isOptional(typeSchemas.queryParams?.schema),
85
- }
86
- : undefined,
87
- headers: typeSchemas.headerParams?.name
88
- ? {
89
- type: typeSchemas.headerParams?.name,
90
- optional: isOptional(typeSchemas.headerParams?.schema),
91
- }
92
- : undefined,
93
- options: {
94
- type: 'Partial<Cypress.RequestOptions>',
95
- default: '{}',
96
- },
42
+ }),
43
+ ],
97
44
  })
98
- }
99
-
100
- export function Request({ baseURL = '', name, dataReturnType, typeSchemas, url, method, paramsType, paramsCasing, pathParamsType }: Props): FabricReactNode {
101
- const path = new URLPath(url, { casing: paramsCasing })
102
-
103
- const params = getParams({ paramsType, paramsCasing, pathParamsType, typeSchemas })
45
+ const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
104
46
 
105
- const returnType =
106
- dataReturnType === 'data' ? `Cypress.Chainable<${typeSchemas.response.name}>` : `Cypress.Chainable<Cypress.Response<${typeSchemas.response.name}>>`
47
+ const responseType = resolver.resolveResponseName(node)
48
+ const returnType = dataReturnType === 'data' ? `Cypress.Chainable<${responseType}>` : `Cypress.Chainable<Cypress.Response<${responseType}>>`
107
49
 
108
- // Build the URL template string - this will convert /pets/:petId to /pets/${petId}
109
- const urlTemplate = path.toTemplateString({ prefix: baseURL })
110
-
111
- // Build request options object
112
- const requestOptions: string[] = [`method: '${method}'`, `url: ${urlTemplate}`]
50
+ const casedPathParams = ast.caseParams(
51
+ node.parameters.filter((p) => p.in === 'path'),
52
+ paramsCasing,
53
+ )
54
+ // Build a lookup keyed by camelCase-normalized name so that path-template names
55
+ // (e.g. `{pet_id}`) correctly resolve to the function-parameter name (`petId`)
56
+ // even when the OpenAPI spec has inconsistent casing between the two.
57
+ const pathParamNameMap = new Map(casedPathParams.map((p) => [camelCase(p.name), p.name]))
58
+
59
+ const urlPath = new URLPath(node.path, { casing: paramsCasing })
60
+ const urlTemplate = urlPath.toTemplateString({
61
+ prefix: baseURL,
62
+ replacer: (param) => pathParamNameMap.get(camelCase(param)) ?? param,
63
+ })
113
64
 
114
- // Add query params if they exist
115
- if (typeSchemas.queryParams?.name) {
116
- requestOptions.push('qs: params')
65
+ const requestOptions: string[] = [`method: '${node.method}'`, `url: ${urlTemplate}`]
66
+
67
+ const queryParams = node.parameters.filter((p) => p.in === 'query')
68
+ if (queryParams.length > 0) {
69
+ const casedQueryParams = ast.caseParams(queryParams, paramsCasing)
70
+ // When paramsCasing renames query params (e.g. page_size → pageSize), we must remap
71
+ // the camelCase keys back to the original API names before passing them to `qs`.
72
+ const needsQsTransform = casedQueryParams.some((p, i) => p.name !== queryParams[i]!.name)
73
+ if (needsQsTransform) {
74
+ const pairs = queryParams.map((orig, i) => `${orig.name}: params.${casedQueryParams[i]!.name}`).join(', ')
75
+ requestOptions.push(`qs: params ? { ${pairs} } : undefined`)
76
+ } else {
77
+ requestOptions.push('qs: params')
78
+ }
117
79
  }
118
80
 
119
- // Add headers if they exist
120
- if (typeSchemas.headerParams?.name) {
121
- requestOptions.push('headers')
81
+ const headerParams = node.parameters.filter((p) => p.in === 'header')
82
+ if (headerParams.length > 0) {
83
+ const casedHeaderParams = ast.caseParams(headerParams, paramsCasing)
84
+ // When paramsCasing renames header params (e.g. x-api-key → xApiKey), we must remap
85
+ // the camelCase keys back to the original API names before passing them to `headers`.
86
+ const needsHeaderTransform = casedHeaderParams.some((p, i) => p.name !== headerParams[i]!.name)
87
+ if (needsHeaderTransform) {
88
+ const pairs = headerParams.map((orig, i) => `'${orig.name}': headers.${casedHeaderParams[i]!.name}`).join(', ')
89
+ requestOptions.push(`headers: headers ? { ${pairs} } : undefined`)
90
+ } else {
91
+ requestOptions.push('headers')
92
+ }
122
93
  }
123
94
 
124
- // Add body if request schema exists
125
- if (typeSchemas.request?.name) {
95
+ if (node.requestBody?.content?.[0]?.schema) {
126
96
  requestOptions.push('body: data')
127
97
  }
128
98
 
129
- // Spread additional Cypress options
130
99
  requestOptions.push('...options')
131
100
 
132
101
  return (
133
102
  <File.Source name={name} isIndexable isExportable>
134
- <Function name={name} export params={params.toConstructor()} returnType={returnType}>
103
+ <Function name={name} export params={paramsSignature} returnType={returnType}>
135
104
  {dataReturnType === 'data'
136
- ? `return cy.request<${typeSchemas.response.name}>({
105
+ ? `return cy.request<${responseType}>({
137
106
  ${requestOptions.join(',\n ')}
138
107
  }).then((res) => res.body)`
139
- : `return cy.request<${typeSchemas.response.name}>({
108
+ : `return cy.request<${responseType}>({
140
109
  ${requestOptions.join(',\n ')}
141
110
  })`}
142
111
  </Function>
143
112
  </File.Source>
144
113
  )
145
114
  }
146
-
147
- Request.getParams = getParams
@@ -1,64 +1,71 @@
1
- import { usePluginDriver } from '@kubb/core/hooks'
2
- import { createReactGenerator } from '@kubb/plugin-oas/generators'
3
- import { useOas, useOperationManager } from '@kubb/plugin-oas/hooks'
4
- import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
1
+ import { ast, defineGenerator } from '@kubb/core'
5
2
  import { pluginTsName } from '@kubb/plugin-ts'
6
- import { File } from '@kubb/react-fabric'
7
- import { Request } from '../components'
8
- import type { PluginCypress } from '../types'
3
+ import { File, jsxRenderer } from '@kubb/renderer-jsx'
4
+ import { Request } from '../components/Request.tsx'
5
+ import type { PluginCypress } from '../types.ts'
9
6
 
10
- export const cypressGenerator = createReactGenerator<PluginCypress>({
7
+ export const cypressGenerator = defineGenerator<PluginCypress>({
11
8
  name: 'cypress',
12
- Operation({ operation, generator, plugin }) {
13
- const {
14
- options: { output, baseURL, dataReturnType, paramsCasing, paramsType, pathParamsType },
15
- } = plugin
16
- const driver = usePluginDriver()
9
+ renderer: jsxRenderer,
10
+ operation(node, ctx) {
11
+ const { adapter, config, resolver, driver, root } = ctx
12
+ const { output, baseURL, dataReturnType, paramsCasing, paramsType, pathParamsType, group } = ctx.options
17
13
 
18
- const oas = useOas()
19
- const { getSchemas, getName, getFile } = useOperationManager(generator)
14
+ const pluginTs = driver.getPlugin(pluginTsName)
20
15
 
21
- const request = {
22
- name: getName(operation, { type: 'function' }),
23
- file: getFile(operation),
16
+ if (!pluginTs) {
17
+ return null
24
18
  }
25
19
 
26
- const type = {
27
- file: getFile(operation, { pluginName: pluginTsName }),
28
- schemas: getSchemas(operation, { pluginName: pluginTsName, type: 'type' }),
29
- }
20
+ const tsResolver = driver.getResolver(pluginTsName)
21
+
22
+ const casedParams = ast.caseParams(node.parameters, paramsCasing)
23
+
24
+ const pathParams = casedParams.filter((p) => p.in === 'path')
25
+ const queryParams = casedParams.filter((p) => p.in === 'query')
26
+ const headerParams = casedParams.filter((p) => p.in === 'header')
27
+
28
+ const importedTypeNames = [
29
+ ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
30
+ ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
31
+ ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
32
+ node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
33
+ tsResolver.resolveResponseName(node),
34
+ ].filter(Boolean)
35
+
36
+ const meta = {
37
+ name: resolver.resolveName(node.operationId),
38
+ file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
39
+ fileTs: tsResolver.resolveFile(
40
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
41
+ {
42
+ root,
43
+ output: pluginTs.options?.output ?? output,
44
+ group: pluginTs.options?.group,
45
+ },
46
+ ),
47
+ } as const
30
48
 
31
49
  return (
32
50
  <File
33
- baseName={request.file.baseName}
34
- path={request.file.path}
35
- meta={request.file.meta}
36
- banner={getBanner({ oas, output, config: driver.config })}
37
- footer={getFooter({ oas, output })}
51
+ baseName={meta.file.baseName}
52
+ path={meta.file.path}
53
+ meta={meta.file.meta}
54
+ banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
55
+ footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
38
56
  >
39
- <File.Import
40
- name={[
41
- type.schemas.request?.name,
42
- type.schemas.response.name,
43
- type.schemas.pathParams?.name,
44
- type.schemas.queryParams?.name,
45
- type.schemas.headerParams?.name,
46
- ...(type.schemas.statusCodes?.map((item) => item.name) || []),
47
- ].filter(Boolean)}
48
- root={request.file.path}
49
- path={type.file.path}
50
- isTypeOnly
51
- />
57
+ {meta.fileTs && importedTypeNames.length > 0 && (
58
+ <File.Import name={Array.from(new Set(importedTypeNames))} root={meta.file.path} path={meta.fileTs.path} isTypeOnly />
59
+ )}
52
60
  <Request
53
- name={request.name}
61
+ name={meta.name}
62
+ node={node}
63
+ resolver={tsResolver}
54
64
  dataReturnType={dataReturnType}
55
65
  paramsCasing={paramsCasing}
56
66
  paramsType={paramsType}
57
67
  pathParamsType={pathParamsType}
58
- typeSchemas={type.schemas}
59
- method={operation.method}
60
68
  baseURL={baseURL}
61
- url={operation.path}
62
69
  />
63
70
  </File>
64
71
  )
package/src/index.ts CHANGED
@@ -1,2 +1,9 @@
1
- export { pluginCypress, pluginCypressName } from './plugin.ts'
2
- export type { PluginCypress } from './types.ts'
1
+ export { Request } from './components/Request.tsx'
2
+
3
+ export { cypressGenerator } from './generators/cypressGenerator.tsx'
4
+
5
+ export { default, pluginCypress, pluginCypressName } from './plugin.ts'
6
+
7
+ export { resolverCypress } from './resolvers/resolverCypress.ts'
8
+
9
+ export type { PluginCypress, ResolverCypress } from './types.ts'