@playcraft/build 0.0.11 → 0.0.14

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 (102) hide show
  1. package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +1 -0
  2. package/dist/analyzers/__tests__/optimization-analyzer.test.js +169 -0
  3. package/dist/analyzers/playable-analyzer.js +3 -2
  4. package/dist/analyzers/scene-asset-collector.js +99 -9
  5. package/dist/base-builder.d.ts +15 -78
  6. package/dist/base-builder.js +34 -735
  7. package/dist/engines/engine-detector.d.ts +38 -0
  8. package/dist/engines/engine-detector.js +201 -0
  9. package/dist/engines/generic-adapter.d.ts +71 -0
  10. package/dist/engines/generic-adapter.js +378 -0
  11. package/dist/engines/index.d.ts +7 -0
  12. package/dist/engines/index.js +7 -0
  13. package/dist/engines/playcanvas-adapter.d.ts +85 -0
  14. package/dist/engines/playcanvas-adapter.js +813 -0
  15. package/dist/generators/config-generator.js +59 -1
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/loaders/playcraft-loader.js +240 -5
  19. package/dist/platforms/adikteev.d.ts +1 -1
  20. package/dist/platforms/adikteev.js +30 -34
  21. package/dist/platforms/applovin.d.ts +1 -1
  22. package/dist/platforms/applovin.js +34 -33
  23. package/dist/platforms/base.d.ts +27 -5
  24. package/dist/platforms/base.js +79 -181
  25. package/dist/platforms/bigo.d.ts +1 -1
  26. package/dist/platforms/bigo.js +28 -28
  27. package/dist/platforms/facebook.d.ts +1 -1
  28. package/dist/platforms/facebook.js +21 -10
  29. package/dist/platforms/google.d.ts +1 -1
  30. package/dist/platforms/google.js +28 -21
  31. package/dist/platforms/index.d.ts +1 -0
  32. package/dist/platforms/index.js +4 -0
  33. package/dist/platforms/inmobi.d.ts +1 -1
  34. package/dist/platforms/inmobi.js +27 -32
  35. package/dist/platforms/ironsource.d.ts +1 -1
  36. package/dist/platforms/ironsource.js +37 -37
  37. package/dist/platforms/liftoff.d.ts +1 -1
  38. package/dist/platforms/liftoff.js +24 -27
  39. package/dist/platforms/mintegral.d.ts +10 -0
  40. package/dist/platforms/mintegral.js +65 -0
  41. package/dist/platforms/moloco.d.ts +1 -1
  42. package/dist/platforms/moloco.js +18 -20
  43. package/dist/platforms/playcraft.d.ts +1 -1
  44. package/dist/platforms/playcraft.js +2 -2
  45. package/dist/platforms/remerge.d.ts +1 -1
  46. package/dist/platforms/remerge.js +19 -20
  47. package/dist/platforms/snapchat.d.ts +1 -1
  48. package/dist/platforms/snapchat.js +35 -23
  49. package/dist/platforms/tiktok.d.ts +1 -1
  50. package/dist/platforms/tiktok.js +28 -24
  51. package/dist/platforms/unity.d.ts +1 -1
  52. package/dist/platforms/unity.js +32 -32
  53. package/dist/playable-builder.d.ts +1 -0
  54. package/dist/playable-builder.js +19 -3
  55. package/dist/templates/__loading__.js +100 -0
  56. package/dist/templates/__modules__.js +47 -0
  57. package/dist/templates/__settings__.template.js +20 -0
  58. package/dist/templates/__start__.js +332 -0
  59. package/dist/templates/index.html +18 -0
  60. package/dist/templates/logo.png +0 -0
  61. package/dist/templates/manifest.json +1 -0
  62. package/dist/templates/patches/cannon.min.js +28 -0
  63. package/dist/templates/patches/lz4.js +10 -0
  64. package/dist/templates/patches/one-page-http-get.js +20 -0
  65. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  66. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  67. package/dist/templates/patches/p2.min.js +27 -0
  68. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  69. package/dist/templates/playcanvas-stable.min.js +16363 -0
  70. package/dist/templates/styles.css +43 -0
  71. package/dist/types.d.ts +114 -1
  72. package/dist/types.js +77 -1
  73. package/dist/utils/ammo-detector.d.ts +9 -0
  74. package/dist/utils/ammo-detector.js +76 -0
  75. package/dist/utils/build-mode-detector.js +2 -0
  76. package/dist/utils/minify.d.ts +32 -0
  77. package/dist/utils/minify.js +82 -0
  78. package/dist/vite/config-builder-generic.d.ts +70 -0
  79. package/dist/vite/config-builder-generic.js +251 -0
  80. package/dist/vite/config-builder.d.ts +8 -0
  81. package/dist/vite/config-builder.js +56 -16
  82. package/dist/vite/platform-configs.d.ts +1 -0
  83. package/dist/vite/platform-configs.js +30 -1
  84. package/dist/vite/plugin-build-state.d.ts +2 -0
  85. package/dist/vite/plugin-build-state.js +5 -3
  86. package/dist/vite/plugin-compress-js.d.ts +21 -0
  87. package/dist/vite/plugin-compress-js.js +213 -0
  88. package/dist/vite/plugin-esm-html-generator.js +15 -2
  89. package/dist/vite/plugin-platform.d.ts +5 -0
  90. package/dist/vite/plugin-platform.js +502 -36
  91. package/dist/vite/plugin-playcanvas.d.ts +1 -0
  92. package/dist/vite/plugin-playcanvas.js +181 -88
  93. package/dist/vite/plugin-source-builder.js +102 -21
  94. package/dist/vite-builder.d.ts +25 -7
  95. package/dist/vite-builder.js +141 -52
  96. package/package.json +4 -2
  97. package/physics/cannon-rigidbody-adapter.js +243 -22
  98. package/templates/__loading__.js +0 -12
  99. package/templates/index.esm.mjs +0 -11
  100. package/templates/patches/one-page-mraid-resize-canvas.js +18 -4
  101. package/templates/patches/playcraft-cta-adapter.js +129 -31
  102. package/templates/patches/scene-physics-defaults.js +49 -0
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import archiver from 'archiver';
6
6
  import { createWriteStream } from 'fs';
