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

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.
@@ -37,6 +37,59 @@ function serializeHeaders(headers) {
37
37
  }
38
38
  return result;
39
39
  }
40
+ /**
41
+ * Serializes the request body into the form `fetch` expects.
42
+ * `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through
43
+ * untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`
44
+ * and JSON-serialized otherwise.
45
+ */
46
+ function serializeBody(data, contentType) {
47
+ if (data === void 0 || data === null) return void 0;
48
+ if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data;
49
+ if (typeof data === "string") return data;
50
+ if (contentType?.includes("application/x-www-form-urlencoded")) return new URLSearchParams(data);
51
+ return JSON.stringify(data);
52
+ }
53
+ /**
54
+ * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
55
+ */
56
+ function detectResponseType(contentType) {
57
+ if (!contentType) return void 0;
58
+ if (contentType.includes("application/json") || contentType.includes("text/json")) return "json";
59
+ if (contentType.includes("text/")) return "text";
60
+ if (contentType.includes("image/") || contentType.includes("application/octet-stream")) return "blob";
61
+ }
62
+ /**
63
+ * Parses a `fetch` response body.
64
+ *
65
+ * - Empty responses (204/205/304 or no body) resolve to `{}`.
66
+ * - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching
67
+ * `Response` method.
68
+ * - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.
69
+ */
70
+ async function parseResponse(response, responseType) {
71
+ if ([
72
+ 204,
73
+ 205,
74
+ 304
75
+ ].includes(response.status) || !response.body) return {};
76
+ switch (responseType ?? detectResponseType(response.headers.get("Content-Type"))) {
77
+ case "text":
78
+ case "document": return await response.text();
79
+ case "blob": return await response.blob();
80
+ case "arraybuffer": return await response.arrayBuffer();
81
+ case "stream": return response.body;
82
+ case "json": return await response.json();
83
+ }
84
+ if (responseType) return await response.json();
85
+ const text = await response.text();
86
+ if (!text) return {};
87
+ try {
88
+ return JSON.parse(text);
89
+ } catch {
90
+ return text;
91
+ }
92
+ }
40
93
  const client = async (paramsConfig, _request) => {
41
94
  const normalizedParams = new URLSearchParams();
42
95
  const config = mergeConfig(getConfig(), paramsConfig);
@@ -45,22 +98,19 @@ const client = async (paramsConfig, _request) => {
45
98
  });
46
99
  let targetUrl = [config.baseURL, config.url].filter(Boolean).join("");
47
100
  if (config.params) targetUrl += `?${normalizedParams}`;
101
+ const headers = {
102
+ ...config.contentType && config.contentType !== "multipart/form-data" ? { "Content-Type": config.contentType } : {},
103
+ ...serializeHeaders(config.headers)
104
+ };
48
105
  const response = await globalThis.fetch(targetUrl, {
49
106
  credentials: config.credentials || "same-origin",
50
107
  method: config.method?.toUpperCase(),
51
- body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),
108
+ body: serializeBody(config.data, headers["Content-Type"] ?? headers["content-type"]),
52
109
  signal: config.signal,
53
- headers: {
54
- ...config.contentType && config.contentType !== "multipart/form-data" ? { "Content-Type": config.contentType } : {},
55
- ...serializeHeaders(config.headers)
56
- }
110
+ headers
57
111
  });
