vite-plugin-automock 1.0.2 → 1.1.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/dist/index.mjs CHANGED
@@ -6,2307 +6,859 @@ import {
6
6
  loadMockData,
7
7
  registerHttpInstance,
8
8
  setMockEnabled
9
- } from "./chunk-NWIN2A3G.mjs";
10
- import {
11
- buildMockIndex,
12
- parseMockModule,
13
- saveMockData,
14
- toPosixPath,
15
- writeMockFile
16
- } from "./chunk-PS6HLNJZ.mjs";
9
+ } from "./chunk-DJWYFFPU.mjs";
17
10
 
18
11
  // src/index.ts
19
- import path4 from "path";
20
- import fs3 from "fs-extra";
12
+ import path6 from "path";
13
+ import fs5 from "fs-extra";
21
14
 
22
15
  // src/middleware.ts
23
16
  import chokidar from "chokidar";
24
17
  import debounce from "lodash.debounce";
25
- import path2 from "path";
26
- import fs from "fs-extra";
18
+ import path4 from "path";
19
+ import fs3 from "fs-extra";
27
20
  import http from "http";
28
21
  import https from "https";
29
22
 
30
- // src/inspector.ts
23
+ // src/mockFileUtils.ts
24
+ import path2 from "path";
25
+ import fs from "fs-extra";
26
+ import prettier from "prettier";
27
+
28
+ // src/utils.ts
31
29
  import path from "path";
