@kubb/plugin-client 5.0.0-beta.30 → 5.0.0-beta.33

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.
@@ -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 * 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\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 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: {\n ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),\n ...serializeHeaders(config.headers),\n },\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\nclient.getConfig = getConfig\nclient.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,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 * 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\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 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: {\n ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),\n ...serializeHeaders(config.headers),\n },\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\nclient.getConfig = getConfig\nclient.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
 
package/extension.yaml CHANGED
@@ -315,31 +315,6 @@ options:
315
315
  }),
316
316
  ],
317
317
  })
318
- - name: contentType
319
- type: "'application/json' | (string & {})"
320
- required: false
321
- description: |
322
- Selects which request/response media type the generator reads from the OpenAPI spec.
323
-
324
- When omitted, Kubb picks the first JSON-compatible media type it finds (`application/json`, `application/vnd.api+json`, anything ending in `+json`). Set this when your spec defines multiple media types for the same operation and you want a non-default one.
325
- examples:
326
- - name: JSON API media type
327
- files:
328
- - lang: typescript
329
- twoslash: false
330
- code: |
331
- import { defineConfig } from 'kubb'
332
- import { pluginTs } from '@kubb/plugin-ts'
333
-
334
- export default defineConfig({
335
- input: { path: './petStore.yaml' },
336
- output: { path: './src/gen' },
337
- plugins: [
338
- pluginTs({
339
- contentType: 'application/vnd.api+json',
340
- }),
341
- ],
342
- })
343
318
  - name: group
344
319
  type: Group
345
320
  required: false
@@ -710,13 +685,13 @@ options:
710
685
  // ...
711
686
  }
712
687
  - name: parser
713
- type: "'client' | 'zod'"
688
+ type: false | 'zod'
714
689
  required: false
715
- default: "'client'"
690
+ default: 'false'
716
691
  description: |
717
692
  Runtime validator applied to the response body before it is returned to the caller.
718
693
 
719
- - `'client'` (default) — no validation. The response is cast to the generated TypeScript type and returned as-is. Fastest path, trusts the API.
694
+ - `false` (default) — no validation. The client has no runtime parser; the response is cast to the generated TypeScript type and returned as-is. Fastest path, trusts the API.
720
695
  - `'zod'` — pipes the response through the Zod schema produced by `@kubb/plugin-zod`. Catches mismatches between spec and API at runtime, at the cost of a parse on every call.
721
696
 
722
697
  Use `'zod'` when you want a defensive boundary against drift between your OpenAPI spec and the live API. Requires `@kubb/plugin-zod` in the plugins list.
@@ -895,18 +870,18 @@ options:
895
870
 
896
871
  const petClient = new Pet()
897
872
  const pet = await petClient.getPetById({ petId: 1 })
898
- - name: wrapper
873
+ - name: sdk
899
874
  type: '{ className: string }'
900
875
  required: false
901
876
  description: |
902
- Generates a single top-level class that composes the per-tag client classes into one entry point. Only meaningful when `clientType: 'class'` (or `'staticClass'`) and `group: { type: 'tag' }` are also set.
877
+ Generates a single SDK class that composes the per-tag client classes into one entry point. Setting `sdk` automatically enables `clientType: 'class'`, so you only need to add `group: { type: 'tag' }` to split the clients per tag.
903
878
 
904
- Use this when you want a single object to hand around your app (`api.pet.findById`, `api.user.login`) instead of importing each tag client separately.
879
+ Use this when you want a single object to hand around your app (`api.petController.findById`, `api.userController.login`) instead of importing each tag client separately.
905
880
  properties:
906
881
  - name: className
907
882
  type: string
908
883
  required: true
909
- description: Name of the generated wrapper class — used as the export name and file name.
884
+ description: Name of the generated SDK class — used as the export name and file name.
910
885
  examples:
911
886
  - name: A composed PetStoreClient
912
887
  files:
@@ -925,9 +900,8 @@ options:
925
900
  pluginTs(),
926
901
  pluginClient({
927
902
  output: { path: './clients' },
928
- clientType: 'class',
929
903
  group: { type: 'tag' },
930
- wrapper: { className: 'PetStoreClient' },
904
+ sdk: { className: 'PetStoreClient' },
931
905
  }),
932
906
  ],
933
907
  })
