@playcraft/build 0.0.13 → 0.0.15
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 +99 -9
- package/dist/base-builder.d.ts +15 -78
- package/dist/base-builder.js +34 -741
- package/dist/engines/engine-detector.d.ts +38 -0
- package/dist/engines/engine-detector.js +201 -0
- package/dist/engines/generic-adapter.d.ts +71 -0
- package/dist/engines/generic-adapter.js +378 -0
- package/dist/engines/index.d.ts +7 -0
- package/dist/engines/index.js +7 -0
- package/dist/engines/playcanvas-adapter.d.ts +85 -0
- package/dist/engines/playcanvas-adapter.js +813 -0
- package/dist/generators/config-generator.js +59 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/loaders/playcraft-loader.js +240 -5
- package/dist/platforms/adikteev.d.ts +1 -1
- package/dist/platforms/adikteev.js +30 -36
- package/dist/platforms/applovin.d.ts +1 -1
- package/dist/platforms/applovin.js +31 -36
- package/dist/platforms/base.d.ts +27 -5
- package/dist/platforms/base.js +79 -181
- package/dist/platforms/bigo.d.ts +1 -1
- package/dist/platforms/bigo.js +28 -28
- package/dist/platforms/facebook.d.ts +1 -1
- package/dist/platforms/facebook.js +21 -10
- package/dist/platforms/google.d.ts +1 -1
- package/dist/platforms/google.js +28 -21
- package/dist/platforms/index.d.ts +1 -0
- package/dist/platforms/index.js +4 -0
- package/dist/platforms/inmobi.d.ts +1 -1
- package/dist/platforms/inmobi.js +27 -34
- package/dist/platforms/ironsource.d.ts +1 -1
- package/dist/platforms/ironsource.js +37 -40
- package/dist/platforms/liftoff.d.ts +1 -1
- package/dist/platforms/liftoff.js +22 -30
- package/dist/platforms/mintegral.d.ts +10 -0
- package/dist/platforms/mintegral.js +65 -0
- package/dist/platforms/moloco.d.ts +1 -1
- package/dist/platforms/moloco.js +18 -20
- package/dist/platforms/playcraft.d.ts +1 -1
- package/dist/platforms/playcraft.js +2 -2
- package/dist/platforms/remerge.d.ts +1 -1
- package/dist/platforms/remerge.js +19 -20
- package/dist/platforms/snapchat.d.ts +1 -1
- package/dist/platforms/snapchat.js +32 -26
- package/dist/platforms/tiktok.d.ts +1 -1
- package/dist/platforms/tiktok.js +28 -24
- package/dist/platforms/unity.d.ts +1 -1
- package/dist/platforms/unity.js +30 -36
- package/dist/playable-builder.d.ts +1 -0
- package/dist/playable-builder.js +16 -2
- package/dist/types.d.ts +113 -1
- package/dist/types.js +77 -1
- package/dist/utils/ammo-detector.d.ts +9 -0
- package/dist/utils/ammo-detector.js +76 -0
- package/dist/utils/build-mode-detector.js +2 -0
- package/dist/utils/minify.d.ts +32 -0
- package/dist/utils/minify.js +82 -0
- package/dist/utils/obfuscate.d.ts +42 -0
- package/dist/utils/obfuscate.js +216 -0
- package/dist/vite/config-builder-generic.d.ts +70 -0
- package/dist/vite/config-builder-generic.js +251 -0
- package/dist/vite/config-builder.d.ts +8 -0
- package/dist/vite/config-builder.js +53 -16
- package/dist/vite/platform-configs.js +29 -1
- package/dist/vite/plugin-compress-js.d.ts +21 -0
- package/dist/vite/plugin-compress-js.js +213 -0
- package/dist/vite/plugin-esm-html-generator.js +5 -1
- package/dist/vite/plugin-obfuscate.d.ts +22 -0
- package/dist/vite/plugin-obfuscate.js +52 -0
- package/dist/vite/plugin-platform.d.ts +5 -0
- package/dist/vite/plugin-platform.js +499 -35
- package/dist/vite/plugin-playcanvas.js +21 -68
- package/dist/vite/plugin-source-builder.js +102 -21
- package/dist/vite-builder.d.ts +25 -7
- package/dist/vite-builder.js +141 -52
- package/package.json +4 -2
- package/physics/cannon-rigidbody-adapter.js +243 -22
- package/templates/__loading__.js +0 -12
- package/templates/index.esm.mjs +0 -11
- package/templates/patches/playcraft-cta-adapter.js +129 -31
- package/templates/patches/scene-physics-defaults.js +49 -0
package/dist/base-builder.js
CHANGED
|
@@ -1,765 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { build as viteBuild } from 'vite';
|
|
5
|
-
import { visualizer } from 'rollup-plugin-visualizer';
|
|
6
|
-
import { viteSourceBuilderPlugin } from './vite/plugin-source-builder.js';
|
|
7
|
-
import { loadPlayCanvasProject } from './loaders/playcanvas-loader.js';
|
|
8
|
-
import { BuildAnalyzer } from './analyzers/build-analyzer.js';
|
|
9
|
-
import { BuildStateManager } from './state/index.js';
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
1
|
+
import { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
|
|
12
2
|
/**
|
|
13
|
-
* 基础构建器 -
|
|
3
|
+
* 基础构建器 - 路由到不同引擎适配器
|
|
14
4
|
*
|
|
15
5
|
* 职责:
|
|
16
|
-
* 1.
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
6
|
+
* 1. 确定引擎类型(从 options 或自动检测)
|
|
7
|
+
* 2. 路由到对应适配器执行 Base Build
|
|
8
|
+
*
|
|
9
|
+
* 适配器:
|
|
10
|
+
* - PlayCanvasAdapter:处理 PlayCanvas 项目(官方构建产物或源代码)
|
|
11
|
+
* - GenericAdapter:处理外部引擎(npm install + npm run build)
|
|
20
12
|
*/
|
|
21
13
|
export class BaseBuilder {
|
|
22
14
|
constructor(projectDir, options) {
|
|
23
15
|
this.projectDir = projectDir;
|
|
24
16
|
this.options = options;
|
|
25
|
-
this.stateManager = new BuildStateManager(projectDir, options.outputDir);
|
|
26
17
|
console.log(`[BaseBuilder] 初始化: projectDir=${projectDir}, outputDir=${options.outputDir}`);
|
|
27
18
|
}
|
|
28
19
|
/**
|
|
29
20
|
* 执行基础构建
|
|
21
|
+
* 根据引擎类型路由到对应适配器
|
|
30
22
|
*/
|
|
31
23
|
async build() {
|
|
32
|
-
//
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const projectType = await this.detectProjectType();
|
|
40
|
-
let result;
|
|
41
|
-
if (projectType === 'official-build') {
|
|
42
|
-
// 官方构建产物 - 直接复制并验证
|
|
43
|
-
result = await this.buildFromOfficial();
|
|
24
|
+
// 1. 确定引擎类型
|
|
25
|
+
const engine = await this.resolveEngine();
|
|
26
|
+
console.log(`[BaseBuilder] 使用引擎: ${engine}`);
|
|
27
|
+
// 2. 路由到对应适配器
|
|
28
|
+
if (engine === 'playcanvas') {
|
|
29
|
+
const adapter = new PlayCanvasAdapter(this.projectDir, this.options);
|
|
30
|
+
return adapter.baseBuild();
|
|
44
31
|
}
|
|
45
32
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(`[BaseBuilder] 检测到 ${formatName} 源代码格式,使用 Vite 构建...`);
|
|
49
|
-
result = await this.buildFromSource();
|
|
50
|
-
}
|
|
51
|
-
// 保存构建元数据
|
|
52
|
-
await this.saveBuildMetadata(result.metadata);
|
|
53
|
-
// 记录构建产物
|
|
54
|
-
await this.recordBuildOutput(result);
|
|
55
|
-
// 结束构建阶段
|
|
56
|
-
this.stateManager.endStage();
|
|
57
|
-
// 生成分析报告(如果启用)
|
|
58
|
-
if (this.options.analyze) {
|
|
59
|
-
// 只在分析模式下保存状态文件(分析报告需要用到)
|
|
60
|
-
await this.stateManager.saveState();
|
|
61
|
-
await this.generateAnalysisReport();
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* 保存构建元数据到输出目录
|
|
67
|
-
*/
|
|
68
|
-
async saveBuildMetadata(metadata) {
|
|
69
|
-
const metadataPath = path.join(this.options.outputDir, '.build-metadata.json');
|
|
70
|
-
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
71
|
-
console.log('[BaseBuilder] 构建元数据已保存');
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* 检测项目类型
|
|
75
|
-
*/
|
|
76
|
-
async detectProjectType() {
|
|
77
|
-
// 1. 检查是否是官方构建产物
|
|
78
|
-
const buildIndicators = [
|
|
79
|
-
path.join(this.projectDir, 'index.html'),
|
|
80
|
-
path.join(this.projectDir, 'config.json'),
|
|
81
|
-
];
|
|
82
|
-
try {
|
|
83
|
-
await fs.access(buildIndicators[0]);
|
|
84
|
-
await fs.access(buildIndicators[1]);
|
|
85
|
-
return 'official-build';
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
// 2. 检查是否是 PlayCraft 项目格式
|
|
89
|
-
const manifestPath = path.join(this.projectDir, 'manifest.json');
|
|
90
|
-
try {
|
|
91
|
-
await fs.access(manifestPath);
|
|
92
|
-
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
93
|
-
const manifest = JSON.parse(manifestContent);
|
|
94
|
-
if (manifest.format === 'playcraft' || Array.isArray(manifest.assets)) {
|
|
95
|
-
return 'playcraft';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
// manifest.json 不存在或格式不对
|
|
100
|
-
}
|
|
101
|
-
// 3. 默认视为源代码项目
|
|
102
|
-
return 'source';
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* 从官方构建产物构建
|
|
107
|
-
*/
|
|
108
|
-
async buildFromOfficial() {
|
|
109
|
-
// 验证必需文件存在
|
|
110
|
-
await this.validateOfficialBuild();
|
|
111
|
-
// 创建输出目录
|
|
112
|
-
await fs.mkdir(this.options.outputDir, { recursive: true });
|
|
113
|
-
// 复制所有文件到输出目录
|
|
114
|
-
const files = await this.copyBuildFiles();
|
|
115
|
-
// 检测构建模式(官方构建产物可能是 Classic 或 ESM)
|
|
116
|
-
const metadata = await this.detectBuildModeFromOutput();
|
|
117
|
-
return {
|
|
118
|
-
outputDir: this.options.outputDir,
|
|
119
|
-
metadata,
|
|
120
|
-
files,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* 从输出目录检测构建模式
|
|
125
|
-
*/
|
|
126
|
-
async detectBuildModeFromOutput() {
|
|
127
|
-
const indexPath = path.join(this.options.outputDir, 'index.html');
|
|
128
|
-
try {
|
|
129
|
-
const html = await fs.readFile(indexPath, 'utf-8');
|
|
130
|
-
const hasImportMap = html.includes('<script type="importmap">');
|
|
131
|
-
if (hasImportMap) {
|
|
132
|
-
const importMapMatch = html.match(/<script type="importmap">\s*(\{[\s\S]*?\})\s*<\/script>/);
|
|
133
|
-
const importMapContent = importMapMatch ? JSON.parse(importMapMatch[1]) : { imports: {} };
|
|
134
|
-
return {
|
|
135
|
-
mode: 'esm',
|
|
136
|
-
importMap: {
|
|
137
|
-
id: 'detected',
|
|
138
|
-
imports: importMapContent.imports || {},
|
|
139
|
-
},
|
|
140
|
-
entryPoint: 'js/index.mjs',
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
// 忽略错误,返回 Classic 模式
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
mode: 'classic',
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* 检测是否为 ESM 格式项目
|
|
153
|
-
*/
|
|
154
|
-
async detectESMFormat() {
|
|
155
|
-
// ESM 格式的特征:存在 js/index.mjs 或 esm-scripts 目录
|
|
156
|
-
const esmIndicators = [
|
|
157
|
-
path.join(this.projectDir, 'js/index.mjs'),
|
|
158
|
-
path.join(this.projectDir, 'esm-scripts'),
|
|
159
|
-
];
|
|
160
|
-
for (const indicator of esmIndicators) {
|
|
161
|
-
try {
|
|
162
|
-
await fs.access(indicator);
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
// 继续检查下一个
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* 验证官方构建产物
|
|
173
|
-
*/
|
|
174
|
-
async validateOfficialBuild() {
|
|
175
|
-
// 检测是否为 ESM 格式
|
|
176
|
-
const isESM = await this.detectESMFormat();
|
|
177
|
-
// ESM 格式不需要 __start__.js,Classic 格式需要
|
|
178
|
-
const requiredFiles = isESM
|
|
179
|
-
? ['index.html', 'config.json']
|
|
180
|
-
: ['index.html', 'config.json', '__start__.js'];
|
|
181
|
-
const missingFiles = [];
|
|
182
|
-
for (const file of requiredFiles) {
|
|
183
|
-
try {
|
|
184
|
-
await fs.access(path.join(this.projectDir, file));
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
missingFiles.push(file);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (missingFiles.length > 0) {
|
|
191
|
-
const formatType = isESM ? 'ESM' : 'Classic';
|
|
192
|
-
throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
193
|
-
`检测到 ${formatType} 格式,请确保项目目录包含完整的构建产物。`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* 复制构建文件到输出目录
|
|
198
|
-
*/
|
|
199
|
-
async copyBuildFiles() {
|
|
200
|
-
const files = {
|
|
201
|
-
html: '',
|
|
202
|
-
engine: null,
|
|
203
|
-
config: '',
|
|
204
|
-
settings: null,
|
|
205
|
-
modules: null,
|
|
206
|
-
start: '',
|
|
207
|
-
scenes: [],
|
|
208
|
-
assets: [],
|
|
209
|
-
};
|
|
210
|
-
// 复制 index.html
|
|
211
|
-
const htmlPath = path.join(this.projectDir, 'index.html');
|
|
212
|
-
const outputHtmlPath = path.join(this.options.outputDir, 'index.html');
|
|
213
|
-
await fs.copyFile(htmlPath, outputHtmlPath);
|
|
214
|
-
files.html = outputHtmlPath;
|
|
215
|
-
// 复制 config.json
|
|
216
|
-
const configPath = path.join(this.projectDir, 'config.json');
|
|
217
|
-
const outputConfigPath = path.join(this.options.outputDir, 'config.json');
|
|
218
|
-
await fs.copyFile(configPath, outputConfigPath);
|
|
219
|
-
files.config = outputConfigPath;
|
|
220
|
-
// 读取 config.json 以获取场景信息
|
|
221
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
222
|
-
const configJson = JSON.parse(configContent);
|
|
223
|
-
// 检测是否为 ESM 格式
|
|
224
|
-
const isESM = await this.detectESMFormat();
|
|
225
|
-
// 复制 __start__.js(Classic 格式必需,ESM 格式不需要)
|
|
226
|
-
if (!isESM) {
|
|
227
|
-
const startPath = path.join(this.projectDir, '__start__.js');
|
|
228
|
-
const outputStartPath = path.join(this.options.outputDir, '__start__.js');
|
|
229
|
-
try {
|
|
230
|
-
await fs.copyFile(startPath, outputStartPath);
|
|
231
|
-
files.start = outputStartPath;
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
console.warn('警告: 无法复制 __start__.js');
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// 复制 __settings__.js(如果存在)
|
|
238
|
-
const settingsPath = path.join(this.projectDir, '__settings__.js');
|
|
239
|
-
try {
|
|
240
|
-
await fs.access(settingsPath);
|
|
241
|
-
const outputSettingsPath = path.join(this.options.outputDir, '__settings__.js');
|
|
242
|
-
await fs.copyFile(settingsPath, outputSettingsPath);
|
|
243
|
-
files.settings = outputSettingsPath;
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
// __settings__.js 不是必需的
|
|
247
|
-
}
|
|
248
|
-
// 复制 __modules__.js(如果存在)
|
|
249
|
-
const modulesPath = path.join(this.projectDir, '__modules__.js');
|
|
250
|
-
try {
|
|
251
|
-
await fs.access(modulesPath);
|
|
252
|
-
const outputModulesPath = path.join(this.options.outputDir, '__modules__.js');
|
|
253
|
-
await fs.copyFile(modulesPath, outputModulesPath);
|
|
254
|
-
files.modules = outputModulesPath;
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
// __modules__.js 不是必需的
|
|
258
|
-
}
|
|
259
|
-
// 复制 PlayCanvas Engine(查找可能的文件名)
|
|
260
|
-
const engineNames = [
|
|
261
|
-
'playcanvas-stable.min.js',
|
|
262
|
-
'playcanvas.min.js',
|
|
263
|
-
'__lib__.js',
|
|
264
|
-
];
|
|
265
|
-
for (const engineName of engineNames) {
|
|
266
|
-
const enginePath = path.join(this.projectDir, engineName);
|
|
267
|
-
try {
|
|
268
|
-
await fs.access(enginePath);
|
|
269
|
-
const outputEnginePath = path.join(this.options.outputDir, engineName);
|
|
270
|
-
await fs.copyFile(enginePath, outputEnginePath);
|
|
271
|
-
files.engine = outputEnginePath;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
// 继续尝试下一个
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// 复制场景文件
|
|
279
|
-
if (configJson.scenes && Array.isArray(configJson.scenes)) {
|
|
280
|
-
for (const scene of configJson.scenes) {
|
|
281
|
-
if (scene.url && !scene.url.startsWith('data:')) {
|
|
282
|
-
const scenePath = path.join(this.projectDir, scene.url);
|
|
283
|
-
try {
|
|
284
|
-
await fs.access(scenePath);
|
|
285
|
-
const sceneDir = path.dirname(scene.url);
|
|
286
|
-
if (sceneDir && sceneDir !== '.') {
|
|
287
|
-
const outputSceneDir = path.join(this.options.outputDir, sceneDir);
|
|
288
|
-
await fs.mkdir(outputSceneDir, { recursive: true });
|
|
289
|
-
}
|
|
290
|
-
const outputScenePath = path.join(this.options.outputDir, scene.url);
|
|
291
|
-
await fs.copyFile(scenePath, outputScenePath);
|
|
292
|
-
files.scenes.push(outputScenePath);
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
console.warn(`警告: 场景文件不存在: ${scene.url}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// 复制资产文件(从 config.json 中的 assets)
|
|
301
|
-
if (configJson.assets) {
|
|
302
|
-
const assetsDir = path.join(this.options.outputDir, 'files');
|
|
303
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
304
|
-
for (const [assetId, assetData] of Object.entries(configJson.assets)) {
|
|
305
|
-
const asset = assetData;
|
|
306
|
-
if (asset.file && asset.file.url && !asset.file.url.startsWith('data:')) {
|
|
307
|
-
const assetPath = path.join(this.projectDir, asset.file.url);
|
|
308
|
-
try {
|
|
309
|
-
await fs.access(assetPath);
|
|
310
|
-
const assetDir = path.dirname(asset.file.url);
|
|
311
|
-
if (assetDir && assetDir !== '.') {
|
|
312
|
-
const outputAssetDir = path.join(this.options.outputDir, assetDir);
|
|
313
|
-
await fs.mkdir(outputAssetDir, { recursive: true });
|
|
314
|
-
}
|
|
315
|
-
const outputAssetPath = path.join(this.options.outputDir, asset.file.url);
|
|
316
|
-
await fs.copyFile(assetPath, outputAssetPath);
|
|
317
|
-
files.assets.push(outputAssetPath);
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
// 资产文件可能不存在(可能是内联的)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// 复制 files/ 目录(如果存在)
|
|
326
|
-
const filesDir = path.join(this.projectDir, 'files');
|
|
327
|
-
try {
|
|
328
|
-
const filesDirStat = await fs.stat(filesDir);
|
|
329
|
-
if (filesDirStat.isDirectory()) {
|
|
330
|
-
const outputFilesDir = path.join(this.options.outputDir, 'files');
|
|
331
|
-
await this.copyDirectory(filesDir, outputFilesDir);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
// files/ 目录可能不存在
|
|
336
|
-
}
|
|
337
|
-
// 复制 styles.css(如果存在)
|
|
338
|
-
const stylesPath = path.join(this.projectDir, 'styles.css');
|
|
339
|
-
try {
|
|
340
|
-
await fs.access(stylesPath);
|
|
341
|
-
const outputStylesPath = path.join(this.options.outputDir, 'styles.css');
|
|
342
|
-
await fs.copyFile(stylesPath, outputStylesPath);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
// styles.css 可能不存在
|
|
346
|
-
}
|
|
347
|
-
// 复制 manifest.json(如果存在)
|
|
348
|
-
const manifestPath = path.join(this.projectDir, 'manifest.json');
|
|
349
|
-
try {
|
|
350
|
-
await fs.access(manifestPath);
|
|
351
|
-
const outputManifestPath = path.join(this.options.outputDir, 'manifest.json');
|
|
352
|
-
await fs.copyFile(manifestPath, outputManifestPath);
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
355
|
-
// manifest.json 可能不存在
|
|
356
|
-
}
|
|
357
|
-
// ESM 格式:复制 js/ 目录(如果存在)
|
|
358
|
-
const jsDir = path.join(this.projectDir, 'js');
|
|
359
|
-
try {
|
|
360
|
-
const jsDirStat = await fs.stat(jsDir);
|
|
361
|
-
if (jsDirStat.isDirectory()) {
|
|
362
|
-
const outputJsDir = path.join(this.options.outputDir, 'js');
|
|
363
|
-
await this.copyDirectory(jsDir, outputJsDir);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
// js/ 目录可能不存在
|
|
368
|
-
}
|
|
369
|
-
// ESM 格式:复制 esm-scripts/ 目录(如果存在)
|
|
370
|
-
const esmScriptsDir = path.join(this.projectDir, 'esm-scripts');
|
|
371
|
-
try {
|
|
372
|
-
const esmScriptsDirStat = await fs.stat(esmScriptsDir);
|
|
373
|
-
if (esmScriptsDirStat.isDirectory()) {
|
|
374
|
-
const outputEsmScriptsDir = path.join(this.options.outputDir, 'esm-scripts');
|
|
375
|
-
await this.copyDirectory(esmScriptsDir, outputEsmScriptsDir);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
// esm-scripts/ 目录可能不存在
|
|
380
|
-
}
|
|
381
|
-
return files;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* 递归复制目录
|
|
385
|
-
*/
|
|
386
|
-
async copyDirectory(src, dest) {
|
|
387
|
-
await fs.mkdir(dest, { recursive: true });
|
|
388
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
389
|
-
for (const entry of entries) {
|
|
390
|
-
const srcPath = path.join(src, entry.name);
|
|
391
|
-
const destPath = path.join(dest, entry.name);
|
|
392
|
-
if (entry.isDirectory()) {
|
|
393
|
-
await this.copyDirectory(srcPath, destPath);
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
await fs.copyFile(srcPath, destPath);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* 从源代码构建(使用 Vite)
|
|
402
|
-
*/
|
|
403
|
-
async buildFromSource() {
|
|
404
|
-
console.log('[BaseBuilder] 使用 Vite 从源代码构建...');
|
|
405
|
-
console.log('[BaseBuilder] ====== 开始检测构建模式 ======');
|
|
406
|
-
// 0. 检测构建模式(Import Map)
|
|
407
|
-
let buildMode = 'classic';
|
|
408
|
-
let importMap = undefined;
|
|
409
|
-
try {
|
|
410
|
-
console.log('[BaseBuilder] 正在加载项目配置...');
|
|
411
|
-
const projectConfig = await loadPlayCanvasProject(this.projectDir);
|
|
412
|
-
console.log('[BaseBuilder] 项目配置加载成功');
|
|
413
|
-
console.log('[BaseBuilder] Import Map:', projectConfig.importMap ? 'YES' : 'NO');
|
|
414
|
-
buildMode = projectConfig.importMap ? 'esm' : 'classic';
|
|
415
|
-
importMap = projectConfig.importMap;
|
|
416
|
-
console.log(`[BaseBuilder] ====== 构建模式: ${buildMode} ======`);
|
|
417
|
-
if (importMap) {
|
|
418
|
-
console.log(`[BaseBuilder] Import Map ID: ${importMap.id}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
console.error('[BaseBuilder] 加载配置出错:', error.message);
|
|
423
|
-
console.log('[BaseBuilder] 无法加载 PlayCanvas 项目配置,使用传统模式');
|
|
424
|
-
}
|
|
425
|
-
// 1. 创建 Vite 配置
|
|
426
|
-
// 默认使用覆盖模式(不清空输出目录),只有显式设置 clean: true 时才清空
|
|
427
|
-
const shouldEmptyOutDir = this.options.clean === true;
|
|
428
|
-
const viteConfig = buildMode === 'esm' ? {
|
|
429
|
-
// ESM 模式:不需要打包用户脚本,只需触发插件复制文件
|
|
430
|
-
root: this.projectDir,
|
|
431
|
-
base: './',
|
|
432
|
-
build: {
|
|
433
|
-
outDir: this.options.outputDir,
|
|
434
|
-
emptyOutDir: shouldEmptyOutDir,
|
|
435
|
-
// 如果启用了分析报告,需要 write: true 以便 visualizer 插件能够生成报告
|
|
436
|
-
write: this.options.analyze ? true : false,
|
|
437
|
-
rollupOptions: {
|
|
438
|
-
input: {
|
|
439
|
-
// 使用虚拟入口,仅用于触发插件
|
|
440
|
-
'esm-entry': 'virtual:esm-entry',
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
plugins: [
|
|
445
|
-
// 虚拟入口插件
|
|
446
|
-
{
|
|
447
|
-
name: 'virtual-esm-entry',
|
|
448
|
-
resolveId(id) {
|
|
449
|
-
if (id === 'virtual:esm-entry') {
|
|
450
|
-
return '\0virtual:esm-entry';
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
453
|
-
},
|
|
454
|
-
load(id) {
|
|
455
|
-
if (id === '\0virtual:esm-entry') {
|
|
456
|
-
return '// ESM mode - no bundling needed';
|
|
457
|
-
}
|
|
458
|
-
return null;
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
// 源代码构建插件
|
|
462
|
-
viteSourceBuilderPlugin({
|
|
463
|
-
projectDir: this.projectDir,
|
|
464
|
-
outputDir: this.options.outputDir,
|
|
465
|
-
selectedScenes: this.options.selectedScenes,
|
|
466
|
-
buildMode: buildMode,
|
|
467
|
-
importMap: importMap,
|
|
468
|
-
}),
|
|
469
|
-
// 打包分析报告(如果启用)
|
|
470
|
-
...(this.options.analyze ? [
|
|
471
|
-
visualizer({
|
|
472
|
-
filename: this.options.analyzeReportPath
|
|
473
|
-
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
474
|
-
: path.join(this.options.outputDir, 'base-bundle-report.html'),
|
|
475
|
-
template: 'treemap',
|
|
476
|
-
gzipSize: true,
|
|
477
|
-
brotliSize: true,
|
|
478
|
-
open: false,
|
|
479
|
-
sourcemap: false,
|
|
480
|
-
}),
|
|
481
|
-
] : []),
|
|
482
|
-
],
|
|
483
|
-
} : {
|
|
484
|
-
// 传统模式:打包用户脚本
|
|
485
|
-
root: this.projectDir,
|
|
486
|
-
base: './',
|
|
487
|
-
build: {
|
|
488
|
-
outDir: this.options.outputDir,
|
|
489
|
-
emptyOutDir: shouldEmptyOutDir,
|
|
490
|
-
// 多文件输出(不内联资源)
|
|
491
|
-
assetsInlineLimit: 0,
|
|
492
|
-
// Base Build 不压缩(保持可读性和调试性)
|
|
493
|
-
minify: false,
|
|
494
|
-
cssMinify: false,
|
|
495
|
-
sourcemap: false,
|
|
496
|
-
rollupOptions: {
|
|
497
|
-
input: {
|
|
498
|
-
// 虚拟模块:用户脚本入口
|
|
499
|
-
'game-scripts': 'virtual:game-scripts',
|
|
500
|
-
},
|
|
501
|
-
output: {
|
|
502
|
-
entryFileNames: '__[name].js',
|
|
503
|
-
chunkFileNames: '__[name]-[hash].js',
|
|
504
|
-
assetFileNames: 'files/assets/[name].[ext]',
|
|
505
|
-
format: 'iife', // 使用 IIFE 格式,避免 ES 模块的 import 语句
|
|
506
|
-
globals: {
|
|
507
|
-
'playcanvas': 'pc',
|
|
508
|
-
'pc': 'pc',
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
// 外部化 PlayCanvas Engine(不打包)
|
|
512
|
-
external: ['pc', 'playcanvas'],
|
|
513
|
-
// 仅对用户脚本模块启用 tree-shake(引擎已外部化)
|
|
514
|
-
treeshake: true,
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
plugins: [
|
|
518
|
-
// 源代码构建插件
|
|
519
|
-
viteSourceBuilderPlugin({
|
|
520
|
-
projectDir: this.projectDir,
|
|
521
|
-
outputDir: this.options.outputDir,
|
|
522
|
-
selectedScenes: this.options.selectedScenes,
|
|
523
|
-
buildMode: buildMode,
|
|
524
|
-
importMap: importMap,
|
|
525
|
-
}),
|
|
526
|
-
// 打包分析报告(如果启用)
|
|
527
|
-
...(this.options.analyze ? [
|
|
528
|
-
visualizer({
|
|
529
|
-
filename: this.options.analyzeReportPath
|
|
530
|
-
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
531
|
-
: path.join(this.options.outputDir, 'base-bundle-report.html'),
|
|
532
|
-
template: 'treemap',
|
|
533
|
-
gzipSize: true,
|
|
534
|
-
brotliSize: true,
|
|
535
|
-
open: false,
|
|
536
|
-
}),
|
|
537
|
-
] : []),
|
|
538
|
-
],
|
|
539
|
-
};
|
|
540
|
-
// DEBUG: 检查 analyze 配置
|
|
541
|
-
if (this.options.analyze) {
|
|
542
|
-
console.log('[BaseBuilder] analyze 已启用,报告路径:', this.options.analyzeReportPath
|
|
543
|
-
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
544
|
-
: path.join(this.options.outputDir, 'base-bundle-report.html'));
|
|
545
|
-
}
|
|
546
|
-
// 2. 执行 Vite 构建
|
|
547
|
-
await viteBuild(viteConfig);
|
|
548
|
-
// 3. 扫描生成的文件
|
|
549
|
-
const files = await this.scanOutputFiles();
|
|
550
|
-
// 4. 构建元数据
|
|
551
|
-
const metadata = {
|
|
552
|
-
mode: buildMode,
|
|
553
|
-
importMap: importMap ? {
|
|
554
|
-
id: importMap.id,
|
|
555
|
-
imports: importMap.content.imports,
|
|
556
|
-
} : undefined,
|
|
557
|
-
entryPoint: buildMode === 'esm' ? 'js/index.mjs' : undefined,
|
|
558
|
-
};
|
|
559
|
-
// 5. 返回构建结果
|
|
560
|
-
return {
|
|
561
|
-
outputDir: this.options.outputDir,
|
|
562
|
-
metadata,
|
|
563
|
-
files,
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* 扫描输出文件
|
|
568
|
-
*/
|
|
569
|
-
async scanOutputFiles() {
|
|
570
|
-
const files = {
|
|
571
|
-
html: path.join(this.options.outputDir, 'index.html'),
|
|
572
|
-
engine: null,
|
|
573
|
-
config: path.join(this.options.outputDir, 'config.json'),
|
|
574
|
-
settings: path.join(this.options.outputDir, '__settings__.js'),
|
|
575
|
-
modules: null,
|
|
576
|
-
start: path.join(this.options.outputDir, '__start__.js'),
|
|
577
|
-
scenes: [],
|
|
578
|
-
assets: [],
|
|
579
|
-
};
|
|
580
|
-
// 检查 engine
|
|
581
|
-
const enginePath = path.join(this.options.outputDir, 'playcanvas-stable.min.js');
|
|
582
|
-
try {
|
|
583
|
-
await fs.access(enginePath);
|
|
584
|
-
files.engine = enginePath;
|
|
585
|
-
}
|
|
586
|
-
catch (error) {
|
|
587
|
-
// engine 可能不存在
|
|
588
|
-
}
|
|
589
|
-
// 检查 modules
|
|
590
|
-
const modulesPath = path.join(this.options.outputDir, '__modules__.js');
|
|
591
|
-
try {
|
|
592
|
-
await fs.access(modulesPath);
|
|
593
|
-
files.modules = modulesPath;
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
// modules 可能不存在
|
|
597
|
-
}
|
|
598
|
-
// 读取 config.json 以获取场景信息
|
|
599
|
-
try {
|
|
600
|
-
const configContent = await fs.readFile(files.config, 'utf-8');
|
|
601
|
-
const configJson = JSON.parse(configContent);
|
|
602
|
-
// 扫描场景文件
|
|
603
|
-
if (configJson.scenes && Array.isArray(configJson.scenes)) {
|
|
604
|
-
for (const scene of configJson.scenes) {
|
|
605
|
-
if (scene.url && !scene.url.startsWith('data:')) {
|
|
606
|
-
const scenePath = path.join(this.options.outputDir, scene.url);
|
|
607
|
-
try {
|
|
608
|
-
await fs.access(scenePath);
|
|
609
|
-
files.scenes.push(scenePath);
|
|
610
|
-
}
|
|
611
|
-
catch (error) {
|
|
612
|
-
// 场景文件可能不存在
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
// 扫描资产文件(从 files/ 目录)
|
|
618
|
-
const filesDir = path.join(this.options.outputDir, 'files');
|
|
619
|
-
try {
|
|
620
|
-
const filesDirStat = await fs.stat(filesDir);
|
|
621
|
-
if (filesDirStat.isDirectory()) {
|
|
622
|
-
await this.scanDirectory(filesDir, files.assets);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
catch (error) {
|
|
626
|
-
// files/ 目录可能不存在
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
console.warn('警告: 无法读取 config.json:', error);
|
|
631
|
-
}
|
|
632
|
-
return files;
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* 递归扫描目录
|
|
636
|
-
*/
|
|
637
|
-
async scanDirectory(dir, files) {
|
|
638
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
639
|
-
for (const entry of entries) {
|
|
640
|
-
const fullPath = path.join(dir, entry.name);
|
|
641
|
-
if (entry.isDirectory()) {
|
|
642
|
-
await this.scanDirectory(fullPath, files);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
files.push(fullPath);
|
|
646
|
-
}
|
|
33
|
+
const adapter = new GenericAdapter(this.projectDir, this.options, engine);
|
|
34
|
+
return adapter.baseBuild();
|
|
647
35
|
}
|
|
648
36
|
}
|
|
649
37
|
/**
|
|
650
|
-
*
|
|
38
|
+
* 解析引擎类型
|
|
39
|
+
* 优先级:
|
|
40
|
+
* 1. options.engine(用户显式指定)
|
|
41
|
+
* 2. 自动检测
|
|
651
42
|
*/
|
|
652
|
-
async
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
this.stateManager.updateAsset('index.html', 'base-build', 'index.html', 'index.html', stat.size);
|
|
43
|
+
async resolveEngine() {
|
|
44
|
+
// 1. 用户显式指定
|
|
45
|
+
if (this.options.engine) {
|
|
46
|
+
const validated = EngineDetector.validateEngine(this.options.engine);
|
|
47
|
+
if (validated) {
|
|
48
|
+
console.log(`[BaseBuilder] 使用用户指定的引擎: ${validated}`);
|
|
49
|
+
return validated;
|
|
660
50
|
}
|
|
661
|
-
|
|
662
|
-
if (result.files.engine) {
|
|
663
|
-
const stat = await fs.stat(result.files.engine);
|
|
664
|
-
const engineName = path.basename(result.files.engine);
|
|
665
|
-
this.stateManager.recordAsset('playcanvas-engine', engineName, result.files.engine, stat.size, 'script');
|
|
666
|
-
this.stateManager.updateAsset('playcanvas-engine', 'base-build', engineName, engineName, stat.size);
|
|
667
|
-
}
|
|
668
|
-
// 记录 config.json
|
|
669
|
-
if (result.files.config) {
|
|
670
|
-
const stat = await fs.stat(result.files.config);
|
|
671
|
-
this.stateManager.recordAsset('config.json', 'config.json', result.files.config, stat.size, 'json');
|
|
672
|
-
this.stateManager.updateAsset('config.json', 'base-build', 'config.json', 'config.json', stat.size);
|
|
673
|
-
}
|
|
674
|
-
// 记录其他脚本文件
|
|
675
|
-
const scriptFiles = [
|
|
676
|
-
{ path: result.files.start, id: '__start__.js' },
|
|
677
|
-
{ path: result.files.settings, id: '__settings__.js' },
|
|
678
|
-
{ path: result.files.modules, id: '__modules__.js' },
|
|
679
|
-
];
|
|
680
|
-
for (const { path: filePath, id } of scriptFiles) {
|
|
681
|
-
if (filePath) {
|
|
682
|
-
try {
|
|
683
|
-
const stat = await fs.stat(filePath);
|
|
684
|
-
const fileName = path.basename(filePath);
|
|
685
|
-
this.stateManager.recordAsset(id, fileName, filePath, stat.size, 'script');
|
|
686
|
-
this.stateManager.updateAsset(id, 'base-build', fileName, fileName, stat.size);
|
|
687
|
-
}
|
|
688
|
-
catch (error) {
|
|
689
|
-
// 文件可能不存在
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
// 记录场景文件
|
|
694
|
-
for (const scenePath of result.files.scenes) {
|
|
695
|
-
try {
|
|
696
|
-
const stat = await fs.stat(scenePath);
|
|
697
|
-
const sceneName = path.basename(scenePath);
|
|
698
|
-
const sceneId = `scene-${sceneName}`;
|
|
699
|
-
this.stateManager.recordAsset(sceneId, sceneName, scenePath, stat.size, 'json');
|
|
700
|
-
this.stateManager.updateAsset(sceneId, 'base-build', sceneName, path.relative(this.options.outputDir, scenePath), stat.size);
|
|
701
|
-
}
|
|
702
|
-
catch (error) {
|
|
703
|
-
// 忽略错误
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
// 记录资产文件
|
|
707
|
-
for (const assetPath of result.files.assets) {
|
|
708
|
-
try {
|
|
709
|
-
const stat = await fs.stat(assetPath);
|
|
710
|
-
const assetName = path.basename(assetPath);
|
|
711
|
-
const assetId = `asset-${assetName}`;
|
|
712
|
-
// 根据文件扩展名判断类型
|
|
713
|
-
const ext = path.extname(assetName).toLowerCase();
|
|
714
|
-
let assetType = 'other';
|
|
715
|
-
if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext)) {
|
|
716
|
-
assetType = 'texture';
|
|
717
|
-
}
|
|
718
|
-
else if (['.mp3', '.wav', '.ogg'].includes(ext)) {
|
|
719
|
-
assetType = 'audio';
|
|
720
|
-
}
|
|
721
|
-
else if (['.glb', '.gltf', '.obj', '.fbx'].includes(ext)) {
|
|
722
|
-
assetType = 'model';
|
|
723
|
-
}
|
|
724
|
-
this.stateManager.recordAsset(assetId, assetName, assetPath, stat.size, assetType);
|
|
725
|
-
this.stateManager.updateAsset(assetId, 'base-build', assetName, path.relative(this.options.outputDir, assetPath), stat.size);
|
|
726
|
-
}
|
|
727
|
-
catch (error) {
|
|
728
|
-
// 忽略错误
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
console.log('[BaseBuilder] 构建产物信息已记录');
|
|
732
|
-
}
|
|
733
|
-
catch (error) {
|
|
734
|
-
console.error('[BaseBuilder] 记录构建产物信息失败:', error);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* 生成构建分析报告
|
|
739
|
-
*/
|
|
740
|
-
async generateAnalysisReport() {
|
|
741
|
-
console.log('\n[BaseBuilder] 生成构建分析报告...');
|
|
742
|
-
try {
|
|
743
|
-
const analyzer = new BuildAnalyzer(this.options.outputDir, this.options.outputDir);
|
|
744
|
-
const report = await analyzer.analyze();
|
|
745
|
-
const reportPath = await analyzer.generateHTMLReport(report);
|
|
746
|
-
// 删除 rollup-plugin-visualizer 生成的报告文件
|
|
747
|
-
const oldReportPath = path.join(this.options.outputDir, 'base-bundle-report.html');
|
|
748
|
-
try {
|
|
749
|
-
await fs.unlink(oldReportPath);
|
|
750
|
-
console.log('[BaseBuilder] 已删除旧的分析报告: base-bundle-report.html');
|
|
751
|
-
}
|
|
752
|
-
catch (error) {
|
|
753
|
-
// 文件可能不存在,忽略错误
|
|
754
|
-
}
|
|
755
|
-
console.log(`\n📊 构建分析报告:`);
|
|
756
|
-
console.log(` 文件总数: ${report.totalFiles}`);
|
|
757
|
-
console.log(` 当前总大小: ${report.totalSizeFormatted}`);
|
|
758
|
-
console.log(` 预估单 HTML 大小: ${report.estimatedHtmlSizeFormatted}`);
|
|
759
|
-
console.log(` 报告路径: ${reportPath}\n`);
|
|
760
|
-
}
|
|
761
|
-
catch (error) {
|
|
762
|
-
console.error('[BaseBuilder] 生成分析报告失败:', error);
|
|
51
|
+
console.warn(`[BaseBuilder] 无效的引擎类型: ${this.options.engine},将自动检测`);
|
|
763
52
|
}
|
|
53
|
+
// 2. 自动检测
|
|
54
|
+
const detected = await EngineDetector.detect(this.projectDir);
|
|
55
|
+
console.log(`[BaseBuilder] 自动检测引擎: ${detected}`);
|
|
56
|
+
return detected;
|
|
764
57
|
}
|
|
765
58
|
}
|