jacky-proxy 1.0.0 → 1.0.1
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/bin/jacky-proxy.js +70 -1
- package/package.json +8 -7
- package/server.js +219 -1
- package/src/commands/migrate.js +321 -23
- package/src/commands/start.js +137 -60
- package/utils/common/match-response.ts +159 -8
package/bin/jacky-proxy.js
CHANGED
|
@@ -38,7 +38,7 @@ program
|
|
|
38
38
|
.option('-p, --port <port>', '监听端口', '5000')
|
|
39
39
|
.option('-m, --mock-id <id>', '接口集 ID(如果未在命令中指定)')
|
|
40
40
|
.option('-c, --config <path>', '配置文件路径', 'proxy.config.json')
|
|
41
|
-
.option('-s, --scenario <name>', '场景名称(当启动 Raw 文件夹时使用)', '
|
|
41
|
+
.option('-s, --scenario <name>', '场景名称(当启动 Raw 文件夹时使用)', '默认场景')
|
|
42
42
|
.option('-t, --target <path>', '目标文件夹路径(当启动 Raw 文件夹时使用)', 'mocks/test-folder')
|
|
43
43
|
.option('--ignore <interfaces>', '要忽略的接口(逗号分隔,当启动 Raw 文件夹时使用)', '')
|
|
44
44
|
.option('--no-migrate', '不自动迁移,直接启动(如果指定的是 Raw 文件夹)')
|
|
@@ -132,6 +132,75 @@ rulesCommand
|
|
|
132
132
|
await rulesTestCommand(options);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
// 在解析之前,检查第一个参数是否是 Raw 文件夹路径
|
|
136
|
+
// 如果是,自动调用 start 命令
|
|
137
|
+
const args = process.argv.slice(2);
|
|
138
|
+
if (args.length > 0) {
|
|
139
|
+
const firstArg = args[0];
|
|
140
|
+
const fs = require('fs');
|
|
141
|
+
const path = require('path');
|
|
142
|
+
|
|
143
|
+
// 检查是否是已知命令
|
|
144
|
+
const knownCommands = ['migrate', 'start', 'config', 'rules'];
|
|
145
|
+
const isKnownCommand = knownCommands.includes(firstArg);
|
|
146
|
+
|
|
147
|
+
// 如果不是已知命令,检查是否是 Raw 文件夹路径
|
|
148
|
+
if (!isKnownCommand) {
|
|
149
|
+
// 规范化路径,确保正确处理相对路径(包括 ./ 开头的路径)
|
|
150
|
+
const fullPath = path.isAbsolute(firstArg)
|
|
151
|
+
? path.resolve(firstArg)
|
|
152
|
+
: path.resolve(process.cwd(), firstArg);
|
|
153
|
+
|
|
154
|
+
// 检查是否是 Raw 文件夹路径:路径必须以 .folder 结尾,且路径存在且是目录
|
|
155
|
+
const isRawFolderPath = (firstArg.endsWith('.folder') || path.basename(fullPath).endsWith('.folder')) &&
|
|
156
|
+
fs.existsSync(fullPath) &&
|
|
157
|
+
fs.statSync(fullPath).isDirectory();
|
|
158
|
+
|
|
159
|
+
if (isRawFolderPath) {
|
|
160
|
+
// 将路径作为 start 命令的参数(使用规范化后的路径)
|
|
161
|
+
const startCommand = require('../src/commands/start');
|
|
162
|
+
const startOptions = {};
|
|
163
|
+
|
|
164
|
+
// 解析其他选项(使用 commander 的选项解析逻辑)
|
|
165
|
+
for (let i = 1; i < args.length; i++) {
|
|
166
|
+
const arg = args[i];
|
|
167
|
+
if (arg === '--port' || arg === '-p') {
|
|
168
|
+
startOptions.port = args[++i];
|
|
169
|
+
} else if (arg === '--scenario' || arg === '-s') {
|
|
170
|
+
startOptions.scenario = args[++i];
|
|
171
|
+
} else if (arg === '--target' || arg === '-t') {
|
|
172
|
+
startOptions.target = args[++i];
|
|
173
|
+
} else if (arg === '--ignore') {
|
|
174
|
+
startOptions.ignore = args[++i];
|
|
175
|
+
} else if (arg === '--debug') {
|
|
176
|
+
startOptions.debug = true;
|
|
177
|
+
} else if (arg === '--no-migrate') {
|
|
178
|
+
startOptions.noMigrate = true;
|
|
179
|
+
} else if (arg === '--config' || arg === '-c') {
|
|
180
|
+
startOptions.config = args[++i];
|
|
181
|
+
} else if (arg.startsWith('--port=')) {
|
|
182
|
+
startOptions.port = arg.split('=')[1];
|
|
183
|
+
} else if (arg.startsWith('--scenario=')) {
|
|
184
|
+
startOptions.scenario = arg.split('=')[1];
|
|
185
|
+
} else if (arg.startsWith('--target=')) {
|
|
186
|
+
startOptions.target = arg.split('=')[1];
|
|
187
|
+
} else if (arg.startsWith('--ignore=')) {
|
|
188
|
+
startOptions.ignore = arg.split('=')[1];
|
|
189
|
+
} else if (arg.startsWith('--config=')) {
|
|
190
|
+
startOptions.config = arg.split('=')[1];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 调用 start 命令,传递原始参数(startCommand 内部会再次解析)
|
|
195
|
+
startCommand(firstArg, startOptions).catch((error) => {
|
|
196
|
+
console.error('启动失败:', error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
135
204
|
// 解析命令行参数
|
|
136
205
|
program.parse();
|
|
137
206
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jacky-proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "通用 Mock 服务器 - 适用于任意网站的 HTTP 请求拦截和 Mock 响应系统",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"jacky-proxy": "bin/jacky-proxy.js"
|
|
7
|
+
"jacky-proxy": "./bin/jacky-proxy.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "npm run config:generate && node server.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
32
|
-
"url": "
|
|
32
|
+
"url": "https://github.com/wangjs-jacky/jacky-proxy.git"
|
|
33
33
|
},
|
|
34
34
|
"bugs": {
|
|
35
35
|
"url": "https://github.com/wangjs-jacky/jacky-proxy/issues"
|
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
"README.md"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"chokidar": "^3.5.3",
|
|
52
53
|
"commander": "^14.0.2",
|
|
53
54
|
"express": "^4.18.2",
|
|
54
|
-
"lodash": "^4.17.21"
|
|
55
|
+
"lodash": "^4.17.21",
|
|
56
|
+
"ts-node": "^10.9.2",
|
|
57
|
+
"typescript": "^5.3.3"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"@types/express": "^4.17.21",
|
|
58
61
|
"@types/lodash": "^4.14.202",
|
|
59
62
|
"@types/node": "^20.10.0",
|
|
60
|
-
"nodemon": "^3.0.1"
|
|
61
|
-
"ts-node": "^10.9.2",
|
|
62
|
-
"typescript": "^5.3.3"
|
|
63
|
+
"nodemon": "^3.0.1"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/server.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const express = require('express');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const chokidar = require('chokidar');
|
|
9
10
|
|
|
10
11
|
// 使用 ts-node 支持 TypeScript 文件
|
|
11
12
|
require('ts-node').register({
|
|
@@ -160,12 +161,24 @@ function loadMockConfig() {
|
|
|
160
161
|
configPath = path.join(workDir, 'proxy.config.json');
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
// 确保路径是绝对路径
|
|
165
|
+
if (!path.isAbsolute(configPath)) {
|
|
166
|
+
configPath = path.resolve(configPath);
|
|
167
|
+
}
|
|
168
|
+
|
|
163
169
|
if (fs.existsSync(configPath)) {
|
|
164
170
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
165
171
|
mockConfig = JSON.parse(configContent);
|
|
172
|
+
console.log(`✅ 已加载配置文件: ${configPath}`);
|
|
166
173
|
return mockConfig;
|
|
167
174
|
} else {
|
|
168
175
|
console.warn(`配置文件不存在: ${configPath}`);
|
|
176
|
+
if (process.env.CONFIG_PATH) {
|
|
177
|
+
console.warn(` CONFIG_PATH 环境变量: ${process.env.CONFIG_PATH}`);
|
|
178
|
+
}
|
|
179
|
+
if (process.env.WORK_DIR) {
|
|
180
|
+
console.warn(` WORK_DIR 环境变量: ${process.env.WORK_DIR}`);
|
|
181
|
+
}
|
|
169
182
|
return null;
|
|
170
183
|
}
|
|
171
184
|
} catch (error) {
|
|
@@ -290,6 +303,204 @@ let mockScenarios = {};
|
|
|
290
303
|
// 格式: Set(['getProductInfo', 'productSearch']) 表示这些接口被禁用
|
|
291
304
|
let disabledInterfaces = new Set();
|
|
292
305
|
|
|
306
|
+
// 文件监听器
|
|
307
|
+
let fileWatcher = null;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 清除模块及其所有依赖的缓存
|
|
311
|
+
*/
|
|
312
|
+
function clearModuleCache(modulePath) {
|
|
313
|
+
const resolvedPath = require.resolve(modulePath);
|
|
314
|
+
|
|
315
|
+
// 清除该模块的缓存
|
|
316
|
+
if (require.cache[resolvedPath]) {
|
|
317
|
+
const module = require.cache[resolvedPath];
|
|
318
|
+
|
|
319
|
+
// 递归清除所有子模块的缓存
|
|
320
|
+
if (module.children) {
|
|
321
|
+
module.children.forEach(child => {
|
|
322
|
+
if (child.filename && (child.filename.endsWith('.json') || child.filename.includes('base-data'))) {
|
|
323
|
+
delete require.cache[child.filename];
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
delete require.cache[resolvedPath];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 清除 base-data 目录下所有 JSON 文件的缓存
|
|
334
|
+
*/
|
|
335
|
+
function clearBaseDataCache(interfaceName) {
|
|
336
|
+
const workDir = getWorkDir();
|
|
337
|
+
const baseDataDir = path.join(workDir, 'base-data', interfaceName);
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(baseDataDir)) {
|
|
340
|
+
const files = fs.readdirSync(baseDataDir);
|
|
341
|
+
files.forEach(file => {
|
|
342
|
+
if (file.endsWith('.json')) {
|
|
343
|
+
const jsonPath = path.join(baseDataDir, file);
|
|
344
|
+
try {
|
|
345
|
+
const resolvedPath = require.resolve(jsonPath);
|
|
346
|
+
delete require.cache[resolvedPath];
|
|
347
|
+
} catch (e) {
|
|
348
|
+
// 如果文件还未被 require,忽略错误
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 重新加载单个 Mock 文件
|
|
357
|
+
*/
|
|
358
|
+
function reloadMockFile(filePath, interfaceName) {
|
|
359
|
+
try {
|
|
360
|
+
// 清除 base-data 中该接口的所有 JSON 文件缓存
|
|
361
|
+
clearBaseDataCache(interfaceName);
|
|
362
|
+
|
|
363
|
+
// 清除 Mock 文件及其所有依赖的缓存
|
|
364
|
+
clearModuleCache(filePath);
|
|
365
|
+
|
|
366
|
+
// 清除 ts-node 的编译缓存(如果存在)
|
|
367
|
+
if (require.extensions['.ts']) {
|
|
368
|
+
// ts-node 可能会缓存编译结果,清除相关缓存
|
|
369
|
+
const tsNodeCache = require.cache;
|
|
370
|
+
Object.keys(tsNodeCache).forEach(key => {
|
|
371
|
+
if (key.includes(filePath) || key.includes(interfaceName)) {
|
|
372
|
+
delete tsNodeCache[key];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 重新加载
|
|
378
|
+
const mockModule = require(filePath);
|
|
379
|
+
const mockHandler = mockModule.default || mockModule;
|
|
380
|
+
|
|
381
|
+
if (typeof mockHandler === 'function') {
|
|
382
|
+
cachedMockFiles[interfaceName] = mockHandler;
|
|
383
|
+
mockFilePaths[interfaceName] = filePath;
|
|
384
|
+
console.log(`${colors.green}🔄${colors.reset} ${colors.dim}热更新 Mock 文件:${colors.reset} ${colors.cyan}${path.basename(filePath)}${colors.reset} ${colors.gray}->${colors.reset} ${colors.bright}${interfaceName}${colors.reset}`);
|
|
385
|
+
return true;
|
|
386
|
+
} else {
|
|
387
|
+
console.warn(`${colors.yellow}⚠${colors.reset} ${colors.yellow}Mock 文件 ${colors.cyan}${path.basename(filePath)}${colors.yellow} 未导出函数${colors.reset}`);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error(`${colors.red}✗${colors.reset} ${colors.red}热更新 Mock 文件失败:${colors.reset} ${colors.cyan}${path.basename(filePath)}${colors.reset} ${colors.red}${error.message}${colors.reset}`);
|
|
392
|
+
if (error.stack) {
|
|
393
|
+
console.error(error.stack);
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 重新加载所有 Mock 文件(当 base-data 变化时)
|
|
401
|
+
*/
|
|
402
|
+
function reloadAllMockFiles() {
|
|
403
|
+
if (!cachedMockId) return;
|
|
404
|
+
|
|
405
|
+
const folderPath = getMockFolderPath(cachedMockId);
|
|
406
|
+
if (!folderPath) return;
|
|
407
|
+
|
|
408
|
+
console.log(`\n${colors.blue}🔄${colors.reset} ${colors.bright}热更新所有 Mock 文件...${colors.reset}`);
|
|
409
|
+
cachedMockFiles = loadMockFiles(folderPath);
|
|
410
|
+
console.log(`${colors.green}✅${colors.reset} ${colors.bright}热更新完成,共 ${colors.green}${Object.keys(cachedMockFiles).length}${colors.reset} ${colors.bright}个 Mock 接口${colors.reset}\n`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 设置文件监听
|
|
415
|
+
*/
|
|
416
|
+
function setupFileWatcher(mockId) {
|
|
417
|
+
// 清除旧的监听器
|
|
418
|
+
if (fileWatcher) {
|
|
419
|
+
fileWatcher.close();
|
|
420
|
+
fileWatcher = null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const folderPath = getMockFolderPath(mockId);
|
|
424
|
+
if (!folderPath) return;
|
|
425
|
+
|
|
426
|
+
const workDir = getWorkDir();
|
|
427
|
+
const baseDataPath = path.join(workDir, 'base-data');
|
|
428
|
+
const mocksPath = folderPath;
|
|
429
|
+
|
|
430
|
+
// 要监听的路径
|
|
431
|
+
const watchPaths = [];
|
|
432
|
+
if (fs.existsSync(baseDataPath)) {
|
|
433
|
+
watchPaths.push(baseDataPath);
|
|
434
|
+
}
|
|
435
|
+
if (fs.existsSync(mocksPath)) {
|
|
436
|
+
watchPaths.push(mocksPath);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (watchPaths.length === 0) return;
|
|
440
|
+
|
|
441
|
+
// 使用 chokidar 监听文件变化
|
|
442
|
+
fileWatcher = chokidar.watch(watchPaths, {
|
|
443
|
+
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
|
|
444
|
+
persistent: true,
|
|
445
|
+
ignoreInitial: true,
|
|
446
|
+
awaitWriteFinish: {
|
|
447
|
+
stabilityThreshold: 200,
|
|
448
|
+
pollInterval: 100
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
fileWatcher
|
|
453
|
+
.on('change', (filePath) => {
|
|
454
|
+
// 延迟处理,避免文件写入未完成
|
|
455
|
+
setTimeout(() => {
|
|
456
|
+
const ext = path.extname(filePath);
|
|
457
|
+
const fileName = path.basename(filePath);
|
|
458
|
+
|
|
459
|
+
// 如果是 Mock 文件变化
|
|
460
|
+
if (filePath.endsWith('.mock.ts') || filePath.endsWith('.mock.js')) {
|
|
461
|
+
const interfaceName = fileName.replace(/\.mock\.(ts|js)$/, '');
|
|
462
|
+
reloadMockFile(filePath, interfaceName);
|
|
463
|
+
}
|
|
464
|
+
// 如果是 base-data 中的 JSON 文件变化
|
|
465
|
+
else if (ext === '.json' && filePath.includes('base-data')) {
|
|
466
|
+
// 找到对应的接口名(从路径中提取)
|
|
467
|
+
const baseDataMatch = filePath.match(/base-data[\/\\]([^\/\\]+)/);
|
|
468
|
+
if (baseDataMatch) {
|
|
469
|
+
const interfaceName = baseDataMatch[1];
|
|
470
|
+
|
|
471
|
+
// 先清除该 JSON 文件的缓存
|
|
472
|
+
try {
|
|
473
|
+
const resolvedPath = require.resolve(filePath);
|
|
474
|
+
delete require.cache[resolvedPath];
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// 如果文件还未被 require,忽略错误
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 清除该接口的所有 base-data 缓存
|
|
480
|
+
clearBaseDataCache(interfaceName);
|
|
481
|
+
|
|
482
|
+
// 重新加载该接口的 Mock 文件(因为 Mock 文件会 import base-data)
|
|
483
|
+
const mockFilePath = path.join(mocksPath, `${interfaceName}.mock.ts`);
|
|
484
|
+
if (fs.existsSync(mockFilePath)) {
|
|
485
|
+
reloadMockFile(mockFilePath, interfaceName);
|
|
486
|
+
} else {
|
|
487
|
+
// 如果找不到对应的 Mock 文件,重新加载所有(以防有依赖关系)
|
|
488
|
+
reloadAllMockFiles();
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// 无法确定接口名,重新加载所有
|
|
492
|
+
reloadAllMockFiles();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}, 100);
|
|
496
|
+
})
|
|
497
|
+
.on('error', (error) => {
|
|
498
|
+
console.error(`${colors.red}✗${colors.reset} ${colors.red}文件监听错误:${colors.reset} ${error.message}`);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
console.log(`${colors.blue}👁️${colors.reset} ${colors.dim}已启用文件热更新监听${colors.reset}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
293
504
|
/**
|
|
294
505
|
* 获取或加载 Mock 文件
|
|
295
506
|
*/
|
|
@@ -309,6 +520,10 @@ function getMockFiles(mockId) {
|
|
|
309
520
|
cachedMockId = mockId;
|
|
310
521
|
|
|
311
522
|
console.log(`${colors.green}✅${colors.reset} ${colors.bright}共加载 ${colors.green}${Object.keys(cachedMockFiles).length}${colors.reset} ${colors.bright}个 Mock 接口${colors.reset}\n`);
|
|
523
|
+
|
|
524
|
+
// 设置文件监听
|
|
525
|
+
setupFileWatcher(mockId);
|
|
526
|
+
|
|
312
527
|
return cachedMockFiles;
|
|
313
528
|
}
|
|
314
529
|
|
|
@@ -473,7 +688,10 @@ async function handleRequest(req, res) {
|
|
|
473
688
|
|
|
474
689
|
|
|
475
690
|
// 检查是否匹配失败
|
|
476
|
-
|
|
691
|
+
// matchResponse 返回错误时的格式:{ error: true, message: '...', ... }
|
|
692
|
+
// 需要检查 error 是否为布尔值 true,而不是仅仅检查 error 字段是否存在
|
|
693
|
+
// 因为响应数据中可能包含 error 字段(如 { error: { code: '', message: '' } })
|
|
694
|
+
if (result && result.body && result.body.error === true) {
|
|
477
695
|
console.log(`${getTimestamp()} ${colors.red}❌ ${actualInterfaceName} 匹配失败${colors.reset}`);
|
|
478
696
|
return res.status(404).json(result.body);
|
|
479
697
|
}
|
package/src/commands/migrate.js
CHANGED
|
@@ -39,30 +39,194 @@ const SOA_FILE_PATTERN = /.*soa2_\d+_\w+\.txt$/;
|
|
|
39
39
|
const SOA_EXTRACT_PATTERN = /soa2_(\d+)_(\w+)/;
|
|
40
40
|
const INDEX_PATTERN = /^\[(\d+)\]\s+(Request|Response)/;
|
|
41
41
|
|
|
42
|
+
// Proxyman 标准格式文件模式
|
|
43
|
+
const PROXYMAN_FILE_PATTERN = /^\[\d+\]\s+(Request|Response)\s+-\s+.+\.txt$/;
|
|
44
|
+
const PROXYMAN_EXTRACT_PATTERN = /^\[(\d+)\]\s+(Request|Response)\s+-\s+(.+?)\.txt$/;
|
|
45
|
+
|
|
42
46
|
/**
|
|
43
47
|
* 从文件中提取 JSON 数据
|
|
48
|
+
* 支持 Request 和 Response 文件
|
|
44
49
|
*/
|
|
45
50
|
function extractJsonFromFile(filePath) {
|
|
46
51
|
try {
|
|
47
52
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
48
53
|
const lines = content.split('\n');
|
|
54
|
+
const filename = path.basename(filePath);
|
|
55
|
+
|
|
56
|
+
// 判断是 Request 还是 Response 文件
|
|
57
|
+
const isRequest = filename.includes('Request');
|
|
58
|
+
const isResponse = filename.includes('Response');
|
|
59
|
+
|
|
60
|
+
// 对于 Request 文件,检查是否是 GET 请求(没有 body)
|
|
61
|
+
if (isRequest) {
|
|
62
|
+
const firstLine = lines[0] || '';
|
|
63
|
+
const methodMatch = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH)\s+/);
|
|
64
|
+
|
|
65
|
+
if (methodMatch) {
|
|
66
|
+
const method = methodMatch[1];
|
|
67
|
+
|
|
68
|
+
// GET 请求通常没有 body,从 URL query 参数中提取
|
|
69
|
+
if (method === 'GET') {
|
|
70
|
+
const urlMatch = firstLine.match(/\s+([^\s?]+)\??([^\s]*)/);
|
|
71
|
+
if (urlMatch) {
|
|
72
|
+
const queryString = urlMatch[2];
|
|
73
|
+
if (queryString) {
|
|
74
|
+
// 解析 query 参数
|
|
75
|
+
const params = {};
|
|
76
|
+
queryString.split('&').forEach(param => {
|
|
77
|
+
const [key, value] = param.split('=');
|
|
78
|
+
if (key) {
|
|
79
|
+
try {
|
|
80
|
+
params[key] = value ? decodeURIComponent(value) : '';
|
|
81
|
+
} catch (e) {
|
|
82
|
+
params[key] = value || '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return Object.keys(params).length > 0 ? params : {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// GET 请求没有 query 参数,返回空对象
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// POST/PUT/DELETE/PATCH 请求,尝试从 body 中提取 JSON
|
|
94
|
+
// 找到第一个空行后的第一个 {
|
|
95
|
+
let jsonStartIndex = -1;
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
if (lines[i].trim() === '' && i + 1 < lines.length) {
|
|
98
|
+
const nextLine = lines[i + 1].trim();
|
|
99
|
+
if (nextLine.startsWith('{') || nextLine.startsWith('[')) {
|
|
100
|
+
jsonStartIndex = i + 1;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 如果没找到空行,直接找第一个 { 或 [
|
|
107
|
+
if (jsonStartIndex === -1) {
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const trimmed = lines[i].trim();
|
|
110
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
111
|
+
jsonStartIndex = i;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (jsonStartIndex === -1) {
|
|
118
|
+
// POST/PUT/DELETE/PATCH 请求没有 body,返回空对象
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 从 jsonStartIndex 开始提取 JSON
|
|
123
|
+
const jsonContent = lines.slice(jsonStartIndex).join('\n').trim();
|
|
124
|
+
|
|
125
|
+
// 验证 JSON 格式
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(jsonContent);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error(`错误: 文件 ${filePath} 的 JSON 格式无效:`, e.message);
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Request 文件但无法识别 HTTP 方法,尝试通用提取
|
|
134
|
+
// 找到第一个空行后的第一个 {
|
|
135
|
+
let jsonStartIndex = -1;
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
if (lines[i].trim() === '' && i + 1 < lines.length) {
|
|
138
|
+
const nextLine = lines[i + 1].trim();
|
|
139
|
+
if (nextLine.startsWith('{') || nextLine.startsWith('[')) {
|
|
140
|
+
jsonStartIndex = i + 1;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (jsonStartIndex === -1) {
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
const trimmed = lines[i].trim();
|
|
149
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
150
|
+
jsonStartIndex = i;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (jsonStartIndex === -1) {
|
|
157
|
+
// 无法提取,返回空对象
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const jsonContent = lines.slice(jsonStartIndex).join('\n').trim();
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(jsonContent);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 对于 Response 文件,提取 JSON body
|
|
171
|
+
if (isResponse) {
|
|
172
|
+
// 找到第一个空行后的第一个 {
|
|
173
|
+
let jsonStartIndex = -1;
|
|
174
|
+
for (let i = 0; i < lines.length; i++) {
|
|
175
|
+
if (lines[i].trim() === '' && i + 1 < lines.length) {
|
|
176
|
+
const nextLine = lines[i + 1].trim();
|
|
177
|
+
if (nextLine.startsWith('{') || nextLine.startsWith('[')) {
|
|
178
|
+
jsonStartIndex = i + 1;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 如果没找到空行,直接找第一个 { 或 [
|
|
185
|
+
if (jsonStartIndex === -1) {
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
const trimmed = lines[i].trim();
|
|
188
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
189
|
+
jsonStartIndex = i;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (jsonStartIndex === -1) {
|
|
196
|
+
console.warn(`警告: 在文件 ${filePath} 中未找到 JSON 数据`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 从 jsonStartIndex 开始提取 JSON
|
|
201
|
+
const jsonContent = lines.slice(jsonStartIndex).join('\n').trim();
|
|
202
|
+
|
|
203
|
+
// 验证 JSON 格式
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(jsonContent);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(`错误: 文件 ${filePath} 的 JSON 格式无效:`, e.message);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
49
211
|
|
|
212
|
+
// 如果无法判断文件类型,使用原来的逻辑
|
|
50
213
|
// 找到第一个空行后的第一个 {
|
|
51
214
|
let jsonStartIndex = -1;
|
|
52
215
|
for (let i = 0; i < lines.length; i++) {
|
|
53
216
|
if (lines[i].trim() === '' && i + 1 < lines.length) {
|
|
54
217
|
const nextLine = lines[i + 1].trim();
|
|
55
|
-
if (nextLine.startsWith('{')) {
|
|
218
|
+
if (nextLine.startsWith('{') || nextLine.startsWith('[')) {
|
|
56
219
|
jsonStartIndex = i + 1;
|
|
57
220
|
break;
|
|
58
221
|
}
|
|
59
222
|
}
|
|
60
223
|
}
|
|
61
224
|
|
|
62
|
-
// 如果没找到空行,直接找第一个 {
|
|
225
|
+
// 如果没找到空行,直接找第一个 { 或 [
|
|
63
226
|
if (jsonStartIndex === -1) {
|
|
64
227
|
for (let i = 0; i < lines.length; i++) {
|
|
65
|
-
|
|
228
|
+
const trimmed = lines[i].trim();
|
|
229
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
66
230
|
jsonStartIndex = i;
|
|
67
231
|
break;
|
|
68
232
|
}
|
|
@@ -92,8 +256,12 @@ function extractJsonFromFile(filePath) {
|
|
|
92
256
|
|
|
93
257
|
/**
|
|
94
258
|
* 从文件名中提取接口信息
|
|
259
|
+
* 支持两种格式:
|
|
260
|
+
* 1. SOA2 格式: soa2_数字_接口名.txt
|
|
261
|
+
* 2. Proxyman 格式: [序号] Request/Response - 域名_路径.txt
|
|
95
262
|
*/
|
|
96
|
-
function extractInterfaceInfo(
|
|
263
|
+
function extractInterfaceInfo(filePath) {
|
|
264
|
+
const filename = path.basename(filePath);
|
|
97
265
|
const indexMatch = filename.match(INDEX_PATTERN);
|
|
98
266
|
if (!indexMatch) {
|
|
99
267
|
return null;
|
|
@@ -102,48 +270,166 @@ function extractInterfaceInfo(filename) {
|
|
|
102
270
|
const index = parseInt(indexMatch[1], 10);
|
|
103
271
|
const type = indexMatch[2].toLowerCase();
|
|
104
272
|
|
|
273
|
+
// 尝试 SOA2 格式
|
|
105
274
|
const soaMatch = filename.match(SOA_EXTRACT_PATTERN);
|
|
106
|
-
if (
|
|
107
|
-
|
|
275
|
+
if (soaMatch) {
|
|
276
|
+
const interfaceId = soaMatch[1];
|
|
277
|
+
let interfaceName = soaMatch[2];
|
|
278
|
+
|
|
279
|
+
if (interfaceName.startsWith('json_')) {
|
|
280
|
+
interfaceName = interfaceName.substring(5);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { index, type, interfaceId, interfaceName };
|
|
108
284
|
}
|
|
109
285
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
286
|
+
// 尝试 Proxyman 格式或其他通用格式
|
|
287
|
+
const proxymanMatch = filename.match(PROXYMAN_EXTRACT_PATTERN);
|
|
288
|
+
if (proxymanMatch) {
|
|
289
|
+
// 从文件名中提取路径部分(域名_路径)
|
|
290
|
+
const pathPart = proxymanMatch[3];
|
|
291
|
+
|
|
292
|
+
// 尝试从请求文件中读取 URL 路径来提取接口名(更准确)
|
|
293
|
+
let interfaceName = null;
|
|
294
|
+
|
|
295
|
+
// 对于 Request 文件,直接从文件内容提取
|
|
296
|
+
if (type === 'request' && fs.existsSync(filePath)) {
|
|
297
|
+
interfaceName = extractInterfaceNameFromRequestFile(filePath);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 对于 Response 文件,尝试找到对应的 Request 文件来提取接口名
|
|
301
|
+
if (!interfaceName && type === 'response') {
|
|
302
|
+
// 查找对应的 Request 文件(相同序号)
|
|
303
|
+
const requestFileName = filename.replace('Response', 'Request');
|
|
304
|
+
const requestFilePath = path.join(path.dirname(filePath), requestFileName);
|
|
305
|
+
if (fs.existsSync(requestFilePath)) {
|
|
306
|
+
interfaceName = extractInterfaceNameFromRequestFile(requestFilePath);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 如果从文件内容中无法提取,则从文件名中提取
|
|
311
|
+
if (!interfaceName) {
|
|
312
|
+
// 从文件名中提取路径部分(域名_路径)
|
|
313
|
+
// 例如: www.klook.cn_v1_experiencesrv_activity_package_service_get_activity_right_price_sources
|
|
314
|
+
// -> get_activity_right_price_sources
|
|
315
|
+
const parts = pathPart.split('_');
|
|
316
|
+
|
|
317
|
+
// 查找包含 "service" 或 "api" 的部分,取后面的部分
|
|
318
|
+
const serviceIndex = parts.findIndex(p => p.includes('service') || p.includes('api'));
|
|
319
|
+
if (serviceIndex !== -1 && serviceIndex < parts.length - 1) {
|
|
320
|
+
interfaceName = parts.slice(serviceIndex + 1).join('_');
|
|
321
|
+
} else if (parts.length > 3) {
|
|
322
|
+
// 如果路径较长,取最后几部分作为接口名
|
|
323
|
+
interfaceName = parts.slice(-3).join('_');
|
|
324
|
+
} else {
|
|
325
|
+
// 默认取最后一部分
|
|
326
|
+
interfaceName = parts[parts.length - 1];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { index, type, interfaceId: null, interfaceName };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 如果都不匹配,但文件名符合 [序号] Request/Response 格式,尝试通用提取
|
|
334
|
+
// 这可以处理其他格式,只要文件名包含 [序号] Request/Response
|
|
335
|
+
if (indexMatch) {
|
|
336
|
+
// 尝试从文件内容中提取接口名
|
|
337
|
+
let interfaceName = null;
|
|
338
|
+
|
|
339
|
+
if (type === 'request' && fs.existsSync(filePath)) {
|
|
340
|
+
interfaceName = extractInterfaceNameFromRequestFile(filePath);
|
|
341
|
+
} else if (type === 'response') {
|
|
342
|
+
// 对于 Response 文件,尝试找到对应的 Request 文件
|
|
343
|
+
const requestFileName = filename.replace('Response', 'Request');
|
|
344
|
+
const requestFilePath = path.join(path.dirname(filePath), requestFileName);
|
|
345
|
+
if (fs.existsSync(requestFilePath)) {
|
|
346
|
+
interfaceName = extractInterfaceNameFromRequestFile(requestFilePath);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 如果无法从文件内容提取,尝试从文件名中提取(去掉序号和类型后的部分)
|
|
351
|
+
if (!interfaceName) {
|
|
352
|
+
// 移除 [序号] Request/Response - 前缀,取剩余部分作为接口名
|
|
353
|
+
const remaining = filename
|
|
354
|
+
.replace(INDEX_PATTERN, '')
|
|
355
|
+
.replace(/^\s*-\s*/, '')
|
|
356
|
+
.replace(/\.txt$/, '')
|
|
357
|
+
.trim();
|
|
358
|
+
|
|
359
|
+
if (remaining) {
|
|
360
|
+
// 如果剩余部分包含路径,提取最后一部分
|
|
361
|
+
const parts = remaining.split(/[_\-\/]/).filter(p => p);
|
|
362
|
+
if (parts.length > 0) {
|
|
363
|
+
interfaceName = parts[parts.length - 1];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (interfaceName) {
|
|
369
|
+
return { index, type, interfaceId: null, interfaceName };
|
|
370
|
+
}
|
|
115
371
|
}
|
|
116
372
|
|
|
117
|
-
return
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 从请求文件中提取接口名
|
|
378
|
+
*/
|
|
379
|
+
function extractInterfaceNameFromRequestFile(filePath) {
|
|
380
|
+
try {
|
|
381
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
382
|
+
const firstLine = content.split('\n')[0];
|
|
383
|
+
// 匹配 HTTP 请求行,例如: GET /v1/experiencesrv/activity/package_service/get_activity_right_price_sources?...
|
|
384
|
+
const urlMatch = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH)\s+([^\s?]+)/);
|
|
385
|
+
if (urlMatch) {
|
|
386
|
+
const urlPath = urlMatch[2];
|
|
387
|
+
// 从路径中提取接口名:取最后一个斜杠后的部分
|
|
388
|
+
const pathParts = urlPath.split('/').filter(p => p);
|
|
389
|
+
if (pathParts.length > 0) {
|
|
390
|
+
return pathParts[pathParts.length - 1];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (e) {
|
|
394
|
+
// 如果读取失败,返回 null
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
118
397
|
}
|
|
119
398
|
|
|
120
399
|
/**
|
|
121
|
-
* 扫描 Raw
|
|
400
|
+
* 扫描 Raw 文件夹,提取所有接口文件
|
|
401
|
+
* 支持多种格式:
|
|
402
|
+
* 1. SOA2 格式: soa2_数字_接口名.txt(必须包含 [序号] Request/Response)
|
|
403
|
+
* 2. Proxyman 标准格式: [序号] Request/Response - 域名_路径.txt
|
|
404
|
+
* 3. 通用格式: [序号] Request/Response - 任意内容.txt(会尝试从文件内容提取接口名)
|
|
122
405
|
*/
|
|
123
406
|
function scanRawFolder(rawFolderPath) {
|
|
124
407
|
const files = fs.readdirSync(rawFolderPath);
|
|
125
|
-
const
|
|
408
|
+
const interfaceFiles = [];
|
|
126
409
|
|
|
127
410
|
for (const file of files) {
|
|
128
|
-
|
|
411
|
+
// 检查文件名是否包含 [序号] Request/Response 格式
|
|
412
|
+
// 这是最通用的格式要求,可以匹配多种文件命名方式
|
|
413
|
+
if (!INDEX_PATTERN.test(file)) {
|
|
129
414
|
continue;
|
|
130
415
|
}
|
|
131
416
|
|
|
132
|
-
const
|
|
417
|
+
const filePath = path.join(rawFolderPath, file);
|
|
418
|
+
const info = extractInterfaceInfo(filePath);
|
|
133
419
|
if (!info) {
|
|
134
|
-
console.warn(`警告: 无法解析文件 ${file}
|
|
420
|
+
console.warn(`警告: 无法解析文件 ${file}(文件名需包含 [序号] Request/Response 格式)`);
|
|
135
421
|
continue;
|
|
136
422
|
}
|
|
137
423
|
|
|
138
|
-
|
|
424
|
+
interfaceFiles.push({
|
|
139
425
|
index: info.index,
|
|
140
426
|
type: info.type,
|
|
141
427
|
interfaceName: info.interfaceName,
|
|
142
|
-
filePath:
|
|
428
|
+
filePath: filePath,
|
|
143
429
|
});
|
|
144
430
|
}
|
|
145
431
|
|
|
146
|
-
return
|
|
432
|
+
return interfaceFiles;
|
|
147
433
|
}
|
|
148
434
|
|
|
149
435
|
/**
|
|
@@ -641,10 +927,14 @@ async function executeMigration(scenarioName, targetFolder, rawFolderPath, rl =
|
|
|
641
927
|
|
|
642
928
|
console.log('扫描 Raw 文件夹...');
|
|
643
929
|
const files = scanRawFolder(rawFolderPath);
|
|
644
|
-
console.log(`找到 ${files.length}
|
|
930
|
+
console.log(`找到 ${files.length} 个接口文件`);
|
|
645
931
|
|
|
646
932
|
if (files.length === 0) {
|
|
647
|
-
console.error('错误:
|
|
933
|
+
console.error('错误: 未找到任何接口文件');
|
|
934
|
+
console.error('提示: 文件名需包含 [序号] Request/Response 格式,例如:');
|
|
935
|
+
console.error(' - [1] Request - api.example.com_get_user_info.txt');
|
|
936
|
+
console.error(' - [1] Response - api.example.com_get_user_info.txt');
|
|
937
|
+
console.error(' - soa2_123_getUserInfo.txt(需包含 [序号] Request/Response)');
|
|
648
938
|
process.exit(1);
|
|
649
939
|
}
|
|
650
940
|
|
|
@@ -705,8 +995,16 @@ async function executeMigration(scenarioName, targetFolder, rawFolderPath, rl =
|
|
|
705
995
|
const requestData = extractJsonFromFile(filePair.request);
|
|
706
996
|
const responseData = extractJsonFromFile(filePair.response);
|
|
707
997
|
|
|
708
|
-
|
|
998
|
+
// 对于 GET 请求,requestData 可能是空对象 {},这是有效的
|
|
999
|
+
// 只有 responseData 是必需的
|
|
1000
|
+
if (requestData === null || responseData === null) {
|
|
709
1001
|
console.warn(`警告: 序号 ${index} 的数据提取失败,跳过`);
|
|
1002
|
+
if (requestData === null) {
|
|
1003
|
+
console.warn(` 请求数据提取失败: ${filePair.request}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (responseData === null) {
|
|
1006
|
+
console.warn(` 响应数据提取失败: ${filePair.response}`);
|
|
1007
|
+
}
|
|
710
1008
|
continue;
|
|
711
1009
|
}
|
|
712
1010
|
|
package/src/commands/start.js
CHANGED
|
@@ -39,10 +39,10 @@ function findProjectRoot() {
|
|
|
39
39
|
function isRawFolder(pathStr) {
|
|
40
40
|
if (!pathStr) return false;
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// 使用 path.resolve 规范化路径(处理 ./ 和 ../ 等相对路径)
|
|
43
43
|
const fullPath = path.isAbsolute(pathStr)
|
|
44
|
-
? pathStr
|
|
45
|
-
: path.
|
|
44
|
+
? path.resolve(pathStr)
|
|
45
|
+
: path.resolve(process.cwd(), pathStr);
|
|
46
46
|
|
|
47
47
|
// 检查路径是否存在且是目录
|
|
48
48
|
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
|
@@ -74,79 +74,146 @@ function findDefaultRawFolder() {
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 检查 Raw 文件夹下是否已有生成的文件
|
|
79
|
+
*/
|
|
80
|
+
function checkRawFolderFiles(rawFolderPath) {
|
|
81
|
+
const baseDataPath = path.join(rawFolderPath, 'base-data');
|
|
82
|
+
const mocksPath = path.join(rawFolderPath, 'mocks');
|
|
83
|
+
const configPath = path.join(rawFolderPath, 'proxy.config.json');
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
hasBaseData: fs.existsSync(baseDataPath) && fs.statSync(baseDataPath).isDirectory(),
|
|
87
|
+
hasMocks: fs.existsSync(mocksPath) && fs.statSync(mocksPath).isDirectory(),
|
|
88
|
+
hasConfig: fs.existsSync(configPath),
|
|
89
|
+
allExist: fs.existsSync(baseDataPath) && fs.existsSync(mocksPath) && fs.existsSync(configPath)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
/**
|
|
78
94
|
* start 命令主函数
|
|
79
95
|
*/
|
|
80
96
|
async function startCommand(mockIdOrPath, options) {
|
|
81
97
|
let finalMockId = mockIdOrPath;
|
|
98
|
+
let workDir = process.cwd(); // 默认工作目录
|
|
82
99
|
|
|
83
100
|
// 检查是否是 Raw 文件夹路径
|
|
84
101
|
if (isRawFolder(mockIdOrPath)) {
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
// 规范化路径,确保正确处理相对路径(包括 ./ 开头的路径)
|
|
87
103
|
const rawFolderPath = path.isAbsolute(mockIdOrPath)
|
|
88
|
-
? mockIdOrPath
|
|
89
|
-
: path.
|
|
104
|
+
? path.resolve(mockIdOrPath)
|
|
105
|
+
: path.resolve(process.cwd(), mockIdOrPath);
|
|
90
106
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
console.error('错误: 指定了 --no-migrate,但提供的是 Raw 文件夹路径');
|
|
94
|
-
console.error('提示: 请先使用 jacky-proxy migrate 命令转换,或移除 --no-migrate 参数');
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 执行迁移
|
|
99
|
-
const migrateCommand = require('./migrate');
|
|
100
|
-
const scenarioName = options.scenario || '场景1';
|
|
101
|
-
const targetFolder = options.target || 'mocks/test-folder';
|
|
102
|
-
const ignoreInterfaces = options.ignore
|
|
103
|
-
? options.ignore.split(',').map(s => s.trim()).filter(s => s)
|
|
104
|
-
: [];
|
|
105
|
-
|
|
106
|
-
console.log('');
|
|
107
|
-
console.log('═══════════════════════════════════════════════════════');
|
|
108
|
-
console.log(' 自动迁移 Raw 文件夹');
|
|
109
|
-
console.log('═══════════════════════════════════════════════════════');
|
|
110
|
-
console.log(`场景名称: ${scenarioName}`);
|
|
111
|
-
console.log(`目标文件夹: ${targetFolder}`);
|
|
112
|
-
console.log(`Raw 文件夹: ${rawFolderPath}`);
|
|
113
|
-
if (ignoreInterfaces.length > 0) {
|
|
114
|
-
console.log(`忽略接口: ${ignoreInterfaces.join(', ')}`);
|
|
115
|
-
}
|
|
116
|
-
console.log('');
|
|
117
|
-
|
|
118
|
-
// 调用迁移命令(非交互式)
|
|
119
|
-
const migrateOptions = {
|
|
120
|
-
scenario: scenarioName,
|
|
121
|
-
target: targetFolder,
|
|
122
|
-
raw: rawFolderPath,
|
|
123
|
-
ignore: options.ignore || '',
|
|
124
|
-
interactive: false
|
|
125
|
-
};
|
|
107
|
+
// 检查 Raw 文件夹下是否已有生成的文件
|
|
108
|
+
const filesCheck = checkRawFolderFiles(rawFolderPath);
|
|
126
109
|
|
|
127
|
-
|
|
128
|
-
|
|
110
|
+
if (filesCheck.allExist) {
|
|
111
|
+
// 文件已存在,直接启动
|
|
112
|
+
console.log('🔍 检测到 Raw 文件夹,且已存在生成的文件,直接启动...');
|
|
113
|
+
console.log(` base-data: ${filesCheck.hasBaseData ? '✓' : '✗'}`);
|
|
114
|
+
console.log(` mocks: ${filesCheck.hasMocks ? '✓' : '✗'}`);
|
|
115
|
+
console.log(` proxy.config.json: ${filesCheck.hasConfig ? '✓' : '✗'}`);
|
|
116
|
+
console.log('');
|
|
129
117
|
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
const configPath = path.join(workDir, options.config || 'proxy.config.json');
|
|
118
|
+
// 从 Raw 文件夹下的配置文件中读取 mock-id
|
|
119
|
+
const configPath = path.join(rawFolderPath, options.config || 'proxy.config.json');
|
|
133
120
|
if (fs.existsSync(configPath)) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
try {
|
|
122
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
123
|
+
// 获取第一个场景的 ID,或者使用默认值
|
|
124
|
+
if (config.folders && config.folders.list && config.folders.list.length > 0) {
|
|
125
|
+
finalMockId = String(config.folders.list[0].id);
|
|
126
|
+
console.log(`✅ 从配置文件读取场景 ID: ${finalMockId}`);
|
|
127
|
+
} else {
|
|
128
|
+
console.warn('警告: 配置文件中没有场景配置,使用默认 ID: 1');
|
|
129
|
+
finalMockId = '1';
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn(`警告: 读取配置文件失败: ${error.message},使用默认 ID: 1`);
|
|
141
133
|
finalMockId = '1';
|
|
142
134
|
}
|
|
143
135
|
} else {
|
|
144
136
|
console.warn('警告: 配置文件不存在,使用默认 ID: 1');
|
|
145
137
|
finalMockId = '1';
|
|
146
138
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
|
|
140
|
+
// 设置工作目录为 Raw 文件夹
|
|
141
|
+
workDir = rawFolderPath;
|
|
142
|
+
} else {
|
|
143
|
+
// 文件不存在,需要生成
|
|
144
|
+
console.log('🔍 检测到 Raw 文件夹,将自动转换后启动...');
|
|
145
|
+
|
|
146
|
+
// 如果设置了 --no-migrate,直接报错
|
|
147
|
+
if (options.noMigrate) {
|
|
148
|
+
console.error('错误: 指定了 --no-migrate,但 Raw 文件夹下还没有生成的文件');
|
|
149
|
+
console.error('提示: 请先使用 jacky-proxy migrate 命令转换,或移除 --no-migrate 参数');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 保存原始工作目录
|
|
154
|
+
const originalCwd = process.cwd();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// 切换到 Raw 文件夹目录,这样 migrate 会在 Raw 文件夹下创建文件
|
|
158
|
+
process.chdir(rawFolderPath);
|
|
159
|
+
|
|
160
|
+
// 执行迁移
|
|
161
|
+
const migrateCommand = require('./migrate');
|
|
162
|
+
const scenarioName = options.scenario || '默认场景';
|
|
163
|
+
const targetFolder = options.target || 'mocks/test-folder';
|
|
164
|
+
const ignoreInterfaces = options.ignore
|
|
165
|
+
? options.ignore.split(',').map(s => s.trim()).filter(s => s)
|
|
166
|
+
: [];
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log('═══════════════════════════════════════════════════════');
|
|
170
|
+
console.log(' 自动迁移 Raw 文件夹');
|
|
171
|
+
console.log('═══════════════════════════════════════════════════════');
|
|
172
|
+
console.log(`场景名称: ${scenarioName}`);
|
|
173
|
+
console.log(`目标文件夹: ${targetFolder}`);
|
|
174
|
+
console.log(`Raw 文件夹: ${rawFolderPath}`);
|
|
175
|
+
if (ignoreInterfaces.length > 0) {
|
|
176
|
+
console.log(`忽略接口: ${ignoreInterfaces.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
|
|
180
|
+
// 调用迁移命令(非交互式)
|
|
181
|
+
// 注意:raw 参数应该是相对于当前工作目录的路径,或者使用绝对路径
|
|
182
|
+
const migrateOptions = {
|
|
183
|
+
scenario: scenarioName,
|
|
184
|
+
target: targetFolder,
|
|
185
|
+
raw: rawFolderPath, // 使用绝对路径
|
|
186
|
+
ignore: options.ignore || '',
|
|
187
|
+
interactive: false
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
await migrateCommand(migrateOptions);
|
|
191
|
+
|
|
192
|
+
// 从配置文件中读取新创建的 mock-id(配置文件在 Raw 文件夹下)
|
|
193
|
+
const configPath = path.join(rawFolderPath, options.config || 'proxy.config.json');
|
|
194
|
+
if (fs.existsSync(configPath)) {
|
|
195
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
196
|
+
const folder = config.folders.list.find(f => f.path === targetFolder);
|
|
197
|
+
if (folder) {
|
|
198
|
+
finalMockId = String(folder.id);
|
|
199
|
+
console.log(`\n✅ 迁移完成,将使用场景 ID: ${finalMockId}`);
|
|
200
|
+
} else {
|
|
201
|
+
console.warn('警告: 无法在配置文件中找到新创建的场景,使用默认 ID: 1');
|
|
202
|
+
finalMockId = '1';
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
console.warn('警告: 配置文件不存在,使用默认 ID: 1');
|
|
206
|
+
finalMockId = '1';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 设置工作目录为 Raw 文件夹
|
|
210
|
+
workDir = rawFolderPath;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
// 恢复原始工作目录
|
|
213
|
+
process.chdir(originalCwd);
|
|
214
|
+
console.error('迁移失败:', error.message);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
150
217
|
}
|
|
151
218
|
} else {
|
|
152
219
|
// 检查是否是数字(mock-id)
|
|
@@ -158,12 +225,22 @@ async function startCommand(mockIdOrPath, options) {
|
|
|
158
225
|
}
|
|
159
226
|
|
|
160
227
|
// 设置环境变量
|
|
161
|
-
const workDir = process.cwd(); // 保存当前工作目录
|
|
162
228
|
process.env.MOCK_ID = finalMockId;
|
|
163
229
|
process.env.PORT = options.port || '5001';
|
|
164
|
-
|
|
165
|
-
|
|
230
|
+
// 确保 workDir 是绝对路径
|
|
231
|
+
const absoluteWorkDir = path.isAbsolute(workDir) ? workDir : path.resolve(workDir);
|
|
232
|
+
process.env.WORK_DIR = absoluteWorkDir; // 设置工作目录环境变量(可能是 Raw 文件夹)
|
|
233
|
+
// 确保 CONFIG_PATH 是绝对路径
|
|
234
|
+
const configFileName = options.config || 'proxy.config.json';
|
|
235
|
+
const configPath = path.isAbsolute(configFileName)
|
|
236
|
+
? configFileName
|
|
237
|
+
: path.join(absoluteWorkDir, configFileName);
|
|
238
|
+
process.env.CONFIG_PATH = path.resolve(configPath); // 确保是绝对路径
|
|
166
239
|
process.env.DEBUG = options.debug ? 'true' : 'false'; // 设置 Debug 模式
|
|
240
|
+
|
|
241
|
+
// 调试信息
|
|
242
|
+
console.log(`📁 工作目录: ${process.env.WORK_DIR}`);
|
|
243
|
+
console.log(`📄 配置文件: ${process.env.CONFIG_PATH}`);
|
|
167
244
|
|
|
168
245
|
console.log('');
|
|
169
246
|
console.log('═══════════════════════════════════════════════════════');
|
|
@@ -309,6 +309,39 @@ export function formatDiffInfo(diffs: DiffInfo[]): string {
|
|
|
309
309
|
return sections.join('\n');
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/**
|
|
313
|
+
* 规范化 query 参数中的数组值
|
|
314
|
+
* Express 会将同名 query 参数解析为数组(如 ?preview=&preview=0 -> ["", "0"])
|
|
315
|
+
* 为了匹配,需要将数组转换为字符串:取最后一个非空值,或如果都是空字符串则取最后一个值
|
|
316
|
+
*/
|
|
317
|
+
function normalizeQueryArrayValues(obj: any): any {
|
|
318
|
+
if (!obj || typeof obj !== 'object') {
|
|
319
|
+
return obj;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (Array.isArray(obj)) {
|
|
323
|
+
return obj.map(item => normalizeQueryArrayValues(item));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result: any = {};
|
|
327
|
+
for (const key in obj) {
|
|
328
|
+
const value = obj[key];
|
|
329
|
+
if (Array.isArray(value)) {
|
|
330
|
+
// 如果是数组,取最后一个非空值,或如果都是空字符串则取最后一个值
|
|
331
|
+
const nonEmptyValues = value.filter(v => v !== '' && v !== null && v !== undefined);
|
|
332
|
+
result[key] = nonEmptyValues.length > 0
|
|
333
|
+
? nonEmptyValues[nonEmptyValues.length - 1]
|
|
334
|
+
: value[value.length - 1];
|
|
335
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
336
|
+
// 递归处理嵌套对象
|
|
337
|
+
result[key] = normalizeQueryArrayValues(value);
|
|
338
|
+
} else {
|
|
339
|
+
result[key] = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
/**
|
|
313
346
|
* 处理请求对象(应用忽略、排序等规则)
|
|
314
347
|
*/
|
|
@@ -354,6 +387,34 @@ function processRequest(
|
|
|
354
387
|
processed.body = body;
|
|
355
388
|
}
|
|
356
389
|
|
|
390
|
+
// 处理 query 参数(GET 请求通常使用 query 参数)
|
|
391
|
+
// 如果请求没有 body,query 参数也会参与匹配
|
|
392
|
+
if (processed.query && typeof processed.query === 'object') {
|
|
393
|
+
let query = { ...processed.query };
|
|
394
|
+
|
|
395
|
+
// 规范化数组值(Express 会将同名 query 参数解析为数组)
|
|
396
|
+
query = normalizeQueryArrayValues(query);
|
|
397
|
+
|
|
398
|
+
// 深度忽略属性
|
|
399
|
+
if (deepIgnore) {
|
|
400
|
+
query = deepIgnoreProps(query, ignoreProps);
|
|
401
|
+
} else {
|
|
402
|
+
// 浅层忽略属性
|
|
403
|
+
for (const prop of ignoreProps) {
|
|
404
|
+
if (!essentialProps.includes(prop)) {
|
|
405
|
+
delete query[prop];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 排序数组属性
|
|
411
|
+
if (sortProps.length > 0) {
|
|
412
|
+
query = sortArrayProps(query, sortProps);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
processed.query = query;
|
|
416
|
+
}
|
|
417
|
+
|
|
357
418
|
return processed;
|
|
358
419
|
}
|
|
359
420
|
|
|
@@ -399,6 +460,15 @@ export function matchResponse(
|
|
|
399
460
|
|
|
400
461
|
const finalSortProps = interfaceConfig?.sortProps || sortProps;
|
|
401
462
|
|
|
463
|
+
// 先提取原始请求数据(在过滤之前),用于后续比较
|
|
464
|
+
// 优先级:body > query
|
|
465
|
+
let originalRequestData: any;
|
|
466
|
+
if (request.body !== undefined && request.body !== null && request.body !== '') {
|
|
467
|
+
originalRequestData = request.body;
|
|
468
|
+
} else if (request.query && typeof request.query === 'object' && Object.keys(request.query).length > 0) {
|
|
469
|
+
originalRequestData = request.query;
|
|
470
|
+
}
|
|
471
|
+
|
|
402
472
|
// 处理真实请求
|
|
403
473
|
let processedRequest = processRequest(
|
|
404
474
|
request,
|
|
@@ -418,10 +488,50 @@ export function matchResponse(
|
|
|
418
488
|
}
|
|
419
489
|
|
|
420
490
|
// 提取实际请求的 body 部分用于比较
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
491
|
+
// 优先级:body > query > 整个请求对象
|
|
492
|
+
// GET 请求通常使用 query 参数,POST 请求使用 body
|
|
493
|
+
let requestBodyForCompare: any;
|
|
494
|
+
|
|
495
|
+
// 检查 body 是否存在且不为空
|
|
496
|
+
const hasBody = processedRequest.body !== undefined &&
|
|
497
|
+
processedRequest.body !== null &&
|
|
498
|
+
processedRequest.body !== '' &&
|
|
499
|
+
(typeof processedRequest.body !== 'object' || Object.keys(processedRequest.body).length > 0);
|
|
500
|
+
|
|
501
|
+
// 检查 query 是否存在且不为空
|
|
502
|
+
const hasQuery = processedRequest.query &&
|
|
503
|
+
typeof processedRequest.query === 'object' &&
|
|
504
|
+
Object.keys(processedRequest.query).length > 0;
|
|
505
|
+
|
|
506
|
+
if (hasBody) {
|
|
507
|
+
requestBodyForCompare = processedRequest.body;
|
|
508
|
+
} else if (hasQuery) {
|
|
509
|
+
// GET 请求:使用 query 参数(已经在 processRequest 中规范化了数组值)
|
|
510
|
+
requestBodyForCompare = processedRequest.query;
|
|
511
|
+
} else if (originalRequestData) {
|
|
512
|
+
// 如果过滤后都为空,使用原始数据(但需要应用过滤规则)
|
|
513
|
+
// 先规范化数组值
|
|
514
|
+
let normalizedData = normalizeQueryArrayValues(originalRequestData);
|
|
515
|
+
|
|
516
|
+
// 对原始数据应用过滤规则
|
|
517
|
+
if (finalDeepIgnore) {
|
|
518
|
+
requestBodyForCompare = deepIgnoreProps(normalizedData, ignoreProps);
|
|
519
|
+
} else {
|
|
520
|
+
requestBodyForCompare = { ...normalizedData };
|
|
521
|
+
for (const prop of ignoreProps) {
|
|
522
|
+
if (!essentialProps.includes(prop)) {
|
|
523
|
+
delete requestBodyForCompare[prop];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// 应用排序
|
|
528
|
+
if (finalSortProps.length > 0) {
|
|
529
|
+
requestBodyForCompare = sortArrayProps(requestBodyForCompare, finalSortProps);
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
// 最后兜底:使用处理后的请求对象(可能包含其他字段)
|
|
533
|
+
requestBodyForCompare = processedRequest;
|
|
534
|
+
}
|
|
425
535
|
|
|
426
536
|
// 检查必需属性
|
|
427
537
|
if (needContainProps.length > 0) {
|
|
@@ -438,6 +548,9 @@ export function matchResponse(
|
|
|
438
548
|
}
|
|
439
549
|
|
|
440
550
|
// 遍历 Mock 请求列表,找到匹配的请求
|
|
551
|
+
// 同时保存处理后的 mock 请求,用于错误信息显示
|
|
552
|
+
const processedMockRequests: any[] = [];
|
|
553
|
+
|
|
441
554
|
for (let i = 0; i < requestList.length; i++) {
|
|
442
555
|
const mockRequest = requestList[i];
|
|
443
556
|
|
|
@@ -455,9 +568,46 @@ export function matchResponse(
|
|
|
455
568
|
);
|
|
456
569
|
|
|
457
570
|
// 提取 mock 请求的 body 部分用于比较
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
571
|
+
// 优先级:body > query > 整个请求对象(与真实请求保持一致)
|
|
572
|
+
let mockBodyForCompare: any;
|
|
573
|
+
|
|
574
|
+
// 检查 body 是否存在且不为空
|
|
575
|
+
const hasMockBody = processedMockRequest.body !== undefined &&
|
|
576
|
+
processedMockRequest.body !== null &&
|
|
577
|
+
processedMockRequest.body !== '' &&
|
|
578
|
+
(typeof processedMockRequest.body !== 'object' || Object.keys(processedMockRequest.body).length > 0);
|
|
579
|
+
|
|
580
|
+
// 检查 query 是否存在且不为空
|
|
581
|
+
const hasMockQuery = processedMockRequest.query &&
|
|
582
|
+
typeof processedMockRequest.query === 'object' &&
|
|
583
|
+
Object.keys(processedMockRequest.query).length > 0;
|
|
584
|
+
|
|
585
|
+
if (hasMockBody) {
|
|
586
|
+
mockBodyForCompare = processedMockRequest.body;
|
|
587
|
+
} else if (hasMockQuery) {
|
|
588
|
+
// GET 请求:使用 query 参数
|
|
589
|
+
mockBodyForCompare = processedMockRequest.query;
|
|
590
|
+
} else {
|
|
591
|
+
// 兜底:Mock 请求通常是纯对象格式(没有 body/query 字段)
|
|
592
|
+
// 如果 processedRequest.body 存在但为空对象,说明原始 mockRequest 被包装成了 { body: mockRequest }
|
|
593
|
+
// 此时应该使用原始 mockRequest(但需要应用相同的过滤规则)
|
|
594
|
+
// 由于 mockRequest 是纯对象,它已经被包装为 { body: mockRequest } 并处理过了
|
|
595
|
+
// 如果 body 是空对象,说明所有属性都被过滤了,但原始 mockRequest 可能还有数据
|
|
596
|
+
// 这里需要重新处理原始 mockRequest,应用相同的过滤规则
|
|
597
|
+
if (mockRequest && typeof mockRequest === 'object' && !mockRequest.body && !mockRequest.query) {
|
|
598
|
+
// 纯对象格式,需要重新处理(应用过滤规则)
|
|
599
|
+
// 创建一个临时对象来应用过滤规则
|
|
600
|
+
const tempRequest = { body: mockRequest };
|
|
601
|
+
const tempProcessed = processRequest(tempRequest, ignoreProps, essentialProps, finalDeepIgnore, finalSortProps);
|
|
602
|
+
mockBodyForCompare = tempProcessed.body || mockRequest;
|
|
603
|
+
} else {
|
|
604
|
+
// 使用处理后的请求对象
|
|
605
|
+
mockBodyForCompare = processedMockRequest;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 保存处理后的 mock 请求(已去除忽略的属性),用于错误信息显示
|
|
610
|
+
processedMockRequests.push(mockBodyForCompare);
|
|
461
611
|
|
|
462
612
|
// 使用 lodash 的 isEqualWith 进行深度比较(只比较 body 部分)
|
|
463
613
|
const isMatch = _.isEqualWith(
|
|
@@ -477,12 +627,13 @@ export function matchResponse(
|
|
|
477
627
|
|
|
478
628
|
// 没有找到匹配的请求,返回错误信息
|
|
479
629
|
// 注意:详细差异信息不再打印到控制台,避免终端输出过多信息
|
|
630
|
+
// 错误信息中的 request 和 mockRequests 都已去除忽略的属性,便于对比
|
|
480
631
|
|
|
481
632
|
return {
|
|
482
633
|
error: true,
|
|
483
634
|
message: '未找到匹配的 Mock 请求',
|
|
484
635
|
request: JSON.stringify(requestBodyForCompare),
|
|
485
|
-
mockRequests: JSON.stringify(
|
|
636
|
+
mockRequests: JSON.stringify(processedMockRequests.map((req, index) => ({
|
|
486
637
|
index,
|
|
487
638
|
request: req
|
|
488
639
|
})))
|