@@ -936,19 +910,19 @@ options:
936
910
  twoslash: false
937
911
  code: |
938
912
  import type { Client, RequestConfig } from './.kubb/client'
939
- import { Pet } from './petController/Pet'
940
- import { Store } from './storeController/Store'
941
- import { User } from './userController/User'
913
+ import { petController } from './petController/petController'
914
+ import { storeController } from './storeController/storeController'
915
+ import { userController } from './userController/userController'
942
916
 
943
917
  export class PetStoreClient {
944
- readonly pet: Pet
945
- readonly store: Store
946
- readonly user: User
918
+ readonly petController: petController
919
+ readonly storeController: storeController
920
+ readonly userController: userController
947
921
 
948
922
  constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
949
- this.pet = new Pet(config)
950
- this.store = new Store(config)
951
- this.user = new User(config)
923
+ this.petController = new petController(config)
924
+ this.storeController = new storeController(config)
925
+ this.userController = new userController(config)
952
926
  }
953
927
  }
954
928
  - name: usage.ts
@@ -959,8 +933,8 @@ options:
959
933
 
960
934
  const api = new PetStoreClient({ baseURL: 'https://petstore.swagger.io/v2' })
961
935
 
962
- const pets = await api.pet.findPetsByTags({ tags: ['available'] })
963
- const user = await api.user.getUserByName({ username: 'john' })
936
+ const pets = await api.petController.findPetsByTags({ tags: ['available'] })
937
+ const user = await api.userController.getUserByName({ username: 'john' })
964
938
  - name: bundle
965
939
  type: boolean
966
940
  required: false
@@ -1283,7 +1257,7 @@ examples:
1283
1257
  },
1284
1258
  },
1285
1259
  operations: true,
1286
- parser: 'client',
1260
+ parser: false,
1287
1261
  exclude: [{ type: 'tag', pattern: 'store' }],
1288
1262
  pathParamsType: 'object',
1289
1263
  dataReturnType: 'full',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.0.0-beta.30",
3
+ "version": "5.0.0-beta.33",
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",
@@ -87,10 +87,10 @@
87
87
  "registry": "https://registry.npmjs.org/"
88
88
  },
89
89
  "dependencies": {
90
- "@kubb/core": "5.0.0-beta.29",
91
- "@kubb/renderer-jsx": "5.0.0-beta.29",
92
- "@kubb/plugin-ts": "5.0.0-beta.30",
93
- "@kubb/plugin-zod": "5.0.0-beta.30"
90
+ "@kubb/core": "5.0.0-beta.33",
91
+ "@kubb/renderer-jsx": "5.0.0-beta.33",
92
+ "@kubb/plugin-ts": "5.0.0-beta.33",
93
+ "@kubb/plugin-zod": "5.0.0-beta.33"
94
94
  },
95
95
  "devDependencies": {
96
96
  "axios": "^1.16.1",
@@ -98,7 +98,7 @@
98
98
  "@internals/utils": "0.0.0"
99
99
  },
100
100
  "peerDependencies": {
101
- "@kubb/renderer-jsx": "5.0.0-beta.29",
101
+ "@kubb/renderer-jsx": "5.0.0-beta.33",
102
102
  "axios": "^1.7.2"
103
103
  },
104
104
  "peerDependenciesMeta": {
@@ -74,6 +74,77 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
74
74
  return result
75
75
  }
76
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
+
77
148
  export type ResponseErrorConfig<TError = unknown> = TError
78
149
 
79
150
  export type Client = <TResponseData, _TError = unknown, TRequestData = unknown>(
@@ -101,21 +172,23 @@ export const client = async <TResponseData, _TError = unknown, RequestData = unk
101
172
  targetUrl += `?${normalizedParams}`
102
173
  }
103
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
+
104
180
  const response = await globalThis.fetch(targetUrl, {
105
181
  credentials: config.credentials || 'same-origin',
106
182
  method: config.method?.toUpperCase(),
107
- body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),
183
+ body: serializeBody(config.data, headers['Content-Type'] ?? headers['content-type']),
108
184
  signal: config.signal,
109
- headers: {
110
- ...(config.contentType && config.contentType !== 'multipart/form-data' ? { 'Content-Type': config.contentType } : {}),
111
- ...serializeHeaders(config.headers),
112
- },
185
+ headers,
113
186
  })
