@playcraft/build 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/analyzers/scene-asset-collector.js +99 -9
  2. package/dist/base-builder.d.ts +15 -78
  3. package/dist/base-builder.js +34 -741
  4. package/dist/engines/engine-detector.d.ts +38 -0
  5. package/dist/engines/engine-detector.js +201 -0
  6. package/dist/engines/generic-adapter.d.ts +71 -0
  7. package/dist/engines/generic-adapter.js +378 -0
  8. package/dist/engines/index.d.ts +7 -0
  9. package/dist/engines/index.js +7 -0
  10. package/dist/engines/playcanvas-adapter.d.ts +85 -0
  11. package/dist/engines/playcanvas-adapter.js +813 -0
  12. package/dist/generators/config-generator.js +59 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/loaders/playcraft-loader.js +240 -5
  16. package/dist/platforms/adikteev.d.ts +1 -1
  17. package/dist/platforms/adikteev.js +30 -36
  18. package/dist/platforms/applovin.d.ts +1 -1
  19. package/dist/platforms/applovin.js +31 -36
  20. package/dist/platforms/base.d.ts +27 -5
  21. package/dist/platforms/base.js +79 -181
  22. package/dist/platforms/bigo.d.ts +1 -1
  23. package/dist/platforms/bigo.js +28 -28
  24. package/dist/platforms/facebook.d.ts +1 -1
  25. package/dist/platforms/facebook.js +21 -10
  26. package/dist/platforms/google.d.ts +1 -1
  27. package/dist/platforms/google.js +28 -21
  28. package/dist/platforms/index.d.ts +1 -0
  29. package/dist/platforms/index.js +4 -0
  30. package/dist/platforms/inmobi.d.ts +1 -1
  31. package/dist/platforms/inmobi.js +27 -34
  32. package/dist/platforms/ironsource.d.ts +1 -1
  33. package/dist/platforms/ironsource.js +37 -40
  34. package/dist/platforms/liftoff.d.ts +1 -1
  35. package/dist/platforms/liftoff.js +22 -30
  36. package/dist/platforms/mintegral.d.ts +10 -0
  37. package/dist/platforms/mintegral.js +65 -0
  38. package/dist/platforms/moloco.d.ts +1 -1
  39. package/dist/platforms/moloco.js +18 -20
  40. package/dist/platforms/playcraft.d.ts +1 -1
  41. package/dist/platforms/playcraft.js +2 -2
  42. package/dist/platforms/remerge.d.ts +1 -1
  43. package/dist/platforms/remerge.js +19 -20
  44. package/dist/platforms/snapchat.d.ts +1 -1
  45. package/dist/platforms/snapchat.js +32 -26
  46. package/dist/platforms/tiktok.d.ts +1 -1
  47. package/dist/platforms/tiktok.js +28 -24
  48. package/dist/platforms/unity.d.ts +1 -1
  49. package/dist/platforms/unity.js +30 -36
  50. package/dist/playable-builder.d.ts +1 -0
  51. package/dist/playable-builder.js +16 -2
  52. package/dist/templates/__loading__.js +100 -0
  53. package/dist/templates/__modules__.js +47 -0
  54. package/dist/templates/__settings__.template.js +20 -0
  55. package/dist/templates/__start__.js +332 -0
  56. package/dist/templates/index.html +18 -0
  57. package/dist/templates/logo.png +0 -0
  58. package/dist/templates/manifest.json +1 -0
  59. package/dist/templates/patches/cannon.min.js +28 -0
  60. package/dist/templates/patches/lz4.js +10 -0
  61. package/dist/templates/patches/one-page-http-get.js +20 -0
  62. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  63. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  64. package/dist/templates/patches/p2.min.js +27 -0
  65. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  66. package/dist/templates/playcanvas-stable.min.js +16363 -0
  67. package/dist/templates/styles.css +43 -0
  68. package/dist/types.d.ts +113 -1
  69. package/dist/types.js +77 -1
  70. package/dist/utils/ammo-detector.d.ts +9 -0
  71. package/dist/utils/ammo-detector.js +76 -0
  72. package/dist/utils/build-mode-detector.js +2 -0
  73. package/dist/utils/minify.d.ts +32 -0
  74. package/dist/utils/minify.js +82 -0
  75. package/dist/vite/config-builder-generic.d.ts +70 -0
  76. package/dist/vite/config-builder-generic.js +251 -0
  77. package/dist/vite/config-builder.d.ts +8 -0
  78. package/dist/vite/config-builder.js +53 -16
  79. package/dist/vite/platform-configs.js +29 -1
  80. package/dist/vite/plugin-compress-js.d.ts +21 -0
  81. package/dist/vite/plugin-compress-js.js +213 -0
  82. package/dist/vite/plugin-esm-html-generator.js +5 -1
  83. package/dist/vite/plugin-platform.d.ts +5 -0
  84. package/dist/vite/plugin-platform.js +499 -35
  85. package/dist/vite/plugin-playcanvas.js +21 -68
  86. package/dist/vite/plugin-source-builder.js +102 -21
  87. package/dist/vite-builder.d.ts +25 -7
  88. package/dist/vite-builder.js +141 -52
  89. package/package.json +4 -2
  90. package/physics/cannon-rigidbody-adapter.js +243 -22
  91. package/templates/__loading__.js +0 -12
  92. package/templates/index.esm.mjs +0 -11
  93. package/templates/patches/playcraft-cta-adapter.js +129 -31
  94. package/templates/patches/scene-physics-defaults.js +49 -0
  95. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  96. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { createRequire } from 'module';
