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