@playcraft/build 0.0.9 → 0.0.10

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.
Files changed (73) hide show
  1. package/README.md +122 -6
  2. package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +1 -0
  3. package/dist/analyzers/__tests__/optimization-analyzer.test.js +169 -0
  4. package/dist/analyzers/build-analyzer.d.ts +98 -0
  5. package/dist/analyzers/build-analyzer.js +1160 -0
  6. package/dist/analyzers/enhanced-report-template.d.ts +13 -0
  7. package/dist/analyzers/enhanced-report-template.js +957 -0
  8. package/dist/analyzers/index.d.ts +6 -0
  9. package/dist/analyzers/index.js +9 -0
  10. package/dist/analyzers/optimization-analyzer.d.ts +88 -0
  11. package/dist/analyzers/optimization-analyzer.js +278 -0
  12. package/dist/analyzers/playable-analyzer.d.ts +91 -0
  13. package/dist/analyzers/playable-analyzer.js +977 -0
  14. package/dist/analyzers/report-template.d.ts +50 -0
  15. package/dist/analyzers/report-template.js +591 -0
  16. package/dist/analyzers/scene-asset-collector.js +8 -0
  17. package/dist/base-builder.d.ts +9 -0
  18. package/dist/base-builder.js +156 -2
  19. package/dist/build-state-manager.d.ts +110 -0
  20. package/dist/build-state-manager.js +169 -0
  21. package/dist/generators/config-generator.d.ts +2 -0
  22. package/dist/generators/config-generator.js +179 -10
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.js +6 -0
  25. package/dist/loaders/playcanvas-loader.d.ts +7 -0
  26. package/dist/loaders/playcanvas-loader.js +17 -0
  27. package/dist/platforms/adikteev.js +4 -2
  28. package/dist/platforms/applovin.js +9 -3
  29. package/dist/platforms/inmobi.js +4 -2
  30. package/dist/platforms/ironsource.js +4 -1
  31. package/dist/platforms/liftoff.js +8 -3
  32. package/dist/platforms/snapchat.js +8 -2
  33. package/dist/platforms/unity.js +8 -2
  34. package/dist/playable-builder.js +3 -1
  35. package/dist/state/build-state-manager.d.ts +174 -0
  36. package/dist/state/build-state-manager.js +235 -0
  37. package/dist/state/index.d.ts +4 -0
  38. package/dist/state/index.js +2 -0
  39. package/dist/state/state-to-report-converter.d.ts +141 -0
  40. package/dist/state/state-to-report-converter.js +177 -0
  41. package/dist/types.d.ts +1 -0
  42. package/dist/utils.d.ts +4 -0
  43. package/dist/utils.js +11 -0
  44. package/dist/vite/config-builder.js +11 -1
  45. package/dist/vite/platform-configs.d.ts +1 -0
  46. package/dist/vite/platform-configs.js +1 -0
  47. package/dist/vite/plugin-build-state.d.ts +13 -0
  48. package/dist/vite/plugin-build-state.js +147 -0
  49. package/dist/vite/plugin-esm-html-generator.js +11 -2
  50. package/dist/vite/plugin-platform.js +3 -1
  51. package/dist/vite/plugin-playcanvas.d.ts +1 -0
  52. package/dist/vite/plugin-playcanvas.js +160 -20
  53. package/dist/vite/plugin-source-builder.js +1 -0
  54. package/dist/vite/plugin-template-minifier.d.ts +20 -0
  55. package/dist/vite/plugin-template-minifier.js +392 -0
  56. package/package.json +12 -12
  57. package/templates/patches/one-page-mraid-resize-canvas.js +18 -4
  58. package/dist/templates/__loading__.js +0 -100
  59. package/dist/templates/__modules__.js +0 -47
  60. package/dist/templates/__settings__.template.js +0 -20
  61. package/dist/templates/__start__.js +0 -332
  62. package/dist/templates/index.html +0 -18
  63. package/dist/templates/logo.png +0 -0
  64. package/dist/templates/manifest.json +0 -1
  65. package/dist/templates/patches/cannon.min.js +0 -28
  66. package/dist/templates/patches/lz4.js +0 -10
  67. package/dist/templates/patches/one-page-http-get.js +0 -20
  68. package/dist/templates/patches/one-page-inline-game-scripts.js +0 -52
  69. package/dist/templates/patches/one-page-mraid-resize-canvas.js +0 -46
  70. package/dist/templates/patches/p2.min.js +0 -27
  71. package/dist/templates/patches/playcraft-no-xhr.js +0 -76
  72. package/dist/templates/playcanvas-stable.min.js +0 -16363
  73. package/dist/templates/styles.css +0 -43
