@playcraft/build 0.0.4 → 0.0.9

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 (64) hide show
  1. package/dist/analyzers/scene-asset-collector.js +210 -1
  2. package/dist/base-builder.d.ts +15 -0
  3. package/dist/base-builder.js +192 -16
  4. package/dist/generators/config-generator.js +29 -3
  5. package/dist/loaders/playcanvas-loader.d.ts +7 -0
  6. package/dist/loaders/playcanvas-loader.js +53 -3
  7. package/dist/platforms/adikteev.d.ts +10 -0
  8. package/dist/platforms/adikteev.js +72 -0
  9. package/dist/platforms/base.d.ts +12 -0
  10. package/dist/platforms/base.js +208 -0
  11. package/dist/platforms/facebook.js +5 -2
  12. package/dist/platforms/index.d.ts +4 -0
  13. package/dist/platforms/index.js +16 -0
  14. package/dist/platforms/inmobi.d.ts +10 -0
  15. package/dist/platforms/inmobi.js +68 -0
  16. package/dist/platforms/ironsource.js +5 -2
  17. package/dist/platforms/moloco.js +5 -2
  18. package/dist/platforms/playcraft.d.ts +33 -0
  19. package/dist/platforms/playcraft.js +44 -0
  20. package/dist/platforms/remerge.d.ts +10 -0
  21. package/dist/platforms/remerge.js +56 -0
  22. package/dist/templates/__loading__.js +100 -0
  23. package/dist/templates/__modules__.js +47 -0
  24. package/dist/templates/__settings__.template.js +20 -0
  25. package/dist/templates/__start__.js +332 -0
  26. package/dist/templates/index.html +18 -0
  27. package/dist/templates/logo.png +0 -0
  28. package/dist/templates/manifest.json +1 -0
  29. package/dist/templates/patches/cannon.min.js +28 -0
  30. package/dist/templates/patches/lz4.js +10 -0
  31. package/dist/templates/patches/one-page-http-get.js +20 -0
  32. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  33. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  34. package/dist/templates/patches/p2.min.js +27 -0
  35. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  36. package/dist/templates/playcanvas-stable.min.js +16363 -0
  37. package/dist/templates/styles.css +43 -0
  38. package/dist/types.d.ts +14 -1
  39. package/dist/utils/build-mode-detector.d.ts +9 -0
  40. package/dist/utils/build-mode-detector.js +42 -0
  41. package/dist/vite/config-builder.d.ts +29 -1
  42. package/dist/vite/config-builder.js +169 -25
  43. package/dist/vite/platform-configs.d.ts +4 -0
  44. package/dist/vite/platform-configs.js +97 -13
  45. package/dist/vite/plugin-esm-html-generator.d.ts +22 -0
  46. package/dist/vite/plugin-esm-html-generator.js +1061 -0
  47. package/dist/vite/plugin-platform.js +56 -17
  48. package/dist/vite/plugin-playcanvas.d.ts +2 -0
  49. package/dist/vite/plugin-playcanvas.js +497 -40
  50. package/dist/vite/plugin-source-builder.d.ts +3 -0
  51. package/dist/vite/plugin-source-builder.js +886 -19
  52. package/dist/vite-builder.d.ts +19 -2
  53. package/dist/vite-builder.js +162 -12
  54. package/package.json +2 -1
  55. package/physics/cannon-es-bundle.js +13092 -0
  56. package/physics/cannon-rigidbody-adapter.js +375 -0
  57. package/physics/connon-integration.js +411 -0
  58. package/templates/__start__.js +8 -3
  59. package/templates/index.esm.html +20 -0
  60. package/templates/index.esm.mjs +502 -0
  61. package/templates/patches/one-page-inline-game-scripts.js +25 -1
  62. package/templates/patches/playcraft-cta-adapter.js +297 -0
  63. package/templates/patches/playcraft-no-xhr.js +25 -1
  64. package/templates/playcanvas-esm-wrapper.mjs +827 -0
@@ -17,16 +17,95 @@ export async function analyzeSceneDependencies(sceneData, assets) {
17
17
  indirectAssets: new Set(),
18
18
  scripts: new Set(),
19
19
  };
