@kubb/plugin-zod 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 +684 -301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +115 -49
- package/dist/index.js +673 -296
- package/dist/index.js.map +1 -1
- package/package.json +13 -23
- package/src/components/Operations.tsx +7 -6
- package/src/components/Zod.tsx +1 -1
- package/src/generators/zodGenerator.tsx +213 -62
- package/src/plugin.ts +24 -21
- package/src/printers/printerZod.ts +131 -110
- package/src/printers/printerZodMini.ts +82 -87
- package/src/resolvers/resolverZod.ts +33 -19
- package/src/types.ts +64 -32
- package/src/utils.ts +114 -37
- package/extension.yaml +0 -502
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveContentTypeVariants } from '@internals/shared'
|
|
2
|
+
import { extractRefName } from '@kubb/ast/utils'
|
|
1
3
|
import type { Adapter } from '@kubb/core'
|
|
2
4
|
import { ast, defineGenerator } from '@kubb/core'
|
|
3
5
|
import type { AdapterOas } from '@kubb/adapter-oas'
|
|
@@ -7,9 +9,50 @@ import { Zod } from '../components/Zod.tsx'
|
|
|
7
9
|
import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
|
|
8
10
|
import { printerZod } from '../printers/printerZod.ts'
|
|
9
11
|
import { printerZodMini } from '../printers/printerZodMini.ts'
|
|
10
|
-
import type { PluginZod } from '../types'
|
|
11
|
-
import { buildSchemaNames } from '../utils.ts'
|
|
12
|
-
|
|
12
|
+
import type { PluginZod, ResolverZod } from '../types'
|
|
13
|
+
import { buildSchemaNames, containsCodec } from '../utils.ts'
|
|
14
|
+
|
|
15
|
+
type StdPrinters = { output: ReturnType<typeof printerZod>; input: ReturnType<typeof printerZod> }
|
|
16
|
+
type ZodPrinterEntry = StdPrinters & { coercion: unknown; guidType: unknown; dateType: unknown }
|
|
17
|
+
type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidType: unknown }
|
|
18
|
+
|
|
19
|
+
// Per-build caches: keyed on resolver (unique per plugin instance per build, GC'd when released)
|
|
20
|
+
const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
|
|
21
|
+
const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
|
|
22
|
+
|
|
23
|
+
type StdPrinterParams = { coercion: unknown; guidType: unknown; dateType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the cached `output`/`input` direction printers for a resolver, building them on
|
|
27
|
+
* first use. The `input` printer encodes `Date → string` for request bodies; `output` decodes
|
|
28
|
+
* `string → Date` for responses. Schemas without `dateType: 'date'` fields print identically.
|
|
29
|
+
*/
|
|
30
|
+
function getStdPrinters(resolver: ResolverZod, params: StdPrinterParams): StdPrinters {
|
|
31
|
+
const cached = zodPrinterCache.get(resolver)
|
|
32
|
+
if (cached && cached.coercion === params.coercion && cached.guidType === params.guidType && cached.dateType === params.dateType) {
|
|
33
|
+
return { output: cached.output, input: cached.input }
|
|
34
|
+
}
|
|
35
|
+
const base = { ...params, resolver } as Parameters<typeof printerZod>[0]
|
|
36
|
+
const output = printerZod({ ...base, direction: 'output' })
|
|
37
|
+
const input = printerZod({ ...base, direction: 'input' })
|
|
38
|
+
zodPrinterCache.set(resolver, { output, input, coercion: params.coercion, guidType: params.guidType, dateType: params.dateType })
|
|
39
|
+
return { output, input }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getMiniPrinter(resolver: ResolverZod, params: { guidType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }) {
|
|
43
|
+
const cached = zodMiniPrinterCache.get(resolver)
|
|
44
|
+
if (cached && cached.guidType === params.guidType) return cached.printer
|
|
45
|
+
const p = printerZodMini({ ...params, resolver } as Parameters<typeof printerZodMini>[0])
|
|
46
|
+
zodMiniPrinterCache.set(resolver, { printer: p, guidType: params.guidType })
|
|
47
|
+
return p
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
|
|
52
|
+
* schema in the spec plus per-operation request/response/parameter schemas.
|
|
53
|
+
* When `mini: true`, schemas use the Zod Mini functional API instead of
|
|
54
|
+
* chainable methods.
|
|
55
|
+
*/
|
|
13
56
|
export const zodGenerator = defineGenerator<PluginZod>({
|
|
14
57
|
name: 'zod',
|
|
15
58
|
renderer: jsxRenderer,
|
|
@@ -22,92 +65,182 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
22
65
|
return
|
|
23
66
|
}
|
|
24
67
|
|
|
25
|
-
const mode = ctx.getMode(output)
|
|
26
68
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
27
|
-
|
|
28
|
-
|
|
69
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
70
|
+
|
|
71
|
+
// A codec component is rendered twice: the canonical (output) schema decodes
|
|
72
|
+
// `string → Date`, and an `${name}InputSchema` variant encodes `Date → string` for requests.
|
|
73
|
+
const hasCodec = !mini && containsCodec(node)
|
|
74
|
+
|
|
75
|
+
const codecRefNames = new Set(
|
|
76
|
+
hasCodec
|
|
77
|
+
? ast.collect<string>(node, {
|
|
78
|
+
schema: (n) => (n.type === 'ref' && n.ref && containsCodec(n) ? (extractRefName(n.ref) ?? undefined) : undefined),
|
|
79
|
+
})
|
|
80
|
+
: [],
|
|
81
|
+
)
|
|
82
|
+
const importEntries = adapter.getImports(node, (schemaName) => ({
|
|
29
83
|
name: resolver.resolveSchemaName(schemaName),
|
|
30
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
84
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
31
85
|
}))
|
|
86
|
+
const inputImportEntries = hasCodec
|
|
87
|
+
? [...codecRefNames].map((schemaName) => ({
|
|
88
|
+
name: [resolver.resolveInputSchemaName(schemaName)],
|
|
89
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
90
|
+
}))
|
|
91
|
+
: []
|
|
92
|
+
const seenImports = new Set<string>()
|
|
93
|
+
const imports = [...importEntries, ...inputImportEntries].filter((imp) => {
|
|
94
|
+
const key = `${Array.isArray(imp.name) ? imp.name.join(',') : imp.name}|${imp.path}`
|
|
95
|
+
if (seenImports.has(key)) return false
|
|
96
|
+
seenImports.add(key)
|
|
97
|
+
return true
|
|
98
|
+
})
|
|
32
99
|
|
|
33
100
|
const meta = {
|
|
34
101
|
name: resolver.resolveSchemaName(node.name),
|
|
35
|
-
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
|
|
102
|
+
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
36
103
|
} as const
|
|
37
104
|
|
|
38
|
-
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) :
|
|
39
|
-
|
|
40
|
-
const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
|
|
105
|
+
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null
|
|
41
106
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
: printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
|
|
107
|
+
const stdPrinters = mini ? null : getStdPrinters(resolver, { coercion, guidType, dateType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })
|
|
108
|
+
const schemaPrinter = mini ? getMiniPrinter(resolver, { guidType, wrapOutput, cyclicSchemas, nodes: printer?.nodes }) : stdPrinters!.output
|
|
45
109
|
|
|
46
110
|
return (
|
|
47
111
|
<File
|
|
48
112
|
baseName={meta.file.baseName}
|
|
49
113
|
path={meta.file.path}
|
|
50
114
|
meta={meta.file.meta}
|
|
51
|
-
banner={resolver.resolveBanner(
|
|
52
|
-
footer={resolver.resolveFooter(
|
|
115
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
116
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
53
117
|
>
|
|
54
118
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
55
|
-
{
|
|
119
|
+
{imports.map((imp) => (
|
|
120
|
+
<File.Import key={[node.name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />
|
|
121
|
+
))}
|
|
56
122
|
|
|
57
123
|
<Zod name={meta.name} node={node} printer={schemaPrinter} inferTypeName={inferTypeName} />
|
|
124
|
+
{hasCodec && stdPrinters && (
|
|
125
|
+
<Zod
|
|
126
|
+
name={resolver.resolveInputSchemaName(node.name)}
|
|
127
|
+
node={node}
|
|
128
|
+
printer={stdPrinters.input}
|
|
129
|
+
inferTypeName={inferred ? resolver.resolveInputSchemaTypeName(node.name) : null}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
58
132
|
</File>
|
|
59
133
|
)
|
|
60
134
|
},
|
|
61
135
|
operation(node, ctx) {
|
|
136
|
+
if (!ast.isHttpOperationNode(node)) return null
|
|
62
137
|
const { adapter, config, resolver, root } = ctx
|
|
63
138
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
|
|
64
139
|
const dateType = (adapter as Adapter<AdapterOas>).options.dateType
|
|
65
140
|
|
|
66
|
-
const mode = ctx.getMode(output)
|
|
67
141
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
68
142
|
|
|
69
143
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
70
144
|
|
|
71
145
|
const meta = {
|
|
72
|
-
file: resolver.resolveFile(
|
|
146
|
+
file: resolver.resolveFile(
|
|
147
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
148
|
+
{ root, output, group: group ?? undefined },
|
|
149
|
+
),
|
|
73
150
|
} as const
|
|
74
151
|
|
|
75
|
-
const cyclicSchemas =
|
|
76
|
-
|
|
77
|
-
function renderSchemaEntry({
|
|
152
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
153
|
+
|
|
154
|
+
function renderSchemaEntry({
|
|
155
|
+
schema,
|
|
156
|
+
name,
|
|
157
|
+
keysToOmit,
|
|
158
|
+
direction = 'output',
|
|
159
|
+
}: {
|
|
160
|
+
schema: ast.SchemaNode | null
|
|
161
|
+
name: string
|
|
162
|
+
keysToOmit?: Array<string> | null
|
|
163
|
+
direction?: 'input' | 'output'
|
|
164
|
+
}) {
|
|
78
165
|
if (!schema) return null
|
|
79
166
|
|
|
80
|
-
const inferTypeName = inferred ? resolver.resolveTypeName(name) :
|
|
167
|
+
const inferTypeName = inferred ? resolver.resolveTypeName(name) : null
|
|
81
168
|
|
|
169
|
+
// In the input direction, refs to codec components resolve to their input variant.
|
|
170
|
+
const codecRefNames =
|
|
171
|
+
direction === 'input' && !mini
|
|
172
|
+
? new Set(
|
|
173
|
+
ast.collect<string>(schema, {
|
|
174
|
+
schema: (n) => (n.type === 'ref' && n.ref && containsCodec(n) ? (extractRefName(n.ref) ?? undefined) : undefined),
|
|
175
|
+
}),
|
|
176
|
+
)
|
|
177
|
+
: null
|
|
82
178
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
83
|
-
name: resolver.resolveSchemaName(schemaName),
|
|
84
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
179
|
+
name: codecRefNames?.has(schemaName) ? resolver.resolveInputSchemaName(schemaName) : resolver.resolveSchemaName(schemaName),
|
|
180
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
85
181
|
}))
|
|
86
182
|
|
|
87
183
|
const schemaPrinter = mini
|
|
88
|
-
?
|
|
89
|
-
|
|
184
|
+
? keysToOmit?.length
|
|
185
|
+
? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
|
|
186
|
+
: getMiniPrinter(resolver, { guidType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })
|
|
187
|
+
: keysToOmit?.length
|
|
188
|
+
? printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes, direction })
|
|
189
|
+
: getStdPrinters(resolver, { coercion, guidType, dateType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })[direction]
|
|
90
190
|
|
|
91
191
|
return (
|
|
92
192
|
<>
|
|
93
|
-
{
|
|
94
|
-
|
|
193
|
+
{imports.map((imp) => (
|
|
194
|
+
<File.Import key={[name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />
|
|
195
|
+
))}
|
|
95
196
|
<Zod name={name} node={schema} printer={schemaPrinter} inferTypeName={inferTypeName} />
|
|
96
197
|
</>
|
|
97
198
|
)
|
|
98
199
|
}
|
|
99
200
|
|
|
100
|
-
|
|
201
|
+
// Multiple content types for a single name — emit one schema per content type plus a union alias.
|
|
202
|
+
function buildContentTypeVariants(
|
|
203
|
+
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
|
|
204
|
+
baseName: string,
|
|
205
|
+
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
|
|
206
|
+
direction?: 'input' | 'output',
|
|
207
|
+
) {
|
|
208
|
+
const variants = resolveContentTypeVariants(entries, baseName)
|
|
209
|
+
const unionSchema = ast.createSchema({
|
|
210
|
+
type: 'union',
|
|
211
|
+
members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
|
|
212
|
+
})
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
{variants.map((variant) =>
|
|
216
|
+
renderSchemaEntry({
|
|
217
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
218
|
+
name: variant.name,
|
|
219
|
+
keysToOmit: variant.keysToOmit,
|
|
220
|
+
direction,
|
|
221
|
+
}),
|
|
222
|
+
)}
|
|
223
|
+
{renderSchemaEntry({ schema: unionSchema, name: baseName, direction })}
|
|
224
|
+
</>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param), direction: 'input' }))
|
|
101
229
|
|
|
102
|
-
const responseSchemas = node.responses.map((res) =>
|
|
103
|
-
|
|
104
|
-
|
|
230
|
+
const responseSchemas = node.responses.map((res) => {
|
|
231
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema)
|
|
232
|
+
if (variants.length > 1) {
|
|
233
|
+
return buildContentTypeVariants(res.content!, resolver.resolveResponseStatusName(node, res.statusCode))
|
|
234
|
+
}
|
|
235
|
+
const primary = variants[0] ?? res.content?.[0]
|
|
236
|
+
return renderSchemaEntry({
|
|
237
|
+
schema: primary?.schema ?? null,
|
|
105
238
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
106
|
-
keysToOmit:
|
|
107
|
-
})
|
|
108
|
-
)
|
|
239
|
+
keysToOmit: primary?.keysToOmit,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
109
242
|
|
|
110
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
243
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))
|
|
111
244
|
const responseUnionSchema =
|
|
112
245
|
responsesWithSchema.length > 0
|
|
113
246
|
? (() => {
|
|
@@ -118,14 +251,16 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
118
251
|
// the response union name, skip generation to avoid redeclaration errors.
|
|
119
252
|
const importedNames = new Set(
|
|
120
253
|
responsesWithSchema.flatMap((res) =>
|
|
121
|
-
res.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
254
|
+
(res.content ?? []).flatMap((entry) =>
|
|
255
|
+
entry.schema
|
|
256
|
+
? adapter
|
|
257
|
+
.getImports(entry.schema, (schemaName) => ({
|
|
258
|
+
name: resolver.resolveSchemaName(schemaName),
|
|
259
|
+
path: '',
|
|
260
|
+
}))
|
|
261
|
+
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
|
|
262
|
+
: [],
|
|
263
|
+
),
|
|
129
264
|
),
|
|
130
265
|
)
|
|
131
266
|
|
|
@@ -143,24 +278,37 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
143
278
|
})()
|
|
144
279
|
: null
|
|
145
280
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
281
|
+
const requestBodyContent = node.requestBody?.content ?? []
|
|
282
|
+
const requestSchema = (() => {
|
|
283
|
+
if (requestBodyContent.length === 0) return null
|
|
284
|
+
if (requestBodyContent.length === 1) {
|
|
285
|
+
const entry = requestBodyContent[0]!
|
|
286
|
+
if (!entry.schema) return null
|
|
287
|
+
return renderSchemaEntry({
|
|
288
|
+
schema: { ...entry.schema, description: node.requestBody!.description ?? entry.schema.description },
|
|
152
289
|
name: resolver.resolveDataName(node),
|
|
153
|
-
keysToOmit:
|
|
290
|
+
keysToOmit: entry.keysToOmit,
|
|
291
|
+
direction: 'input',
|
|
154
292
|
})
|
|
155
|
-
|
|
293
|
+
}
|
|
294
|
+
return buildContentTypeVariants(
|
|
295
|
+
requestBodyContent,
|
|
296
|
+
resolver.resolveDataName(node),
|
|
297
|
+
(schema) => ({
|
|
298
|
+
...schema,
|
|
299
|
+
description: node.requestBody!.description ?? schema.description,
|
|
300
|
+
}),
|
|
301
|
+
'input',
|
|
302
|
+
)
|
|
303
|
+
})()
|
|
156
304
|
|
|
157
305
|
return (
|
|
158
306
|
<File
|
|
159
307
|
baseName={meta.file.baseName}
|
|
160
308
|
path={meta.file.path}
|
|
161
309
|
meta={meta.file.meta}
|
|
162
|
-
banner={resolver.resolveBanner(
|
|
163
|
-
footer={resolver.resolveFooter(
|
|
310
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
311
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
164
312
|
>
|
|
165
313
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
166
314
|
{paramSchemas}
|
|
@@ -171,7 +319,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
171
319
|
)
|
|
172
320
|
},
|
|
173
321
|
operations(nodes, ctx) {
|
|
174
|
-
const {
|
|
322
|
+
const { config, resolver, root } = ctx
|
|
175
323
|
const { output, importPath, group, operations, paramsCasing } = ctx.options
|
|
176
324
|
|
|
177
325
|
if (!operations) {
|
|
@@ -180,10 +328,10 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
180
328
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
181
329
|
|
|
182
330
|
const meta = {
|
|
183
|
-
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
|
|
331
|
+
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
184
332
|
} as const
|
|
185
333
|
|
|
186
|
-
const transformedOperations = nodes.map((node) => {
|
|
334
|
+
const transformedOperations = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
187
335
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
188
336
|
|
|
189
337
|
return {
|
|
@@ -193,8 +341,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
193
341
|
})
|
|
194
342
|
|
|
195
343
|
const imports = transformedOperations.flatMap(({ node, data }) => {
|
|
196
|
-
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string
|
|
197
|
-
const opFile = resolver.resolveFile(
|
|
344
|
+
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as Array<string>
|
|
345
|
+
const opFile = resolver.resolveFile(
|
|
346
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
347
|
+
{ root, output, group: group ?? undefined },
|
|
348
|
+
)
|
|
198
349
|
|
|
199
350
|
return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
|
|
200
351
|
})
|
|
@@ -204,8 +355,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
204
355
|
baseName={meta.file.baseName}
|
|
205
356
|
path={meta.file.path}
|
|
206
357
|
meta={meta.file.meta}
|
|
207
|
-
banner={resolver.resolveBanner(
|
|
208
|
-
footer={resolver.resolveFooter(
|
|
358
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
359
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
209
360
|
>
|
|
210
361
|
<File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
211
362
|
{imports}
|
package/src/plugin.ts
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { definePlugin
|
|
1
|
+
import { createGroupConfig } from '@internals/shared'
|
|
2
|
+
import { definePlugin } from '@kubb/core'
|
|
3
3
|
import { zodGenerator } from './generators/zodGenerator.tsx'
|
|
4
4
|
import { resolverZod } from './resolvers/resolverZod.ts'
|
|
5
5
|
import type { PluginZod } from './types.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Canonical plugin name for `@kubb/plugin-zod
|
|
8
|
+
* Canonical plugin name for `@kubb/plugin-zod`. Used for driver lookups and
|
|
9
|
+
* cross-plugin dependency references.
|
|
9
10
|
*/
|
|
10
11
|
export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Generates Zod
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
|
|
15
|
+
* responses at runtime, build form schemas, or feed back into router libraries
|
|
16
|
+
* that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-client` and
|
|
17
|
+
* set the client's `parser: 'zod'` to validate every response automatically.
|
|
16
18
|
*
|
|
17
|
-
* @example
|
|
19
|
+
* @example
|
|
18
20
|
* ```ts
|
|
19
|
-
* import
|
|
21
|
+
* import { defineConfig } from 'kubb'
|
|
22
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
23
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
24
|
+
*
|
|
20
25
|
* export default defineConfig({
|
|
21
|
-
*
|
|
26
|
+
* input: { path: './petStore.yaml' },
|
|
27
|
+
* output: { path: './src/gen' },
|
|
28
|
+
* plugins: [
|
|
29
|
+
* pluginTs(),
|
|
30
|
+
* pluginZod({
|
|
31
|
+
* output: { path: './zod' },
|
|
32
|
+
* typed: true,
|
|
33
|
+
* }),
|
|
34
|
+
* ],
|
|
22
35
|
* })
|
|
23
36
|
* ```
|
|
24
37
|
*/
|
|
25
38
|
export const pluginZod = definePlugin<PluginZod>((options) => {
|
|
26
39
|
const {
|
|
27
|
-
output = { path: 'zod',
|
|
40
|
+
output = { path: 'zod', barrel: { type: 'named' } },
|
|
28
41
|
group,
|
|
29
42
|
exclude = [],
|
|
30
43
|
include,
|
|
@@ -44,17 +57,7 @@ export const pluginZod = definePlugin<PluginZod>((options) => {
|
|
|
44
57
|
generators: userGenerators = [],
|
|
45
58
|
} = options
|
|
46
59
|
|
|
47
|
-
const groupConfig = group
|
|
48
|
-
? ({
|
|
49
|
-
...group,
|
|
50
|
-
name: (ctx) => {
|
|
51
|
-
if (group.type === 'path') {
|
|
52
|
-
return `${ctx.group.split('/')[1]}`
|
|
53
|
-
}
|
|
54
|
-
return `${camelCase(ctx.group)}Controller`
|
|
55
|
-
},
|
|
56
|
-
} satisfies Group)
|
|
57
|
-
: undefined
|
|
60
|
+
const groupConfig = createGroupConfig(group)
|
|
58
61
|
|
|
59
62
|
return {
|
|
60
63
|
name: pluginZodName,
|