@kubb/plugin-ts 5.0.0-alpha.23 → 5.0.0-alpha.25

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,6 +1,4 @@
1
- import { jsStringEscape, stringify } from '@internals/utils'
2
- import { isStringType, narrowSchema, schemaTypes } from '@kubb/ast'
3
- import type { ArraySchemaNode, SchemaNode } from '@kubb/ast/types'
1
+ import { extractRefName, isStringType, narrowSchema, schemaTypes, syncSchemaRef } from '@kubb/ast'
4
2
  import type { PrinterFactoryOptions } from '@kubb/core'
5
3
  import { definePrinter } from '@kubb/core'
6
4
  import { safePrint } from '@kubb/fabric-core/parsers/typescript'
@@ -8,6 +6,7 @@ import type ts from 'typescript'
8
6
  import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from '../constants.ts'
9
7
  import * as factory from '../factory.ts'
10
8
  import type { PluginTs, ResolverTs } from '../types.ts'
9
+ import { buildPropertyJSDocComments } from '../utils.ts'
11
10
 
12
11
  type TsOptions = {
13
12
  /**
@@ -52,6 +51,12 @@ type TsOptions = {
52
51
  * Resolver used to transform raw schema names into valid TypeScript identifiers.
53
52
  */
54
53
  resolver: ResolverTs
54
+ /**
55
+ * Names of top-level schemas that are enums.
56
+ * When set, the `ref` handler uses the suffixed type name (e.g. `StatusKey`) for enum refs
57
+ * instead of the plain PascalCase name, so imports align with what the enum file actually exports.
58
+ */
59
+ enumSchemaNames?: Set<string>
55
60
  }
56
61
 
57
62
  /**
@@ -59,140 +64,6 @@ type TsOptions = {
59
64
  */
60
65
  type TsPrinter = PrinterFactoryOptions<'typescript', TsOptions, ts.TypeNode, string>
61
66
 
