@playcraft/cli 0.0.14 → 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 -176
- package/dist/fs-handler.js +117 -0
- package/dist/index.js +59 -14
- 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 +5 -4
package/dist/fs-handler.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
/** Maximum recursion depth for file search to prevent performance issues */
|
|
4
|
+
const MAX_SEARCH_DEPTH = 10;
|
|
5
|
+
/** Cache TTL in milliseconds (5 minutes) */
|
|
6
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
3
7
|
export class FSHandler {
|
|
4
8
|
config;
|
|
9
|
+
/** Cache for file name to path mapping */
|
|
10
|
+
filePathCache = new Map();
|
|
5
11
|
constructor(config) {
|
|
6
12
|
this.config = config;
|
|
7
13
|
}
|
|
@@ -93,6 +99,8 @@ export class FSHandler {
|
|
|
93
99
|
else {
|
|
94
100
|
await fs.writeFile(fullPath, content);
|
|
95
101
|
}
|
|
102
|
+
// Invalidate cache for the affected file
|
|
103
|
+
this.invalidateCache(relativePath);
|
|
96
104
|
}
|
|
97
105
|
async deleteFile(relativePath) {
|
|
98
106
|
if (!this.isSafePath(relativePath)) {
|
|
@@ -100,5 +108,114 @@ export class FSHandler {
|
|
|
100
108
|
}
|
|
101
109
|
const fullPath = path.join(this.config.dir, relativePath);
|
|
102
110
|
await fs.unlink(fullPath);
|
|
111
|
+
// Invalidate cache for the deleted file
|
|
112
|
+
this.invalidateCache(relativePath);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Recursively search for a file by name within a directory.
|
|
116
|
+
* Returns the relative path if found, or null.
|
|
117
|
+
* Uses caching and depth limiting to optimize performance.
|
|
118
|
+
*/
|
|
119
|
+
async findFileRecursive(dir, fileName) {
|
|
120
|
+
// Normalize directory path for consistent cache keys
|
|
121
|
+
const normalizedDir = this.normalizePath(dir);
|
|
122
|
+
// Check cache first
|
|
123
|
+
const cacheKey = `${normalizedDir}:${fileName}`;
|
|
124
|
+
const cached = this.filePathCache.get(cacheKey);
|
|
125
|
+
if (cached) {
|
|
126
|
+
// Validate cache entry is not expired
|
|
127
|
+
if (Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
128
|
+
// Verify the cached path still exists
|
|
129
|
+
try {
|
|
130
|
+
const fullPath = path.join(this.config.dir, cached.path);
|
|
131
|
+
await fs.access(fullPath);
|
|
132
|
+
return cached.path;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// File no longer exists, remove from cache
|
|
136
|
+
this.filePathCache.delete(cacheKey);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Cache expired
|
|
141
|
+
this.filePathCache.delete(cacheKey);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const result = await this.findFileRecursiveInternal(normalizedDir, fileName, 0);
|
|
145
|
+
// Cache the result if found
|
|
146
|
+
if (result) {
|
|
147
|
+
this.filePathCache.set(cacheKey, {
|
|
148
|
+
path: result,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Internal recursive search with depth limiting.
|
|
156
|
+
*/
|
|
157
|
+
async findFileRecursiveInternal(dir, fileName, depth) {
|
|
158
|
+
// Limit recursion depth to prevent performance issues
|
|
159
|
+
if (depth >= MAX_SEARCH_DEPTH)
|
|
160
|
+
return null;
|
|
161
|
+
if (!this.isSafePath(dir))
|
|
162
|
+
return null;
|
|
163
|
+
const fullDir = path.join(this.config.dir, dir);
|
|
164
|
+
try {
|
|
165
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
166
|
+
// First pass: check files in current directory (breadth-first approach)
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (entry.name.startsWith('.'))
|
|
169
|
+
continue;
|
|
170
|
+
if (entry.isFile() && entry.name === fileName) {
|
|
171
|
+
return path.posix.join(dir, entry.name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Second pass: recurse into subdirectories
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (entry.name.startsWith('.'))
|
|
177
|
+
continue;
|
|
178
|
+
if (entry.isDirectory()) {
|
|
179
|
+
const rel = path.posix.join(dir, entry.name);
|
|
180
|
+
const found = await this.findFileRecursiveInternal(rel, fileName, depth + 1);
|
|
181
|
+
if (found)
|
|
182
|
+
return found;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// ignore errors (e.g. permission denied)
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Normalize path separators to forward slashes for consistent cache key matching.
|
|
193
|
+
*/
|
|
194
|
+
normalizePath(p) {
|
|
195
|
+
return p.replace(/\\/g, '/');
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Invalidate cache entries for a specific file path.
|
|
199
|
+
* Call this when files are written or deleted.
|
|
200
|
+
*/
|
|
201
|
+
invalidateCache(relativePath) {
|
|
202
|
+
if (!relativePath) {
|
|
203
|
+
this.filePathCache.clear();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Normalize path separators for consistent matching
|
|
207
|
+
const normalizedPath = this.normalizePath(relativePath);
|
|
208
|
+
const fileName = path.basename(normalizedPath);
|
|
209
|
+
const dirName = path.dirname(normalizedPath);
|
|
210
|
+
// Build the exact cache key that would have been used
|
|
211
|
+
// Cache key format: "dir:fileName" (consistent with findFileRecursive)
|
|
212
|
+
const exactKey = `${dirName}:${fileName}`;
|
|
213
|
+
// Remove entries by exact key match or by result path match
|
|
214
|
+
for (const [key, entry] of this.filePathCache) {
|
|
215
|
+
const normalizedEntryPath = this.normalizePath(entry.path);
|
|
216
|
+
if (key === exactKey || normalizedEntryPath === normalizedPath) {
|
|
217
|
+
this.filePathCache.delete(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
103
220
|
}
|
|
104
221
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { statusCommand } from './commands/status.js';
|
|
|
10
10
|
import { logsCommand } from './commands/logs.js';
|
|
11
11
|
import { configCommand } from './commands/config.js';
|
|
12
12
|
import { buildCommand } from './commands/build.js';
|
|
13
|
+
import { buildAllCommand } from './commands/build-all.js';
|
|
13
14
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
14
15
|
import { checkForUpdates } from './utils/version-checker.js';
|
|
15
16
|
import { syncCommand } from './commands/sync.js';
|
|
@@ -124,17 +125,20 @@ program
|
|
|
124
125
|
.command('build')
|
|
125
126
|
.description('打包项目为 Playable Ad(自动执行基础构建和渠道打包)')
|
|
126
127
|
.argument('[project-path]', '项目路径', process.cwd())
|
|
127
|
-
.option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
|
|
128
|
+
.option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo|inmobi|adikteev|remerge|mintegral)')
|
|
128
129
|
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
129
130
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
130
131
|
.option('-c, --config <file>', '配置文件路径')
|
|
131
|
-
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: MainMenu,Gameplay')
|
|
132
|
+
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: "Main Scene" 或 MainMenu,Gameplay')
|
|
133
|
+
.option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
|
|
132
134
|
.option('--clean', '构建前清理旧的输出目录(默认启用)', true)
|
|
133
135
|
.option('--no-clean', '跳过清理旧的输出目录')
|
|
134
|
-
.option('--compress', '
|
|
136
|
+
.option('--compress', '压缩引擎代码(使用 LZ4,仅 PlayCanvas)')
|
|
137
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积,仅 PlayCanvas)')
|
|
138
|
+
.option('--compress-js', '压缩所有 JS 代码(LZ4 源码保护,浏览器中无法查看变量名和方法名)')
|
|
135
139
|
.option('--analyze', '生成打包分析报告')
|
|
136
|
-
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
137
|
-
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
|
140
|
+
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon(仅 PlayCanvas)')
|
|
141
|
+
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon),仅 PlayCanvas')
|
|
138
142
|
.option('--auto-build', '如果是源代码项目,自动执行本地构建')
|
|
139
143
|
.option('--skip-build', '跳过本地构建,直接从源代码打包(可能不完整)')
|
|
140
144
|
.option('--mode <mode>', '构建模式 (full|base|playable)')
|
|
@@ -149,7 +153,7 @@ program
|
|
|
149
153
|
.option('--image-quality <quality>', '图片质量 0-100(默认根据平台配置)', parseInt)
|
|
150
154
|
.option('--convert-webp', '转换为 WebP 格式(默认启用)', true)
|
|
151
155
|
.option('--no-convert-webp', '禁用 WebP 转换')
|
|
152
|
-
.option('--compress-models', '压缩 3D
|
|
156
|
+
.option('--compress-models', '压缩 3D 模型(默认根据平台配置,仅 PlayCanvas/Three.js/Babylon.js)')
|
|
153
157
|
.option('--no-compress-models', '禁用模型压缩')
|
|
154
158
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
155
159
|
.action(async (projectPath, options) => {
|
|
@@ -161,7 +165,8 @@ program
|
|
|
161
165
|
.description('生成可运行的多文件构建产物(阶段1)')
|
|
162
166
|
.argument('<project-path>', '项目路径')
|
|
163
167
|
.option('-o, --output <path>', '输出目录', './build')
|
|
164
|
-
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: MainMenu,Gameplay')
|
|
168
|
+
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: "Main Scene" 或 MainMenu,Gameplay')
|
|
169
|
+
.option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
|
|
165
170
|
.action(async (projectPath, options) => {
|
|
166
171
|
await buildCommand(projectPath, {
|
|
167
172
|
...options,
|
|
@@ -174,13 +179,16 @@ program
|
|
|
174
179
|
.command('analyze')
|
|
175
180
|
.description('生成打包分析报告(可视化 + 体积建议)')
|
|
176
181
|
.argument('[project-path]', '项目路径', process.cwd())
|
|
177
|
-
.option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
|
|
182
|
+
.option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo|inmobi|adikteev|remerge|mintegral)')
|
|
178
183
|
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
179
184
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
180
185
|
.option('-c, --config <file>', '配置文件路径')
|
|
181
|
-
.option('--
|
|
182
|
-
.option('--
|
|
183
|
-
.option('--
|
|
186
|
+
.option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
|
|
187
|
+
.option('--compress', '压缩引擎代码(使用 LZ4,仅 PlayCanvas)')
|
|
188
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积,仅 PlayCanvas)')
|
|
189
|
+
.option('--compress-js', '压缩所有 JS 代码(LZ4 源码保护)')
|
|
190
|
+
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon(仅 PlayCanvas)')
|
|
191
|
+
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon),仅 PlayCanvas')
|
|
184
192
|
.option('--mode <mode>', '构建模式 (full|base|playable)')
|
|
185
193
|
.option('--use-vite', '使用 Vite 构建(默认启用)', true)
|
|
186
194
|
.option('--no-use-vite', '禁用 Vite,使用旧构建器')
|
|
@@ -193,7 +201,7 @@ program
|
|
|
193
201
|
.option('--image-quality <quality>', '图片质量 0-100(默认根据平台配置)', parseInt)
|
|
194
202
|
.option('--convert-webp', '转换为 WebP 格式(默认启用)', true)
|
|
195
203
|
.option('--no-convert-webp', '禁用 WebP 转换')
|
|
196
|
-
.option('--compress-models', '压缩 3D
|
|
204
|
+
.option('--compress-models', '压缩 3D 模型(默认根据平台配置,仅 PlayCanvas/Three.js/Babylon.js)')
|
|
197
205
|
.option('--no-compress-models', '禁用模型压缩')
|
|
198
206
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
199
207
|
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
@@ -208,13 +216,14 @@ program
|
|
|
208
216
|
.command('build-playable')
|
|
209
217
|
.description('将多文件构建产物转换为单HTML Playable Ads(阶段2)')
|
|
210
218
|
.argument('<base-build-dir>', '多文件构建产物目录')
|
|
211
|
-
.requiredOption('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
|
|
219
|
+
.requiredOption('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo|inmobi|adikteev|remerge|mintegral)')
|
|
212
220
|
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
213
221
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
214
222
|
.option('-c, --config <file>', '配置文件路径')
|
|
215
223
|
.option('--clean', '构建前清理旧的输出目录(默认不清理,使用覆盖模式)')
|
|
216
224
|
.option('--no-clean', '使用覆盖模式(默认行为)')
|
|
217
|
-
.option('--compress', '
|
|
225
|
+
.option('--compress', '压缩引擎代码(使用 LZ4)')
|
|
226
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积)')
|
|
218
227
|
.option('--analyze', '生成打包分析报告')
|
|
219
228
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
220
229
|
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
|
@@ -239,6 +248,42 @@ program
|
|
|
239
248
|
mode: 'playable',
|
|
240
249
|
});
|
|
241
250
|
});
|
|
251
|
+
// build-all 命令 - 批量打渠道包
|
|
252
|
+
program
|
|
253
|
+
.command('build-all')
|
|
254
|
+
.description('批量打多渠道包(一次 Base Build + 多渠道 Channel Build)')
|
|
255
|
+
.argument('[project-path]', '项目路径', process.cwd())
|
|
256
|
+
.option('--platforms <platforms>', '目标平台列表(逗号分隔),例如: facebook,google,tiktok,applovin')
|
|
257
|
+
.option('--group <group>', '使用预定义分组 (all|mainstream|html|zip)')
|
|
258
|
+
.option('-o, --output <path>', '输出根目录(每个渠道一个子目录)', './dist')
|
|
259
|
+
.option('-c, --config <file>', '配置文件路径')
|
|
260
|
+
.option('-s, --scenes <scenes>', '选择场景(逗号分隔)')
|
|
261
|
+
.option('--clean', '构建前清理旧的输出目录', true)
|
|
262
|
+
.option('--no-clean', '跳过清理旧的输出目录')
|
|
263
|
+
.option('--compress', '压缩引擎代码(使用 LZ4)')
|
|
264
|
+
.option('--compress-config', '压缩 config.json(使用 LZ4)')
|
|
265
|
+
.option('--compress-js', '压缩所有 JS 代码(LZ4 源码保护)')
|
|
266
|
+
.option('--analyze', '生成打包分析报告')
|
|
267
|
+
.option('--replace-ammo', '检测到 Ammo 时自动替换')
|
|
268
|
+
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
|
|
269
|
+
.option('--use-vite', '使用 Vite 构建(默认启用)', true)
|
|
270
|
+
.option('--no-use-vite', '禁用 Vite,使用旧构建器')
|
|
271
|
+
.option('--css-minify', '压缩 CSS')
|
|
272
|
+
.option('--no-css-minify', '禁用 CSS 压缩')
|
|
273
|
+
.option('--js-minify', '压缩 JS')
|
|
274
|
+
.option('--no-js-minify', '禁用 JS 压缩')
|
|
275
|
+
.option('--compress-images', '压缩图片')
|
|
276
|
+
.option('--no-compress-images', '禁用图片压缩')
|
|
277
|
+
.option('--image-quality <quality>', '图片质量 0-100', parseInt)
|
|
278
|
+
.option('--convert-webp', '转换为 WebP 格式', true)
|
|
279
|
+
.option('--no-convert-webp', '禁用 WebP 转换')
|
|
280
|
+
.option('--compress-models', '压缩 3D 模型')
|
|
281
|
+
.option('--no-compress-models', '禁用模型压缩')
|
|
282
|
+
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
283
|
+
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
284
|
+
.action(async (projectPath, options) => {
|
|
285
|
+
await buildAllCommand(projectPath, options);
|
|
286
|
+
});
|
|
242
287
|
// 新增:inspect 命令 - 检查项目类型
|
|
243
288
|
program
|
|
244
289
|
.command('inspect')
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
}
|