@playcraft/build 0.0.9 → 0.0.10
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/README.md +122 -6
- package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +1 -0
- package/dist/analyzers/__tests__/optimization-analyzer.test.js +169 -0
- package/dist/analyzers/build-analyzer.d.ts +98 -0
- package/dist/analyzers/build-analyzer.js +1160 -0
- package/dist/analyzers/enhanced-report-template.d.ts +13 -0
- package/dist/analyzers/enhanced-report-template.js +957 -0
- package/dist/analyzers/index.d.ts +6 -0
- package/dist/analyzers/index.js +9 -0
- package/dist/analyzers/optimization-analyzer.d.ts +88 -0
- package/dist/analyzers/optimization-analyzer.js +278 -0
- package/dist/analyzers/playable-analyzer.d.ts +91 -0
- package/dist/analyzers/playable-analyzer.js +977 -0
- package/dist/analyzers/report-template.d.ts +50 -0
- package/dist/analyzers/report-template.js +591 -0
- package/dist/analyzers/scene-asset-collector.js +8 -0
- package/dist/base-builder.d.ts +9 -0
- package/dist/base-builder.js +156 -2
- package/dist/build-state-manager.d.ts +110 -0
- package/dist/build-state-manager.js +169 -0
- package/dist/generators/config-generator.d.ts +2 -0
- package/dist/generators/config-generator.js +179 -10
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/loaders/playcanvas-loader.d.ts +7 -0
- package/dist/loaders/playcanvas-loader.js +17 -0
- package/dist/platforms/adikteev.js +4 -2
- package/dist/platforms/applovin.js +9 -3
- package/dist/platforms/inmobi.js +4 -2
- package/dist/platforms/ironsource.js +4 -1
- package/dist/platforms/liftoff.js +8 -3
- package/dist/platforms/snapchat.js +8 -2
- package/dist/platforms/unity.js +8 -2
- package/dist/playable-builder.js +3 -1
- package/dist/state/build-state-manager.d.ts +174 -0
- package/dist/state/build-state-manager.js +235 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +2 -0
- package/dist/state/state-to-report-converter.d.ts +141 -0
- package/dist/state/state-to-report-converter.js +177 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +11 -0
- package/dist/vite/config-builder.js +11 -1
- package/dist/vite/platform-configs.d.ts +1 -0
- package/dist/vite/platform-configs.js +1 -0
- package/dist/vite/plugin-build-state.d.ts +13 -0
- package/dist/vite/plugin-build-state.js +147 -0
- package/dist/vite/plugin-esm-html-generator.js +11 -2
- package/dist/vite/plugin-platform.js +3 -1
- package/dist/vite/plugin-playcanvas.d.ts +1 -0
- package/dist/vite/plugin-playcanvas.js +160 -20
- package/dist/vite/plugin-source-builder.js +1 -0
- package/dist/vite/plugin-template-minifier.d.ts +20 -0
- package/dist/vite/plugin-template-minifier.js +392 -0
- package/package.json +12 -12
- package/templates/patches/one-page-mraid-resize-canvas.js +18 -4
- 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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { BuildStateManager } from '../state/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Vite 插件:记录 Playable Build 状态
|
|
5
|
+
* 在 Vite 打包过程中记录资源的处理信息
|
|
6
|
+
*/
|
|
7
|
+
export function viteBuildStatePlugin(options) {
|
|
8
|
+
let stateManager;
|
|
9
|
+
const transformedModules = new Map();
|
|
10
|
+
return {
|
|
11
|
+
name: 'vite-plugin-build-state',
|
|
12
|
+
enforce: 'post', // 在其他插件之后执行,以便记录最终的处理结果
|
|
13
|
+
async configResolved(config) {
|
|
14
|
+
console.log('[BuildState] 初始化状态管理器...');
|
|
15
|
+
// 创建状态管理器
|
|
16
|
+
stateManager = new BuildStateManager(options.baseBuildDir, options.outputDir);
|
|
17
|
+
// 尝试加载 Base Build 的状态
|
|
18
|
+
const loaded = await stateManager.loadState();
|
|
19
|
+
if (loaded) {
|
|
20
|
+
console.log('[BuildState] 已加载 Base Build 状态');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log('[BuildState] 未找到 Base Build 状态,将创建新状态');
|
|
24
|
+
}
|
|
25
|
+
// 开始 Playable 构建阶段
|
|
26
|
+
stateManager.startStage('playable', {
|
|
27
|
+
platform: options.platform,
|
|
28
|
+
mode: config.mode,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async transform(code, id) {
|
|
32
|
+
// 记录模块转换信息
|
|
33
|
+
if (!id.includes('node_modules') && !id.startsWith('\0')) {
|
|
34
|
+
const originalSize = code.length;
|
|
35
|
+
// 这里我们只记录原始大小,转换后的大小将在后续的 hook 中更新
|
|
36
|
+
transformedModules.set(id, {
|
|
37
|
+
originalSize,
|
|
38
|
+
transformedSize: originalSize, // 初始值
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return null; // 不修改代码
|
|
42
|
+
},
|
|
43
|
+
async generateBundle(options, bundle) {
|
|
44
|
+
console.log('[BuildState] 记录打包产物...');
|
|
45
|
+
// 遍历所有生成的文件
|
|
46
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
47
|
+
if (output.type === 'asset') {
|
|
48
|
+
// 资产文件(图片、字体等)
|
|
49
|
+
const assetId = `playable-asset-${fileName}`;
|
|
50
|
+
const assetSize = typeof output.source === 'string'
|
|
51
|
+
? Buffer.byteLength(output.source, 'utf-8')
|
|
52
|
+
: output.source.length;
|
|
53
|
+
// 尝试从 Base Build 状态中找到原始资产
|
|
54
|
+
const existingAsset = stateManager.getAsset(assetId);
|
|
55
|
+
const originalSize = existingAsset?.originalSize || assetSize;
|
|
56
|
+
// 判断资产类型
|
|
57
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
58
|
+
let assetType = 'other';
|
|
59
|
+
if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext)) {
|
|
60
|
+
assetType = 'texture';
|
|
61
|
+
}
|
|
62
|
+
else if (['.mp3', '.wav', '.ogg'].includes(ext)) {
|
|
63
|
+
assetType = 'audio';
|
|
64
|
+
}
|
|
65
|
+
else if (['.glb', '.gltf', '.obj', '.fbx'].includes(ext)) {
|
|
66
|
+
assetType = 'model';
|
|
67
|
+
}
|
|
68
|
+
else if (ext === '.css') {
|
|
69
|
+
assetType = 'css';
|
|
70
|
+
}
|
|
71
|
+
else if (['.woff', '.woff2', '.ttf', '.otf'].includes(ext)) {
|
|
72
|
+
assetType = 'font';
|
|
73
|
+
}
|
|
74
|
+
// 如果资产不存在,先记录
|
|
75
|
+
if (!existingAsset) {
|
|
76
|
+
stateManager.recordAsset(assetId, fileName, fileName, originalSize, assetType);
|
|
77
|
+
}
|
|
78
|
+
// 更新 Playable 阶段的处理信息
|
|
79
|
+
const optimizations = [];
|
|
80
|
+
// 检测图片压缩
|
|
81
|
+
if (assetType === 'texture' && assetSize < originalSize * 0.9) {
|
|
82
|
+
optimizations.push('image-compress');
|
|
83
|
+
}
|
|
84
|
+
// 检测格式转换(如 PNG -> WebP)
|
|
85
|
+
if (fileName.includes('.webp') && existingAsset?.originalName.includes('.png')) {
|
|
86
|
+
optimizations.push('format-convert');
|
|
87
|
+
}
|
|
88
|
+
stateManager.updateAsset(assetId, 'playable', fileName, fileName, assetSize, {
|
|
89
|
+
optimizations,
|
|
90
|
+
inlined: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (output.type === 'chunk') {
|
|
94
|
+
// JavaScript chunk
|
|
95
|
+
const chunkId = `playable-chunk-${fileName}`;
|
|
96
|
+
const chunkSize = Buffer.byteLength(output.code, 'utf-8');
|
|
97
|
+
// 计算原始大小(所有模块的原始大小之和)
|
|
98
|
+
let originalSize = 0;
|
|
99
|
+
for (const moduleId of Object.keys(output.modules)) {
|
|
100
|
+
const moduleInfo = transformedModules.get(moduleId);
|
|
101
|
+
if (moduleInfo) {
|
|
102
|
+
originalSize += moduleInfo.originalSize;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (originalSize === 0) {
|
|
106
|
+
originalSize = chunkSize; // 回退值
|
|
107
|
+
}
|
|
108
|
+
// 记录 chunk
|
|
109
|
+
stateManager.recordAsset(chunkId, fileName, fileName, originalSize, 'script');
|
|
110
|
+
const optimizations = [];
|
|
111
|
+
// 检测压缩
|
|
112
|
+
if (output.code.length < originalSize * 0.8) {
|
|
113
|
+
optimizations.push('minify');
|
|
114
|
+
}
|
|
115
|
+
// 检测 tree-shaking(通过模块数量判断)
|
|
116
|
+
if (Object.keys(output.modules).length < Object.keys(output.modules).length * 0.9) {
|
|
117
|
+
optimizations.push('tree-shake');
|
|
118
|
+
}
|
|
119
|
+
// 检测代码分割
|
|
120
|
+
if (fileName.includes('chunk') || fileName.includes('-')) {
|
|
121
|
+
optimizations.push('code-split');
|
|
122
|
+
}
|
|
123
|
+
stateManager.updateAsset(chunkId, 'playable', fileName, fileName, chunkSize, {
|
|
124
|
+
optimizations,
|
|
125
|
+
inlined: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
async closeBundle() {
|
|
131
|
+
// 结束 Playable 构建阶段
|
|
132
|
+
stateManager.endStage();
|
|
133
|
+
// 只在分析模式下保存状态文件(分析报告需要用到)
|
|
134
|
+
if (options.analyze) {
|
|
135
|
+
console.log('[BuildState] 保存构建状态...');
|
|
136
|
+
await stateManager.saveState();
|
|
137
|
+
}
|
|
138
|
+
// 输出统计信息
|
|
139
|
+
const stats = stateManager.getStatistics();
|
|
140
|
+
console.log(`[BuildState] 构建统计:`);
|
|
141
|
+
console.log(` - 总资产数: ${stats.totalAssets}`);
|
|
142
|
+
console.log(` - 原始大小: ${(stats.totalOriginalSize / 1024).toFixed(2)} KB`);
|
|
143
|
+
console.log(` - 最终大小: ${(stats.totalFinalSize / 1024).toFixed(2)} KB`);
|
|
144
|
+
console.log(` - 总压缩率: ${(stats.totalCompressionRatio * 100).toFixed(1)}%`);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -724,8 +724,17 @@ ${deferWrapper}`);
|
|
|
724
724
|
fileName: 'esm-bundle',
|
|
725
725
|
formats: ['iife'],
|
|
726
726
|
},
|
|
727
|
-
//
|
|
728
|
-
minify: false,
|
|
727
|
+
// 使用 terser 压缩 JS
|
|
728
|
+
minify: options.minify ? 'terser' : false,
|
|
729
|
+
terserOptions: options.minify ? {
|
|
730
|
+
compress: {
|
|
731
|
+
drop_console: true,
|
|
732
|
+
drop_debugger: true,
|
|
733
|
+
},
|
|
734
|
+
format: {
|
|
735
|
+
comments: false, // 移除所有注释
|
|
736
|
+
},
|
|
737
|
+
} : undefined,
|
|
729
738
|
rollupOptions: {
|
|
730
739
|
// 将 playcanvas 和引擎文件设为外部依赖
|
|
731
740
|
// 引擎已经在 HTML 中单独内联,不需要再次打包
|
|
@@ -196,7 +196,9 @@ async function rewriteConfig(folderPath, assetPrefix) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
async function applyMraidSupport(outDir) {
|
|
199
|
-
|
|
199
|
+
// Note: We no longer patch fillMode to 'NONE' because it causes canvas scaling issues
|
|
200
|
+
// The MRAID resize patch handles the sizing correctly with any fillMode
|
|
201
|
+
// await patchConfigFillMode(outDir); // Removed: causes canvas scaling issues
|
|
200
202
|
await patchStylesForMraid(outDir);
|
|
201
203
|
}
|
|
202
204
|
async function patchConfigFillMode(outDir) {
|
|
@@ -73,6 +73,9 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
73
73
|
}
|
|
74
74
|
// 对于 Classic 模式或 ESM 原生模式,保持原有的多文件结构
|
|
75
75
|
}
|
|
76
|
+
// 注意:HTML 压缩不能在这里执行,因为 Vite 还需要解析 HTML
|
|
77
|
+
// HTML 模板已经在 templates/ 中被压缩为单行
|
|
78
|
+
// 最终的 HTML 压缩由 vite-plugin-singlefile 在最后阶段处理
|
|
76
79
|
return html;
|
|
77
80
|
},
|
|
78
81
|
},
|
|
@@ -107,7 +110,7 @@ async function inlineEngineScript(html, baseBuildDir, options) {
|
|
|
107
110
|
const enginePath = path.join(baseBuildDir, engineName);
|
|
108
111
|
try {
|
|
109
112
|
await fs.access(enginePath);
|
|
110
|
-
|
|
113
|
+
let engineCode = await fs.readFile(enginePath, 'utf-8');
|
|
111
114
|
const patchScripts = await getEnginePatchScripts(options);
|
|
112
115
|
const engineScript = options.compressEngine
|
|
113
116
|
? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
|
|
@@ -202,6 +205,8 @@ async function generateAndInlineSettings(html, baseBuildDir, options, pluginCont
|
|
|
202
205
|
const appProps = configJson.application_properties || {};
|
|
203
206
|
const scripts = appProps.scripts || [];
|
|
204
207
|
const preloadModules = extractPreloadModules(configJson);
|
|
208
|
+
// 保留原始的 powerPreference 设置,默认为 high-performance
|
|
209
|
+
const powerPreference = appProps.powerPreference || 'high-performance';
|
|
205
210
|
const settingsCode = `
|
|
206
211
|
window.ASSET_PREFIX = "";
|
|
207
212
|
window.SCRIPT_PREFIX = "";
|
|
@@ -211,7 +216,7 @@ window.CONTEXT_OPTIONS = {
|
|
|
211
216
|
'alpha': ${appProps.transparentCanvas === true},
|
|
212
217
|
'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
|
|
213
218
|
'deviceTypes': ['webgl2', 'webgl1'],
|
|
214
|
-
'powerPreference': "
|
|
219
|
+
'powerPreference': "${powerPreference}"
|
|
215
220
|
};
|
|
216
221
|
window.SCRIPTS = [${scripts.join(', ')}];
|
|
217
222
|
window.CONFIG_FILENAME = ${configValue};
|
|
@@ -260,8 +265,24 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
260
265
|
if (options.mraidSupport) {
|
|
261
266
|
configJson = applyMraidConfig(configJson);
|
|
262
267
|
}
|
|
263
|
-
// 5. 生成 config data URL
|
|
264
|
-
|
|
268
|
+
// 5. 生成 config.json 的 data URL
|
|
269
|
+
// 如果启用 compressConfigJson,使用 LZ4 压缩后再 base64 编码
|
|
270
|
+
const configJsonString = JSON.stringify(configJson);
|
|
271
|
+
let configDataUrl;
|
|
272
|
+
if (options.compressConfigJson) {
|
|
273
|
+
const lz4 = await loadLz4Module();
|
|
274
|
+
const compressed = lz4.compress(Buffer.from(configJsonString));
|
|
275
|
+
const compressedBase64 = Buffer.from(compressed).toString('base64');
|
|
276
|
+
// 使用 lz4-json 前缀标识压缩格式,运行时需要解压
|
|
277
|
+
configDataUrl = `data:application/x-lz4-json;base64,${compressedBase64}`;
|
|
278
|
+
const originalSize = Buffer.from(configJsonString).length;
|
|
279
|
+
const compressedSize = compressed.length;
|
|
280
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
|
|
281
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: config.json 已压缩 (${formatBytes(originalSize)} → ${formatBytes(compressedSize)}, 减少 ${ratio}%)`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
configDataUrl = `data:application/json;base64,${Buffer.from(configJsonString).toString('base64')}`;
|
|
285
|
+
}
|
|
265
286
|
// 6. 处理场景文件 - 获取第一个场景的 data URL
|
|
266
287
|
let sceneDataUrl = '';
|
|
267
288
|
if (configJson.scenes && configJson.scenes.length > 0) {
|
|
@@ -318,20 +339,21 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
|
|
|
318
339
|
}
|
|
319
340
|
}
|
|
320
341
|
// 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
|
|
321
|
-
// CONFIG_FILENAME = "config.json"
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
342
|
+
// 方案1+3:直接将 CONFIG_FILENAME = "config.json" 替换为 data URL
|
|
343
|
+
// 8.1 替换 CONFIG_FILENAME
|
|
344
|
+
// 格式1: CONFIG_FILENAME = "config.json"
|
|
345
|
+
// 格式2: CONFIG_FILENAME="config.json" (压缩后)
|
|
346
|
+
const configReplaceCount = (html.match(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g) || []).length;
|
|
347
|
+
if (configReplaceCount > 0) {
|
|
348
|
+
html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME="${configDataUrl}"`);
|
|
349
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configReplaceCount} 处 CONFIG_FILENAME`);
|
|
350
|
+
}
|
|
351
|
+
// 8.2 替换 configure("config.json") 调用
|
|
352
|
+
// Vite 压缩后可能是: .configure("config.json",
|
|
331
353
|
const configureReplaceCount = (html.match(/\.configure\s*\(\s*["']config\.json["']/g) || []).length;
|
|
332
354
|
if (configureReplaceCount > 0) {
|
|
333
|
-
html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${
|
|
334
|
-
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")
|
|
355
|
+
html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${configDataUrl}"`);
|
|
356
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")`);
|
|
335
357
|
}
|
|
336
358
|
// 9. 替换 SCENE_PATH
|
|
337
359
|
if (sceneDataUrl) {
|
|
@@ -780,6 +802,33 @@ async function inlineLoadingScript(html, baseBuildDir) {
|
|
|
780
802
|
}
|
|
781
803
|
return html;
|
|
782
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* 压缩 CSS 代码
|
|
807
|
+
* 移除注释、多余空白、合并为单行
|
|
808
|
+
*/
|
|
809
|
+
function minifyCSS(css) {
|
|
810
|
+
return css
|
|
811
|
+
// 移除多行注释 /* ... */
|
|
812
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
813
|
+
// 移除单行注释 // ... (CSS 不标准但有些预处理器会保留)
|
|
814
|
+
.replace(/\/\/.*$/gm, '')
|
|
815
|
+
// 将多个空白字符(空格、换行、制表符)替换为单个空格
|
|
816
|
+
.replace(/\s+/g, ' ')
|
|
817
|
+
// 移除 { 前的空格
|
|
818
|
+
.replace(/\s*\{\s*/g, '{')
|
|
819
|
+
// 移除 } 后的空格
|
|
820
|
+
.replace(/\s*\}\s*/g, '}')
|
|
821
|
+
// 移除 : 前后的空格
|
|
822
|
+
.replace(/\s*:\s*/g, ':')
|
|
823
|
+
// 移除 ; 前后的空格
|
|
824
|
+
.replace(/\s*;\s*/g, ';')
|
|
825
|
+
// 移除 , 后的空格
|
|
826
|
+
.replace(/,\s*/g, ',')
|
|
827
|
+
// 移除最后一个 ; 在 } 前
|
|
828
|
+
.replace(/;}/g, '}')
|
|
829
|
+
// 去除首尾空白
|
|
830
|
+
.trim();
|
|
831
|
+
}
|
|
783
832
|
/**
|
|
784
833
|
* 内联 CSS 文件
|
|
785
834
|
*/
|
|
@@ -799,6 +848,8 @@ async function inlineCSS(html, baseBuildDir, options) {
|
|
|
799
848
|
if (options.mraidSupport && !cssContent.includes('fill-mode-NONE')) {
|
|
800
849
|
cssContent += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
|
|
801
850
|
}
|
|
851
|
+
// 压缩 CSS(移除注释、多余空白、合并为单行)
|
|
852
|
+
cssContent = minifyCSS(cssContent);
|
|
802
853
|
// 替换 link 标签为 style 标签
|
|
803
854
|
html = html.replace(match[0], `<style>${cssContent}</style>`);
|
|
804
855
|
}
|
|
@@ -1004,10 +1055,14 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
1004
1055
|
}
|
|
1005
1056
|
}
|
|
1006
1057
|
// ✅ 核心优化:如果是图片,使用 sharp 压缩
|
|
1058
|
+
// ⚠️ 重要:字体纹理(type === 'font')不能转换为 WebP!
|
|
1059
|
+
// MSDF/位图字体的 PNG 纹理包含精确的距离场/像素数据,
|
|
1060
|
+
// WebP 有损压缩会破坏这些数据导致字体渲染异常
|
|
1007
1061
|
else if (isImageFile(cleanUrl)) {
|
|
1062
|
+
const isFontTexture = asset.type === 'font';
|
|
1008
1063
|
const compressed = await compressImage(buffer, cleanUrl, {
|
|
1009
|
-
convertToWebP:
|
|
1010
|
-
quality: 75,
|
|
1064
|
+
convertToWebP: !isFontTexture, // 字体纹理不转换为 WebP
|
|
1065
|
+
quality: isFontTexture ? 100 : 75, // 字体纹理使用无损压缩
|
|
1011
1066
|
});
|
|
1012
1067
|
// 防御性检查:如果压缩后的 buffer 为空,使用原始 buffer
|
|
1013
1068
|
if (!compressed.buffer || compressed.buffer.length === 0) {
|
|
@@ -1196,10 +1251,15 @@ async function compressImage(buffer, filePath, options) {
|
|
|
1196
1251
|
};
|
|
1197
1252
|
}
|
|
1198
1253
|
// 按原格式压缩
|
|
1254
|
+
const quality = options?.quality ?? 80;
|
|
1199
1255
|
switch (ext) {
|
|
1200
1256
|
case '.png':
|
|
1257
|
+
// 如果 quality >= 100,使用无损压缩(适用于字体纹理)
|
|
1201
1258
|
const pngBuffer = await image
|
|
1202
|
-
.png({
|
|
1259
|
+
.png({
|
|
1260
|
+
compressionLevel: quality >= 100 ? 6 : 9, // 无损时使用中等压缩
|
|
1261
|
+
palette: quality < 100, // 无损时不使用调色板
|
|
1262
|
+
})
|
|
1203
1263
|
.toBuffer();
|
|
1204
1264
|
// 防御性检查
|
|
1205
1265
|
if (!pngBuffer || pngBuffer.length === 0) {
|
|
@@ -1295,7 +1355,9 @@ function buildConfigValue(configJson) {
|
|
|
1295
1355
|
function applyMraidConfig(configJson) {
|
|
1296
1356
|
const next = { ...configJson };
|
|
1297
1357
|
const props = { ...(next.application_properties || {}) };
|
|
1298
|
-
|
|
1358
|
+
// Keep original fillMode (usually KEEP_ASPECT) for correct canvas scaling
|
|
1359
|
+
// The MRAID resize patch will handle the sizing
|
|
1360
|
+
// props.fillMode = 'NONE'; // Removed: causes canvas scaling issues
|
|
1299
1361
|
next.application_properties = props;
|
|
1300
1362
|
return next;
|
|
1301
1363
|
}
|
|
@@ -1339,6 +1401,18 @@ async function getEnginePatchScripts(options) {
|
|
|
1339
1401
|
const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
|
|
1340
1402
|
scripts.push(`<script>${patchCode}</script>`);
|
|
1341
1403
|
}
|
|
1404
|
+
// 如果启用了 config.json 压缩,需要注入 LZ4 解压运行时代码
|
|
1405
|
+
// 注意:必须在 compressEngine 之前检查,因为 compressEngine 也会注入 lz4.js
|
|
1406
|
+
if (options.compressConfigJson && !options.compressEngine) {
|
|
1407
|
+
// 如果没有启用引擎压缩,需要单独注入 lz4.js
|
|
1408
|
+
const lz4Script = await getLz4InlineScript();
|
|
1409
|
+
scripts.push(lz4Script);
|
|
1410
|
+
}
|
|
1411
|
+
// 注入 config.json 解压 patch(拦截 fetch 请求并解压 LZ4 数据)
|
|
1412
|
+
if (options.compressConfigJson) {
|
|
1413
|
+
const configDecompressPatch = buildConfigJsonDecompressPatch();
|
|
1414
|
+
scripts.push(`<script>${configDecompressPatch}</script>`);
|
|
1415
|
+
}
|
|
1342
1416
|
if (options.compressEngine) {
|
|
1343
1417
|
// Focus patch: 确保 canvas 获得焦点以接收键盘事件
|
|
1344
1418
|
const focusPatch = `!function(){var e=function(){var e=document.getElementById("application-canvas");if(!e)return!1;try{e.focus()}catch(t){}e.addEventListener("pointerdown",function(){e.focus()}),e.addEventListener("click",function(){e.focus()});return!0},t=0;if(!e()){var n=setInterval(function(){(e()||++t>50)&&clearInterval(n)},100)}}();`;
|
|
@@ -1379,3 +1453,69 @@ async function readPatchFile(name) {
|
|
|
1379
1453
|
const patchPath = path.resolve(currentDir, '../../templates/patches', name);
|
|
1380
1454
|
return await fs.readFile(patchPath, 'utf-8');
|
|
1381
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 格式化字节数为人类可读格式
|
|
1458
|
+
*/
|
|
1459
|
+
function formatBytes(bytes) {
|
|
1460
|
+
if (bytes === 0)
|
|
1461
|
+
return '0 B';
|
|
1462
|
+
const k = 1024;
|
|
1463
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1464
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1465
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* 构建 config.json 解压 patch
|
|
1469
|
+
*
|
|
1470
|
+
* 这个 patch 拦截 PlayCanvas 的 pc.Http.prototype.get 请求,
|
|
1471
|
+
* 检测是否为 LZ4 压缩的 config.json,如果是则解压后返回。
|
|
1472
|
+
*
|
|
1473
|
+
* 工作原理:
|
|
1474
|
+
* 1. 保存原始 pc.Http.prototype.get 函数
|
|
1475
|
+
* 2. 重写 get,检测 URL 是否为 LZ4 压缩的 JSON data URL
|
|
1476
|
+
* 3. 如果是,解压并通过 callback 返回解析后的 JSON 对象
|
|
1477
|
+
*
|
|
1478
|
+
* 注意:
|
|
1479
|
+
* - PlayCanvas 使用 pc.Http.prototype.get 加载 config.json,不是 fetch
|
|
1480
|
+
* - 使用 lz4.js 提供的 Buffer polyfill 进行解压
|
|
1481
|
+
* - 必须在 pc 对象可用后才能生效,所以使用轮询等待
|
|
1482
|
+
*/
|
|
1483
|
+
function buildConfigJsonDecompressPatch() {
|
|
1484
|
+
// 原始代码:
|
|
1485
|
+
// (function() {
|
|
1486
|
+
// function patch() {
|
|
1487
|
+
// if (!window.pc || !window.pc.Http) return false;
|
|
1488
|
+
// var oldGet = pc.Http.prototype.get;
|
|
1489
|
+
// pc.Http.prototype.get = function(url, options, callback) {
|
|
1490
|
+
// if (typeof options === 'function') {
|
|
1491
|
+
// callback = options;
|
|
1492
|
+
// options = {};
|
|
1493
|
+
// }
|
|
1494
|
+
// if (typeof url === 'string' && url.startsWith('data:application/x-lz4-json;base64,')) {
|
|
1495
|
+
// try {
|
|
1496
|
+
// var base64 = url.replace('data:application/x-lz4-json;base64,', '');
|
|
1497
|
+
// var compressed = new Buffer(base64, 'base64');
|
|
1498
|
+
// var decompressed = lz4.decompress(compressed);
|
|
1499
|
+
// var str = Buffer.from(decompressed).toString('utf8');
|
|
1500
|
+
// var json = JSON.parse(str);
|
|
1501
|
+
// console.log('[LZ4 Config] Decompressed config.json successfully, size:', str.length);
|
|
1502
|
+
// callback(null, json);
|
|
1503
|
+
// } catch(e) {
|
|
1504
|
+
// console.error('[LZ4 Config] Decompression error:', e);
|
|
1505
|
+
// callback(e);
|
|
1506
|
+
// }
|
|
1507
|
+
// return;
|
|
1508
|
+
// }
|
|
1509
|
+
// oldGet.call(this, url, options, callback);
|
|
1510
|
+
// };
|
|
1511
|
+
// return true;
|
|
1512
|
+
// }
|
|
1513
|
+
// if (!patch()) {
|
|
1514
|
+
// var c = 0;
|
|
1515
|
+
// var i = setInterval(function() {
|
|
1516
|
+
// if (patch() || ++c > 100) clearInterval(i);
|
|
1517
|
+
// }, 10);
|
|
1518
|
+
// }
|
|
1519
|
+
// })();
|
|
1520
|
+
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)}}();`;
|
|
1521
|
+
}
|
|
@@ -94,6 +94,7 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
94
94
|
// 生成 config.json(支持场景过滤)
|
|
95
95
|
const config = await generateConfig(projectConfig, {
|
|
96
96
|
selectedScenes: options.selectedScenes,
|
|
97
|
+
projectDir: options.projectDir,
|
|
97
98
|
});
|
|
98
99
|
// 为脚本资产补齐 __game-scripts.js 信息(仅在 Classic 模式下)
|
|
99
100
|
if (options.buildMode !== 'esm') {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite 插件:模板字符串压缩
|
|
3
|
+
*
|
|
4
|
+
* 在构建完成后(closeBundle 阶段)对输出的 HTML 文件进行后处理,
|
|
5
|
+
* 压缩 JS 代码中的多行模板字符串,减小包体积。
|
|
6
|
+
*
|
|
7
|
+
* 这个插件必须在 vite-plugin-singlefile 之后运行,
|
|
8
|
+
* 因为它直接处理最终输出的文件,而不是通过 Vite 的 HTML 处理管道。
|
|
9
|
+
*/
|
|
10
|
+
import type { Plugin } from 'vite';
|
|
11
|
+
export interface TemplateMinifierOptions {
|
|
12
|
+
/** 输出目录 */
|
|
13
|
+
outputDir: string;
|
|
14
|
+
/** 是否启用(默认 true) */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 创建模板字符串压缩插件
|
|
19
|
+
*/
|
|
20
|
+
export declare function viteTemplateMinifierPlugin(options: TemplateMinifierOptions): Plugin;
|