Files
peoplelib/build-portable.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 3ccd044527 feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0
新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件,
批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、
图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文,
页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。

便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化,
避免升级后 data/ 被遗留在旧目录。

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 12:13:02 +08:00

204 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'));
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;
});