fix: resolve transitive deps in build-asar, use Move-Item in swap script

This commit is contained in:
2026-04-16 10:21:14 -07:00
parent 9a52d6b8fc
commit 5e7786d66e
2 changed files with 68 additions and 18 deletions
+65 -15
View File
@@ -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) {
+3 -3
View File
@@ -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
}