@kubb/plugin-fetch 5.13.0-canary.20261105T105941 → 5.13.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.
- package/dist/client-Dn80AjdR.d.ts +39 -0
- package/dist/core-D6FAGTsL.d.ts +2 -0
- package/dist/index.cjs +384 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +385 -227
- package/dist/index.js.map +1 -1
- package/dist/templates/client.d.ts +3 -0
- package/dist/templates/core.d.ts +2 -0
- package/dist/templates/fetch.d.ts +5 -405
- package/dist/templates/serializers.d.ts +1 -179
- package/dist/templates/standardSchema.d.ts +1 -63
- package/docs/guides/server-sent-events.md +281 -45
- package/kubb-extension.json +1 -1
- package/package.json +8 -7
- package/src/generators/clientGenerator.tsx +2 -1
- package/src/plugin.ts +9 -5
- package/src/templates.ts +10 -7
- package/templates/client.ts +111 -0
- package/templates/core.ts +2 -0
- package/templates/fetch.ts +2 -913
- package/templates/serializers.ts +2 -481
- package/templates/standardSchema.ts +2 -109
|
@@ -1,94 +1,330 @@
|
|
|
1
1
|
# Server-sent events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An operation that returns `text/event-stream` gives you a typed stream of events instead of a `RequestResult`. Kubb takes the event types from the OpenAPI document. The runtime in `.kubb/sse.ts` parses the stream the way the WHATWG event stream specification does.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Each event on the wire has up to four fields, and a blank line ends it:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
| Field | Meaning |
|
|
8
|
+
| ------- | -------------------------------------------------------------------------------- |
|
|
9
|
+
| `event` | The event name. `message` when the server sends none. |
|
|
10
|
+
| `data` | The payload. Parsed as JSON unless the document types it as a string. |
|
|
11
|
+
| `id` | The last event ID. It carries over to later events and resumes a dropped stream. |
|
|
12
|
+
| `retry` | The reconnect delay in milliseconds. |
|
|
13
|
+
|
|
14
|
+
## Pick a shape
|
|
15
|
+
|
|
16
|
+
Kubb reads the stream's item schema in one of two shapes, and the item's properties decide which:
|
|
17
|
+
|
|
18
|
+
::field-group
|
|
19
|
+
|
|
20
|
+
:::field{name="event"}
|
|
21
|
+
For several named events, such as `message_delta` and `ping`. The item declares `event` or `data`, and the stream yields `TypedEvent<Item>`. See [Typed events](#typed-events).
|
|
22
|
+
:::
|
|
23
|
+
|
|
24
|
+
:::field{name="data"}
|
|
25
|
+
For one payload on every event, such as a feed of pet changes. The item declares neither, and the stream yields `ServerSentEvent<Item>`. See [Payload only](#payload-only).
|
|
26
|
+
:::
|
|
27
|
+
|
|
28
|
+
::
|
|
29
|
+
|
|
30
|
+
`examples/advanced` shows both shapes: `streamOrderEvents` is typed by event, and `streamPetEvents` is typed by payload.
|
|
31
|
+
|
|
32
|
+
## Typed events
|
|
33
|
+
|
|
34
|
+
### Describe it
|
|
35
|
+
|
|
36
|
+
In OpenAPI 3.2, put one `oneOf` member per event name on `itemSchema`, with a `const` on `event`. Describe JSON inside `data` with `contentMediaType` and `contentSchema`:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
/chat:
|
|
40
|
+
post:
|
|
41
|
+
operationId: streamChat
|
|
42
|
+
responses:
|
|
43
|
+
'200':
|
|
44
|
+
description: One event per oneOf member
|
|
45
|
+
content:
|
|
46
|
+
text/event-stream:
|
|
47
|
+
itemSchema:
|
|
48
|
+
type: object
|
|
49
|
+
required: [event]
|
|
50
|
+
properties:
|
|
51
|
+
data: { type: string }
|
|
52
|
+
event: { type: string }
|
|
53
|
+
id: { type: string }
|
|
54
|
+
retry: { type: integer, minimum: 0 }
|
|
55
|
+
oneOf:
|
|
56
|
+
- properties:
|
|
57
|
+
event: { const: message_delta }
|
|
58
|
+
data:
|
|
59
|
+
contentMediaType: application/json
|
|
60
|
+
contentSchema: { $ref: '#/components/schemas/MessageDelta' }
|
|
61
|
+
- properties:
|
|
62
|
+
event: { const: ping }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In OpenAPI 3.0 and 3.1, put the same `oneOf` on `schema`, with a `discriminator` on `event`. Kubb reads `schema` as one event.
|
|
66
|
+
|
|
67
|
+
### The stream
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
event: message_delta
|
|
71
|
+
id: 1
|
|
72
|
+
data: {"text": "Hel"}
|
|
73
|
+
|
|
74
|
+
event: ping
|
|
75
|
+
data: keep-alive
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This stream yields two events. The `id` carries over to the second one:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
{ event: 'message_delta', id: '1', data: { text: 'Hel' } }
|
|
83
|
+
{ event: 'ping', id: '1', data: 'keep-alive' }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Use it
|
|
87
|
+
|
|
88
|
+
Kubb generates one union member per event name:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
export type StreamChatStatus200 =
|
|
92
|
+
| { data?: MessageDelta; event: 'message_delta'; id?: string; retry?: number }
|
|
93
|
+
| { data?: string; event: 'ping'; id?: string; retry?: number }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A `switch` on `event` narrows `data` in each `case`:
|
|
8
97
|
|
|
9
98
|
```typescript
|
|
10
|
-
import {
|
|
99
|
+
import { streamChat } from './gen/clients/streamChat'
|
|
11
100
|
|
|
12
|
-
const { stream } = await
|
|
101
|
+
const { stream } = await streamChat({ body: { prompt: 'Hi' } })
|
|
13
102
|
|
|
14
103
|
for await (const event of stream) {
|
|
15
|
-
|
|
104
|
+
switch (event.event) {
|
|
105
|
+
case 'message_delta':
|
|
106
|
+
render(event.data?.text) // MessageDelta
|
|
107
|
+
break
|
|
108
|
+
case 'ping':
|
|
109
|
+
break
|
|
110
|
+
}
|
|
16
111
|
}
|
|
17
112
|
```
|
|
18
113
|
|
|
19
|
-
`
|
|
20
|
-
|
|
114
|
+
The stream yields every event the server sends. An event name the document does not declare reaches the `default` of your `switch`, and with `validator: 'zod'` the event schema rejects it, as it would any other response that does not match the document.
|
|
115
|
+
|
|
116
|
+
## Payload only
|
|
117
|
+
|
|
118
|
+
### Describe it
|
|
119
|
+
|
|
120
|
+
Point `itemSchema` at the payload. Keep `schema: { type: string }` next to it, so tools that read OpenAPI 3.0 or 3.1 still accept the document:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
/pet/{petId}/events:
|
|
124
|
+
get:
|
|
125
|
+
operationId: streamPetEvents
|
|
126
|
+
responses:
|
|
127
|
+
'200':
|
|
128
|
+
description: Server-Sent Events stream of pet changes
|
|
129
|
+
content:
|
|
130
|
+
text/event-stream:
|
|
131
|
+
schema:
|
|
132
|
+
type: string
|
|
133
|
+
itemSchema:
|
|
134
|
+
$ref: '#/components/schemas/PetEvent'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
In OpenAPI 3.0 and 3.1, a `schema` without `event` or `data` properties has the same shape.
|
|
138
|
+
|
|
139
|
+
### The stream
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
data: {"type": "updated", "pet": {"id": 1, "name": "Rex"}}
|
|
143
|
+
|
|
144
|
+
data: {"type": "deleted", "pet": {"id": 1, "name": "Rex"}}
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This stream yields two events. The server sends no `event`, so each one is named `message`:
|
|
21
149
|
|
|
22
150
|
```typescript
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
151
|
+
{ event: 'message', data: { type: 'updated', pet: { id: 1, name: 'Rex' } } }
|
|
152
|
+
{ event: 'message', data: { type: 'deleted', pet: { id: 1, name: 'Rex' } } }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Use it
|
|
156
|
+
|
|
157
|
+
Each event is a `ServerSentEvent<PetEvent>`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
type ServerSentEvent<TData> = { data: TData; event: string; id?: string; retry?: number }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Read `data` straight from each event. The result also holds the native `response` of the first connection:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { streamPetEvents } from './gen/clients/streamPetEvents'
|
|
167
|
+
|
|
168
|
+
const { stream, response } = await streamPetEvents({ path: { petId: 1 } })
|
|
169
|
+
console.info(response.status) // 200
|
|
170
|
+
|
|
171
|
+
for await (const { data } of stream) {
|
|
172
|
+
console.info(data.type, data.pet.name) // PetEvent
|
|
28
173
|
}
|
|
29
174
|
```
|
|
30
175
|
|
|
31
|
-
|
|
32
|
-
|
|
176
|
+
## Stream options
|
|
177
|
+
|
|
178
|
+
Pass these on the call, next to its request params:
|
|
33
179
|
|
|
34
|
-
|
|
180
|
+
| Option | What it does |
|
|
181
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| `terminator` | Ends the stream when `data` equals this value, such as `[DONE]`. Without it, the sentinel reaches your loop. |
|
|
183
|
+
| `transform` | Maps each event before validation, for example to camelCase its keys. |
|
|
184
|
+
| `maxEventSize` | Largest event in characters. A larger one throws a `RangeError`. Defaults to `16 * 1024 * 1024`. Set `Infinity` to turn it off. |
|
|
185
|
+
| `signal` | An `AbortSignal` that ends the request and the loop. |
|
|
35
186
|
|
|
36
|
-
|
|
37
|
-
|
|
187
|
+
To stop reading, `break` out of the loop. The runtime then cancels the reader and releases the connection.
|
|
188
|
+
|
|
189
|
+
## Validate events
|
|
190
|
+
|
|
191
|
+
With `validator: 'zod'` in the plugin options, the validator runs on each event: the whole event for typed events, `data` for a payload. A failing event calls `onValidationError`. Return `{ value }` to keep going, or the loop throws a `ParseError`:
|
|
38
192
|
|
|
39
193
|
```typescript
|
|
40
|
-
const { stream } = await
|
|
194
|
+
const { stream } = await streamChat({
|
|
195
|
+
body,
|
|
196
|
+
onValidationError: (error, { value }) => {
|
|
197
|
+
console.warn(error.issues)
|
|
198
|
+
return { value }
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
```
|
|
41
202
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
203
|
+
Set `validate: false` on the call or the client to skip it. See [Handle errors](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/error-handling) for the validation options.
|
|
204
|
+
|
|
205
|
+
## Resume after a drop
|
|
206
|
+
|
|
207
|
+
A dropped connection ends the loop with the transport's error. The result's `lastEventId` is the `id` of the last event the server completed, so a new call with a `Last-Event-ID` header continues where the stream stopped. The runtime does not reconnect by itself, because many streams, such as a chat completion, end on purpose, and the retry policy belongs to your app:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
let lastEventId = ''
|
|
211
|
+
|
|
212
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
213
|
+
const result = await streamChat({ body, headers: lastEventId ? { 'Last-Event-ID': lastEventId } : {} })
|
|
214
|
+
try {
|
|
215
|
+
for await (const event of result.stream) render(event)
|
|
216
|
+
break
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (ResponseError.is(error)) throw error
|
|
219
|
+
lastEventId = result.lastEventId || lastEventId
|
|
220
|
+
}
|
|
45
221
|
}
|
|
46
222
|
```
|
|
47
223
|
|
|
48
|
-
|
|
49
|
-
|
|
224
|
+
`lastEventId` is empty until the server sends an `id`, and an empty `id:` field clears it. An `id` that contains a NULL character is ignored. Each event carries the server's `retry` hint as `retry`.
|
|
225
|
+
|
|
226
|
+
## Handle errors
|
|
227
|
+
|
|
228
|
+
A non-2xx response throws a `ResponseError`, from the call or from the loop:
|
|
50
229
|
|
|
51
230
|
```typescript
|
|
52
|
-
|
|
53
|
-
const { stream } = await streamEvents({ signal: controller.signal })
|
|
231
|
+
import { ResponseError } from './gen/.kubb/client'
|
|
54
232
|
|
|
55
|
-
|
|
56
|
-
|
|
233
|
+
try {
|
|
234
|
+
const { stream } = await streamChat({ body })
|
|
235
|
+
for await (const event of stream) render(event)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (!ResponseError.is(error)) throw error
|
|
238
|
+
console.error('stream rejected', error.status)
|
|
239
|
+
}
|
|
57
240
|
```
|
|
58
241
|
|
|
59
|
-
|
|
242
|
+
With `throwOnError: false`, the call resolves instead of throwing, as a regular call does. The result then carries the `status` and the `error` body, its `data` is `undefined`, and its stream yields nothing. Check `data` rather than `error`, because an empty error body leaves `error` undefined. Checking `status` narrows `error` to the body the document declares for that status:
|
|
60
243
|
|
|
61
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
const result = await streamChat({ body, throwOnError: false })
|
|
246
|
+
if (!result.data) console.error('stream rejected', result.status, result.error)
|
|
247
|
+
else for await (const event of result.data) render(event)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
As on a regular call, `.unwrap()` resolves to the events. A failed call rejects with a `ResponseError` by default, and with the `error` body when the call sets `throwOnError: false`:
|
|
62
251
|
|
|
63
252
|
```typescript
|
|
64
|
-
|
|
253
|
+
for await (const event of await streamChat({ body }).unwrap()) render(event)
|
|
254
|
+
```
|
|
65
255
|
|
|
66
|
-
|
|
67
|
-
console.info(response.headers.get('x-stream-id'))
|
|
256
|
+
A stream call that gets a JSON response, such as an error body, parses it like any other call, so a 2xx JSON body yields an empty stream. An operation that declares another content type next to `text/event-stream` is generated as a normal call.
|
|
68
257
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
258
|
+
Declare heartbeat and `error` events in the document, so each has a `case` in your `switch`.
|
|
259
|
+
|
|
260
|
+
## Return only the events
|
|
261
|
+
|
|
262
|
+
With `returnType: 'data'` in the plugin options, a stream call returns its events, as a regular call returns only its body:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { pluginFetch } from '@kubb/plugin-fetch'
|
|
266
|
+
|
|
267
|
+
pluginFetch({ returnType: 'data' })
|
|
72
268
|
```
|
|
73
269
|
|
|
74
|
-
|
|
270
|
+
Both modes read the same events. The table compares them for one call:
|
|
271
|
+
|
|
272
|
+
| | `returnType: 'full'` (default) | `returnType: 'data'` |
|
|
273
|
+
| ----------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
274
|
+
| The call returns | `Unwrappable<StreamResult<…>>` | `Promise<UnwrappedStreamResult<…>>` |
|
|
275
|
+
| Call it | `const result = await streamPetEvents({ path: { petId: 1 } })` | `const events = await streamPetEvents({ path: { petId: 1 } })` |
|
|
276
|
+
| Loop over the events | `for await (const event of result.stream)` | `for await (const event of events)` |
|
|
277
|
+
| Other ways to loop | `result`, `result.data`, or `.unwrap()` | none |
|
|
278
|
+
| `event.data` | `PetEvent` | `PetEvent` |
|
|
279
|
+
| A failed call, by default | throws a `ResponseError` | throws a `ResponseError` |
|
|
280
|
+
| With `throwOnError: false` | the full result | the full result, as in `'full'` |
|
|
281
|
+
| `status`, `response`, `lastEventId` | available | only with `throwOnError: false` |
|
|
282
|
+
|
|
283
|
+
Loop over the events the call resolves to:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { streamOrderEvents } from './gen/clients/streamOrderEvents'
|
|
287
|
+
import { streamPetEvents } from './gen/clients/streamPetEvents'
|
|
288
|
+
|
|
289
|
+
const events = await streamOrderEvents({ path: { orderId: 7 } })
|
|
290
|
+
|
|
291
|
+
for await (const event of events) {
|
|
292
|
+
if (event.event === 'order_placed') render(event.data) // Order
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for await (const { data } of await streamPetEvents({ path: { petId: 1 } })) {
|
|
296
|
+
console.info(data.type) // PetEvent
|
|
297
|
+
}
|
|
298
|
+
```
|
|
75
299
|
|
|
76
|
-
|
|
300
|
+
A failed call throws a `ResponseError` before the first event, and a dropped connection throws from the loop:
|
|
77
301
|
|
|
78
302
|
```typescript
|
|
79
303
|
import { ResponseError } from './gen/.kubb/client'
|
|
80
304
|
|
|
81
305
|
try {
|
|
82
|
-
|
|
83
|
-
for await (const event of stream) {
|
|
84
|
-
handle(event.data)
|
|
85
|
-
}
|
|
306
|
+
for await (const event of await streamPetEvents({ path: { petId: 1 } })) render(event)
|
|
86
307
|
} catch (error) {
|
|
87
308
|
if (!ResponseError.is(error)) throw error
|
|
88
309
|
console.error('stream rejected', error.status)
|
|
89
310
|
}
|
|
90
311
|
```
|
|
91
312
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
313
|
+
With `throwOnError: false`, the call returns the full result instead, as a regular call does in this mode. The events are its `data` on a 2xx response, and checking `status` narrows `error`:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const result = await streamPetEvents({ path: { petId: 1 }, throwOnError: false })
|
|
317
|
+
|
|
318
|
+
if (!result.data) console.error('stream rejected', result.status, result.error)
|
|
319
|
+
else for await (const event of result.data) render(event)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The events alone carry no `response` or `lastEventId`. To resume a dropped stream, use the default `returnType: 'full'`, or pass `throwOnError: false` to get the full result.
|
|
323
|
+
|
|
324
|
+
## The runtime files
|
|
325
|
+
|
|
326
|
+
`.kubb/sse.ts` holds the parser, `toEventStream`, and the `ServerSentEvent`, `TypedEvent`, `EventStreamOptions`, and `EventStreamResult` types. A stream operation takes `StreamOptions` and returns `StreamResult`, the stream versions of `Options` and `RequestResult`. Kubb writes it only when the document has a `text/event-stream` operation, and `.kubb/client.ts` then re-exports it, so the types import from either file. Kubb regenerates both on each run, so do not edit them:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import type { ServerSentEvent } from './gen/.kubb/client'
|
|
330
|
+
```
|
package/kubb-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-fetch",
|
|
3
|
-
"version": "5.13.0
|
|
3
|
+
"version": "5.13.0",
|
|
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,14 +80,15 @@
|
|
|
80
80
|
"registry": "https://registry.npmjs.org/"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@kubb/kit": "^5.
|
|
84
|
-
"@kubb/plugin-
|
|
85
|
-
"@kubb/plugin-
|
|
86
|
-
"@kubb/
|
|
83
|
+
"@kubb/kit": "^5.16.0",
|
|
84
|
+
"@kubb/plugin-client": "^5.10.0",
|
|
85
|
+
"@kubb/plugin-ts": "^5.4.1",
|
|
86
|
+
"@kubb/plugin-zod": "^5.8.2",
|
|
87
|
+
"@kubb/renderer-jsx": "^5.16.0"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
|
-
"@internals/client": "0.
|
|
90
|
-
"@internals/shared": "0.
|
|
90
|
+
"@internals/client": "0.0.0",
|
|
91
|
+
"@internals/shared": "0.0.0",
|
|
91
92
|
"typescript": "^7.0.2"
|
|
92
93
|
},
|
|
93
94
|
"peerDependencies": {},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createClientGenerator } from '@internals/client'
|
|
2
|
+
import { sseTemplatePath } from '../templates.ts'
|
|
2
3
|
import type { PluginFetch } from '../types.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -6,4 +7,4 @@ import type { PluginFetch } from '../types.ts'
|
|
|
6
7
|
* operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
|
|
7
8
|
* that forwards a single `options` object to the bundled `client` and returns the `RequestResult`.
|
|
8
9
|
*/
|
|
9
|
-
export const clientGenerator = createClientGenerator<PluginFetch>('fetch')
|
|
10
|
+
export const clientGenerator = createClientGenerator<PluginFetch>('fetch', { sse: sseTemplatePath })
|
package/src/plugin.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { version } from '../package.json'
|
|
|
7
7
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
8
8
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
9
9
|
import { clientGenerator } from './generators/clientGenerator.tsx'
|
|
10
|
-
import { fetchClientTemplatePath,
|
|
10
|
+
import { fetchClientTemplatePath, coreTemplatePath, serializersTemplatePath, sseTemplatePath, standardSchemaTemplatePath } from './templates.ts'
|
|
11
11
|
import type { PluginFetch, ResolvedOptions, ResolverClient } from './types.ts'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -79,22 +79,26 @@ export const pluginFetch = defineExtension<PluginFetch>({
|
|
|
79
79
|
|
|
80
80
|
// `sdk` swaps the per-operation functions for the class-based SDK; left unset, the standalone
|
|
81
81
|
// functions (which query plugins consume) stay.
|
|
82
|
-
addGenerator(resolved.sdk ? createSdkGenerator<PluginFetch>() : clientGenerator)
|
|
82
|
+
addGenerator(resolved.sdk ? createSdkGenerator<PluginFetch>({ sse: sseTemplatePath }) : clientGenerator)
|
|
83
83
|
|
|
84
84
|
const root = path.resolve(config.root, config.output.path)
|
|
85
|
-
const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
|
|
86
85
|
|
|
87
86
|
injectFile({
|
|
88
87
|
baseName: 'serializers.ts',
|
|
89
88
|
path: path.resolve(root, '.kubb/serializers.ts'),
|
|
90
|
-
copy:
|
|
89
|
+
copy: serializersTemplatePath,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
injectFile({
|
|
93
|
+
baseName: 'core.ts',
|
|
94
|
+
path: path.resolve(root, '.kubb/core.ts'),
|
|
95
|
+
copy: coreTemplatePath,
|
|
91
96
|
})
|
|
92
97
|
|
|
93
98
|
injectFile({
|
|
94
99
|
baseName: 'client.ts',
|
|
95
100
|
path: path.resolve(root, '.kubb/client.ts'),
|
|
96
101
|
copy: fetchClientTemplatePath,
|
|
97
|
-
footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
|
|
98
102
|
})
|
|
99
103
|
|
|
100
104
|
injectFile({
|
package/src/templates.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
1
2
|
import { fileURLToPath } from 'node:url'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
export const fetchClientTemplatePath = fileURLToPath(new URL('../templates/fetch.ts', import.meta.url))
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
5
|
|
|
6
|
-
/** Absolute path to the fetch
|
|
7
|
-
export const
|
|
6
|
+
/** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
|
|
7
|
+
export const fetchClientTemplatePath = fileURLToPath(new URL('../templates/client.ts', import.meta.url))
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Absolute
|
|
11
|
-
*
|
|
10
|
+
* Absolute paths to the runtime templates `@kubb/plugin-client` ships for every client plugin, copied into `.kubb/core.ts`,
|
|
11
|
+
* `.kubb/serializers.ts`, `.kubb/standardSchema.ts`, and `.kubb/sse.ts`.
|
|
12
12
|
*/
|
|
13
|
-
export const
|
|
13
|
+
export const coreTemplatePath = require.resolve('@kubb/plugin-client/templates/core.ts')
|
|
14
|
+
export const serializersTemplatePath = require.resolve('@kubb/plugin-client/templates/serializers.ts')
|
|
15
|
+
export const standardSchemaTemplatePath = require.resolve('@kubb/plugin-client/templates/standardSchema.ts')
|
|
16
|
+
export const sseTemplatePath = require.resolve('@kubb/plugin-client/templates/sse.ts')
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createClientCore, getResponseContentType, type ResponseType, type Transport, type TransportResult } from './core.ts'
|
|
2
|
+
import { isJsonMediaType } from './serializers.ts'
|
|
3
|
+
import * as core from './core.ts'
|
|
4
|
+
|
|
5
|
+
export * from './core.ts'
|
|
6
|
+
|
|
7
|
+
/** The request and response objects of this transport, carried on every result and error. */
|
|
8
|
+
export type NativeRequest = Request
|
|
9
|
+
export type NativeResponse = Response
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for fields the runtime does not set itself such as `cache`, `redirect`, and Next.js's `next`.
|
|
13
|
+
*/
|
|
14
|
+
export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
|
|
15
|
+
|
|
16
|
+
export type RequestConfig<TBody = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = core.RequestConfig<TBody, TRequest, TResponse> & {
|
|
17
|
+
options?: FetchOptions
|
|
18
|
+
}
|
|
19
|
+
export type Options<TData extends core.DataShape, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = core.Options<
|
|
20
|
+
TData,
|
|
21
|
+
ThrowOnError,
|
|
22
|
+
TRequest,
|
|
23
|
+
TResponse
|
|
24
|
+
> & { options?: FetchOptions }
|
|
25
|
+
export type ClientConfig<TRequest = NativeRequest, TResponse = NativeResponse> = core.ClientConfig<TRequest, TResponse> & { options?: FetchOptions }
|
|
26
|
+
export type ResolvedRequest = core.ResolvedRequest & { options?: FetchOptions }
|
|
27
|
+
export type ResponseError<TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = core.ResponseError<TError, TRequest, TResponse>
|
|
28
|
+
/** The core's `ResponseError` class, typed with this transport's request and response. `instanceof` and `ResponseError.is` both match what the client throws. */
|
|
29
|
+
export const ResponseError = core.ResponseError as {
|
|
30
|
+
new <TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse>(
|
|
31
|
+
config: ConstructorParameters<typeof core.ResponseError<TError, TRequest, TResponse>>[0],
|
|
32
|
+
): core.ResponseError<TError, TRequest, TResponse>
|
|
33
|
+
is: typeof core.ResponseError.is
|
|
34
|
+
// `instanceof` narrows to this type, so it carries the native request and response.
|
|
35
|
+
prototype: core.ResponseError<unknown, NativeRequest, NativeResponse>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectResponseType(contentType: string | null): ResponseType | undefined {
|
|
39
|
+
if (!contentType) return undefined
|
|
40
|
+
if (contentType.includes('text/event-stream')) return 'stream'
|
|
41
|
+
if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
|
|
42
|
+
if (contentType.includes('text/')) return 'text'
|
|
43
|
+
if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
|
|
48
|
+
if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const contentType = response.headers.get('Content-Type')
|
|
53
|
+
// A server may answer a streamed operation with JSON, or with an error body: both are decoded by their content type. Any other streamed success body stays a stream.
|
|
54
|
+
const detected = responseType === 'stream' && (!response.ok || isJsonMediaType(contentType)) ? undefined : responseType
|
|
55
|
+
switch (detected ?? detectResponseType(contentType)) {
|
|
56
|
+
case 'text':
|
|
57
|
+
case 'document':
|
|
58
|
+
return response.text()
|
|
59
|
+
case 'blob':
|
|
60
|
+
return response.blob()
|
|
61
|
+
case 'arraybuffer':
|
|
62
|
+
return response.arrayBuffer()
|
|
63
|
+
case 'stream':
|
|
64
|
+
return response.body ?? undefined
|
|
65
|
+
case 'json': {
|
|
66
|
+
// An empty body with a JSON content-type would make response.json() throw, so treat it as no data.
|
|
67
|
+
const body = await response.text()
|
|
68
|
+
return body ? JSON.parse(body) : undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const text = await response.text()
|
|
73
|
+
if (!text) return undefined
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text)
|
|
76
|
+
} catch {
|
|
77
|
+
return text
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
|
|
83
|
+
*/
|
|
84
|
+
export const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
|
|
85
|
+
const init: RequestInit = {
|
|
86
|
+
...request.options,
|
|
87
|
+
method: request.method,
|
|
88
|
+
headers: request.headers,
|
|
89
|
+
body: request.body,
|
|
90
|
+
signal: request.signal,
|
|
91
|
+
}
|
|
92
|
+
if (request.credentials) init.credentials = request.credentials
|
|
93
|
+
|
|
94
|
+
const nativeRequest = new Request(request.url, init)
|
|
95
|
+
const response = await globalThis.fetch(nativeRequest)
|
|
96
|
+
const data = await parseResponse(response, request.responseType)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
data,
|
|
100
|
+
status: response.status,
|
|
101
|
+
statusText: response.statusText,
|
|
102
|
+
headers: response.headers,
|
|
103
|
+
contentType: getResponseContentType(response.headers),
|
|
104
|
+
request: nativeRequest,
|
|
105
|
+
response,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const client = createClientCore({ defaultTransport })
|
|
110
|
+
|
|
111
|
+
export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)
|