@playcraft/cli 0.0.18 → 0.0.21
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/build-config.js +2 -2
- 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 +217 -17
- package/dist/commands/image.js +470 -0
- package/dist/commands/tools.js +447 -0
- package/dist/index.js +24 -3
- 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 +9 -4
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,12 +330,37 @@ 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
|
// 命令行指定了场景参数
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
338
|
+
const scenesInput = options.scenes.trim().toLowerCase();
|
|
339
|
+
// 检查特殊关键字
|
|
340
|
+
if (scenesInput === 'all') {
|
|
341
|
+
// 打包所有场景
|
|
342
|
+
selectedScenes = undefined; // undefined 表示所有场景
|
|
343
|
+
console.log(`\n🎬 选中场景: 全部场景`);
|
|
344
|
+
}
|
|
345
|
+
else if (scenesInput === 'default') {
|
|
346
|
+
// 打包默认场景
|
|
347
|
+
const projectScenes = await detectProjectScenes(resolvedProjectPath);
|
|
348
|
+
const defaultScene = projectScenes.find(s => s.isMain === true) || projectScenes[0];
|
|
349
|
+
if (defaultScene) {
|
|
350
|
+
selectedScenes = [defaultScene.name || String(defaultScene.id)];
|
|
351
|
+
console.log(`\n🎬 选中场景: ${defaultScene.name || defaultScene.id}${defaultScene.isMain ? ' (默认场景)' : ' (第一个场景)'}`);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
console.log(`\n⚠️ 未找到默认场景,将打包所有场景`);
|
|
355
|
+
selectedScenes = undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// 普通场景名称列表
|
|
360
|
+
selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
|
|
361
|
+
if (selectedScenes.length > 0) {
|
|
362
|
+
console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
|
|
363
|
+
}
|
|
271
364
|
}
|
|
272
365
|
}
|
|
273
366
|
else if (options.mode !== 'playable') {
|
|
@@ -287,7 +380,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
287
380
|
choices: projectScenes.map(scene => ({
|
|
288
381
|
name: scene.isMain ? `${scene.name} (主场景)` : scene.name,
|
|
289
382
|
value: scene.name,
|
|
290
|
-
checked: scene.isMain === true || !projectScenes.some(s => s.isMain)
|
|
383
|
+
checked: scene.isMain === true || !projectScenes.some(s => s.isMain)
|
|
291
384
|
})),
|
|
292
385
|
validate: (answer) => {
|
|
293
386
|
if (answer.length === 0) {
|
|
@@ -329,12 +422,21 @@ export async function buildCommand(projectPath, options) {
|
|
|
329
422
|
console.log(pc.dim(`\nℹ️ 使用覆盖模式,新文件将覆盖旧文件(使用 --clean 可清理旧目录)`));
|
|
330
423
|
}
|
|
331
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
|
+
});
|
|
332
431
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
333
432
|
outputDir: baseBuildOutputDir,
|
|
334
433
|
selectedScenes,
|
|
335
434
|
clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
|
|
336
435
|
analyze: options.analyze, // 传递 analyze 参数
|
|
337
436
|
engine: detectedEngine, // 传递引擎类型
|
|
437
|
+
}, {
|
|
438
|
+
usePlayableScripts: isPlayableScripts,
|
|
439
|
+
playableScriptsConfig,
|
|
338
440
|
});
|
|
339
441
|
const baseBuild = await baseBuilder.build();
|
|
340
442
|
spinner.succeed(pc.green('✅ 基础构建完成!'));
|
|
@@ -363,8 +465,8 @@ export async function buildCommand(projectPath, options) {
|
|
|
363
465
|
];
|
|
364
466
|
// 渠道输出格式配置(根据各渠道Playable规格对照表)
|
|
365
467
|
// 只支持 HTML: applovin, ironsource, unity, moloco, adikteev, remerge
|
|
366
|
-
// 只支持 ZIP:
|
|
367
|
-
// 支持两种: facebook
|
|
468
|
+
// 只支持 ZIP: tiktok, liftoff, bigo, snapchat, inmobi
|
|
469
|
+
// 支持两种: facebook, google
|
|
368
470
|
const platformFormatConfig = {
|
|
369
471
|
facebook: {
|
|
370
472
|
formats: ['html', 'zip'],
|
|
@@ -377,7 +479,11 @@ export async function buildCommand(projectPath, options) {
|
|
|
377
479
|
moloco: { formats: ['html'], default: 'html' },
|
|
378
480
|
adikteev: { formats: ['html'], default: 'html' },
|
|
379
481
|
remerge: { formats: ['html'], default: 'html' },
|
|
380
|
-
google: {
|
|
482
|
+
google: {
|
|
483
|
+
formats: ['html', 'zip'],
|
|
484
|
+
default: 'html',
|
|
485
|
+
description: 'Google Ads 支持两种格式'
|
|
486
|
+
},
|
|
381
487
|
tiktok: { formats: ['zip'], default: 'zip' },
|
|
382
488
|
liftoff: { formats: ['zip'], default: 'zip' },
|
|
383
489
|
bigo: { formats: ['zip'], default: 'zip' },
|
|
@@ -421,10 +527,10 @@ export async function buildCommand(projectPath, options) {
|
|
|
421
527
|
: '选择输出格式:',
|
|
422
528
|
choices: formatConfig.formats.map(f => ({
|
|
423
529
|
name: f === 'html'
|
|
424
|
-
? (selectedPlatform === 'facebook'
|
|
530
|
+
? (selectedPlatform === 'facebook' || selectedPlatform === 'google'
|
|
425
531
|
? 'HTML(单文件,最大 5MB,所有资源内联)'
|
|
426
532
|
: 'HTML(单文件)')
|
|
427
|
-
: (selectedPlatform === 'facebook'
|
|
533
|
+
: (selectedPlatform === 'facebook' || selectedPlatform === 'google'
|
|
428
534
|
? 'ZIP(多文件,最大 5MB,HTML 文件需 < 2MB)'
|
|
429
535
|
: 'ZIP(多文件)'),
|
|
430
536
|
value: f,
|
|
@@ -434,6 +540,47 @@ export async function buildCommand(projectPath, options) {
|
|
|
434
540
|
]);
|
|
435
541
|
selectedFormat = formatAnswer.format;
|
|
436
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
|
+
}
|
|
437
584
|
// 输入商店跳转地址(CTA)- 如果配置文件中没有则必填
|
|
438
585
|
if (!options.storeUrls) {
|
|
439
586
|
console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
|
|
@@ -587,15 +734,68 @@ export async function buildCommand(projectPath, options) {
|
|
|
587
734
|
spinner.text = pc.cyan('执行阶段1: 基础构建...');
|
|
588
735
|
// 将临时目录放在项目所在目录下,避免跨盘符路径问题(Windows 上 C:\Temp 和 D:\project 会导致 Vite 路径计算错误)
|
|
589
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
|
+
});
|
|
590
743
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
591
744
|
outputDir: tempDir,
|
|
592
745
|
selectedScenes,
|
|
593
746
|
analyze: buildOptions.analyze,
|
|
594
747
|
analyzeReportPath: buildOptions.analyze ? 'base-bundle-report.html' : undefined,
|
|
595
748
|
engine: detectedEngine, // 传递引擎类型
|
|
749
|
+
}, {
|
|
750
|
+
usePlayableScripts: isPlayableScripts,
|
|
751
|
+
playableScriptsConfig,
|
|
596
752
|
});
|
|
597
753
|
const baseBuild = await baseBuilder.build();
|
|
598
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
|
+
}
|
|
599
799
|
spinner.text = pc.cyan('✅ 阶段1完成,开始阶段2...');
|
|
600
800
|
}
|
|
601
801
|
// Ammo 检测和替换(仅 PlayCanvas 项目)
|