@kubb/plugin-client 5.0.0-beta.4 → 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.
Files changed (56) hide show
  1. package/README.md +39 -24
  2. package/dist/clients/axios.cjs +26 -4
  3. package/dist/clients/axios.cjs.map +1 -1
  4. package/dist/clients/axios.d.ts +11 -5
  5. package/dist/clients/axios.js +26 -4
  6. package/dist/clients/axios.js.map +1 -1
  7. package/dist/clients/fetch.cjs +77 -9
  8. package/dist/clients/fetch.cjs.map +1 -1
  9. package/dist/clients/fetch.d.ts +10 -3
  10. package/dist/clients/fetch.js +77 -9
  11. package/dist/clients/fetch.js.map +1 -1
  12. package/dist/index.cjs +836 -514
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +206 -105
  15. package/dist/index.js +833 -515
  16. package/dist/index.js.map +1 -1
  17. package/dist/templates/clients/axios.source.cjs +1 -1
  18. package/dist/templates/clients/axios.source.cjs.map +1 -1
  19. package/dist/templates/clients/axios.source.d.ts +1 -1
  20. package/dist/templates/clients/axios.source.js +1 -1
  21. package/dist/templates/clients/axios.source.js.map +1 -1
  22. package/dist/templates/clients/fetch.source.cjs +1 -1
  23. package/dist/templates/clients/fetch.source.cjs.map +1 -1
  24. package/dist/templates/clients/fetch.source.d.ts +1 -1
  25. package/dist/templates/clients/fetch.source.js +1 -1
  26. package/dist/templates/clients/fetch.source.js.map +1 -1
  27. package/dist/templates/config.source.cjs.map +1 -1
  28. package/dist/templates/config.source.d.ts +1 -1
  29. package/dist/templates/config.source.js.map +1 -1
  30. package/package.json +14 -26
  31. package/src/clients/axios.ts +41 -7
  32. package/src/clients/fetch.ts +106 -6
  33. package/src/components/ClassClient.tsx +47 -24
  34. package/src/components/Client.tsx +100 -71
  35. package/src/components/StaticClassClient.tsx +47 -24
  36. package/src/components/Url.tsx +10 -12
  37. package/src/components/WrapperClient.tsx +9 -5
  38. package/src/functionParams.ts +8 -8
  39. package/src/generators/classClientGenerator.tsx +63 -51
  40. package/src/generators/clientGenerator.tsx +45 -48
  41. package/src/generators/groupedClientGenerator.tsx +12 -6
  42. package/src/generators/operationsGenerator.ts +47 -0
  43. package/src/generators/staticClassClientGenerator.tsx +57 -45
  44. package/src/index.ts +2 -1
  45. package/src/plugin.ts +30 -28
  46. package/src/resolvers/resolverClient.ts +32 -8
  47. package/src/types.ts +111 -65
  48. package/src/utils.ts +142 -63
  49. package/extension.yaml +0 -776
  50. package/src/components/Operations.tsx +0 -28
  51. package/src/generators/operationsGenerator.tsx +0 -28
  52. package/templates/clients/axios.ts +0 -73
  53. package/templates/clients/fetch.ts +0 -96
  54. package/templates/config.ts +0 -43
  55. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  56. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/templates/clients/axios.source.ts
