/**
* 代码混淆压缩构建脚本
* 使用 javascript-obfuscator 混淆压缩 dist/ 目录下的所有 .js 文件
*/
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import JavaScriptObfuscator from 'javascript-obfuscator';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 项目根目录 (scripts/ 的父目录)
const rootDir = path.resolve(__dirname, '..');
const distDir = path.join(rootDir, 'dist');
// 混淆配置
const obfuscationOptions = {
compact: true, // 压缩代码 (Minification)
controlFlowFlattening: true, // 控制流平坦化 (混淆核心逻辑)
controlFlowFlatteningThreshold: 0.75,
deadCodeInjection: false, // 死代码注入 (为了控制体积,暂时关闭,如需更高安全性可开启)
debugProtection: false, // 防调试 (为了避免用户使用报错,建议关闭,如果是商业核心逻辑可开启)
disableConsoleOutput: false, // 禁用 console (保留日志输出,如果是库建议禁止,这里作为 MCP server 可能需要日志)
identifierNamesGenerator: 'hexadecimal', // 变量名混淆为十六进制
log: false,
numbersToExpressions: true, // 数字转表达式
renameGlobals: false, // 不重命名全局变量 (防止破坏 Node 环境)
rotateStringArray: true,
selfDefending: true, // 自我防御 (防止格式化)
shuffleStringArray: true,
simplify: true,
splitStrings: true,
splitStringsChunkLength: 10,
stringArray: true, // 字符串加密
stringArrayEncoding: ['base64'],
stringArrayIndexShift: true,
stringArrayWrappersCount: 1,
stringArrayWrappersType: 'variable',
stringArrayThreshold: 0.75,
target: 'node', // 目标环境 Node.js
transformObjectKeys: true,
unicodeEscapeSequence: false
};
async function getAllJsFiles(dir) {
const files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await getAllJsFiles(fullPath));
} else if (entry.name.endsWith('.js') && !entry.name.endsWith('.d.ts')) {
files.push(fullPath);
}
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`读取目录失败: ${dir}`, err);
}
}
return files;
}
// 获取所有需要删除的类型定义文件
async function getTypeDefinitionFiles(dir) {
const files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await getTypeDefinitionFiles(fullPath));
} else if (entry.name.endsWith('.d.ts') || entry.name.endsWith('.d.ts.map')) {
files.push(fullPath);
}
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`读取目录失败: ${dir}`, err);
}
}
return files;
}
async function obfuscateFiles() {
console.log('正在混淆压缩 JavaScript 文件...');
// 检查 dist 目录是否存在
try {
await fs.access(distDir);
} catch {
console.error('错误: dist 目录不存在。请先运行 npm run build');
process.exit(1);
}
// 删除类型定义文件 (.d.ts 和 .d.ts.map)
console.log('正在清理类型定义文件...');
const typeFiles = await getTypeDefinitionFiles(distDir);
for (const file of typeFiles) {
try {
await fs.unlink(file);
console.log(` 🗑️ 已删除: ${path.relative(distDir, file)}`);
} catch (err) {
console.error(` ✗ 删除失败: ${path.relative(distDir, file)} - ${err.message}`);
}
}
const jsFiles = await getAllJsFiles(distDir);
if (jsFiles.length === 0) {
console.warn('没有找到需要混淆的 .js 文件');
return;
}
let successCount = 0;
let failCount = 0;
for (const file of jsFiles) {
try {
const code = await fs.readFile(file, 'utf-8');
const originalSize = code.length;
// 混淆
const obfuscationResult = JavaScriptObfuscator.obfuscate(code, obfuscationOptions);
const obfuscatedCode = obfuscationResult.getObfuscatedCode();
await fs.writeFile(file, obfuscatedCode);
const newSize = obfuscatedCode.length;
// 因为混淆通常会增加体积,这里显示的可能是增加了多少,或者压缩了多少(取决于配置)
// 如果 compact: true 但 obfuscation 逻辑多,体积可能变大。
// 既然用户要求“压缩”,我们最好确保体积没有暴增,但为了安全性,体积增加是正常的。
// 这里的“压缩”更多是指“compact form (无空格换行)”。
const changeStr = newSize > originalSize
? `+${((newSize - originalSize) / originalSize * 100).toFixed(1)}%`
: `-${((originalSize - newSize) / originalSize * 100).toFixed(1)}%`;
console.log(` ✓ ${path.relative(distDir, file)} (${originalSize}B -> ${newSize}B | ${changeStr})`);
successCount++;
} catch (error) {
console.error(` ✗ ${path.relative(distDir, file)}: ${error.message}`);
failCount++;
}
}
console.log(`\n混淆完成! 成功: ${successCount}, 失败: ${failCount}`);
}
obfuscateFiles().catch(console.error);