@playcraft/build 0.0.4 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzers/scene-asset-collector.js +210 -1
- package/dist/base-builder.d.ts +15 -0
- package/dist/base-builder.js +192 -16
- package/dist/generators/config-generator.js +29 -3
- package/dist/loaders/playcanvas-loader.d.ts +7 -0
- package/dist/loaders/playcanvas-loader.js +53 -3
- package/dist/platforms/adikteev.d.ts +10 -0
- package/dist/platforms/adikteev.js +72 -0
- package/dist/platforms/base.d.ts +12 -0
- package/dist/platforms/base.js +208 -0
- package/dist/platforms/facebook.js +5 -2
- package/dist/platforms/index.d.ts +4 -0
- package/dist/platforms/index.js +16 -0
- package/dist/platforms/inmobi.d.ts +10 -0
- package/dist/platforms/inmobi.js +68 -0
- package/dist/platforms/ironsource.js +5 -2
- package/dist/platforms/moloco.js +5 -2
- package/dist/platforms/playcraft.d.ts +33 -0
- package/dist/platforms/playcraft.js +44 -0
- package/dist/platforms/remerge.d.ts +10 -0
- package/dist/platforms/remerge.js +56 -0
- 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 +14 -1
- package/dist/utils/build-mode-detector.d.ts +9 -0
- package/dist/utils/build-mode-detector.js +42 -0
- package/dist/vite/config-builder.d.ts +29 -1
- package/dist/vite/config-builder.js +169 -25
- package/dist/vite/platform-configs.d.ts +4 -0
- package/dist/vite/platform-configs.js +97 -13
- package/dist/vite/plugin-esm-html-generator.d.ts +22 -0
- package/dist/vite/plugin-esm-html-generator.js +1061 -0
- package/dist/vite/plugin-platform.js +56 -17
- package/dist/vite/plugin-playcanvas.d.ts +2 -0
- package/dist/vite/plugin-playcanvas.js +497 -40
- package/dist/vite/plugin-source-builder.d.ts +3 -0
- package/dist/vite/plugin-source-builder.js +886 -19
- package/dist/vite-builder.d.ts +19 -2
- package/dist/vite-builder.js +162 -12
- package/package.json +2 -1
- package/physics/cannon-es-bundle.js +13092 -0
- package/physics/cannon-rigidbody-adapter.js +375 -0
- package/physics/connon-integration.js +411 -0
- package/templates/__start__.js +8 -3
- package/templates/index.esm.html +20 -0
- package/templates/index.esm.mjs +502 -0
- package/templates/patches/one-page-inline-game-scripts.js +25 -1
- package/templates/patches/playcraft-cta-adapter.js +297 -0
- package/templates/patches/playcraft-no-xhr.js +25 -1
- package/templates/playcanvas-esm-wrapper.mjs +827 -0
|
@@ -16,31 +16,65 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
16
16
|
// 保存插件的 this 上下文
|
|
17
17
|
pluginContext = this;
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// 2. 内联 __game-scripts.js(用户脚本)
|
|
27
|
-
if (options.inlineGameScripts) {
|
|
28
|
-
html = await inlineGameScripts(html, options.baseBuildDir);
|
|
19
|
+
// 使用 transformIndexHtml 的对象形式,指定在 pre 阶段执行
|
|
20
|
+
// 这确保在 Vite 处理资源引用之前移除不需要的引用
|
|
21
|
+
transformIndexHtml: {
|
|
22
|
+
order: 'pre',
|
|
23
|
+
async handler(html) {
|
|
24
|
+
if (options.ammoReplacement) {
|
|
25
|
+
html = await injectPhysicsLibrary(html, options);
|
|
29
26
|
}
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
// ⚠️ 在最早期移除 manifest 引用,避免 Vite 将其作为资源处理
|
|
28
|
+
if (options.outputFormat === 'html' && options.convertDataUrls) {
|
|
29
|
+
html = html.replace(/<link[^>]*rel=["']manifest["'][^>]*>/gi, '');
|
|
30
|
+
}
|
|
31
|
+
if (options.outputFormat === 'html') {
|
|
32
|
+
// 1. 内联 PlayCanvas Engine + 引擎补丁
|
|
33
|
+
html = await inlineEngineScript(html, options.baseBuildDir, options);
|
|
34
|
+
// 2. 根据模式处理用户脚本
|
|
35
|
+
if (options.buildMode === 'esm' && options.isESMBundle) {
|
|
36
|
+
// ESM → IIFE:脚本已被 viteESMBundlePlugin 打包并注入到 HTML
|
|
37
|
+
// ESM 模式的 index.esm.mjs 已包含完整的初始化逻辑,不需要 __start__.js 等
|
|
38
|
+
console.log('[PlayCanvasPlugin] ESM Bundle 模式:脚本由 ESMBundle 插件处理');
|
|
39
|
+
// ⚠️ 重要:ESM Bundle 模式也需要将 config.json 和场景资源内联到打包后的代码中
|
|
40
|
+
// 因为打包后的 IIFE 代码中 CONFIG_FILENAME = "config.json" 是硬编码的
|
|
41
|
+
html = await inlineESMBundleAssets(html, options.baseBuildDir, options, pluginContext);
|
|
42
|
+
// 处理 CSS 和 manifest
|
|
43
|
+
html = await inlineCSS(html, options.baseBuildDir, options);
|
|
44
|
+
html = await inlineManifest(html, options.baseBuildDir, options);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Classic 模式或 ESM 原生模式
|
|
48
|
+
if (options.buildMode === 'classic' && options.inlineGameScripts) {
|
|
49
|
+
// Classic 模式:内联 __game-scripts.js
|
|
50
|
+
html = await inlineGameScripts(html, options.baseBuildDir);
|
|
51
|
+
}
|
|
52
|
+
// ESM 原生模式:保持 <script type="module"> 和 import map
|
|
53
|
+
// 3. 内联并转换 __settings__.js(传递插件上下文)
|
|
54
|
+
html = await inlineAndConvertSettings(html, options.baseBuildDir, options, pluginContext);
|
|
55
|
+
// 4. 内联 __modules__.js
|
|
56
|
+
html = await inlineModulesScript(html, options.baseBuildDir);
|
|
57
|
+
// 5. 内联 __start__.js
|
|
58
|
+
html = await inlineStartScript(html, options.baseBuildDir, options);
|
|
59
|
+
// 6. 内联 __loading__.js
|
|
60
|
+
html = await inlineLoadingScript(html, options.baseBuildDir);
|
|
61
|
+
// 7. 内联 CSS
|
|
62
|
+
html = await inlineCSS(html, options.baseBuildDir, options);
|
|
63
|
+
// 8. 处理 manifest.json
|
|
64
|
+
html = await inlineManifest(html, options.baseBuildDir, options);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (options.outputFormat === 'zip') {
|
|
68
|
+
// ZIP 格式:不内联引擎,但需要在 IIFE 之前添加引擎脚本标签
|
|
69
|
+
if (options.buildMode === 'esm' && options.isESMBundle) {
|
|
70
|
+
// ESM → IIFE 模式:IIFE 依赖全局 pc 变量,需要先加载引擎
|
|
71
|
+
console.log('[PlayCanvasPlugin] ZIP + ESM Bundle 模式:添加引擎脚本标签');
|
|
72
|
+
html = addEngineScriptTag(html);
|
|
73
|
+
}
|
|
74
|
+
// 对于 Classic 模式或 ESM 原生模式,保持原有的多文件结构
|
|
75
|
+
}
|
|
76
|
+
return html;
|
|
77
|
+
},
|
|
44
78
|
},
|
|
45
79
|
async transform(code, id) {
|
|
46
80
|
// 转换 __settings__.js 中的资源路径为 data URLs(传递插件上下文)
|
|
@@ -55,11 +89,20 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
55
89
|
* 内联 PlayCanvas Engine
|
|
56
90
|
*/
|
|
57
91
|
async function inlineEngineScript(html, baseBuildDir, options) {
|
|
58
|
-
|
|
92
|
+
// Classic 模式的引擎文件名
|
|
93
|
+
const classicEngineNames = [
|
|
59
94
|
'playcanvas-stable.min.js',
|
|
60
95
|
'playcanvas.min.js',
|
|
61
96
|
'__lib__.js',
|
|
62
97
|
];
|
|
98
|
+
// ESM 模式的引擎文件位置
|
|
99
|
+
const esmEngineNames = [
|
|
100
|
+
'js/playcanvas-engine-umd.js',
|
|
101
|
+
];
|
|
102
|
+
// 根据构建模式选择引擎文件列表
|
|
103
|
+
const engineNames = options.buildMode === 'esm' && options.isESMBundle
|
|
104
|
+
? [...esmEngineNames, ...classicEngineNames] // ESM 优先,然后 Classic
|
|
105
|
+
: classicEngineNames;
|
|
63
106
|
for (const engineName of engineNames) {
|
|
64
107
|
const enginePath = path.join(baseBuildDir, engineName);
|
|
65
108
|
try {
|
|
@@ -69,7 +112,15 @@ async function inlineEngineScript(html, baseBuildDir, options) {
|
|
|
69
112
|
const engineScript = options.compressEngine
|
|
70
113
|
? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
|
|
71
114
|
: `<script>${engineCode}</script>`;
|
|
72
|
-
//
|
|
115
|
+
// 对于 ESM 模式,引擎需要在 </head> 之前插入(确保在用户脚本之前加载)
|
|
116
|
+
if (options.buildMode === 'esm' && options.isESMBundle) {
|
|
117
|
+
// ESM 模式:在 </head> 之前插入引擎脚本
|
|
118
|
+
// 这确保 window.pc 在 IIFE 用户脚本执行前已设置
|
|
119
|
+
html = html.replace('</head>', `${engineScript}${patchScripts}\n</head>`);
|
|
120
|
+
console.log(`[PlayCanvasPlugin] ESM 模式:已内联引擎 ${engineName}`);
|
|
121
|
+
return html;
|
|
122
|
+
}
|
|
123
|
+
// Classic 模式:替换原有的 script 标签
|
|
73
124
|
const scriptPattern = new RegExp(`<script[^>]*src=["']${engineName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*></script>`, 'i');
|
|
74
125
|
html = html.replace(scriptPattern, `${engineScript}${patchScripts}`);
|
|
75
126
|
return html;
|
|
@@ -80,6 +131,18 @@ async function inlineEngineScript(html, baseBuildDir, options) {
|
|
|
80
131
|
}
|
|
81
132
|
return html;
|
|
82
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 为 ZIP 格式添加引擎脚本标签(不内联)
|
|
136
|
+
* 用于 ESM → IIFE 模式,IIFE 依赖全局 pc 变量
|
|
137
|
+
*/
|
|
138
|
+
function addEngineScriptTag(html) {
|
|
139
|
+
// 在 </head> 之前添加引擎脚本标签
|
|
140
|
+
// UMD 版本会设置 window.pc 全局变量
|
|
141
|
+
const engineScriptTag = `<script src="./js/playcanvas-engine-umd.js"></script>`;
|
|
142
|
+
html = html.replace('</head>', `${engineScriptTag}\n</head>`);
|
|
143
|
+
console.log('[PlayCanvasPlugin] ZIP 模式:已添加引擎脚本标签');
|
|
144
|
+
return html;
|
|
145
|
+
}
|
|
83
146
|
/**
|
|
84
147
|
* 内联并转换 __settings__.js
|
|
85
148
|
*/
|
|
@@ -165,6 +228,231 @@ window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
|
|
|
165
228
|
html = html.replace('</head>', `<script>${settingsCode}</script>\n</head>`);
|
|
166
229
|
return html;
|
|
167
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* ESM Bundle 模式专用:将 config.json 和场景资源内联到打包后的 IIFE 代码中
|
|
233
|
+
*
|
|
234
|
+
* 在 ESM Bundle 模式下,index.esm.mjs 已经被打包为 IIFE 并注入到 HTML 的 <script> 标签中
|
|
235
|
+
* 该 IIFE 代码中 CONFIG_FILENAME = "config.json" 是硬编码的字符串
|
|
236
|
+
* 此函数需要将其替换为内联的 data URL,使得单文件 HTML 可以正常工作
|
|
237
|
+
*/
|
|
238
|
+
async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext) {
|
|
239
|
+
console.log('[PlayCanvasPlugin] ESM Bundle 模式:开始内联 config.json 和场景资源...');
|
|
240
|
+
// 1. 读取并处理 config.json
|
|
241
|
+
const configPath = path.join(baseBuildDir, 'config.json');
|
|
242
|
+
let configJson;
|
|
243
|
+
try {
|
|
244
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
245
|
+
configJson = JSON.parse(configContent);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法读取 config.json: ${error}`);
|
|
249
|
+
return html;
|
|
250
|
+
}
|
|
251
|
+
// 2. 内联 config.json 中的所有资源 URL
|
|
252
|
+
if (options.convertDataUrls) {
|
|
253
|
+
configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
|
|
254
|
+
}
|
|
255
|
+
// 3. 处理 Ammo 物理引擎替换
|
|
256
|
+
if (options.ammoReplacement) {
|
|
257
|
+
configJson = stripAmmoAssets(configJson);
|
|
258
|
+
}
|
|
259
|
+
// 4. 应用 MRAID 配置
|
|
260
|
+
if (options.mraidSupport) {
|
|
261
|
+
configJson = applyMraidConfig(configJson);
|
|
262
|
+
}
|
|
263
|
+
// 5. 生成 config data URL
|
|
264
|
+
const configDataUrl = `data:application/json;base64,${Buffer.from(JSON.stringify(configJson)).toString('base64')}`;
|
|
265
|
+
// 6. 处理场景文件 - 获取第一个场景的 data URL
|
|
266
|
+
let sceneDataUrl = '';
|
|
267
|
+
if (configJson.scenes && configJson.scenes.length > 0) {
|
|
268
|
+
const sceneUrl = configJson.scenes[0].url;
|
|
269
|
+
if (sceneUrl && !sceneUrl.startsWith('data:')) {
|
|
270
|
+
const scenePath = path.join(baseBuildDir, sceneUrl);
|
|
271
|
+
try {
|
|
272
|
+
const sceneContent = await fs.readFile(scenePath, 'utf-8');
|
|
273
|
+
sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
|
|
274
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 场景已内联: ${sceneUrl}`);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
console.warn(`[PlayCanvasPlugin] ESM Bundle: 场景文件不存在: ${sceneUrl}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
sceneDataUrl = sceneUrl || '';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// 7. 内联 logo.png
|
|
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
|
+
}
|
|
320
|
+
// 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
|
|
321
|
+
// CONFIG_FILENAME = "config.json" → CONFIG_FILENAME = "data:application/json;base64,..."
|
|
322
|
+
// 注意:替换字符串中的 $ 是特殊字符,需要转义
|
|
323
|
+
const escapedConfigDataUrl = configDataUrl.replace(/\$/g, '$$$$');
|
|
324
|
+
// 替换 CONFIG_FILENAME(支持多种格式)
|
|
325
|
+
// 格式1: const CONFIG_FILENAME = "config.json"
|
|
326
|
+
// 格式2: CONFIG_FILENAME="config.json"
|
|
327
|
+
html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME = "${escapedConfigDataUrl}"`);
|
|
328
|
+
// ⚠️ 重要:Vite 压缩后,变量名会被混淆(如 CONFIG_FILENAME → vh)
|
|
329
|
+
// 需要直接替换 .configure("config.json" 调用中的字符串字面量
|
|
330
|
+
// 格式: .configure("config.json", ...) 或 .configure('config.json', ...)
|
|
331
|
+
const configureReplaceCount = (html.match(/\.configure\s*\(\s*["']config\.json["']/g) || []).length;
|
|
332
|
+
if (configureReplaceCount > 0) {
|
|
333
|
+
html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${escapedConfigDataUrl}"`);
|
|
334
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json") 调用`);
|
|
335
|
+
}
|
|
336
|
+
// 9. 替换 SCENE_PATH
|
|
337
|
+
if (sceneDataUrl) {
|
|
338
|
+
const escapedSceneDataUrl = sceneDataUrl.replace(/\$/g, '$$$$');
|
|
339
|
+
// 查找 SCENE_PATH 的各种格式并替换
|
|
340
|
+
// 格式: const SCENE_PATH = "xxx.json" 或 SCENE_PATH = "xxx"
|
|
341
|
+
html = html.replace(/SCENE_PATH\s*=\s*["'][^"']+\.json["']/g, `SCENE_PATH = "${escapedSceneDataUrl}"`);
|
|
342
|
+
// ⚠️ Vite 压缩后格式:loadScene("xxx.json" 或 loadScene(变量名,
|
|
343
|
+
// 需要同时替换 loadScene 调用中的场景路径字符串
|
|
344
|
+
// 格式1: .loadScene("2412781.json", ...)
|
|
345
|
+
// 格式2: .loadScene(变量名, ...) - 需要替换变量定义
|
|
346
|
+
const sceneUrl = configJson.scenes?.[0]?.url;
|
|
347
|
+
if (sceneUrl && !sceneUrl.startsWith('data:')) {
|
|
348
|
+
// 直接替换 loadScene 中的场景路径字符串
|
|
349
|
+
const scenePathRegex = new RegExp(`\\.loadScene\\s*\\(\\s*["']${sceneUrl.replace(/\./g, '\\.')}["']`, 'g');
|
|
350
|
+
const loadSceneReplaceCount = (html.match(scenePathRegex) || []).length;
|
|
351
|
+
if (loadSceneReplaceCount > 0) {
|
|
352
|
+
html = html.replace(scenePathRegex, `.loadScene("${escapedSceneDataUrl}"`);
|
|
353
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${loadSceneReplaceCount} 处 .loadScene("${sceneUrl}") 调用`);
|
|
354
|
+
}
|
|
355
|
+
// 同时替换压缩后的变量定义:const vh="2412781.json" 或 变量名="2412781.json"
|
|
356
|
+
// 这会处理如 const vh="2412781.json" 这种情况
|
|
357
|
+
const varDefRegex = new RegExp(`(\\w{1,3})\\s*=\\s*["']${sceneUrl.replace(/\./g, '\\.')}["']`, 'g');
|
|
358
|
+
const varDefReplaceCount = (html.match(varDefRegex) || []).length;
|
|
359
|
+
if (varDefReplaceCount > 0) {
|
|
360
|
+
html = html.replace(varDefRegex, `$1="${escapedSceneDataUrl}"`);
|
|
361
|
+
console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${varDefReplaceCount} 处场景路径变量定义`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// 10. 处理 PRELOAD_MODULES(WASM 模块)
|
|
366
|
+
// 在 ESM Bundle 模式下,需要将 PRELOAD_MODULES 中的 URL 也转换为 data URL
|
|
367
|
+
const modulesRegex = /PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/;
|
|
368
|
+
const modulesMatch = html.match(modulesRegex);
|
|
369
|
+
if (modulesMatch) {
|
|
370
|
+
try {
|
|
371
|
+
const modulesStr = modulesMatch[1];
|
|
372
|
+
let modules = new Function(`return ${modulesStr}`)();
|
|
373
|
+
// 过滤 Ammo 模块
|
|
374
|
+
if (options.ammoReplacement) {
|
|
375
|
+
modules = modules.filter((module) => !isAmmoModule(module));
|
|
376
|
+
}
|
|
377
|
+
// 转换模块 URL 为 data URL
|
|
378
|
+
for (const module of modules) {
|
|
379
|
+
if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
|
|
380
|
+
const fallbackPath = path.join(baseBuildDir, module.fallbackUrl);
|
|
381
|
+
try {
|
|
382
|
+
const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
|
|
383
|
+
module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
|
|
384
|
+
// 清空 WASM URL,强制使用 fallback
|
|
385
|
+
module.glueUrl = '';
|
|
386
|
+
module.wasmUrl = '';
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法读取 fallback: ${module.fallbackUrl}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// 替换 PRELOAD_MODULES
|
|
394
|
+
const newModulesStr = JSON.stringify(modules);
|
|
395
|
+
const escapedModulesStr = newModulesStr.replace(/\$/g, '$$$$');
|
|
396
|
+
html = html.replace(modulesRegex, `PRELOAD_MODULES = ${escapedModulesStr};`);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法处理 PRELOAD_MODULES: ${error}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// 11. 注入 ScriptHandler 补丁(参考 PlayCanvas 官方 one-page-inline-game-scripts.js)
|
|
403
|
+
// 这个补丁覆盖 pc.ScriptHandler.prototype._loadScript,使其能够:
|
|
404
|
+
// 1. 正确处理 data URL 格式的脚本
|
|
405
|
+
// 2. 对于空脚本(已打包到 IIFE 中的),直接跳过执行
|
|
406
|
+
const scriptHandlerPatch = `
|
|
407
|
+
<script>
|
|
408
|
+
// ScriptHandler patch for one-page build (empty scripts are already bundled in IIFE)
|
|
409
|
+
(function() {
|
|
410
|
+
if (typeof pc === 'undefined' || !pc.ScriptHandler) return;
|
|
411
|
+
|
|
412
|
+
var originalLoadScript = pc.ScriptHandler.prototype._loadScript;
|
|
413
|
+
pc.ScriptHandler.prototype._loadScript = function(url, callback) {
|
|
414
|
+
// Check if this is a data URL
|
|
415
|
+
if (url && url.startsWith('data:text/javascript;base64,')) {
|
|
416
|
+
var head = document.head;
|
|
417
|
+
var element = document.createElement('script');
|
|
418
|
+
this._cache[url] = element;
|
|
419
|
+
element.async = false;
|
|
420
|
+
|
|
421
|
+
// Decode base64 content
|
|
422
|
+
var index = url.indexOf(',');
|
|
423
|
+
var base64 = url.slice(index + 1);
|
|
424
|
+
|
|
425
|
+
// If empty content (scripts already bundled), skip execution
|
|
426
|
+
if (!base64 || base64.length === 0) {
|
|
427
|
+
// Still create the element for cache consistency
|
|
428
|
+
callback(null, url, element);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
var data = window.atob(base64);
|
|
434
|
+
element.innerText = data;
|
|
435
|
+
head.appendChild(element);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.warn('[ScriptHandler] Failed to decode script:', e);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
callback(null, url, element);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fall back to original implementation for non-data URLs
|
|
445
|
+
return originalLoadScript.call(this, url, callback);
|
|
446
|
+
};
|
|
447
|
+
})();
|
|
448
|
+
</script>`;
|
|
449
|
+
// 将补丁注入到 </head> 之前,确保在引擎之后、脚本加载之前执行
|
|
450
|
+
if (html.includes('</head>')) {
|
|
451
|
+
html = html.replace('</head>', `${scriptHandlerPatch}\n</head>`);
|
|
452
|
+
}
|
|
453
|
+
console.log('[PlayCanvasPlugin] ESM Bundle 模式:资源内联完成');
|
|
454
|
+
return html;
|
|
455
|
+
}
|
|
168
456
|
/**
|
|
169
457
|
* 转换 settings 代码中的资源URL为 data URLs
|
|
170
458
|
*/
|
|
@@ -382,6 +670,24 @@ async function inlineModulesScript(html, baseBuildDir) {
|
|
|
382
670
|
* 注意:脚本需要在 pc.AppBase 创建后执行,所以插入到 </body> 之前
|
|
383
671
|
* 同时包装成一个函数,在 DOMContentLoaded 后执行,确保 PlayCanvas 引擎已完全初始化
|
|
384
672
|
*/
|
|
673
|
+
/**
|
|
674
|
+
* 注入打包后的游戏脚本(ESM → IIFE 场景)
|
|
675
|
+
*/
|
|
676
|
+
async function injectBundledGameScripts(html, baseBuildDir) {
|
|
677
|
+
const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
|
|
678
|
+
try {
|
|
679
|
+
await fs.access(gameScriptsPath);
|
|
680
|
+
const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
|
|
681
|
+
// 在 PlayCanvas Engine 之后、__start__.js 之前插入
|
|
682
|
+
if (html.includes('</head>')) {
|
|
683
|
+
html = html.replace('</head>', `<script>${gameScriptsCode}</script>\n</head>`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.warn('[PlayCanvasPlugin] 未找到打包后的游戏脚本');
|
|
688
|
+
}
|
|
689
|
+
return html;
|
|
690
|
+
}
|
|
385
691
|
async function inlineGameScripts(html, baseBuildDir) {
|
|
386
692
|
const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
|
|
387
693
|
try {
|
|
@@ -599,6 +905,7 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
599
905
|
const skippedAssets = [];
|
|
600
906
|
const optimizedAssets = [];
|
|
601
907
|
const skippedScripts = [];
|
|
908
|
+
const missingAssets = []; // 新增:缺失资源列表
|
|
602
909
|
const SIZE_LIMIT = 1 * 1024 * 1024; // 1MB - 跳过超过这个大小的文件
|
|
603
910
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
604
911
|
const file = asset?.file;
|
|
@@ -611,11 +918,32 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
611
918
|
}
|
|
612
919
|
const cleanUrl = url.split('?')[0];
|
|
613
920
|
const fileName = cleanUrl.toLowerCase();
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
if (cleanUrl === '__game-scripts.js'
|
|
921
|
+
// ⚠️ 脚本资源特殊处理(参考 PlayCanvas 官方 one-page.js 实现)
|
|
922
|
+
// ESM Bundle 模式下,脚本代码已被打包到 IIFE 中执行
|
|
923
|
+
// PlayCanvas 官方做法:将脚本内容设为空,配合引擎补丁跳过执行
|
|
924
|
+
if (cleanUrl === '__game-scripts.js') {
|
|
925
|
+
// __game-scripts.js 是 Classic 模式的打包产物
|
|
926
|
+
skippedScripts.push(asset.name || assetId);
|
|
927
|
+
// 使用空内容的 data URL(PlayCanvas 官方做法)
|
|
928
|
+
file.url = 'data:text/javascript;base64,';
|
|
929
|
+
file.hash = '';
|
|
930
|
+
asset.preload = false;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (asset.type === 'script') {
|
|
618
934
|
skippedScripts.push(asset.name || assetId);
|
|
935
|
+
// ⚠️ ESM 脚本特殊处理(参考 PlayCanvas 官方 one-page.js):
|
|
936
|
+
// 官方工具对于 loadingType !== 0 的脚本,会将内容设为空字符串
|
|
937
|
+
// 然后配合 one-page-inline-game-scripts.js 引擎补丁处理
|
|
938
|
+
//
|
|
939
|
+
// 我们的方案:
|
|
940
|
+
// 1. 将脚本 URL 设为空内容的 data URL
|
|
941
|
+
// 2. 清空 hash 避免追加查询参数
|
|
942
|
+
// 3. 脚本代码已经通过 IIFE 中的 pc.createScript() 注册
|
|
943
|
+
// 4. 需要配合引擎补丁让 ScriptHandler 跳过空脚本
|
|
944
|
+
file.url = 'data:text/javascript;base64,'; // 空 JavaScript
|
|
945
|
+
file.hash = ''; // 清空 hash
|
|
946
|
+
asset.preload = false; // 禁用预加载
|
|
619
947
|
continue;
|
|
620
948
|
}
|
|
621
949
|
// 跳过物理引擎缓存文件和大型文本文件
|
|
@@ -626,8 +954,8 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
626
954
|
skippedAssets.push(`${asset.name || assetId} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
|
|
627
955
|
// 标记为不预加载,避免引擎尝试加载
|
|
628
956
|
asset.preload = false;
|
|
629
|
-
//
|
|
630
|
-
file.url =
|
|
957
|
+
// 提供有效的占位符数据,避免解析错误
|
|
958
|
+
file.url = getPlaceholderDataUrl(asset.type, fileName);
|
|
631
959
|
continue;
|
|
632
960
|
}
|
|
633
961
|
const fullPath = path.join(baseBuildDir, cleanUrl);
|
|
@@ -635,26 +963,76 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
635
963
|
// ✅ 简化逻辑:直接读取文件并判断是否需要压缩
|
|
636
964
|
const buffer = await fs.readFile(fullPath);
|
|
637
965
|
const originalSize = buffer.length;
|
|
966
|
+
// 检查空文件,提供有效的占位符
|
|
967
|
+
if (originalSize === 0) {
|
|
968
|
+
console.warn(`⚠️ 警告: 文件为空,使用占位符: ${asset.name || cleanUrl}`);
|
|
969
|
+
file.url = getPlaceholderDataUrl(asset.type, fileName);
|
|
970
|
+
file.size = 0;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
638
973
|
// 检查实际文件大小,跳过超大文件
|
|
639
974
|
if (originalSize > SIZE_LIMIT) {
|
|
640
975
|
skippedAssets.push(`${asset.name || assetId} (${(originalSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
641
976
|
asset.preload = false;
|
|
642
|
-
|
|
977
|
+
// 提供有效的占位符数据,避免解析错误
|
|
978
|
+
file.url = getPlaceholderDataUrl(asset.type, fileName);
|
|
643
979
|
continue;
|
|
644
980
|
}
|
|
645
981
|
let dataUrl;
|
|
646
982
|
let finalSize;
|
|
983
|
+
// 处理 JSON 文件:去除注释和尾随逗号,验证格式
|
|
984
|
+
if (asset.type === 'json' || fileName.endsWith('.json')) {
|
|
985
|
+
try {
|
|
986
|
+
let jsonText = buffer.toString('utf-8');
|
|
987
|
+
// 移除单行注释 // ...
|
|
988
|
+
jsonText = jsonText.replace(/\/\/.*$/gm, '');
|
|
989
|
+
// 移除多行注释 /* ... */
|
|
990
|
+
jsonText = jsonText.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
991
|
+
// 移除尾随逗号
|
|
992
|
+
jsonText = jsonText.replace(/,(\s*[}\]])/g, '$1');
|
|
993
|
+
// 验证是否为有效 JSON
|
|
994
|
+
const parsed = JSON.parse(jsonText);
|
|
995
|
+
const cleanJson = JSON.stringify(parsed);
|
|
996
|
+
dataUrl = `data:application/json;base64,${Buffer.from(cleanJson).toString('base64')}`;
|
|
997
|
+
finalSize = Buffer.from(cleanJson).length;
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
console.warn(`⚠️ 警告: JSON 文件格式无效,使用空对象: ${asset.name || cleanUrl}`);
|
|
1001
|
+
const emptyJson = '{}';
|
|
1002
|
+
dataUrl = `data:application/json;base64,${Buffer.from(emptyJson).toString('base64')}`;
|
|
1003
|
+
finalSize = 2;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
647
1006
|
// ✅ 核心优化:如果是图片,使用 sharp 压缩
|
|
648
|
-
if (isImageFile(cleanUrl)) {
|
|
1007
|
+
else if (isImageFile(cleanUrl)) {
|
|
649
1008
|
const compressed = await compressImage(buffer, cleanUrl, {
|
|
650
1009
|
convertToWebP: true,
|
|
651
1010
|
quality: 75,
|
|
652
1011
|
});
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1012
|
+
// 防御性检查:如果压缩后的 buffer 为空,使用原始 buffer
|
|
1013
|
+
if (!compressed.buffer || compressed.buffer.length === 0) {
|
|
1014
|
+
console.warn(`⚠️ 警告: 压缩后的图片为空,使用原始文件: ${asset.name || cleanUrl}`);
|
|
1015
|
+
const mime = guessMimeType(cleanUrl);
|
|
1016
|
+
dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
|
|
1017
|
+
finalSize = originalSize;
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
const base64 = compressed.buffer.toString('base64');
|
|
1021
|
+
// 额外检查:确保 base64 不为空
|
|
1022
|
+
if (!base64 || base64.length === 0) {
|
|
1023
|
+
console.warn(`⚠️ 警告: base64 编码为空: ${asset.name || cleanUrl}`);
|
|
1024
|
+
const mime = guessMimeType(cleanUrl);
|
|
1025
|
+
dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
|
|
1026
|
+
finalSize = originalSize;
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
dataUrl = `data:${compressed.mime};base64,${base64}`;
|
|
1030
|
+
finalSize = compressed.buffer.length;
|
|
1031
|
+
}
|
|
1032
|
+
// 记录优化效果
|
|
1033
|
+
const savings = ((1 - finalSize / originalSize) * 100).toFixed(1);
|
|
1034
|
+
optimizedAssets.push(`${asset.name || assetId}: ${(originalSize / 1024).toFixed(1)}KB → ${(finalSize / 1024).toFixed(1)}KB (-${savings}%)`);
|
|
1035
|
+
}
|
|
658
1036
|
}
|
|
659
1037
|
else {
|
|
660
1038
|
// 非图片文件,直接 base64 编码
|
|
@@ -674,7 +1052,13 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
674
1052
|
}
|
|
675
1053
|
}
|
|
676
1054
|
catch (error) {
|
|
677
|
-
|
|
1055
|
+
// 记录缺失资源详情,不只是简单警告
|
|
1056
|
+
missingAssets.push({
|
|
1057
|
+
name: asset.name || assetId,
|
|
1058
|
+
url: cleanUrl,
|
|
1059
|
+
type: asset.type || 'unknown',
|
|
1060
|
+
size: file.size,
|
|
1061
|
+
});
|
|
678
1062
|
}
|
|
679
1063
|
}
|
|
680
1064
|
// 输出统计信息
|
|
@@ -702,6 +1086,40 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
702
1086
|
console.log(` ... 还有 ${skippedScripts.length - 5} 个脚本`);
|
|
703
1087
|
}
|
|
704
1088
|
}
|
|
1089
|
+
// ⚠️ 重要:显示缺失资源的详细警告
|
|
1090
|
+
if (missingAssets.length > 0) {
|
|
1091
|
+
console.log('\n');
|
|
1092
|
+
console.warn(`⚠️ ==================== 缺失资源警告 ====================`);
|
|
1093
|
+
console.warn(`❌ 发现 ${missingAssets.length} 个资源文件不存在!`);
|
|
1094
|
+
console.warn(` 这些资源将不会被打包,可能导致游戏功能缺失!\n`);
|
|
1095
|
+
// 按类型分组显示
|
|
1096
|
+
const groupedByType = {};
|
|
1097
|
+
for (const asset of missingAssets) {
|
|
1098
|
+
const type = asset.type || 'unknown';
|
|
1099
|
+
if (!groupedByType[type]) {
|
|
1100
|
+
groupedByType[type] = [];
|
|
1101
|
+
}
|
|
1102
|
+
groupedByType[type].push(asset);
|
|
1103
|
+
}
|
|
1104
|
+
for (const [type, assets] of Object.entries(groupedByType)) {
|
|
1105
|
+
console.warn(` [${type}] ${assets.length} 个:`);
|
|
1106
|
+
const displayCount = Math.min(assets.length, 5);
|
|
1107
|
+
for (let i = 0; i < displayCount; i++) {
|
|
1108
|
+
const asset = assets[i];
|
|
1109
|
+
const sizeInfo = asset.size ? ` (${(asset.size / 1024).toFixed(1)}KB)` : '';
|
|
1110
|
+
console.warn(` - ${asset.name}${sizeInfo}`);
|
|
1111
|
+
console.warn(` 路径: ${asset.url}`);
|
|
1112
|
+
}
|
|
1113
|
+
if (assets.length > 5) {
|
|
1114
|
+
console.warn(` ... 还有 ${assets.length - 5} 个 ${type} 资源`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
console.warn(`\n 💡 建议:`);
|
|
1118
|
+
console.warn(` 1. 检查 Base Build 是否包含所有资源文件`);
|
|
1119
|
+
console.warn(` 2. 确认资源路径是否正确`);
|
|
1120
|
+
console.warn(` 3. 如果使用场景选择功能,确认所选场景包含所有依赖资源`);
|
|
1121
|
+
console.warn(`⚠️ ======================================================\n`);
|
|
1122
|
+
}
|
|
705
1123
|
return configJson;
|
|
706
1124
|
}
|
|
707
1125
|
async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
@@ -715,6 +1133,30 @@ async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
|
715
1133
|
return startCode;
|
|
716
1134
|
}
|
|
717
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* 为跳过的资源生成有效的占位符 data URL
|
|
1138
|
+
*/
|
|
1139
|
+
function getPlaceholderDataUrl(assetType, fileName) {
|
|
1140
|
+
// 1x1 透明 PNG 的 base64 编码(最小有效图片)
|
|
1141
|
+
const transparentPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
1142
|
+
// JSON 类型:返回空 JSON 对象
|
|
1143
|
+
if (assetType === 'json' || fileName.endsWith('.json')) {
|
|
1144
|
+
return `data:application/json;base64,${Buffer.from('{}').toString('base64')}`;
|
|
1145
|
+
}
|
|
1146
|
+
// 图片/纹理类型:返回 1x1 透明 PNG
|
|
1147
|
+
// 优先检查 assetType,因为文件名可能没有扩展名
|
|
1148
|
+
if (assetType === 'texture' || assetType === 'textureatlas' || isImageFile(fileName)) {
|
|
1149
|
+
return `data:image/png;base64,${transparentPng}`;
|
|
1150
|
+
}
|
|
1151
|
+
// 文本类型:返回单个空格的 base64
|
|
1152
|
+
const mime = guessMimeType(fileName);
|
|
1153
|
+
if (mime.startsWith('text/') || assetType === 'text' || assetType === 'html' || assetType === 'css') {
|
|
1154
|
+
return `data:${mime};base64,${Buffer.from(' ').toString('base64')}`;
|
|
1155
|
+
}
|
|
1156
|
+
// 默认:对于所有其他类型,使用 1x1 透明 PNG 作为通用占位符
|
|
1157
|
+
// 这对大多数二进制格式都是安全的
|
|
1158
|
+
return `data:image/png;base64,${transparentPng}`;
|
|
1159
|
+
}
|
|
718
1160
|
/**
|
|
719
1161
|
* 判断是否为图片文件
|
|
720
1162
|
*/
|
|
@@ -743,6 +1185,11 @@ async function compressImage(buffer, filePath, options) {
|
|
|
743
1185
|
const webpBuffer = await image
|
|
744
1186
|
.webp({ quality, effort: 6 })
|
|
745
1187
|
.toBuffer();
|
|
1188
|
+
// 防御性检查:如果压缩后为空,返回原始 buffer
|
|
1189
|
+
if (!webpBuffer || webpBuffer.length === 0) {
|
|
1190
|
+
console.warn(`警告: WebP 转换后为空,使用原始格式`);
|
|
1191
|
+
return { buffer, mime: guessMimeType(filePath) };
|
|
1192
|
+
}
|
|
746
1193
|
return {
|
|
747
1194
|
buffer: webpBuffer,
|
|
748
1195
|
mime: 'image/webp',
|
|
@@ -754,12 +1201,22 @@ async function compressImage(buffer, filePath, options) {
|
|
|
754
1201
|
const pngBuffer = await image
|
|
755
1202
|
.png({ quality: 80, compressionLevel: 9 })
|
|
756
1203
|
.toBuffer();
|
|
1204
|
+
// 防御性检查
|
|
1205
|
+
if (!pngBuffer || pngBuffer.length === 0) {
|
|
1206
|
+
console.warn(`警告: PNG 压缩后为空,使用原始文件`);
|
|
1207
|
+
return { buffer, mime: 'image/png' };
|
|
1208
|
+
}
|
|
757
1209
|
return { buffer: pngBuffer, mime: 'image/png' };
|
|
758
1210
|
case '.jpg':
|
|
759
1211
|
case '.jpeg':
|
|
760
1212
|
const jpgBuffer = await image
|
|
761
1213
|
.jpeg({ quality: options?.quality ?? 80, mozjpeg: true })
|
|
762
1214
|
.toBuffer();
|
|
1215
|
+
// 防御性检查
|
|
1216
|
+
if (!jpgBuffer || jpgBuffer.length === 0) {
|
|
1217
|
+
console.warn(`警告: JPEG 压缩后为空,使用原始文件`);
|
|
1218
|
+
return { buffer, mime: 'image/jpeg' };
|
|
1219
|
+
}
|
|
763
1220
|
return { buffer: jpgBuffer, mime: 'image/jpeg' };
|
|
764
1221
|
default:
|
|
765
1222
|
// 其他格式不压缩
|