@vafast/cli 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/README.md +64 -10
- package/bin/vafast.js +0 -0
- package/dist/cli.js +4 -3
- package/dist/commands/sync.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{sync-Bokzgg9Z.js → sync-CS1-nDMt.js} +88 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,18 @@ npx vafast sync --url http://localhost:3000 --endpoint /api/contract
|
|
|
32
32
|
| `--url <url>` | 服务端地址(必填) | - |
|
|
33
33
|
| `--out <path>` | 输出文件路径 | `src/api.generated.ts` |
|
|
34
34
|
| `--endpoint <path>` | 契约接口路径 | `/__contract__` |
|
|
35
|
+
| `--strip-prefix <prefix>` | 去掉路径前缀 | - |
|
|
36
|
+
|
|
37
|
+
**示例:**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 去掉 /restfulApi 前缀
|
|
41
|
+
npx vafast sync \
|
|
42
|
+
--url http://localhost:9002 \
|
|
43
|
+
--endpoint /restfulApi/api-spec \
|
|
44
|
+
--out src/types/api/ones.generated.ts \
|
|
45
|
+
--strip-prefix /restfulApi
|
|
46
|
+
```
|
|
35
47
|
|
|
36
48
|
## 工作流程
|
|
37
49
|
|
|
@@ -66,13 +78,23 @@ npx vafast sync --url http://localhost:3000
|
|
|
66
78
|
### 3. 使用生成的类型
|
|
67
79
|
|
|
68
80
|
```typescript
|
|
69
|
-
import {
|
|
70
|
-
import
|
|
81
|
+
import { createClient } from '@vafast/api-client'
|
|
82
|
+
import { createApiClient } from './api.generated'
|
|
71
83
|
|
|
72
|
-
|
|
84
|
+
// 创建底层客户端
|
|
85
|
+
const client = createClient({
|
|
86
|
+
baseURL: 'http://localhost:3000',
|
|
87
|
+
timeout: 30000
|
|
88
|
+
})
|
|
73
89
|
|
|
74
|
-
//
|
|
90
|
+
// 创建类型安全的 API 客户端
|
|
91
|
+
const api = createApiClient(client)
|
|
92
|
+
|
|
93
|
+
// 类型安全的调用(错误路径会被 TypeScript 检测)
|
|
75
94
|
const { data, error } = await api.users.get({ page: 1 })
|
|
95
|
+
|
|
96
|
+
// ❌ TypeScript 会报错
|
|
97
|
+
// api.nonExistent.get() // Error: Property 'nonExistent' does not exist
|
|
76
98
|
```
|
|
77
99
|
|
|
78
100
|
## 自动化
|
|
@@ -82,9 +104,12 @@ const { data, error } = await api.users.get({ page: 1 })
|
|
|
82
104
|
```json
|
|
83
105
|
{
|
|
84
106
|
"scripts": {
|
|
85
|
-
"sync": "vafast sync --url
|
|
86
|
-
"
|
|
87
|
-
"
|
|
107
|
+
"sync:auth": "vafast sync --url http://localhost:9003 --endpoint /authRestfulApi/api-spec --out src/types/api/auth.generated.ts --strip-prefix /authRestfulApi",
|
|
108
|
+
"sync:ones": "vafast sync --url http://localhost:9002 --endpoint /restfulApi/api-spec --out src/types/api/ones.generated.ts --strip-prefix /restfulApi",
|
|
109
|
+
"sync:billing": "vafast sync --url http://localhost:9004 --endpoint /billingRestfulApi/api-spec --out src/types/api/billing.generated.ts --strip-prefix /billingRestfulApi",
|
|
110
|
+
"sync:types": "npm run sync:auth && npm run sync:billing && npm run sync:ones",
|
|
111
|
+
"dev": "vite",
|
|
112
|
+
"build": "npm run sync:types && vite build"
|
|
88
113
|
}
|
|
89
114
|
}
|
|
90
115
|
```
|
|
@@ -113,28 +138,57 @@ const { data, error } = await api.users.get({ page: 1 })
|
|
|
113
138
|
生成的类型:
|
|
114
139
|
|
|
115
140
|
```typescript
|
|
141
|
+
import type { ApiResponse, RequestConfig, Client, EdenClient } from '@vafast/api-client'
|
|
142
|
+
import { eden } from '@vafast/api-client'
|
|
143
|
+
|
|
144
|
+
/** API 契约类型 */
|
|
116
145
|
export type Api = {
|
|
117
146
|
users: {
|
|
118
147
|
get: {
|
|
119
148
|
query: { page?: number }
|
|
120
|
-
return:
|
|
149
|
+
return: any
|
|
121
150
|
}
|
|
122
151
|
post: {
|
|
123
152
|
body: { name?: string }
|
|
124
|
-
return:
|
|
153
|
+
return: any
|
|
125
154
|
}
|
|
126
155
|
}
|
|
127
156
|
}
|
|
157
|
+
|
|
158
|
+
/** API 客户端类型别名 */
|
|
159
|
+
export type ApiClientType = EdenClient<Api>
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 创建类型安全的 API 客户端
|
|
163
|
+
*/
|
|
164
|
+
export function createApiClient(client: Client): EdenClient<Api> {
|
|
165
|
+
return eden<Api>(client)
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**使用方式:**
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { createClient } from '@vafast/api-client'
|
|
173
|
+
import { createApiClient } from './api.generated'
|
|
174
|
+
|
|
175
|
+
const client = createClient({ baseURL: '/api', timeout: 30000 })
|
|
176
|
+
const api = createApiClient(client)
|
|
177
|
+
|
|
178
|
+
// 完整的类型安全
|
|
179
|
+
const { data, error } = await api.users.post({ name: 'John' })
|
|
128
180
|
```
|
|
129
181
|
|
|
130
182
|
## 注意事项
|
|
131
183
|
|
|
132
|
-
1.
|
|
184
|
+
1. **返回类型**:如果后端未定义 `response` schema,生成的返回类型为 `any`(渐进式类型安全)。建议后端添加 `response` schema 获得完整类型检查。
|
|
133
185
|
|
|
134
186
|
2. **服务器必须运行**:执行 `sync` 命令时,服务端必须在运行并暴露契约接口。
|
|
135
187
|
|
|
136
188
|
3. **不要手动修改**:生成的文件会被覆盖,请勿手动修改。
|
|
137
189
|
|
|
190
|
+
4. **类型安全**:生成的 `createApiClient` 返回 `EdenClient<Api>`,TypeScript 会检测错误的 API 路径。
|
|
191
|
+
|
|
138
192
|
## License
|
|
139
193
|
|
|
140
194
|
MIT
|
package/bin/vafast.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { syncTypes } from "./sync-
|
|
1
|
+
import { syncTypes } from "./sync-CS1-nDMt.js";
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
const cli = cac("vafast");
|
|
6
|
-
cli.command("sync", "从服务端同步 API 类型定义").option("--url <url>", "服务端地址(必填)").option("--out <path>", "输出文件路径", { default: "src/api.generated.ts" }).option("--endpoint <path>", "API Spec 接口路径", { default: "/api-spec" }).action(async (options) => {
|
|
6
|
+
cli.command("sync", "从服务端同步 API 类型定义").option("--url <url>", "服务端地址(必填)").option("--out <path>", "输出文件路径", { default: "src/api.generated.ts" }).option("--endpoint <path>", "API Spec 接口路径", { default: "/api-spec" }).option("--strip-prefix <prefix>", "从路径中去掉的前缀(如 /billingRestfulApi)").action(async (options) => {
|
|
7
7
|
if (!options.url) {
|
|
8
8
|
console.error("❌ 请指定服务端地址:--url <url>");
|
|
9
9
|
process.exit(1);
|
|
@@ -11,7 +11,8 @@ cli.command("sync", "从服务端同步 API 类型定义").option("--url <url>",
|
|
|
11
11
|
await syncTypes({
|
|
12
12
|
url: options.url,
|
|
13
13
|
output: options.out,
|
|
14
|
-
endpoint: options.endpoint
|
|
14
|
+
endpoint: options.endpoint,
|
|
15
|
+
stripPrefix: options.stripPrefix
|
|
15
16
|
});
|
|
16
17
|
});
|
|
17
18
|
cli.help();
|
package/dist/commands/sync.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -74,7 +74,7 @@ function objectToType(schema) {
|
|
|
74
74
|
* 同步 API 类型
|
|
75
75
|
*/
|
|
76
76
|
async function syncTypes(options) {
|
|
77
|
-
const { url, output, endpoint } = options;
|
|
77
|
+
const { url, output, endpoint, stripPrefix } = options;
|
|
78
78
|
console.log(`🔄 正在从 ${url}${endpoint} 获取契约...`);
|
|
79
79
|
const contractUrl = new URL(endpoint, url).toString();
|
|
80
80
|
let contract;
|
|
@@ -88,7 +88,8 @@ async function syncTypes(options) {
|
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
90
|
console.log(`✅ 获取到 ${contract.routes.length} 个路由`);
|
|
91
|
-
|
|
91
|
+
if (stripPrefix) console.log(`🔧 去掉路径前缀: ${stripPrefix}`);
|
|
92
|
+
const typeContent = generateTypeDefinition(contract, stripPrefix);
|
|
92
93
|
const outputDir = dirname(output);
|
|
93
94
|
mkdirSync(outputDir, { recursive: true });
|
|
94
95
|
writeFileSync(output, typeContent, "utf-8");
|
|
@@ -102,7 +103,7 @@ async function syncTypes(options) {
|
|
|
102
103
|
/**
|
|
103
104
|
* 生成类型定义文件内容
|
|
104
105
|
*/
|
|
105
|
-
function generateTypeDefinition(contract) {
|
|
106
|
+
function generateTypeDefinition(contract, stripPrefix) {
|
|
106
107
|
const lines = [];
|
|
107
108
|
lines.push("/**");
|
|
108
109
|
lines.push(" * 自动生成的 API 类型定义");
|
|
@@ -112,20 +113,97 @@ function generateTypeDefinition(contract) {
|
|
|
112
113
|
lines.push(" * ⚠️ 请勿手动修改此文件,使用 `vafast sync` 重新生成");
|
|
113
114
|
lines.push(" */");
|
|
114
115
|
lines.push("");
|
|
115
|
-
|
|
116
|
+
lines.push("import type { ApiResponse, RequestConfig, Client, EdenClient } from '@vafast/api-client'");
|
|
117
|
+
lines.push("import { eden } from '@vafast/api-client'");
|
|
118
|
+
lines.push("");
|
|
119
|
+
const routeTree = buildRouteTree(contract.routes, stripPrefix);
|
|
120
|
+
lines.push("/** API 契约类型 */");
|
|
116
121
|
lines.push("export type Api = {");
|
|
117
122
|
lines.push(generateRouteTreeType(routeTree, 1));
|
|
118
123
|
lines.push("}");
|
|
119
124
|
lines.push("");
|
|
125
|
+
lines.push("/** API 客户端类型(提供完整的 IDE 智能提示) */");
|
|
126
|
+
lines.push("export interface ApiClient {");
|
|
127
|
+
lines.push(generateClientType(routeTree, 1));
|
|
128
|
+
lines.push("}");
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("/** API 客户端类型别名(基于 EdenClient 推断,提供完整类型检查) */");
|
|
131
|
+
lines.push("export type ApiClientType = EdenClient<Api>");
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("/**");
|
|
134
|
+
lines.push(" * 创建类型安全的 API 客户端");
|
|
135
|
+
lines.push(" * ");
|
|
136
|
+
lines.push(" * @example");
|
|
137
|
+
lines.push(" * ```typescript");
|
|
138
|
+
lines.push(" * import { createClient } from '@vafast/api-client'");
|
|
139
|
+
lines.push(" * import { createApiClient } from './api.generated'");
|
|
140
|
+
lines.push(" * ");
|
|
141
|
+
lines.push(" * const client = createClient('/api').use(authMiddleware)");
|
|
142
|
+
lines.push(" * const api = createApiClient(client)");
|
|
143
|
+
lines.push(" * ");
|
|
144
|
+
lines.push(" * // 完整的 IDE 智能提示和类型检查");
|
|
145
|
+
lines.push(" * const { data, error } = await api.users.find.post({ current: 1, pageSize: 10 })");
|
|
146
|
+
lines.push(" * // ❌ 错误路径会被 TypeScript 检测到");
|
|
147
|
+
lines.push(" * // api.nonExistent.post() // Error: Property 'nonExistent' does not exist");
|
|
148
|
+
lines.push(" * ```");
|
|
149
|
+
lines.push(" */");
|
|
150
|
+
lines.push("export function createApiClient(client: Client): EdenClient<Api> {");
|
|
151
|
+
lines.push(" return eden<Api>(client)");
|
|
152
|
+
lines.push("}");
|
|
153
|
+
lines.push("");
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 生成客户端接口类型(带完整方法签名,IDE 友好)
|
|
158
|
+
*/
|
|
159
|
+
function generateClientType(tree, indent) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
const pad = " ".repeat(indent);
|
|
162
|
+
for (const [key, node] of tree) {
|
|
163
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(key) || /^\d/.test(key);
|
|
164
|
+
const propName = needsQuotes ? `'${key}'` : key;
|
|
165
|
+
lines.push(`${pad}${propName}: {`);
|
|
166
|
+
for (const [method, route] of node.methods) {
|
|
167
|
+
if (route.description) lines.push(`${pad} /** ${route.description} */`);
|
|
168
|
+
const methodSig = generateMethodSignature(route, method);
|
|
169
|
+
lines.push(`${pad} ${method}: ${methodSig}`);
|
|
170
|
+
}
|
|
171
|
+
if (node.children.size > 0) {
|
|
172
|
+
const childContent = generateClientType(node.children, indent + 1);
|
|
173
|
+
if (childContent) lines.push(childContent);
|
|
174
|
+
}
|
|
175
|
+
lines.push(`${pad}}`);
|
|
176
|
+
}
|
|
120
177
|
return lines.join("\n");
|
|
121
178
|
}
|
|
122
179
|
/**
|
|
180
|
+
* 生成方法签名(函数类型)
|
|
181
|
+
*/
|
|
182
|
+
function generateMethodSignature(route, method) {
|
|
183
|
+
const params = [];
|
|
184
|
+
if (route.schema?.body) {
|
|
185
|
+
const bodyType = schemaToType(route.schema.body);
|
|
186
|
+
params.push(`body: ${bodyType}`);
|
|
187
|
+
}
|
|
188
|
+
if (route.schema?.query) {
|
|
189
|
+
const queryType = schemaToType(route.schema.query);
|
|
190
|
+
params.push(`query?: ${queryType}`);
|
|
191
|
+
}
|
|
192
|
+
params.push("config?: RequestConfig");
|
|
193
|
+
const returnType = route.schema?.response ? schemaToType(route.schema.response) : "any";
|
|
194
|
+
return `(${params.join(", ")}) => Promise<ApiResponse<${returnType}>>`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
123
197
|
* 构建路由树
|
|
124
198
|
*/
|
|
125
|
-
function buildRouteTree(routes) {
|
|
199
|
+
function buildRouteTree(routes, stripPrefix) {
|
|
126
200
|
const root = /* @__PURE__ */ new Map();
|
|
201
|
+
const normalizedPrefix = stripPrefix ? "/" + stripPrefix.replace(/^\/+|\/+$/g, "") : void 0;
|
|
127
202
|
for (const route of routes) {
|
|
128
|
-
|
|
203
|
+
let path = route.path;
|
|
204
|
+
if (normalizedPrefix && path.startsWith(normalizedPrefix)) path = path.slice(normalizedPrefix.length) || "/";
|
|
205
|
+
const segments = path.split("/").filter(Boolean);
|
|
206
|
+
if (segments.length === 0) continue;
|
|
129
207
|
let current = root;
|
|
130
208
|
for (let i = 0; i < segments.length; i++) {
|
|
131
209
|
const segment = segments[i];
|
|
@@ -183,7 +261,10 @@ function generateMethodType(route) {
|
|
|
183
261
|
const paramsType = schemaToType(route.schema.params);
|
|
184
262
|
parts.push(`params: ${paramsType}`);
|
|
185
263
|
}
|
|
186
|
-
|
|
264
|
+
if (route.schema?.response) {
|
|
265
|
+
const responseType = schemaToType(route.schema.response);
|
|
266
|
+
parts.push(`return: ${responseType}`);
|
|
267
|
+
} else parts.push("return: any");
|
|
187
268
|
if (parts.length === 1) return `{ ${parts[0]} }`;
|
|
188
269
|
return `{\n ${parts.join("\n ")}\n }`;
|
|
189
270
|
}
|