@playcraft/build 0.0.17 → 0.0.21

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 (46) hide show
  1. package/dist/analyzers/scene-asset-collector.js +259 -135
  2. package/dist/audio-optimizer.d.ts +70 -0
  3. package/dist/audio-optimizer.js +226 -0
  4. package/dist/base-builder.d.ts +25 -13
  5. package/dist/base-builder.js +69 -29
  6. package/dist/engines/engine-detector.d.ts +13 -4
  7. package/dist/engines/engine-detector.js +74 -10
  8. package/dist/engines/generic-adapter.d.ts +12 -6
  9. package/dist/engines/generic-adapter.js +46 -15
  10. package/dist/engines/index.d.ts +1 -0
  11. package/dist/engines/index.js +1 -0
  12. package/dist/engines/playable-scripts-adapter.d.ts +148 -0
  13. package/dist/engines/playable-scripts-adapter.js +1084 -0
  14. package/dist/engines/playcanvas-adapter.js +3 -0
  15. package/dist/generators/config-generator.js +10 -17
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.js +3 -1
  18. package/dist/platforms/google.d.ts +9 -0
  19. package/dist/platforms/google.js +68 -7
  20. package/dist/templates/__loading__.js +100 -0
  21. package/dist/templates/__modules__.js +47 -0
  22. package/dist/templates/__settings__.template.js +20 -0
  23. package/dist/templates/__start__.js +332 -0
  24. package/dist/templates/index.html +18 -0
  25. package/dist/templates/logo.png +0 -0
  26. package/dist/templates/manifest.json +1 -0
  27. package/dist/templates/patches/cannon.min.js +28 -0
  28. package/dist/templates/patches/lz4.js +10 -0
  29. package/dist/templates/patches/one-page-http-get.js +20 -0
  30. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  31. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  32. package/dist/templates/patches/p2.min.js +27 -0
  33. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  34. package/dist/templates/playcanvas-stable.min.js +16363 -0
  35. package/dist/templates/styles.css +43 -0
  36. package/dist/types.d.ts +60 -13
  37. package/dist/utils/build-mode-detector.js +2 -0
  38. package/dist/vite/plugin-playcanvas.js +14 -19
  39. package/dist/vite/plugin-source-builder.js +383 -97
  40. package/package.json +7 -4
  41. package/dist/utils/obfuscate.d.ts +0 -42
  42. package/dist/utils/obfuscate.js +0 -216
  43. package/dist/vite/plugin-obfuscate.d.ts +0 -22
  44. package/dist/vite/plugin-obfuscate.js +0 -52
  45. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  46. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -444,18 +444,16 @@ function validateAndCleanSceneScripts(sceneData, registeredScripts, _sceneName /
444
444
  // 处理 ESM 格式:scripts 是对象 { scriptName: {...}, ... }
445
445
  if (scriptComp.scripts && typeof scriptComp.scripts === 'object' && !Array.isArray(scriptComp.scripts)) {
446
446
  const validScripts = {};
447
- const validOrder = [];
448
447
  for (const [scriptName, scriptData] of Object.entries(scriptComp.scripts)) {
449
448
  if (registeredScripts.has(scriptName)) {
450
449
  validScripts[scriptName] = scriptData;
451
- if (scriptComp.order?.includes(scriptName)) {
452
- validOrder.push(scriptName);
453
- }
454
450
  }
455
451
  else {
456
452
  removedScripts.push(`${entity.name || entityId}/${scriptName}`);
457
453
  }
458
454
  }
455
+ // BUG: Preserve original order: filter based on the original order array, not scripts object key order
456
+ const validOrder = (scriptComp.order || []).filter((name) => name in validScripts);
459
457
  scriptComp.scripts = validScripts;
460
458
  scriptComp.order = validOrder;
461
459
  // 如果所有脚本都被移除,删除整个 script 组件
@@ -571,6 +569,52 @@ outputDir, selectedScenes) {
571
569
  }
572
570
  }
573
571
  }