32
- var DEFAULT_ROUTE = "/__mock/";
33
- function ensureTrailingSlash(route) {
34
- return route.endsWith("/") ? route : `${route}/`;
30
+ function resolveAbsolutePath(p) {
31
+ return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
35
32
  }
36
- function escapeHtml(value) {
37
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
33
+ function toPosixPath(p) {
34
+ return p.replace(/\\/g, "/");
38
35
  }
39
- function normalizeInspectorConfig(input) {
40
- if (input === false || input === void 0) {
41
- return { route: DEFAULT_ROUTE, enableToggle: true };
42
- }
43
- if (input === true) {
44
- return { route: DEFAULT_ROUTE, enableToggle: true };
45
- }
36
+ function getServerAddress(address, isHttps) {
46
37
  return {
47
- route: ensureTrailingSlash(input.route ?? DEFAULT_ROUTE),
48
- enableToggle: input.enableToggle ?? true
38
+ protocol: isHttps ? "https" : "http",
39
+ host: address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address,
40
+ port: address.port
49
41
  };
50
42
  }
51
- function createInspectorHandler(options) {
52
- if (!options.inspector) {
53
- return null;
54
- }
55
- const inspectorConfig = normalizeInspectorConfig(options.inspector);
56
- const inspectorRoute = ensureTrailingSlash(inspectorConfig.route);
57
- return async (req, res) => {
58
- if (!req.url) {
59
- return false;
60
- }
61
- const url = new URL(req.url, "http://localhost");
62
- if (!url.pathname.startsWith(inspectorRoute)) {
63
- return false;
64
- }
65
- await handleInspectorRequest({
66
- req,
67
- res,
68
- mockDir: options.mockDir,
69
- inspectorRoute,
70
- apiPrefix: options.apiPrefix,
71
- inspectorConfig,
72
- getMockFileMap: options.getMockFileMap
43
+ function sendJson(res, data, status = 200) {
44
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
45
+ res.statusCode = status;
46
+ res.end(typeof data === "string" ? data : JSON.stringify(data));
47
+ }
48
+ function sendError(res, message, status = 500) {
49
+ sendJson(res, { error: message }, status);
50
+ }
51
+ var MAX_BODY_SIZE = 10 * 1024 * 1024;
52
+ function readBody(req) {
53
+ return new Promise((resolve, reject) => {
54
+ const chunks = [];
55
+ let totalSize = 0;
56
+ req.on("data", (chunk) => {
57
+ totalSize += chunk.length;
58
+ if (totalSize > MAX_BODY_SIZE) {
59
+ reject(new Error("Request body too large"));
60
+ req.destroy();
61
+ return;
62
+ }
63
+ chunks.push(chunk);
73
64
  });
74
- return true;
75
- };
65
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
66
+ req.on("error", reject);
67
+ });
76
68
  }
77
- async function handleInspectorRequest(context) {
78
- const { req, res, inspectorRoute } = context;
79
- const url = new URL(req.url || inspectorRoute, "http://localhost");
80
- const normalizedRoute = ensureTrailingSlash(inspectorRoute);
81
- if (url.pathname === normalizedRoute.slice(0, -1) || url.pathname === normalizedRoute) {
82
- await serveInspectorHtml(context);
83
- return;
69
+
70
+ // src/mockFileUtils.ts
71
+ var DEFAULT_CONFIG = {
72
+ enable: true,
73
+ data: null,
74
+ delay: 0,
75
+ status: 200
76
+ };
77
+ var isBufferTextLike = (buffer) => {
78
+ try {
79
+ const sample = buffer.slice(0, 100);
80
+ const nullBytes = [...sample].filter((b) => b === 0).length;
81
+ const controlChars = [...sample].filter(
82
+ (b) => b < 32 && b !== 9 && b !== 10 && b !== 13
83
+ ).length;
84
+ return nullBytes === 0 && controlChars < 5;
85
+ } catch {
86
+ return false;
84
87
  }
85
- const relativePath = url.pathname.startsWith(normalizedRoute) ? url.pathname.slice(normalizedRoute.length) : null;
86
- if (relativePath && relativePath.startsWith("api/")) {
87
- await handleInspectorApi({ ...context, pathname: relativePath.slice(4) });
88
- return;
88
+ };
89
+ var isBinaryResponse = (contentType, data) => {
90
+ if (!contentType) return false;
91
+ const binaryTypes = [
92
+ "application/octet-stream",
93
+ "application/pdf",
94
+ "application/zip",
95
+ "application/x-zip-compressed",
96
+ "application/vnd.ms-excel",
97
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
98
+ "application/msword",
99
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
100
+ "application/vnd.ms-powerpoint",
101
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
102
+ "image/",
103
+ "video/",
104
+ "audio/"
105
+ ];
106
+ return binaryTypes.some((type) => contentType.toLowerCase().includes(type)) || !isBufferTextLike(data);
107
+ };
108
+ var getFileExtension = (contentType, url) => {
109
+ const mimeMap = {
110
+ "application/json": "json",
111
+ "application/pdf": "pdf",
112
+ "application/zip": "zip",
113
+ "application/vnd.ms-excel": "xls",
114
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
115
+ "application/msword": "doc",
116
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
117
+ "application/vnd.ms-powerpoint": "ppt",
118
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
119
+ "image/jpeg": "jpg",
120
+ "image/png": "png",
121
+ "image/gif": "gif",
122
+ "text/plain": "txt",
123
+ "text/html": "html",
124
+ "text/css": "css",
125
+ "application/javascript": "js",
126
+ "text/xml": "xml"
127
+ };
128
+ if (contentType && mimeMap[contentType.toLowerCase()]) {
129
+ return mimeMap[contentType.toLowerCase()];
89
130
  }
90
- res.statusCode = 404;
91
- res.end("Not Found");
92
- }
93
- async function serveInspectorHtml({
94
- res,
95
- inspectorRoute,
96
- apiPrefix,
97
- inspectorConfig
98
- }) {
99
- const routeJson = JSON.stringify(ensureTrailingSlash(inspectorRoute));
100
- const allowToggleJson = JSON.stringify(inspectorConfig.enableToggle);
101
- const apiPrefixEscaped = escapeHtml(apiPrefix);
102
- const html = `<!DOCTYPE html>
103
- <html lang="en">
104
- <head>
105
- <meta charset="UTF-8" />
106
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
107
- <title>Mock Inspector</title>
108
- <style>
109
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
110
-
111
- :root {
112
- --bg-primary: #ffffff;
113
- --bg-secondary: #f9fafb;
114
- --bg-tertiary: #f3f4f6;
115
- --bg-hover: #e5e7eb;
116
- --border-color: #e5e7eb;
117
- --border-subtle: #f3f4f6;
118
- --text-primary: #111827;
119
- --text-secondary: #4b5563;
120
- --text-muted: #9ca3af;
121
- --accent-indigo: #6366f1;
122
- --accent-indigo-light: #e0e7ff;
123
- --accent-indigo-hover: #4f46e5;
124
- --accent-emerald: #10b981;
125
- --accent-emerald-light: #d1fae5;
126
- --accent-amber: #f59e0b;
127
- --accent-amber-light: #fef3c7;
128
- --accent-rose: #ef4444;
129
- --accent-rose-light: #fee2e2;
130
- --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
131
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
132
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
133
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
134
- --radius-sm: 6px;
135
- --radius-md: 8px;
136
- --radius-lg: 12px;
137
- }
138
-
139
- * {
140
- box-sizing: border-box;
141
- }
142
-
143
- body {
144
- margin: 0;
145
- background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 25%, #fdf4ff 50%, #ecfdf5 75%, #f0fdf4 100%);
146
- color: var(--text-primary);
147
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
148
- display: flex;
149
- flex-direction: column;
150
- height: 100vh;
151
- overflow: hidden;
152
- position: relative;
153
- }
154
-
155
- /* Multiple ambient gradient orbs */
156
- body::before {
157
- content: '';
158
- position: fixed;
159
- top: -15%;
160
- right: -10%;
161
- width: 60vw;
162
- height: 60vw;
163
- background: radial-gradient(circle, rgba(99, 102, 241, 0.25) 0%, rgba(139, 92, 246, 0.15) 30%, transparent 70%);
164
- filter: blur(100px);
165
- pointer-events: none;
166
- z-index: 0;
167
- animation: float 20s ease-in-out infinite;
168
- }
169
-
170
- body::after {
171
- content: '';
172
- position: fixed;
173
- bottom: -15%;
174
- left: -10%;
175
- width: 50vw;
176
- height: 50vw;
177
- background: radial-gradient(circle, rgba(16, 185, 129, 0.2) 0%, rgba(34, 197, 94, 0.12) 30%, transparent 70%);
178
- filter: blur(100px);
179
- pointer-events: none;
180
- z-index: 0;
181
- animation: float 25s ease-in-out infinite reverse;
182
- }
183
-
184
- @keyframes float {
185
- 0%, 100% { transform: translate(0, 0) scale(1); }
186
- 33% { transform: translate(30px, -30px) scale(1.05); }
187
- 66% { transform: translate(-20px, 20px) scale(0.95); }
188
- }
189
-
190
- /* Page load animation */
191
- @keyframes fadeSlideIn {
192
- from {
193
- opacity: 0;
194
- transform: translateY(8px);
195
- }
196
- to {
197
- opacity: 1;
198
- transform: translateY(0);
131
+ try {
132
+ const urlObj = new URL(url, "http://localhost");
133
+ const fileName = urlObj.searchParams.get("file_name");
134
+ if (fileName) {
135
+ const extensionMatch = fileName.match(/\.([a-zA-Z0-9]+)$/);
136
+ if (extensionMatch) {
137
+ return extensionMatch[1];
199
138
  }
200
139
  }
201
-
202
- body > * {
203
- animation: fadeSlideIn 0.3s ease-out backwards;
204
- }
205
-
206
- header {
207
- padding: 1rem 1.5rem;
208
- display: flex;
209
- align-items: center;
210
- gap: 1rem;
211
- border-bottom: 1px solid rgba(99, 102, 241, 0.1);
212
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.85) 0%, rgba(238, 242, 255, 0.75) 50%, rgba(250, 245, 255, 0.85) 100%);
213
- backdrop-filter: blur(20px);
214
- flex-shrink: 0;
215
- position: relative;
216
- z-index: 1;
217
- box-shadow: 0 4px 30px rgba(99, 102, 241, 0.1);
218
- }
219
-
220
- header h1 {
221
- font-size: 1.1rem;
222
- margin: 0;
223
- font-weight: 600;
224
- color: var(--text-primary);
225
- letter-spacing: -0.01em;
226
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%);
227
- -webkit-background-clip: text;
228
- -webkit-text-fill-color: transparent;
229
- background-clip: text;
230
- }
231
-
232
- main {
233
- flex: 1;
234
- display: grid;
235
- grid-template-columns: var(--sidebar-width, 380px) 4px 1fr;
236
- background: transparent;
237
- min-height: 0;
238
- overflow: hidden;
239
- position: relative;
240
- z-index: 1;
241
- }
242
-
243
- aside {
244
- background: linear-gradient(180deg, rgba(238, 242, 255, 0.5) 0%, rgba(250, 245, 255, 0.4) 50%, rgba(236, 253, 245, 0.5) 100%);
245
- backdrop-filter: blur(15px);
246
- overflow-y: auto;
247
- overflow-x: hidden;
248
- min-width: 200px;
249
- max-width: 800px;
250
- height: 100%;
251
- border-right: 1px solid rgba(99, 102, 241, 0.15);
252
- }
253
-
254
- aside::-webkit-scrollbar {
255
- width: 6px;
256
- }
257
-
258
- aside::-webkit-scrollbar-track {
259
- background: transparent;
260
- }
261
-
262
- aside::-webkit-scrollbar-thumb {
263
- background: var(--border-color);
264
- border-radius: 3px;
265
- }
266
-
267
- aside::-webkit-scrollbar-thumb:hover {
268
- background: var(--text-muted);
269
- }
270
-
271
- .resizer {
272
- background: var(--border-color);
273
- cursor: col-resize;
274
- position: relative;
275
- user-select: none;
276
- transition: all 0.2s ease;
277
- }
278
-
279
- .resizer:hover,
280
- .resizer.active {
281
- background: var(--accent-indigo);
282
- }
283
-
284
- .resizer::after {
285
- content: '';
286
- position: absolute;
287
- left: 50%;
288
- top: 50%;
289
- transform: translate(-50%, -50%);
290
- width: 3px;
291
- height: 32px;
292
- background: var(--text-muted);
293
- border-radius: 2px;
294
- opacity: 0;
295
- transition: opacity 0.2s ease;
296
- }
297
-
298
- .resizer:hover::after,
299
- .resizer.active::after {
300
- opacity: 1;
301
- background: white;
302
- }
303
-
304
- .global-controls {
305
- padding: 0.75rem 1rem;
306
- border-bottom: 1px solid var(--border-color);
307
- display: flex;
308
- gap: 0.5rem;
309
- background: var(--bg-secondary);
310
- position: sticky;
311
- top: 0;
312
- z-index: 10;
313
- }
314
-
315
- .global-controls .secondary {
316
- flex: 1;
317
- padding: 0.45rem 0.65rem;
318
- font-size: 0.7rem;
319
- display: flex;
320
- align-items: center;
321
- justify-content: center;
322
- gap: 0.3rem;
323
- font-weight: 500;
324
- }
325
-
326
- .global-controls .secondary:hover {
327
- background: var(--accent-indigo-light);
328
- border-color: var(--accent-indigo);
329
- color: var(--accent-indigo);
330
- }
331
-
332
- /* Tree view styles */
333
- .tree-node {
334
- user-select: none;
335
- }
336
-
337
- .tree-node-content {
338
- display: flex;
339
- align-items: center;
340
- padding: 0.4rem 0.6rem;
341
- cursor: pointer;
342
- transition: all 0.2s ease;
343
- border-bottom: 1px solid var(--border-subtle);
344
- position: relative;
345
- }
346
-
347
- .tree-node-content::before {
348
- content: '';
349
- position: absolute;
350
- inset: 0;
351
- background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(236, 72, 153, 0.1) 100%);
352
- opacity: 0;
353
- transition: opacity 0.2s ease;
354
- border-radius: var(--radius-sm);
355
- }
356
-
357
- .tree-node-content:hover::before {
358
- opacity: 1;
359
- }
360
-
361
- .tree-node-content > * {
362
- position: relative;
363
- z-index: 1;
364
- }
365
-
366
- .tree-node-content.selected {
367
- background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(236, 72, 153, 0.15) 100%);
368
- box-shadow: 0 4px 15px rgba(139, 92, 246, 0.25);
369
- }
370
-
371
- .tree-expand-icon {
372
- width: 18px;
373
- height: 18px;
374
- display: flex;
375
- align-items: center;
376
- justify-content: center;
377
- margin-right: 0.25rem;
378
- transition: transform 0.2s ease;
379
- cursor: pointer;
380
- color: var(--text-muted);
381
- font-size: 0.65rem;
382
- }
383
-
384
- .tree-expand-icon.expanded {
385
- transform: rotate(90deg);
386
- }
387
-
388
- .tree-expand-icon.hidden {
389
- visibility: hidden;
390
- }
391
-
392
- .tree-node-checkbox {
393
- appearance: none;
394
- -webkit-appearance: none;
395
- width: 16px;
396
- height: 16px;
397
- cursor: pointer;
398
- margin-right: 0.5rem;
399
- border: 2px solid var(--border-color);
400
- border-radius: 4px;
401
- background: var(--bg-primary);
402
- position: relative;
403
- transition: all 0.15s ease;
404
- flex-shrink: 0;
405
- }
406
-
407
- .tree-node-checkbox:hover {
408
- border-color: var(--accent-emerald);
409
- }
410
-
411
- .tree-node-checkbox:checked {
412
- background: var(--bg-primary);
413
- border-color: var(--accent-emerald);
414
- }
415
-
416
- .tree-node-checkbox:checked::after {
417
- content: '';
418
- position: absolute;
419
- top: 50%;
420
- left: 50%;
421
- width: 3px;
422
- height: 6px;
423
- border: solid var(--accent-emerald);
424
- border-width: 0 2px 2px 0;
425
- transform: translate(-50%, -60%) rotate(45deg);
426
- }
427
-
428
- .tree-node-checkbox:indeterminate {
429
- background: var(--bg-primary);
430
- border-color: var(--accent-emerald);
431
- }
432
-
433
- .tree-node-checkbox:indeterminate::after {
434
- content: '';
435
- position: absolute;
436
- top: 50%;
437
- left: 50%;
438
- width: 8px;
439
- height: 2px;
440
- background: var(--accent-emerald);
441
- transform: translate(-50%, -50%);
442
- }
443
-
444
- .tree-node-label {
445
- flex: 1;
446
- font-size: 0.82rem;
447
- white-space: nowrap;
448
- overflow: hidden;
449
- text-overflow: ellipsis;
450
- min-width: 0;
451
- }
452
-
453
- .tree-node-label.folder {
454
- font-weight: 600;
455
- color: var(--text-primary);
456
- display: flex;
457
- align-items: center;
458
- gap: 0.25rem;
459
- }
460
-
461
- .tree-node-label.folder .tree-node-count {
462
- flex-shrink: 0;
463
- }
464
-
465
- .tree-node-label.file {
466
- color: var(--text-secondary);
467
- }
468
-
469
- .tree-node-method {
470
- font-size: 0.65rem;
471
- padding: 0.15rem 0.45rem;
472
- border-radius: var(--radius-sm);
473
- margin-right: 0.4rem;
474
- font-weight: 600;
475
- text-transform: uppercase;
476
- letter-spacing: 0.03em;
477
- }
478
-
479
- .tree-node-method.get {
480
- background: linear-gradient(135deg, var(--accent-emerald-light) 0%, rgba(16, 185, 129, 0.15) 100%);
481
- color: #047857;
482
- box-shadow: 0 2px 6px rgba(16, 185, 129, 0.15);
483
- }
484
-
485
- .tree-node-method.post {
486
- background: linear-gradient(135deg, var(--accent-amber-light) 0%, rgba(245, 158, 11, 0.15) 100%);
487
- color: #b45309;
488
- box-shadow: 0 2px 6px rgba(245, 158, 11, 0.15);
489
- }
490
-
491
- .tree-node-method.put {
492
- background: linear-gradient(135deg, var(--accent-indigo-light) 0%, rgba(99, 102, 241, 0.15) 100%);
493
- color: #4338ca;
494
- box-shadow: 0 2px 6px rgba(99, 102, 241, 0.15);
495
- }
496
-
497
- .tree-node-method.delete {
498
- background: linear-gradient(135deg, var(--accent-rose-light) 0%, rgba(239, 68, 68, 0.15) 100%);
499
- color: #b91c1c;
500
- box-shadow: 0 2px 6px rgba(239, 68, 68, 0.15);
501
- }
502
-
503
- .tree-node-method.patch {
504
- background: linear-gradient(135deg, #ede9fe 0%, rgba(139, 92, 246, 0.15) 100%);
505
- color: #7c3aed;
506
- box-shadow: 0 2px 6px rgba(139, 92, 246, 0.15);
507
- }
508
-
509
- .tree-children {
510
- padding-left: 1rem;
511
- display: none;
512
- }
513
-
514
- .tree-children.expanded {
515
- display: block;
516
- }
517
-
518
- .tree-node-count {
519
- font-size: 0.7rem;
520
- color: var(--text-muted);
521
- margin-left: 0.4rem;
522
- }
523
-
524
- .tree-node-delete {
525
- display: none;
526
- align-items: center;
527
- justify-content: center;
528
- width: 18px;
529
- height: 18px;
530
- margin-left: auto;
531
- border-radius: var(--radius-sm);
532
- cursor: pointer;
533
- color: var(--accent-rose);
534
- font-size: 0.85rem;
535
- transition: all 0.15s ease;
536
- user-select: none;
537
- }
538
-
539
- .tree-node-delete:hover {
540
- background: var(--accent-rose-light);
541
- }
542
-
543
- .tree-node-content:hover .tree-node-delete {
544
- display: flex;
545
- }
546
-
547
- #mock-details {
548
- height: calc(100% - 60px);
549
- }
550
-
551
- section {
552
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.6) 0%, rgba(238, 242, 255, 0.5) 50%, rgba(250, 245, 255, 0.6) 100%);
553
- backdrop-filter: blur(15px);
554
- padding: 1.5rem;
555
- overflow-y: auto;
556
- overflow-x: hidden;
557
- display: flex;
558
- flex-direction: column;
559
- gap: 1rem;
560
- height: 100%;
561
- }
562
-
563
- section::-webkit-scrollbar {
564
- width: 6px;
565
- }
566
-
567
- section::-webkit-scrollbar-track {
568
- background: transparent;
569
- }
570
-
571
- section::-webkit-scrollbar-thumb {
572
- background: var(--border-color);
573
- border-radius: 3px;
574
- }
575
-
576
- section::-webkit-scrollbar-thumb:hover {
577
- background: var(--text-muted);
578
- }
579
-
580
- section > h3 {
581
- margin: 0;
582
- flex-shrink: 0;
583
- color: var(--text-primary);
584
- font-size: 0.85rem;
585
- font-weight: 600;
586
- text-transform: uppercase;
587
- letter-spacing: 0.05em;
588
- }
589
-
590
- section .data-container {
591
- flex: 1 1 auto;
592
- min-height: 0;
593
- display: flex;
594
- flex-direction: column;
595
- overflow: hidden;
596
- height: 100%;
597
- }
598
-
599
- .controls {
600
- display: flex;
601
- flex-wrap: wrap;
602
- gap: 1rem;
603
- align-items: flex-start;
604
- flex-shrink: 0;
605
- }
606
-
607
- .controls h2 {
608
- width: 100%;
609
- margin: 0 0 0.75rem 0;
610
- font-size: 1rem;
611
- display: flex;
612
- align-items: center;
613
- gap: 0.75rem;
140
+ } catch {
141
+ }
142
+ return "bin";
143
+ };
144
+ async function formatWithPrettier(content) {
145
+ try {
146
+ return await prettier.format(content, { parser: "babel" });
147
+ } catch {
148
+ return content;
149
+ }
150
+ }
151
+ function buildMockFileHeader(pathname, method, search, extra) {
152
+ return `/**
153
+ * Mock data for ${pathname} (${method.toUpperCase()})${search || ""}
154
+ * @description ${pathname}${search || ""}${extra ? ` - ${extra}` : ""}
155
+ * Generated at ${(/* @__PURE__ */ new Date()).toISOString()}
156
+ */`;
157
+ }
158
+ async function saveBinaryMock(filePath, binaryData, pathname, method, search, contentType, url, statusCode) {
159
+ const extension = getFileExtension(contentType, url);
160
+ const binaryFilePath = filePath.replace(/\.js$/, "." + extension);
161
+ if (fs.existsSync(binaryFilePath)) return null;
162
+ fs.writeFileSync(binaryFilePath, binaryData);
163
+ const header = buildMockFileHeader(pathname, method, search, `Binary file (${extension})`);
164
+ const configContent = `${header}
165
+ export default {
166
+ enable: false,
167
+ data: {
168
+ __binaryFile: '${extension}',
169
+ __originalPath: '${pathname}',
170
+ __originalQuery: '${search}',
171
+ __originalUrl: '${pathname}${search || ""}',
172
+ __contentType: '${contentType}',
173
+ __fileSize: ${binaryData.length}
174
+ },
175
+ delay: 0,
176
+ status: ${statusCode || 200}
177
+ }`;
178
+ const formatted = await formatWithPrettier(configContent);
179
+ fs.writeFileSync(filePath, formatted, "utf-8");
180
+ return filePath;
181
+ }
182
+ async function saveJsonMock(filePath, dataStr, pathname, method, search, statusCode) {
183
+ if (fs.existsSync(filePath)) return null;
184
+ let jsonData;
185
+ if (!dataStr || dataStr.trim() === "") {
186
+ jsonData = {
187
+ error: true,
188
+ message: `Empty response (${statusCode || "unknown status"})`,
189
+ status: statusCode || 404,
190
+ data: null
191
+ };
192
+ } else {
193
+ try {
194
+ jsonData = JSON.parse(dataStr);
195
+ if (statusCode && statusCode >= 400) {
196
+ const errorMeta = { __mockStatusCode: statusCode, __isErrorResponse: true };
197
+ jsonData = typeof jsonData === "object" && jsonData !== null ? { ...jsonData, ...errorMeta } : { originalData: jsonData, ...errorMeta };
198
+ }
199
+ } catch {
200
+ jsonData = {
201
+ error: true,
202
+ message: `Non-JSON response (${statusCode || "unknown status"})`,
203
+ status: statusCode || 404,
204
+ data: dataStr,
205
+ __mockStatusCode: statusCode,
206
+ __isErrorResponse: true
207
+ };
614
208
  }
615
-
616
- .controls label input[type="text"] {
617
- padding: 0.4rem 0.6rem;
618
- border-radius: var(--radius-sm);
619
- border: 1px solid var(--border-color);
620
- background: var(--bg-primary);
621
- color: var(--text-primary);
622
- font-family: inherit;
623
- font-size: 0.8rem;
624
- outline: none;
625
- transition: all 0.15s ease;
209
+ }
210
+ const header = buildMockFileHeader(pathname, method, search);
211
+ const content = `${header}
212
+ export default {
213
+ enable: false,
214
+ data: ${JSON.stringify(jsonData)},
215
+ delay: 0,
216
+ status: ${statusCode || 200}
217
+ }`;
218
+ const formatted = await formatWithPrettier(content);
219
+ fs.writeFileSync(filePath, formatted, "utf-8");
220
+ return filePath;
221
+ }
222
+ async function saveMockData(url, method, data, rootDir, statusCode, contentType) {
223
+ try {
224
+ const absoluteRootDir = resolveAbsolutePath(rootDir);
225
+ const urlObj = new URL(url, "http://localhost");
226
+ const pathname = urlObj.pathname;
227
+ const search = urlObj.search;
228
+ const filePath = path2.join(
229
+ absoluteRootDir,
230
+ pathname.replace(/^\//, ""),
231
+ method.toLowerCase() + ".js"
232
+ );
233
+ await fs.ensureDir(path2.dirname(filePath));
234
+ const isBuffer = Buffer.isBuffer(data);
235
+ const binaryData = isBuffer ? data : Buffer.from(data || "");
236
+ const isBinary = isBinaryResponse(contentType, binaryData);
237
+ if (isBinary) {
238
+ return saveBinaryMock(
239
+ filePath,
240
+ binaryData,
241
+ pathname,
242
+ method,
243
+ search,
244
+ contentType,
245
+ url,
246
+ statusCode
247
+ );
626
248
  }
627
-
628
- .controls label input[type="text"]:focus {
629
- border-color: var(--accent-indigo);
630
- box-shadow: 0 0 0 3px var(--accent-indigo-light);
249
+ const dataStr = isBuffer ? data.toString("utf8") : data || "";
250
+ return saveJsonMock(filePath, dataStr, pathname, method, search, statusCode);
251
+ } catch (error) {
252
+ console.error(`Failed to save mock data for ${url}:`, error);
253
+ console.error(
254
+ `URL details: url=${url}, method=${method}, statusCode=${statusCode}, contentType=${contentType}`
255
+ );
256
+ throw error;
257
+ }
258
+ }
259
+ var recursiveReadAllFiles = async (dir) => {
260
+ if (!await fs.pathExists(dir)) return [];
261
+ const files = [];
262
+ try {
263
+ const list = await fs.readdir(dir);
264
+ for (const file of list) {
265
+ const filePath = path2.join(dir, file);
266
+ const stat = await fs.stat(filePath);
267
+ if (stat.isDirectory()) {
268
+ files.push(...await recursiveReadAllFiles(filePath));
269
+ } else {
270
+ files.push(filePath);
271
+ }
631
272
  }
632
-
633
- .badge {
634
- display: inline-flex;
635
- align-items: center;
636
- gap: 0.25rem;
637
- border-radius: var(--radius-sm);
638
- padding: 0.25rem 0.65rem;
639
- font-size: 0.65rem;
640
- font-weight: 600;
641
- background: linear-gradient(135deg, #818cf8 0%, #a78bfa 50%, #f472b6 100%);
642
- color: white;
643
- text-transform: uppercase;
644
- letter-spacing: 0.05em;
645
- box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
273
+ } catch (error) {
274
+ console.error(`Error reading directory ${dir}:`, error);
275
+ }
276
+ return files;
277
+ };
278
+ async function buildMockIndex(mockDir) {
279
+ const mockFileMap = /* @__PURE__ */ new Map();
280
+ if (!await fs.pathExists(mockDir)) {
281
+ await fs.ensureDir(mockDir);
282
+ return mockFileMap;
283
+ }
284
+ const files = await recursiveReadAllFiles(mockDir);
285
+ files.forEach((filePath) => {
286
+ if (!filePath.endsWith(".js")) return;
287
+ try {
288
+ const relativePath = path2.relative(mockDir, filePath);
289
+ const method = path2.basename(filePath, ".js");
290
+ const dirPath = path2.dirname(relativePath);
291
+ const urlPath = "/" + toPosixPath(dirPath);
292
+ const absolutePath = resolveAbsolutePath(filePath);
293
+ const key = `${urlPath}/${method}.js`.toLowerCase();
294
+ mockFileMap.set(key, absolutePath);
295
+ } catch (error) {
296
+ console.error(`[automock] Failed to process file ${filePath}:`, error);
646
297
  }
647
-
648
- textarea {
649
- width: 100%;
650
- flex: 1;
651
- min-height: 300px;
652
- font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
653
- font-size: 0.82rem;
654
- padding: 1rem;
655
- border: 1px solid var(--border-color);
656
- border-radius: var(--radius-md);
657
- resize: none;
658
- background: var(--bg-secondary);
659
- color: var(--text-primary);
660
- overflow-y: auto;
661
- outline: none;
662
- transition: all 0.15s ease;
663
- line-height: 1.6;
298
+ });
299
+ return mockFileMap;
300
+ }
301
+ async function parseMockModule(filePath) {
302
+ const absolutePath = resolveAbsolutePath(filePath);
303
+ if (!fs.existsSync(absolutePath)) {
304
+ throw new Error(`Mock file does not exist: ${absolutePath}`);
305
+ }
306
+ const content = fs.readFileSync(absolutePath, "utf-8");
307
+ const headerCommentMatch = content.match(/^(\/\*\*[\s\S]*?\*\/)/);
308
+ const headerComment = headerCommentMatch ? headerCommentMatch[1] : void 0;
309
+ let description;
310
+ if (headerComment) {
311
+ const descMatch = headerComment.match(/@description\s+(.+?)(?:\n|\*\/)/s);
312
+ if (descMatch) {
313
+ description = descMatch[1].trim();
664
314
  }
665
-
666
- textarea:focus {
667
- border-color: var(--accent-indigo);
668
- box-shadow: 0 0 0 3px var(--accent-indigo-light);
669
- }
670
-
671
- label {
672
- font-size: 0.75rem;
673
- color: var(--text-secondary);
674
- display: flex;
675
- gap: 0.5rem;
676
- align-items: center;
677
- }
678
-
679
- input[type="number"] {
680
- width: 90px;
681
- padding: 0.35rem 0.5rem;
682
- border-radius: var(--radius-sm);
683
- border: 1px solid var(--border-color);
684
- background: var(--bg-primary);
685
- color: var(--text-primary);
686
- font-family: inherit;
687
- font-size: 0.8rem;
688
- outline: none;
689
- transition: all 0.15s ease;
690
- }
691
-
692
- input[type="number"]:focus {
693
- border-color: var(--accent-indigo);
694
- box-shadow: 0 0 0 3px var(--accent-indigo-light);
695
- }
696
-
697
- .actions {
698
- display: flex;
699
- gap: 0.75rem;
700
- flex-shrink: 0;
701
- margin-top: 0.5rem;
702
- }
703
-
704
- button.primary {
705
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);
706
- color: white;
707
- padding: 0.5rem 1rem;
708
- border-radius: var(--radius-md);
709
- border: none;
710
- cursor: pointer;
711
- font-weight: 500;
712
- font-family: inherit;
713
- font-size: 0.8rem;
714
- transition: all 0.2s ease;
715
- box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
716
- display: inline-flex;
717
- align-items: center;
718
- gap: 0.4rem;
719
- position: relative;
720
- overflow: hidden;
721
- }
722
-
723
- button.primary::before {
724
- content: '';
725
- position: absolute;
726
- inset: 0;
727
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
728
- opacity: 0;
729
- transition: opacity 0.3s ease;
730
- }
731
-
732
- button.primary:hover::before {
733
- opacity: 1;
734
- }
735
-
736
- button.primary .btn-icon {
737
- color: white;
738
- }
739
-
740
- button.primary:hover {
741
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #9333ea 100%);
742
- box-shadow: 0 6px 25px rgba(139, 92, 246, 0.5);
743
- transform: translateY(-2px);
744
- }
745
-
746
- button.secondary {
747
- background: var(--bg-primary);
748
- color: var(--text-secondary);
749
- padding: 0.5rem 1rem;
750
- border-radius: var(--radius-md);
751
- border: 1px solid var(--border-color);
752
- cursor: pointer;
753
- font-family: inherit;
754
- font-size: 0.8rem;
755
- transition: all 0.15s ease;
756
- }
757
-
758
- button.secondary:hover {
759
- background: var(--bg-hover);
760
- color: var(--text-primary);
761
- }
762
-
763
- /* Button icons */
764
- .btn-icon {
765
- display: inline-flex;
766
- align-items: center;
767
- justify-content: center;
768
- font-size: 1rem;
769
- font-weight: 300;
770
- line-height: 1;
771
- }
772
-
773
- .btn-icon-check {
774
- display: inline-flex;
775
- align-items: center;
776
- justify-content: center;
777
- font-size: 0.85rem;
778
- font-weight: 600;
779
- line-height: 1;
780
- color: var(--accent-emerald);
781
- }
782
-
783
- .btn-icon-cross {
784
- display: inline-flex;
785
- align-items: center;
786
- justify-content: center;
787
- font-size: 0.85rem;
788
- font-weight: 600;
789
- line-height: 1;
790
- color: var(--accent-rose);
791
- }
792
-
793
- /* Detail panel checkbox */
794
- #toggle-enable {
795
- appearance: none;
796
- -webkit-appearance: none;
797
- width: 16px;
798
- height: 16px;
799
- cursor: pointer;
800
- border: 2px solid var(--border-color);
801
- border-radius: 4px;
802
- background: var(--bg-primary);
803
- position: relative;
804
- transition: all 0.15s ease;
805
- flex-shrink: 0;
806
- }
807
-
808
- #toggle-enable:hover {
809
- border-color: var(--accent-emerald);
810
- }
811
-
812
- #toggle-enable:checked {
813
- background: var(--bg-primary);
814
- border-color: var(--accent-emerald);
815
- }
816
-
817
- #toggle-enable:checked::after {
818
- content: '';
819
- position: absolute;
820
- top: 50%;
821
- left: 50%;
822
- width: 3px;
823
- height: 6px;
824
- border: solid var(--accent-emerald);
825
- border-width: 0 2px 2px 0;
826
- transform: translate(-50%, -60%) rotate(45deg);
827
- }
828
-
829
- .empty {
830
- display: flex;
831
- flex-direction: column;
832
- align-items: center;
833
- justify-content: center;
834
- color: var(--text-muted);
835
- gap: 0.5rem;
836
- height: 100%;
837
- font-size: 0.85rem;
838
- }
839
-
840
- pre {
841
- background: var(--bg-secondary);
842
- padding: 1rem;
843
- border-radius: var(--radius-md);
844
- font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
845
- overflow: auto;
846
- flex: 1;
847
- margin: 0;
848
- min-height: 300px;
849
- color: var(--text-primary);
850
- font-size: 0.82rem;
851
- line-height: 1.6;
852
- border: 1px solid var(--border-color);
853
- }
854
-
855
- textarea.error {
856
- border-color: var(--accent-rose);
857
- box-shadow: 0 0 0 3px var(--accent-rose-light);
858
- }
859
-
860
- /* Modal styles */
861
- .modal-overlay {
862
- position: fixed;
863
- top: 0;
864
- left: 0;
865
- right: 0;
866
- bottom: 0;
867
- background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.4) 100%);
868
- backdrop-filter: blur(8px);
869
- display: none;
870
- align-items: center;
871
- justify-content: center;
872
- z-index: 1000;
873
- }
874
-
875
- .modal-overlay.show {
876
- display: flex;
877
- animation: modalFadeIn 0.2s ease-out;
878
- }
879
-
880
- @keyframes modalFadeIn {
881
- from {
882
- opacity: 0;
883
- }
884
- to {
885
- opacity: 1;
886
- }
887
- }
888
-
889
- .modal {
890
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(238, 242, 255, 0.9) 50%, rgba(250, 245, 255, 0.95) 100%);
891
- backdrop-filter: blur(25px);
892
- border: 1px solid rgba(255, 255, 255, 0.6);
893
- border-radius: var(--radius-lg);
894
- padding: 2rem;
895
- max-width: 500px;
896
- width: 90%;
897
- box-shadow: 0 25px 50px rgba(139, 92, 246, 0.25), 0 0 100px rgba(236, 72, 153, 0.15);
898
- animation: modalSlideUp 0.3s ease-out;
899
- position: relative;
900
- }
901
-
902
- .modal::before {
903
- content: '';
904
- position: absolute;
905
- inset: -2px;
906
- background: linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(236, 72, 153, 0.3), rgba(59, 130, 246, 0.3));
907
- border-radius: calc(var(--radius-lg) + 2px);
908
- z-index: -1;
909
- opacity: 0.5;
910
- }
911
-
912
- @keyframes modalSlideUp {
913
- from {
914
- opacity: 0;
915
- transform: translateY(16px) scale(0.98);
916
- }
917
- to {
918
- opacity: 1;
919
- transform: translateY(0) scale(1);
920
- }
921
- }
922
-
923
- .modal h2 {
924
- margin: 0 0 1.5rem 0;
925
- color: var(--text-primary);
926
- font-size: 1.1rem;
927
- font-weight: 600;
928
- }
929
-
930
- .modal .form-group {
931
- margin-bottom: 1.25rem;
932
- }
933
-
934
- .modal .form-group label {
935
- display: block;
936
- margin-bottom: 0.5rem;
937
- font-weight: 500;
938
- color: var(--text-primary);
939
- font-size: 0.8rem;
940
- }
941
-
942
- .modal .form-group input,
943
- .modal .form-group select,
944
- .modal .form-group textarea {
945
- width: 100%;
946
- padding: 0.6rem;
947
- border: 1px solid var(--border-color);
948
- border-radius: var(--radius-sm);
949
- font-size: 0.85rem;
950
- font-family: inherit;
951
- background: var(--bg-primary);
952
- color: var(--text-primary);
953
- outline: none;
954
- transition: all 0.15s ease;
955
- }
956
-
957
- .modal .form-group input:focus,
958
- .modal .form-group select:focus,
959
- .modal .form-group textarea:focus {
960
- border-color: var(--accent-indigo);
961
- box-shadow: 0 0 0 3px var(--accent-indigo-light);
962
- }
963
-
964
- .modal .form-group textarea {
965
- min-height: 100px;
966
- font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
967
- font-size: 0.8rem;
968
- }
969
-
970
- .modal .form-group small {
971
- display: block;
972
- margin-top: 0.35rem;
973
- color: var(--text-muted);
974
- font-size: 0.7rem;
975
- }
976
-
977
- .modal .form-actions {
978
- display: flex;
979
- gap: 1rem;
980
- justify-content: flex-end;
981
- margin-top: 1.5rem;
982
- }
983
-
984
- /* Animation delays for stagger effect */
985
- header { animation-delay: 0.05s; }
986
- main { animation-delay: 0.1s; }
987
- </style>
988
- </head>
989
- <body>
990
- <header>
991
- <h1>Mock Inspector</h1>
992
- <span class="badge">${apiPrefixEscaped}</span>
993
- <button id="new-api-btn" class="primary" style="margin-left: auto;"><span class="btn-icon">+</span> New API</button>
994
- </header>
995
- <main>
996
- <aside id="sidebar">
997
- <div class="global-controls">
998
- <button id="enable-all" class="secondary"><span class="btn-icon-check">\u2713</span> \u5F00\u542F\u6240\u6709</button>
999
- <button id="disable-all" class="secondary"><span class="btn-icon-cross">\u2717</span> \u5173\u95ED\u6240\u6709</button>
1000
- </div>
1001
- <ul id="mock-list"></ul>
1002
- </aside>
1003
- <div class="resizer" id="resizer"></div>
1004
- <section>
1005
- <div id="mock-details" class="empty">
1006
- <p>Select a mock entry to inspect</p>
1007
- </div>
1008
- </section>
1009
- </main>
1010
-
1011
- <div id="new-api-modal" class="modal-overlay">
1012
- <div class="modal">
1013
- <h2><span class="btn-icon">+</span> New API Mock</h2>
1014
- <form id="new-api-form">
1015
- <div class="form-group">
1016
- <label for="new-api-method">HTTP Method</label>
1017
- <select id="new-api-method" required>
1018
- <option value="get">GET</option>
1019
- <option value="post">POST</option>
1020
- <option value="put">PUT</option>
1021
- <option value="delete">DELETE</option>
1022
- <option value="patch">PATCH</option>
1023
- </select>
1024
- </div>
1025
- <div class="form-group">
1026
- <label for="new-api-path">API Path (without prefix)</label>
1027
- <input type="text" id="new-api-path" placeholder="/users/list" required />
1028
- <small style="color: rgba(15, 23, 42, 0.6); font-size: 0.85rem;">\u4F8B\u5982\uFF1A/users/list \u6216 /api/items</small>
1029
- </div>
1030
- <div class="form-group">
1031
- <label for="new-api-description">Description (Optional)</label>
1032
- <input type="text" id="new-api-description" placeholder="\u4F8B\u5982\uFF1A\u7528\u6237\u5217\u8868\u63A5\u53E3" />
1033
- </div>
1034
- <div class="form-group">
1035
- <label for="new-api-data">Response Data (JSON)</label>
1036
- <textarea id="new-api-data" placeholder='{ "code": 200, "data": [] }'>{ "code": 200, "data": [] }</textarea>
1037
- </div>
1038
- <div class="form-actions">
1039
- <button type="button" id="cancel-new-api">Cancel</button>
1040
- <button type="submit" class="primary">Create</button>
1041
- </div>
1042
- </form>
1043
- </div>
1044
- </div>
1045
-
1046
- <script>
1047
- const inspectorRoute = ${routeJson};
1048
- const apiBase = (inspectorRoute.endsWith('/') ? inspectorRoute.slice(0, -1) : inspectorRoute) + '/api';
1049
- const allowToggle = ${allowToggleJson};
1050
-
1051
- function escapeHtml(value) {
1052
- if (value == null) return '';
1053
- return String(value)
1054
- .replace(/&/g, '&amp;')
1055
- .replace(/</g, '&lt;')
1056
- .replace(/>/g, '&gt;')
1057
- .replace(/"/g, '&quot;')
1058
- .replace(/'/g, '&#39;');
1059
- }
1060
-
1061
- // Tree data structure conversion
1062
- function buildMockTree(mocks) {
1063
- const root = { id: 'root', name: 'root', type: 'folder', children: [], checked: false, indeterminate: false };
1064
-
1065
- mocks.forEach(mock => {
1066
- // Parse the file path to build tree structure
1067
- // Example: "automock/api/v1/asset-groups/prod-db-redis/put.js"
1068
- const parts = mock.file.split('/').filter(p => p);
1069
- let currentNode = root;
1070
-
1071
- parts.forEach((part, index) => {
1072
- const isFile = part.endsWith('.js') || part.endsWith('.ts');
1073
- const nodeName = isFile ? part.replace(/.(js|ts)$/, '') : part;
1074
- const nodeId = parts.slice(0, index + 1).join('/');
1075
-
1076
- let childNode = currentNode.children.find(child => child.name === nodeName);
1077
-
1078
- if (!childNode) {
1079
- childNode = {
1080
- id: nodeId,
1081
- name: nodeName,
1082
- type: isFile ? 'file' : 'folder',
1083
- level: index,
1084
- children: [],
1085
- checked: false,
1086
- indeterminate: false
1087
- };
1088
-
1089
- if (isFile) {
1090
- childNode.mockInfo = mock;
1091
- }
1092
-
1093
- currentNode.children.push(childNode);
1094
- }
1095
-
1096
- currentNode = childNode;
1097
- });
1098
- });
1099
-
1100
- // Initialize checked state based on mock.config.enable
1101
- function initCheckedState(node) {
1102
- if (node.type === 'file' && node.mockInfo) {
1103
- node.checked = node.mockInfo.config.enable || false;
1104
- node.indeterminate = false;
1105
- }
1106
- if (node.children && node.children.length > 0) {
1107
- node.children.forEach(initCheckedState);
1108
- }
1109
- }
1110
-
1111
- initCheckedState(root);
1112
-
1113
- // Calculate parent states
1114
- function updateParentStates(node) {
1115
- if (node.children && node.children.length > 0) {
1116
- node.children.forEach(updateParentStates);
1117
-
1118
- const allChecked = node.children.every(child => child.checked && !child.indeterminate);
1119
- const someChecked = node.children.some(child => child.checked || child.indeterminate);
1120
-
1121
- node.checked = allChecked;
1122
- node.indeterminate = !allChecked && someChecked;
1123
- }
1124
- }
1125
-
1126
- updateParentStates(root);
1127
-
1128
- return root.children;
1129
- }
1130
-
1131
- // Get all leaf node (file) keys under a node
1132
- function getAllFileKeys(node) {
1133
- if (node.type === 'file') {
1134
- return [node.mockInfo?.key].filter(Boolean);
1135
- }
1136
-
1137
- if (node.children && node.children.length > 0) {
1138
- const keys = [];
1139
- node.children.forEach(child => {
1140
- keys.push(...getAllFileKeys(child));
1141
- });
1142
- return keys;
1143
- }
1144
-
1145
- return [];
1146
- }
1147
-
1148
- // Update tree node state recursively
1149
- function updateTreeNodeState(node, checked, updateChildren = true) {
1150
- if (updateChildren && node.children && node.children.length > 0) {
1151
- node.children.forEach(child => {
1152
- updateTreeNodeState(child, checked, true);
1153
- });
1154
- }
1155
-
1156
- node.checked = checked;
1157
- node.indeterminate = false;
1158
- }
1159
-
1160
- // Update parent states bottom-up
1161
- function updateParentNodeState(node, tree) {
1162
- // Find parent node
1163
- function findParent(n, targetId, parent = null) {
1164
- if (n.id === targetId) return parent;
1165
- if (n.children) {
1166
- for (const child of n.children) {
1167
- const result = findParent(child, targetId, n);
1168
- if (result) return result;
1169
- }
1170
- }
1171
- return null;
1172
- }
1173
-
1174
- const parent = findParent({ children: tree }, node.id);
1175
-
1176
- if (parent) {
1177
- const allChecked = parent.children.every(child => child.checked && !child.indeterminate);
1178
- const someChecked = parent.children.some(child => child.checked || child.indeterminate);
1179
-
1180
- parent.checked = allChecked;
1181
- parent.indeterminate = !allChecked && someChecked;
1182
-
1183
- updateParentNodeState(parent, tree);
1184
- }
1185
- }
1186
-
1187
- // Render tree node recursively
1188
- function renderTreeNode(node, level = 0, expandedNodes = new Set()) {
1189
- const hasChildren = node.children && node.children.length > 0;
1190
- const isExpanded = expandedNodes.has(node.id);
1191
- const paddingLeft = level * 1.2 + 0.5;
1192
-
1193
- let html = '<div class="tree-node" data-node-id="' + escapeHtml(node.id) + '" data-node-type="' + node.type + '">';
1194
-
1195
- // Node content
1196
- html += '<div class="tree-node-content" style="padding-left: ' + paddingLeft + 'rem">';
1197
-
1198
- // Expand/collapse icon
1199
- if (hasChildren) {
1200
- html += '<span class="tree-expand-icon' + (isExpanded ? ' expanded' : '') + '">\u25B6</span>';
1201
- } else {
1202
- html += '<span class="tree-expand-icon hidden"></span>';
1203
- }
1204
-
1205
- // Checkbox
1206
- const checkedAttr = node.checked ? 'checked' : '';
1207
- const indeterminateAttr = node.indeterminate ? 'data-indeterminate="true"' : '';
1208
- html += '<input type="checkbox" class="tree-node-checkbox" ' + checkedAttr + ' ' + indeterminateAttr + ' />';
1209
-
1210
- // Node label
1211
- if (node.type === 'file' && node.mockInfo) {
1212
- const mock = node.mockInfo;
1213
- const methodClass = mock.method?.toLowerCase() || 'get';
1214
- html += '<span class="tree-node-method ' + methodClass + '">' + escapeHtml(mock.method?.toUpperCase() || 'GET') + '</span>';
1215
- html += '<span class="tree-node-label file" title="' + escapeHtml(mock.path) + '">' + escapeHtml(mock.path) + '</span>';
1216
- if (mock.description) {
1217
- html += '<span class="tree-node-count" title="' + escapeHtml(mock.description) + '">' + escapeHtml(mock.description) + '</span>';
1218
- }
1219
- // Delete button (only for files)
1220
- html += '<span class="tree-node-delete" data-mock-key="' + escapeHtml(mock.key) + '" data-is-folder="false" title="\u5220\u9664\u6B64 Mock">\u2715</span>';
1221
- } else {
1222
- const fileCount = getAllFileKeys(node);
1223
- const fileKeysJson = JSON.stringify(fileCount);
1224
- // Put count inside the label to avoid layout shift when delete button appears
1225
- html += '<span class="tree-node-label folder">' + escapeHtml(node.name) + ' <span class="tree-node-count">(' + fileCount.length + ')</span></span>';
1226
- // Delete button for folders
1227
- html += '<span class="tree-node-delete" data-mock-keys="' + escapeHtml(fileKeysJson) + '" data-is-folder="true" data-folder-name="' + escapeHtml(node.name) + '" title="\u5220\u9664\u6B64\u6587\u4EF6\u5939\u53CA\u6240\u6709 Mock">\u2715</span>';
1228
- }
1229
-
1230
- html += '</div>';
1231
-
1232
- // Children container
1233
- if (hasChildren) {
1234
- html += '<div class="tree-children' + (isExpanded ? ' expanded' : '') + '">';
1235
- node.children.forEach(child => {
1236
- html += renderTreeNode(child, level + 1, expandedNodes);
1237
- });
1238
- html += '</div>';
1239
- }
1240
-
1241
- html += '</div>';
1242
-
1243
- return html;
1244
- }
1245
-
1246
- // Find node by id in tree
1247
- function findNodeById(nodes, id) {
1248
- for (const node of nodes) {
1249
- if (node.id === id) return node;
1250
- if (node.children) {
1251
- const found = findNodeById(node.children, id);
1252
- if (found) return found;
1253
- }
1254
- }
1255
- return null;
1256
- }
1257
-
1258
- // Toggle tree node expand/collapse
1259
- function toggleTreeNodeExpand(nodeId) {
1260
- const nodeEl = document.querySelector('.tree-node[data-node-id="' + nodeId + '"]');
1261
- if (!nodeEl) return;
1262
-
1263
- const childrenEl = nodeEl.querySelector('.tree-children');
1264
- const iconEl = nodeEl.querySelector('.tree-expand-icon');
1265
-
1266
- if (childrenEl && iconEl && !iconEl.classList.contains('hidden')) {
1267
- const isExpanded = childrenEl.classList.contains('expanded');
1268
- if (isExpanded) {
1269
- childrenEl.classList.remove('expanded');
1270
- iconEl.classList.remove('expanded');
1271
- } else {
1272
- childrenEl.classList.add('expanded');
1273
- iconEl.classList.add('expanded');
1274
- }
1275
- }
1276
- }
1277
-
1278
- function renderToggleSection(mock) {
1279
- if (!allowToggle) {
1280
- return '';
1281
- }
1282
- const checked = mock.config.enable ? 'checked' : '';
1283
- return (
1284
- '<label>' +
1285
- '<input type="checkbox" id="toggle-enable" ' + checked + ' />' +
1286
- 'Enable' +
1287
- '</label>'
1288
- );
1289
- }
1290
-
1291
- function renderDataSection(mock) {
1292
- if (mock.editable) {
1293
- return '<div class="data-container"><textarea id="data-editor"></textarea><div class="actions"><button class="primary" id="save-btn">Save</button></div></div>';
1294
- }
1295
- return '<div class="data-container"><pre id="data-preview"></pre></div>';
1296
- }
1297
-
1298
- async function fetchMocks() {
1299
- try {
1300
- const res = await fetch(apiBase + '/list');
1301
- if (!res.ok) {
1302
- throw new Error('HTTP ' + res.status + ': ' + res.statusText);
1303
- }
1304
- const data = await res.json();
1305
- return data.mocks || [];
1306
- } catch (error) {
1307
- console.error('[Inspector] Failed to fetch mocks:', error);
1308
- return [];
1309
- }
1310
- }
1311
-
1312
- function renderMockList(mocks, preserveExpandedState = false) {
1313
- const list = document.getElementById('mock-list');
1314
- if (!list) {
1315
- console.error('[Inspector] Element #mock-list not found!');
1316
- return;
1317
- }
1318
-
1319
- // Save current expanded state if requested
1320
- let savedExpandedNodes = new Set();
1321
- if (preserveExpandedState) {
1322
- list.querySelectorAll('.tree-children.expanded').forEach(el => {
1323
- const parentNode = el.closest('.tree-node');
1324
- if (parentNode) {
1325
- savedExpandedNodes.add(parentNode.dataset.nodeId);
1326
- }
1327
- });
1328
- }
1329
-
1330
- list.innerHTML = '';
1331
- if (!mocks || mocks.length === 0) {
1332
- console.warn('[Inspector] No mocks to display');
1333
- list.innerHTML = '<div style="padding: 1rem; color: #999;">No mock files found</div>';
1334
- return;
1335
- }
1336
-
1337
- // Build tree structure
1338
- const tree = buildMockTree(mocks);
1339
-
1340
- // Use saved expanded state or expand first level by default
1341
- const expandedNodes = savedExpandedNodes.size > 0 ? savedExpandedNodes : new Set();
1342
- if (expandedNodes.size === 0) {
1343
- tree.forEach(node => {
1344
- if (node.type === 'folder') {
1345
- expandedNodes.add(node.id);
1346
- }
1347
- });
1348
- }
1349
-
1350
- let treeHtml = '';
1351
- tree.forEach(node => {
1352
- const nodeHtml = renderTreeNode(node, 0, expandedNodes);
1353
- treeHtml += nodeHtml;
1354
- });
1355
-
1356
- list.innerHTML = treeHtml;
1357
-
1358
- // Attach event listeners
1359
- attachTreeEventListeners();
1360
- }
1361
-
1362
- // Attach event listeners for tree interactions
1363
- function attachTreeEventListeners() {
1364
- const list = document.getElementById('mock-list');
1365
- if (!list) return;
1366
-
1367
- // Handle expand/collapse clicks
1368
- list.querySelectorAll('.tree-expand-icon').forEach(icon => {
1369
- icon.addEventListener('click', (e) => {
1370
- e.stopPropagation();
1371
- const nodeEl = icon.closest('.tree-node');
1372
- if (nodeEl) {
1373
- const nodeId = nodeEl.dataset.nodeId;
1374
- toggleTreeNodeExpand(nodeId);
1375
- }
1376
- });
1377
- });
1378
-
1379
- // Handle checkbox clicks
1380
- list.querySelectorAll('.tree-node-checkbox').forEach(checkbox => {
1381
- checkbox.addEventListener('click', async (e) => {
1382
- e.stopPropagation();
1383
- const nodeEl = checkbox.closest('.tree-node');
1384
- if (!nodeEl) return;
1385
-
1386
- const nodeId = nodeEl.dataset.nodeId;
1387
- const nodeType = nodeEl.dataset.nodeType;
1388
- const checked = checkbox.checked;
1389
-
1390
- // Get current tree data
1391
- const tree = buildMockTree(currentMocks);
1392
-
1393
- // Find and update node
1394
- const node = findNodeById(tree, nodeId);
1395
- if (node) {
1396
- // Update children
1397
- updateTreeNodeState(node, checked, true);
1398
- // Update parents
1399
- updateParentNodeState(node, tree);
1400
-
1401
- // Apply changes to all affected file nodes
1402
- const affectedKeys = getAllFileKeys(node);
1403
- await Promise.all(affectedKeys.map(key => toggleMockEnable(key, checked)));
1404
-
1405
- // Re-render tree with preserved expanded state
1406
- renderMockList(currentMocks, true);
1407
- }
1408
- });
1409
- });
1410
-
1411
- // Handle node content clicks (for selection)
1412
- list.querySelectorAll('.tree-node-content').forEach(content => {
1413
- content.addEventListener('click', (e) => {
1414
- // Don't select if clicking on checkbox or expand icon
1415
- if (e.target.classList.contains('tree-node-checkbox') ||
1416
- e.target.classList.contains('tree-expand-icon')) {
1417
- return;
1418
- }
1419
-
1420
- const nodeEl = content.closest('.tree-node');
1421
- if (!nodeEl) return;
1422
-
1423
- const nodeId = nodeEl.dataset.nodeId;
1424
- const nodeType = nodeEl.dataset.nodeType;
1425
-
1426
- // Remove previous selection
1427
- document.querySelectorAll('.tree-node-content.selected').forEach(el => {
1428
- el.classList.remove('selected');
1429
- });
1430
-
1431
- // Add selection to current node
1432
- content.classList.add('selected');
1433
-
1434
- // For file nodes, select the mock
1435
- if (nodeType === 'file') {
1436
- const tree = buildMockTree(currentMocks);
1437
- const node = findNodeById(tree, nodeId);
1438
- if (node && node.mockInfo) {
1439
- selectMock(node.mockInfo.key, content);
1440
- }
1441
- }
1442
- });
1443
- });
1444
-
1445
- // Set indeterminate state for checkboxes
1446
- list.querySelectorAll('.tree-node-checkbox[data-indeterminate="true"]').forEach(cb => {
1447
- cb.indeterminate = true;
1448
- });
1449
-
1450
- // Handle delete button clicks
1451
- list.querySelectorAll('.tree-node-delete').forEach(deleteBtn => {
1452
- deleteBtn.addEventListener('click', async (e) => {
1453
- e.stopPropagation();
1454
-
1455
- // Check if user has chosen "never ask again"
1456
- const skipConfirm = localStorage.getItem('mockInspector_skipDeleteConfirm') === 'true';
1457
-
1458
- const isFolder = deleteBtn.dataset.isFolder === 'true';
1459
- let confirmMessage = '';
1460
- let keysToDelete = [];
1461
-
1462
- if (isFolder) {
1463
- // Folder deletion
1464
- const folderName = deleteBtn.dataset.folderName || '';
1465
- const keysJson = deleteBtn.dataset.mockKeys || '[]';
1466
- keysToDelete = JSON.parse(keysJson);
1467
- confirmMessage = '\u786E\u5B9A\u8981\u5220\u9664\u6587\u4EF6\u5939 "' + folderName + '" \u53CA\u5176\u5305\u542B\u7684 ' + keysToDelete.length + ' \u4E2A Mock \u6587\u4EF6\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002';
1468
- } else {
1469
- // Single file deletion
1470
- const mockKey = deleteBtn.dataset.mockKey;
1471
- if (!mockKey) return;
1472
- keysToDelete = [mockKey];
1473
- confirmMessage = '\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u4E2A Mock \u6570\u636E\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002';
1474
- }
1475
-
1476
- // If user chose "never ask again", delete directly
1477
- if (skipConfirm) {
1478
- await performDelete(keysToDelete);
1479
- return;
1480
- }
1481
-
1482
- // Otherwise, show custom confirmation dialog
1483
- showDeleteConfirmDialog(confirmMessage, keysToDelete);
1484
- });
1485
- });
1486
-
1487
- // Custom delete confirmation dialog
1488
- function showDeleteConfirmDialog(message, keysToDelete) {
1489
- const modal = document.getElementById('delete-confirm-modal');
1490
- const messageEl = document.getElementById('delete-confirm-message');
1491
- const neverAskCheckbox = document.getElementById('delete-never-ask');
1492
- const confirmBtn = document.getElementById('delete-confirm-btn');
1493
- const cancelBtn = document.getElementById('delete-cancel-btn');
1494
-
1495
- messageEl.textContent = message;
1496
- neverAskCheckbox.checked = false;
1497
- modal.classList.add('show');
1498
-
1499
- // Remove old event listeners
1500
- const newConfirmBtn = confirmBtn.cloneNode(true);
1501
- const newCancelBtn = cancelBtn.cloneNode(true);
1502
- confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
1503
- cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
1504
-
1505
- // Confirm button handler
1506
- newConfirmBtn.addEventListener('click', async () => {
1507
- // Save preference if "never ask again" is checked
1508
- if (neverAskCheckbox.checked) {
1509
- localStorage.setItem('mockInspector_skipDeleteConfirm', 'true');
1510
- }
1511
- modal.classList.remove('show');
1512
- await performDelete(keysToDelete);
1513
- });
1514
-
1515
- // Cancel button handler
1516
- newCancelBtn.addEventListener('click', () => {
1517
- modal.classList.remove('show');
1518
- });
1519
-
1520
- // Close on overlay click
1521
- modal.addEventListener('click', (e) => {
1522
- if (e.target === modal) {
1523
- modal.classList.remove('show');
1524
- }
1525
- });
1526
- }
1527
-
1528
- // Perform the actual deletion
1529
- async function performDelete(keysToDelete) {
1530
- try {
1531
- // Delete all keys (either single file or entire folder)
1532
- const deletePromises = keysToDelete.map(key =>
1533
- fetch(apiBase + '/delete?key=' + encodeURIComponent(key), {
1534
- method: 'DELETE'
1535
- })
1536
- );
1537
-
1538
- const results = await Promise.all(deletePromises);
1539
-
1540
- // Check if any deletion failed
1541
- const failedDeletions = results.filter(res => !res.ok);
1542
- if (failedDeletions.length > 0) {
1543
- const errors = await Promise.all(failedDeletions.map(res => res.json()));
1544
- throw new Error(errors.map(e => e.error).join(', '));
1545
- }
1546
-
1547
- // Refresh the list
1548
- currentMocks = await fetchMocks();
1549
- renderMockList(currentMocks, true);
1550
-
1551
- // Clear details panel if any deleted mock was selected
1552
- if (keysToDelete.includes(window.currentKey)) {
1553
- window.currentKey = null;
1554
- document.getElementById('mock-details').innerHTML = '<p>Select a mock entry to inspect</p>';
1555
- document.getElementById('mock-details').className = 'empty';
1556
- }
1557
- } catch (error) {
1558
- alert('\u5220\u9664\u5931\u8D25: ' + error.message);
1559
- }
1560
- }
1561
- }
1562
-
1563
- function renderDetails(mock) {
1564
- const container = document.getElementById('mock-details');
1565
- if (!mock) {
1566
- container.className = 'empty';
1567
- container.innerHTML = '<p>Select a mock entry to inspect</p>';
1568
- return;
1569
- }
1570
-
1571
- container.className = '';
1572
- container.innerHTML = [
1573
- '<div style="display: flex; flex-direction: column; height: 100%;">',
1574
- ' <div class="controls">',
1575
- ' <h2>',
1576
- ' <span class="badge">' + escapeHtml(mock.method.toUpperCase()) + '</span>',
1577
- ' ' + escapeHtml(mock.path),
1578
- ' </h2>',
1579
- ' <label style="flex: 1;" >',
1580
- ' Desc: ',
1581
- ' <input type="text" id="description-input" placeholder="\u4F8B\u5982\uFF1A\u7528\u6237\u5217\u8868\u63A5\u53E3" value="' + escapeHtml(mock.description || '') + '" style="width: 100%; margin-top: 0.25rem;" />',
1582
- ' </label>',
1583
- ' <label>Delay <input type="number" id="delay-input" value="' + mock.config.delay + '" min="0" step="50" /> ms</label>',
1584
- ' <label>Status <input type="number" id="status-input" value="' + mock.config.status + '" min="100" max="599" /></label>',
1585
- ' ' + renderToggleSection(mock),
1586
- ' </div>',
1587
- ' <h3>Response Data</h3>',
1588
- ' ' + renderDataSection(mock),
1589
- '</div>'
1590
- ].join('\\n');
1591
-
1592
- const descriptionInput = document.getElementById('description-input');
1593
- if (descriptionInput) {
1594
- descriptionInput.addEventListener('change', () => updateDescription(descriptionInput.value));
1595
- }
1596
-
1597
- if (allowToggle) {
1598
- const enableToggle = document.getElementById('toggle-enable');
1599
- if (enableToggle) {
1600
- enableToggle.addEventListener('change', () => updateConfig({ enable: enableToggle.checked }));
1601
- }
1602
- }
1603
-
1604
- const delayInput = document.getElementById('delay-input');
1605
- if (delayInput) {
1606
- delayInput.addEventListener('change', () => updateConfig({ delay: Number(delayInput.value) || 0 }));
1607
- }
1608
-
1609
- const statusInput = document.getElementById('status-input');
1610
- if (statusInput) {
1611
- statusInput.addEventListener('change', () => updateConfig({ status: Number(statusInput.value) || 200 }));
1612
- }
1613
-
1614
- if (mock.editable) {
1615
- const textarea = document.getElementById('data-editor');
1616
- if (textarea) {
1617
- textarea.value = mock.dataText || '';
1618
- const saveBtn = document.getElementById('save-btn');
1619
- if (saveBtn) {
1620
- saveBtn.addEventListener('click', async () => {
1621
- try {
1622
- const raw = textarea.value || 'null';
1623
- const parsed = JSON.parse(raw);
1624
- await updateConfig({ data: parsed });
1625
- textarea.classList.remove('error');
1626
- } catch (err) {
1627
- textarea.classList.add('error');
1628
- alert('Invalid JSON: ' + err.message);
1629
- }
1630
- });
1631
- }
1632
- }
1633
- } else {
1634
- const pre = document.getElementById('data-preview');
1635
- if (pre) {
1636
- pre.textContent = mock.dataText || '';
1637
- }
1638
- }
1639
- }
1640
-
1641
- let currentMocks = [];
1642
-
1643
- async function updateConfig(partial) {
1644
- if (!window.currentKey) return;
1645
- if (!allowToggle && 'enable' in partial) {
1646
- delete partial.enable;
1647
- }
1648
- const res = await fetch(apiBase + '/update', {
1649
- method: 'POST',
1650
- headers: { 'Content-Type': 'application/json' },
1651
- body: JSON.stringify({ key: window.currentKey, config: partial })
1652
- });
1653
- const data = await res.json();
1654
- const updated = data.mock;
1655
- const index = currentMocks.findIndex((item) => item.key === window.currentKey);
1656
- if (index >= 0) {
1657
- currentMocks[index] = updated;
1658
- renderDetails(updated);
1659
- }
1660
- }
1661
-
1662
- async function updateDescription(description) {
1663
- if (!window.currentKey) return;
1664
- try {
1665
- const res = await fetch(apiBase + '/update', {
1666
- method: 'POST',
1667
- headers: { 'Content-Type': 'application/json' },
1668
- body: JSON.stringify({ key: window.currentKey, description: description })
1669
- });
1670
- const data = await res.json();
1671
- const updated = data.mock;
1672
- const index = currentMocks.findIndex((item) => item.key === window.currentKey);
1673
- if (index >= 0) {
1674
- currentMocks[index] = updated;
1675
- renderDetails(updated);
1676
- // \u91CD\u65B0\u6E32\u67D3\u5217\u8868\u4EE5\u66F4\u65B0\u663E\u793A
1677
- renderMockList(currentMocks, true);
1678
- }
1679
- } catch (error) {
1680
- console.error('[Inspector] Failed to update description:', error);
1681
- alert('\u66F4\u65B0\u63CF\u8FF0\u5931\u8D25: ' + error.message);
1682
- }
1683
- }
1684
-
1685
- async function selectMock(key, button) {
1686
- window.currentKey = key;
1687
- document.querySelectorAll('aside button').forEach((btn) => btn.classList.remove('active'));
1688
- button.classList.add('active');
1689
- const res = await fetch(apiBase + '/detail?key=' + encodeURIComponent(key));
1690
- const data = await res.json();
1691
- const mock = data.mock;
1692
- const index = currentMocks.findIndex((item) => item.key === key);
1693
- if (index >= 0) {
1694
- currentMocks[index] = mock;
1695
- }
1696
- renderDetails(mock);
1697
- }
1698
-
1699
- async function toggleMockEnable(key, enable) {
1700
- try {
1701
- const res = await fetch(apiBase + '/update', {
1702
- method: 'POST',
1703
- headers: { 'Content-Type': 'application/json' },
1704
- body: JSON.stringify({ key: key, config: { enable: enable } })
1705
- });
1706
- const data = await res.json();
1707
- const updated = data.mock;
1708
- const index = currentMocks.findIndex((item) => item.key === key);
1709
- if (index >= 0) {
1710
- currentMocks[index] = updated;
1711
- // \u5982\u679C\u5F53\u524D\u6B63\u5728\u67E5\u770B\u8FD9\u4E2A mock\uFF0C\u66F4\u65B0\u8BE6\u60C5
1712
- if (window.currentKey === key) {
1713
- renderDetails(updated);
1714
- }
1715
- }
1716
- } catch (error) {
1717
- console.error('[Inspector] Failed to toggle mock:', error);
1718
- alert('\u66F4\u65B0\u5931\u8D25: ' + error.message);
1719
- }
1720
- }
1721
-
1722
- function startEditDescription(li, mock) {
1723
- // \u6E05\u7A7A li \u5185\u5BB9\uFF0C\u521B\u5EFA\u7F16\u8F91\u6846
1724
- li.innerHTML = '';
1725
-
1726
- const input = document.createElement('input');
1727
- input.type = 'text';
1728
- input.className = 'description-edit';
1729
- input.value = mock.description || '';
1730
- input.placeholder = '\u8F93\u5165\u4E1A\u52A1\u63CF\u8FF0\uFF0C\u4F8B\u5982\uFF1A\u7528\u6237\u5217\u8868\u63A5\u53E3';
1731
-
1732
- const saveEdit = async () => {
1733
- const newDescription = input.value.trim();
1734
- try {
1735
- const res = await fetch(apiBase + '/update', {
1736
- method: 'POST',
1737
- headers: { 'Content-Type': 'application/json' },
1738
- body: JSON.stringify({ key: mock.key, description: newDescription })
1739
- });
1740
- const data = await res.json();
1741
- const updated = data.mock;
1742
- const index = currentMocks.findIndex((item) => item.key === mock.key);
1743
- if (index >= 0) {
1744
- currentMocks[index] = updated;
1745
- }
1746
- // \u91CD\u65B0\u6E32\u67D3\u5217\u8868
1747
- renderMockList(currentMocks, true);
1748
- // \u5982\u679C\u5F53\u524D\u6B63\u5728\u67E5\u770B\u8FD9\u4E2A mock\uFF0C\u66F4\u65B0\u8BE6\u60C5
1749
- if (window.currentKey === mock.key) {
1750
- renderDetails(updated);
1751
- }
1752
- } catch (error) {
1753
- console.error('[Inspector] Failed to update description:', error);
1754
- alert('\u66F4\u65B0\u5931\u8D25: ' + error.message);
1755
- renderMockList(currentMocks, true);
1756
- }
1757
- };
1758
-
1759
- input.addEventListener('blur', saveEdit);
1760
- input.addEventListener('keydown', (e) => {
1761
- if (e.key === 'Enter') {
1762
- saveEdit();
1763
- } else if (e.key === 'Escape') {
1764
- renderMockList(currentMocks, true);
1765
- }
1766
- });
1767
-
1768
- li.appendChild(input);
1769
- input.focus();
1770
- input.select();
1771
- }
1772
-
1773
- async function enableAllMocks() {
1774
- const promises = currentMocks.map(mock =>
1775
- toggleMockEnable(mock.key, true)
1776
- );
1777
- await Promise.all(promises);
1778
- // \u91CD\u65B0\u83B7\u53D6\u5217\u8868\u4EE5\u66F4\u65B0 UI
1779
- currentMocks = await fetchMocks();
1780
- renderMockList(currentMocks, true);
1781
- }
1782
-
1783
- async function disableAllMocks() {
1784
- const promises = currentMocks.map(mock =>
1785
- toggleMockEnable(mock.key, false)
1786
- );
1787
- await Promise.all(promises);
1788
- // \u91CD\u65B0\u83B7\u53D6\u5217\u8868\u4EE5\u66F4\u65B0 UI
1789
- currentMocks = await fetchMocks();
1790
- renderMockList(currentMocks, true);
1791
- }
1792
-
1793
- // Initialize sidebar resizer functionality
1794
- function initSidebarResizer() {
1795
- const sidebar = document.getElementById('sidebar');
1796
- const resizer = document.getElementById('resizer');
1797
- const main = document.querySelector('main');
1798
-
1799
- if (!sidebar || !resizer || !main) {
1800
- console.warn('[Inspector] Sidebar resizer elements not found');
1801
- return;
1802
- }
1803
-
1804
- // Load saved width from localStorage
1805
- const savedWidth = localStorage.getItem('mockInspectorSidebarWidth');
1806
- if (savedWidth) {
1807
- const width = Math.max(200, Math.min(800, parseInt(savedWidth, 10)));
1808
- main.style.setProperty('--sidebar-width', width + 'px');
1809
- }
1810
-
1811
- let isResizing = false;
1812
- let startX = 0;
1813
- let startWidth = 0;
1814
-
1815
- resizer.addEventListener('mousedown', (e) => {
1816
- isResizing = true;
1817
- startX = e.clientX;
1818
- startWidth = sidebar.offsetWidth;
1819
- resizer.classList.add('active');
1820
- document.body.style.cursor = 'col-resize';
1821
- document.body.style.userSelect = 'none';
1822
- e.preventDefault();
1823
- });
1824
-
1825
- document.addEventListener('mousemove', (e) => {
1826
- if (!isResizing) return;
1827
-
1828
- const deltaX = e.clientX - startX;
1829
- const newWidth = Math.max(200, Math.min(800, startWidth + deltaX));
1830
- main.style.setProperty('--sidebar-width', newWidth + 'px');
1831
- });
1832
-
1833
- document.addEventListener('mouseup', () => {
1834
- if (isResizing) {
1835
- isResizing = false;
1836
- resizer.classList.remove('active');
1837
- document.body.style.cursor = '';
1838
- document.body.style.userSelect = '';
1839
-
1840
- // Save width to localStorage
1841
- const currentWidth = sidebar.offsetWidth;
1842
- localStorage.setItem('mockInspectorSidebarWidth', currentWidth.toString());
1843
- }
1844
- });
1845
-
1846
- // Touch support for mobile devices
1847
- resizer.addEventListener('touchstart', (e) => {
1848
- const touch = e.touches[0];
1849
- isResizing = true;
1850
- startX = touch.clientX;
1851
- startWidth = sidebar.offsetWidth;
1852
- resizer.classList.add('active');
1853
- e.preventDefault();
1854
- }, { passive: false });
1855
-
1856
- document.addEventListener('touchmove', (e) => {
1857
- if (!isResizing) return;
1858
-
1859
- const touch = e.touches[0];
1860
- const deltaX = touch.clientX - startX;
1861
- const newWidth = Math.max(200, Math.min(800, startWidth + deltaX));
1862
- main.style.setProperty('--sidebar-width', newWidth + 'px');
1863
- }, { passive: false });
1864
-
1865
- document.addEventListener('touchend', () => {
1866
- if (isResizing) {
1867
- isResizing = false;
1868
- resizer.classList.remove('active');
1869
-
1870
- // Save width to localStorage
1871
- const currentWidth = sidebar.offsetWidth;
1872
- localStorage.setItem('mockInspectorSidebarWidth', currentWidth.toString());
1873
- }
1874
- });
1875
- }
1876
-
1877
- async function bootstrap() {
1878
- // Initialize sidebar resizer
1879
- initSidebarResizer();
1880
-
1881
- try {
1882
- currentMocks = await fetchMocks();
1883
- renderMockList(currentMocks, true);
1884
-
1885
- // \u7ED1\u5B9A\u5168\u5C40\u63A7\u5236\u6309\u94AE
1886
- const enableAllBtn = document.getElementById('enable-all');
1887
- const disableAllBtn = document.getElementById('disable-all');
1888
- if (enableAllBtn) {
1889
- enableAllBtn.addEventListener('click', async () => {
1890
- enableAllBtn.disabled = true;
1891
- enableAllBtn.textContent = '\u5904\u7406\u4E2D...';
1892
- await enableAllMocks();
1893
- enableAllBtn.disabled = false;
1894
- enableAllBtn.textContent = '\u2713 \u5F00\u542F\u6240\u6709';
1895
- });
1896
- }
1897
- if (disableAllBtn) {
1898
- disableAllBtn.addEventListener('click', async () => {
1899
- disableAllBtn.disabled = true;
1900
- disableAllBtn.textContent = '\u5904\u7406\u4E2D...';
1901
- await disableAllMocks();
1902
- disableAllBtn.disabled = false;
1903
- disableAllBtn.textContent = '\u2717 \u5173\u95ED\u6240\u6709';
1904
- });
1905
- }
1906
-
1907
- // \u7ED1\u5B9A\u65B0\u5EFAAPI\u6309\u94AE
1908
- const newApiBtn = document.getElementById('new-api-btn');
1909
- const newApiModal = document.getElementById('new-api-modal');
1910
- const newApiForm = document.getElementById('new-api-form');
1911
- const cancelNewApi = document.getElementById('cancel-new-api');
1912
-
1913
- if (newApiBtn && newApiModal) {
1914
- newApiBtn.addEventListener('click', () => {
1915
- newApiModal.classList.add('show');
1916
- document.getElementById('new-api-path').focus();
1917
- });
1918
- }
1919
-
1920
- if (cancelNewApi && newApiModal) {
1921
- cancelNewApi.addEventListener('click', () => {
1922
- newApiModal.classList.remove('show');
1923
- newApiForm.reset();
1924
- });
1925
- }
1926
-
1927
- // \u70B9\u51FB\u80CC\u666F\u5173\u95ED\u6A21\u6001\u6846
1928
- if (newApiModal) {
1929
- newApiModal.addEventListener('click', (e) => {
1930
- if (e.target === newApiModal) {
1931
- newApiModal.classList.remove('show');
1932
- newApiForm.reset();
1933
- }
1934
- });
1935
- }
1936
-
1937
- // \u5904\u7406\u8868\u5355\u63D0\u4EA4
1938
- if (newApiForm) {
1939
- newApiForm.addEventListener('submit', async (e) => {
1940
- e.preventDefault();
1941
-
1942
- const method = document.getElementById('new-api-method').value;
1943
- const path = document.getElementById('new-api-path').value.trim();
1944
- const description = document.getElementById('new-api-description').value.trim();
1945
- const dataText = document.getElementById('new-api-data').value.trim();
1946
-
1947
- if (!path) {
1948
- alert('\u8BF7\u8F93\u5165 API \u8DEF\u5F84');
1949
- return;
1950
- }
1951
-
1952
- // \u9A8C\u8BC1JSON
1953
- let data;
1954
- try {
1955
- data = JSON.parse(dataText || '{}');
1956
- } catch (err) {
1957
- alert('Response Data \u4E0D\u662F\u6709\u6548\u7684 JSON: ' + err.message);
1958
- return;
1959
- }
1960
-
1961
- // \u53D1\u9001\u521B\u5EFA\u8BF7\u6C42
1962
- try {
1963
- const submitBtn = newApiForm.querySelector('button[type="submit"]');
1964
- const originalText = submitBtn.textContent;
1965
- submitBtn.disabled = true;
1966
- submitBtn.textContent = 'Creating...';
1967
-
1968
- const res = await fetch(apiBase + '/create', {
1969
- method: 'POST',
1970
- headers: { 'Content-Type': 'application/json' },
1971
- body: JSON.stringify({
1972
- method: method,
1973
- path: path,
1974
- description: description || path,
1975
- data: data
1976
- })
1977
- });
1978
-
1979
- const result = await res.json();
1980
-
1981
- if (!res.ok) {
1982
- throw new Error(result.error || 'Failed to create API');
1983
- }
1984
-
1985
- // \u5173\u95ED\u6A21\u6001\u6846
1986
- newApiModal.classList.remove('show');
1987
- newApiForm.reset();
1988
-
1989
- // \u5237\u65B0\u5217\u8868
1990
- currentMocks = await fetchMocks();
1991
- renderMockList(currentMocks, true);
1992
-
1993
- // \u81EA\u52A8\u9009\u4E2D\u65B0\u521B\u5EFA\u7684 API
1994
- if (result.mock && result.mock.key) {
1995
- const button = document.querySelector('aside button[data-key="' + result.mock.key + '"]');
1996
- if (button) {
1997
- await selectMock(result.mock.key, button);
1998
- }
1999
- }
2000
-
2001
- alert('\u2705 API Mock \u521B\u5EFA\u6210\u529F\uFF01');
2002
-
2003
- submitBtn.disabled = false;
2004
- submitBtn.textContent = originalText;
2005
- } catch (err) {
2006
- alert('\u521B\u5EFA\u5931\u8D25: ' + err.message);
2007
- const submitBtn = newApiForm.querySelector('button[type="submit"]');
2008
- submitBtn.disabled = false;
2009
- submitBtn.textContent = 'Create';
2010
- }
2011
- });
2012
- }
2013
- } catch (error) {
2014
- console.error('[Inspector] Bootstrap failed:', error);
315
+ }
316
+ const { default: mockModule } = await import(`${absolutePath}?t=${Date.now()}`);
317
+ const exportedConfig = typeof mockModule === "function" ? mockModule() : mockModule;
318
+ const hasDynamicData = typeof exportedConfig?.data === "function";
319
+ const config = {
320
+ ...DEFAULT_CONFIG,
321
+ ...exportedConfig ?? {}
322
+ };
323
+ const serializable = !hasDynamicData;
324
+ const dataObj = typeof config.data === "object" && config.data !== null ? config.data : null;
325
+ const isBinary = dataObj !== null && "__binaryFile" in dataObj;
326
+ return {
327
+ config,
328
+ serializable,
329
+ hasDynamicData,
330
+ headerComment,
331
+ description,
332
+ dataText: isBinary ? `/* Binary file: ${dataObj?.__binaryFile} */` : serializable ? JSON.stringify(config.data ?? null, null, 2) : "/* data is generated dynamically and cannot be edited here */",
333
+ isBinary
334
+ };
335
+ }
336
+ async function writeMockFile(filePath, mockInfo) {
337
+ const absolutePath = resolveAbsolutePath(filePath);
338
+ let header = mockInfo.headerComment || "";
339
+ if (mockInfo.description !== void 0) {
340
+ if (header) {
341
+ if (/@description/.test(header)) {
342
+ header = header.replace(
343
+ /@description\s+.+?(?=\n|\*\/)/s,
344
+ `@description ${mockInfo.description}`
345
+ );
346
+ } else {
347
+ header = header.replace(
348
+ /\*\//,
349
+ ` * @description ${mockInfo.description}
350
+ */`
351
+ );
2015
352
  }
2016
353
  }
354
+ }
355
+ const content = header ? `${header}
356
+ ` : "";
357
+ const finalContent = `${content}export default ${JSON.stringify(mockInfo.config, null, 2)}
358
+ `;
359
+ try {
360
+ const formatted = await formatWithPrettier(finalContent);
361
+ fs.writeFileSync(absolutePath, formatted, "utf-8");
362
+ } catch {
363
+ console.error("Error formatting code with prettier, writing raw content");
364
+ fs.writeFileSync(absolutePath, finalContent, "utf-8");
365
+ }
366
+ }
2017
367
 
2018
- bootstrap();
2019
- </script>
2020
-
2021
- <!-- Delete Confirmation Modal -->
2022
- <div id="delete-confirm-modal" class="modal-overlay">
2023
- <div class="modal" style="max-width: 400px;">
2024
- <h2><span class="btn-icon-cross" style="color: var(--accent-rose);">!</span> \u786E\u8BA4\u5220\u9664</h2>
2025
- <p id="delete-confirm-message" style="margin-bottom: 1rem; color: var(--text-secondary);"></p>
2026
- <label style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; cursor: pointer; user-select: none;">
2027
- <input type="checkbox" id="delete-never-ask" style="flex-shrink: 0;" />
2028
- <span>\u4E0D\u518D\u63D0\u9192</span>
2029
- </label>
2030
- <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
2031
- <button id="delete-cancel-btn" class="secondary">\u53D6\u6D88</button>
2032
- <button id="delete-confirm-btn" class="primary" style="background: var(--accent-rose);">\u5220\u9664</button>
2033
- </div>
2034
- </div>
2035
- </div>
2036
- </body>
2037
- </html>`;
2038
- res.setHeader("Content-Type", "text/html; charset=utf-8");
2039
- res.end(html);
368
+ // src/inspector.ts
369
+ import path3 from "path";
370
+ import fs2 from "fs-extra";
371
+ function buildMockItem(key, filePath, mockDir, info) {
372
+ return {
373
+ key,
374
+ file: path3.relative(mockDir, filePath),
375
+ method: key.split("/").pop()?.replace(/\.js$/, "") ?? "get",
376
+ path: key.replace(/\/[^/]+\.js$/, ""),
377
+ config: info.config,
378
+ editable: info.serializable,
379
+ description: info.description,
380
+ dataText: info.dataText
381
+ };
382
+ }
383
+ var DEFAULT_ROUTE = "/__mock/";
384
+ function ensureTrailingSlash(route) {
385
+ return route.endsWith("/") ? route : `${route}/`;
386
+ }
387
+ function escapeHtml(value) {
388
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
389
+ }
390
+ function normalizeInspectorConfig(input) {
391
+ if (input === false || input === void 0) {
392
+ return { route: DEFAULT_ROUTE, enableToggle: true };
393
+ }
394
+ if (input === true) {
395
+ return { route: DEFAULT_ROUTE, enableToggle: true };
396
+ }
397
+ return {
398
+ route: ensureTrailingSlash(input.route ?? DEFAULT_ROUTE),
399
+ enableToggle: input.enableToggle ?? true
400
+ };
401
+ }
402
+ function createInspectorHandler(options) {
403
+ if (!options.inspector) {
404
+ return null;
405
+ }
406
+ const inspectorConfig = normalizeInspectorConfig(options.inspector);
407
+ const inspectorRoute = ensureTrailingSlash(inspectorConfig.route);
408
+ return async (req, res) => {
409
+ if (!req.url) {
410
+ return false;
411
+ }
412
+ const url = new URL(req.url, "http://localhost");
413
+ if (!url.pathname.startsWith(inspectorRoute)) {
414
+ return false;
415
+ }
416
+ await handleInspectorRequest({
417
+ req,
418
+ res,
419
+ mockDir: options.mockDir,
420
+ inspectorRoute,
421
+ apiPrefix: options.apiPrefix,
422
+ inspectorConfig,
423
+ getMockFileMap: options.getMockFileMap
424
+ });
425
+ return true;
426
+ };
427
+ }
428
+ async function handleInspectorRequest(context) {
429
+ const { req, res, inspectorRoute } = context;
430
+ const url = new URL(req.url || inspectorRoute, "http://localhost");
431
+ const normalizedRoute = ensureTrailingSlash(inspectorRoute);
432
+ if (url.pathname === normalizedRoute.slice(0, -1) || url.pathname === normalizedRoute) {
433
+ await serveInspectorHtml(context);
434
+ return;
435
+ }
436
+ const relativePath = url.pathname.startsWith(normalizedRoute) ? url.pathname.slice(normalizedRoute.length) : null;
437
+ if (relativePath && relativePath.startsWith("api/")) {
438
+ await handleInspectorApi({ ...context, pathname: relativePath.slice(4) });
439
+ return;
440
+ }
441
+ res.statusCode = 404;
442
+ res.end("Not Found");
2040
443
  }
2041
- async function handleInspectorApi({
444
+ async function serveInspectorHtml({
2042
445
  res,
2043
- req,
2044
- pathname,
2045
- mockDir,
446
+ inspectorRoute,
2046
447
  apiPrefix,
2047
- inspectorConfig,
2048
- getMockFileMap
448
+ inspectorConfig
2049
449
  }) {
450
+ const routeJson = JSON.stringify(ensureTrailingSlash(inspectorRoute));
451
+ const allowToggleJson = JSON.stringify(inspectorConfig.enableToggle);
452
+ const apiPrefixEscaped = escapeHtml(apiPrefix);
453
+ const templatePath = path3.join(__dirname, "inspector-template.html");
454
+ const template = fs2.readFileSync(templatePath, "utf-8");
455
+ const html = template.replace("__API_PREFIX__", apiPrefixEscaped).replace("__ROUTE_JSON__", routeJson).replace("__ALLOW_TOGGLE_JSON__", allowToggleJson);
456
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
457
+ res.end(html);
458
+ }
459
+ async function handleList(ctx, mockFileMap) {
460
+ const { res, mockDir } = ctx;
461
+ const list = await Promise.all(
462
+ Array.from(mockFileMap.entries()).map(async ([key, filePath]) => {
463
+ const info = await parseMockModule(filePath);
464
+ return buildMockItem(key, filePath, mockDir, info);
465
+ })
466
+ );
467
+ sendJson(res, { mocks: list });
468
+ return true;
469
+ }
470
+ async function handleDetail(ctx, mockFileMap) {
471
+ const { req, res, mockDir } = ctx;
472
+ const url = new URL(req.url || "", "http://localhost");
473
+ const key = url.searchParams.get("key");
474
+ if (!key) {
475
+ sendJson(res, { error: "Missing key" }, 400);
476
+ return true;
477
+ }
478
+ const filePath = mockFileMap.get(key);
479
+ if (!filePath) {
480
+ sendJson(res, { error: "Mock not found" }, 404);
481
+ return true;
482
+ }
483
+ const info = await parseMockModule(filePath);
484
+ sendJson(res, { mock: buildMockItem(key, filePath, mockDir, info) });
485
+ return true;
486
+ }
487
+ async function handleUpdate(ctx, mockFileMap) {
488
+ const { req, res, mockDir, inspectorConfig } = ctx;
489
+ if (req.method !== "POST") {
490
+ sendJson(res, { error: "Method not allowed" }, 405);
491
+ return true;
492
+ }
493
+ const body = await readBody(req);
494
+ let payload;
2050
495
  try {
2051
- const mockFileMap = getMockFileMap();
2052
- if (pathname === "list") {
2053
- const list = await Promise.all(
2054
- Array.from(mockFileMap.entries()).map(async ([key, filePath]) => {
2055
- const info = await parseMockModule(filePath);
2056
- return {
2057
- key,
2058
- file: path.relative(mockDir, filePath),
2059
- method: key.split("/").pop()?.replace(/\.js$/, "") ?? "get",
2060
- path: key.replace(/\/[^/]+\.js$/, ""),
2061
- config: info.config,
2062
- editable: info.serializable,
2063
- description: info.description,
2064
- dataText: info.dataText
2065
- };
2066
- })
2067
- );
2068
- sendJson(res, { mocks: list });
2069
- return;
496
+ payload = JSON.parse(body);
497
+ } catch {
498
+ sendJson(res, { error: "Invalid JSON body" }, 400);
499
+ return true;
500
+ }
501
+ const { key, config, description } = payload || {};
502
+ if (!key) {
503
+ sendJson(res, { error: "Invalid payload: missing key" }, 400);
504
+ return true;
505
+ }
506
+ const filePath = mockFileMap.get(key);
507
+ if (!filePath) {
508
+ sendJson(res, { error: "Mock not found" }, 404);
509
+ return true;
510
+ }
511
+ const info = await parseMockModule(filePath);
512
+ let nextConfig = info.config;
513
+ if (config && typeof config === "object") {
514
+ if (!inspectorConfig.enableToggle && config.enable !== void 0) {
515
+ delete config.enable;
2070
516
  }
2071
- if (pathname === "detail") {
2072
- const url = new URL(req.url || "", "http://localhost");
2073
- const key = url.searchParams.get("key");
2074
- if (!key) {
2075
- sendJson(res, { error: "Missing key" }, 400);
2076
- return;
2077
- }
2078
- const filePath = mockFileMap.get(key);
2079
- if (!filePath) {
2080
- sendJson(res, { error: "Mock not found" }, 404);
2081
- return;
2082
- }
2083
- const info = await parseMockModule(filePath);
2084
- sendJson(res, {
2085
- mock: {
2086
- key,
2087
- file: path.relative(mockDir, filePath),
2088
- method: key.split("/").pop()?.replace(/\.js$/, "") ?? "get",
2089
- path: key.replace(/\/[^/]+\.js$/, ""),
2090
- config: info.config,
2091
- editable: info.serializable,
2092
- description: info.description,
2093
- dataText: info.dataText
517
+ nextConfig = { ...info.config, ...config };
518
+ }
519
+ const nextDescription = description !== void 0 ? description : info.description;
520
+ await writeMockFile(filePath, {
521
+ headerComment: info.headerComment,
522
+ config: nextConfig,
523
+ description: nextDescription
524
+ });
525
+ const updatedInfo = await parseMockModule(filePath);
526
+ sendJson(res, { mock: buildMockItem(key, filePath, mockDir, updatedInfo) });
527
+ return true;
528
+ }
529
+ async function handleCreate(ctx, mockFileMap) {
530
+ const { req, res, mockDir, apiPrefix } = ctx;
531
+ if (req.method !== "POST") {
532
+ sendJson(res, { error: "Method not allowed" }, 405);
533
+ return true;
534
+ }
535
+ const body = await readBody(req);
536
+ let payload;
537
+ try {
538
+ payload = JSON.parse(body);
539
+ } catch {
540
+ sendJson(res, { error: "Invalid JSON body" }, 400);
541
+ return true;
542
+ }
543
+ const { method, path: apiPath, data } = payload || {};
544
+ if (!method || !apiPath) {
545
+ sendJson(res, { error: "Invalid payload: missing method or path" }, 400);
546
+ return true;
547
+ }
548
+ const fullUrl = apiPrefix + apiPath;
549
+ try {
550
+ let dataToSave;
551
+ if (typeof data === "object" && data !== null) {
552
+ dataToSave = JSON.stringify(data, null, 2);
553
+ } else {
554
+ dataToSave = typeof data === "string" ? data : "";
555
+ }
556
+ await saveMockData(fullUrl, method.toLowerCase(), dataToSave, mockDir, 200);
557
+ const newMockFileMap = await buildMockIndex(mockDir);
558
+ for (const [key2, value] of newMockFileMap.entries()) {
559
+ mockFileMap.set(key2, value);
560
+ }
561
+ const normalizedPath = toPosixPath(apiPath);
562
+ const key = (normalizedPath.startsWith("/") ? "" : "/") + normalizedPath.toLowerCase() + "/" + method.toLowerCase() + ".js";
563
+ const filePath = mockFileMap.get(key);
564
+ if (!filePath) {
565
+ sendJson(res, { error: "Mock file created but not found in map" }, 500);
566
+ return true;
567
+ }
568
+ const info = await parseMockModule(filePath);
569
+ sendJson(res, {
570
+ success: true,
571
+ mock: buildMockItem(key, filePath, mockDir, info)
572
+ });
573
+ return true;
574
+ } catch (error) {
575
+ const errorMessage = error instanceof Error ? error.message : String(error);
576
+ sendJson(res, { error: "Failed to create mock: " + errorMessage }, 500);
577
+ return true;
578
+ }
579
+ }
580
+ async function handleDelete(ctx, mockFileMap) {
581
+ const { req, res } = ctx;
582
+ const url = new URL(req.url || "", "http://localhost");
583
+ const key = url.searchParams.get("key");
584
+ if (!key) {
585
+ sendJson(res, { error: "Missing key" }, 400);
586
+ return true;
587
+ }
588
+ const filePath = mockFileMap.get(key);
589
+ if (!filePath) {
590
+ sendJson(res, { error: "Mock not found" }, 404);
591
+ return true;
592
+ }
593
+ try {
594
+ const stats = await fs2.stat(filePath);
595
+ if (stats.isDirectory()) {
596
+ await fs2.remove(filePath);
597
+ for (const [mapKey, mapPath] of mockFileMap.entries()) {
598
+ if (mapPath.startsWith(filePath + path3.sep) || mapPath === filePath) {
599
+ mockFileMap.delete(mapKey);
2094
600
  }
2095
- });
2096
- return;
2097
- }
2098
- if (pathname === "update") {
2099
- if (req.method !== "POST") {
2100
- sendJson(res, { error: "Method not allowed" }, 405);
2101
- return;
2102
- }
2103
- const body = await readBody(req);
2104
- let payload;
2105
- try {
2106
- payload = JSON.parse(body);
2107
- } catch (error) {
2108
- sendJson(res, { error: "Invalid JSON body" }, 400);
2109
- return;
2110
- }
2111
- const { key, config, description } = payload || {};
2112
- if (!key) {
2113
- sendJson(res, { error: "Invalid payload: missing key" }, 400);
2114
- return;
2115
- }
2116
- const filePath = mockFileMap.get(key);
2117
- if (!filePath) {
2118
- sendJson(res, { error: "Mock not found" }, 404);
2119
- return;
2120
601
  }
602
+ } else {
603
+ await fs2.unlink(filePath);
604
+ mockFileMap.delete(key);
605
+ }
606
+ sendJson(res, { success: true });
607
+ } catch (error) {
608
+ const errorMessage = error instanceof Error ? error.message : String(error);
609
+ sendJson(res, { error: "Failed to delete: " + errorMessage }, 500);
610
+ }
611
+ return true;
612
+ }
613
+ async function handleBatchUpdate(ctx, mockFileMap) {
614
+ const { req, res } = ctx;
615
+ if (req.method !== "POST") {
616
+ sendJson(res, { error: "Method not allowed" }, 405);
617
+ return true;
618
+ }
619
+ const body = await readBody(req);
620
+ let payload;
621
+ try {
622
+ payload = JSON.parse(body);
623
+ } catch {
624
+ sendJson(res, { error: "Invalid JSON body" }, 400);
625
+ return true;
626
+ }
627
+ const updates = payload?.updates;
628
+ if (!Array.isArray(updates) || updates.length === 0) {
629
+ sendJson(res, { error: "Invalid payload: updates must be a non-empty array" }, 400);
630
+ return true;
631
+ }
632
+ let updated = 0;
633
+ const errors = [];
634
+ for (const item of updates) {
635
+ const { key, config } = item;
636
+ if (!key) continue;
637
+ const filePath = mockFileMap.get(key);
638
+ if (!filePath) {
639
+ errors.push(`Mock not found: ${key}`);
640
+ continue;
641
+ }
642
+ try {
2121
643
  const info = await parseMockModule(filePath);
2122
- let nextConfig = info.config;
2123
- if (config && typeof config === "object") {
2124
- if (!inspectorConfig.enableToggle && config.enable !== void 0) {
2125
- delete config.enable;
2126
- }
2127
- nextConfig = {
2128
- ...info.config,
2129
- ...config
2130
- };
2131
- }
2132
- const nextDescription = description !== void 0 ? description : info.description;
644
+ const nextConfig = config ? { ...info.config, ...config } : info.config;
2133
645
  await writeMockFile(filePath, {
2134
646
  headerComment: info.headerComment,
2135
647
  config: nextConfig,
2136
- description: nextDescription
2137
- });
2138
- const updatedInfo = await parseMockModule(filePath);
2139
- sendJson(res, {
2140
- mock: {
2141
- key,
2142
- file: path.relative(mockDir, filePath),
2143
- method: key.split("/").pop()?.replace(/\.js$/, "") ?? "get",
2144
- path: key.replace(/\/[^/]+\.js$/, ""),
2145
- config: updatedInfo.config,
2146
- editable: updatedInfo.serializable,
2147
- description: updatedInfo.description,
2148
- dataText: updatedInfo.dataText
2149
- }
648
+ description: info.description
2150
649
  });
650
+ updated++;
651
+ } catch (err) {
652
+ errors.push(`Failed to update ${key}: ${err instanceof Error ? err.message : String(err)}`);
653
+ }
654
+ }
655
+ sendJson(res, { success: true, updated, errors: errors.length > 0 ? errors : void 0 });
656
+ return true;
657
+ }
658
+ var API_ROUTES = {
659
+ list: handleList,
660
+ detail: handleDetail,
661
+ update: handleUpdate,
662
+ "batch-update": handleBatchUpdate,
663
+ create: handleCreate,
664
+ delete: handleDelete
665
+ };
666
+ async function handleInspectorApi(ctx) {
667
+ try {
668
+ const mockFileMap = ctx.getMockFileMap();
669
+ const handler = API_ROUTES[ctx.pathname];
670
+ if (handler) {
671
+ await handler(ctx, mockFileMap);
2151
672
  return;
2152
673
  }
2153
- if (pathname === "create") {
2154
- if (req.method !== "POST") {
2155
- sendJson(res, { error: "Method not allowed" }, 405);
2156
- return;
2157
- }
2158
- const body = await readBody(req);
2159
- let payload;
2160
- try {
2161
- payload = JSON.parse(body);
2162
- } catch (error) {
2163
- sendJson(res, { error: "Invalid JSON body" }, 400);
2164
- return;
2165
- }
2166
- const { method, path: apiPath, description, data } = payload || {};
2167
- if (!method || !apiPath) {
2168
- sendJson(
2169
- res,
2170
- { error: "Invalid payload: missing method or path" },
2171
- 400
674
+ sendJson(ctx.res, { error: "Unknown inspector endpoint" }, 404);
675
+ } catch (error) {
676
+ console.error("[automock] Inspector request failed:", error);
677
+ sendJson(ctx.res, { error: "Inspector failed" }, 500);
678
+ }
679
+ }
680
+
681
+ // src/middleware.ts
682
+ function serveMockResponse(res, mockFilePath, mockResult) {
683
+ const { data, delay = 0, status = 200 } = mockResult;
684
+ setTimeout(() => {
685
+ const isBinaryMock = data && typeof data === "object" && data.__binaryFile;
686
+ if (isBinaryMock) {
687
+ const binaryData = data;
688
+ const binaryFilePath = mockFilePath.replace(
689
+ /\.js$/,
690
+ "." + binaryData.__binaryFile
691
+ );
692
+ if (!fs3.existsSync(binaryFilePath)) {
693
+ console.error(
694
+ `[automock] Binary mock file not found: ${binaryFilePath}`
2172
695
  );
696
+ sendError(res, "Binary mock file not found", 404);
2173
697
  return;
2174
698
  }
2175
- const { saveMockData: saveMockData2 } = await import("./mockFileUtils-MO32XIKQ.mjs");
2176
- const fullUrl = apiPrefix + apiPath;
2177
699
  try {
2178
- let dataToSave;
2179
- if (typeof data === "object" && data !== null) {
2180
- dataToSave = JSON.stringify(data, null, 2);
2181
- } else {
2182
- dataToSave = typeof data === "string" ? data : "";
2183
- }
2184
- await saveMockData2(
2185
- fullUrl,
2186
- method.toLowerCase(),
2187
- dataToSave,
2188
- mockDir,
2189
- 200
2190
- );
2191
- const { buildMockIndex: buildMockIndex2 } = await import("./mockFileUtils-MO32XIKQ.mjs");
2192
- const newMockFileMap = await buildMockIndex2(mockDir);
2193
- for (const [key2, value] of newMockFileMap.entries()) {
2194
- mockFileMap.set(key2, value);
2195
- }
2196
- const normalizedPath = toPosixPath(apiPath);
2197
- const key = (normalizedPath.startsWith("/") ? "" : "/") + normalizedPath.toLowerCase() + "/" + method.toLowerCase() + ".js";
2198
- const filePath = mockFileMap.get(key);
2199
- if (!filePath) {
2200
- sendJson(
2201
- res,
2202
- { error: "Mock file created but not found in map" },
2203
- 500
2204
- );
2205
- return;
2206
- }
2207
- const info = await parseMockModule(filePath);
2208
- sendJson(res, {
2209
- success: true,
2210
- mock: {
2211
- key,
2212
- file: path.relative(mockDir, filePath),
2213
- method: method.toLowerCase(),
2214
- path: apiPath,
2215
- config: info.config,
2216
- editable: info.serializable,
2217
- description: info.description,
2218
- dataText: info.dataText
2219
- }
2220
- });
2221
- return;
700
+ const fileData = fs3.readFileSync(binaryFilePath);
701
+ const contentType = binaryData.__contentType || "application/octet-stream";
702
+ res.setHeader("Content-Type", contentType);
703
+ res.setHeader("Content-Length", fileData.length);
704
+ res.setHeader("X-Mock-Response", "true");
705
+ res.setHeader("X-Mock-Source", "vite-plugin-automock");
706
+ res.setHeader("X-Mock-Binary-File", "true");
707
+ res.statusCode = status;
708
+ res.end(fileData);
2222
709
  } catch (error) {
2223
- const errorMessage = error instanceof Error ? error.message : String(error);
2224
- sendJson(res, { error: "Failed to create mock: " + errorMessage }, 500);
2225
- return;
2226
- }
2227
- }
2228
- if (pathname === "delete") {
2229
- const url = new URL(req.url || "", "http://localhost");
2230
- const key = url.searchParams.get("key");
2231
- if (!key) {
2232
- sendJson(res, { error: "Missing key" }, 400);
2233
- return;
710
+ console.error("[automock] Failed to read binary mock file:", error);
711
+ sendError(res, "Failed to read binary mock file");
2234
712
  }
2235
- const filePath = mockFileMap.get(key);
2236
- if (!filePath) {
2237
- sendJson(res, { error: "Mock not found" }, 404);
713
+ } else {
714
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
715
+ res.setHeader("X-Mock-Response", "true");
716
+ res.setHeader("X-Mock-Source", "vite-plugin-automock");
717
+ res.statusCode = status;
718
+ res.end(typeof data === "string" ? data : JSON.stringify(data));
719
+ }
720
+ }, delay);
721
+ }
722
+ function proxyAndCapture(req, res, options) {
723
+ const { proxyBaseUrl, pathRewrite, shouldSave, method, pathname, mockDir, onMockSaved } = options;
724
+ const targetUrl = proxyBaseUrl + pathRewrite(req.url || "");
725
+ const client = targetUrl.startsWith("https") ? https : http;
726
+ function sendProxyRequest(body) {
727
+ const targetUrlObj = new URL(proxyBaseUrl);
728
+ const proxyOptions = {
729
+ method: req.method,
730
+ headers: {
731
+ ...req.headers,
732
+ host: targetUrlObj.host
733
+ },
734
+ rejectUnauthorized: false
735
+ };
736
+ const proxyReq = client.request(targetUrl, proxyOptions, (proxyRes) => {
737
+ const contentType = proxyRes.headers["content-type"] || "";
738
+ const isJson = contentType.includes("application/json");
739
+ if (!isJson) {
740
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
741
+ proxyRes.pipe(res);
2238
742
  return;
2239
743
  }
2240
- const { unlink, rm } = await import("fs/promises");
2241
- const { stat } = await import("fs/promises");
2242
- try {
2243
- const stats = await stat(filePath);
2244
- if (stats.isDirectory()) {
2245
- await rm(filePath, { recursive: true, force: true });
2246
- for (const [mapKey, mapPath] of mockFileMap.entries()) {
2247
- if (mapPath.startsWith(filePath + path.sep) || mapPath === filePath) {
2248
- mockFileMap.delete(mapKey);
744
+ const chunks = [];
745
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
746
+ proxyRes.on("end", async () => {
747
+ try {
748
+ const responseData = Buffer.concat(chunks);
749
+ if (shouldSave) {
750
+ try {
751
+ console.log(
752
+ `[automock] Capturing mock: ${req.url} -> ${pathname}`
753
+ );
754
+ const savedFilePath = await saveMockData(
755
+ req.url,
756
+ method,
757
+ responseData,
758
+ mockDir,
759
+ proxyRes.statusCode,
760
+ contentType
761
+ );
762
+ if (savedFilePath) {
763
+ console.log(`[automock] Mock saved: ${pathname}`);
764
+ onMockSaved();
765
+ } else {
766
+ console.log(
767
+ `[automock] Mock file already exists, skipped: ${pathname}`
768
+ );
769
+ }
770
+ } catch (saveError) {
771
+ console.error("[automock] Failed to save mock:", saveError);
2249
772
  }
2250
773
  }
2251
- } else {
2252
- await unlink(filePath);
2253
- mockFileMap.delete(key);
774
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
775
+ res.end(responseData);
776
+ } catch (error) {
777
+ console.error("[automock] Failed to process response:", error);
778
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
779
+ res.end(Buffer.concat(chunks));
2254
780
  }
2255
- sendJson(res, { success: true });
2256
- } catch (error) {
2257
- const errorMessage = error instanceof Error ? error.message : String(error);
2258
- sendJson(res, { error: "Failed to delete: " + errorMessage }, 500);
2259
- }
2260
- return;
781
+ });
782
+ });
783
+ proxyReq.on("error", (err) => {
784
+ console.error("[automock] Proxy request failed:", err);
785
+ sendError(res, err.message);
786
+ });
787
+ if (body && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH")) {
788
+ proxyReq.write(body);
2261
789
  }
2262
- sendJson(res, { error: "Unknown inspector endpoint" }, 404);
2263
- } catch (error) {
2264
- console.error("\u274C [automock] Inspector request failed:", error);
2265
- sendJson(res, { error: "Inspector failed" }, 500);
790
+ proxyReq.end();
791
+ }
792
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
793
+ let bodyStr = "";
794
+ req.on("data", (chunk) => bodyStr += chunk.toString());
795
+ req.on("end", () => sendProxyRequest(bodyStr));
796
+ } else if (req.readableEnded) {
797
+ sendProxyRequest();
798
+ } else {
799
+ req.on("end", () => sendProxyRequest());
2266
800
  }
2267
801
  }
2268
- function sendJson(res, payload, status = 200) {
2269
- res.statusCode = status;
2270
- res.setHeader("Content-Type", "application/json; charset=utf-8");
2271
- res.end(JSON.stringify(payload));
2272
- }
2273
- function readBody(req) {
2274
- return new Promise((resolve, reject) => {
2275
- const chunks = [];
2276
- req.on("data", (chunk) => chunks.push(chunk));
2277
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
2278
- req.on("error", reject);
802
+ function printServerUrls(server, urls) {
803
+ server.httpServer?.once("listening", () => {
804
+ const address = server.httpServer?.address();
805
+ if (!address || typeof address !== "object") return;
806
+ const { protocol, host, port } = getServerAddress(
807
+ address,
808
+ !!server.config.server.https
809
+ );
810
+ for (const { path: urlPath, label, color, delay } of urls) {
811
+ setTimeout(() => {
812
+ const fullUrl = `${protocol}://${host}:${port}${urlPath}`;
813
+ console.log(` \u279C ${color}${label}\x1B[0m: \x1B[1m${fullUrl}\x1B[0m`);
814
+ }, delay);
815
+ }
2279
816
  });
2280
817
  }
2281
-
2282
- // src/middleware.ts
2283
818
  function automock(options) {
2284
819
  const {
2285
- mockDir: configMockDir = path2.join(process.cwd(), "mock"),
820
+ mockDir: configMockDir = path4.join(process.cwd(), "mock"),
2286
821
  apiPrefix = "/api",
2287
822
  pathRewrite = (p) => p,
2288
823
  proxyBaseUrl,
2289
824
  inspector = false
2290
825
  } = options;
2291
- const mockDir = path2.isAbsolute(configMockDir) ? configMockDir : path2.resolve(process.cwd(), configMockDir);
2292
- if (!fs.existsSync(mockDir)) {
826
+ const mockDir = resolveAbsolutePath(configMockDir);
827
+ if (!fs3.existsSync(mockDir)) {
2293
828
  try {
2294
- fs.ensureDirSync(mockDir);
2295
- console.log(`\u2705 [automock] Mock directory created: ${mockDir}`);
829
+ fs3.ensureDirSync(mockDir);
830
+ console.log(`[automock] Mock directory created: ${mockDir}`);
2296
831
  } catch (error) {
2297
- console.error(`\u274C [automock] \u521B\u5EFA Mock \u76EE\u5F55\u5931\u8D25: ${mockDir}`, error);
832
+ console.error(`[automock] Failed to create mock directory: ${mockDir}`, error);
2298
833
  }
2299
834
  }
2300
835
  let watcher;
2301
836
  return {
2302
837
  name: "vite-plugin-automock",
2303
- config() {
838
+ config(userConfig) {
839
+ if (proxyBaseUrl && userConfig.server?.proxy) {
840
+ const proxyConfig = userConfig.server.proxy;
841
+ const hasConflictingPrefix = typeof proxyConfig === "object" && apiPrefix in proxyConfig;
842
+ if (hasConflictingPrefix) {
843
+ console.warn(
844
+ `
845
+ \u26A0\uFE0F [automock] WARNING: You have both Vite's server.proxy["${apiPrefix}"] and automock's proxyBaseUrl configured.`
846
+ );
847
+ console.warn(
848
+ ` This may cause request conflicts. Remove the "${apiPrefix}" entry from vite.config.ts server.proxy to avoid issues.`
849
+ );
850
+ console.warn(
851
+ ` Automock will handle all ${apiPrefix} requests when proxyBaseUrl is set.
852
+ `
853
+ );
854
+ }
855
+ }
2304
856
  },
2305
- configureServer(server) {
2306
- let mockFileMap = buildMockIndex(mockDir);
2307
- console.log(`\u2705 [automock] Loaded ${mockFileMap.size} mock files`);
2308
- const rebuildMockFileMap = debounce(() => {
2309
- mockFileMap = buildMockIndex(mockDir);
857
+ async configureServer(server) {
858
+ let mockFileMap = await buildMockIndex(mockDir);
859
+ console.log(`[automock] Loaded ${mockFileMap.size} mock files`);
860
+ const rebuildMockFileMap = debounce(async () => {
861
+ mockFileMap = await buildMockIndex(mockDir);
2310
862
  }, 200);
2311
863
  watcher = chokidar.watch(mockDir, {
2312
864
  ignoreInitial: true,
@@ -2315,247 +867,78 @@ function automock(options) {
2315
867
  });
2316
868
  watcher.on("add", () => rebuildMockFileMap());
2317
869
  watcher.on("unlink", () => rebuildMockFileMap());
2318
- server.httpServer?.on("close", () => {
2319
- watcher?.close();
2320
- });
870
+ server.httpServer?.on("close", () => watcher?.close());
2321
871
  const inspectorHandler = createInspectorHandler({
2322
872
  inspector,
2323
873
  apiPrefix,
2324
874
  mockDir,
2325
875
  getMockFileMap: () => mockFileMap
2326
876
  });
2327
- server.httpServer?.once("listening", () => {
2328
- setTimeout(() => {
2329
- const address = server.httpServer?.address();
2330
- if (address && typeof address === "object") {
2331
- const protocol = server.config.server.https ? "https" : "http";
2332
- const host = address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address;
2333
- const port = address.port;
2334
- const mockApiUrl = `${protocol}://${host}:${port}${apiPrefix}`;
2335
- console.log(
2336
- ` \u279C \x1B[32mMock API\x1B[0m: \x1B[1m${mockApiUrl}\x1B[0m`
2337
- );
2338
- }
2339
- }, 100);
2340
- });
877
+ const urlsToPrint = [
878
+ { path: apiPrefix, label: "Mock API", color: "\x1B[32m", delay: 100 }
879
+ ];
2341
880
  if (inspector) {
2342
881
  const inspectorConfig = normalizeInspectorConfig(inspector);
2343
- server.httpServer?.once("listening", () => {
2344
- setTimeout(() => {
2345
- const address = server.httpServer?.address();
2346
- if (address && typeof address === "object") {
2347
- const protocol = server.config.server.https ? "https" : "http";
2348
- const host = address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address;
2349
- const port = address.port;
2350
- const inspectorUrl = `${protocol}://${host}:${port}${inspectorConfig.route}`;
2351
- console.log(
2352
- ` \u279C \x1B[95mMock Inspector\x1B[0m: \x1B[1m\x1B[96m${inspectorUrl}\x1B[0m`
2353
- );
2354
- }
2355
- }, 150);
882
+ urlsToPrint.push({
883
+ path: inspectorConfig.route,
884
+ label: "Mock Inspector",
885
+ color: "\x1B[95m",
886
+ delay: 150
2356
887
  });
2357
888
  }
889
+ printServerUrls(server, urlsToPrint);
890
+ async function tryLoadMock(key) {
891
+ const mockFilePath = mockFileMap.get(key);
892
+ if (!mockFilePath) return null;
893
+ const absolutePath = resolveAbsolutePath(mockFilePath);
894
+ if (!fs3.existsSync(absolutePath)) return null;
895
+ const { default: mockModule } = await import(`${absolutePath}?t=${Date.now()}`);
896
+ const mockResult = typeof mockModule.data === "function" ? mockModule.data() : mockModule;
897
+ const { enable = true } = mockResult || {};
898
+ return enable ? { absolutePath, mockResult } : null;
899
+ }
2358
900
  server.middlewares.use(
2359
901
  async (req, res, next) => {
2360
902
  if (inspectorHandler && req.url) {
2361
903
  const handled = await inspectorHandler(req, res);
2362
- if (handled) {
2363
- return;
2364
- }
904
+ if (handled) return;
2365
905
  }
2366
906
  if (!req.url?.startsWith(apiPrefix)) {
2367
907
  return next();
2368
908
  }
909
+ const accept = req.headers.accept || "";
910
+ const upgrade = req.headers.upgrade || "";
911
+ if (accept.includes("text/event-stream") || upgrade.toLowerCase() === "websocket") {
912
+ return next();
913
+ }
2369
914
  const method = (req.method || "GET").toLowerCase();
2370
- const urlObj = new URL(req.url, "http://localhost");
2371
- const pathname = urlObj.pathname;
915
+ const pathname = new URL(req.url, "http://localhost").pathname;
2372
916
  const key = `${pathname}/${method}.js`.toLowerCase();
2373
- const isExist = mockFileMap.has(key);
2374
- if (isExist) {
2375
- try {
2376
- const mockFilePath = mockFileMap.get(key);
2377
- const absolutePath = path2.isAbsolute(mockFilePath) ? mockFilePath : path2.resolve(process.cwd(), mockFilePath);
2378
- if (!fs.existsSync(absolutePath)) {
2379
- throw new Error(`Mock file does not exist: ${absolutePath}`);
2380
- }
2381
- const { default: mockModule } = await import(`${absolutePath}?t=${Date.now()}`);
2382
- const mockResult = typeof mockModule.data === "function" ? mockModule.data() : mockModule;
2383
- const {
2384
- enable = true,
2385
- data,
2386
- delay = 0,
2387
- status = 200
2388
- } = mockResult || {};
2389
- if (enable) {
2390
- setTimeout(() => {
2391
- const isBinaryMock = data?.__binaryFile;
2392
- if (isBinaryMock) {
2393
- const binaryFilePath = absolutePath.replace(
2394
- /\.js$/,
2395
- "." + data.__binaryFile
2396
- );
2397
- if (fs.existsSync(binaryFilePath)) {
2398
- try {
2399
- const binaryData = fs.readFileSync(binaryFilePath);
2400
- const contentType = data.__contentType || "application/octet-stream";
2401
- res.setHeader("Content-Type", contentType);
2402
- res.setHeader("Content-Length", binaryData.length);
2403
- res.setHeader("X-Mock-Response", "true");
2404
- res.setHeader("X-Mock-Source", "vite-plugin-automock");
2405
- res.setHeader("X-Mock-Binary-File", "true");
2406
- res.statusCode = status;
2407
- res.end(binaryData);
2408
- } catch (error) {
2409
- console.error(
2410
- "\u274C [automock] \u8BFB\u53D6\u4E8C\u8FDB\u5236mock\u6587\u4EF6\u5931\u8D25:",
2411
- error
2412
- );
2413
- res.statusCode = 500;
2414
- res.end(
2415
- JSON.stringify({
2416
- error: "Failed to read binary mock file"
2417
- })
2418
- );
2419
- }
2420
- } else {
2421
- console.error(
2422
- "\u274C [automock] \u4E8C\u8FDB\u5236mock\u6587\u4EF6\u4E0D\u5B58\u5728:",
2423
- binaryFilePath
2424
- );
2425
- res.statusCode = 404;
2426
- res.end(
2427
- JSON.stringify({ error: "Binary mock file not found" })
2428
- );
2429
- }
2430
- } else {
2431
- res.setHeader(
2432
- "Content-Type",
2433
- "application/json; charset=utf-8"
2434
- );
2435
- res.setHeader("X-Mock-Response", "true");
2436
- res.setHeader("X-Mock-Source", "vite-plugin-automock");
2437
- res.statusCode = status;
2438
- res.end(
2439
- typeof data === "string" ? data : JSON.stringify(data)
2440
- );
2441
- }
2442
- }, delay);
2443
- return;
2444
- }
2445
- } catch (error) {
2446
- console.error(`\u274C [automock] \u52A0\u8F7D mock \u6587\u4EF6\u5931\u8D25:`, error);
917
+ try {
918
+ const mock = await tryLoadMock(key);
919
+ if (mock) {
920
+ serveMockResponse(res, mock.absolutePath, mock.mockResult);
921
+ return;
2447
922
  }
923
+ } catch (error) {
924
+ console.error("[automock] Failed to load mock file:", error);
2448
925
  }
2449
- const shouldSaveMockData = !isExist;
2450
- if (!proxyBaseUrl) {
2451
- res.statusCode = 404;
2452
- res.end(
2453
- JSON.stringify({
2454
- error: "No mock found and proxyBaseUrl not configured"
2455
- })
2456
- );
2457
- return;
2458
- }
926
+ if (!proxyBaseUrl) return next();
2459
927
  try {
2460
- let sendProxyRequest2 = function(body) {
2461
- const targetUrlObj = new URL(proxyBaseUrl);
2462
- const proxyOptions = {
2463
- method: req.method,
2464
- headers: {
2465
- ...req.headers,
2466
- host: targetUrlObj.host
2467
- // Override Host header with target server's host
2468
- },
2469
- rejectUnauthorized: false
2470
- };
2471
- const proxyReq = client.request(
2472
- targetUrl,
2473
- proxyOptions,
2474
- (proxyRes) => {
2475
- const contentType = proxyRes.headers["content-type"];
2476
- const chunks = [];
2477
- proxyRes.on("data", (chunk) => {
2478
- chunks.push(chunk);
2479
- });
2480
- proxyRes.on("end", async () => {
2481
- try {
2482
- const responseData = Buffer.concat(chunks);
2483
- if (shouldSaveMockData) {
2484
- try {
2485
- console.log(
2486
- `\u{1F504} [automock] \u5C1D\u8BD5\u4FDD\u5B58 mock: ${req.url} -> ${pathname}`
2487
- );
2488
- const savedFilePath = await saveMockData(
2489
- req.url,
2490
- method,
2491
- responseData,
2492
- mockDir,
2493
- proxyRes.statusCode,
2494
- contentType
2495
- );
2496
- if (savedFilePath) {
2497
- console.log(
2498
- `\u2705 [automock] \u5DF2\u4FDD\u5B58 mock: ${pathname}`
2499
- );
2500
- mockFileMap = buildMockIndex(mockDir);
2501
- } else {
2502
- console.log(
2503
- `\u2139\uFE0F [automock] mock \u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7: ${pathname}`
2504
- );
2505
- }
2506
- } catch (saveError) {
2507
- console.error(
2508
- "\u274C [automock] \u4FDD\u5B58 mock \u5931\u8D25:",
2509
- saveError
2510
- );
2511
- }
2512
- }
2513
- res.writeHead(
2514
- proxyRes.statusCode || 200,
2515
- proxyRes.headers
2516
- );
2517
- res.end(responseData);
2518
- } catch (error) {
2519
- console.error("\u274C [automock] \u5904\u7406\u54CD\u5E94\u5931\u8D25:", error);
2520
- res.writeHead(
2521
- proxyRes.statusCode || 200,
2522
- proxyRes.headers
2523
- );
2524
- res.end(Buffer.concat(chunks));
2525
- }
2526
- });
2527
- }
2528
- );
2529
- proxyReq.on("error", (err) => {
2530
- console.error("\u274C [automock] \u4EE3\u7406\u8BF7\u6C42\u5931\u8D25:", err);
2531
- res.statusCode = 500;
2532
- res.end(JSON.stringify({ error: err.message }));
2533
- });
2534
- if (body && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH")) {
2535
- proxyReq.write(body);
928
+ proxyAndCapture(req, res, {
929
+ proxyBaseUrl,
930
+ pathRewrite,
931
+ shouldSave: !mockFileMap.has(key),
932
+ method,
933
+ pathname,
934
+ mockDir,
935
+ onMockSaved: async () => {
936
+ mockFileMap = await buildMockIndex(mockDir);
2536
937
  }
2537
- proxyReq.end();
2538
- };
2539
- var sendProxyRequest = sendProxyRequest2;
2540
- const targetUrl = proxyBaseUrl + pathRewrite(req.url || "");
2541
- const client = targetUrl.startsWith("https") ? https : http;
2542
- if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
2543
- let bodyStr = "";
2544
- req.on("data", (chunk) => {
2545
- bodyStr += chunk.toString();
2546
- });
2547
- req.on("end", () => {
2548
- sendProxyRequest2(bodyStr);
2549
- });
2550
- } else {
2551
- req.on("end", () => {
2552
- sendProxyRequest2();
2553
- });
2554
- }
938
+ });
2555
939
  } catch (error) {
2556
- console.error("\u274C [automock] \u4EE3\u7406\u8BF7\u6C42\u5F02\u5E38:", error);
2557
- res.statusCode = 500;
2558
- res.end(JSON.stringify({ error: "Internal server error" }));
940
+ console.error("[automock] Proxy request error:", error);
941
+ sendError(res, "Internal server error");
2559
942
  }
2560
943
  }
2561
944
  );
@@ -2570,8 +953,8 @@ function automock(options) {
2570
953
  }
2571
954
 
2572
955
  // src/mockBundler.ts
2573
- import path3 from "path";
2574
- import fs2 from "fs-extra";
956
+ import path5 from "path";
957
+ import fs4 from "fs-extra";
2575
958
  var DEFAULT_BUNDLE_OPTIONS = {
2576
959
  includeDisabled: true,
2577
960
  log: true
@@ -2579,13 +962,13 @@ var DEFAULT_BUNDLE_OPTIONS = {
2579
962
  async function bundleMockFiles(options) {
2580
963
  const opts = { ...DEFAULT_BUNDLE_OPTIONS, ...options };
2581
964
  const { mockDir, log } = opts;
2582
- if (!fs2.existsSync(mockDir)) {
965
+ if (!fs4.existsSync(mockDir)) {
2583
966
  if (log) {
2584
967
  console.log(`[mock-bundler] Mock directory does not exist: ${mockDir}`);
2585
968
  }
2586
969
  return {};
2587
970
  }
2588
- const mockFileMap = buildMockIndex(mockDir);
971
+ const mockFileMap = await buildMockIndex(mockDir);
2589
972
  const bundle = {};
2590
973
  let bundledCount = 0;
2591
974
  let skippedCount = 0;
@@ -2619,11 +1002,11 @@ async function bundleMockFiles(options) {
2619
1002
  return bundle;
2620
1003
  }
2621
1004
  function writeMockBundle(bundle, outputPath) {
2622
- const outputDir = path3.dirname(outputPath);
2623
- if (!fs2.existsSync(outputDir)) {
2624
- fs2.ensureDirSync(outputDir);
1005
+ const outputDir = path5.dirname(outputPath);
1006
+ if (!fs4.existsSync(outputDir)) {
1007
+ fs4.ensureDirSync(outputDir);
2625
1008
  }
2626
- fs2.writeFileSync(
1009
+ fs4.writeFileSync(
2627
1010
  outputPath,
2628
1011
  JSON.stringify(bundle, null, 2),
2629
1012
  "utf-8"
@@ -2639,25 +1022,40 @@ function automock2(options = {}) {
2639
1022
  } = options;
2640
1023
  const basePlugin = automock(pluginOptions);
2641
1024
  let cachedBundle = null;
1025
+ const ensureBundleExists = () => {
1026
+ const outputPath = path6.join(process.cwd(), bundleOutputPath);
1027
+ if (!fs5.existsSync(outputPath) && cachedBundle) {
1028
+ console.log("[automock] Re-writing mock bundle...");
1029
+ writeMockBundle(cachedBundle, outputPath);
1030
+ }
1031
+ };
2642
1032
  return {
2643
1033
  ...basePlugin,
2644
1034
  name: "vite-plugin-automock-with-bundle",
1035
+ config() {
1036
+ const enabled = pluginOptions.productionMock !== false;
1037
+ return {
1038
+ define: {
1039
+ __AUTOMOCK_ENABLED__: JSON.stringify(enabled)
1040
+ }
1041
+ };
1042
+ },
2645
1043
  buildEnd: async () => {
2646
1044
  if (bundleMockData && pluginOptions.productionMock !== false) {
2647
1045
  try {
2648
- const mockDir = pluginOptions.mockDir || path4.join(process.cwd(), "mock");
2649
- if (!fs3.existsSync(mockDir)) {
1046
+ const mockDir = pluginOptions.mockDir || path6.join(process.cwd(), "mock");
1047
+ if (!fs5.existsSync(mockDir)) {
2650
1048
  console.log("[automock] Mock directory not found, skipping bundle generation");
2651
1049
  console.log("[automock] Using existing mock-data.json if present");
2652
1050
  return;
2653
1051
  }
2654
- const mockFileMap = buildMockIndex(mockDir);
1052
+ const mockFileMap = await buildMockIndex(mockDir);
2655
1053
  if (mockFileMap.size === 0) {
2656
1054
  console.log("[automock] No mock files found, skipping bundle generation");
2657
1055
  return;
2658
1056
  }
2659
1057
  cachedBundle = await bundleMockFiles({ mockDir });
2660
- const outputPath = path4.join(process.cwd(), bundleOutputPath);
1058
+ const outputPath = path6.join(process.cwd(), bundleOutputPath);
2661
1059
  writeMockBundle(cachedBundle, outputPath);
2662
1060
  console.log(`[automock] Mock bundle written to: ${outputPath}`);
2663
1061
  } catch (error) {
@@ -2665,20 +1063,8 @@ function automock2(options = {}) {
2665
1063
  }
2666
1064
  }
2667
1065
  },
2668
- writeBundle: async () => {
2669
- const outputPath = path4.join(process.cwd(), bundleOutputPath);
2670
- if (!fs3.existsSync(outputPath) && cachedBundle) {
2671
- console.log("[automock] Re-writing mock bundle in writeBundle...");
2672
- writeMockBundle(cachedBundle, outputPath);
2673
- }
2674
- },
2675
- closeBundle: async () => {
2676
- const outputPath = path4.join(process.cwd(), bundleOutputPath);
2677
- if (!fs3.existsSync(outputPath) && cachedBundle) {
2678
- console.log("[automock] Re-writing mock bundle in closeBundle...");
2679
- writeMockBundle(cachedBundle, outputPath);
2680
- }
2681
- }
1066
+ writeBundle: ensureBundleExists,
1067
+ closeBundle: ensureBundleExists
2682
1068
  };
2683
1069
  }
2684
1070
  export {