rspack-plugin-mock 1.3.0 → 2.0.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 +7 -2
- package/README.zh-CN.md +7 -2
- package/dist/{helper.d.mts → helper.d.ts} +1 -1
- package/dist/helper.js +172 -0
- package/dist/{index.d.mts → index.d.ts} +3 -3
- package/dist/index.js +84 -0
- package/dist/json5-loader.mjs +8 -0
- package/dist/options-Bd0PSZeT.js +446 -0
- package/dist/{rsbuild.d.mts → rsbuild.d.ts} +1 -1
- package/dist/rsbuild.js +92 -0
- package/dist/{server-4ETytB7L.d.mts → server-_doRwsZm.d.ts} +15 -15
- package/dist/{server.d.mts → server.d.ts} +1 -2
- package/dist/server.js +2 -0
- package/dist/{types-Bt_OTa7I.d.mts → types-GT6M6WuI.d.ts} +3 -3
- package/dist/ws-BcLCWVaK.js +1003 -0
- package/package.json +33 -34
- package/dist/helper.mjs +0 -4
- package/dist/index.mjs +0 -1
- package/dist/json5-loader.cjs +0 -1
- package/dist/options-BUfaThYe.mjs +0 -43
- package/dist/rsbuild.mjs +0 -1
- package/dist/server.mjs +0 -1
- package/dist/ws-BM06pHhL.mjs +0 -1
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import { attemptAsync, isArray, isBoolean, isEmptyObject, isFunction, isPlainObject, isString, objectKeys, partition, random, sleep, sortBy, timestamp, toArray, uniq } from "@pengzhanbo/utils";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ansis from "ansis";
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
import { loadPackageJSONSync } from "local-pkg";
|
|
6
|
+
import { match, parse, pathToRegexp } from "path-to-regexp";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import Debug from "debug";
|
|
10
|
+
import { Volume, createFsFromVolume } from "memfs";
|
|
11
|
+
import { parse as parse$1 } from "node:querystring";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import cors from "cors";
|
|
14
|
+
import bodyParser from "co-body";
|
|
15
|
+
import formidable from "formidable";
|
|
16
|
+
import http from "node:http";
|
|
17
|
+
import { Buffer } from "node:buffer";
|
|
18
|
+
import HTTP_STATUS from "http-status";
|
|
19
|
+
import * as mime from "mime-types";
|
|
20
|
+
import { WebSocketServer } from "ws";
|
|
21
|
+
//#region src/utils/createMatcher.ts
|
|
22
|
+
function createMatcher(include, exclude, defaultIgnore = true) {
|
|
23
|
+
const pattern = [];
|
|
24
|
+
const ignore = [...defaultIgnore ? ["**/node_modules/**"] : [], ...toArray(exclude)];
|
|
25
|
+
toArray(include).forEach((item) => {
|
|
26
|
+
if (item[0] === "!") ignore.push(item.slice(1));
|
|
27
|
+
else pattern.push(item);
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
pattern,
|
|
31
|
+
ignore,
|
|
32
|
+
isMatch: picomatch(pattern, { ignore })
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/utils/doesProxyContextMatchUrl.ts
|
|
37
|
+
const PATTERN_CACHE = /* @__PURE__ */ new Map();
|
|
38
|
+
function doesProxyContextMatchUrl(context, req) {
|
|
39
|
+
const url = req.url;
|
|
40
|
+
if (typeof context === "function") return context(url, req);
|
|
41
|
+
if (context[0] === "^") {
|
|
42
|
+
let pattern = PATTERN_CACHE.get(context);
|
|
43
|
+
if (!pattern) PATTERN_CACHE.set(context, pattern = new RegExp(context));
|
|
44
|
+
return pattern.test(url);
|
|
45
|
+
}
|
|
46
|
+
return url.startsWith(context);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/utils/getDeps.ts
|
|
50
|
+
function getPackageDeps(cwd) {
|
|
51
|
+
const { dependencies, devDependencies, peerDependencies, optionalDependencies } = loadPackageJSONSync(cwd) || {};
|
|
52
|
+
return {
|
|
53
|
+
...dependencies,
|
|
54
|
+
...devDependencies,
|
|
55
|
+
...peerDependencies,
|
|
56
|
+
...optionalDependencies
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function getPackageDepList(cwd) {
|
|
60
|
+
return uniq(objectKeys(getPackageDeps(cwd)));
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/utils/is.ts
|
|
64
|
+
function isStream(stream) {
|
|
65
|
+
return stream !== null && typeof stream === "object" && typeof stream.pipe === "function";
|
|
66
|
+
}
|
|
67
|
+
function isReadableStream(stream) {
|
|
68
|
+
return isStream(stream) && stream.readable !== false && typeof stream._read === "function" && typeof stream._readableState === "object";
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/utils/isObjectSubset.ts
|
|
72
|
+
/**
|
|
73
|
+
* Checks if target object is a subset of source object.
|
|
74
|
+
* That is, all properties and their corresponding values in target exist in source.
|
|
75
|
+
*
|
|
76
|
+
* 深度比较两个对象之间,target 是否属于 source 的子集,
|
|
77
|
+
* 即 target 的所有属性和对应的值,都在 source 中,
|
|
78
|
+
*/
|
|
79
|
+
function isObjectSubset(source, target) {
|
|
80
|
+
if (!target) return true;
|
|
81
|
+
for (const key in target) if (!isIncluded(source[key], target[key])) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function isIncluded(source, target) {
|
|
85
|
+
if (isArray(source) && isArray(target)) {
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
return target.every((ti) => source.some((si, i) => {
|
|
88
|
+
if (seen.has(i)) return false;
|
|
89
|
+
const included = isIncluded(si, ti);
|
|
90
|
+
if (included) seen.add(i);
|
|
91
|
+
return included;
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
if (isPlainObject(source) && isPlainObject(target)) return isObjectSubset(source, target);
|
|
95
|
+
return Object.is(source, target);
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/utils/isPathMatch.ts
|
|
99
|
+
const cache = /* @__PURE__ */ new Map();
|
|
100
|
+
/**
|
|
101
|
+
* 判断 path 是否匹配 pattern
|
|
102
|
+
*/
|
|
103
|
+
function isPathMatch(pattern, path) {
|
|
104
|
+
let regexp = cache.get(pattern);
|
|
105
|
+
if (!regexp) {
|
|
106
|
+
regexp = pathToRegexp(pattern).regexp;
|
|
107
|
+
cache.set(pattern, regexp);
|
|
108
|
+
}
|
|
109
|
+
return regexp.test(path);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/utils/logger.ts
|
|
113
|
+
const logLevels = {
|
|
114
|
+
silent: 0,
|
|
115
|
+
error: 1,
|
|
116
|
+
warn: 2,
|
|
117
|
+
info: 3,
|
|
118
|
+
debug: 4
|
|
119
|
+
};
|
|
120
|
+
function createLogger(prefix, defaultLevel = "info") {
|
|
121
|
+
prefix = `[${prefix}]`;
|
|
122
|
+
function output(type, msg, level) {
|
|
123
|
+
level = isBoolean(level) ? level ? defaultLevel : "error" : level;
|
|
124
|
+
if (logLevels[level] >= logLevels[type]) {
|
|
125
|
+
const method = type === "info" || type === "debug" ? "log" : type;
|
|
126
|
+
const tag = type === "debug" ? ansis.magenta.bold(prefix) : type === "info" ? ansis.cyan.bold(prefix) : type === "warn" ? ansis.yellow.bold(prefix) : ansis.red.bold(prefix);
|
|
127
|
+
const format = `${ansis.dim((/* @__PURE__ */ new Date()).toLocaleTimeString())} ${tag} ${msg}`;
|
|
128
|
+
console[method](format);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
debug(msg, level = defaultLevel) {
|
|
133
|
+
output("debug", msg, level);
|
|
134
|
+
},
|
|
135
|
+
info(msg, level = defaultLevel) {
|
|
136
|
+
output("info", msg, level);
|
|
137
|
+
},
|
|
138
|
+
warn(msg, level = defaultLevel) {
|
|
139
|
+
output("warn", msg, level);
|
|
140
|
+
},
|
|
141
|
+
error(msg, level = defaultLevel) {
|
|
142
|
+
output("error", msg, level);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
getDirname(import.meta.url);
|
|
147
|
+
const vfs = createFsFromVolume(new Volume());
|
|
148
|
+
function getDirname(importMetaUrl) {
|
|
149
|
+
return path.dirname(fileURLToPath(importMetaUrl));
|
|
150
|
+
}
|
|
151
|
+
Debug("vite:mock-dev-server");
|
|
152
|
+
const windowsSlashRE = /\\/g;
|
|
153
|
+
const isWindows = os.platform() === "win32";
|
|
154
|
+
function slash(p) {
|
|
155
|
+
return p.replace(windowsSlashRE, "/");
|
|
156
|
+
}
|
|
157
|
+
function normalizePath(id) {
|
|
158
|
+
return path.posix.normalize(isWindows ? slash(id) : id);
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/utils/urlParse.ts
|
|
162
|
+
/**
|
|
163
|
+
* nodejs 从 19.0.0 开始 弃用 url.parse,因此使用 url.parse 来解析 可能会报错,
|
|
164
|
+
* 使用 URL 来解析
|
|
165
|
+
*/
|
|
166
|
+
function urlParse(input) {
|
|
167
|
+
const url = new URL(input, "http://example.com");
|
|
168
|
+
return {
|
|
169
|
+
pathname: decodeURIComponent(url.pathname),
|
|
170
|
+
query: parse$1(url.search.replace(/^\?/, ""))
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/utils/waitingFor.ts
|
|
175
|
+
function waitingFor(onSuccess, maxRetry = 5) {
|
|
176
|
+
return function wait(getter, retry = 0) {
|
|
177
|
+
const value = getter();
|
|
178
|
+
if (value) onSuccess(value);
|
|
179
|
+
else if (retry < maxRetry) setTimeout(() => wait(getter, retry + 1), 100);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/compiler/processData.ts
|
|
184
|
+
function processRawData(rawData) {
|
|
185
|
+
return rawData.filter((item) => item[0]).map(([raw, __filepath__]) => {
|
|
186
|
+
let mockConfig;
|
|
187
|
+
if (raw.default) if (isArray(raw.default)) mockConfig = raw.default.map((item) => ({
|
|
188
|
+
...item,
|
|
189
|
+
__filepath__
|
|
190
|
+
}));
|
|
191
|
+
else mockConfig = {
|
|
192
|
+
...raw.default,
|
|
193
|
+
__filepath__
|
|
194
|
+
};
|
|
195
|
+
else if ("url" in raw) mockConfig = {
|
|
196
|
+
...raw,
|
|
197
|
+
__filepath__
|
|
198
|
+
};
|
|
199
|
+
else {
|
|
200
|
+
mockConfig = [];
|
|
201
|
+
objectKeys(raw || {}).forEach((key) => {
|
|
202
|
+
const data = raw[key];
|
|
203
|
+
if (isArray(data)) mockConfig.push(...data.map((item) => ({
|
|
204
|
+
...item,
|
|
205
|
+
__filepath__
|
|
206
|
+
})));
|
|
207
|
+
else mockConfig.push({
|
|
208
|
+
...data,
|
|
209
|
+
__filepath__
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return mockConfig;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function processMockData(mockList) {
|
|
217
|
+
const list = [];
|
|
218
|
+
for (const [, handle] of mockList.entries()) if (handle) list.push(...toArray(handle));
|
|
219
|
+
const mocks = {};
|
|
220
|
+
list.filter((mock) => isPlainObject(mock) && mock.enabled !== false && mock.url).forEach((mock) => {
|
|
221
|
+
const { pathname, query } = urlParse(mock.url);
|
|
222
|
+
const list = mocks[pathname] ??= [];
|
|
223
|
+
const current = {
|
|
224
|
+
...mock,
|
|
225
|
+
url: pathname
|
|
226
|
+
};
|
|
227
|
+
if (current.ws !== true) {
|
|
228
|
+
const validator = current.validator;
|
|
229
|
+
if (!isEmptyObject(query)) if (isFunction(validator)) current.validator = function(request) {
|
|
230
|
+
return isObjectSubset(request.query, query) && validator(request);
|
|
231
|
+
};
|
|
232
|
+
else if (validator) {
|
|
233
|
+
current.validator = { ...validator };
|
|
234
|
+
current.validator.query = current.validator.query ? {
|
|
235
|
+
...query,
|
|
236
|
+
...current.validator.query
|
|
237
|
+
} : query;
|
|
238
|
+
} else current.validator = { query };
|
|
239
|
+
}
|
|
240
|
+
list.push(current);
|
|
241
|
+
});
|
|
242
|
+
objectKeys(mocks).forEach((key) => {
|
|
243
|
+
mocks[key] = sortByValidator(mocks[key]);
|
|
244
|
+
});
|
|
245
|
+
return mocks;
|
|
246
|
+
}
|
|
247
|
+
function sortByValidator(mocks) {
|
|
248
|
+
return sortBy(mocks, (item) => {
|
|
249
|
+
if (item.ws === true) return 0;
|
|
250
|
+
const { validator } = item;
|
|
251
|
+
if (!validator || isEmptyObject(validator)) return 2;
|
|
252
|
+
if (isFunction(validator)) return 0;
|
|
253
|
+
return 1 / Object.keys(validator).reduce((prev, key) => prev + keysCount(validator[key]), 0);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function keysCount(obj) {
|
|
257
|
+
if (!obj) return 0;
|
|
258
|
+
return objectKeys(obj).length;
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/core/cors.ts
|
|
262
|
+
/**
|
|
263
|
+
* Create CORS middleware
|
|
264
|
+
*
|
|
265
|
+
* 创建 CORS 中间件
|
|
266
|
+
*
|
|
267
|
+
* @param corsOptions - CORS options / CORS 配置项
|
|
268
|
+
* @returns CORS middleware function or undefined / CORS 中间件函数或未定义
|
|
269
|
+
*/
|
|
270
|
+
function createCors(corsOptions) {
|
|
271
|
+
const corsMiddleware = corsOptions ? cors(corsOptions) : void 0;
|
|
272
|
+
return corsMiddleware ? (req, res) => new Promise((resolve, reject) => corsMiddleware(req, res, (err) => {
|
|
273
|
+
err ? reject(err) : resolve();
|
|
274
|
+
})) : void 0;
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/core/request.ts
|
|
278
|
+
/**
|
|
279
|
+
* 解析请求体 request.body
|
|
280
|
+
*/
|
|
281
|
+
async function parseRequestBody(req, formidableOptions, bodyParserOptions = {}) {
|
|
282
|
+
const method = req.method.toUpperCase();
|
|
283
|
+
if (["HEAD", "OPTIONS"].includes(method)) return void 0;
|
|
284
|
+
const type = req.headers["content-type"]?.toLocaleLowerCase() || "";
|
|
285
|
+
const { limit, formLimit, jsonLimit, textLimit, ...rest } = bodyParserOptions;
|
|
286
|
+
try {
|
|
287
|
+
if (type.startsWith("application/json")) return await bodyParser.json(req, {
|
|
288
|
+
limit: jsonLimit || limit,
|
|
289
|
+
...rest
|
|
290
|
+
});
|
|
291
|
+
if (type.startsWith("application/x-www-form-urlencoded")) return await bodyParser.form(req, {
|
|
292
|
+
limit: formLimit || limit,
|
|
293
|
+
...rest
|
|
294
|
+
});
|
|
295
|
+
if (type.startsWith("text/plain")) return await bodyParser.text(req, {
|
|
296
|
+
limit: textLimit || limit,
|
|
297
|
+
...rest
|
|
298
|
+
});
|
|
299
|
+
if (type.startsWith("multipart/form-data")) return await parseRequestBodyWithMultipart(req, formidableOptions);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error(e);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const DEFAULT_FORMIDABLE_OPTIONS = {
|
|
305
|
+
keepExtensions: true,
|
|
306
|
+
filename(name, ext, part) {
|
|
307
|
+
return part?.originalFilename || `${name}.${Date.now()}${ext ? `.${ext}` : ""}`;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* 解析 request form multipart body
|
|
312
|
+
*/
|
|
313
|
+
async function parseRequestBodyWithMultipart(req, options) {
|
|
314
|
+
const form = formidable({
|
|
315
|
+
...DEFAULT_FORMIDABLE_OPTIONS,
|
|
316
|
+
...options
|
|
317
|
+
});
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
form.parse(req, (error, fields, files) => {
|
|
320
|
+
if (error) {
|
|
321
|
+
reject(error);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
resolve({
|
|
325
|
+
...fields,
|
|
326
|
+
...files
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const matcherCache = /* @__PURE__ */ new Map();
|
|
332
|
+
/**
|
|
333
|
+
* 解析请求 url 中的动态参数 params
|
|
334
|
+
*/
|
|
335
|
+
function parseRequestParams(pattern, url) {
|
|
336
|
+
let matcher = matcherCache.get(pattern);
|
|
337
|
+
if (!matcher) {
|
|
338
|
+
matcher = match(pattern, { decode: decodeURIComponent });
|
|
339
|
+
matcherCache.set(pattern, matcher);
|
|
340
|
+
}
|
|
341
|
+
const matched = matcher(url);
|
|
342
|
+
return matched ? matched.params : {};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 验证请求是否符合 validator
|
|
346
|
+
*/
|
|
347
|
+
function requestValidate(request, validator) {
|
|
348
|
+
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
|
+
}
|
|
350
|
+
function formatLog(prefix, data) {
|
|
351
|
+
return !data || isEmptyObject(data) ? "" : ` ${ansis.gray(`${prefix}:`)}${JSON.stringify(data)}`;
|
|
352
|
+
}
|
|
353
|
+
function requestLog(request, filepath) {
|
|
354
|
+
const { url, method, query, params, body } = request;
|
|
355
|
+
let { pathname } = new URL(url, "http://example.com");
|
|
356
|
+
pathname = ansis.green(decodeURIComponent(pathname));
|
|
357
|
+
const ms = ansis.magenta.bold(method);
|
|
358
|
+
const qs = formatLog("query", query);
|
|
359
|
+
const ps = formatLog("params", params);
|
|
360
|
+
const bs = formatLog("body", body);
|
|
361
|
+
const file = ` ${ansis.dim.underline(`(${filepath})`)}`;
|
|
362
|
+
return `${ms} ${pathname}${qs}${ps}${bs}${file}`;
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/core/findMockData.ts
|
|
366
|
+
/**
|
|
367
|
+
* 查找匹配的 mock data
|
|
368
|
+
*/
|
|
369
|
+
function findMockData(mockList, logger, { pathname, method, request }) {
|
|
370
|
+
return mockList.find((mock) => {
|
|
371
|
+
if (!pathname || !mock || !mock.url || mock.ws) return false;
|
|
372
|
+
if (!(mock.method ? isArray(mock.method) ? mock.method : [mock.method] : ["GET", "POST"]).includes(method)) return false;
|
|
373
|
+
const hasMock = isPathMatch(mock.url, pathname);
|
|
374
|
+
if (hasMock && mock.validator) {
|
|
375
|
+
const params = parseRequestParams(mock.url, pathname);
|
|
376
|
+
if (isFunction(mock.validator)) return mock.validator({
|
|
377
|
+
params,
|
|
378
|
+
...request
|
|
379
|
+
});
|
|
380
|
+
else try {
|
|
381
|
+
return requestValidate({
|
|
382
|
+
params,
|
|
383
|
+
...request
|
|
384
|
+
}, mock.validator);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
const file = mock.__filepath__;
|
|
387
|
+
logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at validator (${ansis.underline(file)})`, mock.log);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return hasMock;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/cookies/constants.ts
|
|
396
|
+
/**
|
|
397
|
+
* RegExp to match field-content in RFC 7230 sec 3.2
|
|
398
|
+
*
|
|
399
|
+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
|
400
|
+
* field-vchar = VCHAR / obs-text
|
|
401
|
+
* obs-text = %x80-FF
|
|
402
|
+
*/
|
|
403
|
+
const fieldContentRegExp = /^[\t\u0020-\u007E\u0080-\u00FF]+$/;
|
|
404
|
+
/**
|
|
405
|
+
* RegExp to match Priority cookie attribute value.
|
|
406
|
+
*/
|
|
407
|
+
const PRIORITY_REGEXP = /^(?:low|medium|high)$/i;
|
|
408
|
+
/**
|
|
409
|
+
* Cache for generated name regular expressions.
|
|
410
|
+
*/
|
|
411
|
+
const REGEXP_CACHE = Object.create(null);
|
|
412
|
+
/**
|
|
413
|
+
* RegExp to match all characters to escape in a RegExp.
|
|
414
|
+
*/
|
|
415
|
+
const REGEXP_ESCAPE_CHARS_REGEXP = /[\^$\\.*+?()[\]{}|]/g;
|
|
416
|
+
/**
|
|
417
|
+
* RegExp to match basic restricted name characters for loose validation.
|
|
418
|
+
*/
|
|
419
|
+
const RESTRICTED_NAME_CHARS_REGEXP = /[;=]/;
|
|
420
|
+
/**
|
|
421
|
+
* RegExp to match basic restricted value characters for loose validation.
|
|
422
|
+
*/
|
|
423
|
+
const RESTRICTED_VALUE_CHARS_REGEXP = /;/;
|
|
424
|
+
/**
|
|
425
|
+
* RegExp to match Same-Site cookie attribute value.
|
|
426
|
+
*/
|
|
427
|
+
const SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i;
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/cookies/Cookie.ts
|
|
430
|
+
var Cookie = class {
|
|
431
|
+
name;
|
|
432
|
+
value;
|
|
433
|
+
maxAge;
|
|
434
|
+
expires;
|
|
435
|
+
path = "/";
|
|
436
|
+
domain;
|
|
437
|
+
secure = false;
|
|
438
|
+
httpOnly = true;
|
|
439
|
+
sameSite = false;
|
|
440
|
+
overwrite = false;
|
|
441
|
+
priority;
|
|
442
|
+
partitioned;
|
|
443
|
+
constructor(name, value, options = {}) {
|
|
444
|
+
if (!fieldContentRegExp.test(name) || RESTRICTED_NAME_CHARS_REGEXP.test(name)) throw new TypeError("argument name is invalid");
|
|
445
|
+
if (value && (!fieldContentRegExp.test(value) || RESTRICTED_VALUE_CHARS_REGEXP.test(value))) throw new TypeError("argument value is invalid");
|
|
446
|
+
this.name = name;
|
|
447
|
+
this.value = value;
|
|
448
|
+
Object.assign(this, options);
|
|
449
|
+
if (!this.value) {
|
|
450
|
+
this.expires = /* @__PURE__ */ new Date(0);
|
|
451
|
+
this.maxAge = void 0;
|
|
452
|
+
}
|
|
453
|
+
if (this.path && !fieldContentRegExp.test(this.path)) throw new TypeError("[Cookie] option path is invalid");
|
|
454
|
+
if (this.domain && !fieldContentRegExp.test(this.domain)) throw new TypeError("[Cookie] option domain is invalid");
|
|
455
|
+
if (typeof this.maxAge === "number" ? Number.isNaN(this.maxAge) || !Number.isFinite(this.maxAge) : this.maxAge) throw new TypeError("[Cookie] option maxAge is invalid");
|
|
456
|
+
if (this.priority && !PRIORITY_REGEXP.test(this.priority)) throw new TypeError("[Cookie] option priority is invalid");
|
|
457
|
+
if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) throw new TypeError("[Cookie] option sameSite is invalid");
|
|
458
|
+
}
|
|
459
|
+
toString() {
|
|
460
|
+
return `${this.name}=${this.value}`;
|
|
461
|
+
}
|
|
462
|
+
toHeader() {
|
|
463
|
+
let header = this.toString();
|
|
464
|
+
if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
|
|
465
|
+
if (this.path) header += `; path=${this.path}`;
|
|
466
|
+
if (this.expires) header += `; expires=${this.expires.toUTCString()}`;
|
|
467
|
+
if (this.domain) header += `; domain=${this.domain}`;
|
|
468
|
+
if (this.priority) header += `; priority=${this.priority.toLowerCase()}`;
|
|
469
|
+
if (this.sameSite) header += `; samesite=${this.sameSite === true ? "strict" : this.sameSite.toLowerCase()}`;
|
|
470
|
+
if (this.secure) header += "; secure";
|
|
471
|
+
if (this.httpOnly) header += "; httponly";
|
|
472
|
+
if (this.partitioned) header += "; partitioned";
|
|
473
|
+
return header;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/cookies/timeSafeCompare.ts
|
|
478
|
+
function bufferEqual(a, b) {
|
|
479
|
+
if (a.length !== b.length) return false;
|
|
480
|
+
if (crypto.timingSafeEqual) return crypto.timingSafeEqual(a, b);
|
|
481
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
function createHmac(key, data) {
|
|
485
|
+
return crypto.createHmac("sha256", key).update(data).digest();
|
|
486
|
+
}
|
|
487
|
+
function timeSafeCompare(a, b) {
|
|
488
|
+
const sa = String(a);
|
|
489
|
+
const sb = String(b);
|
|
490
|
+
const key = crypto.randomBytes(32);
|
|
491
|
+
return bufferEqual(createHmac(key, sa), createHmac(key, sb)) && a === b;
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/cookies/Keygrip.ts
|
|
495
|
+
const SLASH_PATTERN = /[/+=]/g;
|
|
496
|
+
const REPLACE_MAP = {
|
|
497
|
+
"/": "_",
|
|
498
|
+
"+": "-",
|
|
499
|
+
"=": ""
|
|
500
|
+
};
|
|
501
|
+
var Keygrip = class {
|
|
502
|
+
algorithm;
|
|
503
|
+
encoding;
|
|
504
|
+
keys = [];
|
|
505
|
+
constructor(keys, algorithm, encoding) {
|
|
506
|
+
this.keys = keys;
|
|
507
|
+
this.algorithm = algorithm || "sha256";
|
|
508
|
+
this.encoding = encoding || "base64";
|
|
509
|
+
}
|
|
510
|
+
sign(data, key = this.keys[0]) {
|
|
511
|
+
return crypto.createHmac(this.algorithm, key).update(data).digest(this.encoding).replace(SLASH_PATTERN, (m) => REPLACE_MAP[m]);
|
|
512
|
+
}
|
|
513
|
+
index(data, digest) {
|
|
514
|
+
for (let i = 0, l = this.keys.length; i < l; i++) if (timeSafeCompare(digest, this.sign(data, this.keys[i]))) return i;
|
|
515
|
+
return -1;
|
|
516
|
+
}
|
|
517
|
+
verify(data, digest) {
|
|
518
|
+
return this.index(data, digest) > -1;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/cookies/Cookies.ts
|
|
523
|
+
var Cookies = class {
|
|
524
|
+
request;
|
|
525
|
+
response;
|
|
526
|
+
secure;
|
|
527
|
+
keys;
|
|
528
|
+
constructor(req, res, options = {}) {
|
|
529
|
+
this.request = req;
|
|
530
|
+
this.response = res;
|
|
531
|
+
this.secure = options.secure;
|
|
532
|
+
if (options.keys instanceof Keygrip) this.keys = options.keys;
|
|
533
|
+
else if (isArray(options.keys)) this.keys = new Keygrip(options.keys);
|
|
534
|
+
}
|
|
535
|
+
set(name, value, options) {
|
|
536
|
+
const req = this.request;
|
|
537
|
+
const res = this.response;
|
|
538
|
+
const headers = toArray(res.getHeader("Set-Cookie"));
|
|
539
|
+
const cookie = new Cookie(name, value, options);
|
|
540
|
+
const signed = options?.signed ?? !!this.keys;
|
|
541
|
+
const secure = this.secure === void 0 ? req.protocol === "https" || isRequestEncrypted(req) : Boolean(this.secure);
|
|
542
|
+
if (!secure && options?.secure) throw new Error("Cannot send secure cookie over unencrypted connection");
|
|
543
|
+
cookie.secure = options?.secure ?? secure;
|
|
544
|
+
pushCookie(headers, cookie);
|
|
545
|
+
if (signed && options) {
|
|
546
|
+
if (!this.keys) throw new Error(".keys required for signed cookies");
|
|
547
|
+
cookie.value = this.keys.sign(cookie.toString());
|
|
548
|
+
cookie.name += ".sig";
|
|
549
|
+
pushCookie(headers, cookie);
|
|
550
|
+
}
|
|
551
|
+
(res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader).call(res, "Set-Cookie", headers);
|
|
552
|
+
return this;
|
|
553
|
+
}
|
|
554
|
+
get(name, options) {
|
|
555
|
+
const signName = `${name}.sig`;
|
|
556
|
+
const signed = options?.signed ?? !!this.keys;
|
|
557
|
+
const header = this.request.headers.cookie;
|
|
558
|
+
if (!header) return;
|
|
559
|
+
const match = header.match(getPattern(name));
|
|
560
|
+
if (!match) return;
|
|
561
|
+
let value = match[1];
|
|
562
|
+
if (value[0] === "\"") value = value.slice(1, -1);
|
|
563
|
+
if (!options || !signed) return value;
|
|
564
|
+
const remote = this.get(signName);
|
|
565
|
+
if (!remote) return;
|
|
566
|
+
const data = `${name}=${value}`;
|
|
567
|
+
if (!this.keys) throw new Error(".keys required for signed cookies");
|
|
568
|
+
const index = this.keys.index(data, remote);
|
|
569
|
+
if (index < 0) this.set(signName, null, {
|
|
570
|
+
path: "/",
|
|
571
|
+
signed: false
|
|
572
|
+
});
|
|
573
|
+
else {
|
|
574
|
+
index && this.set(signName, this.keys.sign(data), { signed: false });
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Get the pattern to search for a cookie in a string.
|
|
581
|
+
*/
|
|
582
|
+
function getPattern(name) {
|
|
583
|
+
if (!REGEXP_CACHE[name]) REGEXP_CACHE[name] = new RegExp(`(?:^|;) *${name.replace(REGEXP_ESCAPE_CHARS_REGEXP, "\\$&")}=([^;]*)`);
|
|
584
|
+
return REGEXP_CACHE[name];
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Get the encrypted status for a request.
|
|
588
|
+
*/
|
|
589
|
+
function isRequestEncrypted(req) {
|
|
590
|
+
return Boolean(req.socket ? req.socket.encrypted : req.connection.encrypted);
|
|
591
|
+
}
|
|
592
|
+
function pushCookie(headers, cookie) {
|
|
593
|
+
if (cookie.overwrite) {
|
|
594
|
+
for (let i = headers.length - 1; i >= 0; i--) if (headers[i].indexOf(`${cookie.name}=`) === 0) headers.splice(i, 1);
|
|
595
|
+
}
|
|
596
|
+
headers.push(cookie.toHeader());
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/core/matchingWeight.ts
|
|
600
|
+
const tokensCache = {};
|
|
601
|
+
function getTokens(rule) {
|
|
602
|
+
if (tokensCache[rule]) return tokensCache[rule];
|
|
603
|
+
const res = [];
|
|
604
|
+
const flatten = (tokens, group = false) => {
|
|
605
|
+
for (const token of tokens) if (token.type === "text") {
|
|
606
|
+
const sub = token.value.split("/").filter(Boolean);
|
|
607
|
+
sub.length && res.push(...sub.map((v) => ({
|
|
608
|
+
type: "text",
|
|
609
|
+
value: v
|
|
610
|
+
})));
|
|
611
|
+
} else if (token.type === "group") flatten(token.tokens, true);
|
|
612
|
+
else {
|
|
613
|
+
if (group) token.optional = true;
|
|
614
|
+
res.push(token);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
flatten(parse(rule).tokens);
|
|
618
|
+
tokensCache[rule] = res;
|
|
619
|
+
return res;
|
|
620
|
+
}
|
|
621
|
+
function getHighest(rules) {
|
|
622
|
+
let weights = rules.map((rule) => getTokens(rule).length);
|
|
623
|
+
weights = weights.length === 0 ? [1] : weights;
|
|
624
|
+
return Math.max(...weights) + 2;
|
|
625
|
+
}
|
|
626
|
+
function computedWeight(rule, highest) {
|
|
627
|
+
const tokens = getTokens(rule);
|
|
628
|
+
const dym = tokens.filter((token) => token.type !== "text");
|
|
629
|
+
if (dym.length === 0) return 0;
|
|
630
|
+
let weight = dym.length;
|
|
631
|
+
let exp = 0;
|
|
632
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
633
|
+
const token = tokens[i];
|
|
634
|
+
const isDynamic = token.type !== "text";
|
|
635
|
+
const isWildcard = token.type === "wildcard";
|
|
636
|
+
const isOptional = !!token.optional;
|
|
637
|
+
exp += isDynamic ? 1 : 0;
|
|
638
|
+
if (i === tokens.length - 1 && isWildcard) weight += (isOptional ? 5 : 4) * 10 ** (tokens.length === 1 ? highest + 1 : highest);
|
|
639
|
+
else {
|
|
640
|
+
if (isWildcard) weight += 3 * 10 ** (highest - 1);
|
|
641
|
+
else weight += 2 * 10 ** exp;
|
|
642
|
+
if (isOptional) weight += 10 ** exp;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return weight;
|
|
646
|
+
}
|
|
647
|
+
function defaultPriority(rules) {
|
|
648
|
+
const highest = getHighest(rules);
|
|
649
|
+
return rules.sort((a, b) => computedWeight(a, highest) - computedWeight(b, highest));
|
|
650
|
+
}
|
|
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
|
+
*/
|
|
661
|
+
function matchingWeight(rules, url, priority) {
|
|
662
|
+
let matched = defaultPriority(rules.filter((rule) => isPathMatch(rule, url)));
|
|
663
|
+
const { global = [], special = {} } = priority;
|
|
664
|
+
if (global.length === 0 && isEmptyObject(special) || matched.length === 0) return matched;
|
|
665
|
+
const [dynamics, statics] = partition(matched, (rule) => getTokens(rule).filter((token) => token.type !== "text").length > 0);
|
|
666
|
+
const globalMatch = global.filter((rule) => dynamics.includes(rule));
|
|
667
|
+
if (globalMatch.length > 0) matched = uniq([
|
|
668
|
+
...statics,
|
|
669
|
+
...globalMatch,
|
|
670
|
+
...dynamics
|
|
671
|
+
]);
|
|
672
|
+
if (isEmptyObject(special)) return matched;
|
|
673
|
+
const specialRule = Object.keys(special).filter((rule) => matched.includes(rule))[0];
|
|
674
|
+
if (!specialRule) return matched;
|
|
675
|
+
const options = special[specialRule];
|
|
676
|
+
const { rules: lowerRules, when } = isArray(options) ? {
|
|
677
|
+
rules: options,
|
|
678
|
+
when: []
|
|
679
|
+
} : options;
|
|
680
|
+
if (lowerRules.includes(matched[0])) {
|
|
681
|
+
if (when.length === 0 || when.some((path) => pathToRegexp(path).regexp.test(url))) matched = uniq([specialRule, ...matched]);
|
|
682
|
+
}
|
|
683
|
+
return matched;
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/core/requestRecovery.ts
|
|
687
|
+
/**
|
|
688
|
+
* 请求复原
|
|
689
|
+
*
|
|
690
|
+
* 由于 parseReqBody 在解析请求时,会将请求流消费,
|
|
691
|
+
* 导致当接口不需要被 mock,继而由 vite http-proxy 转发时,请求流无法继续。
|
|
692
|
+
* 为此,我们在请求流中记录请求数据,当当前请求无法继续时,可以从备份中恢复请求流
|
|
693
|
+
*/
|
|
694
|
+
const requestCollectCache = /* @__PURE__ */ new WeakMap();
|
|
695
|
+
function collectRequest(req) {
|
|
696
|
+
const chunks = [];
|
|
697
|
+
req.addListener("data", (chunk) => {
|
|
698
|
+
chunks.push(Buffer.from(chunk));
|
|
699
|
+
});
|
|
700
|
+
req.addListener("end", () => {
|
|
701
|
+
if (chunks.length) requestCollectCache.set(req, Buffer.concat(chunks));
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function rewriteRequest(proxyReq, req) {
|
|
705
|
+
const buffer = requestCollectCache.get(req);
|
|
706
|
+
if (buffer) {
|
|
707
|
+
requestCollectCache.delete(req);
|
|
708
|
+
if (!proxyReq.headersSent) proxyReq.setHeader("Content-Length", buffer.byteLength);
|
|
709
|
+
if (!proxyReq.writableEnded) proxyReq.write(buffer);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/core/response.ts
|
|
714
|
+
/**
|
|
715
|
+
* 根据状态码获取状态文本
|
|
716
|
+
*/
|
|
717
|
+
function getHTTPStatusText(status) {
|
|
718
|
+
return HTTP_STATUS[status] || "Unknown";
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* 设置响应状态
|
|
722
|
+
*/
|
|
723
|
+
function provideResponseStatus(response, status = 200, statusText) {
|
|
724
|
+
response.statusCode = status;
|
|
725
|
+
response.statusMessage = statusText || getHTTPStatusText(status);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* 设置响应头
|
|
729
|
+
*/
|
|
730
|
+
async function provideResponseHeaders(req, res, mock, logger) {
|
|
731
|
+
const { headers, type = "json" } = mock;
|
|
732
|
+
const filepath = mock.__filepath__;
|
|
733
|
+
const contentType = mime.contentType(type) || mime.contentType(mime.lookup(type) || "");
|
|
734
|
+
if (contentType) res.setHeader("Content-Type", contentType);
|
|
735
|
+
res.setHeader("Cache-Control", "no-cache,max-age=0");
|
|
736
|
+
res.setHeader("X-Mock-Power-By", "vite-plugin-mock-dev-server");
|
|
737
|
+
if (filepath) res.setHeader("X-File-Path", filepath);
|
|
738
|
+
if (!headers) return;
|
|
739
|
+
try {
|
|
740
|
+
const raw = isFunction(headers) ? await headers(req) : headers;
|
|
741
|
+
Object.keys(raw).forEach((key) => {
|
|
742
|
+
res.setHeader(key, raw[key]);
|
|
743
|
+
});
|
|
744
|
+
} catch (e) {
|
|
745
|
+
logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${e}\n at headers (${ansis.underline(filepath)})`, mock.log);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* 设置响应cookie
|
|
750
|
+
*/
|
|
751
|
+
async function provideResponseCookies(req, res, mock, logger) {
|
|
752
|
+
const { cookies } = mock;
|
|
753
|
+
const filepath = mock.__filepath__;
|
|
754
|
+
if (!cookies) return;
|
|
755
|
+
try {
|
|
756
|
+
const raw = isFunction(cookies) ? await cookies(req) : cookies;
|
|
757
|
+
Object.keys(raw).forEach((key) => {
|
|
758
|
+
const cookie = raw[key];
|
|
759
|
+
if (isArray(cookie)) {
|
|
760
|
+
const [value, options] = cookie;
|
|
761
|
+
res.setCookie(key, value, options);
|
|
762
|
+
} else res.setCookie(key, cookie);
|
|
763
|
+
});
|
|
764
|
+
} catch (e) {
|
|
765
|
+
logger.error(`${ansis.red(`mock error at ${req.url.split("?")[0]}`)}\n${e}\n at cookies (${ansis.underline(filepath)})`, mock.log);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* 设置响应数据
|
|
770
|
+
*/
|
|
771
|
+
function sendResponseData(res, raw, type) {
|
|
772
|
+
if (isReadableStream(raw)) raw.pipe(res);
|
|
773
|
+
else if (Buffer.isBuffer(raw)) res.end(type === "text" || type === "json" ? raw.toString("utf-8") : raw);
|
|
774
|
+
else {
|
|
775
|
+
const content = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
776
|
+
res.end(type === "buffer" ? Buffer.from(content) : content);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* 实际响应延迟
|
|
781
|
+
*/
|
|
782
|
+
async function responseRealDelay(startTime, delay) {
|
|
783
|
+
if (!delay || typeof delay === "number" && delay <= 0 || isArray(delay) && delay.length !== 2) return;
|
|
784
|
+
let realDelay = 0;
|
|
785
|
+
if (isArray(delay)) {
|
|
786
|
+
const [min, max] = delay;
|
|
787
|
+
realDelay = random(min, max);
|
|
788
|
+
} else realDelay = delay - (timestamp() - startTime);
|
|
789
|
+
if (realDelay > 0) await sleep(realDelay);
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/core/mockMiddleware.ts
|
|
793
|
+
function baseMiddleware(compiler, { formidableOptions = {}, bodyParserOptions = {}, proxies, cookiesOptions, logger, priority = {}, cors: corsOptions }) {
|
|
794
|
+
const cors = createCors(corsOptions);
|
|
795
|
+
const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
|
|
796
|
+
const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
|
|
797
|
+
return async function(req, res, next) {
|
|
798
|
+
const startTime = timestamp();
|
|
799
|
+
const { query, pathname } = urlParse(req.url);
|
|
800
|
+
if (!pathname || proxies.length === 0) return next();
|
|
801
|
+
if (contextFilter.length && !contextFilter.some((context) => doesProxyContextMatchUrl(context, req))) return next();
|
|
802
|
+
if (globFilter.length && !isGlobProxiesMatch(pathname)) return next();
|
|
803
|
+
const mockData = compiler.mockData;
|
|
804
|
+
const mockUrls = matchingWeight(Object.keys(mockData), pathname, priority);
|
|
805
|
+
if (mockUrls.length === 0) return next();
|
|
806
|
+
collectRequest(req);
|
|
807
|
+
const { query: refererQuery } = urlParse(req.headers.referer || "");
|
|
808
|
+
const reqBody = await parseRequestBody(req, formidableOptions, bodyParserOptions);
|
|
809
|
+
const cookies = new Cookies(req, res, cookiesOptions);
|
|
810
|
+
const getCookie = cookies.get.bind(cookies);
|
|
811
|
+
const method = req.method.toUpperCase();
|
|
812
|
+
let mock;
|
|
813
|
+
let _mockUrl;
|
|
814
|
+
for (const mockUrl of mockUrls) {
|
|
815
|
+
mock = findMockData(mockData[mockUrl], logger, {
|
|
816
|
+
pathname,
|
|
817
|
+
method,
|
|
818
|
+
request: {
|
|
819
|
+
query,
|
|
820
|
+
refererQuery,
|
|
821
|
+
body: reqBody,
|
|
822
|
+
headers: req.headers,
|
|
823
|
+
getCookie
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
if (mock) {
|
|
827
|
+
_mockUrl = mockUrl;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (!mock) {
|
|
832
|
+
const matched = mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ");
|
|
833
|
+
logger.warn(`${ansis.green(pathname)} matches ${matched} , but mock data is not found.`);
|
|
834
|
+
return next();
|
|
835
|
+
}
|
|
836
|
+
if (cors) {
|
|
837
|
+
const [error] = await attemptAsync(cors, req, res);
|
|
838
|
+
if (error) {
|
|
839
|
+
logger.error(`CORS error: ${error}`);
|
|
840
|
+
return next(error);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const request = req;
|
|
844
|
+
const response = res;
|
|
845
|
+
request.body = reqBody;
|
|
846
|
+
request.query = query;
|
|
847
|
+
request.refererQuery = refererQuery;
|
|
848
|
+
request.params = parseRequestParams(mock.url, pathname);
|
|
849
|
+
request.getCookie = getCookie;
|
|
850
|
+
response.setCookie = cookies.set.bind(cookies);
|
|
851
|
+
const { body, delay, type = "json", response: responseFn, status = 200, statusText, log: logLevel, __filepath__: filepath } = mock;
|
|
852
|
+
provideResponseStatus(response, status, statusText);
|
|
853
|
+
await provideResponseHeaders(request, response, mock, logger);
|
|
854
|
+
await provideResponseCookies(request, response, mock, logger);
|
|
855
|
+
logger.info(requestLog(request, filepath), logLevel);
|
|
856
|
+
logger.debug(`${ansis.magenta("DEBUG")} ${ansis.underline(pathname)} matches: [ ${mockUrls.map((m) => m === _mockUrl ? ansis.underline.bold(m) : ansis.dim(m)).join(", ")} ]\n`);
|
|
857
|
+
if (body) {
|
|
858
|
+
try {
|
|
859
|
+
const content = isFunction(body) ? await body(request) : body;
|
|
860
|
+
await responseRealDelay(startTime, delay);
|
|
861
|
+
sendResponseData(response, content, type);
|
|
862
|
+
} catch (e) {
|
|
863
|
+
logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at body (${ansis.underline(filepath)})`, logLevel);
|
|
864
|
+
provideResponseStatus(response, 500);
|
|
865
|
+
res.end("");
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (responseFn) {
|
|
870
|
+
try {
|
|
871
|
+
await responseRealDelay(startTime, delay);
|
|
872
|
+
await responseFn(request, response, next);
|
|
873
|
+
} catch (e) {
|
|
874
|
+
logger.error(`${ansis.red(`mock error at ${pathname}`)}\n${e}\n at response (${ansis.underline(filepath)})`, logLevel);
|
|
875
|
+
provideResponseStatus(response, 500);
|
|
876
|
+
res.end("");
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
res.end("");
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/core/ws.ts
|
|
885
|
+
function mockWebSocket(compiler, httpServer, { wsProxies: proxies, cookiesOptions, logger }) {
|
|
886
|
+
const hmrMap = /* @__PURE__ */ new Map();
|
|
887
|
+
const poolMap = /* @__PURE__ */ new Map();
|
|
888
|
+
const wssContextMap = /* @__PURE__ */ new WeakMap();
|
|
889
|
+
const getWssMap = (mockUrl) => {
|
|
890
|
+
let wssMap = poolMap.get(mockUrl);
|
|
891
|
+
if (!wssMap) poolMap.set(mockUrl, wssMap = /* @__PURE__ */ new Map());
|
|
892
|
+
return wssMap;
|
|
893
|
+
};
|
|
894
|
+
const addHmr = (filepath, mockUrl) => {
|
|
895
|
+
let urlList = hmrMap.get(filepath);
|
|
896
|
+
if (!urlList) hmrMap.set(filepath, urlList = /* @__PURE__ */ new Set());
|
|
897
|
+
urlList.add(mockUrl);
|
|
898
|
+
};
|
|
899
|
+
const setupWss = (wssMap, wss, mock, context, pathname, filepath) => {
|
|
900
|
+
try {
|
|
901
|
+
mock.setup?.(wss, context);
|
|
902
|
+
wss.on("close", () => wssMap.delete(pathname));
|
|
903
|
+
wss.on("error", (e) => {
|
|
904
|
+
logger.error(`${ansis.red(`WebSocket mock error at ${wss.path}`)}\n${e}\n at setup (${filepath})`, mock.log);
|
|
905
|
+
});
|
|
906
|
+
} catch (e) {
|
|
907
|
+
logger.error(`${ansis.red(`WebSocket mock error at ${wss.path}`)}\n${e}\n at setup (${filepath})`, mock.log);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
const restartWss = (wssMap, wss, mock, pathname, filepath) => {
|
|
911
|
+
const { cleanupList, connectionList, context } = wssContextMap.get(wss);
|
|
912
|
+
cleanupRunner(cleanupList);
|
|
913
|
+
connectionList.forEach(({ ws }) => ws.removeAllListeners());
|
|
914
|
+
wss.removeAllListeners();
|
|
915
|
+
setupWss(wssMap, wss, mock, context, pathname, filepath);
|
|
916
|
+
connectionList.forEach(({ ws, req }) => emitConnection(wss, ws, req, connectionList));
|
|
917
|
+
};
|
|
918
|
+
compiler.on("update", ({ filepath }) => {
|
|
919
|
+
if (!hmrMap.has(filepath)) return;
|
|
920
|
+
const mockUrlList = hmrMap.get(filepath);
|
|
921
|
+
if (!mockUrlList) return;
|
|
922
|
+
for (const mockUrl of mockUrlList.values()) for (const mock of compiler.mockData[mockUrl]) {
|
|
923
|
+
if (!mock.ws || mock.__filepath__ !== filepath) return;
|
|
924
|
+
const wssMap = getWssMap(mockUrl);
|
|
925
|
+
for (const [pathname, wss] of wssMap.entries()) restartWss(wssMap, wss, mock, pathname, filepath);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
const [globFilter, contextFilter] = partition(proxies, (item) => isString(item) && item.includes("*"));
|
|
929
|
+
const { isMatch: isGlobProxiesMatch } = createMatcher(globFilter, [], false);
|
|
930
|
+
httpServer?.on("upgrade", (req, socket, head) => {
|
|
931
|
+
const { pathname, query } = urlParse(req.url);
|
|
932
|
+
if (!pathname || proxies.length === 0) return;
|
|
933
|
+
if (contextFilter.length && !contextFilter.some((context) => doesProxyContextMatchUrl(context, req))) return;
|
|
934
|
+
if (globFilter.length && !isGlobProxiesMatch(pathname)) return;
|
|
935
|
+
const mockData = compiler.mockData;
|
|
936
|
+
const mockUrl = objectKeys(mockData).find((key) => isPathMatch(key, pathname));
|
|
937
|
+
if (!mockUrl) return;
|
|
938
|
+
const mock = mockData[mockUrl].find((mock) => {
|
|
939
|
+
return mock.url && mock.ws && isPathMatch(mock.url, pathname);
|
|
940
|
+
});
|
|
941
|
+
if (!mock) return;
|
|
942
|
+
const filepath = mock.__filepath__;
|
|
943
|
+
addHmr(filepath, mockUrl);
|
|
944
|
+
const wssMap = getWssMap(mockUrl);
|
|
945
|
+
const wss = getWss(wssMap, pathname);
|
|
946
|
+
let wssContext = wssContextMap.get(wss);
|
|
947
|
+
if (!wssContext) {
|
|
948
|
+
const cleanupList = [];
|
|
949
|
+
const context = { onCleanup: (cleanup) => cleanupList.push(cleanup) };
|
|
950
|
+
wssContext = {
|
|
951
|
+
cleanupList,
|
|
952
|
+
context,
|
|
953
|
+
connectionList: []
|
|
954
|
+
};
|
|
955
|
+
wssContextMap.set(wss, wssContext);
|
|
956
|
+
setupWss(wssMap, wss, mock, context, pathname, filepath);
|
|
957
|
+
}
|
|
958
|
+
const request = req;
|
|
959
|
+
const cookies = new Cookies(req, req, cookiesOptions);
|
|
960
|
+
const { query: refererQuery } = urlParse(req.headers.referer || "");
|
|
961
|
+
request.query = query;
|
|
962
|
+
request.refererQuery = refererQuery;
|
|
963
|
+
request.params = parseRequestParams(mockUrl, pathname);
|
|
964
|
+
request.getCookie = cookies.get.bind(cookies);
|
|
965
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
966
|
+
logger.info(`${ansis.magenta(ansis.bold("WebSocket"))} ${ansis.green(req.url)} connected ${ansis.dim(`(${filepath})`)}`, mock.log);
|
|
967
|
+
wssContext.connectionList.push({
|
|
968
|
+
req: request,
|
|
969
|
+
ws
|
|
970
|
+
});
|
|
971
|
+
emitConnection(wss, ws, request, wssContext.connectionList);
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
httpServer?.on("close", () => {
|
|
975
|
+
for (const wssMap of poolMap.values()) {
|
|
976
|
+
for (const wss of wssMap.values()) {
|
|
977
|
+
cleanupRunner(wssContextMap.get(wss).cleanupList);
|
|
978
|
+
wss.close();
|
|
979
|
+
}
|
|
980
|
+
wssMap.clear();
|
|
981
|
+
}
|
|
982
|
+
poolMap.clear();
|
|
983
|
+
hmrMap.clear();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
function getWss(wssMap, pathname) {
|
|
987
|
+
let wss = wssMap.get(pathname);
|
|
988
|
+
if (!wss) wssMap.set(pathname, wss = new WebSocketServer({ noServer: true }));
|
|
989
|
+
return wss;
|
|
990
|
+
}
|
|
991
|
+
function emitConnection(wss, ws, req, connectionList) {
|
|
992
|
+
wss.emit("connection", ws, req);
|
|
993
|
+
ws.on("close", () => {
|
|
994
|
+
const i = connectionList.findIndex((item) => item.ws === ws);
|
|
995
|
+
if (i !== -1) connectionList.splice(i, 1);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function cleanupRunner(cleanupList) {
|
|
999
|
+
let cleanup;
|
|
1000
|
+
while (cleanup = cleanupList.shift()) cleanup?.();
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
export { processRawData as a, normalizePath as c, logLevels as d, getPackageDepList as f, processMockData as i, vfs as l, createMatcher as m, baseMiddleware as n, sortByValidator as o, getPackageDeps as p, rewriteRequest as r, waitingFor as s, mockWebSocket as t, createLogger as u };
|