mock-service-plugin
Version:
Simulated backend interface service
446 lines (438 loc) • 13.5 kB
JavaScript
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import Mock2 from 'mockjs';
import xml2js from 'xml2js';
import { stringify } from 'csv-stringify/sync';
// src/routes.ts
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;
}
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;
}
// src/constant.ts
var 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"
};
// src/parse-mocks-file.ts
var RE = /^\s*\/\*[*\s]*?([^*\r\n]+)[\s\S]*?@url\s+([^\s]+)(?:[\s\S]*?@method\s+([^\s]+))?(?:[\s\S]*?@content-type\s+([^\s]+))?[\s\S]*?\*\//im;
function parseMocksFile(dir) {
const routes = [];
const Detection = /* @__PURE__ */ 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();
}
}
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;
}
var BaseResponseHandler = class {
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
});
}
};
// src/response/json-handler.ts
var JsonResponseHandler = class extends BaseResponseHandler {
async handle(req, res, route) {
try {
this.setHeaders(res, "application/json");
const responseTemplate2 = typeof route.responseTemplate === "string" ? JSON.parse(route.responseTemplate) : route.responseTemplate;
const mockData = Mock2.mock(responseTemplate2);
res.json(mockData);
} catch (error) {
this.handleError(
res,
error instanceof Error ? error : new Error(String(error))
);
}
}
};
var SseResponseHandler = class 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}
`;
if (item.event) message += `event: ${item.event}
`;
if (item.data) {
const dataStr = typeof item.data === "string" ? item.data : JSON.stringify(item.data);
message += `data: ${dataStr}
`;
}
if (item.retry) message += `retry: ${item.retry}
`;
res.write(message + "\n");
};
try {
const mockData = new Function(
"return (" + route.responseTemplate + ")"
)();
if (mockData) {
const data = Mock2.mock(mockData);
const interval = data.interval || 1e3;
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: Mock2.mock(mockData) });
res.end();
}
} catch (e) {
console.error("[SSE Error]:", e);
res.write(`data: ${JSON.stringify({ error: "Mock data error" })}
`);
res.end();
}
req.on("close", () => {
console.log("Client closed SSE connection");
});
}
};
var TextResponseHandler = class 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;
}
const mockData = Mock2.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";
}
};
var XmlResponseHandler = class extends BaseResponseHandler {
constructor(options = {}) {
super(options);
this.builder = new xml2js.Builder();
}
async handle(req, res, route) {
try {
this.setHeaders(res, "application/xml");
const responseTemplate2 = typeof route.responseTemplate === "string" ? JSON.parse(route.responseTemplate) : route.responseTemplate;
const mockData = Mock2.mock(responseTemplate2);
const xml = this.builder.buildObject(mockData);
res.send(xml);
} catch (error) {
this.handleError(
res,
error instanceof Error ? error : new Error(String(error))
);
}
}
};
var CsvResponseHandler = class extends BaseResponseHandler {
async handle(req, res, route) {
try {
this.setHeaders(res, "text/csv");
const responseTemplate2 = typeof route.responseTemplate === "string" ? JSON.parse(route.responseTemplate) : route.responseTemplate;
const mockData = Mock2.mock(responseTemplate2);
const dataArray = Array.isArray(mockData) ? mockData : [mockData];
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;
}, {});
}
};
var ImageResponseHandler = class 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 = Mock2.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";
}
};
// src/response/handler-factory.ts
var 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
// 可以在这里添加更多处理器
};
var ResponseHandlerFactory = class {
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";
}
};
// src/routes.ts
var 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))
);
}
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();
}
};
}
export { createRoutes };
//# sourceMappingURL=routes.js.map
//# sourceMappingURL=routes.js.map