20
+ // 调试:检查场景数据结构
21
+ const hasEntities = !!sceneData.entities;
22
+ const entityCount = hasEntities ? Object.keys(sceneData.entities).length : 0;
23
+ console.log(` [SceneAnalyzer] 场景 "${deps.sceneName}" (ID: ${deps.sceneId})`);
24
+ console.log(` - 有 entities: ${hasEntities}, 实体数量: ${entityCount}`);
20
25
  // 1. 遍历所有实体
21
26
  if (sceneData.entities) {
22
27
  for (const [entityId, entity] of Object.entries(sceneData.entities)) {
23
28
  collectEntityAssets(entity, deps, assets);
24
29
  }
25
30
  }
26
- // 2. 递归收集间接依赖
31
+ else {
32
+ console.warn(` ⚠️ 警告: 场景没有 entities 字段,资源收集可能不完整!`);
33
+ }
34
+ // 2. 收集场景设置中引用的资源(如 skybox、环境贴图等)
35
+ if (sceneData.settings) {
36
+ collectSettingsAssets(sceneData.settings, deps, assets);
37
+ }
38
+ // 3. 递归收集间接依赖
27
39
  collectIndirectDependencies(deps, assets);
28
40
  return deps;
29
41
  }
42
+ /**
43
+ * 收集场景设置中引用的资源
44
+ * 包括 skybox、环境贴图、光照贴图等
45
+ */
46
+ function collectSettingsAssets(settings, deps, assets) {
47
+ if (!settings || typeof settings !== 'object')
48
+ return;
49
+ // 渲染设置中的资源引用
50
+ if (settings.render) {
51
+ const render = settings.render;
52
+ // Skybox
53
+ if (render.skybox) {
54
+ const skyboxId = String(render.skybox);
55
+ if (assets[skyboxId]) {
56
+ deps.directAssets.add(skyboxId);
57
+ }
58
+ }
59
+ // 环境贴图 (ambientBake 相关)
60
+ if (render.ambientBakeSphereMesh) {
61
+ const meshId = String(render.ambientBakeSphereMesh);
62
+ if (assets[meshId]) {
63
+ deps.directAssets.add(meshId);
64
+ }
65
+ }
66
+ }
67
+ // 物理设置中的资源引用(如果有)
68
+ if (settings.physics) {
69
+ findAssetIds(settings.physics, deps, assets, 0);
70
+ }
71
+ // 递归扫描整个 settings 对象以捕获任何其他资源引用
72
+ // 但跳过已知的非资源字段
73
+ const knownNonAssetFields = ['gravity', 'lightingCells', 'global_ambient', 'fog_color',
74
+ 'skyboxRotation', 'skyMeshPosition', 'skyMeshRotation', 'skyMeshScale', 'skyCenter'];
75
+ const scanSettings = (obj, depth = 0) => {
76
+ if (depth > 5)
77
+ return;
78
+ if (obj === null || obj === undefined)
79
+ return;
80
+ if (typeof obj === 'number') {
81
+ const id = String(obj);
82
+ // 检查是否是有效的资源 ID(数字且存在于 assets 中)
83
+ if (assets[id]) {
84
+ deps.directAssets.add(id);
85
+ }
86
+ }
87
+ else if (Array.isArray(obj)) {
88
+ // 跳过坐标/颜色数组(通常是 3 或 4 个小数或小整数)
89
+ // 资源 ID 通常是大于 10000 的正整数,不会被误跳过
90
+ if (obj.length <= 4 && obj.every(v => typeof v === 'number' && (!Number.isInteger(v) || // 浮点数(坐标/颜色)
91
+ (v >= 0 && v < 1000) // 小正整数(颜色分量等)
92
+ ))) {
93
+ return;
94
+ }
95
+ for (const item of obj) {
96
+ scanSettings(item, depth + 1);
97
+ }
98
+ }
99
+ else if (typeof obj === 'object') {
100
+ for (const [key, value] of Object.entries(obj)) {
101
+ if (knownNonAssetFields.includes(key))
102
+ continue;
103
+ scanSettings(value, depth + 1);
104
+ }
105
+ }
106
+ };
107
+ scanSettings(settings);
108
+ }
30
109
  /**
31
110
  * 收集实体的资源引用
32
111
  */
@@ -131,6 +210,21 @@ function collectIndirectDependencies(deps, assets) {
131
210
  }