5
+ import { minifyPatchCode } from '../utils/minify.js';
5
6
  /**
6
7
  * PlayCanvas Vite 插件
7
8
  * 处理 PlayCanvas 特定的资源转换和内联
@@ -302,42 +303,7 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
302
303
  sceneDataUrl = sceneUrl || '';
303
304
  }
304
305
  }
305
- // 7. 内联 logo.png
306
- // index.esm.mjs 中使用 `${ASSET_PREFIX}logo.png` 引用 logo
307
- // 需要将 ASSET_PREFIX 替换为 data URL 或直接替换 logo 引用
308
- if (options.convertDataUrls) {
309
- const logoPath = path.join(baseBuildDir, 'logo.png');
310
- try {
311
- const logoBuffer = await fs.readFile(logoPath);
312
- const logoDataUrl = `data:image/png;base64,${logoBuffer.toString('base64')}`;
313
- // 替换 `${ASSET_PREFIX}logo.png` 模式
314
- // 由于代码被打包为 IIFE,模板字符串可能被转换为字符串拼接
315
- // 格式1: `${ASSET_PREFIX}logo.png`
316
- // 格式2: ASSET_PREFIX + "logo.png"
317
- // 格式3: "".concat(ASSET_PREFIX, "logo.png") (Vite 打包后)
318
- const escapedLogoDataUrl = logoDataUrl.replace(/\$/g, '$$$$');
319
- // 替换模板字符串格式(原始格式)
320
- html = html.replace(/\$\{ASSET_PREFIX\}logo\.png/g, escapedLogoDataUrl);
321
- // 替换字符串拼接格式
322
- html = html.replace(/ASSET_PREFIX\s*\+\s*["']logo\.png["']/g, `"${escapedLogoDataUrl}"`);
323
- // 替换 concat 格式 (Vite/Rollup 打包后常见格式)
324
- html = html.replace(/["']?["']?\.concat\(ASSET_PREFIX,\s*["']logo\.png["']\)/g, `"${escapedLogoDataUrl}"`);
325
- // 替换反引号模板字符串在 IIFE 中的编译结果
326
- // `${ASSET_PREFIX}logo.png` 可能被编译为 ASSET_PREFIX+"logo.png"
327
- html = html.replace(/`\$\{ASSET_PREFIX\}logo\.png`/g, `"${escapedLogoDataUrl}"`);
328
- // ⚠️ 重要:Vite 压缩后,直接查找 "logo.png" 字符串
329
- // 格式: .src="logo.png" 或 src:"logo.png" 或 ="logo.png"
330
- const logoReplaceCount = (html.match(/["']logo\.png["']/g) || []).length;
331
- if (logoReplaceCount > 0) {
332
- html = html.replace(/["']logo\.png["']/g, `"${escapedLogoDataUrl}"`);
333
- console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${logoReplaceCount} 处 "logo.png" 字符串`);
334
- }
335
- console.log(`[PlayCanvasPlugin] ESM Bundle: logo.png 已内联`);
336
- }
337
- catch (error) {
338
- console.warn(`[PlayCanvasPlugin] ESM Bundle: logo.png 不存在,跳过内联`);
339
- }
340
- }
306
+ // 7. (Logo removed - no longer needed)
341
307
  // 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
342
308
  // 方案1+3:直接将 CONFIG_FILENAME = "config.json" 替换为 data URL
343
309
  // 8.1 替换 CONFIG_FILENAME
@@ -425,28 +391,22 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
425
391
  // 这个补丁覆盖 pc.ScriptHandler.prototype._loadScript,使其能够:
426
392
  // 1. 正确处理 data URL 格式的脚本
427
393
  // 2. 对于空脚本(已打包到 IIFE 中的),直接跳过执行
428
- const scriptHandlerPatch = `
429
- <script>
430
- // ScriptHandler patch for one-page build (empty scripts are already bundled in IIFE)
394
+ const scriptHandlerPatchCode = `
431
395
  (function() {
432
396
  if (typeof pc === 'undefined' || !pc.ScriptHandler) return;
433
397
 
434
398
  var originalLoadScript = pc.ScriptHandler.prototype._loadScript;
435
399
  pc.ScriptHandler.prototype._loadScript = function(url, callback) {
436
- // Check if this is a data URL
437
400
  if (url && url.startsWith('data:text/javascript;base64,')) {
438
401
  var head = document.head;
439
402
  var element = document.createElement('script');
440
403
  this._cache[url] = element;
441
404
  element.async = false;
442
405
 
443
- // Decode base64 content
444
406
  var index = url.indexOf(',');
445
407
  var base64 = url.slice(index + 1);
446
408
 
447
- // If empty content (scripts already bundled), skip execution
448
409
  if (!base64 || base64.length === 0) {
449
- // Still create the element for cache consistency
450
410
  callback(null, url, element);
451
411
  return;
452
412
  }
@@ -463,14 +423,13 @@ async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext)
463
423
  return;
464
424
  }
465
425
 
466
- // Fall back to original implementation for non-data URLs
467
426
  return originalLoadScript.call(this, url, callback);
468
427
  };
469
- })();
470
- </script>`;
428
+ })();`;
429
+ const minifiedScriptHandlerPatch = await minifyPatchCode(scriptHandlerPatchCode, 'scriptHandlerPatch');
471
430
  // 将补丁注入到 </head> 之前,确保在引擎之后、脚本加载之前执行
472
431
  if (html.includes('</head>')) {
473
- html = html.replace('</head>', `${scriptHandlerPatch}\n</head>`);
432
+ html = html.replace('</head>', `<script>${minifiedScriptHandlerPatch}</script>\n</head>`);
474
433
  }
475
434
  console.log('[PlayCanvasPlugin] ESM Bundle 模式:资源内联完成');
476
435
  return html;
@@ -759,9 +718,6 @@ async function inlineGameScripts(html, baseBuildDir) {
759
718
  async function inlineStartScript(html, baseBuildDir, options) {
760
719
  const startPath = path.join(baseBuildDir, '__start__.js');
761
720
  let startCode = await fs.readFile(startPath, 'utf-8');
762
- if (options.convertDataUrls) {
763
- startCode = await inlineLogoInStartScript(startCode, baseBuildDir);
764
- }
765
721
  // 替换 script 标签
766
722
  const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
767
723
  if (scriptPattern.test(html)) {
@@ -783,9 +739,6 @@ async function inlineLoadingScript(html, baseBuildDir) {
783
739
  await fs.access(loadingPath);
784
740
  // 文件存在,内联它
785
741
  let loadingCode = await fs.readFile(loadingPath, 'utf-8');
786
- if (loadingCode.includes('${ASSET_PREFIX}logo.png')) {
787
- loadingCode = await inlineLogoInStartScript(loadingCode, baseBuildDir);
788
- }
789
742
  if (scriptPattern.test(html)) {
790
743
  html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
791
744
  }
@@ -1177,17 +1130,6 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
1177
1130
  }
1178
1131
  return configJson;
1179
1132
  }
1180
- async function inlineLogoInStartScript(startCode, baseBuildDir) {
1181
- const logoPath = path.join(baseBuildDir, 'logo.png');
1182
- try {
1183
- const buffer = await fs.readFile(logoPath);
1184
- const dataUrl = `data:image/png;base64,${buffer.toString('base64')}`;
1185
- return startCode.replace(/\$\{ASSET_PREFIX\}logo\.png/g, dataUrl);
1186
- }
1187
- catch (error) {
1188
- return startCode;
1189
- }
1190
- }
1191
1133
  /**
1192
1134
  * 为跳过的资源生成有效的占位符 data URL
1193
1135
  */
