@playcraft/build 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ export type ObfuscateLevel = 'light' | 'medium' | 'heavy';
2
+ export declare function getFflateInflateRuntime(): string;
3
+ /**
4
+ * 对 JS 代码进行混淆 + fflate 压缩,生成自解压 bootstrap
5
+ */
6
+ export declare function obfuscateAndCompress(jsCode: string, level?: ObfuscateLevel): Promise<{
7
+ /** fflate inflate 运行时脚本(<script>标签) */
8
+ runtime: string;
9
+ /** 自解压 bootstrap 脚本(<script>标签) */
10
+ bootstrap: string;
11
+ /** 原始代码大小 */
12
+ originalSize: number;
13
+ /** 混淆后大小 */
14
+ obfuscatedSize: number;
15
+ /** 压缩后大小(binary) */
16
+ compressedSize: number;
17
+ /** 最终大小(base64 + bootstrap) */
18
+ finalSize: number;
19
+ }>;
20
+ /**
21
+ * 使用 fflate 压缩引擎代码(替换 LZ4)
22
+ * 不做混淆,只做压缩
23
+ */
24
+ export declare function compressWithFflate(code: string): {
25
+ runtime: string;
26
+ compressed: string;
27
+ originalSize: number;
28
+ compressedSize: number;
29
+ };
30
+ /**
31
+ * 使用 fflate 压缩 config.json(替换 LZ4)
32
+ */
33
+ export declare function compressConfigJsonWithFflate(jsonString: string): {
34
+ dataUrl: string;
35
+ originalSize: number;
36
+ compressedSize: number;
37
+ };
38
+ /**
39
+ * 构建 config.json 解压 patch(fflate 版本)
40
+ * 拦截 pc.Http.prototype.get,检测 fflate 压缩的 JSON 并解压
41
+ */
42
+ export declare function buildFflateConfigDecompressPatch(): string;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * 代码混淆 + fflate 压缩工具
3
+ *
4
+ * 流程:
5
+ * 1. javascript-obfuscator 混淆(字符串加密、控制流扁平化等)
6
+ * 2. fflate deflate 压缩
7
+ * 3. base64 编码
8
+ * 4. 包装为自解压 bootstrap 脚本
9
+ */
10
+ import JavaScriptObfuscator from 'javascript-obfuscator';
11
+ import { deflateSync } from 'fflate';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ /**
16
+ * 混淆级别预设配置
17
+ */
18
+ const ObfuscatorPresets = {
19
+ // 轻量级:仅字符串加密 + 标识符混淆,体积膨胀 ~30%
20
+ light: {
21
+ compact: true,
22
+ controlFlowFlattening: false,
23
+ deadCodeInjection: false,
24
+ stringArray: true,
25
+ stringArrayEncoding: ['base64'],
26
+ stringArrayThreshold: 0.5,
27
+ stringArrayCallsTransform: true,
28
+ identifierNamesGenerator: 'hexadecimal',
29
+ renameGlobals: false,
30
+ selfDefending: false,
31
+ splitStrings: false,
32
+ transformObjectKeys: false,
33
+ },
34
+ // 中等:字符串加密 + 控制流扁平化,体积膨胀 ~60%
35
+ medium: {
36
+ compact: true,
37
+ controlFlowFlattening: true,
38
+ controlFlowFlatteningThreshold: 0.5,
39
+ deadCodeInjection: false,
40
+ stringArray: true,
41
+ stringArrayEncoding: ['base64'],
42
+ stringArrayThreshold: 0.75,
43
+ stringArrayCallsTransform: true,
44
+ identifierNamesGenerator: 'hexadecimal',
45
+ renameGlobals: false,
46
+ selfDefending: false,
47
+ splitStrings: true,
48
+ splitStringsChunkLength: 10,
49
+ transformObjectKeys: true,
50
+ },
51
+ // 重度:全部开启,体积膨胀 ~100%+
52
+ heavy: {
53
+ compact: true,
54
+ controlFlowFlattening: true,
55
+ controlFlowFlatteningThreshold: 0.75,
56
+ deadCodeInjection: true,
57
+ deadCodeInjectionThreshold: 0.3,
58
+ stringArray: true,
59
+ stringArrayEncoding: ['rc4'],
60
+ stringArrayThreshold: 1,
61
+ stringArrayCallsTransform: true,
62
+ identifierNamesGenerator: 'hexadecimal',
63
+ renameGlobals: false,
64
+ selfDefending: false,
65
+ splitStrings: true,
66
+ splitStringsChunkLength: 5,
67
+ transformObjectKeys: true,
68
+ unicodeEscapeSequence: true,
69
+ },
70
+ };
71
+ /**
72
+ * fflate inflate 运行时代码(浏览器端)
73
+ * 从 fflate 库的 UMD 构建中动态加载,确保与 Node 端使用的 fflate 版本一致
74
+ * 运行后在 window 上暴露 __fflate_inflate 函数
75
+ */
76
+ let _fflateRuntimeCache = null;
77
+ export function getFflateInflateRuntime() {
78
+ if (_fflateRuntimeCache)
79
+ return _fflateRuntimeCache;
80
+ // 找到 fflate 包的 UMD 构建文件
81
+ // 从当前文件位置向上查找 node_modules/fflate/umd/index.js
82
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
83
+ // 向上查找 node_modules 目录(支持 pnpm 的嵌套 node_modules 结构)
84
+ let searchDir = currentDir;
85
+ let fflateUmdPath = '';
86
+ for (let i = 0; i < 10; i++) {
87
+ const candidate = path.join(searchDir, 'node_modules', 'fflate', 'umd', 'index.js');
88
+ if (fs.existsSync(candidate)) {
89
+ fflateUmdPath = candidate;
90
+ break;
91
+ }
92
+ const parent = path.dirname(searchDir);
93
+ if (parent === searchDir)
94
+ break;
95
+ searchDir = parent;
96
+ }
97
+ if (!fflateUmdPath) {
98
+ throw new Error('[fflate] 无法找到 fflate UMD 构建文件,请确保 fflate 已安装');
99
+ }
100
+ const fflateUmd = fs.readFileSync(fflateUmdPath, 'utf-8');
101
+ // 包装 UMD 使其在浏览器环境中执行,并将 inflateSync 暴露为 window.__fflate_inflate
102
+ // UMD 的外层包装会检测环境:typeof self !== 'undefined' ? self : this
103
+ // 通过 var self=window 确保 fflate 挂载到 window.fflate
104
+ _fflateRuntimeCache = `!function(){var self=window;${fflateUmd};window.__fflate_inflate=window.fflate.inflateSync}();`;
105
+ return _fflateRuntimeCache;
106
+ }
107
+ /**
108
+ * 对 JS 代码进行混淆 + fflate 压缩,生成自解压 bootstrap
109
+ */
110
+ export async function obfuscateAndCompress(jsCode, level = 'medium') {
111
+ const originalSize = Buffer.from(jsCode).length;
112
+ console.log(`[Obfuscator] 开始混淆... 级别: ${level}, 原始大小: ${formatBytes(originalSize)}`);
113
+ console.log(`[Obfuscator] 混淆中(这可能需要较长时间)...`);
114
+ const startTime = Date.now();
115
+ // ① javascript-obfuscator 混淆
116
+ const obfuscated = JavaScriptObfuscator.obfuscate(jsCode, {
117
+ ...ObfuscatorPresets[level],
118
+ target: 'browser',
119
+ seed: Date.now(),
120
+ // 不使用 sourceMap
121
+ sourceMap: false,
122
+ // 禁用 console 输出
123
+ disableConsoleOutput: false,
124
+ // 不使用 domain lock
125
+ domainLock: [],
126
+ domainLockRedirectUrl: 'about:blank',
127
+ // 排除一些 PlayCanvas 关键标识符不被混淆
128
+ reservedNames: [
129
+ 'pc', 'PlayCanvasESMBundle', 'PRELOAD_MODULES',
130
+ '__fflate_inflate',
131
+ ],
132
+ reservedStrings: [],
133
+ }).getObfuscatedCode();
134
+ const obfuscatedSize = Buffer.from(obfuscated).length;
135
+ const obfuscateTime = ((Date.now() - startTime) / 1000).toFixed(1);
136
+ console.log(`[Obfuscator] 混淆完成 (${obfuscateTime}s), 大小: ${formatBytes(obfuscatedSize)} (膨胀 ${((obfuscatedSize / originalSize - 1) * 100).toFixed(0)}%)`);
137
+ // ② fflate deflate 压缩
138
+ const textEncoder = new TextEncoder();
139
+ const codeBytes = textEncoder.encode(obfuscated);
140
+ const compressed = deflateSync(codeBytes, { level: 9 });
141
+ const compressedSize = compressed.length;
142
+ console.log(`[Obfuscator] fflate 压缩完成, 大小: ${formatBytes(compressedSize)} (压缩率 ${((1 - compressedSize / obfuscatedSize) * 100).toFixed(0)}%)`);
143
+ // ③ 转 base64
144
+ const base64 = Buffer.from(compressed).toString('base64');
145
+ // ④ 生成 bootstrap 自解压脚本
146
+ // 运行时:base64 解码 → fflate inflate 解压 → 创建 <script> 执行
147
+ const bootstrapCode = `!function(){var b=atob("${base64}"),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));var e=document.createElement("script");e.textContent=decodeURIComponent(escape(s));document.currentScript.parentNode.insertBefore(e,document.currentScript)}();`;
148
+ const runtime = `<script>${getFflateInflateRuntime()}</script>`;
149
+ const bootstrap = `<script>${bootstrapCode}</script>`;
150
+ const finalSize = runtime.length + bootstrap.length;
151
+ console.log(`[Obfuscator] 最终大小: ${formatBytes(finalSize)} (原始 ${formatBytes(originalSize)} → 节省 ${((1 - finalSize / originalSize) * 100).toFixed(0)}%)`);
152
+ return {
153
+ runtime,
154
+ bootstrap,
155
+ originalSize,
156
+ obfuscatedSize,
157
+ compressedSize,
158
+ finalSize,
159
+ };
160
+ }
161
+ /**
162
+ * 使用 fflate 压缩引擎代码(替换 LZ4)
163
+ * 不做混淆,只做压缩
164
+ */
165
+ export function compressWithFflate(code) {
166
+ const originalSize = Buffer.from(code).length;
167
+ // fflate deflate 压缩
168
+ const textEncoder = new TextEncoder();
169
+ const codeBytes = textEncoder.encode(code);
170
+ const compressed = deflateSync(codeBytes, { level: 9 });
171
+ const compressedSize = compressed.length;
172
+ // 转 base64
173
+ const base64 = Buffer.from(compressed).toString('base64');
174
+ // 生成自解压脚本
175
+ const wrapper = `!function(){var b=atob("${base64}"),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));var e=document.createElement("script");e.textContent=decodeURIComponent(escape(s));document.currentScript.parentNode.insertBefore(e,document.currentScript)}();`;
176
+ const runtime = `<script>${getFflateInflateRuntime()}</script>`;
177
+ const compressedScript = `<script>${wrapper}</script>`;
178
+ console.log(`[fflate] 引擎压缩: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (压缩率 ${((1 - compressedSize / originalSize) * 100).toFixed(0)}%)`);
179
+ return {
180
+ runtime,
181
+ compressed: compressedScript,
182
+ originalSize,
183
+ compressedSize,
184
+ };
185
+ }
186
+ /**
187
+ * 使用 fflate 压缩 config.json(替换 LZ4)
188
+ */
189
+ export function compressConfigJsonWithFflate(jsonString) {
190
+ const originalSize = Buffer.from(jsonString).length;
191
+ const textEncoder = new TextEncoder();
192
+ const jsonBytes = textEncoder.encode(jsonString);
193
+ const compressed = deflateSync(jsonBytes, { level: 9 });
194
+ const compressedSize = compressed.length;
195
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
196
+ // 使用 fflate-json 前缀标识格式
197
+ const dataUrl = `data:application/x-fflate-json;base64,${compressedBase64}`;
198
+ const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
199
+ console.log(`[fflate] config.json 压缩: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (减少 ${ratio}%)`);
200
+ return { dataUrl, originalSize, compressedSize };
201
+ }
202
+ /**
203
+ * 构建 config.json 解压 patch(fflate 版本)
204
+ * 拦截 pc.Http.prototype.get,检测 fflate 压缩的 JSON 并解压
205
+ */
206
+ export function buildFflateConfigDecompressPatch() {
207
+ return `!function(){function p(){if(!window.pc||!pc.Http)return!1;var o=pc.Http.prototype.get;return pc.Http.prototype.get=function(e,t,n){"function"==typeof t&&(n=t,t={});if("string"==typeof e&&e.startsWith("data:application/x-fflate-json;base64,")){try{var r=e.replace("data:application/x-fflate-json;base64,",""),b=atob(r),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));s=decodeURIComponent(escape(s));var j=JSON.parse(s);n(null,j)}catch(x){console.error("[fflate Config] Decompression error:",x);n(x)}return}o.call(this,e,t,n)},!0}if(!p()){var c=0,i=setInterval(function(){(p()||++c>100)&&clearInterval(i)},10)}}();`;
208
+ }
209
+ function formatBytes(bytes) {
210
+ if (bytes === 0)
211
+ return '0 B';
212
+ const k = 1024;
213
+ const sizes = ['B', 'KB', 'MB', 'GB'];
214
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
215
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
216
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Vite 插件:代码混淆 + fflate 压缩
3
+ *
4
+ * 在所有其他插件处理完成后,对最终 HTML 中的 IIFE 脚本进行:
5
+ * 1. javascript-obfuscator 混淆
6
+ * 2. fflate deflate 压缩
7
+ * 3. base64 编码 + 自解压 bootstrap 包装
8
+ */
9
+ import type { Plugin } from 'vite';
10
+ import { type ObfuscateLevel } from '../utils/obfuscate.js';
11
+ export interface ObfuscatePluginOptions {
12
+ enabled: boolean;
13
+ level: ObfuscateLevel;
14
+ /** 是否已经注入了 fflate 运行时(如 compressEngine 已启用) */
15
+ fflateRuntimeInjected: boolean;
16
+ }
17
+ /**
18
+ * 代码混淆插件
19
+ *
20
+ * 在 generateBundle 阶段对 HTML 中的 IIFE 脚本块进行混淆+压缩
21
+ */
22
+ export declare function viteObfuscatePlugin(options: ObfuscatePluginOptions): Plugin;
@@ -0,0 +1,52 @@
1
+ import { obfuscateAndCompress } from '../utils/obfuscate.js';
2
+ /**
3
+ * 代码混淆插件
4
+ *
5
+ * 在 generateBundle 阶段对 HTML 中的 IIFE 脚本块进行混淆+压缩
6
+ */
7
+ export function viteObfuscatePlugin(options) {
8
+ return {
9
+ name: 'vite-plugin-obfuscate',
10
+ enforce: 'post',
11
+ async generateBundle(_outputOptions, bundle) {
12
+ if (!options.enabled)
13
+ return;
14
+ // 找到 HTML 文件
15
+ for (const [fileName, chunk] of Object.entries(bundle)) {
16
+ if (!fileName.endsWith('.html') || chunk.type !== 'asset')
17
+ continue;
18
+ const html = typeof chunk.source === 'string' ? chunk.source : new TextDecoder().decode(chunk.source);
19
+ // 查找 IIFE 脚本块(包含 "Bundled ESM Scripts (IIFE)" 注释的 <script> 标签)
20
+ const iifePattern = /<script>\s*\/\*\s*Bundled ESM Scripts \(IIFE\)\s*\*\/\s*([\s\S]*?)\s*<\/script>/;
21
+ const match = html.match(iifePattern);
22
+ if (!match) {
23
+ console.log('[Obfuscate] 未找到 IIFE 脚本块,跳过混淆');
24
+ continue;
25
+ }
26
+ const iifeCode = match[1];
27
+ console.log(`[Obfuscate] 找到 IIFE 脚本块 (${(iifeCode.length / 1024).toFixed(0)} KB),开始混淆...`);
28
+ try {
29
+ const result = await obfuscateAndCompress(iifeCode, options.level);
30
+ // 构建替换内容
31
+ // 如果 fflate 运行时已经通过 compressEngine 注入,则不重复注入
32
+ const runtimeScript = options.fflateRuntimeInjected
33
+ ? ''
34
+ : result.runtime;
35
+ // 替换原始 IIFE 块为:fflate runtime(如需) + 自解压 bootstrap
36
+ const replacement = `${runtimeScript}${result.bootstrap}`;
37
+ const newHtml = html.replace(match[0], replacement);
38
+ // 更新 bundle
39
+ chunk.source = newHtml;
40
+ console.log(`[Obfuscate] 混淆完成!`);
41
+ console.log(` 原始: ${(result.originalSize / 1024).toFixed(0)} KB`);
42
+ console.log(` 混淆后: ${(result.obfuscatedSize / 1024).toFixed(0)} KB (+${((result.obfuscatedSize / result.originalSize - 1) * 100).toFixed(0)}%)`);
43
+ console.log(` 压缩后: ${(result.compressedSize / 1024).toFixed(0)} KB (压缩率 ${((1 - result.compressedSize / result.obfuscatedSize) * 100).toFixed(0)}%)`);
44
+ console.log(` 最终: ${(result.finalSize / 1024).toFixed(0)} KB (vs 原始 ${((result.finalSize / result.originalSize * 100)).toFixed(0)}%)`);
45
+ }
46
+ catch (error) {
47
+ console.error('[Obfuscate] 混淆失败,保留原始代码:', error);
48
+ }
49
+ }
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Vite 插件:模板字符串压缩
3
+ *
4
+ * 在构建完成后(closeBundle 阶段)对输出的 HTML 文件进行后处理,
5
+ * 压缩 JS 代码中的多行模板字符串,减小包体积。
6
+ *
7
+ * 这个插件必须在 vite-plugin-singlefile 之后运行,
8
+ * 因为它直接处理最终输出的文件,而不是通过 Vite 的 HTML 处理管道。
9
+ */
10
+ import type { Plugin } from 'vite';
11
+ export interface TemplateMinifierOptions {
12
+ /** 输出目录 */
13
+ outputDir: string;
14
+ /** 是否启用(默认 true) */
15
+ enabled?: boolean;
16
+ }
17
+ /**
18
+ * 创建模板字符串压缩插件
19
+ */
20
+ export declare function viteTemplateMinifierPlugin(options: TemplateMinifierOptions): Plugin;