@vafast/api-client 0.1.2 → 0.1.4

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.
@@ -0,0 +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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"}
package/package.json CHANGED
@@ -1,20 +1,24 @@
1
1
  {
2
2
  "name": "@vafast/api-client",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Type-safe API client for Vafast framework",
5
5
  "license": "MIT",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
6
10
  "scripts": {
7
- "build": "tsup",
11
+ "build": "tsdown",
8
12
  "dev": "npx tsx watch example/auto-infer.ts",
9
13
  "test": "npx vitest run",
10
14
  "release": "npm run build && npm run test && npx bumpp && npm publish --access=public"
11
15
  },
12
16
  "peerDependencies": {
13
- "vafast": ">=0.3.11"
17
+ "vafast": ">=0.4.23"
14
18
  },
15
19
  "devDependencies": {
16
- "vafast": ">=0.3.11",
17
- "tsup": "^8.1.0",
20
+ "vafast": ">=0.4.23",
21
+ "tsdown": "^0.19.0-beta.4",
18
22
  "typescript": "^5.5.3",
19
23
  "vitest": "^2.1.8"
20
24
  },
@@ -40,13 +44,13 @@
40
44
  ],
41
45
  "repository": {
42
46
  "type": "git",
43
- "url": "https://github.com/vafastjs/vafast-api-client"
47
+ "url": "https://github.com/vafast/vafast-api-client"
44
48
  },
45
49
  "author": {
46
50
  "name": "Vafast Team",
47
- "url": "https://github.com/vafastjs",
51
+ "url": "https://github.com/vafast",
48
52
  "email": "team@vafast.dev"
49
53
  },
50
- "homepage": "https://github.com/vafastjs/vafast-api-client",
51
- "bugs": "https://github.com/vafastjs/vafast-api-client/issues"
54
+ "homepage": "https://github.com/vafast/vafast-api-client",
55
+ "bugs": "https://github.com/vafast/vafast-api-client/issues"
52
56
  }
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();