@playcraft/build 0.1.40-beta → 0.1.42
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 +119 -14
- package/dist/engines/generic-adapter.d.ts +6 -1
- package/dist/engines/generic-adapter.js +81 -30
- package/dist/engines/playable-scripts-adapter.d.ts +7 -20
- package/dist/engines/playable-scripts-adapter.js +50 -158
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,17 +3,26 @@ 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)最低
|
|
6
13
|
*/
|
|
7
14
|
const ENGINE_DETECTION_MAP = [
|
|
8
|
-
//
|
|
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 框架 / 渲染库
|
|
9
24
|
{ match: (n) => n === 'phaser', engine: 'phaser', priority: 100 },
|
|
10
25
|
{ 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 },
|
|
17
26
|
];
|
|
18
27
|
/**
|
|
19
28
|
* 构建工具检测映射表 - 优先级高于引擎检测
|
|
@@ -40,6 +49,78 @@ const BUILD_TOOL_DETECTION_MAP = [
|
|
|
40
49
|
priority: 200,
|
|
41
50
|
},
|
|
42
51
|
];
|
|
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
|
+
}
|
|
43
124
|
/**
|
|
44
125
|
* 引擎检测器
|
|
45
126
|
* 根据项目文件结构自动检测引擎类型和构建工具
|
|
@@ -58,24 +139,48 @@ export class EngineDetector {
|
|
|
58
139
|
}
|
|
59
140
|
// 2. 检测 package.json
|
|
60
141
|
const pkg = await this.readPackageJson(projectDir);
|
|
142
|
+
const fileEngine = await detectEngineFromProjectFiles(projectDir);
|
|
61
143
|
if (!pkg) {
|
|
144
|
+
if (fileEngine) {
|
|
145
|
+
console.log(`[EngineDetector] 检测到引擎: ${fileEngine}(通过文件特征)`);
|
|
146
|
+
return { engine: fileEngine, buildTool: 'generic' };
|
|
147
|
+
}
|
|
62
148
|
throw new Error(`[EngineDetector] 无法识别项目类型:找不到 package.json 或 PlayCanvas 文件结构\n` +
|
|
63
149
|
`项目目录: ${projectDir}\n` +
|
|
64
150
|
`请确保这是一个有效的项目目录。`);
|
|
65
151
|
}
|
|
66
|
-
// 3.
|
|
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 链路。
|
|
67
162
|
const buildToolResult = this.detectBuildToolFromPackageJson(pkg);
|
|
68
163
|
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
|
+
}
|
|
69
170
|
console.log(`[EngineDetector] 检测到构建工具: ${buildToolResult.buildTool}(隐含引擎: ${buildToolResult.engine})`);
|
|
70
171
|
return buildToolResult;
|
|
71
172
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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' };
|
|
77
182
|
}
|
|
78
|
-
//
|
|
183
|
+
// 7. 兜底
|
|
79
184
|
console.log('[EngineDetector] 无法识别具体引擎,使用 generic 类型');
|
|
80
185
|
return { engine: 'generic', buildTool: 'generic' };
|
|
81
186
|
}
|
|
@@ -47,7 +47,12 @@ export declare class GenericAdapter {
|
|
|
47
47
|
private runNpmBuild;
|
|
48
48
|
/**
|
|
49
49
|
* 查找构建输出目录
|
|
50
|
-
* 按优先级搜索:dist/、build/、out
|
|
50
|
+
* 按优先级搜索:dist/、build/、out/,并兼容编辑器型引擎常见的
|
|
51
|
+
* 多一层输出目录(例如 Cocos 的 build/web-mobile/)。
|
|
52
|
+
*
|
|
53
|
+
* 刻意**不包含 `public/`**:Vite / webpack 都约定它是「直接拷贝到产物根的静态
|
|
54
|
+
* 资源目录」,放的是 favicon、robots.txt 这类原样文件,本身不是构建产物。
|
|
55
|
+
* 一旦里面有 index.html,就会被误当成构建结果整目录复制出去。
|
|
51
56
|
*/
|
|
52
57
|
private findBuildOutputDir;
|
|
53
58
|
/**
|
|
@@ -40,11 +40,19 @@ export class GenericAdapter {
|
|
|
40
40
|
console.log('[GenericAdapter] 执行依赖安装...');
|
|
41
41
|
await this.runNpmInstall(packageManager);
|
|
42
42
|
}
|
|
43
|
-
// 4.
|
|
43
|
+
// 4. 检测构建命令。**必须先构建、再取产物**,不允许因为「找不到脚本」就
|
|
44
|
+
// 复用工程里现成的目录:那会把残留的 `dist/` 或 Vite 的 `public/` 当成
|
|
45
|
+
// 构建结果,用户拿到的是过期或完全不对的内容。
|
|
46
|
+
//
|
|
47
|
+
// 平台导入的工程都带 `builds.config.js`,由 playable-scripts 构建链负责
|
|
48
|
+
// 产出产物,正常路径下这里必然能拿到 build 脚本;拿不到就是工程本身有问题,
|
|
49
|
+
// 应当明确报错而不是静默降级。
|
|
44
50
|
const buildScript = await this.detectBuildScript();
|
|
45
51
|
if (!buildScript) {
|
|
46
|
-
throw new Error(`[GenericAdapter]
|
|
47
|
-
|
|
52
|
+
throw new Error(`[GenericAdapter] 未找到可用的构建命令,拒绝复用工程内现成的目录。\n` +
|
|
53
|
+
`项目目录: ${this.projectDir}\n` +
|
|
54
|
+
`请在 package.json 的 scripts 中声明 build(支持 build / build:prod / ` +
|
|
55
|
+
`build:production / dist / compile 之一)。`);
|
|
48
56
|
}
|
|
49
57
|
console.log(`[GenericAdapter] 检测到构建命令: ${buildScript}`);
|
|
50
58
|
// 5. 执行构建
|
|
@@ -76,9 +84,6 @@ export class GenericAdapter {
|
|
|
76
84
|
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
77
85
|
{ file: 'yarn.lock', manager: 'yarn' },
|
|
78
86
|
{ 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' },
|
|
82
87
|
];
|
|
83
88
|
for (const { file, manager } of lockFiles) {
|
|
84
89
|
try {
|
|
@@ -140,25 +145,28 @@ export class GenericAdapter {
|
|
|
140
145
|
* 检测 build script
|
|
141
146
|
*/
|
|
142
147
|
async detectBuildScript() {
|
|
148
|
+
const pkgPath = path.join(this.projectDir, 'package.json');
|
|
149
|
+
// package.json 读不到 / 解析失败属于「工程本身有问题」,直接抛出并说明原因。
|
|
150
|
+
// 不能静默返回 null —— 那会让调用方以为「只是没配脚本」,掩盖真实故障。
|
|
151
|
+
let pkg;
|
|
143
152
|
try {
|
|
144
|
-
|
|
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;
|
|
153
|
+
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
158
154
|
}
|
|
159
|
-
catch {
|
|
155
|
+
catch (error) {
|
|
156
|
+
throw new Error(`[GenericAdapter] 无法读取或解析 package.json,构建中止。\n` +
|
|
157
|
+
`路径: ${pkgPath}\n` +
|
|
158
|
+
`原因: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
}
|
|
160
|
+
if (!pkg.scripts)
|
|
160
161
|
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
|
+
}
|
|
161
168
|
}
|
|
169
|
+
return null;
|
|
162
170
|
}
|
|
163
171
|
/**
|
|
164
172
|
* 执行 npm run build
|
|
@@ -166,7 +174,7 @@ export class GenericAdapter {
|
|
|
166
174
|
*/
|
|
167
175
|
async runNpmBuild(packageManager, script) {
|
|
168
176
|
const cmd = packageManager === 'npm' ? 'npm' : packageManager;
|
|
169
|
-
const args = packageManager === 'npm'
|
|
177
|
+
const args = packageManager === 'npm' ? ['run', script] : [script];
|
|
170
178
|
console.log(`[GenericAdapter] 运行: ${cmd} ${args.join(' ')}`);
|
|
171
179
|
await this.runCommandWithTimeout(cmd, args, {
|
|
172
180
|
cwd: this.projectDir,
|
|
@@ -183,10 +191,27 @@ export class GenericAdapter {
|
|
|
183
191
|
}
|
|
184
192
|
/**
|
|
185
193
|
* 查找构建输出目录
|
|
186
|
-
* 按优先级搜索:dist/、build/、out
|
|
194
|
+
* 按优先级搜索:dist/、build/、out/,并兼容编辑器型引擎常见的
|
|
195
|
+
* 多一层输出目录(例如 Cocos 的 build/web-mobile/)。
|
|
196
|
+
*
|
|
197
|
+
* 刻意**不包含 `public/`**:Vite / webpack 都约定它是「直接拷贝到产物根的静态
|
|
198
|
+
* 资源目录」,放的是 favicon、robots.txt 这类原样文件,本身不是构建产物。
|
|
199
|
+
* 一旦里面有 index.html,就会被误当成构建结果整目录复制出去。
|
|
187
200
|
*/
|
|
188
201
|
async findBuildOutputDir() {
|
|
189
|
-
const possibleDirs = [
|
|
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
|
+
];
|
|
190
215
|
for (const dir of possibleDirs) {
|
|
191
216
|
const fullPath = path.join(this.projectDir, dir);
|
|
192
217
|
try {
|
|
@@ -204,6 +229,38 @@ export class GenericAdapter {
|
|
|
204
229
|
// 目录不存在,继续检查下一个
|
|
205
230
|
}
|
|
206
231
|
}
|
|
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
|
+
}
|
|
207
264
|
// 如果没找到,尝试从 package.json 读取
|
|
208
265
|
try {
|
|
209
266
|
const pkgPath = path.join(this.projectDir, 'package.json');
|
|
@@ -285,12 +342,6 @@ export class GenericAdapter {
|
|
|
285
342
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
286
343
|
for (const entry of entries) {
|
|
287
344
|
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;
|
|
294
345
|
const destPath = path.join(dest, entry.name);
|
|
295
346
|
// 安全检查:确保目标路径在输出目录内
|
|
296
347
|
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
|
|
8
|
+
* - 通过平台内置的 devkit 调用 playable-scripts 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
|
|
30
|
+
* 3. 调用平台内置的 playable-scripts 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 → 未指定主题时显式禁用(避免 builds 意外产出全部主题变体)
|
|
49
49
|
* - build.outputDir → 强制与 PlayCraft 输出目录一致
|
|
50
50
|
*/
|
|
51
51
|
private mergeConfig;
|
|
@@ -69,12 +69,6 @@ export declare class PlayableScriptsAdapter {
|
|
|
69
69
|
* 递归查找源码文件
|
|
70
70
|
*/
|
|
71
71
|
private findSourceFiles;
|
|
72
|
-
/**
|
|
73
|
-
* 准备构建环境
|
|
74
|
-
* - 执行包名迁移
|
|
75
|
-
* - 确保项目本地 devkit 和 loader 可用
|
|
76
|
-
*/
|
|
77
|
-
private prepareBuildEnvironment;
|
|
78
72
|
/**
|
|
79
73
|
* 调用 playable-scripts 命令
|
|
80
74
|
*
|
|
@@ -82,14 +76,11 @@ export declare class PlayableScriptsAdapter {
|
|
|
82
76
|
* 1. 准备构建环境(包名迁移、安装本地 devkit 和 loader)
|
|
83
77
|
* 2. 执行构建(使用项目本地 devkit)
|
|
84
78
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
79
|
+
* 单渠道与批量统一使用 `builds`:
|
|
80
|
+
* - devkit 仅在 `builds` 上持续迭代并新增特性,`build` 已停止更新
|
|
81
|
+
* - 单渠道通过 `--channels <channel>` 表达,无需再走 `build <channel>`
|
|
87
82
|
*/
|
|
88
83
|
private executePlayableScripts;
|
|
89
|
-
/**
|
|
90
|
-
* 从参数列表中移除指定参数及其值
|
|
91
|
-
*/
|
|
92
|
-
private removeArg;
|
|
93
84
|
/**
|
|
94
85
|
* 统一构造 pnpm CLI 参数:
|
|
95
86
|
* - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
|
|
@@ -120,7 +111,7 @@ export declare class PlayableScriptsAdapter {
|
|
|
120
111
|
*/
|
|
121
112
|
private ensureBuildLoaders;
|
|
122
113
|
/**
|
|
123
|
-
* 确保 package.json 中有
|
|
114
|
+
* 确保 package.json 中有 builds 脚本
|
|
124
115
|
* devkit 必须通过 npm script 调用,不能直接执行 .js 文件
|
|
125
116
|
*/
|
|
126
117
|
private ensureBuildScripts;
|
|
@@ -141,10 +132,6 @@ export declare class PlayableScriptsAdapter {
|
|
|
141
132
|
* 注意:只收集目标渠道的产物,过滤掉其他渠道
|
|
142
133
|
*/
|
|
143
134
|
private scanOutputDir;
|
|
144
|
-
/**
|
|
145
|
-
* 清理输出目录,删除所有旧产物
|
|
146
|
-
*/
|
|
147
|
-
private cleanOutputDir;
|
|
148
135
|
/**
|
|
149
136
|
* 递归删除目录
|
|
150
137
|
*/
|
|
@@ -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
|
|
53
|
+
* - 通过平台内置的 devkit 调用 playable-scripts 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
|
|
111
|
+
* 3. 调用平台内置的 playable-scripts builds
|
|
112
112
|
* 4. 收集构建产物
|
|
113
113
|
* 5. 复制到输出目录
|
|
114
114
|
* 6. 写入元数据(skipChannelBuild: true)
|
|
@@ -124,20 +124,9 @@ export class PlayableScriptsAdapter {
|
|
|
124
124
|
const cliArgs = this.buildCLIArgs();
|
|
125
125
|
// 3. 打印构建参数信息
|
|
126
126
|
this.logBuildParams();
|
|
127
|
-
// 4.
|
|
128
|
-
//
|
|
129
|
-
|
|
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
|
-
}
|
|
127
|
+
// 4. 输出目录清理统一交给 devkit(builds 内部负责):
|
|
128
|
+
// 预清理会与 devkit 自身的清理阶段竞争,在 Windows 上触发 EBUSY,因此这里不再处理。
|
|
129
|
+
console.log('[PlayableScriptsAdapter] 输出目录清理交由 devkit 负责...');
|
|
141
130
|
// 5. 执行 playable-scripts 构建(使用平台内置的 devkit)
|
|
142
131
|
console.log('[PlayableScriptsAdapter] 执行 playable-scripts 构建...');
|
|
143
132
|
await this.executePlayableScripts(cliArgs);
|
|
@@ -194,13 +183,10 @@ export class PlayableScriptsAdapter {
|
|
|
194
183
|
* 将 playcraft.config.json 的参数注入到项目的 builds.config.js
|
|
195
184
|
* - storeUrls → googlePlayUrl / appStoreUrl
|
|
196
185
|
* - naming → naming
|
|
197
|
-
* - themes.enabled →
|
|
186
|
+
* - themes.enabled → 未指定主题时显式禁用(避免 builds 意外产出全部主题变体)
|
|
198
187
|
* - build.outputDir → 强制与 PlayCraft 输出目录一致
|
|
199
188
|
*/
|
|
200
189
|
async mergeConfig() {
|
|
201
|
-
// 判断是否为批量构建模式
|
|
202
|
-
const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
|
|
203
|
-
(this.config?.themes?.enabled);
|
|
204
190
|
// 读取或创建 builds.config.js
|
|
205
191
|
const configPath = path.join(this.projectDir, 'builds.config.js');
|
|
206
192
|
let configContent;
|
|
@@ -228,29 +214,27 @@ export class PlayableScriptsAdapter {
|
|
|
228
214
|
if (this.config?.naming) {
|
|
229
215
|
injections.push(` naming: ${JSON.stringify(this.config.naming)}`);
|
|
230
216
|
}
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
console.log(`[PlayableScriptsAdapter] 批量构建模式:启用主题切换 (${validThemes.length} 个主题)`);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
// 指定的主题不存在,禁用主题切换
|
|
244
|
-
injections.push(` themes: { enabled: false }`);
|
|
245
|
-
console.log('[PlayableScriptsAdapter] 批量构建模式:指定主题未找到,使用默认主题');
|
|
246
|
-
}
|
|
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} 个主题)`);
|
|
247
226
|
}
|
|
248
227
|
else {
|
|
249
|
-
//
|
|
228
|
+
// 指定的主题不存在,禁用主题切换
|
|
250
229
|
injections.push(` themes: { enabled: false }`);
|
|
251
|
-
console.log('[PlayableScriptsAdapter]
|
|
230
|
+
console.log('[PlayableScriptsAdapter] 指定主题未找到,使用默认主题');
|
|
252
231
|
}
|
|
253
232
|
}
|
|
233
|
+
else {
|
|
234
|
+
// 用户未指定主题:禁用主题切换,使用当前默认主题
|
|
235
|
+
injections.push(` themes: { enabled: false }`);
|
|
236
|
+
console.log('[PlayableScriptsAdapter] 使用当前默认主题');
|
|
237
|
+
}
|
|
254
238
|
// 始终注入配置,确保 build.outputDir 被覆盖
|
|
255
239
|
// 在 module.exports 的对象末尾注入配置
|
|
256
240
|
// 策略:创建一个 wrapper 文件来合并配置
|
|
@@ -432,24 +416,6 @@ export class PlayableScriptsAdapter {
|
|
|
432
416
|
}
|
|
433
417
|
return files;
|
|
434
418
|
}
|
|
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
|
-
}
|
|
453
419
|
/**
|
|
454
420
|
* 调用 playable-scripts 命令
|
|
455
421
|
*
|
|
@@ -457,8 +423,9 @@ export class PlayableScriptsAdapter {
|
|
|
457
423
|
* 1. 准备构建环境(包名迁移、安装本地 devkit 和 loader)
|
|
458
424
|
* 2. 执行构建(使用项目本地 devkit)
|
|
459
425
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
426
|
+
* 单渠道与批量统一使用 `builds`:
|
|
427
|
+
* - devkit 仅在 `builds` 上持续迭代并新增特性,`build` 已停止更新
|
|
428
|
+
* - 单渠道通过 `--channels <channel>` 表达,无需再走 `build <channel>`
|
|
462
429
|
*/
|
|
463
430
|
async executePlayableScripts(cliArgs) {
|
|
464
431
|
// 1. 执行包名迁移
|
|
@@ -473,61 +440,35 @@ export class PlayableScriptsAdapter {
|
|
|
473
440
|
// 3. 确保构建 loader 存在
|
|
474
441
|
await this.ensureBuildLoaders();
|
|
475
442
|
const config = this.config ?? {};
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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 = '批量构建模式';
|
|
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(', ')}`);
|
|
507
460
|
}
|
|
508
461
|
else {
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
: 'google';
|
|
513
|
-
const filteredArgs = this.removeArg(cliArgs, '--channels');
|
|
514
|
-
args = ['build', channel, ...filteredArgs];
|
|
515
|
-
modeLabel = `单渠道构建模式: ${channel}`;
|
|
516
|
-
}
|
|
517
|
-
console.log(`[PlayableScriptsAdapter] ${modeLabel}`);
|
|
462
|
+
// 未指定主题:使用 devkit 默认行为(当前 index.ts 指向的主题)
|
|
463
|
+
console.log(`[PlayableScriptsAdapter] 使用当前默认主题 (src/theme/index.ts)`);
|
|
464
|
+
}
|
|
518
465
|
// devkit 必须通过 npm script 调用,不能直接执行 .js 文件
|
|
519
|
-
// 1. 确保 package.json 中有 build/builds 脚本
|
|
520
466
|
await this.ensureBuildScripts();
|
|
521
|
-
|
|
522
|
-
const
|
|
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 和所有参数
|
|
467
|
+
const scriptName = 'builds';
|
|
468
|
+
const runArgs = args.slice(1);
|
|
528
469
|
console.log(`[PlayableScriptsAdapter] 运行: pnpm run ${scriptName} ${runArgs.length > 0 ? runArgs.join(' ') : ''}`);
|
|
529
470
|
try {
|
|
530
|
-
// 注意: package.json 的 scripts 已经是 "playable-scripts
|
|
471
|
+
// 注意: package.json 的 scripts 已经是 "playable-scripts builds"
|
|
531
472
|
// 所以不需要 '--' 分隔符,直接传参数即可
|
|
532
473
|
const pnpmArgs = runArgs.length > 0
|
|
533
474
|
? ['run', scriptName, ...runArgs]
|
|
@@ -566,21 +507,6 @@ export class PlayableScriptsAdapter {
|
|
|
566
507
|
console.error(`[PlayableScriptsAdapter] 请检查 devkit 构建日志,确认构建是否成功`);
|
|
567
508
|
}
|
|
568
509
|
}
|
|
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
|
-
}
|
|
584
510
|
/**
|
|
585
511
|
* 统一构造 pnpm CLI 参数:
|
|
586
512
|
* - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
|
|
@@ -971,7 +897,7 @@ export class PlayableScriptsAdapter {
|
|
|
971
897
|
}
|
|
972
898
|
}
|
|
973
899
|
/**
|
|
974
|
-
* 确保 package.json 中有
|
|
900
|
+
* 确保 package.json 中有 builds 脚本
|
|
975
901
|
* devkit 必须通过 npm script 调用,不能直接执行 .js 文件
|
|
976
902
|
*/
|
|
977
903
|
async ensureBuildScripts() {
|
|
@@ -982,21 +908,10 @@ export class PlayableScriptsAdapter {
|
|
|
982
908
|
if (!pkg.scripts) {
|
|
983
909
|
pkg.scripts = {};
|
|
984
910
|
}
|
|
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 脚本
|
|
993
911
|
if (!pkg.scripts.builds) {
|
|
994
912
|
pkg.scripts.builds = 'playable-scripts builds';
|
|
995
|
-
modified = true;
|
|
996
|
-
console.log('[PlayableScriptsAdapter] 注入 package.json script: builds');
|
|
997
|
-
}
|
|
998
|
-
if (modified) {
|
|
999
913
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
|
914
|
+
console.log('[PlayableScriptsAdapter] 注入 package.json script: builds');
|
|
1000
915
|
console.log('[PlayableScriptsAdapter] ✓ package.json 已更新');
|
|
1001
916
|
}
|
|
1002
917
|
}
|
|
@@ -1125,29 +1040,6 @@ export class PlayableScriptsAdapter {
|
|
|
1125
1040
|
}
|
|
1126
1041
|
}
|
|
1127
1042
|
// ==================== 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
|
-
}
|
|
1151
1043
|
/**
|
|
1152
1044
|
* 递归删除目录
|
|
1153
1045
|
*/
|