/ scripts / electron-after-pack.cjs
electron-after-pack.cjs
 1  'use strict'
 2  
 3  /* eslint-disable @typescript-eslint/no-require-imports */
 4  const path = require('node:path')
 5  const fs = require('node:fs')
 6  const { spawnSync } = require('node:child_process')
 7  
 8  const ARCH_NAMES = { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }
 9  
10  // Native modules that live in .next/standalone/node_modules and load inside the
11  // Electron child process. Root node_modules are already rebuilt for the target
12  // Electron ABI by electron-builder's @electron/rebuild pass before this hook
13  // fires, so copying those .node files into the packaged standalone is the most
14  // reliable way to keep the two trees in sync per architecture.
15  const NATIVE_MODULES = [
16    'better-sqlite3',
17    '@mongodb-js/zstd',
18    'node-liblzma',
19    'utf-8-validate',
20  ]
21  
22  function copyIfExists(src, dest) {
23    if (!fs.existsSync(src)) return false
24    fs.mkdirSync(path.dirname(dest), { recursive: true })
25    fs.copyFileSync(src, dest)
26    return true
27  }
28  
29  function syncNativeBuildDir(rootPkgDir, standalonePkgDir) {
30    if (!fs.existsSync(rootPkgDir) || !fs.existsSync(standalonePkgDir)) return false
31    const rootBuild = path.join(rootPkgDir, 'build', 'Release')
32    const standaloneBuild = path.join(standalonePkgDir, 'build', 'Release')
33    if (!fs.existsSync(rootBuild)) return false
34    fs.mkdirSync(standaloneBuild, { recursive: true })
35    let synced = false
36    for (const entry of fs.readdirSync(rootBuild)) {
37      if (!entry.endsWith('.node')) continue
38      copyIfExists(path.join(rootBuild, entry), path.join(standaloneBuild, entry))
39      synced = true
40    }
41    return synced
42  }
43  
44  exports.default = async function afterPack(context) {
45    if (context.electronPlatformName !== 'darwin') return
46  
47    const projectDir = context.packager.info.projectDir
48    const appName = context.packager.appInfo.productFilename
49    const appPath = path.join(context.appOutDir, `${appName}.app`)
50    const standaloneNodeModules = path.join(appPath, 'Contents', 'Resources', '.next', 'standalone', 'node_modules')
51    const rootNodeModules = path.join(projectDir, 'node_modules')
52    const archName = ARCH_NAMES[context.arch]
53    if (!archName) throw new Error(`afterPack: unknown arch ${context.arch}`)
54  
55    console.log(`[after-pack] syncing native modules into standalone for arch=${archName}`)
56    for (const moduleName of NATIVE_MODULES) {
57      const rootPkg = path.join(rootNodeModules, moduleName)
58      const standalonePkg = path.join(standaloneNodeModules, moduleName)
59      const synced = syncNativeBuildDir(rootPkg, standalonePkg)
60      console.log(`[after-pack]   ${moduleName}: ${synced ? 'synced' : 'skipped'}`)
61    }
62  
63    console.log(`[after-pack] ad-hoc signing ${appPath}`)
64    const codesign = spawnSync(
65      'codesign',
66      [
67        '--sign', '-',
68        '--force',
69        '--deep',
70        '--timestamp=none',
71        '--preserve-metadata=entitlements,requirements,flags,runtime',
72        appPath,
73      ],
74      { stdio: 'inherit' },
75    )
76    if (codesign.status !== 0) {
77      throw new Error(`afterPack: codesign ad-hoc failed with status ${codesign.status}`)
78    }
79  }