/** * 打包前置构建脚本 * 在 electron-builder 打包前: * 1. 编译 shared + Node 服务端 * 2. 确保 Python 依赖就绪 * 3. 将所有服务端资源复制到 client/build/server-bundle/ * 4. 复制 Playwright 浏览器到 client/build/playwright-browsers/ * * 使用方式: node scripts/build-server.js */ const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const ROOT = path.resolve(__dirname, '..', '..'); const CLIENT_DIR = path.resolve(__dirname, '..'); const SERVER_DIR = path.join(ROOT, 'server'); const PYTHON_DIR = path.join(SERVER_DIR, 'python'); const SHARED_DIR = path.join(ROOT, 'shared'); const BUNDLE_DIR = path.join(CLIENT_DIR, '_bundle'); const SERVER_BUNDLE = path.join(BUNDLE_DIR, 'server'); const SHARED_BUNDLE = path.join(BUNDLE_DIR, 'shared'); const PLAYWRIGHT_DEST = path.join(BUNDLE_DIR, 'playwright'); const isWin = process.platform === 'win32'; function run(cmd, cwd) { console.log(`\n> [${path.basename(cwd)}] ${cmd}`); execSync(cmd, { cwd, stdio: 'inherit', shell: true }); } function step(msg) { console.log(`\n${'='.repeat(60)}`); console.log(` ${msg}`); console.log('='.repeat(60)); } function cleanDir(dir) { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } fs.mkdirSync(dir, { recursive: true }); } function copyDir(src, dest, options = {}) { const { filter, dereference = true } = options; if (!fs.existsSync(src)) { console.log(` 跳过(不存在): ${src}`); return; } fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); // 解析可能的 symlink let stat; try { stat = dereference ? fs.statSync(srcPath) : fs.lstatSync(srcPath); } catch { continue; } if (stat.isDirectory()) { if (filter && filter.skipDir && filter.skipDir(entry.name, srcPath)) continue; copyDir(srcPath, destPath, options); } else if (stat.isFile()) { if (filter && filter.skipFile && filter.skipFile(entry.name, srcPath)) continue; fs.copyFileSync(srcPath, destPath); } } } function copyFile(src, dest) { if (!fs.existsSync(src)) { console.log(` 跳过(不存在): ${src}`); return; } fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); } function dirSizeMB(dir) { try { const out = execSync( isWin ? `powershell -c "(Get-ChildItem -Recurse '${dir}' -File | Measure-Object -Property Length -Sum).Sum"` : `du -sb "${dir}" | cut -f1`, { encoding: 'utf-8', shell: true } ).trim(); return Math.round(parseInt(out || '0') / 1024 / 1024); } catch { return -1; } } // ========================================================= // 1. 编译 shared 包 // ========================================================= step('1/7 编译 shared 包'); if (fs.existsSync(path.join(SHARED_DIR, 'package.json'))) { run('npm run build', SHARED_DIR); } else { console.log(' 跳过(shared 目录不存在)'); } // ========================================================= // 2. 编译 Node 服务端 // ========================================================= step('2/7 编译 Node 服务端'); try { run('npx tsc', SERVER_DIR); } catch { const distDir = path.join(SERVER_DIR, 'dist'); if (fs.existsSync(distDir) && fs.readdirSync(distDir).length > 0) { console.log(' ⚠ tsc 有类型警告,但 dist/ 已生成,继续'); } else { throw new Error('Node 服务端编译失败且 dist/ 未生成'); } } // ========================================================= // 3. 检查 Python venv // ========================================================= step('3/7 检查 Python 虚拟环境'); const venvPython = isWin ? path.join(PYTHON_DIR, 'venv', 'Scripts', 'python.exe') : path.join(PYTHON_DIR, 'venv', 'bin', 'python'); if (fs.existsSync(venvPython)) { console.log(` venv 已存在: ${venvPython}`); const reqTxt = path.join(PYTHON_DIR, 'requirements.txt'); if (fs.existsSync(reqTxt)) { console.log(' 安装/更新 Python 依赖...'); run(`"${venvPython}" -m pip install -r requirements.txt --quiet`, PYTHON_DIR); } } else { console.log(' venv 不存在,尝试创建...'); try { run(`${isWin ? 'python' : 'python3'} -m venv venv`, PYTHON_DIR); const reqTxt = path.join(PYTHON_DIR, 'requirements.txt'); if (fs.existsSync(reqTxt)) { run(`"${venvPython}" -m pip install -r requirements.txt`, PYTHON_DIR); } } catch (e) { console.warn(` ⚠ 创建 venv 失败: ${e.message}`); } } if (fs.existsSync(venvPython)) { console.log(' 确保 Playwright chromium 已安装...'); try { run(`"${venvPython}" -m playwright install chromium`, PYTHON_DIR); } catch (e) { console.warn(` ⚠ Playwright install 失败: ${e.message}`); } } // ========================================================= // 4. 复制 shared 包(server 依赖它,必须先准备好) // ========================================================= step('4/7 复制 shared 包'); cleanDir(SHARED_BUNDLE); copyDir(path.join(SHARED_DIR, 'dist'), path.join(SHARED_BUNDLE, 'dist')); copyFile(path.join(SHARED_DIR, 'package.json'), path.join(SHARED_BUNDLE, 'package.json')); console.log(' shared 包已就绪'); // ========================================================= // 5. 复制服务端资源 + 安装依赖 // ========================================================= step('5/7 复制服务端资源并安装依赖'); cleanDir(SERVER_BUNDLE); // 5a. 先复制 package.json 并修改 workspace 引用 console.log(' 复制 package.json ...'); copyFile(path.join(SERVER_DIR, 'package.json'), path.join(SERVER_BUNDLE, 'package.json')); const bundlePkgPath = path.join(SERVER_BUNDLE, 'package.json'); const bundlePkg = JSON.parse(fs.readFileSync(bundlePkgPath, 'utf-8')); if (bundlePkg.dependencies && bundlePkg.dependencies['@media-manager/shared']) { bundlePkg.dependencies['@media-manager/shared'] = 'file:../shared'; fs.writeFileSync(bundlePkgPath, JSON.stringify(bundlePkg, null, 2), 'utf-8'); console.log(' -> @media-manager/shared: workspace:* => file:../shared'); } // 5b. npm install 生成干净的 node_modules(避免 pnpm symlink 问题) console.log(' 运行 npm install --omit=dev (耐心等待) ...'); run('npm install --omit=dev', SERVER_BUNDLE); console.log(` -> node_modules: ${dirSizeMB(path.join(SERVER_BUNDLE, 'node_modules'))} MB`); // 5c. 复制编译后的代码 console.log(' 复制 server/dist/ ...'); copyDir(path.join(SERVER_DIR, 'dist'), path.join(SERVER_BUNDLE, 'dist')); console.log(` -> dist: ${dirSizeMB(path.join(SERVER_BUNDLE, 'dist'))} MB`); // 5d. 复制 .env console.log(' 复制 .env ...'); copyFile(path.join(SERVER_DIR, '.env'), path.join(SERVER_BUNDLE, '.env')); // 5e. 创建 uploads 目录 fs.mkdirSync(path.join(SERVER_BUNDLE, 'uploads'), { recursive: true }); const gitkeep = path.join(SERVER_DIR, 'uploads', '.gitkeep'); if (fs.existsSync(gitkeep)) { fs.copyFileSync(gitkeep, path.join(SERVER_BUNDLE, 'uploads', '.gitkeep')); } // 5f. 复制 Python 服务(源码 + venv) console.log(' 复制 server/python/ ...'); copyDir( PYTHON_DIR, path.join(SERVER_BUNDLE, 'python'), { dereference: true, filter: { skipDir: (name) => name === '__pycache__' || name === '.pytest_cache', skipFile: (name) => name.endsWith('.pyc'), }, } ); console.log(` -> python: ${dirSizeMB(path.join(SERVER_BUNDLE, 'python'))} MB`); // ========================================================= // 6. 复制 Playwright 浏览器 // ========================================================= step('6/7 复制 Playwright 浏览器'); function findPlaywrightBrowsersDir() { if (process.env.PLAYWRIGHT_BROWSERS_PATH && fs.existsSync(process.env.PLAYWRIGHT_BROWSERS_PATH)) { return process.env.PLAYWRIGHT_BROWSERS_PATH; } const candidates = []; if (isWin) { candidates.push(path.join(process.env.LOCALAPPDATA || '', 'ms-playwright')); } else if (process.platform === 'darwin') { candidates.push(path.join(process.env.HOME || '', 'Library', 'Caches', 'ms-playwright')); } else { candidates.push(path.join(process.env.HOME || '', '.cache', 'ms-playwright')); } for (const c of candidates) { if (c && fs.existsSync(c)) return c; } return null; } const pwBrowsersDir = findPlaywrightBrowsersDir(); if (!pwBrowsersDir) { console.error(' ⚠ 未找到 Playwright 浏览器缓存目录'); } else { cleanDir(PLAYWRIGHT_DEST); const entries = fs.readdirSync(pwBrowsersDir); const needed = entries.filter(n => n.startsWith('chromium') || n.startsWith('ffmpeg')); if (needed.length === 0) { console.error(' ⚠ 未找到 chromium'); } else { for (const dir of needed) { console.log(` 复制 ${dir} ...`); copyDir(path.join(pwBrowsersDir, dir), path.join(PLAYWRIGHT_DEST, dir)); console.log(` -> ${dirSizeMB(path.join(PLAYWRIGHT_DEST, dir))} MB`); } } } // ========================================================= step('7/7 验证打包资源'); function verify(label, p) { const exists = fs.existsSync(p); const icon = exists ? '✓' : '✗'; console.log(` ${icon} ${label}: ${p} (${exists ? '存在' : '不存在!'})`); if (!exists) { console.error(` ⚠ 缺少关键资源,打包后服务将无法启动!`); } } verify('server dist/app.js', path.join(SERVER_BUNDLE, 'dist', 'app.js')); verify('server node_modules/', path.join(SERVER_BUNDLE, 'node_modules')); verify('server .env', path.join(SERVER_BUNDLE, '.env')); verify('server python/app.py', path.join(SERVER_BUNDLE, 'python', 'app.py')); verify('shared dist/', path.join(SHARED_BUNDLE, 'dist')); verify('playwright/', PLAYWRIGHT_DEST); step('构建完成!所有资源已准备就绪'); console.log(` server: ${SERVER_BUNDLE}`); console.log(` shared: ${SHARED_BUNDLE}`); console.log(` playwright: ${PLAYWRIGHT_DEST}`);