@playcraft/build 0.0.13 → 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.
- package/dist/analyzers/scene-asset-collector.js +99 -9
- package/dist/base-builder.d.ts +15 -78
- package/dist/base-builder.js +34 -741
- package/dist/engines/engine-detector.d.ts +38 -0
- package/dist/engines/engine-detector.js +201 -0
- package/dist/engines/generic-adapter.d.ts +71 -0
- package/dist/engines/generic-adapter.js +378 -0
- package/dist/engines/index.d.ts +7 -0
- package/dist/engines/index.js +7 -0
- package/dist/engines/playcanvas-adapter.d.ts +85 -0
- package/dist/engines/playcanvas-adapter.js +813 -0
- package/dist/generators/config-generator.js +59 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/loaders/playcraft-loader.js +240 -5
- package/dist/platforms/adikteev.d.ts +1 -1
- package/dist/platforms/adikteev.js +30 -36
- package/dist/platforms/applovin.d.ts +1 -1
- package/dist/platforms/applovin.js +31 -36
- package/dist/platforms/base.d.ts +27 -5
- package/dist/platforms/base.js +79 -181
- package/dist/platforms/bigo.d.ts +1 -1
- package/dist/platforms/bigo.js +28 -28
- package/dist/platforms/facebook.d.ts +1 -1
- package/dist/platforms/facebook.js +21 -10
- package/dist/platforms/google.d.ts +1 -1
- package/dist/platforms/google.js +28 -21
- package/dist/platforms/index.d.ts +1 -0
- package/dist/platforms/index.js +4 -0
- package/dist/platforms/inmobi.d.ts +1 -1
- package/dist/platforms/inmobi.js +27 -34
- package/dist/platforms/ironsource.d.ts +1 -1
- package/dist/platforms/ironsource.js +37 -40
- package/dist/platforms/liftoff.d.ts +1 -1
- package/dist/platforms/liftoff.js +22 -30
- package/dist/platforms/mintegral.d.ts +10 -0
- package/dist/platforms/mintegral.js +65 -0
- package/dist/platforms/moloco.d.ts +1 -1
- package/dist/platforms/moloco.js +18 -20
- package/dist/platforms/playcraft.d.ts +1 -1
- package/dist/platforms/playcraft.js +2 -2
- package/dist/platforms/remerge.d.ts +1 -1
- package/dist/platforms/remerge.js +19 -20
- package/dist/platforms/snapchat.d.ts +1 -1
- package/dist/platforms/snapchat.js +32 -26
- package/dist/platforms/tiktok.d.ts +1 -1
- package/dist/platforms/tiktok.js +28 -24
- package/dist/platforms/unity.d.ts +1 -1
- package/dist/platforms/unity.js +30 -36
- package/dist/playable-builder.d.ts +1 -0
- package/dist/playable-builder.js +16 -2
- package/dist/types.d.ts +113 -1
- package/dist/types.js +77 -1
- package/dist/utils/ammo-detector.d.ts +9 -0
- package/dist/utils/ammo-detector.js +76 -0
- package/dist/utils/build-mode-detector.js +2 -0
- package/dist/utils/minify.d.ts +32 -0
- package/dist/utils/minify.js +82 -0
- package/dist/utils/obfuscate.d.ts +42 -0
- package/dist/utils/obfuscate.js +216 -0
- package/dist/vite/config-builder-generic.d.ts +70 -0
- package/dist/vite/config-builder-generic.js +251 -0
- package/dist/vite/config-builder.d.ts +8 -0
- package/dist/vite/config-builder.js +53 -16
- package/dist/vite/platform-configs.js +29 -1
- package/dist/vite/plugin-compress-js.d.ts +21 -0
- package/dist/vite/plugin-compress-js.js +213 -0
- package/dist/vite/plugin-esm-html-generator.js +5 -1
- package/dist/vite/plugin-obfuscate.d.ts +22 -0
- package/dist/vite/plugin-obfuscate.js +52 -0
- package/dist/vite/plugin-platform.d.ts +5 -0
- package/dist/vite/plugin-platform.js +499 -35
- package/dist/vite/plugin-playcanvas.js +21 -68
- package/dist/vite/plugin-source-builder.js +102 -21
- package/dist/vite-builder.d.ts +25 -7
- package/dist/vite-builder.js +141 -52
- package/package.json +4 -2
- package/physics/cannon-rigidbody-adapter.js +243 -22
- package/templates/__loading__.js +0 -12
- package/templates/index.esm.mjs +0 -11
- package/templates/patches/playcraft-cta-adapter.js +129 -31
- 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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
145
|
-
//
|
|
146
|
-
html = html.replace(/(\bconst\s+CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${
|
|
147
|
-
|
|
148
|
-
//
|
|
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
|
-
//
|
|
151
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
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,35 +205,207 @@ 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 = "${
|
|
208
|
+
return `window.CONFIG_FILENAME = "${configFilename}";`;
|
|
170
209
|
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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(
|
|
184
|
-
//
|
|
185
|
-
|
|
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
410
|
// Note: We no longer patch fillMode to 'NONE' because it causes canvas scaling issues
|
|
200
411
|
// The MRAID resize patch handles the sizing correctly with any fillMode
|
|
@@ -231,7 +442,8 @@ async function patchStylesForMraid(outDir) {
|
|
|
231
442
|
}
|
|
232
443
|
async function injectMraidResizePatch(html) {
|
|
233
444
|
const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
|
|
234
|
-
|
|
445
|
+
const minified = await minifyPatchCode(patchCode, 'mraid-resize-platform');
|
|
446
|
+
return html.replace('</head>', `<script>${minified}</script>\n</head>`);
|
|
235
447
|
}
|
|
236
448
|
async function readPatchFile(name) {
|
|
237
449
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -253,7 +465,9 @@ async function copyBaseBuildAssets(baseBuildDir, outDir) {
|
|
|
253
465
|
continue;
|
|
254
466
|
}
|
|
255
467
|
// 跳过以 __ 开头的临时目录(如 __dist__, __esm-temp__, __esm-work__ 等)
|
|
256
|
-
|
|
468
|
+
// 注意:不能跳过 __ 开头的文件,因为 Classic 模式下
|
|
469
|
+
// __start__.js, __settings__.js, __modules__.js, __loading__.js, __game-scripts.js 都是必需的
|
|
470
|
+
if (entry.isDirectory() && entry.name.startsWith('__')) {
|
|
257
471
|
continue;
|
|
258
472
|
}
|
|
259
473
|
const srcPath = path.join(baseBuildDir, entry.name);
|
|
@@ -280,3 +494,253 @@ async function copyDirectory(src, dest) {
|
|
|
280
494
|
}
|
|
281
495
|
}
|
|
282
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
|
+
}
|