@vafast/api-client 0.1.5 → 0.2.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/README.md +164 -210
- package/dist/index.d.mts +91 -160
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +29 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,221 +1,199 @@
|
|
|
1
1
|
# @vafast/api-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
类型安全的 Eden 风格 API 客户端,支持从 vafast 路由自动推断类型。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- 🔒 **完整类型推断** - 从路由定义自动推断 API 类型,无需手动定义接口
|
|
8
|
-
- 🎯 **无需 `as const`** - `defineRoutes()` 自动保留字面量类型
|
|
9
|
-
- 🌊 **SSE 流式响应** - 内置 Server-Sent Events 支持,包含自动重连
|
|
10
|
-
- ⏹️ **请求取消** - 支持 AbortController 取消进行中的请求
|
|
11
|
-
- 🔗 **链式调用** - 优雅的 `api.users({ id }).posts.get()` 语法
|
|
12
|
-
- 📦 **轻量** - 仅 8KB (gzip)
|
|
13
|
-
|
|
14
|
-
## 📦 安装
|
|
5
|
+
## 安装
|
|
15
6
|
|
|
16
7
|
```bash
|
|
17
|
-
npm install @vafast/api-client
|
|
8
|
+
npm install @vafast/api-client vafast
|
|
18
9
|
```
|
|
19
10
|
|
|
20
|
-
##
|
|
11
|
+
## 快速开始
|
|
21
12
|
|
|
22
|
-
### 1
|
|
13
|
+
### 方式 1:从 vafast 路由自动推断类型(推荐)
|
|
23
14
|
|
|
24
15
|
```typescript
|
|
25
|
-
//
|
|
26
|
-
import {
|
|
16
|
+
// ============= 服务端 =============
|
|
17
|
+
import { defineRoute, defineRoutes, Type, Server } from 'vafast'
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{
|
|
19
|
+
// 定义路由(使用 as const 保留字面量类型)
|
|
20
|
+
const routeDefinitions = [
|
|
21
|
+
defineRoute({
|
|
31
22
|
method: 'GET',
|
|
32
23
|
path: '/users',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
{
|
|
24
|
+
schema: { query: Type.Object({ page: Type.Number() }) },
|
|
25
|
+
handler: ({ query }) => ({ users: [], page: query.page })
|
|
26
|
+
}),
|
|
27
|
+
defineRoute({
|
|
39
28
|
method: 'POST',
|
|
40
29
|
path: '/users',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
{
|
|
30
|
+
schema: { body: Type.Object({ name: Type.String() }) },
|
|
31
|
+
handler: ({ body }) => ({ id: '1', name: body.name })
|
|
32
|
+
}),
|
|
33
|
+
defineRoute({
|
|
47
34
|
method: 'GET',
|
|
48
35
|
path: '/users/:id',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
yield { data: { text: word } }
|
|
65
|
-
await new Promise(r => setTimeout(r, 100))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
yield { event: 'end', data: { message: 'Done!' } }
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
])
|
|
36
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
37
|
+
handler: ({ params }) => ({ id: params.id, name: 'John' })
|
|
38
|
+
})
|
|
39
|
+
] as const
|
|
40
|
+
|
|
41
|
+
// 创建服务器
|
|
42
|
+
const routes = defineRoutes(routeDefinitions)
|
|
43
|
+
const server = new Server(routes)
|
|
44
|
+
|
|
45
|
+
// ============= 客户端 =============
|
|
46
|
+
import { eden, InferEden } from '@vafast/api-client'
|
|
47
|
+
|
|
48
|
+
// 自动推断类型
|
|
49
|
+
type Api = InferEden<typeof routeDefinitions>
|
|
50
|
+
const api = eden<Api>('http://localhost:3000')
|
|
73
51
|
|
|
74
|
-
//
|
|
75
|
-
|
|
52
|
+
// ✅ 完全类型安全
|
|
53
|
+
const { data } = await api.users.get({ page: 1 }) // query 有类型提示
|
|
54
|
+
const { data: user } = await api.users({ id: '123' }).get() // 动态参数
|
|
76
55
|
```
|
|
77
56
|
|
|
78
|
-
### 2
|
|
57
|
+
### 方式 2:手动定义契约(非 vafast API)
|
|
79
58
|
|
|
80
59
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
import { eden } from '@vafast/api-client'
|
|
61
|
+
|
|
62
|
+
// 手动定义契约类型
|
|
63
|
+
type MyApi = {
|
|
64
|
+
users: {
|
|
65
|
+
get: { query: { page: number }; return: { users: User[]; total: number } }
|
|
66
|
+
post: { body: { name: string }; return: User }
|
|
67
|
+
':id': {
|
|
68
|
+
get: { return: User | null }
|
|
69
|
+
put: { body: Partial<User>; return: User }
|
|
70
|
+
delete: { return: { success: boolean } }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
type Api = InferEden<AppRoutes>
|
|
75
|
+
const api = eden<MyApi>('https://api.example.com')
|
|
87
76
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
77
|
+
// 调用方式完全相同
|
|
78
|
+
const { data } = await api.users.get({ page: 1 })
|
|
79
|
+
const { data: user } = await api.users({ id: '123' }).get()
|
|
80
|
+
```
|
|
93
81
|
|
|
94
|
-
|
|
95
|
-
async function main() {
|
|
96
|
-
// GET /users?page=1
|
|
97
|
-
const { data: users } = await api.users.get({ page: 1 })
|
|
98
|
-
console.log(users?.total) // ✅ 类型推断
|
|
82
|
+
## 调用方式
|
|
99
83
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
email: 'john@example.com'
|
|
104
|
-
})
|
|
105
|
-
console.log(newUser?.id) // ✅ 类型推断
|
|
84
|
+
```typescript
|
|
85
|
+
// GET 请求 + query 参数
|
|
86
|
+
const { data, error } = await api.users.get({ page: 1, limit: 10 })
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
88
|
+
// POST 请求 + body
|
|
89
|
+
const { data, error } = await api.users.post({ name: 'John', email: 'john@example.com' })
|
|
90
|
+
|
|
91
|
+
// 动态路径参数
|
|
92
|
+
const { data, error } = await api.users({ id: '123' }).get()
|
|
93
|
+
const { data, error } = await api.users({ id: '123' }).put({ name: 'Jane' })
|
|
94
|
+
const { data, error } = await api.users({ id: '123' }).delete()
|
|
95
|
+
|
|
96
|
+
// 嵌套路径
|
|
97
|
+
const { data, error } = await api.users({ id: '123' }).posts.get()
|
|
98
|
+
const { data, error } = await api.users({ id: '123' }).posts({ id: '456' }).get()
|
|
111
99
|
```
|
|
112
100
|
|
|
113
|
-
##
|
|
101
|
+
## Go 风格错误处理
|
|
114
102
|
|
|
115
|
-
|
|
103
|
+
```typescript
|
|
104
|
+
const { data, error } = await api.users.get()
|
|
116
105
|
|
|
117
|
-
|
|
106
|
+
if (error) {
|
|
107
|
+
// error: { code: number; message: string }
|
|
108
|
+
console.error(`错误 ${error.code}: ${error.message}`)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// data 在这里保证非 null
|
|
113
|
+
console.log(data.users)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 配置选项
|
|
118
117
|
|
|
119
118
|
```typescript
|
|
120
119
|
const api = eden<Api>('http://localhost:3000', {
|
|
121
120
|
// 默认请求头
|
|
122
|
-
headers: {
|
|
121
|
+
headers: {
|
|
122
|
+
'Authorization': 'Bearer token123'
|
|
123
|
+
},
|
|
123
124
|
|
|
124
|
-
//
|
|
125
|
-
timeout:
|
|
125
|
+
// 请求超时(毫秒)
|
|
126
|
+
timeout: 30000,
|
|
126
127
|
|
|
127
128
|
// 请求拦截器
|
|
128
|
-
onRequest: (request) => {
|
|
129
|
-
|
|
129
|
+
onRequest: async (request) => {
|
|
130
|
+
// 可以修改请求
|
|
130
131
|
return request
|
|
131
132
|
},
|
|
132
133
|
|
|
133
134
|
// 响应拦截器
|
|
134
|
-
onResponse: (response) => {
|
|
135
|
-
|
|
135
|
+
onResponse: async (response) => {
|
|
136
|
+
// 可以修改响应
|
|
136
137
|
return response
|
|
137
138
|
},
|
|
138
139
|
|
|
139
|
-
//
|
|
140
|
+
// 错误回调
|
|
140
141
|
onError: (error) => {
|
|
141
|
-
console.error('Error:', error.message)
|
|
142
|
+
console.error('API Error:', error.code, error.message)
|
|
142
143
|
}
|
|
143
144
|
})
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// GET 请求(带 query 参数)
|
|
150
|
-
api.users.get({ page: 1, limit: 10 })
|
|
151
|
-
|
|
152
|
-
// POST 请求(带 body)
|
|
153
|
-
api.users.post({ name: 'John', email: 'john@example.com' })
|
|
154
|
-
|
|
155
|
-
// PUT 请求
|
|
156
|
-
api.users({ id: '123' }).put({ name: 'Jane' })
|
|
157
|
-
|
|
158
|
-
// DELETE 请求
|
|
159
|
-
api.users({ id: '123' }).delete()
|
|
160
|
-
|
|
161
|
-
// PATCH 请求
|
|
162
|
-
api.users({ id: '123' }).patch({ name: 'Updated' })
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### 路径参数
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// 使用函数调用传递参数
|
|
169
|
-
api.users({ id: '123' }).get() // GET /users/123
|
|
170
|
-
api.users({ id: '123' }).posts.get() // GET /users/123/posts
|
|
171
|
-
api.users({ id: '123' }).posts({ postId: '456' }).get() // GET /users/123/posts/456
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 请求取消
|
|
147
|
+
## SSE 流式响应
|
|
175
148
|
|
|
176
149
|
```typescript
|
|
177
|
-
|
|
150
|
+
import { defineRoute, Type } from 'vafast'
|
|
178
151
|
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
152
|
+
// 服务端定义 SSE 路由
|
|
153
|
+
const routeDefinitions = [
|
|
154
|
+
defineRoute({
|
|
155
|
+
method: 'GET',
|
|
156
|
+
path: '/chat/stream',
|
|
157
|
+
schema: { query: Type.Object({ prompt: Type.String() }) },
|
|
158
|
+
handler: async function* ({ query }) {
|
|
159
|
+
yield { data: { text: 'Hello' } }
|
|
160
|
+
yield { data: { text: 'World' } }
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
] as const
|
|
164
|
+
|
|
165
|
+
// 客户端(手动标记 SSE)
|
|
166
|
+
type Api = {
|
|
167
|
+
chat: {
|
|
168
|
+
stream: {
|
|
169
|
+
get: {
|
|
170
|
+
query: { prompt: string }
|
|
171
|
+
return: { text: string }
|
|
172
|
+
sse: { readonly __brand: 'SSE' }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
188
176
|
}
|
|
189
|
-
```
|
|
190
177
|
|
|
191
|
-
|
|
178
|
+
const api = eden<Api>('http://localhost:3000')
|
|
192
179
|
|
|
193
|
-
|
|
194
|
-
// 覆盖全局配置
|
|
195
|
-
const result = await api.users.get({ page: 1 }, {
|
|
196
|
-
headers: { 'X-Custom-Header': 'value' },
|
|
197
|
-
timeout: 10000,
|
|
198
|
-
signal: abortController.signal
|
|
199
|
-
})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## 🌊 SSE 流式响应
|
|
203
|
-
|
|
204
|
-
### 基本用法
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
180
|
+
// 订阅 SSE 流
|
|
207
181
|
const subscription = api.chat.stream.subscribe(
|
|
208
|
-
{ prompt: 'Hello
|
|
182
|
+
{ prompt: 'Hello' },
|
|
209
183
|
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
184
|
+
onMessage: (data) => {
|
|
185
|
+
console.log('收到消息:', data.text)
|
|
186
|
+
},
|
|
187
|
+
onError: (error) => {
|
|
188
|
+
console.error('错误:', error.message)
|
|
189
|
+
},
|
|
190
|
+
onOpen: () => console.log('连接建立'),
|
|
191
|
+
onClose: () => console.log('连接关闭'),
|
|
214
192
|
onReconnect: (attempt, max) => console.log(`重连中 ${attempt}/${max}`),
|
|
215
193
|
onMaxReconnects: () => console.log('达到最大重连次数')
|
|
216
194
|
},
|
|
217
195
|
{
|
|
218
|
-
reconnectInterval: 3000, //
|
|
196
|
+
reconnectInterval: 3000, // 重连间隔
|
|
219
197
|
maxReconnects: 5 // 最大重连次数
|
|
220
198
|
}
|
|
221
199
|
)
|
|
@@ -224,75 +202,51 @@ const subscription = api.chat.stream.subscribe(
|
|
|
224
202
|
subscription.unsubscribe()
|
|
225
203
|
```
|
|
226
204
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
- ✅ **自动重连** - 网络断开后自动重连
|
|
230
|
-
- ✅ **断点续传** - 使用 `Last-Event-ID` 从断点继续
|
|
231
|
-
- ✅ **可配置重连策略** - 自定义重连间隔和最大次数
|
|
232
|
-
- ✅ **事件类型支持** - 支持自定义事件名称
|
|
233
|
-
|
|
234
|
-
## 🔧 类型定义
|
|
235
|
-
|
|
236
|
-
### `InferEden<T>`
|
|
237
|
-
|
|
238
|
-
从 vafast 路由数组推断 API 契约类型。
|
|
205
|
+
## 请求取消
|
|
239
206
|
|
|
240
207
|
```typescript
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const routes = defineRoutes([...])
|
|
244
|
-
type Api = InferEden<typeof routes>
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### `EdenClient<T>`
|
|
248
|
-
|
|
249
|
-
Eden 客户端类型。
|
|
208
|
+
const controller = new AbortController()
|
|
250
209
|
|
|
251
|
-
|
|
252
|
-
import { EdenClient } from '@vafast/api-client'
|
|
210
|
+
const promise = api.users.get({ page: 1 }, { signal: controller.signal })
|
|
253
211
|
|
|
254
|
-
|
|
212
|
+
// 取消请求
|
|
213
|
+
controller.abort()
|
|
255
214
|
```
|
|
256
215
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
API 响应类型。
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
interface ApiResponse<T> {
|
|
263
|
-
data: T | null // 响应数据
|
|
264
|
-
error: Error | null // 错误信息
|
|
265
|
-
status: number // HTTP 状态码
|
|
266
|
-
headers: Headers // 响应头
|
|
267
|
-
response: Response // 原始 Response
|
|
268
|
-
}
|
|
269
|
-
```
|
|
216
|
+
## API
|
|
270
217
|
|
|
271
|
-
### `
|
|
218
|
+
### `eden<T>(baseURL, config?)`
|
|
272
219
|
|
|
273
|
-
|
|
220
|
+
创建 API 客户端实例。
|
|
274
221
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
headers
|
|
278
|
-
timeout
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
222
|
+
- `baseURL` - API 基础 URL
|
|
223
|
+
- `config` - 可选配置
|
|
224
|
+
- `headers` - 默认请求头
|
|
225
|
+
- `timeout` - 请求超时(毫秒)
|
|
226
|
+
- `onRequest` - 请求拦截器
|
|
227
|
+
- `onResponse` - 响应拦截器
|
|
228
|
+
- `onError` - 错误回调
|
|
282
229
|
|
|
283
|
-
|
|
230
|
+
### `InferEden<T>`
|
|
284
231
|
|
|
285
|
-
|
|
232
|
+
从 `defineRoute` 数组推断 Eden 契约类型。
|
|
286
233
|
|
|
287
|
-
|
|
288
|
-
|
|
234
|
+
```typescript
|
|
235
|
+
import { defineRoute, Type } from 'vafast'
|
|
236
|
+
import { InferEden } from '@vafast/api-client'
|
|
289
237
|
|
|
290
|
-
|
|
238
|
+
const routeDefinitions = [
|
|
239
|
+
defineRoute({
|
|
240
|
+
method: 'GET',
|
|
241
|
+
path: '/users',
|
|
242
|
+
schema: { query: Type.Object({ page: Type.Number() }) },
|
|
243
|
+
handler: ({ query }) => ({ users: [], page: query.page })
|
|
244
|
+
})
|
|
245
|
+
] as const
|
|
291
246
|
|
|
292
|
-
|
|
293
|
-
npm test
|
|
247
|
+
type Api = InferEden<typeof routeDefinitions>
|
|
294
248
|
```
|
|
295
249
|
|
|
296
|
-
##
|
|
250
|
+
## License
|
|
297
251
|
|
|
298
252
|
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -16,114 +16,109 @@ interface RequestConfig {
|
|
|
16
16
|
signal?: AbortSignal;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* API
|
|
19
|
+
* API 错误 - Go 风格结构化错误
|
|
20
|
+
*/
|
|
21
|
+
interface ApiError {
|
|
22
|
+
/** 错误码(HTTP 状态码或业务错误码) */
|
|
23
|
+
code: number;
|
|
24
|
+
/** 错误消息 */
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* API 响应 - Go 风格
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const { data, error } = await api.users.get()
|
|
33
|
+
* if (error) {
|
|
34
|
+
* console.error(`错误码: ${error.code}, 消息: ${error.message}`)
|
|
35
|
+
* return
|
|
36
|
+
* }
|
|
37
|
+
* console.log(data)
|
|
38
|
+
* ```
|
|
20
39
|
*/
|
|
21
40
|
interface ApiResponse<T = unknown> {
|
|
22
|
-
/**
|
|
41
|
+
/** 响应数据,成功时有值,失败时为 null */
|
|
23
42
|
data: T | null;
|
|
24
|
-
/**
|
|
25
|
-
error:
|
|
26
|
-
/** HTTP 状态码 */
|
|
27
|
-
status: number;
|
|
28
|
-
/** 响应头 */
|
|
29
|
-
headers: Headers;
|
|
30
|
-
/** 原始响应对象 */
|
|
31
|
-
response: Response;
|
|
43
|
+
/** 错误信息,成功时为 null,失败时有值 */
|
|
44
|
+
error: ApiError | null;
|
|
32
45
|
}
|
|
33
46
|
//#endregion
|
|
34
47
|
//#region src/core/eden.d.ts
|
|
48
|
+
|
|
35
49
|
interface EdenConfig {
|
|
36
50
|
headers?: Record<string, string>;
|
|
37
51
|
onRequest?: (request: Request) => Request | Promise<Request>;
|
|
38
52
|
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
|
|
39
|
-
onError?: (error:
|
|
53
|
+
onError?: (error: ApiError) => void;
|
|
40
54
|
timeout?: number;
|
|
41
55
|
}
|
|
42
|
-
/**
|
|
43
|
-
* SSE 事件
|
|
44
|
-
*/
|
|
45
56
|
interface SSEEvent<T = unknown> {
|
|
46
57
|
event?: string;
|
|
47
58
|
data: T;
|
|
48
59
|
id?: string;
|
|
49
60
|
retry?: number;
|
|
50
61
|
}
|
|
51
|
-
/**
|
|
52
|
-
* SSE 订阅选项
|
|
53
|
-
*/
|
|
54
62
|
interface SSESubscribeOptions {
|
|
55
|
-
/** 自定义请求头 */
|
|
56
63
|
headers?: Record<string, string>;
|
|
57
|
-
/** 重连间隔(毫秒) */
|
|
58
64
|
reconnectInterval?: number;
|
|
59
|
-
/** 最大重连次数 */
|
|
60
65
|
maxReconnects?: number;
|
|
61
|
-
/** 连接超时(毫秒) */
|
|
62
66
|
timeout?: number;
|
|
63
67
|
}
|
|
64
|
-
/**
|
|
65
|
-
* SSE 订阅结果
|
|
66
|
-
*/
|
|
67
68
|
interface SSESubscription<T = unknown> {
|
|
68
|
-
/** 取消订阅 */
|
|
69
69
|
unsubscribe: () => void;
|
|
70
|
-
/** 是否已连接 */
|
|
71
70
|
readonly connected: boolean;
|
|
72
71
|
}
|
|
73
|
-
/**
|
|
74
|
-
* 从 TypeBox Schema 提取静态类型
|
|
75
|
-
* 使用 TypeBox 内部的 static 属性提取类型
|
|
76
|
-
*/
|
|
72
|
+
/** 从 TypeBox Schema 提取静态类型 */
|
|
77
73
|
type InferStatic<T> = T extends {
|
|
78
74
|
static: infer S;
|
|
79
75
|
} ? S : T;
|
|
80
|
-
/** 从
|
|
81
|
-
type
|
|
82
|
-
__returnType: infer R;
|
|
83
|
-
} ? R : unknown;
|
|
84
|
-
/** 从 InferableHandler 提取 Schema */
|
|
85
|
-
type ExtractSchema<T> = T extends {
|
|
86
|
-
__schema: infer S;
|
|
87
|
-
} ? S : {};
|
|
88
|
-
/** 检查是否是 SSE Handler(使用品牌类型检测) */
|
|
89
|
-
type IsSSEHandler<T> = T extends {
|
|
90
|
-
__sse: {
|
|
91
|
-
readonly __brand: 'SSE';
|
|
92
|
-
};
|
|
93
|
-
} ? true : false;
|
|
94
|
-
/** 从 Schema 提取各部分类型 */
|
|
95
|
-
type GetQuery<S> = S extends {
|
|
76
|
+
/** 从 Schema 对象提取各部分类型 */
|
|
77
|
+
type GetSchemaQuery<S> = S extends {
|
|
96
78
|
query: infer Q;
|
|
97
79
|
} ? InferStatic<Q> : undefined;
|
|
98
|
-
type
|
|
80
|
+
type GetSchemaBody<S> = S extends {
|
|
99
81
|
body: infer B;
|
|
100
82
|
} ? InferStatic<B> : undefined;
|
|
101
|
-
type
|
|
83
|
+
type GetSchemaParams<S> = S extends {
|
|
102
84
|
params: infer P;
|
|
103
85
|
} ? InferStatic<P> : undefined;
|
|
86
|
+
/**
|
|
87
|
+
* 从 handler 函数推断返回类型
|
|
88
|
+
* handler: (ctx) => TReturn | Promise<TReturn>
|
|
89
|
+
*/
|
|
90
|
+
type InferHandlerReturn<H$1> = H$1 extends ((...args: never[]) => infer R) ? R extends Promise<infer T> ? T : R : unknown;
|
|
104
91
|
/** 移除开头斜杠:/users → users */
|
|
105
92
|
type TrimSlash<P$1 extends string> = P$1 extends `/${infer R}` ? R : P$1;
|
|
106
93
|
/** 检查是否是动态参数段::id → true */
|
|
107
94
|
type IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false;
|
|
108
|
-
/** 清理 undefined 字段 */
|
|
109
95
|
type Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] };
|
|
110
|
-
/** SSE 标记类型 */
|
|
111
96
|
type SSEBrand = {
|
|
112
97
|
readonly __brand: 'SSE';
|
|
113
98
|
};
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
99
|
+
/**
|
|
100
|
+
* 从 defineRoute 返回的路由配置构建方法定义
|
|
101
|
+
*
|
|
102
|
+
* defineRoute 返回的 LeafRouteConfig 结构:
|
|
103
|
+
* {
|
|
104
|
+
* method: TMethod,
|
|
105
|
+
* path: TPath,
|
|
106
|
+
* schema?: TSchema,
|
|
107
|
+
* handler: (ctx) => TReturn | Promise<TReturn>
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
type BuildMethodDef<R> = R extends {
|
|
111
|
+
readonly schema?: infer TSchema;
|
|
112
|
+
readonly handler: infer THandler;
|
|
113
|
+
} ? Clean<{
|
|
114
|
+
query: GetSchemaQuery<TSchema>;
|
|
115
|
+
body: GetSchemaBody<TSchema>;
|
|
116
|
+
params: GetSchemaParams<TSchema>;
|
|
117
|
+
return: InferHandlerReturn<THandler>;
|
|
122
118
|
}> : Clean<{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return: ExtractReturn<R['handler']>;
|
|
119
|
+
return: R extends {
|
|
120
|
+
readonly handler: infer H;
|
|
121
|
+
} ? InferHandlerReturn<H> : unknown;
|
|
127
122
|
}>;
|
|
128
123
|
/**
|
|
129
124
|
* 递归构建嵌套路径结构
|
|
@@ -134,79 +129,55 @@ type BuildPath<Path extends string, Method extends string, Def> = Path extends `
|
|
|
134
129
|
':id': BuildPath<Rest, Method, Def>;
|
|
135
130
|
} : { [K in First]: BuildPath<Rest, Method, Def> } : IsDynamicSegment<Path> extends true ? {
|
|
136
131
|
':id': { [M in Method]: Def };
|
|
137
|
-
} : { [K in Path]: { [M in Method]: Def } };
|
|
132
|
+
} : Path extends '' ? { [M in Method]: Def } : { [K in Path]: { [M in Method]: Def } };
|
|
138
133
|
/**
|
|
139
134
|
* 从单个路由生成嵌套类型结构
|
|
140
135
|
*/
|
|
141
|
-
type RouteToTree<R extends {
|
|
142
|
-
readonly method: string;
|
|
143
|
-
readonly path: string;
|
|
144
|
-
|
|
145
|
-
}> = BuildPath<TrimSlash<R['path']>, Lowercase<R['method']>, BuildMethodDef<R>>;
|
|
136
|
+
type RouteToTree<R> = R extends {
|
|
137
|
+
readonly method: infer M extends string;
|
|
138
|
+
readonly path: infer P extends string;
|
|
139
|
+
} ? BuildPath<TrimSlash<P>, Lowercase<M>, BuildMethodDef<R>> : {};
|
|
146
140
|
type DeepMerge<A, B$1> = { [K in keyof A | keyof B$1]: K extends keyof A & keyof B$1 ? A[K] extends object ? B$1[K] extends object ? DeepMerge<A[K], B$1[K]> : A[K] & B$1[K] : A[K] & B$1[K] : K extends keyof A ? A[K] : K extends keyof B$1 ? B$1[K] : never };
|
|
147
141
|
/** 递归合并路由数组为单一类型结构 */
|
|
148
|
-
type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First extends {
|
|
149
|
-
readonly method: string;
|
|
150
|
-
readonly path: string;
|
|
151
|
-
readonly handler: unknown;
|
|
152
|
-
}] ? RouteToTree<First> : T extends readonly [infer First extends {
|
|
153
|
-
readonly method: string;
|
|
154
|
-
readonly path: string;
|
|
155
|
-
readonly handler: unknown;
|
|
156
|
-
}, ...infer Rest extends readonly {
|
|
157
|
-
readonly method: string;
|
|
158
|
-
readonly path: string;
|
|
159
|
-
readonly handler: unknown;
|
|
160
|
-
}[]] ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>> : {};
|
|
142
|
+
type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First] ? RouteToTree<First> : T extends readonly [infer First, ...infer Rest] ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>> : {};
|
|
161
143
|
/**
|
|
162
|
-
* 从
|
|
144
|
+
* 从 defineRoute 数组自动推断 Eden 契约
|
|
163
145
|
*
|
|
164
146
|
* @example
|
|
165
147
|
* ```typescript
|
|
166
|
-
* import {
|
|
167
|
-
* import { eden, InferEden } from 'vafast
|
|
148
|
+
* import { defineRoute, defineRoutes, Type } from 'vafast'
|
|
149
|
+
* import { eden, InferEden } from '@vafast/api-client'
|
|
168
150
|
*
|
|
169
|
-
* //
|
|
170
|
-
* const
|
|
171
|
-
* {
|
|
151
|
+
* // 定义路由(使用 as const 保留字面量类型)
|
|
152
|
+
* const routeDefinitions = [
|
|
153
|
+
* defineRoute({
|
|
172
154
|
* method: 'GET',
|
|
173
155
|
* path: '/users',
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* async function* ({ query }) {
|
|
185
|
-
* yield { data: { text: 'Hello' } }
|
|
186
|
-
* }
|
|
187
|
-
* )
|
|
188
|
-
* }
|
|
189
|
-
* ])
|
|
156
|
+
* schema: { query: Type.Object({ page: Type.Number() }) },
|
|
157
|
+
* handler: ({ query }) => ({ users: [], total: 0 })
|
|
158
|
+
* }),
|
|
159
|
+
* defineRoute({
|
|
160
|
+
* method: 'POST',
|
|
161
|
+
* path: '/users',
|
|
162
|
+
* schema: { body: Type.Object({ name: Type.String() }) },
|
|
163
|
+
* handler: ({ body }) => ({ id: '1', name: body.name })
|
|
164
|
+
* })
|
|
165
|
+
* ] as const
|
|
190
166
|
*
|
|
191
|
-
*
|
|
192
|
-
* const
|
|
167
|
+
* // 服务端
|
|
168
|
+
* const routes = defineRoutes(routeDefinitions)
|
|
169
|
+
* const server = new Server(routes)
|
|
193
170
|
*
|
|
194
|
-
* //
|
|
195
|
-
*
|
|
171
|
+
* // 客户端类型推断
|
|
172
|
+
* type Api = InferEden<typeof routeDefinitions>
|
|
173
|
+
* const api = eden<Api>('http://localhost:3000')
|
|
196
174
|
*
|
|
197
|
-
* //
|
|
198
|
-
* api.
|
|
199
|
-
*
|
|
200
|
-
* onError: (err) => console.error(err)
|
|
201
|
-
* })
|
|
175
|
+
* // 类型安全的调用
|
|
176
|
+
* const { data } = await api.users.get({ page: 1 }) // ✅ query 类型推断
|
|
177
|
+
* const { data: user } = await api.users.post({ name: 'John' }) // ✅ body 类型推断
|
|
202
178
|
* ```
|
|
203
179
|
*/
|
|
204
|
-
type InferEden<T extends readonly
|
|
205
|
-
readonly method: string;
|
|
206
|
-
readonly path: string;
|
|
207
|
-
readonly handler: unknown;
|
|
208
|
-
}[]> = MergeRoutes<T>;
|
|
209
|
-
/** HTTP 方法定义 */
|
|
180
|
+
type InferEden<T extends readonly unknown[]> = MergeRoutes<T>;
|
|
210
181
|
interface MethodDef {
|
|
211
182
|
query?: unknown;
|
|
212
183
|
body?: unknown;
|
|
@@ -214,22 +185,14 @@ interface MethodDef {
|
|
|
214
185
|
return: unknown;
|
|
215
186
|
sse?: SSEBrand;
|
|
216
187
|
}
|
|
217
|
-
/** SSE 订阅回调 */
|
|
218
188
|
interface SSECallbacks<T> {
|
|
219
|
-
/** 收到消息 */
|
|
220
189
|
onMessage: (data: T) => void;
|
|
221
|
-
|
|
222
|
-
onError?: (error: Error) => void;
|
|
223
|
-
/** 连接打开 */
|
|
190
|
+
onError?: (error: ApiError) => void;
|
|
224
191
|
onOpen?: () => void;
|
|
225
|
-
/** 连接关闭 */
|
|
226
192
|
onClose?: () => void;
|
|
227
|
-
/** 正在重连 */
|
|
228
193
|
onReconnect?: (attempt: number, maxAttempts: number) => void;
|
|
229
|
-
/** 达到最大重连次数 */
|
|
230
194
|
onMaxReconnects?: () => void;
|
|
231
195
|
}
|
|
232
|
-
/** 从方法定义提取调用签名 */
|
|
233
196
|
type MethodCall<M$1 extends MethodDef, HasParams extends boolean = false> = M$1 extends {
|
|
234
197
|
sse: SSEBrand;
|
|
235
198
|
} ? M$1 extends {
|
|
@@ -241,56 +204,24 @@ type MethodCall<M$1 extends MethodDef, HasParams extends boolean = false> = M$1
|
|
|
241
204
|
} ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : M$1 extends {
|
|
242
205
|
body: infer B;
|
|
243
206
|
} ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : (config?: RequestConfig) => Promise<ApiResponse<M$1['return']>>;
|
|
244
|
-
/** 检查是否是 SSE 端点(检测品牌类型标记) */
|
|
245
207
|
type IsSSEEndpoint<M$1> = M$1 extends {
|
|
246
208
|
sse: {
|
|
247
209
|
readonly __brand: 'SSE';
|
|
248
210
|
};
|
|
249
211
|
} ? true : false;
|
|
250
|
-
/** 端点类型 - 包含 subscribe 方法用于 SSE */
|
|
251
212
|
type Endpoint<T, HasParams extends boolean = false> = { [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never } & (T extends {
|
|
252
213
|
get: infer M extends MethodDef;
|
|
253
214
|
} ? IsSSEEndpoint<M> extends true ? {
|
|
254
215
|
subscribe: MethodCall<M, HasParams>;
|
|
255
216
|
} : {} : {});
|
|
256
|
-
/** HTTP 方法名 */
|
|
257
217
|
type HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
258
|
-
/** 递归构建客户端类型 */
|
|
259
218
|
type EdenClient<T, HasParams extends boolean = false> = { [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: T[K] extends {
|
|
260
219
|
':id': infer Child;
|
|
261
220
|
} ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false> : EdenClient<T[K], false> } & Endpoint<T, HasParams>;
|
|
262
221
|
/**
|
|
263
222
|
* 创建 Eden 风格的类型安全 API 客户端
|
|
264
|
-
*
|
|
265
|
-
* @param baseURL - API 基础 URL
|
|
266
|
-
* @param config - 可选配置
|
|
267
|
-
* @returns 类型安全的 API 客户端
|
|
268
|
-
*
|
|
269
|
-
* @example
|
|
270
|
-
* ```typescript
|
|
271
|
-
* // 使用自动推断的契约(无需 as const)
|
|
272
|
-
* const routes = defineRoutes([
|
|
273
|
-
* route('GET', '/users', createHandler(...)),
|
|
274
|
-
* route('GET', '/chat/stream', createSSEHandler(...))
|
|
275
|
-
* ])
|
|
276
|
-
*
|
|
277
|
-
* type Api = InferEden<typeof routes>
|
|
278
|
-
* const api = eden<Api>('http://localhost:3000')
|
|
279
|
-
*
|
|
280
|
-
* // 普通请求
|
|
281
|
-
* const { data } = await api.users.get({ page: 1 })
|
|
282
|
-
*
|
|
283
|
-
* // SSE 流式请求
|
|
284
|
-
* const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
|
|
285
|
-
* onMessage: (data) => console.log(data),
|
|
286
|
-
* onError: (err) => console.error(err)
|
|
287
|
-
* })
|
|
288
|
-
*
|
|
289
|
-
* // 取消订阅
|
|
290
|
-
* sub.unsubscribe()
|
|
291
|
-
* ```
|
|
292
223
|
*/
|
|
293
224
|
declare function eden<T>(baseURL: string, config?: EdenConfig): EdenClient<T>;
|
|
294
225
|
//#endregion
|
|
295
|
-
export { type ApiResponse, type EdenClient, type EdenConfig, type HTTPMethod, type InferEden, type RequestConfig, type SSEEvent, type SSESubscribeOptions, type SSESubscription, eden };
|
|
226
|
+
export { type ApiError, type ApiResponse, type EdenClient, type EdenConfig, type HTTPMethod, type InferEden, type RequestConfig, type SSEEvent, type SSESubscribeOptions, type SSESubscription, eden };
|
|
296
227
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/eden.ts"],"sourcesContent":[],"mappings":";;AAKA;AAKA;AAYA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/eden.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
//#region src/core/eden.ts
|
|
2
|
-
/**
|
|
3
|
-
* 解析 SSE 事件流
|
|
4
|
-
*/
|
|
5
2
|
async function* parseSSEStream(reader) {
|
|
6
3
|
const decoder = new TextDecoder();
|
|
7
4
|
let buffer = "";
|
|
@@ -32,34 +29,6 @@ async function* parseSSEStream(reader) {
|
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
34
31
|
* 创建 Eden 风格的类型安全 API 客户端
|
|
35
|
-
*
|
|
36
|
-
* @param baseURL - API 基础 URL
|
|
37
|
-
* @param config - 可选配置
|
|
38
|
-
* @returns 类型安全的 API 客户端
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* // 使用自动推断的契约(无需 as const)
|
|
43
|
-
* const routes = defineRoutes([
|
|
44
|
-
* route('GET', '/users', createHandler(...)),
|
|
45
|
-
* route('GET', '/chat/stream', createSSEHandler(...))
|
|
46
|
-
* ])
|
|
47
|
-
*
|
|
48
|
-
* type Api = InferEden<typeof routes>
|
|
49
|
-
* const api = eden<Api>('http://localhost:3000')
|
|
50
|
-
*
|
|
51
|
-
* // 普通请求
|
|
52
|
-
* const { data } = await api.users.get({ page: 1 })
|
|
53
|
-
*
|
|
54
|
-
* // SSE 流式请求
|
|
55
|
-
* const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
|
|
56
|
-
* onMessage: (data) => console.log(data),
|
|
57
|
-
* onError: (err) => console.error(err)
|
|
58
|
-
* })
|
|
59
|
-
*
|
|
60
|
-
* // 取消订阅
|
|
61
|
-
* sub.unsubscribe()
|
|
62
|
-
* ```
|
|
63
32
|
*/
|
|
64
33
|
function eden(baseURL, config) {
|
|
65
34
|
const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {};
|
|
@@ -95,25 +64,33 @@ function eden(baseURL, config) {
|
|
|
95
64
|
let responseData = null;
|
|
96
65
|
if (contentType?.includes("application/json")) responseData = await response.json();
|
|
97
66
|
else if (contentType?.includes("text/")) responseData = await response.text();
|
|
98
|
-
let result
|
|
67
|
+
let result;
|
|
68
|
+
if (response.ok) result = {
|
|
99
69
|
data: responseData,
|
|
100
|
-
error:
|
|
101
|
-
status: response.status,
|
|
102
|
-
headers: response.headers,
|
|
103
|
-
response
|
|
70
|
+
error: null
|
|
104
71
|
};
|
|
72
|
+
else {
|
|
73
|
+
const errorBody = responseData;
|
|
74
|
+
result = {
|
|
75
|
+
data: null,
|
|
76
|
+
error: {
|
|
77
|
+
code: errorBody?.code ?? response.status,
|
|
78
|
+
message: errorBody?.message ?? `HTTP ${response.status}`
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
105
82
|
if (onResponse) result = await onResponse(result);
|
|
106
|
-
if (
|
|
83
|
+
if (result.error && onError) onError(result.error);
|
|
107
84
|
return result;
|
|
108
85
|
} catch (error) {
|
|
109
|
-
const
|
|
110
|
-
|
|
86
|
+
const apiError = {
|
|
87
|
+
code: 0,
|
|
88
|
+
message: (error instanceof Error ? error : new Error(String(error))).message || "网络错误"
|
|
89
|
+
};
|
|
90
|
+
if (onError) onError(apiError);
|
|
111
91
|
return {
|
|
112
92
|
data: null,
|
|
113
|
-
error:
|
|
114
|
-
status: 0,
|
|
115
|
-
headers: new Headers(),
|
|
116
|
-
response: new Response()
|
|
93
|
+
error: apiError
|
|
117
94
|
};
|
|
118
95
|
}
|
|
119
96
|
}
|
|
@@ -152,7 +129,10 @@ function eden(baseURL, config) {
|
|
|
152
129
|
const reader = response.body.getReader();
|
|
153
130
|
for await (const event of parseSSEStream(reader)) {
|
|
154
131
|
if (event.id) lastEventId = event.id;
|
|
155
|
-
if (event.event === "error") callbacks.onError?.(
|
|
132
|
+
if (event.event === "error") callbacks.onError?.({
|
|
133
|
+
code: -1,
|
|
134
|
+
message: String(event.data)
|
|
135
|
+
});
|
|
156
136
|
else callbacks.onMessage(event.data);
|
|
157
137
|
}
|
|
158
138
|
connected = false;
|
|
@@ -160,7 +140,11 @@ function eden(baseURL, config) {
|
|
|
160
140
|
} catch (error) {
|
|
161
141
|
connected = false;
|
|
162
142
|
if (error.name === "AbortError" || isUnsubscribed) return;
|
|
163
|
-
|
|
143
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
144
|
+
callbacks.onError?.({
|
|
145
|
+
code: 0,
|
|
146
|
+
message: err.message || "SSE 连接错误"
|
|
147
|
+
});
|
|
164
148
|
if (reconnectCount < maxReconnects) {
|
|
165
149
|
reconnectCount++;
|
|
166
150
|
callbacks.onReconnect?.(reconnectCount, maxReconnects);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core/eden.ts"],"sourcesContent":["/**\n * Eden 风格 API 客户端\n * \n * 最自然的链式调用:\n * - api.users.get() // GET /users\n * - api.users.post({ name }) // POST /users\n * - api.users({ id }).get() // GET /users/:id\n * - api.users({ id }).delete() // DELETE /users/:id\n * - api.chat.stream.subscribe() // SSE 流式响应\n */\n\nimport type { ApiResponse, RequestConfig } from '../types'\n\n// ============= 配置 =============\n\nexport interface EdenConfig {\n headers?: Record<string, string>\n onRequest?: (request: Request) => Request | Promise<Request>\n onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>\n onError?: (error: Error) => void\n timeout?: number\n}\n\n// ============= SSE 类型 =============\n\n/**\n * SSE 事件\n */\nexport interface SSEEvent<T = unknown> {\n event?: string\n data: T\n id?: string\n retry?: number\n}\n\n/**\n * SSE 订阅选项\n */\nexport interface SSESubscribeOptions {\n /** 自定义请求头 */\n headers?: Record<string, string>\n /** 重连间隔(毫秒) */\n reconnectInterval?: number\n /** 最大重连次数 */\n maxReconnects?: number\n /** 连接超时(毫秒) */\n timeout?: number\n}\n\n/**\n * SSE 订阅结果\n */\nexport interface SSESubscription<T = unknown> {\n /** 取消订阅 */\n unsubscribe: () => void\n /** 是否已连接 */\n readonly connected: boolean\n}\n\n// ============= 基础类型工具 =============\n\n/**\n * 从 TypeBox Schema 提取静态类型\n * 使用 TypeBox 内部的 static 属性提取类型\n */\ntype InferStatic<T> = T extends { static: infer S } ? S : T\n\n/** 从 InferableHandler 提取返回类型 */\ntype ExtractReturn<T> = T extends { __returnType: infer R } ? R : unknown\n\n/** 从 InferableHandler 提取 Schema */\ntype ExtractSchema<T> = T extends { __schema: infer S } ? S : {}\n\n/** 检查是否是 SSE Handler(使用品牌类型检测) */\ntype IsSSEHandler<T> = T extends { __sse: { readonly __brand: 'SSE' } } ? true : false\n\n/** 从 Schema 提取各部分类型 */\ntype GetQuery<S> = S extends { query: infer Q } ? InferStatic<Q> : undefined\ntype GetBody<S> = S extends { body: infer B } ? InferStatic<B> : undefined\ntype GetParams<S> = S extends { params: infer P } ? InferStatic<P> : undefined\n\n// ============= 路径处理 =============\n\n/** 移除开头斜杠:/users → users */\ntype TrimSlash<P extends string> = P extends `/${infer R}` ? R : P\n\n/** 获取第一段:users/posts → users */\ntype Head<P extends string> = P extends `${infer H}/${string}` ? H : P\n\n/** 获取剩余段:users/posts → posts */\ntype Tail<P extends string> = P extends `${string}/${infer T}` ? T : never\n\n/** 检查是否是动态参数段::id → true */\ntype IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false\n\n// ============= 核心类型推断 =============\n\n/** 清理 undefined 字段 */\ntype Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }\n\n/** SSE 标记类型 */\ntype SSEBrand = { readonly __brand: 'SSE' }\n\n/** 从路由构建方法定义 */\ntype BuildMethodDef<R extends { readonly handler: unknown }> = \n IsSSEHandler<R['handler']> extends true\n ? Clean<{\n query: GetQuery<ExtractSchema<R['handler']>>\n params: GetParams<ExtractSchema<R['handler']>>\n return: ExtractReturn<R['handler']>\n sse: SSEBrand\n }>\n : Clean<{\n query: GetQuery<ExtractSchema<R['handler']>>\n body: GetBody<ExtractSchema<R['handler']>>\n params: GetParams<ExtractSchema<R['handler']>>\n return: ExtractReturn<R['handler']>\n }>\n\n/**\n * 递归构建嵌套路径结构\n * \n * 处理动态参数:/users/:id → { users: { ':id': { ... } } }\n */\ntype BuildPath<Path extends string, Method extends string, Def> =\n Path extends `${infer First}/${infer Rest}`\n ? IsDynamicSegment<First> extends true\n ? { ':id': BuildPath<Rest, Method, Def> }\n : { [K in First]: BuildPath<Rest, Method, Def> }\n : IsDynamicSegment<Path> extends true\n ? { ':id': { [M in Method]: Def } }\n : { [K in Path]: { [M in Method]: Def } }\n\n/**\n * 从单个路由生成嵌套类型结构\n */\ntype RouteToTree<R extends { readonly method: string; readonly path: string; readonly handler: unknown }> =\n BuildPath<TrimSlash<R['path']>, Lowercase<R['method']>, BuildMethodDef<R>>\n\n// ============= 深度合并多个路由 =============\n\ntype DeepMerge<A, B> = {\n [K in keyof A | keyof B]: \n K extends keyof A & keyof B\n ? A[K] extends object\n ? B[K] extends object\n ? DeepMerge<A[K], B[K]>\n : A[K] & B[K]\n : A[K] & B[K]\n : K extends keyof A\n ? A[K]\n : K extends keyof B\n ? B[K]\n : never\n}\n\n/** 递归合并路由数组为单一类型结构 */\ntype MergeRoutes<T extends readonly unknown[]> = \n T extends readonly [infer First extends { readonly method: string; readonly path: string; readonly handler: unknown }]\n ? RouteToTree<First>\n : T extends readonly [\n infer First extends { readonly method: string; readonly path: string; readonly handler: unknown }, \n ...infer Rest extends readonly { readonly method: string; readonly path: string; readonly handler: unknown }[]\n ]\n ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>>\n : {}\n\n/**\n * 从 vafast 路由数组自动推断 Eden 契约\n * \n * @example\n * ```typescript\n * import { defineRoutes, createHandler, Type } from 'vafast'\n * import { eden, InferEden } from 'vafast-api-client'\n * \n * // ✨ defineRoutes() 自动保留字面量类型,无需 as const\n * const routes = defineRoutes([\n * {\n * method: 'GET',\n * path: '/users',\n * handler: createHandler(\n * { query: Type.Object({ page: Type.Number() }) },\n * async ({ query }) => ({ users: [], total: 0 })\n * )\n * },\n * {\n * method: 'GET',\n * path: '/chat/stream',\n * handler: createSSEHandler(\n * { query: Type.Object({ prompt: Type.String() }) },\n * async function* ({ query }) {\n * yield { data: { text: 'Hello' } }\n * }\n * )\n * }\n * ])\n * \n * type Api = InferEden<typeof routes>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 普通请求\n * const { data } = await api.users.get({ page: 1 })\n * \n * // SSE 流式请求\n * api.chat.stream.subscribe({ prompt: 'Hi' }, {\n * onMessage: (data) => console.log(data),\n * onError: (err) => console.error(err)\n * })\n * ```\n */\nexport type InferEden<T extends readonly { readonly method: string; readonly path: string; readonly handler: unknown }[]> = \n MergeRoutes<T>\n\n// ============= 契约类型(手动定义时使用) =============\n\n/** HTTP 方法定义 */\ninterface MethodDef {\n query?: unknown\n body?: unknown\n params?: unknown\n return: unknown\n sse?: SSEBrand\n}\n\n/** 路由节点 */\ntype RouteNode = {\n get?: MethodDef\n post?: MethodDef\n put?: MethodDef\n patch?: MethodDef\n delete?: MethodDef\n ':id'?: RouteNode\n [key: string]: MethodDef | RouteNode | undefined\n}\n\n// ============= 客户端类型 =============\n\n/** SSE 订阅回调 */\ninterface SSECallbacks<T> {\n /** 收到消息 */\n onMessage: (data: T) => void\n /** 发生错误 */\n onError?: (error: Error) => void\n /** 连接打开 */\n onOpen?: () => void\n /** 连接关闭 */\n onClose?: () => void\n /** 正在重连 */\n onReconnect?: (attempt: number, maxAttempts: number) => void\n /** 达到最大重连次数 */\n onMaxReconnects?: () => void\n}\n\n/** 从方法定义提取调用签名 */\ntype MethodCall<M extends MethodDef, HasParams extends boolean = false> = \n M extends { sse: SSEBrand }\n ? M extends { query: infer Q }\n ? (query: Q, callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : (callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : HasParams extends true\n ? M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { query: infer Q }\n ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n\n/** 检查是否是 SSE 端点(检测品牌类型标记) */\ntype IsSSEEndpoint<M> = M extends { sse: { readonly __brand: 'SSE' } } ? true : false\n\n/** 端点类型 - 包含 subscribe 方法用于 SSE */\ntype Endpoint<T, HasParams extends boolean = false> = \n // HTTP 方法\n {\n [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: \n T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never\n } \n // SSE subscribe 方法(如果 GET 是 SSE)\n & (T extends { get: infer M extends MethodDef }\n ? IsSSEEndpoint<M> extends true \n ? { subscribe: MethodCall<M, HasParams> }\n : {}\n : {})\n\n/** 检查节点是否有动态参数子路由 */\ntype HasDynamicChild<T> = T extends { ':id': unknown } ? true : false\n\n/** HTTP 方法名 */\ntype HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'\n\n/** 递归构建客户端类型 */\nexport type EdenClient<T, HasParams extends boolean = false> = {\n // 嵌套路径(排除 HTTP 方法和动态参数)\n [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: \n T[K] extends { ':id': infer Child }\n // 有动态参数子路由\n ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false>\n // 普通嵌套路由\n : EdenClient<T[K], false>\n} & Endpoint<T, HasParams>\n\n// ============= SSE 解析器 =============\n\n/**\n * 解析 SSE 事件流\n */\nasync function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<SSEEvent, void, unknown> {\n const decoder = new TextDecoder();\n let buffer = '';\n \n while (true) {\n const { done, value } = await reader.read();\n \n if (done) break;\n \n buffer += decoder.decode(value, { stream: true });\n \n // 按双换行分割事件\n const events = buffer.split('\\n\\n');\n buffer = events.pop() || ''; // 保留未完成的部分\n \n for (const eventStr of events) {\n if (!eventStr.trim()) continue;\n \n const event: SSEEvent = { data: '' };\n const lines = eventStr.split('\\n');\n let dataLines: string[] = [];\n \n for (const line of lines) {\n if (line.startsWith('event:')) {\n event.event = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trim());\n } else if (line.startsWith('id:')) {\n event.id = line.slice(3).trim();\n } else if (line.startsWith('retry:')) {\n event.retry = parseInt(line.slice(6).trim(), 10);\n }\n }\n \n // 合并多行 data\n const dataStr = dataLines.join('\\n');\n \n // 尝试解析 JSON\n try {\n event.data = JSON.parse(dataStr);\n } catch {\n event.data = dataStr;\n }\n \n yield event;\n }\n }\n}\n\n// ============= 实现 =============\n\n/**\n * 创建 Eden 风格的类型安全 API 客户端\n * \n * @param baseURL - API 基础 URL\n * @param config - 可选配置\n * @returns 类型安全的 API 客户端\n * \n * @example\n * ```typescript\n * // 使用自动推断的契约(无需 as const)\n * const routes = defineRoutes([\n * route('GET', '/users', createHandler(...)),\n * route('GET', '/chat/stream', createSSEHandler(...))\n * ])\n * \n * type Api = InferEden<typeof routes>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 普通请求\n * const { data } = await api.users.get({ page: 1 })\n * \n * // SSE 流式请求\n * const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {\n * onMessage: (data) => console.log(data),\n * onError: (err) => console.error(err)\n * })\n * \n * // 取消订阅\n * sub.unsubscribe()\n * ```\n */\nexport function eden<T>(\n baseURL: string,\n config?: EdenConfig\n): EdenClient<T> {\n const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {}\n\n // 发送普通请求\n async function request<TReturn>(\n method: string,\n path: string,\n data?: unknown,\n requestConfig?: RequestConfig\n ): Promise<ApiResponse<TReturn>> {\n const url = new URL(path, baseURL)\n \n // Query 参数\n if (method === 'GET' && data && typeof data === 'object') {\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...defaultHeaders,\n ...requestConfig?.headers,\n }\n\n // 支持用户传入的取消信号,或内部超时取消\n const controller = new AbortController()\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n \n // 合并用户的 signal 和内部的超时 signal\n const userSignal = requestConfig?.signal\n const requestTimeout = requestConfig?.timeout ?? timeout\n \n if (userSignal) {\n // 如果用户已取消,直接中止\n if (userSignal.aborted) {\n controller.abort()\n } else {\n // 监听用户取消\n userSignal.addEventListener('abort', () => controller.abort())\n }\n }\n \n if (requestTimeout) {\n timeoutId = setTimeout(() => controller.abort(), requestTimeout)\n }\n\n const fetchOptions: RequestInit = { \n method, \n headers,\n signal: controller.signal \n }\n\n // Body\n if (method !== 'GET' && method !== 'HEAD' && data) {\n fetchOptions.body = JSON.stringify(data)\n }\n\n let req = new Request(url.toString(), fetchOptions)\n \n if (onRequest) {\n req = await onRequest(req)\n }\n\n try {\n const response = await fetch(req)\n \n if (timeoutId) clearTimeout(timeoutId)\n \n const contentType = response.headers.get('content-type')\n let responseData: TReturn | null = null\n \n if (contentType?.includes('application/json')) {\n responseData = await response.json()\n } else if (contentType?.includes('text/')) {\n responseData = await response.text() as unknown as TReturn\n }\n\n let result: ApiResponse<TReturn> = {\n data: responseData,\n error: response.ok ? null : new Error(`HTTP ${response.status}`),\n status: response.status,\n headers: response.headers,\n response,\n }\n\n if (onResponse) {\n result = await onResponse(result)\n }\n\n if (!response.ok && onError) {\n onError(result.error!)\n }\n\n return result\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n if (onError) onError(err)\n return {\n data: null,\n error: err,\n status: 0,\n headers: new Headers(),\n response: new Response(),\n }\n }\n }\n\n // SSE 订阅(支持自动重连)\n function subscribe<TData>(\n path: string,\n query: Record<string, unknown> | undefined,\n callbacks: SSECallbacks<TData>,\n options?: SSESubscribeOptions\n ): SSESubscription<TData> {\n const url = new URL(path, baseURL)\n \n // 添加 query 参数\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let abortController: AbortController | null = new AbortController()\n let connected = false\n let reconnectCount = 0\n let isUnsubscribed = false\n let lastEventId: string | undefined\n \n // 重连配置\n const reconnectInterval = options?.reconnectInterval ?? 3000\n const maxReconnects = options?.maxReconnects ?? 5\n\n const connect = async () => {\n if (isUnsubscribed) return\n \n try {\n // 重新创建 AbortController(重连时需要新的)\n abortController = new AbortController()\n \n const headers: Record<string, string> = {\n 'Accept': 'text/event-stream',\n ...defaultHeaders,\n ...options?.headers,\n }\n \n // SSE 规范:发送 Last-Event-ID 用于断点续传\n if (lastEventId) {\n headers['Last-Event-ID'] = lastEventId\n }\n \n const response = await fetch(url.toString(), {\n method: 'GET',\n headers,\n signal: abortController.signal,\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`)\n }\n\n if (!response.body) {\n throw new Error('No response body')\n }\n\n // 连接成功,重置重连计数\n connected = true\n reconnectCount = 0\n callbacks.onOpen?.()\n\n const reader = response.body.getReader()\n \n for await (const event of parseSSEStream(reader)) {\n // 保存最后的事件 ID 用于重连\n if (event.id) {\n lastEventId = event.id\n }\n \n // 服务端可以通过 retry 字段动态调整重连间隔\n // 这里不改变配置,仅记录\n \n if (event.event === 'error') {\n callbacks.onError?.(new Error(String(event.data)))\n } else {\n callbacks.onMessage(event.data as TData)\n }\n }\n\n // 流正常结束\n connected = false\n callbacks.onClose?.()\n \n } catch (error) {\n connected = false\n \n // 用户主动取消,不重连\n if ((error as Error).name === 'AbortError' || isUnsubscribed) {\n return\n }\n \n callbacks.onError?.(error as Error)\n \n // 自动重连\n if (reconnectCount < maxReconnects) {\n reconnectCount++\n callbacks.onReconnect?.(reconnectCount, maxReconnects)\n \n // 延迟后重连\n setTimeout(() => {\n if (!isUnsubscribed) {\n connect()\n }\n }, reconnectInterval)\n } else {\n // 达到最大重连次数\n callbacks.onMaxReconnects?.()\n }\n }\n }\n\n connect()\n\n return {\n unsubscribe: () => {\n isUnsubscribed = true\n abortController?.abort()\n abortController = null\n connected = false\n },\n get connected() {\n return connected\n }\n }\n }\n\n // 创建端点代理\n function createEndpoint(basePath: string): unknown {\n const methods = ['get', 'post', 'put', 'patch', 'delete']\n \n // 创建可调用的代理(支持参数化路由 api.users({ id }) 调用)\n const handler = (params: Record<string, string>) => {\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n\n return new Proxy(handler as unknown as object, {\n get(_, prop: string) {\n // HTTP 方法\n if (methods.includes(prop)) {\n const httpMethod = prop.toUpperCase()\n return (data?: unknown, cfg?: RequestConfig) => {\n return request(httpMethod, basePath, data, cfg)\n }\n }\n \n // SSE 订阅\n if (prop === 'subscribe') {\n return <TData>(\n queryOrCallbacks: Record<string, unknown> | SSECallbacks<TData>,\n callbacksOrOptions?: SSECallbacks<TData> | SSESubscribeOptions,\n options?: SSESubscribeOptions\n ) => {\n // 判断第一个参数是 query 还是 callbacks\n if (typeof queryOrCallbacks === 'object' && 'onMessage' in queryOrCallbacks) {\n // subscribe(callbacks, options)\n return subscribe<TData>(\n basePath, \n undefined, \n queryOrCallbacks as SSECallbacks<TData>,\n callbacksOrOptions as SSESubscribeOptions\n )\n } else {\n // subscribe(query, callbacks, options)\n return subscribe<TData>(\n basePath,\n queryOrCallbacks as Record<string, unknown>,\n callbacksOrOptions as SSECallbacks<TData>,\n options\n )\n }\n }\n }\n \n // 嵌套路径\n const childPath = `${basePath}/${prop}`\n return createEndpoint(childPath)\n },\n apply(_, __, args) {\n // 调用函数处理参数化路由\n const params = args[0] as Record<string, string>\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n })\n }\n\n // 根代理\n return new Proxy({} as EdenClient<T>, {\n get(_, prop: string) {\n return createEndpoint(`/${prop}`)\n }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core/eden.ts"],"sourcesContent":["/**\n * Eden 风格 API 客户端\n * \n * 最自然的链式调用:\n * - api.users.get() // GET /users\n * - api.users.post({ name }) // POST /users\n * - api.users({ id }).get() // GET /users/:id\n * - api.users({ id }).delete() // DELETE /users/:id\n * - api.chat.stream.subscribe() // SSE 流式响应\n * \n * @example\n * ```typescript\n * import { defineRoute } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 定义路由(保留类型)\n * const routeDefinitions = [\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], page: query.page })\n * })\n * ] as const\n * \n * // 客户端推断类型\n * type Api = InferEden<typeof routeDefinitions>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全调用\n * const { data } = await api.users.get({ page: 1 })\n * ```\n */\n\nimport type { ApiResponse, ApiError, RequestConfig } from '../types'\n\n// ============= 配置 =============\n\nexport interface EdenConfig {\n headers?: Record<string, string>\n onRequest?: (request: Request) => Request | Promise<Request>\n onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>\n onError?: (error: ApiError) => void\n timeout?: number\n}\n\n// ============= SSE 类型 =============\n\nexport interface SSEEvent<T = unknown> {\n event?: string\n data: T\n id?: string\n retry?: number\n}\n\nexport interface SSESubscribeOptions {\n headers?: Record<string, string>\n reconnectInterval?: number\n maxReconnects?: number\n timeout?: number\n}\n\nexport interface SSESubscription<T = unknown> {\n unsubscribe: () => void\n readonly connected: boolean\n}\n\n// ============= 基础类型工具 =============\n\n/** 从 TypeBox Schema 提取静态类型 */\ntype InferStatic<T> = T extends { static: infer S } ? S : T\n\n/** 检查是否是 SSE Handler */\ntype IsSSEHandler<T> = T extends { __sse: { readonly __brand: 'SSE' } } ? true : false\n\n/** 从 Schema 对象提取各部分类型 */\ntype GetSchemaQuery<S> = S extends { query: infer Q } ? InferStatic<Q> : undefined\ntype GetSchemaBody<S> = S extends { body: infer B } ? InferStatic<B> : undefined\ntype GetSchemaParams<S> = S extends { params: infer P } ? InferStatic<P> : undefined\n\n/** \n * 从 handler 函数推断返回类型\n * handler: (ctx) => TReturn | Promise<TReturn>\n */\ntype InferHandlerReturn<H> = H extends (...args: never[]) => infer R\n ? R extends Promise<infer T> ? T : R\n : unknown\n\n// ============= 路径处理 =============\n\n/** 移除开头斜杠:/users → users */\ntype TrimSlash<P extends string> = P extends `/${infer R}` ? R : P\n\n/** 获取第一段:users/posts → users */\ntype Head<P extends string> = P extends `${infer H}/${string}` ? H : P\n\n/** 获取剩余段:users/posts → posts */\ntype Tail<P extends string> = P extends `${string}/${infer T}` ? T : never\n\n/** 检查是否是动态参数段::id → true */\ntype IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false\n\n// ============= 清理 undefined 字段 =============\n\ntype Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }\n\n// ============= SSE 标记类型 =============\n\ntype SSEBrand = { readonly __brand: 'SSE' }\n\n// ============= 核心类型推断(适配新的 defineRoute) =============\n\n/**\n * 从 defineRoute 返回的路由配置构建方法定义\n * \n * defineRoute 返回的 LeafRouteConfig 结构:\n * {\n * method: TMethod,\n * path: TPath,\n * schema?: TSchema,\n * handler: (ctx) => TReturn | Promise<TReturn>\n * }\n */\ntype BuildMethodDef<R> = R extends {\n readonly schema?: infer TSchema\n readonly handler: infer THandler\n}\n ? Clean<{\n query: GetSchemaQuery<TSchema>\n body: GetSchemaBody<TSchema>\n params: GetSchemaParams<TSchema>\n return: InferHandlerReturn<THandler>\n }>\n : Clean<{\n return: R extends { readonly handler: infer H } ? InferHandlerReturn<H> : unknown\n }>\n\n/**\n * 递归构建嵌套路径结构\n * \n * 处理动态参数:/users/:id → { users: { ':id': { ... } } }\n */\ntype BuildPath<Path extends string, Method extends string, Def> =\n Path extends `${infer First}/${infer Rest}`\n ? IsDynamicSegment<First> extends true\n ? { ':id': BuildPath<Rest, Method, Def> }\n : { [K in First]: BuildPath<Rest, Method, Def> }\n : IsDynamicSegment<Path> extends true\n ? { ':id': { [M in Method]: Def } }\n : Path extends ''\n ? { [M in Method]: Def }\n : { [K in Path]: { [M in Method]: Def } }\n\n/**\n * 从单个路由生成嵌套类型结构\n */\ntype RouteToTree<R> = R extends {\n readonly method: infer M extends string\n readonly path: infer P extends string\n}\n ? BuildPath<TrimSlash<P>, Lowercase<M>, BuildMethodDef<R>>\n : {}\n\n// ============= 深度合并多个路由 =============\n\ntype DeepMerge<A, B> = {\n [K in keyof A | keyof B]: \n K extends keyof A & keyof B\n ? A[K] extends object\n ? B[K] extends object\n ? DeepMerge<A[K], B[K]>\n : A[K] & B[K]\n : A[K] & B[K]\n : K extends keyof A\n ? A[K]\n : K extends keyof B\n ? B[K]\n : never\n}\n\n/** 递归合并路由数组为单一类型结构 */\ntype MergeRoutes<T extends readonly unknown[]> = \n T extends readonly [infer First]\n ? RouteToTree<First>\n : T extends readonly [infer First, ...infer Rest]\n ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>>\n : {}\n\n/**\n * 从 defineRoute 数组自动推断 Eden 契约\n * \n * @example\n * ```typescript\n * import { defineRoute, defineRoutes, Type } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 定义路由(使用 as const 保留字面量类型)\n * const routeDefinitions = [\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], total: 0 })\n * }),\n * defineRoute({\n * method: 'POST',\n * path: '/users',\n * schema: { body: Type.Object({ name: Type.String() }) },\n * handler: ({ body }) => ({ id: '1', name: body.name })\n * })\n * ] as const\n * \n * // 服务端\n * const routes = defineRoutes(routeDefinitions)\n * const server = new Server(routes)\n * \n * // 客户端类型推断\n * type Api = InferEden<typeof routeDefinitions>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全的调用\n * const { data } = await api.users.get({ page: 1 }) // ✅ query 类型推断\n * const { data: user } = await api.users.post({ name: 'John' }) // ✅ body 类型推断\n * ```\n */\nexport type InferEden<T extends readonly unknown[]> = MergeRoutes<T>\n\n// ============= 契约类型(手动定义时使用) =============\n\ninterface MethodDef {\n query?: unknown\n body?: unknown\n params?: unknown\n return: unknown\n sse?: SSEBrand\n}\n\ntype RouteNode = {\n get?: MethodDef\n post?: MethodDef\n put?: MethodDef\n patch?: MethodDef\n delete?: MethodDef\n ':id'?: RouteNode\n [key: string]: MethodDef | RouteNode | undefined\n}\n\n// ============= 客户端类型 =============\n\ninterface SSECallbacks<T> {\n onMessage: (data: T) => void\n onError?: (error: ApiError) => void\n onOpen?: () => void\n onClose?: () => void\n onReconnect?: (attempt: number, maxAttempts: number) => void\n onMaxReconnects?: () => void\n}\n\ntype MethodCall<M extends MethodDef, HasParams extends boolean = false> = \n M extends { sse: SSEBrand }\n ? M extends { query: infer Q }\n ? (query: Q, callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : (callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : HasParams extends true\n ? M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { query: infer Q }\n ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n\ntype IsSSEEndpoint<M> = M extends { sse: { readonly __brand: 'SSE' } } ? true : false\n\ntype Endpoint<T, HasParams extends boolean = false> = \n {\n [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: \n T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never\n } \n & (T extends { get: infer M extends MethodDef }\n ? IsSSEEndpoint<M> extends true \n ? { subscribe: MethodCall<M, HasParams> }\n : {}\n : {})\n\ntype HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'\n\nexport type EdenClient<T, HasParams extends boolean = false> = {\n [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: \n T[K] extends { ':id': infer Child }\n ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false>\n : EdenClient<T[K], false>\n} & Endpoint<T, HasParams>\n\n// ============= SSE 解析器 =============\n\nasync function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<SSEEvent, void, unknown> {\n const decoder = new TextDecoder();\n let buffer = '';\n \n while (true) {\n const { done, value } = await reader.read();\n \n if (done) break;\n \n buffer += decoder.decode(value, { stream: true });\n \n const events = buffer.split('\\n\\n');\n buffer = events.pop() || '';\n \n for (const eventStr of events) {\n if (!eventStr.trim()) continue;\n \n const event: SSEEvent = { data: '' };\n const lines = eventStr.split('\\n');\n let dataLines: string[] = [];\n \n for (const line of lines) {\n if (line.startsWith('event:')) {\n event.event = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trim());\n } else if (line.startsWith('id:')) {\n event.id = line.slice(3).trim();\n } else if (line.startsWith('retry:')) {\n event.retry = parseInt(line.slice(6).trim(), 10);\n }\n }\n \n const dataStr = dataLines.join('\\n');\n \n try {\n event.data = JSON.parse(dataStr);\n } catch {\n event.data = dataStr;\n }\n \n yield event;\n }\n }\n}\n\n// ============= 实现 =============\n\n/**\n * 创建 Eden 风格的类型安全 API 客户端\n */\nexport function eden<T>(\n baseURL: string,\n config?: EdenConfig\n): EdenClient<T> {\n const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {}\n\n async function request<TReturn>(\n method: string,\n path: string,\n data?: unknown,\n requestConfig?: RequestConfig\n ): Promise<ApiResponse<TReturn>> {\n const url = new URL(path, baseURL)\n \n if (method === 'GET' && data && typeof data === 'object') {\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...defaultHeaders,\n ...requestConfig?.headers,\n }\n\n const controller = new AbortController()\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n \n const userSignal = requestConfig?.signal\n const requestTimeout = requestConfig?.timeout ?? timeout\n \n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n } else {\n userSignal.addEventListener('abort', () => controller.abort())\n }\n }\n \n if (requestTimeout) {\n timeoutId = setTimeout(() => controller.abort(), requestTimeout)\n }\n\n const fetchOptions: RequestInit = { \n method, \n headers,\n signal: controller.signal \n }\n\n if (method !== 'GET' && method !== 'HEAD' && data) {\n fetchOptions.body = JSON.stringify(data)\n }\n\n let req = new Request(url.toString(), fetchOptions)\n \n if (onRequest) {\n req = await onRequest(req)\n }\n\n try {\n const response = await fetch(req)\n \n if (timeoutId) clearTimeout(timeoutId)\n \n const contentType = response.headers.get('content-type')\n let responseData: TReturn | null = null\n \n if (contentType?.includes('application/json')) {\n responseData = await response.json()\n } else if (contentType?.includes('text/')) {\n responseData = await response.text() as unknown as TReturn\n }\n\n let result: ApiResponse<TReturn>\n \n if (response.ok) {\n result = { data: responseData, error: null }\n } else {\n const errorBody = responseData as { code?: number; message?: string } | null\n result = {\n data: null,\n error: {\n code: errorBody?.code ?? response.status,\n message: errorBody?.message ?? `HTTP ${response.status}`\n }\n }\n }\n\n if (onResponse) {\n result = await onResponse(result)\n }\n\n if (result.error && onError) {\n onError(result.error)\n }\n\n return result\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n const apiError: ApiError = { code: 0, message: err.message || '网络错误' }\n if (onError) onError(apiError)\n return {\n data: null,\n error: apiError,\n }\n }\n }\n\n function subscribe<TData>(\n path: string,\n query: Record<string, unknown> | undefined,\n callbacks: SSECallbacks<TData>,\n options?: SSESubscribeOptions\n ): SSESubscription<TData> {\n const url = new URL(path, baseURL)\n \n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let abortController: AbortController | null = new AbortController()\n let connected = false\n let reconnectCount = 0\n let isUnsubscribed = false\n let lastEventId: string | undefined\n \n const reconnectInterval = options?.reconnectInterval ?? 3000\n const maxReconnects = options?.maxReconnects ?? 5\n\n const connect = async () => {\n if (isUnsubscribed) return\n \n try {\n abortController = new AbortController()\n \n const headers: Record<string, string> = {\n 'Accept': 'text/event-stream',\n ...defaultHeaders,\n ...options?.headers,\n }\n \n if (lastEventId) {\n headers['Last-Event-ID'] = lastEventId\n }\n \n const response = await fetch(url.toString(), {\n method: 'GET',\n headers,\n signal: abortController.signal,\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`)\n }\n\n if (!response.body) {\n throw new Error('No response body')\n }\n\n connected = true\n reconnectCount = 0\n callbacks.onOpen?.()\n\n const reader = response.body.getReader()\n \n for await (const event of parseSSEStream(reader)) {\n if (event.id) {\n lastEventId = event.id\n }\n \n if (event.event === 'error') {\n callbacks.onError?.({ code: -1, message: String(event.data) })\n } else {\n callbacks.onMessage(event.data as TData)\n }\n }\n\n connected = false\n callbacks.onClose?.()\n \n } catch (error) {\n connected = false\n \n if ((error as Error).name === 'AbortError' || isUnsubscribed) {\n return\n }\n \n const err = error instanceof Error ? error : new Error(String(error))\n callbacks.onError?.({ code: 0, message: err.message || 'SSE 连接错误' })\n \n if (reconnectCount < maxReconnects) {\n reconnectCount++\n callbacks.onReconnect?.(reconnectCount, maxReconnects)\n \n setTimeout(() => {\n if (!isUnsubscribed) {\n connect()\n }\n }, reconnectInterval)\n } else {\n callbacks.onMaxReconnects?.()\n }\n }\n }\n\n connect()\n\n return {\n unsubscribe: () => {\n isUnsubscribed = true\n abortController?.abort()\n abortController = null\n connected = false\n },\n get connected() {\n return connected\n }\n }\n }\n\n function createEndpoint(basePath: string): unknown {\n const methods = ['get', 'post', 'put', 'patch', 'delete']\n \n const handler = (params: Record<string, string>) => {\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n\n return new Proxy(handler as unknown as object, {\n get(_, prop: string) {\n if (methods.includes(prop)) {\n const httpMethod = prop.toUpperCase()\n return (data?: unknown, cfg?: RequestConfig) => {\n return request(httpMethod, basePath, data, cfg)\n }\n }\n \n if (prop === 'subscribe') {\n return <TData>(\n queryOrCallbacks: Record<string, unknown> | SSECallbacks<TData>,\n callbacksOrOptions?: SSECallbacks<TData> | SSESubscribeOptions,\n options?: SSESubscribeOptions\n ) => {\n if (typeof queryOrCallbacks === 'object' && 'onMessage' in queryOrCallbacks) {\n return subscribe<TData>(\n basePath, \n undefined, \n queryOrCallbacks as SSECallbacks<TData>,\n callbacksOrOptions as SSESubscribeOptions\n )\n } else {\n return subscribe<TData>(\n basePath,\n queryOrCallbacks as Record<string, unknown>,\n callbacksOrOptions as SSECallbacks<TData>,\n options\n )\n }\n }\n }\n \n const childPath = `${basePath}/${prop}`\n return createEndpoint(childPath)\n },\n apply(_, __, args) {\n const params = args[0] as Record<string, string>\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n })\n }\n\n return new Proxy({} as EdenClient<T>, {\n get(_, prop: string) {\n return createEndpoint(`/${prop}`)\n }\n })\n}\n"],"mappings":";AAySA,gBAAgB,eACd,QACyC;CACzC,MAAM,UAAU,IAAI,aAAa;CACjC,IAAI,SAAS;AAEb,QAAO,MAAM;EACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAE3C,MAAI,KAAM;AAEV,YAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;EAEjD,MAAM,SAAS,OAAO,MAAM,OAAO;AACnC,WAAS,OAAO,KAAK,IAAI;AAEzB,OAAK,MAAM,YAAY,QAAQ;AAC7B,OAAI,CAAC,SAAS,MAAM,CAAE;GAEtB,MAAM,QAAkB,EAAE,MAAM,IAAI;GACpC,MAAM,QAAQ,SAAS,MAAM,KAAK;GAClC,IAAI,YAAsB,EAAE;AAE5B,QAAK,MAAM,QAAQ,MACjB,KAAI,KAAK,WAAW,SAAS,CAC3B,OAAM,QAAQ,KAAK,MAAM,EAAE,CAAC,MAAM;YACzB,KAAK,WAAW,QAAQ,CACjC,WAAU,KAAK,KAAK,MAAM,EAAE,CAAC,MAAM,CAAC;YAC3B,KAAK,WAAW,MAAM,CAC/B,OAAM,KAAK,KAAK,MAAM,EAAE,CAAC,MAAM;YACtB,KAAK,WAAW,SAAS,CAClC,OAAM,QAAQ,SAAS,KAAK,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG;GAIpD,MAAM,UAAU,UAAU,KAAK,KAAK;AAEpC,OAAI;AACF,UAAM,OAAO,KAAK,MAAM,QAAQ;WAC1B;AACN,UAAM,OAAO;;AAGf,SAAM;;;;;;;AAUZ,SAAgB,KACd,SACA,QACe;CACf,MAAM,EAAE,SAAS,gBAAgB,WAAW,YAAY,SAAS,YAAY,UAAU,EAAE;CAEzF,eAAe,QACb,QACA,MACA,MACA,eAC+B;EAC/B,MAAM,MAAM,IAAI,IAAI,MAAM,QAAQ;AAElC,MAAI,WAAW,SAAS,QAAQ,OAAO,SAAS,UAC9C;QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAgC,CACxE,KAAI,UAAU,UAAa,UAAU,KACnC,KAAI,aAAa,IAAI,KAAK,OAAO,MAAM,CAAC;;EAK9C,MAAM,UAAkC;GACtC,gBAAgB;GAChB,GAAG;GACH,GAAG,eAAe;GACnB;EAED,MAAM,aAAa,IAAI,iBAAiB;EACxC,IAAI;EAEJ,MAAM,aAAa,eAAe;EAClC,MAAM,iBAAiB,eAAe,WAAW;AAEjD,MAAI,WACF,KAAI,WAAW,QACb,YAAW,OAAO;MAElB,YAAW,iBAAiB,eAAe,WAAW,OAAO,CAAC;AAIlE,MAAI,eACF,aAAY,iBAAiB,WAAW,OAAO,EAAE,eAAe;EAGlE,MAAM,eAA4B;GAChC;GACA;GACA,QAAQ,WAAW;GACpB;AAED,MAAI,WAAW,SAAS,WAAW,UAAU,KAC3C,cAAa,OAAO,KAAK,UAAU,KAAK;EAG1C,IAAI,MAAM,IAAI,QAAQ,IAAI,UAAU,EAAE,aAAa;AAEnD,MAAI,UACF,OAAM,MAAM,UAAU,IAAI;AAG5B,MAAI;GACF,MAAM,WAAW,MAAM,MAAM,IAAI;AAEjC,OAAI,UAAW,cAAa,UAAU;GAEtC,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe;GACxD,IAAI,eAA+B;AAEnC,OAAI,aAAa,SAAS,mBAAmB,CAC3C,gBAAe,MAAM,SAAS,MAAM;YAC3B,aAAa,SAAS,QAAQ,CACvC,gBAAe,MAAM,SAAS,MAAM;GAGtC,IAAI;AAEJ,OAAI,SAAS,GACX,UAAS;IAAE,MAAM;IAAc,OAAO;IAAM;QACvC;IACL,MAAM,YAAY;AAClB,aAAS;KACP,MAAM;KACN,OAAO;MACL,MAAM,WAAW,QAAQ,SAAS;MAClC,SAAS,WAAW,WAAW,QAAQ,SAAS;MACjD;KACF;;AAGH,OAAI,WACF,UAAS,MAAM,WAAW,OAAO;AAGnC,OAAI,OAAO,SAAS,QAClB,SAAQ,OAAO,MAAM;AAGvB,UAAO;WACA,OAAO;GAEd,MAAM,WAAqB;IAAE,MAAM;IAAG,UAD1B,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,EAClB,WAAW;IAAQ;AACtE,OAAI,QAAS,SAAQ,SAAS;AAC9B,UAAO;IACL,MAAM;IACN,OAAO;IACR;;;CAIL,SAAS,UACP,MACA,OACA,WACA,SACwB;EACxB,MAAM,MAAM,IAAI,IAAI,MAAM,QAAQ;AAElC,MAAI,OACF;QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,CAC9C,KAAI,UAAU,UAAa,UAAU,KACnC,KAAI,aAAa,IAAI,KAAK,OAAO,MAAM,CAAC;;EAK9C,IAAI,kBAA0C,IAAI,iBAAiB;EACnE,IAAI,YAAY;EAChB,IAAI,iBAAiB;EACrB,IAAI,iBAAiB;EACrB,IAAI;EAEJ,MAAM,oBAAoB,SAAS,qBAAqB;EACxD,MAAM,gBAAgB,SAAS,iBAAiB;EAEhD,MAAM,UAAU,YAAY;AAC1B,OAAI,eAAgB;AAEpB,OAAI;AACF,sBAAkB,IAAI,iBAAiB;IAEvC,MAAM,UAAkC;KACtC,UAAU;KACV,GAAG;KACH,GAAG,SAAS;KACb;AAED,QAAI,YACF,SAAQ,mBAAmB;IAG7B,MAAM,WAAW,MAAM,MAAM,IAAI,UAAU,EAAE;KAC3C,QAAQ;KACR;KACA,QAAQ,gBAAgB;KACzB,CAAC;AAEF,QAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,QAAQ,SAAS,SAAS;AAG5C,QAAI,CAAC,SAAS,KACZ,OAAM,IAAI,MAAM,mBAAmB;AAGrC,gBAAY;AACZ,qBAAiB;AACjB,cAAU,UAAU;IAEpB,MAAM,SAAS,SAAS,KAAK,WAAW;AAExC,eAAW,MAAM,SAAS,eAAe,OAAO,EAAE;AAChD,SAAI,MAAM,GACR,eAAc,MAAM;AAGtB,SAAI,MAAM,UAAU,QAClB,WAAU,UAAU;MAAE,MAAM;MAAI,SAAS,OAAO,MAAM,KAAK;MAAE,CAAC;SAE9D,WAAU,UAAU,MAAM,KAAc;;AAI5C,gBAAY;AACZ,cAAU,WAAW;YAEd,OAAO;AACd,gBAAY;AAEZ,QAAK,MAAgB,SAAS,gBAAgB,eAC5C;IAGF,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AACrE,cAAU,UAAU;KAAE,MAAM;KAAG,SAAS,IAAI,WAAW;KAAY,CAAC;AAEpE,QAAI,iBAAiB,eAAe;AAClC;AACA,eAAU,cAAc,gBAAgB,cAAc;AAEtD,sBAAiB;AACf,UAAI,CAAC,eACH,UAAS;QAEV,kBAAkB;UAErB,WAAU,mBAAmB;;;AAKnC,WAAS;AAET,SAAO;GACL,mBAAmB;AACjB,qBAAiB;AACjB,qBAAiB,OAAO;AACxB,sBAAkB;AAClB,gBAAY;;GAEd,IAAI,YAAY;AACd,WAAO;;GAEV;;CAGH,SAAS,eAAe,UAA2B;EACjD,MAAM,UAAU;GAAC;GAAO;GAAQ;GAAO;GAAS;GAAS;EAEzD,MAAM,WAAW,WAAmC;GAClD,MAAM,aAAa,OAAO,OAAO,OAAO,CAAC;AAEzC,UAAO,eADS,GAAG,SAAS,GAAG,mBAAmB,WAAW,GAC/B;;AAGhC,SAAO,IAAI,MAAM,SAA8B;GAC7C,IAAI,GAAG,MAAc;AACnB,QAAI,QAAQ,SAAS,KAAK,EAAE;KAC1B,MAAM,aAAa,KAAK,aAAa;AACrC,aAAQ,MAAgB,QAAwB;AAC9C,aAAO,QAAQ,YAAY,UAAU,MAAM,IAAI;;;AAInD,QAAI,SAAS,YACX,SACE,kBACA,oBACA,YACG;AACH,SAAI,OAAO,qBAAqB,YAAY,eAAe,iBACzD,QAAO,UACL,UACA,QACA,kBACA,mBACD;SAED,QAAO,UACL,UACA,kBACA,oBACA,QACD;;AAMP,WAAO,eADW,GAAG,SAAS,GAAG,OACD;;GAElC,MAAM,GAAG,IAAI,MAAM;IACjB,MAAM,SAAS,KAAK;IACpB,MAAM,aAAa,OAAO,OAAO,OAAO,CAAC;AAEzC,WAAO,eADS,GAAG,SAAS,GAAG,mBAAmB,WAAW,GAC/B;;GAEjC,CAAC;;AAGJ,QAAO,IAAI,MAAM,EAAE,EAAmB,EACpC,IAAI,GAAG,MAAc;AACnB,SAAO,eAAe,IAAI,OAAO;IAEpC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vafast/api-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Type-safe API client for Vafast framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"release": "npm run build && npm run test && npx bumpp && npm publish --access=public"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"vafast": "
|
|
17
|
+
"vafast": "^0.5.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"vafast": "
|
|
20
|
+
"vafast": "^0.5.1",
|
|
21
21
|
"tsdown": "^0.19.0-beta.4",
|
|
22
22
|
"typescript": "^5.5.3",
|
|
23
23
|
"vitest": "^2.1.8"
|