diff --git a/scripts/build-asar.js b/scripts/build-asar.js index e7347d0..e4cc005 100644 --- a/scripts/build-asar.js +++ b/scripts/build-asar.js @@ -4,13 +4,15 @@ * 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 } = require('fs'); +const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream, existsSync } = require('fs'); const { join } = require('path'); const { createHash } = require('crypto'); const asar = require('@electron/asar'); @@ -19,14 +21,51 @@ const ROOT = join(__dirname, '..'); const STAGING = join(ROOT, 'out', 'asar-staging'); const OUTPUT = join(ROOT, 'out', 'asar'); -// Same node_modules list as electron-builder.yml -const NODE_MODULES = [ +// 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'); } @@ -39,7 +78,12 @@ async function main() { } execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env }); - // 2. Prepare staging directory + // 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 }); @@ -49,35 +93,41 @@ async function main() { // Copy package.json (Electron reads "main" from it) cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json')); - // Copy required node_modules (excluding .ts and .map files) - for (const mod of NODE_MODULES) { + // 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); - cpSync(src, dest, { - recursive: true, - filter: (s) => !shouldExclude(s), - }); + + // 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), + }); + } } - // 3. Pack into asar + // 4. Pack into asar mkdirSync(OUTPUT, { recursive: true }); const asarPath = join(OUTPUT, 'app.asar'); await asar.createPackage(STAGING, asarPath); - console.log('Created:', asarPath); + console.log('\nCreated:', asarPath); - // 4. Generate checksums.sha256 + // 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); - // 5. Cleanup staging + // 6. Cleanup staging rmSync(STAGING, { recursive: true, force: true }); - // Print size + // 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) { diff --git a/src/main/updater.ts b/src/main/updater.ts index 36698e6..8648af2 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -80,12 +80,12 @@ if (-not (Test-Path $pending)) { exit 1 } try { if (Test-Path $backup) { Remove-Item $backup -Force } - Rename-Item $asar $backup -Force - Rename-Item $pending $asar -Force + Move-Item $asar $backup -Force + Move-Item $pending $asar -Force if (Test-Path $backup) { Remove-Item $backup -Force } } catch { if ((Test-Path $backup) -and -not (Test-Path $asar)) { - Rename-Item $backup $asar -Force + Move-Item $backup $asar -Force } exit 1 }