@kubb/plugin-ts 5.0.0-beta.30 → 5.0.0-beta.31

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-ts",
3
- "version": "5.0.0-beta.30",
3
+ "version": "5.0.0-beta.31",
4
4
  "description": "Generate TypeScript types, interfaces, and enums from your OpenAPI specification. The foundational plugin that powers type safety across the entire Kubb ecosystem.",
5
5
  "keywords": [
6
6
  "code-generation",
@@ -46,9 +46,9 @@
46
46
  "registry": "https://registry.npmjs.org/"
47
47
  },
48
48
  "dependencies": {
49
- "@kubb/core": "5.0.0-beta.29",
50
- "@kubb/parser-ts": "5.0.0-beta.29",
51
- "@kubb/renderer-jsx": "5.0.0-beta.29",
49
+ "@kubb/core": "5.0.0-beta.31",
50
+ "@kubb/parser-ts": "5.0.0-beta.31",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.31",
52
52
  "remeda": "^2.34.1",
53
53
  "typescript": "^6.0.3"
54
54
  },
@@ -57,7 +57,7 @@
57
57
  "@internals/utils": "0.0.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@kubb/renderer-jsx": "5.0.0-beta.29"
60
+ "@kubb/renderer-jsx": "5.0.0-beta.31"
61
61
  },
62
62
  "size-limit": [
63
63
  {
@@ -1,3 +1,4 @@
1
+ import { resolveContentTypeVariants } from '@internals/shared'
1
2
  import { ast, defineGenerator } from '@kubb/core'
2
3
  import { File, jsxRendererSync } from '@kubb/renderer-jsx'
3
4
  import { Type } from '../components/Type.tsx'
@@ -6,24 +7,6 @@ import { printerTs } from '../printers/printerTs.ts'
6
7
  import type { PluginTs } from '../types'
7
8
  import { buildData, buildResponses, buildResponseUnion } from '../utils.ts'
8
9
 
9
- function getContentTypeSuffix(contentType: string): string {
10
- const baseType = contentType.split(';')[0]!.trim()
11
- if (baseType === 'application/json') return 'Json'
12
- if (baseType === 'multipart/form-data') return 'FormData'
13
- if (baseType === 'application/x-www-form-urlencoded') return 'FormUrlEncoded'
14
- const subtype = baseType.split('/').pop() ?? baseType
15
- const parts = subtype.split(/[^a-zA-Z0-9]+/).filter(Boolean)
16
- if (parts.length === 0) return 'Unknown'
17
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
18
- }
19
-
20
- function getPerContentTypeName(dataName: string, suffix: string): string {
21
- if (dataName.endsWith('Data')) {
22
- return suffix.endsWith('Data') ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`
23
- }
24
- return dataName + suffix
25
- }
26
-
27
10
  /**
28
11
  * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
29
12
  * schema in the spec plus per-operation request, response, and parameter
@@ -168,6 +151,34 @@ export const typeGenerator = defineGenerator<PluginTs>({
168
151
  )
169
152
  }
170
153
 
154
+ /**
155
+ * Emits an individual type per content type plus a union alias under `baseName`.
156
+ * Shared by the request body and multi-content-type responses.
157
+ */
158
+ function buildContentTypeVariants(
159
+ entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
160
+ baseName: string,
161
+ decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
162
+ ) {
163
+ const variants = resolveContentTypeVariants(entries, baseName)
164
+ const unionSchema = ast.createSchema({
165
+ type: 'union',
166
+ members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
167
+ })
168
+ return (
169
+ <>
170
+ {variants.map((variant) =>
171
+ renderSchemaType({
172
+ schema: decorate ? decorate(variant.schema) : variant.schema,
173
+ name: variant.name,
174
+ keysToOmit: variant.keysToOmit,
175
+ }),
176
+ )}
177
+ {renderSchemaType({ schema: unionSchema, name: baseName })}
178
+ </>
179
+ )
180
+ }
181
+
171
182
  const paramTypes = params.map((param) =>
172
183
  renderSchemaType({
173
184
  schema: param.schema,
@@ -192,52 +203,27 @@ export const typeGenerator = defineGenerator<PluginTs>({
192
203
  })
193
204
  }
194
205
  // Multiple content types — generate individual types + union alias
195
- const dataName = resolver.resolveDataName(node)
196
- const usedNames = new Set<string>()
197
- const individualItems = requestBodyContent
198
- .filter((entry) => entry.schema)
199
- .map((entry) => {
200
- const baseSuffix = getContentTypeSuffix(entry.contentType)
201
- let individualName = getPerContentTypeName(dataName, baseSuffix)
202
- let counter = 2
203
- while (usedNames.has(individualName)) {
204
- individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`)
205
- }
206
- usedNames.add(individualName)
207
- return {
208
- name: individualName,
209
- rendered: renderSchemaType({
210
- schema: {
211
- ...entry.schema!,
212
- description: node.requestBody!.description ?? entry.schema!.description,
213
- },
214
- name: individualName,
215
- keysToOmit: entry.keysToOmit,
216
- }),
217
- }
218
- })
219
- const unionSchema = ast.createSchema({
220
- type: 'union',
221
- members: individualItems.map((item) => ast.createSchema({ type: 'ref', name: item.name })),
222
- })
223
- const unionType = renderSchemaType({ schema: unionSchema, name: dataName })
224
- return (
225
- <>
226
- {individualItems.map((item) => item.rendered)}
227
- {unionType}
228
- </>
229
- )
206
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
207
+ ...schema,
208
+ description: node.requestBody!.description ?? schema.description,
209
+ }))
230
210
  }