@@ -0,0 +1,147 @@
1
+ import path from 'path';
2
+ import { BuildStateManager } from '../state/index.js';
3
+ /**
4
+ * Vite 插件:记录 Playable Build 状态
5
+ * 在 Vite 打包过程中记录资源的处理信息
6
+ */
7
+ export function viteBuildStatePlugin(options) {
8
+ let stateManager;
9
+ const transformedModules = new Map();
10
+ return {
11
+ name: 'vite-plugin-build-state',
12
+ enforce: 'post', // 在其他插件之后执行,以便记录最终的处理结果
13
+ async configResolved(config) {
14
+ console.log('[BuildState] 初始化状态管理器...');
15
+ // 创建状态管理器
16
+ stateManager = new BuildStateManager(options.baseBuildDir, options.outputDir);
17
+ // 尝试加载 Base Build 的状态
18
+ const loaded = await stateManager.loadState();
19
+ if (loaded) {
20
+ console.log('[BuildState] 已加载 Base Build 状态');
21
+ }
22
+ else {
23
+ console.log('[BuildState] 未找到 Base Build 状态,将创建新状态');
24
+ }
25
+ // 开始 Playable 构建阶段
26
+ stateManager.startStage('playable', {
27
+ platform: options.platform,
28
+ mode: config.mode,
29
+ });
30
+ },
31
+ async transform(code, id) {
32
+ // 记录模块转换信息
33
+ if (!id.includes('node_modules') && !id.startsWith('\0')) {
34
+ const originalSize = code.length;
35
+ // 这里我们只记录原始大小,转换后的大小将在后续的 hook 中更新
36
+ transformedModules.set(id, {
37
+ originalSize,
38
+ transformedSize: originalSize, // 初始值
39
+ });
40
+ }
41
+ return null; // 不修改代码
42
+ },
43
+ async generateBundle(options, bundle) {
44
+ console.log('[BuildState] 记录打包产物...');
45
+ // 遍历所有生成的文件
46
+ for (const [fileName, output] of Object.entries(bundle)) {
47
+ if (output.type === 'asset') {
48
+ // 资产文件(图片、字体等)
49
+ const assetId = `playable-asset-${fileName}`;
50
+ const assetSize = typeof output.source === 'string'
51
+ ? Buffer.byteLength(output.source, 'utf-8')
52
+ : output.source.length;
53
+ // 尝试从 Base Build 状态中找到原始资产
54
+ const existingAsset = stateManager.getAsset(assetId);
55
+ const originalSize = existingAsset?.originalSize || assetSize;
56
+ // 判断资产类型
57
+ const ext = path.extname(fileName).toLowerCase();
58
+ let assetType = 'other';
59
+ if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext)) {
60
+ assetType = 'texture';
61
+ }
62
+ else if (['.mp3', '.wav', '.ogg'].includes(ext)) {
63
+ assetType = 'audio';
64
+ }
65
+ else if (['.glb', '.gltf', '.obj', '.fbx'].includes(ext)) {
66
+ assetType = 'model';
67
+ }
68
+ else if (ext === '.css') {
69
+ assetType = 'css';
70
+ }
71
+ else if (['.woff', '.woff2', '.ttf', '.otf'].includes(ext)) {
72
+ assetType = 'font';
73
+ }
74
+ // 如果资产不存在,先记录
75
+ if (!existingAsset) {
76
+ stateManager.recordAsset(assetId, fileName, fileName, originalSize, assetType);
77
+ }
78
+ // 更新 Playable 阶段的处理信息
79
+ const optimizations = [];
80
+ // 检测图片压缩
81
+ if (assetType === 'texture' && assetSize < originalSize * 0.9) {
82
+ optimizations.push('image-compress');
83
+ }
84
+ // 检测格式转换(如 PNG -> WebP)
85
+ if (fileName.includes('.webp') && existingAsset?.originalName.includes('.png')) {
86
+ optimizations.push('format-convert');
87
+ }
88
+ stateManager.updateAsset(assetId, 'playable', fileName, fileName, assetSize, {
89
+ optimizations,
90
+ inlined: false,
91
+ });
92
+ }
93
+ else if (output.type === 'chunk') {
94
+ // JavaScript chunk
95
+ const chunkId = `playable-chunk-${fileName}`;
96
+ const chunkSize = Buffer.byteLength(output.code, 'utf-8');
97
+ // 计算原始大小(所有模块的原始大小之和)
98
+ let originalSize = 0;
99
+ for (const moduleId of Object.keys(output.modules)) {
100
+ const moduleInfo = transformedModules.get(moduleId);
101
+ if (moduleInfo) {
102
+ originalSize += moduleInfo.originalSize;
103
+ }
104
+ }
105
+ if (originalSize === 0) {
106
+ originalSize = chunkSize; // 回退值
107
+ }
108
+ // 记录 chunk
109
+ stateManager.recordAsset(chunkId, fileName, fileName, originalSize, 'script');
110
+ const optimizations = [];
111
+ // 检测压缩
112
+ if (output.code.length < originalSize * 0.8) {
113
+ optimizations.push('minify');
114
+ }
115
+ // 检测 tree-shaking(通过模块数量判断)
116
+ if (Object.keys(output.modules).length < Object.keys(output.modules).length * 0.9) {
117
+ optimizations.push('tree-shake');
118
+ }
119
+ // 检测代码分割
120
+ if (fileName.includes('chunk') || fileName.includes('-')) {
121
+ optimizations.push('code-split');
122
+ }
123
+ stateManager.updateAsset(chunkId, 'playable', fileName, fileName, chunkSize, {
124
+ optimizations,
125
+ inlined: false,
126
+ });
127
+ }
128
+ }
129
+ },
130
+ async closeBundle() {
131
+ // 结束 Playable 构建阶段
132
+ stateManager.endStage();
133
+ // 只在分析模式下保存状态文件(分析报告需要用到)
134
+ if (options.analyze) {
135
+ console.log('[BuildState] 保存构建状态...');
136
+ await stateManager.saveState();
137
+ }
138
+ // 输出统计信息
139
+ const stats = stateManager.getStatistics();
140
+ console.log(`[BuildState] 构建统计:`);
141
+ console.log(` - 总资产数: ${stats.totalAssets}`);
142
+ console.log(` - 原始大小: ${(stats.totalOriginalSize / 1024).toFixed(2)} KB`);
143
+ console.log(` - 最终大小: ${(stats.totalFinalSize / 1024).toFixed(2)} KB`);
144
+ console.log(` - 总压缩率: ${(stats.totalCompressionRatio * 100).toFixed(1)}%`);
145
+ },
146
+ };
147
+ }
@@ -724,8 +724,17 @@ ${deferWrapper}`);
724
724
  fileName: 'esm-bundle',
725
725
  formats: ['iife'],
726
726
  },
727
- // 暂时禁用压缩来调试语法错误
728
- minify: false,
727
+ // 使用 terser 压缩 JS
728
+ minify: options.minify ? 'terser' : false,
729
+ terserOptions: options.minify ? {
730
+ compress: {
731
+ drop_console: true,
732
+ drop_debugger: true,
733
+ },
734
+ format: {
735
+ comments: false, // 移除所有注释
736
+ },
737
+ } : undefined,
729
738
  rollupOptions: {
730
739
  // 将 playcanvas 和引擎文件设为外部依赖
731
740
  // 引擎已经在 HTML 中单独内联,不需要再次打包
@@ -196,7 +196,9 @@ async function rewriteConfig(folderPath, assetPrefix) {
196
196
  }
197
197
  }
198
198
  async function applyMraidSupport(outDir) {
199
- await patchConfigFillMode(outDir);
199
+ // Note: We no longer patch fillMode to 'NONE' because it causes canvas scaling issues
200
+ // The MRAID resize patch handles the sizing correctly with any fillMode
201
+ // await patchConfigFillMode(outDir); // Removed: causes canvas scaling issues
200
202
  await patchStylesForMraid(outDir);
201
203
  }
202
204
  async function patchConfigFillMode(outDir) {
@@ -9,6 +9,7 @@ export interface PlayCanvasPluginOptions {
9
9
  patchXhrOut: boolean;
10
10
  inlineGameScripts: boolean;
11
11
  compressEngine: boolean;
12
+ compressConfigJson: boolean;
12
13
  configJsonInline: boolean;
13
14
  mraidSupport: boolean;
14
15
  ammoReplacement?: 'p2' | 'cannon';
@@ -73,6 +73,9 @@ export function vitePlayCanvasPlugin(options) {
73
73
  }
74
74
  // 对于 Classic 模式或 ESM 原生模式,保持原有的多文件结构
75
75
  }
76
+ // 注意:HTML 压缩不能在这里执行,因为 Vite 还需要解析 HTML
77
+ // HTML 模板已经在 templates/ 中被压缩为单行
78
+ // 最终的 HTML 压缩由 vite-plugin-singlefile 在最后阶段处理
76
79
  return html;
77
80
  },
78
81
  },
@@ -107,7 +110,7 @@ async function inlineEngineScript(html, baseBuildDir, options) {
107
110
  const enginePath = path.join(baseBuildDir, engineName);
108
111
  try {
109
112
  await fs.access(enginePath);
110
- const engineCode = await fs.readFile(enginePath, 'utf-8');
113
+ let engineCode = await fs.readFile(enginePath, 'utf-8');
111
114
  const patchScripts = await getEnginePatchScripts(options);
112
115
  const engineScript = options.compressEngine
113
116
  ? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
@@ -202,6 +205,8 @@ async function generateAndInlineSettings(html, baseBuildDir, options, pluginCont
202
205
  const appProps = configJson.application_properties || {};
203
206
  const scripts = appProps.scripts || [];
204
207
  const preloadModules = extractPreloadModules(configJson);
208
+ // 保留原始的 powerPreference 设置,默认为 high-performance
209
+ const powerPreference = appProps.powerPreference || 'high-performance';
205
210
  const settingsCode = `
