mihawk
Version:
A tiny & simple mock server tool, support json,js,cjs,ts(typescript).
160 lines (159 loc) • 8.22 kB
JavaScript
;
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;
}
}