@kubb/plugin-client 5.12.0-canary.20261109T134631 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-Dq0z61hu.d.ts +554 -0
  3. package/dist/index.cjs +1396 -802
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +126 -74
  6. package/dist/index.d.ts +126 -74
  7. package/dist/index.js +1394 -804
  8. package/dist/index.js.map +1 -1
  9. package/dist/serializers-BRgfPGr1.d.ts +119 -0
  10. package/dist/{standardSchema-BShJWz2i.d.ts → standardSchema-CGuiSK7F.d.ts} +15 -37
  11. package/dist/templates/client.d.ts +3 -2
  12. package/dist/templates/core.d.ts +3 -3
  13. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  14. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  15. package/dist/templates/mediaTypes.d.ts +2 -0
  16. package/dist/templates/serializers/cookies.d.ts +5 -0
  17. package/dist/templates/serializers/headers.d.ts +5 -0
  18. package/dist/templates/serializers/path.d.ts +5 -0
  19. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  20. package/dist/templates/serializers/query.d.ts +5 -0
  21. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  22. package/dist/templates/serializers.d.ts +2 -2
  23. package/dist/templates/sse.d.ts +20 -113
  24. package/dist/templates/standardSchema.d.ts +1 -1
  25. package/docs/guides/authentication.md +9 -1
  26. package/docs/guides/migrate-to-v6.md +139 -0
  27. package/docs/guides/validate-requests-and-responses.md +8 -3
  28. package/docs/guides/write-your-client.md +117 -11
  29. package/docs/options.md +24 -0
  30. package/docs/overview.md +1 -1
  31. package/kubb-extension.json +1 -1
  32. package/package.json +8 -7
  33. package/src/builders/callConfig.ts +80 -0
  34. package/src/builders/contentType.ts +35 -0
  35. package/src/builders/generics.ts +29 -0
  36. package/src/builders/operationBody.ts +43 -0
  37. package/src/builders/operationData.ts +64 -0
  38. package/src/builders/returnStatement.ts +103 -0
  39. package/src/builders/security.ts +125 -0
  40. package/src/builders/signature.ts +71 -0
  41. package/src/builders/styles.ts +94 -0
  42. package/src/builders/validator.ts +84 -0
  43. package/src/builders/validatorOptions.ts +53 -0
  44. package/src/clientTypes.ts +195 -0
  45. package/src/components/ClientRuntime.tsx +97 -0
  46. package/src/components/Operation.tsx +72 -0
  47. package/src/components/SdkClient.tsx +100 -0
  48. package/src/components/SdkFacade.tsx +35 -0
  49. package/src/generators/clientGenerator.tsx +91 -6
  50. package/src/generators/sdkGenerator.tsx +209 -0
  51. package/src/index.ts +3 -1
  52. package/src/macros.ts +9 -0
  53. package/src/plugin.ts +9 -9
  54. package/src/resolver.ts +28 -0
  55. package/src/setup.ts +90 -0
  56. package/src/types.ts +16 -2
  57. package/templates/client.ts +2 -1
  58. package/templates/core.ts +735 -286
  59. package/templates/mediaTypes/formData.ts +59 -0
  60. package/templates/mediaTypes/urlencoded.ts +33 -0
  61. package/templates/mediaTypes.ts +147 -0
  62. package/templates/serializers/cookies.ts +58 -0
  63. package/templates/serializers/headers.ts +35 -0
  64. package/templates/serializers/path.ts +19 -0
  65. package/templates/serializers/pathStyles.ts +27 -0
  66. package/templates/serializers/query.ts +21 -0
  67. package/templates/serializers/queryStyles.ts +107 -0
  68. package/templates/serializers.ts +48 -364
  69. package/templates/sse.ts +87 -178
  70. package/templates/standardSchema.ts +16 -37
  71. package/dist/client-BxO6Dygu.d.ts +0 -355
  72. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
