rspack-plugin-mock 2.0.0 → 2.1.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,5 +1,6 @@
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, 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";
@@ -15,6 +16,7 @@ import bodyParser from "co-body";
15
16
  import formidable from "formidable";
16
17
  import http from "node:http";
17
18
  import { Buffer } from "node:buffer";
19
+ import zlib from "node:zlib";
18
20
  import HTTP_STATUS from "http-status";
19
21
  import * as mime from "mime-types";
20
22
  import { WebSocketServer } from "ws";
@@ -67,6 +69,19 @@ function isStream(stream) {
67
69
  function isReadableStream(stream) {
68
70
  return isStream(stream) && stream.readable !== false && typeof stream._read === "function" && typeof stream._readableState === "object";
69
71
  }
72
+ /**
73
+ * 判断内容类型是否为文本类型
74
+ *
75
+ * @param contentType 内容类型
76
+ * @returns 是否为文本类型
77
+ */
78
+ function isTextContent(contentType) {
79
+ return [
80
+ "text",
81
+ "json",
82
+ "xml"
83
+ ].some((type) => contentType.includes(type));
84
+ }
70
85
  //#endregion
71
86
  //#region src/utils/isObjectSubset.ts
72
87
  /**
@@ -109,39 +124,13 @@ function isPathMatch(pattern, path) {
109
124
  return regexp.test(path);
110
125
  }
111
126
  //#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
- };
127
+ //#region src/utils/matchScene.ts
128
+ function matchScene(activeScene, mockScene) {
129
+ if (!mockScene) return true;
130
+ const scenes = toArray(mockScene);
131
+ if (activeScene.length === 0 && scenes.length > 0) return false;
132
+ if (scenes.length === 0) return true;
133
+ return scenes.some((s) => activeScene.includes(s));
145
134
  }
146
135
  getDirname(import.meta.url);
147
136
  const vfs = createFsFromVolume(new Volume());
@@ -258,7 +247,7 @@ function keysCount(obj) {
258
247
  return objectKeys(obj).length;
259
248
  }
260
249
  //#endregion
261
- //#region src/core/cors.ts
250
+ //#region src/mockHttp/cors.ts
262
251
  /**
263
252
  * Create CORS middleware
264
253
  *
@@ -274,11 +263,19 @@ function createCors(corsOptions) {
274
263
  })) : void 0;
275
264
  }
276
265
  //#endregion
277
- //#region src/core/request.ts
266
+ //#region src/mockHttp/request.ts
278
267
  /**
268
+ * Parse request body
269
+ *
279
270
  * 解析请求体 request.body
271
+ *
272
+ * @param req - Incoming message object / 入站消息对象
273
+ * @param logger - Logger instance / 日志实例
274
+ * @param formidableOptions - Formidable options for multipart form data / 用于 multipart 表单数据的 Formidable 配置项
275
+ * @param bodyParserOptions - Body parser options / 请求体解析配置项
276
+ * @returns Parsed request body / 解析后的请求体
280
277
  */
281
- async function parseRequestBody(req, formidableOptions, bodyParserOptions = {}) {
278
+ async function parseRequestBody(req, logger, formidableOptions, bodyParserOptions = {}) {
282
279
  const method = req.method.toUpperCase();
283
280
  if (["HEAD", "OPTIONS"].includes(method)) return void 0;
284
281
  const type = req.headers["content-type"]?.toLocaleLowerCase() || "";
@@ -298,9 +295,14 @@ async function parseRequestBody(req, formidableOptions, bodyParserOptions = {})
298
295
  });
299
296
  if (type.startsWith("multipart/form-data")) return await parseRequestBodyWithMultipart(req, formidableOptions);
300
297
  } catch (e) {
301
- console.error(e);
298
+ logger.error(e);
302
299
  }
303
300
  }
301
+ /**
302
+ * Default formidable options
303
+ *
304
+ * 默认的 formidable 配置项
305
+ */
304
306
  const DEFAULT_FORMIDABLE_OPTIONS = {
305
307
  keepExtensions: true,
306
308
  filename(name, ext, part) {
@@ -308,7 +310,13 @@ const DEFAULT_FORMIDABLE_OPTIONS = {
308
310
  }
309
311
  };
310
312
  /**
313
+ * Parse request body with multipart form data
314
+ *
311
315
  * 解析 request form multipart body
316
+ *
317
+ * @param req - Incoming message object / 入站消息对象
318
+ * @param options - Formidable options / Formidable 配置项
319
+ * @returns Parsed request body / 解析后的请求体
312
320
  */
313
321
  async function parseRequestBodyWithMultipart(req, options) {
314
322
  const form = formidable({
@@ -328,9 +336,20 @@ async function parseRequestBodyWithMultipart(req, options) {
328
336
  });
329
337
  });
330
338
  }
339
+ /**
340
+ * Cache for path-to-regexp match functions
341
+ *
342
+ * path-to-regexp 匹配函数缓存
343
+ */
331
344
  const matcherCache = /* @__PURE__ */ new Map();
