@kubb/plugin-ts 5.0.0-alpha.9 → 5.0.0-beta.10

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 (44) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +26 -7
  3. package/dist/index.cjs +1526 -5
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.ts +558 -27
  6. package/dist/index.js +1488 -2
  7. package/dist/index.js.map +1 -0
  8. package/extension.yaml +632 -0
  9. package/package.json +43 -65
  10. package/src/components/{v2/Enum.tsx → Enum.tsx} +33 -17
  11. package/src/components/Type.tsx +31 -161
  12. package/src/constants.ts +10 -0
  13. package/src/factory.ts +295 -39
  14. package/src/generators/typeGenerator.tsx +248 -420
  15. package/src/index.ts +9 -3
  16. package/src/plugin.ts +66 -205
  17. package/src/printers/functionPrinter.ts +197 -0
  18. package/src/printers/printerTs.ts +329 -0
  19. package/src/resolvers/resolverTs.ts +66 -0
  20. package/src/types.ts +233 -221
  21. package/src/utils.ts +129 -0
  22. package/dist/components-CRu8IKY3.js +0 -729
  23. package/dist/components-CRu8IKY3.js.map +0 -1
  24. package/dist/components-DeNDKlzf.cjs +0 -982
  25. package/dist/components-DeNDKlzf.cjs.map +0 -1
  26. package/dist/components.cjs +0 -3
  27. package/dist/components.d.ts +0 -36
  28. package/dist/components.js +0 -2
  29. package/dist/generators.cjs +0 -4
  30. package/dist/generators.d.ts +0 -509
  31. package/dist/generators.js +0 -2
  32. package/dist/plugin-BZkBwnEA.js +0 -1269
  33. package/dist/plugin-BZkBwnEA.js.map +0 -1
  34. package/dist/plugin-Bunz1oGa.cjs +0 -1322
  35. package/dist/plugin-Bunz1oGa.cjs.map +0 -1
  36. package/dist/types-mSXmB8WU.d.ts +0 -298
  37. package/src/components/index.ts +0 -1
  38. package/src/components/v2/Type.tsx +0 -59
  39. package/src/generators/index.ts +0 -2
  40. package/src/generators/v2/typeGenerator.tsx +0 -167
  41. package/src/generators/v2/utils.ts +0 -140
  42. package/src/parser.ts +0 -389
  43. package/src/printer.ts +0 -368
  44. package/src/resolverTs.ts +0 -77
