mock-service-cli 3.3.6 → 3.5.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.
@@ -0,0 +1,278 @@
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
+ const fullPath = path.resolve(explorerRoot, '.' + dirPath);
52
+
53
+ // 防止路径遍历攻击
54
+ if (!fullPath.startsWith(path.resolve(explorerRoot))) {
55
+ return res.status(403).json({ error: 'Access denied' });
56
+ }
57
+
58
+ if (!existsSync(fullPath)) {
59
+ return res.status(404).json({ error: 'Path not found' });
60
+ }
61
+
62
+ const stats = statSync(fullPath);
63
+ if (!stats.isDirectory()) {
64
+ return res.status(400).json({ error: 'Not a directory' });
65
+ }
66
+
67
+ try {
68
+ const files = readdirSync(fullPath, { withFileTypes: true });
69
+ const result = [];
70
+
71
+ files.forEach(file => {
72
+ const filePath = path.join(fullPath, file.name);
73
+ const fileStats = statSync(filePath);
74
+ const relativePath = path.join(dirPath, file.name);
75
+
76
+ result.push({
77
+ name: file.name,
78
+ path: relativePath.replace(/\\/g, '/'),
79
+ isDirectory: file.isDirectory(),
80
+ size: fileStats.size,
81
+ mtime: fileStats.mtime,
82
+ isHidden: file.name.startsWith('.')
83
+ });
84
+ });
85
+
86
+ // 排序:目录在前,文件在后,然后按名称排序
87
+ result.sort((a, b) => {
88
+ if (a.isDirectory !== b.isDirectory) {
89
+ return a.isDirectory ? -1 : 1;
90
+ }
91
+ return a.name.localeCompare(b.name);
92
+ });
93
+
94
+ res.json({
95
+ currentPath: dirPath,
96
+ parentPath: dirPath === '/' ? null : path.dirname(dirPath).replace(/\\/g, '/'),
97
+ files: result
98
+ });
99
+ } catch (error) {
100
+ res.status(500).json({ error: error.message });
101
+ }
102
+ });
103
+
104
+ // 文件预览 API
105
+ app.get('/__api/file', (req, res) => {
106
+ let filePath = req.query.path || '/';
107
+ filePath = decodeURIComponent(filePath);
108
+
109
+ const fullPath = path.resolve(explorerRoot, '.' + filePath);
110
+
111
+ // 防止路径遍历攻击
112
+ if (!fullPath.startsWith(path.resolve(explorerRoot))) {
113
+ return res.status(403).json({ error: 'Access denied' });
114
+ }
115
+
116
+ if (!existsSync(fullPath)) {
117
+ return res.status(404).json({ error: 'File not found' });
118
+ }
119
+
120
+ const stats = statSync(fullPath);
121
+ if (stats.isDirectory()) {
122
+ return res.status(400).json({ error: 'Is a directory' });
123
+ }
124
+
125
+ const ext = path.extname(filePath).toLowerCase();
126
+ const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico'];
127
+ const textExts = ['.txt', '.json', '.js', '.css', '.html', '.xml', '.md', '.csv', '.yaml', '.yml', '.log'];
128
+
129
+ if (imageExts.includes(ext)) {
130
+ res.sendFile(fullPath);
131
+ } else if (textExts.includes(ext) || stats.size < 1024 * 1024) {
132
+ // 小于1MB的文件尝试作为文本读取
133
+ try {
134
+ const content = readFileSync(fullPath, 'utf-8');
135
+ res.json({
136
+ name: path.basename(filePath),
137
+ type: 'text',
138
+ content: content,
139
+ size: stats.size
140
+ });
141
+ } catch (error) {
142
+ // 如果不能作为文本读取,直接提供下载
143
+ res.download(fullPath);
144
+ }
145
+ } else {
146
+ res.download(fullPath);
147
+ }
148
+ });
149
+
150
+ // 在系统文件管理器中打开目录/文件 API
151
+ app.post('/__api/open-in-explorer', (req, res) => {
152
+ let filePath = req.body.path || '/';
153
+ filePath = decodeURIComponent(filePath);
154
+
155
+ const fullPath = path.resolve(explorerRoot, '.' + filePath);
156
+
157
+ // 防止路径遍历攻击
158
+ if (!fullPath.startsWith(path.resolve(explorerRoot))) {
159
+ return res.status(403).json({ error: 'Access denied' });
160
+ }
161
+
162
+ if (!existsSync(fullPath)) {
163
+ return res.status(404).json({ error: 'Path not found' });
164
+ }
165
+
166
+ let openCommand;
167
+ switch (process.platform) {
168
+ case 'darwin':
169
+ openCommand = `open "${fullPath}"`;
170
+ break;
171
+ case 'win32':
172
+ openCommand = `explorer "${fullPath}"`;
173
+ break;
174
+ case 'linux':
175
+ openCommand = `xdg-open "${fullPath}"`;
176
+ break;
177
+ default:
178
+ return res.status(400).json({ error: 'Unsupported platform' });
179
+ }
180
+
181
+ exec(openCommand, error => {
182
+ if (error) {
183
+ console.error(colors.red(`Failed to open in explorer: ${error.message}`));
184
+ return res.status(500).json({ error: 'Failed to open in explorer' });
185
+ }
186
+ res.json({ success: true, path: fullPath });
187
+ });
188
+ });
189
+
190
+ startServer();
191
+ }
192
+
193
+ function crossDomain() {
194
+ return (req, res, next) => {
195
+ res.header('Access-Control-Allow-Origin', '*');
196
+ res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
197
+ res.header('Access-Control-Allow-Headers', '*');
198
+ if (req.method === 'OPTIONS') res.status(200);
199
+ next();
200
+ };
201
+ }
202
+
203
+ function startServer() {
204
+ const http = require('http').createServer(app);
205
+
206
+ http.listen(Number.parseInt(process.env.PORT, 10), () => {
207
+ console.info(
208
+ [
209
+ colors.yellow(`\nStarting up file-explorer-server, serving `),
210
+ colors.cyan(explorerRoot),
211
+ colors.yellow(` ${dateFormat('YYYY-mm-dd HH:MM:SS', new Date())}`)
212
+ ].join('')
213
+ );
214
+ console.info(
215
+ [
216
+ colors.yellow('\n🌍 file-explorer-server version: '),
217
+ colors.cyan(require('../package.json').version),
218
+ '\n'
219
+ ].join('')
220
+ );
221
+ console.info(colors.yellow(`\n File explorer server available on:\n`));
222
+ console.info(' http://localhost:' + colors.green(process.env.PORT));
223
+ Object.keys(ifaces).forEach(function (dev) {
224
+ ifaces[dev].forEach(function (details) {
225
+ if (details.family === 'IPv4') {
226
+ console.info(' http://' + details.address + ':' + colors.green(process.env.PORT));
227
+ }
228
+ });
229
+ });
230
+
231
+ // 自动打开浏览器
232
+ if (process.env.OPEN_API_OVERVIEW && !process.env.RESTARTED) {
233
+ const url = `http://localhost:${process.env.PORT}`;
234
+ console.info(colors.yellow(`\nOpening file explorer...`));
235
+ let openCommand;
236
+ switch (process.platform) {
237
+ case 'darwin':
238
+ openCommand = `open ${url}`;
239
+ break;
240
+ case 'win32':
241
+ openCommand = `start ${url}`;
242
+ break;
243
+ case 'linux':
244
+ openCommand = `xdg-open ${url}`;
245
+ break;
246
+ default:
247
+ console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${url}`));
248
+ return;
249
+ }
250
+ exec(openCommand, error => {
251
+ if (error) {
252
+ console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${url}`));
253
+ }
254
+ });
255
+ }
256
+ });
257
+ }
258
+
259
+ if (process.platform === 'win32') {
260
+ require('readline')
261
+ .createInterface({
262
+ input: process.stdin,
263
+ output: process.stdout
264
+ })
265
+ .on('SIGINT', function () {
266
+ process.emit('SIGINT');
267
+ });
268
+ }
269
+
270
+ process.on('SIGINT', function () {
271
+ log.info(colors.red('file-explorer-server process stopped.'));
272
+ process.exit();
273
+ });
274
+
275
+ process.on('SIGTERM', function () {
276
+ log.info(colors.red('file-explorer-server process stopped.'));
277
+ process.exit();
278
+ });
package/lib/mockServer.js CHANGED
@@ -4,7 +4,8 @@ const express = require('express'), // 引入express
4
4
  os = require('os'),