206
211
  window.ASSET_PREFIX = "";
207
212
  window.SCRIPT_PREFIX = "";
@@ -211,7 +216,7 @@ window.CONTEXT_OPTIONS = {
211
216
  'alpha': ${appProps.transparentCanvas === true},
212
217
  'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
213
218
  'deviceTypes': ['webgl2', 'webgl1'],
214
- 'powerPreference': "default"
219
+ 'powerPreference': "${powerPreference}"
215
220
  };
216
221
  window.SCRIPTS = [${scripts.join(', ')}];
217
222
  window.CONFIG_FILENAME = ${configValue};
@@ -260,8 +265,24 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
260
265
  if (options.mraidSupport) {
261
266
  configJson = applyMraidConfig(configJson);
262
267
  }
263
- // 5. 生成 config data URL
264
- const configDataUrl = `data:application/json;base64,${Buffer.from(JSON.stringify(configJson)).toString('base64')}`;
268
+ // 5. 生成 config.json data URL
269
+ // 如果启用 compressConfigJson,使用 LZ4 压缩后再 base64 编码
270
+ const configJsonString = JSON.stringify(configJson);
271
+ let configDataUrl;
272
+ if (options.compressConfigJson) {
273
+ const lz4 = await loadLz4Module();
274
+ const compressed = lz4.compress(Buffer.from(configJsonString));
275
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
276
+ // 使用 lz4-json 前缀标识压缩格式,运行时需要解压
277
+ configDataUrl = `data:application/x-lz4-json;base64,${compressedBase64}`;
278
+ const originalSize = Buffer.from(configJsonString).length;
279
+ const compressedSize = compressed.length;
280
+ const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
281
+ console.log(`[PlayCanvasPlugin] ESM Bundle: config.json 已压缩 (${formatBytes(originalSize)} → ${formatBytes(compressedSize)}, 减少 ${ratio}%)`);
282
+ }
283
+ else {
284
+ configDataUrl = `data:application/json;base64,${Buffer.from(configJsonString).toString('base64')}`;
285
+ }
265
286
  // 6. 处理场景文件 - 获取第一个场景的 data URL
