@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.
@@ -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 = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
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 = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
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
- return settingsCode; // 已经是 data URL 或为空
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 = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
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.hash = '';
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
- // ⚠️ ESM 脚本特殊处理(参考 PlayCanvas 官方 one-page.js):
940
- // 官方工具对于 loadingType !== 0 的脚本,会将内容设为空字符串
941
- // 然后配合 one-page-inline-game-scripts.js 引擎补丁处理
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
- file.url = getPlaceholderDataUrl(asset.type, fileName);
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.preload = false;
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 (isImageFile(cleanUrl)) {
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
- if ('hash' in file) {
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
- return `"data:application/json;base64,${Buffer.from(configText).toString('base64')}"`;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- });