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