const fs = require('fs-extra'); const { execSync } = require('child_process'); const os = require('os'); const path = require('path'); const chalk = require('chalk'); // 获取当前脚本所在的目录 const scriptDir = __dirname; const rootDir = path.resolve(scriptDir, '..'); const argv2 = process.argv[2]; const customerList = argv2?.split(' ') ?? ''; const publicDir = path.join(rootDir, 'public'); const distDir = path.join(rootDir, 'dist'); const customerListDir = path.join(rootDir, 'customer_list'); const customerProjectListDir = path.join(rootDir, 'src', 'views', 'project'); const firstCustomerName = customerList[0]?.split(':')[0]; /** 公共文件夹,所有客户文件夹共享文件 */ const commonDir = path.join(customerListDir, 'common'); const item = customerList[0]; const customerSplit = item?.split(':'); const deployEnv = customerSplit?.[1]; // 是否为生产环境 const isPro = deployEnv==='pro'; // const deployEnv = process.argv[3]; const logColor = (text, color) => { console.log(chalk[color](text)); }; const logError = (text) => { logColor(text, 'red'); }; const logSuccess = (text) => { logColor(text, 'green'); }; const logWarn = (text) => { logColor(text, 'yellow'); }; /** * 退出脚本 */ const exit = () => { process.exit(1); // 退出脚本 }; /** * 检查文件是否存在 * @param {*} file * @param {*} fileText */ const checkFileExist = (file, fileText = '') => { // 验证源文件夹是否存在 if (!fs.existsSync(file)) { console.error(chalk.red(`${fileText ? fileText + ' ' : ''}"${file}" 不存在!`)); exit(); // 退出脚本 } }; const checkCustomerDirExist = (customer) => { const customerDir = path.join(customerListDir, customer); checkFileExist(customerDir, '客户文件夹'); }; /** * 检查命令,是否加上用户名。以及用户文件夹是否存在 * @param {*} command * @param {*} customer */ const checkCustomer = (command, customer = firstCustomerName) => { if (!customer) { console.error(chalk.red(`请正确使用命令 “${command} [customer]” `)); exit(); // 退出脚本 } checkCustomerDirExist(customer); }; const replaceFileContent = (path, callback) => { const data = fs.readFileSync(path, 'utf8'); if (!data) return; const newData = callback(data); fs.writeFileSync(path, newData, 'utf8'); }; const replaceGlobImportPattern = /import.meta.glob\s*\((\s*.*\/views\/\*\*.*)\)/; const matchAllPattern = '../views/**/*.{vue,tsx}'; /** * 修改 src/router/backEnd.ts 文件,只导入当前项目文件 * @param {*} command */ const updateImportGlob = () => { const backEndFilePath = path.join(rootDir, 'src', 'router', 'backEnd.ts'); checkFileExist(backEndFilePath, `${backEndFilePath}文件`); const subFile = fs.readdirSync(customerListDir); const filterSubFile = subFile?.filter((item) => item !== firstCustomerName); replaceFileContent(backEndFilePath, (data) => { const excludeCustomer = filterSubFile && filterSubFile.length > 0 ? `/(${filterSubFile.join('|')})` : ''; const excludePattern = `!../views/project${excludeCustomer}/**/*`; const replaceStr = `import.meta.glob(['${matchAllPattern}', '${excludePattern}'])`; const newData = data.replace(replaceGlobImportPattern, replaceStr); return newData; }); }; const restoreImportGlob = () => { const backEndFilePath = path.join(rootDir, 'src', 'router', 'backEnd.ts'); checkFileExist(backEndFilePath, `${backEndFilePath}文件`); replaceFileContent(backEndFilePath, (data) => { const replaceStr = `import.meta.glob('${matchAllPattern}')`; const newData = data.replace(replaceGlobImportPattern, replaceStr); return newData; }); }; /** * 获取当前日期是第几周 * @param dateTime 当前传入的日期值 * @returns 返回第几周数字值 */ const getWeek = (dateTime) => { const temptTime = new Date(dateTime.getTime()); // 周几 const weekday = temptTime.getDay() || 7; // 周1+5天=周六 temptTime.setDate(temptTime.getDate() - weekday + 1 + 5); let firstDay = new Date(temptTime.getFullYear(), 0, 1); const dayOfWeek = firstDay.getDay(); let spendDay = 1; if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1; firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay); const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000); const result = Math.ceil(d / 7); return result; }; /** * 时间日期转换 * @param date 当前时间,new Date() 格式 * @param format 需要转换的时间格式字符串,默认值YYYY-mm-dd HH:MM:SS * @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd` * @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ" * @description format 星期:"YYYY-mm-dd HH:MM:SS WWW" * @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ" * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @returns 返回拼接后的时间字符串 */ const formatDate = (date, format = 'YYYY-mm-dd HH:MM:SS') => { const we = date.getDay(); // 星期 const z = getWeek(date); // 周 const qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度 const opt = { 'Y+': date.getFullYear().toString(), // 年 'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1) 'd+': date.getDate().toString(), // 日 'H+': date.getHours().toString(), // 时 'M+': date.getMinutes().toString(), // 分 'S+': date.getSeconds().toString(), // 秒 'q+': qut, // 季度 }; // 中文数字 (星期) const week = { 0: '日', 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六', }; // 中文数字(季度) const quarter = { 1: '一', 2: '二', 3: '三', 4: '四', }; if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]); if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]); if (/(Z+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + ''); for (const k in opt) { const r = new RegExp('(' + k + ')').exec(format); // 若输入的长度不为1,则前面补零 if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0')); } return format; }; /** * 复制文件到 public 文件夹中 * (只能复制一个客户文件配置) * @param {*} command * @param {*} customer */ function copyFile() { // 确保 public 文件,不存在则创建 fs.ensureDirSync(publicDir); // 清空 public 文件夹 fs.emptyDirSync(publicDir); // 使用 filter 过滤器,排除特定文件 const filter = (src, dest) => { const fileName = path.basename(src); // 排除特定文件 if (fileName === 'deploy.json') { return false; } // 其他文件保留 return true; }; const customerDir = path.join(customerListDir, firstCustomerName); // 复制源文件夹中的所有文件到 public 文件夹 fs.copySync(customerDir, publicDir, { filter }); fs.copySync(commonDir, publicDir); } /** * 上传文件到服务器 * @param {*} command */ const uploadFiles = async () => { let customerConfig = {}; for (let index = 0; index < customerList.length; index++) { const item = customerList[index]; const customerSplit = item.split(':'); const customerName = customerSplit[0]; checkCustomerDirExist(customerName); const deployJSON = path.join(customerListDir, customerName, 'deploy.json'); // 验证部署配置文件是否存在 checkFileExist(deployJSON, '配置文件'); // 读取 JSON 文件 const config = await fs.readJson(deployJSON).catch((err) => { console.error(`读取配置文件“${deployJSON}”出错:`, err); exit(); }); let deployConfig = null; const deployEnv = customerSplit[1]; if (deployEnv === 'pro') { deployConfig = config?.product; } else { deployConfig = config?.test; } if (!deployConfig || Object.values(deployConfig).some((item) => !item)) { console.error(chalk.red(`${customerName} ${deployEnv} 配置不完整!`)); exit(); // 退出脚本 } // 缓存部署配置 customerConfig[item] = deployConfig; } for (const customerDeployName in customerConfig) { if (Object.hasOwnProperty.call(customerConfig, customerDeployName)) { const customerName = customerDeployName.split(':')[0]; const deployConfig = customerConfig[customerDeployName]; const remoteFolderPath = deployConfig.path; // 获取用户临时目录 const userTempDir = os.tmpdir(); const timeStamp = new Date().getTime(); const uploadScriptFile = path.join(userTempDir, `psftpBatchFile${timeStamp}${customerDeployName}`); // 构建 sftp 命令 const sftpCommand = 'psftp'; // 上传 dist 中的 assets 和 index.html let uploadCommand = `put -r ${path.join(distDir, 'assets')} ${remoteFolderPath + '/assets'} put ${path.join(distDir, 'index.html')} ${remoteFolderPath + '/index.html'}\n`; const customerFolder = path.join(customerListDir, customerName); // 客户文件夹中除 deploy.json 全部上传 try { const contents = fs.readdirSync(customerFolder); for (let index = 0; index < contents.length; index++) { const item = contents[index]; if (item === 'deploy.json') { continue; } const itemPath = path.join(customerFolder, item); const remotePath = remoteFolderPath + `/${item}`; const stats = fs.statSync(itemPath); // 文件夹需要加 -r if (stats.isDirectory()) { uploadCommand += `put -r ${itemPath} ${remotePath}\n`; // 文件不需要加 } else if (stats.isFile()) { uploadCommand += `put ${itemPath} ${remotePath}\n`; } } } catch (error) { console.error(`读取“${customerFolder}”失败`, error); } try { // 批量备份文件夹到本地 // FIXME: 文件夹如果是中文,会乱码 // uploadCommand=`get -r /D:/IStation.SQI.WebAirp E:\\139.224.246.185_bak\\IStation.SQI.WebAirp` fs.writeFileSync(uploadScriptFile, uploadCommand); const sftpArgs = [ `${deployConfig.username}@${deployConfig.host} -P ${deployConfig.port} -pw ${deployConfig.password} -b ${uploadScriptFile}`, ]; // 完整的执行命令 const fullCommand = `${sftpCommand} ${sftpArgs.join(' ')}`; logSuccess(`正在上传【${customerName}】项目至服务器【${deployConfig.host}:${remoteFolderPath}】...`); execSync(fullCommand, { stdio: ['pipe', 'inherit', 'inherit'] }); } catch (error) { console.error(chalk.red(error)); } finally { if (fs.existsSync(uploadScriptFile)) { fs.unlinkSync(uploadScriptFile); } } } } logSuccess(`${formatDate(new Date(), 'HH:MM:SS')} > 🎉🎉🎉【${customerList.join(',')}】项目已成功部署!🎉🎉🎉`); }; /** * 切换分支 */ const changeBranch = () =>{ if (isPro) { try { execSync('git checkout master', { stdio: 'inherit' }); } catch (error) {} } else { try { execSync('git checkout test', { stdio: 'inherit' }); } catch (error) {} } } module.exports = { isPro, firstCustomerName, exit, customerList, replaceFileContent, checkFileExist, //#region ====================== 文件路径 ====================== rootDir, scriptDir, publicDir, distDir, customerListDir, //#endregion //#region ====================== 工具函数 ====================== checkCustomerDirExist, //#endregion checkCustomer, copyFile, uploadFiles, //#region ====================== 打印 ====================== logError, logSuccess, logWarn, //#endregion formatDate, updateImportGlob, restoreImportGlob, deployEnv, changeBranch };