rspack-plugin-mock 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,24 @@
1
- import { attemptAsync, isArray, isBoolean, isEmptyObject, isFunction, isPlainObject, isString, objectKeys, partition, random, sleep, sortBy, timestamp, toArray, uniq } from "@pengzhanbo/utils";
2
1
  import path from "node:path";
2
+ import { attempt, attemptAsync, deepEqual, hasOwn, isArray, isBoolean, isEmptyObject, isFunction, isPlainObject, isString, kebabCase, objectKeys, omit, partition, random, sleep, sortBy, timestamp, toArray, uniq } from "@pengzhanbo/utils";
3
+ import fs, { promises } from "node:fs";
3
4
  import ansis from "ansis";
4
5
  import picomatch from "picomatch";
5
6
  import { loadPackageJSONSync } from "local-pkg";
6
7
  import { match, parse, pathToRegexp } from "path-to-regexp";
7
8
  import os from "node:os";
8
9
  import { fileURLToPath } from "node:url";
9
- import Debug from "debug";
10
10
  import { Volume, createFsFromVolume } from "memfs";
11
11
  import { parse as parse$1 } from "node:querystring";
12
- import crypto from "node:crypto";
13
12
  import cors from "cors";
14
13
  import bodyParser from "co-body";
15
14
  import formidable from "formidable";
16
- import http from "node:http";
15
+ import Cookies from "cookies";
17
16
  import { Buffer } from "node:buffer";
17
+ import zlib from "node:zlib";
18
18
  import HTTP_STATUS from "http-status";
19
19
  import * as mime from "mime-types";
20
20
  import { WebSocketServer } from "ws";
21
- //#region src/utils/createMatcher.ts
21
+
22
22
  function createMatcher(include, exclude, defaultIgnore = true) {
23
23
  const pattern = [];
24
24
  const ignore = [...defaultIgnore ? ["**/node_modules/**"] : [], ...toArray(exclude)];
@@ -32,12 +32,12 @@ function createMatcher(include, exclude, defaultIgnore = true) {
32
32
  isMatch: picomatch(pattern, { ignore })
33
33
  };
34
34
  }