572
+ /**
573
+ * 构建文件夹 ID -> 完整目录路径的映射
574
+ * PlayCraft Git 项目的 assets.json 中,资产的 path 字段是文件夹 ID 数组(如 [30097022, 30097024]),
575
+ * 需要通过递归遍历构建每个 folder ID 对应的完整路径(如 "LiteCreator/System")
576
+ */
577
+ function buildFolderFullPathMap(assets) {
578
+ const folderMap = new Map();
579
+ const buildPath = (folderId, visited = new Set()) => {
580
+ if (visited.has(folderId))
581
+ return '';
582
+ if (folderMap.has(folderId))
583
+ return folderMap.get(folderId);
584
+ visited.add(folderId);
585
+ const folder = assets[folderId];
586
+ if (!folder || folder.type !== 'folder')
587
+ return '';
588
+ const pathIds = folder.path || [];
589
+ if (pathIds.length === 0) {
590
+ folderMap.set(folderId, folder.name);
591
+ return folder.name;
592
+ }
593
+ // 最后一个 ID 是直接父文件夹
594
+ const parentId = String(pathIds[pathIds.length - 1]);
595
+ const parentPath = buildPath(parentId, visited);
596
+ const fullPath = parentPath ? `${parentPath}/${folder.name}` : folder.name;
597
+ folderMap.set(folderId, fullPath);
598
+ return fullPath;
599
+ };
600
+ for (const [id, asset] of Object.entries(assets)) {
601
+ if (asset.type === 'folder') {
602
+ buildPath(id);
603
+ }
604
+ }
605
+ return folderMap;
606
+ }
607
+ /**
608
+ * 从资产的 path 数组(文件夹 ID 列表)重建 treePath
609
+ * path 数组中最后一个 ID 是直接父文件夹
610
+ */
611
+ function resolveTreePathFromPathArray(pathIds, folderFullPathMap) {
612
+ if (!pathIds || !Array.isArray(pathIds) || pathIds.length === 0) {
613
+ return '';
614
+ }
615
+ const parentFolderId = String(pathIds[pathIds.length - 1]);
616
+ return folderFullPathMap.get(parentFolderId) || '';
617
+ }
574
618
  /**
575
619
  * 规范化资源 URL(补齐 file.url)
576
620
  * 源码导出中的资源只有 file.filename,没有 file.url
@@ -580,6 +624,8 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
580
624
  const assets = projectConfig.format === 'playcanvas'
581
625
  ? projectConfig.assets
582
626
  : projectConfig.assets;
627
+ // 构建文件夹 ID -> 完整路径的映射(用于从 path 数组重建 treePath)
628
+ const folderFullPathMap = buildFolderFullPathMap(assets);
583
629
  let normalizedCount = 0;
584
630
  let notFoundCount = 0;
585
631
  for (const [assetId, asset] of Object.entries(assets)) {
@@ -620,13 +666,21 @@ async function normalizeAssetUrls(projectConfig, projectDir) {
620
666
  path.join('files', 'assets', String(assetId), filename),
621
667
  path.join('files', filename),
622
668
  ];
623
- // PlayCraft 结构:使用 treePath 或 path 字段推断文件位置
669
+ // PlayCraft 结构:使用 treePath 推断文件位置
624
670
  if (assetData.treePath) {
671
+ candidates.push(path.join('assets', assetData.treePath, filename));
625
672
  candidates.push(path.join(assetData.treePath, filename));
626
673
  }
627
- if (assetData.path && typeof assetData.path === 'string') {
628
- candidates.push(assetData.path);
674
+ // PlayCraft Git 仓库结构:从 path 数组(文件夹 ID 列表)重建 treePath
675
+ if (assetData.path && Array.isArray(assetData.path) && assetData.path.length > 0) {
676
+ const computedTreePath = resolveTreePathFromPathArray(assetData.path, folderFullPathMap);
677
+ if (computedTreePath) {
678
+ candidates.push(path.join('assets', computedTreePath, filename));
679
+ candidates.push(path.join(computedTreePath, filename));
680
+ }
629
681
  }
682
+ // 最后尝试直接在 assets/ 根目录下查找(顶层文件)
683
+ candidates.push(path.join('assets', filename));
630
684
  let found = false;
631
685
  for (const rel of candidates) {
632
686
  const abs = path.join(projectDir, rel);
@@ -687,6 +741,156 @@ async function patchScriptAssets(config, outputDir) {
687
741
  }
688
742
  }
689
743
  }
744
+ /**
745
+ * Analyze script file import dependencies and add missing dependency scripts to config.assets.
746
+ * This handles cases like: BoardRandomFiller.mjs imports ./BoardShapeGenerator.mjs,
747
+ * which is not directly referenced in the scene or templates.
748
+ */
749
+ async function collectScriptImportDependencies(projectConfig, projectDir, importMap, configAssets) {
750
+ const assets = projectConfig.format === 'playcanvas'
751
+ ? projectConfig.assets
752
+ : projectConfig.assets;
753
+ // Build folder ID -> name mapping
754
+ const folderMap = new Map();
755
+ for (const [assetId, asset] of Object.entries(assets)) {
756
+ if (asset.type === 'folder') {
757
+ folderMap.set(assetId, asset.name);
758
+ }
759
+ }
760
+ // Build reverse mapping: targetPath -> assetId (for ALL script assets, not just configAssets)
761
+ const targetPathToAssetId = new Map();
762
+ for (const [assetId, asset] of Object.entries(assets)) {
763
+ const assetData = asset;
764
+ if (assetData.type !== 'script')
765
+ continue;
766
+ const filename = assetData.file?.filename || assetData.name + '.mjs';
767
+ const pathIds = assetData.path || [];
768
+ const folderPath = buildScriptFolderPath(pathIds, folderMap);
769
+ const targetPath = resolveTargetPathFromImportMap(folderPath, filename, importMap);
770
+ if (targetPath) {
771
+ targetPathToAssetId.set(targetPath.replace(/\\/g, '/'), assetId);
772
+ }
773
+ }
774
+ // Regex to extract import specifiers from script content
775
+ const importRegex = /(from\s+['"])([^'"]+)(['"])|(\bimport\s*\(\s*['"])([^'"]+)(['"]\s*\))/g;
776
+ const importsMap = importMap.content.imports || {};
777
+ // Iteratively discover new script dependencies
778
+ const processedScripts = new Set();
779
+ let newDepsFound = true;
780
+ while (newDepsFound) {
781
+ newDepsFound = false;
782
+ const currentScriptIds = Object.keys(configAssets).filter(id => configAssets[id].type === 'script' && !processedScripts.has(id));
783
+ for (const scriptId of currentScriptIds) {
784
+ processedScripts.add(scriptId);
785
+ const assetData = assets[scriptId];
786
+ if (!assetData)
787
+ continue;
788
+ // Resolve source file path
789
+ const sourcePath = await resolveScriptSourcePath(assetData, scriptId, projectDir);
790
+ if (!sourcePath)
791
+ continue;
792
+ let content;
793
+ try {
794
+ content = await fs.readFile(sourcePath, 'utf-8');
795
+ }
796
+ catch {
797
+ continue;
798
+ }
799
+ // Get current script's target path for relative path resolution
800
+ const filename = assetData.file?.filename || assetData.name + '.mjs';
801
+ const pathIds = assetData.path || [];
802
+ const folderPath = buildScriptFolderPath(pathIds, folderMap);
803
+ const scriptTargetPath = resolveTargetPathFromImportMap(folderPath, filename, importMap);
804
+ if (!scriptTargetPath)
805
+ continue;
806
+ const scriptDir = path.dirname(scriptTargetPath).replace(/\\/g, '/');
807
+ // Parse all import statements
808
+ let match;
809
+ importRegex.lastIndex = 0;
810
+ while ((match = importRegex.exec(content)) !== null) {
811
+ const modulePath = match[2] || match[5];
812
+ if (!modulePath)
813
+ continue;
814
+ let resolvedTargetPath = null;
815
+ if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
816
+ // Relative import: resolve against script's directory
817
+ const joined = path.posix.join(scriptDir, modulePath);
818
+ resolvedTargetPath = path.posix.normalize(joined);
819
+ }
820
+ else if (!modulePath.startsWith('/') && !modulePath.startsWith('http')) {
821
+ // Import Map alias: resolve via import map
822
+ resolvedTargetPath = resolveModulePathViaImportMap(modulePath, importsMap);
823
+ }
824
+ if (!resolvedTargetPath)
825
+ continue;
826
+ resolvedTargetPath = resolvedTargetPath.replace(/\\/g, '/');
827
+ // Look up the asset by target path
828
+ const depAssetId = targetPathToAssetId.get(resolvedTargetPath);
829
+ if (depAssetId === undefined) {
830
+ console.warn(`Warning: resolvedTargetPath '${resolvedTargetPath}' not found in targetPathToAssetId`);
831
+ }
832
+ else if (!configAssets[depAssetId]) {
833
+ // Found a missing dependency — add it to configAssets
834
+ configAssets[depAssetId] = assets[depAssetId];
835
+ newDepsFound = true;
836
+ console.log(`📜 [ScriptDeps] ${assetData.name} imports ${assets[depAssetId].name} (${depAssetId}) — added to configAssets`);
837
+ }
838
+ }
839
+ }
840
+ }
841
+ }
842
+ /**
843
+ * Resolve the source file path for a script asset.
844
+ * @param assets - 可选,所有资产的 map,用于从 path 数组重建 treePath(PlayCraft Git 仓库场景)
845
+ */
846
+ async function resolveScriptSourcePath(assetData, scriptId, projectDir, assets) {
847
+ if (assetData.file?.url) {
848
+ const fullPath = path.join(projectDir, assetData.file.url);
849
+ try {
850
+ await fs.access(fullPath);
851
+ return fullPath;
852
+ }
853
+ catch {
854
+ // file.url 指向的文件不存在,继续搜索
855
+ }
856
+ }
857
+ if (assetData.file?.filename) {
858
+ const filename = assetData.file.filename;
859
+ const revision = assetData.revision ?? 1;
860
+ const candidates = [
861
+ // PlayCanvas 标准结构
862
+ path.join(projectDir, 'files', 'assets', String(scriptId), String(revision), filename),
863
+ path.join(projectDir, 'files', 'assets', String(scriptId), '1', filename),
864
+ path.join(projectDir, 'files', 'assets', String(scriptId), filename),
865
+ ];
866
+ // PlayCraft 结构:使用 treePath
867
+ if (assetData.treePath) {
868
+ candidates.push(path.join(projectDir, 'assets', assetData.treePath, filename));
869
+ candidates.push(path.join(projectDir, assetData.treePath, filename));
870
+ }
871
+ // PlayCraft Git 仓库:从 path 数组重建 treePath
872
+ if (assets && assetData.path && Array.isArray(assetData.path) && assetData.path.length > 0) {
873
+ const folderFullPathMap = buildFolderFullPathMap(assets);
874
+ const computedTreePath = resolveTreePathFromPathArray(assetData.path, folderFullPathMap);
875
+ if (computedTreePath) {
876
+ candidates.push(path.join(projectDir, 'assets', computedTreePath, filename));
877
+ candidates.push(path.join(projectDir, computedTreePath, filename));
878
+ }
879
+ }
880
+ // 最后尝试直接在 assets/ 根目录下查找
881
+ candidates.push(path.join(projectDir, 'assets', filename));
882
+ for (const candidate of candidates) {
883
+ try {
884
+ await fs.access(candidate);
885
+ return candidate;
886
+ }
887
+ catch {
888
+ // try next
889
+ }
890
+ }
891
+ }
892
+ return null;
893
+ }
690
894
  /**
691
895
  * 生成 ESM 模板文件
692
896
  */
