@kubb/plugin-ts 5.0.0-beta.4 → 5.0.0-beta.42
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 +39 -22
- package/dist/index.cjs +432 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +84 -62
- package/dist/index.js +427 -125
- package/dist/index.js.map +1 -1
- package/extension.yaml +713 -265
- package/package.json +10 -12
- package/src/components/Enum.tsx +3 -3
- package/src/factory.ts +54 -25
- package/src/generators/typeGenerator.tsx +100 -39
- package/src/plugin.ts +20 -21
- package/src/printers/functionPrinter.ts +2 -2
- 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/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
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.42",
|
|
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
|
],
|
|
@@ -49,17 +46,17 @@
|
|
|
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.0",
|
|
49
|
+
"@kubb/core": "5.0.0-beta.42",
|
|
50
|
+
"@kubb/parser-ts": "5.0.0-beta.42",
|
|
51
|
+
"@kubb/renderer-jsx": "5.0.0-beta.42",
|
|
56
52
|
"typescript": "^6.0.3"
|
|
57
53
|
},
|
|
58
54
|
"devDependencies": {
|
|
55
|
+
"@internals/shared": "0.0.0",
|
|
59
56
|
"@internals/utils": "0.0.0"
|
|
60
57
|
},
|
|
61
58
|
"peerDependencies": {
|
|
62
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
59
|
+
"@kubb/renderer-jsx": "5.0.0-beta.42"
|
|
63
60
|
},
|
|
64
61
|
"size-limit": [
|
|
65
62
|
{
|
|
@@ -78,6 +75,7 @@
|
|
|
78
75
|
"lint:fix": "oxlint --fix .",
|
|
79
76
|
"release": "pnpm publish --no-git-check",
|
|
80
77
|
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
|
|
78
|
+
"release:stage": "pnpm stage publish --no-git-check",
|
|
81
79
|
"start": "tsdown --watch",
|
|
82
80
|
"test": "vitest --passWithNoTests",
|
|
83
81
|
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
|
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
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { camelCase, pascalCase, screamingSnakeCase, snakeCase } from '@internals/utils'
|
|
2
2
|
import { ast } from '@kubb/core'
|
|
3
|
-
import { isNumber, sortBy } from 'remeda'
|
|
4
3
|
import ts from 'typescript'
|
|
5
4
|
import { OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
|
|
6
5
|
|
|
7
6
|
const { SyntaxKind, factory } = ts
|
|
8
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Compares two strings by UTF-16 code unit, keeping sorted output identical across platforms
|
|
10
|
+
* regardless of locale.
|
|
11
|
+
*/
|
|
12
|
+
function compareStrings(a: string, b: string): number {
|
|
13
|
+
if (a < b) return -1
|
|
14
|
+
if (a > b) return 1
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isNumber(value: unknown): value is number {
|
|
19
|
+
return typeof value === 'number' && !Number.isNaN(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
// https://ts-ast-viewer.com/
|
|
10
23
|
|
|
11
24
|
/**
|
|
@@ -27,13 +40,30 @@ export const syntaxKind = {
|
|
|
27
40
|
stringLiteral: SyntaxKind.StringLiteral,
|
|
28
41
|
} as const
|
|
29
42
|
|
|
43
|
+
function isNonNullable<T>(value: T | null | undefined): value is T {
|
|
44
|
+
return value !== null && value !== undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
function isValidIdentifier(str: string): boolean {
|
|
31
48
|
if (!str.length || str.trim() !== str) {
|
|
32
49
|
return false
|
|
33
50
|
}
|
|
34
|
-
const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest)
|
|
35
51
|
|
|
36
|
-
|
|
52
|
+
// Mirrors `ts.isIdentifierText`, which is not in the public type declarations.
|
|
53
|
+
// Walking by code point with `isIdentifierStart`/`isIdentifierPart` rejects
|
|
54
|
+
// invalid names such as private identifiers (`#FOO`), forcing `propertyName`
|
|
55
|
+
// to quote them.
|
|
56
|
+
let ch = str.codePointAt(0)!
|
|
57
|
+
if (!ts.isIdentifierStart(ch, ts.ScriptTarget.Latest)) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
for (let i = ch > 0xffff ? 2 : 1; i < str.length; i += ch > 0xffff ? 2 : 1) {
|
|
61
|
+
ch = str.codePointAt(i)!
|
|
62
|
+
if (!ts.isIdentifierPart(ch, ts.ScriptTarget.Latest)) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
function propertyName(name: string | ts.PropertyName): ts.PropertyName {
|
|
@@ -158,7 +188,9 @@ export function createPropertySignature({
|
|
|
158
188
|
type?: ts.TypeNode
|
|
159
189
|
}) {
|
|
160
190
|
return factory.createPropertySignature(
|
|
161
|
-
[...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
|
|
191
|
+
[...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
|
|
192
|
+
(modifier): modifier is ts.Modifier => modifier !== undefined,
|
|
193
|
+
),
|
|
162
194
|
propertyName(name),
|
|
163
195
|
createQuestionToken(questionToken),
|
|
164
196
|
type,
|
|
@@ -192,7 +224,7 @@ export function createParameterSignature(
|
|
|
192
224
|
* Creates a JSDoc comment node from an array of comment strings.
|
|
193
225
|
* Returns null if no comments are provided.
|
|
194
226
|
*/
|
|
195
|
-
export function createJSDoc({ comments }: { comments: string
|
|
227
|
+
export function createJSDoc({ comments }: { comments: Array<string> }) {
|
|
196
228
|
if (!comments.length) {
|
|
197
229
|
return null
|
|
198
230
|
}
|
|
@@ -332,7 +364,7 @@ export function createTypeDeclaration({
|
|
|
332
364
|
/**
|
|
333
365
|
* Creates a TypeScript namespace declaration (exported module).
|
|
334
366
|
*/
|
|
335
|
-
export function createNamespaceDeclaration({ statements, name }: { name: string; statements: ts.Statement
|
|
367
|
+
export function createNamespaceDeclaration({ statements, name }: { name: string; statements: Array<ts.Statement> }) {
|
|
336
368
|
return factory.createModuleDeclaration(
|
|
337
369
|
[factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
338
370
|
factory.createIdentifier(name),
|
|
@@ -366,13 +398,8 @@ export function createImportDeclaration({
|
|
|
366
398
|
isNameSpace?: boolean
|
|
367
399
|
}) {
|
|
368
400
|
if (!Array.isArray(name)) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (isNameSpace) {
|
|
373
|
-
importPropertyName = undefined
|
|
374
|
-
importName = factory.createNamespaceImport(factory.createIdentifier(name))
|
|
375
|
-
}
|
|
401
|
+
const importPropertyName = isNameSpace ? undefined : factory.createIdentifier(name)
|
|
402
|
+
const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : undefined
|
|
376
403
|
|
|
377
404
|
return factory.createImportDeclaration(
|
|
378
405
|
undefined,
|
|
@@ -383,7 +410,7 @@ export function createImportDeclaration({
|
|
|
383
410
|
}
|
|
384
411
|
|
|
385
412
|
// Sort the imports alphabetically for consistent output across platforms
|
|
386
|
-
const sortedName =
|
|
413
|
+
const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'object' ? a.propertyName : a, typeof b === 'object' ? b.propertyName : b))
|
|
387
414
|
|
|
388
415
|
return factory.createImportDeclaration(
|
|
389
416
|
undefined,
|
|
@@ -442,7 +469,7 @@ export function createExportDeclaration({
|
|
|
442
469
|
}
|
|
443
470
|
|
|
444
471
|
// Sort the exports alphabetically for consistent output across platforms
|
|
445
|
-
const sortedName =
|
|
472
|
+
const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'string' ? a : a.text, typeof b === 'string' ? b : b.text))
|
|
446
473
|
|
|
447
474
|
return factory.createExportDeclaration(
|
|
448
475
|
undefined,
|
|
@@ -518,7 +545,7 @@ export function createEnumDeclaration({
|
|
|
518
545
|
* Enum name in PascalCase.
|
|
519
546
|
*/
|
|
520
547
|
typeName: string
|
|
521
|
-
enums: [key: string | number, value: string | number | boolean]
|
|
548
|
+
enums: Array<[key: string | number, value: string | number | boolean]>
|
|
522
549
|
/**
|
|
523
550
|
* Choose the casing for enum key names.
|
|
524
551
|
* @default 'none'
|
|
@@ -553,7 +580,7 @@ export function createEnumDeclaration({
|
|
|
553
580
|
|
|
554
581
|
return undefined
|
|
555
582
|
})
|
|
556
|
-
.filter(
|
|
583
|
+
.filter((node): node is ts.LiteralTypeNode => node !== undefined),
|
|
557
584
|
),
|
|
558
585
|
),
|
|
559
586
|
]
|
|
@@ -563,7 +590,9 @@ export function createEnumDeclaration({
|
|
|
563
590
|
return [
|
|
564
591
|
undefined,
|
|
565
592
|
factory.createEnumDeclaration(
|
|
566
|
-
[factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
|
|
593
|
+
[factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
|
|
594
|
+
(modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
|
|
595
|
+
),
|
|
567
596
|
factory.createIdentifier(typeName),
|
|
568
597
|
enums
|
|
569
598
|
.map(([key, value]) => {
|
|
@@ -594,7 +623,7 @@ export function createEnumDeclaration({
|
|
|
594
623
|
|
|
595
624
|
return undefined
|
|
596
625
|
})
|
|
597
|
-
.filter(
|
|
626
|
+
.filter((member): member is ts.EnumMember => member !== undefined),
|
|
598
627
|
),
|
|
599
628
|
]
|
|
600
629
|
}
|
|
@@ -657,7 +686,7 @@ export function createEnumDeclaration({
|
|
|
657
686
|
|
|
658
687
|
return undefined
|
|
659
688
|
})
|
|
660
|
-
.filter(
|
|
689
|
+
.filter((property): property is ts.PropertyAssignment => property !== undefined),
|
|
661
690
|
true,
|
|
662
691
|
),
|
|
663
692
|
factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
|
|
@@ -737,8 +766,8 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
|
|
|
737
766
|
}
|
|
738
767
|
|
|
739
768
|
const segments = normalized.split(/(\{[^}]+\})/)
|
|
740
|
-
const parts: string
|
|
741
|
-
const parameterIndices: number
|
|
769
|
+
const parts: Array<string> = []
|
|
770
|
+
const parameterIndices: Array<number> = []
|
|
742
771
|
|
|
743
772
|
segments.forEach((segment) => {
|
|
744
773
|
if (segment.startsWith('{') && segment.endsWith('}')) {
|
|
@@ -750,7 +779,7 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
|
|
|
750
779
|
})
|
|
751
780
|
|
|
752
781
|
const head = ts.factory.createTemplateHead(parts[0] || '')
|
|
753
|
-
const templateSpans: ts.TemplateLiteralTypeSpan
|
|
782
|
+
const templateSpans: Array<ts.TemplateLiteralTypeSpan> = []
|
|
754
783
|
|
|
755
784
|
parameterIndices.forEach((paramIndex, i) => {
|
|
756
785
|
const isLast = i === parameterIndices.length - 1
|
|
@@ -885,7 +914,7 @@ export function buildMemberNodes(
|
|
|
885
914
|
members: Array<ast.SchemaNode> | undefined,
|
|
886
915
|
print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
|
|
887
916
|
): Array<ts.TypeNode> {
|
|
888
|
-
return (members ?? []).map(print).filter(
|
|
917
|
+
return (members ?? []).map(print).filter(isNonNullable)
|
|
889
918
|
}
|
|
890
919
|
|
|
891
920
|
/**
|
|
@@ -893,7 +922,7 @@ export function buildMemberNodes(
|
|
|
893
922
|
* applying min/max slice and optional/rest element rules.
|
|
894
923
|
*/
|
|
895
924
|
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(
|
|
925
|
+
let items = (node.items ?? []).map(print).filter(isNonNullable)
|
|
897
926
|
|
|
898
927
|
const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
|
|
899
928
|
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,
|
|
@@ -64,11 +64,11 @@ function rank(param: ast.FunctionParameterNode | ast.ParameterGroupNode): number
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function sortParams(params: ReadonlyArray<ast.FunctionParameterNode | ast.ParameterGroupNode>): Array<ast.FunctionParameterNode | ast.ParameterGroupNode> {
|
|
67
|
-
return
|
|
67
|
+
return params.toSorted((a, b) => rank(a) - rank(b))
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function sortChildParams(params: Array<ast.FunctionParameterNode>): Array<ast.FunctionParameterNode> {
|
|
71
|
-
return
|
|
71
|
+
return params.toSorted((a, b) => rank(a) - rank(b))
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|