@kubb/plugin-ts 5.0.0-beta.3 → 5.0.0-beta.31
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 +416 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +81 -58
- package/dist/index.js +415 -120
- 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 +100 -39
- package/src/plugin.ts +20 -21
- 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.31",
|
|
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.31",
|
|
50
|
+
"@kubb/parser-ts": "5.0.0-beta.31",
|
|
51
|
+
"@kubb/renderer-jsx": "5.0.0-beta.31",
|
|
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.31"
|
|
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,21 @@
|
|
|
1
|
+
import { resolveContentTypeVariants } from '@internals/shared'
|
|
1
2
|
import { ast, defineGenerator } from '@kubb/core'
|
|
2
|
-
import { File,
|
|
3
|
+
import { File, jsxRendererSync } from '@kubb/renderer-jsx'
|
|
3
4
|
import { Type } from '../components/Type.tsx'
|
|
4
5
|
import { ENUM_TYPES_WITH_KEY_SUFFIX } from '../constants.ts'
|
|
5
6
|
import { printerTs } from '../printers/printerTs.ts'
|
|
6
7
|
import type { PluginTs } from '../types'
|
|
7
8
|
import { buildData, buildResponses, buildResponseUnion } from '../utils.ts'
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
|
|
12
|
+
* schema in the spec plus per-operation request, response, and parameter
|
|
13
|
+
* types. Drop-replace with a custom `Generator<PluginTs>` to change how
|
|
14
|
+
* TypeScript output is produced.
|
|
15
|
+
*/
|
|
9
16
|
export const typeGenerator = defineGenerator<PluginTs>({
|
|
10
17
|
name: 'typescript',
|
|
11
|
-
renderer:
|
|
18
|
+
renderer: jsxRendererSync,
|
|
12
19
|
schema(node, ctx) {
|
|
13
20
|
const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options
|
|
14
21
|
const { adapter, config, resolver, root } = ctx
|
|
@@ -19,7 +26,7 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
19
26
|
const mode = ctx.getMode(output)
|
|
20
27
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
21
28
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
22
|
-
const enumSchemaNames = new Set(
|
|
29
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
23
30
|
|
|
24
31
|
function resolveImportName(schemaName: string): string {
|
|
25
32
|
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
|
|
@@ -30,14 +37,14 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
30
37
|
|
|
31
38
|
const imports = adapter.getImports(node, (schemaName) => ({
|
|
32
39
|
name: resolveImportName(schemaName),
|
|
33
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
40
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
34
41
|
}))
|
|
35
42
|
|
|
36
43
|
const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
|
|
37
44
|
|
|
38
45
|
const meta = {
|
|
39
46
|
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 }),
|
|
47
|
+
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
41
48
|
} as const
|
|
42
49
|
|
|
43
50
|
const schemaPrinter = printerTs({
|
|
@@ -58,8 +65,8 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
58
65
|
baseName={meta.file.baseName}
|
|
59
66
|
path={meta.file.path}
|
|
60
67
|
meta={meta.file.meta}
|
|
61
|
-
banner={resolver.resolveBanner(
|
|
62
|
-
footer={resolver.resolveFooter(
|
|
68
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
69
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
63
70
|
>
|
|
64
71
|
{mode === 'split' &&
|
|
65
72
|
imports.map((imp) => (
|
|
@@ -86,12 +93,15 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
86
93
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
87
94
|
|
|
88
95
|
const meta = {
|
|
89
|
-
file: resolver.resolveFile(
|
|
96
|
+
file: resolver.resolveFile(
|
|
97
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
98
|
+
{ root, output, group: group ?? undefined },
|
|
99
|
+
),
|
|
90
100
|
} as const
|
|
91
101
|
|
|
92
102
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
93
103
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
94
|
-
const enumSchemaNames = new Set(
|
|
104
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
95
105
|
|
|
96
106
|
function resolveImportName(schemaName: string): string {
|
|
97
107
|
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
|
|
@@ -100,12 +110,12 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
100
110
|
return resolver.resolveTypeName(schemaName)
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
|
|
113
|
+
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
|
|
104
114
|
if (!schema) return null
|
|
105
115
|
|
|
106
116
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
107
117
|
name: resolveImportName(schemaName),
|
|
108
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
118
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
109
119
|
}))
|
|
110
120
|
|
|
111
121
|
const schemaPrinter = printerTs({
|
|
@@ -141,6 +151,34 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
141
151
|
)
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Emits an individual type per content type plus a union alias under `baseName`.
|
|
156
|
+
* Shared by the request body and multi-content-type responses.
|
|
157
|
+
*/
|
|
158
|
+
function buildContentTypeVariants(
|
|
159
|
+
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
|
|
160
|
+
baseName: string,
|
|
161
|
+
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
|
|
162
|
+
) {
|
|
163
|
+
const variants = resolveContentTypeVariants(entries, baseName)
|
|
164
|
+
const unionSchema = ast.createSchema({
|
|
165
|
+
type: 'union',
|
|
166
|
+
members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
|
|
167
|
+
})
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
{variants.map((variant) =>
|
|
171
|
+
renderSchemaType({
|
|
172
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
173
|
+
name: variant.name,
|
|
174
|
+
keysToOmit: variant.keysToOmit,
|
|
175
|
+
}),
|
|
176
|
+
)}
|
|
177
|
+
{renderSchemaType({ schema: unionSchema, name: baseName })}
|
|
178
|
+
</>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
const paramTypes = params.map((param) =>
|
|
145
183
|
renderSchemaType({
|
|
146
184
|
schema: param.schema,
|
|
@@ -148,24 +186,44 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
148
186
|
}),
|
|
149
187
|
)
|
|
150
188
|
|
|
151
|
-
const
|
|
152
|
-
|
|
189
|
+
const requestBodyContent = node.requestBody?.content ?? []
|
|
190
|
+
|
|
191
|
+
function buildRequestType() {
|
|
192
|
+
if (requestBodyContent.length === 0) return null
|
|
193
|
+
if (requestBodyContent.length === 1) {
|
|
194
|
+
const entry = requestBodyContent[0]!
|
|
195
|
+
if (!entry.schema) return null
|
|
196
|
+
return renderSchemaType({
|
|
153
197
|
schema: {
|
|
154
|
-
...
|
|
155
|
-
description: node.requestBody
|
|
198
|
+
...entry.schema,
|
|
199
|
+
description: node.requestBody!.description ?? entry.schema.description,
|
|
156
200
|
},
|
|
157
201
|
name: resolver.resolveDataName(node),
|
|
158
|
-
keysToOmit:
|
|
202
|
+
keysToOmit: entry.keysToOmit,
|
|
159
203
|
})
|
|
160
|
-
|
|
204
|
+
}
|
|
205
|
+
// Multiple content types — generate individual types + union alias
|
|
206
|
+
return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
|
|
207
|
+
...schema,
|
|
208
|
+
description: node.requestBody!.description ?? schema.description,
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
161
211
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
212
|
+
const requestType = buildRequestType()
|
|
213
|
+
|
|
214
|
+
const responseTypes = node.responses.map((res) => {
|
|
215
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema)
|
|
216
|
+
// Multiple content types for a single status code — generate a union of the variants.
|
|
217
|
+
if (variants.length > 1) {
|
|
218
|
+
return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode))
|
|
219
|
+
}
|
|
220
|
+
const primary = variants[0] ?? res.content?.[0]
|
|
221
|
+
return renderSchemaType({
|
|
222
|
+
schema: primary?.schema ?? null,
|
|
165
223
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
166
|
-
keysToOmit:
|
|
167
|
-
})
|
|
168
|
-
)
|
|
224
|
+
keysToOmit: primary?.keysToOmit,
|
|
225
|
+
})
|
|
226
|
+
})
|
|
169
227
|
|
|
170
228
|
const dataType = renderSchemaType({
|
|
171
229
|
schema: buildData({ ...node, parameters: params }, { resolver }),
|
|
@@ -177,26 +235,27 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
177
235
|
name: resolver.resolveResponsesName(node),
|
|
178
236
|
})
|
|
179
237
|
|
|
180
|
-
|
|
181
|
-
|
|
238
|
+
function buildResponseType() {
|
|
239
|
+
const hasSchema = (res: ast.ResponseNode) => (res.content ?? []).some((entry) => entry.schema)
|
|
240
|
+
if (!node.responses.some(hasSchema)) {
|
|
182
241
|
return null
|
|
183
242
|
}
|
|
184
243
|
|
|
185
244
|
const responseName = resolver.resolveResponseName(node)
|
|
186
245
|
|
|
187
|
-
|
|
188
|
-
// has the same resolved name to avoid redeclaration errors.
|
|
189
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
246
|
+
const responsesWithSchema = node.responses.filter(hasSchema)
|
|
190
247
|
const importedNames = new Set(
|
|
191
248
|
responsesWithSchema.flatMap((res) =>
|
|
192
|
-
res.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
249
|
+
(res.content ?? []).flatMap((entry) =>
|
|
250
|
+
entry.schema
|
|
251
|
+
? adapter
|
|
252
|
+
.getImports(entry.schema, (schemaName) => ({
|
|
253
|
+
name: resolveImportName(schemaName),
|
|
254
|
+
path: '',
|
|
255
|
+
}))
|
|
256
|
+
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
|
|
257
|
+
: [],
|
|
258
|
+
),
|
|
200
259
|
),
|
|
201
260
|
)
|
|
202
261
|
|
|
@@ -211,15 +270,17 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
211
270
|
},
|
|
212
271
|
name: responseName,
|
|
213
272
|
})
|
|
214
|
-
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const responseType = buildResponseType()
|
|
215
276
|
|
|
216
277
|
return (
|
|
217
278
|
<File
|
|
218
279
|
baseName={meta.file.baseName}
|
|
219
280
|
path={meta.file.path}
|
|
220
281
|
meta={meta.file.meta}
|
|
221
|
-
banner={resolver.resolveBanner(
|
|
222
|
-
footer={resolver.resolveFooter(
|
|
282
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
283
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
223
284
|
>
|
|
224
285
|
{paramTypes}
|
|
225
286
|
{responseTypes}
|
package/src/plugin.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { definePlugin
|
|
1
|
+
import { createGroupConfig } from '@internals/shared'
|
|
2
|
+
import { definePlugin } from '@kubb/core'
|
|
3
3
|
import { typeGenerator } from './generators/typeGenerator.tsx'
|
|
4
4
|
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
|
*/
|
|
@@ -45,17 +54,7 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
|
|
|
45
54
|
generators: userGenerators = [],
|
|
46
55
|
} = options
|
|
47
56
|
|
|
48
|
-
const groupConfig = group
|
|
49
|
-
? ({
|
|
50
|
-
...group,
|
|
51
|
-
name: (ctx) => {
|
|
52
|
-
if (group.type === 'path') {
|
|
53
|
-
return `${ctx.group.split('/')[1]}`
|
|
54
|
-
}
|
|
55
|
-
return `${camelCase(ctx.group)}Controller`
|
|
56
|
-
},
|
|
57
|
-
} satisfies Group)
|
|
58
|
-
: undefined
|
|
57
|
+
const groupConfig = createGroupConfig(group, { suffix: 'Controller' })
|
|
59
58
|
|
|
60
59
|
return {
|
|
61
60
|
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
|
})
|