build-server.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /**
  2. * 打包前置构建脚本
  3. * 在 electron-builder 打包前:
  4. * 1. 编译 shared + Node 服务端
  5. * 2. 确保 Python 依赖就绪
  6. * 3. 将所有服务端资源复制到 client/build/server-bundle/
  7. * 4. 复制 Playwright 浏览器到 client/build/playwright-browsers/
  8. *
  9. * 使用方式: node scripts/build-server.js
  10. */
  11. const { execSync } = require('child_process');
  12. const path = require('path');
  13. const fs = require('fs');
  14. const ROOT = path.resolve(__dirname, '..', '..');
  15. const CLIENT_DIR = path.resolve(__dirname, '..');
  16. const SERVER_DIR = path.join(ROOT, 'server');
  17. const PYTHON_DIR = path.join(SERVER_DIR, 'python');
  18. const SHARED_DIR = path.join(ROOT, 'shared');
  19. const BUNDLE_DIR = path.join(CLIENT_DIR, '_bundle');
  20. const SERVER_BUNDLE = path.join(BUNDLE_DIR, 'server');
  21. const SHARED_BUNDLE = path.join(BUNDLE_DIR, 'shared');
  22. const PLAYWRIGHT_DEST = path.join(BUNDLE_DIR, 'playwright');
  23. const isWin = process.platform === 'win32';
  24. function run(cmd, cwd) {
  25. console.log(`\n> [${path.basename(cwd)}] ${cmd}`);
  26. execSync(cmd, { cwd, stdio: 'inherit', shell: true });
  27. }
  28. function step(msg) {
  29. console.log(`\n${'='.repeat(60)}`);
  30. console.log(` ${msg}`);
  31. console.log('='.repeat(60));
  32. }
  33. function cleanDir(dir) {
  34. if (fs.existsSync(dir)) {
  35. fs.rmSync(dir, { recursive: true, force: true });
  36. }
  37. fs.mkdirSync(dir, { recursive: true });
  38. }
  39. function copyDir(src, dest, options = {}) {
  40. const { filter, dereference = true } = options;
  41. if (!fs.existsSync(src)) {
  42. console.log(` 跳过(不存在): ${src}`);
  43. return;
  44. }
  45. fs.mkdirSync(dest, { recursive: true });
  46. for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
  47. const srcPath = path.join(src, entry.name);
  48. const destPath = path.join(dest, entry.name);
  49. // 解析可能的 symlink
  50. let stat;
  51. try {
  52. stat = dereference ? fs.statSync(srcPath) : fs.lstatSync(srcPath);
  53. } catch {
  54. continue;
  55. }
  56. if (stat.isDirectory()) {
  57. if (filter && filter.skipDir && filter.skipDir(entry.name, srcPath)) continue;
  58. copyDir(srcPath, destPath, options);
  59. } else if (stat.isFile()) {
  60. if (filter && filter.skipFile && filter.skipFile(entry.name, srcPath)) continue;
  61. fs.copyFileSync(srcPath, destPath);
  62. }
  63. }
  64. }
  65. function copyFile(src, dest) {
  66. if (!fs.existsSync(src)) {
  67. console.log(` 跳过(不存在): ${src}`);
  68. return;
  69. }
  70. fs.mkdirSync(path.dirname(dest), { recursive: true });
  71. fs.copyFileSync(src, dest);
  72. }
  73. function dirSizeMB(dir) {
  74. try {
  75. const out = execSync(
  76. isWin
  77. ? `powershell -c "(Get-ChildItem -Recurse '${dir}' -File | Measure-Object -Property Length -Sum).Sum"`
  78. : `du -sb "${dir}" | cut -f1`,
  79. { encoding: 'utf-8', shell: true }
  80. ).trim();
  81. return Math.round(parseInt(out || '0') / 1024 / 1024);
  82. } catch {
  83. return -1;
  84. }
  85. }
  86. // =========================================================
  87. // 1. 编译 shared 包
  88. // =========================================================
  89. step('1/7 编译 shared 包');
  90. if (fs.existsSync(path.join(SHARED_DIR, 'package.json'))) {
  91. run('npm run build', SHARED_DIR);
  92. } else {
  93. console.log(' 跳过(shared 目录不存在)');
  94. }
  95. // =========================================================
  96. // 2. 编译 Node 服务端
  97. // =========================================================
  98. step('2/7 编译 Node 服务端');
  99. try {
  100. run('npx tsc', SERVER_DIR);
  101. } catch {
  102. const distDir = path.join(SERVER_DIR, 'dist');
  103. if (fs.existsSync(distDir) && fs.readdirSync(distDir).length > 0) {
  104. console.log(' ⚠ tsc 有类型警告,但 dist/ 已生成,继续');
  105. } else {
  106. throw new Error('Node 服务端编译失败且 dist/ 未生成');
  107. }
  108. }
  109. // =========================================================
  110. // 3. 检查 Python venv
  111. // =========================================================
  112. step('3/7 检查 Python 虚拟环境');
  113. const venvPython = isWin
  114. ? path.join(PYTHON_DIR, 'venv', 'Scripts', 'python.exe')
  115. : path.join(PYTHON_DIR, 'venv', 'bin', 'python');
  116. if (fs.existsSync(venvPython)) {
  117. console.log(` venv 已存在: ${venvPython}`);
  118. const reqTxt = path.join(PYTHON_DIR, 'requirements.txt');
  119. if (fs.existsSync(reqTxt)) {
  120. console.log(' 安装/更新 Python 依赖...');
  121. run(`"${venvPython}" -m pip install -r requirements.txt --quiet`, PYTHON_DIR);
  122. }
  123. } else {
  124. console.log(' venv 不存在,尝试创建...');
  125. try {
  126. run(`${isWin ? 'python' : 'python3'} -m venv venv`, PYTHON_DIR);
  127. const reqTxt = path.join(PYTHON_DIR, 'requirements.txt');
  128. if (fs.existsSync(reqTxt)) {
  129. run(`"${venvPython}" -m pip install -r requirements.txt`, PYTHON_DIR);
  130. }
  131. } catch (e) {
  132. console.warn(` ⚠ 创建 venv 失败: ${e.message}`);
  133. }
  134. }
  135. if (fs.existsSync(venvPython)) {
  136. console.log(' 确保 Playwright chromium 已安装...');
  137. try {
  138. run(`"${venvPython}" -m playwright install chromium`, PYTHON_DIR);
  139. } catch (e) {
  140. console.warn(` ⚠ Playwright install 失败: ${e.message}`);
  141. }
  142. }
  143. // =========================================================
  144. // 4. 复制 shared 包(server 依赖它,必须先准备好)
  145. // =========================================================
  146. step('4/7 复制 shared 包');
  147. cleanDir(SHARED_BUNDLE);
  148. copyDir(path.join(SHARED_DIR, 'dist'), path.join(SHARED_BUNDLE, 'dist'));
  149. copyFile(path.join(SHARED_DIR, 'package.json'), path.join(SHARED_BUNDLE, 'package.json'));
  150. console.log(' shared 包已就绪');
  151. // =========================================================
  152. // 5. 复制服务端资源 + 安装依赖
  153. // =========================================================
  154. step('5/7 复制服务端资源并安装依赖');
  155. cleanDir(SERVER_BUNDLE);
  156. // 5a. 先复制 package.json 并修改 workspace 引用
  157. console.log(' 复制 package.json ...');
  158. copyFile(path.join(SERVER_DIR, 'package.json'), path.join(SERVER_BUNDLE, 'package.json'));
  159. const bundlePkgPath = path.join(SERVER_BUNDLE, 'package.json');
  160. const bundlePkg = JSON.parse(fs.readFileSync(bundlePkgPath, 'utf-8'));
  161. if (bundlePkg.dependencies && bundlePkg.dependencies['@media-manager/shared']) {
  162. bundlePkg.dependencies['@media-manager/shared'] = 'file:../shared';
  163. fs.writeFileSync(bundlePkgPath, JSON.stringify(bundlePkg, null, 2), 'utf-8');
  164. console.log(' -> @media-manager/shared: workspace:* => file:../shared');
  165. }
  166. // 5b. npm install 生成干净的 node_modules(避免 pnpm symlink 问题)
  167. console.log(' 运行 npm install --omit=dev (耐心等待) ...');
  168. run('npm install --omit=dev', SERVER_BUNDLE);
  169. console.log(` -> node_modules: ${dirSizeMB(path.join(SERVER_BUNDLE, 'node_modules'))} MB`);
  170. // 5c. 复制编译后的代码
  171. console.log(' 复制 server/dist/ ...');
  172. copyDir(path.join(SERVER_DIR, 'dist'), path.join(SERVER_BUNDLE, 'dist'));
  173. console.log(` -> dist: ${dirSizeMB(path.join(SERVER_BUNDLE, 'dist'))} MB`);
  174. // 5d. 复制 .env
  175. console.log(' 复制 .env ...');
  176. copyFile(path.join(SERVER_DIR, '.env'), path.join(SERVER_BUNDLE, '.env'));
  177. // 5e. 创建 uploads 目录
  178. fs.mkdirSync(path.join(SERVER_BUNDLE, 'uploads'), { recursive: true });
  179. const gitkeep = path.join(SERVER_DIR, 'uploads', '.gitkeep');
  180. if (fs.existsSync(gitkeep)) {
  181. fs.copyFileSync(gitkeep, path.join(SERVER_BUNDLE, 'uploads', '.gitkeep'));
  182. }
  183. // 5f. 复制 Python 服务(源码 + venv)
  184. console.log(' 复制 server/python/ ...');
  185. copyDir(
  186. PYTHON_DIR,
  187. path.join(SERVER_BUNDLE, 'python'),
  188. {
  189. dereference: true,
  190. filter: {
  191. skipDir: (name) => name === '__pycache__' || name === '.pytest_cache',
  192. skipFile: (name) => name.endsWith('.pyc'),
  193. },
  194. }
  195. );
  196. console.log(` -> python: ${dirSizeMB(path.join(SERVER_BUNDLE, 'python'))} MB`);
  197. // =========================================================
  198. // 6. 复制 Playwright 浏览器
  199. // =========================================================
  200. step('6/7 复制 Playwright 浏览器');
  201. function findPlaywrightBrowsersDir() {
  202. if (process.env.PLAYWRIGHT_BROWSERS_PATH && fs.existsSync(process.env.PLAYWRIGHT_BROWSERS_PATH)) {
  203. return process.env.PLAYWRIGHT_BROWSERS_PATH;
  204. }
  205. const candidates = [];
  206. if (isWin) {
  207. candidates.push(path.join(process.env.LOCALAPPDATA || '', 'ms-playwright'));
  208. } else if (process.platform === 'darwin') {
  209. candidates.push(path.join(process.env.HOME || '', 'Library', 'Caches', 'ms-playwright'));
  210. } else {
  211. candidates.push(path.join(process.env.HOME || '', '.cache', 'ms-playwright'));
  212. }
  213. for (const c of candidates) {
  214. if (c && fs.existsSync(c)) return c;
  215. }
  216. return null;
  217. }
  218. const pwBrowsersDir = findPlaywrightBrowsersDir();
  219. if (!pwBrowsersDir) {
  220. console.error(' ⚠ 未找到 Playwright 浏览器缓存目录');
  221. } else {
  222. cleanDir(PLAYWRIGHT_DEST);
  223. const entries = fs.readdirSync(pwBrowsersDir);
  224. const needed = entries.filter(n => n.startsWith('chromium') || n.startsWith('ffmpeg'));
  225. if (needed.length === 0) {
  226. console.error(' ⚠ 未找到 chromium');
  227. } else {
  228. for (const dir of needed) {
  229. console.log(` 复制 ${dir} ...`);
  230. copyDir(path.join(pwBrowsersDir, dir), path.join(PLAYWRIGHT_DEST, dir));
  231. console.log(` -> ${dirSizeMB(path.join(PLAYWRIGHT_DEST, dir))} MB`);
  232. }
  233. }
  234. }
  235. // =========================================================
  236. step('7/7 验证打包资源');
  237. function verify(label, p) {
  238. const exists = fs.existsSync(p);
  239. const icon = exists ? '✓' : '✗';
  240. console.log(` ${icon} ${label}: ${p} (${exists ? '存在' : '不存在!'})`);
  241. if (!exists) {
  242. console.error(` ⚠ 缺少关键资源,打包后服务将无法启动!`);
  243. }
  244. }
  245. verify('server dist/app.js', path.join(SERVER_BUNDLE, 'dist', 'app.js'));
  246. verify('server node_modules/', path.join(SERVER_BUNDLE, 'node_modules'));
  247. verify('server .env', path.join(SERVER_BUNDLE, '.env'));
  248. verify('server python/app.py', path.join(SERVER_BUNDLE, 'python', 'app.py'));
  249. verify('shared dist/', path.join(SHARED_BUNDLE, 'dist'));
  250. verify('playwright/', PLAYWRIGHT_DEST);
  251. step('构建完成!所有资源已准备就绪');
  252. console.log(` server: ${SERVER_BUNDLE}`);
  253. console.log(` shared: ${SHARED_BUNDLE}`);
  254. console.log(` playwright: ${PLAYWRIGHT_DEST}`);