UNPKG

mihawk

Version:

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

223 lines (222 loc) 8.16 kB
'use strict'; import vm from 'vm'; import path from 'path'; import { transpileModule, ModuleKind, ScriptTarget } from 'typescript'; import { existsSync, readFileSync } from 'fs-extra'; import * as json5 from 'json5'; import Colors from 'color-cc'; import LRUCache from 'lru-cache'; import { CWD } from '../consts'; import { absifyPath, getRootAbsPath, isPathInDir, relPathToCWD, unixifyPath } from '../utils/path'; import { Printer } from '../utils/print'; import { isNil, isNumStrict } from '../utils/is'; import { getByteSize } from '../utils/str'; const LOGFLAG_LOADER = Colors.cyan('[loader]') + Colors.gray(':'); const _cacheJson = new LRUCache({ max: 50 }); export async function loadJson(jsonFilePath, options) { const { noCache = false, noLogPrint = false } = options || {}; jsonFilePath = absifyPath(jsonFilePath); const json = await _loadFileWithCache(jsonFilePath, { cacheObj: _cacheJson, forceRefresh: noCache, noLogPrint, resolveData: async (JsonStr) => { let jsonData = {}; try { jsonData = JsonStr ? json5.parse(JsonStr) : {}; } catch (error) { Printer.error(LOGFLAG_LOADER, 'Parse json file failed!', Colors.gray(jsonFilePath), '\n', error); jsonData = {}; } return jsonData; }, }); return json; } export async function loadJS(jsFilePath, options) { const { noCache = false, noLogPrint = false } = options || {}; jsFilePath = absifyPath(jsFilePath); if (noCache) { refreshTsOrJs(jsFilePath); } try { const mod = require(jsFilePath); !noLogPrint && Printer.log(LOGFLAG_LOADER, `LoadJS${noCache ? Colors.gray('(nocache)') : ''}: ${Colors.gray(unixifyPath(relPathToCWD(jsFilePath)))}`); return mod; } catch (error) { Printer.error(LOGFLAG_LOADER, Colors.red('Load js file failed!'), Colors.gray(jsFilePath), '\n', error); return null; } } export async function loadTS(tsFilePath, options) { const { noCache = false, noLogPrint = false } = options || {}; tsFilePath = absifyPath(tsFilePath); if (!require.extensions['.ts']) { Printer.warn(LOGFLAG_LOADER, Colors.warn('Need to invoke enableRequireTsFile() first before load ts file')); return null; } if (noCache) { refreshTsOrJs(tsFilePath); } try { const mod = require(tsFilePath); !noLogPrint && Printer.log(LOGFLAG_LOADER, `LoadTS${noCache ? Colors.gray('(nocache)') : ''}: ${Colors.gray(unixifyPath(relPathToCWD(tsFilePath)))}`); const res = mod?.default; if (isNil(res)) { Printer.warn(LOGFLAG_LOADER, Colors.yellow('ts file should export default, but not found'), res); } return res; } catch (error) { Printer.error(LOGFLAG_LOADER, Colors.red('Load ts file failed!'), Colors.gray(tsFilePath), '\n', error); return null; } } export function refreshJson(jsonFilePath) { return _cacheJson.has(jsonFilePath) && _cacheJson.del(jsonFilePath); } export function refreshTsOrJs(filePath) { return _clearSelfAndAncestorsCache(filePath); } export function enableRequireTsFile(tsconfig) { if (!require.extensions['.ts']) { require.extensions['.ts'] = _genTsFileRequireHandle(tsconfig || {}); } } export function loadFileFromRoot(relFilePath) { const data = require(path.resolve(getRootAbsPath(), relFilePath)); return data; } export function readPackageJson() { return loadFileFromRoot('./package.json'); } async function _loadFileWithCache(filePath, options) { const { cacheObj, resolveData, forceRefresh = false, noLogPrint = false, maxSize = 0 } = options; let cacheData = null; if (!forceRefresh && cacheObj.has(filePath)) { cacheData = cacheObj.get(filePath); } else { let fileContent = null; const isFileExist = existsSync(filePath); try { if (isFileExist) { fileContent = readFileSync(filePath, 'utf-8'); !noLogPrint && Printer.log(LOGFLAG_LOADER, `LoadJson${forceRefresh ? Colors.gray('(nocache)') : ''}: ${Colors.gray(unixifyPath(relPathToCWD(filePath)))}`); } } catch (error) { Printer.error(LOGFLAG_LOADER, 'Read file failed!', Colors.gray(filePath), '\n', error); } if (typeof resolveData === 'function') { cacheData = await resolveData(fileContent); } else { cacheData = fileContent; } if (cacheData) { if (maxSize !== undefined && isNumStrict(maxSize) && maxSize > 0) { if (getByteSize(fileContent) > maxSize * 1024) { Printer.warn(LOGFLAG_LOADER, 'File content is too large, skip cache it!', Colors.gray(filePath)); } else { cacheObj.set(filePath, cacheData); } } else { cacheObj.set(filePath, cacheData); } } } return cacheData; } function _genTsFileRequireHandle(tsconfig) { tsconfig = (tsconfig || {}); const tsTranspileOption = { ...tsconfig, compilerOptions: { ...tsconfig.compilerOptions, module: ModuleKind.CommonJS, target: ScriptTarget.ES2015, moduleResolution: 'node', allowSyntheticDefaultImports: true, allowJs: true, resolveJsonModule: true, esModuleInterop: true, }, }; return function (module, tsFilePath) { const tsCode = readFileSync(tsFilePath, 'utf8'); const result = transpileModule(tsCode, { ...tsTranspileOption, fileName: tsFilePath, }); const jsCode = result.outputText; const vmContext = vm.createContext({ ...global }); vmContext.global = global; vmContext.require = require; vmContext.module = module; vmContext.exports = module.exports; vmContext.__dirname = path.dirname(tsFilePath); vmContext.__filename = tsFilePath; vmContext.console = console; vmContext.process = process; vmContext.Buffer = Buffer; vm.runInNewContext(jsCode, vmContext, { filename: tsFilePath, displayErrors: true, }); }; } function _clearRequireCache(filename) { if (CWD === filename || !isPathInDir(filename, CWD)) { return; } filename = absifyPath(filename); const mod = require.cache[filename]; if (!mod) { return; } const parent = mod?.parent; mod.loaded = false; delete require.cache[filename]; if (parent && typeof parent === 'object') { try { const parentChildList = parent.children; if (Array.isArray(parentChildList)) { const index = parentChildList.findIndex(item => item.filename === filename); if (index > -1) { parentChildList.splice(index, 1); } } const pathCache = module?.constructor?._pathCache; if (pathCache && typeof pathCache === 'object') { Object.keys(pathCache).forEach(key => { if (pathCache[key]?.includes(filename)) { delete pathCache[key]; } }); } } catch (error) { Printer.error(LOGFLAG_LOADER, 'Clear require.cache failed!', Colors.gray(filename), '\n', error); } } } function _clearSelfAndAncestorsCache(filename) { filename = absifyPath(filename); const PKG_ROOT = getRootAbsPath(); if (!(filename === CWD || isPathInDir(filename, CWD) || filename === PKG_ROOT || isPathInDir(filename, PKG_ROOT))) { return; } const mod = require.cache[filename]; if (!mod) { return; } const parent = mod?.parent; const parentId = parent?.id; _clearRequireCache(filename); parentId && _clearSelfAndAncestorsCache(parentId); }