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.
- package/README.md +406 -65
- package/README.zh-CN.md +406 -66
- package/dist/helper.d.ts +46 -8
- package/dist/helper.js +109 -14
- package/dist/{types-GT6M6WuI.d.ts → index-BMMGM-eD.d.ts} +537 -172
- package/dist/index.d.ts +9 -6
- package/dist/index.js +70 -67
- package/dist/json5-loader.mjs +2 -2
- package/dist/{ws-BcLCWVaK.js → logger-CzeuHKAL.js} +662 -102
- package/dist/{options-Bd0PSZeT.js → options-CI0LRpJZ.js} +48 -7
- package/dist/rsbuild.d.ts +4 -3
- package/dist/rsbuild.js +29 -41
- package/dist/{server-_doRwsZm.d.ts → server-pC78IYdH.d.ts} +48 -24
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/package.json +8 -3
|
@@ -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/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
381
|
-
|
|
437
|
+
else {
|
|
438
|
+
const [error, validated] = attempt(requestValidate, {
|
|
382
439
|
params,
|
|
383
440
|
...request
|
|
384
441
|
}, mock.validator);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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/
|
|
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/
|
|
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.
|
|
1161
|
+
req.on("data", (chunk) => {
|
|
698
1162
|
chunks.push(Buffer.from(chunk));
|
|
699
1163
|
});
|
|
700
|
-
req.
|
|
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/
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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/
|
|
793
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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)}
|
|
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
|
-
|
|
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
|
-
}
|
|
863
|
-
|
|
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
|
-
|
|
1394
|
+
const [error] = await attemptAsync(async () => {
|
|
871
1395
|
await responseRealDelay(startTime, delay);
|
|
872
1396
|
await responseFn(request, response, next);
|
|
873
|
-
}
|
|
874
|
-
|
|
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/
|
|
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
|
-
|
|
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 };
|