/* eslint-disable no-await-in-loop */ /* * @Author: cheaz * @Date: 2024-08-01 16:27:18 * @LastEditors: cheaz * @LastEditTime: 2024-08-12 22:09:47 * @Description: 工程发布至私有仓库 */ const params = process.argv.slice(2)[2]; // 获取打包参数 const fs = require('fs'); const path = require('path'); const fsPromise = require('fs').promises; const { exec } = require('child_process'); const { Parser } = require('acorn'); const vueParser = require('vue-eslint-parser'); const { resolveConfig } = require('vite'); let packageVersion = require('./package.json').version; let packageName = require('./package.json').name; const entryFile = require('./package.json').main; const packageDependencies = require('./package.json').dependencies; // 自定义文件夹列表 const customPathList = [`src/${packageName}`, `src/views/${packageName}`]; // 添加支持多模块打包 packageName = params || packageName; params && (packageVersion = require(`./src/views/${params}/package.json`).version); const viteConfig = require('./vite.config.js'); let aliasPlugin; let resolvPlugin; /** * 加载vite配置,从vite中获取alias和resolve插件 */ const getResolvePlugin = async () => { const vitePlugins = (await resolveConfig(viteConfig, 'build', 'production', 'production')).worker.plugins; aliasPlugin = vitePlugins.find(({ name }) => name === 'alias'); resolvPlugin = vitePlugins.find(({ name }) => name === 'vite:resolve'); }; /** * 分析依赖,先调用vite alias插件,再调用resolve插件,获取依赖的绝对路径 * @param {*} importee 被引用内容 * @param {*} importer 引用者 * @returns 被依赖内容的绝对路径 */ const resolveModule = async (importee, importer) => { let aliasResult = await aliasPlugin.resolveId.call(Promise, importee, importer); aliasResult = aliasResult || importee; const rst = await resolvPlugin.resolveId.call({ getModuleInfo: () => ({}) }, aliasResult, importer, {}); return rst?.id || rst; }; /** * 获取package.json中的依赖项,排除宿主中已存在的依赖 * @returns */ const getPackageDependencies = () => { const existDependencies = [ '@yufp/constant', '@yufp/icon', '@yufp/layout', '@yufp/layout-micro', '@yufp/layout-utils', '@yufp/locale', '@yufp/svgicon', '@yufp/toolbar', '@yufp/xy-utils', '@yuxp/compile', '@yuxp/integration', 'xy-utils', 'axios', 'element-ui', 'fingerprintjs2', 'nprogress', 'oca-web', 'qs', 'vue', 'vue-i18n', 'vue-router', 'vue-seamless-scroll', 'vuex', 'vuex-persist', 'yuwp-ui', ]; return Object.fromEntries( Object.keys(packageDependencies) .filter((dep) => !existDependencies.includes(dep)) .map((key) => [key, packageDependencies[key]]), ); }; /** * 判断文件是否需要拷贝到包中 * 文件在 src/${packageName} 和 src/views/${packageName} 中,且仅支持 .js .vue 文件 * @param {String} filePath 文件路径 * @returns 是否为自定义的文件 */ const isCustomFile = (filePath) => { console.log('filePath===', filePath) if(!filePath) return false; if (!filePath.endsWith('.js') && !filePath.endsWith('.vue')) { return false; } if (!path.isAbsolute(filePath)) { filePath = path.join(__dirname, filePath); } const absolutPathList = customPathList.map((filePath) => path.join(__dirname, filePath)); return !absolutPathList.every((absolutPath) => path.relative(absolutPath, filePath).startsWith('..')); }; /** * 分析一个文件的依赖 * 将文件中 @/views/xxx.js 转换为 ./views/xxx.js 的实际相对路径 * 返回转换后的内容(字符串)以及此文件所依赖的文件 * @param {*} entryFile 所分析的文件 * @param {*} onlyCustomFile 是否限制自定义目录,true为限制,入口文件直接引用的文件不限制 * @returns dependecyFileList:依赖的文件列表,replacedFile替换后的内容 */ const parseFileDependency = async (entryFile = './src/index.js', onlyCustomFile = true) => { const entryFileContent = String(fs.readFileSync(entryFile)); let result; let replacedFile = entryFileContent; if (entryFile.endsWith('.vue')) { result = vueParser.parse(entryFileContent, { ecmaVersion: 2020, sourceType: 'module' }); } else if (entryFile.endsWith('.js')) { result = Parser.parse(entryFileContent, { ecmaVersion: 2020, sourceType: 'module' }); } else { return { dependecyFileList: [], replacedFile: '' }; } const dependecyFileList = await Promise.all( result.body .filter(({ type }) => type === 'ImportDeclaration') .map(async ({ source }) => { const absolutPath = await resolveModule(source.value, entryFile); if (absolutPath === null) return null; if (!onlyCustomFile || isCustomFile(absolutPath)) { const toFile = path.relative( path.dirname(path.isAbsolute(entryFile) ? entryFile : path.join(__dirname, entryFile)), absolutPath, ); replacedFile = replacedFile.replaceAll(source.value, `./${toFile.replaceAll(`\\`, '/')}`); return absolutPath; } return null; }), ); return { dependecyFileList: dependecyFileList.filter((a) => a !== null), replacedFile, }; }; /** * 递归解析文件,调用vite resolve plugin,分析所依赖的文件。 * 并将依赖转换为无alias的相对路径,并写道destDir中。 * @param {*} entryFile 入口文件。 */ const parseAndeCopyCustomJs = async (entryFile) => { let parseFileStack = [entryFile]; const parsedSet = new Set(); const writeFileQueue = []; while (parseFileStack.length) { const parseFile = parseFileStack.pop(); parsedSet.add(parseFile); // eslint-disable-next-line no-await-in-loop // entryFile支持引入非自定义路径下的路径,其他不支持 const { dependecyFileList, replacedFile } = await parseFileDependency(parseFile, parseFile !== entryFile); const writeFilePath = `${packageName}/${path.relative(__dirname, parseFile)}`; // eslint-disable-next-line no-await-in-loop writeFileQueue.push( fsPromise .mkdir(path.dirname(path.join(__dirname, writeFilePath)), { recursive: true }) .then(() => fsPromise.writeFile(writeFilePath, replacedFile)), ); const filted = dependecyFileList.filter((f) => !parsedSet.has(f)); // console.log(`${parseFile}依赖:${filted}${filted.length}`); parseFileStack = parseFileStack.concat(filted); } // 等待所有文件写入完成 await Promise.all(writeFileQueue); console.log(`拷贝自定义文件列表:${[...parsedSet].join('\n')} 共${parsedSet.size}个文件`); }; /** * 递归复制目录 * @param {*} src 源目录 * @param {*} dest 目标目录 */ async function copyDirectory(src, dest) { // 确保目标目录存在 if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } // 获取源目录的内容 const files = await fsPromise.readdir(src, { withFileTypes: true }); for (const file of files) { const srcPath = path.join(src, file.name); const destPath = path.join(dest, file.name); if (file.isDirectory()) { // 如果是目录,则递归复制该目录 // eslint-disable-next-line no-await-in-loop await copyDirectory(srcPath, destPath); } else { // 如果是文件,则直接复制 // console.log(`Copying ${srcPath} to ${destPath}`); try { // eslint-disable-next-line no-await-in-loop await fsPromise.copyFile(srcPath, destPath); } catch (error) { console.error(`Error copying ${srcPath}:`, error); } } } } /** * 递归删除目录或文件 * @param {*} fileUrl * @returns */ function deleteTarget(fileUrl) { // 如果当前url不存在,则退出 if (!fs.existsSync(fileUrl)) return; // 当前文件为文件夹时 if (fs.statSync(fileUrl).isDirectory()) { const files = fs.readdirSync(fileUrl); const len = files.length; let removeNumber = 0; if (len > 0) { files.forEach((file) => { removeNumber++; const url = `${fileUrl}/${file}`; if (fs.statSync(url).isDirectory()) { deleteTarget(url); } else { fs.unlinkSync(url); } }); if (removeNumber === len) { // 删除当前文件夹下的所有文件后,删除当前空文件夹(注:所有的都采用同步删除) fs.rmdirSync(fileUrl); console.log(`删除文件夹${fileUrl}成功`); } } else { fs.rmdirSync(fileUrl); } } else { // 当前文件为文件时 fs.unlinkSync(fileUrl); console.log(`删除文件${fileUrl}成功`); } } (async function run(params) { await getResolvePlugin(); await parseAndeCopyCustomJs(entryFile); await copyDirectory(path.join(__dirname, 'src/views/xpVue'), path.join(__dirname, packageName, 'xpVue')); console.log('xpCopy completed.'); // 复制目录到指定位置 await copyDirectory(path.join(__dirname, 'src/views', packageName), path.join(__dirname, packageName)); console.log('Copy completed.'); // 写入package.json 文件 const packageContent = JSON.stringify( { name: packageName, version: packageVersion, license: 'MIT', description: 'Lightweight Rule Engine Frontend Page Code.', author: 'oca@yusys.com.cn', main: entryFile, dependencies: getPackageDependencies(), }, '\n', 4, ); fs.writeFileSync(`${packageName}/package.json`, packageContent); console.log('INFO=====准备将OCA推送至私有仓库'); console.log(`INFO=====包名:${packageName}`); console.log(`INFO=====版本:${packageVersion}`); exec(`cd ${packageName} && yarn publish`, (error, stdout, stderr) => { if (error) { console.error(`ERROR=====执行出错: ${error}`); if (`${error}`.indexOf('not allow updating') > -1) { console.error(`ERROR=====仓库上已存在该版本,不允许覆盖推送,请调整发布版本号。`); } return; } // 发布完成删除文件夹 setTimeout(() => { let deleteName = packageName; if (packageName.includes('/')) { deleteName = packageName.split('/')[0]; } deleteTarget(path.join(__dirname, deleteName)); }, 1000); console.log(`INFO=====${stdout}`); }); })();