3
- const source = "import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport axios from 'axios'\n\ndeclare const AXIOS_BASE: string\ndeclare const AXIOS_HEADERS: string\n\n/**\n * Subset of AxiosRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n validateStatus?: (status: number) => boolean\n headers?: AxiosRequestConfig['headers']\n}\n\n/**\n * Subset of AxiosResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: AxiosResponse['headers']\n}\n\nexport type ResponseErrorConfig<TError = unknown> = AxiosError<TError>\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nlet _config: Partial<RequestConfig> = {\n baseURL: typeof AXIOS_BASE !== 'undefined' ? AXIOS_BASE : undefined,\n headers: typeof AXIOS_HEADERS !== 'undefined' ? JSON.parse(AXIOS_HEADERS) : undefined,\n}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: RequestConfig) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...merged.headers,\n ...config.headers,\n },\n }\n }, {})\n}\n\nexport const axiosInstance = axios.create(getConfig())\n\nexport const fetch = async <TData, TError = unknown, TVariables = unknown>(\n config: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n return axiosInstance.request<TData, ResponseConfig<TData>>(mergeConfig(getConfig(), config)).catch((e: AxiosError<TError>) => {\n throw e\n })\n}\n\nfetch.getConfig = getConfig\nfetch.setConfig = setConfig\n";
3
+ const source = "import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport axios from 'axios'\n\ndeclare const AXIOS_BASE: string\ndeclare const AXIOS_HEADERS: string\n\n/**\n * Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).\n * Non-string values are JSON-serialized before the request is sent.\n */\nexport type HeaderValue = string | number | boolean | null | undefined | object\nexport type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>\n\n/**\n * Subset of AxiosRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n validateStatus?: (status: number) => boolean\n headers?: HeadersInit\n contentType?: string\n}\n\n/**\n * Subset of AxiosResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: AxiosResponse['headers']\n}\n\nexport type ResponseErrorConfig<TError = unknown> = AxiosError<TError>\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nlet _config: Partial<RequestConfig> = {\n baseURL: typeof AXIOS_BASE !== 'undefined' ? AXIOS_BASE : undefined,\n headers: typeof AXIOS_HEADERS !== 'undefined' ? JSON.parse(AXIOS_HEADERS) : undefined,\n}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: RequestConfig) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\n/**\n * Serializes header values into the string form `axios` ultimately puts on the wire.\n * Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`\n * are sent in their canonical JSON-string form rather than `[object Object]`.\n */\nfunction serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {\n if (!headers) return {}\n const entries = Array.isArray(headers) ? headers : Object.entries(headers)\n const result: Record<string, string> = {}\n for (const [key, value] of entries) {\n if (value === undefined || value === null) continue\n result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)\n }\n return result\n}\n\nexport const axiosInstance = axios.create(getConfig() as AxiosRequestConfig)\n\nexport const client = async <TData, TError = unknown, TVariables = unknown>(\n config: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const requestConfig = mergeConfig(getConfig(), config)\n const { contentType, headers, ...axiosConfig } = requestConfig\n return axiosInstance\n .request<TData, ResponseConfig<TData>>({\n ...axiosConfig,\n headers: {\n ...(contentType && contentType !== 'multipart/form-data' ? { 'Content-Type': contentType } : {}),\n ...serializeHeaders(headers),\n },\n })\n .catch((e: AxiosError<TError>) => {\n throw e\n })\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n";
4
4
  //#endregion
5
5
  exports.source = source;
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"axios.source.cjs","names":[],"sources":["../../../src/templates/clients/axios.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../../templates/clients/axios.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";;AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"axios.source.cjs","names":[],"sources":[],"mappings":""}
@@ -1,4 +1,4 @@
1
- import { t as __name } from "../../chunk--u3MIqq1.js";
1
+ import { t as __name } from "../../chunk-C0LytTxp.js";
2
2
 
3
3
  //#region src/templates/clients/axios.source.d.ts
4
4
  declare const source: string;
@@ -1,5 +1,5 @@
1
1
  //#region src/templates/clients/axios.source.ts
