@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
|
@@ -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
|
-
|
|
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
|
*/
|
package/dist/base-builder.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/base-builder.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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:
|
|
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(([
|
|
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
|
-
|
|
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 源代码项目
|