Files
YMhut-box-C-/box-old/build-scripts/build.js
T
QWQLwToo 46a3674381
build-winui / winui (push) Has been cancelled
Add legacy Electron app
2026-06-26 13:29:02 +08:00

671 lines
25 KiB
JavaScript
Raw 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.
// build-scripts/build.js
const { minify } = require('terser');
const fs = require('fs-extra');
const path = require('path');
// [修复] 设置控制台输出编码为 UTF-8,解决乱码问题
if (process.platform === 'win32') {
try {
// Windows 下设置控制台编码
process.stdout.setDefaultEncoding('utf8');
process.stderr.setDefaultEncoding('utf8');
// 尝试设置控制台代码页为 UTF-8
const { execSync } = require('child_process');
try {
execSync('chcp 65001 >nul 2>&1', { encoding: 'utf8' });
} catch (e) {
// 忽略编码设置错误
}
} catch (e) {
// 忽略编码设置错误
}
}
// ====================================================================================
// --- 🚀 核心配置区 ---
// ====================================================================================
const rootDir = path.join(__dirname, '..');
const outputDir = path.join(rootDir, 'app_dist');
// ====================================================================================
// --- 🔍 自动化识别需要保留的内容 ---
// ====================================================================================
/**
* [自动化] 从 preload.js 和 main.js 中提取所有 IPC 通信频道名称
* @returns {Promise<Set<string>>} - 返回所有频道名称的集合
*/
async function extractIpcChannels() {
const channels = new Set();
try {
// 从 preload.js 提取
const preloadPath = path.join(rootDir, 'preload.js');
if (await fs.pathExists(preloadPath)) {
const preloadContent = await fs.readFile(preloadPath, 'utf8');
// 匹配 ipcRenderer.invoke('channel'), ipcRenderer.send('channel'), ipcRenderer.on('channel')
const preloadRegex = /ipcRenderer\.(?:invoke|send|on)\(['"]([^'"]+)['"]/g;
let match;
while ((match = preloadRegex.exec(preloadContent)) !== null) {
channels.add(match[1]);
}
}
// 从 main.js 提取
const mainPath = path.join(rootDir, 'main.js');
if (await fs.pathExists(mainPath)) {
const mainContent = await fs.readFile(mainPath, 'utf8');
// 匹配 ipcMain.handle('channel'), ipcMain.on('channel')
const mainRegex = /ipcMain\.(?:handle|on)\(['"]([^'"]+)['"]/g;
let match;
while ((match = mainRegex.exec(mainContent)) !== null) {
channels.add(match[1]);
}
}
return channels;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取 IPC 频道失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取 IPC 频道失败:', error);
}
return channels;
}
}
/**
* [自动化] 从 contextBridge.exposeInMainWorld 中提取所有暴露的 API 名称
* @returns {Promise<Set<string>>} - 返回所有 API 名称的集合
*/
async function extractExposedApis() {
const apis = new Set();
try {
const preloadPath = path.join(rootDir, 'preload.js');
if (await fs.pathExists(preloadPath)) {
const content = await fs.readFile(preloadPath, 'utf8');
// 匹配 contextBridge.exposeInMainWorld('electronAPI', { ... })
const exposeRegex = /contextBridge\.exposeInMainWorld\(['"]([^'"]+)['"]/g;
let match;
while ((match = exposeRegex.exec(content)) !== null) {
apis.add(match[1]);
}
// 提取对象中的所有方法名
const objectRegex = /contextBridge\.exposeInMainWorld\([^,]+,\s*\{([^}]+)\}/s;
const objectMatch = content.match(objectRegex);
if (objectMatch) {
const objectContent = objectMatch[1];
// 匹配方法名: methodName:
const methodRegex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g;
let methodMatch;
while ((methodMatch = methodRegex.exec(objectContent)) !== null) {
apis.add(methodMatch[1]);
}
}
}
return apis;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取暴露 API 失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取暴露 API 失败:', error);
}
return apis;
}
}
/**
* [自动化] 从所有 JS 文件中提取 export 的类名、函数名和变量名
* @returns {Promise<{names: Set<string>, strings: Set<string>}>} - 返回标识符和关键字符串
*/
async function extractExportedIdentifiers() {
const names = new Set();
const strings = new Set();
// 添加 ES6 模块关键字
['export', 'default', 'import', 'from', 'as', 'module', 'exports'].forEach(s => strings.add(s));
try {
const jsDir = path.join(rootDir, 'src', 'js');
const files = await findJsFiles(jsDir);
for (const filePath of files) {
try {
const content = await fs.readFile(filePath, 'utf8');
// 提取 export default class ClassName
const classExportRegex = /export\s+default\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
let match;
while ((match = classExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export default new ClassName()
const newExportRegex = /export\s+default\s+new\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = newExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export default functionName
const funcExportRegex = /export\s+default\s+(?:function\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = funcExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export { name1, name2 }
const namedExportRegex = /export\s+\{([^}]+)\}/g;
while ((match = namedExportRegex.exec(content)) !== null) {
const exports = match[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim());
exports.forEach(name => {
if (name && /^[a-zA-Z_$]/.test(name)) {
names.add(name);
}
});
}
// 提取 window.xxx = 或 window['xxx'] =
const windowAssignRegex = /window\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
while ((match = windowAssignRegex.exec(content)) !== null) {
names.add(match[1]);
strings.add(match[1]);
}
// 提取 window['xxx'] =
const windowBracketRegex = /window\[['"]([^'"]+)['"]\]\s*=/g;
while ((match = windowBracketRegex.exec(content)) !== null) {
names.add(match[1]);
strings.add(match[1]);
}
} catch (err) {
// 忽略单个文件的错误
}
}
return { names, strings };
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取导出标识符失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取导出标识符失败:', error);
}
return { names, strings };
}
}
/**
* [自动化] 从代码中提取所有类的方法名(通过分析类定义)
* @returns {Promise<Set<string>>} - 返回所有方法名的集合
*/
async function extractClassMethods() {
const methods = new Set();
try {
const jsDir = path.join(rootDir, 'src', 'js');
const files = await findJsFiles(jsDir);
for (const filePath of files) {
try {
const content = await fs.readFile(filePath, 'utf8');
// 提取类方法: 匹配 class 内部的方法定义
// 1. 匹配 async methodName() { 或 methodName() {
const classMethodRegex = /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g;
let match;
while ((match = classMethodRegex.exec(content)) !== null) {
const methodName = match[1];
// 排除常见的关键字和内置方法
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'catch', 'then', 'finally'].includes(methodName)) {
methods.add(methodName);
}
}
// 2. 也匹配对象方法: methodName: function() { 或 methodName: () => {
const objectMethodRegex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:function|\([^)]*\)\s*=>)/g;
while ((match = objectMethodRegex.exec(content)) !== null) {
const methodName = match[1];
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf'].includes(methodName)) {
methods.add(methodName);
}
}
// 3. 匹配 this.methodName 调用,提取方法名
const thisMethodRegex = /this\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\(|\.bind)/g;
while ((match = thisMethodRegex.exec(content)) !== null) {
const methodName = match[1];
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf'].includes(methodName)) {
methods.add(methodName);
}
}
} catch (err) {
// 忽略单个文件的错误
}
}
return methods;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取类方法失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取类方法失败:', error);
}
return methods;
}
}
/**
* [自动化] 从 main.js 中提取所有使用的 Node.js 内置模块方法名
* @returns {Promise<Set<string>>} - 返回所有方法名的集合
*/
async function extractNodeBuiltinMethods() {
const methods = new Set();
try {
const mainPath = path.join(rootDir, 'main.js');
if (await fs.pathExists(mainPath)) {
const content = await fs.readFile(mainPath, 'utf8');
// 提取 path.xxx, fs.xxx, crypto.xxx, os.xxx 等方法调用
const builtinModuleRegex = /(?:path|fs|crypto|os|https|http|stream|child_process|worker_threads|electron)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\(|\[)/g;
let match;
while ((match = builtinModuleRegex.exec(content)) !== null) {
methods.add(match[1]);
}
}
// 添加常用的 Node.js 内置模块方法(确保不被压缩)
const commonNodeMethods = [
// path 模块
'join', 'resolve', 'dirname', 'basename', 'extname', 'parse', 'format', 'normalize', 'isAbsolute',
// fs 模块
'readFile', 'writeFile', 'readFileSync', 'writeFileSync', 'existsSync', 'mkdirSync', 'statSync', 'readdirSync',
'createReadStream', 'createWriteStream', 'access', 'accessSync', 'unlink', 'unlinkSync', 'rmdir', 'rmdirSync',
// crypto 模块
'createHash', 'createHmac', 'randomBytes', 'createCipheriv', 'createDecipheriv',
// os 模块
'platform', 'arch', 'homedir', 'tmpdir', 'uptime', 'totalmem', 'freemem', 'cpus', 'networkInterfaces',
// stream 模块
'pipeline', 'Readable', 'Writable', 'Transform',
// child_process 模块
'exec', 'execSync', 'spawn', 'spawnSync',
// https/http 模块
'get', 'request', 'createServer',
// worker_threads 模块
'Worker', 'isMainThread', 'parentPort',
// Electron 模块
'app', 'BrowserWindow', 'ipcMain', 'ipcRenderer', 'dialog', 'shell', 'nativeTheme', 'screen', 'session',
'getPath', 'isPackaged', 'getVersion', 'getLocale', 'quit', 'exit', 'relaunch',
'on', 'handle', 'send', 'invoke', 'webContents', 'loadURL', 'loadFile', 'show', 'hide', 'minimize', 'maximize', 'close',
'showMessageBox', 'showOpenDialog', 'showSaveDialog', 'openExternal', 'openPath',
'shouldUseDarkColors', 'themeSource', 'getAllDisplays', 'getPrimaryDisplay',
'defaultSession', 'fromPartition', 'webRequest', 'onHeadersReceived', 'setPreloads'
];
commonNodeMethods.forEach(method => methods.add(method));
return methods;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取 Node.js 内置方法失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取 Node.js 内置方法失败:', error);
}
return methods;
}
}
/**
* 递归查找所有 JS 文件
*/
async function findJsFiles(dir) {
let results = [];
try {
const list = await fs.readdir(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(await findJsFiles(filePath));
} else if (filePath.endsWith('.js')) {
results.push(filePath);
}
}
} catch (error) {
// 忽略错误
}
return results;
}
// ====================================================================================
// --- ⚙️ 构建脚本执行区 ---
// ====================================================================================
const sourcesToProcess = [
'main.js',
'preload.js',
'config',
'src'
];
/**
* 判断是否为关键文件(需要特殊处理)
*/
function isCriticalFile(filePath) {
const criticalPatterns = [
/main\.js$/,
/preload\.js$/,
/configManager\.js$/,
/mainPage\.js$/,
/uiManager\.js$/,
/toolStatusManager\.js$/,
/toolHealthChecker\.js$/,
/toolHealthCheckModal\.js$/
];
return criticalPatterns.some(pattern => pattern.test(filePath));
}
/**
* 使用 Terser 压缩代码(轻量级,不会导致方法调用问题)
* @param {string} code - 原始代码
* @param {string} filePath - 文件路径
* @param {Object} reservedData - 预提取的保留数据
* @returns {Promise<string>} - 压缩后的代码
*/
async function minifyCode(code, filePath, reservedData) {
try {
const { reservedNames, reservedStrings } = reservedData;
const result = await minify(code, {
compress: {
drop_console: false, // 保留 console,方便调试
drop_debugger: true,
pure_funcs: [], // 不删除任何函数调用
passes: 2 // 压缩次数
},
mangle: {
reserved: Array.from(reservedNames), // 保留的标识符
// [修复] 完全禁用属性压缩,避免类方法被压缩导致的问题
properties: false // 禁用属性压缩,确保 this.methodName 不被压缩
},
format: {
comments: false // 删除注释
},
keep_classnames: true, // 保留类名
keep_fnames: true, // [修复] 保留函数名,确保类方法名不被压缩
safari10: true // Safari 10 兼容性
});
return result.code || code;
} catch (error) {
const errorMsg = `⚠️ 压缩 ${path.relative(outputDir, filePath)} 失败,使用原始代码: ${error.message}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
return code;
}
}
/**
* 主构建函数
*/
async function run() {
// [修复] 确保输出使用 UTF-8 编码
const log = (message) => {
try {
process.stdout.write(Buffer.from(message + '\n', 'utf8'));
} catch (e) {
console.log(message);
}
};
log('🧹 [1/6] 清理旧的构建目录...');
await fs.remove(outputDir);
await fs.ensureDir(outputDir);
log('🔄 [2/6] 复制并清理文件用于发布...');
const packageJsonPath = path.join(rootDir, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
const prodPackageJson = {
name: packageJson.name,
version: packageJson.version,
productName: packageJson.productName,
description: packageJson.description,
main: packageJson.main,
author: packageJson.author,
dependencies: packageJson.dependencies
};
await fs.writeJson(path.join(outputDir, 'package.json'), prodPackageJson, { spaces: 2 });
log('- 已清理并写入 package.json');
// 复制 lang 目录
const langSrc = path.join(rootDir, 'lang');
const langDest = path.join(outputDir, 'lang');
if (await fs.pathExists(langSrc)) {
await fs.copy(langSrc, langDest);
log('- 已复制 lang 目录到 app_dist');
} else {
throw new Error(`❌ 找不到 lang 目录:${langSrc}`);
}
// 复制 config-template.ini
const configTemplatePath = path.join(rootDir, 'build-scripts', 'config-template.ini');
const configTemplateDest = path.join(outputDir, 'config-template.ini');
if (await fs.pathExists(configTemplatePath)) {
await fs.copy(configTemplatePath, configTemplateDest);
log('- 已复制 config-template.ini 到 app_dist');
} else {
throw new Error(`❌ 找不到 config-template.ini${configTemplatePath}`);
}
// 复制其他源文件
for (const source of sourcesToProcess) {
const sourcePath = path.join(rootDir, source);
const destPath = path.join(outputDir, source);
if (await fs.pathExists(sourcePath)) {
await fs.copy(sourcePath, destPath);
}
}
log('- 其他所有源文件已复制。');
// 自动化识别需要保留的内容(只执行一次)
log('🔍 [3/6] 自动化识别需要保留的内容...');
const ipcChannels = await extractIpcChannels();
const exposedApis = await extractExposedApis();
const exportedIds = await extractExportedIdentifiers();
const classMethods = await extractClassMethods();
const nodeBuiltinMethods = await extractNodeBuiltinMethods();
// 合并所有需要保留的内容
const reservedNames = new Set();
const reservedStrings = new Set();
// 添加 IPC 频道到字符串保留列表
ipcChannels.forEach(ch => reservedStrings.add(ch));
// 添加暴露的 API
exposedApis.forEach(api => {
reservedNames.add(api);
reservedStrings.add(api);
});
// 添加导出标识符
exportedIds.names.forEach(name => reservedNames.add(name));
exportedIds.strings.forEach(str => reservedStrings.add(str));
// 添加类方法名
classMethods.forEach(method => reservedNames.add(method));
// [修复] 添加 MainPage 的关键方法名到保留列表(防止方法被压缩)
// 这些方法名必须保留,否则 this.methodName.bind() 会失败
const mainPageMethods = [
'renderWelcomePage',
'renderToolHealthCheckPage',
'renderLogsPage',
'renderSettingsPage',
'updateActiveNavButton',
'navigateTo',
'initializePage',
'init',
'bindWindowControls',
'bindNavigationEvents',
'bindThemeToggle',
'addRippleEffectListener',
'bindGlobalKeyListener',
'loadLanguage',
'autoCheckUpdates',
'listenForDownloadProgress',
'listenForGlobalNetworkSpeed',
'handleSecretTrigger',
'renderTrafficChart',
'handleLanguageChange',
'updateLockedToolsList',
'updateToolHealthStats',
'bindSettingsPageEvents'
];
mainPageMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 UIManager 的关键方法名到保留列表
const uiManagerMethods = [
'getHealthIndicator',
'renderToolboxPage',
'_renderCurrentPage',
'_filterAndRenderTools'
];
uiManagerMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 Node.js 内置模块方法名(重要!防止 path.join 等被压缩)
nodeBuiltinMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 JavaScript 原生方法
['bind', 'call', 'apply', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
// 添加 Electron 和浏览器相关
['electronAPI', 'window', 'document', 'require', 'module', 'exports', 'ipcRenderer', 'ipcMain', 'contextBridge', 'BrowserWindow', 'app', 'process', 'global', 'console'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
// 添加 Node.js 内置模块名
['path', 'fs', 'crypto', 'os', 'https', 'http', 'stream', 'child_process', 'worker_threads', 'electron', 'util', 'events', 'buffer'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
log(`- 识别完成: ${ipcChannels.size} 个 IPC 频道, ${exposedApis.size} 个暴露 API, ${exportedIds.names.size} 个导出标识符, ${classMethods.size} 个类方法, ${nodeBuiltinMethods.size} 个 Node.js 内置方法`);
log(`- 总计保留: ${reservedNames.size} 个标识符, ${reservedStrings.size} 个字符串`);
// 准备保留数据对象
const reservedData = { reservedNames, reservedStrings };
log('📦 [4/6] 压缩 JavaScript 文件...');
const jsFiles = await findJsFiles(outputDir);
// 分离关键文件和普通文件
const criticalFiles = [];
const normalFiles = [];
for (const filePath of jsFiles) {
if (isCriticalFile(filePath)) {
criticalFiles.push(filePath);
} else {
normalFiles.push(filePath);
}
}
log(`- 发现 ${criticalFiles.length} 个关键文件,${normalFiles.length} 个普通文件`);
// 处理所有文件
let successCount = 0;
let failCount = 0;
for (const filePath of jsFiles) {
try {
const code = await fs.readFile(filePath, 'utf8');
const minified = await minifyCode(code, filePath, reservedData);
await fs.writeFile(filePath, minified);
successCount++;
if (successCount % 10 === 0) {
try {
process.stdout.write(Buffer.from(`\r- 已处理: ${successCount}/${jsFiles.length}`, 'utf8'));
} catch (e) {
process.stdout.write(`\r- 已处理: ${successCount}/${jsFiles.length}`);
}
}
} catch (error) {
const relativePath = path.relative(outputDir, filePath);
const errorMsg = `\n❌ 处理 ${relativePath} 失败: ${error.message || error}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
failCount++;
}
}
log(`\n- 处理完成: ${successCount} 成功, ${failCount} 失败`);
log('📊 [5/6] 生成构建报告...');
const buildReport = {
timestamp: new Date().toISOString(),
version: packageJson.version,
filesProcessed: {
total: jsFiles.length,
critical: criticalFiles.length,
normal: normalFiles.length,
success: successCount,
failed: failCount
},
security: {
minificationEnabled: true,
reservedIdentifiers: {
ipcChannels: ipcChannels.size,
exposedApis: exposedApis.size,
exportedIdentifiers: exportedIds.names.size,
classMethods: classMethods.size,
nodeBuiltinMethods: nodeBuiltinMethods.size,
total: reservedNames.size
},
autoDetectionEnabled: true
}
};
await fs.writeJson(
path.join(outputDir, 'build-report.json'),
buildReport,
{ spaces: 2 }
);
log('- 构建报告已生成');
log('✅ [6/6] 构建过程成功完成!');
log(`📦 最终的应用文件已准备就绪,位于: ${outputDir}`);
log(`🔐 安全措施: 代码压缩 + 自动识别保留内容`);
log(`📈 处理统计: ${jsFiles.length} 个文件 (${successCount} 成功, ${failCount} 失败)`);
}
// 运行构建
run().catch(err => {
const errorMsg = `❌ 在构建过程中发生错误:\n${err.stack || err.message || err}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
process.exit(1);
});