332
345
  /**
346
+ * Parse request URL dynamic parameters
347
+ *
333
348
  * 解析请求 url 中的动态参数 params
349
+ *
350
+ * @param pattern - URL pattern / URL 模式
351
+ * @param url - Request URL / 请求 URL
352
+ * @returns Parsed parameters / 解析后的参数
334
353
  */
335
354
  function parseRequestParams(pattern, url) {
336
355
  let matcher = matcherCache.get(pattern);
@@ -342,15 +361,40 @@ function parseRequestParams(pattern, url) {
342
361
  return matched ? matched.params : {};
343
362
  }
344
363
  /**
364
+ * Validate request against validator
365
+ *
345
366
  * 验证请求是否符合 validator
367
+ *
368
+ * @param request - Request object / 请求对象
369
+ * @param validator - Validator object / 验证器对象
370
+ * @returns Whether the request is valid / 请求是否有效
346
371
  */
347
372
  function requestValidate(request, validator) {
348
373
  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
374
  }
375
+ /**
376
+ * Format log data
377
+ *
378
+ * 格式化日志数据
379
+ *
380
+ * @param prefix - Log prefix / 日志前缀
381
+ * @param data - Data to format / 要格式化的数据
382
+ * @returns Formatted log string / 格式化后的日志字符串
383
+ */
350
384
  function formatLog(prefix, data) {
351
385
  return !data || isEmptyObject(data) ? "" : ` ${ansis.gray(`${prefix}:`)}${JSON.stringify(data)}`;
352
386
  }
353
- function requestLog(request, filepath) {
387
+ /**
388
+ * Generate request log
389
+ *
390
+ * 生成请求日志
391
+ *
392
+ * @param request - Request object / 请求对象
393
+ * @param filepath - Mock file path / Mock 文件路径
394
+ * @param shouldSimulateError - Whether to simulate error / 是否模拟错误
395
+ * @returns Formatted log string / 格式化后的日志字符串
396
+ */
397
+ function requestLog(request, filepath, shouldSimulateError) {
354
398
  const { url, method, query, params, body } = request;
355
399
  let { pathname } = new URL(url, "http://example.com");
356
400
  pathname = ansis.green(decodeURIComponent(pathname));
@@ -358,18 +402,31 @@ function requestLog(request, filepath) {
358
402
  const qs = formatLog("query", query);
359
403
  const ps = formatLog("params", params);
360
404
  const bs = formatLog("body", body);
405
+ const es = shouldSimulateError ? ` 🎲 ${ansis.bgYellow("ERR")}` : "";
361
406
  const file = ` ${ansis.dim.underline(`(${filepath})`)}`;
362
- return `${ms} ${pathname}${qs}${ps}${bs}${file}`;
407
+ return `${ms}${es} ${pathname}${qs}${ps}${bs}${file}`;
363
408
  }
364
409
  //#endregion
365
- //#region src/core/findMockData.ts
410
+ //#region src/mockHttp/matcher.ts
366
411
  /**
412
+ * Find matching mock data
413
+ *
367
414
  * 查找匹配的 mock data
415
+ *
416
+ * @param mockList - Mock options list / Mock 配置列表
417
+ * @param logger - Logger instance / 日志实例
418
+ * @param options - Find options / 查找选项
419
+ * @param options.pathname - Request pathname / 请求路径
420
+ * @param options.method - HTTP method / HTTP 方法
421
+ * @param options.request - Request object / 请求对象
422
+ * @param options.activeScene - Active scene / 当前场景
423
+ * @returns Matched mock HTTP item or undefined / 匹配的 Mock HTTP 项或未定义
368
424
  */
369
- function findMockData(mockList, logger, { pathname, method, request }) {
425
+ function findMockData(mockList, logger, { pathname, method, request, activeScene }) {
370
426
  return mockList.find((mock) => {
371
427
  if (!pathname || !mock || !mock.url || mock.ws) return false;
372
428
  if (!(mock.method ? isArray(mock.method) ? mock.method : [mock.method] : ["GET", "POST"]).includes(method)) return false;
429
+ if (!matchScene(activeScene, mock.scene)) return false;
373
430
  const hasMock = isPathMatch(mock.url, pathname);
374
431
  if (hasMock && mock.validator) {
375
432
  const params = parseRequestParams(mock.url, pathname);
@@ -377,15 +434,17 @@ function findMockData(mockList, logger, { pathname, method, request }) {
377
434
  params,
378
435
  ...request
379
436
  });
380
- else try {
381
- return requestValidate({
437
+ else {
438
+ const [error, validated] = attempt(requestValidate, {
382
439
  params,
383
440
  ...request
384
441
  }, 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;
442
+ if (error) {
443
+ const file = mock.__filepath__;
444
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${error}\n at validator (${ansis.underline(file)})`, mock.log);
445
+ return false;
446
+ }
447
+ return validated;
389
448
  }
390
449
  }
391
450
  return hasMock;
@@ -596,7 +655,412 @@ function pushCookie(headers, cookie) {
596
655
  headers.push(cookie.toHeader());
597
656
  }
598
657
  //#endregion
599
- //#region src/core/matchingWeight.ts
658
+ //#region src/recorder/constants.ts
659
+ const FILTERED_RESPONSE_HEADERS = [
660
+ "date",
661
+ "expires",
662
+ "last-modified",
663
+ "server",
664
+ "x-powered-by",
665
+ "x-aspnet-version",
666
+ "x-nginx-version",
667
+ "via",
668
+ "cache-control",
669
+ "etag",
670
+ "age",
671
+ "connection",
672
+ "keep-alive",
673
+ "proxy-authenticate",
674
+ "proxy-authorization",
675
+ "proxy-connection",
676
+ "trailer",
677
+ "access-control-allow-origin",
678
+ "access-control-allow-credentials",
679
+ "access-control-allow-methods",
680
+ "access-control-allow-headers",
681
+ "access-control-expose-headers",
682
+ "access-control-max-age",
683
+ "origin",
684
+ "p3p",
685
+ "pragma",
686
+ "x-request-id",
687
+ "x-correlation-id",
688
+ "x-trace-id",
689
+ "x-varnish",
690
+ "x-cache",
691
+ "x-cache-hits",
692
+ "x-cache-status",
693
+ "cf-cache-status",
694
+ "cf-ray",
695
+ "cf-request-id",
696
+ "server-timing",
697
+ "x-dns-prefetch-control"
698
+ ];
699
+ //#endregion
700
+ //#region src/recorder/decompress.ts
701
+ /**
702
+ * Decode response body according to encoding
703
+ *
704
+ * 根据编码解码响应体
705
+ *
706
+ * @param rawBody 原始响应体
707
+ * @param encoding 编码
708
+ * @returns 解码后的响应体
709
+ */
710
+ async function decompressBody(rawBody, encoding) {
711
+ try {
712
+ switch (encoding.toLowerCase()) {
713
+ case "gzip":
714
+ case "x-gzip": return {
715
+ body: await gunzip(rawBody),
716
+ encoding: "identity"
717
+ };
718
+ case "deflate":
719
+ case "x-deflate": return {
720
+ body: await deflate(rawBody),
721
+ encoding: "identity"
722
+ };
723
+ case "br": return {
724
+ body: await brotli(rawBody),
725
+ encoding: "identity"
726
+ };
727
+ case "zstd": return {
728
+ body: await zstd(rawBody),
729
+ encoding: "identity"
730
+ };
731
+ }
732
+ } catch {}
733
+ return {
734
+ body: rawBody,
735
+ encoding
736
+ };
737
+ }
738
+ let zstdStreaming = null;
739
+ /**
740
+ * Decompress zstd compressed data
741
+ *
742
+ * 解压缩 zstd 压缩数据
743
+ *
744
+ * zlib.zstdDecompress 从 v22.15.0 开始支持,对于旧版本 Node.js 可以使用 zstd-codec 库
745
+ *
746
+ * @param rawBody 压缩数据
747
+ * @returns 解压缩后的数据
748
+ */
749
+ async function zstd(rawBody) {
750
+ if (zlib.zstdDecompress) return new Promise((resolve, reject) => {
751
+ zlib.zstdDecompress(rawBody, (err, data) => {
752
+ err ? reject(err) : resolve(data);
753
+ });
754
+ });
755
+ if (!zstdStreaming) {
756
+ const { ZstdCodec } = await import("zstd-codec");
757
+ zstdStreaming = await new Promise((resolve) => {
758
+ ZstdCodec.run((binding) => {
759
+ resolve(new binding.Streaming());
760
+ });
761
+ });
762
+ }
763
+ return zstdStreaming.decompress(rawBody, rawBody.length);
764
+ }
765
+ /**
766
+ * Decompress brotli compressed data
767
+ *
768
+ * 解压缩 brotli 压缩数据
769
+ *
770
+ * @param rawBody 压缩数据
771
+ * @returns 解压缩后的数据
772
+ */
773
+ async function brotli(rawBody) {
774
+ return new Promise((resolve, reject) => {
775
+ zlib.brotliDecompress(rawBody, (err, data) => {
776
+ err ? reject(err) : resolve(data);
777
+ });
778
+ });
779
+ }
780
+ async function gunzip(rawBody) {
781
+ return new Promise((resolve, reject) => {
782
+ zlib.gunzip(rawBody, (err, data) => {
783
+ err ? reject(err) : resolve(data);
784
+ });
785
+ });
786
+ }
787
+ async function deflate(rawBody) {
788
+ return new Promise((resolve, reject) => {
789
+ zlib.inflate(rawBody, (err, data) => {
790
+ err ? reject(err) : resolve(data);
791
+ });
792
+ });
793
+ }
794
+ //#endregion
795
+ //#region src/recorder/helper.ts
796
+ const timeFormatter = new Intl.DateTimeFormat("en-US", {
797
+ year: "numeric",
798
+ month: "numeric",
799
+ day: "numeric",
800
+ hour: "numeric",
801
+ minute: "numeric",
802
+ second: "numeric",
803
+ hour12: false
804
+ });
805
+ /**
806
+ * 处理记录的请求
807
+ *
808
+ * @param req 原始请求对象
809
+ * @param pathname 请求路径
810
+ * @param body 请求体
811
+ * @returns 处理后的请求记录
812
+ */
813
+ function processRecordReq(req, pathname, body) {
814
+ const { query } = urlParse(req.url);
815
+ const method = req.method.toUpperCase();
816
+ let bodyType = (req.headers["content-type"] || "").split(";")[0].trim();
817
+ if (bodyType.startsWith("multipart/form-data") && isPlainObject(body)) {
818
+ body = { ...body };
819
+ objectKeys(body).forEach((key) => {
820
+ const value = body[key];
821
+ if (isPlainObject(value) && hasOwn(value, "filepath") && hasOwn(value, "mimetype")) delete body[key];
822
+ });
823
+ }
824
+ if (Buffer.isBuffer(body)) {
825
+ body = body.toString();
826
+ bodyType = "buffer";
827
+ }
828
+ return {
829
+ method,
830
+ pathname,
831
+ query,
832
+ bodyType,
833
+ body
834
+ };
835
+ }
836
+ /**
837
+ * 处理记录的响应
838
+ *
839
+ * @param res 原始响应对象
840
+ * @param body 响应体
841
+ * @returns 处理后的响应记录
842
+ */
843
+ async function processRecordRes(res, body) {
844
+ const status = res.statusCode || 200;
845
+ const statusText = res.statusMessage || "OK";
846
+ const headers = {};
847
+ for (const [key, value] of Object.entries(res.headers)) {
848
+ const lowerKey = key.toLowerCase();
849
+ if (value !== void 0 && !FILTERED_RESPONSE_HEADERS.includes(lowerKey)) headers[key] = String(value);
850
+ }
851
+ const isText = isTextContent(headers["content-type"] || "");
852
+ if (isText) {
853
+ const { body: decodedBody, encoding } = await decompressBody(body, headers["content-encoding"] || "");
854
+ body = Buffer.from(decodedBody);
855
+ headers["content-encoding"] = encoding;
856
+ }
857
+ return {
858
+ status,
859
+ statusText,
860
+ headers,
861
+ body: body.toString(isText ? "utf-8" : "base64")
862
+ };
863
+ }
864
+ /**
865
+ * 判断两个请求是否是同一个请求
866
+ *
867
+ * @param prev 上一个请求
868
+ * @param current 当前请求
869
+ * @returns 是否是同一个请求
870
+ */
871
+ function isSameRecord(prev, current) {
872
+ if (prev.pathname !== current.pathname || prev.method !== current.method) return false;
873
+ if (prev.bodyType !== current.bodyType) return false;
874
+ if (!deepEqual(prev.query, current.query)) return false;
875
+ if (current.bodyType === "buffer" && prev.bodyType === "buffer") {
876
+ const currentBody = Buffer.from(current.body);
877
+ const prevBody = Buffer.from(prev.body);
878
+ if (currentBody.length !== prevBody.length || !currentBody.equals(prevBody)) return false;
879
+ }
880
+ if (!deepEqual(prev.body, current.body)) return false;
881
+ return true;
882
+ }
883
+ /**
884
+ * 创建请求记录过滤器
885
+ *
886
+ * @param filter 记录过滤选项
887
+ * @returns 请求匹配函数
888
+ */
889
+ function createRecordMatcher(filter) {
890
+ if (isFunction(filter)) return filter;
891
+ const { mode = "glob" } = filter;
892
+ const include = toArray(filter.include);
893
+ const exclude = toArray(filter.exclude);
894
+ if (mode === "glob") {
895
+ const { isMatch } = createMatcher(include, exclude);
896
+ return (req) => isMatch(req.pathname);
897
+ }
898
+ return (req) => {
899
+ return include.some((pattern) => isPathMatch(pattern, req.pathname)) && exclude.every((pattern) => !isPathMatch(pattern, req.pathname));
900
+ };
901
+ }
902
+ /**
903
+ * 生成记录文件路径
904
+ *
905
+ * @param pathname 请求路径
906
+ * @param dir 记录目录
907
+ * @returns 记录文件路径
908
+ */
909
+ function getFilepath(pathname, dir) {
910
+ return path.join(dir, `${kebabCase(pathname)}.json`);
911
+ }
912
+ //#endregion
913
+ //#region src/recorder/storage.ts
914
+ const storage = /* @__PURE__ */ new Map();
915
+ /**
916
+ * 读取记录文件
917
+ *
918
+ * @param filepath 记录文件路径
919
+ * @returns 记录的请求数组
920
+ */
921
+ async function readRecordStorage(filepath) {
922
+ if (storage.has(filepath)) return storage.get(filepath);
923
+ try {
924
+ if (!fs.existsSync(filepath)) return [];
925
+ const content = await fs.promises.readFile(filepath, "utf-8") || "[]";
926
+ const data = JSON.parse(content);
927
+ storage.set(filepath, data);
928
+ return data;
929
+ } catch (error) {
930
+ console.error(`Error reading record file ${filepath}:`, error);
931
+ return [];
932
+ }
933
+ }
934
+ /**
935
+ * 写入记录文件
936
+ *
937
+ * @param filepath 记录文件路径
938
+ * @param records 记录的请求数组
939
+ */
940
+ async function writeRecordStorage(filepath, records) {
941
+ try {
942
+ storage.set(filepath, records);
943
+ await fs.promises.mkdir(path.dirname(filepath), { recursive: true });
944
+ await fs.promises.writeFile(filepath, JSON.stringify(records, null, 2), "utf-8");
945
+ } catch (error) {
946
+ console.error(`Error writing record file ${filepath}:`, error);
947
+ }
948
+ }
949
+ const originalReqCache = /* @__PURE__ */ new WeakMap();
950
+ /**
951
+ * Record a request with the raw request object.
952
+ *
953
+ * 记录原始请求对象的请求
954
+ *
955
+ * @param req The original request object / 原始请求对象
956
+ * @param pathname The request pathname / 请求路径名
957
+ * @param body The request body / 请求体
958
+ */
959
+ function recordRequestWithRawReq(req, pathname, body) {
960
+ originalReqCache.set(req, {
961
+ body,
962
+ pathname,
963
+ timestamp: Date.now()
964
+ });
965
+ }
966
+ //#endregion
967
+ //#region src/recorder/Recorder.ts
968
+ /**
969
+ * 请求记录器
970
+ */
971
+ var Recorder = class {
972
+ options;
973
+ filter;
974
+ constructor(options) {
975
+ this.options = options;
976
+ this.filter = createRecordMatcher(options.filter);
977
+ this.addGitignore();
978
+ }
979
+ getPlugin() {
980
+ return (proxyServer) => {
981
+ proxyServer.on("proxyRes", (proxyRes, req) => {
982
+ let chunks = [];
983
+ proxyRes.on("data", (chunk) => chunk && chunks.push(chunk));
984
+ proxyRes.on("end", () => {
985
+ this.record(req, proxyRes, Buffer.concat(chunks));
986
+ chunks = null;
987
+ });
988
+ });
989
+ };
990
+ }
991
+ async record(req, res, resBody) {
992
+ if (!originalReqCache.has(req)) return;
993
+ const { body, pathname, timestamp } = originalReqCache.get(req);
994
+ originalReqCache.delete(req);
995
+ if (!pathname) return;
996
+ const recordReq = processRecordReq(req, pathname, body);
997
+ if (!this.filter(recordReq)) return;
998
+ const { cwd, dir, status, expires, overwrite } = this.options;
999
+ if (status.length !== 0 && !status.includes(res.statusCode || 200)) return;
1000
+ const record = {
1001
+ meta: {
1002
+ timestamp,
1003
+ filepath: "",
1004
+ createAt: timeFormatter.format(timestamp),
1005
+ referer: req.headers.referer || "unknown"
1006
+ },
1007
+ req: recordReq,
1008
+ res: await processRecordRes(res, resBody)
1009
+ };
1010
+ const filepath = getFilepath(pathname, dir);
1011
+ record.meta.filepath = filepath;
1012
+ const absoluteFilepath = path.join(cwd, filepath);
1013
+ const records = (await readRecordStorage(absoluteFilepath)).filter((item) => timestamp - item.meta.timestamp <= expires);
1014
+ const index = records.findIndex((item) => isSameRecord(item.req, record.req) && item.res.status === record.res.status);
1015
+ if (index === -1) records.push(record);
1016
+ else if (overwrite || timestamp - records[index].meta.timestamp > expires) records[index] = record;
1017
+ await writeRecordStorage(absoluteFilepath, records);
1018
+ }
1019
+ async addGitignore() {
1020
+ const options = this.options;
1021
+ if (!options.gitignore) return;
1022
+ const dirname = path.join(options.cwd, options.dir);
1023
+ await promises.mkdir(dirname, { recursive: true });
1024
+ if (!fs.existsSync(path.join(dirname, ".gitignore"))) await promises.writeFile(path.join(dirname, ".gitignore"), "*\n", "utf-8");
1025
+ }
1026
+ };
1027
+ //#endregion
1028
+ //#region src/recorder/replay.ts
1029
+ /**
1030
+ * Replay a recorded request.
1031
+ *
1032
+ * 重放已记录的请求
1033
+ *
1034
+ * @param rawReq The original request object / 原始请求对象
1035
+ * @param pathname The request pathname / 请求路径名
1036
+ * @param body The request body / 请求体
1037
+ * @param options Record options / 录制配置项
1038
+ * @returns The recorded request object if found, otherwise undefined / 如果找到记录的请求对象,则返回该对象,否则返回 undefined
1039
+ */
1040
+ async function replayRecordedRequest(rawReq, pathname, body, options) {
1041
+ const req = processRecordReq(rawReq, pathname, body);
1042
+ const filepath = path.join(options.cwd, getFilepath(req.pathname, options.dir));
1043
+ const timestamp = Date.now();
1044
+ const records = await readRecordStorage(filepath);
1045
+ const matchedList = records.filter((item) => timestamp - item.meta.timestamp < options.expires && isSameRecord(item.req, req));
1046
+ let matched;
1047
+ if (options.status.length === 0) matched = matchedList.find((item) => item.res.status === 200) || records[0];
1048
+ else matched = matchedList.find((item) => options.status.includes(item.res.status));
1049
+ if (matched) {
1050
+ const isText = isTextContent(matched.res.headers["content-type"] || "");
1051
+ return {
1052
+ url: matched.req.pathname,
1053
+ status: matched.res.status,
1054
+ statusText: matched.res.statusText,
1055
+ headers: matched.res.headers,
1056
+ body: Buffer.from(matched.res.body, isText ? "utf-8" : "base64"),
1057
+ type: "buffer",
1058
+ __filepath__: matched.meta.filepath
1059
+ };
1060
+ }
1061
+ }
1062
+ //#endregion
1063
+ //#region src/mockHttp/matchingWeight.ts
600
1064
  const tokensCache = {};
601
1065
  function getTokens(rule) {
602
1066
  if (tokensCache[rule]) return tokensCache[rule];
@@ -683,7 +1147,7 @@ function matchingWeight(rules, url, priority) {
683
1147
  return matched;
684
1148
  }
685
1149
  //#endregion
686
- //#region src/core/requestRecovery.ts
1150
+ //#region src/mockHttp/requestRecovery.ts
687
1151
  /**
688
1152
  * 请求复原
689
1153
  *
@@ -694,10 +1158,10 @@ function matchingWeight(rules, url, priority) {
694
1158
  const requestCollectCache = /* @__PURE__ */ new WeakMap();
695
1159
  function collectRequest(req) {
696
1160
  const chunks = [];
697
- req.addListener("data", (chunk) => {
1161
+ req.on("data", (chunk) => {
698
1162
  chunks.push(Buffer.from(chunk));
699
1163
  });
700
- req.addListener("end", () => {
1164
+ req.on("end", () => {
701
1165
  if (chunks.length) requestCollectCache.set(req, Buffer.concat(chunks));
702
1166
  });
703
1167
  }
@@ -710,22 +1174,40 @@ function rewriteRequest(proxyReq, req) {
710
1174
  }
711
1175
  }
712
1176
  //#endregion
713
- //#region src/core/response.ts
1177
+ //#region src/mockHttp/response.ts
714
1178
  /**
1179
+ * Get HTTP status text by status code
1180
+ *
715
1181
  * 根据状态码获取状态文本
1182
+ *
1183
+ * @param status - HTTP status code / HTTP 状态码
1184
+ * @returns HTTP status text / HTTP 状态文本
716
1185
  */
717
1186
  function getHTTPStatusText(status) {
718
1187
  return HTTP_STATUS[status] || "Unknown";
719
1188
  }
720
1189
  /**
1190
+ * Set response status
1191
+ *
721
1192
  * 设置响应状态
1193
+ *
1194
+ * @param response - Response object / 响应对象
1195
+ * @param status - HTTP status code / HTTP 状态码
1196
+ * @param statusText - HTTP status text / HTTP 状态文本
722
1197
  */
723
1198
  function provideResponseStatus(response, status = 200, statusText) {
724
1199
  response.statusCode = status;
725
1200
  response.statusMessage = statusText || getHTTPStatusText(status);
726
1201
  }
727
1202
  /**
1203
+ * Set response headers
1204
+ *
728
1205
  * 设置响应头
1206
+ *
1207
+ * @param req - Request object / 请求对象
1208
+ * @param res - Response object / 响应对象
1209
+ * @param mock - Mock HTTP item / Mock HTTP 配置项
1210
+ * @param logger - Logger instance / 日志实例
729
1211
  */
730
1212
  async function provideResponseHeaders(req, res, mock, logger) {
731
1213
  const { headers, type = "json" } = mock;
@@ -736,37 +1218,46 @@ async function provideResponseHeaders(req, res, mock, logger) {
736
1218
  res.setHeader("X-Mock-Power-By", "vite-plugin-mock-dev-server");
737
1219
  if (filepath) res.setHeader("X-File-Path", filepath);
738
1220
  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);
1221
+ const [error, data] = await attemptAsync(async () => isFunction(headers) ? await headers(req) : headers);
1222
+ if (error) {
1223
+ logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${error}\n at headers (${ansis.underline(filepath)})`, mock.log);
1224
+ return;
746
1225
  }
1226
+ objectKeys(data).forEach((key) => res.setHeader(key, data[key]));
747
1227
  }
748
1228
  /**
1229
+ * Set response cookies
1230
+ *
749
1231
  * 设置响应cookie
1232
+ *
1233
+ * @param req - Request object / 请求对象
1234
+ * @param res - Response object / 响应对象
1235
+ * @param mock - Mock HTTP item / Mock HTTP 配置项
1236
+ * @param logger - Logger instance / 日志实例
750
1237
  */
751
1238
  async function provideResponseCookies(req, res, mock, logger) {
752
1239
  const { cookies } = mock;
753
- const filepath = mock.__filepath__;
754
1240
  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);
1241
+ const [error, data] = await attemptAsync(async () => isFunction(cookies) ? await cookies(req) : cookies);
1242
+ if (error) {
1243
+ const filepath = mock.__filepath__;
1244
+ logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${error}\n at cookies (${ansis.underline(filepath)})`, mock.log);
1245
+ return;
766
1246
  }
1247
+ objectKeys(data).forEach((key) => {
1248
+ const cookie = data[key];
1249
+ const [value, options] = isArray(cookie) ? cookie : [cookie];
1250
+ res.setCookie(key, value, options);
1251
+ });
767
1252
  }
768
1253
  /**
1254
+ * Send response data
1255
+ *
769
1256
  * 设置响应数据
1257
+ *
1258
+ * @param res - Response object / 响应对象
1259
+ * @param raw - Response body data / 响应体数据
1260
+ * @param type - Response data type / 响应数据类型
770
1261
  */
771
1262
  function sendResponseData(res, raw, type) {
772
1263
  if (isReadableStream(raw)) raw.pipe(res);
@@ -777,7 +1268,12 @@ function sendResponseData(res, raw, type) {
777
1268
  }
778
1269
  }
779
1270
  /**
1271
+ * Apply real response delay
1272
+ *
780
1273
  * 实际响应延迟
1274
+ *
1275
+ * @param startTime - Request start time / 请求开始时间
1276
+ * @param delay - Delay configuration / 延迟配置
781
1277
  */
782
1278
  async function responseRealDelay(startTime, delay) {
783
1279
  if (!delay || typeof delay === "number" && delay <= 0 || isArray(delay) && delay.length !== 2) return;
@@ -789,8 +1285,28 @@ async function responseRealDelay(startTime, delay) {
789
1285
  if (realDelay > 0) await sleep(realDelay);
790
1286
  }
791
1287
  //#endregion
792
- //#region src/core/mockMiddleware.ts
793
- function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions = {}, proxies, cookiesOptions, logger, priority = {}, cors: corsOptions }) {
1288
+ //#region src/mockHttp/middleware.ts
1289
+ /**
1290
+ * Create mock middleware
1291
+ *
1292
+ * 创建 Mock 中间件
1293
+ *
1294
+ * @param compiler - Compiler instance / 编译器实例
1295
+ * @param options - Middleware options / 中间件配置项
1296
+ * @param options.formidableOptions - Formidable options / Formidable 配置项
1297
+ * @param options.bodyParserOptions - Body parser options / 请求体解析配置项
1298
+ * @param options.proxies - Proxy paths / 代理路径
1299
+ * @param options.cookiesOptions - Cookies options / Cookies 配置项
1300
+ * @param options.logger - Logger instance / 日志实例
1301
+ * @param options.priority - Path matching priority / 路径匹配优先级
1302
+ * @param options.cors - CORS options / CORS 配置项
1303
+ * @param options.record - Record options / 录制配置项
1304
+ * @param options.replay - Replay options / 回放配置项
1305
+ * @param options.activeScene - Active scene / 活动场景
1306
+ *
1307
+ * @returns Connect middleware function / Connect 中间件函数
1308
+ */
1309
+ function createMockMiddleware(compiler, { formidableOptions = {}, bodyParserOptions = {}, proxies, cookiesOptions, logger, priority = {}, cors: corsOptions, record, replay, activeScene }) {
794
1310
  const cors = createCors(corsOptions);
795
1311
  const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
796
1312
  const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
@@ -802,35 +1318,38 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
802
1318
  if (globFilter.length && !isGlobProxiesMatch(pathname)) return next();
803
1319
  const mockData = compiler.mockData;
804
1320
  const mockUrls = matchingWeight(Object.keys(mockData), pathname, priority);
805
- if (mockUrls.length === 0) return next();
1321
+ if (mockUrls.length === 0 && !record.enabled) return next();
806
1322
  collectRequest(req);
807
- const { query: refererQuery } = urlParse(req.headers.referer || "");
808
- const reqBody = await parseRequestBody(req, formidableOptions, bodyParserOptions);
809
1323
  const cookies = new Cookies(req, res, cookiesOptions);
810
- const getCookie = cookies.get.bind(cookies);
811
- const method = req.method.toUpperCase();
812
1324
  let mock;
813
1325
  let _mockUrl;
1326
+ const method = req.method.toUpperCase();
1327
+ const extraReq = {
1328
+ query,
1329
+ refererQuery: urlParse(req.headers.referer || "").query,
1330
+ body: await parseRequestBody(req, logger, formidableOptions, bodyParserOptions),
1331
+ headers: req.headers,
1332
+ getCookie: cookies.get.bind(cookies)
1333
+ };
1334
+ const headerScene = req.headers["x-mock-scene"];
1335
+ const effectiveScene = headerScene ? toArray(headerScene).map((item) => item.split(",").map((s) => s.trim())).flat().filter(Boolean) : activeScene;
814
1336
  for (const mockUrl of mockUrls) {
815
1337
  mock = findMockData(mockData[mockUrl], logger, {
816
1338
  pathname,
817
1339
  method,
818
- request: {
819
- query,
820
- refererQuery,
821
- body: reqBody,
822
- headers: req.headers,
823
- getCookie
824
- }
1340
+ request: extraReq,
1341
+ activeScene: effectiveScene
825
1342
  });
826
1343
  if (mock) {
827
1344
  _mockUrl = mockUrl;
828
1345
  break;
829
1346
  }
830
1347
  }
1348
+ if (replay && !mock) mock = await replayRecordedRequest(req, pathname, extraReq.body, record);
831
1349
  if (!mock) {
1350
+ record.enabled && recordRequestWithRawReq(req, pathname, extraReq.body);
832
1351
  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.`);
1352
+ matched.length && logger.warn(`${ansis.green(pathname)} matches ${matched}, but mock data is not found.`);
834
1353
  return next();
835
1354
  }
836
1355
  if (cors) {
@@ -842,36 +1361,42 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
842
1361
  }
843
1362
  const request = req;
844
1363
  const response = res;
845
- request.body = reqBody;
846
- request.query = query;
847
- request.refererQuery = refererQuery;
1364
+ Object.assign(request, extraReq);
848
1365
  request.params = parseRequestParams(mock.url, pathname);
849
- request.getCookie = getCookie;
850
1366
  response.setCookie = cookies.set.bind(cookies);
851
- const { body, delay, type = "json", response: responseFn, status = 200, statusText, log: logLevel, __filepath__: filepath } = mock;
1367
+ const { delay, type = "json", response: responseFn, log: logLevel, error: errorConfig, __filepath__: filepath } = mock;
1368
+ let { body, status = 200, statusText } = mock;
1369
+ const shouldSimulateError = errorConfig && (errorConfig.probability ?? .5) > Math.random();
1370
+ if (shouldSimulateError) {
1371
+ status = errorConfig.status ?? 500;
1372
+ statusText = errorConfig.statusText;
1373
+ body = errorConfig.body;
1374
+ }
852
1375
  provideResponseStatus(response, status, statusText);
853
1376
  await provideResponseHeaders(request, response, mock, logger);
854
1377
  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`);
1378
+ logger.info(requestLog(request, filepath, shouldSimulateError), logLevel);
1379
+ logger.debug(`${ansis.magenta("DEBUG")} ${ansis.underline(pathname)} matches: [ ${mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ")} ]\n`);
857
1380
  if (body) {
858
- try {
1381
+ const [error] = await attemptAsync(async () => {
859
1382
  const content = isFunction(body) ? await body(request) : body;
860
1383
  await responseRealDelay(startTime, delay);
861
1384
  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);
1385
+ });
1386
+ if (error) {
1387
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at body (${ansis.underline.gray(filepath)})`, logLevel);
864
1388
  provideResponseStatus(response, 500);
865
1389
  res.end("");
866
1390
  }
867
1391
  return;
868
1392
  }
869
1393
  if (responseFn) {
870
- try {
1394
+ const [error] = await attemptAsync(async () => {
871
1395
  await responseRealDelay(startTime, delay);
872
1396
  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);
1397
+ });
1398
+ if (error) {
1399
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at response (${ansis.underline.gray(filepath)})`, logLevel);
875
1400
  provideResponseStatus(response, 500);
876
1401
  res.end("");
877
1402
  }
@@ -881,7 +1406,7 @@ function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions =
881
1406
  };
882
1407
  }
883
1408
  //#endregion
884
- //#region src/core/ws.ts
1409
+ //#region src/mockWebsocket/server.ts
885
1410
  function mockWebSocket(compiler, httpServer, { wsProxies: proxies, cookiesOptions, logger }) {
886
1411
  const hmrMap = /* @__PURE__ */ new Map();
887
1412
  const poolMap = /* @__PURE__ */ new Map();
@@ -1000,4 +1525,39 @@ function cleanupRunner(cleanupList) {
1000
1525
  while (cleanup = cleanupList.shift()) cleanup?.();
1001
1526
  }
1002
1527
  //#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 };
1528
+ //#region src/core/logger.ts
1529
+ const logLevels = {
1530
+ silent: 0,
1531
+ error: 1,
1532
+ warn: 2,
1533
+ info: 3,
1534
+ debug: 4
1535
+ };
1536
+ function createLogger(prefix, defaultLevel = "info") {
1537
+ prefix = `[${prefix}]`;
1538
+ function output(type, msg, level) {
1539
+ level = isBoolean(level) ? level ? defaultLevel : "error" : level;
1540
+ if (logLevels[level] >= logLevels[type]) {
1541
+ const method = type === "info" || type === "debug" ? "log" : type;
1542
+ const tag = type === "debug" ? ansis.magenta.bold(prefix) : type === "info" ? ansis.cyan.bold(prefix) : type === "warn" ? ansis.yellow.bold(prefix) : ansis.red.bold(prefix);
1543
+ const format = `${ansis.dim((/* @__PURE__ */ new Date()).toLocaleTimeString())} ${tag} ${msg}`;
1544
+ console[method](format);
1545
+ }
1546
+ }
1547
+ return {
1548
+ debug(msg, level = defaultLevel) {
1549
+ output("debug", msg, level);
1550
+ },
1551
+ info(msg, level = defaultLevel) {
1552
+ output("info", msg, level);
1553
+ },
1554
+ warn(msg, level = defaultLevel) {
1555
+ output("warn", msg, level);
1556
+ },
1557
+ error(msg, level = defaultLevel) {
1558
+ output("error", msg, level);
1559
+ }
1560
+ };
1561
+ }
1562
+ //#endregion
1563
+ export { rewriteRequest as a, processRawData as c, normalizePath as d, vfs as f, createMatcher as h, createMockMiddleware as i, sortByValidator as l, getPackageDeps as m, logLevels as n, Recorder as o, getPackageDepList as p, mockWebSocket as r, processMockData as s, createLogger as t, waitingFor as u };