UNPKG

mock-service-plugin

Version:
446 lines (438 loc) 13.5 kB
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