@playcraft/build 0.1.36-beta1 → 0.1.38-beta

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/README.md CHANGED
@@ -209,4 +209,4 @@ console.log(adapter.getDefaultFormat()); // "html"
209
209
 
210
210
  - `@playcraft/cli`:CLI 工具
211
211
  - `@playcraft/worker`:队列消费与异步构建
212
- - `@playcraft/backend`:类型定义用于参数校验
212
+ - `@playcraft/backend-business-server`:类型定义用于参数校验
@@ -3,26 +3,17 @@ import path from 'path';
3
3
  /**
4
4
  * 引擎检测映射表 - 从 package.json 的 dependencies 检测引擎类型
5
5
  * 复用后端 ExternalImportService 的逻辑
6
- *
7
- * 多引擎项目(如 Three.js + PixiJS:3D 场景 + 2D HUD)必须按优先级定主引擎,
8
- * **不能所有项同分** —— 同分时结果取决于依赖在 package.json 里的书写顺序,
9
- * 换个顺序识别结果就变了。分层口径与后端 `engine-detect.ts` 保持一致:
10
- * - 完整游戏引擎(playcanvas / cocos / layaair / egret)优先级最高
11
- * - 3D 渲染库(three / babylonjs)次之
12
- * - 2D 框架 / 渲染库(phaser / pixijs)最低
13
6
  */
14
7
  const ENGINE_DETECTION_MAP = [
15
- // 完整游戏引擎
16
- { match: (n) => n === 'playcanvas', engine: 'playcanvas', priority: 300 },
17
- { match: (n) => n === 'cocos-creator' || n === 'cc', engine: 'cocos', priority: 300 },
18
- { match: (n) => n.startsWith('laya-') || n.startsWith('@layabox/'), engine: 'layaair', priority: 300 },
19
- { match: (n) => n === 'egret', engine: 'egret', priority: 300 },
20
- // 3D 渲染库
21
- { match: (n) => n === 'three', engine: 'threejs', priority: 200 },
22
- { match: (n) => n === 'babylonjs' || n.startsWith('@babylonjs/'), engine: 'babylonjs', priority: 200 },
23
- // 2D 框架 / 渲染库
8
+ // PlayCanvas - 优先检测(通过文件名而非 package.json)
24
9
  { match: (n) => n === 'phaser', engine: 'phaser', priority: 100 },
25
10
  { match: (n) => n === 'pixi.js' || n.startsWith('@pixi/'), engine: 'pixijs', priority: 100 },
11
+ { match: (n) => n === 'three', engine: 'threejs', priority: 100 },
12
+ { match: (n) => n === 'cocos-creator' || n === 'cc', engine: 'cocos', priority: 100 },
13
+ { match: (n) => n === 'playcanvas', engine: 'playcanvas', priority: 100 },
14
+ { match: (n) => n === 'babylonjs' || n.startsWith('@babylonjs/'), engine: 'babylonjs', priority: 100 },
15
+ { match: (n) => n.startsWith('laya-') || n.startsWith('@layabox/'), engine: 'layaair', priority: 100 },
16
+ { match: (n) => n === 'egret', engine: 'egret', priority: 100 },
26
17
  ];
