@playcraft/cli 0.0.13 → 0.0.15
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 +5 -1
- package/dist/commands/build.js +71 -30
- package/dist/index.js +6 -3
- package/package.json +17 -16
- package/dist/playable/base-builder.js +0 -265
- package/dist/playable/builder.js +0 -1462
- package/dist/playable/converter.js +0 -150
- package/dist/playable/index.js +0 -3
- package/dist/playable/platforms/base.js +0 -12
- package/dist/playable/platforms/facebook.js +0 -37
- package/dist/playable/platforms/index.js +0 -24
- package/dist/playable/platforms/snapchat.js +0 -59
- package/dist/playable/playable-builder.js +0 -521
- package/dist/playable/types.js +0 -1
- package/dist/playable/vite/config-builder.js +0 -136
- package/dist/playable/vite/platform-configs.js +0 -102
- package/dist/playable/vite/plugin-model-compression.js +0 -63
- package/dist/playable/vite/plugin-platform.js +0 -65
- package/dist/playable/vite/plugin-playcanvas.js +0 -454
- package/dist/playable/vite-builder.js +0 -125
package/dist/agent/agent.js
CHANGED
|
@@ -89,7 +89,11 @@ export class PlayCraftAgent {
|
|
|
89
89
|
onDocCreate: async (collection, id) => {
|
|
90
90
|
// Prefer internal endpoints from local backend
|
|
91
91
|
const url = `http://localhost:${this.config.port}/api/${collection}/internal/${id}`;
|
|
92
|
-
const
|
|
92
|
+
const path = `/api/${collection}/internal/${id}`;
|
|
93
|
+
// 生成签名 headers
|
|
94
|
+
const { generateSignedHeaders } = await import('@playcraft/common');
|
|
95
|
+
const headers = generateSignedHeaders('GET', path, emitSecret, 'cli');
|
|
96
|
+
const res = await fetch(url, { headers });
|
|
93
97
|
if (res.ok)
|
|
94
98
|
return await res.json();
|
|
95
99
|
return null;
|
package/dist/commands/build.js
CHANGED
|
@@ -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 } from '@playcraft/build';
|
|
5
|
+
import { BaseBuilder, ViteBuilder, PlayableBuilder, PlayableAnalyzer } from '@playcraft/build';
|
|
6
6
|
import { loadBuildConfig } from '../build-config.js';
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
8
|
/**
|
|
@@ -328,6 +328,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
328
328
|
outputDir: baseBuildOutputDir,
|
|
329
329
|
selectedScenes,
|
|
330
330
|
clean: shouldClean && !isSameAsInput, // 清理逻辑由 BaseBuilder 内部处理
|
|
331
|
+
analyze: options.analyze, // 传递 analyze 参数
|
|
331
332
|
});
|
|
332
333
|
const baseBuild = await baseBuilder.build();
|
|
333
334
|
spinner.succeed(pc.green('✅ 基础构建完成!'));
|
|
@@ -459,6 +460,8 @@ export async function buildCommand(projectPath, options) {
|
|
|
459
460
|
outputDir: path.resolve(options.output || './dist'),
|
|
460
461
|
// 如果命令行指定了 --compress,则使用命令行参数;否则让平台配置生效
|
|
461
462
|
compressEngine: options.compress,
|
|
463
|
+
// 如果命令行指定了 --compress-config,则使用命令行参数;否则让平台配置生效
|
|
464
|
+
compressConfigJson: options.compressConfig,
|
|
462
465
|
analyze: options.analyze || false,
|
|
463
466
|
// Vite 构建选项
|
|
464
467
|
useVite: options.useVite !== false, // 默认使用 Vite
|
|
@@ -615,43 +618,81 @@ export async function buildCommand(projectPath, options) {
|
|
|
615
618
|
console.log(` - 总计: ${totalMB} MB ${status} (限制: ${limitMB} MB)`);
|
|
616
619
|
console.log('\n' + pc.green(`输出: ${outputPath}`));
|
|
617
620
|
if (buildOptions.analyze) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
: path.join(buildOptions.outputDir || './dist', 'bundle-report.html');
|
|
623
|
-
let reportExists = false;
|
|
621
|
+
console.log('\n' + pc.bold('📊 生成分析报告...'));
|
|
622
|
+
// 1. 生成 Base Build 分析报告(如果有)
|
|
623
|
+
const baseReportPath = path.join(baseBuildDir, 'build-analysis-report.html');
|
|
624
|
+
let baseReportExists = false;
|
|
624
625
|
try {
|
|
625
626
|
await fs.access(baseReportPath);
|
|
626
|
-
|
|
627
|
-
reportExists = true;
|
|
627
|
+
baseReportExists = true;
|
|
628
628
|
}
|
|
629
629
|
catch {
|
|
630
|
-
//
|
|
630
|
+
// Base Build 报告不存在
|
|
631
631
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
632
|
+
// 2. 生成 Playable 分析报告(分析最终打包产物)
|
|
633
|
+
try {
|
|
634
|
+
const playableAnalyzer = new PlayableAnalyzer(outputPath, buildOptions.outputDir || './dist', baseReportExists ? baseReportPath : undefined // 传入 Base Build 报告路径
|
|
635
|
+
);
|
|
636
|
+
// 分析最终产物
|
|
637
|
+
const playableReport = await playableAnalyzer.analyze();
|
|
638
|
+
// 如果有 Base Build 报告,添加优化信息
|
|
639
|
+
if (baseReportExists) {
|
|
640
|
+
try {
|
|
641
|
+
const baseReportContent = await fs.readFile(baseReportPath, 'utf-8');
|
|
642
|
+
// 从 Base Build 报告中提取预估大小
|
|
643
|
+
const estimatedMatch = baseReportContent.match(/预估 HTML 大小[^>]*>([^<]+)</);
|
|
644
|
+
if (estimatedMatch) {
|
|
645
|
+
const estimatedSizeStr = estimatedMatch[1].trim();
|
|
646
|
+
// 解析大小字符串(例如 "16.02 MB")
|
|
647
|
+
const sizeMatch = estimatedSizeStr.match(/([\d.]+)\s*(B|KB|MB|GB)/);
|
|
648
|
+
if (sizeMatch) {
|
|
649
|
+
const value = parseFloat(sizeMatch[1]);
|
|
650
|
+
const unit = sizeMatch[2];
|
|
651
|
+
const multipliers = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024 };
|
|
652
|
+
const baseBuildSize = value * multipliers[unit];
|
|
653
|
+
playableReport.optimizationInfo = {
|
|
654
|
+
baseBuildSize,
|
|
655
|
+
baseBuildSizeFormatted: estimatedSizeStr,
|
|
656
|
+
optimizationRate: ((baseBuildSize - playableReport.totalSize) / baseBuildSize) * 100
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
// 忽略解析错误
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// 生成 HTML 报告
|
|
666
|
+
const playableReportPath = await playableAnalyzer.generateHTMLReport(playableReport);
|
|
667
|
+
console.log('\n' + pc.bold('分析报告:'));
|
|
668
|
+
console.log(` - Playable 分析报告: ${playableReportPath}`);
|
|
669
|
+
if (baseReportExists) {
|
|
670
|
+
console.log(` - Base Build 分析报告: ${baseReportPath}`);
|
|
671
|
+
}
|
|
672
|
+
// 显示优化效果
|
|
673
|
+
if (playableReport.optimizationInfo) {
|
|
674
|
+
const opt = playableReport.optimizationInfo;
|
|
675
|
+
console.log('\n' + pc.bold('🎯 优化效果:'));
|
|
676
|
+
console.log(` - Base Build 预估: ${opt.baseBuildSizeFormatted}`);
|
|
677
|
+
console.log(` - 实际打包大小: ${playableReport.totalSizeFormatted}`);
|
|
678
|
+
console.log(` - 优化率: ${pc.green((opt.optimizationRate ?? 0).toFixed(1) + '%')}`);
|
|
679
|
+
}
|
|
680
|
+
// 显示 Top 资源
|
|
681
|
+
const topAssets = playableReport.assets.slice(0, 10);
|
|
682
|
+
if (topAssets.length > 0) {
|
|
683
|
+
console.log('\n' + pc.bold('🔝 Top 10 资源:'));
|
|
684
|
+
topAssets.forEach((asset, index) => {
|
|
685
|
+
console.log(` ${index + 1}. ${asset.name}: ${asset.sizeFormatted} (${asset.percentage.toFixed(2)}%)`);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
640
688
|
}
|
|
641
|
-
|
|
642
|
-
console.
|
|
643
|
-
analysis.topAssets.forEach((asset) => {
|
|
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}`);
|
|
651
|
-
});
|
|
689
|
+
catch (error) {
|
|
690
|
+
console.error(pc.red('生成 Playable 分析报告失败:'), error);
|
|
652
691
|
}
|
|
692
|
+
// 3. 显示旧的分析信息(向后兼容)
|
|
693
|
+
const analysis = await analyzeBaseBuild(baseBuildDir, buildOptions.ammoReplacement);
|
|
653
694
|
if (analysis.hasAmmo) {
|
|
654
|
-
console.log(pc.yellow(
|
|
695
|
+
console.log(pc.yellow(`\n⚠️ 检测到 Ammo.js,建议替换为 ${analysis.suggestedEngine}`));
|
|
655
696
|
}
|
|
656
697
|
}
|
|
657
698
|
// 清理临时目录(调试时注释掉)
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,8 @@ program
|
|
|
131
131
|
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: MainMenu,Gameplay')
|
|
132
132
|
.option('--clean', '构建前清理旧的输出目录(默认启用)', true)
|
|
133
133
|
.option('--no-clean', '跳过清理旧的输出目录')
|
|
134
|
-
.option('--compress', '
|
|
134
|
+
.option('--compress', '压缩引擎代码(使用 LZ4)')
|
|
135
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积)')
|
|
135
136
|
.option('--analyze', '生成打包分析报告')
|
|
136
137
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
137
138
|
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
|
@@ -178,7 +179,8 @@ program
|
|
|
178
179
|
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
179
180
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
180
181
|
.option('-c, --config <file>', '配置文件路径')
|
|
181
|
-
.option('--compress', '
|
|
182
|
+
.option('--compress', '压缩引擎代码(使用 LZ4)')
|
|
183
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积)')
|
|
182
184
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
183
185
|
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
|
184
186
|
.option('--mode <mode>', '构建模式 (full|base|playable)')
|
|
@@ -214,7 +216,8 @@ program
|
|
|
214
216
|
.option('-c, --config <file>', '配置文件路径')
|
|
215
217
|
.option('--clean', '构建前清理旧的输出目录(默认不清理,使用覆盖模式)')
|
|
216
218
|
.option('--no-clean', '使用覆盖模式(默认行为)')
|
|
217
|
-
.option('--compress', '
|
|
219
|
+
.option('--compress', '压缩引擎代码(使用 LZ4)')
|
|
220
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积)')
|
|
218
221
|
.option('--analyze', '生成打包分析报告')
|
|
219
222
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
220
223
|
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"playcraft": "./dist/index.js"
|
|
7
|
+
"playcraft": "./dist/index.js",
|
|
8
|
+
"playcraft-test": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|
|
@@ -21,32 +22,32 @@
|
|
|
21
22
|
"release": "node scripts/release.js"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@playcraft/common": "^0.0.
|
|
25
|
-
"@playcraft/build": "^0.0.
|
|
25
|
+
"@playcraft/common": "^0.0.6",
|
|
26
|
+
"@playcraft/build": "^0.0.13",
|
|
26
27
|
"chokidar": "^4.0.3",
|
|
27
28
|
"commander": "^13.1.0",
|
|
28
|
-
"cors": "^2.8.
|
|
29
|
+
"cors": "^2.8.6",
|
|
29
30
|
"cosmiconfig": "^9.0.0",
|
|
30
31
|
"dotenv": "^17.2.3",
|
|
31
32
|
"drizzle-orm": "^0.45.1",
|
|
32
|
-
"execa": "^9.
|
|
33
|
+
"execa": "^9.6.1",
|
|
33
34
|
"express": "^5.2.1",
|
|
35
|
+
"inquirer": "^9.3.8",
|
|
34
36
|
"latest-version": "^7.0.0",
|
|
35
|
-
"inquirer": "^9.2.12",
|
|
36
37
|
"ora": "^8.2.0",
|
|
37
38
|
"picocolors": "^1.1.1",
|
|
38
39
|
"update-notifier": "^7.3.1",
|
|
39
|
-
"ws": "^8.
|
|
40
|
-
"zod": "^3.
|
|
40
|
+
"ws": "^8.19.0",
|
|
41
|
+
"zod": "^3.25.76"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@types/cors": "^2.8.
|
|
44
|
-
"@types/express": "^5.0.
|
|
45
|
-
"@types/inquirer": "^9.0.
|
|
46
|
-
"@types/node": "^22.
|
|
47
|
-
"@types/update-notifier": "^6.0.
|
|
48
|
-
"@types/ws": "^8.
|
|
49
|
-
"typescript": "^5.
|
|
44
|
+
"@types/cors": "^2.8.19",
|
|
45
|
+
"@types/express": "^5.0.6",
|
|
46
|
+
"@types/inquirer": "^9.0.9",
|
|
47
|
+
"@types/node": "^22.19.8",
|
|
48
|
+
"@types/update-notifier": "^6.0.8",
|
|
49
|
+
"@types/ws": "^8.18.1",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
50
51
|
"vitest": "^3.2.4"
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = path.dirname(__filename);
|
|
6
|
-
/**
|
|
7
|
-
* 基础构建器 - 生成可运行的多文件构建产物
|
|
8
|
-
*
|
|
9
|
-
* 职责:
|
|
10
|
-
* 1. 从源代码或构建产物加载项目
|
|
11
|
-
* 2. 确保所有必需文件存在且格式正确
|
|
12
|
-
* 3. 输出可直接运行的多文件版本
|
|
13
|
-
* 4. 不做任何内联或压缩
|
|
14
|
-
*/
|
|
15
|
-
export class BaseBuilder {
|
|
16
|
-
projectDir;
|
|
17
|
-
options;
|
|
18
|
-
constructor(projectDir, options) {
|
|
19
|
-
this.projectDir = projectDir;
|
|
20
|
-
this.options = options;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 执行基础构建
|
|
24
|
-
*/
|
|
25
|
-
async build() {
|
|
26
|
-
// 1. 检测项目类型
|
|
27
|
-
const projectType = await this.detectProjectType();
|
|
28
|
-
if (projectType === 'official-build') {
|
|
29
|
-
// 官方构建产物 - 直接复制并验证
|
|
30
|
-
return await this.buildFromOfficial();
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// 源代码 - 需要编译
|
|
34
|
-
throw new Error('源代码构建暂未实现,请使用官方构建产物。\n' +
|
|
35
|
-
'💡 推荐:先使用 PlayCanvas REST API 下载构建版本,然后再打包为 Playable Ad。');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* 检测项目类型
|
|
40
|
-
*/
|
|
41
|
-
async detectProjectType() {
|
|
42
|
-
// 检查是否是官方构建产物
|
|
43
|
-
const buildIndicators = [
|
|
44
|
-
path.join(this.projectDir, 'index.html'),
|
|
45
|
-
path.join(this.projectDir, 'config.json'),
|
|
46
|
-
];
|
|
47
|
-
try {
|
|
48
|
-
await fs.access(buildIndicators[0]);
|
|
49
|
-
await fs.access(buildIndicators[1]);
|
|
50
|
-
return 'official-build';
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
return 'source';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* 从官方构建产物构建
|
|
58
|
-
*/
|
|
59
|
-
async buildFromOfficial() {
|
|
60
|
-
// 验证必需文件存在
|
|
61
|
-
await this.validateOfficialBuild();
|
|
62
|
-
// 创建输出目录
|
|
63
|
-
await fs.mkdir(this.options.outputDir, { recursive: true });
|
|
64
|
-
// 复制所有文件到输出目录
|
|
65
|
-
const files = await this.copyBuildFiles();
|
|
66
|
-
return {
|
|
67
|
-
outputDir: this.options.outputDir,
|
|
68
|
-
files,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* 验证官方构建产物
|
|
73
|
-
*/
|
|
74
|
-
async validateOfficialBuild() {
|
|
75
|
-
const requiredFiles = [
|
|
76
|
-
'index.html',
|
|
77
|
-
'config.json',
|
|
78
|
-
'__start__.js',
|
|
79
|
-
];
|
|
80
|
-
const missingFiles = [];
|
|
81
|
-
for (const file of requiredFiles) {
|
|
82
|
-
try {
|
|
83
|
-
await fs.access(path.join(this.projectDir, file));
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
missingFiles.push(file);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (missingFiles.length > 0) {
|
|
90
|
-
throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
91
|
-
`请确保项目目录包含完整的构建产物。`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* 复制构建文件到输出目录
|
|
96
|
-
*/
|
|
97
|
-
async copyBuildFiles() {
|
|
98
|
-
const files = {
|
|
99
|
-
html: '',
|
|
100
|
-
engine: null,
|
|
101
|
-
config: '',
|
|
102
|
-
settings: null,
|
|
103
|
-
modules: null,
|
|
104
|
-
start: '',
|
|
105
|
-
scenes: [],
|
|
106
|
-
assets: [],
|
|
107
|
-
};
|
|
108
|
-
// 复制 index.html
|
|
109
|
-
const htmlPath = path.join(this.projectDir, 'index.html');
|
|
110
|
-
const outputHtmlPath = path.join(this.options.outputDir, 'index.html');
|
|
111
|
-
await fs.copyFile(htmlPath, outputHtmlPath);
|
|
112
|
-
files.html = outputHtmlPath;
|
|
113
|
-
// 复制 config.json
|
|
114
|
-
const configPath = path.join(this.projectDir, 'config.json');
|
|
115
|
-
const outputConfigPath = path.join(this.options.outputDir, 'config.json');
|
|
116
|
-
await fs.copyFile(configPath, outputConfigPath);
|
|
117
|
-
files.config = outputConfigPath;
|
|
118
|
-
// 读取 config.json 以获取场景信息
|
|
119
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
120
|
-
const configJson = JSON.parse(configContent);
|
|
121
|
-
// 复制 __start__.js
|
|
122
|
-
const startPath = path.join(this.projectDir, '__start__.js');
|
|
123
|
-
const outputStartPath = path.join(this.options.outputDir, '__start__.js');
|
|
124
|
-
await fs.copyFile(startPath, outputStartPath);
|
|
125
|
-
files.start = outputStartPath;
|
|
126
|
-
// 复制 __settings__.js(如果存在)
|
|
127
|
-
const settingsPath = path.join(this.projectDir, '__settings__.js');
|
|
128
|
-
try {
|
|
129
|
-
await fs.access(settingsPath);
|
|
130
|
-
const outputSettingsPath = path.join(this.options.outputDir, '__settings__.js');
|
|
131
|
-
await fs.copyFile(settingsPath, outputSettingsPath);
|
|
132
|
-
files.settings = outputSettingsPath;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
// __settings__.js 不是必需的
|
|
136
|
-
}
|
|
137
|
-
// 复制 __modules__.js(如果存在)
|
|
138
|
-
const modulesPath = path.join(this.projectDir, '__modules__.js');
|
|
139
|
-
try {
|
|
140
|
-
await fs.access(modulesPath);
|
|
141
|
-
const outputModulesPath = path.join(this.options.outputDir, '__modules__.js');
|
|
142
|
-
await fs.copyFile(modulesPath, outputModulesPath);
|
|
143
|
-
files.modules = outputModulesPath;
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
// __modules__.js 不是必需的
|
|
147
|
-
}
|
|
148
|
-
// 复制 PlayCanvas Engine(查找可能的文件名)
|
|
149
|
-
const engineNames = [
|
|
150
|
-
'playcanvas-stable.min.js',
|
|
151
|
-
'playcanvas.min.js',
|
|
152
|
-
'__lib__.js',
|
|
153
|
-
];
|
|
154
|
-
for (const engineName of engineNames) {
|
|
155
|
-
const enginePath = path.join(this.projectDir, engineName);
|
|
156
|
-
try {
|
|
157
|
-
await fs.access(enginePath);
|
|
158
|
-
const outputEnginePath = path.join(this.options.outputDir, engineName);
|
|
159
|
-
await fs.copyFile(enginePath, outputEnginePath);
|
|
160
|
-
files.engine = outputEnginePath;
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
// 继续尝试下一个
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// 复制场景文件
|
|
168
|
-
if (configJson.scenes && Array.isArray(configJson.scenes)) {
|
|
169
|
-
for (const scene of configJson.scenes) {
|
|
170
|
-
if (scene.url && !scene.url.startsWith('data:')) {
|
|
171
|
-
const scenePath = path.join(this.projectDir, scene.url);
|
|
172
|
-
try {
|
|
173
|
-
await fs.access(scenePath);
|
|
174
|
-
const sceneDir = path.dirname(scene.url);
|
|
175
|
-
if (sceneDir && sceneDir !== '.') {
|
|
176
|
-
const outputSceneDir = path.join(this.options.outputDir, sceneDir);
|
|
177
|
-
await fs.mkdir(outputSceneDir, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
const outputScenePath = path.join(this.options.outputDir, scene.url);
|
|
180
|
-
await fs.copyFile(scenePath, outputScenePath);
|
|
181
|
-
files.scenes.push(outputScenePath);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
console.warn(`警告: 场景文件不存在: ${scene.url}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// 复制资产文件(从 config.json 中的 assets)
|
|
190
|
-
if (configJson.assets) {
|
|
191
|
-
const assetsDir = path.join(this.options.outputDir, 'files');
|
|
192
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
193
|
-
for (const [assetId, assetData] of Object.entries(configJson.assets)) {
|
|
194
|
-
const asset = assetData;
|
|
195
|
-
if (asset.file && asset.file.url && !asset.file.url.startsWith('data:')) {
|
|
196
|
-
const assetPath = path.join(this.projectDir, asset.file.url);
|
|
197
|
-
try {
|
|
198
|
-
await fs.access(assetPath);
|
|
199
|
-
const assetDir = path.dirname(asset.file.url);
|
|
200
|
-
if (assetDir && assetDir !== '.') {
|
|
201
|
-
const outputAssetDir = path.join(this.options.outputDir, assetDir);
|
|
202
|
-
await fs.mkdir(outputAssetDir, { recursive: true });
|
|
203
|
-
}
|
|
204
|
-
const outputAssetPath = path.join(this.options.outputDir, asset.file.url);
|
|
205
|
-
await fs.copyFile(assetPath, outputAssetPath);
|
|
206
|
-
files.assets.push(outputAssetPath);
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
// 资产文件可能不存在(可能是内联的)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// 复制 files/ 目录(如果存在)
|
|
215
|
-
const filesDir = path.join(this.projectDir, 'files');
|
|
216
|
-
try {
|
|
217
|
-
const filesDirStat = await fs.stat(filesDir);
|
|
218
|
-
if (filesDirStat.isDirectory()) {
|
|
219
|
-
const outputFilesDir = path.join(this.options.outputDir, 'files');
|
|
220
|
-
await this.copyDirectory(filesDir, outputFilesDir);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
// files/ 目录可能不存在
|
|
225
|
-
}
|
|
226
|
-
// 复制 styles.css(如果存在)
|
|
227
|
-
const stylesPath = path.join(this.projectDir, 'styles.css');
|
|
228
|
-
try {
|
|
229
|
-
await fs.access(stylesPath);
|
|
230
|
-
const outputStylesPath = path.join(this.options.outputDir, 'styles.css');
|
|
231
|
-
await fs.copyFile(stylesPath, outputStylesPath);
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
// styles.css 可能不存在
|
|
235
|
-
}
|
|
236
|
-
// 复制 manifest.json(如果存在)
|
|
237
|
-
const manifestPath = path.join(this.projectDir, 'manifest.json');
|
|
238
|
-
try {
|
|
239
|
-
await fs.access(manifestPath);
|
|
240
|
-
const outputManifestPath = path.join(this.options.outputDir, 'manifest.json');
|
|
241
|
-
await fs.copyFile(manifestPath, outputManifestPath);
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
// manifest.json 可能不存在
|
|
245
|
-
}
|
|
246
|
-
return files;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* 递归复制目录
|
|
250
|
-
*/
|
|
251
|
-
async copyDirectory(src, dest) {
|
|
252
|
-
await fs.mkdir(dest, { recursive: true });
|
|
253
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
254
|
-
for (const entry of entries) {
|
|
255
|
-
const srcPath = path.join(src, entry.name);
|
|
256
|
-
const destPath = path.join(dest, entry.name);
|
|
257
|
-
if (entry.isDirectory()) {
|
|
258
|
-
await this.copyDirectory(srcPath, destPath);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
await fs.copyFile(srcPath, destPath);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|