/** * Build a standalone app.asar + checksums.sha256 for minor (patch) updates. * * Replicates what electron-builder packs into the asar (dist/ + package.json + * required node_modules) without running the full installer build. * * Automatically resolves transitive dependencies so sub-deps are never missed. * * Usage: * npm run build:asar # default (github) update source * UPDATE_SOURCE=gitea npm run build:asar # gitea update source */ const { execSync } = require('child_process'); const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream, existsSync } = require('fs'); const { join } = require('path'); const { createHash } = require('crypto'); const asar = require('@electron/asar'); const ROOT = join(__dirname, '..'); const STAGING = join(ROOT, 'out', 'asar-staging'); const OUTPUT = join(ROOT, 'out', 'asar'); // Top-level packages to include (same as electron-builder.yml) const SEED_MODULES = [ 'electron-store', 'conf', 'dot-prop', 'type-fest', 'pkg-up', 'find-up', 'locate-path', 'p-locate', 'p-limit', 'yocto-queue', 'path-exists', 'env-paths', 'json-schema-typed', 'ajv', 'ajv-formats', 'atomically', 'debounce-fn', 'mimic-fn', 'semver', 'onetime', ]; /** * Walk the dependency tree starting from seed modules and collect all * transitive production dependencies. */ function resolveAllDeps(seeds) { const resolved = new Set(); const queue = [...seeds]; while (queue.length > 0) { const mod = queue.shift(); if (resolved.has(mod)) continue; // Check if module exists in top-level node_modules const modDir = join(ROOT, 'node_modules', mod); if (!existsSync(modDir)) { console.warn(` Warning: ${mod} not found in node_modules, skipping`); continue; } resolved.add(mod); // Read its dependencies and enqueue them const pkgPath = join(modDir, 'package.json'); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); const deps = Object.keys(pkg.dependencies || {}); for (const dep of deps) { if (!resolved.has(dep)) queue.push(dep); } } catch { /* ignore malformed package.json */ } } } return resolved; } function shouldExclude(src) { return src.endsWith('.ts') || src.endsWith('.map'); } async function main() { // 1. Run Vite build (pass UPDATE_SOURCE through env) const env = { ...process.env }; if (env.UPDATE_SOURCE) { console.log(`Building with UPDATE_SOURCE=${env.UPDATE_SOURCE}`); } execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env }); // 2. Resolve all dependencies (including transitive) console.log('\nResolving dependencies...'); const allModules = resolveAllDeps(SEED_MODULES); console.log(`Found ${allModules.size} modules (${SEED_MODULES.length} seed + ${allModules.size - SEED_MODULES.length} transitive)`); // 3. Prepare staging directory rmSync(STAGING, { recursive: true, force: true }); mkdirSync(STAGING, { recursive: true }); // Copy dist/ cpSync(join(ROOT, 'dist'), join(STAGING, 'dist'), { recursive: true }); // Copy package.json (Electron reads "main" from it) cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json')); // Copy all resolved node_modules (excluding .ts and .map files) for (const mod of allModules) { const src = join(ROOT, 'node_modules', mod); const dest = join(STAGING, 'node_modules', mod); // Some packages have nested node_modules with their own deps // (hoisted vs non-hoisted). Check both the top-level and nested locations. if (existsSync(src)) { cpSync(src, dest, { recursive: true, filter: (s) => !shouldExclude(s), }); } } // 4. Pack into asar mkdirSync(OUTPUT, { recursive: true }); const asarPath = join(OUTPUT, 'app.asar'); await asar.createPackage(STAGING, asarPath); console.log('\nCreated:', asarPath); // 5. Generate checksums.sha256 const hex = await fileHash(asarPath); const checksumsPath = join(OUTPUT, 'checksums.sha256'); writeFileSync(checksumsPath, `${hex} app.asar\n`); console.log(`SHA-256: ${hex}`); console.log('Created:', checksumsPath); // 6. Cleanup staging rmSync(STAGING, { recursive: true, force: true }); // Print size and module list const size = readFileSync(asarPath).length; console.log(`\napp.asar size: ${(size / 1024).toFixed(1)} KB`); console.log('Modules:', [...allModules].sort().join(', ')); } function fileHash(filePath) { return new Promise((resolve, reject) => { const hash = createHash('sha256'); const stream = createReadStream(filePath); stream.on('data', (chunk) => hash.update(chunk)); stream.on('end', () => resolve(hash.digest('hex'))); stream.on('error', reject); }); } main().catch((err) => { console.error('build-asar failed:', err); process.exit(1); });