266
287
  let sceneDataUrl = '';
267
288
  if (configJson.scenes && configJson.scenes.length > 0) {
@@ -318,20 +339,21 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
318
339
  }
319
340
  }
320
341
  // 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
321
- // CONFIG_FILENAME = "config.json" CONFIG_FILENAME = "data:application/json;base64,..."
322
- // 注意:替换字符串中的 $ 是特殊字符,需要转义
323
- const escapedConfigDataUrl = configDataUrl.replace(/\$/g, '$$$$');
324
- // 替换 CONFIG_FILENAME(支持多种格式)
325
- // 格式1: const CONFIG_FILENAME = "config.json"
326
- // 格式2: CONFIG_FILENAME="config.json"
327
- html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME = "${escapedConfigDataUrl}"`);
328
- // ⚠️ 重要:Vite 压缩后,变量名会被混淆(如 CONFIG_FILENAME vh)
329
- // 需要直接替换 .configure("config.json" 调用中的字符串字面量
330
- // 格式: .configure("config.json", ...) 或 .configure('config.json', ...)
342
+ // 方案1+3:直接将 CONFIG_FILENAME = "config.json" 替换为 data URL
343
+ // 8.1 替换 CONFIG_FILENAME
344
+ // 格式1: CONFIG_FILENAME = "config.json"
345
+ // 格式2: CONFIG_FILENAME="config.json" (压缩后)
346
+ const configReplaceCount = (html.match(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g) || []).length;
347
+ if (configReplaceCount > 0) {
348
+ html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME="${configDataUrl}"`);
349
+ console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configReplaceCount} CONFIG_FILENAME`);
350
+ }
351
+ // 8.2 替换 configure("config.json") 调用
352
+ // Vite 压缩后可能是: .configure("config.json",
331
353
  const configureReplaceCount = (html.match(/\.configure\s*\(\s*["']config\.json["']/g) || []).length;
332
354
  if (configureReplaceCount > 0) {
333
- html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${escapedConfigDataUrl}"`);
334
- console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json") 调用`);
355
+ html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${configDataUrl}"`);
356
+ console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")`);
335
357
  }
