@kubb/plugin-zod 5.0.0-alpha.24 → 5.0.0-alpha.26
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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { caseParams, composeTransformers, createProperty, createSchema, transform } from '@kubb/ast'
|
|
3
|
+
import type { OperationNode, ParameterNode, SchemaNode } from '@kubb/ast/types'
|
|
4
|
+
import { defineGenerator, getMode } from '@kubb/core'
|
|
5
|
+
import { File } from '@kubb/react-fabric'
|
|
6
|
+
import { Operations } from '../components/Operations.tsx'
|
|
7
|
+
import { Zod } from '../components/Zod.tsx'
|
|
8
|
+
import { ZodMini } from '../components/ZodMini.tsx'
|
|
9
|
+
import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
|
|
10
|
+
import type { PluginZod, ResolverZod } from '../types'
|
|
11
|
+
|
|
12
|
+
type BuildGroupedParamsSchemaOptions = {
|
|
13
|
+
params: Array<ParameterNode>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildGroupedParamsSchema({ params, optional }: BuildGroupedParamsSchemaOptions & { optional?: boolean }): SchemaNode {
|
|
17
|
+
return createSchema({
|
|
18
|
+
type: 'object',
|
|
19
|
+
optional,
|
|
20
|
+
primitive: 'object',
|
|
21
|
+
properties: params.map((param) => {
|
|
22
|
+
return createProperty({
|
|
23
|
+
name: param.name,
|
|
24
|
+
required: param.required,
|
|
25
|
+
schema: param.schema,
|
|
26
|
+
})
|
|
27
|
+
}),
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type BuildOperationSchemaOptions = {
|
|
32
|
+
resolver: ResolverZod
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildLegacyResponsesSchemaNode(node: OperationNode, { resolver }: BuildOperationSchemaOptions): SchemaNode | null {
|
|
36
|
+
const isGet = node.method.toLowerCase() === 'get'
|
|
37
|
+
const successResponses = node.responses.filter((res) => {
|
|
38
|
+
const code = Number(res.statusCode)
|
|
39
|
+
return !Number.isNaN(code) && code >= 200 && code < 300
|
|
40
|
+
})
|
|
41
|
+
const errorResponses = node.responses.filter((res) => res.statusCode === 'default' || Number(res.statusCode) >= 400)
|
|
42
|
+
|
|
43
|
+
const responseSchema =
|
|
44
|
+
successResponses.length > 0
|
|
45
|
+
? successResponses.length === 1
|
|
46
|
+
? createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, successResponses[0]!.statusCode) })
|
|
47
|
+
: createSchema({
|
|
48
|
+
type: 'union',
|
|
49
|
+
members: successResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, res.statusCode) })),
|
|
50
|
+
})
|
|
51
|
+
: createSchema({ type: 'any' })
|
|
52
|
+
|
|
53
|
+
const errorsSchema =
|
|
54
|
+
errorResponses.length > 0
|
|
55
|
+
? errorResponses.length === 1
|
|
56
|
+
? createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, errorResponses[0]!.statusCode) })
|
|
57
|
+
: createSchema({
|
|
58
|
+
type: 'union',
|
|
59
|
+
members: errorResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, res.statusCode) })),
|
|
60
|
+
})
|
|
61
|
+
: createSchema({ type: 'any' })
|
|
62
|
+
|
|
63
|
+
const properties = [createProperty({ name: 'Response', required: true, schema: responseSchema })]
|
|
64
|
+
|
|
65
|
+
if (!isGet && node.requestBody?.schema) {
|
|
66
|
+
properties.push(
|
|
67
|
+
createProperty({
|
|
68
|
+
name: 'Request',
|
|
69
|
+
required: true,
|
|
70
|
+
schema: createSchema({ type: 'ref', name: resolver.resolveDataName(node) }),
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const queryParam = node.parameters.find((p) => p.in === 'query')
|
|
76
|
+
if (queryParam) {
|
|
77
|
+
properties.push(
|
|
78
|
+
createProperty({
|
|
79
|
+
name: 'QueryParams',
|
|
80
|
+
required: true,
|
|
81
|
+
schema: createSchema({ type: 'ref', name: resolver.resolveQueryParamsName(node, queryParam) }),
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pathParam = node.parameters.find((p) => p.in === 'path')
|
|
87
|
+
if (pathParam) {
|
|
88
|
+
properties.push(
|
|
89
|
+
createProperty({
|
|
90
|
+
name: 'PathParams',
|
|
91
|
+
required: true,
|
|
92
|
+
schema: createSchema({ type: 'ref', name: resolver.resolvePathParamsName(node, pathParam) }),
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const headerParam = node.parameters.find((p) => p.in === 'header')
|
|
98
|
+
if (headerParam) {
|
|
99
|
+
properties.push(
|
|
100
|
+
createProperty({
|
|
101
|
+
name: 'HeaderParams',
|
|
102
|
+
required: true,
|
|
103
|
+
schema: createSchema({ type: 'ref', name: resolver.resolveHeaderParamsName(node, headerParam) }),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
properties.push(createProperty({ name: 'Errors', required: true, schema: errorsSchema }))
|
|
109
|
+
|
|
110
|
+
return createSchema({ type: 'object', primitive: 'object', properties })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildLegacyResponseUnionSchemaNode(node: OperationNode, { resolver }: BuildOperationSchemaOptions): SchemaNode {
|
|
114
|
+
const successResponses = node.responses.filter((res) => {
|
|
115
|
+
const code = Number(res.statusCode)
|
|
116
|
+
return !Number.isNaN(code) && code >= 200 && code < 300
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
if (successResponses.length === 0) {
|
|
120
|
+
return createSchema({ type: 'any' })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (successResponses.length === 1) {
|
|
124
|
+
return createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, successResponses[0]!.statusCode) })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return createSchema({
|
|
128
|
+
type: 'union',
|
|
129
|
+
members: successResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, res.statusCode) })),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildLegacySchemaNames(node: OperationNode, params: Array<ParameterNode>, resolver: ResolverZod) {
|
|
134
|
+
const pathParam = params.find((p) => p.in === 'path')
|
|
135
|
+
const queryParam = params.find((p) => p.in === 'query')
|
|
136
|
+
const headerParam = params.find((p) => p.in === 'header')
|
|
137
|
+
|
|
138
|
+
const responses: Record<number | string, string> = {}
|
|
139
|
+
const errors: Record<number | string, string> = {}
|
|
140
|
+
|
|
141
|
+
for (const res of node.responses) {
|
|
142
|
+
const name = resolver.resolveResponseStatusName(node, res.statusCode)
|
|
143
|
+
const statusNum = Number(res.statusCode)
|
|
144
|
+
|
|
145
|
+
if (!Number.isNaN(statusNum)) {
|
|
146
|
+
responses[statusNum] = name
|
|
147
|
+
if (statusNum >= 400) {
|
|
148
|
+
errors[statusNum] = name
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
responses['default'] = resolver.resolveResponseName(node)
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
request: node.requestBody?.schema ? resolver.resolveDataName(node) : undefined,
|
|
157
|
+
parameters: {
|
|
158
|
+
path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : undefined,
|
|
159
|
+
query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : undefined,
|
|
160
|
+
header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : undefined,
|
|
161
|
+
},
|
|
162
|
+
responses,
|
|
163
|
+
errors,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const zodGeneratorLegacy = defineGenerator<PluginZod>({
|
|
168
|
+
name: 'zod-legacy',
|
|
169
|
+
type: 'react',
|
|
170
|
+
Schema({ node, adapter, options, config, resolver }) {
|
|
171
|
+
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, transformers = [] } = options
|
|
172
|
+
const transformedNode = transform(node, composeTransformers(...transformers))
|
|
173
|
+
|
|
174
|
+
if (!transformedNode.name) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const root = path.resolve(config.root, config.output.path)
|
|
179
|
+
const mode = getMode(path.resolve(root, output.path))
|
|
180
|
+
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
181
|
+
|
|
182
|
+
const imports = adapter.getImports(transformedNode, (schemaName) => ({
|
|
183
|
+
name: resolver.default(schemaName, 'function'),
|
|
184
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
185
|
+
}))
|
|
186
|
+
|
|
187
|
+
const inferTypeName = inferred ? resolver.resolveInferName(resolver.resolveName(transformedNode.name)) : undefined
|
|
188
|
+
|
|
189
|
+
const meta = {
|
|
190
|
+
name: resolver.default(transformedNode.name, 'function'),
|
|
191
|
+
file: resolver.resolveFile({ name: transformedNode.name, extname: '.ts' }, { root, output, group }),
|
|
192
|
+
} as const
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<File
|
|
196
|
+
baseName={meta.file.baseName}
|
|
197
|
+
path={meta.file.path}
|
|
198
|
+
meta={meta.file.meta}
|
|
199
|
+
banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
|
|
200
|
+
footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
|
|
201
|
+
>
|
|
202
|
+
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
203
|
+
{mode === 'split' &&
|
|
204
|
+
imports.map((imp) => <File.Import key={[transformedNode.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
|
|
205
|
+
|
|
206
|
+
{mini ? (
|
|
207
|
+
<ZodMini name={meta.name} node={transformedNode} guidType={guidType} wrapOutput={wrapOutput} inferTypeName={inferTypeName} resolver={resolver} />
|
|
208
|
+
) : (
|
|
209
|
+
<Zod
|
|
210
|
+
name={meta.name}
|
|
211
|
+
node={transformedNode}
|
|
212
|
+
coercion={coercion}
|
|
213
|
+
guidType={guidType}
|
|
214
|
+
wrapOutput={wrapOutput}
|
|
215
|
+
inferTypeName={inferTypeName}
|
|
216
|
+
resolver={resolver}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
</File>
|
|
220
|
+
)
|
|
221
|
+
},
|
|
222
|
+
Operation({ node, adapter, options, config, resolver }) {
|
|
223
|
+
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, transformers } = options
|
|
224
|
+
|
|
225
|
+
const transformedNode = transform(node, composeTransformers(...transformers))
|
|
226
|
+
|
|
227
|
+
const root = path.resolve(config.root, config.output.path)
|
|
228
|
+
const mode = getMode(path.resolve(root, output.path))
|
|
229
|
+
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
230
|
+
|
|
231
|
+
const params = caseParams(transformedNode.parameters, paramsCasing)
|
|
232
|
+
|
|
233
|
+
const meta = {
|
|
234
|
+
file: resolver.resolveFile(
|
|
235
|
+
{ name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
|
|
236
|
+
{ root, output, group },
|
|
237
|
+
),
|
|
238
|
+
} as const
|
|
239
|
+
|
|
240
|
+
function renderSchemaEntry({ schema, name, keysToOmit }: { schema: SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
|
|
241
|
+
if (!schema) return null
|
|
242
|
+
|
|
243
|
+
const inferTypeName = inferred ? resolver.resolveInferName(name) : undefined
|
|
244
|
+
|
|
245
|
+
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
246
|
+
name: resolver.default(schemaName, 'function'),
|
|
247
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
248
|
+
}))
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<>
|
|
252
|
+
{mode === 'split' &&
|
|
253
|
+
imports.map((imp) => <File.Import key={[name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
|
|
254
|
+
{mini ? (
|
|
255
|
+
<ZodMini
|
|
256
|
+
name={name}
|
|
257
|
+
node={schema}
|
|
258
|
+
guidType={guidType}
|
|
259
|
+
wrapOutput={wrapOutput}
|
|
260
|
+
inferTypeName={inferTypeName}
|
|
261
|
+
resolver={resolver}
|
|
262
|
+
keysToOmit={keysToOmit}
|
|
263
|
+
/>
|
|
264
|
+
) : (
|
|
265
|
+
<Zod
|
|
266
|
+
name={name}
|
|
267
|
+
node={schema}
|
|
268
|
+
coercion={coercion}
|
|
269
|
+
guidType={guidType}
|
|
270
|
+
wrapOutput={wrapOutput}
|
|
271
|
+
inferTypeName={inferTypeName}
|
|
272
|
+
resolver={resolver}
|
|
273
|
+
keysToOmit={keysToOmit}
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
276
|
+
</>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pathParams = params.filter((p) => p.in === 'path')
|
|
281
|
+
const queryParams = params.filter((p) => p.in === 'query')
|
|
282
|
+
const headerParams = params.filter((p) => p.in === 'header')
|
|
283
|
+
|
|
284
|
+
const responseSchemas = transformedNode.responses.map((res) => {
|
|
285
|
+
const responseName = resolver.resolveResponseStatusName(transformedNode, res.statusCode)
|
|
286
|
+
return renderSchemaEntry({
|
|
287
|
+
schema: {
|
|
288
|
+
...res.schema,
|
|
289
|
+
description: res.description ?? res.schema.description,
|
|
290
|
+
},
|
|
291
|
+
name: responseName,
|
|
292
|
+
keysToOmit: res.keysToOmit,
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const requestSchema = transformedNode.requestBody?.schema
|
|
297
|
+
? renderSchemaEntry({
|
|
298
|
+
schema: {
|
|
299
|
+
...transformedNode.requestBody.schema,
|
|
300
|
+
description: transformedNode.requestBody.description ?? transformedNode.requestBody.schema.description,
|
|
301
|
+
},
|
|
302
|
+
name: resolver.resolveDataName(transformedNode),
|
|
303
|
+
keysToOmit: transformedNode.requestBody.keysToOmit,
|
|
304
|
+
})
|
|
305
|
+
: null
|
|
306
|
+
|
|
307
|
+
const legacyParamTypes = [
|
|
308
|
+
pathParams.length > 0
|
|
309
|
+
? renderSchemaEntry({
|
|
310
|
+
schema: buildGroupedParamsSchema({ params: pathParams, optional: pathParams.every((p) => !p.required) }),
|
|
311
|
+
name: resolver.resolvePathParamsName(transformedNode, pathParams[0]!),
|
|
312
|
+
})
|
|
313
|
+
: null,
|
|
314
|
+
queryParams.length > 0
|
|
315
|
+
? renderSchemaEntry({
|
|
316
|
+
schema: buildGroupedParamsSchema({ params: queryParams, optional: queryParams.every((p) => !p.required) }),
|
|
317
|
+
name: resolver.resolveQueryParamsName(transformedNode, queryParams[0]!),
|
|
318
|
+
})
|
|
319
|
+
: null,
|
|
320
|
+
headerParams.length > 0
|
|
321
|
+
? renderSchemaEntry({
|
|
322
|
+
schema: buildGroupedParamsSchema({ params: headerParams, optional: headerParams.every((p) => !p.required) }),
|
|
323
|
+
name: resolver.resolveHeaderParamsName(transformedNode, headerParams[0]!),
|
|
324
|
+
})
|
|
325
|
+
: null,
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
const legacyResponsesSchema = renderSchemaEntry({
|
|
329
|
+
schema: buildLegacyResponsesSchemaNode(transformedNode, { resolver }),
|
|
330
|
+
name: resolver.resolveResponsesName(transformedNode),
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const legacyResponseSchema = renderSchemaEntry({
|
|
334
|
+
schema: buildLegacyResponseUnionSchemaNode(transformedNode, { resolver }),
|
|
335
|
+
name: resolver.resolveResponseName(transformedNode),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<File
|
|
340
|
+
baseName={meta.file.baseName}
|
|
341
|
+
path={meta.file.path}
|
|
342
|
+
meta={meta.file.meta}
|
|
343
|
+
banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
|
|
344
|
+
footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
|
|
345
|
+
>
|
|
346
|
+
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
347
|
+
{legacyParamTypes}
|
|
348
|
+
{responseSchemas}
|
|
349
|
+
{requestSchema}
|
|
350
|
+
{legacyResponseSchema}
|
|
351
|
+
{legacyResponsesSchema}
|
|
352
|
+
</File>
|
|
353
|
+
)
|
|
354
|
+
},
|
|
355
|
+
Operations({ nodes, adapter, options, config, resolver }) {
|
|
356
|
+
const { output, importPath, group, operations, paramsCasing, transformers } = options
|
|
357
|
+
|
|
358
|
+
if (!operations) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const root = path.resolve(config.root, config.output.path)
|
|
363
|
+
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
364
|
+
|
|
365
|
+
const meta = {
|
|
366
|
+
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
|
|
367
|
+
} as const
|
|
368
|
+
|
|
369
|
+
const transformedOperations = nodes.map((node) => {
|
|
370
|
+
const transformedNode = transform(node, composeTransformers(...transformers))
|
|
371
|
+
|
|
372
|
+
const params = caseParams(transformedNode.parameters, paramsCasing)
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
node: transformedNode,
|
|
376
|
+
data: buildLegacySchemaNames(transformedNode, params, resolver),
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const imports = transformedOperations.flatMap(({ node, data }) => {
|
|
381
|
+
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string[]
|
|
382
|
+
const opFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
|
|
383
|
+
|
|
384
|
+
return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<File
|
|
389
|
+
baseName={meta.file.baseName}
|
|
390
|
+
path={meta.file.path}
|
|
391
|
+
meta={meta.file.meta}
|
|
392
|
+
banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
|
|
393
|
+
footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
|
|
394
|
+
>
|
|
395
|
+
<File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
396
|
+
{imports}
|
|
397
|
+
<Operations name="operations" operations={transformedOperations} />
|
|
398
|
+
</File>
|
|
399
|
+
)
|
|
400
|
+
},
|
|
401
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
export { zodGenerator } from './generators/zodGenerator.tsx'
|
|
2
|
+
export { zodGeneratorLegacy } from './generators/zodGeneratorLegacy.tsx'
|
|
3
|
+
|
|
1
4
|
export { pluginZod, pluginZodName } from './plugin.ts'
|
|
2
|
-
|
|
5
|
+
|
|
6
|
+
export { printerZod } from './printers/printerZod.ts'
|
|
7
|
+
export { printerZodMini } from './printers/printerZodMini.ts'
|
|
8
|
+
|
|
9
|
+
export { resolverZod } from './resolvers/resolverZod.ts'
|
|
10
|
+
export { resolverZodLegacy } from './resolvers/resolverZodLegacy.ts'
|
|
11
|
+
|
|
12
|
+
export type { PluginZod, ResolverZod } from './types.ts'
|
package/src/plugin.ts
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { camelCase
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { zodGenerator } from './generators/zodGenerator.tsx'
|
|
8
|
-
import { source as toZodSource } from './templates/ToZod.source.ts'
|
|
2
|
+
import { camelCase } from '@internals/utils'
|
|
3
|
+
import { walk } from '@kubb/ast'
|
|
4
|
+
import type { OperationNode } from '@kubb/ast/types'
|
|
5
|
+
import { createPlugin, type Group, getBarrelFiles, getPreset, runGeneratorOperation, runGeneratorOperations, runGeneratorSchema } from '@kubb/core'
|
|
6
|
+
import { presets } from './presets.ts'
|
|
9
7
|
import type { PluginZod } from './types.ts'
|
|
10
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Canonical plugin name for `@kubb/plugin-zod`, used to identify the plugin in driver lookups and warnings.
|
|
11
|
+
*/
|
|
11
12
|
export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The `@kubb/plugin-zod` plugin factory.
|
|
16
|
+
*
|
|
17
|
+
* Generates Zod validation schemas from an OpenAPI/AST `RootNode`.
|
|
18
|
+
* Walks schemas and operations, delegates rendering to the active generators,
|
|
19
|
+
* and writes barrel files based on `output.barrelType`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
24
|
+
*
|
|
25
|
+
* export default defineConfig({
|
|
26
|
+
* plugins: [pluginZod({ output: { path: 'zod' } })],
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
13
30
|
export const pluginZod = createPlugin<PluginZod>((options) => {
|
|
14
31
|
const {
|
|
15
32
|
output = { path: 'zod', barrelType: 'named' },
|
|
@@ -17,156 +34,115 @@ export const pluginZod = createPlugin<PluginZod>((options) => {
|
|
|
17
34
|
exclude = [],
|
|
18
35
|
include,
|
|
19
36
|
override = [],
|
|
20
|
-
transformers = {},
|
|
21
37
|
dateType = 'string',
|
|
22
|
-
unknownType = 'any',
|
|
23
|
-
emptySchemaType = unknownType,
|
|
24
|
-
integerType = 'number',
|
|
25
38
|
typed = false,
|
|
26
|
-
mapper = {},
|
|
27
39
|
operations = false,
|
|
28
40
|
mini = false,
|
|
29
|
-
version = mini ? '4' : satisfiesDependency('zod', '>=4') ? '4' : '3',
|
|
30
41
|
guidType = 'uuid',
|
|
31
|
-
importPath = mini ? 'zod/mini' :
|
|
42
|
+
importPath = mini ? 'zod/mini' : 'zod',
|
|
32
43
|
coercion = false,
|
|
33
44
|
inferred = false,
|
|
34
|
-
generators = [zodGenerator, operations ? operationsGenerator : undefined].filter(Boolean),
|
|
35
45
|
wrapOutput = undefined,
|
|
36
|
-
|
|
46
|
+
paramsCasing,
|
|
47
|
+
compatibilityPreset = 'default',
|
|
48
|
+
resolvers: userResolvers = [],
|
|
49
|
+
transformers: userTransformers = [],
|
|
50
|
+
generators: userGenerators = [],
|
|
37
51
|
} = options
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
const preset = getPreset({
|
|
54
|
+
preset: compatibilityPreset,
|
|
55
|
+
presets: presets,
|
|
56
|
+
resolvers: userResolvers,
|
|
57
|
+
transformers: userTransformers,
|
|
58
|
+
generators: userGenerators,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let resolveNameWarning = false
|
|
62
|
+
let resolvePathWarning = false
|
|
41
63
|
|
|
42
64
|
return {
|
|
43
65
|
name: pluginZodName,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
transformers,
|
|
47
|
-
include,
|
|
48
|
-
exclude,
|
|
49
|
-
override,
|
|
50
|
-
typed,
|
|
51
|
-
dateType,
|
|
52
|
-
unknownType,
|
|
53
|
-
emptySchemaType,
|
|
54
|
-
integerType,
|
|
55
|
-
mapper,
|
|
56
|
-
importPath,
|
|
57
|
-
coercion,
|
|
58
|
-
operations,
|
|
59
|
-
inferred,
|
|
60
|
-
group,
|
|
61
|
-
wrapOutput,
|
|
62
|
-
version,
|
|
63
|
-
guidType,
|
|
64
|
-
mini,
|
|
65
|
-
usedEnumNames,
|
|
66
|
+
get resolver() {
|
|
67
|
+
return preset.resolver
|
|
66
68
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
get options() {
|
|
70
|
+
return {
|
|
71
|
+
output,
|
|
72
|
+
group: group
|
|
73
|
+
? ({
|
|
74
|
+
...group,
|
|
75
|
+
name: (ctx) => {
|
|
76
|
+
if (group.type === 'path') {
|
|
77
|
+
return `${ctx.group.split('/')[1]}`
|
|
78
|
+
}
|
|
79
|
+
return `${camelCase(ctx.group)}Controller`
|
|
80
|
+
},
|
|
81
|
+
} satisfies Group)
|
|
82
|
+
: undefined,
|
|
83
|
+
dateType,
|
|
84
|
+
typed,
|
|
85
|
+
importPath,
|
|
86
|
+
coercion,
|
|
87
|
+
operations,
|
|
88
|
+
inferred,
|
|
89
|
+
guidType,
|
|
90
|
+
mini,
|
|
91
|
+
wrapOutput,
|
|
92
|
+
paramsCasing,
|
|
93
|
+
transformers: preset.transformers,
|
|
78
94
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (group?.type === 'path') {
|
|
85
|
-
return `${ctx.group.split('/')[1]}`
|
|
86
|
-
}
|
|
87
|
-
return `${camelCase(ctx.group)}Controller`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return path.resolve(
|
|
91
|
-
root,
|
|
92
|
-
output.path,
|
|
93
|
-
groupName({
|
|
94
|
-
group: group.type === 'path' ? options.group.path! : options.group.tag!,
|
|
95
|
-
}),
|
|
96
|
-
baseName,
|
|
97
|
-
)
|
|
95
|
+
},
|
|
96
|
+
resolvePath(baseName, pathMode, options) {
|
|
97
|
+
if (!resolvePathWarning) {
|
|
98
|
+
this.events.emit('warn', 'Do not use resolvePath for pluginZod, use resolverZod.resolvePath instead')
|
|
99
|
+
resolvePathWarning = true
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
return
|
|
102
|
+
return this.plugin.resolver.resolvePath(
|
|
103
|
+
{ baseName, pathMode, tag: options?.group?.tag, path: options?.group?.path },
|
|
104
|
+
{ root: path.resolve(this.config.root, this.config.output.path), output, group: this.plugin.options.group },
|
|
105
|
+
)
|
|
101
106
|
},
|
|
102
107
|
resolveName(name, type) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
if (type === 'type') {
|
|
109
|
-
resolvedName = pascalCase(resolvedName)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (type) {
|
|
113
|
-
return transformers?.name?.(resolvedName, type) || resolvedName
|
|
108
|
+
if (!resolveNameWarning) {
|
|
109
|
+
this.events.emit('warn', 'Do not use resolveName for pluginZod, use resolverZod.default instead')
|
|
110
|
+
resolveNameWarning = true
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
return
|
|
113
|
+
return this.plugin.resolver.default(name, type)
|
|
117
114
|
},
|
|
118
115
|
async install() {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
await this.addFile({
|
|
126
|
-
baseName: 'ToZod.ts',
|
|
127
|
-
path: path.resolve(root, '.kubb/ToZod.ts'),
|
|
128
|
-
sources: [
|
|
129
|
-
{
|
|
130
|
-
name: 'ToZod',
|
|
131
|
-
value: toZodSource,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
imports: [],
|
|
135
|
-
exports: [],
|
|
136
|
-
})
|
|
116
|
+
const { config, fabric, plugin, adapter, rootNode, driver, openInStudio, resolver } = this
|
|
117
|
+
|
|
118
|
+
const root = path.resolve(config.root, config.output.path)
|
|
119
|
+
|
|
120
|
+
if (!adapter) {
|
|
121
|
+
throw new Error(`[${pluginZodName}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`)
|
|
137
122
|
}
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
events: this.events,
|
|
144
|
-
plugin: this.plugin,
|
|
145
|
-
contentType,
|
|
146
|
-
include: undefined,
|
|
147
|
-
override,
|
|
148
|
-
mode,
|
|
149
|
-
output: output.path,
|
|
150
|
-
})
|
|
124
|
+
await openInStudio({ ast: true })
|
|
125
|
+
|
|
126
|
+
const collectedOperations: Array<OperationNode> = []
|
|
127
|
+
const generatorContext = { generators: preset.generators, plugin, resolver, exclude, include, override, fabric, adapter, config, driver }
|
|
151
128
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
129
|
+
await walk(rootNode, {
|
|
130
|
+
depth: 'shallow',
|
|
131
|
+
async schema(schemaNode) {
|
|
132
|
+
await runGeneratorSchema(schemaNode, generatorContext)
|
|
133
|
+
},
|
|
134
|
+
async operation(operationNode) {
|
|
135
|
+
const baseOptions = resolver.resolveOptions(operationNode, { options: plugin.options, exclude, include, override })
|
|
136
|
+
|
|
137
|
+
if (baseOptions !== null) {
|
|
138
|
+
collectedOperations.push(operationNode)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await runGeneratorOperation(operationNode, generatorContext)
|
|
142
|
+
},
|
|
166
143
|
})
|
|
167
144
|
|
|
168
|
-
|
|
169
|
-
await this.upsertFile(...operationFiles)
|
|
145
|
+
await runGeneratorOperations(collectedOperations, generatorContext)
|
|
170
146
|
|
|
171
147
|
const barrelFiles = await getBarrelFiles(this.fabric.files, {
|
|
172
148
|
type: output.barrelType ?? 'named',
|