@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.
- package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +1 -0
- package/dist/analyzers/__tests__/optimization-analyzer.test.js +169 -0
- package/dist/analyzers/playable-analyzer.js +3 -2
- package/dist/analyzers/scene-asset-collector.js +99 -9
- package/dist/base-builder.d.ts +15 -78
- package/dist/base-builder.js +34 -735
- 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 -34
- package/dist/platforms/applovin.d.ts +1 -1
- package/dist/platforms/applovin.js +34 -33
- 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 -32
- package/dist/platforms/ironsource.d.ts +1 -1
- package/dist/platforms/ironsource.js +37 -37
- package/dist/platforms/liftoff.d.ts +1 -1
- package/dist/platforms/liftoff.js +24 -27
- 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 +35 -23
- 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 +32 -32
- package/dist/playable-builder.d.ts +1 -0
- package/dist/playable-builder.js +19 -3
- package/dist/templates/__loading__.js +100 -0
- package/dist/templates/__modules__.js +47 -0
- package/dist/templates/__settings__.template.js +20 -0
- package/dist/templates/__start__.js +332 -0
- package/dist/templates/index.html +18 -0
- package/dist/templates/logo.png +0 -0
- package/dist/templates/manifest.json +1 -0
- package/dist/templates/patches/cannon.min.js +28 -0
- package/dist/templates/patches/lz4.js +10 -0
- package/dist/templates/patches/one-page-http-get.js +20 -0
- package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
- package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
- package/dist/templates/patches/p2.min.js +27 -0
- package/dist/templates/patches/playcraft-no-xhr.js +76 -0
- package/dist/templates/playcanvas-stable.min.js +16363 -0
- package/dist/templates/styles.css +43 -0
- package/dist/types.d.ts +114 -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/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 +56 -16
- package/dist/vite/platform-configs.d.ts +1 -0
- package/dist/vite/platform-configs.js +30 -1
- package/dist/vite/plugin-build-state.d.ts +2 -0
- package/dist/vite/plugin-build-state.js +5 -3
- 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 +15 -2
- package/dist/vite/plugin-platform.d.ts +5 -0
- package/dist/vite/plugin-platform.js +502 -36
- package/dist/vite/plugin-playcanvas.d.ts +1 -0
- package/dist/vite/plugin-playcanvas.js +181 -88
- 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/one-page-mraid-resize-canvas.js +18 -4
- package/templates/patches/playcraft-cta-adapter.js +129 -31
- package/templates/patches/scene-physics-defaults.js +49 -0
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import { minifyPatchCode } from '../utils/minify.js';
|
|
5
6
|
/**
|
|
6
7
|
* PlayCanvas Vite 插件
|
|
7
8
|
* 处理 PlayCanvas 特定的资源转换和内联
|
|
@@ -73,6 +74,9 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
73
74
|
}
|
|
74
75
|
// 对于 Classic 模式或 ESM 原生模式,保持原有的多文件结构
|
|
75
76
|
}
|
|
77
|
+
// 注意:HTML 压缩不能在这里执行,因为 Vite 还需要解析 HTML
|
|
78
|
+
// HTML 模板已经在 templates/ 中被压缩为单行
|
|
79
|
+
// 最终的 HTML 压缩由 vite-plugin-singlefile 在最后阶段处理
|
|
76
80
|
return html;
|
|
77
81
|
},
|
|
78
82
|
},
|
|
@@ -107,7 +111,7 @@ async function inlineEngineScript(html, baseBuildDir, options) {
|
|
|
107
111
|
const enginePath = path.join(baseBuildDir, engineName);
|
|
108
112
|
try {
|
|
109
113
|
await fs.access(enginePath);
|
|
110
|
-
|
|
114
|
+
let engineCode = await fs.readFile(enginePath, 'utf-8');
|
|
111
115
|
const patchScripts = await getEnginePatchScripts(options);
|
|
112
116
|
const engineScript = options.compressEngine
|
|
113
117
|
? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
|
|
@@ -202,6 +206,8 @@ async function generateAndInlineSettings(html, baseBuildDir, options, pluginCont
|
|
|
202
206
|
const appProps = configJson.application_properties || {};
|
|
203
207
|
const scripts = appProps.scripts || [];
|
|
204
208
|
const preloadModules = extractPreloadModules(configJson);
|
|
209
|
+
// 保留原始的 powerPreference 设置,默认为 high-performance
|
|
210
|
+
const powerPreference = appProps.powerPreference || 'high-performance';
|
|
205
211
|
const settingsCode = `
|
|
206
212
|
window.ASSET_PREFIX = "";
|
|
207
213
|
window.SCRIPT_PREFIX = "";
|
|
@@ -211,7 +217,7 @@ window.CONTEXT_OPTIONS = {
|
|
|
211
217
|
'alpha': ${appProps.transparentCanvas === true},
|
|
212
218
|
'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
|
|
213
219
|
'deviceTypes': ['webgl2', 'webgl1'],
|
|
214
|
-
'powerPreference': "
|
|
220
|
+
'powerPreference': "${powerPreference}"
|
|
215
221
|
};
|
|
216
222
|
window.SCRIPTS = [${scripts.join(', ')}];
|
|
217
223
|
window.CONFIG_FILENAME = ${configValue};
|
|
@@ -260,8 +266,24 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
260
266
|
if (options.mraidSupport) {
|
|
261
267
|
configJson = applyMraidConfig(configJson);
|
|
262
268
|
}
|
|
263
|
-
// 5. 生成 config data URL
|
|
264
|
-
|
|
269
|
+
// 5. 生成 config.json 的 data URL
|
|
270
|
+
// 如果启用 compressConfigJson,使用 LZ4 压缩后再 base64 编码
|
|
271
|
+
const configJsonString = JSON.stringify(configJson);
|
|
272
|
+
let configDataUrl;
|
|
273
|
+
if (options.compressConfigJson) {
|
|
274
|
+
const lz4 = await loadLz4Module();
|
|
275
|
+
const compressed = lz4.compress(Buffer.from(configJsonString));
|
|
276
|
+
const compressedBase64 = Buffer.from(compressed).toString('base64');
|
|
277
|
+
// 使用 lz4-json 前缀标识压缩格式,运行时需要解压
|
|
278
|
+
configDataUrl = `data:application/x-lz4-json;base64,${compressedBase64}`;
|
|
279
|
+
const originalSize = Buffer.from(configJsonString).length;
|
|
280
|
+
const compressedSize = compressed.length;
|
|
281
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
|
|
282
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: config.json 已压缩 (${formatBytes(originalSize)} → ${formatBytes(compressedSize)}, 减少 ${ratio}%)`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
configDataUrl = `data:application/json;base64,${Buffer.from(configJsonString).toString('base64')}`;
|
|
286
|
+
}
|
|
265
287
|
// 6. 处理场景文件 - 获取第一个场景的 data URL
|
|
266
288
|
let sceneDataUrl = '';
|
|
267
289
|
if (configJson.scenes && configJson.scenes.length > 0) {
|
|
@@ -281,57 +303,23 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
281
303
|
sceneDataUrl = sceneUrl || '';
|
|
282
304
|
}
|
|
283
305
|
}
|
|
284
|
-
// 7.
|
|
285
|
-
// index.esm.mjs 中使用 `${ASSET_PREFIX}logo.png` 引用 logo
|
|
286
|
-
// 需要将 ASSET_PREFIX 替换为 data URL 或直接替换 logo 引用
|
|
287
|
-
if (options.convertDataUrls) {
|
|
288
|
-
const logoPath = path.join(baseBuildDir, 'logo.png');
|
|
289
|
-
try {
|
|
290
|
-
const logoBuffer = await fs.readFile(logoPath);
|
|
291
|
-
const logoDataUrl = `data:image/png;base64,${logoBuffer.toString('base64')}`;
|
|
292
|
-
// 替换 `${ASSET_PREFIX}logo.png` 模式
|
|
293
|
-
// 由于代码被打包为 IIFE,模板字符串可能被转换为字符串拼接
|
|
294
|
-
// 格式1: `${ASSET_PREFIX}logo.png`
|
|
295
|
-
// 格式2: ASSET_PREFIX + "logo.png"
|
|
296
|
-
// 格式3: "".concat(ASSET_PREFIX, "logo.png") (Vite 打包后)
|
|
297
|
-
const escapedLogoDataUrl = logoDataUrl.replace(/\$/g, '$$$$');
|
|
298
|
-
// 替换模板字符串格式(原始格式)
|
|
299
|
-
html = html.replace(/\$\{ASSET_PREFIX\}logo\.png/g, escapedLogoDataUrl);
|
|
300
|
-
// 替换字符串拼接格式
|
|
301
|
-
html = html.replace(/ASSET_PREFIX\s*\+\s*["']logo\.png["']/g, `"${escapedLogoDataUrl}"`);
|
|
302
|
-
// 替换 concat 格式 (Vite/Rollup 打包后常见格式)
|
|
303
|
-
html = html.replace(/["']?["']?\.concat\(ASSET_PREFIX,\s*["']logo\.png["']\)/g, `"${escapedLogoDataUrl}"`);
|
|
304
|
-
// 替换反引号模板字符串在 IIFE 中的编译结果
|
|
305
|
-
// `${ASSET_PREFIX}logo.png` 可能被编译为 ASSET_PREFIX+"logo.png"
|
|
306
|
-
html = html.replace(/`\$\{ASSET_PREFIX\}logo\.png`/g, `"${escapedLogoDataUrl}"`);
|
|
307
|
-
// ⚠️ 重要:Vite 压缩后,直接查找 "logo.png" 字符串
|
|
308
|
-
// 格式: .src="logo.png" 或 src:"logo.png" 或 ="logo.png"
|
|
309
|
-
const logoReplaceCount = (html.match(/["']logo\.png["']/g) || []).length;
|
|
310
|
-
if (logoReplaceCount > 0) {
|
|
311
|
-
html = html.replace(/["']logo\.png["']/g, `"${escapedLogoDataUrl}"`);
|
|
312
|
-
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${logoReplaceCount} 处 "logo.png" 字符串`);
|
|
313
|
-
}
|
|
314
|
-
console.log(`[PlayCanvasPlugin] ESM Bundle: logo.png 已内联`);
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
console.warn(`[PlayCanvasPlugin] ESM Bundle: logo.png 不存在,跳过内联`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
306
|
+
// 7. (Logo removed - no longer needed)
|
|
320
307
|
// 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
|
|
321
|
-
// CONFIG_FILENAME = "config.json"
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
308
|
+
// 方案1+3:直接将 CONFIG_FILENAME = "config.json" 替换为 data URL
|
|
309
|
+
// 8.1 替换 CONFIG_FILENAME
|
|
310
|
+
// 格式1: CONFIG_FILENAME = "config.json"
|
|
311
|
+
// 格式2: CONFIG_FILENAME="config.json" (压缩后)
|
|
312
|
+
const configReplaceCount = (html.match(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g) || []).length;
|
|
313
|
+
if (configReplaceCount > 0) {
|
|
314
|
+
html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME="${configDataUrl}"`);
|
|
315
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configReplaceCount} 处 CONFIG_FILENAME`);
|
|
316
|
+
}
|
|
317
|
+
// 8.2 替换 configure("config.json") 调用
|
|
318
|
+
// Vite 压缩后可能是: .configure("config.json",
|
|
331
319
|
const configureReplaceCount = (html.match(/\.configure\s*\(\s*["']config\.json["']/g) || []).length;
|
|
332
320
|
if (configureReplaceCount > 0) {
|
|
333
|
-
html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${
|
|
334
|
-
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")
|
|
321
|
+
html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${configDataUrl}"`);
|
|
322
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")`);
|
|
335
323
|
}
|
|
336
324
|
// 9. 替换 SCENE_PATH
|
|
337
325
|
if (sceneDataUrl) {
|
|
@@ -403,28 +391,22 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
403
391
|
// 这个补丁覆盖 pc.ScriptHandler.prototype._loadScript,使其能够:
|
|
404
392
|
// 1. 正确处理 data URL 格式的脚本
|
|
405
393
|
// 2. 对于空脚本(已打包到 IIFE 中的),直接跳过执行
|
|
406
|
-
const
|
|
407
|
-
<script>
|
|
408
|
-
// ScriptHandler patch for one-page build (empty scripts are already bundled in IIFE)
|
|
394
|
+
const scriptHandlerPatchCode = `
|
|
409
395
|
(function() {
|
|
410
396
|
if (typeof pc === 'undefined' || !pc.ScriptHandler) return;
|
|
411
397
|
|
|
412
398
|
var originalLoadScript = pc.ScriptHandler.prototype._loadScript;
|
|
413
399
|
pc.ScriptHandler.prototype._loadScript = function(url, callback) {
|
|
414
|
-
// Check if this is a data URL
|
|
415
400
|
if (url && url.startsWith('data:text/javascript;base64,')) {
|
|
416
401
|
var head = document.head;
|
|
417
402
|
var element = document.createElement('script');
|
|
418
403
|
this._cache[url] = element;
|
|
419
404
|
element.async = false;
|
|
420
405
|
|
|
421
|
-
// Decode base64 content
|
|
422
406
|
var index = url.indexOf(',');
|
|
423
407
|
var base64 = url.slice(index + 1);
|
|
424
408
|
|
|
425
|
-
// If empty content (scripts already bundled), skip execution
|
|
426
409
|
if (!base64 || base64.length === 0) {
|
|
427
|
-
// Still create the element for cache consistency
|
|
428
410
|
callback(null, url, element);
|
|
429
411
|
return;
|
|
430
412
|
}
|
|
@@ -441,14 +423,13 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
441
423
|
return;
|
|
442
424
|
}
|
|
443
425
|
|
|
444
|
-
// Fall back to original implementation for non-data URLs
|
|
445
426
|
return originalLoadScript.call(this, url, callback);
|
|
446
427
|
};
|
|
447
|
-
})()
|
|
448
|
-
|
|
428
|
+
})();`;
|
|
429
|
+
const minifiedScriptHandlerPatch = await minifyPatchCode(scriptHandlerPatchCode, 'scriptHandlerPatch');
|
|
449
430
|
// 将补丁注入到 </head> 之前,确保在引擎之后、脚本加载之前执行
|
|
450
431
|
if (html.includes('</head>')) {
|
|
451
|
-
html = html.replace('</head>',
|
|
432
|
+
html = html.replace('</head>', `<script>${minifiedScriptHandlerPatch}</script>\n</head>`);
|
|
452
433
|
}
|
|
453
434
|
console.log('[PlayCanvasPlugin] ESM Bundle 模式:资源内联完成');
|
|
454
435
|
return html;
|
|
@@ -737,9 +718,6 @@ async function inlineGameScripts(html, baseBuildDir) {
|
|
|
737
718
|
async function inlineStartScript(html, baseBuildDir, options) {
|
|
738
719
|
const startPath = path.join(baseBuildDir, '__start__.js');
|
|
739
720
|
let startCode = await fs.readFile(startPath, 'utf-8');
|
|
740
|
-
if (options.convertDataUrls) {
|
|
741
|
-
startCode = await inlineLogoInStartScript(startCode, baseBuildDir);
|
|
742
|
-
}
|
|
743
721
|
// 替换 script 标签
|
|
744
722
|
const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
|
|
745
723
|
if (scriptPattern.test(html)) {
|
|
@@ -761,9 +739,6 @@ async function inlineLoadingScript(html, baseBuildDir) {
|
|
|
761
739
|
await fs.access(loadingPath);
|
|
762
740
|
// 文件存在,内联它
|
|
763
741
|
let loadingCode = await fs.readFile(loadingPath, 'utf-8');
|
|
764
|
-
if (loadingCode.includes('${ASSET_PREFIX}logo.png')) {
|
|
765
|
-
loadingCode = await inlineLogoInStartScript(loadingCode, baseBuildDir);
|
|
766
|
-
}
|
|
767
742
|
if (scriptPattern.test(html)) {
|
|
768
743
|
html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
|
|
769
744
|
}
|
|
@@ -780,6 +755,33 @@ async function inlineLoadingScript(html, baseBuildDir) {
|
|
|
780
755
|
}
|
|
781
756
|
return html;
|
|
782
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* 压缩 CSS 代码
|
|
760
|
+
* 移除注释、多余空白、合并为单行
|
|
761
|
+
*/
|
|
762
|
+
function minifyCSS(css) {
|
|
763
|
+
return css
|
|
764
|
+
// 移除多行注释 /* ... */
|
|
765
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
766
|
+
// 移除单行注释 // ... (CSS 不标准但有些预处理器会保留)
|
|
767
|
+
.replace(/\/\/.*$/gm, '')
|
|
768
|
+
// 将多个空白字符(空格、换行、制表符)替换为单个空格
|
|
769
|
+
.replace(/\s+/g, ' ')
|
|
770
|
+
// 移除 { 前的空格
|
|
771
|
+
.replace(/\s*\{\s*/g, '{')
|
|
772
|
+
// 移除 } 后的空格
|
|
773
|
+
.replace(/\s*\}\s*/g, '}')
|
|
774
|
+
// 移除 : 前后的空格
|
|
775
|
+
.replace(/\s*:\s*/g, ':')
|
|
776
|
+
// 移除 ; 前后的空格
|
|
777
|
+
.replace(/\s*;\s*/g, ';')
|
|
778
|
+
// 移除 , 后的空格
|
|
779
|
+
.replace(/,\s*/g, ',')
|
|
780
|
+
// 移除最后一个 ; 在 } 前
|
|
781
|
+
.replace(/;}/g, '}')
|
|
782
|
+
// 去除首尾空白
|
|
783
|
+
.trim();
|
|
784
|
+
}
|
|
783
785
|
/**
|
|
784
786
|
* 内联 CSS 文件
|
|
785
787
|
*/
|
|
@@ -799,6 +801,8 @@ async function inlineCSS(html, baseBuildDir, options) {
|
|
|
799
801
|
if (options.mraidSupport && !cssContent.includes('fill-mode-NONE')) {
|
|
800
802
|
cssContent += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
|
|
801
803
|
}
|
|
804
|
+
// 压缩 CSS(移除注释、多余空白、合并为单行)
|
|
805
|
+
cssContent = minifyCSS(cssContent);
|
|
802
806
|
// 替换 link 标签为 style 标签
|
|
803
807
|
html = html.replace(match[0], `<style>${cssContent}</style>`);
|
|
804
808
|
}
|
|
@@ -1004,10 +1008,14 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
1004
1008
|
}
|
|
1005
1009
|
}
|
|
1006
1010
|
// ✅ 核心优化:如果是图片,使用 sharp 压缩
|
|
1011
|
+
// ⚠️ 重要:字体纹理(type === 'font')不能转换为 WebP!
|
|
1012
|
+
// MSDF/位图字体的 PNG 纹理包含精确的距离场/像素数据,
|
|
1013
|
+
// WebP 有损压缩会破坏这些数据导致字体渲染异常
|
|
1007
1014
|
else if (isImageFile(cleanUrl)) {
|
|
1015
|
+
const isFontTexture = asset.type === 'font';
|
|
1008
1016
|
const compressed = await compressImage(buffer, cleanUrl, {
|
|
1009
|
-
convertToWebP:
|
|
1010
|
-
quality: 75,
|
|
1017
|
+
convertToWebP: !isFontTexture, // 字体纹理不转换为 WebP
|
|
1018
|
+
quality: isFontTexture ? 100 : 75, // 字体纹理使用无损压缩
|
|
1011
1019
|
});
|
|
1012
1020
|
// 防御性检查:如果压缩后的 buffer 为空,使用原始 buffer
|
|
1013
1021
|
if (!compressed.buffer || compressed.buffer.length === 0) {
|
|
@@ -1122,17 +1130,6 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
1122
1130
|
}
|
|
1123
1131
|
return configJson;
|
|
1124
1132
|
}
|
|
1125
|
-
async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
1126
|
-
const logoPath = path.join(baseBuildDir, 'logo.png');
|
|
1127
|
-
try {
|
|
1128
|
-
const buffer = await fs.readFile(logoPath);
|
|
1129
|
-
const dataUrl = `data:image/png;base64,${buffer.toString('base64')}`;
|
|
1130
|
-
return startCode.replace(/\$\{ASSET_PREFIX\}logo\.png/g, dataUrl);
|
|
1131
|
-
}
|
|
1132
|
-
catch (error) {
|
|
1133
|
-
return startCode;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
1133
|
/**
|
|
1137
1134
|
* 为跳过的资源生成有效的占位符 data URL
|
|
1138
1135
|
*/
|
|
@@ -1196,10 +1193,15 @@ async function compressImage(buffer, filePath, options) {
|
|
|
1196
1193
|
};
|
|
1197
1194
|
}
|
|
1198
1195
|
// 按原格式压缩
|
|
1196
|
+
const quality = options?.quality ?? 80;
|
|
1199
1197
|
switch (ext) {
|
|
1200
1198
|
case '.png':
|
|
1199
|
+
// 如果 quality >= 100,使用无损压缩(适用于字体纹理)
|
|
1201
1200
|
const pngBuffer = await image
|
|
1202
|
-
.png({
|
|
1201
|
+
.png({
|
|
1202
|
+
compressionLevel: quality >= 100 ? 6 : 9, // 无损时使用中等压缩
|
|
1203
|
+
palette: quality < 100, // 无损时不使用调色板
|
|
1204
|
+
})
|
|
1203
1205
|
.toBuffer();
|
|
1204
1206
|
// 防御性检查
|
|
1205
1207
|
if (!pngBuffer || pngBuffer.length === 0) {
|
|
@@ -1295,7 +1297,9 @@ function buildConfigValue(configJson) {
|
|
|
1295
1297
|
function applyMraidConfig(configJson) {
|
|
1296
1298
|
const next = { ...configJson };
|
|
1297
1299
|
const props = { ...(next.application_properties || {}) };
|
|
1298
|
-
|
|
1300
|
+
// Keep original fillMode (usually KEEP_ASPECT) for correct canvas scaling
|
|
1301
|
+
// The MRAID resize patch will handle the sizing
|
|
1302
|
+
// props.fillMode = 'NONE'; // Removed: causes canvas scaling issues
|
|
1299
1303
|
next.application_properties = props;
|
|
1300
1304
|
return next;
|
|
1301
1305
|
}
|
|
@@ -1325,19 +1329,42 @@ async function getEnginePatchScripts(options) {
|
|
|
1325
1329
|
const scripts = [];
|
|
1326
1330
|
if (options.patchXhrOut) {
|
|
1327
1331
|
const patchCode = await readPatchFile('playcraft-no-xhr.js');
|
|
1328
|
-
|
|
1332
|
+
const minified = await minifyPatchCode(patchCode, 'playcraft-no-xhr.js');
|
|
1333
|
+
scripts.push(`<script>${minified}</script>`);
|
|
1329
1334
|
}
|
|
1330
1335
|
else if (options.configJsonInline) {
|
|
1331
1336
|
const patchCode = await readPatchFile('one-page-http-get.js');
|
|
1332
|
-
|
|
1337
|
+
const minified = await minifyPatchCode(patchCode, 'one-page-http-get.js');
|
|
1338
|
+
scripts.push(`<script>${minified}</script>`);
|
|
1333
1339
|
}
|
|
1334
1340
|
if (options.inlineGameScripts) {
|
|
1335
1341
|
const patchCode = await readPatchFile('one-page-inline-game-scripts.js');
|
|
1336
|
-
scripts.
|
|
1342
|
+
const minified = await minifyPatchCode(patchCode, 'one-page-inline-game-scripts.js');
|
|
1343
|
+
scripts.push(`<script>${minified}</script>`);
|
|
1337
1344
|
}
|
|
1338
1345
|
if (options.mraidSupport) {
|
|
1339
1346
|
const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
|
|
1340
|
-
|
|
1347
|
+
const minified = await minifyPatchCode(patchCode, 'one-page-mraid-resize-canvas.js');
|
|
1348
|
+
scripts.push(`<script>${minified}</script>`);
|
|
1349
|
+
}
|
|
1350
|
+
// 注入 Scene applySettings 补丁:确保 settings.physics/render 存在,避免 "Cannot read properties of undefined (reading 'gravity')" 报错
|
|
1351
|
+
// 场景:ammoReplacement 或部分场景 JSON 缺少 physics 配置
|
|
1352
|
+
const scenePhysicsPatch = await readPatchFile('scene-physics-defaults.js');
|
|
1353
|
+
if (scenePhysicsPatch) {
|
|
1354
|
+
const minified = await minifyPatchCode(scenePhysicsPatch, 'scene-physics-defaults.js');
|
|
1355
|
+
scripts.push(`<script>${minified}</script>`);
|
|
1356
|
+
}
|
|
1357
|
+
// 如果启用了 config.json 压缩,需要注入 LZ4 解压运行时代码
|
|
1358
|
+
// 注意:必须在 compressEngine 之前检查,因为 compressEngine 也会注入 lz4.js
|
|
1359
|
+
if (options.compressConfigJson && !options.compressEngine) {
|
|
1360
|
+
// 如果没有启用引擎压缩,需要单独注入 lz4.js
|
|
1361
|
+
const lz4Script = await getLz4InlineScript();
|
|
1362
|
+
scripts.push(lz4Script);
|
|
1363
|
+
}
|
|
1364
|
+
// 注入 config.json 解压 patch(拦截 fetch 请求并解压 LZ4 数据)
|
|
1365
|
+
if (options.compressConfigJson) {
|
|
1366
|
+
const configDecompressPatch = buildConfigJsonDecompressPatch();
|
|
1367
|
+
scripts.push(`<script>${configDecompressPatch}</script>`);
|
|
1341
1368
|
}
|
|
1342
1369
|
if (options.compressEngine) {
|
|
1343
1370
|
// Focus patch: 确保 canvas 获得焦点以接收键盘事件
|
|
@@ -1379,3 +1406,69 @@ async function readPatchFile(name) {
|
|
|
1379
1406
|
const patchPath = path.resolve(currentDir, '../../templates/patches', name);
|
|
1380
1407
|
return await fs.readFile(patchPath, 'utf-8');
|
|
1381
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* 格式化字节数为人类可读格式
|
|
1411
|
+
*/
|
|
1412
|
+
function formatBytes(bytes) {
|
|
1413
|
+
if (bytes === 0)
|
|
1414
|
+
return '0 B';
|
|
1415
|
+
const k = 1024;
|
|
1416
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1417
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1418
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* 构建 config.json 解压 patch
|
|
1422
|
+
*
|
|
1423
|
+
* 这个 patch 拦截 PlayCanvas 的 pc.Http.prototype.get 请求,
|
|
1424
|
+
* 检测是否为 LZ4 压缩的 config.json,如果是则解压后返回。
|
|
1425
|
+
*
|
|
1426
|
+
* 工作原理:
|
|
1427
|
+
* 1. 保存原始 pc.Http.prototype.get 函数
|
|
1428
|
+
* 2. 重写 get,检测 URL 是否为 LZ4 压缩的 JSON data URL
|
|
1429
|
+
* 3. 如果是,解压并通过 callback 返回解析后的 JSON 对象
|
|
1430
|
+
*
|
|
1431
|
+
* 注意:
|
|
1432
|
+
* - PlayCanvas 使用 pc.Http.prototype.get 加载 config.json,不是 fetch
|
|
1433
|
+
* - 使用 lz4.js 提供的 Buffer polyfill 进行解压
|
|
1434
|
+
* - 必须在 pc 对象可用后才能生效,所以使用轮询等待
|
|
1435
|
+
*/
|
|
1436
|
+
function buildConfigJsonDecompressPatch() {
|
|
1437
|
+
// 原始代码:
|
|
1438
|
+
// (function() {
|
|
1439
|
+
// function patch() {
|
|
1440
|
+
// if (!window.pc || !window.pc.Http) return false;
|
|
1441
|
+
// var oldGet = pc.Http.prototype.get;
|
|
1442
|
+
// pc.Http.prototype.get = function(url, options, callback) {
|
|
1443
|
+
// if (typeof options === 'function') {
|
|
1444
|
+
// callback = options;
|
|
1445
|
+
// options = {};
|
|
1446
|
+
// }
|
|
1447
|
+
// if (typeof url === 'string' && url.startsWith('data:application/x-lz4-json;base64,')) {
|
|
1448
|
+
// try {
|
|
1449
|
+
// var base64 = url.replace('data:application/x-lz4-json;base64,', '');
|
|
1450
|
+
// var compressed = new Buffer(base64, 'base64');
|
|
1451
|
+
// var decompressed = lz4.decompress(compressed);
|
|
1452
|
+
// var str = Buffer.from(decompressed).toString('utf8');
|
|
1453
|
+
// var json = JSON.parse(str);
|
|
1454
|
+
// console.log('[LZ4 Config] Decompressed config.json successfully, size:', str.length);
|
|
1455
|
+
// callback(null, json);
|
|
1456
|
+
// } catch(e) {
|
|
1457
|
+
// console.error('[LZ4 Config] Decompression error:', e);
|
|
1458
|
+
// callback(e);
|
|
1459
|
+
// }
|
|
1460
|
+
// return;
|
|
1461
|
+
// }
|
|
1462
|
+
// oldGet.call(this, url, options, callback);
|
|
1463
|
+
// };
|
|
1464
|
+
// return true;
|
|
1465
|
+
// }
|
|
1466
|
+
// if (!patch()) {
|
|
1467
|
+
// var c = 0;
|
|
1468
|
+
// var i = setInterval(function() {
|
|
1469
|
+
// if (patch() || ++c > 100) clearInterval(i);
|
|
1470
|
+
// }, 10);
|
|
1471
|
+
// }
|
|
1472
|
+
// })();
|
|
1473
|
+
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)}}();`;
|
|
1474
|
+
}
|
|
@@ -52,6 +52,39 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
52
52
|
if (scriptPathMap.has(id)) {
|
|
53
53
|
return scriptPathMap.get(id);
|
|
54
54
|
}
|
|
55
|
+
// 通过 Import Map 解析裸模块说明符(如 gameRule、planck 等)
|
|
56
|
+
// Import Map 中的路径是相对于 assets/ 目录的(Import Map 文件本身在 assets/Import Map.json 中)
|
|
57
|
+
if (options.importMap?.content?.imports && !id.startsWith('./') && !id.startsWith('../') && !id.startsWith('/') && !id.startsWith('\0')) {
|
|
58
|
+
const imports = options.importMap.content.imports;
|
|
59
|
+
const assetsDir = path.join(options.projectDir, 'assets');
|
|
60
|
+
// 精确匹配(如 "gameRule" -> "./Generated/GameRule/BreakshotPoolRule.mjs")
|
|
61
|
+
if (imports[id]) {
|
|
62
|
+
let resolved = imports[id];
|
|
63
|
+
// 去掉开头的 ./
|
|
64
|
+
if (resolved.startsWith('./')) {
|
|
65
|
+
resolved = resolved.slice(2);
|
|
66
|
+
}
|
|
67
|
+
const absPath = path.join(assetsDir, resolved);
|
|
68
|
+
console.log(`[SourceBuilder] Import Map 解析: ${id} -> ${absPath}`);
|
|
69
|
+
return absPath;
|
|
70
|
+
}
|
|
71
|
+
// 前缀匹配(如 "Gameplay/" -> "./LiteCreator/Gameplay/")
|
|
72
|
+
for (const [prefix, target] of Object.entries(imports)) {
|
|
73
|
+
if (prefix.endsWith('/') && id.startsWith(prefix)) {
|
|
74
|
+
const remainder = id.slice(prefix.length);
|
|
75
|
+
let targetBase = target;
|
|
76
|
+
if (targetBase.startsWith('./')) {
|
|
77
|
+
targetBase = targetBase.slice(2);
|
|
78
|
+
}
|
|
79
|
+
if (targetBase.endsWith('/')) {
|
|
80
|
+
targetBase = targetBase.slice(0, -1);
|
|
81
|
+
}
|
|
82
|
+
const absPath = path.join(assetsDir, targetBase, remainder);
|
|
83
|
+
console.log(`[SourceBuilder] Import Map 前缀解析: ${id} -> ${absPath}`);
|
|
84
|
+
return absPath;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
55
88
|
// 处理相对导入(../xxx 或 ./xxx)
|
|
56
89
|
if (importer && (id.startsWith('./') || id.startsWith('../'))) {
|
|
57
90
|
// 获取导入者的模块路径
|
|
@@ -277,7 +310,6 @@ async function copyTemplates(outputDir) {
|
|
|
277
310
|
'__loading__.js',
|
|
278
311
|
'playcanvas-stable.min.js',
|
|
279
312
|
'styles.css',
|
|
280
|
-
'logo.png',
|
|
281
313
|
'manifest.json',
|
|
282
314
|
];
|
|
283
315
|
for (const file of templateFiles) {
|
|
@@ -373,11 +405,21 @@ function collectRegisteredScriptNames(assets) {
|
|
|
373
405
|
if (scriptName) {
|
|
374
406
|
scriptNames.add(scriptName);
|
|
375
407
|
}
|
|
376
|
-
//
|
|
377
|
-
if (asset.data?.scripts
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
408
|
+
// Classic 格式的 data.scripts 可能是对象或数组
|
|
409
|
+
if (asset.data?.scripts) {
|
|
410
|
+
if (Array.isArray(asset.data.scripts)) {
|
|
411
|
+
// 数组格式: [{ name: 'xxx' }, ...]
|
|
412
|
+
for (const s of asset.data.scripts) {
|
|
413
|
+
if (s?.name)
|
|
414
|
+
scriptNames.add(s.name);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (typeof asset.data.scripts === 'object') {
|
|
418
|
+
// 对象格式: { "followCamera": { attributes: {...} }, ... }
|
|
419
|
+
// 这是 PlayCanvas/PlayCraft 中 Classic 脚本的标准格式
|
|
420
|
+
for (const scriptKey of Object.keys(asset.data.scripts)) {
|
|
421
|
+
scriptNames.add(scriptKey);
|
|
422
|
+
}
|
|
381
423
|
}
|
|
382
424
|
}
|
|
383
425
|
}
|
|
@@ -542,8 +584,28 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
|
|
|
542
584
|
let notFoundCount = 0;
|
|
543
585
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
544
586
|
const assetData = asset;
|
|
545
|
-
//
|
|
546
|
-
if (!assetData.file
|
|
587
|
+
// 跳过没有 file 的资源
|
|
588
|
+
if (!assetData.file) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// 如果已有有效的本地 url(非 API URL),验证文件是否存在
|
|
592
|
+
if (assetData.file.url && !assetData.file.url.startsWith('/api/') && !assetData.file.url.startsWith('http')) {
|
|
593
|
+
// 已有有效的本地 url,验证文件存在性
|
|
594
|
+
try {
|
|
595
|
+
await fs.access(path.join(projectDir, assetData.file.url));
|
|
596
|
+
continue; // 文件存在,跳过
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// 文件不存在,清除 url 继续搜索
|
|
600
|
+
delete assetData.file.url;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// 如果 url 是 API URL(PlayCraft 格式残留),清除它
|
|
604
|
+
if (assetData.file.url && (assetData.file.url.startsWith('/api/') || assetData.file.url.startsWith('http'))) {
|
|
605
|
+
delete assetData.file.url;
|
|
606
|
+
}
|
|
607
|
+
// 如果已经有有效 url 了(由之前的 loader 设置的),跳过
|
|
608
|
+
if (assetData.file.url) {
|
|
547
609
|
continue;
|
|
548
610
|
}
|
|
549
611
|
const filename = assetData.file.filename;
|
|
@@ -552,11 +614,19 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
|
|
|
552
614
|
}
|
|
553
615
|
const revision = assetData.revision ?? assetData.file.revision ?? 1;
|
|
554
616
|
const candidates = [
|
|
617
|
+
// PlayCanvas 标准结构
|
|
555
618
|
path.join('files', 'assets', String(assetId), String(revision), filename),
|
|
556
619
|
path.join('files', 'assets', String(assetId), '1', filename),
|
|
557
620
|
path.join('files', 'assets', String(assetId), filename),
|
|
558
621
|
path.join('files', filename),
|
|
559
622
|
];
|
|
623
|
+
// PlayCraft 结构:使用 treePath 或 path 字段推断文件位置
|
|
624
|
+
if (assetData.treePath) {
|
|
625
|
+
candidates.push(path.join(assetData.treePath, filename));
|
|
626
|
+
}
|
|
627
|
+
if (assetData.path && typeof assetData.path === 'string') {
|
|
628
|
+
candidates.push(assetData.path);
|
|
629
|
+
}
|
|
560
630
|
let found = false;
|
|
561
631
|
for (const rel of candidates) {
|
|
562
632
|
const abs = path.join(projectDir, rel);
|
|
@@ -699,7 +769,6 @@ async function generateESMTemplate(projectConfig, options, config) {
|
|
|
699
769
|
'__modules__.js',
|
|
700
770
|
'__loading__.js',
|
|
701
771
|
'styles.css',
|
|
702
|
-
'logo.png',
|
|
703
772
|
'manifest.json',
|
|
704
773
|
];
|
|
705
774
|
for (const file of templateFiles) {
|
|
@@ -737,12 +806,19 @@ function fixImportMapPaths(importMap) {
|
|
|
737
806
|
* 收集 ESM 脚本的导入路径
|
|
738
807
|
*/
|
|
739
808
|
async function collectESMScriptImports(projectConfig, options) {
|
|
740
|
-
|
|
741
|
-
|
|
809
|
+
// 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
|
|
810
|
+
let scriptIds;
|
|
811
|
+
let assets;
|
|
812
|
+
if (projectConfig.format === 'playcanvas') {
|
|
813
|
+
const pcProject = projectConfig;
|
|
814
|
+
scriptIds = pcProject.project.settings?.scripts || [];
|
|
815
|
+
assets = pcProject.assets;
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const pcProject = projectConfig;
|
|
819
|
+
scriptIds = pcProject.manifest.settings?.scripts || [];
|
|
820
|
+
assets = pcProject.assets;
|
|
742
821
|
}
|
|
743
|
-
const pcProject = projectConfig;
|
|
744
|
-
const scriptIds = pcProject.project.settings?.scripts || [];
|
|
745
|
-
const assets = pcProject.assets;
|
|
746
822
|
const importPaths = [];
|
|
747
823
|
// 构建文件夹 ID 到名称的映射
|
|
748
824
|
const folderMap = new Map();
|
|
@@ -868,14 +944,19 @@ async function generateESMEntry(projectConfig, outputDir, scriptImportPaths = []
|
|
|
868
944
|
async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, importMap) {
|
|
869
945
|
const scriptUrlMap = new Map();
|
|
870
946
|
console.log(`[copyUserScriptsForESM] 项目格式: ${projectConfig.format}`);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
947
|
+
// 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
|
|
948
|
+
let scriptIds;
|
|
949
|
+
let assets;
|
|
950
|
+
if (projectConfig.format === 'playcanvas') {
|
|
951
|
+
const pcProject = projectConfig;
|
|
952
|
+
scriptIds = pcProject.project.settings?.scripts || [];
|
|
953
|
+
assets = pcProject.assets;
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
const pcProject = projectConfig;
|
|
957
|
+
scriptIds = pcProject.manifest.settings?.scripts || [];
|
|
958
|
+
assets = pcProject.assets;
|
|
875
959
|
}
|
|
876
|
-
const pcProject = projectConfig;
|
|
877
|
-
const scriptIds = pcProject.project.settings?.scripts || [];
|
|
878
|
-
const assets = pcProject.assets;
|
|
879
960
|
console.log(`[copyUserScriptsForESM] 脚本数量: ${scriptIds.length}`);
|
|
880
961
|
// 构建文件夹 ID 到名称的映射
|
|
881
962
|
const folderMap = new Map();
|