58
112
  return {
59
- data: [
60
- 204,
61
- 205,
62
- 304
63
- ].includes(response.status) || !response.body ? {} : await response.json(),
113
+ data: await parseResponse(response, config.responseType),
64
114
  status: response.status,
65
115
  statusText: response.statusText,
66
116
  headers: response.headers
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.cjs","names":[],"sources":["../../src/clients/fetch.ts"],"sourcesContent":["/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).\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\n * headers like `X-Filter` are sent in their canonical JSON-string form.\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 = <TResponseData, _TError = unknown, TRequestData = unknown>(\n config: RequestConfig<TRequestData>,\n request?: unknown,\n) => Promise<ResponseConfig<TResponseData>>\n\nexport const client = async <TResponseData, _TError = unknown, RequestData = unknown>(\n paramsConfig: RequestConfig<RequestData>,\n _request?: unknown,\n): Promise<ResponseConfig<TResponseData>> => {\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 TResponseData,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n\nexport default client\n"],"mappings":";;;;;;AAsCA,IAAI,UAAkC,EAAE;AAExC,MAAa,kBAAkB;AAE/B,MAAa,aAAa,WAAmC;CAC3D,UAAU;CACV,OAAO,WAAW;;AAGpB,MAAa,eAAwC,GAAG,YAA2C;CACjG,OAAO,QAAQ,QAAoB,QAAQ,WAAW;EACpD,OAAO;GACL,GAAG;GACH,GAAG;GACH,SAAS;IACP,GAAI,MAAM,QAAQ,OAAO,QAAQ,GAAG,OAAO,YAAY,OAAO,QAAQ,GAAG,OAAO;IAChF,GAAI,MAAM,QAAQ,OAAO,QAAQ,GAAG,OAAO,YAAY,OAAO,QAAQ,GAAG,OAAO;IACjF;GACF;IACA,EAAE,CAAC;;;;;;;AAQR,SAAS,iBAAiB,SAA0D;CAClF,IAAI,CAAC,SAAS,OAAO,EAAE;CACvB,MAAM,UAAU,MAAM,QAAQ,QAAQ,GAAG,UAAU,OAAO,QAAQ,QAAQ;CAC1E,MAAM,SAAiC,EAAE;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS;EAClC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM;EAC3C,OAAO,OAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,UAAU,WAAW,KAAK,UAAU,MAAM,GAAG,OAAO,MAAM;;CAErH,OAAO;;AAUT,MAAa,SAAS,OACpB,cACA,aAC2C;CAC3C,MAAM,mBAAmB,IAAI,iBAAiB;CAE9C,MAAM,SAAS,YAAY,WAAW,EAAE,aAAa;CAErD,OAAO,QAAQ,OAAO,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;EAC5D,IAAI,UAAU,KAAA,GACZ,iBAAiB,OAAO,KAAK,UAAU,OAAO,SAAS,MAAM,UAAU,CAAC;GAE1E;CAEF,IAAI,YAAY,CAAC,OAAO,SAAS,OAAO,IAAI,CAAC,OAAO,QAAQ,CAAC,KAAK,GAAG;CAErE,IAAI,OAAO,QACT,aAAa,IAAI;CAGnB,MAAM,WAAW,MAAM,WAAW,MAAM,WAAW;EACjD,aAAa,OAAO,eAAe;EACnC,QAAQ,OAAO,QAAQ,aAAa;EACpC,MAAM,OAAO,gBAAgB,WAAW,OAAO,OAAO,KAAK,UAAU,OAAO,KAAK;EACjF,QAAQ,OAAO;EACf,SAAS;GACP,GAAI,OAAO,eAAe,OAAO,gBAAgB,wBAAwB,EAAE,gBAAgB,OAAO,aAAa,GAAG,EAAE;GACpH,GAAG,iBAAiB,OAAO,QAAQ;GACpC;EACF,CAAC;CAIF,OAAO;EACL,MAHW;GAAC;GAAK;GAAK;GAAI,CAAC,SAAS,SAAS,OAAO,IAAI,CAAC,SAAS,OAAO,EAAE,GAAG,MAAM,SAAS,MAAM;EAInG,QAAQ,SAAS;EACjB,YAAY,SAAS;EACrB,SAAS,SAAS;EACnB;;AAGH,OAAO,YAAY;AACnB,OAAO,YAAY"}
1
+ {"version":3,"file":"fetch.cjs","names":[],"sources":["../../src/clients/fetch.ts"],"sourcesContent":["/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).\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\n * headers like `X-Filter` are sent in their canonical JSON-string form.\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 = <TResponseData, _TError = unknown, TRequestData = unknown>(\n config: RequestConfig<TRequestData>,\n request?: unknown,\n) => Promise<ResponseConfig<TResponseData>>\n\nexport const client = async <TResponseData, _TError = unknown, RequestData = unknown>(\n paramsConfig: RequestConfig<RequestData>,\n _request?: unknown,\n): Promise<ResponseConfig<TResponseData>> => {\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<TResponseData>(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\nexport default client\n"],"mappings":"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"}
@@ -33,6 +33,59 @@ function serializeHeaders(headers) {
33
33
  }
34
34
  return result;
35
35
  }
