@kubb/plugin-cypress 5.0.0-alpha.9 → 5.0.0-beta.4

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.
@@ -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'
package/src/plugin.ts CHANGED
@@ -1,119 +1,95 @@
1
- import path from 'node:path'
2
1
  import { camelCase } from '@internals/utils'
3
- import { createPlugin, type Group, getBarrelFiles, getMode } from '@kubb/core'
4
- import { OperationGenerator, pluginOasName } from '@kubb/plugin-oas'
2
+ import { definePlugin, type Group } from '@kubb/core'
5
3
  import { pluginTsName } from '@kubb/plugin-ts'
6
- import { cypressGenerator } from './generators'
4
+ import { cypressGenerator } from './generators/cypressGenerator.tsx'
5
+ import { resolverCypress } from './resolvers/resolverCypress.ts'
7
6
  import type { PluginCypress } from './types.ts'
8
7
 
8
+ /**
9
+ * Canonical plugin name for `@kubb/plugin-cypress`, used to identify the plugin
10
+ * in driver lookups and warnings.
11
+ */
9
12
  export const pluginCypressName = 'plugin-cypress' satisfies PluginCypress['name']
10
13
 
11
- export const pluginCypress = createPlugin<PluginCypress>((options) => {
14
+ /**
15
+ * The `@kubb/plugin-cypress` plugin factory.
16
+ *
17
+ * Generates Cypress `cy.request()` test functions from an OpenAPI/AST `RootNode`.
18
+ * Walks operations, delegates rendering to the active generators,
19
+ * and writes barrel files based on `output.barrelType`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import pluginCypress from '@kubb/plugin-cypress'
24
+ *
25
+ * export default defineConfig({
26
+ * plugins: [pluginCypress({ output: { path: 'cypress' } })],
27
+ * })
28
+ * ```
29
+ */
30
+ export const pluginCypress = definePlugin<PluginCypress>((options) => {
12
31
  const {
13
32
  output = { path: 'cypress', barrelType: 'named' },
14
33
  group,
15
- dataReturnType = 'data',
16
34
  exclude = [],
17
35
  include,
18
36
  override = [],
19
- transformers = {},
20
- generators = [cypressGenerator].filter(Boolean),
21
- contentType,
37
+ dataReturnType = 'data',
22
38
  baseURL,
23
39
  paramsCasing,
24
40
  paramsType = 'inline',
25
41
  pathParamsType = paramsType === 'object' ? 'object' : options.pathParamsType || 'inline',
42
+ resolver: userResolver,
43
+ transformer: userTransformer,
44
+ generators: userGenerators = [],
26
45
  } = options
27
46
 
28
- return {
29
- name: pluginCypressName,
30
- options: {
31
- output,
32
- dataReturnType,
33
- group,
34
- baseURL,
35
-
36
- paramsCasing,
37
- paramsType,
38
- pathParamsType,
39
- },
40
- pre: [pluginOasName, pluginTsName].filter(Boolean),
41
- resolvePath(baseName, pathMode, options) {
42
- const root = path.resolve(this.config.root, this.config.output.path)
43
- const mode = pathMode ?? getMode(path.resolve(root, output.path))
44
-
45
- if (mode === 'single') {
46
- /**
47
- * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
48
- * Other plugins then need to call addOrAppend instead of just add from the fileManager class
49
- */
50
- return path.resolve(root, output.path)
51
- }
52
-
53
- if (group && (options?.group?.path || options?.group?.tag)) {
54
- const groupName: Group['name'] = group?.name
47
+ const groupConfig = group
48
+ ? ({
49
+ ...group,
50
+ name: group.name
55
51
  ? group.name
56
- : (ctx) => {
57
- if (group?.type === 'path') {
52
+ : (ctx: { group: string }) => {
53
+ if (group.type === 'path') {
58
54
  return `${ctx.group.split('/')[1]}`
59
55
  }
60
56
  return `${camelCase(ctx.group)}Requests`
61
- }
57
+ },
58
+ } satisfies Group)
59
+ : undefined
62
60
 
63
- return path.resolve(
64
- root,
65
- output.path,
66
- groupName({
67
- group: group.type === 'path' ? options.group.path! : options.group.tag!,
68
- }),
69
- baseName,
70
- )
71
- }
72
-
73
- return path.resolve(root, output.path, baseName)
74
- },
75
- resolveName(name, type) {
76
- const resolvedName = camelCase(name, {
77
- isFile: type === 'file',
78
- })
79
-
80
- if (type) {
81
- return transformers?.name?.(resolvedName, type) || resolvedName
82
- }
83
-
84
- return resolvedName
85
- },
86
- async install() {
87
- const root = path.resolve(this.config.root, this.config.output.path)
88
- const mode = getMode(path.resolve(root, output.path))
89
- const oas = await this.getOas()
90
-
91
- const operationGenerator = new OperationGenerator(this.plugin.options, {
92
- fabric: this.fabric,
93
- oas,
94
- driver: this.driver,
95
- events: this.events,
96
- plugin: this.plugin,
97
- contentType,
98
- exclude,
99
- include,
100
- override,
101
- mode,
102
- })
103
-
104
- const files = await operationGenerator.build(...generators)
105
- await this.upsertFile(...files)
106
-
107
- const barrelFiles = await getBarrelFiles(this.fabric.files, {
108
- type: output.barrelType ?? 'named',
109
- root,
110
- output,
111
- meta: {
112
- pluginName: this.plugin.name,
113
- },
114
- })
61
+ return {
62
+ name: pluginCypressName,
63
+ options,
64
+ dependencies: [pluginTsName],
65
+ hooks: {
66
+ 'kubb:plugin:setup'(ctx) {
67
+ const resolver = userResolver ? { ...resolverCypress, ...userResolver } : resolverCypress
115
68
 
116
- await this.upsertFile(...barrelFiles)
69
+ ctx.setOptions({
70
+ output,
71
+ exclude,
72
+ include,
73
+ override,
74
+ dataReturnType,
75
+ group: groupConfig,
76
+ baseURL,
77
+ paramsCasing,
78
+ paramsType,
79
+ pathParamsType,
80
+ resolver,
81
+ })
82
+ ctx.setResolver(resolver)
83
+ if (userTransformer) {
84
+ ctx.setTransformer(userTransformer)
85
+ }
86
+ ctx.addGenerator(cypressGenerator)
87
+ for (const gen of userGenerators) {
88
+ ctx.addGenerator(gen)
89
+ }
90
+ },
117
91
  },
118
92
  }
119
93
  })
94
+
95
+ export default pluginCypress
@@ -0,0 +1,22 @@
1
+ import { camelCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginCypress } from '../types.ts'
4
+
5
+ /**
6
+ * Naming convention resolver for Cypress plugin.
7
+ *
8
+ * Provides default naming helpers using camelCase for functions and file paths.
9
+ *
10
+ * @example
11
+ * `resolverCypress.default('list pets', 'function') // → 'listPets'`
12
+ */
13
+ export const resolverCypress = defineResolver<PluginCypress>((ctx) => ({
14
+ name: 'default',
15
+ pluginName: 'plugin-cypress',
16
+ default(name, type) {
17
+ return camelCase(name, { isFile: type === 'file' })
18
+ },
19
+ resolveName(name) {
20
+ return ctx.default(name, 'function')
21
+ },
22
+ }))