@kubb/plugin-client 5.0.0-beta.4 → 5.0.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -24
- package/dist/clients/axios.cjs +26 -4
- package/dist/clients/axios.cjs.map +1 -1
- package/dist/clients/axios.d.ts +11 -5
- package/dist/clients/axios.js +26 -4
- package/dist/clients/axios.js.map +1 -1
- package/dist/clients/fetch.cjs +77 -9
- package/dist/clients/fetch.cjs.map +1 -1
- package/dist/clients/fetch.d.ts +10 -3
- package/dist/clients/fetch.js +77 -9
- package/dist/clients/fetch.js.map +1 -1
- package/dist/index.cjs +625 -354
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +160 -94
- package/dist/index.js +626 -355
- package/dist/index.js.map +1 -1
- package/dist/templates/clients/axios.source.cjs +1 -1
- package/dist/templates/clients/axios.source.d.ts +1 -1
- package/dist/templates/clients/axios.source.js +1 -1
- package/dist/templates/clients/fetch.source.cjs +1 -1
- package/dist/templates/clients/fetch.source.d.ts +1 -1
- package/dist/templates/clients/fetch.source.js +1 -1
- package/dist/templates/config.source.d.ts +1 -1
- package/extension.yaml +792 -301
- package/package.json +11 -16
- package/src/clients/axios.ts +41 -7
- package/src/clients/fetch.ts +106 -6
- package/src/components/ClassClient.tsx +19 -20
- package/src/components/Client.tsx +74 -53
- package/src/components/Operations.tsx +2 -1
- package/src/components/StaticClassClient.tsx +19 -20
- package/src/components/Url.tsx +8 -9
- package/src/components/WrapperClient.tsx +9 -5
- package/src/functionParams.ts +8 -8
- package/src/generators/classClientGenerator.tsx +51 -47
- package/src/generators/clientGenerator.tsx +37 -48
- package/src/generators/groupedClientGenerator.tsx +14 -8
- package/src/generators/operationsGenerator.tsx +14 -8
- package/src/generators/staticClassClientGenerator.tsx +45 -41
- package/src/plugin.ts +27 -26
- package/src/resolvers/resolverClient.ts +31 -8
- package/src/types.ts +93 -55
- package/src/utils.ts +35 -56
- package/templates/clients/axios.ts +0 -73
- package/templates/clients/fetch.ts +0 -96
- package/templates/config.ts +0 -43
- /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/package.json
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-client",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.0-beta.42",
|
|
4
|
+
"description": "Generate type-safe HTTP clients from your OpenAPI specification. Supports Axios, Fetch, and custom client adapters with full TypeScript inference and zero boilerplate.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
7
7
|
"axios",
|
|
8
|
-
"code-
|
|
8
|
+
"code-generation",
|
|
9
9
|
"codegen",
|
|
10
10
|
"fetch",
|
|
11
11
|
"http-client",
|
|
12
12
|
"kubb",
|
|
13
|
-
"oas",
|
|
14
13
|
"openapi",
|
|
15
|
-
"plugins",
|
|
16
|
-
"rest-api",
|
|
17
|
-
"sdk-generator",
|
|
18
14
|
"swagger",
|
|
19
|
-
"type-safe",
|
|
20
|
-
"type-safety",
|
|
21
15
|
"typescript"
|
|
22
16
|
],
|
|
23
17
|
"license": "MIT",
|
|
@@ -30,7 +24,6 @@
|
|
|
30
24
|
"files": [
|
|
31
25
|
"src",
|
|
32
26
|
"dist",
|
|
33
|
-
"templates",
|
|
34
27
|
"extension.yaml",
|
|
35
28
|
"*.d.ts",
|
|
36
29
|
"*.d.cts",
|
|
@@ -94,17 +87,18 @@
|
|
|
94
87
|
"registry": "https://registry.npmjs.org/"
|
|
95
88
|
},
|
|
96
89
|
"dependencies": {
|
|
97
|
-
"@kubb/core": "5.0.0-beta.
|
|
98
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
99
|
-
"@kubb/plugin-ts": "5.0.0-beta.
|
|
100
|
-
"@kubb/plugin-zod": "5.0.0-beta.
|
|
90
|
+
"@kubb/core": "5.0.0-beta.42",
|
|
91
|
+
"@kubb/renderer-jsx": "5.0.0-beta.42",
|
|
92
|
+
"@kubb/plugin-ts": "5.0.0-beta.42",
|
|
93
|
+
"@kubb/plugin-zod": "5.0.0-beta.42"
|
|
101
94
|
},
|
|
102
95
|
"devDependencies": {
|
|
103
|
-
"axios": "^1.
|
|
96
|
+
"axios": "^1.16.1",
|
|
97
|
+
"@internals/shared": "0.0.0",
|
|
104
98
|
"@internals/utils": "0.0.0"
|
|
105
99
|
},
|
|
106
100
|
"peerDependencies": {
|
|
107
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
101
|
+
"@kubb/renderer-jsx": "5.0.0-beta.42",
|
|
108
102
|
"axios": "^1.7.2"
|
|
109
103
|
},
|
|
110
104
|
"peerDependenciesMeta": {
|
|
@@ -129,6 +123,7 @@
|
|
|
129
123
|
"lint:fix": "oxlint --fix .",
|
|
130
124
|
"release": "pnpm publish --no-git-check",
|
|
131
125
|
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
|
|
126
|
+
"release:stage": "pnpm stage publish --no-git-check",
|
|
132
127
|
"start": "tsdown --watch",
|
|
133
128
|
"test": "vitest --passWithNoTests",
|
|
134
129
|
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
|
package/src/clients/axios.ts
CHANGED
|
@@ -4,6 +4,13 @@ import axios from 'axios'
|
|
|
4
4
|
declare const AXIOS_BASE: string
|
|
5
5
|
declare const AXIOS_HEADERS: string
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).
|
|
9
|
+
* Non-string values are JSON-serialized before the request is sent.
|
|
10
|
+
*/
|
|
11
|
+
export type HeaderValue = string | number | boolean | null | undefined | object
|
|
12
|
+
export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Subset of AxiosRequestConfig
|
|
9
16
|
*/
|
|
@@ -16,8 +23,9 @@ export type RequestConfig<TData = unknown> = {
|
|
|
16
23
|
responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
|
17
24
|
signal?: AbortSignal
|
|
18
25
|
validateStatus?: (status: number) => boolean
|
|
19
|
-
headers?:
|
|
26
|
+
headers?: HeadersInit
|
|
20
27
|
paramsSerializer?: AxiosRequestConfig['paramsSerializer']
|
|
28
|
+
contentType?: string
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/**
|
|
@@ -55,22 +63,48 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
|
|
|
55
63
|
...merged,
|
|
56
64
|
...config,
|
|
57
65
|
headers: {
|
|
58
|
-
...merged.headers,
|
|
59
|
-
...config.headers,
|
|
66
|
+
...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),
|
|
67
|
+
...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
|
|
60
68
|
},
|
|
61
69
|
}
|
|
62
70
|
}, {})
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Serializes header values into the string form axios ultimately puts on the wire.
|
|
75
|
+
* Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`
|
|
76
|
+
* are sent in their canonical JSON-string form rather than `[object Object]`.
|
|
77
|
+
*/
|
|
78
|
+
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
79
|
+
if (!headers) return {}
|
|
80
|
+
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
81
|
+
const result: Record<string, string> = {}
|
|
82
|
+
for (const [key, value] of entries) {
|
|
83
|
+
if (value === undefined || value === null) continue
|
|
84
|
+
result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const axiosInstance = axios.create(getConfig() as AxiosRequestConfig)
|
|
66
90
|
|
|
67
91
|
export const client = async <TResponseData, TError = unknown, TRequestData = unknown>(
|
|
68
92
|
config: RequestConfig<TRequestData>,
|
|
69
93
|
_request?: unknown,
|
|
70
94
|
): Promise<ResponseConfig<TResponseData>> => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
const requestConfig = mergeConfig(getConfig(), config)
|
|
96
|
+
const { contentType, headers, ...axiosConfig } = requestConfig
|
|
97
|
+
return axiosInstance
|
|
98
|
+
.request<TResponseData, ResponseConfig<TResponseData>>({
|
|
99
|
+
...axiosConfig,
|
|
100
|
+
headers: {
|
|
101
|
+
...(contentType && contentType !== 'multipart/form-data' ? { 'Content-Type': contentType } : {}),
|
|
102
|
+
...serializeHeaders(headers),
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.catch((e: AxiosError<TError>) => {
|
|
106
|
+
throw e
|
|
107
|
+
})
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
client.getConfig = getConfig
|
package/src/clients/fetch.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type RequestCredentials = 'omit' | 'same-origin' | 'include'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).
|
|
8
|
+
* Non-string values are JSON-serialized before the request is sent.
|
|
9
|
+
*/
|
|
10
|
+
export type HeaderValue = string | number | boolean | null | undefined | object
|
|
11
|
+
export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* Subset of FetchRequestConfig
|
|
8
15
|
*/
|
|
@@ -14,8 +21,9 @@ export type RequestConfig<TData = unknown> = {
|
|
|
14
21
|
data?: TData | FormData
|
|
15
22
|
responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
|
16
23
|
signal?: AbortSignal
|
|
17
|
-
headers?:
|
|
24
|
+
headers?: HeadersInit
|
|
18
25
|
credentials?: RequestCredentials
|
|
26
|
+
contentType?: string
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -50,6 +58,93 @@ export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T
|
|
|
50
58
|
}, {})
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Serializes header values into the string form `fetch` expects.
|
|
63
|
+
* Objects (including arrays) are JSON-stringified so spec-defined object
|
|
64
|
+
* headers like `X-Filter` are sent in their canonical JSON-string form.
|
|
65
|
+
*/
|
|
66
|
+
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
67
|
+
if (!headers) return {}
|
|
68
|
+
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
69
|
+
const result: Record<string, string> = {}
|
|
70
|
+
for (const [key, value] of entries) {
|
|
71
|
+
if (value === undefined || value === null) continue
|
|
72
|
+
result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Serializes the request body into the form `fetch` expects.
|
|
79
|
+
* `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through
|
|
80
|
+
* untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`
|
|
81
|
+
* and JSON-serialized otherwise.
|
|
82
|
+
*/
|
|
83
|
+
function serializeBody(data: unknown, contentType?: string): BodyInit | undefined {
|
|
84
|
+
if (data === undefined || data === null) return undefined
|
|
85
|
+
if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
86
|
+
return data as BodyInit
|
|
87
|
+
}
|
|
88
|
+
if (typeof data === 'string') return data
|
|
89
|
+
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
90
|
+
return new URLSearchParams(data as Record<string, string>)
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(data)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
|
|
97
|
+
*/
|
|
98
|
+
function detectResponseType(contentType: string | null): RequestConfig['responseType'] {
|
|
99
|
+
if (!contentType) return undefined
|
|
100
|
+
if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
|
|
101
|
+
if (contentType.includes('text/')) return 'text'
|
|
102
|
+
if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parses a `fetch` response body.
|
|
108
|
+
*
|
|
109
|
+
* - Empty responses (204/205/304 or no body) resolve to `{}`.
|
|
110
|
+
* - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching
|
|
111
|
+
* `Response` method.
|
|
112
|
+
* - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.
|
|
113
|
+
*/
|
|
114
|
+
async function parseResponse<TData>(response: Response, responseType?: RequestConfig['responseType']): Promise<TData> {
|
|
115
|
+
if ([204, 205, 304].includes(response.status) || !response.body) {
|
|
116
|
+
return {} as TData
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {
|
|
120
|
+
case 'text':
|
|
121
|
+
case 'document':
|
|
122
|
+
return (await response.text()) as TData
|
|
123
|
+
case 'blob':
|
|
124
|
+
return (await response.blob()) as TData
|
|
125
|
+
case 'arraybuffer':
|
|
126
|
+
return (await response.arrayBuffer()) as TData
|
|
127
|
+
case 'stream':
|
|
128
|
+
return response.body as TData
|
|
129
|
+
case 'json':
|
|
130
|
+
return (await response.json()) as TData
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Explicit but unrecognized responseType keeps the JSON default; otherwise read text and parse it.
|
|
134
|
+
if (responseType) {
|
|
135
|
+
return (await response.json()) as TData
|
|
136
|
+
}
|
|
137
|
+
const text = await response.text()
|
|
138
|
+
if (!text) {
|
|
139
|
+
return {} as TData
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(text) as TData
|
|
143
|
+
} catch {
|
|
144
|
+
return text as TData
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
53
148
|
export type ResponseErrorConfig<TError = unknown> = TError
|
|
54
149
|
|
|
55
150
|
export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
|
|
@@ -77,18 +172,23 @@ export const client = async <TResponseData, _TError = unknown, RequestData = unk
|
|
|
77
172
|
targetUrl += `?${normalizedParams}`
|
|
78
173
|
}
|
|
79
174
|
|
|
80
|
-
const
|
|
175
|
+
const headers: Record<string, string> = {
|
|
176
|
+
...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),
|
|
177
|
+
...serializeHeaders(config.headers),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const response = await globalThis.fetch(targetUrl, {
|
|
81
181
|
credentials: config.credentials || 'same-origin',
|
|
82
182
|
method: config.method?.toUpperCase(),
|
|
83
|
-
body: config.data
|
|
183
|
+
body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),
|
|
84
184
|
signal: config.signal,
|
|
85
|
-
headers
|
|
185
|
+
headers,
|
|
86
186
|
})
|
|
87
187
|
|
|
88
|
-
const data =
|
|
188
|
+
const data = await parseResponse<TResponseData>(response, config.responseType)
|
|
89
189
|
|
|
90
190
|
return {
|
|
91
|
-
data
|
|
191
|
+
data,
|
|
92
192
|
status: response.status,
|
|
93
193
|
statusText: response.statusText,
|
|
94
194
|
headers: response.headers as Headers,
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
|
|
1
2
|
import { buildJSDoc, URLPath } from '@internals/utils'
|
|
2
|
-
import
|
|
3
|
+
import { ast } from '@kubb/core'
|
|
3
4
|
import type { ResolverTs } from '@kubb/plugin-ts'
|
|
4
5
|
import { functionPrinter } from '@kubb/plugin-ts'
|
|
5
6
|
import type { ResolverZod } from '@kubb/plugin-zod'
|
|
6
7
|
import { File } from '@kubb/renderer-jsx'
|
|
7
8
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
8
9
|
import type { PluginClient } from '../types.ts'
|
|
9
|
-
import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement
|
|
10
|
-
import {
|
|
10
|
+
import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement } from '../utils.ts'
|
|
11
|
+
import { buildClientParamsNode } from './Client.tsx'
|
|
11
12
|
|
|
12
13
|
type OperationData = {
|
|
13
14
|
node: ast.OperationNode
|
|
14
15
|
name: string
|
|
15
16
|
tsResolver: ResolverTs
|
|
16
|
-
zodResolver?: ResolverZod
|
|
17
|
+
zodResolver?: ResolverZod | null
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
type Props = {
|
|
@@ -21,7 +22,7 @@ type Props = {
|
|
|
21
22
|
isExportable?: boolean
|
|
22
23
|
isIndexable?: boolean
|
|
23
24
|
operations: Array<OperationData>
|
|
24
|
-
baseURL: string | undefined
|
|
25
|
+
baseURL: string | null | undefined
|
|
25
26
|
dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
|
|
26
27
|
paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
|
|
27
28
|
paramsType: PluginClient['resolvedOptions']['pathParamsType']
|
|
@@ -34,8 +35,8 @@ type GenerateMethodProps = {
|
|
|
34
35
|
node: ast.OperationNode
|
|
35
36
|
name: string
|
|
36
37
|
tsResolver: ResolverTs
|
|
37
|
-
zodResolver?: ResolverZod
|
|
38
|
-
baseURL: string | undefined
|
|
38
|
+
zodResolver?: ResolverZod | null
|
|
39
|
+
baseURL: string | null | undefined
|
|
39
40
|
dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
|
|
40
41
|
parser: PluginClient['resolvedOptions']['parser'] | undefined
|
|
41
42
|
paramsType: PluginClient['resolvedOptions']['paramsType']
|
|
@@ -57,26 +58,25 @@ function generateMethod({
|
|
|
57
58
|
paramsCasing,
|
|
58
59
|
pathParamsType,
|
|
59
60
|
}: GenerateMethodProps): string {
|
|
61
|
+
if (!ast.isHttpOperationNode(node)) return ''
|
|
60
62
|
const path = new URLPath(node.path, { casing: paramsCasing })
|
|
61
|
-
const contentType = node
|
|
62
|
-
const isFormData = contentType === 'multipart/form-data'
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
: undefined
|
|
67
|
-
const headers = buildHeaders(contentType, !!headerParamsName)
|
|
63
|
+
const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
|
|
64
|
+
const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
|
|
65
|
+
const { header: headerParams } = getOperationParameters(node)
|
|
66
|
+
const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
|
|
67
|
+
const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
|
|
68
68
|
const generics = buildGenerics(node, tsResolver)
|
|
69
|
-
const paramsNode =
|
|
69
|
+
const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
|
|
70
70
|
const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
|
|
71
|
-
const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
|
|
72
|
-
const jsdoc = buildJSDoc(
|
|
71
|
+
const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, isMultipleContentTypes, hasFormData, headers })
|
|
72
|
+
const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
|
|
73
73
|
|
|
74
74
|
const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
|
|
75
|
-
const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
|
|
75
|
+
const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
|
|
76
76
|
const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver })
|
|
77
77
|
|
|
78
78
|
const methodBody = [
|
|
79
|
-
|
|
79
|
+
`const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
|
|
80
80
|
'',
|
|
81
81
|
requestDataLine,
|
|
82
82
|
formDataLine,
|
|
@@ -135,4 +135,3 @@ ${methods.join('\n\n')}
|
|
|
135
135
|
</File.Source>
|
|
136
136
|
)
|
|
137
137
|
}
|
|
138
|
-
ClassClient.getParams = Client.getParams
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildOperationComments,
|
|
3
|
+
buildParamsMapping,
|
|
4
|
+
buildRequestConfigType,
|
|
5
|
+
getContentTypeInfo,
|
|
6
|
+
getOperationParameters,
|
|
7
|
+
getResponseType,
|
|
8
|
+
resolveSuccessNames,
|
|
9
|
+
} from '@internals/shared'
|
|
1
10
|
import { isValidVarName, URLPath } from '@internals/utils'
|
|
2
11
|
import { ast } from '@kubb/core'
|
|
3
12
|
import type { ResolverTs } from '@kubb/plugin-ts'
|
|
@@ -7,8 +16,7 @@ import { File, Function } from '@kubb/renderer-jsx'
|
|
|
7
16
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
8
17
|
import { createFunctionParams } from '../functionParams.ts'
|
|
9
18
|
import type { PluginClient } from '../types.ts'
|
|
10
|
-
import {
|
|
11
|
-
import { Url } from './Url.tsx'
|
|
19
|
+
import { buildUrlParamsNode } from './Url.tsx'
|
|
12
20
|
|
|
13
21
|
type Props = {
|
|
14
22
|
name: string
|
|
@@ -18,7 +26,7 @@ type Props = {
|
|
|
18
26
|
isConfigurable?: boolean
|
|
19
27
|
returnType?: string
|
|
20
28
|
|
|
21
|
-
baseURL: string | undefined
|
|
29
|
+
baseURL: string | null | undefined
|
|
22
30
|
dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
|
|
23
31
|
paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
|
|
24
32
|
paramsType: PluginClient['resolvedOptions']['pathParamsType']
|
|
@@ -26,7 +34,7 @@ type Props = {
|
|
|
26
34
|
parser: PluginClient['resolvedOptions']['parser'] | undefined
|
|
27
35
|
node: ast.OperationNode
|
|
28
36
|
tsResolver: ResolverTs
|
|
29
|
-
zodResolver?: ResolverZod
|
|
37
|
+
zodResolver?: ResolverZod | null
|
|
30
38
|
children?: KubbReactNode
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -41,26 +49,33 @@ type GetParamsProps = {
|
|
|
41
49
|
|
|
42
50
|
const declarationPrinter = functionPrinter({ mode: 'declaration' })
|
|
43
51
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
export function buildClientParamsNode({
|
|
53
|
+
paramsType,
|
|
54
|
+
paramsCasing,
|
|
55
|
+
pathParamsType,
|
|
56
|
+
node,
|
|
57
|
+
tsResolver,
|
|
58
|
+
isConfigurable,
|
|
59
|
+
}: GetParamsProps): ast.FunctionParametersNode {
|
|
47
60
|
return ast.createOperationParams(node, {
|
|
48
61
|
paramsType,
|
|
49
62
|
pathParamsType: paramsType === 'object' ? 'object' : pathParamsType === 'object' ? 'object' : 'inline',
|
|
50
63
|
paramsCasing,
|
|
51
64
|
resolver: tsResolver,
|
|
52
|
-
extraParams:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
extraParams: [
|
|
66
|
+
...(isConfigurable
|
|
67
|
+
? [
|
|
68
|
+
ast.createFunctionParameter({
|
|
69
|
+
name: 'config',
|
|
70
|
+
type: ast.createParamsType({
|
|
71
|
+
variant: 'reference',
|
|
72
|
+
name: buildRequestConfigType(node, tsResolver),
|
|
73
|
+
}),
|
|
74
|
+
default: '{}',
|
|
59
75
|
}),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: [],
|
|
76
|
+
]
|
|
77
|
+
: []),
|
|
78
|
+
],
|
|
64
79
|
})
|
|
65
80
|
}
|
|
66
81
|
|
|
@@ -82,28 +97,27 @@ export function Client({
|
|
|
82
97
|
children,
|
|
83
98
|
isConfigurable = true,
|
|
84
99
|
}: Props): KubbReactNode {
|
|
100
|
+
if (!ast.isHttpOperationNode(node)) return null
|
|
85
101
|
const path = new URLPath(node.path)
|
|
86
|
-
const contentType = node
|
|
87
|
-
const isFormData = contentType === 'multipart/form-data'
|
|
102
|
+
const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
|
|
103
|
+
const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
|
|
104
|
+
const responseType = getResponseType(node)
|
|
88
105
|
|
|
89
|
-
const originalPathParams
|
|
90
|
-
const casedPathParams =
|
|
91
|
-
const originalQueryParams = node.parameters.filter((p) => p.in === 'query')
|
|
92
|
-
const casedQueryParams = ast.caseParams(originalQueryParams, paramsCasing)
|
|
93
|
-
const originalHeaderParams = node.parameters.filter((p) => p.in === 'header')
|
|
94
|
-
const casedHeaderParams = ast.caseParams(originalHeaderParams, paramsCasing)
|
|
106
|
+
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
|
|
107
|
+
const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
|
|
95
108
|
|
|
96
|
-
const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) :
|
|
97
|
-
const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) :
|
|
98
|
-
const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) :
|
|
109
|
+
const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null
|
|
110
|
+
const queryParamsMapping = paramsCasing ? buildParamsMapping(originalQueryParams, casedQueryParams) : null
|
|
111
|
+
const headerParamsMapping = paramsCasing ? buildParamsMapping(originalHeaderParams, casedHeaderParams) : null
|
|
99
112
|
|
|
100
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) :
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
113
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null
|
|
114
|
+
const successNames = resolveSuccessNames(node, tsResolver)
|
|
115
|
+
const responseName = successNames.length > 0 ? successNames.join(' | ') : tsResolver.resolveResponseName(node)
|
|
116
|
+
const queryParamsName = originalQueryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, originalQueryParams[0]!) : null
|
|
117
|
+
const headerParamsName = originalHeaderParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, originalHeaderParams[0]!) : null
|
|
104
118
|
|
|
105
|
-
const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) :
|
|
106
|
-
const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) :
|
|
119
|
+
const zodResponseName = zodResolver && parser === 'zod' ? zodResolver.resolveResponseName?.(node) : null
|
|
120
|
+
const zodRequestName = zodResolver && parser === 'zod' && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null
|
|
107
121
|
|
|
108
122
|
const errorNames = node.responses
|
|
109
123
|
.filter((r) => {
|
|
@@ -113,14 +127,14 @@ export function Client({
|
|
|
113
127
|
.map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
114
128
|
|
|
115
129
|
const headers = [
|
|
116
|
-
contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` :
|
|
117
|
-
headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') :
|
|
130
|
+
!isMultipleContentTypes && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null,
|
|
131
|
+
headerParamsName ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null,
|
|
118
132
|
].filter(Boolean)
|
|
119
133
|
|
|
120
134
|
const TError = `ResponseErrorConfig<${errorNames.length > 0 ? errorNames.join(' | ') : 'Error'}>`
|
|
121
135
|
|
|
122
136
|
const generics = [responseName, TError, requestName || 'unknown'].filter(Boolean)
|
|
123
|
-
const paramsNode =
|
|
137
|
+
const paramsNode = buildClientParamsNode({
|
|
124
138
|
paramsType,
|
|
125
139
|
paramsCasing,
|
|
126
140
|
pathParamsType,
|
|
@@ -130,7 +144,7 @@ export function Client({
|
|
|
130
144
|
})
|
|
131
145
|
const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
|
|
132
146
|
|
|
133
|
-
const urlParamsNode =
|
|
147
|
+
const urlParamsNode = buildUrlParamsNode({
|
|
134
148
|
paramsType,
|
|
135
149
|
paramsCasing,
|
|
136
150
|
pathParamsType,
|
|
@@ -155,23 +169,30 @@ export function Client({
|
|
|
155
169
|
? {
|
|
156
170
|
value: `\`${baseURL}\``,
|
|
157
171
|
}
|
|
158
|
-
:
|
|
159
|
-
params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) :
|
|
172
|
+
: null,
|
|
173
|
+
params: queryParamsName ? (queryParamsMapping ? { value: 'mappedParams' } : {}) : null,
|
|
160
174
|
data: requestName
|
|
161
175
|
? {
|
|
162
|
-
value:
|
|
176
|
+
value:
|
|
177
|
+
isMultipleContentTypes && hasFormData
|
|
178
|
+
? "contentType === 'multipart/form-data' ? formData as FormData : requestData"
|
|
179
|
+
: isFormData
|
|
180
|
+
? 'formData as FormData'
|
|
181
|
+
: 'requestData',
|
|
163
182
|
}
|
|
164
|
-
:
|
|
183
|
+
: null,
|
|
184
|
+
contentType: isConfigurable && isMultipleContentTypes ? {} : null,
|
|
185
|
+
responseType: responseType ? { value: JSON.stringify(responseType) } : null,
|
|
165
186
|
requestConfig: isConfigurable
|
|
166
187
|
? {
|
|
167
188
|
mode: 'inlineSpread',
|
|
168
189
|
}
|
|
169
|
-
:
|
|
190
|
+
: null,
|
|
170
191
|
headers: headers.length
|
|
171
192
|
? {
|
|
172
193
|
value: isConfigurable ? `{ ${headers.join(', ')}, ...requestConfig.headers }` : `{ ${headers.join(', ')} }`,
|
|
173
194
|
}
|
|
174
|
-
:
|
|
195
|
+
: null,
|
|
175
196
|
},
|
|
176
197
|
},
|
|
177
198
|
})
|
|
@@ -182,8 +203,8 @@ export function Client({
|
|
|
182
203
|
<>
|
|
183
204
|
{dataReturnType === 'full' && parser === 'zod' && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`}
|
|
184
205
|
{dataReturnType === 'data' && parser === 'zod' && zodResponseName && `return ${zodResponseName}.parse(res.data)`}
|
|
185
|
-
{dataReturnType === 'full' && parser
|
|
186
|
-
{dataReturnType === 'data' && parser
|
|
206
|
+
{dataReturnType === 'full' && parser !== 'zod' && 'return res'}
|
|
207
|
+
{dataReturnType === 'data' && parser !== 'zod' && 'return res.data'}
|
|
187
208
|
</>
|
|
188
209
|
)
|
|
189
210
|
|
|
@@ -198,11 +219,13 @@ export function Client({
|
|
|
198
219
|
export={isExportable}
|
|
199
220
|
params={paramsSignature}
|
|
200
221
|
JSDoc={{
|
|
201
|
-
comments:
|
|
222
|
+
comments: buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }),
|
|
202
223
|
}}
|
|
203
224
|
returnType={returnType}
|
|
204
225
|
>
|
|
205
|
-
{isConfigurable
|
|
226
|
+
{isConfigurable
|
|
227
|
+
? `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${JSON.stringify(contentType)}, ` : ''}...requestConfig } = config`
|
|
228
|
+
: ''}
|
|
206
229
|
<br />
|
|
207
230
|
<br />
|
|
208
231
|
{pathParamsMapping &&
|
|
@@ -236,11 +259,11 @@ export function Client({
|
|
|
236
259
|
)}
|
|
237
260
|
{parser === 'zod' && zodRequestName ? `const requestData = ${zodRequestName}.parse(data)` : requestName && 'const requestData = data'}
|
|
238
261
|
<br />
|
|
239
|
-
{isFormData && requestName && 'const formData = buildFormData(requestData)'}
|
|
262
|
+
{(isFormData || (isMultipleContentTypes && hasFormData)) && requestName && 'const formData = buildFormData(requestData)'}
|
|
240
263
|
<br />
|
|
241
264
|
{isConfigurable
|
|
242
265
|
? `const res = await request<${generics.join(', ')}>(${clientParams.toCall()})`
|
|
243
|
-
: `const res = await
|
|
266
|
+
: `const res = await client<${generics.join(', ')}>(${clientParams.toCall()})`}
|
|
244
267
|
<br />
|
|
245
268
|
{childrenElement}
|
|
246
269
|
</Function>
|
|
@@ -248,5 +271,3 @@ export function Client({
|
|
|
248
271
|
</>
|
|
249
272
|
)
|
|
250
273
|
}
|
|
251
|
-
|
|
252
|
-
Client.getParams = getParams
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { URLPath } from '@internals/utils'
|
|
2
|
-
import
|
|
2
|
+
import { ast } from '@kubb/core'
|
|
3
3
|
import { Const, File } from '@kubb/renderer-jsx'
|
|
4
4
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
5
5
|
|
|
@@ -12,6 +12,7 @@ export function Operations({ name, nodes }: OperationsProps): KubbReactNode {
|
|
|
12
12
|
const operationsObject: Record<string, { path: string; method: string }> = {}
|
|
13
13
|
|
|
14
14
|
nodes.forEach((node) => {
|
|
15
|
+
if (!ast.isHttpOperationNode(node)) return
|
|
15
16
|
operationsObject[node.operationId] = {
|
|
16
17
|
path: new URLPath(node.path).URL,
|
|
17
18
|
method: node.method.toLowerCase(),
|