UNPKG

mihawk

Version:

A tiny & simple mock server tool, support json,js,cjs,ts(typescript).

253 lines (252 loc) 11.9 kB
'use strict'; 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, Debugger } 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); Debugger.log('formated options:', options); 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) || []; Debugger.log('Existed routes by scann:', existedRoutes); 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) { Debugger.log('Detected websocket server existed, will destory it...'); 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) { Debugger.log('Detected http/https server existed, will destory it...'); 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) { Debugger.log('Detected websocket server existed, will close it...'); 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) { Debugger.log('Detected http/https server existed, will destory it...'); 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); } } }, }; }