@playcraft/build 0.1.42 → 0.1.43-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 +1 -1
- package/dist/engines/engine-detector.js +14 -119
- package/dist/engines/generic-adapter.d.ts +1 -6
- package/dist/engines/generic-adapter.js +30 -81
- package/dist/engines/playable-scripts-adapter.d.ts +20 -7
- package/dist/engines/playable-scripts-adapter.js +158 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
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]
|
|
53
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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 →
|
|
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
|
-
*
|
|
80
|
-
* -
|
|
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.
|
|
128
|
-
//
|
|
129
|
-
|
|
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 →
|
|
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
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
*
|
|
427
|
-
* -
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
//
|
|
463
|
-
|
|
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
|
-
|
|
468
|
-
const
|
|
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
|
-
|
|
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
|
*/
|