62
- /**
63
- * Converts a primitive const value to a TypeScript literal type node.
64
- * Handles negative numbers via a prefix unary expression.
65
- */
66
- function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined {
67
- if (format === 'boolean') {
68
- return factory.createLiteralTypeNode(value === true ? factory.createTrue() : factory.createFalse())
69
- }
70
- if (format === 'number' && typeof value === 'number') {
71
- if (value < 0) {
72
- return factory.createLiteralTypeNode(factory.createPrefixUnaryExpression(factory.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value))))
73
- }
74
- return factory.createLiteralTypeNode(factory.createNumericLiteral(value))
75
- }
76
- return factory.createLiteralTypeNode(factory.createStringLiteral(String(value)))
77
- }
78
-
79
- /**
80
- * Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
81
- */
82
- function dateOrStringNode(node: { representation?: string }): ts.TypeNode {
83
- return node.representation === 'date' ? factory.createTypeReferenceNode(factory.createIdentifier('Date')) : factory.keywordTypeNodes.string
84
- }
85
-
86
- /**
87
- * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
88
- */
89
- function buildMemberNodes(members: Array<SchemaNode> | undefined, print: (node: SchemaNode) => ts.TypeNode | null | undefined): Array<ts.TypeNode> {
90
- return (members ?? []).map(print).filter(Boolean)
91
- }
92
-
93
- /**
94
- * Builds a TypeScript tuple type node from an array schema's `items`,
95
- * applying min/max slice and optional/rest element rules.
96
- */
97
- function buildTupleNode(node: ArraySchemaNode, print: (node: SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
98
- let items = (node.items ?? []).map(print).filter(Boolean)
99
-
100
- const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
101
- const { min, max } = node
102
-
103
- if (max !== undefined) {
104
- items = items.slice(0, max)
105
- if (items.length < max && restNode) {
106
- items = [...items, ...Array(max - items.length).fill(restNode)]
107
- }
108
- }
109
-
110
- if (min !== undefined) {
111
- items = items.map((item, i) => (i >= min ? factory.createOptionalTypeNode(item) : item))
112
- }
113
-
114
- if (max === undefined && restNode) {
115
- items.push(factory.createRestTypeNode(factory.createArrayTypeNode(restNode)))
116
- }
117
-
118
- return factory.createTupleTypeNode(items)
119
- }
120
-
121
- /**
122
- * Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
123
- */
124
- function buildPropertyType(schema: SchemaNode, baseType: ts.TypeNode, optionalType: TsOptions['optionalType']): ts.TypeNode {
125
- const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType)
126
-
127
- let type = baseType
128
-
129
- if (schema.nullable) {
130
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] })
131
- }
132
-
133
- if ((schema.nullish || schema.optional) && addsUndefined) {
134
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] })
135
- }
136
-
137
- return type
138
- }
139
-
140
- /**
141
- * Collects JSDoc annotation strings (description, deprecated, min/max, pattern, default, example, type) for a schema node.
142
- */
143
- function buildPropertyJSDocComments(schema: SchemaNode): Array<string | undefined> {
144
- const isArray = schema.type === 'array'
145
-
146
- return [
147
- 'description' in schema && schema.description ? `@description ${jsStringEscape(schema.description)}` : undefined,
148
- 'deprecated' in schema && schema.deprecated ? '@deprecated' : undefined,
149
- // minItems/maxItems on arrays should not be emitted as @minLength/@maxLength
150
- !isArray && 'min' in schema && schema.min !== undefined ? `@minLength ${schema.min}` : undefined,
151
- !isArray && 'max' in schema && schema.max !== undefined ? `@maxLength ${schema.max}` : undefined,
152
- 'pattern' in schema && schema.pattern ? `@pattern ${schema.pattern}` : undefined,
153
- 'default' in schema && schema.default !== undefined
154
- ? `@default ${'primitive' in schema && schema.primitive === 'string' ? stringify(schema.default as string) : schema.default}`
155
- : undefined,
156
- 'example' in schema && schema.example !== undefined ? `@example ${schema.example}` : undefined,
157
- 'primitive' in schema && schema.primitive
158
- ? [`@type ${schema.primitive || 'unknown'}`, 'optional' in schema && schema.optional ? ' | undefined' : undefined].filter(Boolean).join('')
159
- : undefined,
160
- ]
161
- }
162
-
163
- /**
164
- * Creates TypeScript index signatures for `additionalProperties` and `patternProperties` on an object schema node.
165
- */
166
- function buildIndexSignatures(
167
- node: { additionalProperties?: SchemaNode | boolean; patternProperties?: Record<string, SchemaNode> },
168
- propertyCount: number,
169
- print: (node: SchemaNode) => ts.TypeNode | null | undefined,
170
- ): Array<ts.TypeElement> {
171
- const elements: Array<ts.TypeElement> = []
172
-
173
- if (node.additionalProperties && node.additionalProperties !== true) {
174
- const additionalType = print(node.additionalProperties) ?? factory.keywordTypeNodes.unknown
175
-
176
- elements.push(factory.createIndexSignature(propertyCount > 0 ? factory.keywordTypeNodes.unknown : additionalType))
177
- } else if (node.additionalProperties === true) {
178
- elements.push(factory.createIndexSignature(factory.keywordTypeNodes.unknown))
179
- }
180
-
181
- if (node.patternProperties) {
182
- const first = Object.values(node.patternProperties)[0]
183
- if (first) {
184
- let patternType = print(first) ?? factory.keywordTypeNodes.unknown
185
-
186
- if (first.nullable) {
187
- patternType = factory.createUnionDeclaration({ nodes: [patternType, factory.keywordTypeNodes.null] })
188
- }
189
- elements.push(factory.createIndexSignature(patternType))
190
- }
191
- }
192
-
193
- return elements
194
- }
195
-
196
67
  /**
197
68
  * TypeScript type printer built with `definePrinter`.
198
69
  *
@@ -239,12 +110,14 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
239
110
  }
240
111
  return factory.keywordTypeNodes.string
241
112
  },
113
+ ipv4: () => factory.keywordTypeNodes.string,
114
+ ipv6: () => factory.keywordTypeNodes.string,
242
115
  datetime: () => factory.keywordTypeNodes.string,
243
116
  number: () => factory.keywordTypeNodes.number,
244
117
  integer: () => factory.keywordTypeNodes.number,
245
118
  bigint: () => factory.keywordTypeNodes.bigint,
246
- date: dateOrStringNode,
247
- time: dateOrStringNode,
119
+ date: factory.dateOrStringNode,
120
+ time: factory.dateOrStringNode,
248
121
  ref(node) {
249
122
  if (!node.name) {
250
123
  return undefined
@@ -253,8 +126,18 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
253
126
  // Use the canonical name from the $ref path — node.name may have been overridden
254
127
  // (e.g. by single-member allOf flatten using the property-derived child name).
255
128
  // Inline refs (without $ref) from utils already carry resolved type names.
256
- const refName = node.ref ? (node.ref.split('/').at(-1) ?? node.name) : node.name
257
- const name = node.ref ? this.options.resolver.default(refName, 'type') : refName
129
+ const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
130
+
131
+ // When a Key suffix is configured, enum refs must use the suffixed name (e.g. `StatusKey`)
132
+ // so the reference matches what the enum file actually exports.
133
+ const isEnumRef =
134
+ node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix && this.options.enumSchemaNames?.has(refName)
135
+
136
+ const name = isEnumRef
137
+ ? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.enumTypeSuffix!)
138
+ : node.ref
139
+ ? this.options.resolver.default(refName, 'type')
140
+ : refName
258
141
 
259
142
  return factory.createTypeReferenceNode(name, undefined)
260
143
  },
@@ -263,8 +146,8 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
263
146
 
264
147
  if (this.options.enumType === 'inlineLiteral' || !node.name) {
265
148
  const literalNodes = values
266
- .filter((v): v is string | number | boolean => v !== null)
267
- .map((value) => constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
149
+ .filter((v): v is string | number | boolean => v !== null && v !== undefined)
150
+ .map((value) => factory.constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
268
151
  .filter(Boolean)
269
152
 
270
153
  return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
@@ -272,7 +155,7 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
272
155
 
273
156
  const resolvedName =
274
157
  ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix
275
- ? this.options.resolver.resolveEnumKeyName(node as unknown as SchemaNode, this.options.enumTypeSuffix)
158
+ ? this.options.resolver.resolveEnumKeyName(node, this.options.enumTypeSuffix)
276
159
  : this.options.resolver.default(node.name, 'type')
277
160
 
278
161
  return factory.createTypeReferenceNode(resolvedName, undefined)
@@ -303,10 +186,10 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
303
186
  return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
304
187
  }
305
188
 
306
- return factory.createUnionDeclaration({ withParentheses: true, nodes: buildMemberNodes(members, this.transform) }) ?? undefined
189
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(members, this.transform) }) ?? undefined
307
190
  },
308
191
  intersection(node) {
309
- return factory.createIntersectionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.transform) }) ?? undefined
192
+ return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? undefined
310
193
  },
311
194
  array(node) {
312
195
  const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -314,7 +197,7 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
314
197
  return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined
315
198
  },
316
199
  tuple(node) {
317
- return buildTupleNode(node, this.transform)
200
+ return factory.buildTupleNode(node, this.transform)
318
201
  },
319
202
  object(node) {
320
203
  const { transform, options } = this
@@ -323,19 +206,20 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
323
206
 
324
207
  const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => {
325
208
  const baseType = transform(prop.schema) ?? factory.keywordTypeNodes.unknown
326
- const type = buildPropertyType(prop.schema, baseType, options.optionalType)
209
+ const type = factory.buildPropertyType(prop.schema, baseType, options.optionalType)
210
+ const propMeta = syncSchemaRef(prop.schema)
327
211
 
328
212
  const propertyNode = factory.createPropertySignature({
329
213
  questionToken: prop.schema.optional || prop.schema.nullish ? addsQuestionToken : false,
330
214
  name: prop.name,
331
215
  type,
332
- readOnly: prop.schema.readOnly,
216
+ readOnly: propMeta?.readOnly,
333
217
  })
334
218
 
335
219
  return factory.appendJSDocToNode({ node: propertyNode, comments: buildPropertyJSDocComments(prop.schema) })
336
220
  })
337
221
 
338
- const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, transform)]
222
+ const allElements = [...propertyNodes, ...factory.buildIndexSignatures(node, propertyNodes.length, transform)]
339
223
 
340
224
  if (!allElements.length) {
341
225
  return factory.keywordTypeNodes.object
@@ -345,50 +229,50 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
345
229
  },
346
230
  },
347
231
  print(node) {
348
- let type = this.transform(node)
232
+ const { name, syntaxType = 'type', description, keysToOmit } = this.options
349
233
 
350
- if (!type) {
351
- return null
352
- }
234
+ let base = this.transform(node)
235
+ if (!base) return null
353
236
 
354
- // Apply top-level nullable / optional union modifiers.
355
- if (node.nullable) {
356
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] })
237
+ // For ref nodes, structural metadata lives on node.schema rather than the ref node itself.
238
+ const meta = syncSchemaRef(node)
239
+
240
+ // Without name, apply modifiers inline and return.
241
+ if (!name) {
242
+ if (meta.nullable) {
243
+ base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.null] })
244
+ }
245
+ if ((meta.nullish || meta.optional) && addsUndefined) {
246
+ base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.undefined] })
247
+ }
248
+ return safePrint(base)
357
249
  }
358
250
 
359
- if ((node.nullish || node.optional) && addsUndefined) {
360
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] })
251
+ // When keysToOmit is present, wrap with Omit first, then apply nullable/optional
252
+ // modifiers so they are not swallowed by NonNullable inside createOmitDeclaration.
253
+ let inner: ts.TypeNode = keysToOmit?.length ? factory.createOmitDeclaration({ keys: keysToOmit, type: base, nonNullable: true }) : base
254
+
255
+ if (meta.nullable) {
256
+ inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.null] })
361
257
  }
362
258
 
363
- // Without name, return the type node as-is (no declaration wrapping).
364
- const { name, syntaxType = 'type', description, keysToOmit } = this.options
365
- if (!name) {
366
- return safePrint(type)
259
+ // For named type declarations (type aliases), optional/nullish always produces | undefined
260
+ // regardless of optionalType the questionToken ? modifier only applies to object properties.
261
+ if (meta.nullish || meta.optional) {
262
+ inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.undefined] })
367
263
  }
368
264
 
369
- const useTypeGeneration = syntaxType === 'type' || type.kind === factory.syntaxKind.union || !!keysToOmit?.length
265
+ const useTypeGeneration = syntaxType === 'type' || inner.kind === factory.syntaxKind.union || !!keysToOmit?.length
370
266
 
371
267
  const typeNode = factory.createTypeDeclaration({
372
268
  name,
373
269
  isExportable: true,
374
- type: keysToOmit?.length
375
- ? factory.createOmitDeclaration({
376
- keys: keysToOmit,
377
- type,
378
- nonNullable: true,
379
- })
380
- : type,
270
+ type: inner,
381
271
  syntax: useTypeGeneration ? 'type' : 'interface',
382
- comments: [
383
- node?.title ? jsStringEscape(node.title) : undefined,
384
- description ? `@description ${jsStringEscape(description)}` : undefined,
385
- node?.deprecated ? '@deprecated' : undefined,
386
- node && 'min' in node && node.min !== undefined ? `@minLength ${node.min}` : undefined,
387
- node && 'max' in node && node.max !== undefined ? `@maxLength ${node.max}` : undefined,
388
- node && 'pattern' in node && node.pattern ? `@pattern ${node.pattern}` : undefined,
389
- node?.default ? `@default ${node.default}` : undefined,
390
- node?.example ? `@example ${node.example}` : undefined,
391
- ],
272
+ comments: buildPropertyJSDocComments({
273
+ ...meta,
274
+ description,
275
+ }),
392
276
  })
393
277
 
394
278
  return safePrint(typeNode)
@@ -2,7 +2,7 @@ import { pascalCase } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginTs } from '../types.ts'
4
4
 
5
- function resolveName(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
5
+ function toTypeName(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
6
6
  return pascalCase(name, { isFile: type === 'file' })
7
7
  }
8
8
 
@@ -28,7 +28,7 @@ export const resolverTs = defineResolver<PluginTs>(() => {
28
28
  name: 'default',
29
29
  pluginName: 'plugin-ts',
30
30
  default(name, type) {
31
- return resolveName(name, type)
31
+ return toTypeName(name, type)
32
32
  },
33
33
  resolveName(name) {
34
34
  return this.default(name, 'function')
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { OperationParamsResolver } from '@kubb/ast'
2
- import type { OperationNode, ParameterNode, SchemaNode, StatusCode, Visitor } from '@kubb/ast/types'
2
+ import type { OperationNode, ParameterNode, StatusCode, Visitor } from '@kubb/ast/types'
3
3
  import type {
4
4
  CompatibilityPreset,
5
5
  Exclude,
@@ -35,7 +35,9 @@ export type ResolverTs = Resolver &
35
35
  * resolver.resolvePathName('list pets', 'file') // → 'ListPets'
36
36
  */
