147 lines
5.1 KiB
JavaScript
147 lines
5.1 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|