@playcraft/cli 0.0.19 → 0.0.22
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/cli-root-help.js +22 -0
- package/dist/commands/audio.js +219 -0
- package/dist/commands/build-all.js +129 -13
- package/dist/commands/build.js +191 -14
- package/dist/commands/image.js +470 -0
- package/dist/commands/tools.js +438 -0
- package/dist/index.js +22 -1
- package/dist/playable/base-builder.js +265 -0
- package/dist/playable/builder.js +1462 -0
- package/dist/playable/converter.js +150 -0
- package/dist/playable/index.js +3 -0
- package/dist/playable/platforms/base.js +12 -0
- package/dist/playable/platforms/facebook.js +37 -0
- package/dist/playable/platforms/index.js +24 -0
- package/dist/playable/platforms/snapchat.js +59 -0
- package/dist/playable/playable-builder.js +521 -0
- package/dist/playable/types.js +1 -0
- package/dist/playable/vite/config-builder.js +136 -0
- package/dist/playable/vite/platform-configs.js +102 -0
- package/dist/playable/vite/plugin-model-compression.js +63 -0
- package/dist/playable/vite/plugin-platform.js +65 -0
- package/dist/playable/vite/plugin-playcanvas.js +454 -0
- package/dist/playable/vite-builder.js +125 -0
- package/dist/utils/agent-api-client.js +82 -0
- package/dist/utils/audio-processor.js +269 -0
- package/package.json +8 -3
package/dist/commands/build.js
CHANGED
|
@@ -5,6 +5,23 @@ import ora from 'ora';
|
|
|
5
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
|
+
/**
|
|
9
|
+
* 类型守卫:检查对象是否符合 PlayableScriptsConfig 类型
|
|
10
|
+
*/
|
|
11
|
+
function isPlayableScriptsConfig(obj) {
|
|
12
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// 检查关键可选字段的类型(如果存在)
|
|
16
|
+
const config = obj;
|
|
17
|
+
if (config.channels !== undefined && !Array.isArray(config.channels)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (config.themes !== undefined && (typeof config.themes !== 'object' || config.themes === null)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* 检测是否是基础构建产物(多文件版本)
|
|
10
27
|
*/
|
|
@@ -32,6 +49,40 @@ async function detectBaseBuild(dir) {
|
|
|
32
49
|
return false;
|
|
33
50
|
}
|
|
34
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 构建 PlayableScriptsConfig,合并 CLI 选择的参数和文件配置
|
|
54
|
+
*/
|
|
55
|
+
function buildPlayableScriptsConfig(fileConfigPlayableScripts, options) {
|
|
56
|
+
// 使用类型守卫检查 playableScripts 是否符合 PlayableScriptsConfig 类型
|
|
57
|
+
let config = fileConfigPlayableScripts && isPlayableScriptsConfig(fileConfigPlayableScripts)
|
|
58
|
+
? { ...fileConfigPlayableScripts }
|
|
59
|
+
: undefined;
|
|
60
|
+
// 将 CLI 选择的 platform 转换为 channels
|
|
61
|
+
if (options.platform) {
|
|
62
|
+
if (!config) {
|
|
63
|
+
config = {};
|
|
64
|
+
}
|
|
65
|
+
config.channels = [options.platform];
|
|
66
|
+
}
|
|
67
|
+
// 将 CLI 选择的 storeUrls 合并到配置
|
|
68
|
+
if (options.storeUrls) {
|
|
69
|
+
if (!config) {
|
|
70
|
+
config = {};
|
|
71
|
+
}
|
|
72
|
+
config.storeUrls = options.storeUrls;
|
|
73
|
+
}
|
|
74
|
+
// 将选择的主题合并到配置
|
|
75
|
+
if (options.selectedThemes && options.selectedThemes.length > 0) {
|
|
76
|
+
if (!config) {
|
|
77
|
+
config = {};
|
|
78
|
+
}
|
|
79
|
+
config.themes = {
|
|
80
|
+
enabled: true,
|
|
81
|
+
whitelist: options.selectedThemes,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
35
86
|
/**
|
|
36
87
|
* 检测项目场景列表(支持 PlayCanvas 和 PlayCraft 两种格式)
|
|
37
88
|
*/
|
|
@@ -43,7 +94,6 @@ async function detectProjectScenes(projectPath) {
|
|
|
43
94
|
const manifestContent = await fs.readFile(manifestJsonPath, 'utf-8');
|
|
44
95
|
const manifestData = JSON.parse(manifestContent);
|
|
45
96
|
if (manifestData.scenes && Array.isArray(manifestData.scenes)) {
|
|
46
|
-
// PlayCraft manifest.json 中的场景列表
|
|
47
97
|
const scenes = manifestData.scenes.map((scene) => ({
|
|
48
98
|
id: String(scene.id || scene.name),
|
|
49
99
|
name: scene.name || `Scene ${scene.id}`,
|
|
@@ -201,25 +251,43 @@ export async function buildCommand(projectPath, options) {
|
|
|
201
251
|
// 清理选项(默认为 false,使用覆盖模式)
|
|
202
252
|
const shouldClean = options.clean === true;
|
|
203
253
|
try {
|
|
204
|
-
// ======
|
|
254
|
+
// ====== 引擎 + 构建工具检测 ======
|
|
205
255
|
spinner.text = pc.cyan('检测项目引擎类型...');
|
|
206
256
|
let detectedEngine;
|
|
207
|
-
|
|
257
|
+
let detectionResult;
|
|
258
|
+
if (options.usePlayableScripts) {
|
|
259
|
+
// 用户显式指定 --use-playable-scripts
|
|
260
|
+
detectionResult = { engine: 'generic', buildTool: 'playable-scripts' };
|
|
261
|
+
detectedEngine = 'generic';
|
|
262
|
+
console.log(pc.cyan(`\n🔧 使用 @playcraft/devkit 构建工具(用户显式指定)`));
|
|
263
|
+
}
|
|
264
|
+
else if (options.engine) {
|
|
208
265
|
// 用户指定了引擎类型
|
|
209
266
|
detectedEngine = options.engine;
|
|
267
|
+
detectionResult = {
|
|
268
|
+
engine: detectedEngine,
|
|
269
|
+
buildTool: detectedEngine === 'playcanvas' ? 'playcanvas-native' : 'generic',
|
|
270
|
+
};
|
|
210
271
|
console.log(pc.dim(`\nℹ️ 使用指定引擎: ${detectedEngine}`));
|
|
211
272
|
}
|
|
212
273
|
else {
|
|
213
|
-
// 自动检测引擎类型
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
274
|
+
// 自动检测引擎类型 + 构建工具
|
|
275
|
+
detectionResult = await EngineDetector.detectFull(resolvedProjectPath);
|
|
276
|
+
detectedEngine = detectionResult.engine;
|
|
277
|
+
console.log(pc.dim(`\nℹ️ 检测到引擎: ${detectedEngine}, 构建工具: ${detectionResult.buildTool}`));
|
|
278
|
+
// 对于 playable-scripts 项目,显示专用提示
|
|
279
|
+
if (detectionResult.buildTool === 'playable-scripts') {
|
|
280
|
+
console.log(pc.cyan(`\n🔧 检测到 @playcraft/devkit 构建工具`));
|
|
281
|
+
console.log(pc.dim(' 将使用 playable-scripts 进行构建(产物已含渠道适配和混淆压缩)'));
|
|
282
|
+
}
|
|
283
|
+
else if (detectedEngine !== 'playcanvas') {
|
|
284
|
+
// 对于外部引擎(非 playable-scripts),显示普通提示
|
|
218
285
|
console.log(pc.cyan(`\n🎮 检测到 ${detectedEngine} 项目`));
|
|
219
286
|
console.log(pc.dim(' 将使用 npm install + npm run build 进行构建'));
|
|
220
287
|
console.log(pc.yellow(' ⚠️ 构建时间可能较长(约 2-5 分钟)\n'));
|
|
221
288
|
}
|
|
222
289
|
}
|
|
290
|
+
const isPlayableScripts = detectionResult.buildTool === 'playable-scripts';
|
|
223
291
|
const isPlayCanvas = detectedEngine === 'playcanvas';
|
|
224
292
|
const requiresNpmBuild = detectedEngine !== 'playcanvas';
|
|
225
293
|
if (!options.baseOnly && !options.mode) {
|
|
@@ -262,6 +330,8 @@ export async function buildCommand(projectPath, options) {
|
|
|
262
330
|
}
|
|
263
331
|
// 解析场景选择参数(支持 PlayCanvas 和 PlayCraft 项目)
|
|
264
332
|
let selectedScenes;
|
|
333
|
+
// 主题选择(仅用于 playable-scripts 项目)
|
|
334
|
+
let selectedThemes;
|
|
265
335
|
if (isPlayCanvas) {
|
|
266
336
|
if (options.scenes) {
|
|
267
337
|
// 命令行指定了场景参数
|
|
@@ -310,7 +380,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
310
380
|
choices: projectScenes.map(scene => ({
|
|
311
381
|
name: scene.isMain ? `${scene.name} (主场景)` : scene.name,
|
|
312
382
|
value: scene.name,
|
|
313
|
-
checked: scene.isMain === true || !projectScenes.some(s => s.isMain)
|
|
383
|
+
checked: scene.isMain === true || !projectScenes.some(s => s.isMain)
|
|
314
384
|
})),
|
|
315
385
|
validate: (answer) => {
|
|
316
386
|
if (answer.length === 0) {
|
|
@@ -352,12 +422,21 @@ export async function buildCommand(projectPath, options) {
|
|
|
352
422
|
console.log(pc.dim(`\nℹ️ 使用覆盖模式,新文件将覆盖旧文件(使用 --clean 可清理旧目录)`));
|
|
353
423
|
}
|
|
354
424
|
spinner.text = pc.cyan('执行阶段1: 基础构建...');
|
|
425
|
+
// 构建 playableScriptsConfig,合并 CLI 选择的参数
|
|
426
|
+
const playableScriptsConfig = buildPlayableScriptsConfig(fileConfig?.playableScripts, {
|
|
427
|
+
platform: options.platform,
|
|
428
|
+
storeUrls: options.storeUrls,
|
|
429
|
+
selectedThemes,
|
|
430
|
+
});
|
|
355
431
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
356
432
|
outputDir: baseBuildOutputDir,
|
|
357
433
|
selectedScenes,
|
|
358
434
|
clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
|
|
359
435
|
analyze: options.analyze, // 传递 analyze 参数
|
|
360
436
|
engine: detectedEngine, // 传递引擎类型
|
|
437
|
+
}, {
|
|
438
|
+
usePlayableScripts: isPlayableScripts,
|
|
439
|
+
playableScriptsConfig,
|
|
361
440
|
});
|
|
362
441
|
const baseBuild = await baseBuilder.build();
|
|
363
442
|
spinner.succeed(pc.green('✅ 基础构建完成!'));
|
|
@@ -386,8 +465,8 @@ export async function buildCommand(projectPath, options) {
|
|
|
386
465
|
];
|
|
387
466
|
// 渠道输出格式配置(根据各渠道Playable规格对照表)
|
|
388
467
|
// 只支持 HTML: applovin, ironsource, unity, moloco, adikteev, remerge
|
|
389
|
-
// 只支持 ZIP:
|
|
390
|
-
// 支持两种: facebook
|
|
468
|
+
// 只支持 ZIP: tiktok, liftoff, bigo, snapchat, inmobi
|
|
469
|
+
// 支持两种: facebook, google
|
|
391
470
|
const platformFormatConfig = {
|
|
392
471
|
facebook: {
|
|
393
472
|
formats: ['html', 'zip'],
|
|
@@ -400,7 +479,11 @@ export async function buildCommand(projectPath, options) {
|
|
|
400
479
|
moloco: { formats: ['html'], default: 'html' },
|
|
401
480
|
adikteev: { formats: ['html'], default: 'html' },
|
|
402
481
|
remerge: { formats: ['html'], default: 'html' },
|
|
403
|
-
google: {
|
|
482
|
+
google: {
|
|
483
|
+
formats: ['html', 'zip'],
|
|
484
|
+
default: 'html',
|
|
485
|
+
description: 'Google Ads 支持两种格式'
|
|
486
|
+
},
|
|
404
487
|
tiktok: { formats: ['zip'], default: 'zip' },
|
|
405
488
|
liftoff: { formats: ['zip'], default: 'zip' },
|
|
406
489
|
bigo: { formats: ['zip'], default: 'zip' },
|
|
@@ -444,10 +527,10 @@ export async function buildCommand(projectPath, options) {
|
|
|
444
527
|
: '选择输出格式:',
|
|
445
528
|
choices: formatConfig.formats.map(f => ({
|
|
446
529
|
name: f === 'html'
|
|
447
|
-
? (selectedPlatform === 'facebook'
|
|
530
|
+
? (selectedPlatform === 'facebook' || selectedPlatform === 'google'
|
|
448
531
|
? 'HTML(单文件,最大 5MB,所有资源内联)'
|
|
449
532
|
: 'HTML(单文件)')
|
|
450
|
-
: (selectedPlatform === 'facebook'
|
|
533
|
+
: (selectedPlatform === 'facebook' || selectedPlatform === 'google'
|
|
451
534
|
? 'ZIP(多文件,最大 5MB,HTML 文件需 < 2MB)'
|
|
452
535
|
: 'ZIP(多文件)'),
|
|
453
536
|
value: f,
|
|
@@ -457,6 +540,47 @@ export async function buildCommand(projectPath, options) {
|
|
|
457
540
|
]);
|
|
458
541
|
selectedFormat = formatAnswer.format;
|
|
459
542
|
}
|
|
543
|
+
// 如果是 playable-scripts 项目,交互式选择主题
|
|
544
|
+
if (isPlayableScripts) {
|
|
545
|
+
const themeDir = path.join(resolvedProjectPath, 'src', 'theme');
|
|
546
|
+
try {
|
|
547
|
+
const themeEntries = await fs.readdir(themeDir, { withFileTypes: true });
|
|
548
|
+
const availableThemes = themeEntries
|
|
549
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('tiles-'))
|
|
550
|
+
.map(e => e.name)
|
|
551
|
+
.sort();
|
|
552
|
+
if (availableThemes.length > 1) {
|
|
553
|
+
console.log(pc.cyan('\n🎨 选择要构建的主题:'));
|
|
554
|
+
const themeAnswer = await inquirer.prompt([
|
|
555
|
+
{
|
|
556
|
+
type: 'list',
|
|
557
|
+
name: 'theme',
|
|
558
|
+
message: '选择主题:',
|
|
559
|
+
choices: [
|
|
560
|
+
{ name: '📌 使用当前默认主题 (src/theme/index.ts)', value: '__default__' },
|
|
561
|
+
...availableThemes.map(t => ({ name: `🎨 ${t}`, value: t })),
|
|
562
|
+
],
|
|
563
|
+
default: '__default__',
|
|
564
|
+
},
|
|
565
|
+
]);
|
|
566
|
+
// 如果用户选择了具体主题(而非默认)
|
|
567
|
+
if (themeAnswer.theme !== '__default__') {
|
|
568
|
+
selectedThemes = [themeAnswer.theme];
|
|
569
|
+
console.log(pc.green(`✅ 已选择主题: ${themeAnswer.theme}`));
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
console.log(pc.dim(' 使用当前默认主题'));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else if (availableThemes.length === 1) {
|
|
576
|
+
console.log(pc.dim(`\n🎨 检测到 1 个主题: ${availableThemes[0]}`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
// 无主题目录,跳过
|
|
581
|
+
console.log(pc.dim(' 无主题目录,跳过主题选择'));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
460
584
|
// 输入商店跳转地址(CTA)- 如果配置文件中没有则必填
|
|
461
585
|
if (!options.storeUrls) {
|
|
462
586
|
console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
|
|
@@ -610,15 +734,68 @@ export async function buildCommand(projectPath, options) {
|
|
|
610
734
|
spinner.text = pc.cyan('执行阶段1: 基础构建...');
|
|
611
735
|
// 将临时目录放在项目所在目录下,避免跨盘符路径问题(Windows 上 C:\Temp 和 D:\project 会导致 Vite 路径计算错误)
|
|
612
736
|
const tempDir = path.join(resolvedProjectPath, '.playcraft-temp', `base-build-${Date.now()}`);
|
|
737
|
+
// 构建 playableScriptsConfig,合并 CLI 选择的参数
|
|
738
|
+
const playableScriptsConfig = buildPlayableScriptsConfig(fileConfig?.playableScripts, {
|
|
739
|
+
platform: options.platform,
|
|
740
|
+
storeUrls: options.storeUrls,
|
|
741
|
+
selectedThemes,
|
|
742
|
+
});
|
|
613
743
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
614
744
|
outputDir: tempDir,
|
|
615
745
|
selectedScenes,
|
|
616
746
|
analyze: buildOptions.analyze,
|
|
617
747
|
analyzeReportPath: buildOptions.analyze ? 'base-bundle-report.html' : undefined,
|
|
618
748
|
engine: detectedEngine, // 传递引擎类型
|
|
749
|
+
}, {
|
|
750
|
+
usePlayableScripts: isPlayableScripts,
|
|
751
|
+
playableScriptsConfig,
|
|
619
752
|
});
|
|
620
753
|
const baseBuild = await baseBuilder.build();
|
|
621
754
|
baseBuildDir = baseBuild.outputDir;
|
|
755
|
+
// 如果是 playable-scripts 构建,产物已是最终格式,跳过阶段2
|
|
756
|
+
if (baseBuild.metadata.skipChannelBuild) {
|
|
757
|
+
spinner.succeed(pc.green('📦 @playcraft/devkit 构建完成!'));
|
|
758
|
+
// 将产物从临时目录复制到用户指定的输出目录
|
|
759
|
+
const finalOutputDir = buildOptions.outputDir || path.resolve(options.output || './dist');
|
|
760
|
+
await fs.mkdir(finalOutputDir, { recursive: true });
|
|
761
|
+
// 复制所有产物到最终输出目录,保留相对目录结构
|
|
762
|
+
for (const asset of baseBuild.files.assets) {
|
|
763
|
+
// 计算相对于基础构建输出目录的相对路径,保留目录结构
|
|
764
|
+
const relativePath = path.relative(baseBuildDir, asset);
|
|
765
|
+
const destPath = path.join(finalOutputDir, relativePath);
|
|
766
|
+
// 确保目标目录存在
|
|
767
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
768
|
+
await fs.copyFile(asset, destPath);
|
|
769
|
+
}
|
|
770
|
+
// 显示产物信息
|
|
771
|
+
console.log('\n' + pc.bold('构建产物:'));
|
|
772
|
+
for (const asset of baseBuild.files.assets) {
|
|
773
|
+
// 使用相对路径显示,保持目录结构可见
|
|
774
|
+
const relativePath = path.relative(baseBuildDir, asset);
|
|
775
|
+
const finalPath = path.join(finalOutputDir, relativePath);
|
|
776
|
+
try {
|
|
777
|
+
const stat = await fs.stat(finalPath);
|
|
778
|
+
const sizeMB = (stat.size / 1024 / 1024).toFixed(2);
|
|
779
|
+
console.log(` - ${relativePath}: ${sizeMB} MB`);
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
console.log(` - ${relativePath}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
console.log('\n' + pc.green(`输出目录: ${finalOutputDir}`));
|
|
786
|
+
console.log(pc.dim('💡 产物已包含渠道适配、MRAID 注入、代码混淆和 fflate 压缩'));
|
|
787
|
+
// 清理临时目录
|
|
788
|
+
try {
|
|
789
|
+
const tempBaseDir = path.join(resolvedProjectPath, '.playcraft-temp');
|
|
790
|
+
await fs.rm(tempBaseDir, { recursive: true, force: true });
|
|
791
|
+
console.log(pc.dim(`\n🗑️ 已清理临时目录`));
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
// 记录清理错误信息
|
|
795
|
+
console.log(pc.dim(`\n⚠️ 清理临时目录失败: ${error instanceof Error ? error.message : String(error)}`));
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
622
799
|
spinner.text = pc.cyan('✅ 阶段1完成,开始阶段2...');
|
|
623
800
|
}
|
|
624
801
|
// Ammo 检测和替换(仅 PlayCanvas 项目)
|