@playcraft/cli 0.0.14 → 0.0.17

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.
@@ -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
- // 尝试读取 scenes.json
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([
@@ -258,57 +255,60 @@ export async function buildCommand(projectPath, options) {
258
255
  options.output = fileConfig.outputDir;
259
256
  }
260
257
  }
261
- // 解析场景选择参数(需要在 baseOnly 之前)
258
+ // 解析场景选择参数(支持 PlayCanvas 和 PlayCraft 项目)
262
259
  let selectedScenes;
263
- if (options.scenes) {
264
- // 命令行指定了场景参数
265
- selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
266
- if (selectedScenes.length > 0) {
267
- console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
260
+ if (isPlayCanvas) {
261
+ if (options.scenes) {
262
+ // 命令行指定了场景参数
263
+ selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
264
+ if (selectedScenes.length > 0) {
265
+ console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
266
+ }
268
267
  }
269
- }
270
- else {
271
- // 没有指定场景参数,检测项目场景并提供交互选择
272
- spinner.stop();
273
- const projectScenes = await detectProjectScenes(resolvedProjectPath);
274
- if (projectScenes.length > 1) {
275
- // 有多个场景,提示用户选择
276
- console.log(pc.cyan(`\n🎬 检测到 ${projectScenes.length} 个场景`));
277
- console.log(pc.dim('💡 提示: 只打包选中的场景可以显著减小文件大小(通常可减小 30-60%)\n'));
278
- const sceneAnswer = await inquirer.prompt([
279
- {
280
- type: 'checkbox',
281
- name: 'selectedScenes',
282
- message: '选择要打包的场景(使用空格选择,回车确认):',
283
- choices: projectScenes.map(scene => ({
284
- name: scene.name,
285
- value: scene.name,
286
- checked: true // 默认全选
287
- })),
288
- validate: (answer) => {
289
- if (answer.length === 0) {
290
- return '请至少选择一个场景';
268
+ else if (options.mode !== 'playable') {
269
+ // 没有指定场景参数,且不是仅做 playable build(playable 不需要选场景),检测项目场景并提供交互选择
270
+ spinner.stop();
271
+ const projectScenes = await detectProjectScenes(resolvedProjectPath);
272
+ if (projectScenes.length > 1) {
273
+ // 有多个场景,提示用户选择
274
+ console.log(pc.cyan(`\n🎬 检测到 ${projectScenes.length} 个场景`));
275
+ console.log(pc.dim('💡 提示: 只打包选中的场景可以显著减小文件大小(通常可减小 30-60%)\n'));
276
+ // PlayCraft 项目标记主场景
277
+ const sceneAnswer = await inquirer.prompt([
278
+ {
279
+ type: 'checkbox',
280
+ name: 'selectedScenes',
281
+ message: '选择要打包的场景(使用空格选择,回车确认):',
282
+ choices: projectScenes.map(scene => ({
283
+ name: scene.isMain ? `${scene.name} (主场景)` : scene.name,
284
+ value: scene.name,
285
+ checked: scene.isMain === true || !projectScenes.some(s => s.isMain) // 有 isMain 标记则只默认选主场景,否则全选
286
+ })),
287
+ validate: (answer) => {
288
+ if (answer.length === 0) {
289
+ return '请至少选择一个场景';
290
+ }
291
+ return true;
291
292
  }
292
- return true;
293
293
  }
294
- }
295
- ]);
296
- selectedScenes = sceneAnswer.selectedScenes;
297
- if (selectedScenes && selectedScenes.length > 0) {
298
- if (selectedScenes.length === projectScenes.length) {
299
- console.log(pc.green(`✅ 已选择所有场景 (${selectedScenes.length} 个)`));
300
- // 全选则不传递参数(向后兼容)
301
- selectedScenes = undefined;
302
- }
303
- else {
304
- console.log(pc.green(`✅ 已选择 ${selectedScenes.length} / ${projectScenes.length} 个场景: ${selectedScenes.join(', ')}`));
294
+ ]);
295
+ selectedScenes = sceneAnswer.selectedScenes;
296
+ if (selectedScenes && selectedScenes.length > 0) {
297
+ if (selectedScenes.length === projectScenes.length) {
298
+ console.log(pc.green(`✅ 已选择所有场景 (${selectedScenes.length} )`));
299
+ // 全选则不传递参数(向后兼容)
300
+ selectedScenes = undefined;
301
+ }
302
+ else {
303
+ console.log(pc.green(`✅ 已选择 ${selectedScenes.length} / ${projectScenes.length} 个场景: ${selectedScenes.join(', ')}`));
304
+ }
305
305
  }
306
306
  }
307
+ else if (projectScenes.length === 1) {
308
+ console.log(pc.dim(`ℹ️ 项目只有 1 个场景: ${projectScenes[0].name}`));
309
+ }
310
+ spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
307
311
  }
308
- else if (projectScenes.length === 1) {
309
- console.log(pc.dim(`ℹ️ 项目只有 1 个场景: ${projectScenes[0].name}`));
310
- }
311
- spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
312
312
  }
313
313
  // 如果只执行基础构建
314
314
  if (options.baseOnly || options.mode === 'base') {
@@ -329,6 +329,7 @@ export async function buildCommand(projectPath, options) {
329
329
  selectedScenes,
330
330
  clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
331
331
  analyze: options.analyze, // 传递 analyze 参数
332
+ engine: detectedEngine, // 传递引擎类型
332
333
  });
333
334
  const baseBuild = await baseBuilder.build();
334
335
  spinner.succeed(pc.green('✅ 基础构建完成!'));
@@ -353,6 +354,7 @@ export async function buildCommand(projectPath, options) {
353
354
  'inmobi',
354
355
  'adikteev',
355
356
  'remerge',
357
+ 'mintegral',
356
358
  ];
357
359
  // 渠道输出格式配置(根据各渠道Playable规格对照表)
358
360
  // 只支持 HTML: applovin, ironsource, unity, moloco, adikteev, remerge
@@ -376,6 +378,7 @@ export async function buildCommand(projectPath, options) {
376
378
  bigo: { formats: ['zip'], default: 'zip' },
377
379
  snapchat: { formats: ['zip'], default: 'zip' },
378
380
  inmobi: { formats: ['zip'], default: 'zip' },
381
+ mintegral: { formats: ['zip'], default: 'zip' },
379
382
  };
380
383
  // 获取渠道支持的格式
381
384
  const getPlatformFormats = (platform) => {
@@ -426,6 +429,27 @@ export async function buildCommand(projectPath, options) {
426
429
  ]);
427
430
  selectedFormat = formatAnswer.format;
428
431
  }
432
+ // 输入商店跳转地址(CTA)- 必填
433
+ console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
434
+ console.log(pc.dim(' iOS 和 Android 地址均为必填'));
435
+ const storeUrlAnswer = await inquirer.prompt([
436
+ {
437
+ type: 'input',
438
+ name: 'iosStoreUrl',
439
+ message: 'iOS App Store URL:',
440
+ validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
441
+ },
442
+ {
443
+ type: 'input',
444
+ name: 'androidStoreUrl',
445
+ message: 'Android Google Play URL:',
446
+ validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
447
+ },
448
+ ]);
449
+ options.storeUrls = {
450
+ ios: storeUrlAnswer.iosStoreUrl.trim(),
451
+ android: storeUrlAnswer.androidStoreUrl.trim(),
452
+ };
429
453
  // 选择输出目录
430
454
  const outputAnswer = await inquirer.prompt([
431
455
  {
@@ -452,20 +476,47 @@ export async function buildCommand(projectPath, options) {
452
476
  // 未指定格式,使用默认值
453
477
  options.format = formatConfig.default;
454
478
  }
479
+ // 如果命令行未传入 storeUrls,交互式输入(必填)
480
+ if (!options.storeUrls) {
481
+ spinner.stop();
482
+ console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
483
+ console.log(pc.dim(' iOS 和 Android 地址均为必填'));
484
+ const storeUrlAnswer = await inquirer.prompt([
485
+ {
486
+ type: 'input',
487
+ name: 'iosStoreUrl',
488
+ message: 'iOS App Store URL:',
489
+ validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
490
+ },
491
+ {
492
+ type: 'input',
493
+ name: 'androidStoreUrl',
494
+ message: 'Android Google Play URL:',
495
+ validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
496
+ },
497
+ ]);
498
+ options.storeUrls = {
499
+ ios: storeUrlAnswer.iosStoreUrl.trim(),
500
+ android: storeUrlAnswer.androidStoreUrl.trim(),
501
+ };
502
+ spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
503
+ }
455
504
  }
456
505
  // 解析配置
457
506
  const buildOptions = {
458
507
  platform: options.platform,
459
508
  format: options.format || 'html',
460
509
  outputDir: path.resolve(options.output || './dist'),
461
- // 如果命令行指定了 --compress,则使用命令行参数;否则让平台配置生效
462
- compressEngine: options.compress,
510
+ // 压缩选项(默认全部开启)
511
+ compressEngine: options.compress ?? true,
512
+ compressConfigJson: options.compressConfig ?? true,
513
+ compressJS: options.compressJs ?? true,
463
514
  analyze: options.analyze || false,
464
515
  // Vite 构建选项
465
516
  useVite: options.useVite !== false, // 默认使用 Vite
466
- // 压缩选项
467
- cssMinify: options.cssMinify,
468
- jsMinify: options.jsMinify,
517
+ // 压缩选项(默认全部开启)
518
+ cssMinify: options.cssMinify ?? true,
519
+ jsMinify: options.jsMinify ?? true,
469
520
  compressImages: options.compressImages,
470
521
  imageQuality: options.imageQuality,
471
522
  convertToWebP: options.convertToWebP,
@@ -475,6 +526,8 @@ export async function buildCommand(projectPath, options) {
475
526
  selectedScenes,
476
527
  // ESM 选项
477
528
  ...(options.esmMode ? { esmMode: options.esmMode } : {}),
529
+ // 商店跳转地址
530
+ ...(options.storeUrls ? { storeUrls: options.storeUrls } : {}),
478
531
  };
479
532
  if (fileConfig) {
480
533
  Object.assign(buildOptions, fileConfig);
@@ -529,77 +582,86 @@ export async function buildCommand(projectPath, options) {
529
582
  selectedScenes,
530
583
  analyze: buildOptions.analyze,
531
584
  analyzeReportPath: buildOptions.analyze ? 'base-bundle-report.html' : undefined,
585
+ engine: detectedEngine, // 传递引擎类型
532
586
  });
533
587
  const baseBuild = await baseBuilder.build();
534
588
  baseBuildDir = baseBuild.outputDir;
535
589
  spinner.text = pc.cyan('✅ 阶段1完成,开始阶段2...');
536
590
  }
537
- const ammoCheck = await detectAmmoUsage(baseBuildDir);
538
- const requestedAmmoEngine = options.ammoEngine;
539
- if (requestedAmmoEngine === 'p2' || requestedAmmoEngine === 'cannon') {
540
- buildOptions.ammoReplacement = requestedAmmoEngine;
541
- }
542
- else if (options.replaceAmmo && ammoCheck.hasAmmo && ammoCheck.suggestedEngine) {
543
- buildOptions.ammoReplacement = ammoCheck.suggestedEngine;
544
- }
545
- else if (ammoCheck.hasAmmo && !buildOptions.ammoReplacement) {
546
- spinner.stop();
547
- // 根据是否使用 3D 物理提供不同的选项
548
- const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
549
- if (ammoCheck.use3dPhysics) {
550
- // 3D 项目 - 可以使用 Cannon.js 替换
551
- console.log(pc.yellow(`⚠️ 检测到 3D 物理项目,Ammo.js 约 1.8MB`));
552
- console.log(pc.gray(` 提示: Cannon.js (~400KB) 可以减小 75% 的体积`));
553
- const replaceAnswer = await inquirer.prompt([
554
- {
555
- type: 'list',
556
- name: 'ammoAction',
557
- message: `如何处理 Ammo.js?`,
558
- choices: [
559
- { name: '保留 Ammo.js(完整物理功能,文件较大)', value: 'keep' },
560
- { name: '替换为 Cannon.js(推荐 3D 项目,~400KB,自动适配)', value: 'cannon' },
561
- ],
562
- default: 'cannon',
563
- },
564
- ]);
565
- if (replaceAnswer.ammoAction === 'cannon') {
566
- buildOptions.ammoReplacement = 'cannon';
567
- console.log(pc.green(' ✓ 将使用 Cannon.js 替换 Ammo.js(自动兼容 rigidbody API)'));
568
- }
591
+ // Ammo 检测和替换(仅 PlayCanvas 项目)
592
+ if (isPlayCanvas) {
593
+ const ammoCheck = await detectAmmoUsage(baseBuildDir);
594
+ const requestedAmmoEngine = options.ammoEngine;
595
+ if (requestedAmmoEngine === 'p2' || requestedAmmoEngine === 'cannon') {
596
+ buildOptions.ammoReplacement = requestedAmmoEngine;
569
597
  }
570
- else {
571
- // 2D 项目 - 可以使用 p2.js 替换
572
- const replaceAnswer = await inquirer.prompt([
573
- {
574
- type: 'list',
575
- name: 'ammoAction',
576
- message: `检测到 Ammo.js(项目使用 ${physicsType} 物理),如何处理?`,
577
- choices: [
578
- { name: '不替换(保留 Ammo.js,文件较大)', value: 'keep' },
579
- { name: '替换为 p2.js(推荐 2D 项目,~60KB)', value: 'p2' },
580
- ],
581
- default: 'p2',
582
- },
583
- ]);
584
- if (replaceAnswer.ammoAction === 'p2') {
585
- buildOptions.ammoReplacement = 'p2';
598
+ else if (options.replaceAmmo && ammoCheck.hasAmmo && ammoCheck.suggestedEngine) {
599
+ buildOptions.ammoReplacement = ammoCheck.suggestedEngine;
600
+ }
601
+ else if (ammoCheck.hasAmmo && !buildOptions.ammoReplacement) {
602
+ spinner.stop();
603
+ // 根据是否使用 3D 物理提供不同的选项
604
+ const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
605
+ if (ammoCheck.use3dPhysics) {
606
+ // 3D 项目 - 可以使用 Cannon.js 替换
607
+ console.log(pc.yellow(`⚠️ 检测到 3D 物理项目,Ammo.js 1.8MB`));
608
+ console.log(pc.gray(` 提示: Cannon.js (~400KB) 可以减小 75% 的体积`));
609
+ const replaceAnswer = await inquirer.prompt([
610
+ {
611
+ type: 'list',
612
+ name: 'ammoAction',
613
+ message: `如何处理 Ammo.js?`,
614
+ choices: [
615
+ { name: '保留 Ammo.js(完整物理功能,文件较大)', value: 'keep' },
616
+ { name: '替换为 Cannon.js(推荐 3D 项目,~400KB,自动适配)', value: 'cannon' },
617
+ ],
618
+ default: 'cannon',
619
+ },
620
+ ]);
621
+ if (replaceAnswer.ammoAction === 'cannon') {
622
+ buildOptions.ammoReplacement = 'cannon';
623
+ console.log(pc.green(' ✓ 将使用 Cannon.js 替换 Ammo.js(自动兼容 rigidbody API)'));
624
+ }
625
+ }
626
+ else {
627
+ // 2D 项目 - 可以使用 p2.js 替换
628
+ const replaceAnswer = await inquirer.prompt([
629
+ {
630
+ type: 'list',
631
+ name: 'ammoAction',
632
+ message: `检测到 Ammo.js(项目使用 ${physicsType} 物理),如何处理?`,
633
+ choices: [
634
+ { name: '不替换(保留 Ammo.js,文件较大)', value: 'keep' },
635
+ { name: '替换为 p2.js(推荐 2D 项目,~60KB)', value: 'p2' },
636
+ ],
637
+ default: 'p2',
638
+ },
639
+ ]);
640
+ if (replaceAnswer.ammoAction === 'p2') {
641
+ buildOptions.ammoReplacement = 'p2';
642
+ }
586
643
  }
644
+ spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
587
645
  }
588
- spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
589
646
  }
590
647
  // 2. 执行阶段2:转换为Playable Ads
591
648
  spinner.text = pc.cyan('执行阶段2: 转换为单HTML Playable Ads...');
592
649
  let outputPath;
593
650
  let sizeReport;
651
+ // 构建选项中传递引擎类型
652
+ const viteBuildOptions = {
653
+ ...buildOptions,
654
+ engine: detectedEngine,
655
+ };
594
656
  if (buildOptions.useVite !== false) {
595
657
  // 使用 Vite 构建
596
- const viteBuilder = new ViteBuilder(baseBuildDir, buildOptions);
658
+ const viteBuilder = new ViteBuilder(baseBuildDir, viteBuildOptions);
597
659
  outputPath = await viteBuilder.build();
598
660
  sizeReport = viteBuilder.getSizeReport();
599
661
  }
600
662
  else {
601
663
  // 使用旧的 PlayableBuilder(向后兼容)
602
- const playableBuilder = new PlayableBuilder(baseBuildDir, buildOptions);
664
+ const playableBuilder = new PlayableBuilder(baseBuildDir, viteBuildOptions);
603
665
  outputPath = await playableBuilder.build();
604
666
  sizeReport = playableBuilder.getSizeReport();
605
667
  }