mihawk
Version:
A tiny & simple mock server tool, support json,js,cjs,ts(typescript).
247 lines (246 loc) • 11.4 kB
JavaScript
;
import http from 'http';
import https from 'https';
import { promisify } from 'util';
import Koa from 'koa';
import Colors from 'color-cc';
import mdwBodyParser from 'koa-bodyparser';
import mdwSSL from 'koa-sslify';
import mdwConnect from 'koa-connect';
import { existsSync, ensureDirSync, readFileSync } from 'fs-extra';
import dedupe from 'free-dedupe';
import { Printer } from './utils/print';
import { formatOptionsByConfig } from './composites/rc';
import { enableRequireTsFile, loadJS, loadTS, loadJson } from './composites/loader';
import { relPathToCWD, getRootAbsPath, unixifyPath, absifyPath } from './utils/path';
import mdwFavicon from './middlewares/favicon';
import mdwCertFileDown from './middlewares/cert-file';
import mdwCommon from './middlewares/common';
import mdwError from './middlewares/error';
import mdwCors from './middlewares/cors';
import mdwHdCache from './middlewares/cache';
import mdw404 from './middlewares/404';
import mdwRoutes from './middlewares/routes';
import mdwMock from './middlewares/mock';
import { isPortInUse, getMyIp, supportLocalHost } from './utils/net';
import { enhanceServer } from './utils/server';
import { isObjStrict } from './utils/is';
import { scanExistedRoutes } from './composites/scanner';
import { delNillProps } from './utils/obj';
import WsCtrl from './composites/websocket';
import { sleep } from './utils/async';
import { ASSET_CERT_CA_CRT_PATH, ASSET_CERT_LOCAL_CRT_PATH, ASSET_CERT_LOCAL_KEY_PATH, ASSET_FAVICON_PATH } from './root';
const PKG_ROOT_PATH = getRootAbsPath();
export default async function mihawk(config, isRestart = false) {
delete config._;
delete config['--'];
delete config.$schema;
delNillProps(config);
!isRestart && Printer.log('config:', config);
const options = formatOptionsByConfig(config);
const { cors, https: httpsConfig, useHttps, host, port, mockDir, mockDataDirPath, dataFileExt, useLogicFile, isTypesctiptMode, tsconfigPath, routesFilePath, middlewareFilePath, useWS, socketConfig, socketFilePath, } = options;
const loadLogicFile = isTypesctiptMode ? loadTS : loadJS;
const loadRoutesFile = useLogicFile ? loadLogicFile : loadJson;
const isPortAlreadyInUse = await isPortInUse(port);
if (isPortAlreadyInUse) {
Printer.error(Colors.yellow(`Port ${port} is already in use`));
process.exit(1);
}
ensureDirSync(mockDataDirPath);
if (isTypesctiptMode) {
let tsconfig = null;
if (existsSync(tsconfigPath)) {
tsconfig = require(tsconfigPath);
}
else {
!isRestart && Printer.log(Colors.gray(`Skip load "${unixifyPath(relPathToCWD(tsconfigPath))}"(file-not-existed), will use default build-in tsconfig.json`));
}
enableRequireTsFile(tsconfig || {});
!isRestart && Printer.log(Colors.success('Enable typescript mode success!'), Colors.gray('You can write logic in routes.ts, middleware.ts, data/**/*.ts'));
}
let routes = {};
if (existsSync(routesFilePath)) {
routes = (await loadRoutesFile(routesFilePath, { noLogPrint: true }));
!isRestart && Printer.log(Colors.success('Load routes file success!'), Colors.gray(unixifyPath(relPathToCWD(routesFilePath))));
}
let diyMiddleware = null;
if (useLogicFile && existsSync(middlewareFilePath)) {
const tmpFunction = await loadLogicFile(middlewareFilePath, { noLogPrint: true });
const isExpressMiddleware = typeof tmpFunction === 'function' && !!tmpFunction.isExpress;
diyMiddleware = isExpressMiddleware ? mdwConnect(tmpFunction) : tmpFunction;
!isRestart &&
Printer.log(Colors.success('Load custom middleware file success!'), Colors.gray(unixifyPath(relPathToCWD(middlewareFilePath))), isExpressMiddleware ? Colors.yellow('Express-Style-Middleware') : '');
}
const app = new Koa();
app.use(mdwError());
useHttps && app.use(mdwSSL({ hostname: host, port }));
useHttps && app.use(mdwCertFileDown());
app.use(mdwFavicon(ASSET_FAVICON_PATH));
app.use(mdwCommon(options));
cors && app.use(mdwCors());
app.use(mdwHdCache());
app.use(mdw404());
app.use(mdwBodyParser({
onerror: (err, ctx) => {
const invalidBody = err?.body;
Printer.error('mdw-body-parser:', Colors.yellow('Occurs error with code'), `${Colors.yellow.bold(`JSON.parse(${invalidBody})`)}${Colors.yellow(', will skip parse it!')}\n`, Colors.yellow(`${err.message}\n`), err);
ctx.status = 200;
ctx.request.body = invalidBody;
},
}));
app.use(mdwRoutes(routes));
typeof diyMiddleware === 'function' && app.use(diyMiddleware);
app.use(mdwMock(options));
const protocol = useHttps ? 'https' : 'http';
const addr1 = `${protocol}://${host}:${port}`;
let server = null;
if (useHttps) {
const httpsOptions = { key: null, cert: null, ca: null };
let key = '', cert = '', ca = '';
if (isObjStrict(httpsConfig)) {
key = httpsConfig.key;
cert = httpsConfig.cert;
ca = httpsConfig.ca;
}
const keyFilePath = absifyPath(key);
const certFilePath = absifyPath(cert);
const caFilePath = absifyPath(ca);
if (!key || !cert || !existsSync(keyFilePath) || !existsSync(certFilePath)) {
httpsOptions.key = readFileSync(ASSET_CERT_LOCAL_KEY_PATH);
httpsOptions.cert = readFileSync(ASSET_CERT_LOCAL_CRT_PATH);
httpsOptions.ca = readFileSync(ASSET_CERT_CA_CRT_PATH);
!isRestart && Printer.log(Colors.gray(`Custom https cert files ware not found, use default build-in https cert files`));
}
else {
httpsOptions.key = readFileSync(keyFilePath);
httpsOptions.cert = readFileSync(certFilePath);
if (ca && existsSync(caFilePath)) {
httpsOptions.ca = readFileSync(caFilePath);
}
!isRestart && Printer.log(Colors.success('Load https cert files success!'), Colors.gray(`key=${key}, cert=${cert}, ca=${ca || ''}`));
}
server = https.createServer(httpsOptions, app.callback());
}
else {
server = http.createServer(app.callback());
}
server.on('error', function (error) {
if (error.syscall !== 'listen') {
throw error;
}
switch (error.code) {
case 'EACCES':
Printer.error(Colors.error(`MockServer failed! Port ${port} requires elevated privileges!!!\n`));
process.exit(1);
break;
case 'EADDRINUSE':
Printer.error(Colors.error(`MockServer failed! Port ${port} is already in use!!!\n`));
process.exit(1);
break;
default:
Printer.error(Colors.red('Server Error:\n'), error);
throw error;
}
});
server.on('listening', function () {
Printer.log(Colors.green(`🚀 ${isRestart ? 'Restart' : 'Start'} mock-server success!`));
!isRestart && Printer.log('Mock directory: ', Colors.gray(unixifyPath(mockDir)));
const existedRoutes = scanExistedRoutes(mockDataDirPath, dataFileExt) || [];
let existedRoutePaths = existedRoutes.map(({ method, path }) => `${method} ${path}`);
existedRoutePaths.push(...Object.keys(routes));
existedRoutePaths = dedupe(existedRoutePaths);
existedRoutePaths.sort();
const existedCount = existedRoutePaths.length;
!isRestart && Printer.log(`Detected-Routes(${Colors.green(existedCount)}):`, existedCount ? existedRoutePaths : Colors.grey('empty'));
!isRestart && Printer.log(`Mock Server address:`);
Printer.log(`${Colors.gray('-')} ${Colors.cyan(addr1)}`);
if (supportLocalHost(host)) {
const addr2 = `${protocol}://${getMyIp()}:${port}`;
Printer.log(`${Colors.gray('-')} ${Colors.cyan(addr2)}`);
}
if (useHttps && !isRestart) {
Printer.log('🗝', Colors.gray(`You can download CA file for https dev, from url -> https://${getMyIp()}:${port}/.cert/ca.crt`));
}
!wsController && console.log();
});
server = enhanceServer(server);
server.listen(port, host);
let wsController = null;
if (useWS) {
const { stomp } = socketConfig || {};
let resolveFunc = null;
if (existsSync(socketFilePath)) {
resolveFunc = await loadLogicFile(socketFilePath, { noLogPrint: true });
!isRestart && Printer.log(Colors.success('Load socket logic file success!'), Colors.gray(unixifyPath(relPathToCWD(socketFilePath))));
}
wsController = new WsCtrl({
stomp,
server,
host,
port,
secure: useHttps,
resolve: resolveFunc,
});
wsController.start(null, isRestart);
}
return {
destory: async () => {
if (wsController) {
const destoryWsSvr = wsController?.destory?.bind(wsController);
if (typeof destoryWsSvr === 'function') {
try {
await destoryWsSvr();
wsController = null;
Printer.log('Websocket server has been destoryed.');
}
catch (error) {
Printer.error(`Destory websocket server failed!\n`, error);
}
}
}
await sleep(0);
if (server) {
const destoryServer = server?.destory?.bind(server);
if (typeof destoryServer === 'function') {
try {
await destoryServer();
server = null;
Printer.log(Colors.success(`Destory mock-server(${Colors.gray(addr1)}) success!`));
}
catch (error) {
Printer.error(`Destory Server Failed!\n`, error);
}
}
}
},
close: async () => {
if (wsController) {
const closeWsSvr = wsController?.close?.bind(wsController);
if (typeof closeWsSvr === 'function') {
try {
await closeWsSvr();
wsController = null;
Printer.log(Colors.success('Close websocket server success!'));
}
catch (error) {
Printer.error(`Close websocket server failed!\n`, error);
}
}
}
await sleep(0);
if (server) {
typeof server?.closeAllConnections === 'function' && server.closeAllConnections();
typeof server?.closeIdleConnections === 'function' && server.closeIdleConnections();
const closeServerAsync = promisify(server.close).bind(server);
try {
await closeServerAsync();
server = null;
Printer.log(Colors.success('Close Mock-Server success!'));
}
catch (error) {
Printer.error(`Close Server Failed!\n`, error);
}
}
},
};
}