/** * Build local Node service assets for the Electron client package. * * Steps: * 1. build shared * 2. compile the Node server * 3. bundle shared output * 4. install production dependencies for the bundled server * 5. copy server dist/.env/uploads * 6. copy Playwright browsers */ 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 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(message) { console.log(`\n${'='.repeat(64)}`); console.log(` ${message}`); console.log('='.repeat(64)); } 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(` skip missing path: ${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); let stat; try { stat = dereference ? fs.statSync(srcPath) : fs.lstatSync(srcPath); } catch { continue; } if (stat.isDirectory()) { if (filter?.skipDir?.(entry.name, srcPath)) continue; copyDir(srcPath, destPath, options); continue; } if (stat.isFile()) { if (filter?.skipFile?.(entry.name, srcPath)) continue; fs.copyFileSync(srcPath, destPath); } } } function copyFile(src, dest) { if (!fs.existsSync(src)) { console.log(` skip missing file: ${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', 10) / 1024 / 1024); } catch { return -1; } } function findPlaywrightBrowsersDir() { if (process.env.PLAYWRIGHT_BROWSERS_PATH && fs.existsSync(process.env.PLAYWRIGHT_BROWSERS_PATH)) { return process.env.PLAYWRIGHT_BROWSERS_PATH; } if (isWin) { const local = process.env.LOCALAPPDATA || ''; const candidate = path.join(local, 'ms-playwright'); return fs.existsSync(candidate) ? candidate : null; } if (process.platform === 'darwin') { const candidate = path.join(process.env.HOME || '', 'Library', 'Caches', 'ms-playwright'); return fs.existsSync(candidate) ? candidate : null; } const candidate = path.join(process.env.HOME || '', '.cache', 'ms-playwright'); return fs.existsSync(candidate) ? candidate : null; } function verify(label, targetPath) { const exists = fs.existsSync(targetPath); console.log(` ${exists ? 'OK' : '!!'} ${label}: ${targetPath}`); if (!exists) { throw new Error(`Missing required bundle asset: ${label}`); } } step('1/6 Build shared package'); if (fs.existsSync(path.join(SHARED_DIR, 'package.json'))) { run('npm run build', SHARED_DIR); } step('2/6 Compile Node server'); try { run('npx tsc', SERVER_DIR); } catch (error) { const distDir = path.join(SERVER_DIR, 'dist'); if (!fs.existsSync(distDir) || fs.readdirSync(distDir).length === 0) { throw error; } console.warn('TypeScript reported issues, but dist/ exists. Continue with the compiled output.'); } step('3/6 Bundle shared artifacts'); 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')); step('4/6 Prepare production server bundle'); cleanDir(SERVER_BUNDLE); 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?.['@media-manager/shared']) { bundlePkg.dependencies['@media-manager/shared'] = 'file:../shared'; fs.writeFileSync(bundlePkgPath, JSON.stringify(bundlePkg, null, 2), 'utf-8'); } run('npm install --omit=dev', SERVER_BUNDLE); console.log(` node_modules size: ${dirSizeMB(path.join(SERVER_BUNDLE, 'node_modules'))} MB`); copyDir(path.join(SERVER_DIR, 'dist'), path.join(SERVER_BUNDLE, 'dist')); copyFile(path.join(SERVER_DIR, '.env'), path.join(SERVER_BUNDLE, '.env')); fs.mkdirSync(path.join(SERVER_BUNDLE, 'uploads'), { recursive: true }); copyFile(path.join(SERVER_DIR, 'uploads', '.gitkeep'), path.join(SERVER_BUNDLE, 'uploads', '.gitkeep')); step('5/6 Bundle Playwright browsers'); const browserCacheDir = findPlaywrightBrowsersDir(); if (!browserCacheDir) { throw new Error('Playwright browser cache not found. Run `npx playwright install chromium` first.'); } cleanDir(PLAYWRIGHT_DEST); const browserEntries = fs.readdirSync(browserCacheDir).filter((name) => name.startsWith('chromium') || name.startsWith('ffmpeg')); if (browserEntries.length === 0) { throw new Error('No Chromium Playwright browser binaries were found.'); } for (const dir of browserEntries) { console.log(` copy ${dir}`); copyDir(path.join(browserCacheDir, dir), path.join(PLAYWRIGHT_DEST, dir)); } console.log(` playwright size: ${dirSizeMB(PLAYWRIGHT_DEST)} MB`); step('6/6 Verify bundle assets'); verify('server dist/app.js', path.join(SERVER_BUNDLE, 'dist', 'app.js')); verify('server node_modules', path.join(SERVER_BUNDLE, 'node_modules')); verify('shared dist', path.join(SHARED_BUNDLE, 'dist')); verify('playwright', PLAYWRIGHT_DEST); step('Bundle ready'); console.log(` server: ${SERVER_BUNDLE}`); console.log(` shared: ${SHARED_BUNDLE}`); console.log(` playwright: ${PLAYWRIGHT_DEST}`);