@kubb/plugin-msw 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.
Files changed (43) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +1 -3
  3. package/dist/components-CLQ77DVn.cjs +584 -0
  4. package/dist/components-CLQ77DVn.cjs.map +1 -0
  5. package/dist/components-vO0FIb2i.js +519 -0
  6. package/dist/components-vO0FIb2i.js.map +1 -0
  7. package/dist/components.cjs +1 -1
  8. package/dist/components.d.ts +17 -21
  9. package/dist/components.js +1 -1
  10. package/dist/generators-BPJCs1x1.js +176 -0
  11. package/dist/generators-BPJCs1x1.js.map +1 -0
  12. package/dist/generators-CrmMwWE4.cjs +186 -0
  13. package/dist/generators-CrmMwWE4.cjs.map +1 -0
  14. package/dist/generators.cjs +1 -1
  15. package/dist/generators.d.ts +4 -471
  16. package/dist/generators.js +1 -1
  17. package/dist/index.cjs +54 -65
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +51 -65
  21. package/dist/index.js.map +1 -1
  22. package/dist/types-Dxu0KMQ4.d.ts +89 -0
  23. package/package.json +59 -57
  24. package/src/components/Handlers.tsx +3 -3
  25. package/src/components/Mock.tsx +36 -28
  26. package/src/components/MockWithFaker.tsx +36 -24
  27. package/src/components/Response.tsx +23 -17
  28. package/src/generators/handlersGenerator.tsx +18 -18
  29. package/src/generators/mswGenerator.tsx +49 -60
  30. package/src/index.ts +1 -1
  31. package/src/plugin.ts +48 -85
  32. package/src/resolvers/resolverMsw.ts +19 -0
  33. package/src/types.ts +45 -22
  34. package/src/utils.ts +109 -0
  35. package/dist/components-8XBwMbFa.cjs +0 -343
  36. package/dist/components-8XBwMbFa.cjs.map +0 -1
  37. package/dist/components-DgtTZkWX.js +0 -277
  38. package/dist/components-DgtTZkWX.js.map +0 -1
  39. package/dist/generators-CY1SNd5X.cjs +0 -171
  40. package/dist/generators-CY1SNd5X.cjs.map +0 -1
  41. package/dist/generators-CvyZTxOm.js +0 -161
  42. package/dist/generators-CvyZTxOm.js.map +0 -1
  43. package/dist/types-MdHRNpgi.d.ts +0 -68
@@ -1,37 +1,37 @@
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'
5
- import { File } from '@kubb/react-fabric'
1
+ import { defineGenerator } from '@kubb/core'
2
+ import { File, jsxRenderer } from '@kubb/renderer-jsx'
6
3
  import { Handlers } from '../components/Handlers.tsx'
7
4
  import type { PluginMsw } from '../types'
5
+ import { transformName } from '../utils.ts'
8
6
 