5
5
  path = require('path'),
6
6
  colors = require('colors/safe'),
7
- portfinder = require('portfinder');
7
+ portfinder = require('portfinder'),
8
+ { exec } = require('child_process');
8
9
  const { createProxyMiddleware } = require('http-proxy-middleware');
9
10
  const ifaces = os.networkInterfaces();
10
11
  const { Server } = require('socket.io');
@@ -19,6 +20,8 @@ const {
19
20
  } = require('./utils');
20
21
  const { genMockFiles, getMockStatFromDir, getMockStatFromFile } = require('./manageMockFiles');
21
22
 
23
+ const registeredApis = [];
24
+
22
25
  const methodRegExp = new RegExp(`((${SupportMethods.join('|')}) +)?([^?]*)`, 'i');
23
26
 
24
27
  const app = express();
@@ -72,6 +75,36 @@ function init() {
72
75
  app.use(crossDomain()); // 允许跨域
73
76
  app.use(bodyParser.json()); // 解析body
74
77
 
78
+ // API 概览页面路由
79
+ app.get('/__api-overview', (req, res) => {
80
+ const htmlPath = path.resolve(__dirname, './api-overview.html');
81
+ if (existsSync(htmlPath)) {
82
+ res.sendFile(htmlPath);
83
+ } else {
84
+ res.status(404).send('API overview page not found');
85
+ }
86
+ });
87
+
88
+ // API 数据路由
89
+ app.get('/__api-data', (req, res) => {
90
+ // 按目录分组 API
91
+ const groupedApis = {};
92
+ registeredApis.forEach(api => {
93
+ // 从文件路径中提取相对路径作为目录
94
+ const dirPath = path.dirname(api.file);
95
+ if (!groupedApis[dirPath]) {
96
+ groupedApis[dirPath] = [];
97
+ }
98
+ groupedApis[dirPath].push(api);
99
+ });
100
+
101
+ res.json({
102
+ apis: registeredApis,
103
+ groupedApis: groupedApis,
104
+ port: process.env.PORT
105
+ });
106
+ });
107
+
75
108
  let requireMockServe = true;
76
109
  if (process.env.SPECIFIED_FILE) {
77
110
  composeRouteFromJsFile(process.env.SPECIFIED_FILE);
@@ -158,6 +191,13 @@ function composeRouteFromJsFile(file) {
158
191
  res.json(fileObject[item]);
159
192
  });
160
193
  }