36
+ /**
37
+ * Serializes the request body into the form `fetch` expects.
38
+ * `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer` and string bodies are passed through
39
+ * untouched. Plain objects are encoded as `URLSearchParams` for `application/x-www-form-urlencoded`
40
+ * and JSON-serialized otherwise.
41
+ */
42
+ function serializeBody(data, contentType) {
43
+ if (data === void 0 || data === null) return void 0;
44
+ if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data;
45
+ if (typeof data === "string") return data;
46
+ if (contentType?.includes("application/x-www-form-urlencoded")) return new URLSearchParams(data);
47
+ return JSON.stringify(data);
48
+ }
49
+ /**
50
+ * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
51
+ */
52
+ function detectResponseType(contentType) {
53
+ if (!contentType) return void 0;
54
+ if (contentType.includes("application/json") || contentType.includes("text/json")) return "json";
55
+ if (contentType.includes("text/")) return "text";
56
+ if (contentType.includes("image/") || contentType.includes("application/octet-stream")) return "blob";
57
+ }
58
+ /**
59
+ * Parses a `fetch` response body.
60
+ *
61
+ * - Empty responses (204/205/304 or no body) resolve to `{}`.
62
+ * - An explicit `responseType` (or one detected from the `Content-Type` header) forces the matching
63
+ * `Response` method.
64
+ * - As a last resort the body is read as text and `JSON.parse`d, falling back to the raw text.
65
+ */
66
+ async function parseResponse(response, responseType) {
67
+ if ([
68
+ 204,
69
+ 205,
70
+ 304
71
+ ].includes(response.status) || !response.body) return {};
72
+ switch (responseType ?? detectResponseType(response.headers.get("Content-Type"))) {
73
+ case "text":
74
+ case "document": return await response.text();
75
+ case "blob": return await response.blob();
76
+ case "arraybuffer": return await response.arrayBuffer();
77
+ case "stream": return response.body;
78
+ case "json": return await response.json();
79
+ }
80
+ if (responseType) return await response.json();
81
+ const text = await response.text();
82
+ if (!text) return {};
83
+ try {
84
+ return JSON.parse(text);
85
+ } catch {
86
+ return text;
87
+ }
88
+ }
36
89
  const client = async (paramsConfig, _request) => {
37
90
  const normalizedParams = new URLSearchParams();
38
91
  const config = mergeConfig(getConfig(), paramsConfig);
@@ -41,22 +94,19 @@ const client = async (paramsConfig, _request) => {
41
94
  });
42
95
  let targetUrl = [config.baseURL, config.url].filter(Boolean).join("");
43
96
  if (config.params) targetUrl += `?${normalizedParams}`;
97
+ const headers = {
98
+ ...config.contentType && config.contentType !== "multipart/form-data" ? { "Content-Type": config.contentType } : {},
99
+ ...serializeHeaders(config.headers)
100
+ };
44
101
  const response = await globalThis.fetch(targetUrl, {
45
102
  credentials: config.credentials || "same-origin",
46
103
  method: config.method?.toUpperCase(),
47
- body: config.data instanceof FormData ? config.data : JSON.stringify(config.data),
104
+ body: serializeBody(config.data, headers["Content-Type"] ?? headers["content-type"]),
48
105
  signal: config.signal,
49
- headers: {
50
- ...config.contentType && config.contentType !== "multipart/form-data" ? { "Content-Type": config.contentType } : {},
51
- ...serializeHeaders(config.headers)
52
- }
106
+ headers
53
107
  });