336
358
  // 9. 替换 SCENE_PATH
337
359
  if (sceneDataUrl) {
@@ -780,6 +802,33 @@ async function inlineLoadingScript(html, baseBuildDir) {
780
802
  }
781
803
  return html;
782
804
  }
805
+ /**
806
+ * 压缩 CSS 代码
807
+ * 移除注释、多余空白、合并为单行
808
+ */
809
+ function minifyCSS(css) {
810
+ return css
811
+ // 移除多行注释 /* ... */
812
+ .replace(/\/\*[\s\S]*?\*\//g, '')
813
+ // 移除单行注释 // ... (CSS 不标准但有些预处理器会保留)
814
+ .replace(/\/\/.*$/gm, '')
815
+ // 将多个空白字符(空格、换行、制表符)替换为单个空格
816
+ .replace(/\s+/g, ' ')
817
+ // 移除 { 前的空格
818
+ .replace(/\s*\{\s*/g, '{')
819
+ // 移除 } 后的空格
820
+ .replace(/\s*\}\s*/g, '}')
821
+ // 移除 : 前后的空格
822
+ .replace(/\s*:\s*/g, ':')
823
+ // 移除 ; 前后的空格
824
+ .replace(/\s*;\s*/g, ';')
825
+ // 移除 , 后的空格
826
+ .replace(/,\s*/g, ',')
827
+ // 移除最后一个 ; 在 } 前
828
+ .replace(/;}/g, '}')
829
+ // 去除首尾空白
830
+ .trim();
831
+ }
783
832
  /**
784
833
  * 内联 CSS 文件
785
834
  */
@@ -799,6 +848,8 @@ async function inlineCSS(html, baseBuildDir, options) {
799
848
  if (options.mraidSupport && !cssContent.includes('fill-mode-NONE')) {
800
849
  cssContent += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
801
850
  }
851
+ // 压缩 CSS(移除注释、多余空白、合并为单行)
852
+ cssContent = minifyCSS(cssContent);
802
853
  // 替换 link 标签为 style 标签
803
854
  html = html.replace(match[0], `<style>${cssContent}</style>`);
804
855
  }
