UNPKG

vue-cli-plugin-mocker

Version:

@vue/cli plugin for mock data

339 lines (338 loc) 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const url_1 = __importDefault(require("url")); const path_1 = __importDefault(require("path")); const body_parser_1 = __importDefault(require("body-parser")); const http_proxy_1 = __importDefault(require("http-proxy")); const path_to_regexp_1 = require("path-to-regexp"); const clear_module_1 = __importDefault(require("clear-module")); const chokidar_1 = __importDefault(require("chokidar")); const safe_1 = __importDefault(require("colors-cli/safe")); const glob_1 = __importDefault(require("glob")); const slash_1 = __importDefault(require("./slash")); const delay_1 = __importDefault(require("./delay")); /* Mocker 代理路由 */ let mocker = {}; let firstLog = true; const pathMatch = (options) => { options = options || {}; return (path) => { const keys = []; const re = path_to_regexp_1.pathToRegexp(path, keys, options); return (pathname, params) => { const m = re.exec(pathname); if (!m) return false; params = params || {}; let key; let param; for (let i = 0; i < keys.length; i++) { key = keys[i]; param = m[i + 1]; // eslint-disable-next-line no-continue if (!param) continue; params[key.name] = decodeURIComponent(param); if (key.repeat) params[key.name] = params[key.name].split(key.delimiter); } return params; }; }; }; /** * The old module's resources to be released. * @param modulePath * @param mockerData */ const cleanCache = (modulePath, mockerData = {}) => { const mockerItem = require(modulePath); const data = mockerItem.default ? mockerItem.default : mockerItem; Object.keys(data).forEach((key) => { delete mockerData[key]; }); // The entry file does not have a .js suffix, // causing the module's resources not to be released. // https://github.com/jaywcjlove/webpack-api-mocker/issues/30 try { modulePath = require.resolve(modulePath); } catch (e) { } const module = require.cache[modulePath]; if (!module) return; // remove reference in module.parent if (module.parent) { module.parent.children.splice(module.parent.children.indexOf(module), 1); } clear_module_1.default(modulePath); }; /** * 获取目录下 mock 文件 * @param mockerDir * @param include * @param ignore */ const getMockerFiles = (mockerDir, include = '**/*.[jt]s', ignore) => { const mockerFiles = glob_1.default.sync(`${mockerDir}/${include}`, { ignore }).map((p) => path_1.default.resolve(p)); return mockerFiles; }; /** * 获取 mock 数据 * @param mockerFile * @param type */ const getMockerData = (mockerFile, type = 'init') => { const mockerFiles = typeof mockerFile === 'string' ? [mockerFile] : mockerFile; // eslint-disable-next-line @typescript-eslint/no-shadow return mockerFiles.reduce((mock, file) => { let mockerItem; switch (type) { case 'update': case 'delete': cleanCache(file, mocker); break; case 'init': case 'add': default: } try { mockerItem = require(file); } catch (e) { } if (mockerItem) { return Object.assign(mocker, mockerItem.default ? mockerItem.default : mockerItem); } return mock; }, {}); }; /** * * @param app * @param data * @param conf * @param include */ exports.default = (app, data, conf = {}, include = '**/*.[jt]s') => { const isWatchDir = typeof data === 'string'; const mockerFiles = getMockerFiles(data, include); mocker = isWatchDir ? getMockerData(mockerFiles) : data; if (!mocker) { return (req, res, next) => { next(); }; } let options = { ...conf, ...(mocker.option || {}) }; const defaultOptions = { changeHost: true, pathRewrite: {}, proxy: {}, httpProxy: {}, bodyParserConf: {}, bodyParserJSON: {}, bodyParserText: {}, bodyParserRaw: {}, bodyParserUrlencoded: {}, watchOptions: {}, header: {}, delay: {}, }; options = { ...defaultOptions, ...options }; if (isWatchDir) { // 监听配置入口文件所在的目录,一般为认为在配置文件/mock 目录下的所有文件 const watcher = chokidar_1.default.watch(data, options.watchOptions); watcher.on('all', (event, path) => { /** * 打印日志 * @param type */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const log = (type) => { let typeName = ''; switch (type) { case 'add': typeName = 'add '; break; case 'change': typeName = 'update '; break; case 'unlink': typeName = 'delete '; break; default: typeName = ''; } const prefix = firstLog ? '\n' : ''; console.log(`${prefix}${safe_1.default.green_b.black(' Done: ')} Hot Mocker ${safe_1.default.green(slash_1.default(path))} file ${typeName}success!`); if (firstLog) { firstLog = false; } }; options = { ...options, ...(mocker.option || {}) }; try { switch (event) { case 'add': getMockerData(path_1.default.resolve(path), 'add'); log(event); break; case 'change': getMockerData(path_1.default.resolve(path), 'update'); options = { ...options, ...(mocker.option || {}) }; log(event); break; case 'unlink': getMockerData(path_1.default.resolve(path), 'delete'); log(event); break; default: } } catch (e) { console.error(`${safe_1.default.red_b.black(' Failed: ')} Hot Mocker ${safe_1.default.red(slash_1.default(path))} file failed!!`); } }); } // 监听文件修改重新加载代码 // 配置热更新 // eslint-disable-next-line consistent-return app.all('/*', (req, res, next) => { /** * Get Proxy key */ const proxyKey = Object.keys(options.proxy).find((name) => { return !!path_to_regexp_1.pathToRegexp(name.replace(new RegExp(`^${req.method} `), '')).exec(req.path); }); /** * Get Mocker key * => `GET /api/:owner/:repo/raw/:ref` * => `GET /api/:owner/:repo/raw/:ref/(.*)` */ const mockerKey = Object.keys(mocker).find((name) => { return !!path_to_regexp_1.pathToRegexp(name.replace(new RegExp(`^${req.method} `), '')).exec(req.path); }); /** * Access Control Allow options. * https://github.com/jaywcjlove/mocker-api/issues/61 */ const accessOptions = { 'Access-Control-Allow-Origin': req.get('Origin') || '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE', 'Access-Control-Allow-Headers': `Content-Type, X-Requested-With,${req.header('access-control-request-headers') || ''}`, 'Access-Control-Allow-Credentials': 'true', ...options.header, }; Object.keys(accessOptions).forEach((keyName) => { res.setHeader(keyName, accessOptions[keyName]); }); // fix issue 34 https://github.com/jaywcjlove/mocker-api/issues/34 // In some cross-origin http request, the browser will send the preflighted options request before sending the request methods written in the code. if (!mockerKey && req.method.toLocaleUpperCase() === 'OPTIONS' && Object.keys(mocker).find((name) => !!path_to_regexp_1.pathToRegexp(name.replace(new RegExp('^(PUT|POST|GET|DELETE) '), '')).exec(req.path))) { return res.sendStatus(200); } if (proxyKey && options.proxy[proxyKey]) { const currentProxy = options.proxy[proxyKey]; const url = url_1.default.parse(currentProxy); if (options.changeHost) { req.headers.host = url.host; } const { options: proxyOptions = {}, listeners: proxyListeners = {}, } = options.httpProxy; /** * rewrite target's url path. Object-keys will be used as RegExp to match paths. * https://github.com/jaywcjlove/mocker-api/issues/62 */ Object.keys(options.pathRewrite).forEach((rgxStr) => { const rePath = req.path.replace(new RegExp(rgxStr), options.pathRewrite[rgxStr]); const currentPath = [rePath]; if (req.url.indexOf('?') > 0) { currentPath.push(req.url.replace(/(.*)\?/, '')); } req.query = url_1.default.parse(req.url, true).query; // eslint-disable-next-line no-multi-assign req.url = req.originalUrl = currentPath.join('?'); }); const proxyHTTP = http_proxy_1.default.createProxyServer({}); proxyHTTP.on('error', (err) => { console.error(`${safe_1.default.red_b.black(` Proxy Failed: ${err.name}`)} ${err.message || ''} ${err.stack || ''} !!`); }); Object.keys(proxyListeners).forEach((event) => { proxyHTTP.on(event, proxyListeners[event]); }); proxyHTTP.web(req, res, { target: url.href, ...proxyOptions }); } else if (mocker[mockerKey]) { let bodyParserMethod = body_parser_1.default.json({ ...options.bodyParserJSON }); // 默认使用json解析 /** * `application/x-www-form-urlencoded; charset=UTF-8` => `application/x-www-form-urlencoded` * Issue: https://github.com/jaywcjlove/mocker-api/issues/50 */ let contentType = req.get('Content-Type'); contentType = contentType && contentType.replace(/;.*$/, ''); if (options.bodyParserConf && options.bodyParserConf[contentType]) { // 如果存在options.bodyParserConf配置 {'text/plain': 'text','text/html': 'text'} // eslint-disable-next-line default-case switch (options.bodyParserConf[contentType] // 获取bodyParser的方法 ) { case 'raw': bodyParserMethod = body_parser_1.default.raw({ ...options.bodyParserRaw }); break; case 'text': bodyParserMethod = body_parser_1.default.text({ ...options.bodyParserText }); break; case 'urlencoded': bodyParserMethod = body_parser_1.default.urlencoded({ extended: false, ...options.bodyParserUrlencoded }); break; case 'json': bodyParserMethod = body_parser_1.default.json({ ...options.bodyParserJSON }); // 使用json解析 break; } } else { // 兼容原来的代码,默认解析 // Compatible with the original code, default parsing // eslint-disable-next-line default-case switch (contentType) { case 'text/plain': bodyParserMethod = body_parser_1.default.raw({ ...options.bodyParserRaw }); break; case 'text/html': bodyParserMethod = body_parser_1.default.text({ ...options.bodyParserText }); break; case 'application/x-www-form-urlencoded': bodyParserMethod = body_parser_1.default.urlencoded({ extended: false, ...options.bodyParserUrlencoded }); break; } } bodyParserMethod(req, res, () => { const result = mocker[mockerKey]; let jsonResult; if (typeof result === 'function') { const rgxStr = ~mockerKey.indexOf(' ') ? ' ' : ''; req.params = pathMatch({ sensitive: false, strict: false, end: false, })(mockerKey.split(new RegExp(rgxStr))[1])(url_1.default.parse(req.url).pathname); jsonResult = result(req, res, next); } else { jsonResult = result; } if (jsonResult) { delay_1.default(jsonResult, options.delay).then(() => { res.json(jsonResult); }); } }); } else { next(); } }); return (req, res, next) => { next(); }; };