@kubb/plugin-ts 5.0.0-beta.4 → 5.0.0-beta.56
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 +513 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +168 -144
- package/dist/index.js +506 -263
- package/dist/index.js.map +1 -1
- package/package.json +13 -22
- package/src/components/Enum.tsx +33 -29
- package/src/components/Type.tsx +6 -11
- package/src/constants.ts +7 -7
- package/src/factory.ts +58 -62
- package/src/generators/typeGenerator.tsx +118 -77
- package/src/plugin.ts +30 -28
- package/src/printers/functionPrinter.ts +2 -2
- package/src/printers/printerTs.ts +44 -54
- package/src/resolvers/resolverTs.ts +28 -25
- package/src/types.ts +113 -86
- package/src/utils.ts +24 -14
- package/extension.yaml +0 -632
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveContentTypeVariants } from '@internals/shared'
|
|
1
2
|
import { ast, defineGenerator } from '@kubb/core'
|
|
2
3
|
import { File, jsxRenderer } from '@kubb/renderer-jsx'
|
|
3
4
|
import { Type } from '../components/Type.tsx'
|
|
@@ -6,45 +7,52 @@ 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
18
|
renderer: jsxRenderer,
|
|
12
19
|
schema(node, ctx) {
|
|
13
|
-
const {
|
|
20
|
+
const { enum: enumOptions, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options
|
|
14
21
|
const { adapter, config, resolver, root } = ctx
|
|
15
22
|
|
|
16
23
|
if (!node.name) {
|
|
17
24
|
return
|
|
18
25
|
}
|
|
19
|
-
const mode = ctx.getMode(output)
|
|
20
26
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
21
27
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
22
|
-
const enumSchemaNames = new Set(
|
|
28
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
23
29
|
|
|
24
30
|
function resolveImportName(schemaName: string): string {
|
|
25
|
-
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(
|
|
26
|
-
return resolver.resolveEnumKeyName({ name: schemaName },
|
|
31
|
+
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && enumOptions.typeSuffix && enumSchemaNames.has(schemaName)) {
|
|
32
|
+
return resolver.resolveEnumKeyName({ name: schemaName }, enumOptions.typeSuffix)
|
|
27
33
|
}
|
|
28
34
|
return resolver.resolveTypeName(schemaName)
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const imports = adapter.getImports(node, (schemaName) => ({
|
|
32
38
|
name: resolveImportName(schemaName),
|
|
33
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
39
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
34
40
|
}))
|
|
35
41
|
|
|
36
42
|
const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
|
|
37
43
|
|
|
38
44
|
const meta = {
|
|
39
|
-
name:
|
|
40
|
-
|
|
45
|
+
name:
|
|
46
|
+
ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && isEnumSchema
|
|
47
|
+
? resolver.resolveEnumKeyName(node, enumOptions.typeSuffix)
|
|
48
|
+
: resolver.resolveTypeName(node.name),
|
|
49
|
+
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
41
50
|
} as const
|
|
42
51
|
|
|
43
52
|
const schemaPrinter = printerTs({
|
|
44
53
|
optionalType,
|
|
45
54
|
arrayType,
|
|
46
|
-
|
|
47
|
-
enumTypeSuffix,
|
|
55
|
+
enum: enumOptions,
|
|
48
56
|
name: meta.name,
|
|
49
57
|
syntaxType,
|
|
50
58
|
description: node.description,
|
|
@@ -58,61 +66,52 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
58
66
|
baseName={meta.file.baseName}
|
|
59
67
|
path={meta.file.path}
|
|
60
68
|
meta={meta.file.meta}
|
|
61
|
-
banner={resolver.resolveBanner(
|
|
62
|
-
footer={resolver.resolveFooter(
|
|
69
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
70
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
63
71
|
>
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<Type
|
|
69
|
-
name={meta.name}
|
|
70
|
-
node={node}
|
|
71
|
-
enumType={enumType}
|
|
72
|
-
enumTypeSuffix={enumTypeSuffix}
|
|
73
|
-
enumKeyCasing={enumKeyCasing}
|
|
74
|
-
resolver={resolver}
|
|
75
|
-
printer={schemaPrinter}
|
|
76
|
-
/>
|
|
72
|
+
{imports.map((imp) => (
|
|
73
|
+
<File.Import key={[node.name, imp.path, imp.isTypeOnly].join('-')} root={meta.file.path} path={imp.path} name={imp.name} isTypeOnly />
|
|
74
|
+
))}
|
|
75
|
+
<Type name={meta.name} node={node} enum={enumOptions} resolver={resolver} printer={schemaPrinter} />
|
|
77
76
|
</File>
|
|
78
77
|
)
|
|
79
78
|
},
|
|
80
79
|
operation(node, ctx) {
|
|
81
|
-
const {
|
|
80
|
+
const { enum: enumOptions, optionalType, arrayType, syntaxType, paramsCasing, group, output, printer } = ctx.options
|
|
82
81
|
const { adapter, config, resolver, root } = ctx
|
|
83
82
|
|
|
84
|
-
const mode = ctx.getMode(output)
|
|
85
|
-
|
|
86
83
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
87
84
|
|
|
88
85
|
const meta = {
|
|
89
|
-
file: resolver.resolveFile(
|
|
86
|
+
file: resolver.resolveFile(
|
|
87
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
88
|
+
{ root, output, group: group ?? undefined },
|
|
89
|
+
),
|
|
90
90
|
} as const
|
|
91
91
|
|
|
92
92
|
// Build a set of schema names that are enums so the ref handler and getImports
|
|
93
93
|
// callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
|
|
94
|
-
const enumSchemaNames = new Set(
|
|
94
|
+
const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
|
|
95
95
|
|
|
96
96
|
function resolveImportName(schemaName: string): string {
|
|
97
|
-
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(
|
|
98
|
-
return resolver.resolveEnumKeyName({ name: schemaName },
|
|
97
|
+
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && enumOptions.typeSuffix && enumSchemaNames.has(schemaName)) {
|
|
98
|
+
return resolver.resolveEnumKeyName({ name: schemaName }, enumOptions.typeSuffix)
|
|
99
99
|
}
|
|
100
100
|
return resolver.resolveTypeName(schemaName)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
|
|
103
|
+
function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
|
|
104
104
|
if (!schema) return null
|
|
105
105
|
|
|
106
106
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
107
107
|
name: resolveImportName(schemaName),
|
|
108
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
108
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
109
109
|
}))
|
|
110
110
|
|
|
111
111
|
const schemaPrinter = printerTs({
|
|
112
112
|
optionalType,
|
|
113
113
|
arrayType,
|
|
114
|
-
|
|
115
|
-
enumTypeSuffix,
|
|
114
|
+
enum: enumOptions,
|
|
116
115
|
name,
|
|
117
116
|
syntaxType,
|
|
118
117
|
description: schema.description,
|
|
@@ -124,19 +123,38 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
124
123
|
|
|
125
124
|
return (
|
|
126
125
|
<>
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
{imports.map((imp) => (
|
|
127
|
+
<File.Import key={[name, imp.path, imp.isTypeOnly].join('-')} root={meta.file.path} path={imp.path} name={imp.name} isTypeOnly />
|
|
128
|
+
))}
|
|
129
|
+
<Type name={name} node={schema} enum={enumOptions} resolver={resolver} printer={schemaPrinter} />
|
|
130
|
+
</>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Emits an individual type per content type plus a union alias under `baseName`.
|
|
136
|
+
* Shared by the request body and multi-content-type responses.
|
|
137
|
+
*/
|
|
138
|
+
function buildContentTypeVariants(
|
|
139
|
+
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
|
|
140
|
+
baseName: string,
|
|
141
|
+
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
|
|
142
|
+
) {
|
|
143
|
+
const variants = resolveContentTypeVariants(entries, baseName)
|
|
144
|
+
const unionSchema = ast.createSchema({
|
|
145
|
+
type: 'union',
|
|
146
|
+
members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
|
|
147
|
+
})
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
{variants.map((variant) =>
|
|
151
|
+
renderSchemaType({
|
|
152
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
153
|
+
name: variant.name,
|
|
154
|
+
keysToOmit: variant.keysToOmit,
|
|
155
|
+
}),
|
|
156
|
+
)}
|
|
157
|
+
{renderSchemaType({ schema: unionSchema, name: baseName })}
|
|
140
158
|
</>
|
|
141
159
|
)
|
|
142
160
|
}
|
|
@@ -148,24 +166,44 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
148
166
|
}),
|
|
149
167
|
)
|
|
150
168
|
|
|
151
|
-
const
|
|
152
|
-
|
|
169
|
+
const requestBodyContent = node.requestBody?.content ?? []
|
|
170
|
+
|
|
171
|
+
function buildRequestType() {
|
|
172
|
+
if (requestBodyContent.length === 0) return null
|
|
173
|
+
if (requestBodyContent.length === 1) {
|
|
174
|
+
const entry = requestBodyContent[0]!
|
|
175
|
+
if (!entry.schema) return null
|
|
176
|
+
return renderSchemaType({
|
|
153
177
|
schema: {
|
|
154
|
-
...
|
|
155
|
-
description: node.requestBody
|
|
178
|
+
...entry.schema,
|
|
179
|
+
description: node.requestBody!.description ?? entry.schema.description,
|
|
156
180
|
},
|
|
157
181
|
name: resolver.resolveDataName(node),
|
|
158
|
-
keysToOmit:
|
|
182
|
+
keysToOmit: entry.keysToOmit,
|
|
159
183
|
})
|
|
160
|
-
|
|
184
|
+
}
|
|
185
|
+
// Multiple content types — generate individual types + union alias
|
|
186
|
+
return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
|
|
187
|
+
...schema,
|
|
188
|
+
description: node.requestBody!.description ?? schema.description,
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
161
191
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
192
|
+
const requestType = buildRequestType()
|
|
193
|
+
|
|
194
|
+
const responseTypes = node.responses.map((res) => {
|
|
195
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema)
|
|
196
|
+
// Multiple content types for a single status code — generate a union of the variants.
|
|
197
|
+
if (variants.length > 1) {
|
|
198
|
+
return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode))
|
|
199
|
+
}
|
|
200
|
+
const primary = variants[0] ?? res.content?.[0]
|
|
201
|
+
return renderSchemaType({
|
|
202
|
+
schema: primary?.schema ?? null,
|
|
165
203
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
166
|
-
keysToOmit:
|
|
167
|
-
})
|
|
168
|
-
)
|
|
204
|
+
keysToOmit: primary?.keysToOmit,
|
|
205
|
+
})
|
|
206
|
+
})
|
|
169
207
|
|
|
170
208
|
const dataType = renderSchemaType({
|
|
171
209
|
schema: buildData({ ...node, parameters: params }, { resolver }),
|
|
@@ -177,26 +215,27 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
177
215
|
name: resolver.resolveResponsesName(node),
|
|
178
216
|
})
|
|
179
217
|
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
function buildResponseType() {
|
|
219
|
+
const hasSchema = (res: ast.ResponseNode) => (res.content ?? []).some((entry) => entry.schema)
|
|
220
|
+
if (!node.responses.some(hasSchema)) {
|
|
182
221
|
return null
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
const responseName = resolver.resolveResponseName(node)
|
|
186
225
|
|
|
187
|
-
|
|
188
|
-
// has the same resolved name to avoid redeclaration errors.
|
|
189
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
226
|
+
const responsesWithSchema = node.responses.filter(hasSchema)
|
|
190
227
|
const importedNames = new Set(
|
|
191
228
|
responsesWithSchema.flatMap((res) =>
|
|
192
|
-
res.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
229
|
+
(res.content ?? []).flatMap((entry) =>
|
|
230
|
+
entry.schema
|
|
231
|
+
? adapter
|
|
232
|
+
.getImports(entry.schema, (schemaName) => ({
|
|
233
|
+
name: resolveImportName(schemaName),
|
|
234
|
+
path: '',
|
|
235
|
+
}))
|
|
236
|
+
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
|
|
237
|
+
: [],
|
|
238
|
+
),
|
|
200
239
|
),
|
|
201
240
|
)
|
|
202
241
|
|
|
@@ -211,15 +250,17 @@ export const typeGenerator = defineGenerator<PluginTs>({
|
|
|
211
250
|
},
|
|
212
251
|
name: responseName,
|
|
213
252
|
})
|
|
214
|
-
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const responseType = buildResponseType()
|
|
215
256
|
|
|
216
257
|
return (
|
|
217
258
|
<File
|
|
218
259
|
baseName={meta.file.baseName}
|
|
219
260
|
path={meta.file.path}
|
|
220
261
|
meta={meta.file.meta}
|
|
221
|
-
banner={resolver.resolveBanner(
|
|
222
|
-
footer={resolver.resolveFooter(
|
|
262
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
263
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
223
264
|
>
|
|
224
265
|
{paramTypes}
|
|
225
266
|
{responseTypes}
|
package/src/plugin.ts
CHANGED
|
@@ -1,40 +1,47 @@
|
|
|
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
|
+
* enum: { type: 'asConst' },
|
|
31
|
+
* optionalType: 'questionTokenAndUndefined',
|
|
32
|
+
* }),
|
|
33
|
+
* ],
|
|
25
34
|
* })
|
|
26
35
|
* ```
|
|
27
36
|
*/
|
|
28
37
|
export const pluginTs = definePlugin<PluginTs>((options) => {
|
|
29
38
|
const {
|
|
30
|
-
output = { path: 'types',
|
|
39
|
+
output = { path: 'types', barrel: { type: 'named' } },
|
|
31
40
|
group,
|
|
32
41
|
exclude = [],
|
|
33
42
|
include,
|
|
34
43
|
override = [],
|
|
35
|
-
|
|
36
|
-
enumTypeSuffix = 'Key',
|
|
37
|
-
enumKeyCasing = 'none',
|
|
44
|
+
enum: enumOptions = {},
|
|
38
45
|
optionalType = 'questionToken',
|
|
39
46
|
arrayType = 'array',
|
|
40
47
|
syntaxType = 'type',
|
|
@@ -45,17 +52,14 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
|
|
|
45
52
|
generators: userGenerators = [],
|
|
46
53
|
} = options
|
|
47
54
|
|
|
48
|
-
const groupConfig = group
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
57
|
-
} satisfies Group)
|
|
58
|
-
: undefined
|
|
55
|
+
const groupConfig = createGroupConfig(group)
|
|
56
|
+
|
|
57
|
+
const resolvedEnum = {
|
|
58
|
+
type: enumOptions.type ?? 'asConst',
|
|
59
|
+
constCasing: enumOptions.constCasing ?? 'camelCase',
|
|
60
|
+
typeSuffix: enumOptions.typeSuffix ?? 'Key',
|
|
61
|
+
keyCasing: enumOptions.keyCasing ?? 'none',
|
|
62
|
+
}
|
|
59
63
|
|
|
60
64
|
return {
|
|
61
65
|
name: pluginTsName,
|
|
@@ -70,9 +74,7 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
|
|
|
70
74
|
optionalType,
|
|
71
75
|
group: groupConfig,
|
|
72
76
|
arrayType,
|
|
73
|
-
|
|
74
|
-
enumTypeSuffix,
|
|
75
|
-
enumKeyCasing,
|
|
77
|
+
enum: resolvedEnum,
|
|
76
78
|
syntaxType,
|
|
77
79
|
paramsCasing,
|
|
78
80
|
printer,
|
|
@@ -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
|
/**
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { extractRefName } from '@kubb/ast/utils'
|
|
1
2
|
import { ast } from '@kubb/core'
|
|
2
|
-
import {
|
|
3
|
+
import { parserTs } from '@kubb/parser-ts'
|
|
3
4
|
import type ts from 'typescript'
|
|
4
5
|
import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from '../constants.ts'
|
|
5
6
|
import * as factory from '../factory.ts'
|
|
6
7
|
import type { PluginTs, ResolverTs } from '../types.ts'
|
|
7
8
|
import { buildPropertyJSDocComments } from '../utils.ts'
|
|
8
9
|
|
|
10
|
+
function isNonNullable<T>(value: T | null | undefined): value is T {
|
|
11
|
+
return value !== null && value !== undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Partial map of node-type overrides for the TypeScript printer.
|
|
11
16
|
*
|
|
@@ -46,23 +51,11 @@ export type PrinterTsOptions = {
|
|
|
46
51
|
*/
|
|
47
52
|
arrayType: PluginTs['resolvedOptions']['arrayType']
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* - `'asConst'` generates as const declarations
|
|
53
|
-
*
|
|
54
|
-
* @default `'inlineLiteral'`
|
|
55
|
-
*/
|
|
56
|
-
enumType: PluginTs['resolvedOptions']['enumType']
|
|
57
|
-
/**
|
|
58
|
-
* Suffix appended to enum key reference names.
|
|
59
|
-
*
|
|
60
|
-
* @example Enum key naming
|
|
61
|
-
* `StatusKey` when `enumType` is `'asConst'`
|
|
62
|
-
*
|
|
63
|
-
* @default `'Key'`
|
|
54
|
+
* Grouped enum settings. The printer emits references to enums, not the enum declarations, so only
|
|
55
|
+
* `type` (the output format) and `typeSuffix` (the enum key reference suffix) matter here.
|
|
56
|
+
* `constCasing` and `keyCasing` are ignored.
|
|
64
57
|
*/
|
|
65
|
-
|
|
58
|
+
enum: PluginTs['resolvedOptions']['enum']
|
|
66
59
|
/**
|
|
67
60
|
* Syntax for generated declarations.
|
|
68
61
|
* - `'type'` generates type aliases
|
|
@@ -85,7 +78,7 @@ export type PrinterTsOptions = {
|
|
|
85
78
|
* Properties to exclude using `Omit<Type, Keys>`.
|
|
86
79
|
* Forces type alias syntax regardless of `syntaxType` setting.
|
|
87
80
|
*/
|
|
88
|
-
keysToOmit?: Array<string>
|
|
81
|
+
keysToOmit?: Array<string> | null
|
|
89
82
|
/**
|
|
90
83
|
* Transforms raw schema names into valid TypeScript identifiers.
|
|
91
84
|
*/
|
|
@@ -120,13 +113,13 @@ type PrinterTs = PrinterTsFactory
|
|
|
120
113
|
*
|
|
121
114
|
* @example Raw type node (no `typeName`)
|
|
122
115
|
* ```ts
|
|
123
|
-
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array',
|
|
116
|
+
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' } })
|
|
124
117
|
* const typeNode = printer.print(schemaNode) // ts.TypeNode
|
|
125
118
|
* ```
|
|
126
119
|
*
|
|
127
120
|
* @example Full declaration (with `typeName`)
|
|
128
121
|
* ```ts
|
|
129
|
-
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array',
|
|
122
|
+
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' }, typeName: 'MyType' })
|
|
130
123
|
* const declaration = printer.print(schemaNode) // ts.TypeAliasDeclaration | ts.InterfaceDeclaration
|
|
131
124
|
* ```
|
|
132
125
|
*/
|
|
@@ -163,21 +156,21 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
163
156
|
time: factory.dateOrStringNode,
|
|
164
157
|
ref(node) {
|
|
165
158
|
if (!node.name) {
|
|
166
|
-
return
|
|
159
|
+
return null
|
|
167
160
|
}
|
|
168
161
|
// Parser-generated refs (with $ref) carry raw schema names that need resolving.
|
|
169
162
|
// Use the canonical name from the $ref path — node.name may have been overridden
|
|
170
163
|
// (e.g. by single-member allOf flatten using the property-derived child name).
|
|
171
164
|
// Inline refs (without $ref) from utils already carry resolved type names.
|
|
172
|
-
const refName = node.ref ? (
|
|
165
|
+
const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
|
|
173
166
|
|
|
174
167
|
// When a Key suffix is configured, enum refs must use the suffixed name (e.g. `StatusKey`)
|
|
175
168
|
// so the reference matches what the enum file actually exports.
|
|
176
169
|
const isEnumRef =
|
|
177
|
-
node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.
|
|
170
|
+
node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix && this.options.enumSchemaNames?.has(refName)
|
|
178
171
|
|
|
179
172
|
const name = isEnumRef
|
|
180
|
-
? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.
|
|
173
|
+
? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.enum.typeSuffix)
|
|
181
174
|
: node.ref
|
|
182
175
|
? this.options.resolver.default(refName, 'type')
|
|
183
176
|
: refName
|
|
@@ -187,18 +180,18 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
187
180
|
enum(node) {
|
|
188
181
|
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
189
182
|
|
|
190
|
-
if (this.options.
|
|
183
|
+
if (this.options.enum.type === 'inlineLiteral' || !node.name) {
|
|
191
184
|
const literalNodes = values
|
|
192
185
|
.filter((v): v is string | number | boolean => v !== null && v !== undefined)
|
|
193
186
|
.map((value) => factory.constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
|
|
194
|
-
.filter(
|
|
187
|
+
.filter(isNonNullable)
|
|
195
188
|
|
|
196
189
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
|
|
197
190
|
}
|
|
198
191
|
|
|
199
192
|
const resolvedName =
|
|
200
|
-
ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.
|
|
201
|
-
? this.options.resolver.resolveEnumKeyName(node, this.options.
|
|
193
|
+
ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix
|
|
194
|
+
? this.options.resolver.resolveEnumKeyName(node, this.options.enum.typeSuffix)
|
|
202
195
|
: this.options.resolver.default(node.name, 'type')
|
|
203
196
|
|
|
204
197
|
return factory.createTypeReferenceNode(resolvedName, undefined)
|
|
@@ -224,7 +217,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
224
217
|
|
|
225
218
|
return this.transform(m)
|
|
226
219
|
})
|
|
227
|
-
.filter(
|
|
220
|
+
.filter(isNonNullable)
|
|
228
221
|
|
|
229
222
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
|
|
230
223
|
}
|
|
@@ -232,15 +225,15 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
232
225
|
return factory.createUnionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(members, this.transform) }) ?? undefined
|
|
233
226
|
},
|
|
234
227
|
intersection(node) {
|
|
235
|
-
return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ??
|
|
228
|
+
return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? null
|
|
236
229
|
},
|
|
237
230
|
array(node) {
|
|
238
|
-
const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(
|
|
231
|
+
const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable)
|
|
239
232
|
|
|
240
|
-
return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ??
|
|
233
|
+
return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? null
|
|
241
234
|
},
|
|
242
235
|
tuple(node) {
|
|
243
|
-
return factory.buildTupleNode(node, this.transform)
|
|
236
|
+
return factory.buildTupleNode(node, this.transform) ?? null
|
|
244
237
|
},
|
|
245
238
|
object(node) {
|
|
246
239
|
const { transform, options } = this
|
|
@@ -275,36 +268,33 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
275
268
|
print(node) {
|
|
276
269
|
const { name, syntaxType = 'type', description, keysToOmit } = this.options
|
|
277
270
|
|
|
278
|
-
|
|
279
|
-
if (!
|
|
271
|
+
const transformed = this.transform(node)
|
|
272
|
+
if (!transformed) return null
|
|
280
273
|
|
|
281
274
|
// For ref nodes, structural metadata lives on node.schema rather than the ref node itself.
|
|
282
275
|
const meta = ast.syncSchemaRef(node)
|
|
283
276
|
|
|
284
277
|
// Without name, apply modifiers inline and return.
|
|
285
278
|
if (!name) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return safePrint(base)
|
|
279
|
+
const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [transformed, factory.keywordTypeNodes.null] }) : transformed
|
|
280
|
+
const result =
|
|
281
|
+
(meta.nullish || meta.optional) && addsUndefined
|
|
282
|
+
? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] })
|
|
283
|
+
: withNullable
|
|
284
|
+
return parserTs.print(result)
|
|
293
285
|
}
|
|
294
286
|
|
|
295
287
|
// When keysToOmit is present, wrap with Omit first, then apply nullable/optional
|
|
296
288
|
// 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
|
-
}
|
|
289
|
+
const inner = (() => {
|
|
290
|
+
const omitted: ts.TypeNode = keysToOmit?.length
|
|
291
|
+
? factory.createOmitDeclaration({ keys: keysToOmit, type: transformed, nonNullable: true })
|
|
292
|
+
: transformed
|
|
293
|
+
const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [omitted, factory.keywordTypeNodes.null] }) : omitted
|
|
294
|
+
// For named type declarations (type aliases), optional/nullish always produces | undefined
|
|
295
|
+
// regardless of optionalType — the questionToken ? modifier only applies to object properties.
|
|
296
|
+
return meta.nullish || meta.optional ? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] }) : withNullable
|
|
297
|
+
})()
|
|
308
298
|
|
|
309
299
|
const useTypeGeneration = syntaxType === 'type' || inner.kind === factory.syntaxKind.union || !!keysToOmit?.length
|
|
310
300
|
|
|
@@ -319,7 +309,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
|
|
|
319
309
|
}),
|
|
320
310
|
})
|
|
321
311
|
|
|
322
|
-
return
|
|
312
|
+
return parserTs.print(typeNode)
|
|
323
313
|
},
|
|
324
314
|
}
|
|
325
315
|
})
|