@@ -1387,19 +1329,30 @@ async function getEnginePatchScripts(options) {
1387
1329
  const scripts = [];
1388
1330
  if (options.patchXhrOut) {
1389
1331
  const patchCode = await readPatchFile('playcraft-no-xhr.js');
1390
- scripts.push(`<script>${patchCode}</script>`);
1332
+ const minified = await minifyPatchCode(patchCode, 'playcraft-no-xhr.js');
1333
+ scripts.push(`<script>${minified}</script>`);
1391
1334
  }
1392
1335
  else if (options.configJsonInline) {
1393
1336
  const patchCode = await readPatchFile('one-page-http-get.js');
1394
- scripts.push(`<script>${patchCode}</script>`);
1337
+ const minified = await minifyPatchCode(patchCode, 'one-page-http-get.js');
1338
+ scripts.push(`<script>${minified}</script>`);
1395
1339
  }
1396
1340
  if (options.inlineGameScripts) {
1397
1341
  const patchCode = await readPatchFile('one-page-inline-game-scripts.js');
1398
- scripts.push(`<script>${patchCode}</script>`);
1342
+ const minified = await minifyPatchCode(patchCode, 'one-page-inline-game-scripts.js');
1343
+ scripts.push(`<script>${minified}</script>`);
1399
1344
  }
1400
1345
  if (options.mraidSupport) {
1401
1346
  const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
1402
- scripts.push(`<script>${patchCode}</script>`);
1347
+ const minified = await minifyPatchCode(patchCode, 'one-page-mraid-resize-canvas.js');
1348
+ scripts.push(`<script>${minified}</script>`);
1349
+ }
1350
+ // 注入 Scene applySettings 补丁:确保 settings.physics/render 存在,避免 "Cannot read properties of undefined (reading 'gravity')" 报错
1351
+ // 场景:ammoReplacement 或部分场景 JSON 缺少 physics 配置
1352
+ const scenePhysicsPatch = await readPatchFile('scene-physics-defaults.js');
1353
+ if (scenePhysicsPatch) {
1354
+ const minified = await minifyPatchCode(scenePhysicsPatch, 'scene-physics-defaults.js');
1355
+ scripts.push(`<script>${minified}</script>`);
1403
1356
  }
1404
1357
  // 如果启用了 config.json 压缩,需要注入 LZ4 解压运行时代码
1405
1358
  // 注意:必须在 compressEngine 之前检查,因为 compressEngine 也会注入 lz4.js
@@ -52,6 +52,39 @@ export function viteSourceBuilderPlugin(options) {
52
52
  if (scriptPathMap.has(id)) {
53
53
  return scriptPathMap.get(id);
54
54
  }
55
+ // 通过 Import Map 解析裸模块说明符(如 gameRule、planck 等)
56
+ // Import Map 中的路径是相对于 assets/ 目录的(Import Map 文件本身在 assets/Import Map.json 中)
57
+ if (options.importMap?.content?.imports && !id.startsWith('./') && !id.startsWith('../') && !id.startsWith('/') && !id.startsWith('\0')) {
58
+ const imports = options.importMap.content.imports;
59
+ const assetsDir = path.join(options.projectDir, 'assets');
60
+ // 精确匹配(如 "gameRule" -> "./Generated/GameRule/BreakshotPoolRule.mjs")
61
+ if (imports[id]) {
62
+ let resolved = imports[id];
63
+ // 去掉开头的 ./
64
+ if (resolved.startsWith('./')) {
65
+ resolved = resolved.slice(2);
66
+ }
67
+ const absPath = path.join(assetsDir, resolved);
68
+ console.log(`[SourceBuilder] Import Map 解析: ${id} -> ${absPath}`);
69
+ return absPath;
70
+ }
71
+ // 前缀匹配(如 "Gameplay/" -> "./LiteCreator/Gameplay/")
72
+ for (const [prefix, target] of Object.entries(imports)) {
73
+ if (prefix.endsWith('/') && id.startsWith(prefix)) {
74
+ const remainder = id.slice(prefix.length);
75
+ let targetBase = target;
76
+ if (targetBase.startsWith('./')) {
77
+ targetBase = targetBase.slice(2);
78
+ }
79
+ if (targetBase.endsWith('/')) {
80
+ targetBase = targetBase.slice(0, -1);
81
+ }
82
+ const absPath = path.join(assetsDir, targetBase, remainder);
83
+ console.log(`[SourceBuilder] Import Map 前缀解析: ${id} -> ${absPath}`);
84
+ return absPath;
85
+ }
86
+ }
87
+ }
55
88
  // 处理相对导入(../xxx 或 ./xxx)
56
89
  if (importer && (id.startsWith('./') || id.startsWith('../'))) {
57
90
  // 获取导入者的模块路径
@@ -277,7 +310,6 @@ async function copyTemplates(outputDir) {
277
310
  '__loading__.js',
278
311
  'playcanvas-stable.min.js',
279
312
  'styles.css',
280
- 'logo.png',
281
313
  'manifest.json',
282
314
  ];
283
315
  for (const file of templateFiles) {
@@ -373,11 +405,21 @@ function collectRegisteredScriptNames(assets) {
373
405
  if (scriptName) {
374
406
  scriptNames.add(scriptName);
375
407
  }
376
- // 如果脚本资产有 data.scripts 数组(Classic 格式),也收集这些名称
377
- if (asset.data?.scripts && Array.isArray(asset.data.scripts)) {
378
- for (const s of asset.data.scripts) {
379
- if (s?.name)
380
- scriptNames.add(s.name);
408
+ // Classic 格式的 data.scripts 可能是对象或数组
409
+ if (asset.data?.scripts) {
410
+ if (Array.isArray(asset.data.scripts)) {
411
+ // 数组格式: [{ name: 'xxx' }, ...]
412
+ for (const s of asset.data.scripts) {
413
+ if (s?.name)
414
+ scriptNames.add(s.name);
415
+ }
416
+ }
417
+ else if (typeof asset.data.scripts === 'object') {
418
+ // 对象格式: { "followCamera": { attributes: {...} }, ... }
419
+ // 这是 PlayCanvas/PlayCraft 中 Classic 脚本的标准格式
420
+ for (const scriptKey of Object.keys(asset.data.scripts)) {
421
+ scriptNames.add(scriptKey);
422
+ }
381
423
  }
382
424
  }
383
425
  }
@@ -542,8 +584,28 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
542
584
  let notFoundCount = 0;
543
585
  for (const [assetId, asset] of Object.entries(assets)) {
544
586
  const assetData = asset;
545
- // 跳过已有 url 或没有 file 的资源
546
- if (!assetData.file || assetData.file.url) {
587
+ // 跳过没有 file 的资源
588
+ if (!assetData.file) {
589
+ continue;
590
+ }
591
+ // 如果已有有效的本地 url(非 API URL),验证文件是否存在
592
+ if (assetData.file.url && !assetData.file.url.startsWith('/api/') && !assetData.file.url.startsWith('http')) {
593
+ // 已有有效的本地 url,验证文件存在性
594
+ try {
595
+ await fs.access(path.join(projectDir, assetData.file.url));
596
+ continue; // 文件存在,跳过
597
+ }
598
+ catch {
599
+ // 文件不存在,清除 url 继续搜索
600
+ delete assetData.file.url;
601
+ }
602
+ }
603
+ // 如果 url 是 API URL(PlayCraft 格式残留),清除它
604
+ if (assetData.file.url && (assetData.file.url.startsWith('/api/') || assetData.file.url.startsWith('http'))) {
605
+ delete assetData.file.url;
606
+ }
607
+ // 如果已经有有效 url 了(由之前的 loader 设置的),跳过
608
+ if (assetData.file.url) {
547
609
  continue;
548
610
  }
549
611
  const filename = assetData.file.filename;
@@ -552,11 +614,19 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
552
614
  }
553
615
  const revision = assetData.revision ?? assetData.file.revision ?? 1;
554
616
  const candidates = [
617
+ // PlayCanvas 标准结构
555
618
  path.join('files', 'assets', String(assetId), String(revision), filename),
556
619
  path.join('files', 'assets', String(assetId), '1', filename),
557
620
  path.join('files', 'assets', String(assetId), filename),
558
621
  path.join('files', filename),
559
622
  ];
623
+ // PlayCraft 结构:使用 treePath 或 path 字段推断文件位置
624
+ if (assetData.treePath) {
625
+ candidates.push(path.join(assetData.treePath, filename));
626
+ }
627
+ if (assetData.path && typeof assetData.path === 'string') {
628
+ candidates.push(assetData.path);
629
+ }
560
630
  let found = false;
561
631
  for (const rel of candidates) {
562
632
  const abs = path.join(projectDir, rel);
@@ -699,7 +769,6 @@ async function generateESMTemplate(projectConfig, options, config) {
699
769
  '__modules__.js',
700
770
  '__loading__.js',
701
771
  'styles.css',
702
- 'logo.png',
703
772
  'manifest.json',
704
773
  ];
705
774
  for (const file of templateFiles) {
@@ -737,12 +806,19 @@ function fixImportMapPaths(importMap) {
737
806
  * 收集 ESM 脚本的导入路径
738
807
  */
739
808
  async function collectESMScriptImports(projectConfig, options) {
740
- if (projectConfig.format !== 'playcanvas') {
741
- return [];
809
+ // 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
810
+ let scriptIds;
811
+ let assets;
812
+ if (projectConfig.format === 'playcanvas') {
813
+ const pcProject = projectConfig;
814
+ scriptIds = pcProject.project.settings?.scripts || [];
815
+ assets = pcProject.assets;
816
+ }
817
+ else {
818
+ const pcProject = projectConfig;
819
+ scriptIds = pcProject.manifest.settings?.scripts || [];
820
+ assets = pcProject.assets;
742
821
  }
743
- const pcProject = projectConfig;
744
- const scriptIds = pcProject.project.settings?.scripts || [];
745
- const assets = pcProject.assets;
746
822
  const importPaths = [];
747
823
  // 构建文件夹 ID 到名称的映射
748
824
  const folderMap = new Map();
@@ -868,14 +944,19 @@ async function generateESMEntry(projectConfig, outputDir, scriptImportPaths = []
868
944
  async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, importMap) {
869
945
  const scriptUrlMap = new Map();
870
946
  console.log(`[copyUserScriptsForESM] 项目格式: ${projectConfig.format}`);
871
- if (projectConfig.format !== 'playcanvas') {
872
- // PlayCraft 格式暂不支持
873
- console.log('[copyUserScriptsForESM] 跳过:非 PlayCanvas 格式');
874
- return scriptUrlMap;
947
+ // 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
948
+ let scriptIds;
949
+ let assets;
950
+ if (projectConfig.format === 'playcanvas') {
951
+ const pcProject = projectConfig;
952
+ scriptIds = pcProject.project.settings?.scripts || [];
953
+ assets = pcProject.assets;
954
+ }
955
+ else {
956
+ const pcProject = projectConfig;
957
+ scriptIds = pcProject.manifest.settings?.scripts || [];
958
+ assets = pcProject.assets;
875
959
  }
876
- const pcProject = projectConfig;
877
- const scriptIds = pcProject.project.settings?.scripts || [];
878
- const assets = pcProject.assets;
879
960
  console.log(`[copyUserScriptsForESM] 脚本数量: ${scriptIds.length}`);
880
961
  // 构建文件夹 ID 到名称的映射
881
962
  const folderMap = new Map();
@@ -10,37 +10,55 @@ export interface ViteBuildOutput {
10
10
  *
11
11
  * 职责:
12
12
  * 1. 验证输入是有效的基础构建
13
- * 2. 创建 Vite 配置
13
+ * 2. 根据引擎类型创建对应的 Vite 配置
14
14
  * 3. 执行 Vite 构建
15
15
  * 4. 将输出复制到最终目录
16
16
  * 5. 验证输出大小
17
17
  * 6. 生成报告
18
+ *
19
+ * 引擎路由:
20
+ * - PlayCanvas:使用 ViteConfigBuilder(处理 config.json、__start__.js 等)
21
+ * - 其他引擎:使用 GenericViteConfigBuilder(通用 HTML 内联 + 平台 SDK 注入)
18
22
  */
19
23
  export declare class ViteBuilder {
20
24
  private baseBuildDir;
21
25
  private options;
22
26
  private sizeReport;
27
+ private engine;
23
28
  constructor(baseBuildDir: string, options: BuildOptions);
24
29
  /**
25
30
  * 执行构建
26
31
  */
27
32
  build(): Promise<string>;
28
33
  /**
29
- * 将 Vite 输出复制到最终目录
34
+ * 读取构建元数据
30
35
  */
31
- private copyOutputToFinalDir;
36
+ private readBuildMetadata;
32
37
  /**
33
- * 递归复制所有文件
38
+ * 验证基础构建
39
+ * 根据引擎类型使用不同的验证规则
34
40
  */
35
- private copyAllFiles;
41
+ private validateBaseBuild;
42
+ /**
43
+ * 验证 PlayCanvas 构建
44
+ */
45
+ private validatePlayCanvasBuild;
46
+ /**
47
+ * 验证通用构建(外部引擎)
48
+ */
49
+ private validateGenericBuild;
36
50
  /**
37
51
  * 检测是否为 ESM 格式
38
52
  */
39
53
  private detectESMFormat;
40
54
  /**
41
- * 验证基础构建
55
+ * 将 Vite 输出复制到最终目录
42
56
  */
43
- private validateBaseBuild;
57
+ private copyOutputToFinalDir;
58
+ /**
59
+ * 递归复制所有文件
60
+ */
61
+ private copyAllFiles;
44
62
  /**
45
63
  * 获取输出路径
46
64
  */
@@ -2,20 +2,26 @@ import { build as viteBuild } from 'vite';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
  import { ViteConfigBuilder } from './vite/config-builder.js';
5
+ import { GenericViteConfigBuilder } from './vite/config-builder-generic.js';
5
6
  import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
6
7
  /**
7
8
  * Vite 构建器 - 使用 Vite 构建 Playable Ads
8
9
  *
9
10
  * 职责:
10
11
  * 1. 验证输入是有效的基础构建
11
- * 2. 创建 Vite 配置
12
+ * 2. 根据引擎类型创建对应的 Vite 配置
12
13
  * 3. 执行 Vite 构建
13
14
  * 4. 将输出复制到最终目录
14
15
  * 5. 验证输出大小
15
16
  * 6. 生成报告
17
+ *
18
+ * 引擎路由:
19
+ * - PlayCanvas:使用 ViteConfigBuilder(处理 config.json、__start__.js 等)
20
+ * - 其他引擎:使用 GenericViteConfigBuilder(通用 HTML 内联 + 平台 SDK 注入)
16
21
  */
17
22
  export class ViteBuilder {
18
23
  constructor(baseBuildDir, options) {
24
+ this.engine = 'playcanvas';
19
25
  this.baseBuildDir = baseBuildDir;
20
26
  this.options = options;
21
27
  const platformConfig = PLATFORM_CONFIGS[options.platform];
@@ -30,26 +36,151 @@ export class ViteBuilder {
30
36
  * 执行构建
31
37
  */
32
38
  async build() {
33
- // 1. 验证输入
34
- await this.validateBaseBuild();
35
- // 2. 创建 Vite 配置
36
- const configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
39
+ // 1. 读取构建元数据获取引擎类型
40
+ const metadata = await this.readBuildMetadata();
41
+ this.engine = this.options.engine ?? metadata?.engine ?? 'playcanvas';
42
+ console.log(`[ViteBuilder] 使用引擎: ${this.engine}`);
43
+ // 2. 验证输入
44
+ await this.validateBaseBuild(metadata);
45
+ // 3. 根据引擎类型创建对应的配置构建器
46
+ let configBuilder;
47
+ if (this.engine === 'playcanvas') {
48
+ console.log('[ViteBuilder] 使用 PlayCanvas 配置构建器');
49
+ configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
50
+ }
51
+ else {
52
+ console.log('[ViteBuilder] 使用通用配置构建器(外部引擎)');
53
+ configBuilder = new GenericViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options, metadata);
54
+ }
37
55
  const viteConfig = await configBuilder.create();
38
- // 3. 执行 Vite 构建
39
- await viteBuild(viteConfig);
40
- // 4. 将输出从临时目录复制到最终目录
56
+ // 4. 执行 Vite 构建
57
+ try {
58
+ await viteBuild(viteConfig);
59
+ }
60
+ catch (error) {
61
+ // 打印完整的 Vite/Rollup 错误信息,便于定位构建失败原因
62
+ console.error('[ViteBuilder] Vite 构建失败:');
63
+ if (error.id) {
64
+ console.error(` - 问题文件: ${error.id}`);
65
+ }
66
+ if (error.loc) {
67
+ console.error(` - 位置: 行 ${error.loc.line}, 列 ${error.loc.column}`);
68
+ }
69
+ if (error.frame) {
70
+ console.error(` - 代码片段:\n${error.frame}`);
71
+ }
72
+ if (error.plugin) {
73
+ console.error(` - 插件: ${error.plugin}`);
74
+ }
75
+ if (error.pluginCode) {
76
+ console.error(` - 插件错误码: ${error.pluginCode}`);
77
+ }
78
+ if (error.cause) {
79
+ console.error(` - 原因:`, error.cause);
80
+ }
81
+ console.error(` - 完整错误:`, error.stack || error.message || error);
82
+ throw error;
83
+ }
84
+ // 5. 将输出从临时目录复制到最终目录
41
85
  const viteOutputDir = configBuilder.getViteOutputDir();
42
86
  const finalOutputDir = configBuilder.getFinalOutputDir();
43
87
  if (viteOutputDir && finalOutputDir && viteOutputDir !== finalOutputDir) {
44
88
  await this.copyOutputToFinalDir(viteOutputDir, finalOutputDir);
45
89
  }
46
- // 5. 验证输出大小
90
+ // 6. 验证输出大小
47
91
  const outputPath = this.getOutputPath();
48
92
  await this.validateSize(outputPath);
49
- // 6. 生成报告
93
+ // 7. 生成报告
50
94
  this.generateReport(outputPath);
51
95
  return outputPath;
52
96
  }
97
+ /**
98
+ * 读取构建元数据
99
+ */
100
+ async readBuildMetadata() {
101
+ const metadataPath = path.join(this.baseBuildDir, '.build-metadata.json');
102
+ try {
103
+ const content = await fs.readFile(metadataPath, 'utf-8');
104
+ const metadata = JSON.parse(content);
105
+ console.log(`[ViteBuilder] 读取构建元数据: engine=${metadata.engine}, mode=${metadata.mode}`);
106
+ return metadata;
107
+ }
108
+ catch (error) {
109
+ console.warn('[ViteBuilder] 无法读取构建元数据,假设为 PlayCanvas');
110
+ return null;
111
+ }
112
+ }
113
+ /**
114
+ * 验证基础构建
115
+ * 根据引擎类型使用不同的验证规则
116
+ */
117
+ async validateBaseBuild(metadata) {
118
+ const engine = this.options.engine ?? metadata?.engine ?? 'playcanvas';
119
+ if (engine === 'playcanvas') {
120
+ await this.validatePlayCanvasBuild();
121
+ }
122
+ else {
123
+ await this.validateGenericBuild();
124
+ }
125
+ }
126
+ /**
127
+ * 验证 PlayCanvas 构建
128
+ */
129
+ async validatePlayCanvasBuild() {
130
+ const isESM = await this.detectESMFormat();
131
+ const requiredFiles = isESM
132
+ ? ['index.html', 'config.json']
133
+ : ['index.html', 'config.json', '__start__.js'];
134
+ const missingFiles = [];
135
+ for (const file of requiredFiles) {
136
+ try {
137
+ await fs.access(path.join(this.baseBuildDir, file));
138
+ }
139
+ catch {
140
+ missingFiles.push(file);
141
+ }
142
+ }
143
+ if (missingFiles.length > 0) {
144
+ const formatType = isESM ? 'ESM' : 'Classic';
145
+ throw new Error(`PlayCanvas 基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
146
+ `检测到 ${formatType} 格式,请确保输入目录包含完整的构建产物。`);
147
+ }
148
+ console.log('[ViteBuilder] PlayCanvas 构建验证通过');
149
+ }
150
+ /**
151
+ * 验证通用构建(外部引擎)
152
+ */
153
+ async validateGenericBuild() {
154
+ // 外部引擎只需要 index.html
155
+ try {
156
+ await fs.access(path.join(this.baseBuildDir, 'index.html'));
157
+ }
158
+ catch {
159
+ throw new Error(`外部引擎构建产物缺少 index.html\n` +
160
+ `构建目录: ${this.baseBuildDir}\n` +
161
+ `请确保 Base Build 成功生成了有效的 HTML 入口文件。`);
162
+ }
163
+ console.log('[ViteBuilder] 外部引擎构建验证通过');
164
+ }
165
+ /**
166
+ * 检测是否为 ESM 格式
167
+ */
168
+ async detectESMFormat() {
169
+ const esmIndicators = [
170
+ path.join(this.baseBuildDir, 'js/index.mjs'),
171
+ path.join(this.baseBuildDir, 'esm-scripts'),
172
+ ];
173
+ for (const indicator of esmIndicators) {
174
+ try {
175
+ await fs.access(indicator);
176
+ return true;
177
+ }
178
+ catch {
179
+ // 继续检查
180
+ }
181
+ }
182
+ return false;
183
+ }
53
184
  /**
54
185
  * 将 Vite 输出复制到最终目录
55
186
  */
@@ -116,48 +247,6 @@ export class ViteBuilder {
116
247
  }
117
248
  console.log(`[ViteBuilder] 已将输出复制到: ${destDir}`);
118
249
  }
119
- /**
120
- * 检测是否为 ESM 格式
121
- */
122
- async detectESMFormat() {
123
- const esmIndicators = [
124
- path.join(this.baseBuildDir, 'js/index.mjs'),
125
- path.join(this.baseBuildDir, 'esm-scripts'),
126
- ];
127
- for (const indicator of esmIndicators) {
128
- try {
129
- await fs.access(indicator);
130
- return true;
131
- }
132
- catch {
133
- // 继续检查
134
- }
135
- }
136
- return false;
137
- }
138
- /**
139
- * 验证基础构建
140
- */
141
- async validateBaseBuild() {
142
- const isESM = await this.detectESMFormat();
143
- const requiredFiles = isESM
144
- ? ['index.html', 'config.json']
145
- : ['index.html', 'config.json', '__start__.js'];
146
- const missingFiles = [];
147
- for (const file of requiredFiles) {
148
- try {
149
- await fs.access(path.join(this.baseBuildDir, file));
150
- }
151
- catch (error) {
152
- missingFiles.push(file);
153
- }
154
- }
155
- if (missingFiles.length > 0) {
156
- const formatType = isESM ? 'ESM' : 'Classic';
157
- throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
158
- `检测到 ${formatType} 格式,请确保输入目录包含完整的构建产物。`);
159
- }
160
- }
161
250
  /**
162
251
  * 获取输出路径
163
252
  */