@playcraft/cli 0.0.15 → 0.0.18
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/agent/agent.js +54 -1
- package/dist/agent/fs-backend.js +312 -8
- package/dist/agent/local-backend.js +249 -18
- package/dist/build-config.js +11 -2
- package/dist/commands/build-all.js +477 -0
- package/dist/commands/build.js +248 -178
- package/dist/fs-handler.js +117 -0
- package/dist/index.js +57 -15
- package/dist/server.js +23 -6
- package/dist/socket.js +7 -2
- package/dist/watcher.js +27 -1
- package/package.json +3 -3
package/dist/commands/build.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import ora from 'ora';
|
|
5
|
-
import { BaseBuilder, ViteBuilder, PlayableBuilder, PlayableAnalyzer } from '@playcraft/build';
|
|
5
|
+
import { BaseBuilder, ViteBuilder, PlayableBuilder, PlayableAnalyzer, EngineDetector, detectAmmoUsage } from '@playcraft/build';
|
|
6
6
|
import { loadBuildConfig } from '../build-config.js';
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
8
|
/**
|
|
@@ -33,11 +33,59 @@ async function detectBaseBuild(dir) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* 检测项目场景列表(支持 PlayCanvas 和 PlayCraft 两种格式)
|
|
37
37
|
*/
|
|
38
38
|
async function detectProjectScenes(projectPath) {
|
|
39
39
|
try {
|
|
40
|
-
// 尝试读取
|
|
40
|
+
// 1. 尝试读取 PlayCraft 格式的 manifest.json
|
|
41
|
+
const manifestJsonPath = path.join(projectPath, 'manifest.json');
|
|
42
|
+
try {
|
|
43
|
+
const manifestContent = await fs.readFile(manifestJsonPath, 'utf-8');
|
|
44
|
+
const manifestData = JSON.parse(manifestContent);
|
|
45
|
+
if (manifestData.scenes && Array.isArray(manifestData.scenes)) {
|
|
46
|
+
// PlayCraft manifest.json 中的场景列表
|
|
47
|
+
const scenes = manifestData.scenes.map((scene) => ({
|
|
48
|
+
id: String(scene.id || scene.name),
|
|
49
|
+
name: scene.name || `Scene ${scene.id}`,
|
|
50
|
+
isMain: scene.isMain === true
|
|
51
|
+
}));
|
|
52
|
+
// 同时检查 scenes/ 目录中是否有不在 manifest 中的场景文件
|
|
53
|
+
const scenesDir = path.join(projectPath, 'scenes');
|
|
54
|
+
try {
|
|
55
|
+
const sceneFiles = await fs.readdir(scenesDir);
|
|
56
|
+
const manifestIds = new Set(scenes.map((s) => String(s.id)));
|
|
57
|
+
for (const f of sceneFiles) {
|
|
58
|
+
if (!f.endsWith('.json'))
|
|
59
|
+
continue;
|
|
60
|
+
const fileId = f.replace(/\.scene\.json$/, '').replace(/\.json$/, '');
|
|
61
|
+
if (!manifestIds.has(fileId)) {
|
|
62
|
+
// 读取场景文件获取名称
|
|
63
|
+
try {
|
|
64
|
+
const sceneContent = await fs.readFile(path.join(scenesDir, f), 'utf-8');
|
|
65
|
+
const sceneData = JSON.parse(sceneContent);
|
|
66
|
+
scenes.push({
|
|
67
|
+
id: fileId,
|
|
68
|
+
name: sceneData.name || `Scene ${fileId}`,
|
|
69
|
+
isMain: false
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
scenes.push({ id: fileId, name: `Scene ${fileId}`, isMain: false });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// scenes 目录不存在
|
|
80
|
+
}
|
|
81
|
+
if (scenes.length > 0)
|
|
82
|
+
return scenes;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// manifest.json 不存在或解析失败
|
|
87
|
+
}
|
|
88
|
+
// 2. 尝试读取 PlayCanvas 格式的 scenes.json
|
|
41
89
|
const scenesJsonPath = path.join(projectPath, 'scenes.json');
|
|
42
90
|
try {
|
|
43
91
|
const scenesContent = await fs.readFile(scenesJsonPath, 'utf-8');
|
|
@@ -60,7 +108,7 @@ async function detectProjectScenes(projectPath) {
|
|
|
60
108
|
catch (error) {
|
|
61
109
|
// scenes.json 不存在或解析失败,尝试其他方法
|
|
62
110
|
}
|
|
63
|
-
// 尝试从 project.json 读取
|
|
111
|
+
// 3. 尝试从 project.json 读取
|
|
64
112
|
const projectJsonPath = path.join(projectPath, 'project.json');
|
|
65
113
|
try {
|
|
66
114
|
const projectContent = await fs.readFile(projectJsonPath, 'utf-8');
|
|
@@ -81,78 +129,6 @@ async function detectProjectScenes(projectPath) {
|
|
|
81
129
|
return [];
|
|
82
130
|
}
|
|
83
131
|
}
|
|
84
|
-
function resolveSuggestedPhysicsEngine(use3dPhysics) {
|
|
85
|
-
return use3dPhysics ? 'cannon' : 'p2';
|
|
86
|
-
}
|
|
87
|
-
async function detectAmmoUsage(baseBuildDir) {
|
|
88
|
-
const configPath = path.join(baseBuildDir, 'config.json');
|
|
89
|
-
let hasAmmo = false;
|
|
90
|
-
let use3dPhysics = false;
|
|
91
|
-
try {
|
|
92
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
93
|
-
const configJson = JSON.parse(configContent);
|
|
94
|
-
// 检查 application_properties.use3dPhysics
|
|
95
|
-
use3dPhysics = Boolean(configJson.application_properties?.use3dPhysics);
|
|
96
|
-
// 如果 use3dPhysics 未设置,通过检查场景内容来推断
|
|
97
|
-
// 检查场景是否包含 3D 物理组件(rigidbody, collision 等)
|
|
98
|
-
if (!use3dPhysics) {
|
|
99
|
-
const scenesDir = baseBuildDir;
|
|
100
|
-
try {
|
|
101
|
-
const files = await fs.readdir(scenesDir);
|
|
102
|
-
for (const file of files) {
|
|
103
|
-
if (file.endsWith('.json') && !['config.json', 'manifest.json'].includes(file)) {
|
|
104
|
-
try {
|
|
105
|
-
const sceneContent = await fs.readFile(path.join(scenesDir, file), 'utf-8');
|
|
106
|
-
// 检查场景是否包含 3D 物理组件
|
|
107
|
-
if (sceneContent.includes('"rigidbody"') ||
|
|
108
|
-
sceneContent.includes('"collision"') ||
|
|
109
|
-
sceneContent.includes('"joint"')) {
|
|
110
|
-
use3dPhysics = true;
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// 忽略单个场景读取失败
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// 忽略目录读取失败
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (configJson.assets) {
|
|
125
|
-
for (const asset of Object.values(configJson.assets)) {
|
|
126
|
-
const assetData = asset;
|
|
127
|
-
const moduleName = assetData?.data?.moduleName?.toLowerCase?.() || '';
|
|
128
|
-
const fileUrl = assetData?.file?.url?.toLowerCase?.() || '';
|
|
129
|
-
if (moduleName.includes('ammo') || fileUrl.includes('ammo')) {
|
|
130
|
-
hasAmmo = true;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
// 忽略解析失败
|
|
138
|
-
}
|
|
139
|
-
if (!hasAmmo) {
|
|
140
|
-
try {
|
|
141
|
-
const settingsContent = await fs.readFile(path.join(baseBuildDir, '__settings__.js'), 'utf-8');
|
|
142
|
-
if (settingsContent.toLowerCase().includes('ammo')) {
|
|
143
|
-
hasAmmo = true;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
// 忽略读取失败
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
hasAmmo,
|
|
152
|
-
suggestedEngine: resolveSuggestedPhysicsEngine(use3dPhysics),
|
|
153
|
-
use3dPhysics,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
132
|
async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
|
|
157
133
|
const assetSizes = [];
|
|
158
134
|
const assetPaths = new Set(); // 用于去重
|
|
@@ -225,6 +201,27 @@ export async function buildCommand(projectPath, options) {
|
|
|
225
201
|
// 清理选项(默认为 false,使用覆盖模式)
|
|
226
202
|
const shouldClean = options.clean === true;
|
|
227
203
|
try {
|
|
204
|
+
// ====== 引擎检测 ======
|
|
205
|
+
spinner.text = pc.cyan('检测项目引擎类型...');
|
|
206
|
+
let detectedEngine;
|
|
207
|
+
if (options.engine) {
|
|
208
|
+
// 用户指定了引擎类型
|
|
209
|
+
detectedEngine = options.engine;
|
|
210
|
+
console.log(pc.dim(`\nℹ️ 使用指定引擎: ${detectedEngine}`));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// 自动检测引擎类型
|
|
214
|
+
detectedEngine = await EngineDetector.detect(resolvedProjectPath);
|
|
215
|
+
console.log(pc.dim(`\nℹ️ 检测到引擎: ${detectedEngine}`));
|
|
216
|
+
// 对于外部引擎,显示提示
|
|
217
|
+
if (detectedEngine !== 'playcanvas') {
|
|
218
|
+
console.log(pc.cyan(`\n🎮 检测到 ${detectedEngine} 项目`));
|
|
219
|
+
console.log(pc.dim(' 将使用 npm install + npm run build 进行构建'));
|
|
220
|
+
console.log(pc.yellow(' ⚠️ 构建时间可能较长(约 2-5 分钟)\n'));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const isPlayCanvas = detectedEngine === 'playcanvas';
|
|
224
|
+
const requiresNpmBuild = detectedEngine !== 'playcanvas';
|
|
228
225
|
if (!options.baseOnly && !options.mode) {
|
|
229
226
|
spinner.stop();
|
|
230
227
|
const modeAnswer = await inquirer.prompt([
|
|
@@ -257,58 +254,66 @@ export async function buildCommand(projectPath, options) {
|
|
|
257
254
|
if (!options.output && fileConfig.outputDir) {
|
|
258
255
|
options.output = fileConfig.outputDir;
|
|
259
256
|
}
|
|
257
|
+
// 从配置文件读取 storeUrls(避免重复输入)
|
|
258
|
+
if (!options.storeUrls && fileConfig.storeUrls) {
|
|
259
|
+
options.storeUrls = fileConfig.storeUrls;
|
|
260
|
+
console.log(pc.dim(`\nℹ️ 从配置文件读取商店地址`));
|
|
261
|
+
}
|
|
260
262
|
}
|
|
261
|
-
//
|
|
263
|
+
// 解析场景选择参数(支持 PlayCanvas 和 PlayCraft 项目)
|
|
262
264
|
let selectedScenes;
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
if (isPlayCanvas) {
|
|
266
|
+
if (options.scenes) {
|
|
267
|
+
// 命令行指定了场景参数
|
|
268
|
+
selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
|
|
269
|
+
if (selectedScenes.length > 0) {
|
|
270
|
+
console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
|
|
271
|
+
}
|
|
268
272
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
273
|
+
else if (options.mode !== 'playable') {
|
|
274
|
+
// 没有指定场景参数,且不是仅做 playable build(playable 不需要选场景),检测项目场景并提供交互选择
|
|
275
|
+
spinner.stop();
|
|
276
|
+
const projectScenes = await detectProjectScenes(resolvedProjectPath);
|
|
277
|
+
if (projectScenes.length > 1) {
|
|
278
|
+
// 有多个场景,提示用户选择
|
|
279
|
+
console.log(pc.cyan(`\n🎬 检测到 ${projectScenes.length} 个场景`));
|
|
280
|
+
console.log(pc.dim('💡 提示: 只打包选中的场景可以显著减小文件大小(通常可减小 30-60%)\n'));
|
|
281
|
+
// 为 PlayCraft 项目标记主场景
|
|
282
|
+
const sceneAnswer = await inquirer.prompt([
|
|
283
|
+
{
|
|
284
|
+
type: 'checkbox',
|
|
285
|
+
name: 'selectedScenes',
|
|
286
|
+
message: '选择要打包的场景(使用空格选择,回车确认):',
|
|
287
|
+
choices: projectScenes.map(scene => ({
|
|
288
|
+
name: scene.isMain ? `${scene.name} (主场景)` : scene.name,
|
|
289
|
+
value: scene.name,
|
|
290
|
+
checked: scene.isMain === true || !projectScenes.some(s => s.isMain) // 有 isMain 标记则只默认选主场景,否则全选
|
|
291
|
+
})),
|
|
292
|
+
validate: (answer) => {
|
|
293
|
+
if (answer.length === 0) {
|
|
294
|
+
return '请至少选择一个场景';
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
291
297
|
}
|
|
292
|
-
return true;
|
|
293
298
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
]);
|
|
300
|
+
selectedScenes = sceneAnswer.selectedScenes;
|
|
301
|
+
if (selectedScenes && selectedScenes.length > 0) {
|
|
302
|
+
if (selectedScenes.length === projectScenes.length) {
|
|
303
|
+
console.log(pc.green(`✅ 已选择所有场景 (${selectedScenes.length} 个)`));
|
|
304
|
+
// 全选则不传递参数(向后兼容)
|
|
305
|
+
selectedScenes = undefined;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.log(pc.green(`✅ 已选择 ${selectedScenes.length} / ${projectScenes.length} 个场景: ${selectedScenes.join(', ')}`));
|
|
309
|
+
}
|
|
305
310
|
}
|
|
306
311
|
}
|
|
312
|
+
else if (projectScenes.length === 1) {
|
|
313
|
+
console.log(pc.dim(`ℹ️ 项目只有 1 个场景: ${projectScenes[0].name}`));
|
|
314
|
+
}
|
|
315
|
+
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
307
316
|
}
|
|
308
|
-
else if (projectScenes.length === 1) {
|
|
309
|
-
console.log(pc.dim(`ℹ️ 项目只有 1 个场景: ${projectScenes[0].name}`));
|
|
310
|
-
}
|
|
311
|
-
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
312
317
|
}
|
|
313
318
|
// 如果只执行基础构建
|
|
314
319
|
if (options.baseOnly || options.mode === 'base') {
|
|
@@ -329,6 +334,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
329
334
|
selectedScenes,
|
|
330
335
|
clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
|
|
331
336
|
analyze: options.analyze, // 传递 analyze 参数
|
|
337
|
+
engine: detectedEngine, // 传递引擎类型
|
|
332
338
|
});
|
|
333
339
|
const baseBuild = await baseBuilder.build();
|
|
334
340
|
spinner.succeed(pc.green('✅ 基础构建完成!'));
|
|
@@ -353,6 +359,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
353
359
|
'inmobi',
|
|
354
360
|
'adikteev',
|
|
355
361
|
'remerge',
|
|
362
|
+
'mintegral',
|
|
356
363
|
];
|
|
357
364
|
// 渠道输出格式配置(根据各渠道Playable规格对照表)
|
|
358
365
|
// 只支持 HTML: applovin, ironsource, unity, moloco, adikteev, remerge
|
|
@@ -376,6 +383,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
376
383
|
bigo: { formats: ['zip'], default: 'zip' },
|
|
377
384
|
snapchat: { formats: ['zip'], default: 'zip' },
|
|
378
385
|
inmobi: { formats: ['zip'], default: 'zip' },
|
|
386
|
+
mintegral: { formats: ['zip'], default: 'zip' },
|
|
379
387
|
};
|
|
380
388
|
// 获取渠道支持的格式
|
|
381
389
|
const getPlatformFormats = (platform) => {
|
|
@@ -426,6 +434,32 @@ export async function buildCommand(projectPath, options) {
|
|
|
426
434
|
]);
|
|
427
435
|
selectedFormat = formatAnswer.format;
|
|
428
436
|
}
|
|
437
|
+
// 输入商店跳转地址(CTA)- 如果配置文件中没有则必填
|
|
438
|
+
if (!options.storeUrls) {
|
|
439
|
+
console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
|
|
440
|
+
console.log(pc.dim(' iOS 和 Android 地址均为必填'));
|
|
441
|
+
const storeUrlAnswer = await inquirer.prompt([
|
|
442
|
+
{
|
|
443
|
+
type: 'input',
|
|
444
|
+
name: 'iosStoreUrl',
|
|
445
|
+
message: 'iOS App Store URL:',
|
|
446
|
+
validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
type: 'input',
|
|
450
|
+
name: 'androidStoreUrl',
|
|
451
|
+
message: 'Android Google Play URL:',
|
|
452
|
+
validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
|
|
453
|
+
},
|
|
454
|
+
]);
|
|
455
|
+
options.storeUrls = {
|
|
456
|
+
ios: storeUrlAnswer.iosStoreUrl.trim(),
|
|
457
|
+
android: storeUrlAnswer.androidStoreUrl.trim(),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
console.log(pc.dim(`\n✅ 使用配置文件中的商店地址`));
|
|
462
|
+
}
|
|
429
463
|
// 选择输出目录
|
|
430
464
|
const outputAnswer = await inquirer.prompt([
|
|
431
465
|
{
|
|
@@ -452,22 +486,47 @@ export async function buildCommand(projectPath, options) {
|
|
|
452
486
|
// 未指定格式,使用默认值
|
|
453
487
|
options.format = formatConfig.default;
|
|
454
488
|
}
|
|
489
|
+
// 如果命令行未传入 storeUrls,交互式输入(必填)
|
|
490
|
+
if (!options.storeUrls) {
|
|
491
|
+
spinner.stop();
|
|
492
|
+
console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
|
|
493
|
+
console.log(pc.dim(' iOS 和 Android 地址均为必填'));
|
|
494
|
+
const storeUrlAnswer = await inquirer.prompt([
|
|
495
|
+
{
|
|
496
|
+
type: 'input',
|
|
497
|
+
name: 'iosStoreUrl',
|
|
498
|
+
message: 'iOS App Store URL:',
|
|
499
|
+
validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
type: 'input',
|
|
503
|
+
name: 'androidStoreUrl',
|
|
504
|
+
message: 'Android Google Play URL:',
|
|
505
|
+
validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
|
|
506
|
+
},
|
|
507
|
+
]);
|
|
508
|
+
options.storeUrls = {
|
|
509
|
+
ios: storeUrlAnswer.iosStoreUrl.trim(),
|
|
510
|
+
android: storeUrlAnswer.androidStoreUrl.trim(),
|
|
511
|
+
};
|
|
512
|
+
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
513
|
+
}
|
|
455
514
|
}
|
|
456
515
|
// 解析配置
|
|
457
516
|
const buildOptions = {
|
|
458
517
|
platform: options.platform,
|
|
459
518
|
format: options.format || 'html',
|
|
460
519
|
outputDir: path.resolve(options.output || './dist'),
|
|
461
|
-
//
|
|
462
|
-
compressEngine: options.compress,
|
|
463
|
-
|
|
464
|
-
|
|
520
|
+
// 压缩选项(默认全部开启)
|
|
521
|
+
compressEngine: options.compress ?? true,
|
|
522
|
+
compressConfigJson: options.compressConfig ?? true,
|
|
523
|
+
compressJS: options.compressJs ?? true,
|
|
465
524
|
analyze: options.analyze || false,
|
|
466
525
|
// Vite 构建选项
|
|
467
526
|
useVite: options.useVite !== false, // 默认使用 Vite
|
|
468
|
-
//
|
|
469
|
-
cssMinify: options.cssMinify,
|
|
470
|
-
jsMinify: options.jsMinify,
|
|
527
|
+
// 压缩选项(默认全部开启)
|
|
528
|
+
cssMinify: options.cssMinify ?? true,
|
|
529
|
+
jsMinify: options.jsMinify ?? true,
|
|
471
530
|
compressImages: options.compressImages,
|
|
472
531
|
imageQuality: options.imageQuality,
|
|
473
532
|
convertToWebP: options.convertToWebP,
|
|
@@ -477,6 +536,8 @@ export async function buildCommand(projectPath, options) {
|
|
|
477
536
|
selectedScenes,
|
|
478
537
|
// ESM 选项
|
|
479
538
|
...(options.esmMode ? { esmMode: options.esmMode } : {}),
|
|
539
|
+
// 商店跳转地址
|
|
540
|
+
...(options.storeUrls ? { storeUrls: options.storeUrls } : {}),
|
|
480
541
|
};
|
|
481
542
|
if (fileConfig) {
|
|
482
543
|
Object.assign(buildOptions, fileConfig);
|
|
@@ -531,77 +592,86 @@ export async function buildCommand(projectPath, options) {
|
|
|
531
592
|
selectedScenes,
|
|
532
593
|
analyze: buildOptions.analyze,
|
|
533
594
|
analyzeReportPath: buildOptions.analyze ? 'base-bundle-report.html' : undefined,
|
|
595
|
+
engine: detectedEngine, // 传递引擎类型
|
|
534
596
|
});
|
|
535
597
|
const baseBuild = await baseBuilder.build();
|
|
536
598
|
baseBuildDir = baseBuild.outputDir;
|
|
537
599
|
spinner.text = pc.cyan('✅ 阶段1完成,开始阶段2...');
|
|
538
600
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
buildOptions.ammoReplacement = ammoCheck.suggestedEngine;
|
|
546
|
-
}
|
|
547
|
-
else if (ammoCheck.hasAmmo && !buildOptions.ammoReplacement) {
|
|
548
|
-
spinner.stop();
|
|
549
|
-
// 根据是否使用 3D 物理提供不同的选项
|
|
550
|
-
const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
|
|
551
|
-
if (ammoCheck.use3dPhysics) {
|
|
552
|
-
// 3D 项目 - 可以使用 Cannon.js 替换
|
|
553
|
-
console.log(pc.yellow(`⚠️ 检测到 3D 物理项目,Ammo.js 约 1.8MB`));
|
|
554
|
-
console.log(pc.gray(` 提示: Cannon.js (~400KB) 可以减小 75% 的体积`));
|
|
555
|
-
const replaceAnswer = await inquirer.prompt([
|
|
556
|
-
{
|
|
557
|
-
type: 'list',
|
|
558
|
-
name: 'ammoAction',
|
|
559
|
-
message: `如何处理 Ammo.js?`,
|
|
560
|
-
choices: [
|
|
561
|
-
{ name: '保留 Ammo.js(完整物理功能,文件较大)', value: 'keep' },
|
|
562
|
-
{ name: '替换为 Cannon.js(推荐 3D 项目,~400KB,自动适配)', value: 'cannon' },
|
|
563
|
-
],
|
|
564
|
-
default: 'cannon',
|
|
565
|
-
},
|
|
566
|
-
]);
|
|
567
|
-
if (replaceAnswer.ammoAction === 'cannon') {
|
|
568
|
-
buildOptions.ammoReplacement = 'cannon';
|
|
569
|
-
console.log(pc.green(' ✓ 将使用 Cannon.js 替换 Ammo.js(自动兼容 rigidbody API)'));
|
|
570
|
-
}
|
|
601
|
+
// Ammo 检测和替换(仅 PlayCanvas 项目)
|
|
602
|
+
if (isPlayCanvas) {
|
|
603
|
+
const ammoCheck = await detectAmmoUsage(baseBuildDir);
|
|
604
|
+
const requestedAmmoEngine = options.ammoEngine;
|
|
605
|
+
if (requestedAmmoEngine === 'p2' || requestedAmmoEngine === 'cannon') {
|
|
606
|
+
buildOptions.ammoReplacement = requestedAmmoEngine;
|
|
571
607
|
}
|
|
572
|
-
else {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
608
|
+
else if (options.replaceAmmo && ammoCheck.hasAmmo && ammoCheck.suggestedEngine) {
|
|
609
|
+
buildOptions.ammoReplacement = ammoCheck.suggestedEngine;
|
|
610
|
+
}
|
|
611
|
+
else if (ammoCheck.hasAmmo && !buildOptions.ammoReplacement) {
|
|
612
|
+
spinner.stop();
|
|
613
|
+
// 根据是否使用 3D 物理提供不同的选项
|
|
614
|
+
const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
|
|
615
|
+
if (ammoCheck.use3dPhysics) {
|
|
616
|
+
// 3D 项目 - 可以使用 Cannon.js 替换
|
|
617
|
+
console.log(pc.yellow(`⚠️ 检测到 3D 物理项目,Ammo.js 约 1.8MB`));
|
|
618
|
+
console.log(pc.gray(` 提示: Cannon.js (~400KB) 可以减小 75% 的体积`));
|
|
619
|
+
const replaceAnswer = await inquirer.prompt([
|
|
620
|
+
{
|
|
621
|
+
type: 'list',
|
|
622
|
+
name: 'ammoAction',
|
|
623
|
+
message: `如何处理 Ammo.js?`,
|
|
624
|
+
choices: [
|
|
625
|
+
{ name: '保留 Ammo.js(完整物理功能,文件较大)', value: 'keep' },
|
|
626
|
+
{ name: '替换为 Cannon.js(推荐 3D 项目,~400KB,自动适配)', value: 'cannon' },
|
|
627
|
+
],
|
|
628
|
+
default: 'cannon',
|
|
629
|
+
},
|
|
630
|
+
]);
|
|
631
|
+
if (replaceAnswer.ammoAction === 'cannon') {
|
|
632
|
+
buildOptions.ammoReplacement = 'cannon';
|
|
633
|
+
console.log(pc.green(' ✓ 将使用 Cannon.js 替换 Ammo.js(自动兼容 rigidbody API)'));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
// 2D 项目 - 可以使用 p2.js 替换
|
|
638
|
+
const replaceAnswer = await inquirer.prompt([
|
|
639
|
+
{
|
|
640
|
+
type: 'list',
|
|
641
|
+
name: 'ammoAction',
|
|
642
|
+
message: `检测到 Ammo.js(项目使用 ${physicsType} 物理),如何处理?`,
|
|
643
|
+
choices: [
|
|
644
|
+
{ name: '不替换(保留 Ammo.js,文件较大)', value: 'keep' },
|
|
645
|
+
{ name: '替换为 p2.js(推荐 2D 项目,~60KB)', value: 'p2' },
|
|
646
|
+
],
|
|
647
|
+
default: 'p2',
|
|
648
|
+
},
|
|
649
|
+
]);
|
|
650
|
+
if (replaceAnswer.ammoAction === 'p2') {
|
|
651
|
+
buildOptions.ammoReplacement = 'p2';
|
|
652
|
+
}
|
|
588
653
|
}
|
|
654
|
+
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
589
655
|
}
|
|
590
|
-
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
591
656
|
}
|
|
592
657
|
// 2. 执行阶段2:转换为Playable Ads
|
|
593
658
|
spinner.text = pc.cyan('执行阶段2: 转换为单HTML Playable Ads...');
|
|
594
659
|
let outputPath;
|
|
595
660
|
let sizeReport;
|
|
661
|
+
// 构建选项中传递引擎类型
|
|
662
|
+
const viteBuildOptions = {
|
|
663
|
+
...buildOptions,
|
|
664
|
+
engine: detectedEngine,
|
|
665
|
+
};
|
|
596
666
|
if (buildOptions.useVite !== false) {
|
|
597
667
|
// 使用 Vite 构建
|
|
598
|
-
const viteBuilder = new ViteBuilder(baseBuildDir,
|
|
668
|
+
const viteBuilder = new ViteBuilder(baseBuildDir, viteBuildOptions);
|
|
599
669
|
outputPath = await viteBuilder.build();
|
|
600
670
|
sizeReport = viteBuilder.getSizeReport();
|
|
601
671
|
}
|
|
602
672
|
else {
|
|
603
673
|
// 使用旧的 PlayableBuilder(向后兼容)
|
|
604
|
-
const playableBuilder = new PlayableBuilder(baseBuildDir,
|
|
674
|
+
const playableBuilder = new PlayableBuilder(baseBuildDir, viteBuildOptions);
|
|
605
675
|
outputPath = await playableBuilder.build();
|
|
606
676
|
sizeReport = playableBuilder.getSizeReport();
|
|
607
677
|
}
|