7
7
  import { PLATFORM_CONFIGS } from './platform-configs.js';
8
+ import { minifyJS, minifyPatchCode } from '../utils/minify.js';
8
9
  /**
9
10
  * 平台 Vite 插件
10
11
  * 注入平台特定代码和处理输出格式
@@ -16,26 +17,45 @@ export function vitePlatformPlugin(options) {
16
17
  transformIndexHtml: {
17
18
  order: 'post',
18
19
  async handler(html) {
19
- // 使用平台适配器修改 HTML
20
- let output = options.adapter.modifyHTML(html, []);
21
- if (options.mraidSupport) {
20
+ // 使用平台适配器修改 HTML(现在是异步的)
21
+ let output = await options.adapter.modifyHTML(html, []);
22
+ // MRAID resize patch 仅对 ZIP 格式在此注入
23
+ // HTML 格式已在 plugin-playcanvas.ts 的 getEnginePatchScripts 中注入(避免重复)
24
+ if (options.mraidSupport && options.outputFormat === 'zip') {
22
25
  output = await injectMraidResizePatch(output);
23
26
  }
24
27
  return output;
25
28
  },
26
29
  },
27
30
  async closeBundle() {
31
+ // 防御性检查:如果 Vite 构建失败,outputDir 可能不存在,跳过后续操作避免二次错误
32
+ const outputDirExists = await fs.access(options.outputDir).then(() => true).catch(() => false);
33
+ if (!outputDirExists) {
34
+ console.warn(`[vite-plugin-platform] 输出目录不存在,跳过 closeBundle: ${options.outputDir}`);
35
+ return;
36
+ }
28
37
  // 如果需要 ZIP 格式,在构建完成后打包
29
38
  const platformConfig = PLATFORM_CONFIGS[options.platform];
30
39
  // 仅多文件输出需要复制基础资源
31
40
  if (options.outputFormat === 'zip') {
32
41
  await copyBaseBuildAssets(options.baseBuildDir, options.outputDir);
42
+ // 处理 __settings__.js 中的 PRELOAD_MODULES(移除 ammo 模块或将 WASM 内联)
43
+ await processPreloadModulesForZip(options.outputDir, options);
33
44
  if (options.mraidSupport) {
34
45
  await applyMraidSupport(options.outputDir);
35
46
  }
36
47
  if (options.externFiles?.enabled) {
37
48
  await applyExternFiles(options.outputDir, options.externFiles);
38
49
  }
50
+ // Google Ads 等渠道只允许特定文件类型(.css/.js/.html/.gif/.png/.jpeg/.svg)
51
+ // 将不受支持的文件(如 .glb/.wasm/.bin/.ttf 等)内联为 base64 到 config.json 中
52
+ if (options.inlineUnsupportedAssets) {
53
+ await inlineUnsupportedAssetsIntoConfig(options.outputDir);
54
+ }
55
+ // 对 ZIP 中的 JS 文件进行 Terser 压缩混淆
56
+ if (options.minifyJSInZip) {
57
+ await minifyJSFilesInDir(options.outputDir);
58
+ }
39
59
  }
40
60
  if (options.outputFormat === 'html') {
41
61
  await keepSingleHtml(options.outputDir, platformConfig.outputFileName);
@@ -122,13 +142,17 @@ async function applyExternFiles(outDir, externFiles) {
122
142
  const destPath = path.join(folderPath, entry.name);
123
143
  await fs.rename(srcPath, destPath);
124
144
  }
125
- await rewriteIndexHtml(outDir, assetPrefix);
126
- await rewriteSettings(folderPath, assetPrefix);
127
- await rewriteConfig(folderPath, assetPrefix);
145
+ // config.json 也移入资产文件夹,最外层只保留 index.html + 资产文件夹
146
+ // CONFIG_FILENAME 需要加上资产文件夹前缀,引擎通过 CONFIG_FILENAME 加载 config
147
+ const configFilename = `${folderName}/config.json`;
148
+ await rewriteIndexHtml(outDir, assetPrefix, configFilename);
149
+ await rewriteSettings(folderPath, assetPrefix, configFilename);
150
+ await rewriteConfig(path.join(folderPath), folderPath, folderName);
128
151
  }
129
- async function rewriteIndexHtml(outDir, assetPrefix) {
152
+ async function rewriteIndexHtml(outDir, assetPrefix, configFilename) {
130
153
  const indexPath = path.join(outDir, 'index.html');
131
154
  let html = await fs.readFile(indexPath, 'utf-8');
155
+ // === 1. 重写 HTML 标签中的外部资源引用 ===
132
156
  html = html.replace(/(<script[^>]*src=["'])([^"']+)(["'][^>]*><\/script>)/gi, (match, start, src, end) => {
133
157
  if (src.startsWith('http') || src.startsWith('data:')) {
134
158
  return match;
@@ -141,22 +165,37 @@ async function rewriteIndexHtml(outDir, assetPrefix) {
141
165
  }
142
166
  return `${start}${assetPrefix}${href}${end}`;
143
167
  });
144
- // 处理内联脚本中的 CONFIG_FILENAME(ESM Bundle 模式)
145
- // 格式: const CONFIG_FILENAME = "config.json"
146
- html = html.replace(/(\bconst\s+CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${assetPrefix}config.json$2`);
147
- // 处理内联脚本中的 ASSET_PREFIX(ESM Bundle 模式)
148
- // 格式: const ASSET_PREFIX = ""
168
+ // === 2. 重写内联脚本中的路径(支持未压缩和压缩/混淆后的格式) ===
169
+ // 2.1 CONFIG_FILENAME / config.json(config 保留在根目录,不加 prefix,避免引擎双重路径)
170
+ html = html.replace(/(\bconst\s+CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
171
+ html = html.replace(/(\.configure\s*\(\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
172
+ // 2.2 ASSET_PREFIX
173
+ // 未压缩格式: const ASSET_PREFIX = ""
149
174
  html = html.replace(/(\bconst\s+ASSET_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
150
- // 处理内联脚本中的 SCRIPT_PREFIX(ESM Bundle 模式)
151
- // 格式: const SCRIPT_PREFIX = ""
175
+ // 压缩后格式: window.ASSET_PREFIX="" window.ASSET_PREFIX = ""
176
+ html = html.replace(/(window\.ASSET_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
177
+ // 压缩后赋值格式: assetPrefix="" 或 .assetPrefix=""
178
+ html = html.replace(/(\.assetPrefix\s*=\s*)(["'])(["'])/g, `$1$2${assetPrefix}$3`);
179
+ // 压缩后: createOptions.assetPrefix=ASSET_PREFIX??"" → 如果 ASSET_PREFIX 被内联为 ""
180
+ // 则变成 createOptions.assetPrefix="" 或 createOptions.assetPrefix=""
181
+ // 2.3 SCRIPT_PREFIX
182
+ // 未压缩格式: const SCRIPT_PREFIX = ""
152
183
  html = html.replace(/(\bconst\s+SCRIPT_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
153
- // 注意:SCENE_PATH 不需要添加 prefix
154
- // SCENE_PATH 是场景的标识符(如 "2412781.json"),而不是完整路径
155
- // PlayCanvas 引擎会用 ASSET_PREFIX + config.json 中的 scenes[].url 来加载场景
156
- // 如果这里也加 prefix,会导致路径重复
184
+ // 压缩后格式: window.SCRIPT_PREFIX=""
185
+ html = html.replace(/(window\.SCRIPT_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
186
+ // 压缩后: .scriptPrefix=""
187
+ html = html.replace(/(\.scriptPrefix\s*=\s*)(["'])(["'])/g, `$1$2${assetPrefix}$3`);
188
+ // 2.4 loadScene 中的场景路径
189
+ // 场景路径不需要加 prefix,因为 PlayCanvas 引擎会用 ASSET_PREFIX + scenes[].url 来加载
190
+ // 但如果 ASSET_PREFIX 已经被正确设置,场景路径会自动正确
191
+ // === 3. 重写 __settings__.js 中的 window 变量(如果存在于内联脚本中) ===
192
+ // Classic 模式下,__settings__.js 的内容可能被内联到 HTML 中
193
+ // 格式可能是 window.ASSET_PREFIX = ""; 或压缩后 window.ASSET_PREFIX="", (逗号分隔)
194
+ // 注意: window.ASSET_PREFIX="" 已在上面第 2.2 节处理过
195
+ html = html.replace(/(window\.CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
157
196
  await fs.writeFile(indexPath, html);
158
197
  }
159
- async function rewriteSettings(folderPath, assetPrefix) {
198
+ async function rewriteSettings(folderPath, assetPrefix, configFilename) {
160
199
  const settingsPath = path.join(folderPath, '__settings__.js');
161
200
  try {
162
201
  let code = await fs.readFile(settingsPath, 'utf-8');
@@ -166,37 +205,211 @@ async function rewriteSettings(folderPath, assetPrefix) {
166
205
  if (value.startsWith('data:') || value.startsWith('http')) {
167
206
  return match;
168
207
  }
169
- return `window.CONFIG_FILENAME = "${assetPrefix}${value}";`;
208
+ return `window.CONFIG_FILENAME = "${configFilename}";`;
170
209
  });
171
- code = code.replace(/window\.SCENE_PATH\s*=\s*"([^"]*)";/, (match, value) => {
172
- if (!value || value.startsWith('data:') || value.startsWith('http')) {
173
- return match;
210
+ // SCENE_PATH 保持为纯文件名(如 226067097.json),引擎会做 ASSET_PREFIX + SCENE_PATH
211
+ // 若在此处加 prefix 会导致 playcraft-assets-xxx/playcraft-assets-xxx/xxx.json 双重路径
212
+ // code = code.replace(...) 保持 SCENE_PATH 原值,不修改
213
+ // 重写 PRELOAD_MODULES 中的文件路径,添加 assetPrefix
214
+ const modulesMatch = code.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
215
+ if (modulesMatch) {
216
+ try {
217
+ const modules = new Function(`return ${modulesMatch[1]}`)();
218
+ for (const module of modules) {
219
+ if (module.glueUrl && !module.glueUrl.startsWith('data:') && !module.glueUrl.startsWith('http')) {
220
+ module.glueUrl = assetPrefix + module.glueUrl;
221
+ }
222
+ if (module.wasmUrl && !module.wasmUrl.startsWith('data:') && !module.wasmUrl.startsWith('http')) {
223
+ module.wasmUrl = assetPrefix + module.wasmUrl;
224
+ }
225
+ if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:') && !module.fallbackUrl.startsWith('http')) {
226
+ module.fallbackUrl = assetPrefix + module.fallbackUrl;
227
+ }
228
+ }
229
+ const newModulesStr = JSON.stringify(modules);
230
+ code = code.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
174
231
  }
175
- return `window.SCENE_PATH = "${assetPrefix}${value}";`;
176
- });
232
+ catch (error) {
233
+ console.warn(`[Platform] rewriteSettings: 无法处理 PRELOAD_MODULES 路径: ${error}`);
234
+ }
235
+ }
177
236
  await fs.writeFile(settingsPath, code);
178
237
  }
179
238
  catch (error) {
180
239
  // 忽略缺失
181
240
  }
182
241
  }
183
- async function rewriteConfig(folderPath, assetPrefix) {
184
- // 注意:config.json 中的 scenes[].url 不需要重写
185
- // 因为 PlayCanvas 引擎会用 ASSET_PREFIX + url 来构造完整路径
186
- // 如果这里也加 prefix,会导致路径重复
187
- //
188
- // 这个函数保留用于未来可能需要的其他 config.json 修改
189
- const configPath = path.join(folderPath, 'config.json');
242
+ async function rewriteConfig(configDir, _folderPath, _folderName) {
243
+ // config.json 已移入资产文件夹,在此处可对其进行后处理
244
+ const configPath = path.join(configDir, 'config.json');
190
245
  try {
191
- // 目前只需要确保 config.json 存在即可
192
246
  await fs.access(configPath);
193
247
  }
194
248
  catch (error) {
195
249
  // 忽略缺失
196
250
  }
197
251
  }
252
+ /**
253
+ * 处理 ZIP 输出中 __settings__.js 的 PRELOAD_MODULES
254
+ *
255
+ * 问题:ZIP 模式下 __settings__.js 中的 PRELOAD_MODULES 引用了 ammo.wasm.js 等文件,
256
+ * 但这些文件可能:
257
+ * 1. 被 ammoReplacement 替换(此时 ammo 文件不再需要)
258
+ * 2. 被 inlineUnsupportedAssets 内联到 config.json 后删除(.wasm 文件)
259
+ * 3. 在 externFiles 模式下路径发生变化
260
+ *
261
+ * 解决方案:
262
+ * - 如果使用了 ammoReplacement,移除 PRELOAD_MODULES 中的 ammo 模块
263
+ * - 如果使用了 inlineUnsupportedAssets,将 WASM 相关 URL 内联为 base64 或使用 JS fallback
264
+ */
265
+ async function processPreloadModulesForZip(outDir, options) {
266
+ // 1. 处理 __settings__.js 中的 PRELOAD_MODULES
267
+ const settingsPath = path.join(outDir, '__settings__.js');
268
+ let settingsCode;
269
+ try {
270
+ settingsCode = await fs.readFile(settingsPath, 'utf-8');
271
+ }
272
+ catch {
273
+ // __settings__.js 不在根目录,可能还没被 externFiles 处理,先跳过
274
+ return;
275
+ }
276
+ const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
277
+ if (!modulesMatch) {
278
+ return;
279
+ }
280
+ try {
281
+ let modules = new Function(`return ${modulesMatch[1]}`)();
282
+ let modified = false;
283
+ if (options.ammoReplacement) {
284
+ // 移除 ammo 相关模块
285
+ const beforeCount = modules.length;
286
+ modules = modules.filter((module) => !isAmmoModule(module));
287
+ const removed = beforeCount - modules.length;
288
+ if (removed > 0) {
289
+ console.log(`[Platform] ZIP: 已移除 ${removed} 个 Ammo 预加载模块(已替换为 ${options.ammoReplacement})`);
290
+ modified = true;
291
+ }
292
+ // 同时从 config.json 中移除 ammo 相关 assets
293
+ await stripAmmoAssetsFromConfig(outDir);
294
+ }
295
+ else if (options.inlineUnsupportedAssets) {
296
+ // 将 WASM 模块的 URL 转为 data URL 或使用 JS fallback
297
+ for (const module of modules) {
298
+ // 优先使用 fallback JS 版本
299
+ if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
300
+ const fallbackPath = path.join(outDir, module.fallbackUrl);
301
+ try {
302
+ const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
303
+ module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
304
+ // 清空 WASM 相关 URL,强制使用 fallback
305
+ module.glueUrl = '';
306
+ module.wasmUrl = '';
307
+ modified = true;
308
+ console.log(`[Platform] ZIP: 已将模块 ${module.moduleName} 的 fallback 内联为 base64`);
309
+ }
310
+ catch {
311
+ console.warn(`[Platform] ZIP: 无法读取 fallback 文件: ${module.fallbackUrl}`);
312
+ }
313
+ }
314
+ // 如果没有 fallback 或 fallback 读取失败,处理 glueUrl 和 wasmUrl
315
+ if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
316
+ const gluePath = path.join(outDir, module.glueUrl);
317
+ try {
318
+ const glueCode = await fs.readFile(gluePath, 'utf-8');
319
+ module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
320
+ modified = true;
321
+ }
322
+ catch {
323
+ console.warn(`[Platform] ZIP: 无法读取 glue 文件: ${module.glueUrl}`);
324
+ }
325
+ }
326
+ if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
327
+ const wasmPath = path.join(outDir, module.wasmUrl);
328
+ try {
329
+ const wasmBinary = await fs.readFile(wasmPath);
330
+ module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
331
+ modified = true;
332
+ }
333
+ catch {
334
+ console.warn(`[Platform] ZIP: 无法读取 WASM 文件: ${module.wasmUrl}`);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ if (modified) {
340
+ // 写回修改后的 PRELOAD_MODULES
341
+ const newModulesStr = JSON.stringify(modules);
342
+ settingsCode = settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
343
+ await fs.writeFile(settingsPath, settingsCode);
344
+ }
345
+ }
346
+ catch (error) {
347
+ console.warn(`[Platform] ZIP: 无法处理 PRELOAD_MODULES: ${error}`);
348
+ }
349
+ }
350
+ /**
351
+ * 从 config.json 中移除 ammo 相关的 wasm/script assets
352
+ */
353
+ async function stripAmmoAssetsFromConfig(outDir) {
354
+ const configPath = await findConfigJson(outDir);
355
+ if (!configPath)
356
+ return;
357
+ try {
358
+ const raw = await fs.readFile(configPath, 'utf-8');
359
+ const configJson = JSON.parse(raw);
360
+ if (!configJson.assets)
361
+ return;
362
+ const assets = configJson.assets;
363
+ const ammoAssetIds = new Set();
364
+ const ammoRelatedIds = new Set();
365
+ // 找出所有 ammo 相关的 wasm assets 及其关联的 glue/fallback script
366
+ for (const [id, asset] of Object.entries(assets)) {
367
+ const a = asset;
368
+ if (a.type === 'wasm' && a.data?.moduleName?.toLowerCase().includes('ammo')) {
369
+ ammoAssetIds.add(id);
370
+ if (a.data.glueScriptId != null)
371
+ ammoRelatedIds.add(String(a.data.glueScriptId));
372
+ if (a.data.fallbackScriptId != null)
373
+ ammoRelatedIds.add(String(a.data.fallbackScriptId));
374
+ }
375
+ }
376
+ // 也检查 file.url 中包含 ammo 的 assets
377
+ for (const [id, asset] of Object.entries(assets)) {
378
+ const a = asset;
379
+ const url = a.file?.url?.toLowerCase() || '';
380
+ const name = (a.name || '').toLowerCase();
381
+ if (url.includes('ammo') || name.includes('ammo')) {
382
+ ammoRelatedIds.add(id);
383
+ }
384
+ }
385
+ const allAmmoIds = new Set([...ammoAssetIds, ...ammoRelatedIds]);
386
+ if (allAmmoIds.size === 0)
387
+ return;
388
+ for (const id of allAmmoIds) {
389
+ delete assets[id];
390
+ }
391
+ configJson.assets = assets;
392
+ await fs.writeFile(configPath, JSON.stringify(configJson));
393
+ console.log(`[Platform] ZIP: 已从 config.json 中移除 ${allAmmoIds.size} 个 Ammo 相关 assets`);
394
+ }
395
+ catch (error) {
396
+ console.warn(`[Platform] ZIP: 无法处理 config.json 中的 Ammo assets: ${error}`);
397
+ }
398
+ }
399
+ function isAmmoModule(module) {
400
+ const moduleName = module.moduleName?.toLowerCase() ?? '';
401
+ const glueUrl = module.glueUrl?.toLowerCase() ?? '';
402
+ const wasmUrl = module.wasmUrl?.toLowerCase() ?? '';
403
+ const fallbackUrl = module.fallbackUrl?.toLowerCase() ?? '';
404
+ return (moduleName.includes('ammo') ||
405
+ glueUrl.includes('ammo') ||
406
+ wasmUrl.includes('ammo') ||
407
+ fallbackUrl.includes('ammo'));
408
+ }
198
409
  async function applyMraidSupport(outDir) {
199
- await patchConfigFillMode(outDir);
410
+ // Note: We no longer patch fillMode to 'NONE' because it causes canvas scaling issues
411
+ // The MRAID resize patch handles the sizing correctly with any fillMode
412
+ // await patchConfigFillMode(outDir); // Removed: causes canvas scaling issues
200
413
  await patchStylesForMraid(outDir);
201
414
  }
202
415
  async function patchConfigFillMode(outDir) {
@@ -229,7 +442,8 @@ async function patchStylesForMraid(outDir) {
229
442
  }
230
443
  async function injectMraidResizePatch(html) {
231
444
  const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
232
- return html.replace('</head>', `<script>${patchCode}</script>\n</head>`);
445
+ const minified = await minifyPatchCode(patchCode, 'mraid-resize-platform');
446
+ return html.replace('</head>', `<script>${minified}</script>\n</head>`);
233
447
  }
234
448
  async function readPatchFile(name) {
235
449
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
@@ -251,7 +465,9 @@ async function copyBaseBuildAssets(baseBuildDir, outDir) {
251
465
  continue;
252
466
  }
253
467
  // 跳过以 __ 开头的临时目录(如 __dist__, __esm-temp__, __esm-work__ 等)
254
- if (entry.name.startsWith('__')) {
468
+ // 注意:不能跳过 __ 开头的文件,因为 Classic 模式下
469
+ // __start__.js, __settings__.js, __modules__.js, __loading__.js, __game-scripts.js 都是必需的
470
+ if (entry.isDirectory() && entry.name.startsWith('__')) {
255
471
  continue;
256
472
  }
257
473
  const srcPath = path.join(baseBuildDir, entry.name);
@@ -278,3 +494,253 @@ async function copyDirectory(src, dest) {
278
494
  }
279
495
  }
280
496
  }
497
+ /**
498
+ * 遍历目录中的所有 .js 文件,使用 Terser 进行压缩混淆
499
+ * 跳过已经压缩过的 .min.js 文件
500
+ */
501
+ async function minifyJSFilesInDir(dir) {
502
+ const jsFiles = await collectJSFiles(dir);
503
+ if (jsFiles.length === 0)
504
+ return;
505
+ console.log(`\n🔒 正在压缩混淆 ${jsFiles.length} 个 JS 文件...`);
506
+ let totalSaved = 0;
507
+ for (const filePath of jsFiles) {
508
+ try {
509
+ const originalCode = await fs.readFile(filePath, 'utf-8');
510
+ const originalSize = Buffer.byteLength(originalCode, 'utf-8');
511
+ const isESM = filePath.endsWith('.mjs');
512
+ const minified = await minifyJS(originalCode, {
513
+ removeComments: true,
514
+ mangle: true,
515
+ compress: true,
516
+ module: isESM,
517
+ });
518
+ const minifiedSize = Buffer.byteLength(minified, 'utf-8');
519
+ const saved = originalSize - minifiedSize;
520
+ totalSaved += saved;
521
+ await fs.writeFile(filePath, minified);
522
+ const relativePath = path.relative(dir, filePath);
523
+ const ratio = originalSize > 0 ? ((saved / originalSize) * 100).toFixed(1) : '0';
524
+ console.log(` ✓ ${relativePath}: ${(originalSize / 1024).toFixed(1)}KB → ${(minifiedSize / 1024).toFixed(1)}KB (-${ratio}%)`);
525
+ }
526
+ catch (error) {
527
+ console.warn(` ⚠ ${path.relative(dir, filePath)}: 压缩失败,保留原文件`);
528
+ }
529
+ }
530
+ console.log(`✅ JS 压缩混淆完成,共节省 ${(totalSaved / 1024).toFixed(1)}KB`);
531
+ }
532
+ // 已经是压缩过的引擎文件,跳过 Terser 处理(节省时间)
533
+ const SKIP_MINIFY_PATTERNS = [
534
+ 'playcanvas-engine-umd.js',
535
+ 'playcanvas-engine-umd.mjs',
536
+ /playcanvas.*\.min\./,
537
+ ];
538
+ function shouldSkipMinify(fileName) {
539
+ return SKIP_MINIFY_PATTERNS.some(p => typeof p === 'string' ? fileName === p : p.test(fileName));
540
+ }
541
+ async function collectJSFiles(dir) {
542
+ const results = [];
543
+ const entries = await fs.readdir(dir, { withFileTypes: true });
544
+ for (const entry of entries) {
545
+ const fullPath = path.join(dir, entry.name);
546
+ if (entry.isDirectory()) {
547
+ const subFiles = await collectJSFiles(fullPath);
548
+ results.push(...subFiles);
549
+ }
550
+ else if ((entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) &&
551
+ !entry.name.endsWith('.min.js') &&
552
+ !entry.name.endsWith('.min.mjs') &&
553
+ !shouldSkipMinify(entry.name)) {
554
+ results.push(fullPath);
555
+ }
556
+ }
557
+ return results;
558
+ }
559
+ // Google Ads ZIP 支持的文件扩展名白名单
560
+ const GOOGLE_SUPPORTED_EXTENSIONS = new Set([
561
+ '.css', '.js', '.html', '.htm',
562
+ '.gif', '.png', '.jpg', '.jpeg', '.svg',
563
+ ]);
564
+ function isGoogleSupportedFile(filePath) {
565
+ const ext = path.extname(filePath).toLowerCase();
566
+ return GOOGLE_SUPPORTED_EXTENSIONS.has(ext);
567
+ }
568
+ function guessMimeTypeForInline(filePath) {
569
+ const ext = path.extname(filePath).toLowerCase();
570
+ const mimeMap = {
571
+ '.glb': 'model/gltf-binary',
572
+ '.gltf': 'model/gltf+json',
573
+ '.bin': 'application/octet-stream',
574
+ '.wasm': 'application/wasm',
575
+ '.ttf': 'font/ttf',
576
+ '.otf': 'font/otf',
577
+ '.woff': 'font/woff',
578
+ '.woff2': 'font/woff2',
579
+ '.mp3': 'audio/mpeg',
580
+ '.wav': 'audio/wav',
581
+ '.ogg': 'audio/ogg',
582
+ '.mp4': 'video/mp4',
583
+ '.webp': 'image/webp',
584
+ '.json': 'application/json',
585
+ '.txt': 'text/plain',
586
+ };
587
+ return mimeMap[ext] || 'application/octet-stream';
588
+ }
589
+ /**
590
+ * 将 ZIP 中不受 Google Ads 支持的文件内联为 base64 data URL 到 config.json 中,
591
+ * 然后从输出目录中删除这些文件。
592
+ *
593
+ * Google Ads HTML5 验证器只允许:.css, .js, .html, .gif, .png, .jpeg, .svg
594
+ * 不支持的类型(如 .glb, .wasm, .bin, .ttf 等)会导致验证失败。
595
+ */
596
+ async function inlineUnsupportedAssetsIntoConfig(outDir) {
597
+ // 1. 找到 config.json(可能在 outDir 或 externFiles 子目录中)
598
+ const configPath = await findConfigJson(outDir);
599
+ if (!configPath) {
600
+ console.warn('[Platform] inlineUnsupportedAssets: 未找到 config.json,跳过');
601
+ return;
602
+ }
603
+ let configJson;
604
+ try {
605
+ const raw = await fs.readFile(configPath, 'utf-8');
606
+ configJson = JSON.parse(raw);
607
+ }
608
+ catch (error) {
609
+ console.warn(`[Platform] inlineUnsupportedAssets: 无法读取 config.json: ${error}`);
610
+ return;
611
+ }
612
+ if (!configJson?.assets) {
613
+ return;
614
+ }
615
+ const configDir = path.dirname(configPath);
616
+ const assets = configJson.assets;
617
+ const inlinedFiles = [];
618
+ const filesToDelete = [];
619
+ for (const [assetId, asset] of Object.entries(assets)) {
620
+ const file = asset?.file;
621
+ if (!file?.url || typeof file.url !== 'string')
622
+ continue;
623
+ const url = file.url;
624
+ if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://'))
625
+ continue;
626
+ const cleanUrl = url.split('?')[0];
627
+ // 检查此文件是否为 Google 不支持的类型
628
+ if (isGoogleSupportedFile(cleanUrl))
629
+ continue;
630
+ // 尝试从输出目录读取文件
631
+ // 资源路径可能相对于 config.json 所在目录,也可能相对于 outDir,或子目录(externFiles 场景)
632
+ let fullPath = path.join(configDir, cleanUrl);
633
+ try {
634
+ await fs.access(fullPath);
635
+ }
636
+ catch {
637
+ fullPath = path.join(outDir, cleanUrl);
638
+ try {
639
+ await fs.access(fullPath);
640
+ }
641
+ catch {
642
+ // externFiles 场景:config 在根目录,资产在 playcraft-assets-xxx/ 内
643
+ let found = false;
644
+ const entries = await fs.readdir(outDir, { withFileTypes: true });
645
+ for (const entry of entries) {
646
+ if (entry.isDirectory()) {
647
+ const candidate = path.join(outDir, entry.name, cleanUrl);
648
+ try {
649
+ await fs.access(candidate);
650
+ fullPath = candidate;
651
+ found = true;
652
+ break;
653
+ }
654
+ catch {
655
+ /* 继续尝试 */
656
+ }
657
+ }
658
+ }
659
+ if (!found) {
660
+ console.warn(`[Platform] inlineUnsupportedAssets: 文件不存在: ${cleanUrl}`);
661
+ continue;
662
+ }
663
+ }
664
+ }
665
+ try {
666
+ const buffer = await fs.readFile(fullPath);
667
+ const mime = guessMimeTypeForInline(cleanUrl);
668
+ const dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
669
+ file.url = dataUrl;
670
+ file.size = buffer.length;
671
+ if ('hash' in file)
672
+ delete file.hash;
673
+ if ('variants' in file)
674
+ delete file.variants;
675
+ inlinedFiles.push(`${asset.name || assetId} (${cleanUrl}, ${(buffer.length / 1024).toFixed(1)}KB)`);
676
+ filesToDelete.push(fullPath);
677
+ }
678
+ catch (error) {
679
+ console.warn(`[Platform] inlineUnsupportedAssets: 无法读取文件 ${cleanUrl}: ${error}`);
680
+ }
681
+ }
682
+ if (inlinedFiles.length === 0) {
683
+ console.log('[Platform] inlineUnsupportedAssets: 所有文件类型均受支持,无需内联');
684
+ return;
685
+ }
686
+ // 2. 写回 config.json
687
+ await fs.writeFile(configPath, JSON.stringify(configJson));
688
+ console.log(`\n✅ 已将 ${inlinedFiles.length} 个不受支持的文件内联为 base64:`);
689
+ inlinedFiles.forEach(info => console.log(` - ${info}`));
690
+ // 3. 删除已内联的原始文件
691
+ for (const filePath of filesToDelete) {
692
+ try {
693
+ await fs.unlink(filePath);
694
+ }
695
+ catch {
696
+ // 忽略删除失败
697
+ }
698
+ }
699
+ // 4. 清理空目录
700
+ await cleanEmptyDirs(outDir);
701
+ }
702
+ async function findConfigJson(outDir) {
703
+ // 直接在 outDir 中查找
704
+ const directPath = path.join(outDir, 'config.json');
705
+ try {
706
+ await fs.access(directPath);
707
+ return directPath;
708
+ }
709
+ catch { }
710
+ // 在子目录中查找(externFiles 场景)
711
+ const entries = await fs.readdir(outDir, { withFileTypes: true });
712
+ for (const entry of entries) {
713
+ if (entry.isDirectory()) {
714
+ const subPath = path.join(outDir, entry.name, 'config.json');
715
+ try {
716
+ await fs.access(subPath);
717
+ return subPath;
718
+ }
719
+ catch { }
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+ async function cleanEmptyDirs(dir) {
725
+ const entries = await fs.readdir(dir, { withFileTypes: true });
726
+ let isEmpty = true;
727
+ for (const entry of entries) {
728
+ const fullPath = path.join(dir, entry.name);
729
+ if (entry.isDirectory()) {
730
+ const subEmpty = await cleanEmptyDirs(fullPath);
731
+ if (subEmpty) {
732
+ try {
733
+ await fs.rmdir(fullPath);
734
+ }
735
+ catch { }
736
+ }
737
+ else {
738
+ isEmpty = false;
739
+ }
740
+ }
741
+ else {
742
+ isEmpty = false;
743
+ }
744
+ }
745
+ return isEmpty;
746
+ }
@@ -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';