UNPKG

mocker-api

Version:

This is dev support mock RESTful API.

378 lines (360 loc) 12.9 kB
import URL from 'url'; import PATH from 'path'; import * as net from "net"; import * as http from "http"; import { Request, Response, NextFunction, Application } from 'express'; import bodyParser from 'body-parser'; import httpProxy from 'http-proxy'; import * as toRegexp from 'path-to-regexp'; import clearModule from 'clear-module'; import chokidar, { ChokidarOptions } from 'chokidar'; import color from 'colors-cli/safe'; import { proxyHandle } from './proxyHandle'; import { mockerHandle } from './mockerHandle'; export * from './delay'; export * from './utils'; export type ProxyTargetUrl = string | Partial<URL.Url>; export type MockerResultFunction = ((req: Request, res: Response, next?: NextFunction) => void); export type MockerResult = string | number| Array<any> | Record<string, any> | MockerResultFunction; /** * Setting a proxy router. * @example * * ```json * { * '/api/user': { * id: 1, * username: 'kenny', * sex: 6 * }, * 'DELETE /api/user/:id': (req, res) => { * res.send({ status: 'ok', message: '删除成功!' }); * } * } * ``` */ export type MockerProxyRoute = Record<string, MockerResult> & { /** * This is the option parameter setting for apiMocker * Priority processing. * apiMocker(app, path, option) * {@link MockerOption} */ _proxy?: MockerOption; } /** * Listening for proxy events. * This options contains listeners for [node-http-proxy](https://github.com/http-party/node-http-proxy#listening-for-proxy-events). * {typeof httpProxy.on} * {@link httpProxy} */ export interface HttpProxyListeners extends Record<string, any> { start?: ( req: http.IncomingMessage, res: http.ServerResponse, target: ProxyTargetUrl ) => void; proxyReq?: ( proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options: httpProxy.ServerOptions ) => void; proxyRes?: ( proxyRes: http.IncomingMessage, req: http.IncomingMessage, res: http.ServerResponse ) => void; proxyReqWs?: ( proxyReq: http.ClientRequest, req: http.IncomingMessage, socket: net.Socket, options: httpProxy.ServerOptions, head: any ) => void; econnreset?: ( err: Error, req: http.IncomingMessage, res: http.ServerResponse, target: ProxyTargetUrl ) => void end?: ( req: http.IncomingMessage, res: http.ServerResponse, proxyRes: http.IncomingMessage ) => void; /** * This event is emitted once the proxy websocket was closed. */ close?: ( proxyRes: http.IncomingMessage, proxySocket: net.Socket, proxyHead: any ) => void; } export interface MockerOption { /** * priority 'proxy' or 'mocker' * @default `proxy` * @issue [#151](https://github.com/jaywcjlove/mocker-api/issues/151) */ priority?: 'proxy' | 'mocker'; /** * `Boolean` Setting req headers host. */ changeHost?: boolean; /** * rewrite target's url path. * Object-keys will be used as RegExp to match paths. [#62](https://github.com/jaywcjlove/mocker-api/issues/62) * @default `{}` */ pathRewrite?: Record<string, string>, /** * Proxy settings, Turn a path string such as `/user/:name` into a regular expression. [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) * @default `{}` */ proxy?: Record<string, string>, /** * Set the [listen event](https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events) and [configuration](https://github.com/nodejitsu/node-http-proxy#options) of [http-proxy](https://github.com/nodejitsu/node-http-proxy) * @default `{}` */ httpProxy?: { options?: httpProxy.ServerOptions; listeners?: HttpProxyListeners }; /** * bodyParser settings. * @example * * ```js * bodyParser = {"text/plain": "text","text/html": "text"} * ``` * * will parsed `Content-Type='text/plain' and Content-Type='text/html'` with `bodyParser.text` * * @default `{}` */ bodyParserConf?: { [key: string]: 'raw' | 'text' | 'urlencoded' | 'json'; }; /** * [`bodyParserJSON`](https://github.com/expressjs/body-parser/tree/56a2b73c26b2238bc3050ad90af9ab9c62f4eb97#bodyparserjsonoptions) JSON body parser * @default `{}` */ bodyParserJSON?: bodyParser.OptionsJson; /** * [`bodyParserText`](https://github.com/expressjs/body-parser/tree/56a2b73c26b2238bc3050ad90af9ab9c62f4eb97#bodyparsertextoptions) Text body parser * @default `{}` */ bodyParserText?: bodyParser.OptionsText; /** * [`bodyParserRaw`](https://github.com/expressjs/body-parser/tree/56a2b73c26b2238bc3050ad90af9ab9c62f4eb97#bodyparserrawoptions) Raw body parser * @default `{}` */ bodyParserRaw?: bodyParser.Options; /** * [`bodyParserUrlencoded`](https://github.com/expressjs/body-parser/tree/56a2b73c26b2238bc3050ad90af9ab9c62f4eb97#bodyparserurlencodedoptions) URL-encoded form body parser * @default `{}` */ bodyParserUrlencoded?: bodyParser.OptionsUrlencoded; /** * Options object as defined [chokidar api options](https://github.com/paulmillr/chokidar#api) * @default `{}` */ watchOptions?: ChokidarOptions; /** * Access Control Allow options. * @default `{}` * @example * ```js * { * header: { * 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE', * } * } * ``` */ header?: Record<string,string | number | string[]>, /** * `Boolean` the proxy regular expression support full url path. * if the proxy regular expression like /test?a=1&b=1 can be matched */ withFullUrlPath?: boolean } const pathToRegexp = toRegexp.pathToRegexp; let mocker: MockerProxyRoute = {}; module.exports = mockerApi export default function mockerApi(app: Application, watchFile: string | string[] | MockerProxyRoute, conf: MockerOption = {}) { const watchFiles = (Array.isArray(watchFile) ? watchFile : typeof watchFile === 'string' ? [watchFile] : []).map(str => PATH.resolve(str)); if (watchFiles.some(file => !file)) { throw new Error('Mocker file does not exist!.'); } /** * Mybe watch file or pass parameters * https://github.com/jaywcjlove/mocker-api/issues/116 */ const isWatchFilePath = (Array.isArray(watchFile) && watchFile.every(val => typeof val === 'string')) || typeof watchFile === 'string'; mocker = isWatchFilePath ? getConfig() : watchFile; if (!mocker) { return (req: Request, res: Response, next: NextFunction) => { next(); } } let options: MockerOption = {...conf, ...(mocker._proxy || {})} const defaultOptions: MockerOption = { changeHost: true, pathRewrite: {}, proxy: {}, // proxy: proxyConf: {}, httpProxy: {}, // httpProxy: httpProxyConf: {}, bodyParserConf: {}, bodyParserJSON: {}, bodyParserText: {}, bodyParserRaw: {}, bodyParserUrlencoded: {}, watchOptions: {}, header: {}, priority: 'proxy', withFullUrlPath: false } options = { ...defaultOptions, ...options }; // changeHost = true, // pathRewrite = {}, // proxy: proxyConf = {}, // httpProxy: httpProxyConf = {}, // bodyParserConf= {}, // bodyParserJSON = {}, // bodyParserText = {}, // bodyParserRaw = {}, // bodyParserUrlencoded = {}, // watchOptions = {}, // header = {} if (isWatchFilePath) { // 监听配置入口文件所在的目录,一般为认为在配置文件/mock 目录下的所有文件 // 加上require.resolve,保证 `./mock/`能够找到`./mock/index.js`,要不然就要监控到上一级目录了 const watcher = chokidar.watch(watchFiles.map(watchFile => PATH.dirname(require.resolve(watchFile))), options.watchOptions); watcher.on('all', (event, path) => { if (event === 'change' || event === 'add') { try { // 当监听的可能是多个配置文件时,需要清理掉更新文件以及入口文件的缓存,重新获取 cleanCache(path); watchFiles.forEach(file => cleanCache(file)); mocker = getConfig(); if (mocker._proxy) { options = { ...options, ...mocker._proxy }; } console.log(`${color.green_b.black(' Done: ')} Hot Mocker ${color.green(path.replace(process.cwd(), ''))} file replacement success!`); } catch (ex) { console.error(`${color.red_b.black(' Failed: ')} Hot Mocker ${color.red(path.replace(process.cwd(), ''))} file replacement failed!!`); } } }) } // 监听文件修改重新加载代码 // 配置热更新 app.all('/*', (req: Request, res: Response, next: NextFunction) => { const getExecUrlPath = (req: Request) => { return options.withFullUrlPath ? req.url : req.path; } /** * Get Proxy key */ const proxyKey = Object.keys(options.proxy).find((kname) => { try { const { regexp } = pathToRegexp(kname.replace((new RegExp('^' + req.method + ' ')), '')) return !!regexp.exec(getExecUrlPath(req)); } catch (error) { console.error(`${color.red_b.black(' Failed: ')} The proxy configuration ${color.red(kname)} contains a syntax error!!\n doc: ${color.blue("https://www.npmjs.com/package/path-to-regexp/v/8.2.0")}`); return false; } }); /** * Get Mocker key * => `GET /api/:owner/:repo/raw/:ref` * => `GET /api/:owner/:repo/raw/:ref/(.*)` */ const mockerKey: string = Object.keys(mocker).find((kname) => { try { const { regexp } = pathToRegexp(kname.replace((new RegExp('^' + req.method + ' ')), '')) return !!regexp.exec(getExecUrlPath(req)); } catch (error) { console.error(`${color.red_b.black(' Failed: ')} The mocker configuration ${color.red(kname)} contains a syntax error!!\n doc: ${color.blue("https://www.npmjs.com/package/path-to-regexp/v/8.2.0")}`); return false; } }); /** * Access Control Allow options. * https://github.com/jaywcjlove/mocker-api/issues/61 */ const accessOptions: MockerOption['header'] = { '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]); }); const proxyKeyString: string = Object.keys(mocker).find((kname) => { try { const { regexp } = pathToRegexp(kname.replace((new RegExp('^(PUT|POST|GET|DELETE) ')), '')) return !!regexp.exec(getExecUrlPath(req)) } catch (error) { console.error(`${color.red_b.black(' Failed: ')} The mocker configuration ${color.red(kname)} contains a syntax error!!\n doc: ${color.blue("https://www.npmjs.com/package/path-to-regexp/v/8.2.0")}`); return false; } }) // 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' && proxyKeyString) { return res.sendStatus(200); } /** * priority 'proxy' or 'mocker' [#151](https://github.com/jaywcjlove/mocker-api/issues/151) */ if (options.priority === 'mocker') { if (mocker[mockerKey]) { return mockerHandle({ req, res, next, mocker, options, mockerKey}) } else if (proxyKey && options.proxy[proxyKey]) { return proxyHandle(req, res, options, proxyKey); } } else { if (proxyKey && options.proxy[proxyKey]) { return proxyHandle(req, res, options, proxyKey); } else if (mocker[mockerKey]) { return mockerHandle({ req, res, next, mocker, options, mockerKey}) } } next(); }); /** * The old module's resources to be released. * @param modulePath */ function cleanCache(modulePath: string) { // 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) {} var module = require.cache[modulePath]; if (!module) return; // https://github.com/jaywcjlove/mocker-api/issues/42 clearModule(modulePath); } /** * Merge multiple Mockers */ function getConfig() { return watchFiles.reduce((mocker, file) => { const mockerItem = require(file); return Object.assign(mocker, mockerItem.default ? mockerItem.default : mockerItem); }, {}) } return (req: Request, res: Response, next: NextFunction) => { next(); } }