publish.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. const createSeparator = (text) => {
  10. const width = 60;
  11. const padding = Math.floor((width - text.length - 2) / 2);
  12. const separator = '='.repeat(padding) + ' ' + text + ' ' + '='.repeat(padding);
  13. return chalk.blue('\n' + separator + '\n');
  14. };
  15. const spinner = ora('正在构建项目...').start()
  16. spinner.stopAndPersist({
  17. text:
  18. ' __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ \n' +
  19. ' | |\n' +
  20. ' | *** 运行发布流程 *** |\n' +
  21. ' | 【pigx管理系统】 |\n' +
  22. ' | __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ |\n'
  23. })
  24. spinner.start()
  25. let speed = 20
  26. let time = 20
  27. const count = 400
  28. const getProgress = () => {
  29. const done = '█'
  30. const undone = '░'
  31. let progress = Math.floor((speed / count) * 50)
  32. if (progress > 50) {
  33. progress = 50
  34. }
  35. const progressBar = done.repeat(progress) + undone.repeat(50 - progress)
  36. return progressBar
  37. }
  38. const interval = setInterval(() => {
  39. spinner.text = `正在构建项目 ${getProgress()} 进度${((speed / count) * 100).toFixed(1)}% 耗时${(time / 10).toFixed(1)}秒 `
  40. speed++
  41. time++
  42. if (speed > count) {
  43. speed = 400
  44. }
  45. }, 100)
  46. const env = loadEnv('', process.cwd())
  47. const command = (list, config, host) => {
  48. return new Promise((resolveCommand, rejectCommand) => {
  49. const ssh = new SSHClient();
  50. const runCommand = (index) => {
  51. if (index >= list.length) {
  52. ssh.end();
  53. console.log(chalk.green('✓'), chalk.blue(`端口 ${host} 的远程命令执行完毕`));
  54. resolveCommand();
  55. return;
  56. }
  57. const cmd = list[index];
  58. console.log(chalk.blue('→'), chalk.gray(`执行命令: ${cmd}`));
  59. ssh.exec(cmd, (err, stream) => {
  60. if (err) {
  61. console.error(chalk.red('✗'), chalk.red(`命令执行失败: ${err}`));
  62. ssh.end();
  63. rejectCommand(err);
  64. return;
  65. }
  66. let hasError = false;
  67. stream.on('close', (code, signal) => {
  68. if (hasError && !cmd.includes('docker build')) {
  69. console.error(chalk.red('✗'), chalk.red(`命令执行失败,退出码: ${code}`));
  70. ssh.end();
  71. rejectCommand(new Error(`命令 "${cmd}" 失败,退出码: ${code}`));
  72. return;
  73. }
  74. console.log(chalk.green('✓'), chalk.gray(`命令执行完成: ${cmd}`));
  75. runCommand(index + 1);
  76. }).on('data', (data) => {
  77. console.log(chalk.gray(data.toString())); // 实时输出命令结果
  78. }).stderr.on('data', (data) => {
  79. hasError = true;
  80. console.error(chalk.yellow('!'), chalk.yellow(data.toString())); // 实时输出错误信息
  81. });
  82. });
  83. };
  84. ssh.on('ready', () => {
  85. console.log(chalk.green('✓'), chalk.blue('SSH 连接成功'));
  86. runCommand(0);
  87. }).connect(config);
  88. // Handle SSH connection errors
  89. ssh.on('error', (err) => {
  90. console.error(chalk.red('✗'), chalk.red('SSH 连接错误:'), err);
  91. rejectCommand(err);
  92. });
  93. });
  94. };
  95. const updateFiles = async () => {
  96. spinner.text = '正在链接服务器...';
  97. const config = {
  98. host: env.VITE_HOST,
  99. port: env.VITE_POST,
  100. username: env.VITE_USERNAME,
  101. password: env.VITE_PASSWORD
  102. };
  103. const sftp = new SftpClient();
  104. if (!fs.existsSync('dist')) {
  105. console.error(chalk.red('✗'), chalk.red('构建目录不存在'));
  106. spinner.fail('构建目录不存在');
  107. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner
  108. return;
  109. }
  110. try {
  111. await sftp.connect(config);
  112. spinner.succeed(chalk.green('服务器连接成功'));
  113. console.log(chalk.blue('→'), chalk.gray('开始上传文件...'));
  114. await sftp.uploadDir('dist', '/data/data-marketing-platform/build');
  115. console.log(chalk.green('✓'), chalk.blue('6888端口文件上传完成'));
  116. sftp.end();
  117. console.log(createSeparator('开始部署 6888 端口'));
  118. await command([
  119. "cd /data/data-marketing-platform && pwd",
  120. "cd /data/data-marketing-platform && ls -la",
  121. "cd /data/data-marketing-platform && docker rm -f seo-vue",
  122. "cd /data/data-marketing-platform && docker rmi seo-vue",
  123. "cd /data/data-marketing-platform && docker build -t seo-vue .",
  124. "cd /data/data-marketing-platform && docker run -d --name seo-vue -p 6888:80 seo-vue"
  125. ], config, "6888");
  126. } catch (err) {
  127. console.error(chalk.red('✗'), chalk.red('部署或文件上传失败:'), err);
  128. spinner.fail(err);
  129. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner
  130. sftp.end();
  131. throw err; // 向上抛出错误,让main函数捕获并处理退出
  132. }
  133. };
  134. // 项目构建
  135. console.log(createSeparator('开始构建项目'));
  136. // 使用 Promise 包装构建过程
  137. const buildWithProgress = () => {
  138. return new Promise((resolve, reject) => {
  139. // 重置进度条
  140. speed = 20;
  141. time = 20;
  142. spinner.start(); // 确保每次构建前启动 spinner
  143. const buildProcess = exec('npm run build');
  144. let buildOutput = '';
  145. buildProcess.stdout.on('data', (data) => {
  146. buildOutput += data;
  147. // 根据输出更新进度
  148. if (data.includes('compiled successfully')) {
  149. speed = count;
  150. }
  151. });
  152. buildProcess.stderr.on('data', (data) => {
  153. buildOutput += data;
  154. });
  155. buildProcess.on('close', async (code) => {
  156. if (code === 0) {
  157. spinner.succeed(chalk.green(`构建成功`));
  158. resolve(buildOutput);
  159. } else {
  160. spinner.fail(chalk.red(`构建失败`));
  161. reject(new Error(buildOutput));
  162. }
  163. });
  164. });
  165. };
  166. // 主构建流程
  167. const main = async () => {
  168. spinner.start(); // 在主流程开始时启动 spinner,用于总体的进度指示
  169. try {
  170. console.log(createSeparator('开始构建 6888 端口'));
  171. // 构建 6888 端口版本
  172. await buildWithProgress();
  173. // 开始部署
  174. await updateFiles();
  175. // 所有任务完成后,打印最终访问路径并停止 spinner
  176. clearInterval(interval); // 在所有任务完成后停止进度条动画
  177. console.log(createSeparator('部署完成'));
  178. console.log(chalk.green('🚀'), chalk.cyan(`6888 端口访问地址: http://${env.VITE_HOST}:6888`));
  179. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台
  180. process.exit(0); // 正常退出,返回控制台
  181. } catch (error) {
  182. console.error(chalk.red('✗'), chalk.red('发布流程出错:'), error);
  183. spinner.fail(error);
  184. spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台
  185. process.exit(1); // 异常退出
  186. }};
  187. // 启动主流程
  188. main();