rspack-plugin-mock 1.3.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.
@@ -0,0 +1,1563 @@
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";
4
+ import ansis from "ansis";
5
+ import picomatch from "picomatch";
6
+ import { loadPackageJSONSync } from "local-pkg";
7
+ import { match, parse, pathToRegexp } from "path-to-regexp";
8
+ import os from "node:os";
9
+ import { fileURLToPath } from "node:url";
10
+ import Debug from "debug";
11
+ import { Volume, createFsFromVolume } from "memfs";
12
+ import { parse as parse$1 } from "node:querystring";
13
+ import crypto from "node:crypto";
14
+ import cors from "cors";
15
+ import bodyParser from "co-body";
16
+ import formidable from "formidable";
17
+ import http from "node:http";
18
+ import { Buffer } from "node:buffer";
19
+ import zlib from "node:zlib";
20
+ import HTTP_STATUS from "http-status";
21
+ import * as mime from "mime-types";
22
+ import { WebSocketServer } from "ws";
23
+ //#region src/utils/createMatcher.ts
24
+ function createMatcher(include, exclude, defaultIgnore = true) {
25
+ const pattern = [];
26
+ const ignore = [...defaultIgnore ? ["**/node_modules/**"] : [], ...toArray(exclude)];
27
+ toArray(include).forEach((item) => {
28
+ if (item[0] === "!") ignore.push(item.slice(1));
29
+ else pattern.push(item);
30
+ });
31
+ return {
32
+ pattern,
33
+ ignore,
34
+ isMatch: picomatch(pattern, { ignore })
35
+ };
36
+ }
37
+ //#endregion
38
+ //#region src/utils/doesProxyContextMatchUrl.ts
39
+ const PATTERN_CACHE = /* @__PURE__ */ new Map();
40
+ function doesProxyContextMatchUrl(context, req) {
41
+ const url = req.url;
42
+ if (typeof context === "function") return context(url, req);
43
+ if (context[0] === "^") {
44
+ let pattern = PATTERN_CACHE.get(context);
45
+ if (!pattern) PATTERN_CACHE.set(context, pattern = new RegExp(context));
46
+ return pattern.test(url);
47
+ }
48
+ return url.startsWith(context);
49
+ }
50
+ //#endregion
51
+ //#region src/utils/getDeps.ts
52
+ function getPackageDeps(cwd) {
53
+ const { dependencies, devDependencies, peerDependencies, optionalDependencies } = loadPackageJSONSync(cwd) || {};
54
+ return {
55
+ ...dependencies,
56
+ ...devDependencies,
57
+ ...peerDependencies,
58
+ ...optionalDependencies
59
+ };
60
+ }
61
+ function getPackageDepList(cwd) {
62
+ return uniq(objectKeys(getPackageDeps(cwd)));
63
+ }
64
+ //#endregion
65
+ //#region src/utils/is.ts
66
+ function isStream(stream) {
67
+ return stream !== null && typeof stream === "object" && typeof stream.pipe === "function";
68
+ }
69
+ function isReadableStream(stream) {
70
+ return isStream(stream) && stream.readable !== false && typeof stream._read === "function" && typeof stream._readableState === "object";
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
+ }
85
+ //#endregion
86
+ //#region src/utils/isObjectSubset.ts
87
+ /**
88
+ * Checks if target object is a subset of source object.
89
+ * That is, all properties and their corresponding values in target exist in source.
90
+ *
91
+ * 深度比较两个对象之间,target 是否属于 source 的子集,
92
+ * 即 target 的所有属性和对应的值,都在 source 中,
93
+ */
94
+ function isObjectSubset(source, target) {
95
+ if (!target) return true;
96
+ for (const key in target) if (!isIncluded(source[key], target[key])) return false;
97
+ return true;
98
+ }
99
+ function isIncluded(source, target) {
100
+ if (isArray(source) && isArray(target)) {
101
+ const seen = /* @__PURE__ */ new Set();
102
+ return target.every((ti) => source.some((si, i) => {
103
+ if (seen.has(i)) return false;
104
+ const included = isIncluded(si, ti);
105
+ if (included) seen.add(i);
106
+ return included;
107
+ }));
108
+ }
109
+ if (isPlainObject(source) && isPlainObject(target)) return isObjectSubset(source, target);
110
+ return Object.is(source, target);
111
+ }
112
+ //#endregion
113
+ //#region src/utils/isPathMatch.ts
114
+ const cache = /* @__PURE__ */ new Map();
115
+ /**
116
+ * 判断 path 是否匹配 pattern
117
+ */
118
+ function isPathMatch(pattern, path) {
119
+ let regexp = cache.get(pattern);
120
+ if (!regexp) {
121
+ regexp = pathToRegexp(pattern).regexp;
122
+ cache.set(pattern, regexp);
123
+ }
124
+ return regexp.test(path);
125
+ }
126
+ //#endregion
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));
134
+ }
135
+ getDirname(import.meta.url);
136
+ const vfs = createFsFromVolume(new Volume());
137
+ function getDirname(importMetaUrl) {
138
+ return path.dirname(fileURLToPath(importMetaUrl));
139
+ }
140
+ Debug("vite:mock-dev-server");
141
+ const windowsSlashRE = /\\/g;
142
+ const isWindows = os.platform() === "win32";
143
+ function slash(p) {
144
+ return p.replace(windowsSlashRE, "/");
145
+ }
146
+ function normalizePath(id) {
147
+ return path.posix.normalize(isWindows ? slash(id) : id);
148
+ }
149
+ //#endregion
150
+ //#region src/utils/urlParse.ts
151
+ /**
152
+ * nodejs 从 19.0.0 开始 弃用 url.parse,因此使用 url.parse 来解析 可能会报错,
153
+ * 使用 URL 来解析
154
+ */
155
+ function urlParse(input) {
156
+ const url = new URL(input, "http://example.com");
157
+ return {
158
+ pathname: decodeURIComponent(url.pathname),
159
+ query: parse$1(url.search.replace(/^\?/, ""))
160
+ };
161
+ }
162
+ //#endregion
163
+ //#region src/utils/waitingFor.ts
164
+ function waitingFor(onSuccess, maxRetry = 5) {
165
+ return function wait(getter, retry = 0) {
166
+ const value = getter();
167
+ if (value) onSuccess(value);
168
+ else if (retry < maxRetry) setTimeout(() => wait(getter, retry + 1), 100);
169
+ };
170
+ }
171
+ //#endregion
172
+ //#region src/compiler/processData.ts
173
+ function processRawData(rawData) {
174
+ return rawData.filter((item) => item[0]).map(([raw, __filepath__]) => {
175
+ let mockConfig;
176
+ if (raw.default) if (isArray(raw.default)) mockConfig = raw.default.map((item) => ({
177
+ ...item,
178
+ __filepath__
179
+ }));
180
+ else mockConfig = {
181
+ ...raw.default,
182
+ __filepath__
183
+ };
184
+ else if ("url" in raw) mockConfig = {
185
+ ...raw,
186
+ __filepath__
187
+ };
188
+ else {
189
+ mockConfig = [];
190
+ objectKeys(raw || {}).forEach((key) => {
191
+ const data = raw[key];
192
+ if (isArray(data)) mockConfig.push(...data.map((item) => ({
193
+ ...item,
194
+ __filepath__
195
+ })));
196
+ else mockConfig.push({
197
+ ...data,
198
+ __filepath__
199
+ });
200
+ });
201
+ }
202
+ return mockConfig;
203
+ });
204
+ }
205
+ function processMockData(mockList) {
206
+ const list = [];
207
+ for (const [, handle] of mockList.entries()) if (handle) list.push(...toArray(handle));
208
+ const mocks = {};
209
+ list.filter((mock) => isPlainObject(mock) && mock.enabled !== false && mock.url).forEach((mock) => {
210
+ const { pathname, query } = urlParse(mock.url);
211
+ const list = mocks[pathname] ??= [];
212
+ const current = {
213
+ ...mock,
214
+ url: pathname
215
+ };
216
+ if (current.ws !== true) {
217
+ const validator = current.validator;
218
+ if (!isEmptyObject(query)) if (isFunction(validator)) current.validator = function(request) {
219
+ return isObjectSubset(request.query, query) && validator(request);
220
+ };
221
+ else if (validator) {
222
+ current.validator = { ...validator };
223
+ current.validator.query = current.validator.query ? {
224
+ ...query,
225
+ ...current.validator.query
226
+ } : query;
227
+ } else current.validator = { query };
228
+ }
229
+ list.push(current);
230
+ });
231
+ objectKeys(mocks).forEach((key) => {
232
+ mocks[key] = sortByValidator(mocks[key]);
233
+ });
234
+ return mocks;
235
+ }
236
+ function sortByValidator(mocks) {
237
+ return sortBy(mocks, (item) => {
238
+ if (item.ws === true) return 0;
239
+ const { validator } = item;
240
+ if (!validator || isEmptyObject(validator)) return 2;
241
+ if (isFunction(validator)) return 0;
242
+ return 1 / Object.keys(validator).reduce((prev, key) => prev + keysCount(validator[key]), 0);
243
+ });
244
+ }
245
+ function keysCount(obj) {
246
+ if (!obj) return 0;
247
+ return objectKeys(obj).length;
248
+ }
249
+ //#endregion
250
+ //#region src/mockHttp/cors.ts
251
+ /**
252
+ * Create CORS middleware
253
+ *
254
+ * 创建 CORS 中间件
255
+ *
256
+ * @param corsOptions - CORS options / CORS 配置项
257
+ * @returns CORS middleware function or undefined / CORS 中间件函数或未定义
258
+ */
259
+ function createCors(corsOptions) {
260
+ const corsMiddleware = corsOptions ? cors(corsOptions) : void 0;
261
+ return corsMiddleware ? (req, res) => new Promise((resolve, reject) => corsMiddleware(req, res, (err) => {
262
+ err ? reject(err) : resolve();
263
+ })) : void 0;
264
+ }
265
+ //#endregion
266
+ //#region src/mockHttp/request.ts
267
+ /**
268
+ * Parse request body
269
+ *
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 / 解析后的请求体
277
+ */
278
+ async function parseRequestBody(req, logger, formidableOptions, bodyParserOptions = {}) {
279
+ const method = req.method.toUpperCase();
280
+ if (["HEAD", "OPTIONS"].includes(method)) return void 0;
281
+ const type = req.headers["content-type"]?.toLocaleLowerCase() || "";
282
+ const { limit, formLimit, jsonLimit, textLimit, ...rest } = bodyParserOptions;
283
+ try {
284
+ if (type.startsWith("application/json")) return await bodyParser.json(req, {
285
+ limit: jsonLimit || limit,
286
+ ...rest
287
+ });
288
+ if (type.startsWith("application/x-www-form-urlencoded")) return await bodyParser.form(req, {
289
+ limit: formLimit || limit,
290
+ ...rest
291
+ });
292
+ if (type.startsWith("text/plain")) return await bodyParser.text(req, {
293
+ limit: textLimit || limit,
294
+ ...rest
295
+ });
296
+ if (type.startsWith("multipart/form-data")) return await parseRequestBodyWithMultipart(req, formidableOptions);
297
+ } catch (e) {
298
+ logger.error(e);
299
+ }
300
+ }
301
+ /**
302
+ * Default formidable options
303
+ *
304
+ * 默认的 formidable 配置项
305
+ */
306
+ const DEFAULT_FORMIDABLE_OPTIONS = {
307
+ keepExtensions: true,
308
+ filename(name, ext, part) {
309
+ return part?.originalFilename || `${name}.${Date.now()}${ext ? `.${ext}` : ""}`;
310
+ }
311
+ };
312
+ /**
313
+ * Parse request body with multipart form data
314
+ *
315
+ * 解析 request form multipart body
316
+ *
317
+ * @param req - Incoming message object / 入站消息对象
318
+ * @param options - Formidable options / Formidable 配置项
319
+ * @returns Parsed request body / 解析后的请求体
320
+ */
321
+ async function parseRequestBodyWithMultipart(req, options) {
322
+ const form = formidable({
323
+ ...DEFAULT_FORMIDABLE_OPTIONS,
324
+ ...options
325
+ });
326
+ return new Promise((resolve, reject) => {
327
+ form.parse(req, (error, fields, files) => {
328
+ if (error) {
329
+ reject(error);
330
+ return;
331
+ }
332
+ resolve({
333
+ ...fields,
334
+ ...files
335
+ });
336
+ });
337
+ });
338
+ }
339
+ /**
340
+ * Cache for path-to-regexp match functions
341
+ *
342
+ * path-to-regexp 匹配函数缓存
343
+ */
344
+ const matcherCache = /* @__PURE__ */ new Map();
345
+ /**
346
+ * Parse request URL dynamic parameters
347
+ *
348
+ * 解析请求 url 中的动态参数 params
349
+ *
350
+ * @param pattern - URL pattern / URL 模式
351
+ * @param url - Request URL / 请求 URL
352
+ * @returns Parsed parameters / 解析后的参数
353
+ */
354
+ function parseRequestParams(pattern, url) {
355
+ let matcher = matcherCache.get(pattern);
356
+ if (!matcher) {
357
+ matcher = match(pattern, { decode: decodeURIComponent });
358
+ matcherCache.set(pattern, matcher);
359
+ }
360
+ const matched = matcher(url);
361
+ return matched ? matched.params : {};
362
+ }
363
+ /**
364
+ * Validate request against validator
365
+ *
366
+ * 验证请求是否符合 validator
367
+ *
368
+ * @param request - Request object / 请求对象
369
+ * @param validator - Validator object / 验证器对象
370
+ * @returns Whether the request is valid / 请求是否有效
371
+ */
372
+ function requestValidate(request, validator) {
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);
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
+ */
384
+ function formatLog(prefix, data) {
385
+ return !data || isEmptyObject(data) ? "" : ` ${ansis.gray(`${prefix}:`)}${JSON.stringify(data)}`;
386
+ }
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) {
398
+ const { url, method, query, params, body } = request;
399
+ let { pathname } = new URL(url, "http://example.com");
400
+ pathname = ansis.green(decodeURIComponent(pathname));
401
+ const ms = ansis.magenta.bold(method);
402
+ const qs = formatLog("query", query);
403
+ const ps = formatLog("params", params);
404
+ const bs = formatLog("body", body);
405
+ const es = shouldSimulateError ? ` 🎲 ${ansis.bgYellow("ERR")}` : "";
406
+ const file = ` ${ansis.dim.underline(`(${filepath})`)}`;
407
+ return `${ms}${es} ${pathname}${qs}${ps}${bs}${file}`;
408
+ }
409
+ //#endregion
410
+ //#region src/mockHttp/matcher.ts
411
+ /**
412
+ * Find matching mock data
413
+ *
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 项或未定义
424
+ */
425
+ function findMockData(mockList, logger, { pathname, method, request, activeScene }) {
426
+ return mockList.find((mock) => {
427
+ if (!pathname || !mock || !mock.url || mock.ws) return false;
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;
430
+ const hasMock = isPathMatch(mock.url, pathname);
431
+ if (hasMock && mock.validator) {
432
+ const params = parseRequestParams(mock.url, pathname);
433
+ if (isFunction(mock.validator)) return mock.validator({
434
+ params,
435
+ ...request
436
+ });
437
+ else {
438
+ const [error, validated] = attempt(requestValidate, {
439
+ params,
440
+ ...request
441
+ }, mock.validator);
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;
448
+ }
449
+ }
450
+ return hasMock;
451
+ });
452
+ }
453
+ //#endregion
454
+ //#region src/cookies/constants.ts
455
+ /**
456
+ * RegExp to match field-content in RFC 7230 sec 3.2
457
+ *
458
+ * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
459
+ * field-vchar = VCHAR / obs-text
460
+ * obs-text = %x80-FF
461
+ */
462
+ const fieldContentRegExp = /^[\t\u0020-\u007E\u0080-\u00FF]+$/;
463
+ /**
464
+ * RegExp to match Priority cookie attribute value.
465
+ */
466
+ const PRIORITY_REGEXP = /^(?:low|medium|high)$/i;
467
+ /**
468
+ * Cache for generated name regular expressions.
469
+ */
470
+ const REGEXP_CACHE = Object.create(null);
471
+ /**
472
+ * RegExp to match all characters to escape in a RegExp.
473
+ */
474
+ const REGEXP_ESCAPE_CHARS_REGEXP = /[\^$\\.*+?()[\]{}|]/g;
475
+ /**
476
+ * RegExp to match basic restricted name characters for loose validation.
477
+ */
478
+ const RESTRICTED_NAME_CHARS_REGEXP = /[;=]/;
479
+ /**
480
+ * RegExp to match basic restricted value characters for loose validation.
481
+ */
482
+ const RESTRICTED_VALUE_CHARS_REGEXP = /;/;
483
+ /**
484
+ * RegExp to match Same-Site cookie attribute value.
485
+ */
486
+ const SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i;
487
+ //#endregion
488
+ //#region src/cookies/Cookie.ts
489
+ var Cookie = class {
490
+ name;
491
+ value;
492
+ maxAge;
493
+ expires;
494
+ path = "/";
495
+ domain;
496
+ secure = false;
497
+ httpOnly = true;
498
+ sameSite = false;
499
+ overwrite = false;
500
+ priority;
501
+ partitioned;
502
+ constructor(name, value, options = {}) {
503
+ if (!fieldContentRegExp.test(name) || RESTRICTED_NAME_CHARS_REGEXP.test(name)) throw new TypeError("argument name is invalid");
504
+ if (value && (!fieldContentRegExp.test(value) || RESTRICTED_VALUE_CHARS_REGEXP.test(value))) throw new TypeError("argument value is invalid");
505
+ this.name = name;
506
+ this.value = value;
507
+ Object.assign(this, options);
508
+ if (!this.value) {
509
+ this.expires = /* @__PURE__ */ new Date(0);
510
+ this.maxAge = void 0;
511
+ }
512
+ if (this.path && !fieldContentRegExp.test(this.path)) throw new TypeError("[Cookie] option path is invalid");
513
+ if (this.domain && !fieldContentRegExp.test(this.domain)) throw new TypeError("[Cookie] option domain is invalid");
514
+ if (typeof this.maxAge === "number" ? Number.isNaN(this.maxAge) || !Number.isFinite(this.maxAge) : this.maxAge) throw new TypeError("[Cookie] option maxAge is invalid");
515
+ if (this.priority && !PRIORITY_REGEXP.test(this.priority)) throw new TypeError("[Cookie] option priority is invalid");
516
+ if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) throw new TypeError("[Cookie] option sameSite is invalid");
517
+ }
518
+ toString() {
519
+ return `${this.name}=${this.value}`;
520
+ }
521
+ toHeader() {
522
+ let header = this.toString();
523
+ if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
524
+ if (this.path) header += `; path=${this.path}`;
525
+ if (this.expires) header += `; expires=${this.expires.toUTCString()}`;
526
+ if (this.domain) header += `; domain=${this.domain}`;
527
+ if (this.priority) header += `; priority=${this.priority.toLowerCase()}`;
528
+ if (this.sameSite) header += `; samesite=${this.sameSite === true ? "strict" : this.sameSite.toLowerCase()}`;
529
+ if (this.secure) header += "; secure";
530
+ if (this.httpOnly) header += "; httponly";
531
+ if (this.partitioned) header += "; partitioned";
532
+ return header;
533
+ }
534
+ };
535
+ //#endregion
536
+ //#region src/cookies/timeSafeCompare.ts
537
+ function bufferEqual(a, b) {
538
+ if (a.length !== b.length) return false;
539
+ if (crypto.timingSafeEqual) return crypto.timingSafeEqual(a, b);
540
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
541
+ return true;
542
+ }
543
+ function createHmac(key, data) {
544
+ return crypto.createHmac("sha256", key).update(data).digest();
545
+ }
546
+ function timeSafeCompare(a, b) {
547
+ const sa = String(a);
548
+ const sb = String(b);
549
+ const key = crypto.randomBytes(32);
550
+ return bufferEqual(createHmac(key, sa), createHmac(key, sb)) && a === b;
551
+ }
552
+ //#endregion
553
+ //#region src/cookies/Keygrip.ts
554
+ const SLASH_PATTERN = /[/+=]/g;
555
+ const REPLACE_MAP = {
556
+ "/": "_",
557
+ "+": "-",
558
+ "=": ""
559
+ };
560
+ var Keygrip = class {
561
+ algorithm;
562
+ encoding;
563
+ keys = [];
564
+ constructor(keys, algorithm, encoding) {
565
+ this.keys = keys;
566
+ this.algorithm = algorithm || "sha256";
567
+ this.encoding = encoding || "base64";
568
+ }
569
+ sign(data, key = this.keys[0]) {
570
+ return crypto.createHmac(this.algorithm, key).update(data).digest(this.encoding).replace(SLASH_PATTERN, (m) => REPLACE_MAP[m]);
571
+ }
572
+ index(data, digest) {
573
+ for (let i = 0, l = this.keys.length; i < l; i++) if (timeSafeCompare(digest, this.sign(data, this.keys[i]))) return i;
574
+ return -1;
575
+ }
576
+ verify(data, digest) {
577
+ return this.index(data, digest) > -1;
578
+ }
579
+ };
580
+ //#endregion
581
+ //#region src/cookies/Cookies.ts
582
+ var Cookies = class {
583
+ request;
584
+ response;
585
+ secure;
586
+ keys;
587
+ constructor(req, res, options = {}) {
588
+ this.request = req;
589
+ this.response = res;
590
+ this.secure = options.secure;
591
+ if (options.keys instanceof Keygrip) this.keys = options.keys;
592
+ else if (isArray(options.keys)) this.keys = new Keygrip(options.keys);
593
+ }
594
+ set(name, value, options) {
595
+ const req = this.request;
596
+ const res = this.response;
597
+ const headers = toArray(res.getHeader("Set-Cookie"));
598
+ const cookie = new Cookie(name, value, options);
599
+ const signed = options?.signed ?? !!this.keys;
600
+ const secure = this.secure === void 0 ? req.protocol === "https" || isRequestEncrypted(req) : Boolean(this.secure);
601
+ if (!secure && options?.secure) throw new Error("Cannot send secure cookie over unencrypted connection");
602
+ cookie.secure = options?.secure ?? secure;
603
+ pushCookie(headers, cookie);
604
+ if (signed && options) {
605
+ if (!this.keys) throw new Error(".keys required for signed cookies");
606
+ cookie.value = this.keys.sign(cookie.toString());
607
+ cookie.name += ".sig";
608
+ pushCookie(headers, cookie);
609
+ }
610
+ (res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader).call(res, "Set-Cookie", headers);
611
+ return this;
612
+ }
613
+ get(name, options) {
614
+ const signName = `${name}.sig`;
615
+ const signed = options?.signed ?? !!this.keys;
616
+ const header = this.request.headers.cookie;
617
+ if (!header) return;
618
+ const match = header.match(getPattern(name));
619
+ if (!match) return;
620
+ let value = match[1];
621
+ if (value[0] === "\"") value = value.slice(1, -1);
622
+ if (!options || !signed) return value;
623
+ const remote = this.get(signName);
624
+ if (!remote) return;
625
+ const data = `${name}=${value}`;
626
+ if (!this.keys) throw new Error(".keys required for signed cookies");
627
+ const index = this.keys.index(data, remote);
628
+ if (index < 0) this.set(signName, null, {
629
+ path: "/",
630
+ signed: false
631
+ });
632
+ else {
633
+ index && this.set(signName, this.keys.sign(data), { signed: false });
634
+ return value;
635
+ }
636
+ }
637
+ };
638
+ /**
639
+ * Get the pattern to search for a cookie in a string.
640
+ */
641
+ function getPattern(name) {
642
+ if (!REGEXP_CACHE[name]) REGEXP_CACHE[name] = new RegExp(`(?:^|;) *${name.replace(REGEXP_ESCAPE_CHARS_REGEXP, "\\$&")}=([^;]*)`);
643
+ return REGEXP_CACHE[name];
644
+ }
645
+ /**
646
+ * Get the encrypted status for a request.
647
+ */
648
+ function isRequestEncrypted(req) {
649
+ return Boolean(req.socket ? req.socket.encrypted : req.connection.encrypted);
650
+ }
651
+ function pushCookie(headers, cookie) {
652
+ if (cookie.overwrite) {
653
+ for (let i = headers.length - 1; i >= 0; i--) if (headers[i].indexOf(`${cookie.name}=`) === 0) headers.splice(i, 1);
654
+ }
655
+ headers.push(cookie.toHeader());
656
+ }
657
+ //#endregion
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
1064
+ const tokensCache = {};
1065
+ function getTokens(rule) {
1066
+ if (tokensCache[rule]) return tokensCache[rule];
1067
+ const res = [];
1068
+ const flatten = (tokens, group = false) => {
1069
+ for (const token of tokens) if (token.type === "text") {
1070
+ const sub = token.value.split("/").filter(Boolean);
1071
+ sub.length && res.push(...sub.map((v) => ({
1072
+ type: "text",
1073
+ value: v
1074
+ })));
1075
+ } else if (token.type === "group") flatten(token.tokens, true);
1076
+ else {
1077
+ if (group) token.optional = true;
1078
+ res.push(token);
1079
+ }
1080
+ };
1081
+ flatten(parse(rule).tokens);
1082
+ tokensCache[rule] = res;
1083
+ return res;
1084
+ }
1085
+ function getHighest(rules) {
1086
+ let weights = rules.map((rule) => getTokens(rule).length);
1087
+ weights = weights.length === 0 ? [1] : weights;
1088
+ return Math.max(...weights) + 2;
1089
+ }
1090
+ function computedWeight(rule, highest) {
1091
+ const tokens = getTokens(rule);
1092
+ const dym = tokens.filter((token) => token.type !== "text");
1093
+ if (dym.length === 0) return 0;
1094
+ let weight = dym.length;
1095
+ let exp = 0;
1096
+ for (let i = 0; i < tokens.length; i++) {
1097
+ const token = tokens[i];
1098
+ const isDynamic = token.type !== "text";
1099
+ const isWildcard = token.type === "wildcard";
1100
+ const isOptional = !!token.optional;
1101
+ exp += isDynamic ? 1 : 0;
1102
+ if (i === tokens.length - 1 && isWildcard) weight += (isOptional ? 5 : 4) * 10 ** (tokens.length === 1 ? highest + 1 : highest);
1103
+ else {
1104
+ if (isWildcard) weight += 3 * 10 ** (highest - 1);
1105
+ else weight += 2 * 10 ** exp;
1106
+ if (isOptional) weight += 10 ** exp;
1107
+ }
1108
+ }
1109
+ return weight;
1110
+ }
1111
+ function defaultPriority(rules) {
1112
+ const highest = getHighest(rules);
1113
+ return rules.sort((a, b) => computedWeight(a, highest) - computedWeight(b, highest));
1114
+ }
1115
+ /**
1116
+ * Calculate matching weight for mock URLs
1117
+ *
1118
+ * 计算 Mock URL 的匹配权重
1119
+ *
1120
+ * @param rules - Array of URL patterns / URL 模式数组
1121
+ * @param url - Request URL / 请求 URL
1122
+ * @param priority - Priority configuration / 优先级配置
1123
+ * @returns Sorted array of matched rules / 排序后的匹配规则数组
1124
+ */
1125
+ function matchingWeight(rules, url, priority) {
1126
+ let matched = defaultPriority(rules.filter((rule) => isPathMatch(rule, url)));
1127
+ const { global = [], special = {} } = priority;
1128
+ if (global.length === 0 && isEmptyObject(special) || matched.length === 0) return matched;
1129
+ const [dynamics, statics] = partition(matched, (rule) => getTokens(rule).filter((token) => token.type !== "text").length > 0);
1130
+ const globalMatch = global.filter((rule) => dynamics.includes(rule));
1131
+ if (globalMatch.length > 0) matched = uniq([
1132
+ ...statics,
1133
+ ...globalMatch,
1134
+ ...dynamics
1135
+ ]);
1136
+ if (isEmptyObject(special)) return matched;
1137
+ const specialRule = Object.keys(special).filter((rule) => matched.includes(rule))[0];
1138
+ if (!specialRule) return matched;
1139
+ const options = special[specialRule];
1140
+ const { rules: lowerRules, when } = isArray(options) ? {
1141
+ rules: options,
1142
+ when: []
1143
+ } : options;
1144
+ if (lowerRules.includes(matched[0])) {
1145
+ if (when.length === 0 || when.some((path) => pathToRegexp(path).regexp.test(url))) matched = uniq([specialRule, ...matched]);
1146
+ }
1147
+ return matched;
1148
+ }
1149
+ //#endregion
1150
+ //#region src/mockHttp/requestRecovery.ts
1151
+ /**
1152
+ * 请求复原
1153
+ *
1154
+ * 由于 parseReqBody 在解析请求时,会将请求流消费,
1155
+ * 导致当接口不需要被 mock,继而由 vite http-proxy 转发时,请求流无法继续。
1156
+ * 为此,我们在请求流中记录请求数据,当当前请求无法继续时,可以从备份中恢复请求流
1157
+ */
1158
+ const requestCollectCache = /* @__PURE__ */ new WeakMap();
1159
+ function collectRequest(req) {
1160
+ const chunks = [];
1161
+ req.on("data", (chunk) => {
1162
+ chunks.push(Buffer.from(chunk));
1163
+ });
1164
+ req.on("end", () => {
1165
+ if (chunks.length) requestCollectCache.set(req, Buffer.concat(chunks));
1166
+ });
1167
+ }
1168
+ function rewriteRequest(proxyReq, req) {
1169
+ const buffer = requestCollectCache.get(req);
1170
+ if (buffer) {
1171
+ requestCollectCache.delete(req);
1172
+ if (!proxyReq.headersSent) proxyReq.setHeader("Content-Length", buffer.byteLength);
1173
+ if (!proxyReq.writableEnded) proxyReq.write(buffer);
1174
+ }
1175
+ }
1176
+ //#endregion
1177
+ //#region src/mockHttp/response.ts
1178
+ /**
1179
+ * Get HTTP status text by status code
1180
+ *
1181
+ * 根据状态码获取状态文本
1182
+ *
1183
+ * @param status - HTTP status code / HTTP 状态码
1184
+ * @returns HTTP status text / HTTP 状态文本
1185
+ */
1186
+ function getHTTPStatusText(status) {
1187
+ return HTTP_STATUS[status] || "Unknown";
1188
+ }
1189
+ /**
1190
+ * Set response status
1191
+ *
1192
+ * 设置响应状态
1193
+ *
1194
+ * @param response - Response object / 响应对象
1195
+ * @param status - HTTP status code / HTTP 状态码
1196
+ * @param statusText - HTTP status text / HTTP 状态文本
1197
+ */
1198
+ function provideResponseStatus(response, status = 200, statusText) {
1199
+ response.statusCode = status;
1200
+ response.statusMessage = statusText || getHTTPStatusText(status);
1201
+ }
1202
+ /**
1203
+ * Set response headers
1204
+ *
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 / 日志实例
1211
+ */
1212
+ async function provideResponseHeaders(req, res, mock, logger) {
1213
+ const { headers, type = "json" } = mock;
1214
+ const filepath = mock.__filepath__;
1215
+ const contentType = mime.contentType(type) || mime.contentType(mime.lookup(type) || "");
1216
+ if (contentType) res.setHeader("Content-Type", contentType);
1217
+ res.setHeader("Cache-Control", "no-cache,max-age=0");
1218
+ res.setHeader("X-Mock-Power-By", "vite-plugin-mock-dev-server");
1219
+ if (filepath) res.setHeader("X-File-Path", filepath);
1220
+ if (!headers) return;
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;
1225
+ }
1226
+ objectKeys(data).forEach((key) => res.setHeader(key, data[key]));
1227
+ }
1228
+ /**
1229
+ * Set response cookies
1230
+ *
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 / 日志实例
1237
+ */
1238
+ async function provideResponseCookies(req, res, mock, logger) {
1239
+ const { cookies } = mock;
1240
+ if (!cookies) return;
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;
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
+ });
1252
+ }
1253
+ /**
1254
+ * Send response data
1255
+ *
1256
+ * 设置响应数据
1257
+ *
1258
+ * @param res - Response object / 响应对象
1259
+ * @param raw - Response body data / 响应体数据
1260
+ * @param type - Response data type / 响应数据类型
1261
+ */
1262
+ function sendResponseData(res, raw, type) {
1263
+ if (isReadableStream(raw)) raw.pipe(res);
1264
+ else if (Buffer.isBuffer(raw)) res.end(type === "text" || type === "json" ? raw.toString("utf-8") : raw);
1265
+ else {
1266
+ const content = typeof raw === "string" ? raw : JSON.stringify(raw);
1267
+ res.end(type === "buffer" ? Buffer.from(content) : content);
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Apply real response delay
1272
+ *
1273
+ * 实际响应延迟
1274
+ *
1275
+ * @param startTime - Request start time / 请求开始时间
1276
+ * @param delay - Delay configuration / 延迟配置
1277
+ */
1278
+ async function responseRealDelay(startTime, delay) {
1279
+ if (!delay || typeof delay === "number" && delay <= 0 || isArray(delay) && delay.length !== 2) return;
1280
+ let realDelay = 0;
1281
+ if (isArray(delay)) {
1282
+ const [min, max] = delay;
1283
+ realDelay = random(min, max);
1284
+ } else realDelay = delay - (timestamp() - startTime);
1285
+ if (realDelay > 0) await sleep(realDelay);
1286
+ }
1287
+ //#endregion
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 }) {
1310
+ const cors = createCors(corsOptions);
1311
+ const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
1312
+ const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
1313
+ return async function(req, res, next) {
1314
+ const startTime = timestamp();
1315
+ const { query, pathname } = urlParse(req.url);
1316
+ if (!pathname || proxies.length === 0) return next();
1317
+ if (contextFilter.length && !contextFilter.some((context) => doesProxyContextMatchUrl(context, req))) return next();
1318
+ if (globFilter.length && !isGlobProxiesMatch(pathname)) return next();
1319
+ const mockData = compiler.mockData;
1320
+ const mockUrls = matchingWeight(Object.keys(mockData), pathname, priority);
1321
+ if (mockUrls.length === 0 && !record.enabled) return next();
1322
+ collectRequest(req);
1323
+ const cookies = new Cookies(req, res, cookiesOptions);
1324
+ let mock;
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;
1336
+ for (const mockUrl of mockUrls) {
1337
+ mock = findMockData(mockData[mockUrl], logger, {
1338
+ pathname,
1339
+ method,
1340
+ request: extraReq,
1341
+ activeScene: effectiveScene
1342
+ });
1343
+ if (mock) {
1344
+ _mockUrl = mockUrl;
1345
+ break;
1346
+ }
1347
+ }
1348
+ if (replay && !mock) mock = await replayRecordedRequest(req, pathname, extraReq.body, record);
1349
+ if (!mock) {
1350
+ record.enabled && recordRequestWithRawReq(req, pathname, extraReq.body);
1351
+ const matched = mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ");
1352
+ matched.length && logger.warn(`${ansis.green(pathname)} matches ${matched}, but mock data is not found.`);
1353
+ return next();
1354
+ }
1355
+ if (cors) {
1356
+ const [error] = await attemptAsync(cors, req, res);
1357
+ if (error) {
1358
+ logger.error(`CORS error: ${error}`);
1359
+ return next(error);
1360
+ }
1361
+ }
1362
+ const request = req;
1363
+ const response = res;
1364
+ Object.assign(request, extraReq);
1365
+ request.params = parseRequestParams(mock.url, pathname);
1366
+ response.setCookie = cookies.set.bind(cookies);
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
+ }
1375
+ provideResponseStatus(response, status, statusText);
1376
+ await provideResponseHeaders(request, response, mock, logger);
1377
+ await provideResponseCookies(request, response, mock, logger);
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`);
1380
+ if (body) {
1381
+ const [error] = await attemptAsync(async () => {
1382
+ const content = isFunction(body) ? await body(request) : body;
1383
+ await responseRealDelay(startTime, delay);
1384
+ sendResponseData(response, content, type);
1385
+ });
1386
+ if (error) {
1387
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at body (${ansis.underline.gray(filepath)})`, logLevel);
1388
+ provideResponseStatus(response, 500);
1389
+ res.end("");
1390
+ }
1391
+ return;
1392
+ }
1393
+ if (responseFn) {
1394
+ const [error] = await attemptAsync(async () => {
1395
+ await responseRealDelay(startTime, delay);
1396
+ await responseFn(request, response, next);
1397
+ });
1398
+ if (error) {
1399
+ logger.error(`${ansis.red(`mock error at ${pathname}`)}\n ${error}\n at response (${ansis.underline.gray(filepath)})`, logLevel);
1400
+ provideResponseStatus(response, 500);
1401
+ res.end("");
1402
+ }
1403
+ return;
1404
+ }
1405
+ res.end("");
1406
+ };
1407
+ }
1408
+ //#endregion
1409
+ //#region src/mockWebsocket/server.ts
1410
+ function mockWebSocket(compiler, httpServer, { wsProxies: proxies, cookiesOptions, logger }) {
1411
+ const hmrMap = /* @__PURE__ */ new Map();
1412
+ const poolMap = /* @__PURE__ */ new Map();
1413
+ const wssContextMap = /* @__PURE__ */ new WeakMap();
1414
+ const getWssMap = (mockUrl) => {
1415
+ let wssMap = poolMap.get(mockUrl);
1416
+ if (!wssMap) poolMap.set(mockUrl, wssMap = /* @__PURE__ */ new Map());
1417
+ return wssMap;
1418
+ };
1419
+ const addHmr = (filepath, mockUrl) => {
1420
+ let urlList = hmrMap.get(filepath);
1421
+ if (!urlList) hmrMap.set(filepath, urlList = /* @__PURE__ */ new Set());
1422
+ urlList.add(mockUrl);
1423
+ };
1424
+ const setupWss = (wssMap, wss, mock, context, pathname, filepath) => {
1425
+ try {
1426
+ mock.setup?.(wss, context);
1427
+ wss.on("close", () => wssMap.delete(pathname));
1428
+ wss.on("error", (e) => {
1429
+ logger.error(`${ansis.red(`WebSocket mock error at ${wss.path}`)}\n${e}\n at setup (${filepath})`, mock.log);
1430
+ });
1431
+ } catch (e) {
1432
+ logger.error(`${ansis.red(`WebSocket mock error at ${wss.path}`)}\n${e}\n at setup (${filepath})`, mock.log);
1433
+ }
1434
+ };
1435
+ const restartWss = (wssMap, wss, mock, pathname, filepath) => {
1436
+ const { cleanupList, connectionList, context } = wssContextMap.get(wss);
1437
+ cleanupRunner(cleanupList);
1438
+ connectionList.forEach(({ ws }) => ws.removeAllListeners());
1439
+ wss.removeAllListeners();
1440
+ setupWss(wssMap, wss, mock, context, pathname, filepath);
1441
+ connectionList.forEach(({ ws, req }) => emitConnection(wss, ws, req, connectionList));
1442
+ };
1443
+ compiler.on("update", ({ filepath }) => {
1444
+ if (!hmrMap.has(filepath)) return;
1445
+ const mockUrlList = hmrMap.get(filepath);
1446
+ if (!mockUrlList) return;
1447
+ for (const mockUrl of mockUrlList.values()) for (const mock of compiler.mockData[mockUrl]) {
1448
+ if (!mock.ws || mock.__filepath__ !== filepath) return;
1449
+ const wssMap = getWssMap(mockUrl);
1450
+ for (const [pathname, wss] of wssMap.entries()) restartWss(wssMap, wss, mock, pathname, filepath);
1451
+ }
1452
+ });
1453
+ const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
1454
+ const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
1455
+ httpServer?.on("upgrade", (req, socket, head) => {
1456
+ const { pathname, query } = urlParse(req.url);
1457
+ if (!pathname || proxies.length === 0) return;
1458
+ if (contextFilter.length && !contextFilter.some((context) => doesProxyContextMatchUrl(context, req))) return;
1459
+ if (globFilter.length && !isGlobProxiesMatch(pathname)) return;
1460
+ const mockData = compiler.mockData;
1461
+ const mockUrl = objectKeys(mockData).find((key) => isPathMatch(key, pathname));
1462
+ if (!mockUrl) return;
1463
+ const mock = mockData[mockUrl].find((mock) => {
1464
+ return mock.url && mock.ws && isPathMatch(mock.url, pathname);
1465
+ });
1466
+ if (!mock) return;
1467
+ const filepath = mock.__filepath__;
1468
+ addHmr(filepath, mockUrl);
1469
+ const wssMap = getWssMap(mockUrl);
1470
+ const wss = getWss(wssMap, pathname);
1471
+ let wssContext = wssContextMap.get(wss);
1472
+ if (!wssContext) {
1473
+ const cleanupList = [];
1474
+ const context = { onCleanup: (cleanup) => cleanupList.push(cleanup) };
1475
+ wssContext = {
1476
+ cleanupList,
1477
+ context,
1478
+ connectionList: []
1479
+ };
1480
+ wssContextMap.set(wss, wssContext);
1481
+ setupWss(wssMap, wss, mock, context, pathname, filepath);
1482
+ }
1483
+ const request = req;
1484
+ const cookies = new Cookies(req, req, cookiesOptions);
1485
+ const { query: refererQuery } = urlParse(req.headers.referer || "");
1486
+ request.query = query;
1487
+ request.refererQuery = refererQuery;
1488
+ request.params = parseRequestParams(mockUrl, pathname);
1489
+ request.getCookie = cookies.get.bind(cookies);
1490
+ wss.handleUpgrade(request, socket, head, (ws) => {
1491
+ logger.info(`${ansis.magenta(ansis.bold("WebSocket"))} ${ansis.green(req.url)} connected ${ansis.dim(`(${filepath})`)}`, mock.log);
1492
+ wssContext.connectionList.push({
1493
+ req: request,
1494
+ ws
1495
+ });
1496
+ emitConnection(wss, ws, request, wssContext.connectionList);
1497
+ });
1498
+ });
1499
+ httpServer?.on("close", () => {
1500
+ for (const wssMap of poolMap.values()) {
1501
+ for (const wss of wssMap.values()) {
1502
+ cleanupRunner(wssContextMap.get(wss).cleanupList);
1503
+ wss.close();
1504
+ }
1505
+ wssMap.clear();
1506
+ }
1507
+ poolMap.clear();
1508
+ hmrMap.clear();
1509
+ });
1510
+ }
1511
+ function getWss(wssMap, pathname) {
1512
+ let wss = wssMap.get(pathname);
1513
+ if (!wss) wssMap.set(pathname, wss = new WebSocketServer({ noServer: true }));
1514
+ return wss;
1515
+ }
1516
+ function emitConnection(wss, ws, req, connectionList) {
1517
+ wss.emit("connection", ws, req);
1518
+ ws.on("close", () => {
1519
+ const i = connectionList.findIndex((item) => item.ws === ws);
1520
+ if (i !== -1) connectionList.splice(i, 1);
1521
+ });
1522
+ }
1523
+ function cleanupRunner(cleanupList) {
1524
+ let cleanup;
1525
+ while (cleanup = cleanupList.shift()) cleanup?.();
1526
+ }
1527
+ //#endregion
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 };