mock-service-plugin
Version:
1,269 lines (1,235 loc) • 41 kB
JavaScript
import express from 'express';
import watch from 'watch';
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Mock from 'mockjs';
import xml2js from 'xml2js';
function getDirname(importMetaUrl) {
return path.dirname(fileURLToPath(importMetaUrl));
}
function walk(dir) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach((file) => {
file = dir + "/" + file;
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
results = results.concat(walk(file));
}
else {
results.push(file);
}
});
return results;
}
/**
* 匹配请求路径到路由表中的路由
* @param {Array<MockRoute>} routes 路由表数组
* @param {string} method HTTP方法
* @param {string} urlpath 请求路径
* @returns {MockRoute|null} 匹配到的路由对象和参数,不匹配则返回null
*/
function matchRoute(routes, method, urlpath) {
// 移除查询参数
const pathWithoutQuery = urlpath.split("?")[0];
// 先尝试精确匹配
const exactMatch = routes.find((r) => r.method === method && r.restfulTemplateUrl === urlpath);
if (exactMatch)
return exactMatch;
// 分割请求路径
const pathParts = pathWithoutQuery.split("/").filter((part) => part !== "");
// 检查每个路由的动态匹配
for (const route of routes) {
// 方法不匹配则跳过
if (route.method !== method)
continue;
const routeParts = route.path.split("/").filter((part) => part !== "");
// 长度不匹配则跳过
if (routeParts.length !== pathParts.length)
continue;
let isMatch = true;
for (let i = 0; i < routeParts.length; i++) {
const routePart = routeParts[i];
const pathPart = pathParts[i];
if (routePart.startsWith(":")) {
// 动态参数部分
routePart.slice(1);
}
else if (routePart !== pathPart) {
// 静态部分不匹配
isMatch = false;
break;
}
}
if (isMatch) {
return route;
}
}
// 没有匹配的路由
return null;
}
const CONTENT_TYPES = {
".json": "application/json",
".txt": "text/plain",
".html": "text/html",
".xml": "application/xml",
".csv": "text/csv",
".md": "text/markdown",
".pdf": "application/pdf",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".css": "text/css",
".js": "application/javascript",
".yaml": "application/x-yaml",
".yml": "application/x-yaml",
".sse": "text/event-stream",
};
// 优化正则表达式,支持灵活注解位置
const RE = /^\s*\/\*[*\s]*?([^*\r\n]+)[\s\S]*?@url\s+([^\s]+)(?:[\s\S]*?@method\s+([^\s]+))?(?:[\s\S]*?@content-type\s+([^\s]+))?[\s\S]*?\*\//im;
/**
* 解析 mock 文件目录,提取路由信息
* @param {string} dir - mock 文件目录路径
* @returns {MockRoute[]} 路由配置数组
*/
function parseMocksFile(dir) {
const routes = [];
const Detection = new Set();
const files = walk(dir);
(files || []).forEach((filepath) => {
let content = fs.readFileSync(filepath, "utf8").trim() || "{}";
const fileExt = path.extname(filepath).toLowerCase();
const fileName = path.basename(filepath, path.extname(filepath));
let restfulTemplateUrl = filepath;
let describe = "No description";
let contentType = CONTENT_TYPES[fileExt] || "application/json";
let method = "GET";
// 尝试匹配注解
const match = content.match(RE);
if (match) {
describe = match[1].trim();
restfulTemplateUrl = match[2].trim();
if (match[3]) {
method = match[3].trim().toUpperCase() || "GET";
}
if (match[4]) {
contentType = match[4].trim();
}
// 提取注释后的实际内容
const contentEndIndex = content.indexOf("*/") + 2;
if (contentEndIndex > 2) {
content = content.substring(contentEndIndex).trim();
}
}
// 规范化 restfulTemplateUrl
if (!restfulTemplateUrl.startsWith("/")) {
restfulTemplateUrl = "/" + restfulTemplateUrl;
}
// 提取路径及查询参数
const [routePath, query] = restfulTemplateUrl.split("?");
// 创建路由键(方法 + 路径)
const routeKey = `${method}:${routePath}`;
if (Detection.has(routeKey)) {
console.warn(`[Mock Warn]: [${filepath}: ${routeKey}] already exists and will be overwritten`);
}
else {
Detection.add(routeKey);
const params = {
filepath,
fileName,
fileExt,
routeKey,
method,
path: routePath,
query,
restfulTemplateUrl,
describe,
contentType,
responseTemplate: content,
};
routes.push(params);
}
});
return routes;
}
class BaseResponseHandler {
constructor(options = {}) {
this.options = options;
}
/**
* 设置响应头
* @param {Response} res - 响应对象
* @param {string} contentType - 内容类型
*/
setHeaders(res, contentType) {
res.set("Content-Type", contentType);
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "GET,HEAD,PUT,POST,DELETE,PATCH");
}
/**
* 处理文件响应
* @param {string} filePath - 文件路径
* @returns {Promise<Buffer>}
*/
async readFile(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, (err, data) => {
if (err)
reject(err);
else
resolve(data);
});
});
}
/**
* 处理错误
* @param {Response} res - 响应对象
* @param {Error} error - 错误对象
*/
handleError(res, error) {
console.error(`[Response Error] ${error.message}`);
res.status(500).json({
error: "Internal Server Error",
message: error.message,
});
}
}
class JsonResponseHandler extends BaseResponseHandler {
async handle(req, res, route) {
try {
this.setHeaders(res, "application/json");
// 如果模板是字符串,尝试解析为 JSON
const responseTemplate = typeof route.responseTemplate === "string"
? JSON.parse(route.responseTemplate)
: route.responseTemplate;
// 使用 mockjs 渲染模板
const mockData = Mock.mock(responseTemplate);
res.json(mockData);
}
catch (error) {
this.handleError(res, error instanceof Error ? error : new Error(String(error)));
}
}
}
class SseResponseHandler extends BaseResponseHandler {
async handle(req, res, route) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const sendEvent = (item) => {
let message = "";
if (item.id)
message += `id: ${item.id}\n`;
if (item.event)
message += `event: ${item.event}\n`;
if (item.data) {
const dataStr = typeof item.data === "string" ? item.data : JSON.stringify(item.data);
message += `data: ${dataStr}\n`;
}
if (item.retry)
message += `retry: ${item.retry}\n`;
res.write(message + "\n");
};
try {
const mockData = new Function("return (" + route.responseTemplate + ")")();
if (mockData) {
const data = Mock.mock(mockData);
const interval = data.interval || 1000;
if (Array.isArray(data.items)) {
let index = 0;
const sendNext = () => {
if (index < data.items.length) {
sendEvent(data.items[index]);
index++;
setTimeout(sendNext, interval);
}
else {
res.end();
}
};
sendNext();
}
else {
sendEvent({ data });
res.end();
}
}
else {
sendEvent({ data: Mock.mock(mockData) });
res.end();
}
}
catch (e) {
console.error("[SSE Error]:", e);
res.write(`data: ${JSON.stringify({ error: "Mock data error" })}\n\n`);
res.end();
}
req.on("close", () => {
console.log("Client closed SSE connection");
});
}
}
class TextResponseHandler extends BaseResponseHandler {
async handle(req, res, route) {
try {
const contentType = this.getContentType(route);
this.setHeaders(res, contentType);
// 如果模板是字符串,直接使用
if (typeof route.responseTemplate === "string") {
res.send(route.responseTemplate);
return;
}
// 如果是对象,使用 mockjs 处理
const mockData = Mock.mock(route.responseTemplate);
res.send(mockData);
return;
}
catch (error) {
this.handleError(res, error instanceof Error ? error : new Error(String(error)));
}
}
getContentType(route) {
const extension = route.filepath
? route.filepath.split(".").pop()?.toLowerCase() || ""
: "";
const contentTypeMap = {
txt: "text/plain",
html: "text/html",
md: "text/markdown",
css: "text/css",
js: "application/javascript",
};
return contentTypeMap[extension] || "text/plain";
}
}
class XmlResponseHandler extends BaseResponseHandler {
constructor(options = {}) {
super(options);
this.builder = new xml2js.Builder();
}
async handle(req, res, route) {
try {
this.setHeaders(res, "application/xml");
// 如果模板是字符串,尝试解析为对象
const responseTemplate = typeof route.responseTemplate === "string"
? JSON.parse(route.responseTemplate)
: route.responseTemplate;
// 使用 mockjs 处理数据
const mockData = Mock.mock(responseTemplate);
// 转换为 XML
const xml = this.builder.buildObject(mockData);
res.send(xml);
}
catch (error) {
this.handleError(res, error instanceof Error ? error : new Error(String(error)));
}
}
}
// Lodash implementation of `get`
const charCodeOfDot = ".".charCodeAt(0);
const reEscapeChar = /\\(\\)?/g;
const rePropName = RegExp(
// Match anything that isn't a dot or bracket.
"[^.[\\]]+" +
"|" +
// Or match property names within brackets.
"\\[(?:" +
// Match a non-string expression.
"([^\"'][^[]*)" +
"|" +
// Or match strings (supports escaping characters).
"([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2" +
")\\]" +
"|" +
// Or match "" as the space between consecutive dots or empty brackets.
"(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))",
"g",
);
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
const reIsPlainProp = /^\w*$/;
const getTag = function (value) {
return Object.prototype.toString.call(value);
};
const isSymbol = function (value) {
const type = typeof value;
return (
type === "symbol" ||
(type === "object" && value && getTag(value) === "[object Symbol]")
);
};
const isKey = function (value, object) {
if (Array.isArray(value)) {
return false;
}
const type = typeof value;
if (
type === "number" ||
type === "symbol" ||
type === "boolean" ||
!value ||
isSymbol(value)
) {
return true;
}
return (
reIsPlainProp.test(value) ||
!reIsDeepProp.test(value) ||
(object != null && value in Object(object))
);
};
const stringToPath = function (string) {
const result = [];
if (string.charCodeAt(0) === charCodeOfDot) {
result.push("");
}
string.replace(rePropName, function (match, expression, quote, subString) {
let key = match;
if (quote) {
key = subString.replace(reEscapeChar, "$1");
} else if (expression) {
key = expression.trim();
}
result.push(key);
});
return result;
};
const castPath = function (value, object) {
if (Array.isArray(value)) {
return value;
} else {
return isKey(value, object) ? [value] : stringToPath(value);
}
};
const toKey = function (value) {
if (typeof value === "string" || isSymbol(value)) return value;
const result = `${value}`;
// eslint-disable-next-line
return result == "0" && 1 / value == -INFINITY ? "-0" : result;
};
const get = function (object, path) {
path = castPath(path, object);
let index = 0;
const length = path.length;
while (object != null && index < length) {
object = object[toKey(path[index++])];
}
return index && index === length ? object : undefined;
};
const is_object = function (obj) {
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
};
const normalize_columns = function (columns) {
if (columns === undefined || columns === null) {
return [undefined, undefined];
}
if (typeof columns !== "object") {
return [Error('Invalid option "columns": expect an array or an object')];
}
if (!Array.isArray(columns)) {
const newcolumns = [];
for (const k in columns) {
newcolumns.push({
key: k,
header: columns[k],
});
}
columns = newcolumns;
} else {
const newcolumns = [];
for (const column of columns) {
if (typeof column === "string") {
newcolumns.push({
key: column,
header: column,
});
} else if (
typeof column === "object" &&
column !== null &&
!Array.isArray(column)
) {
if (!column.key) {
return [
Error('Invalid column definition: property "key" is required'),
];
}
if (column.header === undefined) {
column.header = column.key;
}
newcolumns.push(column);
} else {
return [
Error("Invalid column definition: expect a string or an object"),
];
}
}
columns = newcolumns;
}
return [undefined, columns];
};
class CsvError extends Error {
constructor(code, message, ...contexts) {
if (Array.isArray(message)) message = message.join(" ");
super(message);
if (Error.captureStackTrace !== undefined) {
Error.captureStackTrace(this, CsvError);
}
this.code = code;
for (const context of contexts) {
for (const key in context) {
const value = context[key];
this[key] = Buffer.isBuffer(value)
? value.toString()
: value == null
? value
: JSON.parse(JSON.stringify(value));
}
}
}
}
const underscore = function (str) {
return str.replace(/([A-Z])/g, function (_, match) {
return "_" + match.toLowerCase();
});
};
const normalize_options = function (opts) {
const options = {};
// Merge with user options
for (const opt in opts) {
options[underscore(opt)] = opts[opt];
}
// Normalize option `bom`
if (
options.bom === undefined ||
options.bom === null ||
options.bom === false
) {
options.bom = false;
} else if (options.bom !== true) {
return [
new CsvError("CSV_OPTION_BOOLEAN_INVALID_TYPE", [
"option `bom` is optional and must be a boolean value,",
`got ${JSON.stringify(options.bom)}`,
]),
];
}
// Normalize option `delimiter`
if (options.delimiter === undefined || options.delimiter === null) {
options.delimiter = ",";
} else if (Buffer.isBuffer(options.delimiter)) {
options.delimiter = options.delimiter.toString();
} else if (typeof options.delimiter !== "string") {
return [
new CsvError("CSV_OPTION_DELIMITER_INVALID_TYPE", [
"option `delimiter` must be a buffer or a string,",
`got ${JSON.stringify(options.delimiter)}`,
]),
];
}
// Normalize option `quote`
if (options.quote === undefined || options.quote === null) {
options.quote = '"';
} else if (options.quote === true) {
options.quote = '"';
} else if (options.quote === false) {
options.quote = "";
} else if (Buffer.isBuffer(options.quote)) {
options.quote = options.quote.toString();
} else if (typeof options.quote !== "string") {
return [
new CsvError("CSV_OPTION_QUOTE_INVALID_TYPE", [
"option `quote` must be a boolean, a buffer or a string,",
`got ${JSON.stringify(options.quote)}`,
]),
];
}
// Normalize option `quoted`
if (options.quoted === undefined || options.quoted === null) {
options.quoted = false;
}
// Normalize option `escape_formulas`
if (
options.escape_formulas === undefined ||
options.escape_formulas === null
) {
options.escape_formulas = false;
} else if (typeof options.escape_formulas !== "boolean") {
return [
new CsvError("CSV_OPTION_ESCAPE_FORMULAS_INVALID_TYPE", [
"option `escape_formulas` must be a boolean,",
`got ${JSON.stringify(options.escape_formulas)}`,
]),
];
}
// Normalize option `quoted_empty`
if (options.quoted_empty === undefined || options.quoted_empty === null) {
options.quoted_empty = undefined;
}
// Normalize option `quoted_match`
if (
options.quoted_match === undefined ||
options.quoted_match === null ||
options.quoted_match === false
) {
options.quoted_match = null;
} else if (!Array.isArray(options.quoted_match)) {
options.quoted_match = [options.quoted_match];
}
if (options.quoted_match) {
for (const quoted_match of options.quoted_match) {
const isString = typeof quoted_match === "string";
const isRegExp = quoted_match instanceof RegExp;
if (!isString && !isRegExp) {
return [
Error(
`Invalid Option: quoted_match must be a string or a regex, got ${JSON.stringify(quoted_match)}`,
),
];
}
}
}
// Normalize option `quoted_string`
if (options.quoted_string === undefined || options.quoted_string === null) {
options.quoted_string = false;
}
// Normalize option `eof`
if (options.eof === undefined || options.eof === null) {
options.eof = true;
}
// Normalize option `escape`
if (options.escape === undefined || options.escape === null) {
options.escape = '"';
} else if (Buffer.isBuffer(options.escape)) {
options.escape = options.escape.toString();
} else if (typeof options.escape !== "string") {
return [
Error(
`Invalid Option: escape must be a buffer or a string, got ${JSON.stringify(options.escape)}`,
),
];
}
if (options.escape.length > 1) {
return [
Error(
`Invalid Option: escape must be one character, got ${options.escape.length} characters`,
),
];
}
// Normalize option `header`
if (options.header === undefined || options.header === null) {
options.header = false;
}
// Normalize option `columns`
const [errColumns, columns] = normalize_columns(options.columns);
if (errColumns !== undefined) return [errColumns];
options.columns = columns;
// Normalize option `quoted`
if (options.quoted === undefined || options.quoted === null) {
options.quoted = false;
}
// Normalize option `cast`
if (options.cast === undefined || options.cast === null) {
options.cast = {};
}
// Normalize option cast.bigint
if (options.cast.bigint === undefined || options.cast.bigint === null) {
// Cast boolean to string by default
options.cast.bigint = (value) => "" + value;
}
// Normalize option cast.boolean
if (options.cast.boolean === undefined || options.cast.boolean === null) {
// Cast boolean to string by default
options.cast.boolean = (value) => (value ? "1" : "");
}
// Normalize option cast.date
if (options.cast.date === undefined || options.cast.date === null) {
// Cast date to timestamp string by default
options.cast.date = (value) => "" + value.getTime();
}
// Normalize option cast.number
if (options.cast.number === undefined || options.cast.number === null) {
// Cast number to string using native casting by default
options.cast.number = (value) => "" + value;
}
// Normalize option cast.object
if (options.cast.object === undefined || options.cast.object === null) {
// Stringify object as JSON by default
options.cast.object = (value) => JSON.stringify(value);
}
// Normalize option cast.string
if (options.cast.string === undefined || options.cast.string === null) {
// Leave string untouched
options.cast.string = function (value) {
return value;
};
}
// Normalize option `on_record`
if (
options.on_record !== undefined &&
typeof options.on_record !== "function"
) {
return [Error(`Invalid Option: "on_record" must be a function.`)];
}
// Normalize option `record_delimiter`
if (
options.record_delimiter === undefined ||
options.record_delimiter === null
) {
options.record_delimiter = "\n";
} else if (Buffer.isBuffer(options.record_delimiter)) {
options.record_delimiter = options.record_delimiter.toString();
} else if (typeof options.record_delimiter !== "string") {
return [
Error(
`Invalid Option: record_delimiter must be a buffer or a string, got ${JSON.stringify(options.record_delimiter)}`,
),
];
}
switch (options.record_delimiter) {
case "unix":
options.record_delimiter = "\n";
break;
case "mac":
options.record_delimiter = "\r";
break;
case "windows":
options.record_delimiter = "\r\n";
break;
case "ascii":
options.record_delimiter = "\u001e";
break;
case "unicode":
options.record_delimiter = "\u2028";
break;
}
return [undefined, options];
};
const bom_utf8 = Buffer.from([239, 187, 191]);
const stringifier = function (options, state, info) {
return {
options: options,
state: state,
info: info,
__transform: function (chunk, push) {
// Chunk validation
if (!Array.isArray(chunk) && typeof chunk !== "object") {
return Error(
`Invalid Record: expect an array or an object, got ${JSON.stringify(chunk)}`,
);
}
// Detect columns from the first record
if (this.info.records === 0) {
if (Array.isArray(chunk)) {
if (
this.options.header === true &&
this.options.columns === undefined
) {
return Error(
"Undiscoverable Columns: header option requires column option or object records",
);
}
} else if (this.options.columns === undefined) {
const [err, columns] = normalize_columns(Object.keys(chunk));
if (err) return;
this.options.columns = columns;
}
}
// Emit the header
if (this.info.records === 0) {
this.bom(push);
const err = this.headers(push);
if (err) return err;
}
// Emit and stringify the record if an object or an array
try {
// this.emit('record', chunk, this.info.records);
if (this.options.on_record) {
this.options.on_record(chunk, this.info.records);
}
} catch (err) {
return err;
}
// Convert the record into a string
let err, chunk_string;
if (this.options.eof) {
[err, chunk_string] = this.stringify(chunk);
if (err) return err;
if (chunk_string === undefined) {
return;
} else {
chunk_string = chunk_string + this.options.record_delimiter;
}
} else {
[err, chunk_string] = this.stringify(chunk);
if (err) return err;
if (chunk_string === undefined) {
return;
} else {
if (this.options.header || this.info.records) {
chunk_string = this.options.record_delimiter + chunk_string;
}
}
}
// Emit the csv
this.info.records++;
push(chunk_string);
},
stringify: function (chunk, chunkIsHeader = false) {
if (typeof chunk !== "object") {
return [undefined, chunk];
}
const { columns } = this.options;
const record = [];
// Record is an array
if (Array.isArray(chunk)) {
// We are getting an array but the user has specified output columns. In
// this case, we respect the columns indexes
if (columns) {
chunk.splice(columns.length);
}
// Cast record elements
for (let i = 0; i < chunk.length; i++) {
const field = chunk[i];
const [err, value] = this.__cast(field, {
index: i,
column: i,
records: this.info.records,
header: chunkIsHeader,
});
if (err) return [err];
record[i] = [value, field];
}
// Record is a literal object
// `columns` is always defined: it is either provided or discovered.
} else {
for (let i = 0; i < columns.length; i++) {
const field = get(chunk, columns[i].key);
const [err, value] = this.__cast(field, {
index: i,
column: columns[i].key,
records: this.info.records,
header: chunkIsHeader,
});
if (err) return [err];
record[i] = [value, field];
}
}
let csvrecord = "";
for (let i = 0; i < record.length; i++) {
let options, err;
let [value, field] = record[i];
if (typeof value === "string") {
options = this.options;
} else if (is_object(value)) {
options = value;
value = options.value;
delete options.value;
if (
typeof value !== "string" &&
value !== undefined &&
value !== null
) {
if (err)
return [
Error(
`Invalid Casting Value: returned value must return a string, null or undefined, got ${JSON.stringify(value)}`,
),
];
}
options = { ...this.options, ...options };
[err, options] = normalize_options(options);
if (err !== undefined) {
return [err];
}
} else if (value === undefined || value === null) {
options = this.options;
} else {
return [
Error(
`Invalid Casting Value: returned value must return a string, an object, null or undefined, got ${JSON.stringify(value)}`,
),
];
}
const {
delimiter,
escape,
quote,
quoted,
quoted_empty,
quoted_string,
quoted_match,
record_delimiter,
escape_formulas,
} = options;
if ("" === value && "" === field) {
let quotedMatch =
quoted_match &&
quoted_match.filter((quoted_match) => {
if (typeof quoted_match === "string") {
return value.indexOf(quoted_match) !== -1;
} else {
return quoted_match.test(value);
}
});
quotedMatch = quotedMatch && quotedMatch.length > 0;
const shouldQuote =
quotedMatch ||
true === quoted_empty ||
(true === quoted_string && false !== quoted_empty);
if (shouldQuote === true) {
value = quote + value + quote;
}
csvrecord += value;
} else if (value) {
if (typeof value !== "string") {
return [
Error(
`Formatter must return a string, null or undefined, got ${JSON.stringify(value)}`,
),
];
}
const containsdelimiter =
delimiter.length && value.indexOf(delimiter) >= 0;
const containsQuote = quote !== "" && value.indexOf(quote) >= 0;
const containsEscape = value.indexOf(escape) >= 0 && escape !== quote;
const containsRecordDelimiter = value.indexOf(record_delimiter) >= 0;
const quotedString = quoted_string && typeof field === "string";
let quotedMatch =
quoted_match &&
quoted_match.filter((quoted_match) => {
if (typeof quoted_match === "string") {
return value.indexOf(quoted_match) !== -1;
} else {
return quoted_match.test(value);
}
});
quotedMatch = quotedMatch && quotedMatch.length > 0;
// See https://github.com/adaltas/node-csv/pull/387
// More about CSV injection or formula injection, when websites embed
// untrusted input inside CSV files:
// https://owasp.org/www-community/attacks/CSV_Injection
// http://georgemauer.net/2017/10/07/csv-injection.html
// Apple Numbers unicode normalization is empirical from testing
if (escape_formulas) {
switch (value[0]) {
case "=":
case "+":
case "-":
case "@":
case "\t":
case "\r":
case "\uFF1D": // Unicode '='
case "\uFF0B": // Unicode '+'
case "\uFF0D": // Unicode '-'
case "\uFF20": // Unicode '@'
value = `'${value}`;
break;
}
}
const shouldQuote =
containsQuote === true ||
containsdelimiter ||
containsRecordDelimiter ||
quoted ||
quotedString ||
quotedMatch;
if (shouldQuote === true && containsEscape === true) {
const regexp =
escape === "\\"
? new RegExp(escape + escape, "g")
: new RegExp(escape, "g");
value = value.replace(regexp, escape + escape);
}
if (containsQuote === true) {
const regexp = new RegExp(quote, "g");
value = value.replace(regexp, escape + quote);
}
if (shouldQuote === true) {
value = quote + value + quote;
}
csvrecord += value;
} else if (
quoted_empty === true ||
(field === "" && quoted_string === true && quoted_empty !== false)
) {
csvrecord += quote + quote;
}
if (i !== record.length - 1) {
csvrecord += delimiter;
}
}
return [undefined, csvrecord];
},
bom: function (push) {
if (this.options.bom !== true) {
return;
}
push(bom_utf8);
},
headers: function (push) {
if (this.options.header === false) {
return;
}
if (this.options.columns === undefined) {
return;
}
let err;
let headers = this.options.columns.map((column) => column.header);
if (this.options.eof) {
[err, headers] = this.stringify(headers, true);
headers += this.options.record_delimiter;
} else {
[err, headers] = this.stringify(headers);
}
if (err) return err;
push(headers);
},
__cast: function (value, context) {
const type = typeof value;
try {
if (type === "string") {
// Fine for 99% of the cases
return [undefined, this.options.cast.string(value, context)];
} else if (type === "bigint") {
return [undefined, this.options.cast.bigint(value, context)];
} else if (type === "number") {
return [undefined, this.options.cast.number(value, context)];
} else if (type === "boolean") {
return [undefined, this.options.cast.boolean(value, context)];
} else if (value instanceof Date) {
return [undefined, this.options.cast.date(value, context)];
} else if (type === "object" && value !== null) {
return [undefined, this.options.cast.object(value, context)];
} else {
return [undefined, value, value];
}
} catch (err) {
return [err];
}
},
};
};
const stringify = function (records, opts = {}) {
const data = [];
const [err, options] = normalize_options(opts);
if (err !== undefined) throw err;
const state = {
stop: false,
};
// Information
const info = {
records: 0,
};
const api = stringifier(options, state, info);
for (const record of records) {
const err = api.__transform(record, function (record) {
data.push(record);
});
if (err !== undefined) throw err;
}
if (data.length === 0) {
api.bom((d) => {
data.push(d);
});
const err = api.headers((headers) => {
data.push(headers);
});
if (err !== undefined) throw err;
}
return data.join("");
};
class CsvResponseHandler extends BaseResponseHandler {
async handle(req, res, route) {
try {
this.setHeaders(res, "text/csv");
// 如果模板是字符串,尝试解析为对象
const responseTemplate = typeof route.responseTemplate === "string"
? JSON.parse(route.responseTemplate)
: route.responseTemplate;
// 使用 mockjs 处理数据
const mockData = Mock.mock(responseTemplate);
// 确保数据是数组格式
const dataArray = Array.isArray(mockData) ? mockData : [mockData];
// 转换为 CSV
const csv = stringify(dataArray, {
header: true,
columns: this.getColumns(dataArray[0]),
});
res.send(csv);
}
catch (error) {
this.handleError(res, error instanceof Error ? error : new Error(String(error)));
}
}
getColumns(data) {
if (!data)
return {};
return Object.keys(data).reduce((acc, key) => {
acc[key] = key;
return acc;
}, {});
}
}
class ImageResponseHandler extends BaseResponseHandler {
async handle(req, res, route) {
try {
const contentType = this.getContentType(route);
this.setHeaders(res, contentType);
// 如果模板是字符串,假设它是图片路径
if (typeof route.responseTemplate === "string") {
const imagePath = route.responseTemplate;
const imageBuffer = await this.readFile(imagePath);
res.send(imageBuffer);
return;
}
// 如果是对象,尝试获取图片路径
const mockData = Mock.mock(route.responseTemplate);
if (typeof mockData === "string") {
const imagePath = mockData;
const imageBuffer = await this.readFile(imagePath);
res.send(imageBuffer);
return;
}
throw new Error("Invalid image responseTemplate format");
}
catch (error) {
this.handleError(res, error instanceof Error ? error : new Error(String(error)));
}
}
getContentType(route) {
const extension = route.filepath
? route.filepath.split(".").pop()?.toLowerCase() || ""
: "";
const contentTypeMap = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
};
return contentTypeMap[extension] || "image/png";
}
}
const CONTENT_TYPE_HANDLERS = {
"application/json": JsonResponseHandler,
"text/event-stream": SseResponseHandler,
"text/plain": TextResponseHandler,
"text/html": TextResponseHandler,
"text/markdown": TextResponseHandler,
"text/css": TextResponseHandler,
"application/javascript": TextResponseHandler,
"application/xml": XmlResponseHandler,
"text/csv": CsvResponseHandler,
"image/png": ImageResponseHandler,
"image/jpeg": ImageResponseHandler,
"image/gif": ImageResponseHandler,
"image/svg+xml": ImageResponseHandler,
// 可以在这里添加更多处理器
};
class ResponseHandlerFactory {
static getHandler(contentType) {
const HandlerClass = CONTENT_TYPE_HANDLERS[contentType];
if (!HandlerClass) {
throw new Error(`No handler found for content type: ${contentType}`);
}
return new HandlerClass();
}
static getContentTypeByExtension(extension) {
return CONTENT_TYPES[extension.toLowerCase()] || "application/json";
}
}
const responseTemplate = fs.readFileSync(path.join(getDirname(import.meta.url), "../index.html"), "utf8");
function createRoutes(mockPath) {
return async function (req, res, next) {
const mockRoutes = parseMocksFile(mockPath);
if (req.url === "/") {
return res.send(responseTemplate.replace("@menuList", JSON.stringify(mockRoutes)));
}
// 处理 CORS 预检请求
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "GET,HEAD,PUT,POST,DELETE,PATCH");
const allowedHeaders = req.headers["access-control-request-headers"];
if (allowedHeaders) {
res.set("Access-Control-Allow-Headers", allowedHeaders);
}
return res.send("");
}
// 查找匹配的路由
const matchedRoute = matchRoute(mockRoutes, req.method, req.url);
if (matchedRoute) {
try {
// 获取内容类型
const contentType = matchedRoute.contentType ||
ResponseHandlerFactory.getContentTypeByExtension(path.extname(matchedRoute.filepath));
// 获取对应的响应处理器
const handler = ResponseHandlerFactory.getHandler(contentType);
// 处理响应
await handler.handle(req, res, matchedRoute);
console.log(chalk.magentaBright("- [Mock Interface] "), req.url);
}
catch (error) {
console.error(chalk.red(`[Mock Error] Failed to mock response for ${req.url}:`), error);
res.status(500).json({ error: "Failed to generate mock response" });
}
}
else {
next();
}
};
}
function startServer({ path, port = 3000 }) {
const mockPort = port;
const app = express();
app.use("/", createRoutes(path));
watch.watchTree(path, () => {
try {
// 重新创建路由处理器
app.use("/", createRoutes(path));
console.info("模块更新成功");
}
catch (error) {
console.error("模块更新失败:", error);
}
});
const server = app.listen(mockPort, () => {
const address = server.address();
if (address && typeof address !== "string") {
address.address;
const port = address.port;
console.log("\n", "------------------------------------", "\n", chalk.green(`[mock server]: http://localhost:${port}`), "\n", "------------------------------------", "\n");
}
});
}
/*
* @Description: mock service plugin
* @Author: Gleason
* @Date: 2021-04-11 14:26:23
* @LastEditors: Gleason
* @LastEditTime: 2022-03-20 00:11:27
*/
/**
* @file plugin entry point
* @author gleason
*/
/**
* @class mockServicePlugin
*
* @param {Object} param data that plugin needs
*/
class MockServicePlugin {
constructor({ path, port = 3000 }) {
this.path = path;
this.port = port;
}
apply() {
startServer({ path: this.path, port: this.port });
}
}
export { MockServicePlugin as default };
//# sourceMappingURL=index.js.map