@kubb/plugin-client 5.0.0-beta.56 → 5.0.0-beta.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.56",
3
+ "version": "5.0.0-beta.64",
4
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",
@@ -22,7 +22,6 @@
22
22
  "directory": "packages/plugin-client"
23
23
  },
24
24
  "files": [
25
- "src",
26
25
  "dist",
27
26
  "*.d.ts",
28
27
  "*.d.cts",
@@ -86,11 +85,11 @@
86
85
  "registry": "https://registry.npmjs.org/"
87
86
  },
88
87
  "dependencies": {
89
- "@kubb/ast": "5.0.0-beta.55",
90
- "@kubb/core": "5.0.0-beta.55",
91
- "@kubb/renderer-jsx": "5.0.0-beta.55",
92
- "@kubb/plugin-ts": "5.0.0-beta.56",
93
- "@kubb/plugin-zod": "5.0.0-beta.56"
88
+ "@kubb/ast": "5.0.0-beta.63",
89
+ "@kubb/core": "5.0.0-beta.63",
90
+ "@kubb/renderer-jsx": "5.0.0-beta.63",
91
+ "@kubb/plugin-ts": "5.0.0-beta.64",
92
+ "@kubb/plugin-zod": "5.0.0-beta.64"
94
93
  },
95
94
  "devDependencies": {
96
95
  "axios": "^1.17.0",
@@ -98,7 +97,7 @@
98
97
  "@internals/utils": "0.0.0"
99
98
  },
100
99
  "peerDependencies": {
101
- "@kubb/renderer-jsx": "5.0.0-beta.55",
100
+ "@kubb/renderer-jsx": "5.0.0-beta.63",
102
101
  "axios": "^1.7.2"
103
102
  },
