@playcraft/cli 0.0.10 → 0.0.12
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 +202 -0
- package/dist/agent/api-proxy.js +68 -0
- package/dist/agent/cloud-connection.js +233 -0
- package/dist/agent/cloud-connection.test.js +67 -0
- package/dist/agent/fs-backend.js +158 -0
- package/dist/agent/local-backend.js +359 -0
- package/dist/agent/local-backend.test.js +52 -0
- package/dist/commands/build.js +260 -58
- package/dist/commands/fix-ids.js +43 -0
- package/dist/commands/start.js +14 -129
- package/dist/commands/sync.js +40 -0
- package/dist/commands/upgrade.js +71 -0
- package/dist/config.js +2 -0
- package/dist/fs-handler.js +21 -0
- package/dist/index.js +51 -0
- package/dist/server.js +28 -0
- package/dist/socket.js +80 -21
- package/dist/sync/sync-engine.js +213 -0
- package/dist/sync/sync-manager.js +62 -0
- package/dist/sync/sync-manager.test.js +80 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/updater.js +89 -0
- package/dist/utils/version-checker.js +84 -0
- package/package.json +11 -3
package/dist/commands/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import pc from 'picocolors';
|
|
5
4
|
import ora from 'ora';
|
|
6
5
|
import { BaseBuilder, ViteBuilder, PlayableBuilder } from '@playcraft/build';
|
|
@@ -10,16 +9,24 @@ import inquirer from 'inquirer';
|
|
|
10
9
|
* 检测是否是基础构建产物(多文件版本)
|
|
11
10
|
*/
|
|
12
11
|
async function detectBaseBuild(dir) {
|
|
13
|
-
|
|
12
|
+
// 基础文件:所有格式都需要
|
|
13
|
+
const baseIndicators = [
|
|
14
14
|
path.join(dir, 'index.html'),
|
|
15
15
|
path.join(dir, 'config.json'),
|
|
16
|
-
path.join(dir, '__start__.js'),
|
|
17
16
|
];
|
|
17
|
+
// Classic 格式特有
|
|
18
|
+
const classicIndicator = path.join(dir, '__start__.js');
|
|
19
|
+
// ESM 格式特有
|
|
20
|
+
const esmIndicator = path.join(dir, 'js/index.mjs');
|
|
18
21
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// 检查基础文件
|
|
23
|
+
for (const indicator of baseIndicators) {
|
|
24
|
+
await fs.access(indicator);
|
|
25
|
+
}
|
|
26
|
+
// 检查格式特有文件(满足其一即可)
|
|
27
|
+
const isClassic = await fs.access(classicIndicator).then(() => true).catch(() => false);
|
|
28
|
+
const isESM = await fs.access(esmIndicator).then(() => true).catch(() => false);
|
|
29
|
+
return isClassic || isESM;
|
|
23
30
|
}
|
|
24
31
|
catch (error) {
|
|
25
32
|
return false;
|
|
@@ -84,7 +91,36 @@ async function detectAmmoUsage(baseBuildDir) {
|
|
|
84
91
|
try {
|
|
85
92
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
86
93
|
const configJson = JSON.parse(configContent);
|
|
94
|
+
// 检查 application_properties.use3dPhysics
|
|
87
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
|
+
}
|
|
88
124
|
if (configJson.assets) {
|
|
89
125
|
for (const asset of Object.values(configJson.assets)) {
|
|
90
126
|
const assetData = asset;
|
|
@@ -119,6 +155,7 @@ async function detectAmmoUsage(baseBuildDir) {
|
|
|
119
155
|
}
|
|
120
156
|
async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
|
|
121
157
|
const assetSizes = [];
|
|
158
|
+
const assetPaths = new Set(); // 用于去重
|
|
122
159
|
let engineSize = 0;
|
|
123
160
|
const ammoCheck = await detectAmmoUsage(baseBuildDir);
|
|
124
161
|
const engineCandidates = [
|
|
@@ -151,10 +188,15 @@ async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
|
|
|
151
188
|
if (ammoReplacement && lowerUrl.includes('ammo')) {
|
|
152
189
|
continue;
|
|
153
190
|
}
|
|
191
|
+
// 跳过已经处理过的资源
|
|
192
|
+
if (assetPaths.has(cleanUrl)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
154
195
|
const assetPath = path.join(baseBuildDir, cleanUrl);
|
|
155
196
|
try {
|
|
156
197
|
const stats = await fs.stat(assetPath);
|
|
157
198
|
assetSizes.push({ path: cleanUrl, size: stats.size });
|
|
199
|
+
assetPaths.add(cleanUrl); // 标记为已处理
|
|
158
200
|
}
|
|
159
201
|
catch (error) {
|
|
160
202
|
// ignore missing assets
|
|
@@ -174,10 +216,15 @@ async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
|
|
|
174
216
|
};
|
|
175
217
|
}
|
|
176
218
|
export async function buildCommand(projectPath, options) {
|
|
219
|
+
// 解析项目路径 - 在 spinner 之前执行以便调试
|
|
220
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
221
|
+
console.log(`[DEBUG] projectPath: ${projectPath}`);
|
|
222
|
+
console.log(`[DEBUG] resolvedProjectPath: ${resolvedProjectPath}`);
|
|
223
|
+
console.log(`[DEBUG] cwd: ${process.cwd()}`);
|
|
177
224
|
const spinner = ora(pc.cyan('🔨 开始打包 Playable Ad...')).start();
|
|
225
|
+
// 清理选项(默认为 false,使用覆盖模式)
|
|
226
|
+
const shouldClean = options.clean === true;
|
|
178
227
|
try {
|
|
179
|
-
// 解析项目路径
|
|
180
|
-
const resolvedProjectPath = path.resolve(projectPath);
|
|
181
228
|
if (!options.baseOnly && !options.mode) {
|
|
182
229
|
spinner.stop();
|
|
183
230
|
const modeAnswer = await inquirer.prompt([
|
|
@@ -265,10 +312,22 @@ export async function buildCommand(projectPath, options) {
|
|
|
265
312
|
}
|
|
266
313
|
// 如果只执行基础构建
|
|
267
314
|
if (options.baseOnly || options.mode === 'base') {
|
|
315
|
+
// 确定输出目录(Base Build 默认 ./build)
|
|
316
|
+
const baseBuildOutputDir = path.resolve(options.output || './build');
|
|
317
|
+
// 安全检查:防止输出目录与输入目录相同时被清理
|
|
318
|
+
const isSameAsInput = path.resolve(baseBuildOutputDir) === path.resolve(resolvedProjectPath);
|
|
319
|
+
// 如果输出目录与输入目录相同,警告用户并禁用清理
|
|
320
|
+
if (shouldClean && isSameAsInput) {
|
|
321
|
+
console.log(pc.yellow(`\n⚠️ 输出目录与输入目录相同,跳过清理以保护源文件`));
|
|
322
|
+
}
|
|
323
|
+
else if (!shouldClean) {
|
|
324
|
+
console.log(pc.dim(`\nℹ️ 使用覆盖模式,新文件将覆盖旧文件(使用 --clean 可清理旧目录)`));
|
|
325
|
+
}
|
|
268
326
|
spinner.text = pc.cyan('执行阶段1: 基础构建...');
|
|
269
327
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
270
|
-
outputDir:
|
|
328
|
+
outputDir: baseBuildOutputDir,
|
|
271
329
|
selectedScenes,
|
|
330
|
+
clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
|
|
272
331
|
});
|
|
273
332
|
const baseBuild = await baseBuilder.build();
|
|
274
333
|
spinner.succeed(pc.green('✅ 基础构建完成!'));
|
|
@@ -276,7 +335,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
276
335
|
console.log(pc.dim('💡 提示: 可以在浏览器中打开 index.html 测试游戏'));
|
|
277
336
|
return;
|
|
278
337
|
}
|
|
279
|
-
// 完整流程:阶段1 + 阶段2
|
|
338
|
+
// 完整流程:阶段1 + 阶段2(Playable Ads)
|
|
280
339
|
const shouldRunPlayableOnly = options.mode === 'playable';
|
|
281
340
|
// 验证平台
|
|
282
341
|
const validPlatforms = [
|
|
@@ -294,30 +353,80 @@ export async function buildCommand(projectPath, options) {
|
|
|
294
353
|
'adikteev',
|
|
295
354
|
'remerge',
|
|
296
355
|
];
|
|
356
|
+
// 渠道输出格式配置(根据各渠道Playable规格对照表)
|
|
357
|
+
// 只支持 HTML: applovin, ironsource, unity, moloco, adikteev, remerge
|
|
358
|
+
// 只支持 ZIP: google, tiktok, liftoff, bigo, snapchat, inmobi
|
|
359
|
+
// 支持两种: facebook
|
|
360
|
+
const platformFormatConfig = {
|
|
361
|
+
facebook: {
|
|
362
|
+
formats: ['html', 'zip'],
|
|
363
|
+
default: 'html',
|
|
364
|
+
description: 'Facebook 支持两种格式'
|
|
365
|
+
},
|
|
366
|
+
applovin: { formats: ['html'], default: 'html' },
|
|
367
|
+
ironsource: { formats: ['html'], default: 'html' },
|
|
368
|
+
unity: { formats: ['html'], default: 'html' },
|
|
369
|
+
moloco: { formats: ['html'], default: 'html' },
|
|
370
|
+
adikteev: { formats: ['html'], default: 'html' },
|
|
371
|
+
remerge: { formats: ['html'], default: 'html' },
|
|
372
|
+
google: { formats: ['zip'], default: 'zip' },
|
|
373
|
+
tiktok: { formats: ['zip'], default: 'zip' },
|
|
374
|
+
liftoff: { formats: ['zip'], default: 'zip' },
|
|
375
|
+
bigo: { formats: ['zip'], default: 'zip' },
|
|
376
|
+
snapchat: { formats: ['zip'], default: 'zip' },
|
|
377
|
+
inmobi: { formats: ['zip'], default: 'zip' },
|
|
378
|
+
};
|
|
379
|
+
// 获取渠道支持的格式
|
|
380
|
+
const getPlatformFormats = (platform) => {
|
|
381
|
+
return platformFormatConfig[platform] || { formats: ['html', 'zip'], default: 'html' };
|
|
382
|
+
};
|
|
297
383
|
if (!options.platform || !validPlatforms.includes(options.platform)) {
|
|
298
384
|
spinner.stop();
|
|
299
|
-
|
|
385
|
+
// 先选择平台
|
|
386
|
+
const platformAnswer = await inquirer.prompt([
|
|
300
387
|
{
|
|
301
388
|
type: 'list',
|
|
302
389
|
name: 'platform',
|
|
303
390
|
message: '选择目标平台:',
|
|
304
391
|
choices: validPlatforms,
|
|
305
392
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
393
|
+
]);
|
|
394
|
+
const selectedPlatform = platformAnswer.platform;
|
|
395
|
+
const formatConfig = getPlatformFormats(selectedPlatform);
|
|
396
|
+
// 根据平台支持的格式决定是否需要选择
|
|
397
|
+
let selectedFormat;
|
|
398
|
+
if (formatConfig.formats.length === 1) {
|
|
399
|
+
// 只支持一种格式,直接使用
|
|
400
|
+
selectedFormat = formatConfig.formats[0];
|
|
401
|
+
const formatName = selectedFormat === 'html' ? 'HTML(单文件)' : 'ZIP(多文件)';
|
|
402
|
+
console.log(pc.dim(`ℹ️ ${selectedPlatform} 渠道仅支持 ${formatName} 格式`));
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// 支持多种格式,让用户选择
|
|
406
|
+
const formatAnswer = await inquirer.prompt([
|
|
407
|
+
{
|
|
408
|
+
type: 'list',
|
|
409
|
+
name: 'format',
|
|
410
|
+
message: formatConfig.description
|
|
411
|
+
? `选择输出格式(${formatConfig.description}):`
|
|
412
|
+
: '选择输出格式:',
|
|
413
|
+
choices: formatConfig.formats.map(f => ({
|
|
414
|
+
name: f === 'html'
|
|
415
|
+
? (selectedPlatform === 'facebook'
|
|
416
|
+
? 'HTML(单文件,最大 5MB,所有资源内联)'
|
|
417
|
+
: 'HTML(单文件)')
|
|
418
|
+
: (selectedPlatform === 'facebook'
|
|
419
|
+
? 'ZIP(多文件,最大 5MB,HTML 文件需 < 2MB)'
|
|
420
|
+
: 'ZIP(多文件)'),
|
|
421
|
+
value: f,
|
|
422
|
+
})),
|
|
423
|
+
default: formatConfig.default,
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
selectedFormat = formatAnswer.format;
|
|
427
|
+
}
|
|
428
|
+
// 选择输出目录
|
|
429
|
+
const outputAnswer = await inquirer.prompt([
|
|
321
430
|
{
|
|
322
431
|
type: 'input',
|
|
323
432
|
name: 'output',
|
|
@@ -325,11 +434,24 @@ export async function buildCommand(projectPath, options) {
|
|
|
325
434
|
default: options.output || './dist',
|
|
326
435
|
},
|
|
327
436
|
]);
|
|
328
|
-
options.platform =
|
|
329
|
-
options.format =
|
|
330
|
-
options.output =
|
|
437
|
+
options.platform = selectedPlatform;
|
|
438
|
+
options.format = selectedFormat;
|
|
439
|
+
options.output = outputAnswer.output;
|
|
331
440
|
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
332
441
|
}
|
|
442
|
+
else {
|
|
443
|
+
// 命令行已指定平台,检查格式是否符合要求
|
|
444
|
+
const formatConfig = getPlatformFormats(options.platform);
|
|
445
|
+
if (options.format && !formatConfig.formats.includes(options.format)) {
|
|
446
|
+
const supportedFormats = formatConfig.formats.join(' 或 ');
|
|
447
|
+
console.log(pc.yellow(`\n⚠️ ${options.platform} 渠道仅支持 ${supportedFormats} 格式,已自动切换为 ${formatConfig.default}`));
|
|
448
|
+
options.format = formatConfig.default;
|
|
449
|
+
}
|
|
450
|
+
else if (!options.format) {
|
|
451
|
+
// 未指定格式,使用默认值
|
|
452
|
+
options.format = formatConfig.default;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
333
455
|
// 解析配置
|
|
334
456
|
const buildOptions = {
|
|
335
457
|
platform: options.platform,
|
|
@@ -350,18 +472,44 @@ export async function buildCommand(projectPath, options) {
|
|
|
350
472
|
modelCompression: options.modelCompression,
|
|
351
473
|
// 场景选择
|
|
352
474
|
selectedScenes,
|
|
475
|
+
// ESM 选项
|
|
476
|
+
...(options.esmMode ? { esmMode: options.esmMode } : {}),
|
|
353
477
|
};
|
|
354
478
|
if (fileConfig) {
|
|
355
479
|
Object.assign(buildOptions, fileConfig);
|
|
356
480
|
}
|
|
357
|
-
// 1.
|
|
481
|
+
// 1. 先检测输入类型(在清理目录之前!)
|
|
358
482
|
spinner.text = pc.cyan('检测项目类型...');
|
|
359
483
|
const isBaseBuild = await detectBaseBuild(resolvedProjectPath);
|
|
484
|
+
// 2. 验证 playable-only 模式的前置条件
|
|
485
|
+
if (shouldRunPlayableOnly && !isBaseBuild) {
|
|
486
|
+
// 检查是否是原始 PlayCanvas 编辑器项目
|
|
487
|
+
const isEditorProject = await fs.access(path.join(resolvedProjectPath, 'scenes.json')).then(() => true).catch(() => false);
|
|
488
|
+
if (isEditorProject) {
|
|
489
|
+
throw new Error('检测到这是 PlayCanvas 编辑器项目,不是多文件构建产物。\n\n' +
|
|
490
|
+
'请使用以下命令之一:\n' +
|
|
491
|
+
' 1. playcraft build <项目路径> # 完整构建(推荐)\n' +
|
|
492
|
+
' 2. playcraft build-base <项目路径> # 先生成多文件产物\n' +
|
|
493
|
+
' playcraft build-playable <产物路径> # 再转换为 Playable\n\n' +
|
|
494
|
+
'💡 提示: build-playable 命令仅用于已有的多文件构建产物');
|
|
495
|
+
}
|
|
496
|
+
throw new Error('当前目录不是多文件构建产物,请先运行完整构建或使用 build-base 生成。');
|
|
497
|
+
}
|
|
498
|
+
// 3. 验证通过后,显示清理/覆盖模式提示(实际清理由 ViteBuilder 内部处理)
|
|
499
|
+
const playableOutputDir = buildOptions.outputDir || path.resolve(options.output || './dist');
|
|
500
|
+
// 安全检查:防止输出目录与输入目录相同时被清理
|
|
501
|
+
const isSameAsInput = path.resolve(playableOutputDir) === path.resolve(resolvedProjectPath);
|
|
502
|
+
if (shouldClean && isSameAsInput) {
|
|
503
|
+
console.log(pc.yellow(`\n⚠️ 输出目录与输入目录相同,跳过清理以保护源文件`));
|
|
504
|
+
}
|
|
505
|
+
else if (!shouldClean) {
|
|
506
|
+
console.log(pc.dim(`\nℹ️ 使用覆盖模式,新文件将覆盖旧文件(使用 --clean 可清理旧目录)`));
|
|
507
|
+
}
|
|
508
|
+
// 注意:ViteBuilder 内部会通过 emptyOutDir: true 自动清空输出目录
|
|
509
|
+
// 4. 确定 baseBuildDir
|
|
360
510
|
let baseBuildDir;
|
|
361
511
|
if (shouldRunPlayableOnly) {
|
|
362
|
-
|
|
363
|
-
throw new Error('当前目录不是多文件构建产物,请先运行完整构建或使用 build-base 生成。');
|
|
364
|
-
}
|
|
512
|
+
// 已经在上面验证过 isBaseBuild === true
|
|
365
513
|
spinner.text = pc.cyan('✅ 使用多文件构建产物,直接执行阶段2');
|
|
366
514
|
baseBuildDir = resolvedProjectPath;
|
|
367
515
|
}
|
|
@@ -373,10 +521,13 @@ export async function buildCommand(projectPath, options) {
|
|
|
373
521
|
else {
|
|
374
522
|
// 输入是源代码或官方构建,先执行阶段1
|
|
375
523
|
spinner.text = pc.cyan('执行阶段1: 基础构建...');
|
|
376
|
-
|
|
524
|
+
// 将临时目录放在项目所在目录下,避免跨盘符路径问题(Windows 上 C:\Temp 和 D:\project 会导致 Vite 路径计算错误)
|
|
525
|
+
const tempDir = path.join(resolvedProjectPath, '.playcraft-temp', `base-build-${Date.now()}`);
|
|
377
526
|
const baseBuilder = new BaseBuilder(resolvedProjectPath, {
|
|
378
527
|
outputDir: tempDir,
|
|
379
528
|
selectedScenes,
|
|
529
|
+
analyze: buildOptions.analyze,
|
|
530
|
+
analyzeReportPath: buildOptions.analyze ? 'base-bundle-report.html' : undefined,
|
|
380
531
|
});
|
|
381
532
|
const baseBuild = await baseBuilder.build();
|
|
382
533
|
baseBuildDir = baseBuild.outputDir;
|
|
@@ -390,18 +541,48 @@ export async function buildCommand(projectPath, options) {
|
|
|
390
541
|
else if (options.replaceAmmo && ammoCheck.hasAmmo && ammoCheck.suggestedEngine) {
|
|
391
542
|
buildOptions.ammoReplacement = ammoCheck.suggestedEngine;
|
|
392
543
|
}
|
|
393
|
-
else if (ammoCheck.hasAmmo &&
|
|
544
|
+
else if (ammoCheck.hasAmmo && !buildOptions.ammoReplacement) {
|
|
394
545
|
spinner.stop();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
546
|
+
// 根据是否使用 3D 物理提供不同的选项
|
|
547
|
+
const physicsType = ammoCheck.use3dPhysics ? '3D' : '2D';
|
|
548
|
+
if (ammoCheck.use3dPhysics) {
|
|
549
|
+
// 3D 项目 - 可以使用 Cannon.js 替换
|
|
550
|
+
console.log(pc.yellow(`⚠️ 检测到 3D 物理项目,Ammo.js 约 1.8MB`));
|
|
551
|
+
console.log(pc.gray(` 提示: Cannon.js (~400KB) 可以减小 75% 的体积`));
|
|
552
|
+
const replaceAnswer = await inquirer.prompt([
|
|
553
|
+
{
|
|
554
|
+
type: 'list',
|
|
555
|
+
name: 'ammoAction',
|
|
556
|
+
message: `如何处理 Ammo.js?`,
|
|
557
|
+
choices: [
|
|
558
|
+
{ name: '保留 Ammo.js(完整物理功能,文件较大)', value: 'keep' },
|
|
559
|
+
{ name: '替换为 Cannon.js(推荐 3D 项目,~400KB,自动适配)', value: 'cannon' },
|
|
560
|
+
],
|
|
561
|
+
default: 'cannon',
|
|
562
|
+
},
|
|
563
|
+
]);
|
|
564
|
+
if (replaceAnswer.ammoAction === 'cannon') {
|
|
565
|
+
buildOptions.ammoReplacement = 'cannon';
|
|
566
|
+
console.log(pc.green(' ✓ 将使用 Cannon.js 替换 Ammo.js(自动兼容 rigidbody API)'));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// 2D 项目 - 可以使用 p2.js 替换
|
|
571
|
+
const replaceAnswer = await inquirer.prompt([
|
|
572
|
+
{
|
|
573
|
+
type: 'list',
|
|
574
|
+
name: 'ammoAction',
|
|
575
|
+
message: `检测到 Ammo.js(项目使用 ${physicsType} 物理),如何处理?`,
|
|
576
|
+
choices: [
|
|
577
|
+
{ name: '不替换(保留 Ammo.js,文件较大)', value: 'keep' },
|
|
578
|
+
{ name: '替换为 p2.js(推荐 2D 项目,~60KB)', value: 'p2' },
|
|
579
|
+
],
|
|
580
|
+
default: 'p2',
|
|
581
|
+
},
|
|
582
|
+
]);
|
|
583
|
+
if (replaceAnswer.ammoAction === 'p2') {
|
|
584
|
+
buildOptions.ammoReplacement = 'p2';
|
|
585
|
+
}
|
|
405
586
|
}
|
|
406
587
|
spinner.start(pc.cyan('🔨 开始打包 Playable Ad...'));
|
|
407
588
|
}
|
|
@@ -434,12 +615,25 @@ export async function buildCommand(projectPath, options) {
|
|
|
434
615
|
console.log(` - 总计: ${totalMB} MB ${status} (限制: ${limitMB} MB)`);
|
|
435
616
|
console.log('\n' + pc.green(`输出: ${outputPath}`));
|
|
436
617
|
if (buildOptions.analyze) {
|
|
437
|
-
|
|
618
|
+
// 从 Base Build 复制分析报告到最终输出目录
|
|
619
|
+
const baseReportPath = path.join(baseBuildDir, 'base-bundle-report.html');
|
|
620
|
+
const finalReportPath = buildOptions.analyzeReportPath
|
|
438
621
|
? buildOptions.analyzeReportPath
|
|
439
622
|
: path.join(buildOptions.outputDir || './dist', 'bundle-report.html');
|
|
623
|
+
let reportExists = false;
|
|
624
|
+
try {
|
|
625
|
+
await fs.access(baseReportPath);
|
|
626
|
+
await fs.copyFile(baseReportPath, finalReportPath);
|
|
627
|
+
reportExists = true;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// 报告文件不存在(例如从官方构建产物直接复制时),这是正常的
|
|
631
|
+
}
|
|
440
632
|
const analysis = await analyzeBaseBuild(baseBuildDir, buildOptions.ammoReplacement);
|
|
441
633
|
console.log('\n' + pc.bold('分析报告:'));
|
|
442
|
-
|
|
634
|
+
if (reportExists) {
|
|
635
|
+
console.log(` - 可视化报告: ${finalReportPath}`);
|
|
636
|
+
}
|
|
443
637
|
if (analysis.engineSize > 0) {
|
|
444
638
|
const engineMB = (analysis.engineSize / 1024 / 1024).toFixed(2);
|
|
445
639
|
console.log(` - 引擎大小: ${engineMB} MB`);
|
|
@@ -447,22 +641,30 @@ export async function buildCommand(projectPath, options) {
|
|
|
447
641
|
if (analysis.topAssets.length > 0) {
|
|
448
642
|
console.log(' - 资源 Top:');
|
|
449
643
|
analysis.topAssets.forEach((asset) => {
|
|
450
|
-
const
|
|
451
|
-
|
|
644
|
+
const sizeKB = asset.size / 1024;
|
|
645
|
+
const sizeMB = sizeKB / 1024;
|
|
646
|
+
// 自动选择合适的单位
|
|
647
|
+
const sizeStr = sizeMB >= 0.01
|
|
648
|
+
? `${sizeMB.toFixed(2)} MB`
|
|
649
|
+
: `${sizeKB.toFixed(2)} KB`;
|
|
650
|
+
console.log(` • ${asset.path}: ${sizeStr}`);
|
|
452
651
|
});
|
|
453
652
|
}
|
|
454
653
|
if (analysis.hasAmmo) {
|
|
455
654
|
console.log(pc.yellow(` - 检测到 Ammo.js,建议替换为 ${analysis.suggestedEngine}`));
|
|
456
655
|
}
|
|
457
656
|
}
|
|
458
|
-
//
|
|
459
|
-
if (!isBaseBuild && baseBuildDir.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
657
|
+
// 清理临时目录(调试时注释掉)
|
|
658
|
+
if (!isBaseBuild && baseBuildDir.includes('.playcraft-temp')) {
|
|
659
|
+
// 调试:暂时保留临时目录
|
|
660
|
+
console.log(pc.yellow(`[DEBUG] 临时目录保留在: ${baseBuildDir}`));
|
|
661
|
+
// try {
|
|
662
|
+
// // 清理整个 .playcraft-temp 目录
|
|
663
|
+
// const tempBaseDir = path.join(resolvedProjectPath, '.playcraft-temp');
|
|
664
|
+
// await fs.rm(tempBaseDir, { recursive: true, force: true });
|
|
665
|
+
// } catch (error) {
|
|
666
|
+
// // 忽略清理错误
|
|
667
|
+
// }
|
|
466
668
|
}
|
|
467
669
|
}
|
|
468
670
|
catch (error) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fix-ids 命令:修复已导入项目的 ID 映射(将 Sandbox 中的 PlayCanvas ID 转换为 PlayCraft numericId)
|
|
3
|
+
*/
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
export async function fixIdsCommand(options) {
|
|
6
|
+
const { projectId, apiUrl, token } = options;
|
|
7
|
+
const baseUrl = (apiUrl || process.env.BACKEND_API_URL || process.env.PLAYCRAFT_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
|
8
|
+
const url = `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/fix-ids`;
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
};
|
|
12
|
+
if (token || process.env.PLAYCRAFT_TOKEN) {
|
|
13
|
+
headers['Authorization'] = `Bearer ${token || process.env.PLAYCRAFT_TOKEN}`;
|
|
14
|
+
}
|
|
15
|
+
if (process.env.NEXT_PUBLIC_LOCAL_AUTH_BYPASS === 'true' && process.env.NEXT_PUBLIC_LOCAL_USER) {
|
|
16
|
+
headers['X-Local-User'] = process.env.NEXT_PUBLIC_LOCAL_USER;
|
|
17
|
+
}
|
|
18
|
+
console.log(pc.dim(`Fixing ID mapping for project ${projectId}...`));
|
|
19
|
+
console.log(pc.dim(`POST ${url}`));
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers,
|
|
24
|
+
});
|
|
25
|
+
const body = await res.json().catch(() => ({}));
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const msg = body.message || res.statusText || String(res.status);
|
|
28
|
+
console.error(pc.red(`Failed to fix ID mapping: ${msg}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (body.success !== true) {
|
|
32
|
+
console.error(pc.red('Unexpected response from server'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
console.log(pc.green('ID mapping fixed successfully'));
|
|
36
|
+
console.log(pc.dim('Please reload the editor to see changes.'));
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
console.error(pc.red(`Request failed: ${message}`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|