@kubb/plugin-ts 5.0.0-alpha.2 → 5.0.0-alpha.20

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 (57) hide show
  1. package/dist/Type-B70QnSzH.cjs +688 -0
  2. package/dist/Type-B70QnSzH.cjs.map +1 -0
  3. package/dist/Type-CMC7L-38.js +671 -0
  4. package/dist/Type-CMC7L-38.js.map +1 -0
  5. package/dist/casing-Cp-jbC_k.js +84 -0
  6. package/dist/casing-Cp-jbC_k.js.map +1 -0
  7. package/dist/casing-D2uQKLWS.cjs +144 -0
  8. package/dist/casing-D2uQKLWS.cjs.map +1 -0
  9. package/dist/components.cjs +3 -2
  10. package/dist/components.d.ts +41 -11
  11. package/dist/components.js +2 -2
  12. package/dist/generators-BFkr7ecU.js +556 -0
  13. package/dist/generators-BFkr7ecU.js.map +1 -0
  14. package/dist/generators-xHWQCNd9.cjs +560 -0
  15. package/dist/generators-xHWQCNd9.cjs.map +1 -0
  16. package/dist/generators.cjs +2 -2
  17. package/dist/generators.d.ts +3 -491
  18. package/dist/generators.js +1 -1
  19. package/dist/index.cjs +146 -3
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +145 -1
  23. package/dist/index.js.map +1 -0
  24. package/dist/resolvers-DsKabI0F.js +184 -0
  25. package/dist/resolvers-DsKabI0F.js.map +1 -0
  26. package/dist/resolvers-YIpeP5YD.cjs +194 -0
  27. package/dist/resolvers-YIpeP5YD.cjs.map +1 -0
  28. package/dist/resolvers.cjs +4 -0
  29. package/dist/resolvers.d.ts +52 -0
  30. package/dist/resolvers.js +2 -0
  31. package/dist/types-zqLMbIqZ.d.ts +340 -0
  32. package/package.json +15 -8
  33. package/src/components/Enum.tsx +83 -0
  34. package/src/components/Type.tsx +25 -144
  35. package/src/components/index.ts +1 -0
  36. package/src/constants.ts +29 -0
  37. package/src/factory.ts +14 -16
  38. package/src/generators/typeGenerator.tsx +221 -414
  39. package/src/generators/utils.ts +308 -0
  40. package/src/index.ts +1 -1
  41. package/src/plugin.ts +74 -87
  42. package/src/presets.ts +23 -0
  43. package/src/printer.ts +256 -92
  44. package/src/resolvers/index.ts +2 -0
  45. package/src/resolvers/resolverTs.ts +104 -0
  46. package/src/resolvers/resolverTsLegacy.ts +87 -0
  47. package/src/types.ts +234 -63
  48. package/dist/components-9wydyqUx.cjs +0 -848
  49. package/dist/components-9wydyqUx.cjs.map +0 -1
  50. package/dist/components-LmqJfxMv.js +0 -721
  51. package/dist/components-LmqJfxMv.js.map +0 -1
  52. package/dist/plugin-CNkzbtpl.cjs +0 -508
  53. package/dist/plugin-CNkzbtpl.cjs.map +0 -1
  54. package/dist/plugin-DoLrDl9P.js +0 -476
  55. package/dist/plugin-DoLrDl9P.js.map +0 -1
  56. package/dist/types-BpeKGgCn.d.ts +0 -170
  57. package/src/parser.ts +0 -396
package/src/printer.ts CHANGED
@@ -1,27 +1,61 @@
1
1
  import { jsStringEscape, stringify } from '@internals/utils'
2
+ import { isStringType, narrowSchema, schemaTypes } from '@kubb/ast'
2
3
  import type { ArraySchemaNode, SchemaNode } from '@kubb/ast/types'
3
4
  import type { PrinterFactoryOptions } from '@kubb/core'
4
5
  import { definePrinter } from '@kubb/core'
5
6
  import type ts from 'typescript'
7
+ import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
6
8
  import * as factory from './factory.ts'
9
+ import type { PluginTs, ResolverTs } from './types.ts'
7
10
 
