UNPKG

@aniyajs/rotor

Version:

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

789 lines (692 loc) 20.4 kB
"use strict"; 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 };