54
108
  return {
55
- data: [
56
- 204,
57
- 205,
58
- 304
59
- ].includes(response.status) || !response.body ? {} : await response.json(),
109
+ data: await parseResponse(response, config.responseType),
60
110
  status: response.status,
61
111
  statusText: response.statusText,
62
112
  headers: response.headers
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","names":[],"sources":["../../src/clients/fetch.ts"],"sourcesContent":["/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).\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\n * headers like `X-Filter` are sent in their canonical JSON-string form.\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 = <TResponseData, _TError = unknown, TRequestData = unknown>(\n config: RequestConfig<TRequestData>,\n request?: unknown,\n) => Promise<ResponseConfig<TResponseData>>\n\nexport const client = async <TResponseData, _TError = unknown, RequestData = unknown>(\n paramsConfig: RequestConfig<RequestData>,\n _request?: unknown,\n): Promise<ResponseConfig<TResponseData>> => {\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 TResponseData,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers as Headers,\n }\n}\n\nclient.getConfig = getConfig\nclient.setConfig = setConfig\n\nexport default client\n"],"mappings":";;AAsCA,IAAI,UAAkC,EAAE;AAExC,MAAa,kBAAkB;AAE/B,MAAa,aAAa,WAAmC;CAC3D,UAAU;CACV,OAAO,WAAW;;AAGpB,MAAa,eAAwC,GAAG,YAA2C;CACjG,OAAO,QAAQ,QAAoB,QAAQ,WAAW;EACpD,OAAO;GACL,GAAG;GACH,GAAG;GACH,SAAS;IACP,GAAI,MAAM,QAAQ,OAAO,QAAQ,GAAG,OAAO,YAAY,OAAO,QAAQ,GAAG,OAAO;IAChF,GAAI,MAAM,QAAQ,OAAO,QAAQ,GAAG,OAAO,YAAY,OAAO,QAAQ,GAAG,OAAO;IACjF;GACF;IACA,EAAE,CAAC;;;;;;;AAQR,SAAS,iBAAiB,SAA0D;CAClF,IAAI,CAAC,SAAS,OAAO,EAAE;CACvB,MAAM,UAAU,MAAM,QAAQ,QAAQ,GAAG,UAAU,OAAO,QAAQ,QAAQ;CAC1E,MAAM,SAAiC,EAAE;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS;EAClC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM;EAC3C,OAAO,OAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,UAAU,WAAW,KAAK,UAAU,MAAM,GAAG,OAAO,MAAM;;CAErH,OAAO;;AAUT,MAAa,SAAS,OACpB,cACA,aAC2C;CAC3C,MAAM,mBAAmB,IAAI,iBAAiB;CAE9C,MAAM,SAAS,YAAY,WAAW,EAAE,aAAa;CAErD,OAAO,QAAQ,OAAO,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;EAC5D,IAAI,UAAU,KAAA,GACZ,iBAAiB,OAAO,KAAK,UAAU,OAAO,SAAS,MAAM,UAAU,CAAC;GAE1E;CAEF,IAAI,YAAY,CAAC,OAAO,SAAS,OAAO,IAAI,CAAC,OAAO,QAAQ,CAAC,KAAK,GAAG;CAErE,IAAI,OAAO,QACT,aAAa,IAAI;CAGnB,MAAM,WAAW,MAAM,WAAW,MAAM,WAAW;EACjD,aAAa,OAAO,eAAe;EACnC,QAAQ,OAAO,QAAQ,aAAa;EACpC,MAAM,OAAO,gBAAgB,WAAW,OAAO,OAAO,KAAK,UAAU,OAAO,KAAK;EACjF,QAAQ,OAAO;EACf,SAAS;GACP,GAAI,OAAO,eAAe,OAAO,gBAAgB,wBAAwB,EAAE,gBAAgB,OAAO,aAAa,GAAG,EAAE;GACpH,GAAG,iBAAiB,OAAO,QAAQ;GACpC;EACF,CAAC;CAIF,OAAO;EACL,MAHW;GAAC;GAAK;GAAK;GAAI,CAAC,SAAS,SAAS,OAAO,IAAI,CAAC,SAAS,OAAO,EAAE,GAAG,MAAM,SAAS,MAAM;EAInG,QAAQ,SAAS;EACjB,YAAY,SAAS;EACrB,SAAS,SAAS;EACnB;;AAGH,OAAO,YAAY;AACnB,OAAO,YAAY"}
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../src/clients/fetch.ts"],"sourcesContent":["/**\n * RequestCredentials\n */\nexport type RequestCredentials = 'omit' | 'same-origin' | 'include'\n\n/**\n * Header values may be objects (e.g. JSON-encoded filter headers like `X-Filter`).\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\n * headers like `X-Filter` are sent in their canonical JSON-string form.\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 = <TResponseData, _TError = unknown, TRequestData = unknown>(\n config: RequestConfig<TRequestData>,\n request?: unknown,\n) => Promise<ResponseConfig<TResponseData>>\n\nexport const client = async <TResponseData, _TError = unknown, RequestData = unknown>(\n paramsConfig: RequestConfig<RequestData>,\n _request?: unknown,\n): Promise<ResponseConfig<TResponseData>> => {\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<TResponseData>(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\nexport default client\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -360,11 +360,30 @@ var URLPath = class {
360
360
  };
361
361
  //#endregion
362
362
  //#region ../../internals/shared/src/operation.ts
363
+ /**
364
+ * Builds the `ResolverFileParams` every operation generator passes to
365
+ * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
366
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
367
+ * that was repeated at dozens of call sites across the client and query plugins.
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
372
+ * ```
373
+ */
374
+ function operationFileEntry(node, name, extname = ".ts") {
375
+ return {
376
+ name,
377
+ extname,
378
+ tag: node.tags[0] ?? "default",
379
+ path: node.path
380
+ };
381
+ }
363
382
  function getOperationLink(node, link) {
364
383
  if (!link) return null;
365
384
  if (typeof link === "function") return link(node) ?? null;
366
385
  if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
367
- return `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`;
386
+ return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
368
387
  }