104
103
  "peerDependenciesMeta": {
@@ -1,113 +0,0 @@
1
- import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
2
- import axios from 'axios'
3
-
4
- declare const AXIOS_BASE: string
5
- declare const AXIOS_HEADERS: string
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
-
14
- /**
15
- * Subset of AxiosRequestConfig
16
- */
17
- export type RequestConfig<TData = unknown> = {
18
- baseURL?: string
19
- url?: string
20
- method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
21
- params?: unknown
22
- data?: TData | FormData
23
- responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
24
- signal?: AbortSignal
25
- validateStatus?: (status: number) => boolean
26
- headers?: HeadersInit
27
- paramsSerializer?: AxiosRequestConfig['paramsSerializer']
28
- contentType?: string
29
- }
30
-
31
- /**
32
- * Subset of AxiosResponse
33
- */
34
- export type ResponseConfig<TData = unknown> = {
35
- data: TData
36
- status: number
37
- statusText: string
38
- headers: AxiosResponse['headers']
39
- }
40
-
41
- export type ResponseErrorConfig<TError = unknown> = AxiosError<TError>
42
-
43
- export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
44
- config: RequestConfig<TRequestData>,
45
- request?: unknown,
46
- ) => Promise<ResponseConfig<TResponseData>>
47
-
48
- let _config: Partial<RequestConfig> = {
49
- baseURL: typeof AXIOS_BASE !== 'undefined' ? AXIOS_BASE : undefined,
50
- headers: typeof AXIOS_HEADERS !== 'undefined' ? JSON.parse(AXIOS_HEADERS) : undefined,
51
- }
52
-
53
- export const getConfig = () => _config
54
-
55
- export const setConfig = (config: RequestConfig) => {
56
- _config = config
57
- return getConfig()
58
- }
59
-
60
- export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {
61
- return configs.reduce<Partial<T>>((merged, config) => {
62
- return {
63
- ...merged,
64
- ...config,
65
- headers: {
66
- ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),
67
- ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
68
- },
69
- }
70
- }, {})
71
- }
72
-
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)
90
-
91
- export const client = async <TResponseData, TError = unknown, TRequestData = unknown>(
92
- config: RequestConfig<TRequestData>,
93
- _request?: unknown,
94
- ): Promise<ResponseConfig<TResponseData>> => {
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
- })
108
- }
109
-
110
- client.getConfig = getConfig
111
- client.setConfig = setConfig
112
-
113
- export default client
@@ -1,201 +0,0 @@
1
- /**
2
- * RequestCredentials
3
- */
4
- export type RequestCredentials = 'omit' | 'same-origin' | 'include'
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
-
13
- /**
14
- * Subset of FetchRequestConfig
15
- */
16
- export type RequestConfig<TData = unknown> = {
17
- baseURL?: string
18
- url?: string
19
- method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
20
- params?: unknown
21
- data?: TData | FormData
22
- responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
23
- signal?: AbortSignal
24
- headers?: HeadersInit
25
- credentials?: RequestCredentials
26
- contentType?: string
27
- }
28
-
29
- /**
30
- * Subset of FetchResponse
31
- */
32
- export type ResponseConfig<TData = unknown> = {
33
- data: TData
34
- status: number
35
- statusText: string
36
- headers: Headers
37
- }
38
-
39
- let _config: Partial<RequestConfig> = {}
40
-
41
- export const getConfig = () => _config
42
-
43
- export const setConfig = (config: Partial<RequestConfig>) => {
44
- _config = config
45
- return getConfig()
46
- }
47
-
48
- export const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {
49
- return configs.reduce<Partial<T>>((merged, config) => {
50
- return {
51
- ...merged,
52
- ...config,
53
- headers: {
54
- ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),
55
- ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),
56
- },
57
- }
58
- }, {})
59
- }
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
-
148
- export type ResponseErrorConfig<TError = unknown> = TError
149
-
150
- export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
151
- config: RequestConfig<TRequestData>,
152
- request?: unknown,
153
- ) => Promise<ResponseConfig<TResponseData>>
154
-
155
- export const client = async <TResponseData, _TError = unknown, RequestData = unknown>(
156
- paramsConfig: RequestConfig<RequestData>,
157
- _request?: unknown,
158
- ): Promise<ResponseConfig<TResponseData>> => {
159
- const normalizedParams = new URLSearchParams()
160
-
161
- const config = mergeConfig(getConfig(), paramsConfig)
162
-
163
- Object.entries(config.params || {}).forEach(([key, value]) => {
164
- if (value !== undefined) {
165
- normalizedParams.append(key, value === null ? 'null' : value.toString())
166
- }
167
- })
168
-
169
- let targetUrl = [config.baseURL, config.url].filter(Boolean).join('')
170
-
171
- if (config.params) {
172
- targetUrl += `?${normalizedParams}`
173
- }
174
-
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, {
181
- credentials: config.credentials || 'same-origin',
182
- method: config.method?.toUpperCase(),
183
- body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),
184
- signal: config.signal,
185
- headers,
186
- })
187
-
188
- const data = await parseResponse<TResponseData>(response, config.responseType)
189
-
190
- return {
191
- data,
192
- status: response.status,
193
- statusText: response.statusText,
194
- headers: response.headers as Headers,
195
- }
196
- }
197
-
198
- client.getConfig = getConfig
199
- client.setConfig = setConfig
200
-
201
- export default client
@@ -1,161 +0,0 @@
1
- import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
2
- import { Url } from '@internals/utils'
3
- import { buildJSDoc, stringify } from '@kubb/ast/utils'
4
- import { ast } from '@kubb/core'
5
- import type { ResolverTs } from '@kubb/plugin-ts'
6
- import { functionPrinter } from '@kubb/plugin-ts'
7
- import type { ResolverZod } from '@kubb/plugin-zod'
8
- import { File } from '@kubb/renderer-jsx'
9
- import type { KubbReactNode } from '@kubb/renderer-jsx/types'
10
- import type { PluginClient } from '../types.ts'
11
- import {
12
- buildClassClientParams,
13
- buildFormDataLine,
14
- buildGenerics,
15
- buildHeaders,
16
- buildQueryParamsLine,
17
- buildRequestDataLine,
18
- buildReturnStatement,
19
- resolveQueryParamsParser,
20
- } from '../utils.ts'
21
- import { buildClientParamsNode } from './Client.tsx'
22
-
23
- type OperationData = {
24
- node: ast.OperationNode
25
- name: string
26
- tsResolver: ResolverTs
27
- zodResolver?: ResolverZod | null
28
- }
29
-
30
- type Props = {
31
- name: string
32
- isExportable?: boolean
33
- isIndexable?: boolean
34
- operations: Array<OperationData>
35
- baseURL: string | null | undefined
36
- dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
37
- paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
38
- paramsType: PluginClient['resolvedOptions']['pathParamsType']
39
- pathParamsType: PluginClient['resolvedOptions']['pathParamsType']
40
- parser: PluginClient['resolvedOptions']['parser'] | undefined
41
- children?: KubbReactNode
42
- }
43
-
44
- type GenerateMethodProps = {
45
- node: ast.OperationNode
46
- name: string
47
- tsResolver: ResolverTs
48
- zodResolver?: ResolverZod | null
49
- baseURL: string | null | undefined
50
- dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
51
- parser: PluginClient['resolvedOptions']['parser'] | undefined
52
- paramsType: PluginClient['resolvedOptions']['paramsType']
53
- paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
54
- pathParamsType: PluginClient['resolvedOptions']['pathParamsType']
55
- }
56
-
57
- const declarationPrinter = functionPrinter({ mode: 'declaration' })
58
-
59
- function generateMethod({
60
- node,
61
- name,
62
- tsResolver,
63
- zodResolver,
64
- baseURL,
65
- dataReturnType,
66
- parser,
67
- paramsType,
68
- paramsCasing,
69
- pathParamsType,
70
- }: GenerateMethodProps): string {
71
- if (!ast.isHttpOperationNode(node)) return ''
72
- const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
73
- const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
74
- const { header: headerParams } = getOperationParameters(node)
75
- const headerParamsName = headerParams.length > 0 ? tsResolver.resolveHeaderParamsName(node, headerParams[0]!) : null
76
- const headers = isMultipleContentTypes ? (headerParamsName ? ['...headers'] : []) : buildHeaders(contentType, !!headerParamsName)
77
- const generics = buildGenerics(node, tsResolver, { dataReturnType, zodResolver, parser })
78
- const paramsNode = buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
79
- const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
80
- const { query: queryParams } = getOperationParameters(node)
81
- const zodQueryParamsName =
82
- zodResolver && resolveQueryParamsParser(parser) === 'zod' && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]!) : null
83
- const clientParams = buildClassClientParams({
84
- node,
85
- url: Url.toTemplateString(node.path, { casing: paramsCasing }),
86
- baseURL,
87
- tsResolver,
88
- isFormData,
89
- isMultipleContentTypes,
90
- hasFormData,
91
- headers,
92
- zodQueryParamsName,
93
- })
94
- const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }))
95
-
96
- const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
97
- const queryParamsLine = buildQueryParamsLine({ parser, node, zodResolver })
98
- const formDataLine = buildFormDataLine(isFormData || (isMultipleContentTypes && hasFormData), !!node.requestBody?.content?.[0]?.schema)
99
- const returnStatement = buildReturnStatement({ dataReturnType, parser, node, zodResolver, tsResolver })
100
-
101
- const methodBody = [
102
- `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
103
- '',
104
- requestDataLine,
105
- queryParamsLine,
106
- formDataLine,
107
- `const res = await request<${generics.join(', ')}>(${clientParams.toCall()})`,
108
- returnStatement,
109
- ]
110
- .filter(Boolean)
111
- .map((line) => ` ${line}`)
112
- .join('\n')
113
-
114
- return `${jsdoc}async ${name}(${paramsSignature}) {\n${methodBody}\n }`
115
- }
116
-
117
- export function ClassClient({
118
- name,
119
- isExportable = true,
120
- isIndexable = true,
121
- operations,
122
- baseURL,
123
- dataReturnType,
124
- parser,
125
- paramsType,
126
- paramsCasing,
127
- pathParamsType,
128
- children,
129
- }: Props): KubbReactNode {
130
- const methods = operations.map(({ node, name: methodName, tsResolver, zodResolver }) =>
131
- generateMethod({
132
- node,
133
- name: methodName,
134
- tsResolver,
135
- zodResolver,
136
- baseURL,
137
- dataReturnType,
138
- parser,
139
- paramsType,
140
- paramsCasing,
141
- pathParamsType,
142
- }),
143
- )
144
-
145
- const classCode = `export class ${name} {
146
- #config: Partial<RequestConfig> & { client?: Client }
147
-
148
- constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
149
- this.#config = config
150
- }
151
-
152
- ${methods.join('\n\n')}
153
- }`
154
-
155
- return (
156
- <File.Source name={name} isExportable={isExportable} isIndexable={isIndexable}>
157
- {classCode}
158
- {children}
159
- </File.Source>
160
- )
161
- }