build-server.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * Build local Node service assets for the Electron client package.
  3. *
  4. * Steps:
  5. * 1. build shared
  6. * 2. compile the Node server
  7. * 3. bundle shared output
  8. * 4. install production dependencies for the bundled server
  9. * 5. copy server dist/.env/uploads
  10. * 6. copy Playwright browsers
  11. */
  12. const { execSync } = require('child_process');
  13. const path = require('path');
  14. const fs = require('fs');
  15. const ROOT = path.resolve(__dirname, '..', '..');
  16. const CLIENT_DIR = path.resolve(__dirname, '..');
  17. const SERVER_DIR = path.join(ROOT, 'server');
  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(message) {
  29. console.log(`\n${'='.repeat(64)}`);
  30. console.log(` ${message}`);
  31. console.log('='.repeat(64));
  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(` skip missing path: ${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. let stat;
  50. try {
  51. stat = dereference ? fs.statSync(srcPath) : fs.lstatSync(srcPath);
  52. } catch {
  53. continue;
  54. }
  55. if (stat.isDirectory()) {
  56. if (filter?.skipDir?.(entry.name, srcPath)) continue;
  57. copyDir(srcPath, destPath, options);
  58. continue;
  59. }
  60. if (stat.isFile()) {
  61. if (filter?.skipFile?.(entry.name, srcPath)) continue;
  62. fs.copyFileSync(srcPath, destPath);
  63. }
  64. }
  65. }
  66. function copyFile(src, dest) {
  67. if (!fs.existsSync(src)) {
  68. console.log(` skip missing file: ${src}`);
  69. return;
  70. }
  71. fs.mkdirSync(path.dirname(dest), { recursive: true });
  72. fs.copyFileSync(src, dest);
  73. }
  74. function dirSizeMB(dir) {
  75. try {
  76. const out = execSync(
  77. isWin
  78. ? `powershell -c "(Get-ChildItem -Recurse '${dir}' -File | Measure-Object -Property Length -Sum).Sum"`
  79. : `du -sb "${dir}" | cut -f1`,
  80. { encoding: 'utf-8', shell: true }
  81. ).trim();
  82. return Math.round(parseInt(out || '0', 10) / 1024 / 1024);
  83. } catch {
  84. return -1;
  85. }
  86. }
  87. function findPlaywrightBrowsersDir() {
  88. if (process.env.PLAYWRIGHT_BROWSERS_PATH && fs.existsSync(process.env.PLAYWRIGHT_BROWSERS_PATH)) {
  89. return process.env.PLAYWRIGHT_BROWSERS_PATH;
  90. }
  91. if (isWin) {
  92. const local = process.env.LOCALAPPDATA || '';
  93. const candidate = path.join(local, 'ms-playwright');
  94. return fs.existsSync(candidate) ? candidate : null;
  95. }
  96. if (process.platform === 'darwin') {
  97. const candidate = path.join(process.env.HOME || '', 'Library', 'Caches', 'ms-playwright');
  98. return fs.existsSync(candidate) ? candidate : null;
  99. }
  100. const candidate = path.join(process.env.HOME || '', '.cache', 'ms-playwright');
  101. return fs.existsSync(candidate) ? candidate : null;
  102. }
  103. function verify(label, targetPath) {
  104. const exists = fs.existsSync(targetPath);
  105. console.log(` ${exists ? 'OK' : '!!'} ${label}: ${targetPath}`);
  106. if (!exists) {
  107. throw new Error(`Missing required bundle asset: ${label}`);
  108. }
  109. }
  110. step('1/6 Build shared package');
  111. if (fs.existsSync(path.join(SHARED_DIR, 'package.json'))) {
  112. run('npm run build', SHARED_DIR);
  113. }
  114. step('2/6 Compile Node server');
  115. try {
  116. run('npx tsc', SERVER_DIR);
  117. } catch (error) {
  118. const distDir = path.join(SERVER_DIR, 'dist');
  119. if (!fs.existsSync(distDir) || fs.readdirSync(distDir).length === 0) {
  120. throw error;
  121. }
  122. console.warn('TypeScript reported issues, but dist/ exists. Continue with the compiled output.');
  123. }
  124. step('3/6 Bundle shared artifacts');
  125. cleanDir(SHARED_BUNDLE);
  126. copyDir(path.join(SHARED_DIR, 'dist'), path.join(SHARED_BUNDLE, 'dist'));
  127. copyFile(path.join(SHARED_DIR, 'package.json'), path.join(SHARED_BUNDLE, 'package.json'));
  128. step('4/6 Prepare production server bundle');
  129. cleanDir(SERVER_BUNDLE);
  130. copyFile(path.join(SERVER_DIR, 'package.json'), path.join(SERVER_BUNDLE, 'package.json'));
  131. const bundlePkgPath = path.join(SERVER_BUNDLE, 'package.json');
  132. const bundlePkg = JSON.parse(fs.readFileSync(bundlePkgPath, 'utf-8'));
  133. if (bundlePkg.dependencies?.['@media-manager/shared']) {
  134. bundlePkg.dependencies['@media-manager/shared'] = 'file:../shared';
  135. fs.writeFileSync(bundlePkgPath, JSON.stringify(bundlePkg, null, 2), 'utf-8');
  136. }
  137. run('npm install --omit=dev', SERVER_BUNDLE);
  138. console.log(` node_modules size: ${dirSizeMB(path.join(SERVER_BUNDLE, 'node_modules'))} MB`);
  139. copyDir(path.join(SERVER_DIR, 'dist'), path.join(SERVER_BUNDLE, 'dist'));
  140. copyFile(path.join(SERVER_DIR, '.env'), path.join(SERVER_BUNDLE, '.env'));
  141. fs.mkdirSync(path.join(SERVER_BUNDLE, 'uploads'), { recursive: true });
  142. copyFile(path.join(SERVER_DIR, 'uploads', '.gitkeep'), path.join(SERVER_BUNDLE, 'uploads', '.gitkeep'));
  143. step('5/6 Bundle Playwright browsers');
  144. const browserCacheDir = findPlaywrightBrowsersDir();
  145. if (!browserCacheDir) {
  146. throw new Error('Playwright browser cache not found. Run `npx playwright install chromium` first.');
  147. }
  148. cleanDir(PLAYWRIGHT_DEST);
  149. const browserEntries = fs.readdirSync(browserCacheDir).filter((name) => name.startsWith('chromium') || name.startsWith('ffmpeg'));
  150. if (browserEntries.length === 0) {
  151. throw new Error('No Chromium Playwright browser binaries were found.');
  152. }
  153. for (const dir of browserEntries) {
  154. console.log(` copy ${dir}`);
  155. copyDir(path.join(browserCacheDir, dir), path.join(PLAYWRIGHT_DEST, dir));
  156. }
  157. console.log(` playwright size: ${dirSizeMB(PLAYWRIGHT_DEST)} MB`);
  158. step('6/6 Verify bundle assets');
  159. verify('server dist/app.js', path.join(SERVER_BUNDLE, 'dist', 'app.js'));
  160. verify('server node_modules', path.join(SERVER_BUNDLE, 'node_modules'));
  161. verify('shared dist', path.join(SHARED_BUNDLE, 'dist'));
  162. verify('playwright', PLAYWRIGHT_DEST);
  163. step('Bundle ready');
  164. console.log(` server: ${SERVER_BUNDLE}`);
  165. console.log(` shared: ${SHARED_BUNDLE}`);
  166. console.log(` playwright: ${PLAYWRIGHT_DEST}`);