231
211
 
232
212
  const requestType = buildRequestType()
233
213
 
234
- const responseTypes = node.responses.map((res) =>
235
- renderSchemaType({
236
- schema: res.content?.[0]?.schema ?? null,
214
+ const responseTypes = node.responses.map((res) => {
215
+ const variants = (res.content ?? []).filter((entry) => entry.schema)
216
+ // Multiple content types for a single status code — generate a union of the variants.
217
+ if (variants.length > 1) {
218
+ return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode))
219
+ }
220
+ const primary = variants[0] ?? res.content?.[0]
221
+ return renderSchemaType({
222
+ schema: primary?.schema ?? null,
237
223
  name: resolver.resolveResponseStatusName(node, res.statusCode),
238
- keysToOmit: res.content?.[0]?.keysToOmit,
239
- }),
240
- )
224
+ keysToOmit: primary?.keysToOmit,
225
+ })
226
+ })
241
227
 
242
228
  const dataType = renderSchemaType({
243
229
  schema: buildData({ ...node, parameters: params }, { resolver }),
@@ -250,23 +236,26 @@ export const typeGenerator = defineGenerator<PluginTs>({
250
236
  })
251
237
 
252
238
  function buildResponseType() {
253
- if (!node.responses.some((res) => res.content?.[0]?.schema)) {
239
+ const hasSchema = (res: ast.ResponseNode) => (res.content ?? []).some((entry) => entry.schema)
240
+ if (!node.responses.some(hasSchema)) {
254
241
  return null
255
242
  }
256
243
 
257
244
  const responseName = resolver.resolveResponseName(node)
258
245
 
259
- const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
246
+ const responsesWithSchema = node.responses.filter(hasSchema)
260
247
  const importedNames = new Set(
261
248
  responsesWithSchema.flatMap((res) =>
262
- res.content?.[0]?.schema
263
- ? adapter
264
- .getImports(res.content[0].schema, (schemaName) => ({
265
- name: resolveImportName(schemaName),
266
- path: '',
267
- }))
268
- .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
269
- : [],
249
+ (res.content ?? []).flatMap((entry) =>
250
+ entry.schema
251
+ ? adapter
252
+ .getImports(entry.schema, (schemaName) => ({
253
+ name: resolveImportName(schemaName),
254
+ path: '',
255
+ }))
256
+ .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
257
+ : [],
258
+ ),
270
259
  ),
271
260
  )
272
261
 
package/src/plugin.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { camelCase } from '@internals/utils'
2
- import { definePlugin, type Group } from '@kubb/core'
1
+ import { createGroupConfig } from '@internals/shared'
2
+ import { definePlugin } from '@kubb/core'
3
3
  import { typeGenerator } from './generators/typeGenerator.tsx'
4
4
  import { resolverTs } from './resolvers/resolverTs.ts'
5
5
  import type { PluginTs } from './types.ts'
@@ -54,17 +54,7 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
54
54
  generators: userGenerators = [],
55
55
  } = options
56
56
 
57
- const groupConfig = group
58
- ? ({
59
- ...group,
60
- name: (ctx) => {
61
- if (group.type === 'path') {
62
- return `${ctx.group.split('/')[1]}`
63
- }
64
- return `${camelCase(ctx.group)}Controller`
65
- },
66
- } satisfies Group)
67
- : null
57
+ const groupConfig = createGroupConfig(group, { suffix: 'Controller' })
68
58
 
69
59
  return {
70
60
  name: pluginTsName,
package/src/utils.ts CHANGED
@@ -127,7 +127,7 @@ export function buildResponses(node: ast.OperationNode, { resolver }: BuildOpera
127
127
  }
128
128
 
129
129
  export function buildResponseUnion(node: ast.OperationNode, { resolver }: BuildOperationSchemaOptions): ast.SchemaNode | null {
130
- const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
130
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))
131
131
 
132
132
  if (responsesWithSchema.length === 0) {
133
133
  return null