build-electron.mjs
1 #!/usr/bin/env node 2 3 import fs from 'node:fs' 4 import path from 'node:path' 5 import { spawnSync } from 'node:child_process' 6 import { fileURLToPath } from 'node:url' 7 8 const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') 9 const nativeModules = [ 10 'better-sqlite3', 11 '@mongodb-js/zstd', 12 'node-liblzma', 13 'utf-8-validate', 14 ] 15 16 const args = new Set(process.argv.slice(2)) 17 const skipNext = args.has('--skip-next') 18 const publishAlways = args.has('--publish') 19 // --skip-rebuild accepted for backwards compat; electron-builder + the 20 // afterPack hook (scripts/electron-after-pack.cjs) now handle native module 21 // ABI per-architecture so there is no pre-package rebuild step to skip. 22 if (args.has('--skip-rebuild')) { 23 // no-op 24 } 25 const platformFlag = args.has('--mac') ? '--mac' 26 : args.has('--win') ? '--win' 27 : args.has('--linux') ? '--linux' 28 : null 29 30 function run(cmd, cmdArgs, env = {}) { 31 const status = runWithStatus(cmd, cmdArgs, env) 32 if (status !== 0) process.exit(status) 33 } 34 35 function runWithStatus(cmd, cmdArgs, env = {}) { 36 const result = spawnSync(cmd, cmdArgs, { 37 cwd: repoRoot, 38 stdio: 'inherit', 39 env: { ...process.env, ...env }, 40 shell: process.platform === 'win32', 41 }) 42 if (result.status !== 0) { 43 console.error(`[build-electron] ${cmd} ${cmdArgs.join(' ')} failed with status ${result.status}`) 44 return result.status ?? 1 45 } 46 return 0 47 } 48 49 function restoreHostNativeModules() { 50 console.log('[build-electron] restoring host native modules…') 51 run('npm', ['rebuild', ...nativeModules, '--silent']) 52 } 53 54 function copyDir(src, dest) { 55 fs.mkdirSync(dest, { recursive: true }) 56 for (const entry of fs.readdirSync(src, { withFileTypes: true })) { 57 const from = path.join(src, entry.name) 58 const to = path.join(dest, entry.name) 59 if (entry.isDirectory()) copyDir(from, to) 60 else fs.copyFileSync(from, to) 61 } 62 } 63 64 console.log('[build-electron] compiling electron main process…') 65 run('npx', ['--no-install', 'tsc', '-p', 'electron/tsconfig.json']) 66 67 if (!skipNext) { 68 console.log('[build-electron] running next build…') 69 run('npm', ['run', 'build']) 70 } 71 72 const standaloneDir = path.join(repoRoot, '.next', 'standalone') 73 if (!fs.existsSync(standaloneDir)) { 74 console.error(`[build-electron] missing ${standaloneDir}. Did next build fail?`) 75 process.exit(1) 76 } 77 78 console.log('[build-electron] copying static + public into standalone…') 79 const nextStatic = path.join(repoRoot, '.next', 'static') 80 const standaloneNextStatic = path.join(standaloneDir, '.next', 'static') 81 if (fs.existsSync(nextStatic)) { 82 fs.rmSync(standaloneNextStatic, { recursive: true, force: true }) 83 copyDir(nextStatic, standaloneNextStatic) 84 } 85 const publicDir = path.join(repoRoot, 'public') 86 const standalonePublic = path.join(standaloneDir, 'public') 87 if (fs.existsSync(publicDir)) { 88 fs.rmSync(standalonePublic, { recursive: true, force: true }) 89 copyDir(publicDir, standalonePublic) 90 } 91 92 // Native modules inside .next/standalone/node_modules are rebuilt per-arch by 93 // the electron-builder afterPack hook (scripts/electron-after-pack.cjs), which 94 // runs electron-rebuild against the packaged .app's copy of standalone/. Doing 95 // it here would only rebuild once for the host arch and be overwritten during 96 // packaging anyway. 97 98 console.log('[build-electron] running electron-builder…') 99 const builderArgs = [] 100 if (platformFlag) builderArgs.push(platformFlag) 101 if (publishAlways) { 102 builderArgs.push('--publish', 'always') 103 } else { 104 builderArgs.push('--publish', 'never') 105 } 106 const builderStatus = runWithStatus('npx', ['--no-install', 'electron-builder', ...builderArgs]) 107 restoreHostNativeModules() 108 if (builderStatus !== 0) process.exit(builderStatus) 109 110 console.log('[build-electron] done. Artifacts in release/')