@playcraft/cli 0.0.15 → 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.
- 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/commands/build-all.js +477 -0
- package/dist/commands/build.js +238 -178
- package/dist/fs-handler.js +117 -0
- package/dist/index.js +57 -15
- 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/server.js +23 -6
- package/dist/socket.js +7 -2
- package/dist/watcher.js +27 -1
- package/package.json +3 -3
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { BaseBuilder, ViteBuilder, PlayableBuilder, detectAmmoUsage } from '@playcraft/build';
|
|
6
|
+
import { loadBuildConfig } from '../build-config.js';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
/**
|
|
9
|
+
* 所有可用的渠道平台
|
|
10
|
+
*/
|
|
11
|
+
const ALL_PLATFORMS = [
|
|
12
|
+
'facebook',
|
|
13
|
+
'snapchat',
|
|
14
|
+
'ironsource',
|
|
15
|
+
'applovin',
|
|
16
|
+
'google',
|
|
17
|
+
'tiktok',
|
|
18
|
+
'unity',
|
|
19
|
+
'liftoff',
|
|
20
|
+
'moloco',
|
|
21
|
+
'bigo',
|
|
22
|
+
'inmobi',
|
|
23
|
+
'adikteev',
|
|
24
|
+
'remerge',
|
|
25
|
+
'mintegral',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* 渠道分组(方便用户快速选择)
|
|
29
|
+
*/
|
|
30
|
+
const PLATFORM_GROUPS = {
|
|
31
|
+
all: {
|
|
32
|
+
name: '全部渠道',
|
|
33
|
+
platforms: [...ALL_PLATFORMS],
|
|
34
|
+
},
|
|
35
|
+
html: {
|
|
36
|
+
name: '仅 HTML 渠道 (applovin, ironsource, unity, moloco, adikteev, remerge)',
|
|
37
|
+
platforms: ['applovin', 'ironsource', 'unity', 'moloco', 'adikteev', 'remerge'],
|
|
38
|
+
},
|
|
39
|
+
zip: {
|
|
40
|
+
name: '仅 ZIP 渠道 (google, tiktok, liftoff, bigo, snapchat, inmobi, mintegral)',
|
|
41
|
+
platforms: ['google', 'tiktok', 'liftoff', 'bigo', 'snapchat', 'inmobi', 'mintegral'],
|
|
42
|
+
},
|
|
43
|
+
mainstream: {
|
|
44
|
+
name: '主流渠道 (facebook, google, tiktok, applovin, ironsource, unity)',
|
|
45
|
+
platforms: ['facebook', 'google', 'tiktok', 'applovin', 'ironsource', 'unity'],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* 渠道默认输出格式配置
|
|
50
|
+
*/
|
|
51
|
+
const platformFormatConfig = {
|
|
52
|
+
facebook: { formats: ['html', 'zip'], default: 'html' },
|
|
53
|
+
applovin: { formats: ['html'], default: 'html' },
|
|
54
|
+
ironsource: { formats: ['html'], default: 'html' },
|
|
55
|
+
unity: { formats: ['html'], default: 'html' },
|
|
56
|
+
moloco: { formats: ['html'], default: 'html' },
|
|
57
|
+
adikteev: { formats: ['html'], default: 'html' },
|
|
58
|
+
remerge: { formats: ['html'], default: 'html' },
|
|
59
|
+
google: { formats: ['zip'], default: 'zip' },
|
|
60
|
+
tiktok: { formats: ['zip'], default: 'zip' },
|
|
61
|
+
liftoff: { formats: ['zip'], default: 'zip' },
|
|
62
|
+
bigo: { formats: ['zip'], default: 'zip' },
|
|
63
|
+
snapchat: { formats: ['zip'], default: 'zip' },
|
|
64
|
+
inmobi: { formats: ['zip'], default: 'zip' },
|
|
65
|
+
mintegral: { formats: ['zip'], default: 'zip' },
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 检测是否是基础构建产物(多文件版本)
|
|
69
|
+
*/
|
|
70
|
+
async function detectBaseBuild(dir) {
|
|
71
|
+
const baseIndicators = [
|
|
72
|
+
path.join(dir, 'index.html'),
|
|
73
|
+
path.join(dir, 'config.json'),
|
|
74
|
+
];
|
|
75
|
+
const classicIndicator = path.join(dir, '__start__.js');
|
|
76
|
+
const esmIndicator = path.join(dir, 'js/index.mjs');
|
|
77
|
+
try {
|
|
78
|
+
for (const indicator of baseIndicators) {
|
|
79
|
+
await fs.access(indicator);
|
|
80
|
+
}
|
|
81
|
+
const isClassic = await fs.access(classicIndicator).then(() => true).catch(() => false);
|
|
82
|
+
const isESM = await fs.access(esmIndicator).then(() => true).catch(() => false);
|
|
83
|
+
return isClassic || isESM;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 检测项目场景列表
|
|
91
|
+
*/
|
|
92
|
+
async function detectProjectScenes(projectPath) {
|
|
93
|
+
try {
|
|
94
|
+
const scenesJsonPath = path.join(projectPath, 'scenes.json');
|
|
95
|
+
try {
|
|
96
|
+
const scenesContent = await fs.readFile(scenesJsonPath, 'utf-8');
|
|
97
|
+
const scenesData = JSON.parse(scenesContent);
|
|
98
|
+
if (typeof scenesData === 'object' && !Array.isArray(scenesData)) {
|
|
99
|
+
return Object.entries(scenesData).map(([id, scene]) => ({
|
|
100
|
+
id: scene.id || scene.scene || id,
|
|
101
|
+
name: scene.name || `Scene ${id}`,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(scenesData)) {
|
|
105
|
+
return scenesData.map(scene => ({
|
|
106
|
+
id: scene.id || scene.scene || scene.uniqueId,
|
|
107
|
+
name: scene.name || `Scene ${scene.id}`,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// scenes.json 不存在或解析失败
|
|
113
|
+
}
|
|
114
|
+
const projectJsonPath = path.join(projectPath, 'project.json');
|
|
115
|
+
try {
|
|
116
|
+
const projectContent = await fs.readFile(projectJsonPath, 'utf-8');
|
|
117
|
+
const projectData = JSON.parse(projectContent);
|
|
118
|
+
if (projectData.scenes && Array.isArray(projectData.scenes)) {
|
|
119
|
+
return projectData.scenes.map((scene) => ({
|
|
120
|
+
id: scene.id || scene.scene || scene.uniqueId,
|
|
121
|
+
name: scene.name || `Scene ${scene.id}`,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// project.json 不存在或解析失败
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 批量打渠道包命令
|
|
136
|
+
*
|
|
137
|
+
* 核心思路:执行一次 Base Build(阶段1),然后对多个渠道分别执行 Channel Build(阶段2)
|
|
138
|
+
*/
|
|
139
|
+
export async function buildAllCommand(projectPath, options) {
|
|
140
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
141
|
+
const spinner = ora(pc.cyan('🚀 批量打渠道包...')).start();
|
|
142
|
+
let isBaseBuild = false;
|
|
143
|
+
let baseBuildDir;
|
|
144
|
+
try {
|
|
145
|
+
// 1. 确定要构建的平台列表
|
|
146
|
+
let targetPlatforms;
|
|
147
|
+
if (options.platforms) {
|
|
148
|
+
// 命令行指定了平台列表
|
|
149
|
+
targetPlatforms = options.platforms.split(',').map(p => p.trim()).filter(p => ALL_PLATFORMS.includes(p));
|
|
150
|
+
if (targetPlatforms.length === 0) {
|
|
151
|
+
throw new Error(`无效的平台列表: ${options.platforms}\n可用平台: ${ALL_PLATFORMS.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (options.group && PLATFORM_GROUPS[options.group]) {
|
|
155
|
+
// 使用预定义分组
|
|
156
|
+
targetPlatforms = PLATFORM_GROUPS[options.group].platforms;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// 交互式选择
|
|
160
|
+
spinner.stop();
|
|
161
|
+
const groupAnswer = await inquirer.prompt([
|
|
162
|
+
{
|
|
163
|
+
type: 'list',
|
|
164
|
+
name: 'selection',
|
|
165
|
+
message: '选择要批量构建的渠道:',
|
|
166
|
+
choices: [
|
|
167
|
+
{ name: '🌐 全部渠道(13个)', value: 'all' },
|
|
168
|
+
{ name: '⭐ 主流渠道(facebook, google, tiktok, applovin, ironsource, unity)', value: 'mainstream' },
|
|
169
|
+
{ name: '📄 仅 HTML 渠道(applovin, ironsource, unity, moloco, adikteev, remerge)', value: 'html' },
|
|
170
|
+
{ name: '📦 仅 ZIP 渠道(google, tiktok, liftoff, bigo, snapchat, inmobi)', value: 'zip' },
|
|
171
|
+
{ name: '🔧 自定义选择', value: 'custom' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
if (groupAnswer.selection === 'custom') {
|
|
176
|
+
const customAnswer = await inquirer.prompt([
|
|
177
|
+
{
|
|
178
|
+
type: 'checkbox',
|
|
179
|
+
name: 'platforms',
|
|
180
|
+
message: '选择要构建的渠道(空格选择,回车确认):',
|
|
181
|
+
choices: ALL_PLATFORMS.map(p => {
|
|
182
|
+
const fmt = platformFormatConfig[p];
|
|
183
|
+
const formatLabel = fmt.formats.length === 1 ? fmt.default.toUpperCase() : 'HTML/ZIP';
|
|
184
|
+
return { name: `${p} (${formatLabel})`, value: p };
|
|
185
|
+
}),
|
|
186
|
+
validate: (answer) => {
|
|
187
|
+
if (answer.length === 0)
|
|
188
|
+
return '请至少选择一个渠道';
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
targetPlatforms = customAnswer.platforms;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
targetPlatforms = PLATFORM_GROUPS[groupAnswer.selection].platforms;
|
|
197
|
+
}
|
|
198
|
+
spinner.start(pc.cyan('🚀 批量打渠道包...'));
|
|
199
|
+
}
|
|
200
|
+
console.log(pc.cyan(`\n📋 目标渠道 (${targetPlatforms.length}个): ${targetPlatforms.join(', ')}`));
|
|
201
|
+
// 2. 加载构建配置
|
|
202
|
+
const fileConfig = await loadBuildConfig({
|
|
203
|
+
configPath: options.config,
|
|
204
|
+
projectPath: resolvedProjectPath,
|
|
205
|
+
});
|
|
206
|
+
// 3. 解析场景选择
|
|
207
|
+
let selectedScenes;
|
|
208
|
+
if (options.scenes) {
|
|
209
|
+
selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
|
|
210
|
+
if (selectedScenes.length > 0) {
|
|
211
|
+
console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
spinner.stop();
|
|
216
|
+
const projectScenes = await detectProjectScenes(resolvedProjectPath);
|
|
217
|
+
if (projectScenes.length > 1) {
|
|
218
|
+
console.log(pc.cyan(`\n🎬 检测到 ${projectScenes.length} 个场景`));
|
|
219
|
+
console.log(pc.dim('💡 提示: 只打包选中的场景可以显著减小文件大小\n'));
|
|
220
|
+
const sceneAnswer = await inquirer.prompt([
|
|
221
|
+
{
|
|
222
|
+
type: 'checkbox',
|
|
223
|
+
name: 'selectedScenes',
|
|
224
|
+
message: '选择要打包的场景:',
|
|
225
|
+
choices: projectScenes.map(scene => ({
|
|
226
|
+
name: scene.name,
|
|
227
|
+
value: scene.name,
|
|
228
|
+
checked: true,
|
|
229
|
+
})),
|
|
230
|
+
validate: (answer) => {
|
|
231
|
+
if (answer.length === 0)
|
|
232
|
+
return '请至少选择一个场景';
|
|
233
|
+
return true;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
selectedScenes = sceneAnswer.selectedScenes;
|
|
238
|
+
if (selectedScenes && selectedScenes.length === projectScenes.length) {
|
|
239
|
+
selectedScenes = undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
spinner.start(pc.cyan('🚀 批量打渠道包...'));
|
|
243
|
+
}
|
|
244
|
+
// 3.5 输入商店跳转地址(CTA)- 必填
|
|
245
|
+
if (!options.iosStoreUrl || !options.androidStoreUrl) {
|
|
246
|
+
spinner.stop();
|
|
247
|
+
console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
|
|
248
|
+
console.log(pc.dim(' iOS 和 Android 地址均为必填'));
|
|
249
|
+
const storeUrlAnswer = await inquirer.prompt([
|
|
250
|
+
{
|
|
251
|
+
type: 'input',
|
|
252
|
+
name: 'iosStoreUrl',
|
|
253
|
+
message: 'iOS App Store URL:',
|
|
254
|
+
default: options.iosStoreUrl || '',
|
|
255
|
+
validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
type: 'input',
|
|
259
|
+
name: 'androidStoreUrl',
|
|
260
|
+
message: 'Android Google Play URL:',
|
|
261
|
+
default: options.androidStoreUrl || '',
|
|
262
|
+
validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
options.iosStoreUrl = storeUrlAnswer.iosStoreUrl.trim();
|
|
266
|
+
options.androidStoreUrl = storeUrlAnswer.androidStoreUrl.trim();
|
|
267
|
+
spinner.start(pc.cyan('🚀 批量打渠道包...'));
|
|
268
|
+
}
|
|
269
|
+
// 4. 执行阶段1:Base Build(只执行一次)
|
|
270
|
+
isBaseBuild = await detectBaseBuild(resolvedProjectPath);
|
|
271
|
+
if (isBaseBuild) {
|
|
272
|
+
spinner.text = pc.cyan('✅ 检测到多文件构建产物,跳过阶段1');
|
|
273
|
+
baseBuildDir = resolvedProjectPath;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
spinner.text = pc.cyan('执行阶段1: 基础构建(仅一次)...');
|
|
277
|
+
const tempDir = path.join(resolvedProjectPath, '.playcraft-temp', `base-build-${Date.now()}`);
|
|
278
|
+
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
279
|
+
outputDir: tempDir,
|
|
280
|
+
selectedScenes,
|
|
281
|
+
analyze: options.analyze,
|
|
282
|
+
});
|
|
283
|
+
const baseBuild = await baseBuilder.build();
|
|
284
|
+
baseBuildDir = baseBuild.outputDir;
|
|
285
|
+
spinner.succeed(pc.green('✅ 阶段1: 基础构建完成'));
|
|
286
|
+
spinner.start(pc.cyan('开始阶段2: 批量渠道打包...'));
|
|
287
|
+
}
|
|
288
|
+
// 5. 检测 Ammo.js 并处理
|
|
289
|
+
const ammoCheck = await detectAmmoUsage(baseBuildDir);
|
|
290
|
+
let ammoReplacement;
|
|
291
|
+
if (options.ammoEngine === 'p2' || options.ammoEngine === 'cannon') {
|
|
292
|
+
ammoReplacement = options.ammoEngine;
|
|
293
|
+
}
|
|
294
|
+
else if (options.replaceAmmo && ammoCheck.hasAmmo) {
|
|
295
|
+
ammoReplacement = ammoCheck.suggestedEngine;
|
|
296
|
+
}
|
|
297
|
+
else if (ammoCheck.hasAmmo) {
|
|
298
|
+
spinner.stop();
|
|
299
|
+
const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
|
|
300
|
+
const replaceAnswer = await inquirer.prompt([
|
|
301
|
+
{
|
|
302
|
+
type: 'list',
|
|
303
|
+
name: 'ammoAction',
|
|
304
|
+
message: `检测到 Ammo.js(${physicsType} 物理),如何处理?`,
|
|
305
|
+
choices: ammoCheck.use3dPhysics
|
|
306
|
+
? [
|
|
307
|
+
{ name: '保留 Ammo.js', value: 'keep' },
|
|
308
|
+
{ name: '替换为 Cannon.js(推荐 3D 项目)', value: 'cannon' },
|
|
309
|
+
]
|
|
310
|
+
: [
|
|
311
|
+
{ name: '保留 Ammo.js', value: 'keep' },
|
|
312
|
+
{ name: '替换为 p2.js(推荐 2D 项目)', value: 'p2' },
|
|
313
|
+
],
|
|
314
|
+
default: ammoCheck.use3dPhysics ? 'cannon' : 'p2',
|
|
315
|
+
},
|
|
316
|
+
]);
|
|
317
|
+
if (replaceAnswer.ammoAction !== 'keep') {
|
|
318
|
+
ammoReplacement = replaceAnswer.ammoAction;
|
|
319
|
+
}
|
|
320
|
+
spinner.start(pc.cyan('开始阶段2: 批量渠道打包...'));
|
|
321
|
+
}
|
|
322
|
+
// 6. 逐个渠道执行阶段2
|
|
323
|
+
const outputBaseDir = path.resolve(options.output || './dist');
|
|
324
|
+
const results = [];
|
|
325
|
+
const startTime = Date.now();
|
|
326
|
+
for (let i = 0; i < targetPlatforms.length; i++) {
|
|
327
|
+
const platform = targetPlatforms[i];
|
|
328
|
+
const formatConfig = platformFormatConfig[platform];
|
|
329
|
+
const format = formatConfig?.default || 'html';
|
|
330
|
+
const platformOutputDir = path.join(outputBaseDir, platform);
|
|
331
|
+
const platformStartTime = Date.now();
|
|
332
|
+
spinner.text = pc.cyan(`[${i + 1}/${targetPlatforms.length}] 正在构建 ${platform} (${format})...`);
|
|
333
|
+
try {
|
|
334
|
+
const buildOptions = {
|
|
335
|
+
platform: platform,
|
|
336
|
+
format,
|
|
337
|
+
outputDir: platformOutputDir,
|
|
338
|
+
compressEngine: options.compress ?? true,
|
|
339
|
+
compressConfigJson: options.compressConfig ?? true,
|
|
340
|
+
compressJS: options.compressJs ?? true,
|
|
341
|
+
analyze: options.analyze || false,
|
|
342
|
+
useVite: options.useVite !== false,
|
|
343
|
+
cssMinify: options.cssMinify ?? true,
|
|
344
|
+
jsMinify: options.jsMinify ?? true,
|
|
345
|
+
compressImages: options.compressImages ?? true,
|
|
346
|
+
imageQuality: options.imageQuality,
|
|
347
|
+
convertToWebP: options.convertToWebP ?? true,
|
|
348
|
+
compressModels: options.compressModels ?? true,
|
|
349
|
+
modelCompression: options.modelCompression,
|
|
350
|
+
selectedScenes,
|
|
351
|
+
ammoReplacement,
|
|
352
|
+
...(options.esmMode ? { esmMode: options.esmMode } : {}),
|
|
353
|
+
};
|
|
354
|
+
// 透传商店跳转地址
|
|
355
|
+
if (options.iosStoreUrl || options.androidStoreUrl) {
|
|
356
|
+
buildOptions.storeUrls = {
|
|
357
|
+
...(options.iosStoreUrl ? { ios: options.iosStoreUrl } : {}),
|
|
358
|
+
...(options.androidStoreUrl ? { android: options.androidStoreUrl } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (fileConfig) {
|
|
362
|
+
// 合并文件配置,但不覆盖已设置的 platform/format/outputDir
|
|
363
|
+
const { platform: _p, format: _f, outputDir: _o, ...rest } = fileConfig;
|
|
364
|
+
Object.assign(buildOptions, rest);
|
|
365
|
+
// 恢复关键字段
|
|
366
|
+
buildOptions.platform = platform;
|
|
367
|
+
buildOptions.format = format;
|
|
368
|
+
buildOptions.outputDir = platformOutputDir;
|
|
369
|
+
}
|
|
370
|
+
let outputPath;
|
|
371
|
+
let sizeReport;
|
|
372
|
+
if (buildOptions.useVite !== false) {
|
|
373
|
+
const viteBuilder = new ViteBuilder(baseBuildDir, buildOptions);
|
|
374
|
+
outputPath = await viteBuilder.build();
|
|
375
|
+
sizeReport = viteBuilder.getSizeReport();
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const playableBuilder = new PlayableBuilder(baseBuildDir, buildOptions);
|
|
379
|
+
outputPath = await playableBuilder.build();
|
|
380
|
+
sizeReport = playableBuilder.getSizeReport();
|
|
381
|
+
}
|
|
382
|
+
const duration = Date.now() - platformStartTime;
|
|
383
|
+
results.push({
|
|
384
|
+
platform,
|
|
385
|
+
format,
|
|
386
|
+
success: true,
|
|
387
|
+
outputPath,
|
|
388
|
+
duration,
|
|
389
|
+
sizeReport,
|
|
390
|
+
});
|
|
391
|
+
const totalMB = (sizeReport.total / 1024 / 1024).toFixed(2);
|
|
392
|
+
const limitMB = (sizeReport.limit / 1024 / 1024).toFixed(2);
|
|
393
|
+
const sizeOk = sizeReport.total <= sizeReport.limit;
|
|
394
|
+
console.log(` ${sizeOk ? pc.green('✅') : pc.red('❌')} ${platform} (${format}) - ${totalMB}MB / ${limitMB}MB - ${(duration / 1000).toFixed(1)}s`);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
const duration = Date.now() - platformStartTime;
|
|
398
|
+
results.push({
|
|
399
|
+
platform,
|
|
400
|
+
format,
|
|
401
|
+
success: false,
|
|
402
|
+
error: error.message,
|
|
403
|
+
duration,
|
|
404
|
+
});
|
|
405
|
+
console.log(` ${pc.red('❌')} ${platform} (${format}) - 失败: ${error.message}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const totalDuration = Date.now() - startTime;
|
|
409
|
+
spinner.stop();
|
|
410
|
+
// 7. 输出汇总报告
|
|
411
|
+
const successCount = results.filter(r => r.success).length;
|
|
412
|
+
const failCount = results.filter(r => !r.success).length;
|
|
413
|
+
console.log('\n' + pc.bold('═══════════════════════════════════════════'));
|
|
414
|
+
console.log(pc.bold('📊 批量构建报告'));
|
|
415
|
+
console.log(pc.bold('═══════════════════════════════════════════'));
|
|
416
|
+
console.log(` 总渠道数: ${targetPlatforms.length}`);
|
|
417
|
+
console.log(` ${pc.green(`成功: ${successCount}`)} ${failCount > 0 ? pc.red(`失败: ${failCount}`) : ''}`);
|
|
418
|
+
console.log(` 总耗时: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
419
|
+
console.log(` 输出目录: ${outputBaseDir}`);
|
|
420
|
+
// 成功列表
|
|
421
|
+
const successResults = results.filter(r => r.success);
|
|
422
|
+
if (successResults.length > 0) {
|
|
423
|
+
console.log('\n' + pc.bold(pc.green('✅ 成功:')));
|
|
424
|
+
for (const r of successResults) {
|
|
425
|
+
const totalMB = r.sizeReport ? (r.sizeReport.total / 1024 / 1024).toFixed(2) : '?';
|
|
426
|
+
const limitMB = r.sizeReport ? (r.sizeReport.limit / 1024 / 1024).toFixed(2) : '?';
|
|
427
|
+
const sizeOk = r.sizeReport ? r.sizeReport.total <= r.sizeReport.limit : true;
|
|
428
|
+
const sizeIcon = sizeOk ? '' : pc.yellow(' ⚠️ 超限');
|
|
429
|
+
console.log(` ${r.platform.padEnd(14)} ${r.format.padEnd(5)} ${totalMB}MB / ${limitMB}MB ${(r.duration / 1000).toFixed(1)}s${sizeIcon}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// 失败列表
|
|
433
|
+
const failResults = results.filter(r => !r.success);
|
|
434
|
+
if (failResults.length > 0) {
|
|
435
|
+
console.log('\n' + pc.bold(pc.red('❌ 失败:')));
|
|
436
|
+
for (const r of failResults) {
|
|
437
|
+
console.log(` ${r.platform.padEnd(14)} ${r.error}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// 输出目录结构
|
|
441
|
+
console.log('\n' + pc.bold('📁 输出目录结构:'));
|
|
442
|
+
console.log(` ${outputBaseDir}/`);
|
|
443
|
+
for (const r of successResults) {
|
|
444
|
+
console.log(` ├── ${r.platform}/`);
|
|
445
|
+
}
|
|
446
|
+
console.log('');
|
|
447
|
+
// 如果有失败的,以非零码退出
|
|
448
|
+
if (failCount > 0) {
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
spinner.fail(pc.red('批量构建失败'));
|
|
454
|
+
console.error(pc.red(`错误: ${error.message}`));
|
|
455
|
+
if (error.stack) {
|
|
456
|
+
console.error(pc.dim(error.stack));
|
|
457
|
+
}
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
// 清理临时目录,避免磁盘空间浪费
|
|
462
|
+
if (!isBaseBuild && baseBuildDir && baseBuildDir.includes('.playcraft-temp')) {
|
|
463
|
+
try {
|
|
464
|
+
await fs.rm(baseBuildDir, { recursive: true, force: true });
|
|
465
|
+
// 如果 .playcraft-temp 父目录为空,也一并清理
|
|
466
|
+
const tempParent = path.dirname(baseBuildDir);
|
|
467
|
+
const remaining = await fs.readdir(tempParent);
|
|
468
|
+
if (remaining.length === 0) {
|
|
469
|
+
await fs.rm(tempParent, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// 清理失败不影响构建结果
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|