37
37
  resolvePathName(name: string, type?: 'file' | 'function' | 'type' | 'const'): string
38
- /** Resolves the request body type name (required on ResolverTs). */
38
+ /**
39
+ * Resolves the request body type name for an operation (required on ResolverTs).
40
+ */
39
41
  resolveDataName(node: OperationNode): string
40
42
 
41
43
  /**
@@ -76,7 +78,7 @@ export type ResolverTs = Resolver &
76
78
  * resolver.resolveEnumKeyName(node, 'Value') // → 'PetStatusValue'
77
79
  * resolver.resolveEnumKeyName(node, '') // → 'PetStatus'
78
80
  */
79
- resolveEnumKeyName(node: SchemaNode, enumTypeSuffix: string): string
81
+ resolveEnumKeyName(node: { name?: string | null }, enumTypeSuffix: string): string
80
82
  /**
81
83
  * Resolves the name for an operation's grouped path parameters type.
82
84
  *
@@ -100,6 +102,92 @@ export type ResolverTs = Resolver &
100
102
  resolveHeaderParamsName(node: OperationNode, param: ParameterNode): string
101
103
  }
102
104
 
105
+ type EnumKeyCasing = 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none'
106
+
107
+ /**
108
+ * Discriminated union that ties `enumTypeSuffix` and `enumKeyCasing` to the enum types that actually use them.
109
+ *
110
+ * - `'asConst'` / `'asPascalConst'` — emit a `const` object; both `enumTypeSuffix` (type-alias suffix) and
111
+ * `enumKeyCasing` (key formatting) are meaningful.
112
+ * - `'enum'` / `'constEnum'` — emit a TypeScript enum; `enumKeyCasing` applies to member names,
113
+ * but there is no separate type alias so `enumTypeSuffix` is not used.
114
+ * - `'literal'` / `'inlineLiteral'` — emit only union literals; keys are discarded entirely,
115
+ * so neither `enumTypeSuffix` nor `enumKeyCasing` have any effect.
116
+ */
117
+ type EnumTypeOptions =
118
+ | {
119
+ /**
120
+ * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
121
+ * - 'asConst' generates const objects with camelCase names and as const assertion.
122
+ * - 'asPascalConst' generates const objects with PascalCase names and as const assertion.
123
+ * @default 'asConst'
124
+ */
125
+ enumType?: 'asConst' | 'asPascalConst'
126
+ /**
127
+ * Suffix appended to the generated type alias name.
128
+ *
129
+ * Only affects the type alias — the const object name is unchanged.
130
+ *
131
+ * @default 'Key'
132
+ * @example enumTypeSuffix: 'Value' → `export type PetStatusValue = …`
133
+ */
134
+ enumTypeSuffix?: string
135
+ /**
136
+ * Choose the casing for enum key names.
137
+ * - 'screamingSnakeCase' generates keys in SCREAMING_SNAKE_CASE format.
138
+ * - 'snakeCase' generates keys in snake_case format.
139
+ * - 'pascalCase' generates keys in PascalCase format.
140
+ * - 'camelCase' generates keys in camelCase format.
141
+ * - 'none' uses the enum value as-is without transformation.
142
+ * @default 'none'
143
+ */
144
+ enumKeyCasing?: EnumKeyCasing
145
+ }
146
+ | {
147
+ /**
148
+ * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
149
+ * - 'enum' generates TypeScript enum declarations.
150
+ * - 'constEnum' generates TypeScript const enum declarations.
151
+ * @default 'asConst'
152
+ */
153
+ enumType?: 'enum' | 'constEnum'
154
+ /**
155
+ * `enumTypeSuffix` has no effect for this `enumType`.
156
+ * It is only used when `enumType` is `'asConst'` or `'asPascalConst'`.
157
+ */
158
+ enumTypeSuffix?: never
159
+ /**
160
+ * Choose the casing for enum key names.
161
+ * - 'screamingSnakeCase' generates keys in SCREAMING_SNAKE_CASE format.
162
+ * - 'snakeCase' generates keys in snake_case format.
163
+ * - 'pascalCase' generates keys in PascalCase format.
164
+ * - 'camelCase' generates keys in camelCase format.
165
+ * - 'none' uses the enum value as-is without transformation.
166
+ * @default 'none'
167
+ */
168
+ enumKeyCasing?: EnumKeyCasing
169
+ }
170
+ | {
171
+ /**
172
+ * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
173
+ * - 'literal' generates literal union types.
174
+ * - 'inlineLiteral' will inline enum values directly into the type (default in v5).
175
+ * @default 'asConst'
176
+ * @note In Kubb v5, 'inlineLiteral' becomes the default.
177
+ */
178
+ enumType?: 'literal' | 'inlineLiteral'
179
+ /**
180
+ * `enumTypeSuffix` has no effect for this `enumType`.
181
+ * It is only used when `enumType` is `'asConst'` or `'asPascalConst'`.
182
+ */
183
+ enumTypeSuffix?: never
184
+ /**
185
+ * `enumKeyCasing` has no effect for this `enumType`.
186
+ * Literal and inlineLiteral modes emit only values — keys are discarded entirely.
187
+ */
188
+ enumKeyCasing?: never
189
+ }
190
+
103
191
  export type Options = {
104
192
  /**
105
193
  * Specify the export location for the files and define the behavior of the output
@@ -127,37 +215,6 @@ export type Options = {
127
215
  * Array containing override parameters to override `options` based on tags/operations/methods/paths.
128
216
  */
129
217
  override?: Array<Override<ResolvedOptions>>
130
- /**
131
- * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
132
- * - 'enum' generates TypeScript enum declarations.
133
- * - 'asConst' generates const objects with camelCase names and as const assertion.
134
- * - 'asPascalConst' generates const objects with PascalCase names and as const assertion.
135
- * - 'constEnum' generates TypeScript const enum declarations.
136
- * - 'literal' generates literal union types.
137
- * - 'inlineLiteral' inline enum values directly into the type (default in v5).
138
- * @default 'asConst'
139
- * @note In Kubb v5, 'inlineLiteral' becomes the default.
140
- */
141
- enumType?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
142
- /**
143
- * Suffix appended to the generated type alias name when `enumType` is `asConst` or `asPascalConst`.
144
- *
145
- * Only affects the type alias — the const object name is unchanged.
146
- *
147
- * @default 'Key'
148
- * @example enumTypeSuffix: 'Value' → `export type PetStatusValue = …`
149
- */
150
- enumTypeSuffix?: string
151
- /**
152
- * Choose the casing for enum key names.
153
- * - 'screamingSnakeCase' generates keys in SCREAMING_SNAKE_CASE format.
154
- * - 'snakeCase' generates keys in snake_case format.
155
- * - 'pascalCase' generates keys in PascalCase format.
156
- * - 'camelCase' generates keys in camelCase format.
157
- * - 'none' uses the enum value as-is without transformation.
158
- * @default 'none'
159
- */
160
- enumKeyCasing?: 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none'
161
218
  /**
162
219
  * Switch between type or interface for creating TypeScript types.
163
220
  * - 'type' generates type alias declarations.
@@ -228,14 +285,14 @@ export type Options = {
228
285
  * ```
229
286
  */
230
287
  transformers?: Array<Visitor>
231
- }
288
+ } & EnumTypeOptions
232
289
 
