UNPKG

mihawk

Version:

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

160 lines (159 loc) 8.22 kB
'use strict'; import { join } from 'path'; import Colors from 'color-cc'; import { existsSync } from 'fs-extra'; import deepMerge from 'deepmerge'; import { writeJSONSafeSync } from '../utils/file'; import { Printer, Debugger } from '../utils/print'; import { absifyPath, formatPath, formatMockPath } from '../utils/path'; import { loadJS, loadJson, loadTS } from '../composites/loader'; import { isObjStrict } from '../utils/is'; import { LOG_ARROW, MOCK_DATA_DIR_NAME } from '../consts'; import { jsonRequest } from '../utils/request'; import { initMockLogicFile } from './init-file'; const RESOLVER_NAME = '[resolver]'; const LOGFLAG_RESOLVER = `${Colors.cyan(RESOLVER_NAME)}${Colors.gray(':')}`; export function createDataResolver(options) { const { mockDir, cache, useLogicFile, isTypesctiptMode, mockDataDirPath: MOCK_DATA_DIR_PATH, dataFileExt: JSON_EXT, logicFileExt: LOGIC_EXT, autoCreateMockLogicFile = false, setJsonByRemote, useRemoteData, } = options || {}; const loadConvertLogicFile = isTypesctiptMode ? loadTS : loadJS; const DATA_BASE_PATH = formatPath(join(mockDir, MOCK_DATA_DIR_NAME)); return async function getMockData(ctx) { const { disableLogPrint, mockRelPath, routePath } = ctx || {}; const mockRelPathNoExt = formatMockPath(mockRelPath); !disableLogPrint && Printer.log(LOGFLAG_RESOLVER, `${Colors.cyan(routePath)} ${LOG_ARROW} ${Colors.green(`./${mockRelPathNoExt}`)}`); const jsonPath = `${mockRelPathNoExt}.${JSON_EXT}`; const jsonPath4log = `${DATA_BASE_PATH}/${jsonPath}`; const mockJsonAbsPath = absifyPath(join(MOCK_DATA_DIR_PATH, jsonPath)); const initData = { code: 200, data: 'Empty data', msg: `Auto init file: ${jsonPath4log}` }; let mockJson = initData; if (existsSync(mockJsonAbsPath)) { let jsonData = null; if (useRemoteData && setJsonByRemote?.coverExistedJson) { const { method, headers, request } = ctx || {}; const body = request?.body; const remoteData = await fetchRemoteData(mockRelPath, { method, headers, body }, options); if (isObjStrict(remoteData)) { writeJSONSafeSync(mockJsonAbsPath, remoteData); jsonData = remoteData; } else { Printer.warn(LOGFLAG_RESOLVER, Colors.yellow(`RemoteData isn't a normal json response!`), Colors.yellow('Unexception value='), remoteData); jsonData = null; } } if (!jsonData) { jsonData = await loadJson(mockJsonAbsPath, { noCache: !cache }); } if (isObjStrict(jsonData)) { jsonData = cache ? jsonData : deepMerge({}, jsonData); } else { Printer.warn(LOGFLAG_RESOLVER, Colors.yellow(`MockDataFile isn't a normal json file!`), Colors.gray(jsonPath4log), Colors.yellow('Unexception value='), jsonData); } mockJson = jsonData || initData; } else { let finalInitData = initData; let finalInitType = 'default'; let remoteData = null; if (useRemoteData) { const { method, headers, request } = ctx || {}; const body = request?.body; remoteData = await fetchRemoteData(mockRelPath, { method, headers, body }, options); } if (remoteData) { finalInitData = remoteData; finalInitType = 'fallbackRemoteData'; } Debugger.log(RESOLVER_NAME, `MockDataFile isn't exists, will auto create it with ${finalInitType}...`, jsonPath4log); writeJSONSafeSync(mockJsonAbsPath, finalInitData); } ctx.set('X-Mock-Use-Default', mockJson === initData ? '1' : '0'); ctx.set('X-Mock-Use-Logic', 'none'); if (useLogicFile) { const logicPath = `${mockRelPathNoExt}.${LOGIC_EXT}`; const logicPath4log = `${DATA_BASE_PATH}/${logicPath}`; const mockLogicAbsPath = join(MOCK_DATA_DIR_PATH, logicPath); if (existsSync(mockLogicAbsPath)) { const dataConvertor = await loadConvertLogicFile(mockLogicAbsPath, { noCache: !cache }); if (typeof dataConvertor === 'function') { const { request } = ctx || {}; const extra = request; try { mockJson = await dataConvertor(mockJson, extra); ctx.set('X-Mock-Use-Logic', LOGIC_EXT); } catch (error) { Printer.error(LOGFLAG_RESOLVER, Colors.error(`Convert-function of MockLogicFile exec failed!`), Colors.yellow(logicPath4log), '\n', error); Printer.log(Colors.yellow(`Will return json (${jsonPath4log}) instead.`)); } if (!isObjStrict(mockJson)) { Printer.warn(LOGFLAG_RESOLVER, Colors.yellow("Convert-function of MockLogicFile, isn't return an json-object!"), Colors.gray(logicPath4log)); } } else { const exportInfo = isTypesctiptMode ? 'export default' : 'module.exports'; Printer.warn(LOGFLAG_RESOLVER, Colors.yellow(`MockLogicFile isn't ${exportInfo} a convert-function!`), Colors.gray(logicPath4log)); } } else { if (autoCreateMockLogicFile) { Printer.warn(LOGFLAG_RESOLVER, "MockLogicFile isn't exists, will auto ctreate it...", Colors.gray(logicPath4log)); initMockLogicFile(mockLogicAbsPath, { routePath, jsonPath4log, logicPath4log, logicFileExt: LOGIC_EXT, overwrite: false }); } else { Printer.warn(LOGFLAG_RESOLVER, Colors.yellow("MockLogicFile isn't exists!"), Colors.gray(logicPath4log)); } } } return mockJson; }; } async function fetchRemoteData(reqPath, reqOptions, mhkOptions) { const { setJsonByRemote } = mhkOptions; if (typeof setJsonByRemote !== 'object') { Debugger.log(LOGFLAG_RESOLVER, 'FetchRemoteData: setJsonByRemote isnot a object, will skip remote data fetch!'); return null; } if (!setJsonByRemote?.enable) { Debugger.log(LOGFLAG_RESOLVER, 'FetchRemoteData: setJsonByRemote.enable != true, will skip remote data fetch!'); return null; } if (!setJsonByRemote?.target) { Printer.warn(LOGFLAG_RESOLVER, 'FetchRemoteData: setJsonByRemote.target is not defined, will skip remote data fetch!'); return null; } try { const { target, timeout = 10000, changeOrigin, rewrite } = setJsonByRemote; if (typeof rewrite === 'function') { reqPath = rewrite(reqPath); } const requestPath = `${target}/${reqPath}`; const { method = 'GET', headers: originalHeaders = {}, body } = reqOptions || {}; const headers = { ...originalHeaders, 'Cache-Control': 'no-cache', Accept: 'application/json', }; const targetUrl = new URL(target); const targetHost = targetUrl.host; if (changeOrigin && targetHost) { headers.Host = targetHost; Debugger.log(LOGFLAG_RESOLVER, `Apply changeOrigin: ${Colors.cyan(headers.Host)}`); } Printer.log(LOGFLAG_RESOLVER, `FetchRemoteData: Fetching remote data from ${Colors.cyan(requestPath)}`); const data = await jsonRequest(requestPath, { method, timeout, headers, body }); if (isObjStrict(data)) { data.mihawkMessage = `Auto init json data from remote: ${requestPath}`; return data; } else { Printer.error(LOGFLAG_RESOLVER, 'FetchRemoteData: Invalid response data format', data); return null; } } catch (error) { Printer?.error(LOGFLAG_RESOLVER, 'FetchRemoteData: Remote data fetch failed:', error); return null; } }