@playcraft/build 0.0.43 → 0.0.45
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/engines/playable-scripts-adapter.d.ts +1 -1
- package/dist/engines/playable-scripts-adapter.js +55 -21
- package/dist/platforms/bigo.js +3 -5
- package/dist/platforms/inmobi.js +3 -5
- package/dist/platforms/liftoff.js +3 -5
- package/dist/platforms/mintegral.js +4 -6
- package/dist/platforms/snapchat.js +1 -1
- package/dist/platforms/tiktok.js +3 -5
- package/dist/vite/platform-configs.js +7 -7
- package/dist/vite/plugin-playcanvas.js +151 -46
- package/package.json +1 -1
- package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +0 -1
- package/dist/analyzers/__tests__/optimization-analyzer.test.js +0 -169
- package/dist/templates/__loading__.js +0 -100
- package/dist/templates/__modules__.js +0 -47
- package/dist/templates/__settings__.template.js +0 -20
- package/dist/templates/__start__.js +0 -332
- package/dist/templates/index.html +0 -18
- package/dist/templates/logo.png +0 -0
- package/dist/templates/manifest.json +0 -1
- package/dist/templates/patches/cannon.min.js +0 -28
- package/dist/templates/patches/lz4.js +0 -10
- package/dist/templates/patches/one-page-http-get.js +0 -20
- package/dist/templates/patches/one-page-inline-game-scripts.js +0 -52
- package/dist/templates/patches/one-page-mraid-resize-canvas.js +0 -46
- package/dist/templates/patches/p2.min.js +0 -27
- package/dist/templates/patches/playcraft-no-xhr.js +0 -76
- package/dist/templates/playcanvas-stable.min.js +0 -16363
- package/dist/templates/styles.css +0 -43
|
@@ -64,6 +64,7 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
64
64
|
// 8. 处理 manifest.json
|
|
65
65
|
html = await inlineManifest(html, options.baseBuildDir, options);
|
|
66
66
|
}
|
|
67
|
+
html = await compressInlineBuildJsonDataUrls(html, options);
|
|
67
68
|
}
|
|
68
69
|
else if (options.outputFormat === 'zip') {
|
|
69
70
|
// ZIP 格式:不内联引擎,但需要在 IIFE 之前添加引擎脚本标签
|
|
@@ -87,6 +88,20 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
87
88
|
}
|
|
88
89
|
return code;
|
|
89
90
|
},
|
|
91
|
+
async generateBundle(_outputOptions, bundle) {
|
|
92
|
+
if (options.outputFormat !== 'html') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const asset of Object.values(bundle)) {
|
|
96
|
+
if (asset.type !== 'asset' || !asset.fileName.endsWith('.html')) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const source = typeof asset.source === 'string'
|
|
100
|
+
? asset.source
|
|
101
|
+
: new TextDecoder().decode(asset.source);
|
|
102
|
+
asset.source = await compressInlineBuildJsonDataUrls(source, options);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
90
105
|
};
|
|
91
106
|
}
|
|
92
107
|
/**
|
|
@@ -184,7 +199,7 @@ async function generateAndInlineSettings(html, baseBuildDir, options, pluginCont
|
|
|
184
199
|
configJson = applyMraidConfig(configJson);
|
|
185
200
|
}
|
|
186
201
|
// 生成 config 值
|
|
187
|
-
const configValue = buildConfigValue(configJson);
|
|
202
|
+
const configValue = await buildConfigValue(configJson, options, 'config.json');
|
|
188
203
|
// 生成 scene data URL
|
|
189
204
|
let sceneDataUrl = '';
|
|
190
205
|
if (configJson.scenes && configJson.scenes.length > 0) {
|
|
@@ -193,14 +208,14 @@ async function generateAndInlineSettings(html, baseBuildDir, options, pluginCont
|
|
|
193
208
|
const scenePath = path.join(baseBuildDir, sceneUrl);
|
|
194
209
|
try {
|
|
195
210
|
const sceneContent = await fs.readFile(scenePath, 'utf-8');
|
|
196
|
-
sceneDataUrl = `
|
|
211
|
+
sceneDataUrl = await buildJsonDataUrl(sceneContent, options, `scene ${sceneUrl}`);
|
|
197
212
|
}
|
|
198
213
|
catch (error) {
|
|
199
214
|
console.warn(`警告: 场景文件不存在: ${sceneUrl}`);
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
else {
|
|
203
|
-
sceneDataUrl = sceneUrl;
|
|
218
|
+
sceneDataUrl = await normalizeJsonDataUrl(sceneUrl || '', options, `scene ${sceneUrl || 'inline data URL'}`);
|
|
204
219
|
}
|
|
205
220
|
}
|
|
206
221
|
const appProps = configJson.application_properties || {};
|
|
@@ -292,7 +307,7 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
292
307
|
const scenePath = path.join(baseBuildDir, sceneUrl);
|
|
293
308
|
try {
|
|
294
309
|
const sceneContent = await fs.readFile(scenePath, 'utf-8');
|
|
295
|
-
sceneDataUrl = `
|
|
310
|
+
sceneDataUrl = await buildJsonDataUrl(sceneContent, options, `scene ${sceneUrl}`);
|
|
296
311
|
console.log(`[PlayCanvasPlugin] ESM Bundle: 场景已内联: ${sceneUrl}`);
|
|
297
312
|
}
|
|
298
313
|
catch (error) {
|
|
@@ -300,7 +315,7 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
300
315
|
}
|
|
301
316
|
}
|
|
302
317
|
else {
|
|
303
|
-
sceneDataUrl = sceneUrl || '';
|
|
318
|
+
sceneDataUrl = await normalizeJsonDataUrl(sceneUrl || '', options, `scene ${sceneUrl || 'inline data URL'}`);
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
321
|
// 7. (Logo removed - no longer needed)
|
|
@@ -326,7 +341,7 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
326
341
|
const escapedSceneDataUrl = sceneDataUrl.replace(/\$/g, '$$$$');
|
|
327
342
|
// 查找 SCENE_PATH 的各种格式并替换
|
|
328
343
|
// 格式: const SCENE_PATH = "xxx.json" 或 SCENE_PATH = "xxx"
|
|
329
|
-
html = html.replace(/SCENE_PATH\s*=\s*["'][^"']+\.json["']/g, `SCENE_PATH = "${escapedSceneDataUrl}"`);
|
|
344
|
+
html = html.replace(/SCENE_PATH\s*=\s*["'](?:[^"']+\.json|data:application\/json;base64,[^"']+)["']/g, `SCENE_PATH = "${escapedSceneDataUrl}"`);
|
|
330
345
|
// ⚠️ Vite 压缩后格式:loadScene("xxx.json" 或 loadScene(变量名,
|
|
331
346
|
// 需要同时替换 loadScene 调用中的场景路径字符串
|
|
332
347
|
// 格式1: .loadScene("2412781.json", ...)
|
|
@@ -349,6 +364,10 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
349
364
|
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${varDefReplaceCount} 处场景路径变量定义`);
|
|
350
365
|
}
|
|
351
366
|
}
|
|
367
|
+
else if (sceneUrl && sceneUrl !== sceneDataUrl && sceneUrl.startsWith('data:application/json;base64,')) {
|
|
368
|
+
html = html.split(sceneUrl).join(escapedSceneDataUrl);
|
|
369
|
+
console.log('[PlayCanvasPlugin] ESM Bundle: 已重新压缩内联场景 data URL');
|
|
370
|
+
}
|
|
352
371
|
}
|
|
353
372
|
// 10. 处理 PRELOAD_MODULES(WASM 模块)
|
|
354
373
|
// 在 ESM Bundle 模式下,需要将 PRELOAD_MODULES 中的 URL 也转换为 data URL
|
|
@@ -441,7 +460,7 @@ async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options, pl
|
|
|
441
460
|
// 1. 转换 config.json(传递插件上下文)
|
|
442
461
|
settingsCode = await convertConfigUrl(settingsCode, baseBuildDir, options, pluginContext);
|
|
443
462
|
// 2. 转换场景文件
|
|
444
|
-
settingsCode = await convertSceneUrl(settingsCode, baseBuildDir);
|
|
463
|
+
settingsCode = await convertSceneUrl(settingsCode, baseBuildDir, options);
|
|
445
464
|
// 3. 转换 PRELOAD_MODULES(优先使用 JS fallback)
|
|
446
465
|
settingsCode = await convertPreloadModules(settingsCode, baseBuildDir, options);
|
|
447
466
|
return settingsCode;
|
|
@@ -471,7 +490,7 @@ async function convertConfigUrl(settingsCode, baseBuildDir, options, pluginConte
|
|
|
471
490
|
if (options.mraidSupport) {
|
|
472
491
|
configJson = applyMraidConfig(configJson);
|
|
473
492
|
}
|
|
474
|
-
const configValue = buildConfigValue(configJson);
|
|
493
|
+
const configValue = await buildConfigValue(configJson, options, 'config.json');
|
|
475
494
|
return settingsCode.replace(/window\.CONFIG_FILENAME\s*=\s*"[^"]+"/, `window.CONFIG_FILENAME = ${configValue}`);
|
|
476
495
|
}
|
|
477
496
|
catch (error) {
|
|
@@ -482,19 +501,23 @@ async function convertConfigUrl(settingsCode, baseBuildDir, options, pluginConte
|
|
|
482
501
|
/**
|
|
483
502
|
* 转换 SCENE_PATH 为 data URL
|
|
484
503
|
*/
|
|
485
|
-
async function convertSceneUrl(settingsCode, baseBuildDir) {
|
|
504
|
+
async function convertSceneUrl(settingsCode, baseBuildDir, options) {
|
|
486
505
|
const sceneMatch = settingsCode.match(/window\.SCENE_PATH\s*=\s*"([^"]+)"/);
|
|
487
506
|
if (!sceneMatch) {
|
|
488
507
|
return settingsCode;
|
|
489
508
|
}
|
|
490
509
|
const scenePath = sceneMatch[1];
|
|
491
510
|
if (scenePath.startsWith('data:') || !scenePath) {
|
|
492
|
-
|
|
511
|
+
const sceneDataUrl = await normalizeJsonDataUrl(scenePath, options, `scene ${scenePath ? 'inline data URL' : 'empty'}`);
|
|
512
|
+
if (sceneDataUrl === scenePath) {
|
|
513
|
+
return settingsCode; // 已经是 data URL 或为空
|
|
514
|
+
}
|
|
515
|
+
return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
|
|
493
516
|
}
|
|
494
517
|
const fullScenePath = path.join(baseBuildDir, scenePath);
|
|
495
518
|
try {
|
|
496
519
|
const sceneContent = await fs.readFile(fullScenePath, 'utf-8');
|
|
497
|
-
const sceneDataUrl = `
|
|
520
|
+
const sceneDataUrl = await buildJsonDataUrl(sceneContent, options, `scene ${scenePath}`);
|
|
498
521
|
return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
|
|
499
522
|
}
|
|
500
523
|
catch (error) {
|
|
@@ -911,6 +934,30 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
911
934
|
const skippedScripts = [];
|
|
912
935
|
const missingAssets = []; // 新增:缺失资源列表
|
|
913
936
|
const SIZE_LIMIT = 1 * 1024 * 1024; // 1MB - 跳过超过这个大小的文件
|
|
937
|
+
const clearDataUrlMetadata = (targetFile) => {
|
|
938
|
+
if ('hash' in targetFile) {
|
|
939
|
+
delete targetFile.hash;
|
|
940
|
+
}
|
|
941
|
+
if ('variants' in targetFile) {
|
|
942
|
+
delete targetFile.variants;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const usePlaceholderAsset = (targetAsset, targetFile, targetFileName, size) => {
|
|
946
|
+
targetFile.url = getPlaceholderDataUrl(targetAsset.type, targetFileName);
|
|
947
|
+
if (size !== undefined) {
|
|
948
|
+
targetFile.size = size;
|
|
949
|
+
}
|
|
950
|
+
clearDataUrlMetadata(targetFile);
|
|
951
|
+
targetAsset.preload = false;
|
|
952
|
+
};
|
|
953
|
+
const isImageAsset = (targetAsset, targetFile, targetFileName) => {
|
|
954
|
+
const hasImageFile = isImageFile(targetFileName) ||
|
|
955
|
+
isImageFile(targetFile?.filename || '') ||
|
|
956
|
+
isImageFile(targetAsset.name || '');
|
|
957
|
+
return (targetAsset.type === 'texture' ||
|
|
958
|
+
targetAsset.type === 'textureatlas' ||
|
|
959
|
+
hasImageFile);
|
|
960
|
+
};
|
|
914
961
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
915
962
|
const file = asset?.file;
|
|
916
963
|
if (!file?.url || typeof file.url !== 'string') {
|
|
@@ -922,32 +969,22 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
922
969
|
}
|
|
923
970
|
const cleanUrl = url.split('?')[0];
|
|
924
971
|
const fileName = cleanUrl.toLowerCase();
|
|
972
|
+
const imageAsset = isImageAsset(asset, file, fileName);
|
|
925
973
|
// ⚠️ 脚本资源特殊处理(参考 PlayCanvas 官方 one-page.js 实现)
|
|
926
974
|
// ESM Bundle 模式下,脚本代码已被打包到 IIFE 中执行
|
|
927
975
|
// PlayCanvas 官方做法:将脚本内容设为空,配合引擎补丁跳过执行
|
|
928
976
|
if (cleanUrl === '__game-scripts.js') {
|
|
929
|
-
// __game-scripts.js 是 Classic 模式的打包产物
|
|
930
977
|
skippedScripts.push(asset.name || assetId);
|
|
931
|
-
// 使用空内容的 data URL(PlayCanvas 官方做法)
|
|
932
978
|
file.url = 'data:text/javascript;base64,';
|
|
933
|
-
file
|
|
979
|
+
clearDataUrlMetadata(file);
|
|
934
980
|
asset.preload = false;
|
|
935
981
|
continue;
|
|
936
982
|
}
|
|
937
983
|
if (asset.type === 'script') {
|
|
938
984
|
skippedScripts.push(asset.name || assetId);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
//
|
|
943
|
-
// 我们的方案:
|
|
944
|
-
// 1. 将脚本 URL 设为空内容的 data URL
|
|
945
|
-
// 2. 清空 hash 避免追加查询参数
|
|
946
|
-
// 3. 脚本代码已经通过 IIFE 中的 pc.createScript() 注册
|
|
947
|
-
// 4. 需要配合引擎补丁让 ScriptHandler 跳过空脚本
|
|
948
|
-
file.url = 'data:text/javascript;base64,'; // 空 JavaScript
|
|
949
|
-
file.hash = ''; // 清空 hash
|
|
950
|
-
asset.preload = false; // 禁用预加载
|
|
985
|
+
file.url = 'data:text/javascript;base64,';
|
|
986
|
+
clearDataUrlMetadata(file);
|
|
987
|
+
asset.preload = false;
|
|
951
988
|
continue;
|
|
952
989
|
}
|
|
953
990
|
// 跳过物理引擎缓存文件和大型文本文件
|
|
@@ -956,10 +993,7 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
956
993
|
fileName.includes('deferredbrowsermetrics') ||
|
|
957
994
|
(fileName.endsWith('.txt') && file.size && file.size > SIZE_LIMIT)) {
|
|
958
995
|
skippedAssets.push(`${asset.name || assetId} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
|
|
959
|
-
|
|
960
|
-
asset.preload = false;
|
|
961
|
-
// 提供有效的占位符数据,避免解析错误
|
|
962
|
-
file.url = getPlaceholderDataUrl(asset.type, fileName);
|
|
996
|
+
usePlaceholderAsset(asset, file, fileName);
|
|
963
997
|
continue;
|
|
964
998
|
}
|
|
965
999
|
const fullPath = path.join(baseBuildDir, cleanUrl);
|
|
@@ -970,16 +1004,13 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
970
1004
|
// 检查空文件,提供有效的占位符
|
|
971
1005
|
if (originalSize === 0) {
|
|
972
1006
|
console.warn(`⚠️ 警告: 文件为空,使用占位符: ${asset.name || cleanUrl}`);
|
|
973
|
-
|
|
974
|
-
file.size = 0;
|
|
1007
|
+
usePlaceholderAsset(asset, file, fileName, 0);
|
|
975
1008
|
continue;
|
|
976
1009
|
}
|
|
977
|
-
//
|
|
978
|
-
if (originalSize > SIZE_LIMIT) {
|
|
1010
|
+
// Non-image files cannot be optimized here, so keep the original size guard.
|
|
1011
|
+
if (originalSize > SIZE_LIMIT && !imageAsset) {
|
|
979
1012
|
skippedAssets.push(`${asset.name || assetId} (${(originalSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
980
|
-
asset
|
|
981
|
-
// 提供有效的占位符数据,避免解析错误
|
|
982
|
-
file.url = getPlaceholderDataUrl(asset.type, fileName);
|
|
1013
|
+
usePlaceholderAsset(asset, file, fileName);
|
|
983
1014
|
continue;
|
|
984
1015
|
}
|
|
985
1016
|
let dataUrl;
|
|
@@ -1011,7 +1042,7 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
1011
1042
|
// ⚠️ 重要:字体纹理(type === 'font')不能转换为 WebP!
|
|
1012
1043
|
// MSDF/位图字体的 PNG 纹理包含精确的距离场/像素数据,
|
|
1013
1044
|
// WebP 有损压缩会破坏这些数据导致字体渲染异常
|
|
1014
|
-
else if (
|
|
1045
|
+
else if (imageAsset) {
|
|
1015
1046
|
const isFontTexture = asset.type === 'font';
|
|
1016
1047
|
const compressed = await compressImage(buffer, cleanUrl, {
|
|
1017
1048
|
convertToWebP: !isFontTexture, // 字体纹理不转换为 WebP
|
|
@@ -1048,16 +1079,19 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
1048
1079
|
dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
|
|
1049
1080
|
finalSize = originalSize;
|
|
1050
1081
|
}
|
|
1082
|
+
if (finalSize > SIZE_LIMIT && !imageAsset) {
|
|
1083
|
+
skippedAssets.push(`${asset.name || assetId} (${(finalSize / 1024 / 1024).toFixed(2)}MB after optimization)`);
|
|
1084
|
+
usePlaceholderAsset(asset, file, fileName, finalSize);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (finalSize > SIZE_LIMIT && imageAsset) {
|
|
1088
|
+
console.warn(`[PlayCanvasPlugin] Image asset kept as data URL even though it exceeds inline limit: ${asset.name || assetId} (${(finalSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
1089
|
+
}
|
|
1051
1090
|
// 更新 asset 配置
|
|
1052
1091
|
file.url = dataUrl;
|
|
1053
1092
|
file.size = finalSize;
|
|
1054
1093
|
// data URL 不需要 hash/variants,避免引擎追加 ?t=hash 造成无效 URL
|
|
1055
|
-
|
|
1056
|
-
delete file.hash;
|
|
1057
|
-
}
|
|
1058
|
-
if ('variants' in file) {
|
|
1059
|
-
delete file.variants;
|
|
1060
|
-
}
|
|
1094
|
+
clearDataUrlMetadata(file);
|
|
1061
1095
|
}
|
|
1062
1096
|
catch (error) {
|
|
1063
1097
|
// 记录缺失资源详情,不只是简单警告
|
|
@@ -1284,9 +1318,80 @@ function guessMimeType(filePath) {
|
|
|
1284
1318
|
return 'application/octet-stream';
|
|
1285
1319
|
}
|
|
1286
1320
|
}
|
|
1287
|
-
function buildConfigValue(configJson) {
|
|
1321
|
+
async function buildConfigValue(configJson, options, label) {
|
|
1288
1322
|
const configText = JSON.stringify(configJson);
|
|
1289
|
-
|
|
1323
|
+
const dataUrl = await buildJsonDataUrl(configText, options, label);
|
|
1324
|
+
return `"${dataUrl}"`;
|
|
1325
|
+
}
|
|
1326
|
+
async function buildJsonDataUrl(jsonText, options, label) {
|
|
1327
|
+
let compactJsonText = jsonText;
|
|
1328
|
+
try {
|
|
1329
|
+
compactJsonText = JSON.stringify(JSON.parse(jsonText));
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// Keep the original text if it is not strict JSON.
|
|
1333
|
+
}
|
|
1334
|
+
if (!options.compressConfigJson) {
|
|
1335
|
+
return `data:application/json;base64,${Buffer.from(compactJsonText).toString('base64')}`;
|
|
1336
|
+
}
|
|
1337
|
+
const lz4 = await loadLz4Module();
|
|
1338
|
+
const source = Buffer.from(compactJsonText);
|
|
1339
|
+
const compressed = lz4.compress(source);
|
|
1340
|
+
const compressedBase64 = Buffer.from(compressed).toString('base64');
|
|
1341
|
+
const ratio = ((1 - compressed.length / source.length) * 100).toFixed(1);
|
|
1342
|
+
console.log(`[PlayCanvasPlugin] ${label} compressed (${formatBytes(source.length)} -> ${formatBytes(compressed.length)}, -${ratio}%)`);
|
|
1343
|
+
return `data:application/x-lz4-json;base64,${compressedBase64}`;
|
|
1344
|
+
}
|
|
1345
|
+
async function normalizeJsonDataUrl(dataUrl, options, label) {
|
|
1346
|
+
const plainJsonPrefix = 'data:application/json;base64,';
|
|
1347
|
+
if (!options.compressConfigJson || !dataUrl.startsWith(plainJsonPrefix)) {
|
|
1348
|
+
return dataUrl;
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
const jsonText = Buffer.from(dataUrl.slice(plainJsonPrefix.length), 'base64').toString('utf-8');
|
|
1352
|
+
return await buildJsonDataUrl(jsonText, options, label);
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
console.warn(`[PlayCanvasPlugin] 无法重新压缩 JSON data URL: ${label}`, error);
|
|
1356
|
+
return dataUrl;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function compressInlineBuildJsonDataUrls(html, options) {
|
|
1360
|
+
if (!options.compressConfigJson) {
|
|
1361
|
+
return html;
|
|
1362
|
+
}
|
|
1363
|
+
const pattern = /data:application\/json;base64,[A-Za-z0-9+/=]+/g;
|
|
1364
|
+
let nextHtml = '';
|
|
1365
|
+
let lastIndex = 0;
|
|
1366
|
+
let compressedCount = 0;
|
|
1367
|
+
for (const match of html.matchAll(pattern)) {
|
|
1368
|
+
const dataUrl = match[0];
|
|
1369
|
+
const index = match.index ?? 0;
|
|
1370
|
+
try {
|
|
1371
|
+
const jsonText = Buffer
|
|
1372
|
+
.from(dataUrl.slice('data:application/json;base64,'.length), 'base64')
|
|
1373
|
+
.toString('utf-8');
|
|
1374
|
+
const parsed = JSON.parse(jsonText);
|
|
1375
|
+
const isSceneJson = parsed && typeof parsed === 'object' && parsed.entities && parsed.settings;
|
|
1376
|
+
const isConfigJson = parsed && typeof parsed === 'object' && parsed.application_properties && parsed.assets;
|
|
1377
|
+
if (!isSceneJson && !isConfigJson) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const label = isSceneJson ? 'inline scene data URL' : 'inline config data URL';
|
|
1381
|
+
const compressedDataUrl = await buildJsonDataUrl(jsonText, options, label);
|
|
1382
|
+
nextHtml += html.slice(lastIndex, index) + compressedDataUrl;
|
|
1383
|
+
lastIndex = index + dataUrl.length;
|
|
1384
|
+
compressedCount++;
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (compressedCount === 0) {
|
|
1391
|
+
return html;
|
|
1392
|
+
}
|
|
1393
|
+
console.log(`[PlayCanvasPlugin] 重新压缩 ${compressedCount} 个内联 JSON data URL`);
|
|
1394
|
+
return nextHtml + html.slice(lastIndex);
|
|
1290
1395
|
}
|
|
1291
1396
|
function applyMraidConfig(configJson) {
|
|
1292
1397
|
const next = { ...configJson };
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { OptimizationAnalyzer } from '../optimization-analyzer.js';
|
|
3
|
-
describe('OptimizationAnalyzer', () => {
|
|
4
|
-
it('should detect large uncompressed files', () => {
|
|
5
|
-
const state = {
|
|
6
|
-
version: '1.0.0',
|
|
7
|
-
buildTime: Date.now(),
|
|
8
|
-
assets: {
|
|
9
|
-
'large-file': {
|
|
10
|
-
id: 'large-file',
|
|
11
|
-
originalName: 'large-file.js',
|
|
12
|
-
originalPath: '/path/to/large-file.js',
|
|
13
|
-
originalSize: 200 * 1024, // 200KB
|
|
14
|
-
finalSize: 195 * 1024, // 只压缩了 5KB
|
|
15
|
-
totalCompressionRatio: 0.025, // 2.5% 压缩率
|
|
16
|
-
type: 'script',
|
|
17
|
-
processingHistory: [
|
|
18
|
-
{
|
|
19
|
-
stage: 'base-build',
|
|
20
|
-
name: 'large-file.js',
|
|
21
|
-
size: 195 * 1024,
|
|
22
|
-
optimizations: [],
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
stages: [],
|
|
28
|
-
};
|
|
29
|
-
const analyzer = new OptimizationAnalyzer(state);
|
|
30
|
-
const result = analyzer.analyze();
|
|
31
|
-
expect(result.totalSuggestions).toBeGreaterThan(0);
|
|
32
|
-
expect(result.suggestions.some(s => s.type === 'large-uncompressed')).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
it('should detect PNG images that can be converted to WebP', () => {
|
|
35
|
-
const state = {
|
|
36
|
-
version: '1.0.0',
|
|
37
|
-
buildTime: Date.now(),
|
|
38
|
-
assets: {
|
|
39
|
-
'image': {
|
|
40
|
-
id: 'image',
|
|
41
|
-
originalName: 'image.png',
|
|
42
|
-
originalPath: '/path/to/image.png',
|
|
43
|
-
originalSize: 100 * 1024, // 100KB
|
|
44
|
-
finalSize: 100 * 1024,
|
|
45
|
-
totalCompressionRatio: 0,
|
|
46
|
-
type: 'texture',
|
|
47
|
-
processingHistory: [
|
|
48
|
-
{
|
|
49
|
-
stage: 'base-build',
|
|
50
|
-
name: 'image.png',
|
|
51
|
-
size: 100 * 1024,
|
|
52
|
-
optimizations: [],
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
stages: [],
|
|
58
|
-
};
|
|
59
|
-
const analyzer = new OptimizationAnalyzer(state);
|
|
60
|
-
const result = analyzer.analyze();
|
|
61
|
-
expect(result.suggestions.some(s => s.type === 'format-conversion')).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
it('should detect missing minification', () => {
|
|
64
|
-
const state = {
|
|
65
|
-
version: '1.0.0',
|
|
66
|
-
buildTime: Date.now(),
|
|
67
|
-
assets: {
|
|
68
|
-
'script': {
|
|
69
|
-
id: 'script',
|
|
70
|
-
originalName: 'script.js',
|
|
71
|
-
originalPath: '/path/to/script.js',
|
|
72
|
-
originalSize: 50 * 1024, // 50KB
|
|
73
|
-
finalSize: 50 * 1024,
|
|
74
|
-
totalCompressionRatio: 0,
|
|
75
|
-
type: 'script',
|
|
76
|
-
processingHistory: [
|
|
77
|
-
{
|
|
78
|
-
stage: 'base-build',
|
|
79
|
-
name: 'script.js',
|
|
80
|
-
size: 50 * 1024,
|
|
81
|
-
optimizations: [], // 没有 minify
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
stages: [],
|
|
87
|
-
};
|
|
88
|
-
const analyzer = new OptimizationAnalyzer(state);
|
|
89
|
-
const result = analyzer.analyze();
|
|
90
|
-
expect(result.suggestions.some(s => s.type === 'enable-minify')).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
it('should group suggestions by severity', () => {
|
|
93
|
-
const state = {
|
|
94
|
-
version: '1.0.0',
|
|
95
|
-
buildTime: Date.now(),
|
|
96
|
-
assets: {
|
|
97
|
-
'large-uncompressed': {
|
|
98
|
-
id: 'large-uncompressed',
|
|
99
|
-
originalName: 'large.js',
|
|
100
|
-
originalPath: '/path/to/large.js',
|
|
101
|
-
originalSize: 200 * 1024,
|
|
102
|
-
finalSize: 195 * 1024,
|
|
103
|
-
totalCompressionRatio: 0.025,
|
|
104
|
-
type: 'script',
|
|
105
|
-
processingHistory: [
|
|
106
|
-
{
|
|
107
|
-
stage: 'base-build',
|
|
108
|
-
name: 'large.js',
|
|
109
|
-
size: 195 * 1024,
|
|
110
|
-
optimizations: [],
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
'png-image': {
|
|
115
|
-
id: 'png-image',
|
|
116
|
-
originalName: 'image.png',
|
|
117
|
-
originalPath: '/path/to/image.png',
|
|
118
|
-
originalSize: 100 * 1024,
|
|
119
|
-
finalSize: 100 * 1024,
|
|
120
|
-
totalCompressionRatio: 0,
|
|
121
|
-
type: 'texture',
|
|
122
|
-
processingHistory: [
|
|
123
|
-
{
|
|
124
|
-
stage: 'base-build',
|
|
125
|
-
name: 'image.png',
|
|
126
|
-
size: 100 * 1024,
|
|
127
|
-
optimizations: [],
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
stages: [],
|
|
133
|
-
};
|
|
134
|
-
const analyzer = new OptimizationAnalyzer(state);
|
|
135
|
-
const result = analyzer.analyze();
|
|
136
|
-
expect(result.bySeverity.high.length).toBeGreaterThan(0);
|
|
137
|
-
expect(result.bySeverity.medium.length).toBeGreaterThan(0);
|
|
138
|
-
});
|
|
139
|
-
it('should calculate total estimated savings', () => {
|
|
140
|
-
const state = {
|
|
141
|
-
version: '1.0.0',
|
|
142
|
-
buildTime: Date.now(),
|
|
143
|
-
assets: {
|
|
144
|
-
'large-file': {
|
|
145
|
-
id: 'large-file',
|
|
146
|
-
originalName: 'large-file.js',
|
|
147
|
-
originalPath: '/path/to/large-file.js',
|
|
148
|
-
originalSize: 200 * 1024,
|
|
149
|
-
finalSize: 195 * 1024,
|
|
150
|
-
totalCompressionRatio: 0.025,
|
|
151
|
-
type: 'script',
|
|
152
|
-
processingHistory: [
|
|
153
|
-
{
|
|
154
|
-
stage: 'base-build',
|
|
155
|
-
name: 'large-file.js',
|
|
156
|
-
size: 195 * 1024,
|
|
157
|
-
optimizations: [],
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
stages: [],
|
|
163
|
-
};
|
|
164
|
-
const analyzer = new OptimizationAnalyzer(state);
|
|
165
|
-
const result = analyzer.analyze();
|
|
166
|
-
expect(result.totalEstimatedSavings).toBeGreaterThan(0);
|
|
167
|
-
expect(result.estimatedOptimizationRate).toBeGreaterThan(0);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
pc.script.createLoadingScreen((app) => {
|
|
2
|
-
const createCss = () => {
|
|
3
|
-
const css = `
|
|
4
|
-
body {
|
|
5
|
-
background-color: #283538;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
#application-splash-wrapper {
|
|
9
|
-
position: absolute;
|
|
10
|
-
top: 0;
|
|
11
|
-
left: 0;
|
|
12
|
-
height: 100%;
|
|
13
|
-
width: 100%;
|
|
14
|
-
background-color: #283538;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#application-splash {
|
|
18
|
-
position: absolute;
|
|
19
|
-
top: calc(50% - 28px);
|
|
20
|
-
width: 264px;
|
|
21
|
-
left: calc(50% - 132px);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
#application-splash img {
|
|
25
|
-
width: 100%;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#progress-bar-container {
|
|
29
|
-
margin: 20px auto 0 auto;
|
|
30
|
-
height: 2px;
|
|
31
|
-
width: 100%;
|
|
32
|
-
background-color: #1d292c;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
#progress-bar {
|
|
36
|
-
width: 0%;
|
|
37
|
-
height: 100%;
|
|
38
|
-
background-color: #f60;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@media (max-width: 480px) {
|
|
42
|
-
#application-splash {
|
|
43
|
-
width: 170px;
|
|
44
|
-
left: calc(50% - 85px);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
`;
|
|
48
|
-
|
|
49
|
-
const style = document.createElement('style');
|
|
50
|
-
style.textContent = css;
|
|
51
|
-
document.head.appendChild(style);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const showSplash = () => {
|
|
55
|
-
const wrapper = document.createElement('div');
|
|
56
|
-
wrapper.id = 'application-splash-wrapper';
|
|
57
|
-
document.body.appendChild(wrapper);
|
|
58
|
-
|
|
59
|
-
const splash = document.createElement('div');
|
|
60
|
-
splash.id = 'application-splash';
|
|
61
|
-
wrapper.appendChild(splash);
|
|
62
|
-
splash.style.display = 'none';
|
|
63
|
-
|
|
64
|
-
const logo = document.createElement('img');
|
|
65
|
-
logo.src = `${ASSET_PREFIX}logo.png`;
|
|
66
|
-
splash.appendChild(logo);
|
|
67
|
-
logo.onload = () => {
|
|
68
|
-
splash.style.display = 'block';
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const container = document.createElement('div');
|
|
72
|
-
container.id = 'progress-bar-container';
|
|
73
|
-
splash.appendChild(container);
|
|
74
|
-
|
|
75
|
-
const bar = document.createElement('div');
|
|
76
|
-
bar.id = 'progress-bar';
|
|
77
|
-
container.appendChild(bar);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const setProgress = (value) => {
|
|
81
|
-
const bar = document.getElementById('progress-bar');
|
|
82
|
-
if (bar) {
|
|
83
|
-
value = Math.min(1, Math.max(0, value));
|
|
84
|
-
bar.style.width = `${value * 100}%`;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const hideSplash = () => {
|
|
89
|
-
document.getElementById('application-splash-wrapper').remove();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
createCss();
|
|
93
|
-
showSplash();
|
|
94
|
-
|
|
95
|
-
app.on('preload:end', () => {
|
|
96
|
-
app.off('preload:progress');
|
|
97
|
-
});
|
|
98
|
-
app.on('preload:progress', setProgress);
|
|
99
|
-
app.on('start', hideSplash);
|
|
100
|
-
});
|