json-api-mocker 2.3.0 → 3.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/DESIGN.md +227 -0
- package/README.ch.md +95 -224
- package/README.md +98 -227
- package/dist/cli.js +37 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -2
- package/dist/server.d.ts +17 -14
- package/dist/server.js +290 -100
- package/dist/types.d.ts +26 -21
- package/package.json +21 -11
- package/web/assets/index-C4eZNDku.js +25 -0
- package/web/assets/index-CGhPNH5w.css +1 -0
- package/web/index.html +15 -0
- package/web/monaco-editor-worker-loader.js +8 -0
- package/web/vite.svg +1 -0
package/dist/server.js
CHANGED
@@ -4,135 +4,325 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.MockServer = void 0;
|
7
|
-
const mockjs_1 = __importDefault(require("mockjs"));
|
8
7
|
const express_1 = __importDefault(require("express"));
|
9
8
|
const cors_1 = __importDefault(require("cors"));
|
9
|
+
const fs_1 = require("fs");
|
10
|
+
const path_1 = require("path");
|
11
|
+
const uuid_1 = require("uuid");
|
12
|
+
const mockjs_1 = __importDefault(require("mockjs"));
|
13
|
+
const multer_1 = __importDefault(require("multer"));
|
14
|
+
const ws_1 = require("ws");
|
10
15
|
class MockServer {
|
11
|
-
constructor(
|
16
|
+
constructor(serverConfig = {}, configPath = 'data.json') {
|
17
|
+
this.logs = []; // 存储请求日志
|
18
|
+
this.MAX_LOGS = 1000; // 最大日志数量
|
19
|
+
this.clients = new Set();
|
12
20
|
this.app = (0, express_1.default)();
|
13
|
-
this.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
console.log(` Method: ${req.method}`);
|
18
|
-
console.log(` URL: ${req.url}`);
|
19
|
-
console.log(` Query Params: ${JSON.stringify(req.query)}`);
|
20
|
-
console.log(` Body: ${JSON.stringify(req.body)}`);
|
21
|
-
res.on('finish', () => {
|
22
|
-
const duration = Date.now() - startTime;
|
23
|
-
console.log(`[${new Date().toISOString()}] Response ${requestId}:`);
|
24
|
-
console.log(` Status: ${res.statusCode}`);
|
25
|
-
console.log(` Duration: ${duration}ms`);
|
26
|
-
console.log('----------------------------------------');
|
27
|
-
});
|
28
|
-
next();
|
29
|
-
};
|
30
|
-
this.config = config;
|
31
|
-
this.configPath = configPath;
|
32
|
-
this.setupMiddleware();
|
33
|
-
this.setupRoutes();
|
34
|
-
}
|
35
|
-
getApp() {
|
36
|
-
return this.app;
|
37
|
-
}
|
38
|
-
setupMiddleware() {
|
21
|
+
this.serverConfig = serverConfig;
|
22
|
+
this.configPath = (0, path_1.join)(process.cwd(), configPath);
|
23
|
+
this.config = this.loadConfig();
|
24
|
+
// 添加跨域支持
|
39
25
|
this.app.use((0, cors_1.default)());
|
40
26
|
this.app.use(express_1.default.json());
|
41
|
-
|
42
|
-
this.
|
27
|
+
// 注意:先设置配置管理接口,再设置mock接口
|
28
|
+
this.setupConfigRoutes();
|
29
|
+
this.setupMockRoutes();
|
30
|
+
// 使用内存存储
|
31
|
+
this.upload = (0, multer_1.default)({
|
32
|
+
storage: multer_1.default.memoryStorage()
|
33
|
+
});
|
34
|
+
// 添加文件上传路由
|
35
|
+
this.setupUploadRoute();
|
36
|
+
// 使用固定的 WebSocket 端口
|
37
|
+
this.wss = new ws_1.WebSocketServer({ port: serverConfig.wsPort || 88866 });
|
38
|
+
this.wss.on('connection', (ws) => {
|
39
|
+
this.clients.add(ws);
|
40
|
+
ws.send(JSON.stringify({
|
41
|
+
type: 'init',
|
42
|
+
data: this.logs
|
43
|
+
}));
|
44
|
+
ws.on('close', () => {
|
45
|
+
this.clients.delete(ws);
|
46
|
+
});
|
47
|
+
});
|
43
48
|
}
|
44
|
-
|
49
|
+
loadConfig() {
|
45
50
|
try {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
const data = (0, fs_1.readFileSync)(this.configPath, 'utf-8');
|
52
|
+
const fullConfig = JSON.parse(data);
|
53
|
+
// 确保返回的是数组
|
54
|
+
const routes = fullConfig.routes || [];
|
55
|
+
// 验每个配置项
|
56
|
+
return routes.filter(api => {
|
57
|
+
const isValid = api &&
|
58
|
+
api.id &&
|
59
|
+
api.route &&
|
60
|
+
typeof api.route.path === 'string' &&
|
61
|
+
api.route.methods &&
|
62
|
+
typeof api.route.methods === 'object';
|
63
|
+
if (!isValid) {
|
64
|
+
console.warn('Filtered out invalid API config:', api);
|
65
|
+
}
|
66
|
+
return isValid;
|
67
|
+
});
|
53
68
|
}
|
54
69
|
catch (error) {
|
55
|
-
console.error('Error
|
56
|
-
return
|
70
|
+
console.error('Error loading config:', error);
|
71
|
+
return [];
|
57
72
|
}
|
58
73
|
}
|
59
|
-
|
60
|
-
|
74
|
+
saveConfig() {
|
75
|
+
try {
|
76
|
+
const data = (0, fs_1.readFileSync)(this.configPath, 'utf-8');
|
77
|
+
const fullConfig = JSON.parse(data);
|
78
|
+
// 处理每个 API 的响应数据
|
79
|
+
const processedRoutes = this.config.map(api => ({
|
80
|
+
...api,
|
81
|
+
route: {
|
82
|
+
...api.route,
|
83
|
+
methods: Object.entries(api.route.methods).reduce((acc, [method, config]) => ({
|
84
|
+
...acc,
|
85
|
+
[method]: {
|
86
|
+
...config,
|
87
|
+
// 解析并重新格式化 JSON,移除多余的转义
|
88
|
+
response: typeof config.response === 'string'
|
89
|
+
? JSON.stringify(JSON.parse(config.response))
|
90
|
+
: JSON.stringify(config.response),
|
91
|
+
headers: config.headers || {}
|
92
|
+
}
|
93
|
+
}), {})
|
94
|
+
}
|
95
|
+
}));
|
96
|
+
fullConfig.routes = processedRoutes;
|
97
|
+
(0, fs_1.writeFileSync)(this.configPath, JSON.stringify(fullConfig, null, 2));
|
98
|
+
}
|
99
|
+
catch (error) {
|
100
|
+
console.error('Error saving config:', error);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
setupConfigRoutes() {
|
104
|
+
const baseProxy = this.serverConfig.baseProxy || '';
|
105
|
+
// 获取所有API配置
|
106
|
+
this.app.get('/api/_config', (req, res) => {
|
107
|
+
console.log('Sending config:', this.config);
|
108
|
+
res.json(this.config);
|
109
|
+
});
|
110
|
+
// 创建新API配置
|
111
|
+
this.app.post('/api/_config', (req, res) => {
|
112
|
+
const newApi = {
|
113
|
+
id: (0, uuid_1.v4)(),
|
114
|
+
route: req.body.route
|
115
|
+
};
|
116
|
+
this.config.push(newApi);
|
117
|
+
this.saveConfig();
|
118
|
+
res.json(newApi);
|
119
|
+
});
|
120
|
+
// 更新API配置
|
121
|
+
this.app.put('/api/_config/:id', (req, res) => {
|
122
|
+
const index = this.config.findIndex(api => api.id === req.params.id);
|
123
|
+
if (index === -1) {
|
124
|
+
res.status(404).json({ error: 'API not found' });
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
this.config[index] = {
|
128
|
+
id: req.params.id,
|
129
|
+
route: req.body.route
|
130
|
+
};
|
131
|
+
this.saveConfig();
|
132
|
+
res.json(this.config[index]);
|
133
|
+
});
|
134
|
+
// 删除API配置
|
135
|
+
this.app.delete('/api/_config/:id', (req, res) => {
|
136
|
+
const index = this.config.findIndex(api => api.id === req.params.id);
|
137
|
+
if (index === -1) {
|
138
|
+
res.status(404).json({ error: 'API not found' });
|
139
|
+
return;
|
140
|
+
}
|
141
|
+
this.config.splice(index, 1);
|
142
|
+
this.saveConfig();
|
143
|
+
res.status(204).send();
|
144
|
+
});
|
145
|
+
// 添加日志接口
|
146
|
+
this.app.get('/api/_logs', (req, res) => {
|
147
|
+
const { page = 1, size = 20 } = req.query;
|
148
|
+
const start = (Number(page) - 1) * Number(size);
|
149
|
+
const end = start + Number(size);
|
150
|
+
const total = this.logs.length;
|
151
|
+
res.json({
|
152
|
+
total,
|
153
|
+
list: this.logs.slice(start, end)
|
154
|
+
});
|
155
|
+
});
|
156
|
+
// 清除日志
|
157
|
+
this.app.delete('/api/_logs', (req, res) => {
|
158
|
+
this.logs = [];
|
159
|
+
res.status(204).send();
|
160
|
+
});
|
161
|
+
}
|
162
|
+
setupMockRoutes() {
|
163
|
+
const baseProxy = this.serverConfig.baseProxy || '';
|
164
|
+
this.app.all('*', (req, res, next) => {
|
165
|
+
// 如果是配置接口,跳过日志记录
|
166
|
+
if (req.path === '/api/_config' || req.path.startsWith('/api/_config/') ||
|
167
|
+
req.path === '/api/_logs' || req.path.startsWith('/api/_logs/')) {
|
168
|
+
return next();
|
169
|
+
}
|
170
|
+
const startTime = Date.now();
|
171
|
+
const originalSend = res.send;
|
172
|
+
// 修复 this 指向问题
|
173
|
+
const self = this;
|
174
|
+
// 记录请求日志
|
175
|
+
res.send = function (body) {
|
176
|
+
const endTime = Date.now();
|
177
|
+
const duration = endTime - startTime;
|
178
|
+
const log = {
|
179
|
+
id: (0, uuid_1.v4)(),
|
180
|
+
path: req.path,
|
181
|
+
method: req.method,
|
182
|
+
timestamp: new Date().toISOString(),
|
183
|
+
status: res.statusCode,
|
184
|
+
duration,
|
185
|
+
params: {
|
186
|
+
query: req.query,
|
187
|
+
body: req.body,
|
188
|
+
params: req.params
|
189
|
+
},
|
190
|
+
requestBody: req.body,
|
191
|
+
responseBody: body
|
192
|
+
};
|
193
|
+
// 使用正确的 this 引用
|
194
|
+
self.logs.unshift(log);
|
195
|
+
if (self.logs.length > self.MAX_LOGS) {
|
196
|
+
self.logs.pop();
|
197
|
+
}
|
198
|
+
// 广播新日志
|
199
|
+
self.broadcastLog(log);
|
200
|
+
return originalSend.call(this, body);
|
201
|
+
};
|
202
|
+
next();
|
203
|
+
});
|
204
|
+
// 处理所有mock请求,但排除配置接口
|
205
|
+
this.app.all('*', (req, res, next) => {
|
61
206
|
try {
|
62
|
-
|
63
|
-
if (
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
207
|
+
// 如果是配置接口,跳过这个中间件
|
208
|
+
if (req.path === '/api/_config' || req.path.startsWith('/api/_config/')) {
|
209
|
+
return next();
|
210
|
+
}
|
211
|
+
console.log('Received request for path:', req.path);
|
212
|
+
console.log('Current config:', this.config);
|
213
|
+
if (!Array.isArray(this.config)) {
|
214
|
+
console.error('Config is not an array:', this.config);
|
215
|
+
res.status(500).json({ error: 'Internal server error' });
|
216
|
+
return;
|
217
|
+
}
|
218
|
+
const api = this.config.find(api => {
|
219
|
+
if (!api || !api.route || typeof api.route.path !== 'string') {
|
220
|
+
console.error('Invalid API config:', api);
|
221
|
+
return false;
|
222
|
+
}
|
223
|
+
// 移除 baseProxy 前缀再比较
|
224
|
+
const requestPath = req.path.replace(baseProxy, '');
|
225
|
+
return api.route.path === requestPath;
|
226
|
+
});
|
227
|
+
if (!api) {
|
228
|
+
res.status(404).json({ error: 'API not found' });
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
if (!api.route || !api.route.methods) {
|
232
|
+
res.status(500).json({ error: 'Invalid API configuration' });
|
233
|
+
return;
|
234
|
+
}
|
235
|
+
const method = req.method.toLowerCase();
|
236
|
+
const methodConfig = api.route.methods[method];
|
237
|
+
if (!methodConfig) {
|
238
|
+
res.status(405).json({ error: 'Method not allowed' });
|
239
|
+
return;
|
240
|
+
}
|
241
|
+
if (methodConfig.delay) {
|
242
|
+
setTimeout(() => {
|
243
|
+
this.sendResponse(res, methodConfig);
|
244
|
+
}, methodConfig.delay);
|
245
|
+
}
|
246
|
+
else {
|
247
|
+
this.sendResponse(res, methodConfig);
|
71
248
|
}
|
72
|
-
res.json(responseData);
|
73
249
|
}
|
74
250
|
catch (error) {
|
75
251
|
console.error('Error handling request:', error);
|
76
252
|
res.status(500).json({ error: 'Internal server error' });
|
77
253
|
}
|
78
|
-
};
|
79
|
-
}
|
80
|
-
setupRoutes() {
|
81
|
-
this.config.routes.forEach((route) => {
|
82
|
-
Object.entries(route.methods).forEach(([method, methodConfig]) => {
|
83
|
-
this.createRoute(route.path, method, methodConfig);
|
84
|
-
});
|
85
254
|
});
|
86
255
|
}
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
});
|
256
|
+
sendResponse(res, config) {
|
257
|
+
try {
|
258
|
+
// 设置响应头
|
259
|
+
if (config.headers) {
|
260
|
+
Object.entries(config.headers).forEach(([key, value]) => {
|
261
|
+
res.setHeader(key, value);
|
262
|
+
});
|
263
|
+
}
|
264
|
+
// 处理响应数据
|
265
|
+
let responseData = config.response;
|
266
|
+
if (typeof responseData === 'string') {
|
267
|
+
try {
|
268
|
+
responseData = JSON.parse(responseData);
|
101
269
|
}
|
102
|
-
|
103
|
-
|
270
|
+
catch (e) {
|
271
|
+
console.error('Failed to parse response string:', e);
|
272
|
+
responseData = { error: 'Invalid response format' };
|
104
273
|
}
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
case 'delete':
|
110
|
-
this.app.delete(`${fullPath}/:id`, this.handleRequest(config));
|
111
|
-
break;
|
274
|
+
}
|
275
|
+
// 使用 Mock.js 处理数据
|
276
|
+
const mockedData = mockjs_1.default.mock(responseData);
|
277
|
+
res.status(config.status || 200).json(mockedData);
|
112
278
|
}
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
if (config.mock?.enabled && config.mock.template) {
|
120
|
-
return mockjs_1.default.mock(config.mock.template);
|
279
|
+
catch (error) {
|
280
|
+
console.error('Error processing response:', error);
|
281
|
+
res.status(500).json({
|
282
|
+
error: 'Internal server error',
|
283
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
284
|
+
});
|
121
285
|
}
|
122
|
-
return config.response;
|
123
286
|
}
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
console.log(` ${method.toUpperCase()} http://localhost:${this.config.server.port}${this.config.server.baseProxy}${route.path}`);
|
287
|
+
setupUploadRoute() {
|
288
|
+
// 处理文件上传
|
289
|
+
this.app.post('/api/upload', this.upload.single('file'), (req, res) => {
|
290
|
+
if (!req.file) {
|
291
|
+
return res.status(400).json({
|
292
|
+
code: 400,
|
293
|
+
message: 'No file uploaded',
|
294
|
+
data: null
|
133
295
|
});
|
296
|
+
}
|
297
|
+
// 只返回模拟数据,不保存文件
|
298
|
+
res.json({
|
299
|
+
code: 200,
|
300
|
+
message: 'success',
|
301
|
+
data: {
|
302
|
+
url: mockjs_1.default.Random.image('200x200'),
|
303
|
+
filename: req.file.originalname,
|
304
|
+
size: req.file.size
|
305
|
+
}
|
134
306
|
});
|
135
307
|
});
|
136
308
|
}
|
309
|
+
start(port) {
|
310
|
+
const serverPort = port || this.serverConfig.port || 3000;
|
311
|
+
this.app.listen(serverPort, () => {
|
312
|
+
console.log(`Mock server is running on http://localhost:${serverPort}`);
|
313
|
+
});
|
314
|
+
}
|
315
|
+
// 广播日志给所有客户端
|
316
|
+
broadcastLog(log) {
|
317
|
+
const message = JSON.stringify({
|
318
|
+
type: 'log',
|
319
|
+
data: log
|
320
|
+
});
|
321
|
+
this.clients.forEach(client => {
|
322
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
323
|
+
client.send(message);
|
324
|
+
}
|
325
|
+
});
|
326
|
+
}
|
137
327
|
}
|
138
328
|
exports.MockServer = MockServer;
|
package/dist/types.d.ts
CHANGED
@@ -1,23 +1,8 @@
|
|
1
|
-
export interface ServerConfig {
|
2
|
-
port: number;
|
3
|
-
baseProxy: string;
|
4
|
-
}
|
5
|
-
export interface PaginationConfig {
|
6
|
-
enabled: boolean;
|
7
|
-
pageSize: number;
|
8
|
-
totalCount: number;
|
9
|
-
}
|
10
|
-
export interface MockConfig {
|
11
|
-
enabled: boolean;
|
12
|
-
total?: number;
|
13
|
-
template: Record<string, any>;
|
14
|
-
}
|
15
1
|
export interface MethodConfig {
|
16
|
-
|
17
|
-
response
|
18
|
-
|
19
|
-
|
20
|
-
requestSchema?: Record<string, string>;
|
2
|
+
status?: number;
|
3
|
+
response: any;
|
4
|
+
headers?: Record<string, string>;
|
5
|
+
delay?: number;
|
21
6
|
}
|
22
7
|
export interface RouteConfig {
|
23
8
|
path: string;
|
@@ -25,7 +10,27 @@ export interface RouteConfig {
|
|
25
10
|
[key: string]: MethodConfig;
|
26
11
|
};
|
27
12
|
}
|
13
|
+
export interface ApiConfig {
|
14
|
+
id: string;
|
15
|
+
route: RouteConfig;
|
16
|
+
}
|
17
|
+
export interface RequestLog {
|
18
|
+
id: string;
|
19
|
+
path: string;
|
20
|
+
method: string;
|
21
|
+
timestamp: string;
|
22
|
+
status: number;
|
23
|
+
duration: number;
|
24
|
+
params: {
|
25
|
+
query: any;
|
26
|
+
body: any;
|
27
|
+
params: any;
|
28
|
+
};
|
29
|
+
requestBody?: any;
|
30
|
+
responseBody?: any;
|
31
|
+
}
|
28
32
|
export interface Config {
|
29
|
-
|
30
|
-
|
33
|
+
port?: number;
|
34
|
+
baseProxy?: string;
|
35
|
+
wsPort?: number;
|
31
36
|
}
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "json-api-mocker",
|
3
|
-
"version": "
|
4
|
-
"description": "A mock server
|
3
|
+
"version": "3.0.0",
|
4
|
+
"description": "A mock server with visual management interface",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"types": "dist/index.d.ts",
|
7
7
|
"bin": {
|
@@ -9,19 +9,18 @@
|
|
9
9
|
},
|
10
10
|
"files": [
|
11
11
|
"dist",
|
12
|
-
"
|
12
|
+
"web",
|
13
13
|
"README.ch.md",
|
14
|
+
"README.md",
|
14
15
|
"CONFIG.md",
|
15
|
-
"CONFIG.ch.md"
|
16
|
+
"CONFIG.ch.md",
|
17
|
+
"DESIGN.md"
|
16
18
|
],
|
17
19
|
"scripts": {
|
18
20
|
"dev": "nodemon",
|
19
|
-
"build": "tsc",
|
20
|
-
"start": "node dist/
|
21
|
-
"
|
22
|
-
"test:watch": "jest --watch",
|
23
|
-
"test:coverage": "jest --coverage",
|
24
|
-
"prepublishOnly": "npm run build"
|
21
|
+
"build": "tsc && pnpm copy-web",
|
22
|
+
"start": "node dist/cli.js -o",
|
23
|
+
"copy-web": "rimraf web && mkdir web && node scripts/copy-web.js"
|
25
24
|
},
|
26
25
|
"keywords": [
|
27
26
|
"mock",
|
@@ -44,21 +43,32 @@
|
|
44
43
|
},
|
45
44
|
"homepage": "https://github.com/Selteve/json-api-mocker#readme",
|
46
45
|
"dependencies": {
|
46
|
+
"commander": "^8.3.0",
|
47
47
|
"cors": "^2.8.5",
|
48
48
|
"express": "^4.17.1",
|
49
49
|
"mockjs": "^1.1.0",
|
50
|
-
"multer": "^1.4.5-lts.1"
|
50
|
+
"multer": "^1.4.5-lts.1",
|
51
|
+
"open": "^8.4.0",
|
52
|
+
"uuid": "^9.0.0",
|
53
|
+
"ws": "^8.2.3"
|
51
54
|
},
|
52
55
|
"devDependencies": {
|
56
|
+
"@types/commander": "^2.12.2",
|
53
57
|
"@types/cors": "^2.8.13",
|
54
58
|
"@types/express": "^4.17.13",
|
59
|
+
"@types/fs-extra": "^11.0.4",
|
55
60
|
"@types/jest": "^27.5.2",
|
56
61
|
"@types/mockjs": "^1.0.10",
|
57
62
|
"@types/multer": "^1.4.12",
|
58
63
|
"@types/node": "^16.18.0",
|
64
|
+
"@types/open": "^6.2.1",
|
59
65
|
"@types/supertest": "^6.0.2",
|
66
|
+
"@types/uuid": "^9.0.8",
|
67
|
+
"@types/ws": "^8.5.13",
|
68
|
+
"fs-extra": "^11.2.0",
|
60
69
|
"jest": "^27.5.1",
|
61
70
|
"nodemon": "^2.0.22",
|
71
|
+
"rimraf": "^3.0.2",
|
62
72
|
"supertest": "^7.0.0",
|
63
73
|
"ts-jest": "^27.1.5",
|
64
74
|
"ts-node": "^10.9.1",
|