233
290
  type ResolvedOptions = {
234
291
  output: Output
235
292
  group: Group | undefined
236
293
  enumType: NonNullable<Options['enumType']>
237
294
  enumTypeSuffix: NonNullable<Options['enumTypeSuffix']>
238
- enumKeyCasing: NonNullable<Options['enumKeyCasing']>
295
+ enumKeyCasing: EnumKeyCasing
239
296
  optionalType: NonNullable<Options['optionalType']>
240
297
  arrayType: NonNullable<Options['arrayType']>
241
298
  syntaxType: NonNullable<Options['syntaxType']>
package/src/utils.ts CHANGED
@@ -1,19 +1,47 @@
1
- import { createProperty, createSchema } from '@kubb/ast'
1
+ import { jsStringEscape, stringify } from '@internals/utils'
2
+ import { createProperty, createSchema, syncSchemaRef } from '@kubb/ast'
2
3
  import type { OperationNode, ParameterNode, SchemaNode } from '@kubb/ast/types'
3
4
  import type { ResolverTs } from './types.ts'
4
5
 
6
+ /**
7
+ * Collects JSDoc annotation strings for a schema node.
8
+ *
9
+ * Only uses official JSDoc tags from https://jsdoc.app/: `@description`, `@deprecated`, `@default`, `@example`, `@type`.
10
+ * Constraint metadata (min/max length, pattern, multipleOf, min/maxProperties) is emitted as plain-text lines.
11
+
12
+ */
13
+ export function buildPropertyJSDocComments(schema: SchemaNode): Array<string | undefined> {
14
+ const meta = syncSchemaRef(schema)
15
+
16
+ const isArray = meta?.primitive === 'array'
17
+
18
+ return [
19
+ meta && 'description' in meta && meta.description ? `@description ${jsStringEscape(meta.description)}` : undefined,
20
+ meta && 'deprecated' in meta && meta.deprecated ? '@deprecated' : undefined,
21
+ // minItems/maxItems on arrays should not be emitted as @minLength/@maxLength
22
+ !isArray && meta && 'min' in meta && meta.min !== undefined ? `@minLength ${meta.min}` : undefined,
23
+ !isArray && meta && 'max' in meta && meta.max !== undefined ? `@maxLength ${meta.max}` : undefined,
24
+ meta && 'pattern' in meta && meta.pattern ? `@pattern ${meta.pattern}` : undefined,
25
+ meta && 'default' in meta && meta.default !== undefined
26
+ ? `@default ${'primitive' in meta && meta.primitive === 'string' ? stringify(meta.default as string) : meta.default}`
27
+ : undefined,
28
+ meta && 'example' in meta && meta.example !== undefined ? `@example ${meta.example}` : undefined,
29
+ meta && 'primitive' in meta && meta.primitive
30
+ ? [`@type ${meta.primitive}`, 'optional' in schema && schema.optional ? ' | undefined' : undefined].filter(Boolean).join('')
31
+ : undefined,
32
+ ].filter(Boolean)
33
+ }
34
+
5
35
  type BuildParamsSchemaOptions = {
6
36
  params: Array<ParameterNode>
7
- node: OperationNode
8
37
  resolver: ResolverTs
9
38
  }
10
39
 
11
40
  type BuildOperationSchemaOptions = {
12
- node: OperationNode
13
41
  resolver: ResolverTs
14
42
  }
15
43
 
16
- export function buildParams({ params, node, resolver }: BuildParamsSchemaOptions): SchemaNode {
44
+ export function buildParams(node: OperationNode, { params, resolver }: BuildParamsSchemaOptions): SchemaNode {
17
45
  return createSchema({
18
46
  type: 'object',
19
47
  properties: params.map((param) =>
@@ -29,7 +57,7 @@ export function buildParams({ params, node, resolver }: BuildParamsSchemaOptions
29
57
  })
30
58
  }
31
59
 
32
- export function buildData({ node, resolver }: BuildOperationSchemaOptions): SchemaNode {
60
+ export function buildData(node: OperationNode, { resolver }: BuildOperationSchemaOptions): SchemaNode {
33
61
  const pathParams = node.parameters.filter((p) => p.in === 'path')
34
62
  const queryParams = node.parameters.filter((p) => p.in === 'query')
35
63
  const headerParams = node.parameters.filter((p) => p.in === 'header')
@@ -42,26 +70,26 @@ export function buildData({ node, resolver }: BuildOperationSchemaOptions): Sche
42
70
  name: 'data',
43
71
  schema: node.requestBody?.schema
44
72
  ? createSchema({ type: 'ref', name: resolver.resolveDataName(node), optional: true })
45
- : createSchema({ type: 'never', optional: true }),
73
+ : createSchema({ type: 'never', primitive: undefined, optional: true }),
46
74
  }),
47
75
  createProperty({
48
76
  name: 'pathParams',
49
77
  required: pathParams.length > 0,
50
- schema: pathParams.length > 0 ? buildParams({ params: pathParams, node, resolver }) : createSchema({ type: 'never' }),
78
+ schema: pathParams.length > 0 ? buildParams(node, { params: pathParams, resolver }) : createSchema({ type: 'never', primitive: undefined }),
51
79
  }),
52
80
  createProperty({
53
81
  name: 'queryParams',
54
82
  schema:
55
83
  queryParams.length > 0
56
- ? createSchema({ ...buildParams({ params: queryParams, node, resolver }), optional: true })
57
- : createSchema({ type: 'never', optional: true }),
84
+ ? createSchema({ ...buildParams(node, { params: queryParams, resolver }), optional: true })
85
+ : createSchema({ type: 'never', primitive: undefined, optional: true }),
58
86
  }),
59
87
  createProperty({
60
88
  name: 'headerParams',
61
89
  schema:
62
90
  headerParams.length > 0
63
- ? createSchema({ ...buildParams({ params: headerParams, node, resolver }), optional: true })
64
- : createSchema({ type: 'never', optional: true }),
91
+ ? createSchema({ ...buildParams(node, { params: headerParams, resolver }), optional: true })
92
+ : createSchema({ type: 'never', primitive: undefined, optional: true }),
65
93
  }),
66
94
  createProperty({
67
95
  name: 'url',
@@ -72,7 +100,7 @@ export function buildData({ node, resolver }: BuildOperationSchemaOptions): Sche
72
100
  })
73
101
  }
74
102
 
75
- export function buildResponses({ node, resolver }: BuildOperationSchemaOptions): SchemaNode | null {
103
+ export function buildResponses(node: OperationNode, { resolver }: BuildOperationSchemaOptions): SchemaNode | null {
76
104
  if (node.responses.length === 0) {
77
105
  return null
78
106
  }
@@ -89,7 +117,7 @@ export function buildResponses({ node, resolver }: BuildOperationSchemaOptions):
89
117
  })
90
118
  }
91
119
 
92
- export function buildResponseUnion({ node, resolver }: BuildOperationSchemaOptions): SchemaNode | null {
120
+ export function buildResponseUnion(node: OperationNode, { resolver }: BuildOperationSchemaOptions): SchemaNode | null {
93
121
  const responsesWithSchema = node.responses.filter((res) => res.schema)
94
122
 
95
123
  if (responsesWithSchema.length === 0) {