194
+ // 收集 API 信息
195
+ registeredApis.push({
196
+ method: reqMethod.toUpperCase(),
197
+ url: reqUrl,
198
+ file: file,
199
+ data: fileObject[item]
200
+ });
161
201
  });
162
202
  }
163
203
  /**
@@ -199,6 +239,13 @@ function parseMockFiles(specialDir = '../mock') {
199
239
  app[method](apiUrl, function (req, res) {
200
240
  res.json(fileObject[method]);
201
241
  });
242
+ // 收集 API 信息
243
+ registeredApis.push({
244
+ method: method.toUpperCase(),
245
+ url: apiUrl,
246
+ file: filePath,
247
+ data: fileObject[method]
248
+ });
202
249
  });
203
250
  continue;
204
251
  }
@@ -361,6 +408,13 @@ function startServer() {
361
408
  );
362
409
  console.info(colors.yellow(`\n Mock server available on:\n`));
363
410
  console.info(' http://localhost:' + colors.green(process.env.PORT));
411
+ console.info(
412
+ ' ' +
413
+ colors.blue('API Overview: ') +
414
+ 'http://localhost:' +
415
+ colors.green(process.env.PORT) +
416
+ colors.green('/__api-overview')
417
+ );
364
418
  Object.keys(ifaces).forEach(function (dev) {
365
419
  ifaces[dev].forEach(function (details) {
366
420
  if (details.family === 'IPv4') {
@@ -369,6 +423,32 @@ function startServer() {
369
423
  });
370
424
  });
371
425
  }
426
+
427
+ // 自动打开浏览器
428
+ if (process.env.OPEN_API_OVERVIEW && !process.env.RESTARTED) {
429
+ const apiOverviewUrl = `http://localhost:${process.env.PORT}/__api-overview`;
430
+ console.info(colors.yellow(`\nOpening API overview page...`));
431
+ let openCommand;
432
+ switch (process.platform) {
433
+ case 'darwin':
434
+ openCommand = `open ${apiOverviewUrl}`;
435
+ break;
436
+ case 'win32':
437
+ openCommand = `start ${apiOverviewUrl}`;
438
+ break;
439
+ case 'linux':
440
+ openCommand = `xdg-open ${apiOverviewUrl}`;
441
+ break;
442
+ default:
443
+ console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${apiOverviewUrl}`));
444
+ return;
445
+ }
446
+ exec(openCommand, error => {
447
+ if (error) {
448
+ console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${apiOverviewUrl}`));
449
+ }
450
+ });
451
+ }
372
452
  });
373
453
  }
374
454
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mock-service-cli",
3
- "version": "3.3.6",
4
- "description": "🦅 Local Mock/Static/SPA server, Http?s request proxy",
3
+ "version": "3.5.0",
4
+ "description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
5
5
  "main": "./lib/mockServer.js",
6
6
  "bin": {
7
7
  "mock-service-cli": "bin/mock-service-cli"
@@ -35,7 +35,12 @@
35
35
  "Mock Server CLI",
36
36
  "SPA Server",
37
37
  "mock-service-cli",
38
- "Socket server"
38
+ "Socket server",
39
+ "API overview page",
40
+ "File explorer",
41
+ "File browser",
42
+ "File manager",
43
+ "express"
39
44
  ],
40
45
  "repository": {
41
46
  "type": "git",