8
11
  type TsOptions = {
9
12
  /**
10
13
  * @default `'questionToken'`
11
14
  */
12
- optionalType: 'questionToken' | 'undefined' | 'questionTokenAndUndefined'
15
+ optionalType: PluginTs['resolvedOptions']['optionalType']
13
16
  /**
14
17
  * @default `'array'`
15
18
  */
16
- arrayType: 'array' | 'generic'
19
+ arrayType: PluginTs['resolvedOptions']['arrayType']
17
20
  /**
18
21
  * @default `'inlineLiteral'`
19
22
  */
20
- enumType: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
23
+ enumType: PluginTs['resolvedOptions']['enumType']
24
+ /**
25
+ * Controls whether a `type` alias or `interface` declaration is emitted.
26
+ * @default `'type'`
27
+ */
28
+ syntaxType?: PluginTs['resolvedOptions']['syntaxType']
29
+ /**
30
+ * When set, `printer.print(node)` produces a full `type Name = …` declaration.
31
+ * When omitted, `printer.print(node)` returns the raw type node.
32
+ */
33
+ typeName?: string
34
+
35
+ /**
36
+ * JSDoc `@description` comment added to the generated type or interface declaration.
37
+ */
38
+ description?: string
39
+ /**
40
+ * Property keys to exclude from the generated type via `Omit<Type, Keys>`.
41
+ * Forces type-alias syntax even when `syntaxType` is `'interface'`.
42
+ */
43
+ keysToOmit?: Array<string>
44
+ /**
45
+ * Resolver used to transform raw schema names into valid TypeScript identifiers.
46
+ */
47
+ resolver: ResolverTs
21
48
  }
22
49
 
23
- type TsPrinter = PrinterFactoryOptions<'typescript', TsOptions, ts.TypeNode>
50
+ /**
51
+ * TypeScript printer factory options: maps `SchemaNode` → `ts.TypeNode` (raw) or `ts.Node` (full declaration).
52
+ */
53
+ type TsPrinter = PrinterFactoryOptions<'typescript', TsOptions, ts.TypeNode, ts.Node>
24
54
 
55
+ /**
56
+ * Converts a primitive const value to a TypeScript literal type node.
57
+ * Handles negative numbers via a prefix unary expression.
58
+ */
25
59
  function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined {
26
60
  if (format === 'boolean') {
27
61
  return factory.createLiteralTypeNode(value === true ? factory.createTrue() : factory.createFalse())
@@ -35,16 +69,26 @@ function constToTypeNode(value: string | number | boolean, format: 'string' | 'n
35
69
  return factory.createLiteralTypeNode(factory.createStringLiteral(String(value)))
36
70
  }
37
71
 
72
+ /**
73
+ * Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
74
+ */
38
75
  function dateOrStringNode(node: { representation?: string }): ts.TypeNode {
39
76
  return node.representation === 'date' ? factory.createTypeReferenceNode(factory.createIdentifier('Date')) : factory.keywordTypeNodes.string
40
77
  }
41
78
 
79
+ /**
80
+ * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
81
+ */
42
82
  function buildMemberNodes(members: Array<SchemaNode> | undefined, print: (node: SchemaNode) => ts.TypeNode | null | undefined): Array<ts.TypeNode> {
43
- return (members ?? []).map(print).filter(Boolean) as Array<ts.TypeNode>
83
+ return (members ?? []).map(print).filter(Boolean)
44
84
  }
45
85
 
86
+ /**
87
+ * Builds a TypeScript tuple type node from an array schema's `items`,
88
+ * applying min/max slice and optional/rest element rules.
89
+ */
46
90
  function buildTupleNode(node: ArraySchemaNode, print: (node: SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
47
- let items = (node.items ?? []).map(print).filter(Boolean) as Array<ts.TypeNode>
91
+ let items = (node.items ?? []).map(print).filter(Boolean)
48
92
 
49
93
  const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
50
94
  const { min, max } = node
@@ -67,28 +111,37 @@ function buildTupleNode(node: ArraySchemaNode, print: (node: SchemaNode) => ts.T
67
111
  return factory.createTupleTypeNode(items)
68
112
  }
69
113
 
114
+ /**
115
+ * Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
116
+ */
70
117
  function buildPropertyType(schema: SchemaNode, baseType: ts.TypeNode, optionalType: TsOptions['optionalType']): ts.TypeNode {
71
- const addsUndefined = ['undefined', 'questionTokenAndUndefined'].includes(optionalType)
118
+ const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType)
72
119
 
73
120
  let type = baseType
74
121
 
75
122
  if (schema.nullable) {
76
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] }) as ts.TypeNode
123
+ type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] })
77
124
  }
