/** * after-pack.cjs * * electron-builder afterPack hook for zn-ai * * This hook runs AFTER electron-builder finishes packing. * It handles: * 1. Copying and bundling electron/scripts/ directory * 2. Ensuring required dependencies (playwright, chromium-bidi, bytenode) are included * 3. Copying bundled OpenClaw runtime dependencies that electron-builder may skip * 4. Cleaning up unnecessary development files to reduce package size */ const fs = require('fs-extra'); const path = require('path'); const esbuild = require('esbuild'); const ARCH_NAME_MAP = { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal', }; const REQUIRED_RUNTIME_BINARIES = { win32: ['uv.exe', 'node.exe'], darwin: ['uv'], linux: ['uv'], }; function normalizePackArch(arch) { if (typeof arch === 'string') { return arch; } return ARCH_NAME_MAP[arch] || String(arch); } function normalizePlatformName(electronPlatformName) { switch (electronPlatformName) { case 'win': return 'win32'; case 'mac': return 'darwin'; default: return electronPlatformName; } } function resolveBundledRuntimeTarget(context) { const platform = normalizePlatformName(context.electronPlatformName); const arch = normalizePackArch(context.arch); return `${platform}-${arch}`; } async function copyBundledRuntimeBinaries(context, overrides = {}) { const target = resolveBundledRuntimeTarget(context); const platform = normalizePlatformName(context.electronPlatformName); const sourceRoot = overrides.sourceRoot || path.join(__dirname, '..', 'resources', 'bin', target); const resourcesDir = overrides.resourcesDir || resolveResourcesDir(context); const destRoot = overrides.destRoot || path.join(resourcesDir, 'bin'); const requiredFiles = overrides.requiredFiles || REQUIRED_RUNTIME_BINARIES[platform] || []; if (!(await fs.pathExists(sourceRoot))) { throw new Error( `[after-pack] Missing bundled runtime directory for ${target}: ${sourceRoot}. ` + `Run the bundled runtime preparation step before packaging.`, ); } await fs.ensureDir(destRoot); const entries = await fs.readdir(sourceRoot, { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile()) { continue; } await fs.copy(path.join(sourceRoot, entry.name), path.join(destRoot, entry.name), { overwrite: true, }); } const missing = requiredFiles.filter((fileName) => !fs.existsSync(path.join(destRoot, fileName))); if (missing.length > 0) { throw new Error( `[after-pack] Missing required bundled runtime binaries for ${target}: ${missing.join(', ')}.`, ); } console.log(`[after-pack] Copied bundled runtime binaries for ${target} -> ${destRoot}`); } /** * Remove development artifacts from a directory (recursive). * Removes: test directories, TypeScript definitions, source maps, docs, etc. */ function cleanupUnnecessaryFiles(dir) { let removedCount = 0; const REMOVE_DIRS = new Set([ 'test', 'tests', '__tests__', '.github', 'docs', 'examples', 'example', ]); const REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown']; const REMOVE_FILE_NAMES = new Set([ '.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md', 'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig', ]); function walk(currentDir) { let entries; try { entries = fs.readdirSync(currentDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { if (REMOVE_DIRS.has(entry.name)) { try { fs.rmSync(fullPath, { recursive: true, force: true }); removedCount++; } catch { /* ignore */ } } else { walk(fullPath); } } else if (entry.isFile()) { const name = entry.name; if (REMOVE_FILE_NAMES.has(name) || REMOVE_FILE_EXTS.some(e => name.endsWith(e))) { try { fs.rmSync(fullPath, { force: true }); removedCount++; } catch { /* ignore */ } } } } } walk(dir); return removedCount; } /** * Clean up platform-specific native packages (optional). * Currently not needed for zn-ai, but kept as a placeholder. */ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) { // No platform-specific native packages identified for zn-ai yet return 0; } function resolveResourcesDir(context) { if (context.electronPlatformName === 'darwin') { if (context.appOutDir.endsWith('.app')) { return path.join(context.appOutDir, 'Contents', 'Resources'); } const appName = context.packager?.appInfo?.productFilename || 'NIANXX'; return path.join(context.appOutDir, `${appName}.app`, 'Contents', 'Resources'); } return path.join(context.appOutDir, 'resources'); } async function copyBundledOpenClaw(context) { const srcRoot = path.join(__dirname, '..', 'build', 'openclaw'); if (!(await fs.pathExists(srcRoot))) { console.warn('[after-pack] build/openclaw not found. Run bundle-openclaw first.'); return; } const resourcesDir = resolveResourcesDir(context); const destRoot = path.join(resourcesDir, 'openclaw'); const srcNodeModules = path.join(srcRoot, 'node_modules'); const destNodeModules = path.join(destRoot, 'node_modules'); await fs.ensureDir(resourcesDir); if (!(await fs.pathExists(destRoot))) { await fs.copy(srcRoot, destRoot, { filter: (src) => { const relative = path.relative(srcRoot, src); if (!relative) return true; return relative.split(path.sep)[0] !== 'node_modules'; }, }); console.log('[after-pack] Copied bundled OpenClaw runtime root.'); } if (!(await fs.pathExists(srcNodeModules))) { console.warn('[after-pack] build/openclaw/node_modules not found. OpenClaw runtime will be incomplete.'); return; } if (!(await fs.pathExists(destNodeModules))) { await fs.copy(srcNodeModules, destNodeModules); console.log('[after-pack] Copied bundled OpenClaw node_modules.'); } } module.exports = async function afterPack(context) { const { appOutDir, electronPlatformName: platform, arch } = context; const resourcesDir = resolveResourcesDir(context); console.log(`Running afterPack hook for ${platform}-${arch}`); console.log(`App output directory: ${appOutDir}`); await copyBundledRuntimeBinaries(context, { resourcesDir }); // 1. Handle electron/scripts/ directory const scriptsSrc = path.join(__dirname, '..', 'electron/scripts'); const scriptsDest = path.join(resourcesDir, 'scripts'); if (await fs.pathExists(scriptsSrc)) { await fs.ensureDir(scriptsDest); const files = await fs.readdir(scriptsSrc); for (const file of files) { const srcFile = path.join(scriptsSrc, file); const destFile = path.join(scriptsDest, file); if (file.endsWith('.js')) { // Bundle JavaScript files with esbuild try { await esbuild.build({ entryPoints: [srcFile], outfile: destFile, bundle: true, platform: 'node', target: 'node24', // Adjust based on Electron version external: ['electron'], // Exclude electron from bundling format: 'cjs', }); console.log(`Bundled: ${file}`); } catch (error) { console.error(`Failed to bundle ${file}:`, error); // Fallback to copying the file await fs.copy(srcFile, destFile); } } else { // Copy other files as-is await fs.copy(srcFile, destFile); console.log(`Copied: ${file}`); } } } await copyBundledOpenClaw(context); // 2. Ensure required dependencies are included // electron-builder may exclude some dependencies due to .gitignore rules const ensureDependency = async (depName) => { const src = path.join(__dirname, '..', 'node_modules', depName); const dest = path.join(resourcesDir, 'node_modules', depName); if (await fs.pathExists(src)) { await fs.ensureDir(path.dirname(dest)); if (!(await fs.pathExists(dest))) { await fs.copy(src, dest); console.log(`Copied dependency: ${depName}`); } } }; // List of dependencies that need to be explicitly copied const requiredDeps = ['playwright', 'playwright-core', 'chromium-bidi', 'bytenode']; for (const dep of requiredDeps) { await ensureDependency(dep); } // 3. Clean up unnecessary development files from node_modules (skip if SKIP_AFTERPACK_CLEANUP is set) if (!process.env.SKIP_AFTERPACK_CLEANUP) { const nodeModulesDest = path.join(resourcesDir, 'node_modules'); if (await fs.pathExists(nodeModulesDest)) { console.log('Cleaning up development files in node_modules...'); const removed = cleanupUnnecessaryFiles(nodeModulesDest); console.log(`Removed ${removed} unnecessary files/directories from node_modules.`); } // 4. Clean up unnecessary files in scripts directory if (await fs.pathExists(scriptsDest)) { console.log('Cleaning up development files in scripts directory...'); const removedScripts = cleanupUnnecessaryFiles(scriptsDest); console.log(`Removed ${removedScripts} unnecessary files/directories from scripts.`); } } else { console.log('Skipping afterPack cleanup (SKIP_AFTERPACK_CLEANUP is set)'); } // 5. Optional: platform-specific native package cleanup // cleanupNativePlatformPackages(nodeModulesDest, platform, arch); console.log('afterPack hook completed successfully'); }; module.exports.__test__ = { normalizePackArch, normalizePlatformName, resolveBundledRuntimeTarget, copyBundledRuntimeBinaries, };