@wx-sab/renkei 1.2.4 → 1.3.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 +166 -0
- package/lib/commands/api.js +67 -12
- package/lib/core/config-utils.d.ts +8 -0
- package/lib/core/config-utils.js +45 -0
- package/lib/core/folder-selector.d.ts +10 -0
- package/lib/core/folder-selector.js +147 -0
- package/lib/core/generate.js +33 -45
- package/lib/core/interface.d.ts +16 -0
- package/package.json +3 -1
- package/templates/api.ejs +3 -3
- package/templates/procedure-call.ejs +5 -0
package/README.md
CHANGED
|
@@ -157,8 +157,174 @@ export default defineConfig([
|
|
|
157
157
|
]);
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
### CLI 命令
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 生成所有服务的所有接口
|
|
164
|
+
renkei api
|
|
165
|
+
|
|
166
|
+
# 只生成指定服务的接口
|
|
167
|
+
renkei api -s dp-service
|
|
168
|
+
|
|
169
|
+
# 只生成指定文件夹的接口(命令行方式)
|
|
170
|
+
renkei api -f "用户管理,订单管理"
|
|
171
|
+
|
|
172
|
+
# 交互式选择要生成的文件夹
|
|
173
|
+
renkei api -i
|
|
174
|
+
|
|
175
|
+
# 组合使用:交互式选择指定服务的文件夹
|
|
176
|
+
renkei api -s dp-service -i
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 交互式选择文件夹
|
|
180
|
+
|
|
181
|
+
当运行 `renkei api -i` 时,工具会:
|
|
182
|
+
|
|
183
|
+
1. 分析每个服务的 swagger/apifox 文档
|
|
184
|
+
2. 提取所有文件夹信息(基于 `x-apifox-folder` 字段)
|
|
185
|
+
3. 显示交互式多选界面,默认选中"全部"选项
|
|
186
|
+
4. 用户可以选择:
|
|
187
|
+
- 保持选中"全部":生成所有接口
|
|
188
|
+
- 取消"全部",选择具体文件夹:只生成选中的接口
|
|
189
|
+
|
|
190
|
+
示例输出:
|
|
191
|
+
```
|
|
192
|
+
正在分析服务: dp-service
|
|
193
|
+
|
|
194
|
+
发现以下文件夹:
|
|
195
|
+
1. 用户管理 (15 个接口)
|
|
196
|
+
2. 订单管理 (23 个接口)
|
|
197
|
+
3. 商品管理 (18 个接口)
|
|
198
|
+
4. 支付管理 (12 个接口)
|
|
199
|
+
|
|
200
|
+
? 请选择要生成的文件夹 (空格选择,回车确认) (Press <space> to select, <a> to toggle all, <i> to invert selection)
|
|
201
|
+
❯◉ 全部 (共 68 个接口)
|
|
202
|
+
── 具体文件夹 ──
|
|
203
|
+
◯ 用户管理 (15 个接口)
|
|
204
|
+
◯ 订单管理 (23 个接口)
|
|
205
|
+
◯ 商品管理 (18 个接口)
|
|
206
|
+
◯ 支付管理 (12 个接口)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**操作说明:**
|
|
210
|
+
- 默认选中"全部",生成所有接口
|
|
211
|
+
- 按 `空格` 取消"全部",然后选择具体文件夹
|
|
212
|
+
- 按 `a` 切换全选/全不选
|
|
213
|
+
- 按 `i` 反选
|
|
214
|
+
- 按 `回车` 确认选择
|
|
215
|
+
|
|
216
|
+
### 文件夹过滤功能
|
|
217
|
+
|
|
218
|
+
支持三种方式指定要生成的文件夹:
|
|
219
|
+
|
|
220
|
+
#### 1. 配置文件方式
|
|
221
|
+
|
|
222
|
+
在配置文件中通过 `folders` 字段指定:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { defineConfig } from "@wx-sab/renkei";
|
|
226
|
+
|
|
227
|
+
export default defineConfig([
|
|
228
|
+
{
|
|
229
|
+
serviceName: "dp-service",
|
|
230
|
+
apifoxProjectId: "2954843",
|
|
231
|
+
// 只生成这两个文件夹的接口
|
|
232
|
+
folders: ["用户管理", "订单管理"],
|
|
233
|
+
customRequestUrl: (routeData) => {
|
|
234
|
+
return "/" + routeData["x-apifox-folder"].split("/")[0] + routeData.route;
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
]);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### 2. 命令行参数方式
|
|
241
|
+
|
|
242
|
+
使用 `-f` 或 `--folder` 参数:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# 只生成指定文件夹的接口
|
|
246
|
+
renkei api -f "用户管理,订单管理"
|
|
247
|
+
|
|
248
|
+
# 指定服务和文件夹
|
|
249
|
+
renkei api -s dp-service -f "用户管理,订单管理"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### 3. 交互式选择方式(推荐)
|
|
253
|
+
|
|
254
|
+
使用 `-i` 或 `--interactive` 参数:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# 交互式选择所有服务的文件夹
|
|
258
|
+
renkei api -i
|
|
259
|
+
|
|
260
|
+
# 交互式选择指定服务的文件夹
|
|
261
|
+
renkei api -s dp-service -i
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### 注意事项
|
|
265
|
+
|
|
266
|
+
1. **文件夹名称匹配**:文件夹名称基于 swagger/apifox 文档中的 `x-apifox-folder` 字段,取第一级目录名称
|
|
267
|
+
2. **优先级**:交互式选择 > 命令行参数 > 配置文件
|
|
268
|
+
3. **空数组**:如果 `folders` 为空数组或不设置,则生成所有接口
|
|
269
|
+
4. **多服务**:每个服务可以独立配置不同的文件夹列表
|
|
270
|
+
|
|
160
271
|
## Examples
|
|
161
272
|
|
|
273
|
+
### 文件夹过滤示例
|
|
274
|
+
|
|
275
|
+
#### 场景1:只生成特定模块的接口
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { defineConfig } from "@wx-sab/renkei";
|
|
279
|
+
|
|
280
|
+
export default defineConfig([
|
|
281
|
+
{
|
|
282
|
+
serviceName: "dp-service",
|
|
283
|
+
apifoxProjectId: "2954843",
|
|
284
|
+
// 只生成用户管理和订单管理模块的接口
|
|
285
|
+
folders: ["用户管理", "订单管理"],
|
|
286
|
+
customRequestUrl: (routeData) => {
|
|
287
|
+
return "/" + routeData["x-apifox-folder"].split("/")[0] + routeData.route;
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
]);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### 场景2:多服务配置,每个服务过滤不同文件夹
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { defineConfig } from "@wx-sab/renkei";
|
|
297
|
+
|
|
298
|
+
export default defineConfig([
|
|
299
|
+
{
|
|
300
|
+
serviceName: "dp-service",
|
|
301
|
+
apifoxProjectId: "2954843",
|
|
302
|
+
folders: ["用户管理", "订单管理"],
|
|
303
|
+
customRequestUrl: (routeData) => {
|
|
304
|
+
return "/" + routeData["x-apifox-folder"].split("/")[0] + routeData.route;
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
serviceName: "cop-profile",
|
|
309
|
+
apifoxProjectId: "1234567",
|
|
310
|
+
folders: ["个人中心", "权限管理"],
|
|
311
|
+
customRequestUrl: (routeData) => {
|
|
312
|
+
return "/cop-profile" + routeData.route;
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
]);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### 场景3:命令行快速过滤
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# 快速生成特定文件夹的接口,无需修改配置文件
|
|
322
|
+
renkei api -s dp-service -f "用户管理,订单管理,商品管理"
|
|
323
|
+
|
|
324
|
+
# 交互式选择,适合不确定有哪些文件夹时使用
|
|
325
|
+
renkei api -i
|
|
326
|
+
```
|
|
327
|
+
|
|
162
328
|
### 自定义接口的 mock 服务
|
|
163
329
|
```ts
|
|
164
330
|
import { request } from '@/utils/request'
|
package/lib/commands/api.js
CHANGED
|
@@ -1,37 +1,92 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
13
|
};
|
|
5
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const
|
|
15
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
7
16
|
const path_1 = __importDefault(require("path"));
|
|
8
17
|
const commander_1 = __importDefault(require("commander"));
|
|
9
18
|
const generate_1 = require("../core/generate");
|
|
10
19
|
const constants_1 = require("../core/constants");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const readProjectConfigFile = () => {
|
|
20
|
+
const folder_selector_1 = require("../core/folder-selector");
|
|
21
|
+
const config_utils_1 = require("../core/config-utils");
|
|
22
|
+
const readProjectConfigFile = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
15
23
|
for (const file of constants_1.CONFIG_FILE_NAMES) {
|
|
16
24
|
const fullFilePath = path_1.default.resolve(process.cwd(), file);
|
|
17
|
-
|
|
25
|
+
try {
|
|
26
|
+
yield promises_1.default.access(fullFilePath);
|
|
18
27
|
if (fullFilePath.endsWith('.ts')) {
|
|
19
28
|
return require(fullFilePath).default;
|
|
20
29
|
}
|
|
21
30
|
return require(fullFilePath);
|
|
22
31
|
}
|
|
32
|
+
catch (_a) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
return null;
|
|
25
|
-
};
|
|
26
|
-
const
|
|
27
|
-
var _a;
|
|
28
|
-
// 只生成指定服务
|
|
37
|
+
});
|
|
38
|
+
const parseCommandOptions = (param) => {
|
|
39
|
+
var _a, _b;
|
|
29
40
|
const services = (((_a = param.service) === null || _a === void 0 ? void 0 : _a.split(',')) || []).filter(Boolean);
|
|
30
|
-
const
|
|
41
|
+
const folders = (((_b = param.folder) === null || _b === void 0 ? void 0 : _b.split(',')) || []).filter(Boolean);
|
|
42
|
+
return { services, folders };
|
|
43
|
+
};
|
|
44
|
+
const handleInteractiveSelection = (config, services) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
const _configs = (0, config_utils_1.collectConfig)(config).filter((conf) => {
|
|
46
|
+
if (services.length > 0) {
|
|
47
|
+
return services.includes(conf.serviceName);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const folderSelections = yield (0, folder_selector_1.selectFoldersForServices)(_configs);
|
|
53
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections, services);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('交互式选择失败:', error.message);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const handleCommandLineFolders = (config, services, folders) => {
|
|
62
|
+
const folderMap = new Map();
|
|
63
|
+
(0, config_utils_1.collectConfig)(config).forEach((conf) => {
|
|
64
|
+
if (services.length === 0 || services.includes(conf.serviceName)) {
|
|
65
|
+
folderMap.set(conf.serviceName, folders);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderMap, services);
|
|
69
|
+
};
|
|
70
|
+
const parse = new commander_1.default.Command('api')
|
|
71
|
+
.option('-s, --service <service>', '只获取指定的服务,多个服务用","分隔', ',')
|
|
72
|
+
.option('-f, --folder <folder>', '只获取指定的文件夹,多个文件夹用","分隔', ',')
|
|
73
|
+
.option('-i, --interactive', '启用交互式选择文件夹', false)
|
|
74
|
+
.action((param) => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const { services, folders } = parseCommandOptions(param);
|
|
76
|
+
const config = yield readProjectConfigFile();
|
|
31
77
|
if (!config) {
|
|
32
78
|
console.warn('未找到配置文件');
|
|
33
79
|
return;
|
|
34
80
|
}
|
|
81
|
+
if (param.interactive) {
|
|
82
|
+
const success = yield handleInteractiveSelection(config, services);
|
|
83
|
+
if (!success) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (folders.length > 0) {
|
|
88
|
+
handleCommandLineFolders(config, services, folders);
|
|
89
|
+
}
|
|
35
90
|
(0, generate_1.generateApis)(config, services);
|
|
36
|
-
});
|
|
91
|
+
}));
|
|
37
92
|
exports.default = parse;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GenerateConfig, ServiceConfig } from './interface';
|
|
2
|
+
export declare const SKIP_ROUTE_MARKER: "__SKIP__";
|
|
3
|
+
export declare const collectConfig: (conf: GenerateConfig | GenerateConfig[], collects?: GenerateConfig[]) => GenerateConfig[];
|
|
4
|
+
export interface FolderSelectionResult {
|
|
5
|
+
serviceName: string;
|
|
6
|
+
folders: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare const applyFoldersToConfig: (conf: ServiceConfig | ServiceConfig[], folderSelections: Map<string, string[]>, services?: string[]) => void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.applyFoldersToConfig = exports.collectConfig = exports.SKIP_ROUTE_MARKER = void 0;
|
|
7
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
8
|
+
exports.SKIP_ROUTE_MARKER = '__SKIP__';
|
|
9
|
+
const collectConfig = (conf, collects = []) => {
|
|
10
|
+
if (!conf)
|
|
11
|
+
return collects;
|
|
12
|
+
if (Array.isArray(conf)) {
|
|
13
|
+
conf.forEach((con) => (0, exports.collectConfig)(con, collects));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
collects.push(lodash_1.default.omit(conf, 'projects'));
|
|
17
|
+
if (Array.isArray(conf.projects)) {
|
|
18
|
+
conf.projects.forEach((project) => (0, exports.collectConfig)(Object.assign(Object.assign({}, lodash_1.default.omit(conf, 'projects')), project), collects));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return collects;
|
|
22
|
+
};
|
|
23
|
+
exports.collectConfig = collectConfig;
|
|
24
|
+
const applyFoldersToConfig = (conf, folderSelections, services = []) => {
|
|
25
|
+
if (Array.isArray(conf)) {
|
|
26
|
+
conf.forEach((c) => (0, exports.applyFoldersToConfig)(c, folderSelections, services));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const serviceName = conf.serviceName;
|
|
30
|
+
const selectedFolders = folderSelections.get(serviceName);
|
|
31
|
+
if (selectedFolders && selectedFolders.length > 0) {
|
|
32
|
+
conf.folders = selectedFolders;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(conf.projects)) {
|
|
35
|
+
conf.projects.forEach((p) => {
|
|
36
|
+
const projectServiceName = p.serviceName || serviceName;
|
|
37
|
+
const projectFolders = folderSelections.get(projectServiceName);
|
|
38
|
+
if (projectFolders && projectFolders.length > 0) {
|
|
39
|
+
p.folders = projectFolders;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.applyFoldersToConfig = applyFoldersToConfig;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GenerateConfig } from './interface';
|
|
2
|
+
export interface FolderInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const fetchSwaggerDocument: (config: GenerateConfig) => Promise<any>;
|
|
8
|
+
export declare const extractFoldersFromSwagger: (swaggerDoc: any) => FolderInfo[];
|
|
9
|
+
export declare const selectFolders: (folders: FolderInfo[]) => Promise<string[]>;
|
|
10
|
+
export declare const selectFoldersForServices: (configs: GenerateConfig[]) => Promise<Map<string, string[]>>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.selectFoldersForServices = exports.selectFolders = exports.extractFoldersFromSwagger = exports.fetchSwaggerDocument = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
18
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
19
|
+
const utils_1 = require("./utils");
|
|
20
|
+
const fetchSwaggerDocument = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
const finalConfig = (0, utils_1.generateConfig)(config);
|
|
23
|
+
if (!finalConfig.source) {
|
|
24
|
+
throw new Error('未配置文档源');
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = yield axios_1.default.get(finalConfig.source, {
|
|
28
|
+
timeout: 30000,
|
|
29
|
+
validateStatus: (status) => status < 400
|
|
30
|
+
});
|
|
31
|
+
return response.data;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = ((_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || error.message;
|
|
35
|
+
throw new Error(`获取文档失败: ${message}`, { cause: error });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
exports.fetchSwaggerDocument = fetchSwaggerDocument;
|
|
39
|
+
const extractFoldersFromSwagger = (swaggerDoc) => {
|
|
40
|
+
const folderMap = new Map();
|
|
41
|
+
if (swaggerDoc.paths) {
|
|
42
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
43
|
+
if (methods && typeof methods === 'object') {
|
|
44
|
+
Object.values(methods).forEach((methodInfo) => {
|
|
45
|
+
if (methodInfo && methodInfo['x-apifox-folder']) {
|
|
46
|
+
const folder = methodInfo['x-apifox-folder'];
|
|
47
|
+
const folderPath = folder.split('/')[0];
|
|
48
|
+
if (folderMap.has(folderPath)) {
|
|
49
|
+
const info = folderMap.get(folderPath);
|
|
50
|
+
info.count++;
|
|
51
|
+
info.paths.add(path);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
folderMap.set(folderPath, {
|
|
55
|
+
count: 1,
|
|
56
|
+
paths: new Set([path])
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const folders = [];
|
|
65
|
+
folderMap.forEach((info, name) => {
|
|
66
|
+
folders.push({
|
|
67
|
+
name,
|
|
68
|
+
path: name,
|
|
69
|
+
count: info.count
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return lodash_1.default.sortBy(folders, ['name']);
|
|
73
|
+
};
|
|
74
|
+
exports.extractFoldersFromSwagger = extractFoldersFromSwagger;
|
|
75
|
+
const selectFolders = (folders) => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
if (folders.length === 0) {
|
|
77
|
+
console.log('未找到任何文件夹,将生成所有接口');
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const totalCount = folders.reduce((sum, f) => sum + f.count, 0);
|
|
81
|
+
console.log('\n发现以下文件夹:');
|
|
82
|
+
folders.forEach((folder, index) => {
|
|
83
|
+
console.log(` ${index + 1}. ${folder.name} (${folder.count} 个接口)`);
|
|
84
|
+
});
|
|
85
|
+
const { selectedFolders } = yield inquirer_1.default.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'checkbox',
|
|
88
|
+
name: 'selectedFolders',
|
|
89
|
+
message: '请选择要生成的文件夹 (空格选择,回车确认)',
|
|
90
|
+
choices: [
|
|
91
|
+
{
|
|
92
|
+
name: `全部 (共 ${totalCount} 个接口)`,
|
|
93
|
+
value: '__ALL__',
|
|
94
|
+
checked: true
|
|
95
|
+
},
|
|
96
|
+
new inquirer_1.default.Separator('── 具体文件夹 ──'),
|
|
97
|
+
...folders.map(f => ({
|
|
98
|
+
name: `${f.name} (${f.count} 个接口)`,
|
|
99
|
+
value: f.name,
|
|
100
|
+
checked: false
|
|
101
|
+
}))
|
|
102
|
+
],
|
|
103
|
+
pageSize: 15,
|
|
104
|
+
loop: false
|
|
105
|
+
}
|
|
106
|
+
]);
|
|
107
|
+
// 如果选择了"全部"选项,返回空数组(表示生成所有接口)
|
|
108
|
+
if (selectedFolders.includes('__ALL__')) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return selectedFolders;
|
|
112
|
+
});
|
|
113
|
+
exports.selectFolders = selectFolders;
|
|
114
|
+
const selectFoldersForServices = (configs) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const folderSelections = new Map();
|
|
116
|
+
const failedServices = [];
|
|
117
|
+
for (const config of configs) {
|
|
118
|
+
console.log(`\n正在分析服务: ${config.serviceName}`);
|
|
119
|
+
try {
|
|
120
|
+
const swaggerDoc = yield (0, exports.fetchSwaggerDocument)(config);
|
|
121
|
+
const folders = (0, exports.extractFoldersFromSwagger)(swaggerDoc);
|
|
122
|
+
if (folders.length > 0) {
|
|
123
|
+
const selectedFolders = yield (0, exports.selectFolders)(folders);
|
|
124
|
+
folderSelections.set(config.serviceName, selectedFolders);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(` 服务 ${config.serviceName} 未找到文件夹信息,将生成所有接口`);
|
|
128
|
+
folderSelections.set(config.serviceName, []);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errorMessage = error.message || '未知错误';
|
|
133
|
+
console.error(` 获取服务 ${config.serviceName} 的文档失败: ${errorMessage}`);
|
|
134
|
+
failedServices.push({ serviceName: config.serviceName, error: errorMessage });
|
|
135
|
+
folderSelections.set(config.serviceName, []);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (failedServices.length > 0) {
|
|
139
|
+
console.log('\n⚠️ 部分服务获取失败:');
|
|
140
|
+
failedServices.forEach(({ serviceName, error }) => {
|
|
141
|
+
console.log(` - ${serviceName}: ${error}`);
|
|
142
|
+
});
|
|
143
|
+
console.log('这些服务将生成所有接口\n');
|
|
144
|
+
}
|
|
145
|
+
return folderSelections;
|
|
146
|
+
});
|
|
147
|
+
exports.selectFoldersForServices = selectFoldersForServices;
|
package/lib/core/generate.js
CHANGED
|
@@ -28,36 +28,26 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
28
28
|
const swagger_typescript_api_1 = require("swagger-typescript-api");
|
|
29
29
|
const cli_progress_1 = __importDefault(require("cli-progress"));
|
|
30
30
|
const utils_1 = require("./utils");
|
|
31
|
+
const config_utils_1 = require("./config-utils");
|
|
31
32
|
/**
|
|
32
33
|
* 单个配置生成
|
|
33
34
|
*/
|
|
34
35
|
const generate = (conf) => {
|
|
35
|
-
// 最终使用的配置项
|
|
36
36
|
const config = (0, utils_1.generateConfig)(conf);
|
|
37
|
-
// 模型命名空间
|
|
38
37
|
const modelNamespace = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.modelNamespace));
|
|
39
|
-
// 服务名
|
|
40
38
|
const serviceName = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.serviceName));
|
|
41
|
-
// 存放已生成的方法名
|
|
42
39
|
const methodNameSet = new Set();
|
|
40
|
+
const skippedRoutes = new Set();
|
|
43
41
|
return (0, swagger_typescript_api_1.generateApi)({
|
|
44
42
|
url: config.source,
|
|
45
|
-
// 文件名
|
|
46
43
|
name: config.name,
|
|
47
|
-
// 输出目录
|
|
48
44
|
output: path_1.default.resolve(config.output, config.serviceName),
|
|
49
|
-
// 模块化?
|
|
50
45
|
modular: false,
|
|
51
|
-
// 静默
|
|
52
46
|
silent: true,
|
|
53
47
|
extractRequestParams: true,
|
|
54
|
-
// 清空输出目录?
|
|
55
48
|
cleanOutput: false,
|
|
56
|
-
// 生成请求客户端
|
|
57
49
|
generateClient: false,
|
|
58
|
-
// 模板文件
|
|
59
50
|
templates: path_1.default.resolve(__dirname, "../../templates"),
|
|
60
|
-
// 额外的,此处为了生成接口
|
|
61
51
|
extraTemplates: [
|
|
62
52
|
{
|
|
63
53
|
name: "index.ts",
|
|
@@ -67,7 +57,19 @@ const generate = (conf) => {
|
|
|
67
57
|
hooks: {
|
|
68
58
|
onCreateRouteName(routeNameInfo, rawRouteInfo) {
|
|
69
59
|
var _a, _b, _c;
|
|
70
|
-
|
|
60
|
+
if (config.folders && config.folders.length > 0) {
|
|
61
|
+
const extendedRouteInfo = rawRouteInfo;
|
|
62
|
+
const folder = extendedRouteInfo['x-apifox-folder'] || '';
|
|
63
|
+
const folderPath = folder.split('/')[0];
|
|
64
|
+
if (!config.folders.includes(folderPath)) {
|
|
65
|
+
skippedRoutes.add(routeNameInfo.original);
|
|
66
|
+
return {
|
|
67
|
+
duplicate: false,
|
|
68
|
+
original: config_utils_1.SKIP_ROUTE_MARKER,
|
|
69
|
+
usage: config_utils_1.SKIP_ROUTE_MARKER
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
71
73
|
const newRoute = (_a = config.customRequestUrl) === null || _a === void 0 ? void 0 : _a.call(config, rawRouteInfo, config);
|
|
72
74
|
if (typeof newRoute === "string") {
|
|
73
75
|
rawRouteInfo.route = newRoute;
|
|
@@ -77,12 +79,10 @@ const generate = (conf) => {
|
|
|
77
79
|
// @ts-ignore
|
|
78
80
|
rawRouteInfo.mockUrl = newRoute.mockUrl;
|
|
79
81
|
}
|
|
80
|
-
// 检测路径中的常见错误
|
|
81
82
|
if (/ /.test(rawRouteInfo.route)) {
|
|
82
83
|
throw Error(`语法错误,路径中存在空格,请核对,可以尝试通过配置 customRequestUrl 进行处理,当前处理节点为:${rawRouteInfo.route}`);
|
|
83
84
|
}
|
|
84
85
|
else if (/([\u4e00-\u9fa5])/.test(rawRouteInfo.route)) {
|
|
85
|
-
// 如果路径里面包含中文,警告用户,因为通常不会写带中文的api,但是直接抄配置文件很有可能由于大小项目的apifox目录组织结构不同,导致这个问题。
|
|
86
86
|
console.warn(`警告:检测到请求路径中存在中文,这通常是因为错误配置了 customRequestUrl,请进行核对,当前处理路径为:${rawRouteInfo.route} `);
|
|
87
87
|
}
|
|
88
88
|
const methodName = (_c = (_b = config === null || config === void 0 ? void 0 : config.customRouteName) === null || _b === void 0 ? void 0 : _b.call(config, routeNameInfo, rawRouteInfo)) !== null && _c !== void 0 ? _c : (0, utils_1.generateRouteName)(rawRouteInfo.method, rawRouteInfo.route);
|
|
@@ -96,10 +96,10 @@ const generate = (conf) => {
|
|
|
96
96
|
},
|
|
97
97
|
onPrepareConfig(currentConfiguration) {
|
|
98
98
|
return Object.assign(Object.assign({}, currentConfiguration), { modelNamespace,
|
|
99
|
-
serviceName, requestTempalte: config.requestTempalte
|
|
99
|
+
serviceName, requestTempalte: config.requestTempalte, skippedRoutes,
|
|
100
|
+
SKIP_ROUTE_MARKER: config_utils_1.SKIP_ROUTE_MARKER });
|
|
100
101
|
},
|
|
101
102
|
onParseSchema(originalSchema, parsedSchema) {
|
|
102
|
-
// 针对解析出的 any 做转换,主要是 swagger 3.0
|
|
103
103
|
if (parsedSchema.name === "any" &&
|
|
104
104
|
typeof parsedSchema.content === "string" &&
|
|
105
105
|
parsedSchema.$ref) {
|
|
@@ -109,8 +109,6 @@ const generate = (conf) => {
|
|
|
109
109
|
},
|
|
110
110
|
onCreateRoute(routeData) {
|
|
111
111
|
var _a;
|
|
112
|
-
// 将 /api/v1/order-mains/{orderId} 转成 /api/v1/order-mains/${orderId}
|
|
113
|
-
// routeData.raw.route 是进过 customRequestUrl 转换过的
|
|
114
112
|
// @ts-ignore
|
|
115
113
|
routeData.request.path = (_a = routeData.raw.route) === null || _a === void 0 ? void 0 : _a.replace(/\{([^}]+)\}/g, "${$1}");
|
|
116
114
|
return routeData;
|
|
@@ -122,36 +120,17 @@ const generate = (conf) => {
|
|
|
122
120
|
});
|
|
123
121
|
};
|
|
124
122
|
exports.generate = generate;
|
|
125
|
-
|
|
126
|
-
const generateApis = (config, services // 指定服务
|
|
127
|
-
) => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
const generateApis = (config, services) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
124
|
var _a, e_1, _b, _c;
|
|
129
|
-
|
|
130
|
-
const collectConfig = (conf, collects = []) => {
|
|
131
|
-
if (!conf)
|
|
132
|
-
return collects;
|
|
133
|
-
if (Array.isArray(conf)) {
|
|
134
|
-
conf.forEach((con) => collectConfig(con, collects));
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
collects.push(lodash_1.default.omit(conf, "projects"));
|
|
138
|
-
if (Array.isArray(conf.projects)) {
|
|
139
|
-
conf.projects.forEach((project) => collectConfig(Object.assign(Object.assign({}, lodash_1.default.omit(conf, "projects")), project), collects));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return collects;
|
|
143
|
-
};
|
|
144
|
-
const _configs = collectConfig(config).filter((conf) => {
|
|
145
|
-
// 只筛选指定的服务
|
|
125
|
+
const _configs = (0, config_utils_1.collectConfig)(config).filter((conf) => {
|
|
146
126
|
if (services.length > 0) {
|
|
147
127
|
return services.includes(conf.serviceName);
|
|
148
128
|
}
|
|
149
129
|
return true;
|
|
150
130
|
});
|
|
151
|
-
// 进度条控制
|
|
152
131
|
const progress = new cli_progress_1.default.SingleBar({});
|
|
153
132
|
progress.start(_configs.length, 0);
|
|
154
|
-
|
|
133
|
+
const failedServices = [];
|
|
155
134
|
try {
|
|
156
135
|
for (var _d = true, _configs_1 = __asyncValues(_configs), _configs_1_1; _configs_1_1 = yield _configs_1.next(), _a = _configs_1_1.done, !_a; _d = true) {
|
|
157
136
|
_c = _configs_1_1.value;
|
|
@@ -162,7 +141,10 @@ const generateApis = (config, services // 指定服务
|
|
|
162
141
|
}
|
|
163
142
|
catch (e) {
|
|
164
143
|
console.error(e.message);
|
|
165
|
-
failedServices.push(
|
|
144
|
+
failedServices.push({
|
|
145
|
+
serviceName: conf.serviceName,
|
|
146
|
+
error: e.message || '未知错误'
|
|
147
|
+
});
|
|
166
148
|
}
|
|
167
149
|
finally {
|
|
168
150
|
progress.increment(1);
|
|
@@ -177,8 +159,14 @@ const generateApis = (config, services // 指定服务
|
|
|
177
159
|
finally { if (e_1) throw e_1.error; }
|
|
178
160
|
}
|
|
179
161
|
progress.stop();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
162
|
+
if (failedServices.length > 0) {
|
|
163
|
+
console.info('✅ 执行完毕,以下服务生成失败:');
|
|
164
|
+
failedServices.forEach(({ serviceName, error }) => {
|
|
165
|
+
console.info(` - ${serviceName}: ${error}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.info('✅ 执行完毕');
|
|
170
|
+
}
|
|
183
171
|
});
|
|
184
172
|
exports.generateApis = generateApis;
|
package/lib/core/interface.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { GenerateApiParams, RawRouteInfo, RouteNameInfo } from "swagger-typescript-api";
|
|
2
|
+
export interface ExtendedRawRouteInfo extends RawRouteInfo {
|
|
3
|
+
'x-apifox-folder'?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ConfigWithProjects extends BasicConfig {
|
|
6
|
+
projects?: ConfigWithProjects[];
|
|
7
|
+
}
|
|
8
|
+
export interface ServiceConfig {
|
|
9
|
+
serviceName: string;
|
|
10
|
+
folders?: string[];
|
|
11
|
+
projects?: ServiceConfig[];
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* 基础配置
|
|
4
15
|
*/
|
|
@@ -60,6 +71,11 @@ export type BasicConfig = {
|
|
|
60
71
|
* 自定义生成的接口名
|
|
61
72
|
*/
|
|
62
73
|
customRouteName?(routeNameInfo: RouteNameInfo, rawRouteInfo: RawRouteInfo): string;
|
|
74
|
+
/**
|
|
75
|
+
* 要生成的文件夹列表(文件夹名称)
|
|
76
|
+
* 如果为空数组,则生成所有接口
|
|
77
|
+
*/
|
|
78
|
+
folders?: string[];
|
|
63
79
|
};
|
|
64
80
|
/**
|
|
65
81
|
* 如果 sourceType 是 apifox,则适用该配置项
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wx-sab/renkei",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "解析swagger转为ts代码,日本动漫《游戏王》中的‘Renkei’,意为连携或协作。",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/cli-progress": "^3.11.5",
|
|
24
|
+
"@types/inquirer": "9.0.7",
|
|
24
25
|
"@types/lodash": "^4.14.196",
|
|
25
26
|
"@types/node": "^20.4.7",
|
|
26
27
|
"typescript": "^5.1.6"
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"axios": "^1.4.0",
|
|
34
35
|
"cli-progress": "^3.12.0",
|
|
35
36
|
"commander": "^11.0.0",
|
|
37
|
+
"inquirer": "8.2.6",
|
|
36
38
|
"lodash": "^4.17.21",
|
|
37
39
|
"swagger-typescript-api": "13.0.3",
|
|
38
40
|
"ts-node": "^10.9.1"
|
package/templates/api.ejs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%
|
|
2
|
-
const { apiConfig, routes, utils, config, requestTemplate, modelNamespace, serviceName, namespace, requestTempalte, modelTypes } = it;
|
|
2
|
+
const { apiConfig, routes, utils, config, requestTemplate, modelNamespace, serviceName, namespace, requestTempalte, modelTypes, SKIP_ROUTE_MARKER } = it;
|
|
3
3
|
const { info, servers, externalDocs } = apiConfig;
|
|
4
4
|
const { _, require, formatDescription } = utils;
|
|
5
5
|
|
|
@@ -92,9 +92,9 @@ export class <%~ serviceName %> {
|
|
|
92
92
|
|
|
93
93
|
<% routes.combined && routes.combined.forEach(({ routes = [], moduleName }) => { %>
|
|
94
94
|
<% routes.forEach((route) => { %>
|
|
95
|
-
|
|
95
|
+
<% if (route.routeName.usage !== SKIP_ROUTE_MARKER) { %>
|
|
96
96
|
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
|
97
|
-
|
|
97
|
+
<% } %>
|
|
98
98
|
<% }) %>
|
|
99
99
|
<% }) %>
|
|
100
100
|
}
|
|
@@ -24,6 +24,11 @@ const getListItemType = (type) => type.replace(/\((\w+)\)\[\]/g, (match, key) =>
|
|
|
24
24
|
const getFullType = (type) => {
|
|
25
25
|
const originType = type;
|
|
26
26
|
|
|
27
|
+
const trimmedType = typeof type === "string" ? type.trim() : "";
|
|
28
|
+
if (trimmedType.startsWith("{") && trimmedType.endsWith("}")) {
|
|
29
|
+
return type;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
// 处理列表list
|
|
28
33
|
const matchList = type.match(/\((\w+)\)\[\]/)
|
|
29
34
|
if (matchList) {
|