78
125
 
79
126
  if ((schema.nullish || schema.optional) && addsUndefined) {
80
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] }) as ts.TypeNode
127
+ type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] })
81
128
  }
82
129
 
83
130
  return type
84
131
  }
85
132
 
133
+ /**
134
+ * Collects JSDoc annotation strings (description, deprecated, min/max, pattern, default, example, type) for a schema node.
135
+ */
86
136
  function buildPropertyJSDocComments(schema: SchemaNode): Array<string | undefined> {
137
+ const isArray = schema.type === 'array'
138
+
87
139
  return [
88
- 'description' in schema && schema.description ? `@description ${jsStringEscape(schema.description as string)}` : undefined,
140
+ 'description' in schema && schema.description ? `@description ${jsStringEscape(schema.description)}` : undefined,
89
141
  'deprecated' in schema && schema.deprecated ? '@deprecated' : undefined,
90
- 'min' in schema && schema.min !== undefined ? `@minLength ${schema.min}` : undefined,
91
- 'max' in schema && schema.max !== undefined ? `@maxLength ${schema.max}` : undefined,
142
+ // minItems/maxItems on arrays should not be emitted as @minLength/@maxLength
143
+ !isArray && 'min' in schema && schema.min !== undefined ? `@minLength ${schema.min}` : undefined,
144
+ !isArray && 'max' in schema && schema.max !== undefined ? `@maxLength ${schema.max}` : undefined,
92
145
  'pattern' in schema && schema.pattern ? `@pattern ${schema.pattern}` : undefined,
93
146
  'default' in schema && schema.default !== undefined
94
147
  ? `@default ${'primitive' in schema && schema.primitive === 'string' ? stringify(schema.default as string) : schema.default}`
@@ -100,6 +153,9 @@ function buildPropertyJSDocComments(schema: SchemaNode): Array<string | undefine
100
153
  ]
101
154
  }
102
155
 