35
- //#endregion
36
- //#region src/utils/doesProxyContextMatchUrl.ts
37
- const PATTERN_CACHE = /* @__PURE__ */ new Map();
35
+
36
+
37
+ const PATTERN_CACHE = new Map();
38
38
  function doesProxyContextMatchUrl(context, req) {
39
39
  const url = req.url;
40
- if (typeof context === "function") return context(url, req);
40
+ if (typeof context === "function") return !!context(url, req);
41
41
  if (context[0] === "^") {
42
42
  let pattern = PATTERN_CACHE.get(context);
43
43
  if (!pattern) PATTERN_CACHE.set(context, pattern = new RegExp(context));
@@ -45,8 +45,8 @@ function doesProxyContextMatchUrl(context, req) {
45
45
  }
46
46
  return url.startsWith(context);
47
47
  }
48
- //#endregion
49
- //#region src/utils/getDeps.ts
48
+
49
+
50
50
  function getPackageDeps(cwd) {
51
51
  const { dependencies, devDependencies, peerDependencies, optionalDependencies } = loadPackageJSONSync(cwd) || {};
52
52
  return {
@@ -57,25 +57,28 @@ function getPackageDeps(cwd) {
57
57
  };
58
58
  }
59
59
  function getPackageDepList(cwd) {
60
- return uniq(objectKeys(getPackageDeps(cwd)));
60
+ const deps = getPackageDeps(cwd);
61
+ return uniq(objectKeys(deps));
61
62
  }
62
- //#endregion
63
- //#region src/utils/is.ts
63
+
64
+
64
65
  function isStream(stream) {
65
66
  return stream !== null && typeof stream === "object" && typeof stream.pipe === "function";
66
67
  }
67
68
  function isReadableStream(stream) {
68
69
  return isStream(stream) && stream.readable !== false && typeof stream._read === "function" && typeof stream._readableState === "object";
69
70
  }
70
- //#endregion
71
- //#region src/utils/isObjectSubset.ts
72
- /**
73
- * Checks if target object is a subset of source object.
74
- * That is, all properties and their corresponding values in target exist in source.
75
- *
76
- * 深度比较两个对象之间,target 是否属于 source 的子集,
77
- * 即 target 的所有属性和对应的值,都在 source 中,
78
- */
71
+
72
+ function isTextContent(contentType) {
73
+ return [
74
+ "text",
75
+ "json",
76
+ "xml"
77
+ ].some((type) => contentType.includes(type));
78
+ }
79
+
80
+
81
+
79
82
  function isObjectSubset(source, target) {
80
83
  if (!target) return true;
81
84
  for (const key in target) if (!isIncluded(source[key], target[key])) return false;
@@ -83,7 +86,7 @@ function isObjectSubset(source, target) {
83
86
  }
84
87
  function isIncluded(source, target) {
85
88
  if (isArray(source) && isArray(target)) {
86
- const seen = /* @__PURE__ */ new Set();
89
+ const seen = new Set();
87
90
  return target.every((ti) => source.some((si, i) => {
88
91
  if (seen.has(i)) return false;
89
92
  const included = isIncluded(si, ti);
@@ -94,12 +97,10 @@ function isIncluded(source, target) {
94
97
  if (isPlainObject(source) && isPlainObject(target)) return isObjectSubset(source, target);
95
98
  return Object.is(source, target);
96
99
  }
97
- //#endregion
98
- //#region src/utils/isPathMatch.ts
99
- const cache = /* @__PURE__ */ new Map();
100
- /**
101
- * 判断 path 是否匹配 pattern
102
- */
100
+
101
+
102
+ const cache = new Map();
103
+
103
104
  function isPathMatch(pattern, path) {
104
105
  let regexp = cache.get(pattern);
105
106
  if (!regexp) {
@@ -108,47 +109,20 @@ function isPathMatch(pattern, path) {
108
109
  }
109
110
  return regexp.test(path);
110
111
  }
111
- //#endregion
112
- //#region src/utils/logger.ts
113
- const logLevels = {
114
- silent: 0,
115
- error: 1,
116
- warn: 2,
117
- info: 3,
118
- debug: 4
119
- };
120
- function createLogger(prefix, defaultLevel = "info") {
121
- prefix = `[${prefix}]`;
122
- function output(type, msg, level) {
123
- level = isBoolean(level) ? level ? defaultLevel : "error" : level;
124
- if (logLevels[level] >= logLevels[type]) {
125
- const method = type === "info" || type === "debug" ? "log" : type;
126
- const tag = type === "debug" ? ansis.magenta.bold(prefix) : type === "info" ? ansis.cyan.bold(prefix) : type === "warn" ? ansis.yellow.bold(prefix) : ansis.red.bold(prefix);
127
- const format = `${ansis.dim((/* @__PURE__ */ new Date()).toLocaleTimeString())} ${tag} ${msg}`;
128
- console[method](format);
129
- }
130
- }
131
- return {
132
- debug(msg, level = defaultLevel) {
133
- output("debug", msg, level);
134
- },
135
- info(msg, level = defaultLevel) {
136
- output("info", msg, level);
137
- },
138
- warn(msg, level = defaultLevel) {
139
- output("warn", msg, level);
140
- },
141
- error(msg, level = defaultLevel) {
142
- output("error", msg, level);
143
- }
144
- };
112
+
113
+
114
+ function matchScene(activeScene, mockScene) {
115
+ if (!mockScene) return true;
116
+ const scenes = toArray(mockScene);
117
+ if (activeScene.length === 0 && scenes.length > 0) return false;
118
+ if (scenes.length === 0) return true;
119
+ return scenes.some((s) => activeScene.includes(s));
145
120
  }
146
121
  getDirname(import.meta.url);
147
122
  const vfs = createFsFromVolume(new Volume());
148
123
  function getDirname(importMetaUrl) {
149
124
  return path.dirname(fileURLToPath(importMetaUrl));
150
125
  }
151
- Debug("vite:mock-dev-server");
152
126
  const windowsSlashRE = /\\/g;
153
127
  const isWindows = os.platform() === "win32";
154
128
  function slash(p) {
@@ -157,12 +131,9 @@ function slash(p) {
157
131
  function normalizePath(id) {
158
132
  return path.posix.normalize(isWindows ? slash(id) : id);
159
133
  }
160
- //#endregion
161
- //#region src/utils/urlParse.ts
162
- /**
163
- * nodejs 从 19.0.0 开始 弃用 url.parse,因此使用 url.parse 来解析 可能会报错,
164
- * 使用 URL 来解析
165
- */
134
+
135
+
136
+
166
137
  function urlParse(input) {
167
138
  const url = new URL(input, "http://example.com");
168
139
  return {
@@ -170,29 +141,21 @@ function urlParse(input) {
170
141
  query: parse$1(url.search.replace(/^\?/, ""))
171
142
  };
172
143
  }
173
- //#endregion
174
- //#region src/utils/waitingFor.ts
175
- function waitingFor(onSuccess, maxRetry = 5) {
176
- return function wait(getter, retry = 0) {
177
- const value = getter();
178
- if (value) onSuccess(value);
179
- else if (retry < maxRetry) setTimeout(() => wait(getter, retry + 1), 100);
180
- };
181
- }
182
- //#endregion
183
- //#region src/compiler/processData.ts
144
+
145
+
184
146
  function processRawData(rawData) {
185
147
  return rawData.filter((item) => item[0]).map(([raw, __filepath__]) => {
186
148
  let mockConfig;
187
- if (raw.default) if (isArray(raw.default)) mockConfig = raw.default.map((item) => ({
188
- ...item,
189
- __filepath__
190
- }));
191
- else mockConfig = {
192
- ...raw.default,
193
- __filepath__
194
- };
195
- else if ("url" in raw) mockConfig = {
149
+ if (raw.default) {
150
+ if (isArray(raw.default)) mockConfig = raw.default.map((item) => ({
151
+ ...item,
152
+ __filepath__
153
+ }));
154
+ else mockConfig = {
155
+ ...raw.default,
156
+ __filepath__
157
+ };
158
+ } else if ("url" in raw) mockConfig = {
196
159
  ...raw,
197
160
  __filepath__
198
161
  };
@@ -226,16 +189,18 @@ function processMockData(mockList) {
226
189
  };
227
190
  if (current.ws !== true) {
228
191
  const validator = current.validator;
229
- if (!isEmptyObject(query)) if (isFunction(validator)) current.validator = function(request) {
230
- return isObjectSubset(request.query, query) && validator(request);
231
- };
232
- else if (validator) {
233
- current.validator = { ...validator };
234
- current.validator.query = current.validator.query ? {
235
- ...query,
236
- ...current.validator.query
237
- } : query;
238
- } else current.validator = { query };
192
+ if (!isEmptyObject(query)) {
193
+ if (isFunction(validator)) current.validator = function(request) {
194
+ return isObjectSubset(request.query, query) && validator(request);
195
+ };
196
+ else if (validator) {
197
+ current.validator = { ...validator };
198
+ current.validator.query = current.validator.query ? {
199
+ ...query,
200
+ ...current.validator.query
201
+ } : query;
202
+ } else current.validator = { query };
203
+ }
239
204
  }
240
205
  list.push(current);
241
206
  });
@@ -257,28 +222,19 @@ function keysCount(obj) {
257
222
  if (!obj) return 0;
258
223
  return objectKeys(obj).length;
259
224
  }
260
- //#endregion
261
- //#region src/core/cors.ts
262
- /**
263
- * Create CORS middleware
264
- *
265
- * 创建 CORS 中间件
266
- *
267
- * @param corsOptions - CORS options / CORS 配置项
268
- * @returns CORS middleware function or undefined / CORS 中间件函数或未定义
269
- */
225
+
226
+
227
+
270
228
  function createCors(corsOptions) {
271
229
  const corsMiddleware = corsOptions ? cors(corsOptions) : void 0;
272
230
  return corsMiddleware ? (req, res) => new Promise((resolve, reject) => corsMiddleware(req, res, (err) => {
273
231
  err ? reject(err) : resolve();
274
232
  })) : void 0;
275
233
  }
276
- //#endregion
277
- //#region src/core/request.ts
278
- /**
279
- * 解析请求体 request.body
280
- */
281
- async function parseRequestBody(req, formidableOptions, bodyParserOptions = {}) {
234
+
235
+
236
+
237
+ async function parseRequestBody(req, logger, formidableOptions, bodyParserOptions = {}) {
282
238
  const method = req.method.toUpperCase();
283
239
  if (["HEAD", "OPTIONS"].includes(method)) return void 0;
284
240
  const type = req.headers["content-type"]?.toLocaleLowerCase() || "";
@@ -298,18 +254,17 @@ async function parseRequestBody(req, formidableOptions, bodyParserOptions = {})
298
254
  });
299
255
  if (type.startsWith("multipart/form-data")) return await parseRequestBodyWithMultipart(req, formidableOptions);
300
256
  } catch (e) {
301
- console.error(e);
257
+ logger.error(e);
302
258
  }
303
259
  }
260
+
304
261
  const DEFAULT_FORMIDABLE_OPTIONS = {
305
262
  keepExtensions: true,
306
263
  filename(name, ext, part) {
307
264
  return part?.originalFilename || `${name}.${Date.now()}${ext ? `.${ext}` : ""}`;
308
265
  }
309
266
  };
310
- /**
311
- * 解析 request form multipart body
312
- */
267
+
313
268
  async function parseRequestBodyWithMultipart(req, options) {
314
269
  const form = formidable({
315
270
  ...DEFAULT_FORMIDABLE_OPTIONS,
@@ -328,10 +283,9 @@ async function parseRequestBodyWithMultipart(req, options) {
328
283
  });
329
284
  });
330
285
  }
331
- const matcherCache = /* @__PURE__ */ new Map();
332
- /**
333
- * 解析请求 url 中的动态参数 params
334
- */
286
+
287
+ const matcherCache = new Map();
288
+
335
289
  function parseRequestParams(pattern, url) {
336
290
  let matcher = matcherCache.get(pattern);
337
291
  if (!matcher) {
@@ -341,16 +295,16 @@ function parseRequestParams(pattern, url) {
341
295
  const matched = matcher(url);
342
296
  return matched ? matched.params : {};
343
297
  }
344
- /**
345
- * 验证请求是否符合 validator
346
- */
298
+
347
299
  function requestValidate(request, validator) {
348
300
  return isObjectSubset(request.headers, validator.headers) && isObjectSubset(request.body, validator.body) && isObjectSubset(request.params, validator.params) && isObjectSubset(request.query, validator.query) && isObjectSubset(request.refererQuery, validator.refererQuery);
349
301
  }
302
+
350
303
  function formatLog(prefix, data) {
351
304
  return !data || isEmptyObject(data) ? "" : ` ${ansis.gray(`${prefix}:`)}${JSON.stringify(data)}`;
352
305
  }
353
- function requestLog(request, filepath) {
306
+
307
+ function requestLog(request, filepath, shouldSimulateError) {
354
308
  const { url, method, query, params, body } = request;
355
309
  let { pathname } = new URL(url, "http://example.com");
356
310
  pathname = ansis.green(decodeURIComponent(pathname));
@@ -358,18 +312,18 @@ function requestLog(request, filepath) {
358
312
  const qs = formatLog("query", query);
359
313
  const ps = formatLog("params", params);
360
314
  const bs = formatLog("body", body);
315
+ const es = shouldSimulateError ? ` 🎲 ${ansis.bgYellow("ERR")}` : "";
361
316
  const file = ` ${ansis.dim.underline(`(${filepath})`)}`;
362
- return `${ms} ${pathname}${qs}${ps}${bs}${file}`;
363
- }
364
- //#endregion
365
- //#region src/core/findMockData.ts
366
- /**
367
- * 查找匹配的 mock data
368
- */
369
- function findMockData(mockList, logger, { pathname, method, request }) {
317
+ return `${ms}${es} ${pathname}${qs}${ps}${bs}${file}`;
318
+ }
319
+
320
+
321
+
322
+ function findMockData(mockList, logger, { pathname, method, request, activeScene }) {
370
323
  return mockList.find((mock) => {
371
324
  if (!pathname || !mock || !mock.url || mock.ws) return false;
372
325
  if (!(mock.method ? isArray(mock.method) ? mock.method : [mock.method] : ["GET", "POST"]).includes(method)) return false;
326
+ if (!matchScene(activeScene, mock.scene)) return false;
373
327
  const hasMock = isPathMatch(mock.url, pathname);
374
328
  if (hasMock && mock.validator) {
375
329
  const params = parseRequestParams(mock.url, pathname);
@@ -377,226 +331,345 @@ function findMockData(mockList, logger, { pathname, method, request }) {
377
331
  params,
378
332
  ...request
379
333
  });
380
- else try {
381
- return requestValidate({
334
+ else {
335
+ const [error, validated] = attempt(requestValidate, {
382
336
  params,
383
337
  ...request
384
338
  }, mock.validator);
385
- } catch (e) {
386
- const file = mock.__filepath__;
387
- logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at validator (${ansis.underline(file)})`, mock.log);
388
- return false;
339
+ if (error) {
340
+ const file = mock.__filepath__;
341
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${error}\n at validator (${ansis.underline(file)})`, mock.log);
342
+ return false;
343
+ }
344
+ return validated;
389
345
  }
390
346
  }
391
347
  return hasMock;
392
348
  });
393
349
  }
394
- //#endregion
395
- //#region src/cookies/constants.ts
396
- /**
397
- * RegExp to match field-content in RFC 7230 sec 3.2
398
- *
399
- * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
400
- * field-vchar = VCHAR / obs-text
401
- * obs-text = %x80-FF
402
- */
403
- const fieldContentRegExp = /^[\t\u0020-\u007E\u0080-\u00FF]+$/;
404
- /**
405
- * RegExp to match Priority cookie attribute value.
406
- */
407
- const PRIORITY_REGEXP = /^(?:low|medium|high)$/i;
408
- /**
409
- * Cache for generated name regular expressions.
410
- */
411
- const REGEXP_CACHE = Object.create(null);
412
- /**
413
- * RegExp to match all characters to escape in a RegExp.
414
- */
415
- const REGEXP_ESCAPE_CHARS_REGEXP = /[\^$\\.*+?()[\]{}|]/g;
416
- /**
417
- * RegExp to match basic restricted name characters for loose validation.
418
- */
419
- const RESTRICTED_NAME_CHARS_REGEXP = /[;=]/;
420
- /**
421
- * RegExp to match basic restricted value characters for loose validation.
422
- */
423
- const RESTRICTED_VALUE_CHARS_REGEXP = /;/;
424
- /**
425
- * RegExp to match Same-Site cookie attribute value.
426
- */
427
- const SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i;
428
- //#endregion
429
- //#region src/cookies/Cookie.ts
430
- var Cookie = class {
431
- name;
432
- value;
433
- maxAge;
434
- expires;
435
- path = "/";
436
- domain;
437
- secure = false;
438
- httpOnly = true;
439
- sameSite = false;
440
- overwrite = false;
441
- priority;
442
- partitioned;
443
- constructor(name, value, options = {}) {
444
- if (!fieldContentRegExp.test(name) || RESTRICTED_NAME_CHARS_REGEXP.test(name)) throw new TypeError("argument name is invalid");
445
- if (value && (!fieldContentRegExp.test(value) || RESTRICTED_VALUE_CHARS_REGEXP.test(value))) throw new TypeError("argument value is invalid");
446
- this.name = name;
447
- this.value = value;
448
- Object.assign(this, options);
449
- if (!this.value) {
450
- this.expires = /* @__PURE__ */ new Date(0);
451
- this.maxAge = void 0;
350
+
351
+
352
+ const FILTERED_RESPONSE_HEADERS = [
353
+ "date",
354
+ "expires",
355
+ "last-modified",
356
+ "server",
357
+ "x-powered-by",
358
+ "x-aspnet-version",
359
+ "x-nginx-version",
360
+ "via",
361
+ "cache-control",
362
+ "etag",
363
+ "age",
364
+ "connection",
365
+ "keep-alive",
366
+ "proxy-authenticate",
367
+ "proxy-authorization",
368
+ "proxy-connection",
369
+ "trailer",
370
+ "access-control-allow-origin",
371
+ "access-control-allow-credentials",
372
+ "access-control-allow-methods",
373
+ "access-control-allow-headers",
374
+ "access-control-expose-headers",
375
+ "access-control-max-age",
376
+ "origin",
377
+ "p3p",
378
+ "pragma",
379
+ "x-request-id",
380
+ "x-correlation-id",
381
+ "x-trace-id",
382
+ "x-varnish",
383
+ "x-cache",
384
+ "x-cache-hits",
385
+ "x-cache-status",
386
+ "cf-cache-status",
387
+ "cf-ray",
388
+ "cf-request-id",
389
+ "server-timing",
390
+ "x-dns-prefetch-control"
391
+ ];
392
+
393
+
394
+
395
+ async function decompressBody(rawBody, encoding) {
396
+ try {
397
+ switch (encoding.toLowerCase()) {
398
+ case "gzip":
399
+ case "x-gzip": return {
400
+ body: await gunzip(rawBody),
401
+ encoding: "identity"
402
+ };
403
+ case "deflate":
404
+ case "x-deflate": return {
405
+ body: await deflate(rawBody),
406
+ encoding: "identity"
407
+ };
408
+ case "br": return {
409
+ body: await brotli(rawBody),
410
+ encoding: "identity"
411
+ };
412
+ case "zstd": return {
413
+ body: await zstd(rawBody),
414
+ encoding: "identity"
415
+ };
452
416
  }
453
- if (this.path && !fieldContentRegExp.test(this.path)) throw new TypeError("[Cookie] option path is invalid");
454
- if (this.domain && !fieldContentRegExp.test(this.domain)) throw new TypeError("[Cookie] option domain is invalid");
455
- if (typeof this.maxAge === "number" ? Number.isNaN(this.maxAge) || !Number.isFinite(this.maxAge) : this.maxAge) throw new TypeError("[Cookie] option maxAge is invalid");
456
- if (this.priority && !PRIORITY_REGEXP.test(this.priority)) throw new TypeError("[Cookie] option priority is invalid");
457
- if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) throw new TypeError("[Cookie] option sameSite is invalid");
417
+ } catch {}
418
+ return {
419
+ body: rawBody,
420
+ encoding
421
+ };
422
+ }
423
+ let zstdStreaming = null;
424
+
425
+ async function zstd(rawBody) {
426
+ if (zlib.zstdDecompress) return new Promise((resolve, reject) => {
427
+ zlib.zstdDecompress(rawBody, (err, data) => {
428
+ err ? reject(err) : resolve(data);
429
+ });
430
+ });
431
+ if (!zstdStreaming) {
432
+ const { ZstdCodec } = await import("zstd-codec");
433
+ zstdStreaming = await new Promise((resolve) => {
434
+ ZstdCodec.run((binding) => {
435
+ resolve(new binding.Streaming());
436
+ });
437
+ });
438
+ }
439
+ return zstdStreaming.decompress(rawBody, rawBody.length);
440
+ }
441
+
442
+ async function brotli(rawBody) {
443
+ return new Promise((resolve, reject) => {
444
+ zlib.brotliDecompress(rawBody, (err, data) => {
445
+ err ? reject(err) : resolve(data);
446
+ });
447
+ });
448
+ }
449
+ async function gunzip(rawBody) {
450
+ return new Promise((resolve, reject) => {
451
+ zlib.gunzip(rawBody, (err, data) => {
452
+ err ? reject(err) : resolve(data);
453
+ });
454
+ });
455
+ }
456
+ async function deflate(rawBody) {
457
+ return new Promise((resolve, reject) => {
458
+ zlib.inflate(rawBody, (err, data) => {
459
+ err ? reject(err) : resolve(data);
460
+ });
461
+ });
462
+ }
463
+
464
+
465
+ const timeFormatter = new Intl.DateTimeFormat("en-US", {
466
+ year: "numeric",
467
+ month: "numeric",
468
+ day: "numeric",
469
+ hour: "numeric",
470
+ minute: "numeric",
471
+ second: "numeric",
472
+ hour12: false
473
+ });
474
+
475
+ function processRecordReq(req, pathname, body) {
476
+ const { query } = urlParse(req.url);
477
+ const method = req.method.toUpperCase();
478
+ let bodyType = (req.headers["content-type"] || "").split(";")[0].trim();
479
+ if (bodyType.startsWith("multipart/form-data") && isPlainObject(body)) {
480
+ body = { ...body };
481
+ objectKeys(body).forEach((key) => {
482
+ const value = body[key];
483
+ if (isPlainObject(value) && hasOwn(value, "filepath") && hasOwn(value, "mimetype")) delete body[key];
484
+ });
458
485
  }
459
- toString() {
460
- return `${this.name}=${this.value}`;
486
+ if (Buffer.isBuffer(body)) {
487
+ body = body.toString();
488
+ bodyType = "buffer";
461
489
  }
462
- toHeader() {
463
- let header = this.toString();
464
- if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
465
- if (this.path) header += `; path=${this.path}`;
466
- if (this.expires) header += `; expires=${this.expires.toUTCString()}`;
467
- if (this.domain) header += `; domain=${this.domain}`;
468
- if (this.priority) header += `; priority=${this.priority.toLowerCase()}`;
469
- if (this.sameSite) header += `; samesite=${this.sameSite === true ? "strict" : this.sameSite.toLowerCase()}`;
470
- if (this.secure) header += "; secure";
471
- if (this.httpOnly) header += "; httponly";
472
- if (this.partitioned) header += "; partitioned";
473
- return header;
490
+ return {
491
+ method,
492
+ pathname,
493
+ query,
494
+ bodyType,
495
+ body
496
+ };
497
+ }
498
+
499
+ async function processRecordRes(res, body) {
500
+ const status = res.statusCode || 200;
501
+ const statusText = res.statusMessage || "OK";
502
+ const headers = {};
503
+ for (const [key, value] of Object.entries(res.headers)) {
504
+ const lowerKey = key.toLowerCase();
505
+ if (value !== void 0 && !FILTERED_RESPONSE_HEADERS.includes(lowerKey)) headers[key] = String(value);
474
506
  }
475
- };
476
- //#endregion
477
- //#region src/cookies/timeSafeCompare.ts
478
- function bufferEqual(a, b) {
479
- if (a.length !== b.length) return false;
480
- if (crypto.timingSafeEqual) return crypto.timingSafeEqual(a, b);
481
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
507
+ const isText = isTextContent(headers["content-type"] || "");
508
+ if (isText) {
509
+ const { body: decodedBody, encoding } = await decompressBody(body, headers["content-encoding"] || "");
510
+ body = Buffer.from(decodedBody);
511
+ headers["content-encoding"] = encoding;
512
+ }
513
+ return {
514
+ status,
515
+ statusText,
516
+ headers,
517
+ body: body.toString(isText ? "utf-8" : "base64")
518
+ };
519
+ }
520
+
521
+ function isSameRecord(prev, current) {
522
+ if (prev.pathname !== current.pathname || prev.method !== current.method) return false;
523
+ if (prev.bodyType !== current.bodyType) return false;
524
+ if (!deepEqual(prev.query, current.query)) return false;
525
+ if (current.bodyType === "buffer" && prev.bodyType === "buffer") {
526
+ const currentBody = Buffer.from(current.body);
527
+ const prevBody = Buffer.from(prev.body);
528
+ if (currentBody.length !== prevBody.length || !currentBody.equals(prevBody)) return false;
529
+ }
530
+ if (!deepEqual(prev.body, current.body)) return false;
482
531
  return true;
483
532
  }
484
- function createHmac(key, data) {
485
- return crypto.createHmac("sha256", key).update(data).digest();
486
- }
487
- function timeSafeCompare(a, b) {
488
- const sa = String(a);
489
- const sb = String(b);
490
- const key = crypto.randomBytes(32);
491
- return bufferEqual(createHmac(key, sa), createHmac(key, sb)) && a === b;
492
- }
493
- //#endregion
494
- //#region src/cookies/Keygrip.ts
495
- const SLASH_PATTERN = /[/+=]/g;
496
- const REPLACE_MAP = {
497
- "/": "_",
498
- "+": "-",
499
- "=": ""
500
- };
501
- var Keygrip = class {
502
- algorithm;
503
- encoding;
504
- keys = [];
505
- constructor(keys, algorithm, encoding) {
506
- this.keys = keys;
507
- this.algorithm = algorithm || "sha256";
508
- this.encoding = encoding || "base64";
533
+
534
+ function createRecordMatcher(filter) {
535
+ if (isFunction(filter)) return filter;
536
+ const { mode = "glob" } = filter;
537
+ const include = toArray(filter.include);
538
+ const exclude = toArray(filter.exclude);
539
+ if (mode === "glob") {
540
+ const { isMatch } = createMatcher(include, exclude);
541
+ return (req) => isMatch(req.pathname);
509
542
  }
510
- sign(data, key = this.keys[0]) {
511
- return crypto.createHmac(this.algorithm, key).update(data).digest(this.encoding).replace(SLASH_PATTERN, (m) => REPLACE_MAP[m]);
543
+ return (req) => {
544
+ return include.some((pattern) => isPathMatch(pattern, req.pathname)) && exclude.every((pattern) => !isPathMatch(pattern, req.pathname));
545
+ };
546
+ }
547
+
548
+ function getFilepath(pathname, dir) {
549
+ return path.join(dir, `${kebabCase(pathname)}.json`);
550
+ }
551
+
552
+
553
+ const storage = new Map();
554
+
555
+ async function readRecordStorage(filepath) {
556
+ if (storage.has(filepath)) return storage.get(filepath);
557
+ try {
558
+ if (!fs.existsSync(filepath)) return [];
559
+ const content = await fs.promises.readFile(filepath, "utf-8") || "[]";
560
+ const data = JSON.parse(content);
561
+ storage.set(filepath, data);
562
+ return data;
563
+ } catch (error) {
564
+ console.error(`Error reading record file ${filepath}:`, error);
565
+ return [];
512
566
  }
513
- index(data, digest) {
514
- for (let i = 0, l = this.keys.length; i < l; i++) if (timeSafeCompare(digest, this.sign(data, this.keys[i]))) return i;
515
- return -1;
567
+ }
568
+
569
+ async function writeRecordStorage(filepath, records) {
570
+ try {
571
+ storage.set(filepath, records);
572
+ await fs.promises.mkdir(path.dirname(filepath), { recursive: true });
573
+ await fs.promises.writeFile(filepath, JSON.stringify(records, null, 2), "utf-8");
574
+ } catch (error) {
575
+ console.error(`Error writing record file ${filepath}:`, error);
516
576
  }
517
- verify(data, digest) {
518
- return this.index(data, digest) > -1;
577
+ }
578
+ const originalReqCache = new WeakMap();
579
+
580
+ function recordRequestWithRawReq(req, pathname, body) {
581
+ originalReqCache.set(req, {
582
+ body,
583
+ pathname,
584
+ timestamp: Date.now()
585
+ });
586
+ }
587
+
588
+
589
+
590
+ var Recorder = class {
591
+ options;
592
+ filter;
593
+ constructor(options) {
594
+ this.options = options;
595
+ this.filter = createRecordMatcher(options.filter);
596
+ this.addGitignore();
519
597
  }
520
- };
521
- //#endregion
522
- //#region src/cookies/Cookies.ts
523
- var Cookies = class {
524
- request;
525
- response;
526
- secure;
527
- keys;
528
- constructor(req, res, options = {}) {
529
- this.request = req;
530
- this.response = res;
531
- this.secure = options.secure;
532
- if (options.keys instanceof Keygrip) this.keys = options.keys;
533
- else if (isArray(options.keys)) this.keys = new Keygrip(options.keys);
598
+ getPlugin() {
599
+ return (proxyServer) => {
600
+ proxyServer.on("proxyRes", (proxyRes, req) => {
601
+ let chunks = [];
602
+ proxyRes.on("data", (chunk) => chunk && chunks.push(chunk));
603
+ proxyRes.on("end", () => {
604
+ this.record(req, proxyRes, Buffer.concat(chunks));
605
+ chunks = null;
606
+ });
607
+ });
608
+ };
534
609
  }
535
- set(name, value, options) {
536
- const req = this.request;
537
- const res = this.response;
538
- const headers = toArray(res.getHeader("Set-Cookie"));
539
- const cookie = new Cookie(name, value, options);
540
- const signed = options?.signed ?? !!this.keys;
541
- const secure = this.secure === void 0 ? req.protocol === "https" || isRequestEncrypted(req) : Boolean(this.secure);
542
- if (!secure && options?.secure) throw new Error("Cannot send secure cookie over unencrypted connection");
543
- cookie.secure = options?.secure ?? secure;
544
- pushCookie(headers, cookie);
545
- if (signed && options) {
546
- if (!this.keys) throw new Error(".keys required for signed cookies");
547
- cookie.value = this.keys.sign(cookie.toString());
548
- cookie.name += ".sig";
549
- pushCookie(headers, cookie);
550
- }
551
- (res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader).call(res, "Set-Cookie", headers);
552
- return this;
610
+ async record(req, res, resBody) {
611
+ if (!originalReqCache.has(req)) return;
612
+ const { body, pathname, timestamp } = originalReqCache.get(req);
613
+ originalReqCache.delete(req);
614
+ if (!pathname) return;
615
+ const recordReq = processRecordReq(req, pathname, body);
616
+ if (!this.filter(recordReq)) return;
617
+ const { cwd, dir, status, expires, overwrite } = this.options;
618
+ if (status.length !== 0 && !status.includes(res.statusCode || 200)) return;
619
+ const record = {
620
+ meta: {
621
+ timestamp,
622
+ filepath: "",
623
+ createAt: timeFormatter.format(timestamp),
624
+ referer: req.headers.referer || "unknown"
625
+ },
626
+ req: recordReq,
627
+ res: await processRecordRes(res, resBody)
628
+ };
629
+ const filepath = getFilepath(pathname, dir);
630
+ record.meta.filepath = filepath;
631
+ const absoluteFilepath = path.join(cwd, filepath);
632
+ const records = (await readRecordStorage(absoluteFilepath)).filter((item) => timestamp - item.meta.timestamp <= expires);
633
+ const index = records.findIndex((item) => isSameRecord(item.req, record.req) && item.res.status === record.res.status);
634
+ if (index === -1) records.push(record);
635
+ else if (overwrite || timestamp - records[index].meta.timestamp > expires) records[index] = record;
636
+ await writeRecordStorage(absoluteFilepath, records);
553
637
  }
554
- get(name, options) {
555
- const signName = `${name}.sig`;
556
- const signed = options?.signed ?? !!this.keys;
557
- const header = this.request.headers.cookie;
558
- if (!header) return;
559
- const match = header.match(getPattern(name));
560
- if (!match) return;
561
- let value = match[1];
562
- if (value[0] === "\"") value = value.slice(1, -1);
563
- if (!options || !signed) return value;
564
- const remote = this.get(signName);
565
- if (!remote) return;
566
- const data = `${name}=${value}`;
567
- if (!this.keys) throw new Error(".keys required for signed cookies");
568
- const index = this.keys.index(data, remote);
569
- if (index < 0) this.set(signName, null, {
570
- path: "/",
571
- signed: false
572
- });
573
- else {
574
- index && this.set(signName, this.keys.sign(data), { signed: false });
575
- return value;
576
- }
638
+ async addGitignore() {
639
+ const options = this.options;
640
+ if (!options.gitignore) return;
641
+ const dirname = path.join(options.cwd, options.dir);
642
+ await promises.mkdir(dirname, { recursive: true });
643
+ if (!fs.existsSync(path.join(dirname, ".gitignore"))) await promises.writeFile(path.join(dirname, ".gitignore"), "*\n", "utf-8");
577
644
  }
578
645
  };
579
- /**
580
- * Get the pattern to search for a cookie in a string.
581
- */
582
- function getPattern(name) {
583
- if (!REGEXP_CACHE[name]) REGEXP_CACHE[name] = new RegExp(`(?:^|;) *${name.replace(REGEXP_ESCAPE_CHARS_REGEXP, "\\$&")}=([^;]*)`);
584
- return REGEXP_CACHE[name];
585
- }
586
- /**
587
- * Get the encrypted status for a request.
588
- */
589
- function isRequestEncrypted(req) {
590
- return Boolean(req.socket ? req.socket.encrypted : req.connection.encrypted);
591
- }
592
- function pushCookie(headers, cookie) {
593
- if (cookie.overwrite) {
594
- for (let i = headers.length - 1; i >= 0; i--) if (headers[i].indexOf(`${cookie.name}=`) === 0) headers.splice(i, 1);
646
+
647
+
648
+
649
+ async function replayRecordedRequest(rawReq, pathname, body, options) {
650
+ const req = processRecordReq(rawReq, pathname, body);
651
+ const filepath = path.join(options.cwd, getFilepath(req.pathname, options.dir));
652
+ const timestamp = Date.now();
653
+ const records = await readRecordStorage(filepath);
654
+ const matchedList = records.filter((item) => timestamp - item.meta.timestamp < options.expires && isSameRecord(item.req, req));
655
+ let matched;
656
+ if (options.status.length === 0) matched = matchedList.find((item) => item.res.status === 200) || records[0];
657
+ else matched = matchedList.find((item) => options.status.includes(item.res.status));
658
+ if (matched) {
659
+ const isText = isTextContent(matched.res.headers["content-type"] || "");
660
+ return {
661
+ url: matched.req.pathname,
662
+ status: matched.res.status,
663
+ statusText: matched.res.statusText,
664
+ headers: matched.res.headers,
665
+ body: Buffer.from(matched.res.body, isText ? "utf-8" : "base64"),
666
+ type: "buffer",
667
+ __filepath__: matched.meta.filepath
668
+ };
595
669
  }
596
- headers.push(cookie.toHeader());
597
670
  }
598
- //#endregion
599
- //#region src/core/matchingWeight.ts
671
+
672
+
600
673
  const tokensCache = {};
601
674
  function getTokens(rule) {
602
675
  if (tokensCache[rule]) return tokensCache[rule];
@@ -648,16 +721,7 @@ function defaultPriority(rules) {
648
721
  const highest = getHighest(rules);
649
722
  return rules.sort((a, b) => computedWeight(a, highest) - computedWeight(b, highest));
650
723
  }
651
- /**
652
- * Calculate matching weight for mock URLs
653
- *
654
- * 计算 Mock URL 的匹配权重
655
- *
656
- * @param rules - Array of URL patterns / URL 模式数组
657
- * @param url - Request URL / 请求 URL
658
- * @param priority - Priority configuration / 优先级配置
659
- * @returns Sorted array of matched rules / 排序后的匹配规则数组
660
- */
724
+
661
725
  function matchingWeight(rules, url, priority) {
662
726
  let matched = defaultPriority(rules.filter((rule) => isPathMatch(rule, url)));
663
727
  const { global = [], special = {} } = priority;
@@ -682,22 +746,16 @@ function matchingWeight(rules, url, priority) {
682
746
  }
683
747
  return matched;
684
748
  }
685
- //#endregion
686
- //#region src/core/requestRecovery.ts
687
- /**
688
- * 请求复原
689
- *
690
- * 由于 parseReqBody 在解析请求时,会将请求流消费,
691
- * 导致当接口不需要被 mock,继而由 vite http-proxy 转发时,请求流无法继续。
692
- * 为此,我们在请求流中记录请求数据,当当前请求无法继续时,可以从备份中恢复请求流
693
- */
694
- const requestCollectCache = /* @__PURE__ */ new WeakMap();
749
+
750
+
751
+
752
+ const requestCollectCache = new WeakMap();
695
753
  function collectRequest(req) {
696
754
  const chunks = [];
697
- req.addListener("data", (chunk) => {
755
+ req.on("data", (chunk) => {
698
756
  chunks.push(Buffer.from(chunk));
699
757
  });
700
- req.addListener("end", () => {
758
+ req.on("end", () => {
701
759
  if (chunks.length) requestCollectCache.set(req, Buffer.concat(chunks));
702
760
  });
703
761
  }
@@ -709,24 +767,18 @@ function rewriteRequest(proxyReq, req) {
709
767
  if (!proxyReq.writableEnded) proxyReq.write(buffer);
710
768
  }
711
769
  }
712
- //#endregion
713
- //#region src/core/response.ts
714
- /**
715
- * 根据状态码获取状态文本
716
- */
770
+
771
+
772
+
717
773
  function getHTTPStatusText(status) {
718
774
  return HTTP_STATUS[status] || "Unknown";
719
775
  }
720
- /**
721
- * 设置响应状态
722
- */
776
+
723
777
  function provideResponseStatus(response, status = 200, statusText) {
724
778
  response.statusCode = status;
725
779
  response.statusMessage = statusText || getHTTPStatusText(status);
726
780
  }
727
- /**
728
- * 设置响应头
729
- */
781
+
730
782
  async function provideResponseHeaders(req, res, mock, logger) {
731
783
  const { headers, type = "json" } = mock;
732
784
  const filepath = mock.__filepath__;
@@ -736,38 +788,30 @@ async function provideResponseHeaders(req, res, mock, logger) {
736
788
  res.setHeader("X-Mock-Power-By", "vite-plugin-mock-dev-server");
737
789
  if (filepath) res.setHeader("X-File-Path", filepath);
738
790
  if (!headers) return;
739
- try {
740
- const raw = isFunction(headers) ? await headers(req) : headers;
741
- Object.keys(raw).forEach((key) => {
742
- res.setHeader(key, raw[key]);
743
- });
744
- } catch (e) {
745
- logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${e}\n at headers (${ansis.underline(filepath)})`, mock.log);
791
+ const [error, data] = await attemptAsync(async () => isFunction(headers) ? await headers(req) : headers);
792
+ if (error) {
793
+ logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${error}\n at headers (${ansis.underline(filepath)})`, mock.log);
794
+ return;
746
795
  }
796
+ objectKeys(data).forEach((key) => res.setHeader(key, data[key]));
747
797
  }
748
- /**
749
- * 设置响应cookie
750
- */
798
+
751
799
  async function provideResponseCookies(req, res, mock, logger) {
752
800
  const { cookies } = mock;
753
- const filepath = mock.__filepath__;
754
801
  if (!cookies) return;
755
- try {
756
- const raw = isFunction(cookies) ? await cookies(req) : cookies;
757
- Object.keys(raw).forEach((key) => {
758
- const cookie = raw[key];
759
- if (isArray(cookie)) {
760
- const [value, options] = cookie;
761
- res.setCookie(key, value, options);
762
- } else res.setCookie(key, cookie);
763
- });
764
- } catch (e) {
765
- logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${e}\n at cookies (${ansis.underline(filepath)})`, mock.log);
802
+ const [error, data] = await attemptAsync(async () => isFunction(cookies) ? await cookies(req) : cookies);
803
+ if (error) {
804
+ const filepath = mock.__filepath__;
805
+ logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${error}\n at cookies (${ansis.underline(filepath)})`, mock.log);
806
+ return;
766
807
  }
808
+ objectKeys(data).forEach((key) => {
809
+ const cookie = data[key];
810
+ const [value, options] = isArray(cookie) ? cookie : [cookie];
811
+ res.setCookie(key, value, options);
812
+ });
767
813
  }
768
- /**
769
- * 设置响应数据
770
- */
814
+
771
815
  function sendResponseData(res, raw, type) {
772
816
  if (isReadableStream(raw)) raw.pipe(res);
773
817
  else if (Buffer.isBuffer(raw)) res.end(type === "text" || type === "json" ? raw.toString("utf-8") : raw);
@@ -776,9 +820,7 @@ function sendResponseData(res, raw, type) {
776
820
  res.end(type === "buffer" ? Buffer.from(content) : content);
777
821
  }
778
822
  }
779
- /**
780
- * 实际响应延迟
781
- */
823
+
782
824
  async function responseRealDelay(startTime, delay) {
783
825
  if (!delay || typeof delay === "number" && delay <= 0 || isArray(delay) && delay.length !== 2) return;
784
826
  let realDelay = 0;
@@ -788,9 +830,10 @@ async function responseRealDelay(startTime, delay) {
788
830
  } else realDelay = delay - (timestamp() - startTime);
789
831
  if (realDelay > 0) await sleep(realDelay);
790
832
  }
791
- //#endregion
792
- //#region src/core/mockMiddleware.ts
793
- function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions = {}, proxies, cookiesOptions, logger, priority = {}, cors: corsOptions }) {
833
+
834
+
835
+
836
+ function createMockMiddleware(compiler, { formidableOptions = {}, bodyParserOptions = {}, proxies, cookiesOptions, logger, priority = {}, cors: corsOptions, record, replay, activeScene }) {
794
837
  const cors = createCors(corsOptions);
795
838
  const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
796
839
  const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
@@ -802,35 +845,38 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
802
845
  if (globFilter.length && !isGlobProxiesMatch(pathname)) return next();
803
846
  const mockData = compiler.mockData;
804
847
  const mockUrls = matchingWeight(Object.keys(mockData), pathname, priority);
805
- if (mockUrls.length === 0) return next();
848
+ if (mockUrls.length === 0 && !record.enabled) return next();
806
849
  collectRequest(req);
807
- const { query: refererQuery } = urlParse(req.headers.referer || "");
808
- const reqBody = await parseRequestBody(req, formidableOptions, bodyParserOptions);
809
850
  const cookies = new Cookies(req, res, cookiesOptions);
810
- const getCookie = cookies.get.bind(cookies);
811
- const method = req.method.toUpperCase();
812
851
  let mock;
813
852
  let _mockUrl;
853
+ const method = req.method.toUpperCase();
854
+ const extraReq = {
855
+ query,
856
+ refererQuery: urlParse(req.headers.referer || "").query,
857
+ body: await parseRequestBody(req, logger, formidableOptions, bodyParserOptions),
858
+ headers: req.headers,
859
+ getCookie: cookies.get.bind(cookies)
860
+ };
861
+ const headerScene = req.headers["x-mock-scene"];
862
+ const effectiveScene = headerScene ? toArray(headerScene).map((item) => item.split(",").map((s) => s.trim())).flat().filter(Boolean) : activeScene;
814
863
  for (const mockUrl of mockUrls) {
815
864
  mock = findMockData(mockData[mockUrl], logger, {
816
865
  pathname,
817
866
  method,
818
- request: {
819
- query,
820
- refererQuery,
821
- body: reqBody,
822
- headers: req.headers,
823
- getCookie
824
- }
867
+ request: extraReq,
868
+ activeScene: effectiveScene
825
869
  });
826
870
  if (mock) {
827
871
  _mockUrl = mockUrl;
828
872
  break;
829
873
  }
830
874
  }
875
+ if (replay && !mock) mock = await replayRecordedRequest(req, pathname, extraReq.body, record);
831
876
  if (!mock) {
877
+ record.enabled && recordRequestWithRawReq(req, pathname, extraReq.body);
832
878
  const matched = mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ");
833
- logger.warn(`${ansis.green(pathname)} matches ${matched} , but mock data is not found.`);
879
+ matched.length && logger.warn(`${ansis.green(pathname)} matches ${matched}, but mock data is not found.`);
834
880
  return next();
835
881
  }
836
882
  if (cors) {
@@ -842,36 +888,42 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
842
888
  }
843
889
  const request = req;
844
890
  const response = res;
845
- request.body = reqBody;
846
- request.query = query;
847
- request.refererQuery = refererQuery;
891
+ Object.assign(request, omit(extraReq, ["headers"]));
848
892
  request.params = parseRequestParams(mock.url, pathname);
849
- request.getCookie = getCookie;
850
893
  response.setCookie = cookies.set.bind(cookies);
851
- const { body, delay, type = "json", response: responseFn, status = 200, statusText, log: logLevel, __filepath__: filepath } = mock;
894
+ const { delay, type = "json", response: responseFn, log: logLevel, error: errorConfig, __filepath__: filepath } = mock;
895
+ let { body, status = 200, statusText } = mock;
896
+ const shouldSimulateError = errorConfig && (errorConfig.probability ?? .5) > Math.random();
897
+ if (shouldSimulateError) {
898
+ status = errorConfig.status ?? 500;
899
+ statusText = errorConfig.statusText;
900
+ body = errorConfig.body;
901
+ }
852
902
  provideResponseStatus(response, status, statusText);
853
903
  await provideResponseHeaders(request, response, mock, logger);
854
904
  await provideResponseCookies(request, response, mock, logger);
855
- logger.info(requestLog(request, filepath), logLevel);
856
- logger.debug(`${ansis.magenta("DEBUG")} ${ansis.underline(pathname)} matches: [ ${mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ")} ]\n`);
905
+ logger.info(requestLog(request, filepath, shouldSimulateError), logLevel);
906
+ logger.debug(`${ansis.magenta("DEBUG")} ${ansis.underline(pathname)} matches: [ ${mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ")} ]\n`);
857
907
  if (body) {
858
- try {
908
+ const [error] = await attemptAsync(async () => {
859
909
  const content = isFunction(body) ? await body(request) : body;
860
910
  await responseRealDelay(startTime, delay);
861
911
  sendResponseData(response, content, type);
862
- } catch (e) {
863
- logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at body (${ansis.underline(filepath)})`, logLevel);
912
+ });
913
+ if (error) {
914
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at body (${ansis.underline.gray(filepath)})`, logLevel);
864
915
  provideResponseStatus(response, 500);
865
916
  res.end("");
866
917
  }
867
918
  return;
868
919
  }
869
920
  if (responseFn) {
870
- try {
921
+ const [error] = await attemptAsync(async () => {
871
922
  await responseRealDelay(startTime, delay);
872
923
  await responseFn(request, response, next);
873
- } catch (e) {
874
- logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at response (${ansis.underline(filepath)})`, logLevel);
924
+ });
925
+ if (error) {
926
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at response (${ansis.underline.gray(filepath)})`, logLevel);
875
927
  provideResponseStatus(response, 500);
876
928
  res.end("");
877
929
  }
@@ -880,20 +932,20 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
880
932
  res.end("");
881
933
  };
882
934
  }
883
- //#endregion
884
- //#region src/core/ws.ts
935
+
936
+
885
937
  function mockWebSocket(compiler, httpServer, { wsProxies: proxies, cookiesOptions, logger }) {
886
- const hmrMap = /* @__PURE__ */ new Map();
887
- const poolMap = /* @__PURE__ */ new Map();
888
- const wssContextMap = /* @__PURE__ */ new WeakMap();
938
+ const hmrMap = new Map();
939
+ const poolMap = new Map();
940
+ const wssContextMap = new WeakMap();
889
941
  const getWssMap = (mockUrl) => {
890
942
  let wssMap = poolMap.get(mockUrl);
891
- if (!wssMap) poolMap.set(mockUrl, wssMap = /* @__PURE__ */ new Map());
943
+ if (!wssMap) poolMap.set(mockUrl, wssMap = new Map());
892
944
  return wssMap;
893
945
  };
894
946
  const addHmr = (filepath, mockUrl) => {
895
947
  let urlList = hmrMap.get(filepath);
896
- if (!urlList) hmrMap.set(filepath, urlList = /* @__PURE__ */ new Set());
948
+ if (!urlList) hmrMap.set(filepath, urlList = new Set());
897
949
  urlList.add(mockUrl);
898
950
  };
899
951
  const setupWss = (wssMap, wss, mock, context, pathname, filepath) => {
@@ -999,5 +1051,40 @@ function cleanupRunner(cleanupList) {
999
1051
  let cleanup;
1000
1052
  while (cleanup = cleanupList.shift()) cleanup?.();
1001
1053
  }
1002
- //#endregion
1003
- export { processRawData as a, normalizePath as c, logLevels as d, getPackageDepList as f, processMockData as i, vfs as l, createMatcher as m, baseMiddleware as n, sortByValidator as o, getPackageDeps as p, rewriteRequest as r, waitingFor as s, mockWebSocket as t, createLogger as u };
1054
+
1055
+
1056
+ const logLevels = {
1057
+ silent: 0,
1058
+ error: 1,
1059
+ warn: 2,
1060
+ info: 3,
1061
+ debug: 4
1062
+ };
1063
+ function createLogger(prefix, defaultLevel = "info") {
1064
+ prefix = `[${prefix}]`;
1065
+ function output(type, msg, level) {
1066
+ level = isBoolean(level) ? level ? defaultLevel : "error" : level;
1067
+ if (logLevels[level] >= logLevels[type]) {
1068
+ const method = type === "info" || type === "debug" ? "log" : type;
1069
+ const tag = type === "debug" ? ansis.magenta.bold(prefix) : type === "info" ? ansis.cyan.bold(prefix) : type === "warn" ? ansis.yellow.bold(prefix) : ansis.red.bold(prefix);
1070
+ const format = `${ansis.dim(( new Date()).toLocaleTimeString())} ${tag} ${msg}`;
1071
+ console[method](format);
1072
+ }
1073
+ }
1074
+ return {
1075
+ debug(msg, level = defaultLevel) {
1076
+ output("debug", msg, level);
1077
+ },
1078
+ info(msg, level = defaultLevel) {
1079
+ output("info", msg, level);
1080
+ },
1081
+ warn(msg, level = defaultLevel) {
1082
+ output("warn", msg, level);
1083
+ },
1084
+ error(msg, level = defaultLevel) {
1085
+ output("error", msg, level);
1086
+ }
1087
+ };
1088
+ }
1089
+
1090
+ export { rewriteRequest as a, processRawData as c, vfs as d, getPackageDepList as f, createMockMiddleware as i, sortByValidator as l, createMatcher as m, logLevels as n, Recorder as o, getPackageDeps as p, mockWebSocket as r, processMockData as s, createLogger as t, normalizePath as u };