@@ -0,0 +1,139 @@
1
+ # Migrate to v6
2
+
3
+ `@kubb/plugin-client` 6 calls your client with type arguments and streams through `client.stream`. Update the `client` export of your module, then regenerate. Find your code in the table, then follow its section.
4
+
5
+ ## What to change
6
+
7
+ | Your code | What to do |
8
+ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | Exports a `client` that takes no type arguments | Make it generic. See [A generic client call](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#a-generic-client-call). |
10
+ | Exports `toEventStream` or `EventStreamResult` | Add a `stream` method to `client`. See [A stream method](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#a-stream-method-instead-of-toeventstream). |
11
+ | Declares `responseType` on `RequestConfig`, or lacks `contentType` or `cookies` | Add the new fields. See [New fields on the request config](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#new-fields-on-the-request-config). |
12
+ | Reads `security` entries as single schemes | Handle arrays and the new fields. See [Security requirements](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#security-requirements). |
13
+ | Imports a `…Status2XX` type, or writes a `'2XX'` key | Write the range in lower case. See [Status ranges](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#status-ranges). |
14
+ | Groups by tag in an OpenAPI 3.2 document whose tags set `parent` | Update the import paths. See [Tag folders](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#tag-folders). |
15
+ | Has `servers` on a path or operation, or reads XML, YAML, CSV, or binary bodies | Check [Behavior changes](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#behavior-changes). It compiles. |
16
+ | Wants `.unwrap()` or a call that resolves to the body | Set `returnType`. See [Return type](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#return-type). This is optional. |
17
+
18
+ ## Upgrade steps
19
+
20
+ 1. Upgrade `kubb` and `@kubb/plugin-client` together. v6 needs Kubb 5.18.4 or later:
21
+
22
+ ```bash
23
+ npm install --save-dev kubb@latest @kubb/plugin-client@6
24
+ ```
25
+
26
+ 2. Update your client module with the sections below.
27
+
28
+ 3. Regenerate the operation files:
29
+
30
+ ```bash
31
+ npx kubb generate
32
+ ```
33
+
34
+ 4. Run the type checker. A missing export or field in your client module shows up as a compile error in the generated files:
35
+
36
+ ```bash
37
+ npx tsc --noEmit
38
+ ```
39
+
40
+ ## A generic client call
41
+
42
+ A generated function now calls `client<TResponses, ThrowOnError>(config)`. Make `client` generic:
43
+
44
+ ```typescript
45
+ export const client = <TResponses, ThrowOnError extends boolean = true>(config: RequestConfig) =>
46
+ send(config) as Promise<RequestResult<TResponses, ThrowOnError>>
47
+ ```
48
+
49
+ ## A stream method instead of toEventStream
50
+
51
+ A Server-Sent Events or JSON Lines operation calls `client.stream<TResponses, ThrowOnError, TItem>(config)`. Remove `toEventStream` and `EventStreamResult`, add `stream` to `client`, and export `StreamResult`, `SuccessOf`, `TypedEvent`, and `ServerSentEvent`:
52
+
53
+ ```typescript
54
+ export type StreamResult<TResponses, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>> = {
55
+ stream: AsyncGenerator<TEvent>
56
+ response: Response
57
+ }
58
+
59
+ export function client<TResponses, ThrowOnError extends boolean = true>(config: RequestConfig) {
60
+ return send<TResponses, ThrowOnError>(config)
61
+ }
62
+
63
+ client.stream = async <TResponses, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>>(
64
+ config: RequestConfig,
65
+ ): Promise<StreamResult<TResponses, ThrowOnError, TEvent>> => {
66
+ // send the request and parse the body into TEvent values
67
+ }
68
+ ```
69
+
70
+ A Server-Sent Events call also passes `eventShape: 'event'` or `'data'`. A JSON Lines call, such as `application/jsonl` or `application/x-ndjson`, passes the media type in `contentType.response` and expects one parsed item per line. See [Write your client](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client).
71
+
72
+ ## New fields on the request config
73
+
74
+ Add these fields to your `RequestConfig`, or the generated functions fail to compile:
75
+
76
+ ```typescript
77
+ export type RequestConfig = {
78
+ // ...
79
+ contentType?: string | { request?: string; response?: string }
80
+ // Replaces `responseType`. Binary downloads pass 'blob'.
81
+ parseAs?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'
82
+ eventShape?: 'event' | 'data'
83
+ // Name and value pairs for `in: cookie` parameters. Join them into a `Cookie` header on the server. Browsers ignore a `Cookie` header set by script and send their own cookies.
84
+ cookies?: object
85
+ // `style`, `explode`, `allowReserved`, and `contentType` per parameter, grouped by location.
86
+ styles?: object
87
+ }
88
+ ```
89
+
90
+ A cookie parameter with the OpenAPI 3.2 `cookie` style passes `style: 'cookie'` in `styles`. Send its values without percent-encoding. See [Serialize parameters](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#serialize-parameters).
91
+
92
+ ## Security requirements
93
+
94
+ A `security` entry is an array when one requirement needs several schemes together, such as an API key and a session cookie. Send all of them, or skip the entry.
95
+
96
+ An `oauth2` scheme now also passes `flows` and `metadataUrl`, and an `openIdConnect` scheme passes `url`. Accept them on your auth type:
97
+
98
+ ```typescript
99
+ export type Auth = {
100
+ type: string
101
+ scheme?: string
102
+ name?: string
103
+ in?: string
104
+ scopes?: Array<string>
105
+ flows?: Record<string, Record<string, string>>
106
+ metadataUrl?: string
107
+ url?: string
108
+ }
109
+ ```
110
+
111
+ ## Status ranges
112
+
113
+ A status range is written in lower case in generated names and keys. Rename the imports, and the range keys in a responses record or `validator.error` map you wrote by hand. An upper-case key such as `'2XX'` is no longer read:
114
+
115
+ ```diff
116
+ - import type { ListPetsStatus4XX } from './gen/types/ListPets'
117
+ + import type { ListPetsStatus4xx } from './gen/types/ListPets'
118
+ ```
119
+
120
+ ## Tag folders
121
+
122
+ With `group: { type: 'tag' }`, a tag whose OpenAPI 3.2 `parent` is `store` now writes to `store/payment/` instead of `payment/`. `importPath` is written into every file as given, so one relative path cannot serve files at two depths, such as `store/` and `store/payment/`. Pass `group.name` to keep one folder per tag, or set `importPath` to a package name or alias that resolves from every folder:
123
+
124
+ ```typescript
125
+ pluginClient({ importPath: '../../../client', group: { type: 'tag', name: ({ group }) => group } })
126
+ ```
127
+
128
+ ## Behavior changes
129
+
130
+ These compile without errors, so check them by hand:
131
+
132
+ - A path or operation with its own `servers` passes that server as `baseURL` on the call. Your client must use `config.baseURL` when it is set.
133
+ - A `2xx` response key is a success, and an exact code such as `200` wins over a range listed before it.
134
+ - An XML, YAML, or CSV response is typed `string`. An image, audio, video, PDF, or `application/octet-stream` response is typed `Blob`. When every success content type is binary, the call passes `parseAs: 'blob'`.
135
+ - An operation whose only success content type is JSON Lines returns a `StreamResult`, not the whole body as one string.
136
+
137
+ ## Return type
138
+
139
+ `returnType` is new and defaults to `'plain'`, so existing code keeps working. With `'full'`, generated calls wrap the result in `withUnwrap`, so your module must also export `withUnwrap`, `resolveThrowOnError`, and the `Unwrappable` type. With `'data'`, calls resolve to the body when `throwOnError` is `true`, and to the full result when it is `false`. Your module must then export `unwrapResult`, `resolveThrowOnError`, and the `UnwrappedResult` type. A stream operation also imports `StreamOptions` with `StreamResult` for `'full'`, or with `UnwrappedStreamResult` for `'data'`. Both modes need a `StreamResult` with `data` and `error`, as the one in `@kubb/plugin-client/templates/core.ts` has, so the minimal type above is not enough. `@kubb/plugin-client/templates/core.ts` exports all of these. See [returnType](https://kubb.dev/extensions/kubb-labs/plugin-client/options#returntype).
@@ -29,11 +29,13 @@ Each generated call now includes the schemas:
29
29
  request({
30
30
  method: 'POST',
31
31
  url: '/pet',
32
- validator: { response: addPetResponseSchema, error: addPetErrorSchema },
33
32
  ...config,
33
+ validator: { response: addPetResponseSchema, error: { 405: addPetStatus405Schema }, ...config.validator },
34
34
  })
35
35
  ```
36
36
 
37
+ A `validator` passed on one call is merged per key, so `validator: { request }` adds a request schema and keeps the generated response schema. A per-call `error` replaces the generated error map as a whole.
38
+
37
39
  Add a `validator` field to your `RequestConfig`, then run the schemas in `client`. Zod schemas follow [Standard Schema](https://standardschema.dev), so `~standard.validate` works with any compatible library:
38
40
 
39
41
  ```typescript [src/client.ts]
@@ -42,7 +44,7 @@ type Schema = { '~standard': { validate: (value: unknown) => Result | Promise<Re
42
44
 
43
45
  export type RequestConfig = {
44
46
  // ...
45
- validator?: { request?: Schema; response?: Schema; error?: Schema }
47
+ validator?: { request?: Schema; response?: Schema; error?: Schema | Record<string, Schema> }
46
48
  }
47
49
 
48
50
  // inside client(), after reading the body
@@ -53,6 +55,8 @@ const validatedBody = result && 'value' in result ? result.value : body
53
55
 
54
56
  Return `validatedBody` as `data`, so a schema that transforms the value takes effect. Without a response schema, `validatedBody` is the original `body`.
55
57
 
58
+ `error` is keyed by status, such as `{ 404: schema, '5xx': schema, default: schema }`. Pick the exact status, then its range, then `default`, and skip validation when none matches.
59
+
56
60
  Set `validator: { request: 'zod' }` or `{ response: 'zod' }` to pass one direction only. Without `pluginZod()` in the plugins list, generation stops with an error.
57
61
 
58
62
  ## Validate params
@@ -63,12 +67,13 @@ With `validator: { params: 'zod' }`, each call also passes the schemas for its p
63
67
  request({
64
68
  method: 'PUT',
65
69
  url: '/events/{day}',
70
+ ...config,
66
71
  validator: {
67
72
  path: putEventPathSchema,
68
73
  query: putEventQuerySchema,
69
74
  headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape),
75
+ ...config.validator,
70
76
  },
71
- ...config,
72
77
  })
73
78
  ```
74
79
 
@@ -10,7 +10,9 @@ The generated functions import your module through [`importPath`](https://kubb.d
10
10
  | `Options` | type | The grouped options object a generated function accepts. |
11
11
  | `RequestResult` | type | What a generated function resolves to. |
12
12
 
13
- If your spec has server-sent event operations, also export `toEventStream` and the `EventStreamResult`, `SuccessOf`, and `TypedEvent` types. A generated function calls `toEventStream<TData>(promise)` with the promise your `client` returns and nothing else, and leaves the response validator on `config.validator.response`, so your module decides how to validate events.
13
+ A generated function calls `client<TResponses, ThrowOnError>(config)`, so type `client` to return `Promise<RequestResult<TResponses, ThrowOnError>>`.
14
+
15
+ If your spec has server-sent event or JSON Lines operations, `client` also needs a `stream` method, and the module exports the `StreamResult`, `SuccessOf`, `TypedEvent`, and `ServerSentEvent` types. A generated function calls `client.stream<TResponses, ThrowOnError, TItem>(config)` and returns `Promise<StreamResult<TResponses, ThrowOnError, TItem>>`. `TItem` is `TypedEvent<…>` when the document names each event and the success type for JSON Lines. When the document types only the payload, `TItem` is left out and defaults to `ServerSentEvent<SuccessOf<TResponses>>`. A Server-Sent Events call passes `eventShape: 'event'` or `'data'` to say whether `config.validator.response` checks the whole event or its `data`.
14
16
 
15
17
  The query plugins (`@kubb/plugin-react-query`, `@kubb/plugin-vue-query`, `@kubb/plugin-swr`) and `@kubb/plugin-mcp` call the generated functions with `throwOnError: true` and read `data` off the result. The query plugins also import two more types from your module:
16
18
 
@@ -21,6 +23,23 @@ The query plugins (`@kubb/plugin-react-query`, `@kubb/plugin-vue-query`, `@kubb/
21
23
 
22
24
  With a relative `importPath`, the hooks resolve it from the generated operation file, so one path serves both.
23
25
 
26
+ ## Build on the shared runtime
27
+
28
+ To reuse the runtime of the fetch and axios clients instead of writing your own request pipeline:
29
+
30
+ 1. Copy `core.ts`, `mediaTypes.ts`, `serializers.ts`, `standardSchema.ts`, `sse.ts`, and the `mediaTypes/` and `serializers/` files your spec needs from `@kubb/plugin-client/templates` next to your `client.ts`.
31
+ 2. In `client.ts`, export the three types the core imports, listed below.
32
+ 3. Write a `Transport` function that sends a `ResolvedRequest` and returns a web `Response`. If your library does not return one, wrap its bytes in `new Response(bytes, { status, headers })`.
33
+ 4. Export `createLazyClient((config) => createClientCore({ defaultTransport, ...config }))` as `client`. Register the form and stream media types your spec needs with `client.setConfig({ mediaTypes: { 'multipart/form-data': multipartFormData, ...streamMediaTypes } })`, and add `export * from './core.ts'` and `export * from './sse.ts'`. The core has no serializers of its own, so also set `serializer: { query: serializeQuery, path: serializePath }` from `serializers/query.ts` and `serializers/path.ts`, or their styled versions, and `header` and `cookie` for a spec with such parameters.
34
+
35
+ | Export | Purpose | On fetch | On axios |
36
+ | ---------------- | ------------------------------------------------------- | ------------- | -------------------- |
37
+ | `NativeRequest` | The request object on a result and a `ResponseError`. | `Request` | `AxiosRequestConfig` |
38
+ | `NativeResponse` | The response object on a result and a `ResponseError`. | `Response` | `AxiosResponse` |
39
+ | `NativeOptions` | The `options` a call passes to your HTTP library as is. | `RequestInit` | `AxiosRequestConfig` |
40
+
41
+ [`@kubb/plugin-fetch/templates/client.ts`](https://github.com/kubb-labs/plugins/blob/main/packages/plugin-fetch/templates/client.ts) is a complete example.
42
+
24
43
  ## What a generated function looks like
25
44
 
26
45
  ```typescript
@@ -31,27 +50,100 @@ import { client } from '../../../client'
31
50
  export function getPetById<ThrowOnError extends boolean = true>(
32
51
  options: Options<GetPetByIdOptions, ThrowOnError>,
33
52
  ): Promise<RequestResult<GetPetByIdResponses, ThrowOnError>> {
34
- const { client: request = client, ...config } = options
53
+ const { client: apiClient = client, ...config } = options
35
54
 
36
- return request({
55
+ return apiClient<GetPetByIdResponses, ThrowOnError>({
37
56
  method: 'GET',
38
57
  url: '/pet/{petId}',
39
58
  security: [{ type: 'apiKey', name: 'api_key', in: 'header' }, { type: 'oauth2' }],
40
59
  ...config,
41
60
  throwOnError: config.throwOnError ?? true,
42
- }) as Promise<RequestResult<GetPetByIdResponses, ThrowOnError>>
61
+ })
62
+ }
63
+ ```
64
+
65
+ Your `client` receives `method`, the `url` template with `{param}` placeholders, `security`, the grouped `path`, `query`, `headers`, `cookies`, and `body`, and `throwOnError`. It returns a promise. `cookies` holds name and value pairs, which your client joins into a `Cookie` header, as the example below does. A `security` entry is one scheme, or an array of schemes to send together.
66
+
67
+ Read the body by the response `Content-Type`. An operation with only binary success content also passes `parseAs: 'blob'`.
68
+
69
+ A call can also pass `contentType`: `request` is the body's media type, and `response` is the media type to ask for in `Accept`. A streamed operation always passes its media type, such as `contentType: { response: 'text/event-stream' }` or `contentType: { response: 'application/jsonl' }`. A call whose response offers `text/event-stream` next to another content type passes `text/event-stream` too, unless the caller picks another one. `RequestConfig` needs the field.
70
+
71
+ ## Serialize parameters
72
+
73
+ When the spec sets `style`, `explode`, `allowReserved`, or `contentType` on a parameter, the generated call also passes `styles`, grouped by location and keyed by the parameter name:
74
+
75
+ ```typescript
76
+ styles: {
77
+ path: { petId: { style: 'matrix', explode: true } },
78
+ query: { tags: { style: 'pipeDelimited', explode: false } },
43
79
  }
44
80
  ```
45
81
 
46
- Your `client` receives `method`, the `url` template with `{param}` placeholders, `security`, the grouped `path`, `query`, `headers`, and `body`, and `throwOnError`. It returns a promise.
82
+ A parameter without an entry uses the default for its location. The shared runtime's `serializers.ts` applies these styles. A client you write from scratch declares `styles` on `RequestConfig` and applies the styles itself, or ignores them when the spec only uses the defaults.
83
+
84
+ ### Query
85
+
86
+ Query parameters default to the `form` style. Arrays explode into repeated keys unless the spec
87
+ says otherwise, and `spaceDelimited`, `pipeDelimited`, and `deepObject` change how arrays and
88
+ objects collapse.
89
+
90
+ | Style | `explode` | Input | Result |
91
+ | ---------------- | --------- | ------------------- | ---------------- |
92
+ | `form` (default) | `true` | `{ id: [3, 4, 5] }` | `id=3&id=4&id=5` |
93
+ | `form` | `false` | `{ id: [3, 4, 5] }` | `id=3,4,5` |
94
+ | `spaceDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3%204%205` |
95
+ | `pipeDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3\|4\|5` |
96
+ | `deepObject` | `n/a` | `{ a: { b: 1 } }` | `a%5Bb%5D=1` |
97
+
98
+ With `explode: true`, `spaceDelimited` and `pipeDelimited` fall back to repeated keys like `form`,
99
+ so the delimiter only shows with `explode: false`.
100
+
101
+ ### Path
47
102
 
48
- A non-JSON response also passes `responseType`. It is a string such as `'blob'` when every response content type uses one parser, or a map from content type to parser when they differ, for example `{ 'audio/mpeg': 'blob', 'text/event-stream': 'stream' }`. Read the response `Content-Type`, look it up in the map (exact type, then `type/*`, then `*/*`), and parse the body with that parser.
103
+ Path parameters default to the `simple` style, which emits the bare value. `label` prefixes a
104
+ `.` and `matrix` prefixes a `;name=` segment. The results below are the serialized segment for a
105
+ parameter named `id`.
106
+
107
+ | Style | `explode` | Input | Result |
108
+ | ------------------ | --------- | ---------------- | ----------------- |
109
+ | `simple` (default) | `false` | `[3, 4, 5]` | `3,4,5` |
110
+ | `label` | `true` | `[3, 4, 5]` | `.3.4.5` |
111
+ | `matrix` | `true` | `[3, 4, 5]` | `;id=3;id=4;id=5` |
112
+ | `simple` | `false` | `{ x: 1, y: 2 }` | `x,1,y,2` |
113
+
114
+ ### Header and cookie
115
+
116
+ Header parameters use the `simple` style and cookie parameters use the `form` style, so the metadata
117
+ for these locations carries only `explode`, plus `style: 'cookie'` for a cookie parameter with the
118
+ OpenAPI 3.2 `cookie` style. Header values are sent as-is. Cookie values are URL-encoded into a single
119
+ `Cookie` header. The `cookie` style sends them unencoded, as RFC 6265 describes, defaults `explode` to
120
+ `true`, and throws on a value with a character such as `;` or a space, or on an exploded object key that is not a valid
121
+ cookie name, such as `a=b`.
122
+
123
+ | Location | `explode` | Input | Result |
124
+ | ---------------------- | --------- | --------------------------------- | ------------------------- |
125
+ | header | `false` | `[3, 4]` | `X-Ids: 3,4` |
126
+ | header | `true` | `{ role: 'admin' }` | `X-Filter: role=admin` |
127
+ | cookie | `false` | `{ session: 'abc', ids: [1, 2] }` | `session=abc; ids=1,2` |
128
+ | cookie | `true` | `{ ids: [1, 2] }` | `ids=1; ids=2` |
129
+ | cookie, `cookie` style | default | `{ sid: 'a%20b', ids: [1, 2] }` | `sid=a%20b; ids=1; ids=2` |
49
130
 
50
131
  ## A minimal client
51
132
 
52
133
  The client below uses `fetch`. Replace the transport with the HTTP library you use.
53
134
 
54
135
  ```typescript [src/client.ts]
136
+ export type Auth = {
137
+ type: string
138
+ scheme?: string
139
+ name?: string
140
+ in?: string
141
+ scopes?: Array<string>
142
+ flows?: Record<string, Record<string, string>>
143
+ metadataUrl?: string
144
+ url?: string
145
+ }
146
+
55
147
  export type RequestConfig = {
56
148
  method: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
57
149
  url: string
@@ -59,15 +151,18 @@ export type RequestConfig = {
59
151
  headers?: object
60
152
  path?: object
61
153
  query?: object
154
+ cookies?: object
62
155
  body?: unknown
63
156
  signal?: AbortSignal
64
- responseType?: string | Record<string, string>
157
+ parseAs?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'
158
+ contentType?: string | { request?: string; response?: string }
65
159
  throwOnError?: boolean
66
- security?: Array<{ type: string; name?: string; in?: string; scopes?: Array<string> }>
160
+ security?: Array<Auth | Array<Auth>>
161
+ styles?: object
67
162
  client?: typeof client
68
163
  }
69
164
 
70
- type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
165
+ type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
71
166
 
72
167
  export type Options<TData extends DataShape, ThrowOnError extends boolean = true> = Omit<RequestConfig, keyof DataShape | 'url' | 'method'> &
73
168
  TData & { throwOnError?: ThrowOnError }
@@ -82,13 +177,20 @@ export type ResponseErrorConfig<TError = unknown> = Error & { body?: TError }
82
177
  // Set this to your API host. `new URL()` needs an absolute URL.
83
178
  export const settings = { baseURL: 'https://petstore3.swagger.io/api/v3' }
84
179
 
85
- export async function client(config: RequestConfig) {
180
+ async function send(config: RequestConfig) {
86
181
  const path: Record<string, unknown> = { ...config.path }
87
182
  const url = new URL((config.baseURL ?? settings.baseURL) + config.url.replace(/\{(\w+)\}/g, (_, key) => encodeURIComponent(String(path[key]))))
183
+ const cookie = Object.entries(config.cookies ?? {})
184
+ .filter(([, value]) => value != null)
185
+ .map(([name, value]) => `${name}=${encodeURIComponent(String(value))}`)
186
+ .join('; ')
88
187
 
89
188
  const response = await fetch(url, {
90
189
  method: config.method,
91
- headers: config.headers as Record<string, string>,
190
+ headers: {
191
+ ...(config.headers as Record<string, string>),
192
+ ...(cookie ? { Cookie: cookie } : {}),
193
+ },
92
194
  body: config.body === undefined ? undefined : JSON.stringify(config.body),
93
195
  signal: config.signal,
94
196
  })
@@ -98,6 +200,10 @@ export async function client(config: RequestConfig) {
98
200
  if (config.throwOnError ?? true) throw new Error(`Request failed with status ${response.status}`)
99
201
  return { data: undefined, error: body ?? { status: response.status }, response }
100
202
  }
203
+
204
+ // Generated functions pass their responses type and `ThrowOnError`.
205
+ export const client = <TResponses, ThrowOnError extends boolean = true>(config: RequestConfig) =>
206
+ send(config) as Promise<RequestResult<TResponses, ThrowOnError>>
101
207
  ```
102
208
 
103
209
  The [example project](https://github.com/kubb-labs/plugins/tree/main/examples/client) has a fuller version with a base URL setter, query handling, and an async auth hook. The types in this sketch are minimal. Add status-aware response types when you need them.
package/docs/options.md CHANGED
@@ -11,6 +11,7 @@ Pass these options to `pluginClient()`. Shared options link to [Shared plugin op
11
11
  | [`group`](https://kubb.dev/docs/5.x/reference/plugin-options#group) | Split output by tag or path: `type`, `name`. | None |
12
12
  | [`throwOnErrorDefault`](#throwonerrordefault) | Default `throwOnError` value passed to your client. | `true` |
13
13
  | [`validator`](#validator) | Pass Zod schemas to your client. | `false` |
14
+ | [`returnType`](#returntype) | Shape of the value a generated operation resolves to. | `'plain'` |
14
15
  | [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match. | None |
15
16
  | [`exclude`](https://kubb.dev/docs/5.x/reference/plugin-options#exclude) | Skip operations that match. | `[]` |
16
17
  | [`override`](https://kubb.dev/docs/5.x/reference/plugin-options#override) | Apply different options per pattern. | `[]` |
@@ -49,6 +50,29 @@ Default for the `throwOnError` field that every generated function passes to you
49
50
 
50
51
  The setting applies to the whole plugin, so you cannot set it in `override`.
51
52
 
53
+ ### returnType
54
+
55
+ Shape of the value a generated operation resolves to.
56
+
57
+ - `'plain'`: returns what your `client` returns.
58
+ - `'full'`: wraps the call in `withUnwrap`, so the caller can `await` it or call `.unwrap()`. Your module must export `withUnwrap`, `resolveThrowOnError`, and the `Unwrappable` type.
59
+ - `'data'`: passes the call through `unwrapResult` and resolves to the body. Your module must export `unwrapResult`, `resolveThrowOnError`, and the `UnwrappedResult` type.
60
+
61
+ `@kubb/plugin-client/templates/core.ts` exports all of these, so your module can re-export them from there.
62
+
63
+ | | |
64
+ | -------- | ----------------------------- |
65
+ | Type | `'plain' \| 'full' \| 'data'` |
66
+ | Required | `false` |
67
+ | Default | `'plain'` |
68
+
69
+ ```typescript
70
+ pluginClient({ importPath: '../../client', returnType: 'full' })
71
+ // const pet = await getPetById({ path: { petId: 1 } }).unwrap()
72
+ ```
73
+
74
+ The setting applies to the whole plugin, so you cannot set it in `override`.
75
+
52
76
  ### validator
53
77
 
54
78
  Passes Zod schemas from `@kubb/plugin-zod` to your client on `config.validator`. The plugin does not run the schemas. Your client reads `config.validator` and runs them. See [validate requests and responses](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/validate-requests-and-responses).
package/docs/overview.md CHANGED
@@ -6,7 +6,7 @@ Each function takes grouped `path`, `query`, `headers`, and `body` parameters an
6
6
 
7
7
  ## Setup
8
8
 
9
- - Set the required `importPath` option to your client module. The module must export `client` and the `Options` and `RequestResult` types. When the spec has server-sent event operations, it must also export `toEventStream` and the `EventStreamResult`, `SuccessOf`, and `TypedEvent` types.
9
+ - Set the required `importPath` option to your client module. The module must export a generic `client` and the `Options` and `RequestResult` types. When the spec has server-sent event or JSON Lines operations, `client` also needs a `stream` method, and the module exports the `StreamResult`, `SuccessOf`, `TypedEvent`, and `ServerSentEvent` types.
10
10
  - Add `@kubb/plugin-ts` for the operation types, or `@kubb/plugin-zod` with `inferred: true`.
11
11
  - Add `@kubb/plugin-zod` when you set `validator`, and install `zod` v4. Generation fails without the plugin.
12
12
 
@@ -20,7 +20,7 @@
20
20
  "dependencies": ["plugin-ts"],
21
21
  "optionalDependencies": ["plugin-zod"],
22
22
  "compatibility": {
23
- "kubb": ">=5.16.0",
23
+ "kubb": ">=5.18.4",
24
24
  "node": ">=22.22"
25
25
  },
26
26
  "options": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-client",
3
- "version": "5.12.0-canary.20261109T134631",
3
+ "version": "6.0.0",
4
4
  "description": "Generate typed API operations against your own client module.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -61,14 +61,15 @@
61
61
  "registry": "https://registry.npmjs.org/"
62
62
  },
63
63
  "dependencies": {
64
- "@kubb/kit": "^5.16.0",
65
- "@kubb/plugin-ts": "^5.5.0-canary.20261109T134631",
66
- "@kubb/plugin-zod": "^5.9.0-canary.20261109T134631",
67
- "@kubb/renderer-jsx": "^5.16.0"
64
+ "@kubb/kit": "^5.18.6",
65
+ "@kubb/plugin-ts": "^5.5.0",
66
+ "@kubb/plugin-zod": "^5.9.0",
67
+ "@kubb/renderer-jsx": "^5.18.6"
68
68
  },
69
69
  "devDependencies": {
70
- "@internals/client": "0.1.0-canary.20261109T134631",
71
- "@internals/shared": "0.1.0-canary.20261109T134631",
70
+ "@internals/client": "0.0.0",
71
+ "@internals/shared": "0.0.0",
72
+ "@internals/utils": "0.0.0",
72
73
  "typescript": "^7.0.2"
73
74
  },
74
75
  "peerDependencies": {},
@@ -0,0 +1,80 @@
1
+ import { toLiteral } from '@internals/utils'
2
+ import { getEventStream, hasTextEventData } from '@internals/shared'
3
+
4
+ import { ast, text } from '@kubb/kit'
5
+ import type { ReturnTypeOption } from '../types.ts'
6
+ import { buildContentType } from './contentType.ts'
7
+ import { buildSecurityMetadata, type SecurityRequirement } from './security.ts'
8
+ import { buildStyles } from './styles.ts'
9
+ import { buildValidatorLiteral, type ValidatorHooks } from './validator.ts'
10
+
11
+ /**
12
+ * `parseAs: 'blob'` when every success content type is binary, so a download is a `Blob` whatever its media type, and
13
+ * also when the server sends no `Content-Type`. Any other body is decoded by its media type at runtime.
14
+ */
15
+ function buildParseAsLiteral(content: Array<ast.ContentNode>): string | null {
16
+ return content.length > 0 && content.every((entry) => entry.schema?.type === 'blob') ? `parseAs: 'blob'` : null
17
+ }
18
+
19
+ /**
20
+ * Builds the call config literal a generated operation forwards to the client, shared by the standalone
21
+ * functions and the SDK methods. The operation's own `baseURL` comes before the `...config` spread, so a per-call one wins. The
22
+ * `validator` and `throwOnError` entries come after it, so a per-call `validator` is merged per key with the generated one instead
23
+ * of replacing it. `styles` is merged the same way, per parameter, unless `returnType` is `'plain'`.
24
+ *
25
+ * `mergeContentType` is `true` when the caller must destructure `contentType` from `options`, and
26
+ * `eventStream` is `true` when the operation streams `text/event-stream` or JSON Lines. A Server-Sent Events call
27
+ * carries its `eventShape`, which tells the runtime what the `response` validator checks on each event, and `rawData` when
28
+ * the document types each event's `data` as a string, so the runtime keeps it as text.
29
+ */
30
+ export function buildCallConfig({
31
+ node,
32
+ validatorHooks,
33
+ security,
34
+ baseURL,
35
+ throwOnErrorDefault,
36
+ returnType,
37
+ }: {
38
+ node: ast.HttpOperationNode
39
+ validatorHooks: ValidatorHooks | null
40
+ security?: Array<SecurityRequirement>
41
+ baseURL?: string
42
+ throwOnErrorDefault: boolean
43
+ returnType: ReturnTypeOption
44
+ }): { callConfig: string; mergeContentType: boolean; eventStream: boolean } {
45
+ const stream = getEventStream(node)
46
+ const content = ast.http.responses.findPrimary(node)?.content ?? []
47
+ const securityLiteral = buildSecurityMetadata({ security })
48
+ // A `'plain'` client is the user's own, which may type `styles` as `unknown`, so it keeps the generated styles as a default.
49
+ const ownRuntime = returnType !== 'plain'
50
+ const stylesLiteral = buildStyles({ node, merge: ownRuntime })
51
+ const styles = stylesLiteral ? `styles: ${stylesLiteral}` : null
52
+ const validatorLiteral = buildValidatorLiteral(validatorHooks, ['request', 'path', 'query', 'headers', 'cookies', 'response', 'error'])
53
+ const { literal: contentTypeLiteral, merge: mergeContentType } = buildContentType({ node })
54
+ // The runtime's call types `throwOnError` as the operation's `ThrowOnError`, which the shared helper keeps. A `'plain'`
55
+ // client takes a boolean.
56
+ const throwOnError = ownRuntime
57
+ ? `throwOnError: resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault})`
58
+ : `throwOnError: config.throwOnError ?? ${throwOnErrorDefault}`
59
+
60
+ const rawData = hasTextEventData(node)
61
+ const callConfig = `{ ${[
62
+ `method: ${text.quote(node.method.toUpperCase())}`,
63
+ `url: ${text.quote(node.path)}`,
64
+ baseURL ? `baseURL: ${JSON.stringify(baseURL)}` : null,
65
+ securityLiteral ? `security: ${securityLiteral}` : null,
66
+ ownRuntime ? null : styles,
67
+ contentTypeLiteral,
68
+ buildParseAsLiteral(content),
69
+ stream === 'event' || stream === 'data' ? `eventShape: '${stream}'` : null,
70
+ rawData ? `rawData: ${toLiteral(rawData)}` : null,
71
+ '...config',
72
+ ownRuntime ? styles : null,
73
+ validatorLiteral,
74
+ throwOnError,
75
+ ]
76
+ .filter(Boolean)
77
+ .join(', ')} }`
78
+
79
+ return { callConfig, mergeContentType, eventStream: stream !== null }
80
+ }
@@ -0,0 +1,35 @@
1
+ import { getContentTypeInfo, getResponseContentTypeInfo, hasEventStreamContent, isEventStream } from '@internals/shared'
2
+ import { ast, text } from '@kubb/kit'
3
+
4
+ /**
5
+ * Builds the `contentType` entry baked into the call config. It holds the request default when the operation has a body
6
+ * and `response` set to the streamed media type when it streams items, or `text/event-stream` when it offers events next to
7
+ * another type, and is `null` with neither.
8
+ *
9
+ * `merge` is `true` when the generated function must destructure `contentType` from `options` and merge it over the
10
+ * baked defaults, because the caller can also pick a response content type or a stream must keep its `Accept`.
11
+ */
12
+ export function buildContentType({ node }: { node: ast.HttpOperationNode }): { literal: string | null; merge: boolean } {
13
+ const { defaultContentType } = getContentTypeInfo(node)
14
+ const hasRequestBody = Boolean(ast.http.requestBody.getSchema(node))
15
+ // The first declared type is the default; the caller overrides it on `contentType`.
16
+ const requestContentType = hasRequestBody ? defaultContentType : null
17
+ // A stream asks for its own media type, and a response that offers events next to JSON asks for the events, so a client whose
18
+ // default `Accept` prefers JSON, such as axios, gets the stream. The caller picks another type with `contentType.response`.
19
+ const streamType = isEventStream(node) ? ast.mediaType.getBase(getResponseContentTypeInfo(node).defaultContentType) : null
20
+ const response = streamType || hasEventStreamContent(node) ? `response: ${text.quote(streamType ?? 'text/event-stream')}` : null
21
+
22
+ if (!requestContentType) return { literal: response && `contentType: { ${response} }`, merge: false }
23
+
24
+ // When the caller can also pick a response content type, or a stream must keep its `Accept`, a partial `contentType` would
25
+ // replace the baked default through `...config`, so merge the caller's choice over it instead.
26
+ const merge = Boolean(response) || getResponseContentTypeInfo(node).isMultipleContentTypes
27
+ const defaults = [`request: ${text.quote(requestContentType)}`, response].filter(Boolean).join(', ')
28
+
29
+ return {
30
+ merge,
31
+ literal: merge
32
+ ? `contentType: { ${defaults}, ...(typeof contentType === 'string' ? { request: contentType } : contentType) }`
33
+ : `contentType: { ${defaults} }`,
34
+ }
35
+ }
@@ -0,0 +1,29 @@
1
+ import type { ast } from '@kubb/kit'
2
+ import { buildResponseHeadersType, buildResultTypeRef, type OperationTypeNames } from '@internals/client'
3
+ import type { ReturnTypeOption } from '../types.ts'
4
+
5
+ /**
6
+ * Builds the `RequestResult` type arguments of one operation: responses, `ThrowOnError` and the response headers, when a response declares any.
7
+ *
8
+ * @example
9
+ * `buildRequestResultGenerics({ node, types }) // "LoginUserResponses, ThrowOnError, { '200': LoginUserStatus200Headers }"`
10
+ */
11
+ export function buildRequestResultGenerics({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string {
12
+ return [types.response.responses(node), 'ThrowOnError', buildResponseHeadersType({ node, types })].filter(Boolean).join(', ')
13
+ }
14
+
15
+ /**
16
+ * Builds the full return type an operation's function signature uses: `Unwrappable<RequestResult>`
17
+ * for the default `returnType: 'full'`, already a promise so no further wrapping is needed, a plain
18
+ * `Promise<RequestResult>` for the internal `'plain'` shape, or a
19
+ * `Promise` of the runtime's `UnwrappedResult` when `returnType: 'data'` narrows a resolved call
20
+ * down to the bare success body.
21
+ *
22
+ * @example
23
+ * `buildResultType({ node, types, returnType: 'data' }) // 'Promise<UnwrappedResult<AddPetResponses, ThrowOnError>>'`
24
+ */
25
+ export function buildResultType({ node, types, returnType }: { node: ast.OperationNode; types: OperationTypeNames; returnType: ReturnTypeOption }): string {
26
+ const name = returnType === 'data' ? 'UnwrappedResult' : 'RequestResult'
27
+ const result = buildResultTypeRef(name, types.response.responses(node), buildResponseHeadersType({ node, types }), ['ThrowOnError'])
28
+ return returnType === 'full' ? `Unwrappable<${result}>` : `Promise<${result}>`
29
+ }