@@ -1004,10 +1055,14 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
1004
1055
  }
1005
1056
  }
1006
1057
  // ✅ 核心优化:如果是图片,使用 sharp 压缩
1058
+ // ⚠️ 重要:字体纹理(type === 'font')不能转换为 WebP!
1059
+ // MSDF/位图字体的 PNG 纹理包含精确的距离场/像素数据,
1060
+ // WebP 有损压缩会破坏这些数据导致字体渲染异常
1007
1061
  else if (isImageFile(cleanUrl)) {
1062
+ const isFontTexture = asset.type === 'font';
1008
1063
  const compressed = await compressImage(buffer, cleanUrl, {
1009
- convertToWebP: true,
1010
- quality: 75,
1064
+ convertToWebP: !isFontTexture, // 字体纹理不转换为 WebP
1065
+ quality: isFontTexture ? 100 : 75, // 字体纹理使用无损压缩
1011
1066
  });
1012
1067
  // 防御性检查:如果压缩后的 buffer 为空,使用原始 buffer
1013
1068
  if (!compressed.buffer || compressed.buffer.length === 0) {
@@ -1196,10 +1251,15 @@ async function compressImage(buffer, filePath, options) {
1196
1251
  };
1197
1252
  }
1198
1253
  // 按原格式压缩
1254
+ const quality = options?.quality ?? 80;
1199
1255
  switch (ext) {
1200
1256
  case '.png':
1257
+ // 如果 quality >= 100,使用无损压缩(适用于字体纹理)
1201
1258
  const pngBuffer = await image
1202
- .png({ quality: 80, compressionLevel: 9 })
1259
+ .png({
1260
+ compressionLevel: quality >= 100 ? 6 : 9, // 无损时使用中等压缩
1261
+ palette: quality < 100, // 无损时不使用调色板
1262
+ })
1203
1263
  .toBuffer();
1204
1264
  // 防御性检查
1205
1265
  if (!pngBuffer || pngBuffer.length === 0) {
@@ -1295,7 +1355,9 @@ function buildConfigValue(configJson) {
1295
1355
  function applyMraidConfig(configJson) {
1296
1356
  const next = { ...configJson };
1297
1357
  const props = { ...(next.application_properties || {}) };
1298
- props.fillMode = 'NONE';
1358
+ // Keep original fillMode (usually KEEP_ASPECT) for correct canvas scaling
1359
+ // The MRAID resize patch will handle the sizing
1360
+ // props.fillMode = 'NONE'; // Removed: causes canvas scaling issues
1299
1361
  next.application_properties = props;
1300
1362
  return next;
1301
1363
  }
@@ -1339,6 +1401,18 @@ async function getEnginePatchScripts(options) {
1339
1401
  const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
1340
1402
  scripts.push(`<script>${patchCode}</script>`);
1341
1403
  }
1404
+ // 如果启用了 config.json 压缩,需要注入 LZ4 解压运行时代码
1405
+ // 注意:必须在 compressEngine 之前检查,因为 compressEngine 也会注入 lz4.js
1406
+ if (options.compressConfigJson && !options.compressEngine) {
1407
+ // 如果没有启用引擎压缩,需要单独注入 lz4.js
1408
+ const lz4Script = await getLz4InlineScript();
1409
+ scripts.push(lz4Script);
1410
+ }
1411
+ // 注入 config.json 解压 patch(拦截 fetch 请求并解压 LZ4 数据)
1412
+ if (options.compressConfigJson) {
1413
+ const configDecompressPatch = buildConfigJsonDecompressPatch();
1414
+ scripts.push(`<script>${configDecompressPatch}</script>`);
1415
+ }
1342
1416
  if (options.compressEngine) {
1343
1417
  // Focus patch: 确保 canvas 获得焦点以接收键盘事件
1344
1418
  const focusPatch = `!function(){var e=function(){var e=document.getElementById("application-canvas");if(!e)return!1;try{e.focus()}catch(t){}e.addEventListener("pointerdown",function(){e.focus()}),e.addEventListener("click",function(){e.focus()});return!0},t=0;if(!e()){var n=setInterval(function(){(e()||++t>50)&&clearInterval(n)},100)}}();`;
@@ -1379,3 +1453,69 @@ async function readPatchFile(name) {
1379
1453
  const patchPath = path.resolve(currentDir, '../../templates/patches', name);
1380
1454
  return await fs.readFile(patchPath, 'utf-8');
1381
1455
  }
1456
+ /**
1457
+ * 格式化字节数为人类可读格式
1458
+ */
1459
+ function formatBytes(bytes) {
1460
+ if (bytes === 0)
1461
+ return '0 B';
1462
+ const k = 1024;
1463
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1464
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1465
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
1466
+ }
1467
+ /**
1468
+ * 构建 config.json 解压 patch
1469
+ *
1470
+ * 这个 patch 拦截 PlayCanvas 的 pc.Http.prototype.get 请求,
1471
+ * 检测是否为 LZ4 压缩的 config.json,如果是则解压后返回。
1472
+ *
1473
+ * 工作原理:
1474
+ * 1. 保存原始 pc.Http.prototype.get 函数
1475
+ * 2. 重写 get,检测 URL 是否为 LZ4 压缩的 JSON data URL
1476
+ * 3. 如果是,解压并通过 callback 返回解析后的 JSON 对象
1477
+ *
1478
+ * 注意:
1479
+ * - PlayCanvas 使用 pc.Http.prototype.get 加载 config.json,不是 fetch
1480
+ * - 使用 lz4.js 提供的 Buffer polyfill 进行解压
1481
+ * - 必须在 pc 对象可用后才能生效,所以使用轮询等待
1482
+ */
1483
+ function buildConfigJsonDecompressPatch() {
1484
+ // 原始代码:
1485
+ // (function() {
1486
+ // function patch() {
1487
+ // if (!window.pc || !window.pc.Http) return false;
1488
+ // var oldGet = pc.Http.prototype.get;
1489
+ // pc.Http.prototype.get = function(url, options, callback) {
1490
+ // if (typeof options === 'function') {
1491
+ // callback = options;
1492
+ // options = {};
1493
+ // }
1494
+ // if (typeof url === 'string' && url.startsWith('data:application/x-lz4-json;base64,')) {
1495
+ // try {
1496
+ // var base64 = url.replace('data:application/x-lz4-json;base64,', '');
1497
+ // var compressed = new Buffer(base64, 'base64');
1498
+ // var decompressed = lz4.decompress(compressed);
1499
+ // var str = Buffer.from(decompressed).toString('utf8');
1500
+ // var json = JSON.parse(str);
1501
+ // console.log('[LZ4 Config] Decompressed config.json successfully, size:', str.length);
1502
+ // callback(null, json);
1503
+ // } catch(e) {
1504
+ // console.error('[LZ4 Config] Decompression error:', e);
1505
+ // callback(e);
1506
+ // }
1507
+ // return;
1508
+ // }
1509
+ // oldGet.call(this, url, options, callback);
1510
+ // };
1511
+ // return true;
1512
+ // }
1513
+ // if (!patch()) {
1514
+ // var c = 0;
1515
+ // var i = setInterval(function() {
1516
+ // if (patch() || ++c > 100) clearInterval(i);
1517
+ // }, 10);
1518
+ // }
1519
+ // })();
1520
+ 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-lz4-json;base64,")){try{var r=e.replace("data:application/x-lz4-json;base64,",""),a=new Buffer(r,"base64"),d=lz4.decompress(a),s=Buffer.from(d).toString("utf8"),j=JSON.parse(s);console.log("[LZ4 Config] Decompressed config.json successfully, size:",s.length);n(null,j)}catch(x){console.error("[LZ4 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)}}();`;
1521
+ }
@@ -94,6 +94,7 @@ export function viteSourceBuilderPlugin(options) {
94
94
  // 生成 config.json(支持场景过滤)
95
95
  const config = await generateConfig(projectConfig, {
96
96
  selectedScenes: options.selectedScenes,
97
+ projectDir: options.projectDir,
97
98
  });
98
99
  // 为脚本资产补齐 __game-scripts.js 信息(仅在 Classic 模式下)
99
100
  if (options.buildMode !== 'esm') {
@@ -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;