132
211
  break;
133
212
  case 'cubemap':
213
+ // Cubemap 引用 6 个纹理面(textures 数组)
214
+ if (asset.data?.textures && Array.isArray(asset.data.textures)) {
215
+ for (const textureId of asset.data.textures) {
216
+ if (textureId !== null && textureId !== undefined) {
217
+ const id = String(textureId);
218
+ if (assets[id] && !visited.has(id) && !deps.directAssets.has(id)) {
219
+ deps.indirectAssets.add(id);
220
+ queue.push(id);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ // 还要扫描其他可能的数据
226
+ findAssetIdsInData(asset.data, deps, assets, queue, visited);
227
+ break;
134
228
  case 'texture':
135
229
  // 纹理资源通常是叶子节点,但可能引用其他纹理(如合成纹理)
136
230
  findAssetIdsInData(asset.data, deps, assets, queue, visited);
@@ -146,6 +240,30 @@ function collectIndirectDependencies(deps, assets) {
146
240
  }
147
241
  findAssetIdsInData(asset.data, deps, assets, queue, visited);
148
242
  break;
243
+ case 'template':
244
+ // 模板资产内部的 entities 可能引用其他资产(字体、纹理、脚本等)
245
+ if (asset.data?.entities) {
246
+ for (const entity of Object.values(asset.data.entities)) {
247
+ findAssetIdsInData(entity, deps, assets, queue, visited);
248
+ }
249
+ }
250
+ break;
251
+ case 'render':
252
+ // Render 资源引用 containerAsset (GLB/GLTF 模型)
253
+ if (asset.data?.containerAsset) {
254
+ const id = String(asset.data.containerAsset);
255
+ if (assets[id] && !visited.has(id) && !deps.directAssets.has(id)) {
256
+ deps.indirectAssets.add(id);
257
+ queue.push(id);
258
+ }
259
+ }
260
+ findAssetIdsInData(asset.data, deps, assets, queue, visited);
261
+ break;
262
+ case 'container':
263
+ // Container 可能引用内部的 render 和 material 资源
264
+ // 主要是 GLB/GLTF 文件,它本身是独立的
265
+ findAssetIdsInData(asset.data, deps, assets, queue, visited);
266
+ break;
149
267
  }
150
268
  }
151
269
  }
@@ -211,8 +329,99 @@ export async function collectScenesAssets(scenes, assets, globalScriptIds) {
211
329
  allAssetIds.add(id);
212
330
  }
213
331
  }
332
+ // 收集所有 template 资源及其依赖
333
+ // template 通常是运行时通过 assets.find() 动态实例化的,需要包含所有 preload 的模板
334
+ console.log(`\n📦 收集 Template 资源依赖...`);
335
+ const templateDeps = collectAllTemplatesDependencies(assets);
336
+ let templateAssetsCount = 0;
337
+ for (const id of templateDeps) {
338
+ if (!allAssetIds.has(id)) {
339
+ allAssetIds.add(id);
340
+ templateAssetsCount++;
341
+ }
342
+ }
343
+ console.log(` - 从 Template 添加了 ${templateAssetsCount} 个额外资源`);
214
344
  return allAssetIds;
215
345
  }