27
18
  /**
28
19
  * 构建工具检测映射表 - 优先级高于引擎检测
@@ -49,78 +40,6 @@ const BUILD_TOOL_DETECTION_MAP = [
49
40
  priority: 200,
50
41
  },
51
42
  ];
52
- /**
53
- * 文件级引擎特征只做静态读取,不执行用户工程里的 JavaScript。
54
- *
55
- * 这条兜底覆盖引擎运行时由编辑器/平台内置、因此不会出现在 package.json
56
- * 的项目(典型是 Cocos Creator)。它也让 `packages/build` 与外部导入层的
57
- * 识别结果保持一致,避免导入时识别为 Cocos、真正构建时又退回 generic。
58
- */
59
- const FILE_ENGINE_LABELS = {
60
- phaser: 'phaser',
61
- pixi: 'pixijs',
62
- pixijs: 'pixijs',
63
- three: 'threejs',
64
- threejs: 'threejs',
65
- playcanvas: 'playcanvas',
66
- cocos: 'cocos',
67
- cocoscreator: 'cocos',
68
- cc: 'cocos',
69
- babylon: 'babylonjs',
70
- babylonjs: 'babylonjs',
71
- laya: 'layaair',
72
- layaair: 'layaair',
73
- egret: 'egret',
74
- };
75
- function normalizeFileEngineKey(value) {
76
- return value.trim().toLowerCase().replace(/[\s.]/g, '');
77
- }
78
- /** 从 builds.config.js 静态读取 engine 字符串字段,不执行文件内容。 */
79
- function readEngineFromBuildConfig(source) {
80
- // 仅移除注释,再做字段匹配;不会触发 require/import 或执行任意代码。
81
- const withoutComments = source
82
- .replace(/\/\*[\s\S]*?\*\//g, '')
83
- .replace(/(^|\s)\/\/.*$/gm, '$1');
84
- const match = /\bengine\s*:\s*['"`]([^'"`]+)['"`]/.exec(withoutComments);
85
- if (!match)
86
- return null;
87
- return FILE_ENGINE_LABELS[normalizeFileEngineKey(match[1])] ?? null;
88
- }
89
- function isRecord(value) {
90
- return typeof value === 'object' && value !== null && !Array.isArray(value);
91
- }
92
- /** 校验 Cocos Creator 配置内容,避免仅凭同名文件误判。 */
93
- function isCocosConfig(source) {
94
- try {
95
- const parsed = JSON.parse(source);
96
- return isRecord(parsed) && (isRecord(parsed.builder) || isRecord(parsed.engine));
97
- }
98
- catch {
99
- return false;
100
- }
101
- }
102
- async function detectEngineFromProjectFiles(projectDir) {
103
- try {
104
- const source = await fs.readFile(path.join(projectDir, 'builds.config.js'), 'utf8');
105
- const engine = readEngineFromBuildConfig(source);
106
- if (engine)
107
- return engine;
108
- }
109
- catch {
110
- // 文件不存在或不可读,继续检查 Cocos 工程标记。
111
- }
112
- for (const relativePath of ['settings/cocos.config.json', 'profiles/cocos.config.json']) {
113
- try {
114
- const source = await fs.readFile(path.join(projectDir, relativePath), 'utf8');
115
- if (isCocosConfig(source))
116
- return 'cocos';
117
- }
118
- catch {
119
- // 单个标记缺失不影响另一个位置的检查。
120
- }
121
- }
122
- return null;
123
- }
124
43
  /**
125
44
  * 引擎检测器
126
45
  * 根据项目文件结构自动检测引擎类型和构建工具
@@ -139,48 +58,24 @@ export class EngineDetector {
139
58
  }
140
59
  // 2. 检测 package.json
141
60
  const pkg = await this.readPackageJson(projectDir);
142
- const fileEngine = await detectEngineFromProjectFiles(projectDir);
143
61
  if (!pkg) {
144
- if (fileEngine) {
145
- console.log(`[EngineDetector] 检测到引擎: ${fileEngine}(通过文件特征)`);
146
- return { engine: fileEngine, buildTool: 'generic' };
147
- }
148
62
  throw new Error(`[EngineDetector] 无法识别项目类型:找不到 package.json 或 PlayCanvas 文件结构\n` +
149
63
  `项目目录: ${projectDir}\n` +
150
64
  `请确保这是一个有效的项目目录。`);
151
65
  }
152
- // 3. 先得到 package.json 引擎结果,用于处理「构建工具隐含引擎」与文件特征冲突。
153
- const packageEngine = this.detectFromPackageJson(pkg);
154
- // 4. 优先检测构建工具(BUILD_TOOL_DETECTION_MAP,优先级 200)。
155
- //
156
- // **engine 与 buildTool 必须解耦**:`@playcraft/adsdk` / `@playcraft/devkit`
157
- // 决定的是「用 devkit 打包」这件事,与项目是哪个引擎无关 —— Cocos 同样靠
158
- // devkit 出产物,走的是同一条链路。而 adsdk 表里的 `impliedEngine: 'phaser'`
159
- // 只是「大多数 adsdk 项目是 Phaser」的经验值,一旦项目里已经能明确识别出
160
- // 引擎(依赖或文件特征),就该以识别结果为准,且**不能因此把 buildTool
161
- // 一起退回 generic**,否则 Cocos / Three.js 会被踢出 devkit 链路。
66
+ // 3. 优先检测构建工具(BUILD_TOOL_DETECTION_MAP,优先级 200)
162
67
  const buildToolResult = this.detectBuildToolFromPackageJson(pkg);
163
68
  if (buildToolResult) {
164
- const knownEngine = packageEngine ?? fileEngine;
165
- if (knownEngine && knownEngine !== buildToolResult.engine) {
166
- console.log(`[EngineDetector] 检测到构建工具: ${buildToolResult.buildTool};` +
167
- `引擎以识别结果 ${knownEngine} 为准(构建工具隐含 ${buildToolResult.engine} 不予采用)`);
168
- return { engine: knownEngine, buildTool: buildToolResult.buildTool };
169
- }
170
69
  console.log(`[EngineDetector] 检测到构建工具: ${buildToolResult.buildTool}(隐含引擎: ${buildToolResult.engine})`);
171
70
  return buildToolResult;
172
71
  }
173
- // 5. 检测引擎类型(ENGINE_DETECTION_MAP,优先级 100)
174
- if (packageEngine) {
175
- console.log(`[EngineDetector] 检测到引擎: ${packageEngine}(通过 package.json)`);
176
- return { engine: packageEngine, buildTool: 'generic' };
177
- }
178
- // 6. 依赖未命中时使用文件级特征兜底。
179
- if (fileEngine) {
180
- console.log(`[EngineDetector] 检测到引擎: ${fileEngine}(通过文件特征)`);
181
- return { engine: fileEngine, buildTool: 'generic' };
72
+ // 4. 检测引擎类型(ENGINE_DETECTION_MAP,优先级 100)
73
+ const engine = this.detectFromPackageJson(pkg);
74
+ if (engine) {
75
+ console.log(`[EngineDetector] 检测到引擎: ${engine}(通过 package.json)`);
76
+ return { engine, buildTool: 'generic' };
182
77
  }
183
- // 7. 兜底
78
+ // 5. 兜底
184
79
  console.log('[EngineDetector] 无法识别具体引擎,使用 generic 类型');
185
80
  return { engine: 'generic', buildTool: 'generic' };
186
81
  }
@@ -47,12 +47,7 @@ export declare class GenericAdapter {
47
47
  private runNpmBuild;
48
48
  /**
49
49
  * 查找构建输出目录
50
- * 按优先级搜索:dist/、build/、out/,并兼容编辑器型引擎常见的
51
- * 多一层输出目录(例如 Cocos 的 build/web-mobile/)。
52
- *
53
- * 刻意**不包含 `public/`**:Vite / webpack 都约定它是「直接拷贝到产物根的静态
54
- * 资源目录」,放的是 favicon、robots.txt 这类原样文件,本身不是构建产物。
55
- * 一旦里面有 index.html,就会被误当成构建结果整目录复制出去。
50
+ * 按优先级搜索:dist/、build/、out/、public/
56
51
  */
57
52
  private findBuildOutputDir;
58
53
  /**
@@ -40,19 +40,11 @@ export class GenericAdapter {
40
40
  console.log('[GenericAdapter] 执行依赖安装...');
41
41
  await this.runNpmInstall(packageManager);
42
42
  }
43
- // 4. 检测构建命令。**必须先构建、再取产物**,不允许因为「找不到脚本」就
44
- // 复用工程里现成的目录:那会把残留的 `dist/` 或 Vite 的 `public/` 当成
45
- // 构建结果,用户拿到的是过期或完全不对的内容。
46
- //
47
- // 平台导入的工程都带 `builds.config.js`,由 playable-scripts 构建链负责
48
- // 产出产物,正常路径下这里必然能拿到 build 脚本;拿不到就是工程本身有问题,
49
- // 应当明确报错而不是静默降级。
43
+ // 4. 检测构建命令
50
44
  const buildScript = await this.detectBuildScript();
51
45
  if (!buildScript) {
52
- throw new Error(`[GenericAdapter] 未找到可用的构建命令,拒绝复用工程内现成的目录。\n` +
53
- `项目目录: ${this.projectDir}\n` +
54
- `请在 package.json 的 scripts 中声明 build(支持 build / build:prod / ` +
55
- `build:production / dist / compile 之一)。`);
46
+ throw new Error(`[GenericAdapter] 未找到 build script\n` +
47
+ `请确保 package.json 中包含 scripts.build 字段。`);
56
48
  }
57
49
  console.log(`[GenericAdapter] 检测到构建命令: ${buildScript}`);
58
50
  // 5. 执行构建
@@ -84,6 +76,9 @@ export class GenericAdapter {
84
76
  { file: 'pnpm-lock.yaml', manager: 'pnpm' },
85
77
  { file: 'yarn.lock', manager: 'yarn' },
86
78
  { file: 'package-lock.json', manager: 'npm' },
79
+ { file: 'npm-shrinkwrap.json', manager: 'npm' },
80
+ { file: 'bun.lock', manager: 'bun' },
81
+ { file: 'bun.lockb', manager: 'bun' },
87
82
  ];
88
83
  for (const { file, manager } of lockFiles) {
89
84
  try {
@@ -145,28 +140,25 @@ export class GenericAdapter {
145
140
  * 检测 build script
146
141
  */
147
142
  async detectBuildScript() {
148
- const pkgPath = path.join(this.projectDir, 'package.json');
149
- // package.json 读不到 / 解析失败属于「工程本身有问题」,直接抛出并说明原因。
150
- // 不能静默返回 null —— 那会让调用方以为「只是没配脚本」,掩盖真实故障。
151
- let pkg;
152
143
  try {
153
- pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
154
- }
155
- catch (error) {
156
- throw new Error(`[GenericAdapter] 无法读取或解析 package.json,构建中止。\n` +
157
- `路径: ${pkgPath}\n` +
158
- `原因: ${error instanceof Error ? error.message : String(error)}`);
144
+ const pkgPath = path.join(this.projectDir, 'package.json');
145
+ const content = await fs.readFile(pkgPath, 'utf-8');
146
+ const pkg = JSON.parse(content);
147
+ if (!pkg.scripts) {
148
+ return null;
149
+ }
150
+ // 优先查找常见的 build 命令
151
+ const buildScripts = ['build', 'build:prod', 'build:production', 'dist', 'compile'];
152
+ for (const script of buildScripts) {
153
+ if (pkg.scripts[script]) {
154
+ return script;
155
+ }
156
+ }
157
+ return null;
159
158
  }
160
- if (!pkg.scripts)
159
+ catch {
161
160
  return null;
162
- // 优先查找常见的 build 命令
163
- const buildScripts = ['build', 'build:prod', 'build:production', 'dist', 'compile'];
164
- for (const script of buildScripts) {
165
- if (pkg.scripts[script]) {
166
- return script;
167
- }
168
161
  }
169
- return null;
170
162
  }
171
163
  /**
172
164
  * 执行 npm run build
@@ -174,7 +166,7 @@ export class GenericAdapter {
174
166
  */
175
167
  async runNpmBuild(packageManager, script) {
176
168
  const cmd = packageManager === 'npm' ? 'npm' : packageManager;
177
- const args = packageManager === 'npm' ? ['run', script] : [script];
169
+ const args = packageManager === 'npm' || packageManager === 'bun' ? ['run', script] : [script];
178
170
  console.log(`[GenericAdapter] 运行: ${cmd} ${args.join(' ')}`);
179
171
  await this.runCommandWithTimeout(cmd, args, {
180
172
  cwd: this.projectDir,
@@ -191,27 +183,10 @@ export class GenericAdapter {
191
183
  }
192
184
  /**
193
185
  * 查找构建输出目录
194
- * 按优先级搜索:dist/、build/、out/,并兼容编辑器型引擎常见的
195
- * 多一层输出目录(例如 Cocos 的 build/web-mobile/)。
196
- *
197
- * 刻意**不包含 `public/`**:Vite / webpack 都约定它是「直接拷贝到产物根的静态
198
- * 资源目录」,放的是 favicon、robots.txt 这类原样文件,本身不是构建产物。
199
- * 一旦里面有 index.html,就会被误当成构建结果整目录复制出去。
186
+ * 按优先级搜索:dist/、build/、out/、public/
200
187
  */
201
188
  async findBuildOutputDir() {
202
- const possibleDirs = [
203
- 'dist',
204
- 'build',
205
- 'out',
206
- 'build/web-mobile',
207
- 'build/web-desktop',
208
- 'build/html5',
209
- 'dist/web-mobile',
210
- 'dist/html5',
211
- 'web-mobile',
212
- 'web-desktop',
213
- 'html5',
214
- ];
189
+ const possibleDirs = ['dist', 'build', 'out', 'public'];
215
190
  for (const dir of possibleDirs) {
216
191
  const fullPath = path.join(this.projectDir, dir);
217
192
  try {
@@ -229,38 +204,6 @@ export class GenericAdapter {
229
204
  // 目录不存在,继续检查下一个
230
205
  }
231
206
  }
232
- // 最多向下搜索三层,覆盖未知引擎的嵌套产物目录;跳过依赖和 VCS 目录,
233
- // 避免在大型源码树里把 node_modules 中的 index.html 当成构建结果。
234
- const roots = ['dist', 'build', 'out'];
235
- const ignored = new Set(['node_modules', '.git', 'library', 'temp', 'local']);
236
- const search = async (root, depth) => {
237
- if (depth > 3)
238
- return null;
239
- const fullRoot = path.join(this.projectDir, root);
240
- let entries;
241
- try {
242
- entries = await fs.readdir(fullRoot, { withFileTypes: true });
243
- }
244
- catch {
245
- return null;
246
- }
247
- if (entries.some((entry) => entry.isFile() && entry.name.toLowerCase() === 'index.html')) {
248
- return fullRoot;
249
- }
250
- for (const entry of entries) {
251
- if (!entry.isDirectory() || ignored.has(entry.name))
252
- continue;
253
- const found = await search(path.join(root, entry.name), depth + 1);
254
- if (found)
255
- return found;
256
- }
257
- return null;
258
- };
259
- for (const root of roots) {
260
- const nested = await search(root, 1);
261
- if (nested)
262
- return nested;
263
- }
264
207
  // 如果没找到,尝试从 package.json 读取
265
208
  try {
266
209
  const pkgPath = path.join(this.projectDir, 'package.json');
@@ -342,6 +285,12 @@ export class GenericAdapter {
342
285
  const entries = await fs.readdir(src, { withFileTypes: true });
343
286
  for (const entry of entries) {
344
287
  const srcPath = path.join(src, entry.name);
288
+ if (entry.name === '.playcraft.json')
289
+ continue;
290
+ if (path.basename(src) === '.playcraft' && ['skills', 'skill-artifacts', 'video-replication', 'workspace-context.json'].includes(entry.name))
291
+ continue;
292
+ if (entry.name === 'user_uploads' && path.basename(src) === '.tmp')
293
+ continue;
345
294
  const destPath = path.join(dest, entry.name);
346
295
  // 安全检查:确保目标路径在输出目录内
347
296
  const resolvedDest = path.resolve(destPath);
@@ -5,7 +5,7 @@ import type { BaseBuildOptions, BaseBuildOutput, PlayableScriptsConfig } from '.
5
5
  * 职责:作为 PlayCraft 和 @playcraft/devkit 之间的中间层
6
6
  * - @playcraft/devkit 是 PlayCraft 平台内置的构建工具(已集成到 @playcraft/cli)
7
7
  * - 用户项目只需依赖 @playcraft/adsdk(运行时 SDK),不需要安装构建工具
8
- * - 通过平台内置的 devkit 调用 playable-scripts builds 命令(单渠道与批量统一)
8
+ * - 通过平台内置的 devkit 调用 playable-scripts build/builds 命令
9
9
  * - 将 playcraft.config.json 的参数转换为 playable-scripts CLI 参数
10
10
  * - 收集构建产物并标记 skipChannelBuild
11
11
  *
@@ -27,7 +27,7 @@ export declare class PlayableScriptsAdapter {
27
27
  * 流程:
28
28
  * 1. 合并配置(playcraft.config.json → builds.config.js)
29
29
  * 2. 构建 CLI 参数
30
- * 3. 调用平台内置的 playable-scripts builds
30
+ * 3. 调用平台内置的 playable-scripts build/builds
31
31
  * 4. 收集构建产物
32
32
  * 5. 复制到输出目录
33
33
  * 6. 写入元数据(skipChannelBuild: true)
@@ -45,7 +45,7 @@ export declare class PlayableScriptsAdapter {
45
45
  * 将 playcraft.config.json 的参数注入到项目的 builds.config.js
46
46
  * - storeUrls → googlePlayUrl / appStoreUrl
47
47
  * - naming → naming
48
- * - themes.enabled → 未指定主题时显式禁用(避免 builds 意外产出全部主题变体)
48
+ * - themes.enabled → 批量构建时自动禁用主题
49
49
  * - build.outputDir → 强制与 PlayCraft 输出目录一致
50
50
  */
51
51
  private mergeConfig;
@@ -69,6 +69,12 @@ export declare class PlayableScriptsAdapter {
69
69
  * 递归查找源码文件
70
70
  */
71
71
  private findSourceFiles;
72
+ /**
73
+ * 准备构建环境
74
+ * - 执行包名迁移
75
+ * - 确保项目本地 devkit 和 loader 可用
76
+ */
77
+ private prepareBuildEnvironment;
72
78
  /**
73
79
  * 调用 playable-scripts 命令
74
80
  *
@@ -76,11 +82,14 @@ export declare class PlayableScriptsAdapter {
76
82
  * 1. 准备构建环境(包名迁移、安装本地 devkit 和 loader)
77
83
  * 2. 执行构建(使用项目本地 devkit)
78
84
  *
79
- * 单渠道与批量统一使用 `builds`:
80
- * - devkit 仅在 `builds` 上持续迭代并新增特性,`build` 已停止更新
81
- * - 单渠道通过 `--channels <channel>` 表达,无需再走 `build <channel>`
85
+ * 单渠道模式: <local-devkit> build <channel> [options]
86
+ * 批量模式: <local-devkit> builds [options]
82
87
  */
83
88
  private executePlayableScripts;
89
+ /**
90
+ * 从参数列表中移除指定参数及其值
91
+ */
92
+ private removeArg;
84
93
  /**
85
94
  * 统一构造 pnpm CLI 参数:
86
95
  * - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
@@ -111,7 +120,7 @@ export declare class PlayableScriptsAdapter {
111
120
  */
112
121
  private ensureBuildLoaders;
113
122
  /**
114
- * 确保 package.json 中有 builds 脚本
123
+ * 确保 package.json 中有 build/builds 脚本
115
124
  * devkit 必须通过 npm script 调用,不能直接执行 .js 文件
116
125
  */
117
126
  private ensureBuildScripts;
@@ -132,6 +141,10 @@ export declare class PlayableScriptsAdapter {
132
141
  * 注意:只收集目标渠道的产物,过滤掉其他渠道
133
142
  */
134
143
  private scanOutputDir;
144
+ /**
145
+ * 清理输出目录,删除所有旧产物
146
+ */
147
+ private cleanOutputDir;
135
148
  /**
136
149
  * 递归删除目录
137
150
  */
@@ -50,7 +50,7 @@ const ZIP_FALLBACK_CHANNELS = new Set(['remerge']);
50
50
  * 职责:作为 PlayCraft 和 @playcraft/devkit 之间的中间层
51
51
  * - @playcraft/devkit 是 PlayCraft 平台内置的构建工具(已集成到 @playcraft/cli)
52
52
  * - 用户项目只需依赖 @playcraft/adsdk(运行时 SDK),不需要安装构建工具
53
- * - 通过平台内置的 devkit 调用 playable-scripts builds 命令(单渠道与批量统一)
53
+ * - 通过平台内置的 devkit 调用 playable-scripts build/builds 命令
54
54
  * - 将 playcraft.config.json 的参数转换为 playable-scripts CLI 参数
55
55
  * - 收集构建产物并标记 skipChannelBuild
56
56
  *
@@ -108,7 +108,7 @@ export class PlayableScriptsAdapter {
108
108
  * 流程:
109
109
  * 1. 合并配置(playcraft.config.json → builds.config.js)
110
110
  * 2. 构建 CLI 参数
111
- * 3. 调用平台内置的 playable-scripts builds
111
+ * 3. 调用平台内置的 playable-scripts build/builds
112
112
  * 4. 收集构建产物
113
113
  * 5. 复制到输出目录
114
114
  * 6. 写入元数据(skipChannelBuild: true)
@@ -124,9 +124,20 @@ export class PlayableScriptsAdapter {
124
124
  const cliArgs = this.buildCLIArgs();
125
125
  // 3. 打印构建参数信息
126
126
  this.logBuildParams();
127
- // 4. 输出目录清理统一交给 devkit(builds 内部负责):
128
- // 预清理会与 devkit 自身的清理阶段竞争,在 Windows 上触发 EBUSY,因此这里不再处理。
129
- console.log('[PlayableScriptsAdapter] 输出目录清理交由 devkit 负责...');
127
+ // 4. 清理输出目录(在 devkit 构建前清理,避免旧产物干扰)
128
+ // 判断是否为批量构建模式(多个渠道或有主题)
129
+ const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
130
+ (this.config?.themes?.enabled);
131
+ if (isBatchMode) {
132
+ // 批量构建:不预清理,让 devkit 来清理,避免 EBUSY 错误
133
+ console.log('[PlayableScriptsAdapter] 批量构建模式:跳过预清理(由 devkit 负责)...');
134
+ }
135
+ else {
136
+ // 单渠道构建:清理项目目录下的 dist 目录(playable-scripts 实际输出目录)
137
+ const devkitOutputDir = path.join(this.projectDir, this.config?.outputDir ?? 'dist');
138
+ console.log('[PlayableScriptsAdapter] 单渠道构建模式:清理 devkit 输出目录...');
139
+ await this.cleanOutputDir(devkitOutputDir);
140
+ }
130
141
  // 5. 执行 playable-scripts 构建(使用平台内置的 devkit)
131
142
  console.log('[PlayableScriptsAdapter] 执行 playable-scripts 构建...');
132
143
  await this.executePlayableScripts(cliArgs);
@@ -183,10 +194,13 @@ export class PlayableScriptsAdapter {
183
194
  * 将 playcraft.config.json 的参数注入到项目的 builds.config.js
184
195
  * - storeUrls → googlePlayUrl / appStoreUrl
185
196
  * - naming → naming
186
- * - themes.enabled → 未指定主题时显式禁用(避免 builds 意外产出全部主题变体)
197
+ * - themes.enabled → 批量构建时自动禁用主题
187
198
  * - build.outputDir → 强制与 PlayCraft 输出目录一致
188
199
  */
189
200
  async mergeConfig() {
201
+ // 判断是否为批量构建模式
202
+ const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
203
+ (this.config?.themes?.enabled);
190
204
  // 读取或创建 builds.config.js
191
205
  const configPath = path.join(this.projectDir, 'builds.config.js');
192
206
  let configContent;
@@ -214,27 +228,29 @@ export class PlayableScriptsAdapter {
214
228
  if (this.config?.naming) {
215
229
  injections.push(` naming: ${JSON.stringify(this.config.naming)}`);
216
230
  }
217
- // 主题配置一律显式注入:统一走 builds 后,devkit 会读取项目 builds.config.js
218
- // 的 themes.enabled;若在未指定主题时不显式关闭,单渠道构建会意外产出全部主题变体。
219
- const availableThemes = await this.scanThemes();
220
- if (this.config?.themes?.whitelist && this.config.themes.whitelist.length > 0) {
221
- // 用户指定了主题白名单:启用主题切换
222
- const validThemes = this.config.themes.whitelist.filter(t => availableThemes.includes(t));
223
- if (validThemes.length > 0) {
224
- injections.push(` themes: { enabled: true, sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' }`);
225
- console.log(`[PlayableScriptsAdapter] 启用主题切换 (${validThemes.length} 个主题)`);
231
+ // 批量构建时处理主题配置
232
+ if (isBatchMode) {
233
+ // 扫描可用主题
234
+ const availableThemes = await this.scanThemes();
235
+ if (this.config?.themes?.whitelist && this.config.themes.whitelist.length > 0) {
236
+ // 用户指定了主题白名单:启用主题切换
237
+ const validThemes = this.config.themes.whitelist.filter(t => availableThemes.includes(t));
238
+ if (validThemes.length > 0) {
239
+ injections.push(` themes: { enabled: true, sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' }`);
240
+ console.log(`[PlayableScriptsAdapter] 批量构建模式:启用主题切换 (${validThemes.length} 个主题)`);
241
+ }
242
+ else {
243
+ // 指定的主题不存在,禁用主题切换
244
+ injections.push(` themes: { enabled: false }`);
245
+ console.log('[PlayableScriptsAdapter] 批量构建模式:指定主题未找到,使用默认主题');
246
+ }
226
247
  }
227
248
  else {
228
- // 指定的主题不存在,禁用主题切换
249
+ // 用户未指定主题:禁用主题切换,使用当前默认主题
229
250
  injections.push(` themes: { enabled: false }`);
230
- console.log('[PlayableScriptsAdapter] 指定主题未找到,使用默认主题');
251
+ console.log('[PlayableScriptsAdapter] 批量构建模式:使用当前默认主题');
231
252
  }
232
253
  }
233
- else {
234
- // 用户未指定主题:禁用主题切换,使用当前默认主题
235
- injections.push(` themes: { enabled: false }`);
236
- console.log('[PlayableScriptsAdapter] 使用当前默认主题');
237
- }
238
254
  // 始终注入配置,确保 build.outputDir 被覆盖
239
255
  // 在 module.exports 的对象末尾注入配置
240
256
  // 策略:创建一个 wrapper 文件来合并配置
@@ -416,6 +432,24 @@ export class PlayableScriptsAdapter {
416
432
  }
417
433
  return files;
418
434
  }
435
+ /**
436
+ * 准备构建环境
437
+ * - 执行包名迁移
438
+ * - 确保项目本地 devkit 和 loader 可用
439
+ */
440
+ async prepareBuildEnvironment() {
441
+ // 1. 执行包名迁移
442
+ await this.migratePackageNames();
443
+ // 2. 确保项目本地存在 devkit 和构建依赖
444
+ const localDevkit = await this.ensureLocalDevkit();
445
+ if (!localDevkit) {
446
+ throw new Error('[PlayableScriptsAdapter] 无法安装项目本地的 @playcraft/devkit。\n' +
447
+ '请检查网络连接或手动运行: npm install --save-dev @playcraft/devkit');
448
+ }
449
+ console.log('[PlayableScriptsAdapter] ✅ 使用项目本地 devkit');
450
+ // 3. 确保构建 loader 存在
451
+ await this.ensureBuildLoaders();
452
+ }
419
453
  /**
420
454
  * 调用 playable-scripts 命令
421
455
  *
@@ -423,9 +457,8 @@ export class PlayableScriptsAdapter {
423
457
  * 1. 准备构建环境(包名迁移、安装本地 devkit 和 loader)
424
458
  * 2. 执行构建(使用项目本地 devkit)
425
459
  *
426
- * 单渠道与批量统一使用 `builds`:
427
- * - devkit 仅在 `builds` 上持续迭代并新增特性,`build` 已停止更新
428
- * - 单渠道通过 `--channels <channel>` 表达,无需再走 `build <channel>`
460
+ * 单渠道模式: <local-devkit> build <channel> [options]
461
+ * 批量模式: <local-devkit> builds [options]
429
462
  */
430
463
  async executePlayableScripts(cliArgs) {
431
464
  // 1. 执行包名迁移
@@ -440,35 +473,61 @@ export class PlayableScriptsAdapter {
440
473
  // 3. 确保构建 loader 存在
441
474
  await this.ensureBuildLoaders();
442
475
  const config = this.config ?? {};
443
- // 单渠道与批量统一使用 builds 命令:
444
- // devkit 仅在 builds 上持续迭代并新增特性,build 已停止更新。
445
- const args = ['builds', ...cliArgs];
446
- // 扫描可用主题,命中白名单时追加 -t(devkit 的主题过滤参数)
447
- const availableThemes = await this.scanThemes();
448
- let selectedThemes = [];
449
- if (config.themes?.whitelist && config.themes.whitelist.length > 0) {
450
- // 使用配置中指定的主题白名单,清理可能的引号
451
- const cleanWhitelist = config.themes.whitelist.map(t => t.replace(/^["']|["']$/g, ''));
452
- selectedThemes = cleanWhitelist.filter(t => availableThemes.includes(t));
453
- if (selectedThemes.length === 0) {
454
- console.log(`[PlayableScriptsAdapter] ⚠️ 配置的主题未找到,使用当前默认主题`);
455
- }
456
- }
457
- if (selectedThemes.length > 0) {
458
- args.push('-t', selectedThemes.join(','));
459
- console.log(`[PlayableScriptsAdapter] 构建主题: ${selectedThemes.join(', ')}`);
476
+ const isBatchMode = (config.channels && config.channels.length > 1) ||
477
+ (config.themes?.enabled);
478
+ // 构建命令参数
479
+ let args;
480
+ let modeLabel;
481
+ if (isBatchMode) {
482
+ // 批量模式: <devkit> builds [options]
483
+ args = ['builds', ...cliArgs];
484
+ // 扫描可用主题
485
+ const availableThemes = await this.scanThemes();
486
+ // 确定要构建的主题列表
487
+ let selectedThemes = [];
488
+ if (config.themes?.whitelist && config.themes.whitelist.length > 0) {
489
+ // 使用配置中指定的主题白名单,清理可能的引号
490
+ const cleanWhitelist = config.themes.whitelist.map(t => t.replace(/^["']|["']$/g, ''));
491
+ selectedThemes = cleanWhitelist.filter(t => availableThemes.includes(t));
492
+ if (selectedThemes.length === 0) {
493
+ console.log(`[PlayableScriptsAdapter] ⚠️ 配置的主题未找到,使用当前默认主题`);
494
+ selectedThemes = [];
495
+ }
496
+ }
497
+ // 如果有指定主题,传入 -t 参数
498
+ if (selectedThemes.length > 0) {
499
+ args.push('-t', selectedThemes.join(','));
500
+ console.log(`[PlayableScriptsAdapter] 构建主题: ${selectedThemes.join(', ')}`);
501
+ }
502
+ else {
503
+ // 未指定主题:使用 devkit 默认行为(当前 index.ts 指向的主题)
504
+ console.log(`[PlayableScriptsAdapter] 使用当前默认主题 (src/theme/index.ts)`);
505
+ }
506
+ modeLabel = '批量构建模式';
460
507
  }
461
508
  else {
462
- // 未指定主题:使用 devkit 默认行为(当前 index.ts 指向的主题)
463
- console.log(`[PlayableScriptsAdapter] 使用当前默认主题 (src/theme/index.ts)`);
464
- }
509
+ // 单渠道模式: <devkit> build <channel> [options]
510
+ const channel = config.channels?.[0]
511
+ ? mapChannel(config.channels[0])
512
+ : 'google';
513
+ const filteredArgs = this.removeArg(cliArgs, '--channels');
514
+ args = ['build', channel, ...filteredArgs];
515
+ modeLabel = `单渠道构建模式: ${channel}`;
516
+ }
517
+ console.log(`[PlayableScriptsAdapter] ${modeLabel}`);
465
518
  // devkit 必须通过 npm script 调用,不能直接执行 .js 文件
519
+ // 1. 确保 package.json 中有 build/builds 脚本
466
520
  await this.ensureBuildScripts();
467
- const scriptName = 'builds';
468
- const runArgs = args.slice(1);
521
+ // 2. 通过 pnpm run 执行
522
+ const scriptName = isBatchMode ? 'builds' : 'build';
523
+ // 单渠道: args = ['build', channel, ...options],需要传递 [channel, ...options]
524
+ // 批量: args = ['builds', ...options],需要传递 [...options]
525
+ const runArgs = isBatchMode
526
+ ? args.slice(1) // builds 命令,去掉第一个 'builds'
527
+ : args.slice(1); // build 命令,去掉第一个 'build',保留 channel 和所有参数
469
528
  console.log(`[PlayableScriptsAdapter] 运行: pnpm run ${scriptName} ${runArgs.length > 0 ? runArgs.join(' ') : ''}`);
470
529
  try {
471
- // 注意: package.json 的 scripts 已经是 "playable-scripts builds"
530
+ // 注意: package.json 的 scripts 已经是 "playable-scripts build/builds"
472
531
  // 所以不需要 '--' 分隔符,直接传参数即可
473
532
  const pnpmArgs = runArgs.length > 0
474
533
  ? ['run', scriptName, ...runArgs]
@@ -507,6 +566,21 @@ export class PlayableScriptsAdapter {
507
566
  console.error(`[PlayableScriptsAdapter] 请检查 devkit 构建日志,确认构建是否成功`);
508
567
  }
509
568
  }
569
+ /**
570
+ * 从参数列表中移除指定参数及其值
571
+ */
572
+ removeArg(args, argName) {
573
+ const result = [];
574
+ for (let i = 0; i < args.length; i++) {
575
+ if (args[i] === argName) {
576
+ i++; // 跳过参数值
577
+ }
578
+ else {
579
+ result.push(args[i]);
580
+ }
581
+ }
582
+ return result;
583
+ }
510
584
  /**
511
585
  * 统一构造 pnpm CLI 参数:
512
586
  * - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
@@ -897,7 +971,7 @@ export class PlayableScriptsAdapter {
897
971
  }
898
972
  }
899
973
  /**
900
- * 确保 package.json 中有 builds 脚本
974
+ * 确保 package.json 中有 build/builds 脚本
901
975
  * devkit 必须通过 npm script 调用,不能直接执行 .js 文件
902
976
  */
903
977
  async ensureBuildScripts() {
@@ -908,10 +982,21 @@ export class PlayableScriptsAdapter {
908
982
  if (!pkg.scripts) {
909
983
  pkg.scripts = {};
910
984
  }
985
+ let modified = false;
986
+ // 确保有 build 脚本
987
+ if (!pkg.scripts.build) {
988
+ pkg.scripts.build = 'playable-scripts build';
989
+ modified = true;
990
+ console.log('[PlayableScriptsAdapter] 注入 package.json script: build');
991
+ }
992
+ // 确保有 builds 脚本
911
993
  if (!pkg.scripts.builds) {
912
994
  pkg.scripts.builds = 'playable-scripts builds';
913
- await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
995
+ modified = true;
914
996
  console.log('[PlayableScriptsAdapter] 注入 package.json script: builds');
997
+ }
998
+ if (modified) {
999
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
915
1000
  console.log('[PlayableScriptsAdapter] ✓ package.json 已更新');
916
1001
  }
917
1002
  }
@@ -1040,6 +1125,29 @@ export class PlayableScriptsAdapter {
1040
1125
  }
1041
1126
  }
1042
1127
  // ==================== 6. 复制到输出目录 ====================
1128
+ /**
1129
+ * 清理输出目录,删除所有旧产物
1130
+ */
1131
+ async cleanOutputDir(dir) {
1132
+ try {
1133
+ await fs.access(dir);
1134
+ // 目录存在,递归删除所有内容
1135
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1136
+ for (const entry of entries) {
1137
+ const fullPath = path.join(dir, entry.name);
1138
+ if (entry.isDirectory()) {
1139
+ await this.removeDirRecursive(fullPath);
1140
+ }
1141
+ else {
1142
+ await fs.unlink(fullPath);
1143
+ }
1144
+ }
1145
+ console.log(`[PlayableScriptsAdapter] 已清理输出目录: ${dir}`);
1146
+ }
1147
+ catch {
1148
+ // 目录不存在,无需清理
1149
+ }
1150
+ }
1043
1151
  /**
1044
1152
  * 递归删除目录
1045
1153
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.1.36-beta1",
3
+ "version": "0.1.38-beta",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",