/** * 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'); /** * 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}`); // 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'); };