@playcraft/build 0.0.3 → 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 +17 -0
- package/dist/base-builder.js +211 -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 -39
- package/dist/vite/platform-configs.d.ts +4 -0
- package/dist/vite/platform-configs.js +98 -14
- 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 +579 -49
- package/dist/vite/plugin-source-builder.d.ts +3 -0
- package/dist/vite/plugin-source-builder.js +920 -23
- 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 +33 -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,9 +1,14 @@
|
|
|
1
|
+
import type { BaseBuildMetadata } from './types.js';
|
|
1
2
|
export interface BaseBuildOptions {
|
|
2
3
|
outputDir: string;
|
|
3
4
|
selectedScenes?: string[];
|
|
5
|
+
analyze?: boolean;
|
|
6
|
+
analyzeReportPath?: string;
|
|
7
|
+
clean?: boolean;
|
|
4
8
|
}
|
|
5
9
|
export interface BaseBuildOutput {
|
|
6
10
|
outputDir: string;
|
|
11
|
+
metadata: BaseBuildMetadata;
|
|
7
12
|
files: {
|
|
8
13
|
html: string;
|
|
9
14
|
engine: string | null;
|
|
@@ -32,6 +37,10 @@ export declare class BaseBuilder {
|
|
|
32
37
|
* 执行基础构建
|
|
33
38
|
*/
|
|
34
39
|
build(): Promise<BaseBuildOutput>;
|
|
40
|
+
/**
|
|
41
|
+
* 保存构建元数据到输出目录
|
|
42
|
+
*/
|
|
43
|
+
private saveBuildMetadata;
|
|
35
44
|
/**
|
|
36
45
|
* 检测项目类型
|
|
37
46
|
*/
|
|
@@ -40,6 +49,14 @@ export declare class BaseBuilder {
|
|
|
40
49
|
* 从官方构建产物构建
|
|
41
50
|
*/
|
|
42
51
|
private buildFromOfficial;
|
|
52
|
+
/**
|
|
53
|
+
* 从输出目录检测构建模式
|
|
54
|
+
*/
|
|
55
|
+
private detectBuildModeFromOutput;
|
|
56
|
+
/**
|
|
57
|
+
* 检测是否为 ESM 格式项目
|
|
58
|
+
*/
|
|
59
|
+
private detectESMFormat;
|
|
43
60
|
/**
|
|
44
61
|
* 验证官方构建产物
|
|
45
62
|
*/
|
package/dist/base-builder.js
CHANGED
|
@@ -2,7 +2,9 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { build as viteBuild } from 'vite';
|
|
5
|
+
import { visualizer } from 'rollup-plugin-visualizer';
|
|
5
6
|
import { viteSourceBuilderPlugin } from './vite/plugin-source-builder.js';
|
|
7
|
+
import { loadPlayCanvasProject } from './loaders/playcanvas-loader.js';
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = path.dirname(__filename);
|
|
8
10
|
/**
|
|
@@ -18,6 +20,7 @@ export class BaseBuilder {
|
|
|
18
20
|
constructor(projectDir, options) {
|
|
19
21
|
this.projectDir = projectDir;
|
|
20
22
|
this.options = options;
|
|
23
|
+
console.log(`[BaseBuilder] 初始化: projectDir=${projectDir}, outputDir=${options.outputDir}`);
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* 执行基础构建
|
|
@@ -25,16 +28,28 @@ export class BaseBuilder {
|
|
|
25
28
|
async build() {
|
|
26
29
|
// 1. 检测项目类型
|
|
27
30
|
const projectType = await this.detectProjectType();
|
|
31
|
+
let result;
|
|
28
32
|
if (projectType === 'official-build') {
|
|
29
33
|
// 官方构建产物 - 直接复制并验证
|
|
30
|
-
|
|
34
|
+
result = await this.buildFromOfficial();
|
|
31
35
|
}
|
|
32
36
|
else {
|
|
33
37
|
// 源代码格式(PlayCanvas 或 PlayCraft)- 使用 Vite 生成构建产物
|
|
34
38
|
const formatName = projectType === 'playcraft' ? 'PlayCraft' : 'PlayCanvas';
|
|
35
39
|
console.log(`[BaseBuilder] 检测到 ${formatName} 源代码格式,使用 Vite 构建...`);
|
|
36
|
-
|
|
40
|
+
result = await this.buildFromSource();
|
|
37
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] 构建元数据已保存');
|
|
38
53
|
}
|
|
39
54
|
/**
|
|
40
55
|
* 检测项目类型
|
|
@@ -78,20 +93,72 @@ export class BaseBuilder {
|
|
|
78
93
|
await fs.mkdir(this.options.outputDir, { recursive: true });
|
|
79
94
|
// 复制所有文件到输出目录
|
|
80
95
|
const files = await this.copyBuildFiles();
|
|
96
|
+
// 检测构建模式(官方构建产物可能是 Classic 或 ESM)
|
|
97
|
+
const metadata = await this.detectBuildModeFromOutput();
|
|
81
98
|
return {
|
|
82
99
|
outputDir: this.options.outputDir,
|
|
100
|
+
metadata,
|
|
83
101
|
files,
|
|
84
102
|
};
|
|
85
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
|
+
}
|
|
86
152
|
/**
|
|
87
153
|
* 验证官方构建产物
|
|
88
154
|
*/
|
|
89
155
|
async validateOfficialBuild() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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'];
|
|
95
162
|
const missingFiles = [];
|
|
96
163
|
for (const file of requiredFiles) {
|
|
97
164
|
try {
|
|
@@ -102,8 +169,9 @@ export class BaseBuilder {
|
|
|
102
169
|
}
|
|
103
170
|
}
|
|
104
171
|
if (missingFiles.length > 0) {
|
|
172
|
+
const formatType = isESM ? 'ESM' : 'Classic';
|
|
105
173
|
throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
106
|
-
|
|
174
|
+
`检测到 ${formatType} 格式,请确保项目目录包含完整的构建产物。`);
|
|
107
175
|
}
|
|
108
176
|
}
|
|
109
177
|
/**
|
|
@@ -133,11 +201,20 @@ export class BaseBuilder {
|
|
|
133
201
|
// 读取 config.json 以获取场景信息
|
|
134
202
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
135
203
|
const configJson = JSON.parse(configContent);
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
218
|
// 复制 __settings__.js(如果存在)
|
|
142
219
|
const settingsPath = path.join(this.projectDir, '__settings__.js');
|
|
143
220
|
try {
|
|
@@ -258,6 +335,30 @@ export class BaseBuilder {
|
|
|
258
335
|
catch (error) {
|
|
259
336
|
// manifest.json 可能不存在
|
|
260
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
|
+
}
|
|
261
362
|
return files;
|
|
262
363
|
}
|
|
263
364
|
/**
|
|
@@ -282,13 +383,77 @@ export class BaseBuilder {
|
|
|
282
383
|
*/
|
|
283
384
|
async buildFromSource() {
|
|
284
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
|
+
}
|
|
285
406
|
// 1. 创建 Vite 配置
|
|
286
|
-
|
|
407
|
+
// 默认使用覆盖模式(不清空输出目录),只有显式设置 clean: true 时才清空
|
|
408
|
+
const shouldEmptyOutDir = this.options.clean === true;
|
|
409
|
+
const viteConfig = buildMode === 'esm' ? {
|
|
410
|
+
// ESM 模式:不需要打包用户脚本,只需触发插件复制文件
|
|
287
411
|
root: this.projectDir,
|
|
288
412
|
base: './',
|
|
289
413
|
build: {
|
|
290
414
|
outDir: this.options.outputDir,
|
|
291
|
-
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,
|
|
292
457
|
// 多文件输出(不内联资源)
|
|
293
458
|
assetsInlineLimit: 0,
|
|
294
459
|
// Base Build 不压缩(保持可读性和调试性)
|
|
@@ -304,6 +469,11 @@ export class BaseBuilder {
|
|
|
304
469
|
entryFileNames: '__[name].js',
|
|
305
470
|
chunkFileNames: '__[name]-[hash].js',
|
|
306
471
|
assetFileNames: 'files/assets/[name].[ext]',
|
|
472
|
+
format: 'iife', // 使用 IIFE 格式,避免 ES 模块的 import 语句
|
|
473
|
+
globals: {
|
|
474
|
+
'playcanvas': 'pc',
|
|
475
|
+
'pc': 'pc',
|
|
476
|
+
},
|
|
307
477
|
},
|
|
308
478
|
// 外部化 PlayCanvas Engine(不打包)
|
|
309
479
|
external: ['pc', 'playcanvas'],
|
|
@@ -317,16 +487,41 @@ export class BaseBuilder {
|
|
|
317
487
|
projectDir: this.projectDir,
|
|
318
488
|
outputDir: this.options.outputDir,
|
|
319
489
|
selectedScenes: this.options.selectedScenes,
|
|
490
|
+
buildMode: buildMode,
|
|
491
|
+
importMap: importMap,
|
|
320
492
|
}),
|
|
493
|
+
// 打包分析报告(如果启用)
|
|
494
|
+
...(this.options.analyze ? [
|
|
495
|
+
visualizer({
|
|
496
|
+
filename: this.options.analyzeReportPath
|
|
497
|
+
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
498
|
+
: path.join(this.options.outputDir, 'base-bundle-report.html'),
|
|
499
|
+
template: 'treemap',
|
|
500
|
+
gzipSize: true,
|
|
501
|
+
brotliSize: true,
|
|
502
|
+
open: false,
|
|
503
|
+
sourcemap: false,
|
|
504
|
+
}),
|
|
505
|
+
] : []),
|
|
321
506
|
],
|
|
322
507
|
};
|
|
323
508
|
// 2. 执行 Vite 构建
|
|
324
509
|
await viteBuild(viteConfig);
|
|
325
510
|
// 3. 扫描生成的文件
|
|
326
511
|
const files = await this.scanOutputFiles();
|
|
327
|
-
// 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. 返回构建结果
|
|
328
522
|
return {
|
|
329
523
|
outputDir: this.options.outputDir,
|
|
524
|
+
metadata,
|
|
330
525
|
files,
|
|
331
526
|
};
|
|
332
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;
|