@aniyajs/rotor
Version:
基于webpack5开发的一款专注于打包、运行的工具
657 lines (586 loc) • 17.3 kB
JavaScript
;
const fs = require("fs-extra");
const path = require("path");
const information = require("./information");
const cliProgress = require("cli-progress");
const chalk = require("chalk");
const escape = require("escape-string-regexp");
const recursive = require("recursive-readdir");
const { createHash } = require("crypto");
/**
* 清除终端输出
*/
function clearConsole() {
// process.stdout.write(
// process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H",
// );
}
/**
* @description 检查文件是否存在
*
* @param {Array} files 需要检查文件文件路径数组
* @param {String} prefix 路径前缀,默认值为 ""
*/
function checkFilesExists(files, prefix = "") {
const missFiles = [];
files.forEach((file) => {
const isExist = fs.existsSync(path.join(prefix, file));
if (!isExist) {
missFiles.push(path.join(prefix, file));
}
});
if (missFiles.length) {
information.error(`找不到必须文件: ${missFiles.join(",")}`);
}
}
/**
* 深度读取指定文件的依赖路径数组
*
* @param {string} filePath 文件路径
* @param {string[]} dependentSuffixs 可忽略的依赖后缀
* @param {string[]} filterRule 过滤规则
*
*/
function depthDependFilePaths(filePath, dependentSuffixs, filterRule = null) {
return new Promise((resolve, reject) => {
try {
// 规范化路径
const normalizePath = path.normalize(filePath);
// 判断当前文件是否可被进程可见或读取
fs.accessSync(normalizePath, fs.constants.F_OK | fs.constants.R_OK);
// 获取文件内容
const fileContent = fs.readFileSync(normalizePath, "utf8");
// 获取文件后缀
const fileDirname = path.dirname(normalizePath);
// 获取文件地址规则
const importRegex = /import\s+[\w{},\s]*(from\s+|\s*)['"]([^'"]+)['"]/g;
// 去除所有注释
const newFileContent = fileContent.replace(
/\/\/.*|\/\*[\s\S]*?\*\//g,
"",
);
let importPathFiles = [...newFileContent.matchAll(importRegex)].map(
(f) => f[2],
);
// 依赖文件不存在
if (!importPathFiles.length) {
resolve([]);
}
const promises = [];
const data = [];
importPathFiles.forEach((importPathFile) => {
const importPathResolveFile = path.resolve(fileDirname, importPathFile);
// 获取路径后缀
const existSuffix = path.extname(importPathResolveFile);
// 过滤文件
if (filterRule !== null && filterRule.test(importPathResolveFile)) {
//
} else {
if (dependentSuffixs.includes(existSuffix)) {
promises.push(
depthDependFilePaths(
importPathResolveFile,
dependentSuffixs,
filterRule,
),
);
data.push(importPathResolveFile);
} else {
let curDependentSuffix;
const newDependentSuffixs = JSON.parse(
JSON.stringify(dependentSuffixs),
);
// 契合路径引用先后顺序,找到第一个引用且存在的文件
newDependentSuffixs.forEach((dependentSuffix) => {
if (curDependentSuffix === undefined) {
if (
fs.existsSync(`${importPathResolveFile}${dependentSuffix}`)
) {
curDependentSuffix = dependentSuffix;
promises.push(
depthDependFilePaths(
`${importPathResolveFile}${curDependentSuffix}`,
dependentSuffixs,
filterRule,
),
);
data.push(`${importPathResolveFile}${curDependentSuffix}`);
}
}
});
curDependentSuffix = undefined;
}
}
});
Promise.all(promises).then((results) => {
results.forEach((result) => {
data.push(...result);
});
resolve(
data.reduce((pre, cur) => {
if (pre.indexOf(cur) === -1) {
pre.push(cur);
}
return pre;
}, []),
);
});
} catch (error) {
reject(error);
}
});
}
/**
* 比较数组以查找新元素和删除的元素
*
* @param {*} initData
* @param {*} newData
* @param {*} filterPath
* @return {*}
*/
function itemOrEvent(initData, newData, filterPath = null) {
const removeData = [];
const addData = [];
const filterPathReg = new RegExp(`^${escape(filterPath)}/.+$`);
const allData = [...initData, ...newData].reduce((pre, cur) => {
if (pre.indexOf(cur) === -1) {
pre.push(cur);
}
return pre;
}, []);
allData.forEach((cur) => {
if (filterPath && (cur === filterPath || filterPathReg.test(cur))) {
//
} else {
if (newData.indexOf(cur) === -1) {
removeData.push(cur);
}
if (initData.indexOf(cur) === -1) {
addData.push(cur);
}
}
});
return {
removeData,
addData,
};
}
/**
* 获取指定目录一级所有文件绝对路径,并返回一个数组
*
* @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 {{appConfigIndexDirTempPath: string, appConfigTempIndexJs: string}} 缓存生成的配置文件入口和目录
*/
async function progressBarHandle(virtualArrs = [], files, asyncFn1, asyncFn2) {
try {
const nums = [...virtualArrs, ...files].length;
const progressBar = new cliProgress.SingleBar({
format: "{status} [{bar}] {filename} | {percentage}% | {value}/{total}",
barCompleteChar: "\u2588",
barIncompleteChar: "\u2591",
hideCursor: true,
});
let step = 0;
let timer = null;
let tempInfo = null;
function startProgressBar() {
progressBar.start(nums, step, {
status: chalk.yellow("Caching"),
filename: files.length ? files[step] : 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;
}
}, 100);
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);
}
}
/**
* 递归读取指定目录下的所有文件,并返回它们的相对路径。
* 使用了外部npm包 `recursive-readdir` 来实现递归读取目录的功能。
*
* @param {...any} args - 可接受多个参数,第一个参数通常是目录的路径,后续参数可用来传递给 `recursive` 函数的选项。
* @returns {Promise<Array<string>>} 返回一个Promise对象,成功时解析为包含所有文件相对路径的数组,失败时reject错误对象。
*/
function aRecursive(...args) {
// 创建一个Promise对象,用于异步处理递归读取目录文件的过程
return new Promise((resolve, reject) => {
// 调用recursive函数,异步读取目录下的文件
recursive(...args, function (err, files) {
if (err) {
// 如果发生错误,将错误对象传递给reject,触发Promise的拒绝状态
reject(err);
}
// 无错误时,将读取到的文件路径数组传递给resolve,触发Promise的解决状态
resolve(files);
});
});
}
/**
* 清除传入文件的缓存。
*
* @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 = JSON.stringify(data1);
const str2 = JSON.stringify(data1);
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 {*} value
* @returns
*/
function isNonPrimitiveType(value) {
return (
(typeof value === "object" && value !== null) || typeof value === "function"
);
}
/**
* 深度数据转为字符串
*
* @param {*} data
* @param {*} newData
* @param {*} dataKey
* @returns
*/
function funcOrStr(data, newData, dataKey = null, parType) {
// 类型
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 {
if (`${item}` === `${new Date()}`) {
newData[key] = "CURRENTTIME";
} else {
newData[key] = item;
}
}
if (itemType === "[object Object]" || itemType === "[object Array]") {
newData[key] = funcOrStr(item, newData);
}
}
}
return newData;
}
/**
* 时间处理
*
* @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,
};
}
function createEnvironmentHash(env) {
const hash = createHash("md5");
hash.update(JSON.stringify(env));
return hash.digest("hex");
}
/**
* 匹配所有以`node_modules`结尾但不以`paths.appSrc`开头的路径
*
* @param {string} appSrc
*/
function matchNodeModules(appSrc) {
return new RegExp(
`^(?!${escape(
path.normalize(appSrc + "/").replace(/[\\]+/g, "/"),
)}).+/node_modules/`,
"g",
);
}
/**
* 匹配`paths.appTempPath`路径
*
* @param {string} appTempPath
*/
function matchNodeTemp(appTempPath) {
return new RegExp(`^${escape(appTempPath)}`, "g");
}
module.exports = {
checkFilesExists,
depthDependFilePaths,
itemOrEvent,
reddirResolveFile,
progressBarHandle,
aRecursive,
clearConfigFilesCache,
depthCompareObject,
clearConsole,
funcOrStr,
awhile,
createEnvironmentHash,
matchNodeModules,
matchNodeTemp,
};