2
- const source = "import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport axios from 'axios'\n\ndeclare const AXIOS_BASE: string\ndeclare const AXIOS_HEADERS: string\n\n/**\n * Subset of AxiosRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n validateStatus?: (status: number) => boolean\n headers?: AxiosRequestConfig['headers']\n}\n\n/**\n * Subset of AxiosResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: AxiosResponse['headers']\n}\n\nexport type ResponseErrorConfig<TError = unknown> = AxiosError<TError>\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nlet _config: Partial<RequestConfig> = {\n baseURL: typeof AXIOS_BASE !== 'undefined' ? AXIOS_BASE : undefined,\n headers: typeof AXIOS_HEADERS !== 'undefined' ? JSON.parse(AXIOS_HEADERS) : undefined,\n}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: RequestConfig) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...merged.headers,\n ...config.headers,\n },\n }\n }, {})\n}\n\nexport const axiosInstance = axios.create(getConfig())\n\nexport const fetch = async <TData, TError = unknown, TVariables = unknown>(\n config: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n return axiosInstance.request<TData, ResponseConfig<TData>>(mergeConfig(getConfig(), config)).catch((e: AxiosError<TError>) => {\n throw e\n })\n}\n\nfetch.getConfig = getConfig\nfetch.setConfig = setConfig\n";
2
+ const source = "import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport axios from 'axios'\n\ndeclare const AXIOS_BASE: string\ndeclare const AXIOS_HEADERS: string\n\n/**\n * Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).\n * Non-string values are JSON-serialized before the request is sent.\n */\nexport type HeaderValue = string | number | boolean | null | undefined | object\nexport type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>\n\n/**\n * Subset of AxiosRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n validateStatus?: (status: number) => boolean\n headers?: HeadersInit\n contentType?: string\n}\n\n/**\n * Subset of AxiosResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: AxiosResponse['headers']\n}\n\nexport type ResponseErrorConfig<TError = unknown> = AxiosError<TError>\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nlet _config: Partial<RequestConfig> = {\n baseURL: typeof AXIOS_BASE !== 'undefined' ? AXIOS_BASE : undefined,\n headers: typeof AXIOS_HEADERS !== 'undefined' ? JSON.parse(AXIOS_HEADERS) : undefined,\n}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: RequestConfig) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\n/**\n * Serializes header values into the string form `axios` ultimately puts on the wire.\n * Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`\n * are sent in their canonical JSON-string form rather than `[object Object]`.\n */\nfunction serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {\n if (!headers) return {}\n const entries = Array.isArray(headers) ? headers : Object.entries(headers)\n const result: Record<string, string> = {}\n for (const [key, value] of entries) {\n if (value === undefined || value === null) continue\n result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)\n }\n return result\n}\n\nexport const axiosInstance = axios.create(getConfig() as AxiosRequestConfig)\n\nexport const client = async <TData, TError = unknown, TVariables = unknown>(\n config: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const requestConfig = mergeConfig(getConfig(), config)\n const { contentType, headers, ...axiosConfig } = requestConfig\n return axiosInstance\n .request<TData, ResponseConfig<TData>>({\n ...axiosConfig,\n headers: {\n ...(contentType && contentType !== 'multipart/form-data' ? { 'Content-Type': contentType } : {}),\n ...serializeHeaders(headers),\n },\n })\n .catch((e: AxiosError<TError>) => {\n throw e\n })\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n";
3
3
  //#endregion
