├── 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 |
--------------------------------------------------------------------------------