156
+ /**
157
+ * Creates TypeScript index signatures for `additionalProperties` and `patternProperties` on an object schema node.
158
+ */
103
159
  function buildIndexSignatures(
104
160
  node: { additionalProperties?: SchemaNode | boolean; patternProperties?: Record<string, SchemaNode> },
105
161
  propertyCount: number,
@@ -108,7 +164,8 @@ function buildIndexSignatures(
108
164
  const elements: Array<ts.TypeElement> = []
109
165
 
110
166
  if (node.additionalProperties && node.additionalProperties !== true) {
111
- const additionalType = (print(node.additionalProperties as SchemaNode) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode
167
+ const additionalType = print(node.additionalProperties) ?? factory.keywordTypeNodes.unknown
168
+
112
169
  elements.push(factory.createIndexSignature(propertyCount > 0 ? factory.keywordTypeNodes.unknown : additionalType))
113
170
  } else if (node.additionalProperties === true) {
114
171
  elements.push(factory.createIndexSignature(factory.keywordTypeNodes.unknown))
@@ -117,9 +174,10 @@ function buildIndexSignatures(
117
174
  if (node.patternProperties) {
118
175
  const first = Object.values(node.patternProperties)[0]
119
176
  if (first) {
120
- let patternType = (print(first) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode
177
+ let patternType = print(first) ?? factory.keywordTypeNodes.unknown
178
+
121
179
  if (first.nullable) {
122
- patternType = factory.createUnionDeclaration({ nodes: [patternType, factory.keywordTypeNodes.null] }) as ts.TypeNode
180
+ patternType = factory.createUnionDeclaration({ nodes: [patternType, factory.keywordTypeNodes.null] })
123
181
  }
124
182
  elements.push(factory.createIndexSignature(patternType))
125
183
  }
@@ -129,93 +187,199 @@ function buildIndexSignatures(
129
187
  }
130
188
 
131
189
  /**
132
- * Converts a `SchemaNode` AST node into a TypeScript `ts.TypeNode`.
190
+ * TypeScript type printer built with `definePrinter`.
191
+ *
192
+ * Converts a `SchemaNode` AST node into a TypeScript AST node:
193
+ * - **`printer.print(node)`** — when `options.typeName` is set, returns a full
194
+ * `type Name = …` or `interface Name { … }` declaration (`ts.Node`).
195
+ * Without `typeName`, returns the raw `ts.TypeNode` for the schema.
196
+ *
197
+ * Dispatches on `node.type` to the appropriate handler in `nodes`. Options are closed
198
+ * over per printer instance, so each call to `printerTs(options)` produces an independent printer.
199
+ *
200
+ * @example Raw type node (no `typeName`)
201
+ * ```ts
202
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral' })
203
+ * const typeNode = printer.print(schemaNode) // ts.TypeNode
204
+ * ```
133
205
  *
134
- * Built on `definePrinter` dispatches on `node.type`, with options closed over
135
- * per printer instance. Produces the same `ts.TypeNode` output as the keyword-based
136
- * `parse` in `parser.ts`.
206
+ * @example Full declaration (with `typeName`)
207
+ * ```ts
208
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral', typeName: 'MyType' })
209
+ * const declaration = printer.print(schemaNode) // ts.TypeAliasDeclaration | ts.InterfaceDeclaration
210
+ * ```
137
211
  */
138
- export const printerTs = definePrinter<TsPrinter>((options) => ({
139
- name: 'typescript',
140
- options,
141
- nodes: {
142
- any: () => factory.keywordTypeNodes.any,
143
- unknown: () => factory.keywordTypeNodes.unknown,
144
- void: () => factory.keywordTypeNodes.void,
145
- boolean: () => factory.keywordTypeNodes.boolean,
146
- null: () => factory.keywordTypeNodes.null,
147
- blob: () => factory.createTypeReferenceNode('Blob', []),
148
- string: () => factory.keywordTypeNodes.string,
149
- uuid: () => factory.keywordTypeNodes.string,
150
- email: () => factory.keywordTypeNodes.string,
151
- url: () => factory.keywordTypeNodes.string,
152
- datetime: () => factory.keywordTypeNodes.string,
153
- number: () => factory.keywordTypeNodes.number,
154
- integer: () => factory.keywordTypeNodes.number,
155
- bigint: () => factory.keywordTypeNodes.bigint,
156
- date: (node) => dateOrStringNode(node),
157
- time: (node) => dateOrStringNode(node),
158
- ref(node) {
159
- if (!node.name) {
160
- return undefined
161
- }
162
- return factory.createTypeReferenceNode(node.name, undefined)
163
- },
164
- enum(node) {
165
- const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
166
-
167
- if (this.options.enumType === 'inlineLiteral' || !node.name) {
168
- const literalNodes = values
169
- .filter((v): v is string | number | boolean => v !== null)
170
- .map((value) => constToTypeNode(value, typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string'))
171
- .filter(Boolean) as Array<ts.TypeNode>
212
+ export const printerTs = definePrinter<TsPrinter>((options) => {
213
+ const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(options.optionalType)
214
+
215
+ return {
216
+ name: 'typescript',
217
+ options,
218
+ nodes: {
219
+ any: () => factory.keywordTypeNodes.any,
220
+ unknown: () => factory.keywordTypeNodes.unknown,
221
+ void: () => factory.keywordTypeNodes.void,
222
+ never: () => factory.keywordTypeNodes.never,
223
+ boolean: () => factory.keywordTypeNodes.boolean,
224
+ null: () => factory.keywordTypeNodes.null,
225
+ blob: () => factory.createTypeReferenceNode('Blob', []),
226
+ string: () => factory.keywordTypeNodes.string,
227
+ uuid: () => factory.keywordTypeNodes.string,
228
+ email: () => factory.keywordTypeNodes.string,
229
+ url: (node) => {
230
+ if (node.path) {
231
+ return factory.createUrlTemplateType(node.path)
232
+ }
233
+ return factory.keywordTypeNodes.string
234
+ },
235
+ datetime: () => factory.keywordTypeNodes.string,
236
+ number: () => factory.keywordTypeNodes.number,
237
+ integer: () => factory.keywordTypeNodes.number,
238
+ bigint: () => factory.keywordTypeNodes.bigint,
239
+ date: dateOrStringNode,
240
+ time: dateOrStringNode,
241
+ ref(node) {
242
+ if (!node.name) {
243
+ return undefined
244
+ }
245
+ // Parser-generated refs (with $ref) carry raw schema names that need resolving.
246
+ // Use the canonical name from the $ref path — node.name may have been overridden
247
+ // (e.g. by single-member allOf flatten using the property-derived child name).
248
+ // Inline refs (without $ref) from utils already carry resolved type names.
249
+ const refName = node.ref ? (node.ref.split('/').at(-1) ?? node.name) : node.name
250
+ const name = node.ref ? this.options.resolver.default(refName, 'type') : refName
251
+
252
+ return factory.createTypeReferenceNode(name, undefined)
253
+ },
254
+ enum(node) {
255
+ const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
256
+
257
+ if (this.options.enumType === 'inlineLiteral' || !node.name) {
258
+ const literalNodes = values
259
+ .filter((v): v is string | number | boolean => v !== null)
260
+ .map((value) => constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
261
+ .filter(Boolean)
262
+
263
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
264
+ }
265
+
266
+ const resolvedName = this.options.resolver.default(node.name, 'type')
267
+ const typeName = ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) ? `${resolvedName}Key` : resolvedName
268
+
269
+ return factory.createTypeReferenceNode(typeName, undefined)
270
+ },
271
+ union(node) {
272
+ const members = node.members ?? []
273
+
274
+ const hasStringLiteral = members.some((m) => {
275
+ const enumNode = narrowSchema(m, schemaTypes.enum)
276
+ return enumNode?.primitive === 'string'
277
+ })
278
+ const hasPlainString = members.some((m) => isStringType(m))
279
+
280
+ if (hasStringLiteral && hasPlainString) {
281
+ const memberNodes = members
282
+ .map((m) => {
283
+ if (isStringType(m)) {
284
+ return factory.createIntersectionDeclaration({
285
+ nodes: [factory.keywordTypeNodes.string, factory.createTypeLiteralNode([])],
286
+ withParentheses: true,
287
+ })
288
+ }
289
+
290
+ return this.print(m)
291
+ })
292
+ .filter(Boolean)
293
+
294
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
295
+ }
296
+
297
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: buildMemberNodes(members, this.print) }) ?? undefined
298
+ },
299
+ intersection(node) {
300
+ return factory.createIntersectionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.print) }) ?? undefined
301
+ },
302
+ array(node) {
303
+ const itemNodes = (node.items ?? []).map((item) => this.print(item)).filter(Boolean)
304
+
305
+ return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined
306
+ },
307
+ tuple(node) {
308
+ return buildTupleNode(node, this.print)
309
+ },
310
+ object(node) {
311
+ const { print, options } = this
312
+ const addsQuestionToken = OPTIONAL_ADDS_QUESTION_TOKEN.has(options.optionalType)
313
+
314
+ const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => {
315
+ const baseType = print(prop.schema) ?? factory.keywordTypeNodes.unknown
316
+ const type = buildPropertyType(prop.schema, baseType, options.optionalType)
317
+
318
+ const propertyNode = factory.createPropertySignature({
319
+ questionToken: prop.schema.optional || prop.schema.nullish ? addsQuestionToken : false,
320
+ name: prop.name,
321
+ type,
322
+ readOnly: prop.schema.readOnly,
323
+ })
324
+
325
+ return factory.appendJSDocToNode({ node: propertyNode, comments: buildPropertyJSDocComments(prop.schema) })
326
+ })
172
327
 
173
- return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
174
- }
328
+ const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, print)]
175
329
 
176
- const typeName = ['asConst', 'asPascalConst'].includes(this.options.enumType) ? `${node.name}Key` : node.name
330
+ if (!allElements.length) {
331
+ return factory.keywordTypeNodes.object
332
+ }
177
333
 
178
- return factory.createTypeReferenceNode(typeName, undefined)
179
- },
180
- union(node) {
181
- return factory.createUnionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.print) }) ?? undefined
334
+ return factory.createTypeLiteralNode(allElements)
335
+ },
182
336
  },
183
- intersection(node) {
184
- return factory.createIntersectionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.print) }) ?? undefined
185
- },
186
- array(node) {
187
- const itemNodes = (node.items ?? []).map((item) => this.print(item)).filter(Boolean) as Array<ts.TypeNode>
337
+ print(node) {
338
+ let type = this.print(node)
188
339
 
189
- return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined
190
- },
191
- tuple(node) {
192
- return buildTupleNode(node, this.print)
193
- },
194
- object(node) {
195
- const addsQuestionToken = ['questionToken', 'questionTokenAndUndefined'].includes(this.options.optionalType)
196
- const { print } = this
197
-
198
- const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => {
199
- const baseType = (print(prop.schema) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode
200
- const type = buildPropertyType(prop.schema, baseType, this.options.optionalType)
201
-
202
- const propertyNode = factory.createPropertySignature({
203
- questionToken: prop.schema.optional || prop.schema.nullish ? addsQuestionToken : false,
204
- name: prop.name,
205
- type,
206
- readOnly: prop.schema.readOnly,
207
- })
340
+ if (!type) {
341
+ return undefined
342
+ }
208
343
 
209
- return factory.appendJSDocToNode({ node: propertyNode, comments: buildPropertyJSDocComments(prop.schema) })
210
- })
344
+ // Apply top-level nullable / optional union modifiers.
345
+ if (node.nullable) {
346
+ type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] })
347
+ }
211
348
 
212
- const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, print)]
349
+ if ((node.nullish || node.optional) && addsUndefined) {
350
+ type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] })
351
+ }
213
352
 
