vite-plugin-automock 1.0.0

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