@aniyajs/rotor
Version:
基于webpack5开发的一款专注于打包、运行的工具
789 lines (692 loc) • 20.4 kB
JavaScript
;
const fs = require("fs-extra");
const chalk = require('chalk');
const path = require('path');
const cliProgress = require("cli-progress");
const fg = require('fast-glob');
const { createHash } = require("crypto");
/**
* 判断文件是否存在
* @param {string} files
* @param {string} dirPath
* @param {boolean} flag 是否打印
*
* @returns {boolean} 文件是否存在
*/
function isFileExists(files, dirPath, flag=false) {
const missFiles = [];
files.forEach((file) => {
const isExist = fs.existsSync(path.join(file));
if (!isExist) {
missFiles.push(path.join(file));
}
});
if (missFiles.length) {
!flag && missFiles.forEach((file) => {
console.log(` ${chalk.gray('--')} ${chalk.red(path.relative(dirPath, file))}`);
});
return false;
} else {
return true;
}
}
/**
* 获取指定文件及深度依赖文件路径
* @param {Array} filePaths
* @param {Array} dependentSuffixs
* @param {RegExp} filterRule 过滤规则
*
* @returns {number} 文件路径的深度
*/
function depthDependFilePaths(filePaths, dependentSuffixs, filterRule = null, allPath = []) {
let depthDependPaths = []
filePaths.forEach((filePath) => {
// 文件目录路径
const dirPath = path.dirname(filePath)
// 判断文件是否存在
if (!fs.existsSync(filePath)) {
return allPath
}
// 判断当前文件是否可被进程可见或读取
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
// 获取文件内容
const fileContent = (allPath.filter(item => item === filePath).length > 1) ? '' : fs.readFileSync(filePath, "utf8");
// 获取文件地址规则
const importRegex = /import\s+[\w{},\s]*(from\s+|\s*)[`'"]([^`'"]+)[`'"]/g;
const requireRegex = /require\(['"`]([^'"`]+)['"`]\)/g;
// 去除所有注释
const newFileContent = fileContent.replace(
/\/\/.*|\/\*[\s\S]*?\*\//g,
"",
);
// 获取依赖文件路径
const importPathFiles = [...newFileContent.matchAll(importRegex)].map(
(f) => f[2].trim(),
);
const requirePathFiles = [...newFileContent.matchAll(requireRegex)].map(
(f) => f[1].trim(),
);
const pathFiles = [...importPathFiles, ...requirePathFiles]
// 依赖文件不存在
if (!pathFiles.length) {
return []
}
let newPathFiles = []
let isFind = false
if (pathFiles.length) {
pathFiles.forEach((pathFile) => {
const pathResolveFiles = path.resolve(dirPath, pathFile);
// 获取路径后缀
const existSuffix = path.extname(pathResolveFiles);
if (filterRule && !filterRule.test(pathFile)) {
//
} else {
if (dependentSuffixs.includes(existSuffix)) {
newPathFiles.push(pathResolveFiles)
} else {
// 契合路径引用先后顺序,找到第一个引用且存在的文件
dependentSuffixs.forEach((dependentSuffix) => {
const newPathResolveFiles = `${pathResolveFiles}${dependentSuffix}`;
if (
fs.existsSync(newPathResolveFiles) &&
!isFind
) {
isFind = true
newPathFiles.push(`${pathResolveFiles}${dependentSuffix}`);
}
});
}
}
isFind = false
})
}
depthDependPaths.push(...newPathFiles)
})
if (depthDependPaths.length) {
allPath.push(...depthDependPaths)
return depthDependFilePaths(depthDependPaths, dependentSuffixs, filterRule, allPath)
} else {
return allPath
}
}
/**
* 获取指定目录下的过滤后的所有文件路径
* @param {string} dirPath
* @param {RegExp | null} [filterRule=null]
*
* @returns {string[]} 文件路径数组
*/
function getFilePathsInDirectory(dirPath, filterRule = null) {
if (!fs.existsSync(dirPath)) {
return [];
}
const filePaths = [];
const files = fs.readdirSync(dirPath);
if (!filterRule) {
return files
}
files.forEach((file) => {
const basename = removeExtension(file);
if (filterRule && filterRule.test(basename)) {
const filePath = path.resolve(dirPath, file)
filePaths.push(filePath)
}
});
return filePaths;
}
/**
* 移除文件后缀
*
* @param {any} filePath
*
* @returns
* */
function removeExtension(filePath) {
const { dir, name } = path.parse(filePath);
return path.format({ dir, name });
}
/**
* 清除终端输出
*/
function clearConsole() {
process.stdout.write(
process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H",
);
}
/**
* 比较数组以查找新元素和删除的元素
*
* @param {*} initData
* @param {*} newData
* @return {*}
*/
function itemOrEvent(initData, newData) {
const removeData = [];
const addData = [];
const allData = [...initData, ...newData].reduce((pre, cur) => {
if (pre.indexOf(cur) === -1) {
pre.push(cur);
}
return pre;
}, []);
allData.forEach((cur) => {
if (newData.indexOf(cur) === -1) {
removeData.push(cur);
}
if (initData.indexOf(cur) === -1) {
addData.push(cur);
}
});
return {
removeData,
addData,
};
}
/**
* 获取指定目录下所有文件的相对路径
* @param {string} directory - 要读取的目录路径
* @returns {Promise<string[]>} 返回绝对路径数组
*/
async function aRecursive(directory, ignore = []) {
const patterns = ['**/*'];
const entries = await fg(patterns, {
cwd: directory,
absolute: false,
onlyFiles: true,
dot: true,
followSymbolicLinks: false,
ignore: ignore,
});
return entries.map(entry => path.join(directory, entry));
}
/**
* 清除传入文件的缓存。
*
* @param {string[]} newFiles
* @returns
*/
async function clearConfigFilesCache(newFiles) {
const promisess = [];
newFiles.forEach((newFile) => {
promisess.push(
new Promise((resolve, reject) => {
try {
delete require.cache[require.resolve(newFile)];
resolve(true);
} catch (error) {
reject(error);
}
}),
);
});
try {
await Promise.all(promisess);
return true;
} catch (error) {
return error;
}
}
/**
* 深度比较数据是否相等
*
* @param {*} data1
* @param {*} data2
* @param {*} shouldCompareFields 入参为对象时需要比较的字段数组
* @returns {boolean} true 不等、false 相等
*/
function depthCompareObject(
data1,
data2,
shouldCompareFields = null,
nums = 0,
) {
// 类型
const type1 = Object.prototype.toString.call(data1);
const type2 = Object.prototype.toString.call(data2);
const isComplexType = isNonPrimitiveType(data1);
// 判断类型是否相等
if (type1 !== type2) {
nums++;
return true;
} else {
// 基础数据类型处理
if (!isComplexType) {
// 值为NaN
if ((isNaN(data1) && !isNaN(data2)) || (!isNaN(data1) && isNaN(data2))) {
nums++;
return true;
}
if (data1 !== data2) {
nums++;
return true;
}
}
// 函数类型
if (type1 === "[object Function]") {
const str1 = data1.toString();
const str2 = data2.toString();
if (str1 !== str2) {
nums++;
return true;
}
}
// 对象类型
if (type1 === "[object Object]") {
// 传入对象需要比较的key值数组
const keys1 = Object.keys(data1).filter((item) =>
shouldCompareFields ? shouldCompareFields.includes(item) : true,
);
const keys2 = Object.keys(data2).filter((item) =>
shouldCompareFields ? shouldCompareFields.includes(item) : true,
);
const rootKeys = keys1.length > keys2.length ? keys1 : keys2;
// 如果两者keys值不对等
if (keys1.length !== keys2.length) {
nums++;
return true;
} else {
rootKeys.forEach((key) => {
const res = depthCompareObject(data1[key], data2[key], null, nums);
if (res) {
nums++;
return true;
}
});
}
}
// 数组类型
if (type1 === "[object Array]") {
const longLen = Math.max(data1.length, data2.length);
if (data1.length !== data2.length) {
nums++;
return true;
} else {
for (let i = 0; i < longLen; i++) {
if (i >= data1.length || i >= data2.length) {
break; // 如果其中一个数组已经遍历完,则跳出循环,因为后面的元素不可能是关键的元素了。
} else {
const res = depthCompareObject(data1[i], data2[i], null, nums);
if (res) {
nums++;
return true; // 如果发现不等于,立即返回true并停止执行。
}
}
}
}
}
}
return !!nums;
}
/**
* 深度数据转为字符串
*
* @param {*} data
* @param {*} newData
* @param {*} dataKey
* @returns
*/
function funcOrStr(data, newData) {
// 类型
const type = Object.prototype.toString.call(data);
if (type === "[object Object]" || type === "[object Array]") {
newData = type === "[object Object]" ? {} : [];
for (const key in data) {
const item = data[key];
const itemType = Object.prototype.toString.call(item);
if (itemType === "[object Function]") {
newData[key] = `${item}`;
} else {
newData[key] = item;
}
if (itemType === "[object Object]" || itemType === "[object Array]") {
newData[key] = funcOrStr(item, newData);
}
}
}
return newData;
}
/**
* 获取指定目录一级所有文件绝对路径,并返回一个数组
*
* @param {*} dirpath 目录路径
*/
function reddirResolveFile(dirpath) {
return fs
.readdirSync(dirpath)
.map((mockRelativePath) => path.resolve(dirpath, mockRelativePath))
.filter((mockReolvePath) => {
try {
const isFile = fs
.statSync(path.resolve(dirpath, mockReolvePath))
.isFile();
if (isFile) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
});
}
/**
* 终端进度条,并显示部分信息
*
* @param {*} virtualArrs 需要做虚拟进度部分的路径数组
* @param {*} files 需要真实执行的路径数组
* @param {*} asyncFn1 虚拟进度真实事件执行
* @param {*} asyncFn2 真实执行循环事件
*
* @returns {{appConfigTempPath: string, appConfigTempIndexJs: string}} 缓存生成的配置文件入口和目录
*/
async function progressBarHandle(virtualArrs = [], files, asyncFn1, asyncFn2) {
try {
const nums = [...virtualArrs, ...files].length;
const progressBar = new cliProgress.SingleBar({
format: `{status} ${chalk.gray("[{bar}]")} {filename} | {percentage}% | {value}/{total}`,
barCompleteChar: "\u2588",
barIncompleteChar: "\u2591",
});
let step = 0;
let timer = null;
let tempInfo = null;
function startProgressBar() {
progressBar.start(nums, step, {
status: chalk.yellow("Caching"),
filename: virtualArrs[step],
});
}
function incrementProgressBar(file, tasks) {
progressBar.increment();
progressBar.update(tasks, {
status: chalk.yellow("Caching"),
filename: file,
});
if (tasks === nums) {
progressBar.update(tasks, {
status: chalk.green("Cached"),
filename: "",
});
progressBar.stop();
}
}
// 进度条启动
startProgressBar();
// 虚拟文件加载进度
// 实际tsc转换完成时,直接加载完成
if (virtualArrs.length) {
timer = setInterval(() => {
step++;
incrementProgressBar(virtualArrs[step], step);
if (virtualArrs.length - 1 === step) {
clearInterval(timer);
timer = null;
}
}, 0);
const isSuccess = await asyncFn1();
tempInfo = JSON.parse(JSON.stringify(isSuccess));
if (isSuccess) {
if (timer) {
clearInterval(timer);
timer = null;
}
step = virtualArrs.length;
incrementProgressBar(virtualArrs[step - 1], step);
}
}
if (files.length) {
files.forEach((mockResolveFile) => {
asyncFn2(mockResolveFile);
step++;
incrementProgressBar(mockResolveFile, step);
});
}
if (step === nums) {
return Promise.resolve(tempInfo);
}
} catch (error) {
return Promise.reject(error);
}
}
/**
* 判断当前数据是否为非基础类型
*
* @param {*} value
* @returns
*/
function isNonPrimitiveType(value) {
return (
(typeof value === "object" && value !== null) || typeof value === "function"
);
}
/**
* 将配置对象中字符串形式的函数还原为真正的函数
* @param {any} obj - 原始对象
* @returns {any} 转换后的对象
*/
function reviveFunctions(obj) {
if (obj === null || obj === undefined || typeof obj !== 'object') {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map(reviveFunctions);
}
// 处理普通对象
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
// 判断值是否为函数字符串
if (typeof value === 'string') {
const func = tryParseFunction(value);
result[key] = func !== null ? func : value;
} else {
result[key] = reviveFunctions(value);
}
}
}
return result;
}
/**
* 尝试将字符串解析为函数
* @param {string} str
* @returns {Function|null}
*/
function tryParseFunction(str) {
// 清理字符串前后空格
str = str.trim();
const functionPattern = /^\s*(async\s+)?function\s*\*?\s*(\w+)?\s*\([^)]*\)\s*\{([\s\S]*)\}\s*$/;
const arrowWithBracesPattern = /^\s*(async\s+)?\([^)]*\)\s*=>\s*\{([\s\S]*)\}\s*$/;
const noParenArrowPattern = /^\s*(async\s+)?\w+\s*=>\s*\{([\s\S]*)\}\s*$/; // x => { ... }
const noArgArrowPattern = /^\s*(async\s+)?\(\)\s*=>\s*\{([\s\S]*)\}\s*$/; // () => { ... }
const shorthandArrowPattern = /^\s*(async\s+)?\(?[^()]*\)?\s*=>\s*([^;}]*)\s*;?\s*$/;
let match, isAsync = false, body;
// 1. 普通函数 function () { ... }
if (match = str.match(functionPattern)) {
isAsync = !!match[1];
body = match[3].trim();
}
// 2. 箭头函数带大括号
else if (match = str.match(arrowWithBracesPattern) ||
str.match(noParenArrowPattern) ||
str.match(noArgArrowPattern)) {
// 重新匹配以提取 body
if (match = str.match(/^\s*(async\s+)?[\w()[\]]+\s*=>\s*\{([\s\S]*)\}\s*$/)) {
isAsync = !!match[1];
body = match[2].trim();
} else {
return null;
}
}
// 3. 简写箭头函数 () => value
else if (match = str.match(shorthandArrowPattern)) {
isAsync = !!match[1];
body = `return ${match[2].trim()};`;
}
// 不匹配任何函数模式
else {
return null;
}
try {
// 安全检查:拒绝包含危险操作的函数字符串
const dangerousPatterns = /\b(eval|Function|require|import|process|child_process|exec|spawn)\b/;
if (dangerousPatterns.test(body)) {
return null;
}
return new Function(`
return ${isAsync ? 'async ' : ''}function() {
${body}
}
`)();
} catch (e) {
return null;
}
}
/**
* 匹配`paths.appTempPath`路径
*
* @param {string} appTempPath
*/
function matchNodeTemp(appTempPath) {
return new RegExp(`^${escape(appTempPath)}`, "g");
}
/**
* 匹配所有以`node_modules`结尾但不以`paths.appSrc`开头的路径
*
* @param {string} appSrc
*/
function matchNodeModules(appSrc) {
return new RegExp(
`^(?!${escape(
path.normalize(appSrc + "/").replace(/[\\]+/g, "/"),
)}).+/node_modules/`,
"g",
);
}
function createEnvironmentHash(env) {
const hash = createHash("md5");
hash.update(JSON.stringify(env));
return hash.digest("hex");
}
/**
* 时间处理
*
* @param {*} value
* @param {*} format 时间格式
* @returns
*/
function awhile(value = "", format = "YYYY-MM-DD hh:mm:ss") {
let newFormat = format;
function numFomatter(nums) {
return nums < 10 ? `0${nums}` : `${nums}`;
}
const curDate = value ? new Date(value) : new Date();
const newReg = /([a-zA-Z])\1*/g;
const specReg = /^[^a-zA-Z]*/g;
const formatArr = newFormat.match(newReg).filter((item) => item !== "");
let text = "";
const year = `${curDate.getFullYear()}`;
const month = numFomatter(curDate.getMonth() + 1);
const day = numFomatter(curDate.getDate());
const hour = numFomatter(curDate.getHours());
const minute = numFomatter(curDate.getMinutes());
const second = numFomatter(curDate.getSeconds());
const unix = curDate.getTime();
if (
formatArr[0].length === 4 &&
formatArr[0].toLocaleLowerCase() === "yyyy"
) {
text += year;
newFormat = newFormat.slice(formatArr[0].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (formatArr[1] && formatArr[1].length === 2 && formatArr[1] === "MM") {
text += month;
newFormat = newFormat.slice(formatArr[1].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (
formatArr[2] &&
formatArr[2].length === 2 &&
formatArr[2].toLocaleLowerCase() === "dd"
) {
text += day;
newFormat = newFormat.slice(formatArr[2].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (
formatArr[3] &&
formatArr[3].length === 2 &&
formatArr[3].toLocaleLowerCase() === "hh"
) {
text += hour;
newFormat = newFormat.slice(formatArr[3].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (formatArr[4] && formatArr[4].length === 2 && formatArr[4] === "mm") {
text += minute;
newFormat = newFormat.slice(formatArr[4].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (formatArr[5] && formatArr[5].length === 2 && formatArr[5] === "ss") {
text += second;
newFormat = newFormat.slice(formatArr[5].length);
if (newFormat.match(specReg)[0]) {
text += newFormat.match(specReg)[0];
newFormat = newFormat.slice(newFormat.match(specReg)[0].length);
}
}
if (text.length !== format.length) {
return information.error("入参不规范");
}
return {
text,
unix,
};
}
/**
* 兼容 windows 路径
* @param {*} pathStr
* @returns
*/
const normalizelobPath = (pathStr) => {
return pathStr.replace(/\\/g, '/') + '/**';
};
module.exports = {
isFileExists,
depthDependFilePaths,
getFilePathsInDirectory,
removeExtension,
clearConsole,
itemOrEvent,
aRecursive,
clearConfigFilesCache,
depthCompareObject,
funcOrStr,
reddirResolveFile,
progressBarHandle,
isNonPrimitiveType,
reviveFunctions,
tryParseFunction,
matchNodeModules,
matchNodeTemp,
createEnvironmentHash,
awhile,
normalizelobPath
};