@kubb/plugin-fetch 5.7.0 → 5.7.2

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.
@@ -22,10 +22,14 @@ export type SerializationStyle<TStyle = string> = {
22
22
  explode?: boolean;
23
23
  allowReserved?: boolean;
24
24
  };
25
+ /** The media type of a parameter described with `content`; JSON sends one JSON-encoded value. */
26
+ export type ParamContentType = {
27
+ contentType?: string;
28
+ };
25
29
  /**
26
30
  * The per-parameter query serialization metadata carried by the generated request.
27
31
  */
28
- export type QueryParamStyle = SerializationStyle<QueryStyle>;
32
+ export type QueryParamStyle = SerializationStyle<QueryStyle> & ParamContentType;
29
33
  /**
30
34
  * Serializes the query object into a search string. The optional second argument carries the
31
35
  * per-parameter OpenAPI `style` / `explode` / `allowReserved` metadata; without it arrays explode
@@ -36,7 +40,7 @@ export type QuerySerializer = (params: Record<string, unknown>, options?: Record
36
40
  * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the
37
41
  * OpenAPI `form` style, so only `explode` is configurable.
38
42
  */
39
- export type CookieParamStyle = {
43
+ export type CookieParamStyle = ParamContentType & {
40
44
  explode?: boolean;
41
45
  };
42
46
  /**
@@ -49,7 +53,7 @@ export type CookieParamStyle = {
49
53
  * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin'
50
54
  * ```
51
55
  */
52
- export type HeaderParamStyle = {
56
+ export type HeaderParamStyle = ParamContentType & {
53
57
  explode?: boolean;
54
58
  };
55
59
  /**
@@ -194,6 +194,47 @@
194
194
  "label": "SDK Name",
195
195
  "description": "Root client name for the generated SDK",
196
196
  "type": "text"
197
+ },
198
+ {
199
+ "key": "returnType",
200
+ "path": ["returnType"],
201
+ "label": "Return Type",
202
+ "description": "What a generated function resolves to. full is the whole result with status and error, data is only the success body when it throws, and plain is the client's own promise",
203
+ "type": "select",
204
+ "default": "full",
205
+ "choices": [
206
+ {
207
+ "value": "full"
208
+ },
209
+ {
210
+ "value": "data"
211
+ },
212
+ {
213
+ "value": "plain"
214
+ }
215
+ ]
216
+ },
217
+ {
218
+ "key": "throwOnErrorDefault",
219
+ "path": ["throwOnErrorDefault"],
220
+ "label": "Throw on Error",
221
+ "description": "Throw when a request fails. A per-call throwOnError overrides it",
222
+ "type": "boolean",
223
+ "default": true
224
+ },
225
+ {
226
+ "key": "banner",
227
+ "path": ["output", "banner"],
228
+ "label": "Banner",
229
+ "description": "Text added at the top of every generated file",
230
+ "type": "text"
231
+ },
232
+ {
233
+ "key": "footer",
234
+ "path": ["output", "footer"],
235
+ "label": "Footer",
236
+ "description": "Text added at the end of every generated file",
237
+ "type": "text"
197
238
  }
198
239
  ]
199
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-fetch",
3
- "version": "5.7.0",
3
+ "version": "5.7.2",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on the native Fetch api.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -80,17 +80,17 @@
80
80
  "registry": "https://registry.npmjs.org/"
81
81
  },
82
82
  "dependencies": {
83
- "@kubb/plugin-ts": "^5.0.14",
84
- "@kubb/plugin-zod": "^5.4.12"
83
+ "@kubb/plugin-ts": "^5.0.16",
84
+ "@kubb/plugin-zod": "^5.5.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@internals/client": "0.0.0",
88
88
  "@internals/shared": "0.0.0",
89
- "kubb": "^5.10.3",
90
- "typescript": "^6.0.3"
89
+ "kubb": "^5.12.8",
90
+ "typescript": "^7.0.2"
91
91
  },
92
92
  "peerDependencies": {
93
- "kubb": "^5.10.3"
93
+ "kubb": "^5.12.8"
94
94
  },
95
95
  "engines": {
96
96
  "node": ">=22.12"
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { createSdkGenerator, defaultMacros, isValidatorEnabled, resolverClient } from '@internals/client'
3
3
  import { createGroupConfig } from '@internals/shared'
4
- import { addGenerator, assertPluginOption, defineExtension, injectFile, Resolver, setMacros, setResolver, useConfig } from 'kubb/kit'
4
+ import { addGenerator, defineExtension, injectFile, Resolver, setMacros, setResolver, useConfig } from 'kubb/kit'
5
5
  import { compatibility } from '../kubb-extension.json'
6
6
  import { version } from '../package.json'
7
7
  import { pluginTsName } from '@kubb/plugin-ts'
@@ -74,27 +74,6 @@ export const pluginFetch = defineExtension<PluginFetch>({
74
74
  resolver: userResolver ? Resolver.merge<ResolverClient>(resolverClient, userResolver) : resolverClient,
75
75
  }
76
76
 
77
- const allowedReturnTypes = ['full', 'data', 'plain'] satisfies Array<ResolvedOptions['returnType']>
78
- assertPluginOption({ plugin: pluginFetchName, option: 'returnType', value: returnType, allowed: allowedReturnTypes })
79
- // An override replaces `returnType` per operation, so its values are checked too.
80
- for (const [index, entry] of override.entries()) {
81
- if (entry.options.returnType === undefined) continue
82
- assertPluginOption({
83
- plugin: pluginFetchName,
84
- option: `override[${index}].options.returnType`,
85
- value: entry.options.returnType,
86
- allowed: allowedReturnTypes,
87
- })
88
- }
89
- if (resolved.sdk) {
90
- assertPluginOption({
91
- plugin: pluginFetchName,
92
- option: 'sdk.mode',
93
- value: resolved.sdk.mode,
94
- allowed: ['tag', 'flat'] satisfies Array<NonNullable<ResolvedOptions['sdk']>['mode']>,
95
- })
96
- }
97
-
98
77
  setResolver(resolved.resolver)
99
78
  setMacros([...defaultMacros, ...(options.macros ?? [])])
100
79
 
@@ -25,10 +25,15 @@ export type SerializationStyle<TStyle = string> = {
25
25
  allowReserved?: boolean
26
26
  }
27
27
 
28
+ /** The media type of a parameter described with `content`; JSON sends one JSON-encoded value. */
29
+ export type ParamContentType = {
30
+ contentType?: string
31
+ }
32
+
28
33
  /**
29
34
  * The per-parameter query serialization metadata carried by the generated request.
30
35
  */
31
- export type QueryParamStyle = SerializationStyle<QueryStyle>
36
+ export type QueryParamStyle = SerializationStyle<QueryStyle> & ParamContentType
32
37
 
33
38
  /**
34
39
  * Serializes the query object into a search string. The optional second argument carries the
@@ -41,7 +46,7 @@ export type QuerySerializer = (params: Record<string, unknown>, options?: Record
41
46
  * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the
42
47
  * OpenAPI `form` style, so only `explode` is configurable.
43
48
  */
44
- export type CookieParamStyle = {
49
+ export type CookieParamStyle = ParamContentType & {
45
50
  explode?: boolean
46
51
  }
47
52
 
@@ -55,7 +60,7 @@ export type CookieParamStyle = {
55
60
  * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin'
56
61
  * ```
57
62
  */
58
- export type HeaderParamStyle = {
63
+ export type HeaderParamStyle = ParamContentType & {
59
64
  explode?: boolean
60
65
  }
61
66
 
@@ -195,6 +200,11 @@ function serializeUrlencodedBody(body: Record<string, unknown>, encoding: Record
195
200
  return parts.join('&')
196
201
  }
197
202
 
203
+ function isJsonMediaType(contentType: string | undefined): boolean {
204
+ const type = contentType?.split(';')[0]?.trim().toLowerCase()
205
+ return type === 'application/json' || !!type?.endsWith('+json')
206
+ }
207
+
198
208
  function serializeCookie({ name, value, explode }: { name: string; value: unknown; explode: boolean }): string {
199
209
  if (Array.isArray(value)) {
200
210
  const items = value.filter(notNullish).map((item) => encodeURIComponent(toValue(item)))
@@ -225,7 +235,12 @@ export function serializeCookies(cookies: Record<string, unknown>, styles?: Reco
225
235
  const parts: Array<string> = []
226
236
  for (const [name, value] of Object.entries(cookies)) {
227
237
  if (value === undefined || value === null) continue
228
- parts.push(serializeCookie({ name, value, explode: styles?.[name]?.explode ?? false }))
238
+ const style = styles?.[name]
239
+ if (isJsonMediaType(style?.contentType)) {
240
+ parts.push(`${name}=${encodeURIComponent(JSON.stringify(value, jsonReplacer))}`)
241
+ continue
242
+ }
243
+ parts.push(serializeCookie({ name, value, explode: style?.explode ?? false }))
229
244
  }
230
245
  return parts.join('; ')
231
246
  }
@@ -368,6 +383,10 @@ export const defaultQuerySerializer: QuerySerializer = (params, options) => {
368
383
  const parts: Array<string> = []
369
384
  for (const [key, value] of Object.entries(params)) {
370
385
  const paramOptions = options?.[key]
386
+ if (notNullish(value) && isJsonMediaType(paramOptions?.contentType)) {
387
+ parts.push(`${encodeComponent(key)}=${encodeComponent(JSON.stringify(value, jsonReplacer))}`)
388
+ continue
389
+ }
371
390
  parts.push(...(paramOptions ? serializeStyledQueryParam({ key, value, options: paramOptions }) : serializeDefaultQueryParam(key, value)))
372
391
  }
373
392
  return parts.join('&')
@@ -429,6 +448,11 @@ function serializeHeaderValue(value: unknown, explode: boolean): string {
429
448
  .join(',')
430
449
  }
431
450
 
451
+ /** Header values must be Latin-1, so non-ASCII characters become JSON `\uXXXX` escapes. */
452
+ function toAsciiJson(value: unknown): string {
453
+ return JSON.stringify(value, jsonReplacer).replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
454
+ }
455
+
432
456
  /**
433
457
  * Serializes array and object header parameters with the OpenAPI `simple` style before they are
434
458
  * merged. Header values are not URL-encoded. Primitive values and headers without metadata pass
@@ -439,7 +463,9 @@ export function applyHeaderStyles(headers: HeadersInit | undefined, styles: Reco
439
463
  const entries = Array.isArray(headers) ? headers : Object.entries(headers)
440
464
  return entries.map(([key, value]) => {
441
465
  const style = styles[key]
442
- if (!style || value === undefined || value === null || typeof value !== 'object') return [key, value] as [string, HeaderValue]
466
+ if (!style || value === undefined || value === null) return [key, value] as [string, HeaderValue]
467
+ if (isJsonMediaType(style.contentType)) return [key, toAsciiJson(value)] as [string, HeaderValue]
468
+ if (typeof value !== 'object') return [key, value] as [string, HeaderValue]
443
469
  return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue]
444
470
  })
445
471
  }