114
187
 
115
- const data = [204, 205, 304].includes(response.status) || !response.body ? {} : await response.json()
188
+ const data = await parseResponse<TResponseData>(response, config.responseType)
116
189
 
117
190
  return {
118
- data: data as TResponseData,
191
+ data,
119
192
  status: response.status,
120
193
  statusText: response.statusText,
121
194
  headers: response.headers as Headers,
@@ -1,6 +1,6 @@
1
1
  import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
2
2
  import { buildJSDoc, URLPath } from '@internals/utils'
3
- import type { ast } from '@kubb/core'
3
+ import { ast } from '@kubb/core'
4
4
  import type { ResolverTs } from '@kubb/plugin-ts'
5
5
  import { functionPrinter } from '@kubb/plugin-ts'
6
6
  import type { ResolverZod } from '@kubb/plugin-zod'
@@ -58,6 +58,7 @@ function generateMethod({
58
58
  paramsCasing,
59
59
  pathParamsType,
60
60
  }: GenerateMethodProps): string {
61
+ if (!ast.isHttpOperationNode(node)) return ''
61
62
  const path = new URLPath(node.path, { casing: paramsCasing })
62
63
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
64
  const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
@@ -4,6 +4,7 @@ import {
4
4
  buildRequestConfigType,
5
5
  getContentTypeInfo,
6
6
  getOperationParameters,
7
+ getResponseType,
7
8
  resolveSuccessNames,
8
9
  } from '@internals/shared'
9
10
  import { isValidVarName, URLPath } from '@internals/utils'
@@ -96,9 +97,11 @@ export function Client({
96
97
  children,
97
98
  isConfigurable = true,
98
99
  }: Props): KubbReactNode {
100
+ if (!ast.isHttpOperationNode(node)) return null
99
101
  const path = new URLPath(node.path)
100
102
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
101
103
  const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
104
+ const responseType = getResponseType(node)
102
105
 
103
106
  const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
104
107
  const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing })
@@ -179,6 +182,7 @@ export function Client({
179
182
  }
180
183
  : null,
181
184
  contentType: isConfigurable && isMultipleContentTypes ? {} : null,
185
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
182
186
  requestConfig: isConfigurable
183
187
  ? {
184
188
  mode: 'inlineSpread',
@@ -199,8 +203,8 @@ export function Client({
199
203
  <>
200
204
  {dataReturnType === 'full' && parser === 'zod' && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`}
201
205
  {dataReturnType === 'data' && parser === 'zod' && zodResponseName && `return ${zodResponseName}.parse(res.data)`}
202
- {dataReturnType === 'full' && parser === 'client' && 'return res'}
203
- {dataReturnType === 'data' && parser === 'client' && 'return res.data'}
206
+ {dataReturnType === 'full' && parser !== 'zod' && 'return res'}
207
+ {dataReturnType === 'data' && parser !== 'zod' && 'return res.data'}
204
208
  </>
205
209
  )
206
210
 
@@ -1,5 +1,5 @@
1
1
  import { URLPath } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
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(),
@@ -1,6 +1,6 @@
1
1
  import { buildOperationComments, getContentTypeInfo, getOperationParameters } from '@internals/shared'
2
2
  import { buildJSDoc, URLPath } from '@internals/utils'
3
- import type { ast } from '@kubb/core'
3
+ import { ast } from '@kubb/core'
4
4
  import type { ResolverTs } from '@kubb/plugin-ts'
5
5
  import { functionPrinter } from '@kubb/plugin-ts'
6
6
  import type { ResolverZod } from '@kubb/plugin-zod'
@@ -58,6 +58,7 @@ function generateMethod({
58
58
  paramsCasing,
59
59
  pathParamsType,
60
60
  }: GenerateMethodProps): string {
61
+ if (!ast.isHttpOperationNode(node)) return ''
61
62
  const path = new URLPath(node.path, { casing: paramsCasing })
62
63
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node)
63
64
  const isFormData = !isMultipleContentTypes && contentType === 'multipart/form-data'
@@ -57,6 +57,7 @@ export function Url({
57
57
  node,
58
58
  tsResolver,
59
59
  }: Props): KubbReactNode {
60
+ if (!ast.isHttpOperationNode(node)) return null
60
61
  const path = new URLPath(node.path)
61
62
 
62
63
  const paramsNode = buildUrlParamsNode({
@@ -1,8 +1,7 @@
1
1
  import path from 'node:path'
2
- import { resolveOperationTypeNames } from '@internals/shared'
2
+ import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
3
  import { camelCase } from '@internals/utils'
4
- import type { ast } from '@kubb/core'
5
- import { defineGenerator } from '@kubb/core'
4
+ import { ast, defineGenerator } from '@kubb/core'
6
5
  import type { ResolverTs } from '@kubb/plugin-ts'
7
6
  import { pluginTsName } from '@kubb/plugin-ts'
8
7
  import type { ResolverZod } from '@kubb/plugin-zod'
@@ -62,16 +61,18 @@ export const classClientGenerator = defineGenerator<PluginClient>({
62
61
  const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
63
62
 
64
63
  function buildOperationData(node: ast.OperationNode): OperationData {
65
- const typeFile = tsResolver.resolveFile(
66
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
67
- { root, output: tsPluginOptions?.output ?? output, group: tsPluginOptions?.group },
68
- )
64
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
65
+ root,
66
+ output: tsPluginOptions?.output ?? output,
67
+ group: tsPluginOptions?.group,
68
+ })
69
69
  const zodFile =
70
70
  zodResolver && pluginZod?.options
71
- ? zodResolver.resolveFile(
72
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
73
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group ?? undefined },
74
- )
71
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
72
+ root,
73
+ output: pluginZod.options?.output ?? output,
74
+ group: pluginZod.options?.group ?? undefined,
75
+ })
75
76
  : null
76
77
 
77
78
  return {
@@ -85,6 +86,7 @@ export const classClientGenerator = defineGenerator<PluginClient>({
85
86
  }
86
87
 
87
88
  const controllers = nodes.reduce((acc, operationNode) => {
89
+ if (!ast.isHttpOperationNode(operationNode)) return acc
88
90
  const tag = operationNode.tags[0]
89
91
  const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveGroupName('Client')
90
92
 
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path'
2
- import { resolveOperationTypeNames } from '@internals/shared'
3
- import { defineGenerator } from '@kubb/core'
2
+ import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
+ import { ast, defineGenerator } from '@kubb/core'
4
4
  import { pluginTsName } from '@kubb/plugin-ts'
5
5
  import { pluginZodName } from '@kubb/plugin-zod'
6
6
  import { File, jsxRendererSync } from '@kubb/renderer-jsx'
@@ -17,6 +17,7 @@ export const clientGenerator = defineGenerator<PluginClient>({
17
17
  name: 'client',
18
18
  renderer: jsxRendererSync,
19
19
  operation(node, ctx) {
20
+ if (!ast.isHttpOperationNode(node)) return null
20
21
  const { config, driver, resolver, root } = ctx
21
22
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options
22
23
  const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL
@@ -44,28 +45,19 @@ export const clientGenerator = defineGenerator<PluginClient>({
44
45
  const meta = {
45
46
  name: resolver.resolveName(node.operationId),
46
47
  urlName: resolver.resolveUrlName(node),
47
- file: resolver.resolveFile(
48
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
49
- { root, output, group: group ?? undefined },
50
- ),
51
- fileTs: tsResolver.resolveFile(
52
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
53
- {
54
- root,
55
- output: pluginTs.options?.output ?? output,
56
- group: pluginTs.options?.group ?? undefined,
57
- },
58
- ),
48
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group: group ?? undefined }),
49
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
50
+ root,
51
+ output: pluginTs.options?.output ?? output,
52
+ group: pluginTs.options?.group ?? undefined,
53
+ }),
59
54
  fileZod:
60
55
  zodResolver && pluginZod?.options
61
- ? zodResolver.resolveFile(
62
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
63
- {
64
- root,
65
- output: pluginZod.options.output ?? output,
66
- group: pluginZod.options?.group ?? undefined,
67
- },
68
- )
56
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
57
+ root,
58
+ output: pluginZod.options.output ?? output,
59
+ group: pluginZod.options?.group ?? undefined,
60
+ })
69
61
  : null,
70
62
  } as const
71
63
 
@@ -1,4 +1,4 @@
1
- import { defineGenerator } from '@kubb/core'
1
+ import { ast, defineGenerator } from '@kubb/core'
2
2
  import { File, jsxRendererSync } from '@kubb/renderer-jsx'
3
3
  import { Operations } from '../components/Operations'
4
4
  import type { PluginClient } from '../types'
@@ -27,7 +27,7 @@ export const operationsGenerator = defineGenerator<PluginClient>({
27
27
  banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
28
28
  footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
29
29
  >
30
- <Operations name={name} nodes={nodes} />
30
+ <Operations name={name} nodes={nodes.filter(ast.isHttpOperationNode)} />
31
31
  </File>
32
32
  )
33
33
  },
@@ -1,8 +1,7 @@
1
1
  import path from 'node:path'
2
- import { resolveOperationTypeNames } from '@internals/shared'
2
+ import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
3
  import { camelCase } from '@internals/utils'
4
- import type { ast } from '@kubb/core'
5
- import { defineGenerator } from '@kubb/core'
4
+ import { ast, defineGenerator } from '@kubb/core'
6
5
  import type { ResolverTs } from '@kubb/plugin-ts'
7
6
  import { pluginTsName } from '@kubb/plugin-ts'
8
7
  import type { ResolverZod } from '@kubb/plugin-zod'
@@ -61,16 +60,18 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
61
60
  const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
62
61
 
63
62
  function buildOperationData(node: ast.OperationNode): OperationData {
64
- const typeFile = tsResolver.resolveFile(
65
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
66
- { root, output: tsPluginOptions?.output ?? output, group: tsPluginOptions?.group },
67
- )
63
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
64
+ root,
65
+ output: tsPluginOptions?.output ?? output,
66
+ group: tsPluginOptions?.group,
67
+ })
68
68
  const zodFile =
69
69
  zodResolver && pluginZod?.options
70
- ? zodResolver.resolveFile(
71
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
72
- { root, output: pluginZod.options?.output ?? output, group: pluginZod.options?.group ?? undefined },
73
- )
70
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
71
+ root,
72
+ output: pluginZod.options?.output ?? output,
73
+ group: pluginZod.options?.group ?? undefined,
74
+ })
74
75
  : null
75
76
 
76
77
  return {
@@ -84,6 +85,7 @@ export const staticClassClientGenerator = defineGenerator<PluginClient>({
84
85
  }
85
86
 
86
87
  const controllers = nodes.reduce((acc, operationNode) => {
88
+ if (!ast.isHttpOperationNode(operationNode)) return acc
87
89
  const tag = operationNode.tags[0]
88
90
  const groupName = tag ? (group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag)) : resolver.resolveGroupName('Client')
89
91
 
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
- import { camelCase } from '@internals/utils'
2
+ import { createGroupConfig } from '@internals/shared'
3
3
 
4
- import { ast, definePlugin, type Group } from '@kubb/core'
4
+ import { ast, definePlugin } from '@kubb/core'
5
5
  import { pluginTsName } from '@kubb/plugin-ts'
6
6
  import { pluginZodName } from '@kubb/plugin-zod'
7
7
  import { classClientGenerator } from './generators/classClientGenerator.tsx'
@@ -60,7 +60,7 @@ export const pluginClient = definePlugin<PluginClient>((options) => {
60
60
  operations = false,
61
61
  paramsCasing,
62
62
  clientType = options.sdk ? 'class' : 'function',
63
- parser = 'client',
63
+ parser = false,
64
64
  client = 'axios',
65
65
  importPath,
66
66
  bundle = false,
@@ -80,19 +80,7 @@ export const pluginClient = definePlugin<PluginClient>((options) => {
80
80
  operations ? operationsGenerator : null,
81
81
  ].filter((x): x is NonNullable<typeof x> => Boolean(x))
82
82
 
83
- const groupConfig = group
84
- ? ({
85
- ...group,
86
- name: group.name
87
- ? group.name
88
- : (ctx: { group: string }) => {
89
- if (group.type === 'path') {
90
- return `${ctx.group.split('/')[1]}`
91
- }
92
- return `${camelCase(ctx.group)}Controller`
93
- },
94
- } satisfies Group)
95
- : null
83
+ const groupConfig = createGroupConfig(group, { suffix: 'Controller', honorName: true })
96
84
 
97
85
  return {
98
86
  name: pluginClientName,