vite-plugin-automock 1.0.2 → 1.1.1

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