@kubb/plugin-zod 5.0.0-alpha.9 → 5.0.0-beta.15

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.
Files changed (46) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +25 -7
  3. package/dist/index.cjs +1129 -105
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +369 -4
  6. package/dist/index.js +1116 -105
  7. package/dist/index.js.map +1 -1
  8. package/extension.yaml +505 -0
  9. package/package.json +43 -73
  10. package/src/components/Operations.tsx +25 -18
  11. package/src/components/Zod.tsx +21 -121
  12. package/src/constants.ts +5 -0
  13. package/src/generators/zodGenerator.tsx +213 -166
  14. package/src/index.ts +11 -2
  15. package/src/plugin.ts +67 -156
  16. package/src/printers/printerZod.ts +368 -0
  17. package/src/printers/printerZodMini.ts +313 -0
  18. package/src/resolvers/resolverZod.ts +57 -0
  19. package/src/types.ts +130 -115
  20. package/src/utils.ts +222 -0
  21. package/dist/components-B7zUFnAm.cjs +0 -890
  22. package/dist/components-B7zUFnAm.cjs.map +0 -1
  23. package/dist/components-eECfXVou.js +0 -842
  24. package/dist/components-eECfXVou.js.map +0 -1
  25. package/dist/components.cjs +0 -4
  26. package/dist/components.d.ts +0 -56
  27. package/dist/components.js +0 -2
  28. package/dist/generators-BjPDdJUz.cjs +0 -301
  29. package/dist/generators-BjPDdJUz.cjs.map +0 -1
  30. package/dist/generators-lTWPS6oN.js +0 -290
  31. package/dist/generators-lTWPS6oN.js.map +0 -1
  32. package/dist/generators.cjs +0 -4
  33. package/dist/generators.d.ts +0 -508
  34. package/dist/generators.js +0 -2
  35. package/dist/templates/ToZod.source.cjs +0 -7
  36. package/dist/templates/ToZod.source.cjs.map +0 -1
  37. package/dist/templates/ToZod.source.d.ts +0 -7
  38. package/dist/templates/ToZod.source.js +0 -6
  39. package/dist/templates/ToZod.source.js.map +0 -1
  40. package/dist/types-CoCoOc2u.d.ts +0 -172
  41. package/src/components/index.ts +0 -2
  42. package/src/generators/index.ts +0 -2
  43. package/src/generators/operationsGenerator.tsx +0 -50
  44. package/src/parser.ts +0 -909
  45. package/src/templates/ToZod.source.ts +0 -4
  46. package/templates/ToZod.ts +0 -61
@@ -1,35 +1,42 @@
1
1
  import { stringifyObject } from '@internals/utils'
2
- import type { HttpMethod, Operation } from '@kubb/oas'
3
- import type { SchemaNames } from '@kubb/plugin-oas/hooks'
4
- import { Const, File, Type } from '@kubb/react-fabric'
5
- import type { FabricReactNode } from '@kubb/react-fabric/types'
2
+ import type { ast } from '@kubb/core'
3
+ import { Const, File, Type } from '@kubb/renderer-jsx'
4
+ import type { KubbReactNode } from '@kubb/renderer-jsx/types'
5
+
6
+ type SchemaNames = {
7
+ request: string | undefined
8
+ parameters: {
9
+ path: string | undefined
10
+ query: string | undefined
11
+ header: string | undefined
12
+ }
13
+ responses: { default?: string } & Record<number | string, string>
14
+ errors: Record<number | string, string>
15
+ }
6
16
 
7
17
  type Props = {
8
18
  name: string
9
- operations: Array<{ operation: Operation; data: SchemaNames }>
19
+ operations: Array<{ node: ast.OperationNode; data: SchemaNames }>
10
20
  }
11
21
 
