@kubb/plugin-ts 5.0.0-beta.3 → 5.0.0-beta.30
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/README.md +26 -5
- package/dist/index.cjs +343 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +81 -58
- package/dist/index.js +342 -110
- package/dist/index.js.map +1 -1
- package/extension.yaml +1078 -0
- package/package.json +11 -13
- package/src/components/Enum.tsx +3 -3
- package/src/factory.ts +23 -20
- package/src/generators/typeGenerator.tsx +101 -29
- package/src/plugin.ts +18 -9
- package/src/printers/printerTs.ts +31 -30
- package/src/resolvers/resolverTs.ts +28 -25
- package/src/types.ts +44 -37
- package/src/utils.ts +23 -13
package/package.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-ts",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "TypeScript
|
|
3
|
+
"version": "5.0.0-beta.30",
|
|
4
|
+
"description": "Generate TypeScript types, interfaces, and enums from your OpenAPI specification. The foundational plugin that powers type safety across the entire Kubb ecosystem.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"code-
|
|
6
|
+
"code-generation",
|
|
7
7
|
"codegen",
|
|
8
|
+
"enums",
|
|
8
9
|
"interfaces",
|
|
9
10
|
"kubb",
|
|
10
|
-
"oas",
|
|
11
11
|
"openapi",
|
|
12
|
-
"plugins",
|
|
13
12
|
"swagger",
|
|
14
|
-
"type-definitions",
|
|
15
13
|
"type-generation",
|
|
16
|
-
"type-safe",
|
|
17
14
|
"types",
|
|
18
15
|
"typescript"
|
|
19
16
|
],
|
|
@@ -27,7 +24,7 @@
|
|
|
27
24
|
"files": [
|
|
28
25
|
"src",
|
|
29
26
|
"dist",
|
|
30
|
-
"
|
|
27
|
+
"extension.yaml",
|
|
31
28
|
"!/**/**.test.**",
|
|
32
29
|
"!/**/__tests__/**",
|
|
33
30
|
"!/**/__snapshots__/**"
|
|
@@ -49,17 +46,18 @@
|
|
|
49
46
|
"registry": "https://registry.npmjs.org/"
|
|
50
47
|
},
|
|
51
48
|
"dependencies": {
|
|
52
|
-
"@kubb/core": "5.0.0-beta.
|
|
53
|
-
"@kubb/parser-ts": "5.0.0-beta.
|
|
54
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
55
|
-
"remeda": "^2.34.
|
|
49
|
+
"@kubb/core": "5.0.0-beta.29",
|
|
50
|
+
"@kubb/parser-ts": "5.0.0-beta.29",
|
|
51
|
+
"@kubb/renderer-jsx": "5.0.0-beta.29",
|
|
52
|
+
"remeda": "^2.34.1",
|
|
56
53
|
"typescript": "^6.0.3"
|
|
57
54
|
},
|
|
58
55
|
"devDependencies": {
|
|
56
|
+
"@internals/shared": "0.0.0",
|
|
59
57
|
"@internals/utils": "0.0.0"
|
|
60
58
|
},
|
|
61
59
|
"peerDependencies": {
|
|
62
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
60
|
+
"@kubb/renderer-jsx": "5.0.0-beta.29"
|
|
63
61
|
},
|
|
64
62
|
"size-limit": [
|
|
65
63
|
{
|
package/src/components/Enum.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { camelCase, trimQuotes } from '@internals/utils'
|
|
2
2
|
import type { ast } from '@kubb/core'
|
|
3
|
-
import {
|
|
3
|
+
import { parserTs } from '@kubb/parser-ts'
|
|
4
4
|
import { File } from '@kubb/renderer-jsx'
|
|
5
5
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
6
6
|
import { ENUM_TYPES_WITH_KEY_SUFFIX, ENUM_TYPES_WITH_RUNTIME_VALUE, ENUM_TYPES_WITH_TYPE_ONLY } from '../constants.ts'
|
|
@@ -72,11 +72,11 @@ export function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }
|
|
|
72
72
|
<>
|
|
73
73
|
{nameNode && (
|
|
74
74
|
<File.Source name={enumName} isExportable isIndexable isTypeOnly={false}>
|
|
75
|
-
{
|
|
75
|
+
{parserTs.print(nameNode)}
|
|
76
76
|
</File.Source>
|
|
77
77
|
)}
|
|
78
78
|
<File.Source name={typeName} isIndexable isExportable={ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumType)} isTypeOnly={ENUM_TYPES_WITH_TYPE_ONLY.has(enumType)}>
|
|
79
|
-
{
|
|
79
|
+
{parserTs.print(typeNode)}
|
|
80
80
|
</File.Source>
|
|
81
81
|
</>
|
|
82
82
|
)
|
package/src/factory.ts
CHANGED
|
@@ -27,6 +27,10 @@ export const syntaxKind = {
|
|
|
27
27
|
stringLiteral: SyntaxKind.StringLiteral,
|
|
28
28
|
} as const
|
|
29
29
|
|
|
30
|
+
function isNonNullable<T>(value: T | null | undefined): value is T {
|
|
31
|
+
return value !== null && value !== undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function isValidIdentifier(str: string): boolean {
|
|
31
35
|
if (!str.length || str.trim() !== str) {
|
|
32
36
|
return false
|
|
@@ -158,7 +162,9 @@ export function createPropertySignature({
|
|
|
158
162
|
type?: ts.TypeNode
|
|
159
163
|
}) {
|
|
160
164
|
return factory.createPropertySignature(
|
|
161
|
-
[...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
|
|
165
|
+
[...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
|
|
166
|
+
(modifier): modifier is ts.Modifier => modifier !== undefined,
|
|
167
|
+
),
|
|
162
168
|
propertyName(name),
|
|
163
169
|
createQuestionToken(questionToken),
|
|
164
170
|
type,
|
|
@@ -192,7 +198,7 @@ export function createParameterSignature(
|
|
|
192
198
|
* Creates a JSDoc comment node from an array of comment strings.
|
|
193
199
|
* Returns null if no comments are provided.
|
|
194
200
|
*/
|
|
195
|
-
export function createJSDoc({ comments }: { comments: string
|
|
201
|
+
export function createJSDoc({ comments }: { comments: Array<string> }) {
|
|
196
202
|
if (!comments.length) {
|
|
197
203
|
return null
|
|
198
204
|
}
|
|
@@ -332,7 +338,7 @@ export function createTypeDeclaration({
|
|
|
332
338
|
/**
|
|
333
339
|
* Creates a TypeScript namespace declaration (exported module).
|
|
334
340
|
*/
|
|
335
|
-
export function createNamespaceDeclaration({ statements, name }: { name: string; statements: ts.Statement
|
|
341
|
+
export function createNamespaceDeclaration({ statements, name }: { name: string; statements: Array<ts.Statement> }) {
|
|
336
342
|
return factory.createModuleDeclaration(
|
|
337
343
|
[factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
338
344
|
factory.createIdentifier(name),
|
|
@@ -366,13 +372,8 @@ export function createImportDeclaration({
|
|
|
366
372
|
isNameSpace?: boolean
|
|
367
373
|
}) {
|
|
368
374
|
if (!Array.isArray(name)) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (isNameSpace) {
|
|
373
|
-
importPropertyName = undefined
|
|
374
|
-
importName = factory.createNamespaceImport(factory.createIdentifier(name))
|
|
375
|
-
}
|
|
375
|
+
const importPropertyName = isNameSpace ? undefined : factory.createIdentifier(name)
|
|
376
|
+
const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : undefined
|
|
376
377
|
|
|
377
378
|
return factory.createImportDeclaration(
|
|
378
379
|
undefined,
|
|
@@ -518,7 +519,7 @@ export function createEnumDeclaration({
|
|
|
518
519
|
* Enum name in PascalCase.
|
|
519
520
|
*/
|
|
520
521
|
typeName: string
|
|
521
|
-
enums: [key: string | number, value: string | number | boolean]
|
|
522
|
+
enums: Array<[key: string | number, value: string | number | boolean]>
|
|
522
523
|
/**
|
|
523
524
|
* Choose the casing for enum key names.
|
|
524
525
|
* @default 'none'
|
|
@@ -553,7 +554,7 @@ export function createEnumDeclaration({
|
|
|
553
554
|
|
|
554
555
|
return undefined
|
|
555
556
|
})
|
|
556
|
-
.filter(
|
|
557
|
+
.filter((node): node is ts.LiteralTypeNode => node !== undefined),
|
|
557
558
|
),
|
|
558
559
|
),
|
|
559
560
|
]
|
|
@@ -563,7 +564,9 @@ export function createEnumDeclaration({
|
|
|
563
564
|
return [
|
|
564
565
|
undefined,
|
|
565
566
|
factory.createEnumDeclaration(
|
|
566
|
-
[factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
|
|
567
|
+
[factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
|
|
568
|
+
(modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
|
|
569
|
+
),
|
|
567
570
|
factory.createIdentifier(typeName),
|
|
568
571
|
enums
|
|
569
572
|
.map(([key, value]) => {
|
|
@@ -594,7 +597,7 @@ export function createEnumDeclaration({
|
|
|
594
597
|
|
|
595
598
|
return undefined
|
|
596
599
|
})
|
|
597
|
-
.filter(
|
|
600
|
+
.filter((member): member is ts.EnumMember => member !== undefined),
|
|
598
601
|
),
|
|
599
602
|
]
|
|
600
603
|
}
|
|
@@ -657,7 +660,7 @@ export function createEnumDeclaration({
|
|
|
657
660
|
|
|
658
661
|
return undefined
|
|
659
662
|
})
|
|
660
|
-
.filter(
|
|
663
|
+
.filter((property): property is ts.PropertyAssignment => property !== undefined),
|
|
661
664
|
true,
|
|
662
665
|
),
|
|
663
666
|
factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
|
|
@@ -737,8 +740,8 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
|
|
|
737
740
|
}
|
|
738
741
|
|
|
739
742
|
const segments = normalized.split(/(\{[^}]+\})/)
|
|
740
|
-
const parts: string
|
|
741
|
-
const parameterIndices: number
|
|
743
|
+
const parts: Array<string> = []
|
|
744
|
+
const parameterIndices: Array<number> = []
|
|
742
745
|
|
|
743
746
|
segments.forEach((segment) => {
|
|
744
747
|
if (segment.startsWith('{') && segment.endsWith('}')) {
|
|
@@ -750,7 +753,7 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
|
|
|
750
753
|
})
|
|
751
754
|
|
|
752
755
|
const head = ts.factory.createTemplateHead(parts[0] || '')
|
|
753
|
-
const templateSpans: ts.TemplateLiteralTypeSpan
|
|
756
|
+
const templateSpans: Array<ts.TemplateLiteralTypeSpan> = []
|
|
754
757
|
|
|
755
758
|
parameterIndices.forEach((paramIndex, i) => {
|
|
756
759
|
const isLast = i === parameterIndices.length - 1
|
|
@@ -885,7 +888,7 @@ export function buildMemberNodes(
|
|
|
885
888
|
members: Array<ast.SchemaNode> | undefined,
|
|
886
889
|
print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
|
|
887
890
|
): Array<ts.TypeNode> {
|
|
888
|
-
return (members ?? []).map(print).filter(
|
|
891
|
+
return (members ?? []).map(print).filter(isNonNullable)
|
|
889
892
|
}
|
|
890
893
|
|
|
891
894
|
/**
|
|
@@ -893,7 +896,7 @@ export function buildMemberNodes(
|
|
|
893
896
|
* applying min/max slice and optional/rest element rules.
|
|
894
897
|
*/
|
|
895
898
|
export function buildTupleNode(node: ast.ArraySchemaNode, print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
|
|
896
|
-
let items = (node.items ?? []).map(print).filter(
|
|
899
|
+
let items = (node.items ?? []).map(print).filter(isNonNullable)
|
|
897
900
|
|
|
898
901
|
const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
|
|
899
902
|
const { min, max } = node
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { ast, defineGenerator } from '@kubb/core'
|
|
2
|
-
import { File,
|
|
2
|
+
import { File, jsxRendererSync } from '@kubb/renderer-jsx'
|
|
3
3
|
import { Type } from '../components/Type.tsx'
|
|
4
4
|
import { ENUM_TYPES_WITH_KEY_SUFFIX } from '../constants.ts'
|
|
5
5
|
import { printerTs } from '../printers/printerTs.ts'
|
|
6
6
|
import type { PluginTs } from '../types'
|
|
7
7
|
import { buildData, buildResponses, buildResponseUnion } from '../utils.ts'
|
|
8
8
|
|
|
9
|
+
function getContentTypeSuffix(contentType: string): string {
|
|
10
|
+
const baseType = contentType.split(';')[0]!.trim()
|
|
11
|
+
if (baseType === 'application/json') return 'Json'
|
|
12
|
+
if (baseType === 'multipart/form-data') return 'FormData'
|
|
13
|
+
if (baseType === 'application/x-www-form-urlencoded') return 'FormUrlEncoded'
|
|
14
|
+
const subtype = baseType.split('/').pop() ?? baseType
|
|
15
|
+
const parts = subtype.split(/[^a-zA-Z0-9]+/).filter(Boolean)
|
|
16
|
+
if (parts.length === 0) return 'Unknown'
|
|
17
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getPerContentTypeName(dataName: string, suffix: string): string {
|
|
21
|
+
if (dataName.endsWith('Data')) {
|
|
22
|
+
return suffix.endsWith('Data') ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`
|
|
23
|
+
}
|
|
24
|
+
return dataName + suffix
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
|
|
29
|
+
* schema in the spec plus per-operation request, response, and parameter
|
|
30
|
+
* types. Drop-replace with a custom `Generator<PluginTs>` to change how
|
|
31
|
+
* TypeScript output is produced.
|
|
32
|
+
*/
|
|
9
33
|
export const typeGenerator = defineGenerator<PluginTs>({
|
|
10
34
|
name: 'typescript',
|
|
11
|
-
renderer:
|
|
35
|
+
renderer: jsxRendererSync,
|
|
12
36
|
schema(node, ctx) {
|
|
13
37
|
const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options
|
|
14
38
|
const { adapter, config, resolver, root } = ctx
|
|
@@ -19,7 +43,7 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
19
43
|
const mode = ctx.getMode(output)
|
|
20
44
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
21
45
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
22
|
-
const enumSchemaNames = new Set(
|
|
46
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
23
47
|
|
|
24
48
|
function resolveImportName(schemaName: string): string {
|
|
25
49
|
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
|
|
@@ -30,14 +54,14 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
30
54
|
|
|
31
55
|
const imports = adapter.getImports(node, (schemaName) => ({
|
|
32
56
|
name: resolveImportName(schemaName),
|
|
33
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
57
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
34
58
|
}))
|
|
35
59
|
|
|
36
60
|
const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
|
|
37
61
|
|
|
38
62
|
const meta = {
|
|
39
63
|
name: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && isEnumSchema ? resolver.resolveEnumKeyName(node, enumTypeSuffix) : resolver.resolveTypeName(node.name),
|
|
40
|
-
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
|
|
64
|
+
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
41
65
|
} as const
|
|
42
66
|
|
|
43
67
|
const schemaPrinter = printerTs({
|
|
@@ -58,8 +82,8 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
58
82
|
baseName={meta.file.baseName}
|
|
59
83
|
path={meta.file.path}
|
|
60
84
|
meta={meta.file.meta}
|
|
61
|
-
banner={resolver.resolveBanner(
|
|
62
|
-
footer={resolver.resolveFooter(
|
|
85
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
86
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
63
87
|
>
|
|
64
88
|
{mode === 'split' &&
|
|
65
89
|
imports.map((imp) => (
|
|
@@ -86,12 +110,15 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
86
110
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
87
111
|
|
|
88
112
|
const meta = {
|
|
89
|
-
file: resolver.resolveFile(
|
|
113
|
+
file: resolver.resolveFile(
|
|
114
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
115
|
+
{ root, output, group: group ?? undefined },
|
|
116
|
+
),
|
|
90
117
|
} as const
|
|
91
118
|
|
|
92
119
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
93
120
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
94
|
-
const enumSchemaNames = new Set(
|
|
121
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
95
122
|
|
|
96
123
|
function resolveImportName(schemaName: string): string {
|
|
97
124
|
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
|
|
@@ -100,12 +127,12 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
100
127
|
return resolver.resolveTypeName(schemaName)
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
|
|
130
|
+
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
|
|
104
131
|
if (!schema) return null
|
|
105
132
|
|
|
106
133
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
107
134
|
name: resolveImportName(schemaName),
|
|
108
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
135
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
109
136
|
}))
|
|
110
137
|
|
|
111
138
|
const schemaPrinter = printerTs({
|
|
@@ -148,22 +175,67 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
148
175
|
}),
|
|
149
176
|
)
|
|
150
177
|
|
|
151
|
-
const
|
|
152
|
-
|
|
178
|
+
const requestBodyContent = node.requestBody?.content ?? []
|
|
179
|
+
|
|
180
|
+
function buildRequestType() {
|
|
181
|
+
if (requestBodyContent.length === 0) return null
|
|
182
|
+
if (requestBodyContent.length === 1) {
|
|
183
|
+
const entry = requestBodyContent[0]!
|
|
184
|
+
if (!entry.schema) return null
|
|
185
|
+
return renderSchemaType({
|
|
153
186
|
schema: {
|
|
154
|
-
...
|
|
155
|
-
description: node.requestBody
|
|
187
|
+
...entry.schema,
|
|
188
|
+
description: node.requestBody!.description ?? entry.schema.description,
|
|
156
189
|
},
|
|
157
190
|
name: resolver.resolveDataName(node),
|
|
158
|
-
keysToOmit:
|
|
191
|
+
keysToOmit: entry.keysToOmit,
|
|
159
192
|
})
|
|
160
|
-
|
|
193
|
+
}
|
|
194
|
+
// Multiple content types — generate individual types + union alias
|
|
195
|
+
const dataName = resolver.resolveDataName(node)
|
|
196
|
+
const usedNames = new Set<string>()
|
|
197
|
+
const individualItems = requestBodyContent
|
|
198
|
+
.filter((entry) => entry.schema)
|
|
199
|
+
.map((entry) => {
|
|
200
|
+
const baseSuffix = getContentTypeSuffix(entry.contentType)
|
|
201
|
+
let individualName = getPerContentTypeName(dataName, baseSuffix)
|
|
202
|
+
let counter = 2
|
|
203
|
+
while (usedNames.has(individualName)) {
|
|
204
|
+
individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`)
|
|
205
|
+
}
|
|
206
|
+
usedNames.add(individualName)
|
|
207
|
+
return {
|
|
208
|
+
name: individualName,
|
|
209
|
+
rendered: renderSchemaType({
|
|
210
|
+
schema: {
|
|
211
|
+
...entry.schema!,
|
|
212
|
+
description: node.requestBody!.description ?? entry.schema!.description,
|
|
213
|
+
},
|
|
214
|
+
name: individualName,
|
|
215
|
+
keysToOmit: entry.keysToOmit,
|
|
216
|
+
}),
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
const unionSchema = ast.createSchema({
|
|
220
|
+
type: 'union',
|
|
221
|
+
members: individualItems.map((item) => ast.createSchema({ type: 'ref', name: item.name })),
|
|
222
|
+
})
|
|
223
|
+
const unionType = renderSchemaType({ schema: unionSchema, name: dataName })
|
|
224
|
+
return (
|
|
225
|
+
<>
|
|
226
|
+
{individualItems.map((item) => item.rendered)}
|
|
227
|
+
{unionType}
|
|
228
|
+
</>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const requestType = buildRequestType()
|
|
161
233
|
|
|
162
234
|
const responseTypes = node.responses.map((res) =>
|
|
163
235
|
renderSchemaType({
|
|
164
|
-
schema: res.schema,
|
|
236
|
+
schema: res.content?.[0]?.schema ?? null,
|
|
165
237
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
166
|
-
keysToOmit: res.keysToOmit,
|
|
238
|
+
keysToOmit: res.content?.[0]?.keysToOmit,
|
|
167
239
|
}),
|
|
168
240
|
)
|
|
169
241
|
|
|
@@ -177,21 +249,19 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
177
249
|
name: resolver.resolveResponsesName(node),
|
|
178
250
|
})
|
|
179
251
|
|
|
180
|
-
|
|
181
|
-
if (!node.responses.some((res) => res.schema)) {
|
|
252
|
+
function buildResponseType() {
|
|
253
|
+
if (!node.responses.some((res) => res.content?.[0]?.schema)) {
|
|
182
254
|
return null
|
|
183
255
|
}
|
|
184
256
|
|
|
185
257
|
const responseName = resolver.resolveResponseName(node)
|
|
186
258
|
|
|
187
|
-
|
|
188
|
-
// has the same resolved name to avoid redeclaration errors.
|
|
189
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
259
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
|
|
190
260
|
const importedNames = new Set(
|
|
191
261
|
responsesWithSchema.flatMap((res) =>
|
|
192
|
-
res.schema
|
|
262
|
+
res.content?.[0]?.schema
|
|
193
263
|
? adapter
|
|
194
|
-
.getImports(res.schema, (schemaName) => ({
|
|
264
|
+
.getImports(res.content[0].schema, (schemaName) => ({
|
|
195
265
|
name: resolveImportName(schemaName),
|
|
196
266
|
path: '',
|
|
197
267
|
}))
|
|
@@ -211,15 +281,17 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
211
281
|
},
|
|
212
282
|
name: responseName,
|
|
213
283
|
})
|
|
214
|
-
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const responseType = buildResponseType()
|
|
215
287
|
|
|
216
288
|
return (
|
|
217
289
|
<File
|
|
218
290
|
baseName={meta.file.baseName}
|
|
219
291
|
path={meta.file.path}
|
|
220
292
|
meta={meta.file.meta}
|
|
221
|
-
banner={resolver.resolveBanner(
|
|
222
|
-
footer={resolver.resolveFooter(
|
|
293
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
294
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
223
295
|
>
|
|
224
296
|
{paramTypes}
|
|
225
297
|
{responseTypes}
|
package/src/plugin.ts
CHANGED
|
@@ -5,23 +5,32 @@ import { resolverTs } from './resolvers/resolverTs.ts'
|
|
|
5
5
|
import type { PluginTs } from './types.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Canonical plugin name for `@kubb/plugin-ts
|
|
8
|
+
* Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
|
|
9
|
+
* cross-plugin dependency references.
|
|
9
10
|
*/
|
|
10
11
|
export const pluginTsName = 'plugin-ts' satisfies PluginTs['name']
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* and writes barrel files based on `output.barrelType`.
|
|
14
|
+
* Generates TypeScript `type` aliases and `interface` declarations from an
|
|
15
|
+
* OpenAPI spec. The foundation that every other Kubb plugin builds on:
|
|
16
|
+
* clients, query hooks, mocks, and validators all reference the names this
|
|
17
|
+
* plugin produces.
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
|
-
* import
|
|
21
|
+
* import { defineConfig } from 'kubb'
|
|
22
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
22
23
|
*
|
|
23
24
|
* export default defineConfig({
|
|
24
|
-
*
|
|
25
|
+
* input: { path: './petStore.yaml' },
|
|
26
|
+
* output: { path: './src/gen' },
|
|
27
|
+
* plugins: [
|
|
28
|
+
* pluginTs({
|
|
29
|
+
* output: { path: './types' },
|
|
30
|
+
* enumType: 'asConst',
|
|
31
|
+
* optionalType: 'questionTokenAndUndefined',
|
|
32
|
+
* }),
|
|
33
|
+
* ],
|
|
25
34
|
* })
|
|
26
35
|
* ```
|
|
27
36
|
*/
|
|
@@ -55,7 +64,7 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
|
|
|
55
64
|
return `${camelCase(ctx.group)}Controller`
|
|
56
65
|
},
|
|
57
66
|
} satisfies Group)
|
|
58
|
-
:
|
|
67
|
+
: null
|
|
59
68
|
|
|
60
69
|
return {
|
|
61
70
|
name: pluginTsName,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { ast } from '@kubb/core'
|
|
2
|
-
import {
|
|
2
|
+
import { parserTs } from '@kubb/parser-ts'
|
|
3
3
|
import type ts from 'typescript'
|
|
4
4
|
import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from '../constants.ts'
|
|
5
5
|
import * as factory from '../factory.ts'
|
|
6
6
|
import type { PluginTs, ResolverTs } from '../types.ts'
|
|
7
7
|
import { buildPropertyJSDocComments } from '../utils.ts'
|
|
8
8
|
|
|
9
|
+
function isNonNullable<T>(value: T | null | undefined): value is T {
|
|
10
|
+
return value !== null && value !== undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Partial map of node-type overrides for the TypeScript printer.
|
|
11
15
|
*
|
|
@@ -85,7 +89,7 @@ export type PrinterTsOptions = {
|
|
|
85
89
|
* Properties to exclude using `Omit<Type, Keys>`.
|
|
86
90
|
* Forces type alias syntax regardless of `syntaxType` setting.
|
|
87
91
|
*/
|
|
88
|
-
keysToOmit?: Array<string>
|
|
92
|
+
keysToOmit?: Array<string> | null
|
|
89
93
|
/**
|
|
90
94
|
* Transforms raw schema names into valid TypeScript identifiers.
|
|
91
95
|
*/
|
|
@@ -163,7 +167,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
163
167
|
time: factory.dateOrStringNode,
|
|
164
168
|
ref(node) {
|
|
165
169
|
if (!node.name) {
|
|
166
|
-
return
|
|
170
|
+
return null
|
|
167
171
|
}
|
|
168
172
|
// Parser-generated refs (with $ref) carry raw schema names that need resolving.
|
|
169
173
|
// Use the canonical name from the $ref path — node.name may have been overridden
|
|
@@ -191,7 +195,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
191
195
|
const literalNodes = values
|
|
192
196
|
.filter((v): v is string | number | boolean => v !== null && v !== undefined)
|
|
193
197
|
.map((value) => factory.constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
|
|
194
|
-
.filter(
|
|
198
|
+
.filter(isNonNullable)
|
|
195
199
|
|
|
196
200
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
|
|
197
201
|
}
|
|
@@ -224,7 +228,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
224
228
|
|
|
225
229
|
return this.transform(m)
|
|
226
230
|
})
|
|
227
|
-
.filter(
|
|
231
|
+
.filter(isNonNullable)
|
|
228
232
|
|
|
229
233
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
|
|
230
234
|
}
|
|
@@ -232,15 +236,15 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
232
236
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(members, this.transform) }) ?? undefined
|
|
233
237
|
},
|
|
234
238
|
intersection(node) {
|
|
235
|
-
return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ??
|
|
239
|
+
return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? null
|
|
236
240
|
},
|
|
237
241
|
array(node) {
|
|
238
|
-
const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(
|
|
242
|
+
const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable)
|
|
239
243
|
|
|
240
|
-
return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ??
|
|
244
|
+
return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? null
|
|
241
245
|
},
|
|
242
246
|
tuple(node) {
|
|
243
|
-
return factory.buildTupleNode(node, this.transform)
|
|
247
|
+
return factory.buildTupleNode(node, this.transform) ?? null
|
|
244
248
|
},
|
|
245
249
|
object(node) {
|
|
246
250
|
const { transform, options } = this
|
|
@@ -275,36 +279,33 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
275
279
|
print(node) {
|
|
276
280
|
const { name, syntaxType = 'type', description, keysToOmit } = this.options
|
|
277
281
|
|
|
278
|
-
|
|
279
|
-
if (!
|
|
282
|
+
const transformed = this.transform(node)
|
|
283
|
+
if (!transformed) return null
|
|
280
284
|
|
|
281
285
|
// For ref nodes, structural metadata lives on node.schema rather than the ref node itself.
|
|
282
286
|
const meta = ast.syncSchemaRef(node)
|
|
283
287
|
|
|
284
288
|
// Without name, apply modifiers inline and return.
|
|
285
289
|
if (!name) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return safePrint(base)
|
|
290
|
+
const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [transformed, factory.keywordTypeNodes.null] }) : transformed
|
|
291
|
+
const result =
|
|
292
|
+
(meta.nullish || meta.optional) && addsUndefined
|
|
293
|
+
? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] })
|
|
294
|
+
: withNullable
|
|
295
|
+
return parserTs.print(result)
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
// When keysToOmit is present, wrap with Omit first, then apply nullable/optional
|
|
296
299
|
// modifiers so they are not swallowed by NonNullable inside createOmitDeclaration.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.undefined] })
|
|
307
|
-
}
|
|
300
|
+
const inner = (() => {
|
|
301
|
+
const omitted: ts.TypeNode = keysToOmit?.length
|
|
302
|
+
? factory.createOmitDeclaration({ keys: keysToOmit, type: transformed, nonNullable: true })
|
|
303
|
+
: transformed
|
|
304
|
+
const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [omitted, factory.keywordTypeNodes.null] }) : omitted
|
|
305
|
+
// For named type declarations (type aliases), optional/nullish always produces | undefined
|
|
306
|
+
// regardless of optionalType — the questionToken ? modifier only applies to object properties.
|
|
307
|
+
return meta.nullish || meta.optional ? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] }) : withNullable
|
|
308
|
+
})()
|
|
308
309
|
|
|
309
310
|
const useTypeGeneration = syntaxType === 'type' || inner.kind === factory.syntaxKind.union || !!keysToOmit?.length
|
|
310
311
|
|
|
@@ -319,7 +320,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
319
320
|
}),
|
|
320
321
|
})
|
|
321
322
|
|
|
322
|
-
return
|
|
323
|
+
return parserTs.print(typeNode)
|
|
323
324
|
},
|
|
324
325
|
}
|
|
325
326
|
})
|