mock-service-cli 3.7.0 → 4.0.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/bin/mock-service-cli +1 -266
- package/{lib → dist}/asyncTaskQueue.js +6 -18
- package/dist/cli.js +6608 -0
- package/dist/fileExplorerServer.js +28186 -0
- package/dist/manageMockFiles.js +5020 -0
- package/dist/mockServer.js +53214 -0
- package/dist/packageInfo.js +19 -0
- package/dist/staticServer.js +26688 -0
- package/dist/utils.js +857 -0
- package/package.json +21 -17
- package/lib/fileExplorerServer.js +0 -330
- package/lib/manageMockFiles.js +0 -252
- package/lib/mockServer.js +0 -497
- package/lib/staticServer.js +0 -104
- package/lib/utils.js +0 -297
- /package/{lib → dist}/api-overview.html +0 -0
- /package/{lib → dist}/file-explorer.html +0 -0
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-service-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
|
|
5
|
-
"main": "./
|
|
5
|
+
"main": "./dist/mockServer.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mock-service-cli": "bin/mock-service-cli"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"build": "node ./scripts/build.js",
|
|
11
|
+
"prepack": "npm run build",
|
|
12
|
+
"prestart": "npm run build",
|
|
10
13
|
"start": "node ./bin/mock-service-cli",
|
|
14
|
+
"pretest": "npm run build",
|
|
11
15
|
"test:unit": "tap --reporter=spec test/*.test.js",
|
|
12
|
-
"test": "tap --cov --coverage-report=lcov --reporter=spec test/*.test.js
|
|
16
|
+
"test": "tap --cov --coverage-report=lcov --reporter=spec test/*.test.js",
|
|
13
17
|
"test-watch": "tap --reporter=spec --watch test/*.test.js",
|
|
14
18
|
"fix": "eslint --ext .js,.vue,.css --fix",
|
|
15
19
|
"prettier": "prettier -c --write \"**/*.{ts,js,jsx,css,less,scss,json}\"",
|
|
@@ -21,12 +25,11 @@
|
|
|
21
25
|
"release:major": "standard-version --release-as major && git push --follow-tags origin master"
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
24
|
-
"lib",
|
|
25
28
|
"bin",
|
|
26
|
-
"
|
|
29
|
+
"dist"
|
|
27
30
|
],
|
|
28
31
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
32
|
+
"node": ">=16"
|
|
30
33
|
},
|
|
31
34
|
"author": "chendq <deqiaochen@gmail.com>",
|
|
32
35
|
"keywords": [
|
|
@@ -50,27 +53,28 @@
|
|
|
50
53
|
"license": "MIT",
|
|
51
54
|
"homepage": "https://github.com/chandq/mock-service-cli#readme",
|
|
52
55
|
"dependencies": {
|
|
53
|
-
"
|
|
54
|
-
"colors": "^1.4.0",
|
|
55
|
-
"express": "^4.17.1",
|
|
56
|
-
"fs-extra": "^10.0.0",
|
|
57
|
-
"http-proxy-middleware": "^2.0.6",
|
|
58
|
-
"lodash": "^4.17.21",
|
|
59
|
-
"minimist": "^1.2.6",
|
|
60
|
-
"nodemon": "^2.0.19",
|
|
61
|
-
"portfinder": "^1.0.28",
|
|
62
|
-
"socket.io": "^4.4.0"
|
|
56
|
+
"nodemon": "^2.0.19"
|
|
63
57
|
},
|
|
64
58
|
"devDependencies": {
|
|
65
59
|
"@commitlint/cli": "^8.3.5",
|
|
66
60
|
"@commitlint/config-conventional": "^8.3.4",
|
|
67
61
|
"babel-eslint": "^10.1.0",
|
|
62
|
+
"body-parser": "^1.20.2",
|
|
63
|
+
"colors": "^1.4.0",
|
|
64
|
+
"esbuild": "^0.21.5",
|
|
68
65
|
"eslint": "^7.0.0",
|
|
69
66
|
"eslint-config-populist": "^4.2.0",
|
|
70
67
|
"eslint-plugin-mocha": "^9.0.0",
|
|
68
|
+
"express": "^4.17.1",
|
|
69
|
+
"fs-extra": "^10.0.0",
|
|
71
70
|
"husky": "^4.3.8",
|
|
71
|
+
"http-proxy-middleware": "^2.0.6",
|
|
72
72
|
"lint-staged": "^10.2.2",
|
|
73
|
+
"lodash": "^4.17.21",
|
|
74
|
+
"minimist": "^1.2.6",
|
|
73
75
|
"mockjs": "^0.1.10",
|
|
76
|
+
"portfinder": "^1.0.28",
|
|
77
|
+
"socket.io": "^4.4.0",
|
|
74
78
|
"standard-version": "^8.0.0",
|
|
75
79
|
"tap": "^15.0.10"
|
|
76
80
|
},
|
|
@@ -98,7 +102,7 @@
|
|
|
98
102
|
},
|
|
99
103
|
"directories": {
|
|
100
104
|
"doc": "docs",
|
|
101
|
-
"lib": "lib",
|
|
105
|
+
"lib": "src/lib",
|
|
102
106
|
"test": "test"
|
|
103
107
|
}
|
|
104
108
|
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
const express = require('express'),
|
|
2
|
-
bodyParser = require('body-parser'),
|
|
3
|
-
{ readdirSync, existsSync, readFileSync, statSync, createReadStream } = require('fs'),
|
|
4
|
-
os = require('os'),
|
|
5
|
-
path = require('path'),
|
|
6
|
-
colors = require('colors/safe'),
|
|
7
|
-
portfinder = require('portfinder'),
|
|
8
|
-
{ exec } = require('child_process');
|
|
9
|
-
const ifaces = os.networkInterfaces();
|
|
10
|
-
const { dateFormat, logger } = require('./utils');
|
|
11
|
-
|
|
12
|
-
const app = express();
|
|
13
|
-
const log = logger(process.env.SILENT);
|
|
14
|
-
const argv = JSON.parse(process.env.ARGV);
|
|
15
|
-
|
|
16
|
-
const explorerRoot = process.env.EXPLORER_DIRECTORY || process.cwd();
|
|
17
|
-
const port = argv.p || argv.port;
|
|
18
|
-
|
|
19
|
-
if (!process.env.PORT) {
|
|
20
|
-
portfinder.basePort = port || 8090;
|
|
21
|
-
portfinder.getPort(function (err, foundPort) {
|
|
22
|
-
if (err) {
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
25
|
-
process.env.PORT = foundPort;
|
|
26
|
-
init();
|
|
27
|
-
});
|
|
28
|
-
} else {
|
|
29
|
-
init();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function init() {
|
|
33
|
-
app.use(crossDomain());
|
|
34
|
-
app.use(bodyParser.json());
|
|
35
|
-
|
|
36
|
-
// 文件浏览页面
|
|
37
|
-
app.get('/', (req, res) => {
|
|
38
|
-
const htmlPath = path.resolve(__dirname, './file-explorer.html');
|
|
39
|
-
if (existsSync(htmlPath)) {
|
|
40
|
-
res.sendFile(htmlPath);
|
|
41
|
-
} else {
|
|
42
|
-
res.status(404).send('File explorer page not found');
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// 获取目录内容 API
|
|
47
|
-
app.get('/__api/list', (req, res) => {
|
|
48
|
-
let dirPath = req.query.path || '/';
|
|
49
|
-
dirPath = decodeURIComponent(dirPath);
|
|
50
|
-
|
|
51
|
-
let fullPath;
|
|
52
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
53
|
-
|
|
54
|
-
if (explorerRoot === '/') {
|
|
55
|
-
// 根目录,可以直接使用绝对路径
|
|
56
|
-
fullPath = dirPath;
|
|
57
|
-
} else {
|
|
58
|
-
// 非根目录,应该将 dirPath 视为相对于 explorerRoot 的路径
|
|
59
|
-
// 如果 dirPath 以 / 开头,去掉前面的 /
|
|
60
|
-
const relativePath = dirPath.startsWith('/') ? dirPath.substring(1) : dirPath;
|
|
61
|
-
fullPath = path.resolve(explorerRoot, relativePath);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 防止路径遍历攻击 - 确保路径不会越界
|
|
65
|
-
if (explorerRoot !== '/') {
|
|
66
|
-
if (!fullPath.startsWith(resolvedRoot)) {
|
|
67
|
-
return res.status(403).json({ error: 'Access denied' });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!existsSync(fullPath)) {
|
|
72
|
-
return res.status(404).json({ error: 'Path not found' });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const stats = statSync(fullPath);
|
|
76
|
-
if (!stats.isDirectory()) {
|
|
77
|
-
return res.status(400).json({ error: 'Not a directory' });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const files = readdirSync(fullPath, { withFileTypes: true });
|
|
82
|
-
const result = [];
|
|
83
|
-
|
|
84
|
-
files.forEach(file => {
|
|
85
|
-
const filePath = path.join(fullPath, file.name);
|
|
86
|
-
let fileStats;
|
|
87
|
-
let hasError = false;
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
fileStats = statSync(filePath);
|
|
91
|
-
} catch (statError) {
|
|
92
|
-
hasError = true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const relativePath = path.join(dirPath, file.name);
|
|
96
|
-
|
|
97
|
-
// 使用 statSync 的结果,因为 readdirSync 在根目录对某些特殊目录识别不准确
|
|
98
|
-
const isDirectory = hasError ? file.isDirectory() : fileStats.isDirectory();
|
|
99
|
-
|
|
100
|
-
result.push({
|
|
101
|
-
name: file.name,
|
|
102
|
-
path: relativePath.replace(/\\/g, '/'),
|
|
103
|
-
isDirectory: isDirectory,
|
|
104
|
-
size: hasError ? 0 : fileStats.size,
|
|
105
|
-
mtime: hasError ? new Date() : fileStats.mtime,
|
|
106
|
-
birthtime: hasError ? new Date() : fileStats.birthtime,
|
|
107
|
-
isHidden: file.name.startsWith('.'),
|
|
108
|
-
error: hasError ? 'Cannot access file' : null
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// 排序:目录在前,文件在后,然后按名称排序
|
|
113
|
-
result.sort((a, b) => {
|
|
114
|
-
if (a.isDirectory !== b.isDirectory) {
|
|
115
|
-
return a.isDirectory ? -1 : 1;
|
|
116
|
-
}
|
|
117
|
-
return a.name.localeCompare(b.name);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
res.json({
|
|
121
|
-
currentPath: dirPath,
|
|
122
|
-
parentPath: dirPath === '/' ? null : path.dirname(dirPath).replace(/\\/g, '/'),
|
|
123
|
-
files: result
|
|
124
|
-
});
|
|
125
|
-
} catch (error) {
|
|
126
|
-
res.status(500).json({ error: error.message });
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// 文件预览 API
|
|
131
|
-
app.get('/__api/file', (req, res) => {
|
|
132
|
-
let filePath = req.query.path || '/';
|
|
133
|
-
filePath = decodeURIComponent(filePath);
|
|
134
|
-
|
|
135
|
-
let fullPath;
|
|
136
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
137
|
-
|
|
138
|
-
if (explorerRoot === '/') {
|
|
139
|
-
// 根目录,可以直接使用绝对路径
|
|
140
|
-
fullPath = filePath;
|
|
141
|
-
} else {
|
|
142
|
-
// 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
|
|
143
|
-
// 如果 filePath 以 / 开头,去掉前面的 /
|
|
144
|
-
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
145
|
-
fullPath = path.resolve(explorerRoot, relativePath);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 防止路径遍历攻击 - 确保路径不会越界
|
|
149
|
-
if (explorerRoot !== '/') {
|
|
150
|
-
if (!fullPath.startsWith(resolvedRoot)) {
|
|
151
|
-
return res.status(403).json({ error: 'Access denied' });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (!existsSync(fullPath)) {
|
|
156
|
-
return res.status(404).json({ error: 'File not found' });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const stats = statSync(fullPath);
|
|
160
|
-
if (stats.isDirectory()) {
|
|
161
|
-
return res.status(400).json({ error: 'Is a directory' });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
165
|
-
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico'];
|
|
166
|
-
const textExts = ['.txt', '.json', '.js', '.css', '.html', '.xml', '.md', '.csv', '.yaml', '.yml', '.log'];
|
|
167
|
-
|
|
168
|
-
if (imageExts.includes(ext)) {
|
|
169
|
-
res.sendFile(fullPath);
|
|
170
|
-
} else if (textExts.includes(ext) || stats.size < 1024 * 1024) {
|
|
171
|
-
// 小于1MB的文件尝试作为文本读取
|
|
172
|
-
try {
|
|
173
|
-
const content = readFileSync(fullPath, 'utf-8');
|
|
174
|
-
res.json({
|
|
175
|
-
name: path.basename(filePath),
|
|
176
|
-
type: 'text',
|
|
177
|
-
content: content,
|
|
178
|
-
size: stats.size
|
|
179
|
-
});
|
|
180
|
-
} catch (error) {
|
|
181
|
-
// 如果不能作为文本读取,直接提供下载
|
|
182
|
-
res.download(fullPath);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
res.download(fullPath);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// 在系统文件管理器中打开目录/文件 API
|
|
190
|
-
app.post('/__api/open-in-explorer', (req, res) => {
|
|
191
|
-
let filePath = req.body.path || '/';
|
|
192
|
-
filePath = decodeURIComponent(filePath);
|
|
193
|
-
|
|
194
|
-
let fullPath;
|
|
195
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
196
|
-
|
|
197
|
-
if (explorerRoot === '/') {
|
|
198
|
-
// 根目录,可以直接使用绝对路径
|
|
199
|
-
fullPath = filePath;
|
|
200
|
-
} else {
|
|
201
|
-
// 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
|
|
202
|
-
// 如果 filePath 以 / 开头,去掉前面的 /
|
|
203
|
-
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
204
|
-
fullPath = path.resolve(explorerRoot, relativePath);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 防止路径遍历攻击 - 确保路径不会越界
|
|
208
|
-
if (explorerRoot !== '/') {
|
|
209
|
-
if (!fullPath.startsWith(resolvedRoot)) {
|
|
210
|
-
return res.status(403).json({ error: 'Access denied' });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!existsSync(fullPath)) {
|
|
215
|
-
return res.status(404).json({ error: 'Path not found' });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
let openCommand;
|
|
219
|
-
switch (process.platform) {
|
|
220
|
-
case 'darwin':
|
|
221
|
-
openCommand = `open "${fullPath}"`;
|
|
222
|
-
break;
|
|
223
|
-
case 'win32':
|
|
224
|
-
openCommand = `explorer "${fullPath}"`;
|
|
225
|
-
break;
|
|
226
|
-
case 'linux':
|
|
227
|
-
openCommand = `xdg-open "${fullPath}"`;
|
|
228
|
-
break;
|
|
229
|
-
default:
|
|
230
|
-
return res.status(400).json({ error: 'Unsupported platform' });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
exec(openCommand, error => {
|
|
234
|
-
if (error) {
|
|
235
|
-
console.error(colors.red(`Failed to open in explorer: ${error.message}`));
|
|
236
|
-
return res.status(500).json({ error: 'Failed to open in explorer' });
|
|
237
|
-
}
|
|
238
|
-
res.json({ success: true, path: fullPath });
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
startServer();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function crossDomain() {
|
|
246
|
-
return (req, res, next) => {
|
|
247
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
248
|
-
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|
249
|
-
res.header('Access-Control-Allow-Headers', '*');
|
|
250
|
-
if (req.method === 'OPTIONS') res.status(200);
|
|
251
|
-
next();
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function startServer() {
|
|
256
|
-
const http = require('http').createServer(app);
|
|
257
|
-
|
|
258
|
-
http.listen(Number.parseInt(process.env.PORT, 10), () => {
|
|
259
|
-
console.info(
|
|
260
|
-
[
|
|
261
|
-
colors.yellow(`\nStarting up file-explorer-server, serving `),
|
|
262
|
-
colors.cyan(explorerRoot),
|
|
263
|
-
colors.yellow(` ${dateFormat('YYYY-mm-dd HH:MM:SS', new Date())}`)
|
|
264
|
-
].join('')
|
|
265
|
-
);
|
|
266
|
-
console.info(
|
|
267
|
-
[
|
|
268
|
-
colors.yellow('\n🌍 file-explorer-server version: '),
|
|
269
|
-
colors.cyan(require('../package.json').version),
|
|
270
|
-
'\n'
|
|
271
|
-
].join('')
|
|
272
|
-
);
|
|
273
|
-
console.info(colors.yellow(`\n File explorer server available on:\n`));
|
|
274
|
-
console.info(' http://localhost:' + colors.green(process.env.PORT));
|
|
275
|
-
Object.keys(ifaces).forEach(function (dev) {
|
|
276
|
-
ifaces[dev].forEach(function (details) {
|
|
277
|
-
if (details.family === 'IPv4') {
|
|
278
|
-
console.info(' http://' + details.address + ':' + colors.green(process.env.PORT));
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// 自动打开浏览器
|
|
284
|
-
if (process.env.OPEN_API_OVERVIEW && !process.env.RESTARTED) {
|
|
285
|
-
const url = `http://localhost:${process.env.PORT}`;
|
|
286
|
-
console.info(colors.yellow(`\nOpening file explorer...`));
|
|
287
|
-
let openCommand;
|
|
288
|
-
switch (process.platform) {
|
|
289
|
-
case 'darwin':
|
|
290
|
-
openCommand = `open ${url}`;
|
|
291
|
-
break;
|
|
292
|
-
case 'win32':
|
|
293
|
-
openCommand = `start ${url}`;
|
|
294
|
-
break;
|
|
295
|
-
case 'linux':
|
|
296
|
-
openCommand = `xdg-open ${url}`;
|
|
297
|
-
break;
|
|
298
|
-
default:
|
|
299
|
-
console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${url}`));
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
exec(openCommand, error => {
|
|
303
|
-
if (error) {
|
|
304
|
-
console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${url}`));
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (process.platform === 'win32') {
|
|
312
|
-
require('readline')
|
|
313
|
-
.createInterface({
|
|
314
|
-
input: process.stdin,
|
|
315
|
-
output: process.stdout
|
|
316
|
-
})
|
|
317
|
-
.on('SIGINT', function () {
|
|
318
|
-
process.emit('SIGINT');
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
process.on('SIGINT', function () {
|
|
323
|
-
log.info(colors.red('file-explorer-server process stopped.'));
|
|
324
|
-
process.exit();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
process.on('SIGTERM', function () {
|
|
328
|
-
log.info(colors.red('file-explorer-server process stopped.'));
|
|
329
|
-
process.exit();
|
|
330
|
-
});
|
package/lib/manageMockFiles.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @description: 生成Mock文件、获取mock数据统计
|
|
3
|
-
* @Date: 2021-12-22 16:57:08
|
|
4
|
-
* @LastEditors: chendq
|
|
5
|
-
* @LastEditTime: 2025-06-15 22:25:45
|
|
6
|
-
* @Author: chendq
|
|
7
|
-
*/
|
|
8
|
-
const fsa = require('fs-extra'),
|
|
9
|
-
fs = require('fs'),
|
|
10
|
-
path = require('path'),
|
|
11
|
-
isEqual = require('lodash/isEqual'),
|
|
12
|
-
colors = require('colors/safe');
|
|
13
|
-
const {
|
|
14
|
-
getFileLatestContent,
|
|
15
|
-
SupportMethods,
|
|
16
|
-
filePath2ApiUrl,
|
|
17
|
-
logger,
|
|
18
|
-
getDataType,
|
|
19
|
-
getLogger,
|
|
20
|
-
writeStream
|
|
21
|
-
} = require('./utils');
|
|
22
|
-
const log = logger(process.env.SILENT);
|
|
23
|
-
const processArgv = process.env.ARGV ? JSON.parse(process.env.ARGV) : {};
|
|
24
|
-
|
|
25
|
-
const methodRegExp = new RegExp(`(${SupportMethods.join('|')}) +(.*)`, 'i');
|
|
26
|
-
/**
|
|
27
|
-
* @description: 记录日志文件函数
|
|
28
|
-
* @param {boolean} isWriteLogFile 是否写入日志文件
|
|
29
|
-
* @param {string} mockDir 存放日志文件的目录
|
|
30
|
-
* @return {object}
|
|
31
|
-
*/
|
|
32
|
-
const logFile = (isWriteLogFile = false, mockDir) => {
|
|
33
|
-
if (isWriteLogFile) {
|
|
34
|
-
return getLogger(mockDir);
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
log: function () {},
|
|
38
|
-
error: function () {}
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* @description: 生成mock文件(自动生成mock-list.json文件,并维护<url, [method]>的关系映射)
|
|
43
|
-
* @param {string} apiUrl 请求url
|
|
44
|
-
* @param {string} method 请求方法
|
|
45
|
-
* @param {object} resJsonData 响应数据
|
|
46
|
-
* @param {string} mockDir mock目录
|
|
47
|
-
* @return {*} void
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
const genMockFiles = async function ({ url: apiUrl, method, data: resJsonData, dir: mockDir }) {
|
|
51
|
-
const type = getDataType(resJsonData);
|
|
52
|
-
if (!apiUrl || !method || !resJsonData || !mockDir) {
|
|
53
|
-
log.info(
|
|
54
|
-
colors.red([`参数: apiUrl: ${apiUrl}, method: ${method}, mockDir: ${mockDir} `, `必须是有效值`].join(','))
|
|
55
|
-
);
|
|
56
|
-
return;
|
|
57
|
-
} else if (
|
|
58
|
-
!['object', 'array'].includes(type) ||
|
|
59
|
-
(type === 'object' && Object.keys(resJsonData).length === 0) ||
|
|
60
|
-
(type === 'array' && resJsonData.length === 0)
|
|
61
|
-
) {
|
|
62
|
-
log.info(
|
|
63
|
-
`参数: apiUrl: ${apiUrl}, method: ${method}, resJsonData:`,
|
|
64
|
-
resJsonData,
|
|
65
|
-
colors.red(', 参数字段resJsonData必须是非空的数组或对象!')
|
|
66
|
-
);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
apiUrl = apiUrl.trim().split('?')[0];
|
|
70
|
-
const LOGGER = logFile(processArgv.t || processArgv.track, mockDir);
|
|
71
|
-
const normalizeApiUrl = filePath2ApiUrl(path.normalize(apiUrl));
|
|
72
|
-
const mockListFilePath = path.resolve(process.cwd(), `${mockDir}/mock-list.json`);
|
|
73
|
-
const mockFilePath = path.resolve(process.cwd(), `${mockDir}/${encodeURIComponent(path.normalize(apiUrl))}.json`);
|
|
74
|
-
let newMockListContent = {};
|
|
75
|
-
if (fsa.pathExistsSync(mockListFilePath)) {
|
|
76
|
-
newMockListContent = getFileLatestContent(mockListFilePath);
|
|
77
|
-
} else {
|
|
78
|
-
fsa.ensureFileSync(mockListFilePath);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let isChange = false;
|
|
82
|
-
// update mock-list.json file
|
|
83
|
-
if (!newMockListContent.hasOwnProperty(normalizeApiUrl)) {
|
|
84
|
-
newMockListContent[normalizeApiUrl] = [method];
|
|
85
|
-
isChange = true;
|
|
86
|
-
} else if (!newMockListContent[normalizeApiUrl].includes(method)) {
|
|
87
|
-
newMockListContent[normalizeApiUrl].push(method);
|
|
88
|
-
isChange = true;
|
|
89
|
-
}
|
|
90
|
-
if (isChange) {
|
|
91
|
-
Object.keys(newMockListContent).length > 0 &&
|
|
92
|
-
fs.writeFileSync(mockListFilePath, JSON.stringify(newMockListContent, null, 2));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// mock文件存在
|
|
96
|
-
if (fsa.pathExistsSync(mockFilePath)) {
|
|
97
|
-
const mockFileContent = getFileLatestContent(mockFilePath);
|
|
98
|
-
// 内容一样不作修改
|
|
99
|
-
if (mockFileContent.hasOwnProperty(method) && isEqual(mockFileContent[method], resJsonData)) {
|
|
100
|
-
// log.assert(
|
|
101
|
-
// !_.isEqual(mockFileContent[method], resJsonData),
|
|
102
|
-
// colors.bgYellow(`${method} ${apiUrl}, Mock data is same`)
|
|
103
|
-
// );
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
mockFileContent[method] = resJsonData;
|
|
107
|
-
try {
|
|
108
|
-
await writeStream(mockFilePath, JSON.stringify(mockFileContent, null, 2));
|
|
109
|
-
} catch (error) {
|
|
110
|
-
LOGGER.error(`Update file: ${method} ${apiUrl} ${mockFilePath}`);
|
|
111
|
-
}
|
|
112
|
-
LOGGER.log(`Update file: ${method} ${apiUrl} ${mockFilePath}`);
|
|
113
|
-
} else {
|
|
114
|
-
try {
|
|
115
|
-
await writeStream(mockFilePath, JSON.stringify({ [method]: resJsonData }, null, 2));
|
|
116
|
-
} catch (error) {
|
|
117
|
-
LOGGER.error(`New file: ${method} ${apiUrl} ${mockFilePath}`);
|
|
118
|
-
}
|
|
119
|
-
LOGGER.log(`New file: ${method} ${apiUrl} ${mockFilePath}`);
|
|
120
|
-
}
|
|
121
|
-
// console.debug('file::', `${mockDir}/${apiUrl}`, newMockListContent);
|
|
122
|
-
};
|
|
123
|
-
/**
|
|
124
|
-
* @description: 从js文件中收集mock数据
|
|
125
|
-
* @param {object} mockDataMap
|
|
126
|
-
* @param {string} filePath
|
|
127
|
-
* @return {*} void
|
|
128
|
-
*/
|
|
129
|
-
const collectMockDataFromJsFile = function (mockDataMap, filePath) {
|
|
130
|
-
// 先删除require之前导入的缓存文件,否则获取到的文件内容还是旧内容
|
|
131
|
-
const fileObject = getFileLatestContent(filePath);
|
|
132
|
-
|
|
133
|
-
Object.keys(fileObject).forEach(item => {
|
|
134
|
-
let reqMethod = 'get',
|
|
135
|
-
reqUrl = item;
|
|
136
|
-
if (methodRegExp.exec(item)) {
|
|
137
|
-
const [, method, url] = methodRegExp.exec(item);
|
|
138
|
-
reqMethod = method.toLowerCase();
|
|
139
|
-
reqUrl = path.normalize(url);
|
|
140
|
-
}
|
|
141
|
-
if (typeof fileObject[item] === 'function') {
|
|
142
|
-
mockDataMap[`${reqMethod} ${reqUrl}`] = String(fileObject[item]);
|
|
143
|
-
} else if (typeof fileObject[item] === 'object') {
|
|
144
|
-
mockDataMap[`${reqMethod} ${reqUrl}`] = fileObject[item];
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @description: 递归收集mock数据
|
|
151
|
-
* @param {object} mockDataMap mock数据集
|
|
152
|
-
* @param {string} specialDir 目录
|
|
153
|
-
* @return {*} void
|
|
154
|
-
*/
|
|
155
|
-
const deepCollectMockData = function (mockDataMap, specialDir = '../mock') {
|
|
156
|
-
const files = fs.readdirSync(path.resolve(process.cwd(), specialDir), { withFileTypes: true });
|
|
157
|
-
const curFilesSize = files.length;
|
|
158
|
-
for (let index = 0; index < curFilesSize; index++) {
|
|
159
|
-
const el = files[index];
|
|
160
|
-
const filePath = path.normalize(`${specialDir}/${el.name}`);
|
|
161
|
-
// 递归目录
|
|
162
|
-
if (!el.isFile()) {
|
|
163
|
-
deepCollectMockData(mockDataMap, filePath);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 处理存放mock数据的json文件
|
|
167
|
-
if (el.name.endsWith('.json') && el.name !== 'mock-list.json') {
|
|
168
|
-
const fileObject = getFileLatestContent(filePath);
|
|
169
|
-
Object.keys(fileObject).forEach(method => {
|
|
170
|
-
mockDataMap[`${method.toLocaleLowerCase()} ${decodeURIComponent(el.name).split('.json')[0]}`] =
|
|
171
|
-
fileObject[method];
|
|
172
|
-
});
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 非js文件跳过
|
|
177
|
-
if (!el.name.endsWith('.js')) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
collectMockDataFromJsFile(mockDataMap, filePath);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
/**
|
|
184
|
-
* @description: 从文件目录中获取mock数据统计(若mock-list.json文件不存在则自动生成)
|
|
185
|
-
* @param {string} dirPath
|
|
186
|
-
* @return {object} mock数据统计
|
|
187
|
-
*/
|
|
188
|
-
const getMockStatFromDir = dirPath => {
|
|
189
|
-
if (!fsa.pathExistsSync(dirPath)) {
|
|
190
|
-
getLogger(path.resolve(process.cwd())).error(`入参无效,${dirPath} 目录不存在`);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const mockDataMap = {};
|
|
194
|
-
deepCollectMockData(mockDataMap, dirPath);
|
|
195
|
-
|
|
196
|
-
const mockListFilePath = path.resolve(process.cwd(), `${dirPath}/mock-list.json`);
|
|
197
|
-
// 若不存在mock-list.json文件,则根据现有mock文件重新生成<api, [method]>映射关系
|
|
198
|
-
if (Object.keys(mockDataMap).length > 0 && !fsa.pathExistsSync(mockListFilePath)) {
|
|
199
|
-
fsa.ensureFileSync(mockListFilePath);
|
|
200
|
-
const reverseMockList = {};
|
|
201
|
-
Object.keys(mockDataMap).forEach(it => {
|
|
202
|
-
if (methodRegExp.exec(it)) {
|
|
203
|
-
const [, method, url] = methodRegExp.exec(it);
|
|
204
|
-
if (url.trim() && method.trim()) {
|
|
205
|
-
if (reverseMockList.hasOwnProperty(url)) {
|
|
206
|
-
reverseMockList[url].push(method);
|
|
207
|
-
} else {
|
|
208
|
-
reverseMockList[url] = [method];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
writeStream(mockListFilePath, JSON.stringify(reverseMockList, null, 2));
|
|
214
|
-
}
|
|
215
|
-
return mockDataMap;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* @description: 从js文件中获取mock数据统计
|
|
220
|
-
* @param {string} filePath
|
|
221
|
-
* @return {object} mock数据统计
|
|
222
|
-
*/
|
|
223
|
-
const getMockStatFromFile = filePath => {
|
|
224
|
-
if (!fsa.pathExistsSync(filePath)) {
|
|
225
|
-
getLogger(path.resolve(process.cwd())).error(`入参无效,${filePath} 文件不存在`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const mockDataMap = {};
|
|
229
|
-
collectMockDataFromJsFile(mockDataMap, filePath);
|
|
230
|
-
return mockDataMap;
|
|
231
|
-
};
|
|
232
|
-
/**
|
|
233
|
-
* @description: 判断Mock服务中是否存在某个API
|
|
234
|
-
* @param {object} mockDataMap
|
|
235
|
-
* @param {string} apiUrl
|
|
236
|
-
* @param {string} method
|
|
237
|
-
* @return {boolean}
|
|
238
|
-
*/
|
|
239
|
-
const hasMockApi = function (mockDataMap, apiUrl, method) {
|
|
240
|
-
if (!apiUrl || !method || !mockDataMap || !(mockDataMap instanceof Object)) {
|
|
241
|
-
getLogger(path.resolve(process.cwd())).error(`入参无效,${mockDataMap} 必须是 mock数据的Object对象`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
return mockDataMap.hasOwnProperty(`${method.toLocaleLowerCase()} ${path.normalize(apiUrl)}`);
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
module.exports = {
|
|
248
|
-
genMockFiles,
|
|
249
|
-
getMockStatFromDir,
|
|
250
|
-
getMockStatFromFile,
|
|
251
|
-
hasMockApi
|
|
252
|
-
};
|