vue-cli-plugin-mocker
Version:
@vue/cli plugin for mock data
339 lines (338 loc) • 14.2 kB
JavaScript
;
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();
};
};