346
+ /**
347
+ * 收集所有 preload=true 的 template 资源及其依赖
348
+ * 因为 template 通常是运行时动态实例化的,不一定在场景中直接引用
349
+ */
350
+ function collectAllTemplatesDependencies(assets) {
351
+ const result = new Set();
352
+ const visited = new Set();
353
+ const queue = [];
354
+ // 首先收集所有 preload=true 的 template
355
+ for (const [assetId, asset] of Object.entries(assets)) {
356
+ if (asset.type === 'template' && asset.preload !== false) {
357
+ result.add(assetId);
358
+ queue.push(assetId);
359
+ }
360
+ }
361
+ // 然后递归收集这些 template 的所有依赖
362
+ while (queue.length > 0) {
363
+ const assetId = queue.shift();
364
+ if (visited.has(assetId))
365
+ continue;
366
+ visited.add(assetId);
367
+ const asset = assets[assetId];
368
+ if (!asset)
369
+ continue;
370
+ // 扫描资源数据中引用的其他资源 ID
371
+ const foundIds = new Set();
372
+ scanForAssetIds(asset, assets, foundIds);
373
+ for (const id of foundIds) {
374
+ if (!result.has(id)) {
375
+ result.add(id);
376
+ queue.push(id); // 继续递归收集依赖
377
+ }
378
+ }
379
+ }
380
+ return result;
381
+ }
382
+ /**
383
+ * 扫描对象中可能的资源 ID 引用
384
+ */
385
+ function scanForAssetIds(obj, assets, foundIds, depth = 0) {
386
+ if (depth > 15)
387
+ return;
388
+ if (obj === null || obj === undefined)
389
+ return;
390
+ if (typeof obj === 'number') {
391
+ const id = String(obj);
392
+ if (assets[id]) {
393
+ foundIds.add(id);
394
+ }
395
+ }
396
+ else if (typeof obj === 'string') {
397
+ // 直接检查字符串是否是有效的资源 ID(不再限制为纯数字)
398
+ if (assets[obj]) {
399
+ foundIds.add(obj);
400
+ }
401
+ }
402
+ else if (Array.isArray(obj)) {
403
+ // 跳过坐标/颜色数组(通常是 3 或 4 个小数或小整数)
404
+ // 资源 ID 通常是大于 10000 的正整数,不会被误跳过
405
+ if (obj.length <= 4 && obj.every(v => typeof v === 'number' && (!Number.isInteger(v) || // 浮点数(坐标/颜色)
406
+ (v >= 0 && v < 1000) // 小正整数(颜色分量等)
407
+ ))) {
408
+ return;
409
+ }
410
+ for (const item of obj) {
411
+ scanForAssetIds(item, assets, foundIds, depth + 1);
412
+ }
413
+ }
414
+ else if (typeof obj === 'object') {
415
+ for (const [key, value] of Object.entries(obj)) {
416
+ // 跳过明显不是资源引用的字段
417
+ if (['name', 'enabled', 'position', 'rotation', 'scale', 'tags',
418
+ 'children', 'parent', 'resource_id'].includes(key)) {
419
+ continue;
420
+ }
421
+ scanForAssetIds(value, assets, foundIds, depth + 1);
422
+ }
423
+ }
424
+ }
216
425
  /**
217
426
  * 打印场景依赖统计信息
218
427
  */
@@ -1,11 +1,14 @@
1
+ import type { BaseBuildMetadata } from './types.js';
1
2
  export interface BaseBuildOptions {
2
3
  outputDir: string;
3
4
  selectedScenes?: string[];
4
5
  analyze?: boolean;
5
6
  analyzeReportPath?: string;
7
+ clean?: boolean;
6
8
  }