12
- export function Operations({ name, operations }: Props): FabricReactNode {
13
- const operationsJSON = operations.reduce(
22
+ export function Operations({ name, operations }: Props): KubbReactNode {
23
+ const operationsJSON = operations.reduce<Record<string, unknown>>(
14
24
  (prev, acc) => {
15
- prev[`"${acc.operation.getOperationId()}"`] = acc.data
25
+ prev[`"${acc.node.operationId}"`] = acc.data
16
26
 
17
27
  return prev
18
28
  },
19
29
  {} as Record<string, unknown>,
20
30
  )
21
31
 
22
- const pathsJSON = operations.reduce(
23
- (prev, acc) => {
24
- prev[`"${acc.operation.path}"`] = {
25
- ...(prev[`"${acc.operation.path}"`] || ({} as Record<HttpMethod, string>)),
26
- [acc.operation.method]: `operations["${acc.operation.getOperationId()}"]`,
27
- }
32
+ const pathsJSON = operations.reduce<Record<string, Record<string, string>>>((prev, acc) => {
33
+ prev[`"${acc.node.path}"`] = {
34
+ ...(prev[`"${acc.node.path}"`] ?? {}),
35
+ [acc.node.method]: `operations["${acc.node.operationId}"]`,
36
+ }
28
37
 
29
- return prev
30
- },
31
- {} as Record<string, Record<HttpMethod, string>>,
32
- )
38
+ return prev
39
+ }, {})
33
40
 
34
41
  return (
35
42
  <>
@@ -1,140 +1,40 @@
1
- import { jsStringEscape } from '@internals/utils'
2
- import type { SchemaObject } from '@kubb/oas'
3
- import { isKeyword, type Schema, SchemaGenerator, schemaKeywords } from '@kubb/plugin-oas'
4
- import { Const, File, Type } from '@kubb/react-fabric'
5
- import type { FabricReactNode } from '@kubb/react-fabric/types'
6
- import * as parserZod from '../parser.ts'
7
- import type { PluginZod } from '../types.ts'
1
+ import type { ast } from '@kubb/core'
2
+ import { Const, File, Type } from '@kubb/renderer-jsx'
3
+ import type { KubbReactNode } from '@kubb/renderer-jsx/types'
4
+ import type { PrinterZodFactory } from '../printers/printerZod.ts'
5
+ import type { PrinterZodMiniFactory } from '../printers/printerZodMini.ts'
8
6
 
9
7
  type Props = {
10
8
  name: string
11
- typeName?: string
9
+ node: ast.SchemaNode
10
+ /**
11
+ * Pre-configured printer instance created by the generator.
12
+ * The generator selects `printerZod` or `printerZodMini` based on the `mini` option,
13
+ * then merges in any user-supplied `printer.nodes` overrides.
14
+ */
15
+ printer: ast.Printer<PrinterZodFactory> | ast.Printer<PrinterZodMiniFactory>
12
16
  inferTypeName?: string
13
- tree: Array<Schema>
14
- schema: SchemaObject
15
- description?: string
16
- coercion: PluginZod['resolvedOptions']['coercion']
17
- mapper: PluginZod['resolvedOptions']['mapper']
18
- keysToOmit?: string[]
19
- wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
20
- version: '3' | '4'
21
- guidType: PluginZod['resolvedOptions']['guidType']
22
- emptySchemaType: PluginZod['resolvedOptions']['emptySchemaType']
23
- mini?: boolean
24
17
  }
25
18
 
26
- export function Zod({
27
- name,
28
- typeName,
29
- tree,
30
- schema,
31
- inferTypeName,
32
- mapper,
33
- coercion,
34
- keysToOmit,
35
- description,
36
- wrapOutput,
37
- version,
38
- guidType,
39
- emptySchemaType,
40
- mini = false,
41
- }: Props): FabricReactNode {
42
- const hasTuple = !!SchemaGenerator.find(tree, schemaKeywords.tuple)
19
+ export function Zod({ name, node, printer, inferTypeName }: Props): KubbReactNode {
20
+ const output = printer.print(node)
43
21
 
44
- const schemas = parserZod.sort(tree).filter((item) => {
45
- if (hasTuple && (isKeyword(item, schemaKeywords.min) || isKeyword(item, schemaKeywords.max))) {
46
- return false
47
- }
48
-
49
- return true
50
- })
51
-
52
- // In mini mode, filter out modifiers from the main schema parsing
53
- const baseSchemas = mini ? parserZod.filterMiniModifiers(schemas) : schemas
54
-
55
- const output = baseSchemas
56
- .map((schemaKeyword, index) => {
57
- const siblings = baseSchemas.filter((_, i) => i !== index)
58
-
59
- return parserZod.parse({ schema, parent: undefined, current: schemaKeyword, siblings, name }, { mapper, coercion, wrapOutput, version, guidType, mini })
60
- })
61
- .filter(Boolean)
62
- .join('')
63
-
64
- let suffix = ''
65
- const firstSchema = schemas.at(0)
66
- const lastSchema = schemas.at(-1)
67
-
68
- if (!mini && lastSchema && isKeyword(lastSchema, schemaKeywords.nullable)) {
69
- if (firstSchema && isKeyword(firstSchema, schemaKeywords.ref)) {
70
- if (version === '3') {
71
- suffix = '.unwrap().schema.unwrap()'
72
- } else {
73
- suffix = '.unwrap().unwrap()'
74
- }
75
- } else {
76
- suffix = '.unwrap()'
77
- }
78
- } else if (!mini) {
79
- if (firstSchema && isKeyword(firstSchema, schemaKeywords.ref)) {
80
- if (version === '3') {
81
- suffix = '.schema'
82
- } else {
83
- suffix = '.unwrap()'
84
- }
85
- }
22
+ if (!output) {
23
+ return
86
24
  }
87
25
 
88
- const emptyValue = parserZod.parse(
89
- {
90
- schema,
91
- parent: undefined,
92
- current: {
93
- keyword: schemaKeywords[emptySchemaType],
94
- },
95
- siblings: [],
96
- },
97
- { mapper, coercion, wrapOutput, version, guidType, mini },
98
- )
99
-
100
- let baseSchemaOutput =
101
- [output, keysToOmit?.length ? `${suffix}.omit({ ${keysToOmit.map((key) => `'${key}': true`).join(',')} })` : undefined].filter(Boolean).join('') ||
102
- emptyValue ||
103
- ''
104
-
105
- // For mini mode, wrap the output with modifiers using the parser function
106
- if (mini) {
107
- baseSchemaOutput = parserZod.wrapWithMiniModifiers(baseSchemaOutput, parserZod.extractMiniModifiers(schemas))
108
- }
109
-
110
- const wrappedSchemaOutput = wrapOutput ? wrapOutput({ output: baseSchemaOutput, schema }) || baseSchemaOutput : baseSchemaOutput
111
- const finalOutput = typeName ? `${wrappedSchemaOutput} as unknown as ${version === '4' ? 'z.ZodType' : 'ToZod'}<${typeName}>` : wrappedSchemaOutput
112
-
113
26
  return (
114
27
  <>
115
28
  <File.Source name={name} isExportable isIndexable>
116
- <Const
117
- export
118
- name={name}
119
- JSDoc={{
120
- comments: [description ? `@description ${jsStringEscape(description)}` : undefined].filter(Boolean),
121
- }}
122
- >
123
- {finalOutput}
29
+ <Const export name={name}>
30
+ {output}
124
31
  </Const>
125
32
  </File.Source>
126
33
  {inferTypeName && (
127
34
  <File.Source name={inferTypeName} isExportable isIndexable isTypeOnly>
128
- {typeName && (
129
- <Type export name={inferTypeName}>
130
- {typeName}
131
- </Type>
132
- )}
133
- {!typeName && (
134
- <Type export name={inferTypeName}>
135
- {`z.infer<typeof ${name}>`}
136
- </Type>
137
- )}
35
+ <Type export name={inferTypeName}>
36
+ {`z.infer<typeof ${name}>`}
37
+ </Type>
138
38
  </File.Source>
139
39
  )}
140
40
  </>
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Import paths that use a namespace import (`import * as z from '...'`).
3
+ * All other import paths use a named import (`import { z } from '...'`).
4
+ */
5
+ export const ZOD_NAMESPACE_IMPORTS = new Set(['zod', 'zod/mini'] as const)
@@ -1,201 +1,248 @@
1
- import path from 'node:path'
2
- import { useMode, usePluginDriver } from '@kubb/core/hooks'
3
- import { type OperationSchema as OperationSchemaType, SchemaGenerator, schemaKeywords } from '@kubb/plugin-oas'
4
- import { createReactGenerator } from '@kubb/plugin-oas/generators'
5
- import { useOas, useOperationManager, useSchemaManager } from '@kubb/plugin-oas/hooks'
6
- import { getBanner, getFooter, getImports } from '@kubb/plugin-oas/utils'
7
- import { pluginTsName } from '@kubb/plugin-ts'
8
- import { File } from '@kubb/react-fabric'
9
- import { Zod } from '../components'
10
- import type { PluginZod } from '../types'
11
-
12
- export const zodGenerator = createReactGenerator<PluginZod>({
1
+ import type { Adapter } from '@kubb/core'
2
+ import { ast, defineGenerator } from '@kubb/core'
3
+ import type { AdapterOas } from '@kubb/adapter-oas'
4
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
5
+ import { Operations } from '../components/Operations.tsx'
6
+ import { Zod } from '../components/Zod.tsx'
7
+ import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
8
+ import { printerZod } from '../printers/printerZod.ts'
9
+ import { printerZodMini } from '../printers/printerZodMini.ts'
10
+ import type { PluginZod, ResolverZod } from '../types'
11
+ import { buildSchemaNames } from '../utils.ts'
12
+
13
+ type ZodPrinterEntry = { printer: ReturnType<typeof printerZod>; coercion: unknown; guidType: unknown; dateType: unknown }
14
+ type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidType: unknown }
15
+
16
+ // Per-build caches: keyed on resolver (unique per plugin instance per build, GC'd when released)
17
+ const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
18
+ const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
19
+
20
+ export const zodGenerator = defineGenerator<PluginZod>({
13
21
  name: 'zod',
14
- Operation({ config, operation, generator, plugin }) {
15
- const {
16
- options,
17
- options: { coercion: globalCoercion, inferred, typed, mapper, wrapOutput, version, guidType, mini },
18
- } = plugin
19
-
20
- const mode = useMode()
21
- const driver = usePluginDriver()
22
-
23
- const oas = useOas()
24
- const { getSchemas, getFile, getGroup } = useOperationManager(generator)
25
- const schemaManager = useSchemaManager()
26
-
27
- const file = getFile(operation)
28
- const schemas = getSchemas(operation)
29
- const schemaGenerator = new SchemaGenerator(options, {
30
- fabric: generator.context.fabric,
31
- oas,
32
- plugin,
33
- driver,
34
- events: generator.context.events,
35
- mode,
36
- override: options.override,
37
- })
38
-
39
- const operationSchemas = [schemas.pathParams, schemas.queryParams, schemas.headerParams, schemas.statusCodes, schemas.request, schemas.response]
40
- .flat()
41
- .filter(Boolean)
42
- const toZodPath = path.resolve(config.root, config.output.path, '.kubb/ToZod.ts')
22
+ renderer: jsxRendererSync,
23
+ schema(node, ctx) {
24
+ const { adapter, config, resolver, root, inputNode } = ctx
25
+ const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
26
+ const dateType = (adapter as Adapter<AdapterOas>).options.dateType
27
+
28
+ if (!node.name) {
29
+ return
30
+ }
43
31
 
44
- const mapOperationSchema = ({ name, schema: schemaOriginal, description, keysToOmit: keysToOmitOriginal, ...options }: OperationSchemaType) => {
45
- let schemaObject = schemaOriginal
46
- let keysToOmit = keysToOmitOriginal
32
+ const mode = ctx.getMode(output)
33
+ const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
47
34
 
48
- if ((schemaOriginal.anyOf || schemaOriginal.oneOf) && keysToOmitOriginal && keysToOmitOriginal.length > 0) {
49
- schemaObject = structuredClone(schemaOriginal)
35
+ const imports = adapter.getImports(node, (schemaName) => ({
36
+ name: resolver.resolveSchemaName(schemaName),
37
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
38
+ }))
50
39
 
51
- // Remove $ref so the schema parser generates inline schema instead of a reference
52
- delete schemaObject.$ref
40
+ const meta = {
41
+ name: resolver.resolveSchemaName(node.name),
42
+ file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
43
+ } as const
53
44
 
54
- for (const key of keysToOmitOriginal) {
55
- delete schemaObject.properties?.[key]
56
- }
45
+ const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : undefined
57
46
 
58
- if (Array.isArray(schemaObject.required)) {
59
- schemaObject.required = schemaObject.required.filter((key) => !keysToOmitOriginal.includes(key))
60
- }
47
+ const cyclicSchemas = ast.findCircularSchemas(inputNode.schemas)
61
48
 
62
- keysToOmit = undefined
49
+ const schemaPrinter = mini ? getCachedMiniPrinter() : getCachedStdPrinter()
50
+ function getCachedStdPrinter() {
51
+ const cached = zodPrinterCache.get(resolver)
52
+ if (cached && cached.coercion === coercion && cached.guidType === guidType && cached.dateType === dateType) {
53
+ return cached.printer
63
54
  }
64
-
65
- const hasProperties = Object.keys(schemaObject || {}).length > 0
66
- const hasDefaults = Object.values(schemaObject.properties || {}).some((prop) => prop && Object.hasOwn(prop, 'default'))
67
-
68
- const required = Array.isArray(schemaObject?.required) ? schemaObject.required.length > 0 : !!schemaObject?.required
69
- const optional = !required && !hasDefaults && hasProperties && name.includes('Params')
70
-
71
- if (!optional && Array.isArray(schemaObject.required) && !schemaObject.required.length) {
72
- schemaObject.required = Object.entries(schemaObject.properties || {})
73
- .filter(([_key, value]) => value && Object.hasOwn(value, 'default'))
74
- .map(([key]) => key)
55
+ const p = printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
56
+ zodPrinterCache.set(resolver, { printer: p, coercion, guidType, dateType })
57
+ return p
58
+ }
59
+ function getCachedMiniPrinter() {
60
+ const cached = zodMiniPrinterCache.get(resolver)
61
+ if (cached && cached.guidType === guidType) {
62
+ return cached.printer
75
63
  }
64
+ const p = printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
65
+ zodMiniPrinterCache.set(resolver, { printer: p, guidType })
66
+ return p
67
+ }
76
68
 
77
- const tree = [
78
- ...schemaGenerator.parse({ schema: schemaObject, name, parentName: null }),
79
- optional ? { keyword: schemaKeywords.optional } : undefined,
80
- ].filter(Boolean)
81
- const imports = getImports(tree)
82
- const group = options.operation ? getGroup(options.operation) : undefined
83
-
84
- const coercion = name.includes('Params') ? { numbers: true, strings: false, dates: true } : globalCoercion
85
-
86
- const zod = {
87
- name: schemaManager.getName(name, { type: 'function' }),
88
- inferTypeName: schemaManager.getName(name, { type: 'type' }),
89
- file: schemaManager.getFile(name),
90
- }
69
+ return (
70
+ <File
71
+ baseName={meta.file.baseName}
72
+ path={meta.file.path}
73
+ meta={meta.file.meta}
74
+ banner={resolver.resolveBanner(inputNode, { output, config })}
75
+ footer={resolver.resolveFooter(inputNode, { output, config })}
76
+ >
77
+ <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
78
+ {mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
91
79
 
92
- const type = {
93
- name: schemaManager.getName(name, {
94
- type: 'type',
95
- pluginName: pluginTsName,
96
- }),
97
- file: schemaManager.getFile(options.operationName || name, {
98
- pluginName: pluginTsName,
99
- group,
100
- }),
101
- }
80
+ <Zod name={meta.name} node={node} printer={schemaPrinter} inferTypeName={inferTypeName} />
81
+ </File>
82
+ )
83
+ },
84
+ operation(node, ctx) {
85
+ const { adapter, config, resolver, root, inputNode } = ctx
86
+ const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
87
+ const dateType = (adapter as Adapter<AdapterOas>).options.dateType
88
+
89
+ const mode = ctx.getMode(output)
90
+ const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
91
+
92
+ const params = ast.caseParams(node.parameters, paramsCasing)
93
+
94
+ const meta = {
95
+ file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
96
+ } as const
97
+
98
+ const cyclicSchemas = ast.findCircularSchemas(inputNode.schemas)
99
+
100
+ function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
101
+ if (!schema) return null
102
+
103
+ const inferTypeName = inferred ? resolver.resolveTypeName(name) : undefined
104
+
105
+ const imports = adapter.getImports(schema, (schemaName) => ({
106
+ name: resolver.resolveSchemaName(schemaName),
107
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
108
+ }))
109
+
110
+ const cachedStd = zodPrinterCache.get(resolver)
111
+ const cachedMini = zodMiniPrinterCache.get(resolver)
112
+ const schemaPrinter = mini
113
+ ? keysToOmit?.length
114
+ ? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
115
+ : cachedMini?.guidType === guidType
116
+ ? cachedMini.printer
117
+ : printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
118
+ : keysToOmit?.length
119
+ ? printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
120
+ : cachedStd?.coercion === coercion && cachedStd?.guidType === guidType && cachedStd?.dateType === dateType
121
+ ? cachedStd.printer
122
+ : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
102
123
 
103
124
  return (
104
125
  <>
105
- {typed && <File.Import isTypeOnly root={file.path} path={type.file.path} name={[type.name]} />}
106
- {imports.map((imp) => (
107
- <File.Import key={[imp.path, imp.name, imp.isTypeOnly].join('-')} root={file.path} path={imp.path} name={imp.name} />
108
- ))}
109
- <Zod
110
- name={zod.name}
111
- typeName={typed ? type.name : undefined}
112
- inferTypeName={inferred ? zod.inferTypeName : undefined}
113
- description={description}
114
- tree={tree}
115
- schema={schemaObject}
116
- mapper={mapper}
117
- coercion={coercion}
118
- keysToOmit={keysToOmit}
119
- wrapOutput={wrapOutput}
120
- version={plugin.options.version}
121
- guidType={guidType}
122
- emptySchemaType={plugin.options.emptySchemaType}
123
- mini={mini}
124
- />
126
+ {mode === 'split' &&
127
+ imports.map((imp) => <File.Import key={[name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
128
+ <Zod name={name} node={schema} printer={schemaPrinter} inferTypeName={inferTypeName} />
125
129
  </>
126
130
  )
127
131
  }
128
132
 
129
- const isZodImport = plugin.options.importPath === 'zod' || plugin.options.importPath === 'zod/mini'
133
+ const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param) }))
134
+
135
+ const responseSchemas = node.responses.map((res) =>
136
+ renderSchemaEntry({
137
+ schema: res.schema,
138
+ name: resolver.resolveResponseStatusName(node, res.statusCode),
139
+ keysToOmit: res.keysToOmit,
140
+ }),
141
+ )
142
+
143
+ const responsesWithSchema = node.responses.filter((res) => res.schema)
144
+ const responseUnionSchema =
145
+ responsesWithSchema.length > 0
146
+ ? (() => {
147
+ const responseUnionName = resolver.resolveResponseName(node)
148
+
149
+ // Collect all import names from response schemas to detect naming collisions.
150
+ // When a response is a $ref to a component schema whose resolved name matches
151
+ // the response union name, skip generation to avoid redeclaration errors.
152
+ const importedNames = new Set(
153
+ responsesWithSchema.flatMap((res) =>
154
+ res.schema
155
+ ? adapter
156
+ .getImports(res.schema, (schemaName) => ({
157
+ name: resolver.resolveSchemaName(schemaName),
158
+ path: '',
159
+ }))
160
+ .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
161
+ : [],
162
+ ),
163
+ )
164
+
165
+ if (importedNames.has(responseUnionName)) {
166
+ return null
167
+ }
168
+
169
+ const members = responsesWithSchema.map((res) => ast.createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, res.statusCode) }))
170
+ const unionNode = members.length === 1 ? members[0]! : ast.createSchema({ type: 'union', members })
171
+
172
+ return renderSchemaEntry({
173
+ schema: unionNode,
174
+ name: responseUnionName,
175
+ })
176
+ })()
177
+ : null
178
+
179
+ const requestSchema = node.requestBody?.content?.[0]?.schema
180
+ ? renderSchemaEntry({
181
+ schema: {
182
+ ...node.requestBody.content![0]!.schema!,
183
+ description: node.requestBody.description ?? node.requestBody.content![0]!.schema!.description,
184
+ },
185
+ name: resolver.resolveDataName(node),
186
+ keysToOmit: node.requestBody.content![0]!.keysToOmit,
187
+ })
188
+ : null
130
189
 
131
190
  return (
132
191
  <File
133
- baseName={file.baseName}
134
- path={file.path}
135
- meta={file.meta}
136
- banner={getBanner({ oas, output: plugin.options.output, config: driver.config })}
137
- footer={getFooter({ oas, output: plugin.options.output })}
192
+ baseName={meta.file.baseName}
193
+ path={meta.file.path}
194
+ meta={meta.file.meta}
195
+ banner={resolver.resolveBanner(inputNode, { output, config })}
196
+ footer={resolver.resolveFooter(inputNode, { output, config })}
138
197
  >
139
- <File.Import name={isZodImport ? 'z' : ['z']} path={plugin.options.importPath} isNameSpace={isZodImport} />
140
- {typed && version === '3' && <File.Import name={['ToZod']} isTypeOnly root={file.path} path={toZodPath} />}
141
- {operationSchemas.map(mapOperationSchema)}
198
+ <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
199
+ {paramSchemas}
200
+ {responseSchemas}
201
+ {responseUnionSchema}
202
+ {requestSchema}
142
203
  </File>
143
204
  )
144
205
  },
145
- Schema({ config, schema, plugin }) {
146
- const { getName, getFile } = useSchemaManager()
147
- const {
148
- options: { output, emptySchemaType, coercion, inferred, typed, mapper, importPath, wrapOutput, version, guidType, mini },
149
- } = plugin
150
- const driver = usePluginDriver()
151
- const oas = useOas()
152
-
153
- const imports = getImports(schema.tree)
154
-
155
- const zod = {
156
- name: getName(schema.name, { type: 'function' }),
157
- inferTypeName: getName(schema.name, { type: 'type' }),
158
- file: getFile(schema.name),
159
- }
206
+ operations(nodes, ctx) {
207
+ const { config, resolver, root, inputNode } = ctx
208
+ const { output, importPath, group, operations, paramsCasing } = ctx.options
160
209
 
161
- const type = {
162
- name: getName(schema.name, { type: 'type', pluginName: pluginTsName }),
163
- file: getFile(schema.name, { pluginName: pluginTsName }),
210
+ if (!operations) {
211
+ return
164
212
  }
213
+ const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
165
214
 
166
- const isZodImport = importPath === 'zod' || importPath === 'zod/mini'
167
- const toZodPath = path.resolve(config.root, config.output.path, '.kubb/ToZod.ts')
215
+ const meta = {
216
+ file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
217
+ } as const
218
+
219
+ const transformedOperations = nodes.map((node) => {
220
+ const params = ast.caseParams(node.parameters, paramsCasing)
221
+
222
+ return {
223
+ node,
224
+ data: buildSchemaNames(node, { params, resolver }),
225
+ }
226
+ })
227
+
228
+ const imports = transformedOperations.flatMap(({ node, data }) => {
229
+ const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string[]
230
+ const opFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
231
+
232
+ return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
233
+ })
168
234
 
169
235
  return (
170
236
  <File
171
- baseName={zod.file.baseName}
172
- path={zod.file.path}
173
- meta={zod.file.meta}
174
- banner={getBanner({ oas, output, config: driver.config })}
175
- footer={getFooter({ oas, output })}
237
+ baseName={meta.file.baseName}
238
+ path={meta.file.path}
239
+ meta={meta.file.meta}
240
+ banner={resolver.resolveBanner(inputNode, { output, config })}
241
+ footer={resolver.resolveFooter(inputNode, { output, config })}
176
242
  >
177
- <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
178
- {typed && <File.Import isTypeOnly root={zod.file.path} path={type.file.path} name={[type.name]} />}
179
- {typed && version === '3' && <File.Import name={['ToZod']} isTypeOnly root={zod.file.path} path={toZodPath} />}
180
- {imports.map((imp) => (
181
- <File.Import key={[imp.path, imp.name, imp.isTypeOnly].join('-')} root={zod.file.path} path={imp.path} name={imp.name} />
182
- ))}
183
-
184
- <Zod
185
- name={zod.name}
186
- typeName={typed ? type.name : undefined}
187
- inferTypeName={inferred ? zod.inferTypeName : undefined}
188
- description={schema.value.description}
189
- tree={schema.tree}
190
- schema={schema.value}
191
- mapper={mapper}
192
- coercion={coercion}
193
- wrapOutput={wrapOutput}
194
- version={version}
195
- guidType={guidType}
196
- emptySchemaType={emptySchemaType}
197
- mini={mini}
198
- />
243
+ <File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
244
+ {imports}
245
+ <Operations name="operations" operations={transformedOperations} />
199
246
  </File>
200
247
  )
201
248
  },
package/src/index.ts CHANGED
@@ -1,2 +1,11 @@
1
- export { pluginZod, pluginZodName } from './plugin.ts'
2
- export type { PluginZod } from './types.ts'
1
+ export { zodGenerator } from './generators/zodGenerator.tsx'
2
+
3
+ export { default, pluginZod, pluginZodName } from './plugin.ts'
4
+ export type { PrinterZodFactory, PrinterZodNodes, PrinterZodOptions } from './printers/printerZod.ts'
5
+ export { printerZod } from './printers/printerZod.ts'
6
+ export type { PrinterZodMiniFactory, PrinterZodMiniNodes, PrinterZodMiniOptions } from './printers/printerZodMini.ts'
7
+ export { printerZodMini } from './printers/printerZodMini.ts'
8
+
9
+ export { resolverZod } from './resolvers/resolverZod.ts'
10
+
11
+ export type { PluginZod, ResolverZod } from './types.ts'