| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- /**
- * 打包前置构建脚本
- * 在 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}`);
|