const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = __dirname; const pkg = require('./package.json'); const PRODUCT = pkg.productName || 'PeopleLib'; const TARGET = `${PRODUCT}-windows-x64`; const REQUESTED_OUT = process.env.PEOPLELIB_OUT_DIR ? path.resolve(process.env.PEOPLELIB_OUT_DIR) : path.join(ROOT, 'dist', TARGET); const OUT = REQUESTED_OUT; const APP = path.join(OUT, 'resources', 'app'); function rimraf(p) { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); } function clearOutput(dir) { if (!fs.existsSync(dir)) return; const executable = path.join(dir, `${PRODUCT}.exe`); const probe = executable + '.build-lock-check'; if (fs.existsSync(executable)) { try { fs.renameSync(executable, probe); fs.renameSync(probe, executable); } catch (error) { if (!fs.existsSync(executable) && fs.existsSync(probe)) { try { fs.renameSync(probe, executable); } catch (restoreError) { /* report the original lock error */ } } if (error && (error.code === 'EPERM' || error.code === 'EBUSY' || error.code === 'EACCES')) { throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`); } throw error; } } for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'data') continue; const target = path.join(dir, entry.name); try { fs.rmSync(target, { recursive: true, force: true }); } catch (error) { if (error && (error.code === 'EPERM' || error.code === 'EBUSY')) { throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`); } throw error; } } } function ensureElectronRuntime() { const electronDir = path.join(ROOT, 'node_modules', 'electron'); const distDir = path.join(electronDir, 'dist'); const executable = path.join(distDir, 'electron.exe'); const versionFile = path.join(distDir, 'version'); const expected = String(pkg.devDependencies && pkg.devDependencies.electron || '').replace(/^v/, ''); const installed = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '') : ''; if (installed === expected && fs.existsSync(executable)) return distDir; const installer = path.join(electronDir, 'install.js'); if (!fs.existsSync(installer)) throw new Error('缺少 Electron 安装脚本,请先运行 npm ci'); console.log(`准备 Electron ${expected} 运行时...`); const result = spawnSync(process.execPath, [installer], { cwd: electronDir, env: { ...process.env, ELECTRON_MIRROR: process.env.ELECTRON_MIRROR || process.env.npm_config_electron_mirror || 'https://npmmirror.com/mirrors/electron/' }, stdio: 'inherit' }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(`Electron 运行时安装失败(退出码 ${result.status})`); const prepared = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '') : ''; if (prepared !== expected || !fs.existsSync(executable)) { throw new Error(`Electron 运行时版本无效(期望 ${expected || '未知'},实际 ${prepared || '缺失'})`); } return distDir; } function copyDir(src, dst, skip) { fs.mkdirSync(dst, { recursive: true }); for (const e of fs.readdirSync(src, { withFileTypes: true })) { if (skip && skip(e)) continue; const s = path.join(src, e.name); const d = path.join(dst, e.name); if (e.isDirectory()) copyDir(s, d, skip); else fs.copyFileSync(s, d); } } function pruneLocales(dir) { const keep = new Set(['zh-CN.pak', 'en-US.pak']); for (const name of fs.readdirSync(dir)) { if (!keep.has(name)) fs.unlinkSync(path.join(dir, name)); } } function skipDevFiles(e) { const name = e.name.toLowerCase(); if (e.isDirectory()) return false; if (/\.(d\.ts|d\.ts\.map|ts|tsx|map|flow)$/.test(name)) return true; if (/^(readme|changelog|history|license|licence|notice|authors|contributing|security)/.test(name)) return true; if (/\.(md|markdown)$/.test(name)) return true; return false; } function copyUndici(dst) { const src = path.join(ROOT, 'node_modules', 'undici'); fs.mkdirSync(dst, { recursive: true }); for (const name of ['package.json', 'index.js', 'index-fetch.js', 'LICENSE']) { const file = path.join(src, name); if (fs.existsSync(file)) fs.copyFileSync(file, path.join(dst, name)); } copyDir(path.join(src, 'lib'), path.join(dst, 'lib'), skipDevFiles); } function copyFoliate(dst) { const src = path.join(ROOT, 'node_modules', 'foliate-js'); fs.mkdirSync(path.join(dst, 'vendor'), { recursive: true }); for (const name of ['package.json', 'LICENSE', 'mobi.js']) { fs.copyFileSync(path.join(src, name), path.join(dst, name)); } fs.copyFileSync( path.join(src, 'vendor', 'fflate.js'), path.join(dst, 'vendor', 'fflate.js') ); } async function build() { const electronDist = ensureElectronRuntime(); console.log('清理固定输出目录(保留 data)...'); clearOutput(OUT); console.log('复制 Electron 运行时...'); copyDir(electronDist, OUT); console.log('精简语言包...'); pruneLocales(path.join(OUT, 'locales')); console.log('重命名可执行文件...'); const executable = path.join(OUT, PRODUCT + '.exe'); fs.renameSync(path.join(OUT, 'electron.exe'), executable); rimraf(path.join(OUT, 'resources', 'default_app.asar')); console.log(`应用 ${PRODUCT} 图标...`); const { rcedit } = await import('rcedit'); await rcedit(executable, { icon: path.join(ROOT, 'icons', 'dist', 'book-ai-dark.ico'), 'file-version': pkg.version, 'product-version': pkg.version, 'version-string': { ProductName: PRODUCT, FileDescription: PRODUCT, InternalName: PRODUCT, OriginalFilename: `${PRODUCT}.exe` } }); console.log('组装 app 源码...'); fs.mkdirSync(APP, { recursive: true }); fs.copyFileSync(path.join(ROOT, 'main.js'), path.join(APP, 'main.js')); fs.copyFileSync(path.join(ROOT, 'preload.js'), path.join(APP, 'preload.js')); fs.copyFileSync( path.join(ROOT, 'manga-online-preload.js'), path.join(APP, 'manga-online-preload.js') ); copyDir(path.join(ROOT, 'src'), path.join(APP, 'src'), (e) => e.name.startsWith('_test')); const iconDir = path.join(APP, 'icons', 'dist'); fs.mkdirSync(iconDir, { recursive: true }); for (const name of ['book-ai-dark.ico', 'book-ai-light.ico']) { fs.copyFileSync(path.join(ROOT, 'icons', 'dist', name), path.join(iconDir, name)); } for (const theme of ['dark', 'light']) { const themeDir = path.join(iconDir, theme); fs.mkdirSync(themeDir, { recursive: true }); fs.copyFileSync( path.join(ROOT, 'icons', 'dist', theme, 'icon-32.png'), path.join(themeDir, 'icon-32.png') ); } copyUndici(path.join(APP, 'node_modules', 'undici')); copyFoliate(path.join(APP, 'node_modules', 'foliate-js')); fs.writeFileSync(path.join(APP, 'package.json'), JSON.stringify({ name: pkg.name, version: pkg.version, description: pkg.description, productName: PRODUCT, main: 'main.js', author: pkg.author, license: pkg.license, dependencies: { undici: pkg.dependencies.undici, 'foliate-js': pkg.dependencies['foliate-js'] } }, null, 2)); console.log('\n构建完成:'); console.log(' 目录:', OUT); console.log(' 可执行文件:', executable); } build().catch((error) => { console.error('构建失败:', error && error.message ? error.message : error); process.exitCode = 1; });