UNPKG

mock-service-plugin

Version:
1,269 lines (1,235 loc) 41 kB
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