UNPKG

@aniyajs/rotor

Version:

基于webpack5开发的一款专注于打包、运行的工具

657 lines (586 loc) 17.3 kB
"use strict"; 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, };