9
- export const handlersGenerator = createReactGenerator<PluginMsw>({
7
+ export const handlersGenerator = defineGenerator<PluginMsw>({
10
8
  name: 'plugin-msw',
11
- Operations({ operations, generator, plugin }) {
12
- const driver = usePluginDriver()
9
+ renderer: jsxRenderer,
10
+ operations(nodes, ctx) {
11
+ const { resolver, config, root, adapter } = ctx
12
+ const { output, group, transformers } = ctx.options
13
13
 
14
- const oas = useOas()
15
- const { getName, getFile } = useOperationManager(generator)
14
+ const file = resolver.resolveFile({ name: 'handlers', extname: '.ts' }, { root, output, group })
16
15
 
17
- const file = driver.getFile({ name: 'handlers', extname: '.ts', pluginName: plugin.name })
18
-
19
- const imports = operations.map((operation) => {
20
- const operationFile = getFile(operation, { pluginName: plugin.name })
21
- const operationName = getName(operation, { pluginName: plugin.name, type: 'function' })
16
+ const imports = nodes.map((node) => {
17
+ const operationName = transformName(resolver.resolveName(node.operationId), 'function', transformers)
18
+ const operationFile = resolver.resolveFile(
19
+ { name: resolver.resolveName(node.operationId), extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
20
+ { root, output, group },
21
+ )
22
22
 
23
23
  return <File.Import key={operationFile.path} name={[operationName]} root={file.path} path={operationFile.path} />
24
24
  })
25
25
 
26
- const handlers = operations.map((operation) => `${getName(operation, { type: 'function', pluginName: plugin.name })}()`)
26
+ const handlers = nodes.map((node) => `${transformName(resolver.resolveName(node.operationId), 'function', transformers)}()`)
27
27
 
28
28
  return (
29
29
  <File
30
30
  baseName={file.baseName}
31
31
  path={file.path}
32
32
  meta={file.meta}
33
- banner={getBanner({ oas, output: plugin.options.output, config: driver.config })}
34
- footer={getFooter({ oas, output: plugin.options.output })}
33
+ banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
34
+ footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
35
35
  >
36
36
  {imports}
37
37
  <Handlers name={'handlers'} handlers={handlers} />
@@ -1,94 +1,83 @@
1
- import { usePluginDriver } from '@kubb/core/hooks'
1
+ import { defineGenerator } from '@kubb/core'
2
2
  import { pluginFakerName } from '@kubb/plugin-faker'
3
- import { createReactGenerator } from '@kubb/plugin-oas/generators'
4
- import { useOas, useOperationManager } from '@kubb/plugin-oas/hooks'
5
- import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
6
3
  import { pluginTsName } from '@kubb/plugin-ts'
7
- import { File } from '@kubb/react-fabric'
4
+ import { File, jsxRenderer } from '@kubb/renderer-jsx'
8
5
  import { Mock, MockWithFaker, Response } from '../components'
9
6
  import type { PluginMsw } from '../types'
7
+ import { getResponseTypes, getSuccessResponses, resolveFakerMeta, transformName } from '../utils.ts'
10
8
 
11
- export const mswGenerator = createReactGenerator<PluginMsw>({
9
+ export const mswGenerator = defineGenerator<PluginMsw>({
12
10
  name: 'msw',
13
- Operation({ operation, generator, plugin }) {
14
- const {
15
- options: { output, parser, baseURL },
16
- } = plugin
17
- const driver = usePluginDriver()
18
-
19
- const oas = useOas()
20
- const { getSchemas, getName, getFile } = useOperationManager(generator)
11
+ renderer: jsxRenderer,
12
+ operation(node, ctx) {
13
+ const { driver, resolver, config, root, adapter } = ctx
14
+ const { output, parser, baseURL, group, transformers } = ctx.options
21
15
 
16
+ const fileName = resolver.resolveName(node.operationId)
22
17
  const mock = {
23
- name: getName(operation, { type: 'function' }),
24
- file: getFile(operation),
18
+ name: transformName(fileName, 'function', transformers),
19
+ file: resolver.resolveFile({ name: fileName, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
25
20
  }
26
21
 
27
- const faker = {
28
- file: getFile(operation, { pluginName: pluginFakerName }),
29
- schemas: getSchemas(operation, { pluginName: pluginFakerName, type: 'function' }),
30
- }
22
+ const fakerPlugin = parser === 'faker' ? driver.getPlugin(pluginFakerName) : undefined
23
+ const faker =
24
+ parser === 'faker' && fakerPlugin
25
+ ? resolveFakerMeta(node, {
26
+ root,
27
+ fakerResolver: driver.getResolver(pluginFakerName),
28
+ fakerOutput: fakerPlugin.options?.output ?? output,
29
+ fakerGroup: fakerPlugin.options?.group,
30
+ })
31
+ : undefined
32
+
33
+ const pluginTs = driver.getPlugin(pluginTsName)
34
+ if (!pluginTs) return null
35
+ const tsResolver = driver.getResolver(pluginTsName)
31
36
 
32
37
  const type = {
33
- file: getFile(operation, { pluginName: pluginTsName }),
34
- schemas: getSchemas(operation, { pluginName: pluginTsName, type: 'type' }),
38
+ file: tsResolver.resolveFile(
39
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
40
+ { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
41
+ ),
42
+ responseName: tsResolver.resolveResponseName(node),
35
43
  }
36
44
 
37
- const responseStatusCodes = operation.getResponseStatusCodes()
38
-
39
- const types: [statusCode: number | 'default', typeName: string][] = []
40
-
41
- for (const code of responseStatusCodes) {
42
- if (code === 'default') {
43
- types.push(['default', type.schemas.response.name])
44
- continue
45
- }
45
+ const types = getResponseTypes(node, tsResolver)
46
+ const successResponses = getSuccessResponses(node)
47
+ const hasSuccessSchema = successResponses.some((response) => !!response.schema)
46
48
 
47
- if (code.startsWith('2')) {
48
- types.push([Number(code), type.schemas.response.name])
49
- continue
50
- }
51
-
52
- const codeType = type.schemas.errors?.find((err) => err.statusCode === Number(code))
53
- if (codeType) types.push([Number(code), codeType.name])
54
- }
49
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined
55
50
 
56
51
  return (
57
52
  <File
58
53
  baseName={mock.file.baseName}
59
54
  path={mock.file.path}
60
55
  meta={mock.file.meta}
61
- banner={getBanner({ oas, output, config: driver.config })}
62
- footer={getFooter({ oas, output })}
56
+ banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
57
+ footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
63
58
  >
64
59
  <File.Import name={['http']} path="msw" />
65
- <File.Import name={['ResponseResolver']} isTypeOnly path="msw" />
60
+ <File.Import name={['HttpResponseResolver']} isTypeOnly path="msw" />
66
61
  <File.Import
67
- name={Array.from(new Set([type.schemas.response.name, ...types.map((t) => t[1])]))}
62
+ name={Array.from(new Set([type.responseName, ...types.map((t) => t[1]), ...(requestName ? [requestName] : [])]))}
68
63
  path={type.file.path}
69
64
  root={mock.file.path}
70
65
  isTypeOnly
71
66
  />
72
- {parser === 'faker' && faker.file && faker.schemas.response && (
73
- <File.Import name={[faker.schemas.response.name]} root={mock.file.path} path={faker.file.path} />
74
- )}
67
+ {parser === 'faker' && faker && <File.Import name={[faker.name]} root={mock.file.path} path={faker.file.path} />}
75
68
 
76
69
  {types
77
70
  .filter(([code]) => code !== 'default')
78
- .map(([code, typeName]) => (
79
- <Response typeName={typeName} operation={operation} name={mock.name} statusCode={code as number} />
80
- ))}
81
- {parser === 'faker' && (
82
- <MockWithFaker
83
- name={mock.name}
84
- typeName={type.schemas.response.name}
85
- fakerName={faker.schemas.response.name}
86
- operation={operation}
87
- baseURL={baseURL}
88
- />
89
- )}
90
- {parser === 'data' && (
91
- <Mock name={mock.name} typeName={type.schemas.response.name} fakerName={faker.schemas.response.name} operation={operation} baseURL={baseURL} />
71
+ .map(([code, typeName]) => {
72
+ const response = node.responses.find((item) => item.statusCode === String(code))
73
+ if (!response) return null
74
+ return <Response key={typeName} typeName={typeName} response={response} name={mock.name} />
75
+ })}
76
+
77
+ {parser === 'faker' && faker && hasSuccessSchema ? (
78
+ <MockWithFaker name={mock.name} typeName={type.responseName} requestTypeName={requestName} fakerName={faker.name} node={node} baseURL={baseURL} />
79
+ ) : (
80
+ <Mock name={mock.name} typeName={type.responseName} requestTypeName={requestName} node={node} baseURL={baseURL} />
92
81
  )}
93
82
  </File>
94
83
  )
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { pluginMsw, pluginMswName } from './plugin.ts'
1
+ export { default, pluginMsw, pluginMswName } from './plugin.ts'
2
2
  export type { PluginMsw } from './types.ts'
package/src/plugin.ts CHANGED
@@ -1,15 +1,14 @@
1
- import path from 'node:path'
2
1
  import { camelCase } from '@internals/utils'
3
- import { createPlugin, type Group, getBarrelFiles, getMode } from '@kubb/core'
2
+ import { definePlugin, type Group } from '@kubb/core'
4
3
  import { pluginFakerName } from '@kubb/plugin-faker'
5
- import { OperationGenerator, pluginOasName } from '@kubb/plugin-oas'
6
4
  import { pluginTsName } from '@kubb/plugin-ts'
7
5
  import { handlersGenerator, mswGenerator } from './generators'
6
+ import { resolverMsw } from './resolvers/resolverMsw.ts'
8
7
  import type { PluginMsw } from './types.ts'
9
8
 
10
9
  export const pluginMswName = 'plugin-msw' satisfies PluginMsw['name']
11
10
 
12
- export const pluginMsw = createPlugin<PluginMsw>((options) => {
11
+ export const pluginMsw = definePlugin<PluginMsw>((options) => {
13
12
  const {
14
13
  output = { path: 'handlers', barrelType: 'named' },
15
14
  group,
@@ -19,97 +18,61 @@ export const pluginMsw = createPlugin<PluginMsw>((options) => {
19
18
  transformers = {},
20
19
  handlers = false,
21
20
  parser = 'data',
22
- generators = [mswGenerator, handlers ? handlersGenerator : undefined].filter(Boolean),
23
- contentType,
24
21
  baseURL,
22
+ resolver: userResolver,
23
+ transformer: userTransformer,
24
+ generators: userGenerators = [],
25
25
  } = options
26
26
 
27
- return {
28
- name: pluginMswName,
29
- options: {
30
- output,
31
- parser,
32
- group,
33
- baseURL,
34
- },
35
- pre: [pluginOasName, pluginTsName, parser === 'faker' ? pluginFakerName : undefined].filter(Boolean),
36
- resolvePath(baseName, pathMode, options) {
37
- const root = path.resolve(this.config.root, this.config.output.path)
38
- const mode = pathMode ?? getMode(path.resolve(root, output.path))
39
-
40
- if (mode === 'single') {
41
- /**
42
- * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
43
- * Other plugins then need to call addOrAppend instead of just add from the fileManager class
44
- */
45
- return path.resolve(root, output.path)
46
- }
47
-
48
- if (group && (options?.group?.path || options?.group?.tag)) {
49
- const groupName: Group['name'] = group?.name
27
+ const groupConfig = group
28
+ ? ({
29
+ ...group,
30
+ name: group.name
50
31
  ? group.name
51
- : (ctx) => {
52
- if (group?.type === 'path') {
32
+ : (ctx: { group: string }) => {
33
+ if (group.type === 'path') {
53
34
  return `${ctx.group.split('/')[1]}`
54
35
  }
55
36
  return `${camelCase(ctx.group)}Controller`
56
- }
57
-
58
- return path.resolve(
59
- root,
60
- output.path,
61
- groupName({
62
- group: group.type === 'path' ? options.group.path! : options.group.tag!,
63
- }),
64
- baseName,
65
- )
66
- }
67
-
68
- return path.resolve(root, output.path, baseName)
69
- },
70
- resolveName(name, type) {
71
- const resolvedName = camelCase(name, {
72
- suffix: type ? 'handler' : undefined,
73
- isFile: type === 'file',
74
- })
37
+ },
38
+ } satisfies Group)
39
+ : undefined
75
40
 
76
- if (type) {
77
- return transformers?.name?.(resolvedName, type) || resolvedName
78
- }
79
-
80
- return resolvedName
81
- },
82
- async install() {
83
- const root = path.resolve(this.config.root, this.config.output.path)
84
- const mode = getMode(path.resolve(root, output.path))
85
- const oas = await this.getOas()
86
-
87
- const operationGenerator = new OperationGenerator(this.plugin.options, {
88
- fabric: this.fabric,
89
- oas,
90
- driver: this.driver,
91
- events: this.events,
92
- plugin: this.plugin,
93
- contentType,
94
- exclude,
95
- include,
96
- override,
97
- mode,
98
- })
99
-
100
- const files = await operationGenerator.build(...generators)
101
- await this.upsertFile(...files)
41
+ return {
42
+ name: pluginMswName,
43
+ options,
44
+ dependencies: [pluginTsName, parser === 'faker' ? pluginFakerName : undefined].filter(Boolean),
45
+ hooks: {
46
+ 'kubb:plugin:setup'(ctx) {
47
+ const resolver = userResolver ? { ...resolverMsw, ...userResolver } : resolverMsw
102
48
 
103
- const barrelFiles = await getBarrelFiles(this.fabric.files, {
104
- type: output.barrelType ?? 'named',
105
- root,
106
- output,
107
- meta: {
108
- pluginName: this.plugin.name,
109
- },
110
- })
49
+ ctx.setOptions({
50
+ output,
51
+ parser,
52
+ baseURL,
53
+ group: groupConfig,
54
+ exclude,
55
+ include,
56
+ override,
57
+ handlers,
58
+ transformers,
59
+ resolver,
60
+ })
61
+ ctx.setResolver(resolver)
62
+ if (userTransformer) {
63
+ ctx.setTransformer(userTransformer)
64
+ }
111
65
 
112
- await this.upsertFile(...barrelFiles)
66
+ ctx.addGenerator(mswGenerator)
67
+ if (handlers) {
68
+ ctx.addGenerator(handlersGenerator)
69
+ }
70
+ for (const gen of userGenerators) {
71
+ ctx.addGenerator(gen)
72
+ }
73
+ },
113
74
  },
114
75
  }
115
76
  })
77
+
78
+ export default pluginMsw
@@ -0,0 +1,19 @@
1
+ import { camelCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginMsw } from '../types.ts'
4
+
5
+ /**
6
+ * Naming convention resolver for MSW plugin.
7
+ *
8
+ * Provides default naming helpers using camelCase with a `handler` suffix.
9
+ */
10
+ export const resolverMsw = defineResolver<PluginMsw>((_ctx) => ({
11
+ name: 'default',
12
+ pluginName: 'plugin-msw',
13
+ default(name, type) {
14
+ return camelCase(name, { isFile: type === 'file' })
15
+ },
16
+ resolveName(name) {
17
+ return camelCase(name, { suffix: 'handler' })
18
+ },
19
+ }))
package/src/types.ts CHANGED
@@ -1,65 +1,88 @@
1
- import type { Group, Output, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, ResolveNameParams, Resolver } from '@kubb/core'
2
2
 
3
- import type { contentType, Oas } from '@kubb/oas'
4
- import type { Exclude, Include, Override, ResolvePathOptions } from '@kubb/plugin-oas'
5
- import type { Generator } from '@kubb/plugin-oas/generators'
3
+ /**
4
+ * Resolver for MSW that provides naming methods for handler functions.
5
+ */
6
+ export type ResolverMsw = Resolver & {
7
+ /**
8
+ * Resolves the handler function name for an operation.
9
+ */
10
+ resolveName(this: ResolverMsw, name: string): string
11
+ }
6
12
 
7
13
  export type Options = {
8
14
  /**
9
15
  * Specify the export location for the files and define the behavior of the output
10
- * @default { path: 'mocks', barrelType: 'named' }
16
+ * @default { path: 'handlers', barrelType: 'named' }
11
17
  */
12
- output?: Output<Oas>
13
- /**
14
- * Define which contentType should be used.
15
- * By default, the first JSON valid mediaType is used
16
- */
17
- contentType?: contentType
18
+ output?: Output
18
19
  baseURL?: string
19
20
  /**
20
21
  * Group the MSW mocks based on the provided name.
21
22
  */
22
23
  group?: Group
23
24
  /**
24
- * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
25
+ * Tags, operations, or paths to exclude from generation.
25
26
  */
26
27
  exclude?: Array<Exclude>
27
28
  /**
28
- * Array containing include parameters to include tags/operations/methods/paths.
29
+ * Tags, operations, or paths to include in generation.
29
30
  */
30
31
  include?: Array<Include>
31
32
  /**
32
- * Array containing override parameters to override `options` based on tags/operations/methods/paths.
33
+ * Override options for specific tags, operations, or paths.
33
34
  */
34
35
  override?: Array<Override<ResolvedOptions>>
35
36
  transformers?: {
36
37
  /**
37
- * Customize the names based on the type that is provided by the plugin.
38
+ * Override the default naming for handlers.
38
39
  */
39
40
  name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
40
41
  }
42
+ /**
43
+ * Override naming conventions for function names and types.
44
+ */
45
+ resolver?: Partial<ResolverMsw> & ThisType<ResolverMsw>
46
+ /**
47
+ * AST visitor to transform generated nodes.
48
+ */
49
+ transformer?: ast.Visitor
41
50
  /**
42
51
  * Create `handlers.ts` file with all handlers grouped by methods.
43
52
  * @default false
44
53
  */
45
54
  handlers?: boolean
46
55
  /**
47
- * Which parser should be used before returning the data to the Response of MSW.
48
- * - 'data' uses your custom data to generate the data for the response.
49
- * - 'faker' uses @kubb/plugin-faker to generate the data for the response.
56
+ * Which parser to use for generating response data.
57
+ *
50
58
  * @default 'data'
51
59
  */
52
60
  parser?: 'data' | 'faker'
53
61
  /**
54
- * Define some generators next to the msw generators
62
+ * Additional generators alongside the default generators.
55
63
  */
56
64
  generators?: Array<Generator<PluginMsw>>
57
65
  }
66
+
58
67
  type ResolvedOptions = {
59
- output: Output<Oas>
60
- group: Options['group']
68
+ output: Output
69
+ group: Group | undefined
70
+ exclude: NonNullable<Options['exclude']>
71
+ include: Options['include']
72
+ override: NonNullable<Options['override']>
61
73
  parser: NonNullable<Options['parser']>
62
74
  baseURL: Options['baseURL'] | undefined
75
+ handlers: boolean
76
+ transformers: NonNullable<Options['transformers']>
77
+ resolver: ResolverMsw
63
78
  }
64
79
 
65
- export type PluginMsw = PluginFactoryOptions<'plugin-msw', Options, ResolvedOptions, never, ResolvePathOptions>
80
+ export type PluginMsw = PluginFactoryOptions<'plugin-msw', Options, ResolvedOptions, ResolverMsw>
81
+
82
+ declare global {
83
+ namespace Kubb {
84
+ interface PluginRegistry {
85
+ 'plugin-msw': PluginMsw
86
+ }
87
+ }
88
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { ast } from '@kubb/core'
2
+ import type { ResolverFaker } from '@kubb/plugin-faker'
3
+ import type { ResolverTs } from '@kubb/plugin-ts'
4
+ import type { PluginMsw } from './types.ts'
5
+
6
+ /**
7
+ * Applies a name transformer function to a name if configured, otherwise returns it unchanged.
8
+ */
9
+ export function transformName(name: string, type: 'function' | 'type' | 'file' | 'const', transformers?: PluginMsw['resolvedOptions']['transformers']): string {
10
+ return transformers?.name?.(name, type) || name
11
+ }
12
+
13
+ /**
14
+ * Filters responses to only those with 2xx status codes.
15
+ */
16
+ export function getSuccessResponses(node: ast.OperationNode): ast.ResponseNode[] {
17
+ return node.responses.filter((response) => {
18
+ const code = Number.parseInt(response.statusCode, 10)
19
+ return !Number.isNaN(code) && code >= 200 && code < 300
20
+ })
21
+ }
22
+
23
+ /**
24
+ * Returns the first 2xx response for an operation, if any.
25
+ */
26
+ export function getPrimarySuccessResponse(node: ast.OperationNode): ast.ResponseNode | undefined {
27
+ return getSuccessResponses(node)[0]
28
+ }
29
+
30
+ /**
31
+ * Gets the content type from a response, defaulting to 'application/json' if a schema exists.
32
+ */
33
+ export function getContentType(response: ast.ResponseNode | undefined): string | undefined {
34
+ return getResponseContentType(response) ?? (hasResponseSchema(response) ? 'application/json' : undefined)
35
+ }
36
+
37
+ /**
38
+ * Determines if a response has a schema that is not void or any.
39
+ */
40
+ export function hasResponseSchema(response: ast.ResponseNode | undefined): boolean {
41
+ return !!getResponseContentType(response) || (!!response?.schema && response.schema.type !== 'void' && response.schema.type !== 'any')
42
+ }
43
+
44
+ function getResponseContentType(response: ast.ResponseNode | undefined): string | undefined {
45
+ const contentType = response as unknown as { mediaType?: string | null; contentType?: string | null } | undefined
46
+ const value = contentType?.mediaType ?? contentType?.contentType
47
+ return typeof value === 'string' && value.length > 0 ? value : undefined
48
+ }
49
+
50
+ /**
51
+ * Maps all operation responses to their type names, including status code or 'default' for default responses.
52
+ */
53
+ export function getResponseTypes(node: ast.OperationNode, tsResolver: ResolverTs): Array<[statusCode: number | 'default', typeName: string]> {
54
+ const types: Array<[number | 'default', string]> = []
55
+
56
+ for (const response of node.responses) {
57
+ if (response.statusCode === 'default') {
58
+ types.push(['default', tsResolver.resolveResponseName(node)])
59
+ continue
60
+ }
61
+
62
+ const code = Number.parseInt(response.statusCode, 10)
63
+ if (Number.isNaN(code)) continue
64
+
65
+ if (code >= 200 && code < 300) {
66
+ types.push([code, tsResolver.resolveResponseName(node)])
67
+ continue
68
+ }
69
+
70
+ types.push([code, tsResolver.resolveResponseStatusName(node, response.statusCode)])
71
+ }
72
+
73
+ return types
74
+ }
75
+
76
+ /**
77
+ * Converts an HTTP method to its lowercase MSW equivalent (e.g., 'POST' → 'post').
78
+ */
79
+ export function getMswMethod(node: ast.OperationNode): string {
80
+ return node.method.toLowerCase()
81
+ }
82
+
83
+ /**
84
+ * Converts an OpenAPI-style path to an Express/MSW-style path by replacing `{param}` with `:param`.
85
+ */
86
+ export function getMswUrl(node: ast.OperationNode): string {
87
+ return node.path.replaceAll('{', ':').replaceAll('}', '')
88
+ }
89
+
90
+ /**
91
+ * Resolves faker metadata for an MSW operation, including response name and file path.
92
+ */
93
+ export function resolveFakerMeta(
94
+ node: ast.OperationNode,
95
+ options: {
96
+ root: string
97
+ fakerResolver: ResolverFaker
98
+ fakerOutput: PluginMsw['resolvedOptions']['output']
99
+ fakerGroup: PluginMsw['resolvedOptions']['group']
100
+ },
101
+ ): { name: string; file: { path: string } } {
102
+ const { root, fakerResolver, fakerOutput, fakerGroup } = options
103
+ const tag = node.tags[0] ?? 'default'
104
+
105
+ return {
106
+ name: fakerResolver.resolveResponseName(node),
107
+ file: fakerResolver.resolveFile({ name: node.operationId, extname: '.ts', tag, path: node.path }, { root, output: fakerOutput, group: fakerGroup }),
108
+ }
109
+ }