214
- if (!allElements.length) {
215
- return factory.keywordTypeNodes.object
353
+ // Without typeName, return the type node as-is (no declaration wrapping).
354
+ const { typeName, syntaxType = 'type', description, keysToOmit } = this.options
355
+ if (!typeName) {
356
+ return type
216
357
  }
217
358
 
218
- return factory.createTypeLiteralNode(allElements)
359
+ const useTypeGeneration = syntaxType === 'type' || type.kind === factory.syntaxKind.union || !!keysToOmit?.length
360
+
361
+ return factory.createTypeDeclaration({
362
+ name: typeName,
363
+ isExportable: true,
364
+ type: keysToOmit?.length
365
+ ? factory.createOmitDeclaration({
366
+ keys: keysToOmit,
367
+ type,
368
+ nonNullable: true,
369
+ })
370
+ : type,
371
+ syntax: useTypeGeneration ? 'type' : 'interface',
372
+ comments: [
373
+ node?.title ? jsStringEscape(node.title) : undefined,
374
+ description ? `@description ${jsStringEscape(description)}` : undefined,
375
+ node?.deprecated ? '@deprecated' : undefined,
376
+ node && 'min' in node && node.min !== undefined ? `@minLength ${node.min}` : undefined,
377
+ node && 'max' in node && node.max !== undefined ? `@maxLength ${node.max}` : undefined,
378
+ node && 'pattern' in node && node.pattern ? `@pattern ${node.pattern}` : undefined,
379
+ node?.default ? `@default ${node.default}` : undefined,
380
+ node?.example ? `@example ${node.example}` : undefined,
381
+ ],
382
+ })
219
383
  },
