@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.
- package/dist/clients/fetch.cjs +60 -10
- package/dist/clients/fetch.cjs.map +1 -1
- package/dist/clients/fetch.js +60 -10
- package/dist/clients/fetch.js.map +1 -1
- package/dist/index.cjs +107 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +107 -56
- package/dist/index.js.map +1 -1
- package/dist/templates/clients/fetch.source.cjs +1 -1
- package/dist/templates/clients/fetch.source.js +1 -1
- package/extension.yaml +20 -46
- package/package.json +6 -6
- package/src/clients/fetch.ts +80 -7
- package/src/components/ClassClient.tsx +2 -1
- package/src/components/Client.tsx +6 -2
- package/src/components/Operations.tsx +2 -1
- package/src/components/StaticClassClient.tsx +2 -1
- package/src/components/Url.tsx +1 -0
- package/src/generators/classClientGenerator.tsx +13 -11
- package/src/generators/clientGenerator.tsx +14 -22
- package/src/generators/operationsGenerator.tsx +2 -2
- package/src/generators/staticClassClientGenerator.tsx +13 -11
- package/src/plugin.ts +4 -16
- package/src/types.ts +4 -3
- package/src/utils.ts +6 -4
|
@@ -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:
|
|
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:
|
|
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:
|
|
688
|
+
type: false | 'zod'
|
|
714
689
|
required: false
|
|
715
|
-
default:
|
|
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
|
-
- `
|
|
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:
|
|
873
|
+
- name: sdk
|
|
899
874
|
type: '{ className: string }'
|
|
900
875
|
required: false
|
|
901
876
|
description: |
|
|
902
|
-
Generates a single
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 {
|
|
940
|
-
import {
|
|
941
|
-
import {
|
|
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
|
|
945
|
-
readonly
|
|
946
|
-
readonly
|
|
918
|
+
readonly petController: petController
|
|
919
|
+
readonly storeController: storeController
|
|
920
|
+
readonly userController: userController
|
|
947
921
|
|
|
948
922
|
constructor(config: Partial<RequestConfig> & { client?: Client } = {}) {
|
|
949
|
-
this.
|
|
950
|
-
this.
|
|
951
|
-
this.
|
|
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.
|
|
963
|
-
const user = await api.
|
|
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:
|
|
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.
|
|
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.
|
|
91
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
92
|
-
"@kubb/plugin-ts": "5.0.0-beta.
|
|
93
|
-
"@kubb/plugin-zod": "5.0.0-beta.
|
|
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.
|
|
101
|
+
"@kubb/renderer-jsx": "5.0.0-beta.33",
|
|
102
102
|
"axios": "^1.7.2"
|
|
103
103
|
},
|
|
104
104
|
"peerDependenciesMeta": {
|
package/src/clients/fetch.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
188
|
+
const data = await parseResponse<TResponseData>(response, config.responseType)
|
|
116
189
|
|
|
117
190
|
return {
|
|
118
|
-
data
|
|
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
|
|
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
|
|
203
|
-
{dataReturnType === 'data' && parser
|
|
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
|
|
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
|
|
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'
|
package/src/components/Url.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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 {
|
|
2
|
+
import { createGroupConfig } from '@internals/shared'
|
|
3
3
|
|
|
4
|
-
import { ast, definePlugin
|
|
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 =
|
|
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,
|