publish.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import { exec } from 'child_process'
  2. import { Client as SSHClient } from 'ssh2'
  3. import ora from 'ora'
  4. import SftpClient from 'ssh2-sftp-client'
  5. import chalk from 'chalk'
  6. import fs from 'fs'
  7. import { loadEnv } from 'vite'
  8. // 存储原始的 接口请求地址
  9. let originalApiUrl = '';
  10. // 创建分隔线
  11. const createSeparator = (text) => {
  12. const width = 60;
  13. const padding = Math.floor((width - text.length - 2) / 2);
  14. const separator = '='.repeat(padding) + ' ' + text + ' ' + '='.repeat(padding);
  15. return chalk.blue('\n' + separator + '\n');
  16. };
  17. const spinner = ora('正在构建项目...').start()
  18. spinner.stopAndPersist({
  19. text:
  20. ' __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ \n' +
  21. ' | |\n' +
  22. ' | *** 运行发布流程 *** |\n' +
  23. ' | 【pigx管理系统】 |\n' +
  24. ' | __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ |\n'
  25. })
  26. spinner.start()
  27. let speed = 20
  28. let time = 20
  29. const count = 400
  30. const getProgress = () => {
  31. const done = '█'
  32. const undone = '░'
  33. let progress = Math.floor((speed / count) * 50)
  34. if (progress > 50) {
  35. progress = 50
  36. }
  37. const progressBar = done.repeat(progress) + undone.repeat(50 - progress)
  38. return progressBar
  39. }
  40. const interval = setInterval(() => {
  41. spinner.text = `正在构建项目 ${getProgress()} 进度${((speed / count) * 100).toFixed(1)}% 耗时${(time / 10).toFixed(1)}秒 `
  42. speed++
  43. time++
  44. if (speed > count) {
  45. speed = 400
  46. }
  47. }, 100)
  48. const env = loadEnv('', process.cwd())
  49. const updateProxyConfig = (url) => {
  50. try {
  51. const envPath = '.env';
  52. let content = fs.readFileSync(envPath, 'utf-8');
  53. // 提取当前的 apiBaseUrl (如果需要)
  54. const match = content.match(/VITE_API_URL = (.*)/);
  55. if (!originalApiUrl && match && match[1]) {
  56. originalApiUrl = match[1];
  57. console.log(chalk.gray(`原始 API 地址已保存: ${originalApiUrl}`));
  58. }
  59. // 使用正则表达式替换apiBaseUrl的值,但保留注释
  60. content = content.replace(
  61. /VITE_API_URL = (.*)/,
  62. `VITE_API_URL = ${url}`
  63. );
  64. fs.writeFileSync(envPath, content, 'utf-8');
  65. console.log(chalk.green('✓'), chalk.blue(`已更新 API 地址为: ${url}`));
  66. } catch (error) {
  67. console.error(chalk.red('✗'), chalk.red('更新 API 地址失败:'), error);
  68. process.exit(1);
  69. }
  70. };
  71. const command = (list, config, host) => {
  72. return new Promise((resolveCommand, rejectCommand) => {
  73. const ssh = new SSHClient();
  74. const runCommand = (index) => {
  75. if (index >= list.length) {
  76. ssh.end();
  77. console.log(chalk.green('✓'), chalk.blue(`端口 ${host} 的远程命令执行完毕`));
  78. resolveCommand();
  79. return;
  80. }
  81. const cmd = list[index];
  82. console.log(chalk.blue('→'), chalk.gray(`执行命令: ${cmd}`));
  83. ssh.exec(cmd, (err, stream) => {
  84. if (err) {
  85. console.error(chalk.red('✗'), chalk.red(`命令执行失败: ${err}`));
  86. ssh.end();
  87. rejectCommand(err);
  88. return;
  89. }
  90. let hasError = false;
  91. stream.on('close', (code, signal) => {
  92. if (hasError && !cmd.includes('docker build')) {
  93. console.error(chalk.red('✗'), chalk.red(`命令执行失败,退出码: ${code}`));
  94. ssh.end();
  95. rejectCommand(new Error(`命令 "${cmd}" 失败,退出码: ${code}`));
  96. return;
  97. }
  98. console.log(chalk.green('✓'), chalk.gray(`命令执行完成: ${cmd}`));
  99. runCommand(index + 1);
  100. }).on('data', (data) => {
  101. console.log(chalk.gray(data.toString())); // 实时输出命令结果
  102. }).stderr.on('data', (data) => {
  103. hasError = true;
  104. console.error(chalk.yellow('!'), chalk.yellow(data.toString())); // 实时输出错误信息
  105. });
  106. });
  107. };
  108. ssh.on('ready', () => {
  109. console.log(chalk.green('✓'), chalk.blue('SSH 连接成功'));
  110. runCommand(0);
  111. }).connect(config);
  112. // Handle SSH connection errors
  113. ssh.on('error', (err) => {
  114. console.error(chalk.red('✗'), chalk.red('SSH 连接错误:'), err);
  115. rejectCommand(err);
  116. });
  117. });
  118. };
  119. const updateFiles = async () => {
  120. spinner.text = '正在链接服务器...';
  121. const config = {
  122. host: env.VITE_HOST,
  123. port: env.VITE_POST,
  124. username: env.VITE_USERNAME,
  125. password: env.VITE_PASSWORD
  126. };
  127. const sftp = new SftpClient();
  128. if (!fs.existsSync('dist')) {
  129. console.error(chalk.red('✗'), chalk.red('构建目录不存在'));
  130. spinner.fail('构建目录不存在');
  131. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner
  132. return;
  133. }
  134. try {
  135. await sftp.connect(config);
  136. spinner.succeed(chalk.green('服务器连接成功'));
  137. console.log(chalk.blue('→'), chalk.gray('开始上传文件...'));
  138. await sftp.uploadDir('dist_6888', '/data/seo-vue/dist');
  139. console.log(chalk.green('✓'), chalk.blue('6888端口文件上传完成'));
  140. await sftp.uploadDir('dist_16888', '/data/seo-vue-test/dist');
  141. console.log(chalk.green('✓'), chalk.blue('16888端口文件上传完成'));
  142. sftp.end();
  143. console.log(createSeparator('开始部署 6888 端口'));
  144. await command([
  145. "cd /data/seo-vue && pwd",
  146. "cd /data/seo-vue && ls -la",
  147. "cd /data/seo-vue && docker rm -f seo-vue",
  148. "cd /data/seo-vue && docker rmi seo-vue",
  149. "cd /data/seo-vue && docker build -t seo-vue .",
  150. "cd /data/seo-vue && docker run -d --name seo-vue -p 6888:80 seo-vue"
  151. ], config, "6888");
  152. console.log(createSeparator('6888 端口部署完成'));
  153. console.log(createSeparator('开始部署 16888 端口'));
  154. await command([
  155. "cd /data/seo-vue-test && pwd",
  156. "cd /data/seo-vue-test && ls -la",
  157. "cd /data/seo-vue-test && docker rm -f seo-vue-test",
  158. "cd /data/seo-vue-test && docker rmi seo-vue-test",
  159. "cd /data/seo-vue-test && docker build -t seo-vue-test .",
  160. "cd /data/seo-vue-test && docker run -d --name seo-vue-test -p 16888:80 seo-vue-test"
  161. ], config, "16888");
  162. console.log(createSeparator('16888 端口部署完成'));
  163. } catch (err) {
  164. console.error(chalk.red('✗'), chalk.red('部署或文件上传失败:'), err);
  165. spinner.fail(err);
  166. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner
  167. sftp.end();
  168. throw err; // 向上抛出错误,让main函数捕获并处理退出
  169. }
  170. };
  171. // 项目构建
  172. console.log(createSeparator('开始构建项目'));
  173. // 使用 Promise 包装构建过程
  174. const buildWithProgress = (port, apiUrl) => {
  175. return new Promise((resolve, reject) => {
  176. console.log(chalk.blue(`\n构建 ${port} 端口版本`));
  177. console.log(chalk.gray(`API 地址: ${apiUrl}`));
  178. updateProxyConfig(apiUrl);
  179. // 重置进度条
  180. speed = 20;
  181. time = 20;
  182. spinner.start(); // 确保每次构建前启动 spinner
  183. const buildProcess = exec('npm run build');
  184. let buildOutput = '';
  185. buildProcess.stdout.on('data', (data) => {
  186. buildOutput += data;
  187. // 根据输出更新进度
  188. if (data.includes('compiled successfully')) {
  189. speed = count;
  190. }
  191. });
  192. buildProcess.stderr.on('data', (data) => {
  193. buildOutput += data;
  194. });
  195. buildProcess.on('close', async (code) => {
  196. if (code === 0) {
  197. // 构建成功后,将dist目录复制到对应的端口目录
  198. const distDir = 'dist';
  199. const targetDir = `dist_${port}`;
  200. try {
  201. // 如果目标目录已存在,先删除
  202. if (fs.existsSync(targetDir)) {
  203. fs.rmSync(targetDir, { recursive: true, force: true });
  204. }
  205. // 复制dist目录到目标目录
  206. fs.cpSync(distDir, targetDir, { recursive: true });
  207. console.log(chalk.green('✓'), chalk.blue(`已保存 ${port} 端口构建文件到 ${targetDir}`));
  208. } catch (err) {
  209. console.error(chalk.red('✗'), chalk.red(`保存构建文件失败: ${err}`));
  210. reject(err);
  211. return;
  212. }
  213. spinner.succeed(chalk.green(`构建成功`));
  214. resolve(buildOutput);
  215. } else {
  216. spinner.fail(chalk.red(`构建失败`));
  217. reject(new Error(buildOutput));
  218. }
  219. });
  220. });
  221. };
  222. // 主构建流程
  223. const main = async () => {
  224. spinner.start(); // 在主流程开始时启动 spinner,用于总体的进度指示
  225. try {
  226. const envPath = '.env';
  227. const content = fs.readFileSync(envPath, 'utf-8');
  228. const match = content.match(/VITE_API_URL = (.*)/);
  229. if (match && match[1]) {
  230. originalApiUrl = match[1];
  231. } else {
  232. console.warn(chalk.yellow('!'), chalk.yellow('无法从 env 中读取原始 VITE_API_URL,可能无法还原。'));
  233. }
  234. // 构建 6888 端口版本
  235. await buildWithProgress('6888', 'http://192.168.10.101:9999');
  236. // 构建 16888 端口版本
  237. await buildWithProgress('16888', 'http://192.168.3.17:9999');
  238. // 开始部署
  239. await updateFiles();
  240. // 所有任务完成后,打印最终访问路径并停止 spinner
  241. clearInterval(interval); // 在所有任务完成后停止进度条动画
  242. console.log(createSeparator('部署完成'));
  243. console.log(chalk.green('🎉'), chalk.cyan('所有环境部署成功!'));
  244. console.log(chalk.green('🚀'), chalk.cyan(`6888 端口访问地址: http://${env.VITE_HOST}:6888`));
  245. console.log(chalk.green('🚀'), chalk.cyan(`16888 端口访问地址: http://${env.VITE_HOST}:16888`));
  246. // 还原 VITE_API_URL 到原始值
  247. if (originalApiUrl) {
  248. console.log(createSeparator('还原配置'));
  249. updateProxyConfig(originalApiUrl);
  250. } else {
  251. console.warn(chalk.yellow('!'), chalk.yellow('未找到原始 VITE_API_URL,跳过还原。'));
  252. }
  253. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台
  254. process.exit(0); // 正常退出,返回控制台
  255. } catch (error) {
  256. console.error(chalk.red('✗'), chalk.red('发布流程出错:'), error);
  257. spinner.fail(error);
  258. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台
  259. process.exit(1); // 异常退出
  260. }};
  261. // 启动主流程
  262. main();