@kubb/plugin-zod 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 +25 -5
- package/dist/index.cjs +644 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +103 -34
- package/dist/index.js +640 -199
- package/dist/index.js.map +1 -1
- package/extension.yaml +968 -0
- package/package.json +10 -13
- package/src/components/Operations.tsx +6 -5
- package/src/components/Zod.tsx +1 -1
- package/src/generators/zodGenerator.tsx +210 -60
- package/src/plugin.ts +23 -20
- package/src/printers/printerZod.ts +91 -68
- package/src/printers/printerZodMini.ts +46 -49
- package/src/resolvers/resolverZod.ts +31 -19
- package/src/types.ts +57 -21
- package/src/utils.ts +113 -36
package/package.json
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-zod",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "Zod
|
|
3
|
+
"version": "5.0.0-beta.31",
|
|
4
|
+
"description": "Generate Zod validation schemas from your OpenAPI specification for runtime data parsing and type safety. Pairs perfectly with @kubb/plugin-ts for end-to-end type coverage.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"code-
|
|
6
|
+
"code-generation",
|
|
7
7
|
"codegen",
|
|
8
8
|
"kubb",
|
|
9
|
-
"oas",
|
|
10
9
|
"openapi",
|
|
11
|
-
"
|
|
10
|
+
"parse",
|
|
12
11
|
"runtime-validation",
|
|
13
12
|
"schema",
|
|
14
|
-
"schema-validation",
|
|
15
13
|
"swagger",
|
|
16
|
-
"type-safe",
|
|
17
|
-
"type-safety",
|
|
18
14
|
"typescript",
|
|
19
15
|
"validation",
|
|
20
16
|
"zod"
|
|
@@ -30,7 +26,7 @@
|
|
|
30
26
|
"src",
|
|
31
27
|
"templates",
|
|
32
28
|
"dist",
|
|
33
|
-
"
|
|
29
|
+
"extension.yaml",
|
|
34
30
|
"!/**/**.test.**",
|
|
35
31
|
"!/**/__tests__/**",
|
|
36
32
|
"!/**/__snapshots__/**"
|
|
@@ -52,15 +48,16 @@
|
|
|
52
48
|
"registry": "https://registry.npmjs.org/"
|
|
53
49
|
},
|
|
54
50
|
"dependencies": {
|
|
55
|
-
"@kubb/core": "5.0.0-beta.
|
|
56
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
57
|
-
"remeda": "^2.34.
|
|
51
|
+
"@kubb/core": "5.0.0-beta.31",
|
|
52
|
+
"@kubb/renderer-jsx": "5.0.0-beta.31",
|
|
53
|
+
"remeda": "^2.34.1"
|
|
58
54
|
},
|
|
59
55
|
"devDependencies": {
|
|
56
|
+
"@internals/shared": "0.0.0",
|
|
60
57
|
"@internals/utils": "0.0.0"
|
|
61
58
|
},
|
|
62
59
|
"peerDependencies": {
|
|
63
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
60
|
+
"@kubb/renderer-jsx": "5.0.0-beta.31"
|
|
64
61
|
},
|
|
65
62
|
"size-limit": [
|
|
66
63
|
{
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { stringifyObject } from '@internals/utils'
|
|
2
|
-
import
|
|
2
|
+
import { ast } from '@kubb/core'
|
|
3
3
|
import { Const, File, Type } from '@kubb/renderer-jsx'
|
|
4
4
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
5
5
|
|
|
6
6
|
type SchemaNames = {
|
|
7
|
-
request: string |
|
|
7
|
+
request: string | null
|
|
8
8
|
parameters: {
|
|
9
|
-
path: string |
|
|
10
|
-
query: string |
|
|
11
|
-
header: string |
|
|
9
|
+
path: string | null
|
|
10
|
+
query: string | null
|
|
11
|
+
header: string | null
|
|
12
12
|
}
|
|
13
13
|
responses: { default?: string } & Record<number | string, string>
|
|
14
14
|
errors: Record<number | string, string>
|
|
@@ -30,6 +30,7 @@ export function Operations({ name, operations }: Props): KubbReactNode {
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
const pathsJSON = operations.reduce<Record<string, Record<string, string>>>((prev, acc) => {
|
|
33
|
+
if (!ast.isHttpOperationNode(acc.node)) return prev
|
|
33
34
|
prev[`"${acc.node.path}"`] = {
|
|
34
35
|
...(prev[`"${acc.node.path}"`] ?? {}),
|
|
35
36
|
[acc.node.method]: `operations["${acc.node.operationId}"]`,
|
package/src/components/Zod.tsx
CHANGED
|
@@ -13,7 +13,7 @@ type Props = {
|
|
|
13
13
|
* then merges in any user-supplied `printer.nodes` overrides.
|
|
14
14
|
*/
|
|
15
15
|
printer: ast.Printer<PrinterZodFactory> | ast.Printer<PrinterZodMiniFactory>
|
|
16
|
-
inferTypeName?: string
|
|
16
|
+
inferTypeName?: string | null
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function Zod({ name, node, printer, inferTypeName }: Props): KubbReactNode {
|
|
@@ -1,18 +1,60 @@
|
|
|
1
|
+
import { resolveContentTypeVariants } from '@internals/shared'
|
|
1
2
|
import type { Adapter } from '@kubb/core'
|
|
2
3
|
import { ast, defineGenerator } from '@kubb/core'
|
|
3
4
|
import type { AdapterOas } from '@kubb/adapter-oas'
|
|
4
|
-
import { File,
|
|
5
|
+
import { File, jsxRendererSync } from '@kubb/renderer-jsx'
|
|
5
6
|
import { Operations } from '../components/Operations.tsx'
|
|
6
7
|
import { Zod } from '../components/Zod.tsx'
|
|
7
8
|
import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
|
|
8
9
|
import { printerZod } from '../printers/printerZod.ts'
|
|
9
10
|
import { printerZodMini } from '../printers/printerZodMini.ts'
|
|
10
|
-
import type { PluginZod } from '../types'
|
|
11
|
-
import { buildSchemaNames } from '../utils.ts'
|
|
12
|
-
|
|
11
|
+
import type { PluginZod, ResolverZod } from '../types'
|
|
12
|
+
import { buildSchemaNames, containsCodec } from '../utils.ts'
|
|
13
|
+
|
|
14
|
+
type StdPrinters = { output: ReturnType<typeof printerZod>; input: ReturnType<typeof printerZod> }
|
|
15
|
+
type ZodPrinterEntry = StdPrinters & { coercion: unknown; guidType: unknown; dateType: unknown }
|
|
16
|
+
type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidType: unknown }
|
|
17
|
+
|
|
18
|
+
// Per-build caches: keyed on resolver (unique per plugin instance per build, GC'd when released)
|
|
19
|
+
const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
|
|
20
|
+
const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
|
|
21
|
+
|
|
22
|
+
type StdPrinterParams = { coercion: unknown; guidType: unknown; dateType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the cached `output`/`input` direction printers for a resolver, building them on
|
|
26
|
+
* first use. The `input` printer encodes `Date → string` for request bodies; `output` decodes
|
|
27
|
+
* `string → Date` for responses. Schemas without `dateType: 'date'` fields print identically.
|
|
28
|
+
*/
|
|
29
|
+
function getStdPrinters(resolver: ResolverZod, params: StdPrinterParams): StdPrinters {
|
|
30
|
+
const cached = zodPrinterCache.get(resolver)
|
|
31
|
+
if (cached && cached.coercion === params.coercion && cached.guidType === params.guidType && cached.dateType === params.dateType) {
|
|
32
|
+
return { output: cached.output, input: cached.input }
|
|
33
|
+
}
|
|
34
|
+
const base = { ...params, resolver } as Parameters<typeof printerZod>[0]
|
|
35
|
+
const output = printerZod({ ...base, direction: 'output' })
|
|
36
|
+
const input = printerZod({ ...base, direction: 'input' })
|
|
37
|
+
zodPrinterCache.set(resolver, { output, input, coercion: params.coercion, guidType: params.guidType, dateType: params.dateType })
|
|
38
|
+
return { output, input }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getMiniPrinter(resolver: ResolverZod, params: { guidType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }) {
|
|
42
|
+
const cached = zodMiniPrinterCache.get(resolver)
|
|
43
|
+
if (cached && cached.guidType === params.guidType) return cached.printer
|
|
44
|
+
const p = printerZodMini({ ...params, resolver } as Parameters<typeof printerZodMini>[0])
|
|
45
|
+
zodMiniPrinterCache.set(resolver, { printer: p, guidType: params.guidType })
|
|
46
|
+
return p
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
|
|
51
|
+
* schema in the spec plus per-operation request/response/parameter schemas.
|
|
52
|
+
* When `mini: true`, schemas use the Zod Mini functional API instead of
|
|
53
|
+
* chainable methods.
|
|
54
|
+
*/
|
|
13
55
|
export const zodGenerator = defineGenerator<PluginZod>({
|
|
14
56
|
name: 'zod',
|
|
15
|
-
renderer:
|
|
57
|
+
renderer: jsxRendererSync,
|
|
16
58
|
schema(node, ctx) {
|
|
17
59
|
const { adapter, config, resolver, root } = ctx
|
|
18
60
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
|
|
@@ -24,41 +66,73 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
24
66
|
|
|
25
67
|
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) ? (ast.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
|
-
{mode === 'split' &&
|
|
119
|
+
{mode === 'split' &&
|
|
120
|
+
imports.map((imp) => <File.Import key={[node.name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
|
|
56
121
|
|
|
57
122
|
<Zod name={meta.name} node={node} printer={schemaPrinter} inferTypeName={inferTypeName} />
|
|
123
|
+
{hasCodec && stdPrinters && (
|
|
124
|
+
<Zod
|
|
125
|
+
name={resolver.resolveInputSchemaName(node.name)}
|
|
126
|
+
node={node}
|
|
127
|
+
printer={stdPrinters.input}
|
|
128
|
+
inferTypeName={inferred ? resolver.resolveInputSchemaTypeName(node.name) : null}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
58
131
|
</File>
|
|
59
132
|
)
|
|
60
133
|
},
|
|
61
134
|
operation(node, ctx) {
|
|
135
|
+
if (!ast.isHttpOperationNode(node)) return null
|
|
62
136
|
const { adapter, config, resolver, root } = ctx
|
|
63
137
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
|
|
64
138
|
const dateType = (adapter as Adapter<AdapterOas>).options.dateType
|
|
@@ -69,24 +143,50 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
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) ? (ast.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
|
<>
|
|
@@ -97,17 +197,49 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
97
197
|
)
|
|
98
198
|
}
|
|
99
199
|
|
|
100
|
-
|
|
200
|
+
// Multiple content types for a single name — emit one schema per content type plus a union alias.
|
|
201
|
+
function buildContentTypeVariants(
|
|
202
|
+
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
|
|
203
|
+
baseName: string,
|
|
204
|
+
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
|
|
205
|
+
direction?: 'input' | 'output',
|
|
206
|
+
) {
|
|
207
|
+
const variants = resolveContentTypeVariants(entries, baseName)
|
|
208
|
+
const unionSchema = ast.createSchema({
|
|
209
|
+
type: 'union',
|
|
210
|
+
members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
|
|
211
|
+
})
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{variants.map((variant) =>
|
|
215
|
+
renderSchemaEntry({
|
|
216
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
217
|
+
name: variant.name,
|
|
218
|
+
keysToOmit: variant.keysToOmit,
|
|
219
|
+
direction,
|
|
220
|
+
}),
|
|
221
|
+
)}
|
|
222
|
+
{renderSchemaEntry({ schema: unionSchema, name: baseName, direction })}
|
|
223
|
+
</>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param), direction: 'input' }))
|
|
101
228
|
|
|
102
|
-
const responseSchemas = node.responses.map((res) =>
|
|
103
|
-
|
|
104
|
-
|
|
229
|
+
const responseSchemas = node.responses.map((res) => {
|
|
230
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema)
|
|
231
|
+
if (variants.length > 1) {
|
|
232
|
+
return buildContentTypeVariants(res.content!, resolver.resolveResponseStatusName(node, res.statusCode))
|
|
233
|
+
}
|
|
234
|
+
const primary = variants[0] ?? res.content?.[0]
|
|
235
|
+
return renderSchemaEntry({
|
|
236
|
+
schema: primary?.schema ?? null,
|
|
105
237
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
106
|
-
keysToOmit:
|
|
107
|
-
})
|
|
108
|
-
)
|
|
238
|
+
keysToOmit: primary?.keysToOmit,
|
|
239
|
+
})
|
|
240
|
+
})
|
|
109
241
|
|
|
110
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
242
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))
|
|
111
243
|
const responseUnionSchema =
|
|
112
244
|
responsesWithSchema.length > 0
|
|
113
245
|
? (() => {
|
|
@@ -118,14 +250,16 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
118
250
|
// the response union name, skip generation to avoid redeclaration errors.
|
|
119
251
|
const importedNames = new Set(
|
|
120
252
|
responsesWithSchema.flatMap((res) =>
|
|
121
|
-
res.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
253
|
+
(res.content ?? []).flatMap((entry) =>
|
|
254
|
+
entry.schema
|
|
255
|
+
? adapter
|
|
256
|
+
.getImports(entry.schema, (schemaName) => ({
|
|
257
|
+
name: resolver.resolveSchemaName(schemaName),
|
|
258
|
+
path: '',
|
|
259
|
+
}))
|
|
260
|
+
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
|
|
261
|
+
: [],
|
|
262
|
+
),
|
|
129
263
|
),
|
|
130
264
|
)
|
|
131
265
|
|
|
@@ -143,24 +277,37 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
143
277
|
})()
|
|
144
278
|
: null
|
|
145
279
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
280
|
+
const requestBodyContent = node.requestBody?.content ?? []
|
|
281
|
+
const requestSchema = (() => {
|
|
282
|
+
if (requestBodyContent.length === 0) return null
|
|
283
|
+
if (requestBodyContent.length === 1) {
|
|
284
|
+
const entry = requestBodyContent[0]!
|
|
285
|
+
if (!entry.schema) return null
|
|
286
|
+
return renderSchemaEntry({
|
|
287
|
+
schema: { ...entry.schema, description: node.requestBody!.description ?? entry.schema.description },
|
|
152
288
|
name: resolver.resolveDataName(node),
|
|
153
|
-
keysToOmit:
|
|
289
|
+
keysToOmit: entry.keysToOmit,
|
|
290
|
+
direction: 'input',
|
|
154
291
|
})
|
|
155
|
-
|
|
292
|
+
}
|
|
293
|
+
return buildContentTypeVariants(
|
|
294
|
+
requestBodyContent,
|
|
295
|
+
resolver.resolveDataName(node),
|
|
296
|
+
(schema) => ({
|
|
297
|
+
...schema,
|
|
298
|
+
description: node.requestBody!.description ?? schema.description,
|
|
299
|
+
}),
|
|
300
|
+
'input',
|
|
301
|
+
)
|
|
302
|
+
})()
|
|
156
303
|
|
|
157
304
|
return (
|
|
158
305
|
<File
|
|
159
306
|
baseName={meta.file.baseName}
|
|
160
307
|
path={meta.file.path}
|
|
161
308
|
meta={meta.file.meta}
|
|
162
|
-
banner={resolver.resolveBanner(
|
|
163
|
-
footer={resolver.resolveFooter(
|
|
309
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
310
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
164
311
|
>
|
|
165
312
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
166
313
|
{paramSchemas}
|
|
@@ -171,7 +318,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
171
318
|
)
|
|
172
319
|
},
|
|
173
320
|
operations(nodes, ctx) {
|
|
174
|
-
const {
|
|
321
|
+
const { config, resolver, root } = ctx
|
|
175
322
|
const { output, importPath, group, operations, paramsCasing } = ctx.options
|
|
176
323
|
|
|
177
324
|
if (!operations) {
|
|
@@ -180,10 +327,10 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
180
327
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
181
328
|
|
|
182
329
|
const meta = {
|
|
183
|
-
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
|
|
330
|
+
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
184
331
|
} as const
|
|
185
332
|
|
|
186
|
-
const transformedOperations = nodes.map((node) => {
|
|
333
|
+
const transformedOperations = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
187
334
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
188
335
|
|
|
189
336
|
return {
|
|
@@ -193,8 +340,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
193
340
|
})
|
|
194
341
|
|
|
195
342
|
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(
|
|
343
|
+
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as Array<string>
|
|
344
|
+
const opFile = resolver.resolveFile(
|
|
345
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
346
|
+
{ root, output, group: group ?? undefined },
|
|
347
|
+
)
|
|
198
348
|
|
|
199
349
|
return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
|
|
200
350
|
})
|
|
@@ -204,8 +354,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
204
354
|
baseName={meta.file.baseName}
|
|
205
355
|
path={meta.file.path}
|
|
206
356
|
meta={meta.file.meta}
|
|
207
|
-
banner={resolver.resolveBanner(
|
|
208
|
-
footer={resolver.resolveFooter(
|
|
357
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
358
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
209
359
|
>
|
|
210
360
|
<File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
211
361
|
{imports}
|
package/src/plugin.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
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
|
*/
|
|
@@ -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, { suffix: 'Controller' })
|
|
58
61
|
|
|
59
62
|
return {
|
|
60
63
|
name: pluginZodName,
|