220
- },
221
- }))
384
+ }
385
+ })
@@ -0,0 +1,2 @@
1
+ export { resolverTs } from './resolverTs.ts'
2
+ export { resolverTsLegacy } from './resolverTsLegacy.ts'
@@ -0,0 +1,104 @@
1
+ import { pascalCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginTs } from '../types.ts'
4
+
5
+ function resolveName(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
6
+ return pascalCase(name, { isFile: type === 'file' })
7
+ }
8
+
9
+ /**
10
+ * Resolver for `@kubb/plugin-ts` that provides the default naming and path-resolution
11
+ * helpers used by the plugin. Import this in other plugins to resolve the exact names and
12
+ * paths that `plugin-ts` generates without hardcoding the conventions.
13
+ *
14
+ * The `default` method is automatically injected by `defineResolver` — it uses `camelCase`
15
+ * for identifiers/files and `pascalCase` for type names.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { resolver } from '@kubb/plugin-ts'
20
+ *
21
+ * resolver.default('list pets', 'type') // → 'ListPets'
22
+ * resolver.resolveName('list pets status 200') // → 'listPetsStatus200'
23
+ * resolver.resolveTypedName('list pets status 200') // → 'ListPetsStatus200'
24
+ * resolver.resolvePathName('list pets', 'file') // → 'listPets'
25
+ * ```
26
+ */
27
+ export const resolverTs = defineResolver<PluginTs>(() => {
28
+ return {
29
+ name: 'default',
30
+ default(name, type) {
31
+ return resolveName(name, type)
32
+ },
33
+ resolveName(name) {
34
+ return this.default(name, 'function')
35
+ },
36
+ resolveTypedName(name) {
37
+ return this.default(name, 'type')
38
+ },
39
+ resolvePathName(name, type) {
40
+ return this.default(name, type)
41
+ },
42
+ resolveParamName(node, param) {
43
+ return this.resolveName(`${node.operationId} ${this.default(param.in)} ${param.name}`)
44
+ },
45
+ resolveParamTypedName(node, param) {
46
+ return this.resolveTypedName(`${node.operationId} ${this.default(param.in)} ${param.name}`)
47
+ },
48
+ resolveResponseStatusName(node, statusCode) {
49
+ return this.resolveName(`${node.operationId} Status ${statusCode}`)
50
+ },
51
+ resolveResponseStatusTypedName(node, statusCode) {
52
+ return this.resolveTypedName(`${node.operationId} Status ${statusCode}`)
53
+ },
54
+ resolveDataName(node) {
55
+ return this.resolveName(`${node.operationId} Data`)
56
+ },
57
+ resolveDataTypedName(node) {
58
+ return this.resolveTypedName(`${node.operationId} Data`)
59
+ },
60
+ resolveRequestConfigName(node) {
61
+ return this.resolveName(`${node.operationId} RequestConfig`)
62
+ },
63
+ resolveRequestConfigTypedName(node) {
64
+ return this.resolveTypedName(`${node.operationId} RequestConfig`)
65
+ },
66
+ resolveResponsesName(node) {
67
+ return this.resolveName(`${node.operationId} Responses`)
68
+ },
69
+ resolveResponsesTypedName(node) {
70
+ return this.resolveTypedName(`${node.operationId} Responses`)
71
+ },
72
+ resolveResponseName(node) {
73
+ return this.resolveName(`${node.operationId} Response`)
74
+ },
75
+ resolveResponseTypedName(node) {
76
+ return this.resolveTypedName(`${node.operationId} Response`)
77
+ },
78
+ resolveEnumKeyTypedName(node) {
79
+ return `${this.resolveTypedName(node.name ?? '')}Key`
80
+ },
81
+ resolvePathParamsName(_node) {
82
+ throw new Error("resolvePathParamsName is only available with compatibilityPreset: 'kubbV4'. Use resolveParamName per individual parameter instead.")
83
+ },
84
+ resolvePathParamsTypedName(_node) {
85
+ throw new Error(
86
+ "resolvePathParamsTypedName is only available with compatibilityPreset: 'kubbV4'. Use resolveParamTypedName per individual parameter instead.",
87
+ )
88
+ },
89
+ resolveQueryParamsName(node) {
90
+ return this.resolveName(`${node.operationId} QueryParams`)
91
+ },
92
+ resolveQueryParamsTypedName(node) {
93
+ return this.resolveTypedName(`${node.operationId} QueryParams`)
94
+ },
95
+ resolveHeaderParamsName(_node) {
96
+ throw new Error("resolveHeaderParamsName is only available with compatibilityPreset: 'kubbV4'. Use resolveParamName per individual parameter instead.")
97
+ },
98
+ resolveHeaderParamsTypedName(_node) {
99
+ throw new Error(
100
+ "resolveHeaderParamsTypedName is only available with compatibilityPreset: 'kubbV4'. Use resolveParamTypedName per individual parameter instead.",
101
+ )
102
+ },
103
+ }
104
+ })
@@ -0,0 +1,87 @@
1
+ import { defineResolver } from '@kubb/core'
2
+ import type { PluginTs } from '../types.ts'
3
+ import { resolverTs } from './resolverTs.ts'
4
+
5
+ /**
6
+ * Legacy resolver for `@kubb/plugin-ts` that reproduces the naming conventions
7
+ * used before the v2 resolver refactor. Enable via `compatibilityPreset: 'kubbV4'`
8
+ * (or by composing this resolver manually).
9
+ *
10
+ * Key differences from the default resolver:
11
+ * - Response status types: `<OperationId><StatusCode>` (e.g. `CreatePets201`) instead of `<OperationId>Status201`
12
+ * - Default/error responses: `<OperationId>Error` instead of `<OperationId>StatusDefault`
13
+ * - Request body: `<OperationId>MutationRequest` (non-GET) / `<OperationId>QueryRequest` (GET)
14
+ * - Combined responses type: `<OperationId>Mutation` / `<OperationId>Query`
15
+ * - Response union: `<OperationId>MutationResponse` / `<OperationId>QueryResponse`
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { resolverTsLegacy } from '@kubb/plugin-ts'
20
+ *
21
+ * resolverTsLegacy.resolveResponseStatusTypedName(node, 201) // → 'CreatePets201'
22
+ * resolverTsLegacy.resolveResponseStatusTypedName(node, 'default') // → 'CreatePetsError'
23
+ * resolverTsLegacy.resolveDataTypedName(node) // → 'CreatePetsMutationRequest' (POST)
24
+ * resolverTsLegacy.resolveResponsesTypedName(node) // → 'CreatePetsMutation' (POST)
25
+ * resolverTsLegacy.resolveResponseTypedName(node) // → 'CreatePetsMutationResponse' (POST)
26
+ * ```
27
+ */
28
+ export const resolverTsLegacy = defineResolver<PluginTs>(() => {
29
+ return {
30
+ ...resolverTs,
31
+ name: 'legacy',
32
+ resolveResponseStatusName(node, statusCode) {
33
+ if (statusCode === 'default') {
34
+ return this.resolveName(`${node.operationId} Error`)
35
+ }
36
+ return this.resolveName(`${node.operationId} ${statusCode}`)
37
+ },
38
+ resolveResponseStatusTypedName(node, statusCode) {
39
+ if (statusCode === 'default') {
40
+ return this.resolveTypedName(`${node.operationId} Error`)
41
+ }
42
+ return this.resolveTypedName(`${node.operationId} ${statusCode}`)
43
+ },
44
+ resolveDataName(node) {
45
+ const suffix = node.method === 'GET' ? 'QueryRequest' : 'MutationRequest'
46
+ return this.resolveName(`${node.operationId} ${suffix}`)
47
+ },
48
+ resolveDataTypedName(node) {
49
+ const suffix = node.method === 'GET' ? 'QueryRequest' : 'MutationRequest'
50
+ return this.resolveTypedName(`${node.operationId} ${suffix}`)
51
+ },
52
+ resolveResponsesName(node) {
53
+ const suffix = node.method === 'GET' ? 'Query' : 'Mutation'
54
+ return `${this.default(node.operationId, 'function')}${suffix}`
55
+ },
56
+ resolveResponsesTypedName(node) {
57
+ const suffix = node.method === 'GET' ? 'Query' : 'Mutation'
58
+ return `${this.default(node.operationId, 'type')}${suffix}`
59
+ },
60
+ resolveResponseName(node) {
61
+ const suffix = node.method === 'GET' ? 'QueryResponse' : 'MutationResponse'
62
+ return this.resolveName(`${node.operationId} ${suffix}`)
63
+ },
64
+ resolveResponseTypedName(node) {
65
+ const suffix = node.method === 'GET' ? 'QueryResponse' : 'MutationResponse'
66
+ return this.resolveTypedName(`${node.operationId} ${suffix}`)
67
+ },
68
+ resolvePathParamsName(node) {
69
+ return this.resolveName(`${node.operationId} PathParams`)
70
+ },
71
+ resolvePathParamsTypedName(node) {
72
+ return this.resolveTypedName(`${node.operationId} PathParams`)
73
+ },
74
+ resolveQueryParamsName(node) {
75
+ return this.resolveName(`${node.operationId} QueryParams`)
76
+ },
77
+ resolveQueryParamsTypedName(node) {
78
+ return this.resolveTypedName(`${node.operationId} QueryParams`)
79
+ },
80
+ resolveHeaderParamsName(node) {
81
+ return this.resolveName(`${node.operationId} HeaderParams`)
82
+ },
83
+ resolveHeaderParamsTypedName(node) {
84
+ return this.resolveTypedName(`${node.operationId} HeaderParams`)
85
+ },
86
+ }
87
+ })