@vafast/api-client 0.1.3 → 0.1.5

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/TODO.md DELETED
@@ -1,83 +0,0 @@
1
- # TODO: 类型同步功能
2
-
3
- ## 背景
4
-
5
- 前后端分离项目中,共享类型需要发布 npm 包,流程繁琐且需要私有 npm 服务。
6
-
7
- ## 目标
8
-
9
- 实现一条命令同步 API 类型,无需 npm 发包。
10
-
11
- ## 方案设计
12
-
13
- ### 1. 服务端:暴露类型端点
14
-
15
- ```typescript
16
- // vafast 自动注册端点
17
- // GET /__vafast__/types
18
-
19
- import { serve } from 'vafast'
20
-
21
- serve({
22
- routes,
23
- exposeTypes: true // 开启类型导出端点
24
- })
25
- ```
26
-
27
- 返回内容:
28
- ```typescript
29
- // 自动生成的类型定义
30
- export const routes = [...] as const
31
- export type AppRoutes = typeof routes
32
- ```
33
-
34
- ### 2. 客户端 CLI
35
-
36
- ```bash
37
- # 安装
38
- npm install -D @vafast/cli
39
-
40
- # 同步类型
41
- npx vafast sync --url http://localhost:3000
42
-
43
- # 或指定输出路径
44
- npx vafast sync --url http://localhost:3000 --out src/api.d.ts
45
- ```
46
-
47
- ### 3. 自动化(可选)
48
-
49
- ```json
50
- // package.json
51
- {
52
- "scripts": {
53
- "dev": "vafast sync --url $API_URL && vite",
54
- "build": "vafast sync --url $API_URL && vite build"
55
- }
56
- }
57
- ```
58
-
59
- ## 实现步骤
60
-
61
- - [ ] vafast 核心:添加 `exposeTypes` 选项
62
- - [ ] vafast 核心:实现 `/__vafast__/types` 端点
63
- - [ ] vafast 核心:实现类型序列化(保留字面量类型)
64
- - [ ] @vafast/cli:创建 CLI 包
65
- - [ ] @vafast/cli:实现 `sync` 命令
66
- - [ ] @vafast/cli:支持配置文件 `.vafastrc`
67
-
68
- ## 技术难点
69
-
70
- 1. **类型序列化**:如何将运行时的路由定义导出为 TypeScript 类型字符串
71
- 2. **TypeBox Schema 转换**:将 TypeBox schema 转为 TypeScript 类型
72
- 3. **字面量保留**:确保 `'GET'`、`'/users'` 等字面量类型不被扩展
73
-
74
- ## 参考
75
-
76
- - tRPC:需要 monorepo 或 npm 包共享类型
77
- - Elysia Eden:同样需要共享代码
78
- - OpenAPI Generator:从 JSON Schema 生成类型(可参考)
79
-
80
- ## 优先级
81
-
82
- 中等 - 当前可用方案(npm link / monorepo)能满足需求,此功能为优化体验。
83
-
package/build.ts DELETED
@@ -1,37 +0,0 @@
1
- import { $ } from "bun";
2
- import { build, type Options } from "tsup";
3
-
4
- await $`rm -rf dist`;
5
-
6
- const tsupConfig: Options = {
7
- entry: ["src/**/*.ts"],
8
- splitting: false,
9
- sourcemap: false,
10
- clean: true,
11
- bundle: true,
12
- } satisfies Options;
13
-
14
- await Promise.all([
15
- // ? tsup esm
16
- build({
17
- outDir: "dist",
18
- format: "esm",
19
- target: "node20",
20
- cjsInterop: false,
21
- ...tsupConfig,
22
- }),
23
- // ? tsup cjs
24
- build({
25
- outDir: "dist/cjs",
26
- format: "cjs",
27
- target: "node20",
28
- // dts: true,
29
- ...tsupConfig,
30
- }),
31
- ]);
32
-
33
- await $`tsc --project tsconfig.dts.json`;
34
-
35
- await Promise.all([$`cp dist/*.d.ts dist/cjs`]);
36
-
37
- process.exit();
@@ -1,223 +0,0 @@
1
- /**
2
- * ✨ 自动从 vafast 路由推断契约
3
- *
4
- * 特性:
5
- * 1. 使用 defineRoutes() 自动保留字面量类型
6
- * 2. 支持 SSE 流式响应
7
- * 3. 完整的类型推断
8
- */
9
-
10
- import {
11
- defineRoutes,
12
- createHandler,
13
- createSSEHandler,
14
- Type
15
- } from 'vafast'
16
- import { eden, InferEden } from '../src'
17
-
18
- // ============= 业务类型定义 =============
19
-
20
- interface User {
21
- id: string
22
- name: string
23
- email: string
24
- }
25
-
26
- interface ChatMessage {
27
- text: string
28
- timestamp?: number
29
- }
30
-
31
- // ============= 服务端:定义路由 =============
32
-
33
- /**
34
- * ✨ defineRoutes() 自动保留字面量类型,无需 as const!
35
- */
36
- const routes = defineRoutes([
37
- // GET /users - 获取用户列表
38
- {
39
- method: 'GET',
40
- path: '/users',
41
- handler: createHandler(
42
- {
43
- query: Type.Object({
44
- page: Type.Optional(Type.Number({ default: 1 })),
45
- limit: Type.Optional(Type.Number({ default: 10 }))
46
- })
47
- },
48
- async ({ query }) => ({
49
- users: [] as User[],
50
- total: 0,
51
- page: query.page ?? 1,
52
- limit: query.limit ?? 10
53
- })
54
- )
55
- },
56
-
57
- // POST /users - 创建用户
58
- {
59
- method: 'POST',
60
- path: '/users',
61
- handler: createHandler(
62
- { body: Type.Object({ name: Type.String(), email: Type.String() }) },
63
- async ({ body }) => ({
64
- id: crypto.randomUUID(),
65
- name: body.name,
66
- email: body.email
67
- } as User)
68
- )
69
- },
70
-
71
- // GET /users/:id - 获取单个用户
72
- {
73
- method: 'GET',
74
- path: '/users/:id',
75
- handler: createHandler(
76
- { params: Type.Object({ id: Type.String() }) },
77
- async ({ params }) => ({
78
- id: params.id,
79
- name: 'User',
80
- email: 'user@example.com'
81
- } as User | null)
82
- )
83
- },
84
-
85
- // PUT /users/:id - 更新用户
86
- {
87
- method: 'PUT',
88
- path: '/users/:id',
89
- handler: createHandler(
90
- {
91
- params: Type.Object({ id: Type.String() }),
92
- body: Type.Object({
93
- name: Type.Optional(Type.String()),
94
- email: Type.Optional(Type.String())
95
- })
96
- },
97
- async ({ params, body }) => ({
98
- id: params.id,
99
- name: body?.name ?? 'User',
100
- email: body?.email ?? 'user@example.com'
101
- } as User)
102
- )
103
- },
104
-
105
- // DELETE /users/:id - 删除用户
106
- {
107
- method: 'DELETE',
108
- path: '/users/:id',
109
- handler: createHandler(
110
- { params: Type.Object({ id: Type.String() }) },
111
- async () => ({ success: true, deletedAt: new Date().toISOString() })
112
- )
113
- },
114
-
115
- // 🌊 GET /chat/stream - SSE 流式响应
116
- {
117
- method: 'GET',
118
- path: '/chat/stream',
119
- handler: createSSEHandler(
120
- { query: Type.Object({ prompt: Type.String() }) },
121
- async function* ({ query }) {
122
- // 模拟 AI 流式响应
123
- yield { event: 'start', data: { message: 'Starting...' } }
124
-
125
- const words = `Hello! You said: "${query.prompt}"`.split(' ')
126
- for (const word of words) {
127
- yield { data: { text: word + ' ' } as ChatMessage }
128
- await new Promise(r => setTimeout(r, 100))
129
- }
130
-
131
- yield { event: 'end', data: { message: 'Done!' } }
132
- }
133
- )
134
- }
135
- ])
136
-
137
- // ============= 🎉 自动推断契约类型!=============
138
-
139
- /**
140
- * 从路由定义自动推断 API 契约
141
- * 无需手动定义任何接口!无需 as const!
142
- */
143
- type Api = InferEden<typeof routes>
144
-
145
- // ============= 客户端:完全类型安全的调用 =============
146
-
147
- const api = eden<Api>('http://localhost:3000', {
148
- headers: {
149
- 'Authorization': 'Bearer your-token-here'
150
- },
151
- timeout: 5000,
152
- onError: (error) => {
153
- console.error('API Error:', error.message)
154
- }
155
- })
156
-
157
- async function main() {
158
- console.log('=== 自动推断契约示例(无需 as const)===\n')
159
-
160
- // ✅ GET /users?page=1&limit=10
161
- const usersResult = await api.users.get({ page: 1, limit: 10 })
162
- if (usersResult.data) {
163
- console.log('📋 用户列表:', usersResult.data.users)
164
- console.log(' 总数:', usersResult.data.total)
165
- }
166
-
167
- // ✅ POST /users
168
- const newUserResult = await api.users.post({
169
- name: 'John Doe',
170
- email: 'john@example.com'
171
- })
172
- if (newUserResult.data) {
173
- console.log('\n✨ 新用户:', newUserResult.data.name)
174
- }
175
-
176
- // ✅ GET /users/:id
177
- const userResult = await api.users({ id: '123' }).get()
178
- if (userResult.data) {
179
- console.log('\n👤 用户详情:', userResult.data.name)
180
- }
181
-
182
- // ✅ PUT /users/:id
183
- const updateResult = await api.users({ id: '123' }).put({ name: 'Jane' })
184
- if (updateResult.data) {
185
- console.log('\n📝 更新后:', updateResult.data.name)
186
- }
187
-
188
- // ✅ DELETE /users/:id
189
- const deleteResult = await api.users({ id: '123' }).delete()
190
- if (deleteResult.data) {
191
- console.log('\n🗑️ 删除成功:', deleteResult.data.success)
192
- }
193
-
194
- // 🌊 SSE 流式响应
195
- console.log('\n=== SSE 流式响应 ===\n')
196
-
197
- // SSE 返回类型目前是 unknown,需要手动断言
198
- // 未来版本会改进 SSE 返回类型推断
199
- const subscription = api.chat.stream.subscribe(
200
- { prompt: 'Hello AI!' },
201
- {
202
- onOpen: () => console.log('📡 连接已建立'),
203
- onMessage: (data: unknown) => {
204
- console.log('收到消息:', data)
205
- },
206
- onError: (err) => console.error('❌ 错误:', err.message),
207
- onClose: () => console.log('📴 连接已关闭')
208
- }
209
- )
210
-
211
- // 5 秒后取消订阅
212
- setTimeout(() => {
213
- subscription.unsubscribe()
214
- console.log('\n\n=== 示例完成 ===')
215
- }, 5000)
216
- }
217
-
218
- main().catch(console.error)
219
-
220
- // ============= 导出 =============
221
-
222
- export { routes, api }
223
- export type { Api }
@@ -1,192 +0,0 @@
1
- /**
2
- * SSE 端到端测试
3
- * 测试功能:
4
- * 1. 请求取消 (AbortController)
5
- * 2. SSE 自动重连
6
- */
7
-
8
- import {
9
- defineRoutes,
10
- route,
11
- createHandler,
12
- createSSEHandler,
13
- Type,
14
- serve
15
- } from 'vafast'
16
- import { eden, InferEden } from '../src'
17
-
18
- // 定义路由
19
- const routes = defineRoutes([
20
- // 普通 GET 请求
21
- route('GET', '/hello', createHandler(
22
- { query: Type.Object({ name: Type.Optional(Type.String()) }) },
23
- async ({ query }) => ({ message: `Hello, ${query.name || 'World'}!` })
24
- )),
25
-
26
- // 慢请求(用于测试取消)
27
- route('GET', '/slow', createHandler(
28
- {},
29
- async () => {
30
- await new Promise(r => setTimeout(r, 5000))
31
- return { message: 'Slow response' }
32
- }
33
- )),
34
-
35
- // SSE 流式响应
36
- route('GET', '/stream', createSSEHandler(
37
- { query: Type.Object({ count: Type.Optional(Type.Number({ default: 5 })) }) },
38
- async function* ({ query }) {
39
- const count = query.count ?? 5
40
-
41
- yield { event: 'start', data: { message: '开始流式传输...' } }
42
-
43
- for (let i = 1; i <= count; i++) {
44
- yield { id: String(i), data: { index: i, text: `消息 ${i}/${count}` } }
45
- await new Promise(r => setTimeout(r, 200))
46
- }
47
-
48
- yield { event: 'end', data: { message: '传输完成!' } }
49
- }
50
- ))
51
- ])
52
-
53
- type Api = InferEden<typeof routes>
54
-
55
- async function main() {
56
- // 启动服务器
57
- console.log('🚀 启动服务器...')
58
- const server = serve({
59
- fetch: (req) => {
60
- const url = new URL(req.url)
61
-
62
- // 简单路由
63
- if (url.pathname === '/hello') {
64
- const name = url.searchParams.get('name') || 'World'
65
- return new Response(JSON.stringify({ message: `Hello, ${name}!` }), {
66
- headers: { 'Content-Type': 'application/json' }
67
- })
68
- }
69
-
70
- // 慢请求
71
- if (url.pathname === '/slow') {
72
- return new Promise(resolve => {
73
- setTimeout(() => {
74
- resolve(new Response(JSON.stringify({ message: 'Slow response' }), {
75
- headers: { 'Content-Type': 'application/json' }
76
- }))
77
- }, 5000)
78
- })
79
- }
80
-
81
- if (url.pathname === '/stream') {
82
- const count = parseInt(url.searchParams.get('count') || '5')
83
-
84
- const stream = new ReadableStream({
85
- async start(controller) {
86
- const encoder = new TextEncoder()
87
-
88
- controller.enqueue(encoder.encode(`event: start\ndata: ${JSON.stringify({ message: '开始流式传输...' })}\n\n`))
89
-
90
- for (let i = 1; i <= count; i++) {
91
- controller.enqueue(encoder.encode(`id: ${i}\ndata: ${JSON.stringify({ index: i, text: `消息 ${i}/${count}` })}\n\n`))
92
- await new Promise(r => setTimeout(r, 200))
93
- }
94
-
95
- controller.enqueue(encoder.encode(`event: end\ndata: ${JSON.stringify({ message: '传输完成!' })}\n\n`))
96
- controller.close()
97
- }
98
- })
99
-
100
- return new Response(stream, {
101
- headers: {
102
- 'Content-Type': 'text/event-stream',
103
- 'Cache-Control': 'no-cache',
104
- 'Connection': 'keep-alive'
105
- }
106
- })
107
- }
108
-
109
- return new Response('Not Found', { status: 404 })
110
- },
111
- port: 3456
112
- })
113
-
114
- console.log('✅ 服务器启动在 http://localhost:3456\n')
115
-
116
- // 等待服务器启动
117
- await new Promise(r => setTimeout(r, 500))
118
-
119
- // 创建客户端
120
- const api = eden<Api>('http://localhost:3456')
121
-
122
- // ============= 测试 1: 请求取消 =============
123
- console.log('🧪 测试 1: 请求取消')
124
-
125
- const controller = new AbortController()
126
-
127
- // 发起慢请求
128
- const slowPromise = api.slow.get(undefined, { signal: controller.signal })
129
-
130
- // 100ms 后取消
131
- setTimeout(() => {
132
- controller.abort()
133
- console.log(' ⏹️ 请求已取消')
134
- }, 100)
135
-
136
- const result = await slowPromise
137
- // 取消后 status 为 0,error 可能是 AbortError 或 "This operation was aborted"
138
- if (result.status === 0 && result.error) {
139
- console.log(' ✅ 请求取消成功 (error:', result.error.message || result.error.name, ')\n')
140
- } else {
141
- console.log(' ❌ 请求取消失败: status=', result.status, '\n')
142
- }
143
-
144
- // ============= 测试 2: 普通请求 =============
145
- console.log('🧪 测试 2: 普通请求')
146
- const helloResult = await api.hello.get({ name: 'TypeScript' })
147
- console.log(' 响应:', helloResult.data)
148
- console.log()
149
-
150
- // ============= 测试 3: SSE 流式响应 =============
151
- console.log('🧪 测试 3: SSE 流式响应')
152
-
153
- await new Promise<void>((resolve) => {
154
- const sub = api.stream.subscribe(
155
- { count: 3 },
156
- {
157
- onOpen: () => console.log(' 📡 连接已建立'),
158
- onMessage: (data: unknown) => {
159
- console.log(' 📨', data)
160
- },
161
- onError: (err) => console.log(' ❌ 错误:', err.message),
162
- onClose: () => {
163
- console.log(' 📴 连接已关闭')
164
- resolve()
165
- },
166
- onReconnect: (attempt, max) => {
167
- console.log(` 🔄 重连中 (${attempt}/${max})...`)
168
- },
169
- onMaxReconnects: () => {
170
- console.log(' ⚠️ 达到最大重连次数')
171
- }
172
- },
173
- {
174
- reconnectInterval: 1000,
175
- maxReconnects: 3
176
- }
177
- )
178
-
179
- // 5 秒超时
180
- setTimeout(() => {
181
- sub.unsubscribe()
182
- resolve()
183
- }, 5000)
184
- })
185
-
186
- console.log('\n✅ 所有测试完成!')
187
-
188
- // 关闭服务器
189
- server.stop()
190
- }
191
-
192
- main().catch(console.error)