7
9
  export interface BaseBuildOutput {
8
10
  outputDir: string;
11
+ metadata: BaseBuildMetadata;
9
12
  files: {
10
13
  html: string;
11
14
  engine: string | null;
@@ -34,6 +37,10 @@ export declare class BaseBuilder {
34
37
  * 执行基础构建
35
38
  */
36
39
  build(): Promise<BaseBuildOutput>;
40
+ /**
41
+ * 保存构建元数据到输出目录
42
+ */
43
+ private saveBuildMetadata;
37
44
  /**
38
45
  * 检测项目类型
39
46
  */
@@ -42,6 +49,14 @@ export declare class BaseBuilder {
42
49
  * 从官方构建产物构建
43
50
  */
44
51
  private buildFromOfficial;
52
+ /**
53
+ * 从输出目录检测构建模式
54
+ */
55
+ private detectBuildModeFromOutput;
56
+ /**
57
+ * 检测是否为 ESM 格式项目
58
+ */
59
+ private detectESMFormat;
45
60
  /**
46
61
  * 验证官方构建产物
47
62
  */
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { build as viteBuild } from 'vite';
5
5
  import { visualizer } from 'rollup-plugin-visualizer';
6
6
  import { viteSourceBuilderPlugin } from './vite/plugin-source-builder.js';
7
+ import { loadPlayCanvasProject } from './loaders/playcanvas-loader.js';
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  /**
@@ -19,6 +20,7 @@ export class BaseBuilder {
19
20
  constructor(projectDir, options) {
20
21
  this.projectDir = projectDir;
21
22
  this.options = options;
23
+ console.log(`[BaseBuilder] 初始化: projectDir=${projectDir}, outputDir=${options.outputDir}`);
22
24
  }
23
25
  /**
24
26
  * 执行基础构建
@@ -26,16 +28,28 @@ export class BaseBuilder {
26
28
  async build() {
27
29
  // 1. 检测项目类型
28
30
  const projectType = await this.detectProjectType();
31
+ let result;
29
32
  if (projectType === 'official-build') {
30
33
  // 官方构建产物 - 直接复制并验证
31
- return await this.buildFromOfficial();
34
+ result = await this.buildFromOfficial();
32
35
  }
33
36
  else {
34
37
  // 源代码格式(PlayCanvas 或 PlayCraft)- 使用 Vite 生成构建产物
35
38
  const formatName = projectType === 'playcraft' ? 'PlayCraft' : 'PlayCanvas';
36
39
  console.log(`[BaseBuilder] 检测到 ${formatName} 源代码格式,使用 Vite 构建...`);
37
- return await this.buildFromSource();
40
+ result = await this.buildFromSource();
38
41
  }
42
+ // 保存构建元数据
43
+ await this.saveBuildMetadata(result.metadata);
44
+ return result;
45
+ }
46
+ /**
47
+ * 保存构建元数据到输出目录
48
+ */
49
+ async saveBuildMetadata(metadata) {
50
+ const metadataPath = path.join(this.options.outputDir, '.build-metadata.json');
51
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
52
+ console.log('[BaseBuilder] 构建元数据已保存');
39
53
  }
40
54
  /**
41
55
  * 检测项目类型
@@ -79,20 +93,72 @@ export class BaseBuilder {
79
93
  await fs.mkdir(this.options.outputDir, { recursive: true });
80
94
  // 复制所有文件到输出目录
81
95
  const files = await this.copyBuildFiles();
96
+ // 检测构建模式(官方构建产物可能是 Classic 或 ESM)
97
+ const metadata = await this.detectBuildModeFromOutput();
82
98
  return {
83
99
  outputDir: this.options.outputDir,
100
+ metadata,
84
101
  files,
85
102
  };
86
103
  }
104
+ /**
105
+ * 从输出目录检测构建模式
106
+ */
107
+ async detectBuildModeFromOutput() {
108
+ const indexPath = path.join(this.options.outputDir, 'index.html');
109
+ try {
110
+ const html = await fs.readFile(indexPath, 'utf-8');
111
+ const hasImportMap = html.includes('<script type="importmap">');
112
+ if (hasImportMap) {
113
+ const importMapMatch = html.match(/<script type="importmap">\s*(\{[\s\S]*?\})\s*<\/script>/);
114
+ const importMapContent = importMapMatch ? JSON.parse(importMapMatch[1]) : { imports: {} };
115
+ return {
116
+ mode: 'esm',
117
+ importMap: {
118
+ id: 'detected',
119
+ imports: importMapContent.imports || {},
120
+ },
121
+ entryPoint: 'js/index.mjs',
122
+ };
123
+ }
124
+ }
125
+ catch (error) {
126
+ // 忽略错误,返回 Classic 模式
127
+ }
128
+ return {
129
+ mode: 'classic',
130
+ };
131
+ }
132
+ /**
133
+ * 检测是否为 ESM 格式项目
134
+ */
135
+ async detectESMFormat() {
136
+ // ESM 格式的特征:存在 js/index.mjs 或 esm-scripts 目录
137
+ const esmIndicators = [
138
+ path.join(this.projectDir, 'js/index.mjs'),
139
+ path.join(this.projectDir, 'esm-scripts'),
140
+ ];
141
+ for (const indicator of esmIndicators) {
142
+ try {
143
+ await fs.access(indicator);
144
+ return true;
145
+ }
146
+ catch {
147
+ // 继续检查下一个
148
+ }
149
+ }
150
+ return false;
151
+ }
87
152
  /**
88
153
  * 验证官方构建产物
89
154
  */
90
155
  async validateOfficialBuild() {
91
- const requiredFiles = [
92
- 'index.html',
93
- 'config.json',
94
- '__start__.js',
95
- ];
156
+ // 检测是否为 ESM 格式
157
+ const isESM = await this.detectESMFormat();
158
+ // ESM 格式不需要 __start__.js,Classic 格式需要
159
+ const requiredFiles = isESM
160
+ ? ['index.html', 'config.json']
161
+ : ['index.html', 'config.json', '__start__.js'];
96
162
  const missingFiles = [];
97
163
  for (const file of requiredFiles) {
98
164
  try {
@@ -103,8 +169,9 @@ export class BaseBuilder {
103
169
  }
104
170
  }
105
171
  if (missingFiles.length > 0) {
172
+ const formatType = isESM ? 'ESM' : 'Classic';
106
173
  throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
107
- `请确保项目目录包含完整的构建产物。`);
174
+ `检测到 ${formatType} 格式,请确保项目目录包含完整的构建产物。`);
108
175
  }
109
176
  }
110
177
  /**
@@ -134,11 +201,20 @@ export class BaseBuilder {
134
201
  // 读取 config.json 以获取场景信息
135
202
  const configContent = await fs.readFile(configPath, 'utf-8');
136
203
  const configJson = JSON.parse(configContent);
137
- // 复制 __start__.js
138
- const startPath = path.join(this.projectDir, '__start__.js');
139
- const outputStartPath = path.join(this.options.outputDir, '__start__.js');
140
- await fs.copyFile(startPath, outputStartPath);
141
- files.start = outputStartPath;
204
+ // 检测是否为 ESM 格式
205
+ const isESM = await this.detectESMFormat();
206
+ // 复制 __start__.js(Classic 格式必需,ESM 格式不需要)
207
+ if (!isESM) {
208
+ const startPath = path.join(this.projectDir, '__start__.js');
209
+ const outputStartPath = path.join(this.options.outputDir, '__start__.js');
210
+ try {
211
+ await fs.copyFile(startPath, outputStartPath);
212
+ files.start = outputStartPath;
213
+ }
214
+ catch (error) {
215
+ console.warn('警告: 无法复制 __start__.js');
216
+ }
217
+ }
142
218
  // 复制 __settings__.js(如果存在)
143
219
  const settingsPath = path.join(this.projectDir, '__settings__.js');
144
220
  try {
@@ -259,6 +335,30 @@ export class BaseBuilder {
259
335
  catch (error) {
260
336
  // manifest.json 可能不存在
261
337
  }
338
+ // ESM 格式:复制 js/ 目录(如果存在)
339
+ const jsDir = path.join(this.projectDir, 'js');
340
+ try {
341
+ const jsDirStat = await fs.stat(jsDir);
342
+ if (jsDirStat.isDirectory()) {
343
+ const outputJsDir = path.join(this.options.outputDir, 'js');
344
+ await this.copyDirectory(jsDir, outputJsDir);
345
+ }
346
+ }
347
+ catch (error) {
348
+ // js/ 目录可能不存在
349
+ }
350
+ // ESM 格式:复制 esm-scripts/ 目录(如果存在)
351
+ const esmScriptsDir = path.join(this.projectDir, 'esm-scripts');
352
+ try {
353
+ const esmScriptsDirStat = await fs.stat(esmScriptsDir);
354
+ if (esmScriptsDirStat.isDirectory()) {
355
+ const outputEsmScriptsDir = path.join(this.options.outputDir, 'esm-scripts');
356
+ await this.copyDirectory(esmScriptsDir, outputEsmScriptsDir);
357
+ }
358
+ }
359
+ catch (error) {
360
+ // esm-scripts/ 目录可能不存在
361
+ }
262
362
  return files;
263
363
  }
264
364
  /**
@@ -283,13 +383,77 @@ export class BaseBuilder {
283
383
  */
284
384
  async buildFromSource() {
285
385
  console.log('[BaseBuilder] 使用 Vite 从源代码构建...');
386
+ console.log('[BaseBuilder] ====== 开始检测构建模式 ======');
387
+ // 0. 检测构建模式(Import Map)
388
+ let buildMode = 'classic';
389
+ let importMap = undefined;
390
+ try {
391
+ console.log('[BaseBuilder] 正在加载项目配置...');
392
+ const projectConfig = await loadPlayCanvasProject(this.projectDir);
393
+ console.log('[BaseBuilder] 项目配置加载成功');
394
+ console.log('[BaseBuilder] Import Map:', projectConfig.importMap ? 'YES' : 'NO');
395
+ buildMode = projectConfig.importMap ? 'esm' : 'classic';
396
+ importMap = projectConfig.importMap;
397
+ console.log(`[BaseBuilder] ====== 构建模式: ${buildMode} ======`);
398
+ if (importMap) {
399
+ console.log(`[BaseBuilder] Import Map ID: ${importMap.id}`);
400
+ }
401
+ }
402
+ catch (error) {
403
+ console.error('[BaseBuilder] 加载配置出错:', error.message);
404
+ console.log('[BaseBuilder] 无法加载 PlayCanvas 项目配置,使用传统模式');
405
+ }
286
406
  // 1. 创建 Vite 配置
287
- const viteConfig = {
407
+ // 默认使用覆盖模式(不清空输出目录),只有显式设置 clean: true 时才清空
408
+ const shouldEmptyOutDir = this.options.clean === true;
409
+ const viteConfig = buildMode === 'esm' ? {
410
+ // ESM 模式:不需要打包用户脚本,只需触发插件复制文件
288
411
  root: this.projectDir,
289
412
  base: './',
290
413
  build: {
291
414
  outDir: this.options.outputDir,
292
- emptyOutDir: true,
415
+ emptyOutDir: shouldEmptyOutDir,
416
+ write: false, // 不写入文件,仅用于触发插件
417
+ rollupOptions: {
418
+ input: {
419
+ // 使用虚拟入口,仅用于触发插件
420
+ 'esm-entry': 'virtual:esm-entry',
421
+ },
422
+ },
423
+ },
424
+ plugins: [
425
+ // 虚拟入口插件
426
+ {
427
+ name: 'virtual-esm-entry',
428
+ resolveId(id) {
429
+ if (id === 'virtual:esm-entry') {
430
+ return '\0virtual:esm-entry';
431
+ }
432
+ return null;
433
+ },
434
+ load(id) {
435
+ if (id === '\0virtual:esm-entry') {
436
+ return '// ESM mode - no bundling needed';
437
+ }
438
+ return null;
439
+ },
440
+ },
441
+ // 源代码构建插件
442
+ viteSourceBuilderPlugin({
443
+ projectDir: this.projectDir,
444
+ outputDir: this.options.outputDir,
445
+ selectedScenes: this.options.selectedScenes,
446
+ buildMode: buildMode,
447
+ importMap: importMap,
448
+ }),
449
+ ],
450
+ } : {
451
+ // 传统模式:打包用户脚本
452
+ root: this.projectDir,
453
+ base: './',
454
+ build: {
455
+ outDir: this.options.outputDir,
456
+ emptyOutDir: shouldEmptyOutDir,
293
457
  // 多文件输出(不内联资源)
294
458
  assetsInlineLimit: 0,
295
459
  // Base Build 不压缩(保持可读性和调试性)
@@ -323,6 +487,8 @@ export class BaseBuilder {
323
487
  projectDir: this.projectDir,
324
488
  outputDir: this.options.outputDir,
325
489
  selectedScenes: this.options.selectedScenes,
490
+ buildMode: buildMode,
491
+ importMap: importMap,
326
492
  }),
327
493
  // 打包分析报告(如果启用)
328
494
  ...(this.options.analyze ? [
@@ -343,9 +509,19 @@ export class BaseBuilder {
343
509
  await viteBuild(viteConfig);
344
510
  // 3. 扫描生成的文件
345
511
  const files = await this.scanOutputFiles();
346
- // 4. 返回构建结果
512
+ // 4. 构建元数据
513
+ const metadata = {
514
+ mode: buildMode,
515
+ importMap: importMap ? {
516
+ id: importMap.id,
517
+ imports: importMap.content.imports,
518
+ } : undefined,
519
+ entryPoint: buildMode === 'esm' ? 'js/index.mjs' : undefined,
520
+ };
521
+ // 5. 返回构建结果
347
522
  return {
348
523
  outputDir: this.options.outputDir,
524
+ metadata,
349
525
  files,
350
526
  };
351
527
  }
@@ -61,7 +61,11 @@ export async function generateConfig(projectConfig, options) {
61
61
  if (pcProject.scenes) {
62
62
  allScenes = Array.isArray(pcProject.scenes)
63
63
  ? pcProject.scenes
64
- : Object.entries(pcProject.scenes).map(([id, scene]) => ({ id, ...scene }));
64
+ : Object.entries(pcProject.scenes).map(([keyId, scene]) => {
65
+ // keyId 是场景文件名使用的 ID(如 "2388047")
66
+ // scene 中可能也有 id 字段(如 "2415082"),但场景文件名应该用 keyId
67
+ return { ...scene, sceneFileId: keyId };
68
+ });
65
69
  }
66
70
  // 场景过滤:如果指定了选中的场景,则只包含这些场景
67
71
  let selectedScenes = allScenes;
@@ -79,8 +83,9 @@ export async function generateConfig(projectConfig, options) {
79
83
  });
80
84
  }
81
85
  config.scenes = selectedScenes.map((scene) => ({
82
- name: scene.name || scene.id,
83
- url: scene.url || scene.file || `${scene.id}.json`,
86
+ name: scene.name || scene.sceneFileId || scene.id,
87
+ // 场景文件名优先使用 sceneFileId(来自 scenes.json key)
88
+ url: scene.url || scene.file || `${scene.sceneFileId || scene.id}.json`,
84
89
  }));
85
90
  // 处理资产:基于场景过滤
86
91
  if (pcProject.assets) {
@@ -90,6 +95,15 @@ export async function generateConfig(projectConfig, options) {
90
95
  // 如果启用了场景过滤,分析场景依赖
91
96
  if (options?.selectedScenes && options.selectedScenes.length > 0 && selectedScenes.length > 0) {
92
97
  console.log(`\n🔍 分析场景资源依赖...`);
98
+ // 调试:检查场景数据结构
99
+ for (const scene of selectedScenes) {
100
+ const sceneKeys = Object.keys(scene || {});
101
+ const hasEntities = 'entities' in scene;
102
+ console.log(` [DEBUG] 场景 "${scene.name}" 字段: [${sceneKeys.slice(0, 10).join(', ')}${sceneKeys.length > 10 ? '...' : ''}]`);
103
+ if (!hasEntities) {
104
+ console.warn(` ⚠️ 场景 "${scene.name}" 没有 entities 字段,可能导致资源收集不完整!`);
105
+ }
106
+ }
93
107
  allowedAssetIds = await collectScenesAssets(selectedScenes, pcProject.assets, scriptIds);
94
108
  // 打印每个场景的依赖统计
95
109
  for (const scene of selectedScenes) {
@@ -108,6 +122,12 @@ export async function generateConfig(projectConfig, options) {
108
122
  if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
109
123
  continue;
110
124
  }
125
+ // template 资产始终包含(因为它们通常是运行时通过 assets.find() 动态引用的)
126
+ const assetAny = asset;
127
+ if (assetAny.type === 'template') {
128
+ filtered[assetId] = asset;
129
+ continue;
130
+ }
111
131
  // 场景过滤:如果启用,只包含场景依赖的资源
112
132
  if (allowedAssetIds && !allowedAssetIds.has(assetId)) {
113
133
  continue;
@@ -179,6 +199,12 @@ export async function generateConfig(projectConfig, options) {
179
199
  if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
180
200
  continue;
181
201
  }
202
+ // template 资产始终包含(因为它们通常是运行时通过 assets.find() 动态引用的)
203
+ const assetAny = asset;
204
+ if (assetAny.type === 'template') {
205
+ filtered[assetId] = asset;
206
+ continue;
207
+ }
182
208
  // 场景过滤:如果启用,只包含场景依赖的资源
183
209
  if (allowedAssetIds && !allowedAssetIds.has(assetId)) {
184
210
  continue;
@@ -1,8 +1,15 @@
1
+ export interface ImportMapAsset {
2
+ id: string;
3
+ content: {
4
+ imports: Record<string, string>;
5
+ };
6
+ }
1
7
  export interface PlayCanvasProject {
2
8
  project: any;
3
9
  scenes: any;
4
10
  assets: any;
5
11
  format: 'playcanvas';
12
+ importMap?: ImportMapAsset;
6
13
  }
7
14
  /**
8
15
  * 加载 PlayCanvas 源代码项目