├── package.json ├── .gitignore ├── README.md ├── md5.js └── chaos.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cheerio": "^1.1.2", 4 | "clean-css": "^5.3.3", 5 | "javascript-obfuscator": "^4.1.1", 6 | "minimist": "^1.2.8" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | 26 | *~ 27 | .DS_Store 28 | WorkspaceSettings.xcsettings 29 | *.xcworkspace 30 | xcuserdata 31 | 32 | **/dist/ 33 | **/TargetApp/ 34 | **/Pods/ 35 | **/.idea/ 36 | 37 | 38 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YPH5Pack 2 | 3 | > 一款自动化的 H5 项目打包混淆工具,支持 JavaScript 混淆、CSS 压缩、资源复制,并保留或重构目录结构。 4 | 5 | ## ✨ 功能特色 6 | 7 | - ✅ 混淆所有 JS 代码(内联和外链,使用 `javascript-obfuscator`) 8 | - ✅ 压缩所有 CSS(内联和外链,使用 `clean-css`) 9 | - ✅ 支持保留原始目录结构或统一扁平输出 10 | - ✅ 自动复制未处理的静态资源(图片、字体等) 11 | - ✅ 支持处理整个目录或单个 HTML 文件 12 | - ✅ 输出目录自动带时间戳,便于版本管理与对比 13 | 14 | ## 📦 安装 15 | 16 | 无需安装为全局模块,直接 clone 使用即可: 17 | 18 | ```sh 19 | git clone https://github.com/HansenCCC/YPH5Pack.git 20 | cd YPH5Pack 21 | npm install 22 | # md5.js 依赖 exiftool,按需接入 23 | brew install exiftool 24 | ``` 25 | 26 |
27 | 28 | ## 🚀 使用方法 29 | 30 | ``` 31 | node md5.js ./your-html-folder # 此脚本是修改所有文件的md5值【按需执行】 32 | node chaos.js ./your-html-folder # 此脚本是代码混淆 【按需执行】 33 | ``` 34 | 35 |
36 | 37 | ## 📁 输出结构示例 38 | 39 | ``` 40 | your-html-folder_20250804_114503/ 41 | ├── index.html 42 | ├── obf_main.js 43 | ├── min_style.css 44 | ├── assets/ 45 | │ └── logo.png 46 | └── ... 47 | ``` 48 | -------------------------------------------------------------------------------- /md5.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { execSync } = require('child_process'); 4 | 5 | const ignoredDirs = ['node_modules', '.git', 'dist', 'build', 'out']; 6 | 7 | function isImage(ext) { 8 | return ['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext); 9 | } 10 | 11 | const commentableTextExts = ['.js', '.css', '.vue', '.html', '.txt', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']; 12 | const noCommentExts = ['.json', '.lock', '.yml', '.yaml', '.toml', '.xml']; 13 | const safeAppendExts = ['.md', '.csv', '.properties', '.ini']; 14 | 15 | function randomString(len = 10) { 16 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 17 | let s = ''; 18 | for (let i = 0; i < len; i++) { 19 | s += chars.charAt(Math.floor(Math.random() * chars.length)); 20 | } 21 | return s; 22 | } 23 | 24 | function modifyImageMetadata(filePath) { 25 | const comment = `Modified-${randomString(20)}`; 26 | try { 27 | execSync(`exiftool -overwrite_original -Comment="${comment}" "${filePath}"`); 28 | console.log(`Image metadata modified: ${filePath}`); 29 | } catch (err) { 30 | console.warn(`exiftool未安装或执行失败,跳过图片修改: ${filePath}`); 31 | } 32 | } 33 | 34 | function modifyTextFile(filePath) { 35 | try { 36 | const ext = path.extname(filePath).toLowerCase(); 37 | let content = fs.readFileSync(filePath, 'utf8'); 38 | 39 | if (commentableTextExts.includes(ext)) { 40 | let comment = ''; 41 | if (['.js', '.ts', '.jsx', '.tsx', '.vue', '.mjs', '.cjs'].includes(ext)) { 42 | comment = `/* ${randomString(30)} */\n`; 43 | } else if (ext === '.css') { 44 | comment = `/* ${randomString(30)} */\n`; 45 | } else if (ext === '.html') { 46 | comment = `\n`; 47 | } else if (ext === '.txt') { 48 | comment = `// ${randomString(30)}\n`; 49 | } else { 50 | comment = `/* ${randomString(30)} */\n`; 51 | } 52 | content = comment + content; 53 | } else if (noCommentExts.includes(ext)) { 54 | if (!content.endsWith('\n')) { 55 | content += '\n'; 56 | } else { 57 | content += ' '; 58 | } 59 | } else if (safeAppendExts.includes(ext)) { 60 | if (!content.endsWith('\n')) { 61 | content += '\n'; 62 | } else { 63 | content += ' '; 64 | } 65 | } else { 66 | if (!content.endsWith('\n')) { 67 | content += '\n'; 68 | } else { 69 | content += ' '; 70 | } 71 | } 72 | 73 | fs.writeFileSync(filePath, content, 'utf8'); 74 | console.log(`Text file modified: ${filePath}`); 75 | } catch (err) { 76 | console.error(`Failed to modify text file: ${filePath}`, err.message); 77 | } 78 | } 79 | 80 | function traverseDir(dir) { 81 | const files = fs.readdirSync(dir); 82 | files.forEach(file => { 83 | if (ignoredDirs.includes(file)) { 84 | console.log(`跳过目录: ${path.join(dir, file)}`); 85 | return; 86 | } 87 | 88 | const fullPath = path.join(dir, file); 89 | const stats = fs.statSync(fullPath); 90 | if (stats.isDirectory()) { 91 | traverseDir(fullPath); 92 | } else { 93 | const ext = path.extname(file).toLowerCase(); 94 | if (isImage(ext)) { 95 | modifyImageMetadata(fullPath); 96 | } else { 97 | modifyTextFile(fullPath); 98 | } 99 | } 100 | }); 101 | } 102 | 103 | const targetDir = process.argv[2] || process.cwd(); 104 | 105 | if (!fs.existsSync(targetDir)) { 106 | console.error(`目录不存在: ${targetDir}`); 107 | process.exit(1); 108 | } 109 | 110 | console.log(`开始处理目录: ${targetDir}`); 111 | traverseDir(targetDir); 112 | console.log('全部完成。'); 113 | -------------------------------------------------------------------------------- /chaos.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const cheerio = require('cheerio'); 4 | const obfuscator = require('javascript-obfuscator'); 5 | const CleanCSS = require('clean-css'); 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | 8 | const inputPath = argv._[0]; 9 | if (!inputPath || !fs.existsSync(inputPath)) { 10 | console.error('❌ 请输入有效的文件路径或目录路径'); 11 | process.exit(1); 12 | } 13 | 14 | const keepStructure = argv['keep-structure'] !== false; // 默认为 true 15 | const minifyCss = argv['minify-css'] !== false; // 默认为 true 16 | 17 | // 获取输入路径名(目录或文件名,不带扩展名) 18 | const baseName = path.basename(inputPath, path.extname(inputPath)); 19 | 20 | // 格式化当前时间为 YYYYMMDD_HHMMSS 21 | function getFormattedTime() { 22 | const now = new Date(); 23 | const pad = (n) => n.toString().padStart(2, '0'); 24 | return ( 25 | now.getFullYear().toString() + 26 | pad(now.getMonth() + 1) + 27 | pad(now.getDate()) + 28 | '_' + 29 | pad(now.getHours()) + 30 | pad(now.getMinutes()) + 31 | pad(now.getSeconds()) 32 | ); 33 | } 34 | 35 | // 输出目录:输入路径同级目录下,名字带时间戳 36 | const parentDir = path.dirname(path.resolve(inputPath)); 37 | const outputDir = path.join(parentDir, `${baseName}_${getFormattedTime()}`); 38 | 39 | console.log(`输入路径: ${inputPath}`); 40 | console.log(`输出目录: ${outputDir}`); 41 | 42 | // 根据输入是文件还是目录返回 HTML 文件列表 43 | function getHtmlFiles(dir) { 44 | let results = []; 45 | const list = fs.readdirSync(dir); 46 | for (const file of list) { 47 | const fullPath = path.join(dir, file); 48 | const stat = fs.statSync(fullPath); 49 | if (stat.isDirectory()) { 50 | results = results.concat(getHtmlFiles(fullPath)); 51 | } else if (/\.(html?|HTML?)$/.test(file)) { 52 | results.push(fullPath); 53 | } 54 | } 55 | return results; 56 | } 57 | 58 | function copyAllAssets(fromDir, toDir) { 59 | if (!fs.existsSync(toDir)) fs.mkdirSync(toDir, { recursive: true }); 60 | const items = fs.readdirSync(fromDir); 61 | for (const item of items) { 62 | const srcPath = path.join(fromDir, item); 63 | const dstPath = path.join(toDir, item); 64 | const stat = fs.statSync(srcPath); 65 | if (stat.isDirectory()) { 66 | copyAllAssets(srcPath, dstPath); 67 | } else { 68 | fs.copyFileSync(srcPath, dstPath); 69 | } 70 | } 71 | } 72 | 73 | function getHtmlFilesOrSingle(input) { 74 | const stat = fs.statSync(input); 75 | if (stat.isDirectory()) { 76 | return getHtmlFiles(input); 77 | } else if (stat.isFile() && /\.(html?|HTML?)$/.test(input)) { 78 | return [input]; 79 | } 80 | return []; 81 | } 82 | 83 | // 统一输入路径根,用于计算相对路径 84 | const statInput = fs.statSync(inputPath); 85 | const baseInputPath = statInput.isDirectory() ? inputPath : path.dirname(inputPath); 86 | 87 | copyAllAssets(baseInputPath, outputDir); 88 | 89 | const htmlFiles = getHtmlFilesOrSingle(inputPath); 90 | const handledFiles = new Set(); 91 | 92 | for (const htmlFilePath of htmlFiles) { 93 | const htmlContent = fs.readFileSync(htmlFilePath, 'utf-8'); 94 | const $ = cheerio.load(htmlContent); 95 | const htmlDir = path.dirname(htmlFilePath); 96 | 97 | // 先算当前 HTML 文件输出路径,用于后续相对路径计算 98 | const relHtmlPath = keepStructure 99 | ? path.relative(baseInputPath, htmlFilePath) 100 | : 'obf_' + path.basename(htmlFilePath); 101 | const finalHtmlPath = path.join(outputDir, relHtmlPath); 102 | 103 | // 处理 JS 104 | $('script').each((i, el) => { 105 | const src = $(el).attr('src'); 106 | 107 | if (src) { 108 | const scriptPath = path.resolve(htmlDir, src); 109 | if (fs.existsSync(scriptPath)) { 110 | handledFiles.add(path.resolve(scriptPath)); 111 | 112 | const jsContent = fs.readFileSync(scriptPath, 'utf-8'); 113 | const obfuscatedCode = obfuscator.obfuscate(jsContent, { 114 | compact: true, 115 | controlFlowFlattening: true, 116 | selfDefending: true, 117 | stringArray: true 118 | }).getObfuscatedCode(); 119 | 120 | const relOutputPath = keepStructure 121 | ? path.relative(baseInputPath, scriptPath) 122 | : 'obf_' + path.basename(src); 123 | const newJsPath = path.join(outputDir, relOutputPath); 124 | 125 | fs.mkdirSync(path.dirname(newJsPath), { recursive: true }); 126 | fs.writeFileSync(newJsPath, obfuscatedCode, 'utf-8'); 127 | 128 | // 替换为从当前 HTML 输出文件位置到新 JS 文件的相对路径 129 | const relativeSrc = path.relative(path.dirname(finalHtmlPath), newJsPath).replace(/\\/g, '/'); 130 | $(el).attr('src', relativeSrc); 131 | 132 | console.log(`🔒 混淆并保存 JS: ${newJsPath}`); 133 | } 134 | } else { 135 | // 混淆内联 JS 136 | const inlineCode = $(el).html(); 137 | const obfuscatedCode = obfuscator.obfuscate(inlineCode).getObfuscatedCode(); 138 | $(el).html(obfuscatedCode); 139 | console.log(`🔒 混淆内联 JS`); 140 | } 141 | }); 142 | 143 | // 处理 CSS 144 | $('link[rel="stylesheet"]').each((i, el) => { 145 | const href = $(el).attr('href'); 146 | if (!href || !minifyCss) return; 147 | 148 | const cssPath = path.resolve(htmlDir, href); 149 | if (fs.existsSync(cssPath)) { 150 | handledFiles.add(path.resolve(cssPath)); 151 | 152 | const cssContent = fs.readFileSync(cssPath, 'utf-8'); 153 | const output = new CleanCSS({}).minify(cssContent).styles; 154 | 155 | const relOutputPath = keepStructure 156 | ? path.relative(baseInputPath, cssPath) 157 | : 'min_' + path.basename(cssPath); 158 | const newCssPath = path.join(outputDir, relOutputPath); 159 | 160 | fs.mkdirSync(path.dirname(newCssPath), { recursive: true }); 161 | fs.writeFileSync(newCssPath, output, 'utf-8'); 162 | 163 | const relativeHref = path.relative(path.dirname(finalHtmlPath), newCssPath).replace(/\\/g, '/'); 164 | $(el).attr('href', relativeHref); 165 | 166 | console.log(`🎨 压缩并保存 CSS: ${newCssPath}`); 167 | } 168 | }); 169 | 170 | // 压缩内联 CSS 171 | if (minifyCss) { 172 | $('style').each((i, el) => { 173 | const raw = $(el).html(); 174 | const minified = new CleanCSS({}).minify(raw).styles; 175 | $(el).html(minified); 176 | console.log(`🎨 压缩内联 CSS`); 177 | }); 178 | } 179 | 180 | // 写入处理后的 HTML 181 | fs.mkdirSync(path.dirname(finalHtmlPath), { recursive: true }); 182 | fs.writeFileSync(finalHtmlPath, $.html(), 'utf-8'); 183 | handledFiles.add(path.resolve(htmlFilePath)); 184 | console.log(`✅ 已保存 HTML:${finalHtmlPath}`); 185 | } 186 | 187 | // 复制未处理的静态资源 188 | function copyStaticAssets(fromDir, toDir) { 189 | const items = fs.readdirSync(fromDir); 190 | for (const item of items) { 191 | const fullPath = path.join(fromDir, item); 192 | const relPath = path.relative(baseInputPath, fullPath); 193 | const targetPath = path.join(toDir, relPath); 194 | const stat = fs.statSync(fullPath); 195 | 196 | if (stat.isDirectory()) { 197 | copyStaticAssets(fullPath, toDir); 198 | } else { 199 | const ext = path.extname(item).toLowerCase(); 200 | const isHandled = handledFiles.has(path.resolve(fullPath)); 201 | 202 | // 过滤已处理的文件和 html/js/css,复制其他资源文件 203 | if (!isHandled && !['.html', '.htm', '.js', '.css'].includes(ext)) { 204 | fs.mkdirSync(path.dirname(targetPath), { recursive: true }); 205 | fs.copyFileSync(fullPath, targetPath); 206 | console.log(`📁 复制静态资源: ${relPath}`); 207 | } 208 | } 209 | } 210 | } 211 | 212 | copyStaticAssets(baseInputPath, outputDir); 213 | 214 | console.log('\n🎉 所有文件处理完成'); 215 | --------------------------------------------------------------------------------