@playcraft/cli 0.0.17 → 0.0.19

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.
@@ -3,8 +3,17 @@ import path from 'path';
3
3
  export async function loadBuildConfigFromFile(configPath) {
4
4
  const content = await fs.readFile(configPath, 'utf-8');
5
5
  const parsed = JSON.parse(content);
6
- if (parsed && typeof parsed === 'object' && 'build' in parsed) {
7
- return parsed.build || {};
6
+ if (parsed && typeof parsed === 'object') {
7
+ // 优先从 build 字段读取(创建副本避免修改原始对象,确保是普通对象)
8
+ const buildConfig = (parsed.build && typeof parsed.build === 'object' && !Array.isArray(parsed.build)) ? { ...parsed.build } : {};
9
+ // 如果 build 中没有 storeUrls,尝试从 agent 字段读取
10
+ if (!buildConfig.storeUrls && parsed.agent?.storeUrls) {
11
+ buildConfig.storeUrls = parsed.agent.storeUrls;
12
+ }
13
+ // 如果有 build 或 agent 配置,返回合并后的 buildConfig
14
+ if (parsed.build || parsed.agent) {
15
+ return buildConfig;
16
+ }
8
17
  }
9
18
  return parsed || {};
10
19
  }
@@ -254,15 +254,43 @@ export async function buildCommand(projectPath, options) {
254
254
  if (!options.output && fileConfig.outputDir) {
255
255
  options.output = fileConfig.outputDir;
256
256
  }
257
+ // 从配置文件读取 storeUrls(避免重复输入)
258
+ if (!options.storeUrls && fileConfig.storeUrls) {
259
+ options.storeUrls = fileConfig.storeUrls;
260
+ console.log(pc.dim(`\nℹ️ 从配置文件读取商店地址`));
261
+ }
257
262
  }
258
263
  // 解析场景选择参数(支持 PlayCanvas 和 PlayCraft 项目)
259
264
  let selectedScenes;
260
265
  if (isPlayCanvas) {
261
266
  if (options.scenes) {
262
267
  // 命令行指定了场景参数
263
- selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
264
- if (selectedScenes.length > 0) {
265
- console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
268
+ const scenesInput = options.scenes.trim().toLowerCase();
269
+ // 检查特殊关键字
270
+ if (scenesInput === 'all') {
271
+ // 打包所有场景
272
+ selectedScenes = undefined; // undefined 表示所有场景
273
+ console.log(`\n🎬 选中场景: 全部场景`);
274
+ }
275
+ else if (scenesInput === 'default') {
276
+ // 打包默认场景
277
+ const projectScenes = await detectProjectScenes(resolvedProjectPath);
278
+ const defaultScene = projectScenes.find(s => s.isMain === true) || projectScenes[0];
279
+ if (defaultScene) {
280
+ selectedScenes = [defaultScene.name || String(defaultScene.id)];
281
+ console.log(`\n🎬 选中场景: ${defaultScene.name || defaultScene.id}${defaultScene.isMain ? ' (默认场景)' : ' (第一个场景)'}`);
282
+ }
283
+ else {
284
+ console.log(`\n⚠️ 未找到默认场景,将打包所有场景`);
285
+ selectedScenes = undefined;
286
+ }
287
+ }
288
+ else {
289
+ // 普通场景名称列表
290
+ selectedScenes = options.scenes.split(',').map(s => s.trim()).filter(s => s);
291
+ if (selectedScenes.length > 0) {
292
+ console.log(`\n🎬 选中场景: ${selectedScenes.join(', ')}`);
293
+ }
266
294
  }
267
295
  }
268
296
  else if (options.mode !== 'playable') {
@@ -429,27 +457,32 @@ export async function buildCommand(projectPath, options) {
429
457
  ]);
430
458
  selectedFormat = formatAnswer.format;
431
459
  }
432
- // 输入商店跳转地址(CTA)- 必填
433
- console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
434
- console.log(pc.dim(' iOS Android 地址均为必填'));
435
- const storeUrlAnswer = await inquirer.prompt([
436
- {
437
- type: 'input',
438
- name: 'iosStoreUrl',
439
- message: 'iOS App Store URL:',
440
- validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
441
- },
442
- {
443
- type: 'input',
444
- name: 'androidStoreUrl',
445
- message: 'Android Google Play URL:',
446
- validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
447
- },
448
- ]);
449
- options.storeUrls = {
450
- ios: storeUrlAnswer.iosStoreUrl.trim(),
451
- android: storeUrlAnswer.androidStoreUrl.trim(),
452
- };
460
+ // 输入商店跳转地址(CTA)- 如果配置文件中没有则必填
461
+ if (!options.storeUrls) {
462
+ console.log(pc.cyan('\n🔗 商店跳转地址(CTA 按钮目标)'));
463
+ console.log(pc.dim(' iOS Android 地址均为必填'));
464
+ const storeUrlAnswer = await inquirer.prompt([
465
+ {
466
+ type: 'input',
467
+ name: 'iosStoreUrl',
468
+ message: 'iOS App Store URL:',
469
+ validate: (input) => input.trim() ? true : '请输入 iOS App Store URL',
470
+ },
471
+ {
472
+ type: 'input',
473
+ name: 'androidStoreUrl',
474
+ message: 'Android Google Play URL:',
475
+ validate: (input) => input.trim() ? true : '请输入 Android Google Play URL',
476
+ },
477
+ ]);
478
+ options.storeUrls = {
479
+ ios: storeUrlAnswer.iosStoreUrl.trim(),
480
+ android: storeUrlAnswer.androidStoreUrl.trim(),
481
+ };
482
+ }
483
+ else {
484
+ console.log(pc.dim(`\n✅ 使用配置文件中的商店地址`));
485
+ }
453
486
  // 选择输出目录
454
487
  const outputAnswer = await inquirer.prompt([
455
488
  {
package/dist/index.js CHANGED
@@ -129,7 +129,7 @@ program
129
129
  .option('-f, --format <format>', '输出格式 (html|zip)', 'html')
130
130
  .option('-o, --output <path>', '输出目录', './dist')
131
131
  .option('-c, --config <file>', '配置文件路径')
132
- .option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: "Main Scene" MainMenu,Gameplay')
132
+ .option('-s, --scenes <scenes>', '选择场景: "all"=所有场景, "default"=默认场景, 或逗号分隔的场景名,如 "MainMenu,Gameplay"')
133
133
  .option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
134
134
  .option('--clean', '构建前清理旧的输出目录(默认启用)', true)
135
135
  .option('--no-clean', '跳过清理旧的输出目录')
@@ -165,7 +165,7 @@ program
165
165
  .description('生成可运行的多文件构建产物(阶段1)')
166
166
  .argument('<project-path>', '项目路径')
167
167
  .option('-o, --output <path>', '输出目录', './build')
168
- .option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: "Main Scene" MainMenu,Gameplay')
168
+ .option('-s, --scenes <scenes>', '选择场景: "all"=所有场景, "default"=默认场景, 或逗号分隔的场景名,如 "MainMenu,Gameplay"')
169
169
  .option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
170
170
  .action(async (projectPath, options) => {
171
171
  await buildCommand(projectPath, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,13 +17,13 @@
17
17
  "build": "tsc",
18
18
  "start": "node dist/index.js",
19
19
  "test": "vitest run",
20
- "link": "pnpm build && npm link",
20
+ "link": "node scripts/bump-local.js && pnpm build && npm link",
21
21
  "unlink": "npm unlink -g @playcraft/cli",
22
22
  "release": "node scripts/release.js"
23
23
  },
24
24
  "dependencies": {
25
- "@playcraft/common": "^0.0.7",
26
- "@playcraft/build": "^0.0.14",
25
+ "@playcraft/common": "^0.0.9",
26
+ "@playcraft/build": "^0.0.17",
27
27
  "chokidar": "^4.0.3",
28
28
  "commander": "^13.1.0",
29
29
  "cors": "^2.8.6",
@@ -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
- }