4
4
  export { source };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"axios.source.js","names":[],"sources":["../../../src/templates/clients/axios.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../../templates/clients/axios.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"axios.source.js","names":[],"sources":[],"mappings":""}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/templates/clients/fetch.source.ts
3
- const source = "/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Subset of FetchRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n headers?: [string, string][] | Record<string, string>\n credentials?: RequestCredentials\n}\n\n/**\n * Subset of FetchResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: Headers\n}\n\nlet _config: Partial<RequestConfig> = {}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: Partial<RequestConfig>) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\nexport type ResponseErrorConfig<TError = unknown> = TError\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nexport const fetch = async <TData, _TError = unknown, TVariables = unknown>(\n paramsConfig: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const normalizedParams = new URLSearchParams()\n\n const config = mergeConfig(getConfig(), paramsConfig)\n\n Object.entries(config.params || {}).forEach(([key, value]) => {\n if (value !== undefined) {\n normalizedParams.append(key, value === null ? 'null' : value.toString())\n }\n })\n\n let targetUrl = [config.baseURL, config.url].filter(Boolean).join('')\n\n if (config.params) {\n targetUrl += `?${normalizedParams}`\n }\n\n const response = await globalThis.fetch(targetUrl, {\n credentials: config.credentials || 'same-origin',\n method: config.method?.toUpperCase(),\n body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),\n signal: config.signal,\n headers: config.headers,\n })\n\n const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()\n\n return {\n data: data as TData,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nfetch.getConfig = getConfig\nfetch.setConfig = setConfig\n";
3
+ const source = "/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).\n * Non-string values are JSON-serialized before the request is sent.\n */\nexport type HeaderValue = string | number | boolean | null | undefined | object\nexport type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>\n\n/**\n * Subset of FetchRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n headers?: HeadersInit\n credentials?: RequestCredentials\n contentType?: string\n}\n\n/**\n * Subset of FetchResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: Headers\n}\n\nlet _config: Partial<RequestConfig> = {}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: Partial<RequestConfig>) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\n/**\n * Serializes header values into the string form `fetch` expects.\n * Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`\n * are sent in their canonical JSON-string form rather than `[object Object]`.\n */\nfunction serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {\n if (!headers) return {}\n const entries = Array.isArray(headers) ? headers : Object.entries(headers)\n const result: Record<string, string> = {}\n for (const [key, value] of entries) {\n if (value === undefined || value === null) continue\n result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)\n }\n return result\n}\n\n/**\n * Serializes the request body into the form `fetch` expects.\n * `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through\n * untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`\n * and JSON-serialized otherwise.\n */\nfunction serializeBody(data: unknown, contentType?: string): BodyInit | undefined {\n if (data === undefined || data === null) return undefined\n if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {\n return data as BodyInit\n }\n if (typeof data === 'string') return data\n if (contentType?.includes('application/x-www-form-urlencoded')) {\n return new URLSearchParams(data as Record<string, string>)\n }\n return JSON.stringify(data)\n}\n\n/**\n * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.\n */\nfunction detectResponseType(contentType: string | null): RequestConfig['responseType'] {\n if (!contentType) return undefined\n if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'\n if (contentType.includes('text/')) return 'text'\n if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'\n return undefined\n}\n\n/**\n * Parses a `fetch` response body.\n *\n * - Empty responses (204/205/304 or no body) resolve to `{}`.\n * - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching\n * `Response` method.\n * - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.\n */\nasync function parseResponse<TData>(response: Response, responseType?: RequestConfig['responseType']): Promise<TData> {\n if ([204, 205, 304].includes(response.status) || !response.body) {\n return {} as TData\n }\n\n switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {\n case 'text':\n case 'document':\n return (await response.text()) as TData\n case 'blob':\n return (await response.blob()) as TData\n case 'arraybuffer':\n return (await response.arrayBuffer()) as TData\n case 'stream':\n return response.body as TData\n case 'json':\n return (await response.json()) as TData\n }\n\n // Explicit but unrecognized responseType keeps the JSON default; otherwise read text and parse it.\n if (responseType) {\n return (await response.json()) as TData\n }\n const text = await response.text()\n if (!text) {\n return {} as TData\n }\n try {\n return JSON.parse(text) as TData\n } catch {\n return text as TData\n }\n}\n\nexport type ResponseErrorConfig<TError = unknown> = TError\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nexport const client = async <TData, _TError = unknown, TVariables = unknown>(\n paramsConfig: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const normalizedParams = new URLSearchParams()\n\n const config = mergeConfig(getConfig(), paramsConfig)\n\n Object.entries(config.params || {}).forEach(([key, value]) => {\n if (value !== undefined) {\n normalizedParams.append(key, value === null ? 'null' : value.toString())\n }\n })\n\n let targetUrl = [config.baseURL, config.url].filter(Boolean).join('')\n\n if (config.params) {\n targetUrl += `?${normalizedParams}`\n }\n\n const headers: Record<string, string> = {\n ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),\n ...serializeHeaders(config.headers),\n }\n\n const response = await globalThis.fetch(targetUrl, {\n credentials: config.credentials || 'same-origin',\n method: config.method?.toUpperCase(),\n body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),\n signal: config.signal,\n headers,\n })\n\n const data = await parseResponse<TData>(response, config.responseType)\n\n return {\n data,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n";
4
4
  //#endregion
5
5
  exports.source = source;
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.source.cjs","names":[],"sources":["../../../src/templates/clients/fetch.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../../templates/clients/fetch.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";;AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"fetch.source.cjs","names":[],"sources":[],"mappings":""}
@@ -1,4 +1,4 @@
1
- import { t as __name } from "../../chunk--u3MIqq1.js";
1
+ import { t as __name } from "../../chunk-C0LytTxp.js";
2
2
 
3
3
  //#region src/templates/clients/fetch.source.d.ts
4
4
  declare const source: string;
@@ -1,5 +1,5 @@
1
1
  //#region src/templates/clients/fetch.source.ts
2
- const source = "/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Subset of FetchRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n headers?: [string, string][] | Record<string, string>\n credentials?: RequestCredentials\n}\n\n/**\n * Subset of FetchResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: Headers\n}\n\nlet _config: Partial<RequestConfig> = {}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: Partial<RequestConfig>) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\nexport type ResponseErrorConfig<TError = unknown> = TError\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nexport const fetch = async <TData, _TError = unknown, TVariables = unknown>(\n paramsConfig: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const normalizedParams = new URLSearchParams()\n\n const config = mergeConfig(getConfig(), paramsConfig)\n\n Object.entries(config.params || {}).forEach(([key, value]) => {\n if (value !== undefined) {\n normalizedParams.append(key, value === null ? 'null' : value.toString())\n }\n })\n\n let targetUrl = [config.baseURL, config.url].filter(Boolean).join('')\n\n if (config.params) {\n targetUrl += `?${normalizedParams}`\n }\n\n const response = await globalThis.fetch(targetUrl, {\n credentials: config.credentials || 'same-origin',\n method: config.method?.toUpperCase(),\n body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),\n signal: config.signal,\n headers: config.headers,\n })\n\n const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()\n\n return {\n data: data as TData,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nfetch.getConfig = getConfig\nfetch.setConfig = setConfig\n";
2
+ const source = "/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded headers like `X-Filter` in the Linode API).\n * Non-string values are JSON-serialized before the request is sent.\n */\nexport type HeaderValue = string | number | boolean | null | undefined | object\nexport type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>\n\n/**\n * Subset of FetchRequestConfig\n */\nexport type RequestConfig<TData = unknown> = {\n baseURL?: string\n url?: string\n method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'\n params?: unknown\n data?: TData | FormData\n responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'\n signal?: AbortSignal\n headers?: HeadersInit\n credentials?: RequestCredentials\n contentType?: string\n}\n\n/**\n * Subset of FetchResponse\n */\nexport type ResponseConfig<TData = unknown> = {\n data: TData\n status: number\n statusText: string\n headers: Headers\n}\n\nlet _config: Partial<RequestConfig> = {}\n\nexport const getConfig = () => _config\n\nexport const setConfig = (config: Partial<RequestConfig>) => {\n _config = config\n return getConfig()\n}\n\nexport const mergeConfig = <T extends RequestConfig>(...configs: Array<Partial<T>>): Partial<T> => {\n return configs.reduce<Partial<T>>((merged, config) => {\n return {\n ...merged,\n ...config,\n headers: {\n ...(Array.isArray(merged.headers) ? Object.fromEntries(merged.headers) : merged.headers),\n ...(Array.isArray(config.headers) ? Object.fromEntries(config.headers) : config.headers),\n },\n }\n }, {})\n}\n\n/**\n * Serializes header values into the string form `fetch` expects.\n * Objects (including arrays) are JSON-stringified so spec-defined object headers like `X-Filter`\n * are sent in their canonical JSON-string form rather than `[object Object]`.\n */\nfunction serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {\n if (!headers) return {}\n const entries = Array.isArray(headers) ? headers : Object.entries(headers)\n const result: Record<string, string> = {}\n for (const [key, value] of entries) {\n if (value === undefined || value === null) continue\n result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)\n }\n return result\n}\n\n/**\n * Serializes the request body into the form `fetch` expects.\n * `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through\n * untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`\n * and JSON-serialized otherwise.\n */\nfunction serializeBody(data: unknown, contentType?: string): BodyInit | undefined {\n if (data === undefined || data === null) return undefined\n if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {\n return data as BodyInit\n }\n if (typeof data === 'string') return data\n if (contentType?.includes('application/x-www-form-urlencoded')) {\n return new URLSearchParams(data as Record<string, string>)\n }\n return JSON.stringify(data)\n}\n\n/**\n * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.\n */\nfunction detectResponseType(contentType: string | null): RequestConfig['responseType'] {\n if (!contentType) return undefined\n if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'\n if (contentType.includes('text/')) return 'text'\n if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'\n return undefined\n}\n\n/**\n * Parses a `fetch` response body.\n *\n * - Empty responses (204/205/304 or no body) resolve to `{}`.\n * - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching\n * `Response` method.\n * - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.\n */\nasync function parseResponse<TData>(response: Response, responseType?: RequestConfig['responseType']): Promise<TData> {\n if ([204, 205, 304].includes(response.status) || !response.body) {\n return {} as TData\n }\n\n switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {\n case 'text':\n case 'document':\n return (await response.text()) as TData\n case 'blob':\n return (await response.blob()) as TData\n case 'arraybuffer':\n return (await response.arrayBuffer()) as TData\n case 'stream':\n return response.body as TData\n case 'json':\n return (await response.json()) as TData\n }\n\n // Explicit but unrecognized responseType keeps the JSON default; otherwise read text and parse it.\n if (responseType) {\n return (await response.json()) as TData\n }\n const text = await response.text()\n if (!text) {\n return {} as TData\n }\n try {\n return JSON.parse(text) as TData\n } catch {\n return text as TData\n }\n}\n\nexport type ResponseErrorConfig<TError = unknown> = TError\n\nexport type Client = <TData, _TError = unknown, TVariables = unknown>(config: RequestConfig<TVariables>, request?: unknown) => Promise<ResponseConfig<TData>>\n\nexport const client = async <TData, _TError = unknown, TVariables = unknown>(\n paramsConfig: RequestConfig<TVariables>,\n _request?: unknown,\n): Promise<ResponseConfig<TData>> => {\n const normalizedParams = new URLSearchParams()\n\n const config = mergeConfig(getConfig(), paramsConfig)\n\n Object.entries(config.params || {}).forEach(([key, value]) => {\n if (value !== undefined) {\n normalizedParams.append(key, value === null ? 'null' : value.toString())\n }\n })\n\n let targetUrl = [config.baseURL, config.url].filter(Boolean).join('')\n\n if (config.params) {\n targetUrl += `?${normalizedParams}`\n }\n\n const headers: Record<string, string> = {\n ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),\n ...serializeHeaders(config.headers),\n }\n\n const response = await globalThis.fetch(targetUrl, {\n credentials: config.credentials || 'same-origin',\n method: config.method?.toUpperCase(),\n body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),\n signal: config.signal,\n headers,\n })\n\n const data = await parseResponse<TData>(response, config.responseType)\n\n return {\n data,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n";
3
3
  //#endregion
4
4
  export { source };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.source.js","names":[],"sources":["../../../src/templates/clients/fetch.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../../templates/clients/fetch.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"fetch.source.js","names":[],"sources":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"config.source.cjs","names":[],"sources":["../../src/templates/config.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../templates/config.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";;AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"config.source.cjs","names":[],"sources":[],"mappings":""}
@@ -1,4 +1,4 @@
1
- import { t as __name } from "../chunk--u3MIqq1.js";
1
+ import { t as __name } from "../chunk-C0LytTxp.js";
2
2
 
3
3
  //#region src/templates/config.source.d.ts
4
4
  declare const source: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.source.js","names":[],"sources":["../../src/templates/config.source.ts"],"sourcesContent":["// @ts-expect-error - import attributes are handled at build time by importAttributeTextPlugin\nimport content from '../../templates/config.ts' with { type: 'text' }\n\nexport const source = content as string\n"],"mappings":";AAAA,MAAa,SAAS"}
1
+ {"version":3,"file":"config.source.js","names":[],"sources":[],"mappings":""}
package/package.json CHANGED
@@ -1,23 +1,17 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.4",
4
- "description": "API client generator plugin for Kubb, creating type-safe HTTP clients (Axios, Fetch) from OpenAPI specifications for making API requests.",
3
+ "version": "5.0.0-beta.56",
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-generator",
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,8 +24,6 @@
30
24
  "files": [
31
25
  "src",
32
26
  "dist",
33
- "templates",
34
- "extension.yaml",
35
27
  "*.d.ts",
36
28
  "*.d.cts",
37
29
  "!/**/**.test.**",
@@ -94,17 +86,19 @@
94
86
  "registry": "https://registry.npmjs.org/"
95
87
  },
96
88
  "dependencies": {
97
- "@kubb/core": "5.0.0-beta.4",
98
- "@kubb/renderer-jsx": "5.0.0-beta.4",
99
- "@kubb/plugin-ts": "5.0.0-beta.4",
100
- "@kubb/plugin-zod": "5.0.0-beta.4"
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"
101
94
  },
102
95
  "devDependencies": {
103
- "axios": "^1.15.2",
96
+ "axios": "^1.17.0",
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.4",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.55",
108
102
  "axios": "^1.7.2"
109
103
  },
110
104
  "peerDependenciesMeta": {
@@ -112,23 +106,17 @@
112
106
  "optional": true
113
107
  }
114
108
  },
