@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.
- package/dist/index.cjs +480 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +105 -37
- package/dist/index.js +486 -479
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/Enum.tsx +2 -7
- package/src/components/Type.tsx +8 -0
- package/src/constants.ts +10 -0
- package/src/factory.ts +122 -1
- package/src/generators/typeGenerator.tsx +119 -96
- package/src/generators/typeGeneratorLegacy.tsx +85 -80
- package/src/index.ts +0 -5
- package/src/plugin.ts +19 -49
- package/src/printers/functionPrinter.ts +6 -5
- package/src/printers/printerTs.ts +65 -181
- package/src/resolvers/resolverTs.ts +2 -2
- package/src/types.ts +93 -36
- package/src/utils.ts +41 -13
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
257
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
232
|
+
const { name, syntaxType = 'type', description, keysToOmit } = this.options
|
|
349
233
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
234
|
+
let base = this.transform(node)
|
|
235
|
+
if (!base) return null
|
|
353
236
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
//
|
|
364
|
-
|
|
365
|
-
if (
|
|
366
|
-
|
|
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' ||
|
|
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:
|
|
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
|
-
|
|
384
|
-
description
|
|
385
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
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 {
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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) {
|