@kubb/plugin-client 5.0.0-beta.42 → 5.0.0-beta.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/axios.cjs.map +1 -1
- package/dist/clients/axios.js.map +1 -1
- package/dist/clients/fetch.cjs.map +1 -1
- package/dist/clients/fetch.js.map +1 -1
- package/dist/index.cjs +393 -342
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +57 -22
- package/dist/index.js +389 -342
- package/dist/index.js.map +1 -1
- package/dist/templates/clients/axios.source.cjs.map +1 -1
- package/dist/templates/clients/axios.source.js.map +1 -1
- package/dist/templates/clients/fetch.source.cjs.map +1 -1
- package/dist/templates/clients/fetch.source.js.map +1 -1
- package/dist/templates/config.source.cjs.map +1 -1
- package/dist/templates/config.source.js.map +1 -1
- package/package.json +10 -17
- package/src/components/ClassClient.tsx +31 -7
- package/src/components/Client.tsx +33 -25
- package/src/components/StaticClassClient.tsx +31 -7
- package/src/components/Url.tsx +2 -3
- package/src/generators/classClientGenerator.tsx +20 -12
- package/src/generators/clientGenerator.tsx +18 -10
- package/src/generators/groupedClientGenerator.tsx +2 -2
- package/src/generators/operationsGenerator.ts +47 -0
- package/src/generators/staticClassClientGenerator.tsx +20 -12
- package/src/index.ts +2 -1
- package/src/plugin.ts +5 -4
- package/src/resolvers/resolverClient.ts +5 -4
- package/src/types.ts +29 -21
- package/src/utils.ts +122 -22
- package/extension.yaml +0 -1267
- package/src/components/Operations.tsx +0 -29
- package/src/generators/operationsGenerator.tsx +0 -34
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Url } from '@internals/utils'
|
|
2
|
+
import { ast, defineGenerator } from '@kubb/core'
|
|
3
|
+
import type { PluginClient } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates an `operations.ts` file that re-exports every operation grouped
|
|
7
|
+
* by HTTP method. Enabled when `pluginClient({ operations: true })`. Useful
|
|
8
|
+
* for building meta-tooling on top of the generated client (route
|
|
9
|
+
* registries, API explorers).
|
|
10
|
+
*/
|
|
11
|
+
export const operationsGenerator = defineGenerator<PluginClient>({
|
|
12
|
+
name: 'client',
|
|
13
|
+
operations(nodes, ctx) {
|
|
14
|
+
const { config, resolver, root } = ctx
|
|
15
|
+
const { output, group } = ctx.options
|
|
16
|
+
|
|
17
|
+
const name = 'operations'
|
|
18
|
+
const file = resolver.resolveFile({ name, extname: '.ts' }, { root, output, group: group ?? undefined })
|
|
19
|
+
|
|
20
|
+
const operationsObject: Record<string, { path: string; method: string }> = {}
|
|
21
|
+
for (const node of nodes) {
|
|
22
|
+
if (!ast.isHttpOperationNode(node)) continue
|
|
23
|
+
operationsObject[node.operationId] = {
|
|
24
|
+
path: Url.toPath(node.path),
|
|
25
|
+
method: node.method.toLowerCase(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
ast.createFile({
|
|
31
|
+
baseName: file.baseName,
|
|
32
|
+
path: file.path,
|
|
33
|
+
meta: file.meta,
|
|
34
|
+
banner: resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } }),
|
|
35
|
+
footer: resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } }),
|
|
36
|
+
sources: [
|
|
37
|
+
ast.createSource({
|
|
38
|
+
name,
|
|
39
|
+
isExportable: true,
|
|
40
|
+
isIndexable: true,
|
|
41
|
+
nodes: [ast.createConst({ name, export: true, nodes: [ast.createText(JSON.stringify(operationsObject, undefined, 2))] })],
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
})
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
|
|
2
|
+
import { getOperationParameters, operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
|
|
3
3
|
import { camelCase } from '@internals/utils'
|
|
4
4
|
import { ast, defineGenerator } from '@kubb/core'
|
|
5
5
|
import type { ResolverTs } from '@kubb/plugin-ts'
|
|
6
6
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
7
7
|
import type { ResolverZod } from '@kubb/plugin-zod'
|
|
8
8
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
9
|
-
import { File,
|
|
9
|
+
import { File, jsxRenderer } from '@kubb/renderer-jsx'
|
|
10
10
|
import { StaticClassClient } from '../components/StaticClassClient'
|
|
11
11
|
import type { PluginClient } from '../types'
|
|
12
|
+
import { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from '../utils.ts'
|
|
12
13
|
|
|
13
14
|
type OperationData = {
|
|
14
15
|
node: ast.OperationNode
|
|
@@ -29,10 +30,12 @@ function resolveTypeImportNames(node: ast.OperationNode, tsResolver: ResolverTs)
|
|
|
29
30
|
return resolveOperationTypeNames(node, tsResolver, { order: 'body-response-first' })
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod): Array<string> {
|
|
33
|
+
function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod, parser: PluginClient['resolvedOptions']['parser']): Array<string> {
|
|
34
|
+
const { query: queryParams } = getOperationParameters(node)
|
|
33
35
|
const names: Array<string | null | undefined> = [
|
|
34
|
-
zodResolver.resolveResponseName?.(node),
|
|
35
|
-
node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
|
|
36
|
+
resolveResponseParser(parser) === 'zod' ? zodResolver.resolveResponseName?.(node) : null,
|
|
37
|
+
resolveRequestParser(parser) === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
|
|
38
|
+
resolveQueryParamsParser(parser) === 'zod' && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]!) : null,
|
|
36
39
|
]
|
|
37
40
|
return names.filter((n): n is string => Boolean(n))
|
|
38
41
|
}
|
|
@@ -45,7 +48,7 @@ function resolveZodImportNames(node: ast.OperationNode, zodResolver: ResolverZod
|
|
|
45
48
|
*/
|
|
46
49
|
export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
47
50
|
name: 'staticClassClient',
|
|
48
|
-
renderer:
|
|
51
|
+
renderer: jsxRenderer,
|
|
49
52
|
operations(nodes, ctx) {
|
|
50
53
|
const { config, driver, resolver, root } = ctx
|
|
51
54
|
const { output, group, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath } = ctx.options
|
|
@@ -56,7 +59,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
56
59
|
|
|
57
60
|
const tsResolver = driver.getResolver(pluginTsName)
|
|
58
61
|
const tsPluginOptions = pluginTs.options
|
|
59
|
-
const pluginZod = parser
|
|
62
|
+
const pluginZod = isParserEnabled(parser) ? driver.getPlugin(pluginZodName) : null
|
|
60
63
|
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
|
|
61
64
|
|
|
62
65
|
function buildOperationData(node: ast.OperationNode): OperationData {
|
|
@@ -87,7 +90,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
87
90
|
const controllers = nodes.reduce((acc, operationNode) => {
|
|
88
91
|
if (!ast.isHttpOperationNode(operationNode)) return acc
|
|
89
92
|
const tag = operationNode.tags[0]
|
|
90
|
-
const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.
|
|
93
|
+
const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveClassName('Client')
|
|
91
94
|
|
|
92
95
|
if (!tag && !group) {
|
|
93
96
|
const name = resolver.resolveClassName('ApiClient')
|
|
@@ -144,7 +147,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
144
147
|
|
|
145
148
|
ops.forEach((op) => {
|
|
146
149
|
if (!op.zodFile || !zodResolver) return
|
|
147
|
-
const names = resolveZodImportNames(op.node, zodResolver)
|
|
150
|
+
const names = resolveZodImportNames(op.node, zodResolver, parser)
|
|
148
151
|
if (!zodImportsByFile.has(op.zodFile.path)) {
|
|
149
152
|
zodImportsByFile.set(op.zodFile.path, new Set())
|
|
150
153
|
}
|
|
@@ -162,8 +165,9 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
162
165
|
<>
|
|
163
166
|
{controllers.map(({ name, file, operations: ops }) => {
|
|
164
167
|
const { typeImportsByFile, typeFilesByPath } = collectTypeImports(ops)
|
|
165
|
-
const { zodImportsByFile, zodFilesByPath } =
|
|
166
|
-
|
|
168
|
+
const { zodImportsByFile, zodFilesByPath } = isParserEnabled(parser)
|
|
169
|
+
? collectZodImports(ops)
|
|
170
|
+
: { zodImportsByFile: new Map<string, Set<string>>(), zodFilesByPath: new Map<string, ast.FileNode>() }
|
|
167
171
|
const hasFormData = ops.some((op) => op.node.requestBody?.content?.some((e) => e.contentType === 'multipart/form-data') ?? false)
|
|
168
172
|
|
|
169
173
|
return (
|
|
@@ -196,6 +200,10 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
196
200
|
|
|
197
201
|
{hasFormData && <File.Import name={['buildFormData']} root={file.path} path={path.resolve(root, '.kubb/config.ts')} />}
|
|
198
202
|
|
|
203
|
+
{parser === 'zod' && zodResolver && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && (
|
|
204
|
+
<File.Import name={['z']} path="zod" isTypeOnly />
|
|
205
|
+
)}
|
|
206
|
+
|
|
199
207
|
{Array.from(typeImportsByFile.entries()).map(([filePath, importSet]) => {
|
|
200
208
|
const typeFile = typeFilesByPath.get(filePath)
|
|
201
209
|
if (!typeFile) return null
|
|
@@ -204,7 +212,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
|
|
|
204
212
|
return <File.Import key={filePath} name={importNames} root={file.path} path={typeFile.path} isTypeOnly />
|
|
205
213
|
})}
|
|
206
214
|
|
|
207
|
-
{parser
|
|
215
|
+
{isParserEnabled(parser) &&
|
|
208
216
|
Array.from(zodImportsByFile.entries()).map(([filePath, importSet]) => {
|
|
209
217
|
const zodFile = zodFilesByPath.get(filePath)
|
|
210
218
|
if (!zodFile) return null
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,9 @@ export { Client } from './components/Client.tsx'
|
|
|
2
2
|
export { classClientGenerator } from './generators/classClientGenerator.tsx'
|
|
3
3
|
export { clientGenerator } from './generators/clientGenerator.tsx'
|
|
4
4
|
export { groupedClientGenerator } from './generators/groupedClientGenerator.tsx'
|
|
5
|
-
export { operationsGenerator } from './generators/operationsGenerator.
|
|
5
|
+
export { operationsGenerator } from './generators/operationsGenerator.ts'
|
|
6
6
|
export { staticClassClientGenerator } from './generators/staticClassClientGenerator.tsx'
|
|
7
7
|
export { default, pluginClient, pluginClientName } from './plugin.ts'
|
|
8
8
|
export { resolverClient } from './resolvers/resolverClient.ts'
|
|
9
|
+
export { isParserEnabled, resolveQueryParamsParser, resolveRequestParser, resolveResponseParser } from './utils.ts'
|
|
9
10
|
export type { ClientImportPath, PluginClient, ResolverClient } from './types.ts'
|
package/src/plugin.ts
CHANGED
|
@@ -7,13 +7,14 @@ import { pluginZodName } from '@kubb/plugin-zod'
|
|
|
7
7
|
import { classClientGenerator } from './generators/classClientGenerator.tsx'
|
|
8
8
|
import { clientGenerator } from './generators/clientGenerator.tsx'
|
|
9
9
|
import { groupedClientGenerator } from './generators/groupedClientGenerator.tsx'
|
|
10
|
-
import { operationsGenerator } from './generators/operationsGenerator.
|
|
10
|
+
import { operationsGenerator } from './generators/operationsGenerator.ts'
|
|
11
11
|
import { staticClassClientGenerator } from './generators/staticClassClientGenerator.tsx'
|
|
12
12
|
import { resolverClient } from './resolvers/resolverClient.ts'
|
|
13
13
|
import { source as axiosClientSource } from './templates/clients/axios.source.ts'
|
|
14
14
|
import { source as fetchClientSource } from './templates/clients/fetch.source.ts'
|
|
15
15
|
import { source as configSource } from './templates/config.source.ts'
|
|
16
16
|
import type { PluginClient } from './types.ts'
|
|
17
|
+
import { isParserEnabled } from './utils.ts'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Canonical plugin name for `@kubb/plugin-client`. Used for driver lookups and
|
|
@@ -48,7 +49,7 @@ export const pluginClientName = 'plugin-client' satisfies PluginClient['name']
|
|
|
48
49
|
*/
|
|
49
50
|
export const pluginClient = definePlugin<PluginClient>((options) => {
|
|
50
51
|
const {
|
|
51
|
-
output = { path: 'clients',
|
|
52
|
+
output = { path: 'clients', barrel: { type: 'named' } },
|
|
52
53
|
group,
|
|
53
54
|
exclude = [],
|
|
54
55
|
include,
|
|
@@ -80,12 +81,12 @@ export const pluginClient = definePlugin<PluginClient>((options) => {
|
|
|
80
81
|
operations ? operationsGenerator : null,
|
|
81
82
|
].filter((x): x is NonNullable<typeof x> => Boolean(x))
|
|
82
83
|
|
|
83
|
-
const groupConfig = createGroupConfig(group
|
|
84
|
+
const groupConfig = createGroupConfig(group)
|
|
84
85
|
|
|
85
86
|
return {
|
|
86
87
|
name: pluginClientName,
|
|
87
88
|
options,
|
|
88
|
-
dependencies: [pluginTsName, parser
|
|
89
|
+
dependencies: [pluginTsName, isParserEnabled(parser) ? pluginZodName : null].filter((dependency): dependency is string => Boolean(dependency)),
|
|
89
90
|
hooks: {
|
|
90
91
|
'kubb:plugin:setup'(ctx) {
|
|
91
92
|
const resolver = userResolver ? { ...resolverClient, ...userResolver } : resolverClient
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { camelCase, ensureValidVarName, pascalCase } from '@internals/utils'
|
|
1
|
+
import { camelCase, ensureValidVarName, pascalCase, toFilePath } from '@internals/utils'
|
|
2
2
|
import { defineResolver } from '@kubb/core'
|
|
3
3
|
import type { PluginClient } from '../types.ts'
|
|
4
4
|
|
|
@@ -13,6 +13,7 @@ import type { PluginClient } from '../types.ts'
|
|
|
13
13
|
*
|
|
14
14
|
* resolverClient.default('list pets', 'function') // 'listPets'
|
|
15
15
|
* resolverClient.resolveClassName('pet') // 'Pet'
|
|
16
|
+
* resolverClient.resolveGroupName('pet') // 'PetClient'
|
|
16
17
|
* resolverClient.resolveUrlName(operationNode) // 'getShowPetByIdUrl'
|
|
17
18
|
* ```
|
|
18
19
|
*/
|
|
@@ -20,8 +21,8 @@ export const resolverClient = defineResolver<PluginClient>(() => ({
|
|
|
20
21
|
name: 'default',
|
|
21
22
|
pluginName: 'plugin-client',
|
|
22
23
|
default(name, type) {
|
|
23
|
-
|
|
24
|
-
return
|
|
24
|
+
if (type === 'file') return toFilePath(name)
|
|
25
|
+
return ensureValidVarName(camelCase(name))
|
|
25
26
|
},
|
|
26
27
|
resolveName(name) {
|
|
27
28
|
return this.default(name, 'function')
|
|
@@ -33,7 +34,7 @@ export const resolverClient = defineResolver<PluginClient>(() => ({
|
|
|
33
34
|
return ensureValidVarName(pascalCase(name))
|
|
34
35
|
},
|
|
35
36
|
resolveGroupName(name) {
|
|
36
|
-
return ensureValidVarName(pascalCase(name))
|
|
37
|
+
return ensureValidVarName(pascalCase(`${name} Client`))
|
|
37
38
|
},
|
|
38
39
|
resolveClientPropertyName(name) {
|
|
39
40
|
return ensureValidVarName(camelCase(name))
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
|
|
1
|
+
import type { ast, Exclude, Generator, Group, Include, Output, OutputOptions, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The concrete resolver type for `@kubb/plugin-client`.
|
|
@@ -21,7 +21,12 @@ export type ResolverClient = Resolver & {
|
|
|
21
21
|
*/
|
|
22
22
|
resolveClassName(this: ResolverClient, name: string): string
|
|
23
23
|
/**
|
|
24
|
-
* Resolves the generated class name for tag-based client groups.
|
|
24
|
+
* Resolves the generated class name for tag-based client groups. The default
|
|
25
|
+
* appends a `Client` suffix (tag `pet` becomes `PetClient`) so the class never
|
|
26
|
+
* collides with the schema model of the same name in the barrel.
|
|
27
|
+
*
|
|
28
|
+
* @example Resolving tag-group class names
|
|
29
|
+
* `resolver.resolveGroupName('pet') // -> 'PetClient'`
|
|
25
30
|
*/
|
|
26
31
|
resolveGroupName(this: ResolverClient, name: string): string
|
|
27
32
|
/**
|
|
@@ -112,17 +117,13 @@ type ParamsTypeOptions =
|
|
|
112
117
|
pathParamsType?: 'object' | 'inline'
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Split generated files into subfolders based on the operation's tag.
|
|
124
|
-
*/
|
|
125
|
-
group?: Group
|
|
120
|
+
/**
|
|
121
|
+
* Where the generated client files are written and how they are exported, plus the optional
|
|
122
|
+
* `group` strategy. The `group` option organizes `output.mode: 'directory'` output into per-tag or per-path subdirectories.
|
|
123
|
+
*
|
|
124
|
+
* @default { path: 'clients', barrel: { type: 'named' } }
|
|
125
|
+
*/
|
|
126
|
+
export type Options = OutputOptions & {
|
|
126
127
|
/**
|
|
127
128
|
* Skip operations matching at least one entry in the list.
|
|
128
129
|
*/
|
|
@@ -157,7 +158,9 @@ export type Options = {
|
|
|
157
158
|
/**
|
|
158
159
|
* Shape of the value returned by each generated client function.
|
|
159
160
|
* - `'data'` — only the response body.
|
|
160
|
-
* - `'full'` — the full response
|
|
161
|
+
* - `'full'` — the full response as a discriminated union keyed by HTTP status code.
|
|
162
|
+
* Each member is `{ status: N; data: StatusNType; statusText: string }`,
|
|
163
|
+
* so narrowing on `res.status` also narrows `res.data` to the matching response type.
|
|
161
164
|
*
|
|
162
165
|
* @default 'data'
|
|
163
166
|
*/
|
|
@@ -170,14 +173,19 @@ export type Options = {
|
|
|
170
173
|
*/
|
|
171
174
|
paramsCasing?: 'camelcase'
|
|
172
175
|
/**
|
|
173
|
-
* Validator applied to response bodies
|
|
174
|
-
* - `false` (default)
|
|
175
|
-
*
|
|
176
|
-
* - `'zod'
|
|
176
|
+
* Validator applied to request and response bodies using schemas from `@kubb/plugin-zod`.
|
|
177
|
+
* - `false` (default): no validation. The response is returned as-is.
|
|
178
|
+
* - `'zod'`: validates response bodies only (backward-compatible shorthand).
|
|
179
|
+
* - `{ request?: 'zod'; response?: 'zod' }`: opt in per direction. `request` validates the
|
|
180
|
+
* request body and query parameters before the call. `response` validates the response body.
|
|
177
181
|
*
|
|
178
182
|
* @default false
|
|
183
|
+
* @example Response only (shorthand)
|
|
184
|
+
* `parser: 'zod'`
|
|
185
|
+
* @example Both directions
|
|
186
|
+
* `parser: { request: 'zod', response: 'zod' }`
|
|
179
187
|
*/
|
|
180
|
-
parser?: false | 'zod'
|
|
188
|
+
parser?: false | 'zod' | { request?: 'zod'; response?: 'zod' }
|
|
181
189
|
/**
|
|
182
190
|
* Shape of the generated client.
|
|
183
191
|
* - `'function'` — one standalone async function per operation.
|
|
@@ -206,8 +214,8 @@ export type Options = {
|
|
|
206
214
|
* sdk: { className: 'PetStoreSDK' },
|
|
207
215
|
* })
|
|
208
216
|
* // class PetStoreSDK {
|
|
209
|
-
* // readonly
|
|
210
|
-
* // readonly
|
|
217
|
+
* // readonly petClient: PetClient
|
|
218
|
+
* // readonly storeClient: StoreClient
|
|
211
219
|
* // constructor(config = {}) { ... }
|
|
212
220
|
* // }
|
|
213
221
|
* ```
|
package/src/utils.ts
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
|
-
import { getOperationParameters, getResponseType, resolveSuccessNames } from '@internals/shared'
|
|
2
|
-
import type { URLPath } from '@internals/utils'
|
|
1
|
+
import { buildStatusUnionType, getOperationParameters, getResponseType, resolveSuccessNames } from '@internals/shared'
|
|
3
2
|
import { ast } from '@kubb/core'
|
|
4
3
|
import type { ResolverTs } from '@kubb/plugin-ts'
|
|
5
4
|
import type { ResolverZod } from '@kubb/plugin-zod'
|
|
6
5
|
import { createFunctionParams } from './functionParams.ts'
|
|
7
6
|
import type { PluginClient } from './types.ts'
|
|
8
7
|
|
|
8
|
+
type ParserOption = PluginClient['resolvedOptions']['parser']
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when any direction of the parser uses Zod (used for dependency checks).
|
|
12
|
+
*/
|
|
13
|
+
export function isParserEnabled(parser: ParserOption | undefined | false): boolean {
|
|
14
|
+
if (!parser) return false
|
|
15
|
+
if (parser === 'zod') return true
|
|
16
|
+
return !!(parser.request || parser.response)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns `'zod'` when request body parsing is enabled, `null` otherwise.
|
|
21
|
+
* The string shorthand `'zod'` also enables request body parsing (existing behavior).
|
|
22
|
+
*/
|
|
23
|
+
export function resolveRequestParser(parser: ParserOption | undefined | false): 'zod' | null {
|
|
24
|
+
if (!parser) return null
|
|
25
|
+
if (parser === 'zod') return 'zod'
|
|
26
|
+
return parser.request ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise.
|
|
31
|
+
* Only the object form `{ request: 'zod' }` enables query-params parsing.
|
|
32
|
+
* The string shorthand `'zod'` does not, preserving its existing behavior.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveQueryParamsParser(parser: ParserOption | undefined | false): 'zod' | null {
|
|
35
|
+
if (!parser || parser === 'zod') return null
|
|
36
|
+
return parser.request ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns `'zod'` when response-direction parsing is enabled, `null` otherwise.
|
|
41
|
+
* `parser: 'zod'` (string shorthand) maps to response parsing and returns `'zod'`.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveResponseParser(parser: ParserOption | undefined | false): 'zod' | null {
|
|
44
|
+
if (!parser) return null
|
|
45
|
+
if (parser === 'zod') return 'zod'
|
|
46
|
+
return parser.response ?? null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { buildStatusUnionType }
|
|
50
|
+
|
|
9
51
|
/**
|
|
10
52
|
* Builds HTTP headers array for a client request.
|
|
11
53
|
* Includes Content-Type (if not default) and spreads header parameters if present.
|
|
@@ -18,16 +60,42 @@ export function buildHeaders(contentType: string, hasHeaderParams: boolean): Arr
|
|
|
18
60
|
}
|
|
19
61
|
|
|
20
62
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
63
|
+
* Returns the generic type arguments — response, error, and request body — for a generated
|
|
64
|
+
* client call.
|
|
65
|
+
*
|
|
66
|
+
* When `dataReturnType` is `'full'`, the response generic widens to a union of all documented
|
|
67
|
+
* status types. When `parser` is `'zod'` and a request body schema exists, the request type
|
|
68
|
+
* uses `z.output<typeof schema>` to reflect post-transform types (e.g. date coercion).
|
|
23
69
|
*/
|
|
24
|
-
export function buildGenerics(
|
|
70
|
+
export function buildGenerics(
|
|
71
|
+
node: ast.OperationNode,
|
|
72
|
+
tsResolver: ResolverTs,
|
|
73
|
+
options: {
|
|
74
|
+
dataReturnType?: PluginClient['resolvedOptions']['dataReturnType']
|
|
75
|
+
zodResolver?: ResolverZod | null
|
|
76
|
+
parser?: PluginClient['resolvedOptions']['parser']
|
|
77
|
+
} = {},
|
|
78
|
+
): Array<string> {
|
|
79
|
+
const allStatusNames = node.responses.map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
25
80
|
const successNames = resolveSuccessNames(node, tsResolver)
|
|
26
|
-
const responseName =
|
|
81
|
+
const responseName =
|
|
82
|
+
options.dataReturnType === 'full'
|
|
83
|
+
? allStatusNames.length > 0
|
|
84
|
+
? allStatusNames.join(' | ')
|
|
85
|
+
: tsResolver.resolveResponseName(node)
|
|
86
|
+
: successNames.length > 0
|
|
87
|
+
? successNames.join(' | ')
|
|
88
|
+
: tsResolver.resolveResponseName(node)
|
|
27
89
|
const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
|
|
28
90
|
const errorNames = node.responses.filter((r) => Number.parseInt(r.statusCode, 10) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
29
91
|
const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
|
|
30
|
-
|
|
92
|
+
|
|
93
|
+
const zodRequestName =
|
|
94
|
+
options.parser === 'zod' && options.zodResolver && node.requestBody?.content?.[0]?.schema ? (options.zodResolver.resolveDataName?.(node) ?? null) : null
|
|
95
|
+
|
|
96
|
+
const requestGenericType = zodRequestName ? `z.output<typeof ${zodRequestName}>` : requestName || 'unknown'
|
|
97
|
+
|
|
98
|
+
return [responseName, TError, requestGenericType].filter(Boolean)
|
|
31
99
|
}
|
|
32
100
|
|
|
33
101
|
/**
|
|
@@ -36,22 +104,24 @@ export function buildGenerics(node: ast.OperationNode, tsResolver: ResolverTs):
|
|
|
36
104
|
*/
|
|
37
105
|
export function buildClassClientParams({
|
|
38
106
|
node,
|
|
39
|
-
|
|
107
|
+
url,
|
|
40
108
|
baseURL,
|
|
41
109
|
tsResolver,
|
|
42
110
|
isFormData,
|
|
43
111
|
isMultipleContentTypes,
|
|
44
112
|
hasFormData,
|
|
45
113
|
headers,
|
|
114
|
+
zodQueryParamsName,
|
|
46
115
|
}: {
|
|
47
116
|
node: ast.OperationNode
|
|
48
|
-
|
|
117
|
+
url: string
|
|
49
118
|
baseURL: string | null | undefined
|
|
50
119
|
tsResolver: ResolverTs
|
|
51
120
|
isFormData: boolean
|
|
52
121
|
isMultipleContentTypes: boolean
|
|
53
122
|
hasFormData: boolean
|
|
54
123
|
headers: Array<string>
|
|
124
|
+
zodQueryParamsName?: string | null
|
|
55
125
|
}) {
|
|
56
126
|
const { query: queryParams } = getOperationParameters(node)
|
|
57
127
|
const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]!) : null
|
|
@@ -69,14 +139,14 @@ export function buildClassClientParams({
|
|
|
69
139
|
value: JSON.stringify(ast.isHttpOperationNode(node) ? node.method.toUpperCase() : ''),
|
|
70
140
|
},
|
|
71
141
|
url: {
|
|
72
|
-
value:
|
|
142
|
+
value: url,
|
|
73
143
|
},
|
|
74
144
|
baseURL: baseURL
|
|
75
145
|
? {
|
|
76
146
|
value: JSON.stringify(baseURL),
|
|
77
147
|
}
|
|
78
148
|
: null,
|
|
79
|
-
params: queryParamsName ? {} : null,
|
|
149
|
+
params: queryParamsName ? (zodQueryParamsName ? { value: 'requestParams' } : {}) : null,
|
|
80
150
|
data: requestName
|
|
81
151
|
? {
|
|
82
152
|
value:
|
|
@@ -101,7 +171,7 @@ export function buildClassClientParams({
|
|
|
101
171
|
|
|
102
172
|
/**
|
|
103
173
|
* Builds the request data parsing line for client methods.
|
|
104
|
-
* Applies Zod validation
|
|
174
|
+
* Applies Zod validation when `parser.request === 'zod'`, otherwise assigns data directly.
|
|
105
175
|
*/
|
|
106
176
|
export function buildRequestDataLine({
|
|
107
177
|
parser,
|
|
@@ -112,8 +182,9 @@ export function buildRequestDataLine({
|
|
|
112
182
|
node: ast.OperationNode
|
|
113
183
|
zodResolver?: ResolverZod | null
|
|
114
184
|
}): string {
|
|
115
|
-
const
|
|
116
|
-
|
|
185
|
+
const requestParser = resolveRequestParser(parser)
|
|
186
|
+
const zodRequestName = zodResolver && requestParser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
|
|
187
|
+
if (requestParser === 'zod' && zodRequestName) {
|
|
117
188
|
return `const requestData = ${zodRequestName}.parse(data)`
|
|
118
189
|
}
|
|
119
190
|
if (node.requestBody?.content?.[0]?.schema) {
|
|
@@ -122,6 +193,28 @@ export function buildRequestDataLine({
|
|
|
122
193
|
return ''
|
|
123
194
|
}
|
|
124
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Builds the query parameters parsing line for client methods.
|
|
198
|
+
* Returns an empty string when no query params exist or query-params parsing is not enabled.
|
|
199
|
+
* Only the object form `parser: { request: 'zod' }` triggers this. `parser: 'zod'` does not.
|
|
200
|
+
*/
|
|
201
|
+
export function buildQueryParamsLine({
|
|
202
|
+
parser,
|
|
203
|
+
node,
|
|
204
|
+
zodResolver,
|
|
205
|
+
}: {
|
|
206
|
+
parser: PluginClient['resolvedOptions']['parser'] | undefined
|
|
207
|
+
node: ast.OperationNode
|
|
208
|
+
zodResolver?: ResolverZod | null
|
|
209
|
+
}): string {
|
|
210
|
+
if (resolveQueryParamsParser(parser) !== 'zod' || !zodResolver) return ''
|
|
211
|
+
const { query: queryParams } = getOperationParameters(node)
|
|
212
|
+
if (queryParams.length === 0) return ''
|
|
213
|
+
const zodQueryParamsName = zodResolver.resolveQueryParamsName?.(node, queryParams[0]!)
|
|
214
|
+
if (!zodQueryParamsName) return ''
|
|
215
|
+
return `const requestParams = ${zodQueryParamsName}.parse(params)`
|
|
216
|
+
}
|
|
217
|
+
|
|
125
218
|
/**
|
|
126
219
|
* Builds the form data conversion line for file upload requests.
|
|
127
220
|
* Returns empty string if not applicable.
|
|
@@ -132,28 +225,35 @@ export function buildFormDataLine(isFormData: boolean, hasRequest: boolean): str
|
|
|
132
225
|
|
|
133
226
|
/**
|
|
134
227
|
* Builds the return statement for a client method.
|
|
135
|
-
*
|
|
228
|
+
* When `dataReturnType` is `'full'`, casts the response to the status-discriminated union type.
|
|
229
|
+
* When `parser.response` is `'zod'`, pipes the response body through the Zod schema before returning.
|
|
136
230
|
*/
|
|
137
231
|
export function buildReturnStatement({
|
|
138
232
|
dataReturnType,
|
|
139
233
|
parser,
|
|
140
234
|
node,
|
|
141
235
|
zodResolver,
|
|
236
|
+
tsResolver,
|
|
142
237
|
}: {
|
|
143
238
|
dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
|
|
144
239
|
parser: PluginClient['resolvedOptions']['parser'] | undefined
|
|
145
240
|
node: ast.OperationNode
|
|
146
241
|
zodResolver?: ResolverZod | null
|
|
242
|
+
tsResolver?: ResolverTs | null
|
|
147
243
|
}): string {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
244
|
+
const responseParser = resolveResponseParser(parser)
|
|
245
|
+
const zodResponseName = zodResolver && responseParser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
|
|
246
|
+
|
|
247
|
+
if (dataReturnType === 'full' && tsResolver) {
|
|
248
|
+
const unionType = buildStatusUnionType(node, tsResolver)
|
|
249
|
+
if (responseParser === 'zod' && zodResponseName) {
|
|
250
|
+
return `return {...res, data: ${zodResponseName}.parse(res.data)} as ${unionType}`
|
|
251
|
+
}
|
|
252
|
+
return `return res as ${unionType}`
|
|
151
253
|
}
|
|
152
|
-
|
|
254
|
+
|
|
255
|
+
if (dataReturnType === 'data' && responseParser === 'zod' && zodResponseName) {
|
|
153
256
|
return `return ${zodResponseName}.parse(res.data)`
|
|
154
257
|
}
|
|
155
|
-
if (dataReturnType === 'full' && parser !== 'zod') {
|
|
156
|
-
return 'return res'
|
|
157
|
-
}
|
|
158
258
|
return 'return res.data'
|
|
159
259
|
}
|