115
- "size-limit": [
116
- {
117
- "path": "./dist/*.js",
118
- "limit": "510 KiB",
119
- "gzip": true
120
- }
121
- ],
122
109
  "engines": {
123
110
  "node": ">=22"
124
111
  },
125
112
  "scripts": {
126
- "build": "tsdown && size-limit",
127
- "clean": "npx rimraf ./dist",
113
+ "build": "tsdown",
114
+ "clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
128
115
  "lint": "oxlint .",
129
116
  "lint:fix": "oxlint --fix .",
130
117
  "release": "pnpm publish --no-git-check",
131
118
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
119
+ "release:stage": "pnpm stage publish --no-git-check",
132
120
  "start": "tsdown --watch",
133
121
  "test": "vitest --passWithNoTests",
134
122
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
@@ -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?: AxiosRequestConfig['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
- export const axiosInstance = axios.create(getConfig())
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
- return axiosInstance.request<TResponseData, ResponseConfig<TResponseData>>(mergeConfig(getConfig(), config)).catch((e: AxiosError<TError>) => {
72
- throw e
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
@@ -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?: [string, string][] | Record<string, string>
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 response = await fetch(targetUrl, {
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 instanceof FormData ? config.data : JSON.stringify(config.data),
183
+ body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),
84
184
  signal: config.signal,
85
- headers: config.headers,
185
+ headers,
86
186
  })
87
187
 
88
- const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()
188
+ const data = await parseResponse<TResponseData>(response, config.responseType)
89
189
 
90
190
  return {
91
- data: data as TResponseData,
191
+ data,
92
192
  status: response.status,
93
193
  statusText: response.statusText,
94
194
  headers: response.headers as Headers,
@@ -1,19 +1,30 @@
1
- import { buildJSDoc, URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
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'
3
5
  import type { ResolverTs } from '@kubb/plugin-ts'
4
6
  import { functionPrinter } from '@kubb/plugin-ts'
5
7
  import type { ResolverZod } from '@kubb/plugin-zod'
6
8
  import { File } from '@kubb/renderer-jsx'
7
9
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
8
10
  import type { PluginClient } from '../types.ts'
9
- import { buildClassClientParams, buildFormDataLine, buildGenerics, buildHeaders, buildRequestDataLine, buildReturnStatement, getComments } from '../utils.ts'
10
- import { Client } from './Client.tsx'
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'
11
22
 
12
23
  type OperationData = {
13
24
  node: ast.OperationNode
14
25
  name: string
15
26
  tsResolver: ResolverTs
16
- zodResolver?: ResolverZod
27
+ zodResolver?: ResolverZod | null
17
28
  }
18
29
 
19
30
  type Props = {
@@ -21,7 +32,7 @@ type Props = {
21
32
  isExportable?: boolean
22
33
  isIndexable?: boolean
23
34
  operations: Array<OperationData>
24
- baseURL: string | undefined
35
+ baseURL: string | null | undefined
25
36
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
26
37
  paramsCasing: PluginClient['resolvedOptions']['paramsCasing']
27
38
  paramsType: PluginClient['resolvedOptions']['pathParamsType']
@@ -34,8 +45,8 @@ type GenerateMethodProps = {
34
45
  node: ast.OperationNode
35
46
  name: string
36
47
  tsResolver: ResolverTs
37
- zodResolver?: ResolverZod
38
- baseURL: string | undefined
48
+ zodResolver?: ResolverZod | null
49
+ baseURL: string | null | undefined
39
50
  dataReturnType: PluginClient['resolvedOptions']['dataReturnType']
40
51
  parser: PluginClient['resolvedOptions']['parser'] | undefined
41
52
  paramsType: PluginClient['resolvedOptions']['paramsType']
@@ -57,28 +68,41 @@ function generateMethod({
57
68
  paramsCasing,
58
69
  pathParamsType,
59
70
  }: GenerateMethodProps): string {
60
- const path = new URLPath(node.path, { casing: paramsCasing })
61
- const contentType = node.requestBody?.content?.[0]?.contentType ?? 'application/json'
62
- const isFormData = contentType === 'multipart/form-data'
63
- const headerParamsName =
64
- node.parameters.filter((p) => p.in === 'header').length > 0
65
- ? tsResolver.resolveHeaderParamsName(node, node.parameters.filter((p) => p.in === 'header')[0]!)
66
- : undefined
67
- const headers = buildHeaders(contentType, !!headerParamsName)
68
- const generics = buildGenerics(node, tsResolver)
69
- const paramsNode = ClassClient.getParams({ paramsType, paramsCasing, pathParamsType, node, tsResolver, isConfigurable: true })
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 })
70
79
  const paramsSignature = declarationPrinter.print(paramsNode) ?? ''
71
- const clientParams = buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, headers })
72
- const jsdoc = buildJSDoc(getComments(node))
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 }))
73
95
 
74
96
  const requestDataLine = buildRequestDataLine({ parser, node, zodResolver })
75
- const formDataLine = buildFormDataLine(isFormData, !!node.requestBody?.content?.[0]?.schema)
76
- const returnStatement = buildReturnStatement({ dataReturnType, 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 })
77
100
 
78
101
  const methodBody = [
79
- 'const { client: request = fetch, ...requestConfig } = mergeConfig(this.#config, config)',
102
+ `const { client: request = client, ${isMultipleContentTypes ? `contentType = ${stringify(contentType)}, ` : ''}...requestConfig } = mergeConfig(this.#config, config)`,
80
103
  '',
81
104
  requestDataLine,
105
+ queryParamsLine,
82
106
  formDataLine,
83
107
  `const res = await request<${generics.join(', ')}>(${clientParams.toCall()})`,
84
108
  returnStatement,
@@ -135,4 +159,3 @@ ${methods.join('\n\n')}
135
159
  </File.Source>
136
160
  )
137
161
  }
138
- ClassClient.getParams = Client.getParams