@playcraft/build 0.0.14 → 0.0.17

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.
@@ -267,15 +267,47 @@ export async function generateConfig(projectConfig, options) {
267
267
  let selectedScenes = allScenes;
268
268
  if (options?.selectedScenes && options.selectedScenes.length > 0) {
269
269
  selectedScenes = allScenes.filter(scene => {
270
+ // PlayCanvas 格式: scene.scene 字段存在(场景ID)
271
+ // PlayCraft 格式: 只有 scene.id 字段
270
272
  const sceneId = String(scene.id || scene.scene);
271
273
  const sceneName = scene.name || '';
272
274
  return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
273
275
  });
276
+ // 验证:检查是否所有指定的场景都存在
277
+ if (selectedScenes.length === 0) {
278
+ const availableScenes = allScenes.map(s => s.name || s.id).join(', ');
279
+ // 尝试找到默认场景(主场景或第一个场景)
280
+ const defaultScene = allScenes.find(s => s.isMain === true) || allScenes[0];
281
+ if (defaultScene && allScenes.length > 0) {
282
+ console.warn(`\n⚠️ 警告: 未找到指定的场景,使用默认场景\n` +
283
+ ` 指定的场景: ${options.selectedScenes.join(', ')}\n` +
284
+ ` 可用的场景: ${availableScenes}\n` +
285
+ ` 默认场景: ${defaultScene.name || defaultScene.id}${defaultScene.isMain ? ' (主场景)' : ''}`);
286
+ selectedScenes = [defaultScene];
287
+ }
288
+ else {
289
+ throw new Error(`❌ 未找到任何匹配的场景,且项目中没有可用场景!\n` +
290
+ ` 指定的场景: ${options.selectedScenes.join(', ')}\n` +
291
+ ` 提示: 场景名称区分大小写,请检查拼写是否正确`);
292
+ }
293
+ }
294
+ else {
295
+ // 检查是否有指定的场景未找到(部分匹配)
296
+ const foundSceneNames = new Set(selectedScenes.map(s => s.name));
297
+ const foundSceneIds = new Set(selectedScenes.map(s => String(s.id || s.scene)));
298
+ const notFoundScenes = options.selectedScenes.filter(selected => !foundSceneNames.has(selected) && !foundSceneIds.has(selected));
299
+ if (notFoundScenes.length > 0) {
300
+ const availableScenes = allScenes.map(s => s.name || s.id).join(', ');
301
+ console.warn(`\n⚠️ 警告: 以下场景未找到,将忽略:\n` +
302
+ ` ${notFoundScenes.join(', ')}\n` +
303
+ ` 可用的场景: ${availableScenes}`);
304
+ }
305
+ }
274
306
  console.log(`\n🎬 场景过滤:`);
275
307
  console.log(` - 总场景数: ${allScenes.length}`);
276
308
  console.log(` - 选中场景: ${selectedScenes.length}`);
277
309
  selectedScenes.forEach(scene => {
278
- console.log(` • ${scene.name || scene.id}`);
310
+ console.log(` • ${scene.name || scene.id}${scene.isMain ? ' (主场景)' : ''}`);
279
311
  });
280
312
  }
281
313
  config.scenes = selectedScenes.map((scene) => ({
@@ -368,15 +400,47 @@ export async function generateConfig(projectConfig, options) {
368
400
  let selectedScenes = allScenes;
369
401
  if (options?.selectedScenes && options.selectedScenes.length > 0 && allScenes.length > 0) {
370
402
  selectedScenes = allScenes.filter(scene => {
371
- const sceneId = String(scene.id || scene.name);
403
+ // PlayCraft 格式: 场景对象只有 id name 字段
404
+ // 注意: scene.scene 不存在,但这里的回退是安全的(会使用 scene.id)
405
+ const sceneId = String(scene.id || scene.scene);
372
406
  const sceneName = scene.name || '';
373
407
  return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
374
408
  });
409
+ // 验证:检查是否所有指定的场景都存在
410
+ if (selectedScenes.length === 0) {
411
+ const availableScenes = allScenes.map(s => s.name || s.id).join(', ');
412
+ // 尝试找到默认场景(主场景或第一个场景)
413
+ const defaultScene = allScenes.find(s => s.isMain === true) || allScenes[0];
414
+ if (defaultScene && allScenes.length > 0) {
415
+ console.warn(`\n⚠️ 警告: 未找到指定的场景,使用默认场景\n` +
416
+ ` 指定的场景: ${options.selectedScenes.join(', ')}\n` +
417
+ ` 可用的场景: ${availableScenes}\n` +
418
+ ` 默认场景: ${defaultScene.name || defaultScene.id}${defaultScene.isMain ? ' (主场景)' : ''}`);
419
+ selectedScenes = [defaultScene];
420
+ }
421
+ else {
422
+ throw new Error(`❌ 未找到任何匹配的场景,且项目中没有可用场景!\n` +
423
+ ` 指定的场景: ${options.selectedScenes.join(', ')}\n` +
424
+ ` 提示: 场景名称区分大小写,请检查拼写是否正确`);
425
+ }
426
+ }
427
+ else {
428
+ // 检查是否有指定的场景未找到(部分匹配)
429
+ const foundSceneNames = new Set(selectedScenes.map(s => s.name));
430
+ const foundSceneIds = new Set(selectedScenes.map(s => String(s.id || s.scene)));
431
+ const notFoundScenes = options.selectedScenes.filter(selected => !foundSceneNames.has(selected) && !foundSceneIds.has(selected));
432
+ if (notFoundScenes.length > 0) {
433
+ const availableScenes = allScenes.map(s => s.name || s.id).join(', ');
434
+ console.warn(`\n⚠️ 警告: 以下场景未找到,将忽略:\n` +
435
+ ` ${notFoundScenes.join(', ')}\n` +
436
+ ` 可用的场景: ${availableScenes}`);
437
+ }
438
+ }
375
439
  console.log(`\n🎬 场景过滤:`);
376
440
  console.log(` - 总场景数: ${allScenes.length}`);
377
441
  console.log(` - 选中场景: ${selectedScenes.length}`);
378
442
  selectedScenes.forEach(scene => {
379
- console.log(` • ${scene.name || scene.id}`);
443
+ console.log(` • ${scene.name || scene.id}${scene.isMain ? ' (主场景)' : ''}`);
380
444
  });
381
445
  }
382
446
  if (selectedScenes.length > 0) {
@@ -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;