369
388
  function getContentTypeInfo(node) {
370
389
  const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
@@ -377,6 +396,22 @@ function getContentTypeInfo(node) {
377
396
  hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
378
397
  };
379
398
  }
399
+ /**
400
+ * Derives the default `responseType` for an operation from its primary success response.
401
+ *
402
+ * Returns a value only when that response declares a single non-JSON content type — a binary type
403
+ * (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`, `video/*`) maps to `'blob'`
404
+ * and other `text/*` maps to `'text'`. Otherwise `undefined`, leaving the runtime client's
405
+ * `Content-Type` auto-detection in charge.
406
+ */
407
+ function getResponseType(node) {
408
+ const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
409
+ if (contentTypes.length !== 1) return void 0;
410
+ const baseType = contentTypes[0].split(";")[0].trim().toLowerCase();
411
+ if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
412
+ if (baseType.startsWith("text/")) return "text";
413
+ if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
414
+ }
380
415
  function buildRequestConfigType(node, resolver) {
381
416
  const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
382
417
  const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
@@ -420,6 +455,15 @@ function isErrorStatusCode(statusCode) {
420
455
  const code = getStatusCodeNumber(statusCode);
421
456
  return code !== null && code >= 400;
422
457
  }
458
+ function getSuccessResponses(responses) {
459
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
460
+ }
461
+ function getOperationSuccessResponses(node) {
462
+ return getSuccessResponses(node.responses);
463
+ }
464
+ function getPrimarySuccessResponse(node) {
465
+ return getOperationSuccessResponses(node)[0] ?? null;
466
+ }
423
467
  function resolveErrorNames(node, resolver) {
424
468
  return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
425
469
  }
@@ -462,6 +506,39 @@ function resolveOperationTypeNames(node, resolver, options = {}) {
462
506
  return result;
463
507
  }
464
508
  //#endregion
509
+ //#region ../../internals/shared/src/group.ts
510
+ /**
511
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
512
+ * shared default naming so every plugin groups output consistently:
513
+ *
514
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
515
+ * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
516
+ *
517
+ * Returns `null` when grouping is disabled, matching the per-plugin convention.
518
+ *
519
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
520
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
521
+ * @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
526
+ * createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
527
+ * createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
528
+ * ```
529
+ */
530
+ function createGroupConfig(group, options) {
531
+ if (!group) return null;
532
+ const defaultName = (ctx) => {
533
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
534
+ return `${camelCase(ctx.group)}${options.suffix}`;
535
+ };
536
+ return {
537
+ ...group,
538
+ name: options.honorName && group.name ? group.name : defaultName
539
+ };
540
+ }
541
+ //#endregion
465
542
  //#region ../../internals/shared/src/params.ts
466
543
  function buildParamsMapping(originalParams, mappedParams) {
467
544
  const mapping = {};
@@ -553,6 +630,7 @@ function buildUrlParamsNode({ paramsType, paramsCasing, pathParamsType, node, ts
553
630
  });
554
631
  }
555
632
  function Url({ name, isExportable = true, isIndexable = true, baseURL, paramsType, paramsCasing, pathParamsType, node, tsResolver }) {
633
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
556
634
  const path = new URLPath(node.path);
557
635
  const paramsNode = buildUrlParamsNode({
558
636
  paramsType,
@@ -606,9 +684,11 @@ function buildClientParamsNode({ paramsType, paramsCasing, pathParamsType, node,
606
684
  });
607
685
  }
608
686
  function Client({ name, isExportable = true, isIndexable = true, returnType, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType, node, tsResolver, zodResolver, urlName, children, isConfigurable = true }) {
687
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
609
688
  const path = new URLPath(node.path);
610
689
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
611
690
  const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
691
+ const responseType = getResponseType(node);
612
692
  const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
613
693
  const { path: casedPathParams, query: casedQueryParams, header: casedHeaderParams } = getOperationParameters(node, { paramsCasing });
614
694
  const pathParamsMapping = paramsCasing && !urlName ? buildParamsMapping(originalPathParams, casedPathParams) : null;
@@ -656,6 +736,7 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
656
736
  params: queryParamsName ? queryParamsMapping ? { value: "mappedParams" } : {} : null,
657
737
  data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
658
738
  contentType: isConfigurable && isMultipleContentTypes ? {} : null,
739
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
659
740
  requestConfig: isConfigurable ? { mode: "inlineSpread" } : null,
660
741
  headers: headers.length ? { value: isConfigurable ? `{ ${headers.join(", ")}, ...requestConfig.headers }` : `{ ${headers.join(", ")} }` } : null
661
742
  }
@@ -663,8 +744,8 @@ function Client({ name, isExportable = true, isIndexable = true, returnType, bas
663
744
  const childrenElement = children ? children : /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
664
745
  dataReturnType === "full" && parser === "zod" && zodResponseName && `return {...res, data: ${zodResponseName}.parse(res.data)}`,
665
746
  dataReturnType === "data" && parser === "zod" && zodResponseName && `return ${zodResponseName}.parse(res.data)`,
666
- dataReturnType === "full" && parser === "client" && "return res",
667
- dataReturnType === "data" && parser === "client" && "return res.data"
747
+ dataReturnType === "full" && parser !== "zod" && "return res",
748
+ dataReturnType === "data" && parser !== "zod" && "return res.data"
668
749
  ] });
669
750
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}), /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
670
751
  name,
