@kubb/plugin-zod 5.0.0-alpha.23 → 5.0.0-alpha.25
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/dist/index.cjs +1673 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +317 -2
- package/dist/index.js +1646 -88
- package/dist/index.js.map +1 -1
- package/package.json +5 -33
- package/src/components/Operations.tsx +22 -15
- package/src/components/Zod.tsx +18 -118
- package/src/components/ZodMini.tsx +41 -0
- package/src/constants.ts +5 -0
- package/src/generators/zodGenerator.tsx +165 -158
- package/src/generators/zodGeneratorLegacy.tsx +401 -0
- package/src/index.ts +11 -1
- package/src/plugin.ts +105 -129
- package/src/presets.ts +25 -0
- package/src/printers/printerZod.ts +271 -0
- package/src/printers/printerZodMini.ts +246 -0
- package/src/resolvers/resolverZod.ts +71 -0
- package/src/resolvers/resolverZodLegacy.ts +60 -0
- package/src/types.ts +121 -92
- package/src/utils.ts +248 -0
- package/dist/components-DW4Q2yVq.js +0 -868
- package/dist/components-DW4Q2yVq.js.map +0 -1
- package/dist/components-qFUGs0vU.cjs +0 -916
- package/dist/components-qFUGs0vU.cjs.map +0 -1
- package/dist/components.cjs +0 -4
- package/dist/components.d.ts +0 -56
- package/dist/components.js +0 -2
- package/dist/generators-C9BCTLXg.cjs +0 -301
- package/dist/generators-C9BCTLXg.cjs.map +0 -1
- package/dist/generators-maqx12yN.js +0 -290
- package/dist/generators-maqx12yN.js.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -12
- package/dist/generators.js +0 -2
- package/dist/templates/ToZod.source.cjs +0 -7
- package/dist/templates/ToZod.source.cjs.map +0 -1
- package/dist/templates/ToZod.source.d.ts +0 -7
- package/dist/templates/ToZod.source.js +0 -6
- package/dist/templates/ToZod.source.js.map +0 -1
- package/dist/types-CClg-ikj.d.ts +0 -172
- package/src/components/index.ts +0 -2
- package/src/generators/index.ts +0 -2
- package/src/generators/operationsGenerator.tsx +0 -50
- package/src/parser.ts +0 -952
- package/src/templates/ToZod.source.ts +0 -4
- package/templates/ToZod.ts +0 -61
package/src/presets.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { definePresets } from '@kubb/core'
|
|
2
|
+
import { zodGenerator } from './generators/zodGenerator.tsx'
|
|
3
|
+
import { zodGeneratorLegacy } from './generators/zodGeneratorLegacy.tsx'
|
|
4
|
+
import { resolverZod } from './resolvers/resolverZod.ts'
|
|
5
|
+
import { resolverZodLegacy } from './resolvers/resolverZodLegacy.ts'
|
|
6
|
+
import type { ResolverZod } from './types.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in preset registry for `@kubb/plugin-zod`.
|
|
10
|
+
*
|
|
11
|
+
* - `default` — uses `resolverZod` and `zodGenerator` (current naming conventions).
|
|
12
|
+
* - `kubbV4` — uses `resolverZodLegacy` and `zodGeneratorLegacy` (Kubb v4 naming conventions).
|
|
13
|
+
*/
|
|
14
|
+
export const presets = definePresets<ResolverZod>({
|
|
15
|
+
default: {
|
|
16
|
+
name: 'default',
|
|
17
|
+
resolvers: [resolverZod],
|
|
18
|
+
generators: [zodGenerator],
|
|
19
|
+
},
|
|
20
|
+
kubbV4: {
|
|
21
|
+
name: 'kubbV4',
|
|
22
|
+
resolvers: [resolverZodLegacy],
|
|
23
|
+
generators: [zodGeneratorLegacy],
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { stringify } from '@internals/utils'
|
|
2
|
+
import { createSchema, extractRefName, narrowSchema, syncSchemaRef } from '@kubb/ast'
|
|
3
|
+
import type { PrinterFactoryOptions } from '@kubb/core'
|
|
4
|
+
import { definePrinter } from '@kubb/core'
|
|
5
|
+
import type { PluginZod, ResolverZod } from '../types.ts'
|
|
6
|
+
import { applyModifiers, containsSelfRef, formatLiteral, lengthConstraints, numberConstraints, shouldCoerce } from '../utils.ts'
|
|
7
|
+
|
|
8
|
+
export type ZodOptions = {
|
|
9
|
+
coercion?: PluginZod['resolvedOptions']['coercion']
|
|
10
|
+
guidType?: PluginZod['resolvedOptions']['guidType']
|
|
11
|
+
wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
|
|
12
|
+
resolver?: ResolverZod
|
|
13
|
+
schemaName?: string
|
|
14
|
+
/**
|
|
15
|
+
* Property keys to exclude from the generated object schema via `.omit({ key: true })`.
|
|
16
|
+
*/
|
|
17
|
+
keysToOmit?: Array<string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ZodPrinterFactory = PrinterFactoryOptions<'zod', ZodOptions, string, string>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Zod v4 printer built with `definePrinter`.
|
|
24
|
+
*
|
|
25
|
+
* Converts a `SchemaNode` AST into a **standard** Zod v4 code string
|
|
26
|
+
* using the chainable method API (`.optional()`, `.nullable()`, etc.).
|
|
27
|
+
*
|
|
28
|
+
* For the `zod/mini` functional API, see {@link printerZodMini}.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const printer = printerZod({ coercion: false })
|
|
33
|
+
* const code = printer.print(stringSchemaNode) // "z.string()"
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const printerZod = definePrinter<ZodPrinterFactory>((options) => {
|
|
37
|
+
return {
|
|
38
|
+
name: 'zod',
|
|
39
|
+
options,
|
|
40
|
+
nodes: {
|
|
41
|
+
any: () => 'z.any()',
|
|
42
|
+
unknown: () => 'z.unknown()',
|
|
43
|
+
void: () => 'z.void()',
|
|
44
|
+
never: () => 'z.never()',
|
|
45
|
+
boolean: () => 'z.boolean()',
|
|
46
|
+
null: () => 'z.null()',
|
|
47
|
+
string(node) {
|
|
48
|
+
const base = shouldCoerce(this.options.coercion, 'strings') ? 'z.coerce.string()' : 'z.string()'
|
|
49
|
+
|
|
50
|
+
return `${base}${lengthConstraints(node)}`
|
|
51
|
+
},
|
|
52
|
+
number(node) {
|
|
53
|
+
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number()' : 'z.number()'
|
|
54
|
+
|
|
55
|
+
return `${base}${numberConstraints(node)}`
|
|
56
|
+
},
|
|
57
|
+
integer(node) {
|
|
58
|
+
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number().int()' : 'z.int()'
|
|
59
|
+
|
|
60
|
+
return `${base}${numberConstraints(node)}`
|
|
61
|
+
},
|
|
62
|
+
bigint() {
|
|
63
|
+
return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
|
|
64
|
+
},
|
|
65
|
+
date(node) {
|
|
66
|
+
if (node.representation === 'string') {
|
|
67
|
+
return 'z.iso.date()'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
|
|
71
|
+
},
|
|
72
|
+
datetime(node) {
|
|
73
|
+
if (node.offset) return 'z.iso.datetime({ offset: true })'
|
|
74
|
+
if (node.local) return 'z.iso.datetime({ local: true })'
|
|
75
|
+
|
|
76
|
+
return 'z.iso.datetime()'
|
|
77
|
+
},
|
|
78
|
+
time(node) {
|
|
79
|
+
if (node.representation === 'string') {
|
|
80
|
+
return 'z.iso.time()'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
|
|
84
|
+
},
|
|
85
|
+
uuid(node) {
|
|
86
|
+
const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
|
|
87
|
+
|
|
88
|
+
return `${base}${lengthConstraints(node)}`
|
|
89
|
+
},
|
|
90
|
+
email(node) {
|
|
91
|
+
return `z.email()${lengthConstraints(node)}`
|
|
92
|
+
},
|
|
93
|
+
url(node) {
|
|
94
|
+
return `z.url()${lengthConstraints(node)}`
|
|
95
|
+
},
|
|
96
|
+
ipv4: () => 'z.ipv4()',
|
|
97
|
+
ipv6: () => 'z.ipv6()',
|
|
98
|
+
blob: () => 'z.instanceof(File)',
|
|
99
|
+
enum(node) {
|
|
100
|
+
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
101
|
+
const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
|
|
102
|
+
|
|
103
|
+
// asConst-style enum: use z.union([z.literal(…), …])
|
|
104
|
+
if (node.namedEnumValues?.length) {
|
|
105
|
+
const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
|
|
106
|
+
|
|
107
|
+
if (literals.length === 1) return literals[0]!
|
|
108
|
+
return `z.union([${literals.join(', ')}])`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Regular enum: use z.enum([…])
|
|
112
|
+
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
113
|
+
},
|
|
114
|
+
ref(node) {
|
|
115
|
+
if (!node.name) return undefined
|
|
116
|
+
const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
|
|
117
|
+
const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
|
|
118
|
+
const isSelfRef = node.ref && this.options.schemaName != null && resolvedName === this.options.schemaName
|
|
119
|
+
|
|
120
|
+
if (isSelfRef) {
|
|
121
|
+
return `z.lazy(() => ${resolvedName})`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return resolvedName
|
|
125
|
+
},
|
|
126
|
+
object(node) {
|
|
127
|
+
const properties = node.properties
|
|
128
|
+
.map((prop) => {
|
|
129
|
+
const { name: propName, schema } = prop
|
|
130
|
+
|
|
131
|
+
const meta = syncSchemaRef(schema)
|
|
132
|
+
|
|
133
|
+
const isNullable = meta.nullable
|
|
134
|
+
const isOptional = schema.optional
|
|
135
|
+
const isNullish = schema.nullish
|
|
136
|
+
|
|
137
|
+
const hasSelfRef =
|
|
138
|
+
this.options.schemaName != null && containsSelfRef(schema, { schemaName: this.options.schemaName, resolver: this.options.resolver })
|
|
139
|
+
const baseOutput = this.transform(schema) ?? this.transform(createSchema({ type: 'unknown' }))!
|
|
140
|
+
// Strip z.lazy() wrappers inside object getters — the getter itself provides deferred evaluation
|
|
141
|
+
const resolvedOutput = hasSelfRef ? baseOutput.replaceAll(`z.lazy(() => ${this.options.schemaName})`, this.options.schemaName!) : baseOutput
|
|
142
|
+
|
|
143
|
+
const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: resolvedOutput, schema }) || resolvedOutput : resolvedOutput
|
|
144
|
+
|
|
145
|
+
const value = applyModifiers({
|
|
146
|
+
value: wrappedOutput,
|
|
147
|
+
nullable: isNullable,
|
|
148
|
+
optional: isOptional,
|
|
149
|
+
nullish: isNullish,
|
|
150
|
+
defaultValue: meta.default,
|
|
151
|
+
description: meta.description,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (hasSelfRef) {
|
|
155
|
+
return `get "${propName}"() { return ${value} }`
|
|
156
|
+
}
|
|
157
|
+
return `"${propName}": ${value}`
|
|
158
|
+
})
|
|
159
|
+
.join(',\n ')
|
|
160
|
+
|
|
161
|
+
let result = `z.object({\n ${properties}\n })`
|
|
162
|
+
|
|
163
|
+
// Handle additionalProperties as .catchall() or .strict()
|
|
164
|
+
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
165
|
+
const catchallType = this.transform(node.additionalProperties)
|
|
166
|
+
if (catchallType) {
|
|
167
|
+
result += `.catchall(${catchallType})`
|
|
168
|
+
}
|
|
169
|
+
} else if (node.additionalProperties === true) {
|
|
170
|
+
result += `.catchall(${this.transform(createSchema({ type: 'unknown' }))})`
|
|
171
|
+
} else if (node.additionalProperties === false) {
|
|
172
|
+
result += '.strict()'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
},
|
|
177
|
+
array(node) {
|
|
178
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
179
|
+
const inner = items.join(', ') || this.transform(createSchema({ type: 'unknown' }))!
|
|
180
|
+
let result = `z.array(${inner})${lengthConstraints(node)}`
|
|
181
|
+
|
|
182
|
+
if (node.unique) {
|
|
183
|
+
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
},
|
|
188
|
+
tuple(node) {
|
|
189
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
190
|
+
|
|
191
|
+
return `z.tuple([${items.join(', ')}])`
|
|
192
|
+
},
|
|
193
|
+
union(node) {
|
|
194
|
+
const nodeMembers = node.members ?? []
|
|
195
|
+
const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
|
|
196
|
+
if (members.length === 0) return ''
|
|
197
|
+
if (members.length === 1) return members[0]!
|
|
198
|
+
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
199
|
+
// z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
|
|
200
|
+
// assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
|
|
201
|
+
return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `z.union([${members.join(', ')}])`
|
|
205
|
+
},
|
|
206
|
+
intersection(node) {
|
|
207
|
+
const members = node.members ?? []
|
|
208
|
+
if (members.length === 0) return ''
|
|
209
|
+
|
|
210
|
+
const [first, ...rest] = members
|
|
211
|
+
if (!first) return ''
|
|
212
|
+
|
|
213
|
+
let base = this.transform(first)
|
|
214
|
+
if (!base) return ''
|
|
215
|
+
|
|
216
|
+
for (const member of rest) {
|
|
217
|
+
if (member.primitive === 'string') {
|
|
218
|
+
const s = narrowSchema(member, 'string')
|
|
219
|
+
const c = lengthConstraints(s ?? {})
|
|
220
|
+
if (c) {
|
|
221
|
+
base += c
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
} else if (member.primitive === 'number' || member.primitive === 'integer') {
|
|
225
|
+
const n = narrowSchema(member, 'number') ?? narrowSchema(member, 'integer')
|
|
226
|
+
const c = numberConstraints(n ?? {})
|
|
227
|
+
if (c) {
|
|
228
|
+
base += c
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
} else if (member.primitive === 'array') {
|
|
232
|
+
const a = narrowSchema(member, 'array')
|
|
233
|
+
const c = lengthConstraints(a ?? {})
|
|
234
|
+
if (c) {
|
|
235
|
+
base += c
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const transformed = this.transform(member)
|
|
240
|
+
if (transformed) base = `${base}.and(${transformed})`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return base
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
print(node) {
|
|
247
|
+
const { keysToOmit } = this.options
|
|
248
|
+
|
|
249
|
+
let base = this.transform(node)
|
|
250
|
+
if (!base) return null
|
|
251
|
+
|
|
252
|
+
const meta = syncSchemaRef(node)
|
|
253
|
+
|
|
254
|
+
if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
|
|
255
|
+
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
256
|
+
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
257
|
+
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
258
|
+
base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(', ')} })`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return applyModifiers({
|
|
262
|
+
value: base,
|
|
263
|
+
nullable: meta.nullable,
|
|
264
|
+
optional: meta.optional,
|
|
265
|
+
nullish: meta.nullish,
|
|
266
|
+
defaultValue: meta.default,
|
|
267
|
+
description: meta.description,
|
|
268
|
+
})
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
})
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { stringify } from '@internals/utils'
|
|
2
|
+
import { createSchema, extractRefName, narrowSchema, syncSchemaRef } from '@kubb/ast'
|
|
3
|
+
import type { PrinterFactoryOptions } from '@kubb/core'
|
|
4
|
+
import { definePrinter } from '@kubb/core'
|
|
5
|
+
import type { PluginZod, ResolverZod } from '../types.ts'
|
|
6
|
+
import { applyMiniModifiers, containsSelfRef, formatLiteral, lengthChecksMini, numberChecksMini } from '../utils.ts'
|
|
7
|
+
|
|
8
|
+
export type ZodMiniOptions = {
|
|
9
|
+
guidType?: PluginZod['resolvedOptions']['guidType']
|
|
10
|
+
wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
|
|
11
|
+
resolver?: ResolverZod
|
|
12
|
+
schemaName?: string
|
|
13
|
+
/**
|
|
14
|
+
* Property keys to exclude from the generated object schema via `.omit({ key: true })`.
|
|
15
|
+
*/
|
|
16
|
+
keysToOmit?: Array<string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ZodMiniPrinterFactory = PrinterFactoryOptions<'zod-mini', ZodMiniOptions, string, string>
|
|
20
|
+
/**
|
|
21
|
+
* Zod v4 **Mini** printer built with `definePrinter`.
|
|
22
|
+
*
|
|
23
|
+
* Converts a `SchemaNode` AST into a Zod v4 Mini code string using the
|
|
24
|
+
* functional API (`z.optional(z.string())`) for better tree-shaking.
|
|
25
|
+
*
|
|
26
|
+
* For the standard chainable API, see {@link printerZod}.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const printer = printerZodMini({})
|
|
31
|
+
* const code = printer.print(optionalStringNode) // "z.optional(z.string())"
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const printerZodMini = definePrinter<ZodMiniPrinterFactory>((options) => {
|
|
35
|
+
return {
|
|
36
|
+
name: 'zod-mini',
|
|
37
|
+
options,
|
|
38
|
+
nodes: {
|
|
39
|
+
any: () => 'z.any()',
|
|
40
|
+
unknown: () => 'z.unknown()',
|
|
41
|
+
void: () => 'z.void()',
|
|
42
|
+
never: () => 'z.never()',
|
|
43
|
+
boolean: () => 'z.boolean()',
|
|
44
|
+
null: () => 'z.null()',
|
|
45
|
+
string(node) {
|
|
46
|
+
return `z.string()${lengthChecksMini(node)}`
|
|
47
|
+
},
|
|
48
|
+
number(node) {
|
|
49
|
+
return `z.number()${numberChecksMini(node)}`
|
|
50
|
+
},
|
|
51
|
+
integer(node) {
|
|
52
|
+
return `z.int()${numberChecksMini(node)}`
|
|
53
|
+
},
|
|
54
|
+
bigint(node) {
|
|
55
|
+
return `z.bigint()${numberChecksMini(node)}`
|
|
56
|
+
},
|
|
57
|
+
date(node) {
|
|
58
|
+
if (node.representation === 'string') {
|
|
59
|
+
return 'z.iso.date()'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return 'z.date()'
|
|
63
|
+
},
|
|
64
|
+
datetime() {
|
|
65
|
+
// Mini mode: datetime validation via z.string() (z.iso.datetime not available in mini)
|
|
66
|
+
return 'z.string()'
|
|
67
|
+
},
|
|
68
|
+
time(node) {
|
|
69
|
+
if (node.representation === 'string') {
|
|
70
|
+
return 'z.iso.time()'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return 'z.date()'
|
|
74
|
+
},
|
|
75
|
+
uuid(node) {
|
|
76
|
+
const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
|
|
77
|
+
|
|
78
|
+
return `${base}${lengthChecksMini(node)}`
|
|
79
|
+
},
|
|
80
|
+
email(node) {
|
|
81
|
+
return `z.email()${lengthChecksMini(node)}`
|
|
82
|
+
},
|
|
83
|
+
url(node) {
|
|
84
|
+
return `z.url()${lengthChecksMini(node)}`
|
|
85
|
+
},
|
|
86
|
+
ipv4: () => 'z.ipv4()',
|
|
87
|
+
ipv6: () => 'z.ipv6()',
|
|
88
|
+
blob: () => 'z.instanceof(File)',
|
|
89
|
+
enum(node) {
|
|
90
|
+
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
91
|
+
const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
|
|
92
|
+
|
|
93
|
+
// asConst-style enum: use z.union([z.literal(…), …])
|
|
94
|
+
if (node.namedEnumValues?.length) {
|
|
95
|
+
const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
|
|
96
|
+
if (literals.length === 1) return literals[0]!
|
|
97
|
+
return `z.union([${literals.join(', ')}])`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Regular enum: use z.enum([…])
|
|
101
|
+
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
ref(node) {
|
|
105
|
+
if (!node.name) return undefined
|
|
106
|
+
const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
|
|
107
|
+
const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
|
|
108
|
+
const isSelfRef = node.ref && this.options.schemaName != null && resolvedName === this.options.schemaName
|
|
109
|
+
|
|
110
|
+
if (isSelfRef) {
|
|
111
|
+
return `z.lazy(() => ${resolvedName})`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return resolvedName
|
|
115
|
+
},
|
|
116
|
+
object(node) {
|
|
117
|
+
const properties = node.properties
|
|
118
|
+
.map((prop) => {
|
|
119
|
+
const { name: propName, schema } = prop
|
|
120
|
+
|
|
121
|
+
const meta = syncSchemaRef(schema)
|
|
122
|
+
|
|
123
|
+
const isNullable = meta.nullable
|
|
124
|
+
const isOptional = schema.optional
|
|
125
|
+
const isNullish = schema.nullish
|
|
126
|
+
|
|
127
|
+
const hasSelfRef =
|
|
128
|
+
this.options.schemaName != null && containsSelfRef(schema, { schemaName: this.options.schemaName, resolver: this.options.resolver })
|
|
129
|
+
const baseOutput = this.transform(schema) ?? this.transform(createSchema({ type: 'unknown' }))!
|
|
130
|
+
// Strip z.lazy() wrappers inside object getters — the getter itself provides deferred evaluation
|
|
131
|
+
const resolvedOutput = hasSelfRef ? baseOutput.replaceAll(`z.lazy(() => ${this.options.schemaName})`, this.options.schemaName!) : baseOutput
|
|
132
|
+
|
|
133
|
+
const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: resolvedOutput, schema }) || resolvedOutput : resolvedOutput
|
|
134
|
+
|
|
135
|
+
const value = applyMiniModifiers({
|
|
136
|
+
value: wrappedOutput,
|
|
137
|
+
nullable: isNullable,
|
|
138
|
+
optional: isOptional,
|
|
139
|
+
nullish: isNullish,
|
|
140
|
+
defaultValue: meta.default,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (hasSelfRef) {
|
|
144
|
+
return `get "${propName}"() { return ${value} }`
|
|
145
|
+
}
|
|
146
|
+
return `"${propName}": ${value}`
|
|
147
|
+
})
|
|
148
|
+
.join(',\n ')
|
|
149
|
+
|
|
150
|
+
return `z.object({\n ${properties}\n })`
|
|
151
|
+
},
|
|
152
|
+
array(node) {
|
|
153
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
154
|
+
const inner = items.join(', ') || this.transform(createSchema({ type: 'unknown' }))!
|
|
155
|
+
let result = `z.array(${inner})${lengthChecksMini(node)}`
|
|
156
|
+
|
|
157
|
+
if (node.unique) {
|
|
158
|
+
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
},
|
|
163
|
+
tuple(node) {
|
|
164
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
165
|
+
|
|
166
|
+
return `z.tuple([${items.join(', ')}])`
|
|
167
|
+
},
|
|
168
|
+
union(node) {
|
|
169
|
+
const nodeMembers = node.members ?? []
|
|
170
|
+
const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
|
|
171
|
+
if (members.length === 0) return ''
|
|
172
|
+
if (members.length === 1) return members[0]!
|
|
173
|
+
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
174
|
+
// z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
|
|
175
|
+
// assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
|
|
176
|
+
return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return `z.union([${members.join(', ')}])`
|
|
180
|
+
},
|
|
181
|
+
intersection(node) {
|
|
182
|
+
const members = node.members ?? []
|
|
183
|
+
if (members.length === 0) return ''
|
|
184
|
+
|
|
185
|
+
const [first, ...rest] = members
|
|
186
|
+
if (!first) return ''
|
|
187
|
+
|
|
188
|
+
let base = this.transform(first)
|
|
189
|
+
if (!base) return ''
|
|
190
|
+
|
|
191
|
+
for (const member of rest) {
|
|
192
|
+
if (member.primitive === 'string') {
|
|
193
|
+
const s = narrowSchema(member, 'string')
|
|
194
|
+
const c = lengthChecksMini(s ?? {})
|
|
195
|
+
if (c) {
|
|
196
|
+
base += c
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
} else if (member.primitive === 'number' || member.primitive === 'integer') {
|
|
200
|
+
const n = narrowSchema(member, 'number') ?? narrowSchema(member, 'integer')
|
|
201
|
+
const c = numberChecksMini(n ?? {})
|
|
202
|
+
if (c) {
|
|
203
|
+
base += c
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
} else if (member.primitive === 'array') {
|
|
207
|
+
const a = narrowSchema(member, 'array')
|
|
208
|
+
const c = lengthChecksMini(a ?? {})
|
|
209
|
+
if (c) {
|
|
210
|
+
base += c
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const transformed = this.transform(member)
|
|
215
|
+
if (transformed) base = `z.intersection(${base}, ${transformed})`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return base
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
print(node) {
|
|
223
|
+
const { keysToOmit } = this.options
|
|
224
|
+
|
|
225
|
+
let base = this.transform(node)
|
|
226
|
+
if (!base) return null
|
|
227
|
+
|
|
228
|
+
const meta = syncSchemaRef(node)
|
|
229
|
+
|
|
230
|
+
if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
|
|
231
|
+
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
232
|
+
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
233
|
+
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
234
|
+
base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(', ')} })`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return applyMiniModifiers({
|
|
238
|
+
value: base,
|
|
239
|
+
nullable: meta.nullable,
|
|
240
|
+
optional: meta.optional,
|
|
241
|
+
nullish: meta.nullish,
|
|
242
|
+
defaultValue: meta.default,
|
|
243
|
+
})
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { camelCase, pascalCase } from '@internals/utils'
|
|
2
|
+
import { defineResolver } from '@kubb/core'
|
|
3
|
+
import type { PluginZod } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
function toSchemaName(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {
|
|
6
|
+
const resolved = camelCase(name, {
|
|
7
|
+
suffix: type ? 'schema' : undefined,
|
|
8
|
+
isFile: type === 'file',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
if (type === 'type') {
|
|
12
|
+
return pascalCase(resolved)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return resolved
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default resolver for `@kubb/plugin-zod`.
|
|
20
|
+
*
|
|
21
|
+
* Uses `camelCase` naming with a `Schema` suffix for function/type/const names.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* resolverZod.default('list pets', 'function') // → 'listPetsSchema'
|
|
26
|
+
* resolverZod.default('Pet', 'file') // → 'pet'
|
|
27
|
+
* resolverZod.resolveName('list pets') // → 'listPetsSchema'
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const resolverZod = defineResolver<PluginZod>(() => {
|
|
31
|
+
return {
|
|
32
|
+
name: 'default',
|
|
33
|
+
pluginName: 'plugin-zod',
|
|
34
|
+
default(name, type) {
|
|
35
|
+
return toSchemaName(name, type)
|
|
36
|
+
},
|
|
37
|
+
resolveName(name) {
|
|
38
|
+
return this.default(name, 'function')
|
|
39
|
+
},
|
|
40
|
+
resolveInferName(name) {
|
|
41
|
+
return pascalCase(name)
|
|
42
|
+
},
|
|
43
|
+
resolvePathName(name, type) {
|
|
44
|
+
return this.default(name, type)
|
|
45
|
+
},
|
|
46
|
+
resolveParamName(node, param) {
|
|
47
|
+
return this.resolveName(`${node.operationId} ${param.in} ${param.name}`)
|
|
48
|
+
},
|
|
49
|
+
resolveResponseStatusName(node, statusCode) {
|
|
50
|
+
return this.resolveName(`${node.operationId} Status ${statusCode}`)
|
|
51
|
+
},
|
|
52
|
+
resolveDataName(node) {
|
|
53
|
+
return this.resolveName(`${node.operationId} Data`)
|
|
54
|
+
},
|
|
55
|
+
resolveResponsesName(node) {
|
|
56
|
+
return this.resolveName(`${node.operationId} Responses`)
|
|
57
|
+
},
|
|
58
|
+
resolveResponseName(node) {
|
|
59
|
+
return this.resolveName(`${node.operationId} Response`)
|
|
60
|
+
},
|
|
61
|
+
resolvePathParamsName(node, param) {
|
|
62
|
+
return this.resolveParamName(node, param)
|
|
63
|
+
},
|
|
64
|
+
resolveQueryParamsName(node, param) {
|
|
65
|
+
return this.resolveParamName(node, param)
|
|
66
|
+
},
|
|
67
|
+
resolveHeaderParamsName(node, param) {
|
|
68
|
+
return this.resolveParamName(node, param)
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineResolver } from '@kubb/core'
|
|
2
|
+
import type { PluginZod } from '../types.ts'
|
|
3
|
+
import { resolverZod } from './resolverZod.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Legacy resolver for `@kubb/plugin-zod` that reproduces the naming conventions
|
|
7
|
+
* used in Kubb v4. Enable via `compatibilityPreset: 'kubbV4'`
|
|
8
|
+
* (or by composing this resolver manually).
|
|
9
|
+
*
|
|
10
|
+
* Key differences from the default resolver:
|
|
11
|
+
* - Response status types: `<operationId><StatusCode>Schema` (e.g. `createPets201Schema`) instead of `<operationId>Status201Schema`
|
|
12
|
+
* - Default/error responses: `<operationId>ErrorSchema` instead of `<operationId>StatusDefaultSchema`
|
|
13
|
+
* - Request body: `<operationId>MutationRequestSchema` (non-GET) / `<operationId>QueryRequestSchema` (GET)
|
|
14
|
+
* - Combined responses type: `<operationId>MutationSchema` / `<operationId>QuerySchema`
|
|
15
|
+
* - Response union: `<operationId>MutationResponseSchema` / `<operationId>QueryResponseSchema`
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { resolverZodLegacy } from '@kubb/plugin-zod'
|
|
20
|
+
*
|
|
21
|
+
* resolverZodLegacy.resolveResponseStatusName(node, 201) // → 'createPets201Schema'
|
|
22
|
+
* resolverZodLegacy.resolveResponseStatusName(node, 'default') // → 'createPetsErrorSchema'
|
|
23
|
+
* resolverZodLegacy.resolveDataName(node) // → 'createPetsMutationRequestSchema' (POST)
|
|
24
|
+
* resolverZodLegacy.resolveResponsesName(node) // → 'createPetsMutationSchema' (POST)
|
|
25
|
+
* resolverZodLegacy.resolveResponseName(node) // → 'createPetsMutationResponseSchema' (POST)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const resolverZodLegacy = defineResolver<PluginZod>(() => {
|
|
29
|
+
return {
|
|
30
|
+
...resolverZod,
|
|
31
|
+
pluginName: 'plugin-zod',
|
|
32
|
+
resolveResponseStatusName(node, statusCode) {
|
|
33
|
+
if (statusCode === 'default') {
|
|
34
|
+
return this.resolveName(`${node.operationId} Error`)
|
|
35
|
+
}
|
|
36
|
+
return this.resolveName(`${node.operationId} ${statusCode}`)
|
|
37
|
+
},
|
|
38
|
+
resolveDataName(node) {
|
|
39
|
+
const suffix = node.method === 'GET' ? 'QueryRequest' : 'MutationRequest'
|
|
40
|
+
return this.resolveName(`${node.operationId} ${suffix}`)
|
|
41
|
+
},
|
|
42
|
+
resolveResponsesName(node) {
|
|
43
|
+
const suffix = node.method === 'GET' ? 'Query' : 'Mutation'
|
|
44
|
+
return this.resolveName(`${node.operationId} ${suffix}`)
|
|
45
|
+
},
|
|
46
|
+
resolveResponseName(node) {
|
|
47
|
+
const suffix = node.method === 'GET' ? 'QueryResponse' : 'MutationResponse'
|
|
48
|
+
return this.resolveName(`${node.operationId} ${suffix}`)
|
|
49
|
+
},
|
|
50
|
+
resolvePathParamsName(node, _param) {
|
|
51
|
+
return this.resolveName(`${node.operationId} PathParams`)
|
|
52
|
+
},
|
|
53
|
+
resolveQueryParamsName(node, _param) {
|
|
54
|
+
return this.resolveName(`${node.operationId} QueryParams`)
|
|
55
|
+
},
|
|
56
|
+
resolveHeaderParamsName(node, _param) {
|
|
57
|
+
return this.resolveName(`${node.operationId} HeaderParams`)
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
})
|