@playcraft/build 0.0.2 → 0.0.4
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.d.ts +31 -0
- package/dist/analyzers/scene-asset-collector.js +225 -0
- package/dist/base-builder.d.ts +3 -0
- package/dist/base-builder.js +20 -0
- package/dist/generators/config-generator.d.ts +4 -1
- package/dist/generators/config-generator.js +92 -22
- package/dist/types.d.ts +2 -1
- package/dist/vite/config-builder.js +0 -14
- package/dist/vite/platform-configs.js +77 -1
- package/dist/vite/plugin-playcanvas.js +254 -41
- package/dist/vite/plugin-source-builder.d.ts +1 -0
- package/dist/vite/plugin-source-builder.js +122 -28
- package/package.json +2 -1
- package/templates/patches/one-page-inline-game-scripts.js +8 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene Asset Dependency Collector
|
|
3
|
+
*
|
|
4
|
+
* 分析 PlayCanvas 场景文件,收集所有依赖的资源 ID
|
|
5
|
+
*/
|
|
6
|
+
export interface SceneDependencies {
|
|
7
|
+
sceneId: string;
|
|
8
|
+
sceneName: string;
|
|
9
|
+
directAssets: Set<string>;
|
|
10
|
+
indirectAssets: Set<string>;
|
|
11
|
+
scripts: Set<string>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 分析场景的资源依赖
|
|
15
|
+
* @param sceneData 场景数据对象
|
|
16
|
+
* @param assets 所有资源的映射表
|
|
17
|
+
* @returns 场景的资源依赖信息
|
|
18
|
+
*/
|
|
19
|
+
export declare function analyzeSceneDependencies(sceneData: any, assets: Record<string, any>): Promise<SceneDependencies>;
|
|
20
|
+
/**
|
|
21
|
+
* 分析多个场景的资源依赖并合并
|
|
22
|
+
* @param scenes 场景列表
|
|
23
|
+
* @param assets 所有资源的映射表
|
|
24
|
+
* @param globalScriptIds 全局脚本 ID 列表(来自 settings.scripts)
|
|
25
|
+
* @returns 合并后的资源 ID 集合
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectScenesAssets(scenes: any[], assets: Record<string, any>, globalScriptIds?: Set<string>): Promise<Set<string>>;
|
|
28
|
+
/**
|
|
29
|
+
* 打印场景依赖统计信息
|
|
30
|
+
*/
|
|
31
|
+
export declare function printSceneDependencies(deps: SceneDependencies): void;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene Asset Dependency Collector
|
|
3
|
+
*
|
|
4
|
+
* 分析 PlayCanvas 场景文件,收集所有依赖的资源 ID
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 分析场景的资源依赖
|
|
8
|
+
* @param sceneData 场景数据对象
|
|
9
|
+
* @param assets 所有资源的映射表
|
|
10
|
+
* @returns 场景的资源依赖信息
|
|
11
|
+
*/
|
|
12
|
+
export async function analyzeSceneDependencies(sceneData, assets) {
|
|
13
|
+
const deps = {
|
|
14
|
+
sceneId: sceneData.id || sceneData.scene || 'unknown',
|
|
15
|
+
sceneName: sceneData.name || 'Unknown Scene',
|
|
16
|
+
directAssets: new Set(),
|
|
17
|
+
indirectAssets: new Set(),
|
|
18
|
+
scripts: new Set(),
|
|
19
|
+
};
|
|
20
|
+
// 1. 遍历所有实体
|
|
21
|
+
if (sceneData.entities) {
|
|
22
|
+
for (const [entityId, entity] of Object.entries(sceneData.entities)) {
|
|
23
|
+
collectEntityAssets(entity, deps, assets);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 2. 递归收集间接依赖
|
|
27
|
+
collectIndirectDependencies(deps, assets);
|
|
28
|
+
return deps;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 收集实体的资源引用
|
|
32
|
+
*/
|
|
33
|
+
function collectEntityAssets(entity, deps, assets) {
|
|
34
|
+
if (!entity || typeof entity !== 'object')
|
|
35
|
+
return;
|
|
36
|
+
// 扫描所有组件
|
|
37
|
+
if (entity.components) {
|
|
38
|
+
for (const [componentName, componentData] of Object.entries(entity.components)) {
|
|
39
|
+
if (componentData && typeof componentData === 'object') {
|
|
40
|
+
// 递归扫描组件数据,查找资源 ID
|
|
41
|
+
findAssetIds(componentData, deps, assets);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 扫描其他可能包含资源引用的字段
|
|
46
|
+
const fieldsToScan = ['script', 'scripts', 'data', 'attributes'];
|
|
47
|
+
for (const field of fieldsToScan) {
|
|
48
|
+
if (entity[field]) {
|
|
49
|
+
findAssetIds(entity[field], deps, assets);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 递归查找 JSON 中的资源 ID
|
|
55
|
+
* PlayCanvas 中资源 ID 通常是数字或数字字符串
|
|
56
|
+
*/
|
|
57
|
+
function findAssetIds(obj, deps, assets, depth = 0) {
|
|
58
|
+
// 防止无限递归
|
|
59
|
+
if (depth > 15)
|
|
60
|
+
return;
|
|
61
|
+
if (obj === null || obj === undefined)
|
|
62
|
+
return;
|
|
63
|
+
// 检查是否是资源 ID
|
|
64
|
+
if (typeof obj === 'number' || typeof obj === 'string') {
|
|
65
|
+
const assetId = String(obj);
|
|
66
|
+
// 验证是否是真实的资源 ID
|
|
67
|
+
if (assets[assetId]) {
|
|
68
|
+
const asset = assets[assetId];
|
|
69
|
+
deps.directAssets.add(assetId);
|
|
70
|
+
// 如果是脚本资源,单独记录
|
|
71
|
+
if (asset.type === 'script') {
|
|
72
|
+
deps.scripts.add(assetId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// 递归处理数组
|
|
78
|
+
if (Array.isArray(obj)) {
|
|
79
|
+
for (const item of obj) {
|
|
80
|
+
findAssetIds(item, deps, assets, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// 递归处理对象
|
|
85
|
+
if (typeof obj === 'object') {
|
|
86
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
87
|
+
// 跳过一些不包含资源引用的字段
|
|
88
|
+
if (key === 'position' || key === 'rotation' || key === 'scale' ||
|
|
89
|
+
key === 'enabled' || key === 'name' || key === 'parent' ||
|
|
90
|
+
key === 'children') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
findAssetIds(value, deps, assets, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 收集间接依赖(材质 → 纹理,模型 → 材质等)
|
|
99
|
+
*/
|
|
100
|
+
function collectIndirectDependencies(deps, assets) {
|
|
101
|
+
const visited = new Set();
|
|
102
|
+
const queue = Array.from(deps.directAssets);
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const assetId = queue.shift();
|
|
105
|
+
if (visited.has(assetId))
|
|
106
|
+
continue;
|
|
107
|
+
visited.add(assetId);
|
|
108
|
+
const asset = assets[assetId];
|
|
109
|
+
if (!asset)
|
|
110
|
+
continue;
|
|
111
|
+
// 检查不同类型资源的依赖
|
|
112
|
+
switch (asset.type) {
|
|
113
|
+
case 'material':
|
|
114
|
+
// 材质可能引用纹理
|
|
115
|
+
findAssetIdsInData(asset.data, deps, assets, queue, visited);
|
|
116
|
+
break;
|
|
117
|
+
case 'model':
|
|
118
|
+
// 模型可能引用材质
|
|
119
|
+
findAssetIdsInData(asset.data, deps, assets, queue, visited);
|
|
120
|
+
// 检查材质映射
|
|
121
|
+
if (asset.data?.mapping) {
|
|
122
|
+
for (const [meshName, materialId] of Object.entries(asset.data.mapping)) {
|
|
123
|
+
if (typeof materialId === 'number' || typeof materialId === 'string') {
|
|
124
|
+
const id = String(materialId);
|
|
125
|
+
if (assets[id] && !visited.has(id) && !deps.directAssets.has(id)) {
|
|
126
|
+
deps.indirectAssets.add(id);
|
|
127
|
+
queue.push(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case 'cubemap':
|
|
134
|
+
case 'texture':
|
|
135
|
+
// 纹理资源通常是叶子节点,但可能引用其他纹理(如合成纹理)
|
|
136
|
+
findAssetIdsInData(asset.data, deps, assets, queue, visited);
|
|
137
|
+
break;
|
|
138
|
+
case 'sprite':
|
|
139
|
+
// Sprite 可能引用纹理图集
|
|
140
|
+
if (asset.data?.textureAtlasAsset) {
|
|
141
|
+
const id = String(asset.data.textureAtlasAsset);
|
|
142
|
+
if (assets[id] && !visited.has(id) && !deps.directAssets.has(id)) {
|
|
143
|
+
deps.indirectAssets.add(id);
|
|
144
|
+
queue.push(id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
findAssetIdsInData(asset.data, deps, assets, queue, visited);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 在资源数据中查找资源 ID
|
|
154
|
+
*/
|
|
155
|
+
function findAssetIdsInData(data, deps, assets, queue, visited) {
|
|
156
|
+
if (!data)
|
|
157
|
+
return;
|
|
158
|
+
// 递归扫描 data 对象
|
|
159
|
+
const scan = (obj, depth = 0) => {
|
|
160
|
+
if (depth > 10)
|
|
161
|
+
return;
|
|
162
|
+
if (obj === null || obj === undefined)
|
|
163
|
+
return;
|
|
164
|
+
if (typeof obj === 'number' || typeof obj === 'string') {
|
|
165
|
+
const id = String(obj);
|
|
166
|
+
if (assets[id] && !visited.has(id) && !deps.directAssets.has(id)) {
|
|
167
|
+
deps.indirectAssets.add(id);
|
|
168
|
+
queue.push(id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (Array.isArray(obj)) {
|
|
172
|
+
for (const item of obj) {
|
|
173
|
+
scan(item, depth + 1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (typeof obj === 'object') {
|
|
177
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
178
|
+
// 跳过明显不是资源引用的字段
|
|
179
|
+
if (key === 'name' || key === 'enabled' || key === 'position' ||
|
|
180
|
+
key === 'rotation' || key === 'scale') {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
scan(value, depth + 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
scan(data);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 分析多个场景的资源依赖并合并
|
|
191
|
+
* @param scenes 场景列表
|
|
192
|
+
* @param assets 所有资源的映射表
|
|
193
|
+
* @param globalScriptIds 全局脚本 ID 列表(来自 settings.scripts)
|
|
194
|
+
* @returns 合并后的资源 ID 集合
|
|
195
|
+
*/
|
|
196
|
+
export async function collectScenesAssets(scenes, assets, globalScriptIds) {
|
|
197
|
+
const allAssetIds = new Set();
|
|
198
|
+
for (const scene of scenes) {
|
|
199
|
+
const deps = await analyzeSceneDependencies(scene, assets);
|
|
200
|
+
// 合并直接依赖和间接依赖
|
|
201
|
+
for (const id of deps.directAssets)
|
|
202
|
+
allAssetIds.add(id);
|
|
203
|
+
for (const id of deps.indirectAssets)
|
|
204
|
+
allAssetIds.add(id);
|
|
205
|
+
for (const id of deps.scripts)
|
|
206
|
+
allAssetIds.add(id);
|
|
207
|
+
}
|
|
208
|
+
// 包含全局脚本(在 settings.scripts 中声明的)
|
|
209
|
+
if (globalScriptIds) {
|
|
210
|
+
for (const id of globalScriptIds) {
|
|
211
|
+
allAssetIds.add(id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return allAssetIds;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 打印场景依赖统计信息
|
|
218
|
+
*/
|
|
219
|
+
export function printSceneDependencies(deps) {
|
|
220
|
+
console.log(`\n📊 场景: ${deps.sceneName} (ID: ${deps.sceneId})`);
|
|
221
|
+
console.log(` - 直接引用资源: ${deps.directAssets.size} 个`);
|
|
222
|
+
console.log(` - 间接依赖资源: ${deps.indirectAssets.size} 个`);
|
|
223
|
+
console.log(` - 脚本资源: ${deps.scripts.size} 个`);
|
|
224
|
+
console.log(` - 总计: ${deps.directAssets.size + deps.indirectAssets.size} 个资源`);
|
|
225
|
+
}
|
package/dist/base-builder.d.ts
CHANGED
package/dist/base-builder.js
CHANGED
|
@@ -2,6 +2,7 @@ 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';
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -304,6 +305,11 @@ export class BaseBuilder {
|
|
|
304
305
|
entryFileNames: '__[name].js',
|
|
305
306
|
chunkFileNames: '__[name]-[hash].js',
|
|
306
307
|
assetFileNames: 'files/assets/[name].[ext]',
|
|
308
|
+
format: 'iife', // 使用 IIFE 格式,避免 ES 模块的 import 语句
|
|
309
|
+
globals: {
|
|
310
|
+
'playcanvas': 'pc',
|
|
311
|
+
'pc': 'pc',
|
|
312
|
+
},
|
|
307
313
|
},
|
|
308
314
|
// 外部化 PlayCanvas Engine(不打包)
|
|
309
315
|
external: ['pc', 'playcanvas'],
|
|
@@ -316,7 +322,21 @@ export class BaseBuilder {
|
|
|
316
322
|
viteSourceBuilderPlugin({
|
|
317
323
|
projectDir: this.projectDir,
|
|
318
324
|
outputDir: this.options.outputDir,
|
|
325
|
+
selectedScenes: this.options.selectedScenes,
|
|
319
326
|
}),
|
|
327
|
+
// 打包分析报告(如果启用)
|
|
328
|
+
...(this.options.analyze ? [
|
|
329
|
+
visualizer({
|
|
330
|
+
filename: this.options.analyzeReportPath
|
|
331
|
+
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
332
|
+
: path.join(this.options.outputDir, 'base-bundle-report.html'),
|
|
333
|
+
template: 'treemap',
|
|
334
|
+
gzipSize: true,
|
|
335
|
+
brotliSize: true,
|
|
336
|
+
open: false,
|
|
337
|
+
sourcemap: false,
|
|
338
|
+
}),
|
|
339
|
+
] : []),
|
|
320
340
|
],
|
|
321
341
|
};
|
|
322
342
|
// 2. 执行 Vite 构建
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { PlayCanvasProject } from '../loaders/playcanvas-loader.js';
|
|
2
2
|
import { PlayCraftProject } from '../loaders/playcraft-loader.js';
|
|
3
3
|
export type ProjectConfig = PlayCanvasProject | PlayCraftProject;
|
|
4
|
+
export interface GenerateConfigOptions {
|
|
5
|
+
selectedScenes?: string[];
|
|
6
|
+
}
|
|
4
7
|
/**
|
|
5
8
|
* 生成 PlayCanvas 构建产物格式的 config.json
|
|
6
9
|
*/
|
|
7
|
-
export declare function generateConfig(projectConfig: ProjectConfig): any
|
|
10
|
+
export declare function generateConfig(projectConfig: ProjectConfig, options?: GenerateConfigOptions): Promise<any>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectScenesAssets, printSceneDependencies, analyzeSceneDependencies } from '../analyzers/scene-asset-collector.js';
|
|
1
2
|
function shouldIncludeAsset(asset, scriptIds, requiredScriptIds) {
|
|
2
3
|
if (!asset) {
|
|
3
4
|
return false;
|
|
@@ -41,7 +42,7 @@ function collectRequiredScriptIds(assets) {
|
|
|
41
42
|
/**
|
|
42
43
|
* 生成 PlayCanvas 构建产物格式的 config.json
|
|
43
44
|
*/
|
|
44
|
-
export function generateConfig(projectConfig) {
|
|
45
|
+
export async function generateConfig(projectConfig, options) {
|
|
45
46
|
if (projectConfig.format === 'playcanvas') {
|
|
46
47
|
const pcProject = projectConfig;
|
|
47
48
|
// 从 PlayCanvas 项目格式生成 config.json
|
|
@@ -56,30 +57,62 @@ export function generateConfig(projectConfig) {
|
|
|
56
57
|
config.application_properties.batchGroups = [];
|
|
57
58
|
}
|
|
58
59
|
// 处理场景
|
|
60
|
+
let allScenes = [];
|
|
59
61
|
if (pcProject.scenes) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
allScenes = Array.isArray(pcProject.scenes)
|
|
63
|
+
? pcProject.scenes
|
|
64
|
+
: Object.entries(pcProject.scenes).map(([id, scene]) => ({ id, ...scene }));
|
|
65
|
+
}
|
|
66
|
+
// 场景过滤:如果指定了选中的场景,则只包含这些场景
|
|
67
|
+
let selectedScenes = allScenes;
|
|
68
|
+
if (options?.selectedScenes && options.selectedScenes.length > 0) {
|
|
69
|
+
selectedScenes = allScenes.filter(scene => {
|
|
70
|
+
const sceneId = String(scene.id || scene.scene);
|
|
71
|
+
const sceneName = scene.name || '';
|
|
72
|
+
return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
|
|
73
|
+
});
|
|
74
|
+
console.log(`\n🎬 场景过滤:`);
|
|
75
|
+
console.log(` - 总场景数: ${allScenes.length}`);
|
|
76
|
+
console.log(` - 选中场景: ${selectedScenes.length}`);
|
|
77
|
+
selectedScenes.forEach(scene => {
|
|
78
|
+
console.log(` • ${scene.name || scene.id}`);
|
|
79
|
+
});
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
config.scenes = selectedScenes.map((scene) => ({
|
|
82
|
+
name: scene.name || scene.id,
|
|
83
|
+
url: scene.url || scene.file || `${scene.id}.json`,
|
|
84
|
+
}));
|
|
85
|
+
// 处理资产:基于场景过滤
|
|
75
86
|
if (pcProject.assets) {
|
|
76
87
|
const scriptIds = new Set((pcProject.project.settings?.scripts || []).map((id) => String(id)));
|
|
77
88
|
const requiredScriptIds = collectRequiredScriptIds(pcProject.assets);
|
|
89
|
+
let allowedAssetIds = null;
|
|
90
|
+
// 如果启用了场景过滤,分析场景依赖
|
|
91
|
+
if (options?.selectedScenes && options.selectedScenes.length > 0 && selectedScenes.length > 0) {
|
|
92
|
+
console.log(`\n🔍 分析场景资源依赖...`);
|
|
93
|
+
allowedAssetIds = await collectScenesAssets(selectedScenes, pcProject.assets, scriptIds);
|
|
94
|
+
// 打印每个场景的依赖统计
|
|
95
|
+
for (const scene of selectedScenes) {
|
|
96
|
+
const deps = await analyzeSceneDependencies(scene, pcProject.assets);
|
|
97
|
+
printSceneDependencies(deps);
|
|
98
|
+
}
|
|
99
|
+
console.log(`\n📊 资源过滤统计:`);
|
|
100
|
+
console.log(` - 总资源数: ${Object.keys(pcProject.assets).length}`);
|
|
101
|
+
console.log(` - 全局脚本: ${scriptIds.size}`);
|
|
102
|
+
console.log(` - 场景依赖: ${allowedAssetIds.size}`);
|
|
103
|
+
console.log(` - 将节省: ${Object.keys(pcProject.assets).length - allowedAssetIds.size} 个资源 (${((1 - allowedAssetIds.size / Object.keys(pcProject.assets).length) * 100).toFixed(1)}%)`);
|
|
104
|
+
}
|
|
78
105
|
const filtered = {};
|
|
79
106
|
for (const [assetId, asset] of Object.entries(pcProject.assets)) {
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
// 基本过滤(非运行时资产)
|
|
108
|
+
if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
|
|
109
|
+
continue;
|
|
82
110
|
}
|
|
111
|
+
// 场景过滤:如果启用,只包含场景依赖的资源
|
|
112
|
+
if (allowedAssetIds && !allowedAssetIds.has(assetId)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
filtered[assetId] = asset;
|
|
83
116
|
}
|
|
84
117
|
config.assets = filtered;
|
|
85
118
|
}
|
|
@@ -99,21 +132,58 @@ export function generateConfig(projectConfig) {
|
|
|
99
132
|
config.application_properties.batchGroups = [];
|
|
100
133
|
}
|
|
101
134
|
// 处理场景
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
let allScenes = pcProject.scenes || [];
|
|
136
|
+
let selectedScenes = allScenes;
|
|
137
|
+
if (options?.selectedScenes && options.selectedScenes.length > 0 && allScenes.length > 0) {
|
|
138
|
+
selectedScenes = allScenes.filter(scene => {
|
|
139
|
+
const sceneId = String(scene.id || scene.name);
|
|
140
|
+
const sceneName = scene.name || '';
|
|
141
|
+
return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
|
|
142
|
+
});
|
|
143
|
+
console.log(`\n🎬 场景过滤:`);
|
|
144
|
+
console.log(` - 总场景数: ${allScenes.length}`);
|
|
145
|
+
console.log(` - 选中场景: ${selectedScenes.length}`);
|
|
146
|
+
selectedScenes.forEach(scene => {
|
|
147
|
+
console.log(` • ${scene.name || scene.id}`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (selectedScenes.length > 0) {
|
|
151
|
+
config.scenes = selectedScenes.map((scene) => ({
|
|
104
152
|
name: scene.name || scene.id,
|
|
105
153
|
url: scene.url || `${scene.id || 'scene'}.json`,
|
|
106
154
|
}));
|
|
107
155
|
}
|
|
108
|
-
//
|
|
156
|
+
// 处理资产:基于场景过滤
|
|
109
157
|
if (pcProject.assets) {
|
|
110
158
|
const scriptIds = new Set((pcProject.manifest.settings?.scripts || []).map((id) => String(id)));
|
|
111
159
|
const requiredScriptIds = collectRequiredScriptIds(pcProject.assets);
|
|
160
|
+
let allowedAssetIds = null;
|
|
161
|
+
// 如果启用了场景过滤,分析场景依赖
|
|
162
|
+
if (options?.selectedScenes && options.selectedScenes.length > 0 && selectedScenes.length > 0) {
|
|
163
|
+
console.log(`\n🔍 分析场景资源依赖...`);
|
|
164
|
+
allowedAssetIds = await collectScenesAssets(selectedScenes, pcProject.assets, scriptIds);
|
|
165
|
+
// 打印每个场景的依赖统计
|
|
166
|
+
for (const scene of selectedScenes) {
|
|
167
|
+
const deps = await analyzeSceneDependencies(scene, pcProject.assets);
|
|
168
|
+
printSceneDependencies(deps);
|
|
169
|
+
}
|
|
170
|
+
console.log(`\n📊 资源过滤统计:`);
|
|
171
|
+
console.log(` - 总资源数: ${Object.keys(pcProject.assets).length}`);
|
|
172
|
+
console.log(` - 全局脚本: ${scriptIds.size}`);
|
|
173
|
+
console.log(` - 场景依赖: ${allowedAssetIds.size}`);
|
|
174
|
+
console.log(` - 将节省: ${Object.keys(pcProject.assets).length - allowedAssetIds.size} 个资源 (${((1 - allowedAssetIds.size / Object.keys(pcProject.assets).length) * 100).toFixed(1)}%)`);
|
|
175
|
+
}
|
|
112
176
|
const filtered = {};
|
|
113
177
|
for (const [assetId, asset] of Object.entries(pcProject.assets)) {
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
// 基本过滤(非运行时资产)
|
|
179
|
+
if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// 场景过滤:如果启用,只包含场景依赖的资源
|
|
183
|
+
if (allowedAssetIds && !allowedAssetIds.has(assetId)) {
|
|
184
|
+
continue;
|
|
116
185
|
}
|
|
186
|
+
filtered[assetId] = asset;
|
|
117
187
|
}
|
|
118
188
|
config.assets = filtered;
|
|
119
189
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Platform = 'facebook' | 'snapchat' | 'ironsource' | 'applovin' | 'google' | 'tiktok' | 'unity' | 'liftoff' | 'moloco' | 'bigo';
|
|
1
|
+
export type Platform = 'facebook' | 'snapchat' | 'ironsource' | 'applovin' | 'google' | 'tiktok' | 'unity' | 'liftoff' | 'moloco' | 'bigo' | 'inmobi' | 'adikteev' | 'remerge';
|
|
2
2
|
export type OutputFormat = 'html' | 'zip';
|
|
3
3
|
export interface BuildOptions {
|
|
4
4
|
platform: Platform;
|
|
@@ -25,6 +25,7 @@ export interface BuildOptions {
|
|
|
25
25
|
skipBuild?: boolean;
|
|
26
26
|
buildOptions?: LocalBuildOptions;
|
|
27
27
|
useVite?: boolean;
|
|
28
|
+
selectedScenes?: string[];
|
|
28
29
|
cssMinify?: boolean;
|
|
29
30
|
jsMinify?: boolean;
|
|
30
31
|
compressImages?: boolean;
|
|
@@ -6,7 +6,6 @@ import viteImagemin from '@vheemstra/vite-plugin-imagemin';
|
|
|
6
6
|
import imageminMozjpeg from 'imagemin-mozjpeg';
|
|
7
7
|
import imageminPngquant from 'imagemin-pngquant';
|
|
8
8
|
import imageminWebp from 'imagemin-webp';
|
|
9
|
-
import { visualizer } from 'rollup-plugin-visualizer';
|
|
10
9
|
import { PLATFORM_CONFIGS } from './platform-configs.js';
|
|
11
10
|
import { vitePlayCanvasPlugin } from './plugin-playcanvas.js';
|
|
12
11
|
import { vitePlatformPlugin } from './plugin-platform.js';
|
|
@@ -163,19 +162,6 @@ export class ViteConfigBuilder {
|
|
|
163
162
|
removeViteModuleLoader: true,
|
|
164
163
|
}));
|
|
165
164
|
}
|
|
166
|
-
// 6. 打包分析报告
|
|
167
|
-
if (this.options.analyze) {
|
|
168
|
-
const reportPath = this.options.analyzeReportPath
|
|
169
|
-
? this.options.analyzeReportPath
|
|
170
|
-
: path.join(outputDir, 'bundle-report.html');
|
|
171
|
-
plugins.push(visualizer({
|
|
172
|
-
filename: reportPath,
|
|
173
|
-
template: 'treemap',
|
|
174
|
-
gzipSize: true,
|
|
175
|
-
brotliSize: true,
|
|
176
|
-
open: false,
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
179
165
|
return plugins.filter(Boolean);
|
|
180
166
|
}
|
|
181
167
|
getPlayableOptions(config) {
|
|
@@ -24,7 +24,7 @@ export const PLATFORM_CONFIGS = {
|
|
|
24
24
|
playable: {
|
|
25
25
|
patchXhrOut: true,
|
|
26
26
|
inlineGameScripts: true,
|
|
27
|
-
compressEngine:
|
|
27
|
+
compressEngine: false, // 关闭引擎压缩,便于调试
|
|
28
28
|
configJsonInline: true,
|
|
29
29
|
},
|
|
30
30
|
},
|
|
@@ -254,4 +254,80 @@ export const PLATFORM_CONFIGS = {
|
|
|
254
254
|
externFiles: true,
|
|
255
255
|
},
|
|
256
256
|
},
|
|
257
|
+
inmobi: {
|
|
258
|
+
sizeLimit: 5 * 1024 * 1024, // 5MB
|
|
259
|
+
outputFormat: 'zip',
|
|
260
|
+
minifyCSS: true,
|
|
261
|
+
minifyJS: true,
|
|
262
|
+
compressImages: true,
|
|
263
|
+
compressModels: true,
|
|
264
|
+
injectScripts: ['mraid'],
|
|
265
|
+
outputFileName: 'index.html',
|
|
266
|
+
includeSourcemap: false,
|
|
267
|
+
imageQuality: {
|
|
268
|
+
jpg: 75,
|
|
269
|
+
png: [0.7, 0.8],
|
|
270
|
+
webp: 75,
|
|
271
|
+
},
|
|
272
|
+
modelCompression: {
|
|
273
|
+
method: 'draco',
|
|
274
|
+
quality: 0.8,
|
|
275
|
+
},
|
|
276
|
+
playable: {
|
|
277
|
+
inlineGameScripts: true,
|
|
278
|
+
externFiles: true,
|
|
279
|
+
mraidSupport: true,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
adikteev: {
|
|
283
|
+
sizeLimit: 5 * 1024 * 1024, // 5MB
|
|
284
|
+
outputFormat: 'html',
|
|
285
|
+
minifyCSS: true,
|
|
286
|
+
minifyJS: true,
|
|
287
|
+
compressImages: true,
|
|
288
|
+
compressModels: true,
|
|
289
|
+
injectScripts: ['mraid3'],
|
|
290
|
+
outputFileName: 'index.html',
|
|
291
|
+
includeSourcemap: false,
|
|
292
|
+
imageQuality: {
|
|
293
|
+
jpg: 75,
|
|
294
|
+
png: [0.7, 0.8],
|
|
295
|
+
webp: 75,
|
|
296
|
+
},
|
|
297
|
+
modelCompression: {
|
|
298
|
+
method: 'draco',
|
|
299
|
+
quality: 0.8,
|
|
300
|
+
},
|
|
301
|
+
playable: {
|
|
302
|
+
patchXhrOut: true,
|
|
303
|
+
inlineGameScripts: true,
|
|
304
|
+
configJsonInline: true,
|
|
305
|
+
mraidSupport: true,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
remerge: {
|
|
309
|
+
sizeLimit: 5 * 1024 * 1024, // 5MB
|
|
310
|
+
outputFormat: 'html',
|
|
311
|
+
minifyCSS: true,
|
|
312
|
+
minifyJS: true,
|
|
313
|
+
compressImages: true,
|
|
314
|
+
compressModels: true,
|
|
315
|
+
injectScripts: ['fbPlayableAd'],
|
|
316
|
+
outputFileName: 'index.html',
|
|
317
|
+
includeSourcemap: false,
|
|
318
|
+
imageQuality: {
|
|
319
|
+
jpg: 75,
|
|
320
|
+
png: [0.7, 0.8],
|
|
321
|
+
webp: 75,
|
|
322
|
+
},
|
|
323
|
+
modelCompression: {
|
|
324
|
+
method: 'draco',
|
|
325
|
+
quality: 0.8,
|
|
326
|
+
},
|
|
327
|
+
playable: {
|
|
328
|
+
patchXhrOut: true,
|
|
329
|
+
inlineGameScripts: true,
|
|
330
|
+
configJsonInline: true,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
257
333
|
};
|
|
@@ -7,9 +7,15 @@ import { createRequire } from 'module';
|
|
|
7
7
|
* 处理 PlayCanvas 特定的资源转换和内联
|
|
8
8
|
*/
|
|
9
9
|
export function vitePlayCanvasPlugin(options) {
|
|
10
|
+
// 保存插件上下文,用于调用 Vite 的 load 方法
|
|
11
|
+
let pluginContext = null;
|
|
10
12
|
return {
|
|
11
13
|
name: 'vite-plugin-playcanvas',
|
|
12
14
|
enforce: 'pre',
|
|
15
|
+
buildStart() {
|
|
16
|
+
// 保存插件的 this 上下文
|
|
17
|
+
pluginContext = this;
|
|
18
|
+
},
|
|
13
19
|
async transformIndexHtml(html) {
|
|
14
20
|
if (options.ammoReplacement) {
|
|
15
21
|
html = await injectPhysicsLibrary(html, options);
|
|
@@ -17,25 +23,29 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
17
23
|
if (options.outputFormat === 'html') {
|
|
18
24
|
// 1. 内联 PlayCanvas Engine + 引擎补丁
|
|
19
25
|
html = await inlineEngineScript(html, options.baseBuildDir, options);
|
|
20
|
-
// 2.
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
// 2. 内联 __game-scripts.js(用户脚本)
|
|
27
|
+
if (options.inlineGameScripts) {
|
|
28
|
+
html = await inlineGameScripts(html, options.baseBuildDir);
|
|
29
|
+
}
|
|
30
|
+
// 3. 内联并转换 __settings__.js(传递插件上下文)
|
|
31
|
+
html = await inlineAndConvertSettings(html, options.baseBuildDir, options, pluginContext);
|
|
32
|
+
// 4. 内联 __modules__.js
|
|
23
33
|
html = await inlineModulesScript(html, options.baseBuildDir);
|
|
24
|
-
//
|
|
34
|
+
// 5. 内联 __start__.js
|
|
25
35
|
html = await inlineStartScript(html, options.baseBuildDir, options);
|
|
26
|
-
//
|
|
36
|
+
// 6. 内联 __loading__.js
|
|
27
37
|
html = await inlineLoadingScript(html, options.baseBuildDir);
|
|
28
|
-
//
|
|
38
|
+
// 7. 内联 CSS
|
|
29
39
|
html = await inlineCSS(html, options.baseBuildDir, options);
|
|
30
|
-
//
|
|
40
|
+
// 8. 处理 manifest.json
|
|
31
41
|
html = await inlineManifest(html, options.baseBuildDir, options);
|
|
32
42
|
}
|
|
33
43
|
return html;
|
|
34
44
|
},
|
|
35
45
|
async transform(code, id) {
|
|
36
|
-
// 转换 __settings__.js 中的资源路径为 data URLs
|
|
46
|
+
// 转换 __settings__.js 中的资源路径为 data URLs(传递插件上下文)
|
|
37
47
|
if (id.endsWith('__settings__.js')) {
|
|
38
|
-
return await convertSettingsToDataUrls(code, options.baseBuildDir, options);
|
|
48
|
+
return await convertSettingsToDataUrls(code, options.baseBuildDir, options, pluginContext);
|
|
39
49
|
}
|
|
40
50
|
return code;
|
|
41
51
|
},
|
|
@@ -73,18 +83,18 @@ async function inlineEngineScript(html, baseBuildDir, options) {
|
|
|
73
83
|
/**
|
|
74
84
|
* 内联并转换 __settings__.js
|
|
75
85
|
*/
|
|
76
|
-
async function inlineAndConvertSettings(html, baseBuildDir, options) {
|
|
86
|
+
async function inlineAndConvertSettings(html, baseBuildDir, options, pluginContext) {
|
|
77
87
|
const settingsPath = path.join(baseBuildDir, '__settings__.js');
|
|
78
88
|
try {
|
|
79
89
|
await fs.access(settingsPath);
|
|
80
90
|
}
|
|
81
91
|
catch (error) {
|
|
82
92
|
// __settings__.js 不存在,尝试从 config.json 生成
|
|
83
|
-
return await generateAndInlineSettings(html, baseBuildDir, options);
|
|
93
|
+
return await generateAndInlineSettings(html, baseBuildDir, options, pluginContext);
|
|
84
94
|
}
|
|
85
95
|
let settingsCode = await fs.readFile(settingsPath, 'utf-8');
|
|
86
|
-
// 转换资源URL为data URLs
|
|
87
|
-
settingsCode = await convertSettingsToDataUrls(settingsCode, baseBuildDir, options);
|
|
96
|
+
// 转换资源URL为data URLs(传递插件上下文)
|
|
97
|
+
settingsCode = await convertSettingsToDataUrls(settingsCode, baseBuildDir, options, pluginContext);
|
|
88
98
|
// 替换 script 标签
|
|
89
99
|
const scriptPattern = /<script[^>]*src=["']__settings__\.js["'][^>]*><\/script>/i;
|
|
90
100
|
html = html.replace(scriptPattern, `<script>${settingsCode}</script>`);
|
|
@@ -93,12 +103,12 @@ async function inlineAndConvertSettings(html, baseBuildDir, options) {
|
|
|
93
103
|
/**
|
|
94
104
|
* 生成并内联 settings(如果 __settings__.js 不存在)
|
|
95
105
|
*/
|
|
96
|
-
async function generateAndInlineSettings(html, baseBuildDir, options) {
|
|
106
|
+
async function generateAndInlineSettings(html, baseBuildDir, options, pluginContext) {
|
|
97
107
|
const configPath = path.join(baseBuildDir, 'config.json');
|
|
98
108
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
99
109
|
let configJson = JSON.parse(configContent);
|
|
100
110
|
if (options.convertDataUrls) {
|
|
101
|
-
configJson = await inlineConfigAssetUrls(configJson, baseBuildDir);
|
|
111
|
+
configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
|
|
102
112
|
}
|
|
103
113
|
if (options.ammoReplacement) {
|
|
104
114
|
configJson = stripAmmoAssets(configJson);
|
|
@@ -158,9 +168,9 @@ window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
|
|
|
158
168
|
/**
|
|
159
169
|
* 转换 settings 代码中的资源URL为 data URLs
|
|
160
170
|
*/
|
|
161
|
-
async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options) {
|
|
162
|
-
// 1. 转换 config.json
|
|
163
|
-
settingsCode = await convertConfigUrl(settingsCode, baseBuildDir, options);
|
|
171
|
+
async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options, pluginContext) {
|
|
172
|
+
// 1. 转换 config.json(传递插件上下文)
|
|
173
|
+
settingsCode = await convertConfigUrl(settingsCode, baseBuildDir, options, pluginContext);
|
|
164
174
|
// 2. 转换场景文件
|
|
165
175
|
settingsCode = await convertSceneUrl(settingsCode, baseBuildDir);
|
|
166
176
|
// 3. 转换 PRELOAD_MODULES(优先使用 JS fallback)
|
|
@@ -170,7 +180,7 @@ async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options) {
|
|
|
170
180
|
/**
|
|
171
181
|
* 转换 CONFIG_FILENAME 为 data URL
|
|
172
182
|
*/
|
|
173
|
-
async function convertConfigUrl(settingsCode, baseBuildDir, options) {
|
|
183
|
+
async function convertConfigUrl(settingsCode, baseBuildDir, options, pluginContext) {
|
|
174
184
|
const configMatch = settingsCode.match(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)"/);
|
|
175
185
|
if (!configMatch) {
|
|
176
186
|
return settingsCode;
|
|
@@ -184,7 +194,7 @@ async function convertConfigUrl(settingsCode, baseBuildDir, options) {
|
|
|
184
194
|
const configContent = await fs.readFile(fullConfigPath, 'utf-8');
|
|
185
195
|
let configJson = JSON.parse(configContent);
|
|
186
196
|
if (options.convertDataUrls) {
|
|
187
|
-
configJson = await inlineConfigAssetUrls(configJson, baseBuildDir);
|
|
197
|
+
configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
|
|
188
198
|
}
|
|
189
199
|
if (options.ammoReplacement) {
|
|
190
200
|
configJson = stripAmmoAssets(configJson);
|
|
@@ -369,6 +379,8 @@ async function inlineModulesScript(html, baseBuildDir) {
|
|
|
369
379
|
}
|
|
370
380
|
/**
|
|
371
381
|
* 内联 __game-scripts.js
|
|
382
|
+
* 注意:脚本需要在 pc.AppBase 创建后执行,所以插入到 </body> 之前
|
|
383
|
+
* 同时包装成一个函数,在 DOMContentLoaded 后执行,确保 PlayCanvas 引擎已完全初始化
|
|
372
384
|
*/
|
|
373
385
|
async function inlineGameScripts(html, baseBuildDir) {
|
|
374
386
|
const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
|
|
@@ -380,13 +392,36 @@ async function inlineGameScripts(html, baseBuildDir) {
|
|
|
380
392
|
return html;
|
|
381
393
|
}
|
|
382
394
|
const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
// 将脚本包装,确保在 pc.script 初始化后执行
|
|
396
|
+
// pc.createScript 需要 pc.script._scripts 已初始化
|
|
397
|
+
const wrappedCode = `
|
|
398
|
+
<script>
|
|
399
|
+
(function() {
|
|
400
|
+
// 等待 pc.script 准备好
|
|
401
|
+
function initGameScripts() {
|
|
402
|
+
if (typeof pc === 'undefined' || !pc.script) {
|
|
403
|
+
setTimeout(initGameScripts, 10);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
${gameScriptsCode}
|
|
407
|
+
}
|
|
408
|
+
// 延迟执行,确保引擎初始化完成
|
|
409
|
+
if (document.readyState === 'loading') {
|
|
410
|
+
document.addEventListener('DOMContentLoaded', initGameScripts);
|
|
411
|
+
} else {
|
|
412
|
+
setTimeout(initGameScripts, 0);
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
</script>`;
|
|
416
|
+
// 在 </body> 之前插入,确保在其他脚本之后执行
|
|
417
|
+
if (html.includes('</body>')) {
|
|
418
|
+
html = html.replace('</body>', `${wrappedCode}\n</body>`);
|
|
419
|
+
}
|
|
420
|
+
else if (html.includes('</html>')) {
|
|
421
|
+
html = html.replace('</html>', `${wrappedCode}\n</html>`);
|
|
386
422
|
}
|
|
387
423
|
else {
|
|
388
|
-
|
|
389
|
-
html = html.replace('<body>', `<body>\n<script>${gameScriptsCode}</script>\n`);
|
|
424
|
+
html += wrappedCode;
|
|
390
425
|
}
|
|
391
426
|
return html;
|
|
392
427
|
}
|
|
@@ -507,16 +542,44 @@ async function injectPhysicsLibrary(html, options) {
|
|
|
507
542
|
if (!library) {
|
|
508
543
|
return html;
|
|
509
544
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
545
|
+
let scriptsToInject = [];
|
|
546
|
+
if (library === 'p2') {
|
|
547
|
+
// Inject p2.js (2D physics)
|
|
548
|
+
const p2Code = await readPatchFile('p2.min.js');
|
|
549
|
+
if (p2Code) {
|
|
550
|
+
scriptsToInject.push(`<script>${p2Code}</script>`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (library === 'cannon') {
|
|
554
|
+
// Inject Cannon.js (3D physics) + adapter
|
|
555
|
+
// Get __dirname equivalent in ES modules
|
|
556
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
557
|
+
const __dirname = path.dirname(__filename);
|
|
558
|
+
const physicsDir = path.join(__dirname, '../../physics');
|
|
559
|
+
try {
|
|
560
|
+
// 1. Cannon.js engine
|
|
561
|
+
const cannonPath = path.join(physicsDir, 'cannon-es-bundle.js');
|
|
562
|
+
const cannonCode = await fs.readFile(cannonPath, 'utf-8');
|
|
563
|
+
scriptsToInject.push(`<script>${cannonCode}</script>`);
|
|
564
|
+
// 2. Rigidbody adapter (compatibility layer)
|
|
565
|
+
const adapterPath = path.join(physicsDir, 'cannon-rigidbody-adapter.js');
|
|
566
|
+
const adapterCode = await fs.readFile(adapterPath, 'utf-8');
|
|
567
|
+
scriptsToInject.push(`<script>${adapterCode}</script>`);
|
|
568
|
+
console.log('[PlayCanvas Plugin] Injected Cannon.js physics engine with rigidbody adapter');
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
console.warn('[PlayCanvas Plugin] Failed to load Cannon.js files:', error);
|
|
572
|
+
return html;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (scriptsToInject.length === 0) {
|
|
513
576
|
return html;
|
|
514
577
|
}
|
|
515
|
-
const
|
|
578
|
+
const injectedScripts = scriptsToInject.join('\n');
|
|
516
579
|
if (html.includes('</head>')) {
|
|
517
|
-
return html.replace('</head>', `${
|
|
580
|
+
return html.replace('</head>', `${injectedScripts}\n</head>`);
|
|
518
581
|
}
|
|
519
|
-
return `${
|
|
582
|
+
return `${injectedScripts}\n${html}`;
|
|
520
583
|
}
|
|
521
584
|
function isAmmoModule(module) {
|
|
522
585
|
const moduleName = module.moduleName?.toLowerCase() ?? '';
|
|
@@ -528,12 +591,16 @@ function isAmmoModule(module) {
|
|
|
528
591
|
wasmUrl.includes('ammo') ||
|
|
529
592
|
fallbackUrl.includes('ammo'));
|
|
530
593
|
}
|
|
531
|
-
async function inlineConfigAssetUrls(configJson, baseBuildDir) {
|
|
594
|
+
async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
532
595
|
if (!configJson?.assets) {
|
|
533
596
|
return configJson;
|
|
534
597
|
}
|
|
535
598
|
const assets = configJson.assets;
|
|
536
|
-
|
|
599
|
+
const skippedAssets = [];
|
|
600
|
+
const optimizedAssets = [];
|
|
601
|
+
const skippedScripts = [];
|
|
602
|
+
const SIZE_LIMIT = 1 * 1024 * 1024; // 1MB - 跳过超过这个大小的文件
|
|
603
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
537
604
|
const file = asset?.file;
|
|
538
605
|
if (!file?.url || typeof file.url !== 'string') {
|
|
539
606
|
continue;
|
|
@@ -543,12 +610,61 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir) {
|
|
|
543
610
|
continue;
|
|
544
611
|
}
|
|
545
612
|
const cleanUrl = url.split('?')[0];
|
|
613
|
+
const fileName = cleanUrl.toLowerCase();
|
|
614
|
+
// 跳过指向 __game-scripts.js 的脚本资源
|
|
615
|
+
// 这些资源已经被打包到 __game-scripts.js 中,不需要单独内联
|
|
616
|
+
// __game-scripts.js 会通过 transformIndexHtml 钩子内联到 HTML 中
|
|
617
|
+
if (cleanUrl === '__game-scripts.js' || asset.type === 'script') {
|
|
618
|
+
skippedScripts.push(asset.name || assetId);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// 跳过物理引擎缓存文件和大型文本文件
|
|
622
|
+
if (fileName.endsWith('.pma.txt') ||
|
|
623
|
+
fileName.includes('browsermetrics') ||
|
|
624
|
+
fileName.includes('deferredbrowsermetrics') ||
|
|
625
|
+
(fileName.endsWith('.txt') && file.size && file.size > SIZE_LIMIT)) {
|
|
626
|
+
skippedAssets.push(`${asset.name || assetId} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
|
|
627
|
+
// 标记为不预加载,避免引擎尝试加载
|
|
628
|
+
asset.preload = false;
|
|
629
|
+
// 清空 URL,避免加载失败
|
|
630
|
+
file.url = 'data:text/plain;base64,';
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
546
633
|
const fullPath = path.join(baseBuildDir, cleanUrl);
|
|
547
634
|
try {
|
|
635
|
+
// ✅ 简化逻辑:直接读取文件并判断是否需要压缩
|
|
548
636
|
const buffer = await fs.readFile(fullPath);
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
637
|
+
const originalSize = buffer.length;
|
|
638
|
+
// 检查实际文件大小,跳过超大文件
|
|
639
|
+
if (originalSize > SIZE_LIMIT) {
|
|
640
|
+
skippedAssets.push(`${asset.name || assetId} (${(originalSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
641
|
+
asset.preload = false;
|
|
642
|
+
file.url = 'data:text/plain;base64,';
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
let dataUrl;
|
|
646
|
+
let finalSize;
|
|
647
|
+
// ✅ 核心优化:如果是图片,使用 sharp 压缩
|
|
648
|
+
if (isImageFile(cleanUrl)) {
|
|
649
|
+
const compressed = await compressImage(buffer, cleanUrl, {
|
|
650
|
+
convertToWebP: true,
|
|
651
|
+
quality: 75,
|
|
652
|
+
});
|
|
653
|
+
dataUrl = `data:${compressed.mime};base64,${compressed.buffer.toString('base64')}`;
|
|
654
|
+
finalSize = compressed.buffer.length;
|
|
655
|
+
// 记录优化效果
|
|
656
|
+
const savings = ((1 - finalSize / originalSize) * 100).toFixed(1);
|
|
657
|
+
optimizedAssets.push(`${asset.name || assetId}: ${(originalSize / 1024).toFixed(1)}KB → ${(finalSize / 1024).toFixed(1)}KB (-${savings}%)`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
// 非图片文件,直接 base64 编码
|
|
661
|
+
const mime = guessMimeType(cleanUrl);
|
|
662
|
+
dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
|
|
663
|
+
finalSize = originalSize;
|
|
664
|
+
}
|
|
665
|
+
// 更新 asset 配置
|
|
666
|
+
file.url = dataUrl;
|
|
667
|
+
file.size = finalSize;
|
|
552
668
|
// data URL 不需要 hash/variants,避免引擎追加 ?t=hash 造成无效 URL
|
|
553
669
|
if ('hash' in file) {
|
|
554
670
|
delete file.hash;
|
|
@@ -561,6 +677,31 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir) {
|
|
|
561
677
|
console.warn(`警告: 资源文件不存在: ${url}`);
|
|
562
678
|
}
|
|
563
679
|
}
|
|
680
|
+
// 输出统计信息
|
|
681
|
+
if (optimizedAssets.length > 0) {
|
|
682
|
+
console.log(`\n✅ 通过 Vite 优化了 ${optimizedAssets.length} 个资源:`);
|
|
683
|
+
optimizedAssets.slice(0, 5).forEach(info => console.log(` - ${info}`));
|
|
684
|
+
if (optimizedAssets.length > 5) {
|
|
685
|
+
console.log(` ... 还有 ${optimizedAssets.length - 5} 个资源`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (skippedAssets.length > 0) {
|
|
689
|
+
console.log(`\n⚠️ 已跳过 ${skippedAssets.length} 个大型资源文件 (>1MB):`);
|
|
690
|
+
skippedAssets.slice(0, 10).forEach(name => console.log(` - ${name}`));
|
|
691
|
+
if (skippedAssets.length > 10) {
|
|
692
|
+
console.log(` ... 还有 ${skippedAssets.length - 10} 个文件`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (skippedScripts.length > 0) {
|
|
696
|
+
console.log(`\n📦 跳过了 ${skippedScripts.length} 个脚本资源(已打包到 __game-scripts.js)`);
|
|
697
|
+
if (skippedScripts.length <= 10) {
|
|
698
|
+
skippedScripts.forEach(name => console.log(` - ${name}`));
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
skippedScripts.slice(0, 5).forEach(name => console.log(` - ${name}`));
|
|
702
|
+
console.log(` ... 还有 ${skippedScripts.length - 5} 个脚本`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
564
705
|
return configJson;
|
|
565
706
|
}
|
|
566
707
|
async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
@@ -574,6 +715,80 @@ async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
|
574
715
|
return startCode;
|
|
575
716
|
}
|
|
576
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* 判断是否为图片文件
|
|
720
|
+
*/
|
|
721
|
+
function isImageFile(filePath) {
|
|
722
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
723
|
+
return ['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 使用 sharp 压缩图片
|
|
727
|
+
* @param buffer 原始图片 buffer
|
|
728
|
+
* @param filePath 文件路径(用于判断格式)
|
|
729
|
+
* @param options 压缩选项
|
|
730
|
+
*/
|
|
731
|
+
async function compressImage(buffer, filePath, options) {
|
|
732
|
+
try {
|
|
733
|
+
// 动态导入 sharp
|
|
734
|
+
const sharp = (await import('sharp')).default;
|
|
735
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
736
|
+
const convertToWebP = options?.convertToWebP !== false;
|
|
737
|
+
let image = sharp(buffer);
|
|
738
|
+
// 获取元数据
|
|
739
|
+
const metadata = await image.metadata();
|
|
740
|
+
if (convertToWebP && ['.png', '.jpg', '.jpeg'].includes(ext)) {
|
|
741
|
+
// 转换为 WebP(节省 60-70% 空间)
|
|
742
|
+
const quality = options?.quality ?? 75;
|
|
743
|
+
const webpBuffer = await image
|
|
744
|
+
.webp({ quality, effort: 6 })
|
|
745
|
+
.toBuffer();
|
|
746
|
+
return {
|
|
747
|
+
buffer: webpBuffer,
|
|
748
|
+
mime: 'image/webp',
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
// 按原格式压缩
|
|
752
|
+
switch (ext) {
|
|
753
|
+
case '.png':
|
|
754
|
+
const pngBuffer = await image
|
|
755
|
+
.png({ quality: 80, compressionLevel: 9 })
|
|
756
|
+
.toBuffer();
|
|
757
|
+
return { buffer: pngBuffer, mime: 'image/png' };
|
|
758
|
+
case '.jpg':
|
|
759
|
+
case '.jpeg':
|
|
760
|
+
const jpgBuffer = await image
|
|
761
|
+
.jpeg({ quality: options?.quality ?? 80, mozjpeg: true })
|
|
762
|
+
.toBuffer();
|
|
763
|
+
return { buffer: jpgBuffer, mime: 'image/jpeg' };
|
|
764
|
+
default:
|
|
765
|
+
// 其他格式不压缩
|
|
766
|
+
return { buffer, mime: guessMimeType(filePath) };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
// 如果 sharp 失败,返回原始 buffer
|
|
771
|
+
console.warn(`警告: sharp 压缩失败,使用原始文件:`, error);
|
|
772
|
+
return { buffer, mime: guessMimeType(filePath) };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 从 Vite 返回的代码中提取 data URL
|
|
777
|
+
*/
|
|
778
|
+
function extractDataUrl(code) {
|
|
779
|
+
// 格式 1: export default "data:..."
|
|
780
|
+
let match = code.match(/export\s+default\s+["']([^"']*data:[^"']+)["']/);
|
|
781
|
+
if (match)
|
|
782
|
+
return match[1];
|
|
783
|
+
// 格式 2: "data:..."
|
|
784
|
+
match = code.match(/["']([^"']*data:[^"']+)["']/);
|
|
785
|
+
if (match)
|
|
786
|
+
return match[1];
|
|
787
|
+
// 格式 3: 整个代码就是 data URL
|
|
788
|
+
if (code.trim().startsWith('data:'))
|
|
789
|
+
return code.trim();
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
577
792
|
function guessMimeType(filePath) {
|
|
578
793
|
const ext = path.extname(filePath).toLowerCase();
|
|
579
794
|
switch (ext) {
|
|
@@ -690,11 +905,9 @@ async function buildCompressedEngineScript(engineCode) {
|
|
|
690
905
|
const base64 = Buffer.from(compressed).toString('base64');
|
|
691
906
|
// 遵循 PlayCanvas 官方 one-page.js 的实现方式
|
|
692
907
|
// 参考: https://github.com/playcanvas/playcanvas-rest-api-tools/blob/main/one-page.js
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
// 4. 插入到当前脚本之前(保持执行顺序)
|
|
697
|
-
const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.innerText=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
|
|
908
|
+
// 官方使用 new Buffer() 因为 lz4.js 提供了 Buffer polyfill
|
|
909
|
+
// 使用 textContent 而不是 innerText 以避免浏览器对内容的处理
|
|
910
|
+
const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.textContent=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
|
|
698
911
|
return `<script>${wrapper}</script>`;
|
|
699
912
|
}
|
|
700
913
|
async function loadLz4Module() {
|
|
@@ -15,6 +15,8 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
15
15
|
let projectFormat = null;
|
|
16
16
|
let projectConfig = null;
|
|
17
17
|
let userScripts = [];
|
|
18
|
+
let scriptPathMap = new Map(); // 模块路径 -> 文件系统路径
|
|
19
|
+
let fileToModuleMap = new Map(); // 文件系统路径 -> 模块路径
|
|
18
20
|
return {
|
|
19
21
|
name: 'vite-plugin-source-builder',
|
|
20
22
|
enforce: 'pre',
|
|
@@ -35,23 +37,46 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
35
37
|
// 3. 收集用户脚本
|
|
36
38
|
userScripts = await collectUserScripts(projectConfig, options.projectDir);
|
|
37
39
|
console.log(`[SourceBuilder] 收集到 ${userScripts.length} 个用户脚本`);
|
|
40
|
+
// 4. 构建脚本路径映射(基于文件夹层次)
|
|
41
|
+
const mappings = buildScriptPathMapping(projectConfig, userScripts, options.projectDir);
|
|
42
|
+
scriptPathMap = mappings.moduleToFile;
|
|
43
|
+
fileToModuleMap = mappings.fileToModule;
|
|
44
|
+
console.log(`[SourceBuilder] 构建了 ${scriptPathMap.size} 个路径映射`);
|
|
38
45
|
},
|
|
39
|
-
//
|
|
40
|
-
resolveId(id) {
|
|
46
|
+
// 处理虚拟入口模块和自定义导入路径
|
|
47
|
+
resolveId(id, importer) {
|
|
41
48
|
if (id === 'virtual:game-scripts') {
|
|
42
49
|
return '\0virtual:game-scripts';
|
|
43
50
|
}
|
|
51
|
+
// 解析自定义导入路径(如 Gameplay/GameplaySystem.mjs)
|
|
52
|
+
if (scriptPathMap.has(id)) {
|
|
53
|
+
return scriptPathMap.get(id);
|
|
54
|
+
}
|
|
55
|
+
// 处理相对导入(../xxx 或 ./xxx)
|
|
56
|
+
if (importer && (id.startsWith('./') || id.startsWith('../'))) {
|
|
57
|
+
// 获取导入者的模块路径
|
|
58
|
+
const importerModulePath = fileToModuleMap.get(importer);
|
|
59
|
+
if (importerModulePath) {
|
|
60
|
+
// 基于模块路径解析相对导入
|
|
61
|
+
const importerDir = path.dirname(importerModulePath);
|
|
62
|
+
const resolvedModulePath = path.posix.join(importerDir, id);
|
|
63
|
+
// 从模块路径映射中查找实际文件
|
|
64
|
+
if (scriptPathMap.has(resolvedModulePath)) {
|
|
65
|
+
return scriptPathMap.get(resolvedModulePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
44
69
|
return null;
|
|
45
70
|
},
|
|
46
71
|
load(id) {
|
|
47
72
|
if (id === '\0virtual:game-scripts') {
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
73
|
+
// 生成入口文件,直接导入所有真实的脚本文件
|
|
74
|
+
// 让 Vite 自然地处理这些文件及其依赖关系
|
|
75
|
+
const imports = userScripts.map(script => {
|
|
76
|
+
const scriptPath = path.join(options.projectDir, script.path);
|
|
77
|
+
return `import '${scriptPath}';`;
|
|
78
|
+
}).join('\n');
|
|
79
|
+
return imports || '// No user scripts';
|
|
55
80
|
}
|
|
56
81
|
return null;
|
|
57
82
|
},
|
|
@@ -61,14 +86,20 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
61
86
|
}
|
|
62
87
|
// 确保输出目录存在
|
|
63
88
|
await fs.mkdir(options.outputDir, { recursive: true });
|
|
64
|
-
// 生成 config.json
|
|
65
|
-
const config = generateConfig(projectConfig
|
|
89
|
+
// 生成 config.json(支持场景过滤)
|
|
90
|
+
const config = await generateConfig(projectConfig, {
|
|
91
|
+
selectedScenes: options.selectedScenes,
|
|
92
|
+
});
|
|
66
93
|
// 为脚本资产补齐 __game-scripts.js 信息
|
|
67
94
|
await patchScriptAssets(config, options.outputDir);
|
|
68
95
|
await fs.writeFile(path.join(options.outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
69
96
|
console.log('[SourceBuilder] 生成 config.json');
|
|
70
97
|
// 生成 __settings__.js
|
|
71
|
-
|
|
98
|
+
// 如果 config.scenes 中有场景,使用第一个场景的 URL
|
|
99
|
+
const firstScenePath = config.scenes && config.scenes.length > 0
|
|
100
|
+
? config.scenes[0].url
|
|
101
|
+
: undefined;
|
|
102
|
+
const settings = generateSettings(projectConfig, firstScenePath);
|
|
72
103
|
await fs.writeFile(path.join(options.outputDir, '__settings__.js'), settings, 'utf-8');
|
|
73
104
|
console.log('[SourceBuilder] 生成 __settings__.js');
|
|
74
105
|
// 复制模板文件
|
|
@@ -77,8 +108,8 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
77
108
|
// 复制资源文件
|
|
78
109
|
await copyAssets(projectConfig, options.projectDir, options.outputDir, config.assets);
|
|
79
110
|
console.log('[SourceBuilder] 复制资源文件');
|
|
80
|
-
//
|
|
81
|
-
await generateSceneFiles(projectConfig, options.projectDir, options.outputDir);
|
|
111
|
+
// 生成场景文件(只生成 config 中包含的场景)
|
|
112
|
+
await generateSceneFiles(projectConfig, options.projectDir, options.outputDir, config.scenes);
|
|
82
113
|
console.log('[SourceBuilder] 生成场景文件');
|
|
83
114
|
},
|
|
84
115
|
};
|
|
@@ -102,6 +133,54 @@ async function detectFormat(projectDir) {
|
|
|
102
133
|
// 默认为 PlayCanvas
|
|
103
134
|
return 'playcanvas';
|
|
104
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* 构建脚本路径映射(基于文件夹层次结构)
|
|
138
|
+
*/
|
|
139
|
+
function buildScriptPathMapping(projectConfig, userScripts, projectDir) {
|
|
140
|
+
const moduleToFile = new Map();
|
|
141
|
+
const fileToModule = new Map();
|
|
142
|
+
const assets = projectConfig.format === 'playcanvas'
|
|
143
|
+
? projectConfig.assets
|
|
144
|
+
: projectConfig.assets;
|
|
145
|
+
// 构建文件夹 ID 到名称的映射
|
|
146
|
+
const folderMap = new Map();
|
|
147
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
148
|
+
const assetData = asset;
|
|
149
|
+
if (assetData.type === 'folder') {
|
|
150
|
+
folderMap.set(assetId, assetData.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 为每个脚本构建模块路径
|
|
154
|
+
for (const script of userScripts) {
|
|
155
|
+
const asset = assets[script.id];
|
|
156
|
+
if (!asset)
|
|
157
|
+
continue;
|
|
158
|
+
const pathIds = asset.path || [];
|
|
159
|
+
const pathNames = [];
|
|
160
|
+
// 跳过根文件夹(第一个元素),构建路径
|
|
161
|
+
for (let i = 1; i < pathIds.length; i++) {
|
|
162
|
+
const folderId = String(pathIds[i]);
|
|
163
|
+
const folderName = folderMap.get(folderId);
|
|
164
|
+
if (folderName) {
|
|
165
|
+
pathNames.push(folderName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// 添加文件名
|
|
169
|
+
const filename = asset.name || asset.file?.filename;
|
|
170
|
+
if (filename) {
|
|
171
|
+
pathNames.push(filename);
|
|
172
|
+
}
|
|
173
|
+
const modulePath = pathNames.join('/');
|
|
174
|
+
const fullPath = path.join(projectDir, script.path);
|
|
175
|
+
// 添加双向映射
|
|
176
|
+
moduleToFile.set(modulePath, fullPath);
|
|
177
|
+
fileToModule.set(fullPath, modulePath);
|
|
178
|
+
// 同时添加不带扩展名的映射
|
|
179
|
+
const withoutExt = modulePath.replace(/\.mjs$/, '');
|
|
180
|
+
moduleToFile.set(withoutExt, fullPath);
|
|
181
|
+
}
|
|
182
|
+
return { moduleToFile, fileToModule };
|
|
183
|
+
}
|
|
105
184
|
/**
|
|
106
185
|
* 收集用户脚本
|
|
107
186
|
*/
|
|
@@ -161,17 +240,6 @@ async function collectUserScripts(projectConfig, projectDir) {
|
|
|
161
240
|
}
|
|
162
241
|
return scripts;
|
|
163
242
|
}
|
|
164
|
-
/**
|
|
165
|
-
* 包装 PlayCanvas 脚本
|
|
166
|
-
*/
|
|
167
|
-
function wrapPlayCanvasScript(code, scriptName, script) {
|
|
168
|
-
// 清理脚本名称(移除特殊字符)
|
|
169
|
-
const cleanName = scriptName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
170
|
-
return `
|
|
171
|
-
var ${cleanName} = pc.createScript('${cleanName}');
|
|
172
|
-
${code}
|
|
173
|
-
`.trim();
|
|
174
|
-
}
|
|
175
243
|
/**
|
|
176
244
|
* 复制模板文件
|
|
177
245
|
*/
|
|
@@ -211,12 +279,18 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
211
279
|
const assets = assetsOverride || (projectConfig.format === 'playcanvas'
|
|
212
280
|
? projectConfig.assets
|
|
213
281
|
: projectConfig.assets);
|
|
282
|
+
// 使用 Set 去重,避免重复复制同一个文件
|
|
283
|
+
const copiedUrls = new Set();
|
|
214
284
|
// 遍历 assets
|
|
215
285
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
216
286
|
const assetData = asset;
|
|
217
287
|
if (!assetData.file?.url || assetData.file.url.startsWith('data:')) {
|
|
218
288
|
continue;
|
|
219
289
|
}
|
|
290
|
+
// 跳过已经复制过的 URL
|
|
291
|
+
if (copiedUrls.has(assetData.file.url)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
220
294
|
const sourcePath = path.join(projectDir, assetData.file.url);
|
|
221
295
|
const targetPath = path.join(outputDir, assetData.file.url);
|
|
222
296
|
try {
|
|
@@ -227,6 +301,8 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
227
301
|
await fs.mkdir(targetDir, { recursive: true });
|
|
228
302
|
// 复制文件
|
|
229
303
|
await fs.copyFile(sourcePath, targetPath);
|
|
304
|
+
// 标记为已复制
|
|
305
|
+
copiedUrls.add(assetData.file.url);
|
|
230
306
|
}
|
|
231
307
|
catch (error) {
|
|
232
308
|
// 资源文件可能不存在(可能是内联的)
|
|
@@ -237,14 +313,23 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
237
313
|
/**
|
|
238
314
|
* 生成场景文件
|
|
239
315
|
*/
|
|
240
|
-
async function generateSceneFiles(projectConfig, projectDir, outputDir) {
|
|
316
|
+
async function generateSceneFiles(projectConfig, projectDir, outputDir, selectedScenes) {
|
|
241
317
|
if (projectConfig.format === 'playcanvas') {
|
|
242
318
|
const pcProject = projectConfig;
|
|
243
319
|
// PlayCanvas 格式:scenes.json 中已包含场景数据
|
|
244
320
|
if (pcProject.scenes) {
|
|
245
|
-
|
|
321
|
+
let scenes = Array.isArray(pcProject.scenes)
|
|
246
322
|
? pcProject.scenes
|
|
247
323
|
: Object.entries(pcProject.scenes).map(([id, scene]) => ({ id, ...scene }));
|
|
324
|
+
// 如果提供了 selectedScenes,只生成这些场景
|
|
325
|
+
if (selectedScenes && selectedScenes.length > 0) {
|
|
326
|
+
const selectedUrls = new Set(selectedScenes.map(s => s.url));
|
|
327
|
+
scenes = scenes.filter((scene) => {
|
|
328
|
+
const sceneId = scene.id || scene.name || 'scene';
|
|
329
|
+
const sceneUrl = `${sceneId}.json`;
|
|
330
|
+
return selectedUrls.has(sceneUrl);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
248
333
|
for (const scene of scenes) {
|
|
249
334
|
const sceneData = scene;
|
|
250
335
|
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
@@ -257,7 +342,16 @@ async function generateSceneFiles(projectConfig, projectDir, outputDir) {
|
|
|
257
342
|
else {
|
|
258
343
|
// PlayCraft 格式:从 scenes/ 目录读取
|
|
259
344
|
const pcProject = projectConfig;
|
|
260
|
-
|
|
345
|
+
let scenes = pcProject.scenes;
|
|
346
|
+
// 如果提供了 selectedScenes,只生成这些场景
|
|
347
|
+
if (selectedScenes && selectedScenes.length > 0) {
|
|
348
|
+
const selectedNames = new Set(selectedScenes.map(s => s.name));
|
|
349
|
+
scenes = scenes.filter((scene) => {
|
|
350
|
+
const sceneName = scene.name || scene.id || 'scene';
|
|
351
|
+
return selectedNames.has(sceneName);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const scene of scenes) {
|
|
261
355
|
const sceneData = scene;
|
|
262
356
|
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
263
357
|
const scenePath = path.join(outputDir, `${sceneId}.json`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/build",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"lightningcss": "^1.27.0",
|
|
39
39
|
"mime-types": "^2.1.35",
|
|
40
40
|
"rollup-plugin-visualizer": "^6.0.5",
|
|
41
|
+
"sharp": "^0.33.0",
|
|
41
42
|
"terser": "^5.30.0",
|
|
42
43
|
"vite": "^6.0.0",
|
|
43
44
|
"vite-plugin-singlefile": "^2.0.0"
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
pc.ScriptHandler.prototype._loadScript = function (url, callback) {
|
|
3
|
+
// 如果 URL 不是 data URL(如 __game-scripts.js),则跳过加载
|
|
4
|
+
// 因为脚本已经在 HTML 中作为 <script> 标签执行过了
|
|
5
|
+
if (!url.startsWith('data:')) {
|
|
6
|
+
// 直接调用回调,不创建新的脚本元素
|
|
7
|
+
callback(null, url, null);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
var head = document.head;
|
|
4
12
|
var element = document.createElement('script');
|
|
5
13
|
this._cache[url] = element;
|