@@ -740,16 +821,18 @@ function buildClassClientParams({ node, path, baseURL, tsResolver, isFormData, i
740
821
  const { query: queryParams } = getOperationParameters(node);
741
822
  const queryParamsName = queryParams.length > 0 ? tsResolver.resolveQueryParamsName(node, queryParams[0]) : null;
742
823
  const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : null;
824
+ const responseType = getResponseType(node);
743
825
  return createFunctionParams({ config: {
744
826
  mode: "object",
745
827
  children: {
746
828
  requestConfig: { mode: "inlineSpread" },
747
- method: { value: JSON.stringify(node.method.toUpperCase()) },
829
+ method: { value: JSON.stringify(_kubb_core.ast.isHttpOperationNode(node) ? node.method.toUpperCase() : "") },
748
830
  url: { value: path.template },
749
831
  baseURL: baseURL ? { value: JSON.stringify(baseURL) } : null,
750
832
  params: queryParamsName ? {} : null,
751
833
  data: requestName ? { value: isMultipleContentTypes && hasFormData ? "contentType === 'multipart/form-data' ? formData as FormData : requestData" : isFormData ? "formData as FormData" : "requestData" } : null,
752
834
  contentType: isMultipleContentTypes ? {} : null,
835
+ responseType: responseType ? { value: JSON.stringify(responseType) } : null,
753
836
  headers: headers.length ? { value: `{ ${headers.join(", ")}, ...requestConfig.headers }` } : null
754
837
  }
755
838
  } });
@@ -779,13 +862,14 @@ function buildReturnStatement({ dataReturnType, parser, node, zodResolver }) {
779
862
  const zodResponseName = zodResolver && parser === "zod" ? zodResolver.resolveResponseName?.(node) : null;
780
863
  if (dataReturnType === "full" && parser === "zod" && zodResponseName) return `return {...res, data: ${zodResponseName}.parse(res.data)}`;
781
864
  if (dataReturnType === "data" && parser === "zod" && zodResponseName) return `return ${zodResponseName}.parse(res.data)`;
782
- if (dataReturnType === "full" && parser === "client") return "return res";
865
+ if (dataReturnType === "full" && parser !== "zod") return "return res";
783
866
  return "return res.data";
784
867
  }
785
868
  //#endregion
786
869
  //#region src/components/ClassClient.tsx
787
870
  const declarationPrinter$1 = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
788
871
  function generateMethod$1({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
872
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return "";
789
873
  const path = new URLPath(node.path, { casing: paramsCasing });
790
874
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
791
875
  const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
@@ -913,22 +997,12 @@ const classClientGenerator = (0, _kubb_core.defineGenerator)({
913
997
  const pluginZod = parser === "zod" ? driver.getPlugin(_kubb_plugin_zod.pluginZodName) : null;
914
998
  const zodResolver = pluginZod ? driver.getResolver(_kubb_plugin_zod.pluginZodName) : null;
915
999
  function buildOperationData(node) {
916
- const typeFile = tsResolver.resolveFile({
917
- name: node.operationId,
918
- extname: ".ts",
919
- tag: node.tags[0] ?? "default",
920
- path: node.path
921
- }, {
1000
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
922
1001
  root,
923
1002
  output: tsPluginOptions?.output ?? output,
924
1003
  group: tsPluginOptions?.group
925
1004
  });
926
- const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile({
927
- name: node.operationId,
928
- extname: ".ts",
929
- tag: node.tags[0] ?? "default",
930
- path: node.path
931
- }, {
1005
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
932
1006
  root,
933
1007
  output: pluginZod.options?.output ?? output,
934
1008
  group: pluginZod.options?.group ?? void 0
@@ -943,6 +1017,7 @@ const classClientGenerator = (0, _kubb_core.defineGenerator)({
943
1017
  };
944
1018
  }
945
1019
  const controllers = nodes.reduce((acc, operationNode) => {
1020
+ if (!_kubb_core.ast.isHttpOperationNode(operationNode)) return acc;
946
1021
  const tag = operationNode.tags[0];
947
1022
  const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
948
1023
  if (!tag && !group) {
@@ -1202,6 +1277,7 @@ const clientGenerator = (0, _kubb_core.defineGenerator)({
1202
1277
  name: "client",
1203
1278
  renderer: _kubb_renderer_jsx.jsxRendererSync,
1204
1279
  operation(node, ctx) {
1280
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
1205
1281
  const { config, driver, resolver, root } = ctx;
1206
1282
  const { output, urlType, dataReturnType, paramsCasing, paramsType, pathParamsType, parser, importPath, group } = ctx.options;
1207
1283
  const baseURL = ctx.options.baseURL ?? ctx.meta.baseURL;
@@ -1215,32 +1291,17 @@ const clientGenerator = (0, _kubb_core.defineGenerator)({
1215
1291
  const meta = {
1216
1292
  name: resolver.resolveName(node.operationId),
1217
1293
  urlName: resolver.resolveUrlName(node),
1218
- file: resolver.resolveFile({
1219
- name: node.operationId,
1220
- extname: ".ts",
1221
- tag: node.tags[0] ?? "default",
1222
- path: node.path
1223
- }, {
1294
+ file: resolver.resolveFile(operationFileEntry(node, node.operationId), {
1224
1295
  root,
1225
1296
  output,
1226
1297
  group: group ?? void 0
1227
1298
  }),
1228
- fileTs: tsResolver.resolveFile({
1229
- name: node.operationId,
1230
- extname: ".ts",
1231
- tag: node.tags[0] ?? "default",
1232
- path: node.path
1233
- }, {
1299
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1234
1300
  root,
1235
1301
  output: pluginTs.options?.output ?? output,
1236
1302
  group: pluginTs.options?.group ?? void 0
1237
1303
  }),
1238
- fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile({
1239
- name: node.operationId,
1240
- extname: ".ts",
1241
- tag: node.tags[0] ?? "default",
1242
- path: node.path
1243
- }, {
1304
+ fileZod: zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1244
1305
  root,
1245
1306
  output: pluginZod.options.output ?? output,
1246
1307
  group: pluginZod.options?.group ?? void 0
@@ -1434,6 +1495,7 @@ const groupedClientGenerator = (0, _kubb_core.defineGenerator)({
1434
1495
  function Operations({ name, nodes }) {
1435
1496
  const operationsObject = {};
1436
1497
  nodes.forEach((node) => {
1498
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return;
1437
1499
  operationsObject[node.operationId] = {
1438
1500
  path: new URLPath(node.path).URL,
1439
1501
  method: node.method.toLowerCase()
@@ -1495,7 +1557,7 @@ const operationsGenerator = (0, _kubb_core.defineGenerator)({
1495
1557
  }),
1496
1558
  children: /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(Operations, {
1497
1559
  name,
1498
- nodes
1560
+ nodes: nodes.filter(_kubb_core.ast.isHttpOperationNode)
1499
1561
  })
1500
1562
  });
1501
1563
  }
@@ -1504,6 +1566,7 @@ const operationsGenerator = (0, _kubb_core.defineGenerator)({
1504
1566
  //#region src/components/StaticClassClient.tsx
1505
1567
  const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
1506
1568
  function generateMethod({ node, name, tsResolver, zodResolver, baseURL, dataReturnType, parser, paramsType, paramsCasing, pathParamsType }) {
1569
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return "";
1507
1570
  const path = new URLPath(node.path, { casing: paramsCasing });
1508
1571
  const { defaultContentType: contentType, isMultipleContentTypes, hasFormData } = getContentTypeInfo(node);
1509
1572
  const isFormData = !isMultipleContentTypes && contentType === "multipart/form-data";
@@ -1604,22 +1667,12 @@ const staticClassClientGenerator = (0, _kubb_core.defineGenerator)({
1604
1667
  const pluginZod = parser === "zod" ? driver.getPlugin(_kubb_plugin_zod.pluginZodName) : null;
1605
1668
  const zodResolver = pluginZod ? driver.getResolver(_kubb_plugin_zod.pluginZodName) : null;
1606
1669
  function buildOperationData(node) {
1607
- const typeFile = tsResolver.resolveFile({
1608
- name: node.operationId,
1609
- extname: ".ts",
1610
- tag: node.tags[0] ?? "default",
1611
- path: node.path
1612
- }, {
1670
+ const typeFile = tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
1613
1671
  root,
1614
1672
  output: tsPluginOptions?.output ?? output,
1615
1673
  group: tsPluginOptions?.group
1616
1674
  });
1617
- const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile({
1618
- name: node.operationId,
1619
- extname: ".ts",
1620
- tag: node.tags[0] ?? "default",
1621
- path: node.path
1622
- }, {
1675
+ const zodFile = zodResolver && pluginZod?.options ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
1623
1676
  root,
1624
1677
  output: pluginZod.options?.output ?? output,
1625
1678
  group: pluginZod.options?.group ?? void 0
@@ -1634,6 +1687,7 @@ const staticClassClientGenerator = (0, _kubb_core.defineGenerator)({
1634
1687
  };
1635
1688
  }
1636
1689
  const controllers = nodes.reduce((acc, operationNode) => {
1690
+ if (!_kubb_core.ast.isHttpOperationNode(operationNode)) return acc;
1637
1691
  const tag = operationNode.tags[0];
1638
1692
  const groupName = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.resolveGroupName(tag) : resolver.resolveGroupName("Client");
1639
1693
  if (!tag && !group) {
@@ -1903,20 +1957,17 @@ const pluginClient = (0, _kubb_core.definePlugin)((options) => {
1903
1957
  const { output = {
1904
1958
  path: "clients",
1905
1959
  barrelType: "named"
1906
- }, group, exclude = [], include, override = [], urlType = false, dataReturnType = "data", paramsType = "inline", pathParamsType = paramsType === "object" ? "object" : options.pathParamsType || "inline", operations = false, paramsCasing, clientType = options.sdk ? "class" : "function", parser = "client", client = "axios", importPath, bundle = false, sdk, baseURL, resolver: userResolver, transformer: userTransformer } = options;
1960
+ }, group, exclude = [], include, override = [], urlType = false, dataReturnType = "data", paramsType = "inline", pathParamsType = paramsType === "object" ? "object" : options.pathParamsType || "inline", operations = false, paramsCasing, clientType = options.sdk ? "class" : "function", parser = false, client = "axios", importPath, bundle = false, sdk, baseURL, resolver: userResolver, transformer: userTransformer } = options;
1907
1961
  const resolvedImportPath = importPath ?? (!bundle ? `@kubb/plugin-client/clients/${client}` : void 0);
1908
1962
  const selectedGenerators = options.generators ?? [
1909
1963
  clientType === "staticClass" ? staticClassClientGenerator : clientType === "class" ? classClientGenerator : clientGenerator,
1910
1964
  group && clientType === "function" ? groupedClientGenerator : null,
1911
1965
  operations ? operationsGenerator : null
1912
1966
  ].filter((x) => Boolean(x));
1913
- const groupConfig = group ? {
1914
- ...group,
1915
- name: group.name ? group.name : (ctx) => {
1916
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1917
- return `${camelCase(ctx.group)}Controller`;
1918
- }
1919
- } : null;
1967
+ const groupConfig = createGroupConfig(group, {
1968
+ suffix: "Controller",
1969
+ honorName: true
1970
+ });
1920
1971
  return {
1921
1972
  name: pluginClientName,
1922
1973
  options,