@@ -0,0 +1,329 @@
1
+ import { ast } from '@kubb/core'
2
+ import { safePrint } from '@kubb/parser-ts'
3
+ import type ts from 'typescript'
4
+ import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from '../constants.ts'
5
+ import * as factory from '../factory.ts'
6
+ import type { PluginTs, ResolverTs } from '../types.ts'
7
+ import { buildPropertyJSDocComments } from '../utils.ts'
8
+
9
+ function isNonNullable<T>(value: T | null | undefined): value is T {
10
+ return value !== null && value !== undefined
11
+ }
12
+
13
+ /**
14
+ * Partial map of node-type overrides for the TypeScript printer.
15
+ *
16
+ * Each key is a `SchemaType` string (e.g. `'date'`, `'string'`). The function
17
+ * replaces the built-in handler for that node type. Use `this.transform` to
18
+ * recurse into nested schema nodes, and `this.options` to read printer options.
19
+ *
20
+ * @example Override the `date` handler
21
+ * ```ts
22
+ * pluginTs({
23
+ * printer: {
24
+ * nodes: {
25
+ * date(node) {
26
+ * return ts.factory.createTypeReferenceNode('Date', [])
27
+ * },
28
+ * },
29
+ * },
30
+ * })
31
+ * ```
32
+ */
33
+ export type PrinterTsNodes = ast.PrinterPartial<ts.TypeNode, PrinterTsOptions>
34
+
35
+ export type PrinterTsOptions = {
36
+ /**
37
+ * Mark parameters as optional with `?` or `| undefined`.
38
+ * - `'questionToken'` adds `?` to properties
39
+ * - `'undefined'` adds `| undefined` to types
40
+ *
41
+ * @default `'questionToken'`
42
+ */
43
+ optionalType: PluginTs['resolvedOptions']['optionalType']
44
+ /**
45
+ * Array representation style.
46
+ * - `'array'` uses bracket notation (`T[]`)
47
+ * - `'generic'` uses generic syntax (`Array<T>`)
48
+ *
49
+ * @default `'array'`
50
+ */
51
+ arrayType: PluginTs['resolvedOptions']['arrayType']
52
+ /**
53
+ * Enum output format.
54
+ * - `'inlineLiteral'` embeds literal unions inline
55
+ * - `'asPascalConst'` generates named const unions
56
+ * - `'asConst'` generates as const declarations
57
+ *
58
+ * @default `'inlineLiteral'`
59
+ */
60
+ enumType: PluginTs['resolvedOptions']['enumType']
61
+ /**
62
+ * Suffix appended to enum key reference names.
63
+ *
64
+ * @example Enum key naming
65
+ * `StatusKey` when `enumType` is `'asConst'`
66
+ *
67
+ * @default `'Key'`
68
+ */
69
+ enumTypeSuffix?: PluginTs['resolvedOptions']['enumTypeSuffix']
70
+ /**
71
+ * Syntax for generated declarations.
72
+ * - `'type'` generates type aliases
73
+ * - `'interface'` generates interface declarations
74
+ *
75
+ * @default `'type'`
76
+ */
77
+ syntaxType?: PluginTs['resolvedOptions']['syntaxType']
78
+ /**
79
+ * Exported name for the type declaration.
80
+ * When omitted, returns only the raw type node.
81
+ */
82
+ name?: string
83
+
84
+ /**
85
+ * JSDoc comment to attach to the generated type.
86
+ */
87
+ description?: string
88
+ /**
89
+ * Properties to exclude using `Omit<Type, Keys>`.
90
+ * Forces type alias syntax regardless of `syntaxType` setting.
91
+ */
92
+ keysToOmit?: Array<string>
93
+ /**
94
+ * Transforms raw schema names into valid TypeScript identifiers.
95
+ */
96
+ resolver: ResolverTs
97
+ /**
98
+ * Schema names that represent enums for suffixed key references.
99
+ */
100
+ enumSchemaNames?: Set<string>
101
+ /**
102
+ * Custom handler map for node type overrides.
103
+ */
104
+ nodes?: PrinterTsNodes
105
+ }
106
+
107
+ /**
108
+ * TypeScript printer factory options: maps `SchemaNode` → `ts.TypeNode` (raw) or `ts.Node` (full declaration).
109
+ */
110
+ export type PrinterTsFactory = ast.PrinterFactoryOptions<'typescript', PrinterTsOptions, ts.TypeNode, string>
111
+
112
+ type PrinterTs = PrinterTsFactory
113
+
114
+ /**
115
+ * TypeScript type printer built with `definePrinter`.
116
+ *
117
+ * Converts a `SchemaNode` AST node into a TypeScript AST node:
118
+ * - **`printer.print(node)`** — when `options.typeName` is set, returns a full
119
+ * `type Name = …` or `interface Name { … }` declaration (`ts.Node`).
120
+ * Without `typeName`, returns the raw `ts.TypeNode` for the schema.
121
+ *
122
+ * Dispatches on `node.type` to the appropriate handler in `nodes`. Options are closed
123
+ * over per printer instance, so each call to `printerTs(options)` produces an independent printer.
124
+ *
125
+ * @example Raw type node (no `typeName`)
126
+ * ```ts
127
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral' })
128
+ * const typeNode = printer.print(schemaNode) // ts.TypeNode
129
+ * ```
130
+ *
131
+ * @example Full declaration (with `typeName`)
132
+ * ```ts
133
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral', typeName: 'MyType' })
134
+ * const declaration = printer.print(schemaNode) // ts.TypeAliasDeclaration | ts.InterfaceDeclaration
135
+ * ```
136
+ */
137
+ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
138
+ const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(options.optionalType)
139
+
140
+ return {
141
+ name: 'typescript',
142
+ options,
143
+ nodes: {
144
+ any: () => factory.keywordTypeNodes.any,
145
+ unknown: () => factory.keywordTypeNodes.unknown,
146
+ void: () => factory.keywordTypeNodes.void,
147
+ never: () => factory.keywordTypeNodes.never,
148
+ boolean: () => factory.keywordTypeNodes.boolean,
149
+ null: () => factory.keywordTypeNodes.null,
150
+ blob: () => factory.createTypeReferenceNode('Blob', []),
151
+ string: () => factory.keywordTypeNodes.string,
152
+ uuid: () => factory.keywordTypeNodes.string,
153
+ email: () => factory.keywordTypeNodes.string,
154
+ url: (node) => {
155
+ if (node.path) {
156
+ return factory.createUrlTemplateType(node.path)
157
+ }
158
+ return factory.keywordTypeNodes.string
159
+ },
160
+ ipv4: () => factory.keywordTypeNodes.string,
161
+ ipv6: () => factory.keywordTypeNodes.string,
162
+ datetime: () => factory.keywordTypeNodes.string,
163
+ number: () => factory.keywordTypeNodes.number,
164
+ integer: () => factory.keywordTypeNodes.number,
165
+ bigint: () => factory.keywordTypeNodes.bigint,
166
+ date: factory.dateOrStringNode,
167
+ time: factory.dateOrStringNode,
168
+ ref(node) {
169
+ if (!node.name) {
170
+ return undefined
171
+ }
172
+ // Parser-generated refs (with $ref) carry raw schema names that need resolving.
173
+ // Use the canonical name from the $ref path — node.name may have been overridden
174
+ // (e.g. by single-member allOf flatten using the property-derived child name).
175
+ // Inline refs (without $ref) from utils already carry resolved type names.
176
+ const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
177
+
178
+ // When a Key suffix is configured, enum refs must use the suffixed name (e.g. `StatusKey`)
179
+ // so the reference matches what the enum file actually exports.
180
+ const isEnumRef =
181
+ node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix && this.options.enumSchemaNames?.has(refName)
182
+
183
+ const name = isEnumRef
184
+ ? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.enumTypeSuffix!)
185
+ : node.ref
186
+ ? this.options.resolver.default(refName, 'type')
187
+ : refName
188
+
189
+ return factory.createTypeReferenceNode(name, undefined)
190
+ },
191
+ enum(node) {
192
+ const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
193
+
194
+ if (this.options.enumType === 'inlineLiteral' || !node.name) {
195
+ const literalNodes = values
196
+ .filter((v): v is string | number | boolean => v !== null && v !== undefined)
197
+ .map((value) => factory.constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
198
+ .filter(isNonNullable)
199
+
200
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
201
+ }
202
+
203
+ const resolvedName =
204
+ ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix
205
+ ? this.options.resolver.resolveEnumKeyName(node, this.options.enumTypeSuffix)
206
+ : this.options.resolver.default(node.name, 'type')
207
+
208
+ return factory.createTypeReferenceNode(resolvedName, undefined)
209
+ },
210
+ union(node) {
211
+ const members = node.members ?? []
212
+
213
+ const hasStringLiteral = members.some((m) => {
214
+ const enumNode = ast.narrowSchema(m, ast.schemaTypes.enum)
215
+ return enumNode?.primitive === 'string'
216
+ })
217
+ const hasPlainString = members.some((m) => ast.isStringType(m))
218
+
219
+ if (hasStringLiteral && hasPlainString) {
220
+ const memberNodes = members
221
+ .map((m) => {
222
+ if (ast.isStringType(m)) {
223
+ return factory.createIntersectionDeclaration({
224
+ nodes: [factory.keywordTypeNodes.string, factory.createTypeLiteralNode([])],
225
+ withParentheses: true,
226
+ })
227
+ }
228
+
229
+ return this.transform(m)
230
+ })
231
+ .filter(isNonNullable)
232
+
233
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
234
+ }
235
+
236
+ return factory.createUnionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(members, this.transform) }) ?? undefined
237
+ },
238
+ intersection(node) {
239
+ return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? undefined
240
+ },
241
+ array(node) {
242
+ const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable)
243
+
244
+ return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined
245
+ },
246
+ tuple(node) {
247
+ return factory.buildTupleNode(node, this.transform)
248
+ },
249
+ object(node) {
250
+ const { transform, options } = this
251
+
252
+ const addsQuestionToken = OPTIONAL_ADDS_QUESTION_TOKEN.has(options.optionalType)
253
+
254
+ const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => {
255
+ const baseType = transform(prop.schema) ?? factory.keywordTypeNodes.unknown
256
+ const type = factory.buildPropertyType(prop.schema, baseType, options.optionalType)
257
+ const propMeta = ast.syncSchemaRef(prop.schema)
258
+
259
+ const propertyNode = factory.createPropertySignature({
260
+ questionToken: prop.schema.optional || prop.schema.nullish ? addsQuestionToken : false,
261
+ name: prop.name,
262
+ type,
263
+ readOnly: propMeta?.readOnly,
264
+ })
265
+
266
+ return factory.appendJSDocToNode({ node: propertyNode, comments: buildPropertyJSDocComments(prop.schema) })
267
+ })
268
+
269
+ const allElements = [...propertyNodes, ...factory.buildIndexSignatures(node, propertyNodes.length, transform)]
270
+
271
+ if (!allElements.length) {
272
+ return factory.keywordTypeNodes.object
273
+ }
274
+
275
+ return factory.createTypeLiteralNode(allElements)
276
+ },
277
+ ...options.nodes,
278
+ },
279
+ print(node) {
280
+ const { name, syntaxType = 'type', description, keysToOmit } = this.options
281
+
282
+ let base = this.transform(node)
283
+ if (!base) return null
284
+
285
+ // For ref nodes, structural metadata lives on node.schema rather than the ref node itself.
286
+ const meta = ast.syncSchemaRef(node)
287
+
288
+ // Without name, apply modifiers inline and return.
289
+ if (!name) {
290
+ if (meta.nullable) {
291
+ base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.null] })
292
+ }
293
+ if ((meta.nullish || meta.optional) && addsUndefined) {
294
+ base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.undefined] })
295
+ }
296
+ return safePrint(base)
297
+ }
298
+
299
+ // When keysToOmit is present, wrap with Omit first, then apply nullable/optional
300
+ // modifiers so they are not swallowed by NonNullable inside createOmitDeclaration.
301
+ let inner: ts.TypeNode = keysToOmit?.length ? factory.createOmitDeclaration({ keys: keysToOmit, type: base, nonNullable: true }) : base
302
+
303
+ if (meta.nullable) {
304
+ inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.null] })
305
+ }
306
+
307
+ // For named type declarations (type aliases), optional/nullish always produces | undefined
308
+ // regardless of optionalType — the questionToken ? modifier only applies to object properties.
309
+ if (meta.nullish || meta.optional) {
310
+ inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.undefined] })
311
+ }
312
+
313
+ const useTypeGeneration = syntaxType === 'type' || inner.kind === factory.syntaxKind.union || !!keysToOmit?.length
314
+
315
+ const typeNode = factory.createTypeDeclaration({
316
+ name,
317
+ isExportable: true,
318
+ type: inner,
319
+ syntax: useTypeGeneration ? 'type' : 'interface',
320
+ comments: buildPropertyJSDocComments({
321
+ ...meta,
322
+ description,
323
+ }),
324
+ })
325
+
326
+ return safePrint(typeNode)
327
+ },
328
+ }
329
+ })
@@ -0,0 +1,66 @@
1
+ import { pascalCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginTs } from '../types.ts'
4
+
5
+ /**
6
+ * Resolver for `@kubb/plugin-ts` that provides the default naming and path-resolution
7
+ * helpers used by the plugin. Import this in other plugins to resolve the exact names and
8
+ * paths that `plugin-ts` generates without hardcoding the conventions.
9
+ *
10
+ * The `default` method is automatically injected by `defineResolver` — it uses `camelCase`
11
+ * for identifiers/files and `pascalCase` for type names.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { resolver } from '@kubb/plugin-ts'
16
+ *
17
+ * resolver.default('list pets', 'type') // → 'ListPets'
18
+ * resolver.resolveName('list pets status 200') // → 'ListPetsStatus200'
19
+ * resolver.resolvePathName('list pets', 'file') // → 'listPets'
20
+ * ```
21
+ */
22
+ export const resolverTs = defineResolver<PluginTs>(() => {
23
+ return {
24
+ name: 'default',
25
+ pluginName: 'plugin-ts',
26
+ default(name, type) {
27
+ return pascalCase(name, { isFile: type === 'file' })
28
+ },
29
+ resolveTypeName(name) {
30
+ return pascalCase(name)
31
+ },
32
+ resolvePathName(name, type) {
33
+ return pascalCase(name, { isFile: type === 'file' })
34
+ },
35
+ resolveParamName(node, param) {
36
+ return this.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`)
37
+ },
38
+ resolveResponseStatusName(node, statusCode) {
39
+ return this.resolveTypeName(`${node.operationId} Status ${statusCode}`)
40
+ },
41
+ resolveDataName(node) {
42
+ return this.resolveTypeName(`${node.operationId} Data`)
43
+ },
44
+ resolveRequestConfigName(node) {
45
+ return this.resolveTypeName(`${node.operationId} RequestConfig`)
46
+ },
47
+ resolveResponsesName(node) {
48
+ return this.resolveTypeName(`${node.operationId} Responses`)
49
+ },
50
+ resolveResponseName(node) {
51
+ return this.resolveTypeName(`${node.operationId} Response`)
52
+ },
53
+ resolveEnumKeyName(node, enumTypeSuffix = 'key') {
54
+ return `${this.resolveTypeName(node.name ?? '')}${enumTypeSuffix}`
55
+ },
56
+ resolvePathParamsName(node, param) {
57
+ return this.resolveParamName(node, param)
58
+ },
59
+ resolveQueryParamsName(node, param) {
60
+ return this.resolveParamName(node, param)
61
+ },
62
+ resolveHeaderParamsName(node, param) {
63
+ return this.resolveParamName(node, param)
64
+ },
65
+ }
66
+ })