@@ -728,9 +932,13 @@ async function generateESMTemplate(projectConfig, options, config) {
728
932
  catch (error) {
729
933
  console.warn('[SourceBuilder] 警告: 无法复制 ESM 包装器:', error);
730
934
  }
731
- // 3. 收集用户脚本的导入路径
732
- const scriptImportPaths = await collectESMScriptImports(projectConfig, options);
733
- // 4. Import Map 中提取 gameRule 路径
935
+ // 3. 分析脚本文件的 import 依赖,将缺失的依赖脚本添加到 config.assets
936
+ if (options.importMap) {
937
+ await collectScriptImportDependencies(projectConfig, options.projectDir, options.importMap, config.assets);
938
+ }
939
+ // 4. 收集用户脚本的导入路径(传入 config.assets 以包含 template 依赖 + import 依赖的脚本)
940
+ const scriptImportPaths = await collectESMScriptImports(projectConfig, options, config.assets);
941
+ // 5. 从 Import Map 中提取 gameRule 路径
734
942
  let gameRulePath = '';
735
943
  if (options.importMap?.content?.imports) {
736
944
  const imports = options.importMap.content.imports;
@@ -743,11 +951,99 @@ async function generateESMTemplate(projectConfig, options, config) {
743
951
  console.log(`[SourceBuilder] GameRule 路径: ${gameRulePath}`);
744
952
  }
745
953
  }
746
- // 5. 生成 js/index.mjs(包含脚本导入),使用过滤后的 config.scenes
954
+ // 5.1 从场景 SystemManager 的 GameConfig.json 中收集 Rule 脚本
955
+ if (options.importMap && projectConfig.format === 'playcanvas') {
956
+ const allAssets = projectConfig.assets || {};
957
+ const pcScenes = projectConfig.scenes;
958
+ // Iterate over selected scenes (from config.scenes) and find the scene data
959
+ for (const sceneRef of (config.scenes || [])) {
960
+ // Extract scene file ID from url (e.g. "4577025.json" -> "4577025")
961
+ const sceneFileId = (sceneRef.url || '').replace(/\.json$/, '');
962
+ const sceneData = pcScenes?.[sceneFileId];
963
+ if (!sceneData?.entities)
964
+ continue;
965
+ // Find SystemManager entity
966
+ for (const [, entity] of Object.entries(sceneData.entities)) {
967
+ if (entity?.name !== 'SystemManager')
968
+ continue;
969
+ const smScript = entity.components?.script?.scripts?.systemManager;
970
+ if (!smScript?.attributes?.gameConfigAsset)
971
+ continue;
972
+ const jsonAssetId = String(smScript.attributes.gameConfigAsset);
973
+ const jsonAsset = allAssets[jsonAssetId];
974
+ if (!jsonAsset || jsonAsset.type !== 'json')
975
+ continue;
976
+ // Read the GameConfig JSON file
977
+ const jsonFilePath = await resolveScriptSourcePath(jsonAsset, jsonAssetId, options.projectDir, allAssets);
978
+ if (!jsonFilePath)
979
+ continue;
980
+ let gameConfig;
981
+ try {
982
+ const content = await fs.readFile(jsonFilePath, 'utf-8');
983
+ gameConfig = JSON.parse(content);
984
+ }
985
+ catch {
986
+ console.warn(`[SourceBuilder] 无法读取 GameConfig.json (${jsonAssetId})`);
987
+ continue;
988
+ }
989
+ const ruleName = gameConfig?.Gameplay?.Rule;
990
+ if (!ruleName)
991
+ continue;
992
+ // Resolve Rule script: look for a script asset named "{ruleName}.mjs"
993
+ const ruleFileName = `${ruleName}.mjs`;
994
+ let ruleAssetId = null;
995
+ for (const [assetId, asset] of Object.entries(allAssets)) {
996
+ if (asset.type === 'script' && (asset.file?.filename === ruleFileName || asset.name === ruleFileName)) {
997
+ ruleAssetId = assetId;
998
+ break;
999
+ }
1000
+ }
1001
+ if (ruleAssetId && !config.assets[ruleAssetId]) {
1002
+ config.assets[ruleAssetId] = allAssets[ruleAssetId];
1003
+ console.log(`📜 [GameConfig] Rule "${ruleName}" → 资源 ${ruleAssetId} — added to configAssets`);
1004
+ // Also collect its import dependencies
1005
+ if (options.importMap) {
1006
+ await collectScriptImportDependencies(projectConfig, options.projectDir, options.importMap, config.assets);
1007
+ }
1008
+ // Rebuild script import paths to include the new Rule script
1009
+ const newPaths = await collectESMScriptImports(projectConfig, options, config.assets);
1010
+ for (const p of newPaths) {
1011
+ if (!scriptImportPaths.includes(p)) {
1012
+ scriptImportPaths.push(p);
1013
+ }
1014
+ }
1015
+ }
1016
+ else if (ruleAssetId) {
1017
+ console.log(`📜 [GameConfig] Rule "${ruleName}" → 资源 ${ruleAssetId} — already in configAssets`);
1018
+ }
1019
+ else {
1020
+ console.warn(`⚠️ [GameConfig] Rule "${ruleName}" 对应的脚本 ${ruleFileName} 未找到`);
1021
+ }
1022
+ // Use the rule path from 5.1 if Import Map didn't provide a gameRule mapping (step 5)
1023
+ if (ruleAssetId && !gameRulePath) {
1024
+ const ruleAsset = allAssets[ruleAssetId];
1025
+ // Build folder map for path resolution
1026
+ const folderMap = new Map();
1027
+ for (const [aid, a] of Object.entries(allAssets)) {
1028
+ if (a.type === 'folder')
1029
+ folderMap.set(aid, a.name);
1030
+ }
1031
+ const ruleFilename = ruleAsset.file?.filename || ruleAsset.name + '.mjs';
1032
+ const ruleFolderPath = buildScriptFolderPath(ruleAsset.path || [], folderMap);
1033
+ const targetPath = resolveTargetPathFromImportMap(ruleFolderPath, ruleFilename, options.importMap);
1034
+ if (targetPath) {
1035
+ gameRulePath = '../' + targetPath;
1036
+ console.log(`📜 [GameConfig] 使用 5.1 收集的 Rule 路径: ${gameRulePath}`);
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ // 6. 生成 js/index.mjs(包含脚本导入),使用过滤后的 config.scenes
747
1043
  await generateESMEntry(projectConfig, outputDir, scriptImportPaths, config.scenes, gameRulePath);
748
- // 5. 在 ESM 模式下,复制用户脚本为独立文件,并更新 config.assets 中的 URL
1044
+ // 7. 在 ESM 模式下,复制用户脚本为独立文件,并更新 config.assets 中的 URL
749
1045
  if (options.importMap) {
750
- const scriptUrlMap = await copyUserScriptsForESM(projectConfig, options.projectDir, outputDir, options.importMap);
1046
+ const scriptUrlMap = await copyUserScriptsForESM(projectConfig, options.projectDir, outputDir, options.importMap, config.assets);
751
1047
  console.log('[SourceBuilder] 用户脚本已复制为独立 ESM 模块');
752
1048
  // 更新 config.assets 中脚本的 URL
753
1049
  if (scriptUrlMap.size > 0 && config.assets) {
@@ -760,10 +1056,10 @@ async function generateESMTemplate(projectConfig, options, config) {
760
1056
  await fs.writeFile(path.join(outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
761
1057
  console.log(`[SourceBuilder] 更新了 ${scriptUrlMap.size} 个脚本的 URL`);
762
1058
  }
763
- // 6. 复制 Import Map 中引用的第三方库文件
1059
+ // 8. 复制 Import Map 中引用的第三方库文件
764
1060
  await copyImportMapLibraries(projectConfig, options.projectDir, outputDir, options.importMap);
765
1061
  }
766
- // 7. 复制其他必要的模板文件
1062
+ // 9. 复制其他必要的模板文件
767
1063
  const templateFiles = [
768
1064
  '__start__.js', // 可能不需要了,但先保留
769
1065
  '__modules__.js',
@@ -804,20 +1100,43 @@ function fixImportMapPaths(importMap) {
804
1100
  }
805
1101
  /**
806
1102
  * 收集 ESM 脚本的导入路径
1103
+ * @param configAssets - generateConfig 过滤后的 config.assets,包含 template 依赖的脚本
807
1104
  */
808
- async function collectESMScriptImports(projectConfig, options) {
809
- // 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
810
- let scriptIds;
1105
+ async function collectESMScriptImports(projectConfig, options, configAssets) {
1106
+ // 获取资产(支持 PlayCanvas 和 PlayCraft 两种格式)
811
1107
  let assets;
812
1108
  if (projectConfig.format === 'playcanvas') {
813
- const pcProject = projectConfig;
814
- scriptIds = pcProject.project.settings?.scripts || [];
815
- assets = pcProject.assets;
1109
+ assets = projectConfig.assets;
816
1110
  }
817
1111
  else {
818
- const pcProject = projectConfig;
819
- scriptIds = pcProject.manifest.settings?.scripts || [];
820
- assets = pcProject.assets;
1112
+ assets = projectConfig.assets;
1113
+ }
1114
+ // 收集所有需要导入的脚本 ID:
1115
+ // 1. config.assets 中的 script 类型资产(已包含场景依赖 + template 依赖的脚本)
1116
+ // 2. 回退到 settings.scripts(当没有 configAssets 时)
1117
+ const scriptIdsToImport = new Set();
1118
+ if (configAssets) {
1119
+ console.log(`📜 [DEBUG-ESMImports] 使用 configAssets 收集脚本, configAssets 总数: ${Object.keys(configAssets).length}`);
1120
+ for (const [assetId, asset] of Object.entries(configAssets)) {
1121
+ if (asset.type === 'script') {
1122
+ scriptIdsToImport.add(assetId);
1123
+ }
1124
+ }
1125
+ console.log(`[ESMImports] 从 configAssets 收集到 ${scriptIdsToImport.size} 个脚本`);
1126
+ }
1127
+ else {
1128
+ console.log(`[ESMImports] configAssets 未传入, 回退到 settings.scripts`);
1129
+ // Fallback: use settings.scripts
1130
+ let scriptIds;
1131
+ if (projectConfig.format === 'playcanvas') {
1132
+ scriptIds = projectConfig.project.settings?.scripts || [];
1133
+ }
1134
+ else {
1135
+ scriptIds = projectConfig.manifest.settings?.scripts || [];
1136
+ }
1137
+ for (const id of scriptIds) {
1138
+ scriptIdsToImport.add(String(id));
1139
+ }
821
1140
  }
822
1141
  const importPaths = [];
823
1142
  // 构建文件夹 ID 到名称的映射
@@ -829,15 +1148,11 @@ async function collectESMScriptImports(projectConfig, options) {
829
1148
  }
830
1149
  }
831
1150
  // 遍历每个脚本,构建导入路径
832
- for (const scriptId of scriptIds) {
1151
+ for (const scriptId of scriptIdsToImport) {
833
1152
  const asset = assets[String(scriptId)];
834
1153
  if (!asset || asset.type !== 'script')
835
1154
  continue;
836
1155
  const assetData = asset;
837
- // 检查 preload 属性
838
- const preload = assetData.preload !== false;
839
- if (!preload)
840
- continue;
841
1156
  // 获取正确的文件名(带扩展名)
842
1157
  const filename = assetData.file?.filename || assetData.name + '.mjs';
843
1158
  // 获取脚本的文件夹路径
@@ -919,7 +1234,7 @@ async function generateESMEntry(projectConfig, outputDir, scriptImportPaths = []
919
1234
  const preloadModules = [];
920
1235
  // TODO: 从 assets 中提取 WASM 模块
921
1236
  // 生成脚本导入路径数组(延迟加载)
922
- console.log(`[SourceBuilder] 生成 ${scriptImportPaths.length} 个脚本导入路径`);
1237
+ console.log(`[ESMEntry] 生成 ${scriptImportPaths.length} 个脚本导入路径`);
923
1238
  // 替换模板占位符
924
1239
  const entry = esmTemplate
925
1240
  .replace(/\{\{SCRIPT_IMPORT_PATHS\}\}/g, JSON.stringify(scriptImportPaths))
@@ -939,25 +1254,43 @@ async function generateESMEntry(projectConfig, outputDir, scriptImportPaths = []
939
1254
  }
940
1255
  /**
941
1256
  * 在 ESM 模式下复制用户脚本为独立文件
1257
+ * @param configAssets - generateConfig 过滤后的 config.assets,包含 template 依赖的脚本
942
1258
  * @returns 脚本ID到新URL的映射
943
1259
  */
944
- async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, importMap) {
1260
+ async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, importMap, configAssets) {
945
1261
  const scriptUrlMap = new Map();
946
- console.log(`[copyUserScriptsForESM] 项目格式: ${projectConfig.format}`);
947
- // 获取脚本 ID 列表和资产(支持 PlayCanvas 和 PlayCraft 两种格式)
948
- let scriptIds;
1262
+ // 获取资产(支持 PlayCanvas 和 PlayCraft 两种格式)
949
1263
  let assets;
950
1264
  if (projectConfig.format === 'playcanvas') {
951
- const pcProject = projectConfig;
952
- scriptIds = pcProject.project.settings?.scripts || [];
953
- assets = pcProject.assets;
1265
+ assets = projectConfig.assets;
954
1266
  }
955
1267
  else {
956
- const pcProject = projectConfig;
957
- scriptIds = pcProject.manifest.settings?.scripts || [];
958
- assets = pcProject.assets;
1268
+ assets = projectConfig.assets;
1269
+ }
1270
+ // 收集所有需要复制的脚本 ID:
1271
+ // 1. config.assets 中的 script 类型资产(已包含场景依赖 + template 依赖的脚本)
1272
+ // 2. 回退到 settings.scripts(当没有 configAssets 时)
1273
+ const scriptIdsToProcess = new Set();
1274
+ if (configAssets) {
1275
+ for (const [assetId, asset] of Object.entries(configAssets)) {
1276
+ if (asset.type === 'script') {
1277
+ scriptIdsToProcess.add(assetId);
1278
+ }
1279
+ }
959
1280
  }
960
- console.log(`[copyUserScriptsForESM] 脚本数量: ${scriptIds.length}`);
1281
+ else {
1282
+ let scriptIds;
1283
+ if (projectConfig.format === 'playcanvas') {
1284
+ scriptIds = projectConfig.project.settings?.scripts || [];
1285
+ }
1286
+ else {
1287
+ scriptIds = projectConfig.manifest.settings?.scripts || [];
1288
+ }
1289
+ for (const id of scriptIds) {
1290
+ scriptIdsToProcess.add(String(id));
1291
+ }
1292
+ }
1293
+ console.log(`[copyUserScriptsForESM] 复制 ${scriptIdsToProcess.size} 个脚本`);
961
1294
  // 构建文件夹 ID 到名称的映射
962
1295
  const folderMap = new Map();
963
1296
  for (const [assetId, asset] of Object.entries(assets)) {
@@ -967,42 +1300,13 @@ async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, impor
967
1300
  }
968
1301
  }
969
1302
  // 遍历每个脚本
970
- for (const scriptId of scriptIds) {
1303
+ for (const scriptId of scriptIdsToProcess) {
971
1304
  const asset = assets[String(scriptId)];
972
1305
  if (!asset || asset.type !== 'script')
973
1306
  continue;
974
1307
  const assetData = asset;
975
- // 检查 preload 属性
976
- const preload = assetData.preload !== false;
977
- if (!preload) {
978
- continue; // 跳过 non-preload 脚本
979
- }
980
- // 获取脚本源文件路径
981
- let sourcePath = null;
982
- const scriptIdStr = String(scriptId);
983
- if (assetData.file?.url) {
984
- sourcePath = path.join(projectDir, assetData.file.url);
985
- }
986
- else if (assetData.file?.filename) {
987
- // 如果没有 file.url,根据 filename 和 revision 构建路径
988
- const filename = assetData.file.filename;
989
- const revision = assetData.revision ?? 1;
990
- const candidates = [
991
- path.join(projectDir, 'files', 'assets', scriptIdStr, String(revision), filename),
992
- path.join(projectDir, 'files', 'assets', scriptIdStr, '1', filename),
993
- path.join(projectDir, 'files', 'assets', scriptIdStr, filename),
994
- ];
995
- for (const candidate of candidates) {
996
- try {
997
- await fs.access(candidate);
998
- sourcePath = candidate;
999
- break;
1000
- }
1001
- catch {
1002
- // 继续尝试下一个
1003
- }
1004
- }
1005
- }
1308
+ // 获取脚本源文件路径(使用统一的解析函数,支持 PlayCraft Git 仓库结构)
1309
+ const sourcePath = await resolveScriptSourcePath(assetData, String(scriptId), projectDir, assets);
1006
1310
  if (!sourcePath) {
1007
1311
  console.warn(`[ESM] 无法找到脚本 ${assetData.name} 的源文件,跳过`);
1008
1312
  continue;
@@ -1312,7 +1616,7 @@ async function copyImportMapLibraries(projectConfig, projectDir, outputDir, impo
1312
1616
  // 文件不存在,需要复制
1313
1617
  }
1314
1618
  // 查找源文件
1315
- // 1. 首先检查是否是 asset 引用(通过 name 查找)
1619
+ // 1. 首先检查是否是 asset 引用(通过 name 查找),使用统一的解析函数
1316
1620
  let sourceFound = false;
1317
1621
  const filename = path.basename(outputRelPath);
1318
1622
  for (const [assetId, asset] of Object.entries(assets)) {
@@ -1321,29 +1625,8 @@ async function copyImportMapLibraries(projectConfig, projectDir, outputDir, impo
1321
1625
  continue;
1322
1626
  if (assetData.name !== key && assetData.file?.filename !== filename)
1323
1627
  continue;
1324
- // 找到匹配的资源
1325
- let sourcePath = null;
1326
- if (assetData.file?.url) {
1327
- sourcePath = path.join(projectDir, assetData.file.url);
1328
- }
1329
- else if (assetData.file?.filename) {
1330
- const revision = assetData.revision ?? 1;
1331
- const candidates = [
1332
- path.join(projectDir, 'files', 'assets', assetId, String(revision), assetData.file.filename),
1333
- path.join(projectDir, 'files', 'assets', assetId, '1', assetData.file.filename),
1334
- path.join(projectDir, 'files', 'assets', assetId, assetData.file.filename),
1335
- ];
1336
- for (const candidate of candidates) {
1337
- try {
1338
- await fs.access(candidate);
1339
- sourcePath = candidate;
1340
- break;
1341
- }
1342
- catch {
1343
- // 继续尝试
1344
- }
1345
- }
1346
- }
1628
+ // 使用统一的解析函数查找源文件(支持 PlayCraft Git 仓库结构)
1629
+ const sourcePath = await resolveScriptSourcePath(assetData, assetId, projectDir, assets);
1347
1630
  if (sourcePath) {
1348
1631
  try {
1349
1632
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
@@ -1365,6 +1648,9 @@ async function copyImportMapLibraries(projectConfig, projectDir, outputDir, impo
1365
1648
  path.join(projectDir, originalPath),
1366
1649
  path.join(projectDir, filename),
1367
1650
  path.join(projectDir, 'Lib', filename),
1651
+ // PlayCraft Git 仓库结构
1652
+ path.join(projectDir, 'assets', filename),
1653
+ path.join(projectDir, 'assets', originalPath),
1368
1654
  ];
1369
1655
  for (const candidate of candidates) {
1370
1656
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.17",
3
+ "version": "0.0.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,12 +8,15 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
11
12
  "types": "./dist/index.d.ts"
12
13
  },
13
14
  "./types": {
14
15
  "import": "./dist/types.js",
16
+ "require": "./dist/types.js",
15
17
  "types": "./dist/types.d.ts"
16
- }
18
+ },
19
+ "./dist/*": "./dist/*"
17
20
  },
18
21
  "files": [
19
22
  "dist",
@@ -42,8 +45,8 @@
42
45
  "sharp": "^0.34.5",
43
46
  "javascript-obfuscator": "^5.3.0",
44
47
  "terser": "^5.46.0",
45
- "vite": "^6.4.1",
46
- "vite-plugin-singlefile": "^2.3.0"
48
+ "vite": "^8.0.3",
49
+ "vite-plugin-singlefile": "^2.3.2"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@types/archiver": "^6.0.4",