@playcraft/build 0.0.17 → 0.0.21

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.
Files changed (46) hide show
  1. package/dist/analyzers/scene-asset-collector.js +259 -135
  2. package/dist/audio-optimizer.d.ts +70 -0
  3. package/dist/audio-optimizer.js +226 -0
  4. package/dist/base-builder.d.ts +25 -13
  5. package/dist/base-builder.js +69 -29
  6. package/dist/engines/engine-detector.d.ts +13 -4
  7. package/dist/engines/engine-detector.js +74 -10
  8. package/dist/engines/generic-adapter.d.ts +12 -6
  9. package/dist/engines/generic-adapter.js +46 -15
  10. package/dist/engines/index.d.ts +1 -0
  11. package/dist/engines/index.js +1 -0
  12. package/dist/engines/playable-scripts-adapter.d.ts +148 -0
  13. package/dist/engines/playable-scripts-adapter.js +1084 -0
  14. package/dist/engines/playcanvas-adapter.js +3 -0
  15. package/dist/generators/config-generator.js +10 -17
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.js +3 -1
  18. package/dist/platforms/google.d.ts +9 -0
  19. package/dist/platforms/google.js +68 -7
  20. package/dist/templates/__loading__.js +100 -0
  21. package/dist/templates/__modules__.js +47 -0
  22. package/dist/templates/__settings__.template.js +20 -0
  23. package/dist/templates/__start__.js +332 -0
  24. package/dist/templates/index.html +18 -0
  25. package/dist/templates/logo.png +0 -0
  26. package/dist/templates/manifest.json +1 -0
  27. package/dist/templates/patches/cannon.min.js +28 -0
  28. package/dist/templates/patches/lz4.js +10 -0
  29. package/dist/templates/patches/one-page-http-get.js +20 -0
  30. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  31. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  32. package/dist/templates/patches/p2.min.js +27 -0
  33. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  34. package/dist/templates/playcanvas-stable.min.js +16363 -0
  35. package/dist/templates/styles.css +43 -0
  36. package/dist/types.d.ts +60 -13
  37. package/dist/utils/build-mode-detector.js +2 -0
  38. package/dist/vite/plugin-playcanvas.js +14 -19
  39. package/dist/vite/plugin-source-builder.js +383 -97
  40. package/package.json +7 -4
  41. package/dist/utils/obfuscate.d.ts +0 -42
  42. package/dist/utils/obfuscate.js +0 -216
  43. package/dist/vite/plugin-obfuscate.d.ts +0 -22
  44. package/dist/vite/plugin-obfuscate.js +0 -52
  45. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  46. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -0,0 +1,226 @@
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
+ const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a']);
7
+ const SPECTRAL_SUFFIX = '.spectral.json';
8
+ /**
9
+ * Read SpectralAudioEngine runtime source from packages/build/src/runtime/
10
+ */
11
+ async function readSpectralRuntime() {
12
+ const runtimePath = path.join(__dirname, 'runtime', 'spectral-audio-engine.js');
13
+ try {
14
+ return await fs.readFile(runtimePath, 'utf-8');
15
+ }
16
+ catch {
17
+ // Fallback minimal runtime if file not found
18
+ return '/* SpectralAudioEngine runtime not found */';
19
+ }
20
+ }
21
+ function formatScanError(err) {
22
+ return err instanceof Error ? err.message : String(err);
23
+ }
24
+ /**
25
+ * Recursively scan a directory for audio and spectral JSON files.
26
+ */
27
+ async function scanDirectory(dir, baseDir, report) {
28
+ const audioFiles = [];
29
+ const spectralFiles = [];
30
+ const resolvedRoot = path.resolve(dir);
31
+ async function walk(current) {
32
+ let entries;
33
+ try {
34
+ entries = (await fs.readdir(current, { withFileTypes: true }));
35
+ }
36
+ catch (err) {
37
+ const rel = path.relative(baseDir, current) || current;
38
+ if (path.resolve(current) === resolvedRoot) {
39
+ throw new Error(`[AudioOptimizer] Cannot read build directory "${current}": ${formatScanError(err)}`);
40
+ }
41
+ report.warn(`[AudioOptimizer] Skipping unreadable directory "${rel}": ${formatScanError(err)}`, err);
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ const fullPath = path.join(current, entry.name);
46
+ if (entry.isDirectory()) {
47
+ await walk(fullPath);
48
+ }
49
+ else if (entry.isFile()) {
50
+ const ext = path.extname(entry.name).toLowerCase();
51
+ if (AUDIO_EXTENSIONS.has(ext)) {
52
+ try {
53
+ const st = await fs.stat(fullPath);
54
+ audioFiles.push({
55
+ filePath: fullPath,
56
+ relativePath: path.relative(baseDir, fullPath),
57
+ size: st.size,
58
+ hasSpectral: false,
59
+ });
60
+ }
61
+ catch (err) {
62
+ const rel = path.relative(baseDir, fullPath);
63
+ report.warn(`[AudioOptimizer] Could not stat audio file "${rel}": ${formatScanError(err)}`, err);
64
+ }
65
+ }
66
+ else if (entry.name.endsWith(SPECTRAL_SUFFIX)) {
67
+ spectralFiles.push(fullPath);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ await walk(dir);
73
+ // Cross-reference: for each audio file, check if a spectral counterpart exists
74
+ const spectralSet = new Set(spectralFiles);
75
+ for (const audio of audioFiles) {
76
+ const spectralPath = audio.filePath.replace(/\.[^.]+$/, SPECTRAL_SUFFIX);
77
+ if (spectralSet.has(spectralPath)) {
78
+ audio.hasSpectral = true;
79
+ try {
80
+ const st = await fs.stat(spectralPath);
81
+ audio.spectralSize = st.size;
82
+ audio.potentialSavings = audio.size - st.size;
83
+ }
84
+ catch (err) {
85
+ report.warn(`[AudioOptimizer] Could not stat spectral JSON for "${audio.relativePath}" ` +
86
+ `("${path.relative(baseDir, spectralPath)}"): ${formatScanError(err)}`, err);
87
+ audio.hasSpectral = false;
88
+ audio.spectralSize = undefined;
89
+ audio.potentialSavings = undefined;
90
+ }
91
+ }
92
+ }
93
+ return { audioFiles, spectralFiles };
94
+ }
95
+ /**
96
+ * Inject SpectralAudioEngine runtime into an HTML string.
97
+ * Inserts a <script> tag before </head> (or at top of <body> if no </head>).
98
+ */
99
+ function injectRuntimeIntoHtml(html, runtimeCode) {
100
+ const scriptTag = `<script>/* SpectralAudioEngine v1 */\n${runtimeCode}\n</script>`;
101
+ const headCloseIdx = html.lastIndexOf('</head>');
102
+ if (headCloseIdx !== -1) {
103
+ return html.slice(0, headCloseIdx) + scriptTag + '\n' + html.slice(headCloseIdx);
104
+ }
105
+ const bodyOpenIdx = html.indexOf('<body');
106
+ if (bodyOpenIdx !== -1) {
107
+ const bodyTagEnd = html.indexOf('>', bodyOpenIdx) + 1;
108
+ return html.slice(0, bodyTagEnd) + '\n' + scriptTag + html.slice(bodyTagEnd);
109
+ }
110
+ return scriptTag + '\n' + html;
111
+ }
112
+ /**
113
+ * AudioOptimizer — Build-time audio analysis and optimization.
114
+ *
115
+ * Scans a build directory for audio files and .spectral.json counterparts.
116
+ * When spectral files are present, optionally injects the SpectralAudioEngine
117
+ * runtime (< 3KB) into the output HTML so the browser can synthesize them.
118
+ *
119
+ * Usage:
120
+ * const optimizer = new AudioOptimizer({ buildDir: './dist', htmlOutputPath: './dist/index.html' });
121
+ * const report = await optimizer.optimize();
122
+ * console.log(report);
123
+ */
124
+ export class AudioOptimizer {
125
+ constructor(options) {
126
+ this.options = {
127
+ injectRuntime: true,
128
+ htmlOutputPath: '',
129
+ verbose: false,
130
+ ...options,
131
+ };
132
+ }
133
+ /**
134
+ * Run the optimizer. Returns a report with audio file details and optimization results.
135
+ */
136
+ async optimize() {
137
+ const { buildDir, injectRuntime, htmlOutputPath, verbose } = this.options;
138
+ const log = (msg) => {
139
+ if (verbose)
140
+ console.log(`[AudioOptimizer] ${msg}`);
141
+ };
142
+ log(`Scanning ${buildDir} for audio files...`);
143
+ const warn = (message, cause) => {
144
+ console.warn(message);
145
+ if (verbose && cause !== undefined) {
146
+ console.warn(cause);
147
+ }
148
+ };
149
+ const { audioFiles, spectralFiles } = await scanDirectory(buildDir, buildDir, { warn });
150
+ const totalAudioSize = audioFiles.reduce((sum, f) => sum + f.size, 0);
151
+ const totalSpectralSize = audioFiles.reduce((sum, f) => {
152
+ return sum + (f.hasSpectral && f.spectralSize !== undefined ? f.spectralSize : f.size);
153
+ }, 0);
154
+ log(`Found ${audioFiles.length} audio file(s), ${spectralFiles.length} spectral JSON(s)`);
155
+ log(`Total audio: ${(totalAudioSize / 1024).toFixed(1)}KB → with spectral: ${(totalSpectralSize / 1024).toFixed(1)}KB`);
156
+ let runtimeInjected = false;
157
+ let runtimeSize = 0;
158
+ if (injectRuntime && spectralFiles.length > 0 && htmlOutputPath) {
159
+ log(`Injecting SpectralAudioEngine runtime into ${htmlOutputPath}...`);
160
+ try {
161
+ const [html, runtimeCode] = await Promise.all([
162
+ fs.readFile(htmlOutputPath, 'utf-8'),
163
+ readSpectralRuntime(),
164
+ ]);
165
+ runtimeSize = Buffer.byteLength(runtimeCode, 'utf-8');
166
+ const injectedHtml = injectRuntimeIntoHtml(html, runtimeCode);
167
+ await fs.writeFile(htmlOutputPath, injectedHtml, 'utf-8');
168
+ runtimeInjected = true;
169
+ log(`Runtime injected (${runtimeSize}B)`);
170
+ }
171
+ catch (err) {
172
+ log(`Warning: failed to inject runtime: ${err.message}`);
173
+ }
174
+ }
175
+ else if (spectralFiles.length === 0) {
176
+ log('No spectral files found — runtime injection skipped');
177
+ }
178
+ const netSavings = totalAudioSize - totalSpectralSize - runtimeSize;
179
+ if (verbose) {
180
+ for (const f of audioFiles) {
181
+ if (f.hasSpectral) {
182
+ log(` ✓ ${f.relativePath} ${(f.size / 1024).toFixed(1)}KB → spectral ${(f.spectralSize ?? 0)}B ` +
183
+ `(saved ${((f.potentialSavings ?? 0) / 1024).toFixed(1)}KB)`);
184
+ }
185
+ else {
186
+ log(` - ${f.relativePath} ${(f.size / 1024).toFixed(1)}KB (no spectral counterpart)`);
187
+ }
188
+ }
189
+ }
190
+ return {
191
+ audioFiles,
192
+ spectralFiles,
193
+ totalAudioSize,
194
+ totalSpectralSize,
195
+ runtimeInjected,
196
+ runtimeSize,
197
+ netSavings,
198
+ };
199
+ }
200
+ /**
201
+ * Print a human-readable summary of the optimization report.
202
+ */
203
+ static printReport(report) {
204
+ console.log('\n── Audio Optimization Report ──────────────────────');
205
+ console.log(` Audio files: ${report.audioFiles.length}`);
206
+ console.log(` Spectral JSONs: ${report.spectralFiles.length}`);
207
+ console.log(` Total audio: ${(report.totalAudioSize / 1024).toFixed(1)}KB`);
208
+ if (report.spectralFiles.length > 0) {
209
+ console.log(` With spectral: ${(report.totalSpectralSize / 1024).toFixed(1)}KB`);
210
+ if (report.runtimeInjected) {
211
+ console.log(` + Runtime: +${report.runtimeSize}B`);
212
+ }
213
+ const savings = report.netSavings;
214
+ const pct = report.totalAudioSize > 0
215
+ ? ((savings / report.totalAudioSize) * 100).toFixed(1)
216
+ : '0';
217
+ if (savings > 0) {
218
+ console.log(` Net savings: ${(savings / 1024).toFixed(1)}KB (${pct}%)`);
219
+ }
220
+ }
221
+ if (report.runtimeInjected) {
222
+ console.log(' ✓ SpectralAudioEngine runtime injected');
223
+ }
224
+ console.log('────────────────────────────────────────────────────\n');
225
+ }
226
+ }
@@ -1,30 +1,42 @@
1
- import type { BaseBuildOptions, BaseBuildOutput } from './types.js';
1
+ import type { BaseBuildOptions, BaseBuildOutput, PlayableScriptsConfig } from './types.js';
2
2
  /**
3
- * 基础构建器 - 路由到不同引擎适配器
3
+ * 基础构建器 - 路由到不同构建工具适配器
4
4
  *
5
5
  * 职责:
6
- * 1. 确定引擎类型(从 options 或自动检测)
7
- * 2. 路由到对应适配器执行 Base Build
6
+ * 1. 确定引擎类型和构建工具(从 options 或自动检测)
7
+ * 2. 根据构建工具路由到对应适配器执行 Base Build
8
8
  *
9
- * 适配器:
10
- * - PlayCanvasAdapter:处理 PlayCanvas 项目(官方构建产物或源代码)
11
- * - GenericAdapter:处理外部引擎(npm install + npm run build)
9
+ * 路由规则(按 buildTool):
10
+ * - playcanvas-native → PlayCanvasAdapter:处理 PlayCanvas 项目
11
+ * - playable-scripts PlayableScriptsAdapter:处理 @playcraft/devkit 项目
12
+ * - generic → GenericAdapter:处理外部引擎(npm install + npm run build)
12
13
  */
13
14
  export declare class BaseBuilder {
14
15
  private projectDir;
15
16
  private options;
16
- constructor(projectDir: string, options: BaseBuildOptions);
17
+ private playableScriptsConfig?;
18
+ private usePlayableScripts?;
19
+ private _metadata?;
20
+ constructor(projectDir: string, options: BaseBuildOptions, extraOptions?: {
21
+ playableScriptsConfig?: PlayableScriptsConfig;
22
+ usePlayableScripts?: boolean;
23
+ });
24
+ /**
25
+ * 获取构建元数据(在 build() 完成后可用)
26
+ */
27
+ get metadata(): BaseBuildOutput['metadata'] | undefined;
17
28
  /**
18
29
  * 执行基础构建
19
- * 根据引擎类型路由到对应适配器
30
+ * 根据构建工具路由到对应适配器
20
31
  */
21
32
  build(): Promise<BaseBuildOutput>;
22
33
  /**
23
- * 解析引擎类型
34
+ * 解析引擎类型和构建工具
24
35
  * 优先级:
25
- * 1. options.engine(用户显式指定)
26
- * 2. 自动检测
36
+ * 1. --use-playable-scripts 显式指定
37
+ * 2. options.engine 用户显式指定引擎
38
+ * 3. 自动检测(detectFull)
27
39
  */
28
- private resolveEngine;
40
+ private resolveDetection;
29
41
  }
30
42
  export type { BaseBuildOptions, BaseBuildOutput } from './types.js';
@@ -1,58 +1,98 @@
1
- import { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
1
+ import { EngineDetector, PlayCanvasAdapter, GenericAdapter, PlayableScriptsAdapter } from './engines/index.js';
2
2
  /**
3
- * 基础构建器 - 路由到不同引擎适配器
3
+ * 基础构建器 - 路由到不同构建工具适配器
4
4
  *
5
5
  * 职责:
6
- * 1. 确定引擎类型(从 options 或自动检测)
7
- * 2. 路由到对应适配器执行 Base Build
6
+ * 1. 确定引擎类型和构建工具(从 options 或自动检测)
7
+ * 2. 根据构建工具路由到对应适配器执行 Base Build
8
8
  *
9
- * 适配器:
10
- * - PlayCanvasAdapter:处理 PlayCanvas 项目(官方构建产物或源代码)
11
- * - GenericAdapter:处理外部引擎(npm install + npm run build)
9
+ * 路由规则(按 buildTool):
10
+ * - playcanvas-native → PlayCanvasAdapter:处理 PlayCanvas 项目
11
+ * - playable-scripts PlayableScriptsAdapter:处理 @playcraft/devkit 项目
12
+ * - generic → GenericAdapter:处理外部引擎(npm install + npm run build)
12
13
  */
13
14
  export class BaseBuilder {
14
- constructor(projectDir, options) {
15
+ constructor(projectDir, options, extraOptions) {
15
16
  this.projectDir = projectDir;
16
17
  this.options = options;
18
+ this.playableScriptsConfig = extraOptions?.playableScriptsConfig;
19
+ this.usePlayableScripts = extraOptions?.usePlayableScripts;
17
20
  console.log(`[BaseBuilder] 初始化: projectDir=${projectDir}, outputDir=${options.outputDir}`);
18
21
  }
22
+ /**
23
+ * 获取构建元数据(在 build() 完成后可用)
24
+ */
25
+ get metadata() {
26
+ return this._metadata;
27
+ }
19
28
  /**
20
29
  * 执行基础构建
21
- * 根据引擎类型路由到对应适配器
30
+ * 根据构建工具路由到对应适配器
22
31
  */
23
32
  async build() {
24
- // 1. 确定引擎类型
25
- const engine = await this.resolveEngine();
26
- console.log(`[BaseBuilder] 使用引擎: ${engine}`);
27
- // 2. 路由到对应适配器
28
- if (engine === 'playcanvas') {
29
- const adapter = new PlayCanvasAdapter(this.projectDir, this.options);
30
- return adapter.baseBuild();
31
- }
32
- else {
33
- const adapter = new GenericAdapter(this.projectDir, this.options, engine);
34
- return adapter.baseBuild();
33
+ // 1. 确定引擎类型和构建工具
34
+ const { engine, buildTool } = await this.resolveDetection();
35
+ console.log(`[BaseBuilder] 使用引擎: ${engine}, 构建工具: ${buildTool}`);
36
+ // 2. 根据构建工具路由到对应适配器
37
+ let result;
38
+ switch (buildTool) {
39
+ case 'playcanvas-native': {
40
+ const adapter = new PlayCanvasAdapter(this.projectDir, this.options);
41
+ result = await adapter.baseBuild();
42
+ break;
43
+ }
44
+ case 'playable-scripts': {
45
+ const adapter = new PlayableScriptsAdapter(this.projectDir, this.options, this.playableScriptsConfig);
46
+ result = await adapter.baseBuild();
47
+ break;
48
+ }
49
+ case 'generic':
50
+ default: {
51
+ const adapter = new GenericAdapter(this.projectDir, this.options, engine);
52
+ result = await adapter.baseBuild();
53
+ break;
54
+ }
35
55
  }
56
+ // 3. 存储 metadata 供外部访问
57
+ this._metadata = result.metadata;
58
+ return result;
36
59
  }
37
60
  /**
38
- * 解析引擎类型
61
+ * 解析引擎类型和构建工具
39
62
  * 优先级:
40
- * 1. options.engine(用户显式指定)
41
- * 2. 自动检测
63
+ * 1. --use-playable-scripts 显式指定
64
+ * 2. options.engine 用户显式指定引擎
65
+ * 3. 自动检测(detectFull)
42
66
  */
43
- async resolveEngine() {
44
- // 1. 用户显式指定
67
+ async resolveDetection() {
68
+ // 1. 用户显式指定 --use-playable-scripts
69
+ if (this.usePlayableScripts) {
70
+ console.log('[BaseBuilder] 用户显式指定使用 playable-scripts 构建工具');
71
+ return { engine: 'generic', buildTool: 'playable-scripts' };
72
+ }
73
+ // 2. 用户显式指定引擎
45
74
  if (this.options.engine) {
46
75
  const validated = EngineDetector.validateEngine(this.options.engine);
47
76
  if (validated) {
48
77
  console.log(`[BaseBuilder] 使用用户指定的引擎: ${validated}`);
49
- return validated;
78
+ // PlayCanvas 直接使用原生适配器
79
+ if (validated === 'playcanvas') {
80
+ return { engine: 'playcanvas', buildTool: 'playcanvas-native' };
81
+ }
82
+ // 其他引擎:检测是否可用 playable-scripts(@playcraft/adsdk 项目)
83
+ const detected = await EngineDetector.detectFull(this.projectDir);
84
+ if (detected.buildTool === 'playable-scripts') {
85
+ console.log(`[BaseBuilder] 检测到 playable-scripts 项目,优先使用 devkit 构建`);
86
+ return detected;
87
+ }
88
+ // 否则使用 generic 适配器
89
+ return { engine: validated, buildTool: 'generic' };
50
90
  }
51
91
  console.warn(`[BaseBuilder] 无效的引擎类型: ${this.options.engine},将自动检测`);
52
92
  }
53
- // 2. 自动检测
54
- const detected = await EngineDetector.detect(this.projectDir);
55
- console.log(`[BaseBuilder] 自动检测引擎: ${detected}`);
93
+ // 3. 自动检测
94
+ const detected = await EngineDetector.detectFull(this.projectDir);
95
+ console.log(`[BaseBuilder] 自动检测结果: engine=${detected.engine}, buildTool=${detected.buildTool}`);
56
96
  return detected;
57
97
  }
58
98
  }
@@ -1,14 +1,19 @@
1
- import type { EngineType } from '../types.js';
1
+ import type { EngineType, DetectionResult } from '../types.js';
2
2
  /**
3
3
  * 引擎检测器
4
- * 根据项目文件结构自动检测引擎类型
4
+ * 根据项目文件结构自动检测引擎类型和构建工具
5
5
  */
6
6
  export declare class EngineDetector {
7
7
  /**
8
- * 检测项目的引擎类型
8
+ * 完整检测:返回引擎类型 + 构建工具的二维检测结果
9
+ * @param projectDir 项目目录
10
+ * @returns 包含 engine 和 buildTool 的检测结果
11
+ */
12
+ static detectFull(projectDir: string): Promise<DetectionResult>;
13
+ /**
14
+ * 检测项目的引擎类型(向后兼容)
9
15
  * @param projectDir 项目目录
10
16
  * @returns 引擎类型
11
- * @throws 如果无法识别引擎,抛出错误
12
17
  */
13
18
  static detect(projectDir: string): Promise<EngineType>;
14
19
  /**
@@ -19,6 +24,10 @@ export declare class EngineDetector {
19
24
  * 读取 package.json
20
25
  */
21
26
  private static readPackageJson;
27
+ /**
28
+ * 从 package.json 检测构建工具(优先级 200,高于引擎检测)
29
+ */
30
+ private static detectBuildToolFromPackageJson;
22
31
  /**
23
32
  * 从 package.json 检测引擎类型
24
33
  */
@@ -15,22 +15,46 @@ const ENGINE_DETECTION_MAP = [
15
15
  { match: (n) => n.startsWith('laya-') || n.startsWith('@layabox/'), engine: 'layaair', priority: 100 },
16
16
  { match: (n) => n === 'egret', engine: 'egret', priority: 100 },
17
17
  ];
18
+ /**
19
+ * 构建工具检测映射表 - 优先级高于引擎检测
20
+ * 当检测到特定构建工具或 SDK 时,可以隐含引擎类型和构建工具
21
+ *
22
+ * 注意:@playcraft/devkit 是 PlayCraft 平台内置的构建工具(已集成到 @playcraft/cli)。
23
+ * 用户项目只需依赖 @playcraft/adsdk(运行时 SDK),不需要安装构建工具。
24
+ * 检测到 adsdk 时,自动使用平台内置的 devkit 进行构建。
25
+ */
26
+ const BUILD_TOOL_DETECTION_MAP = [
27
+ {
28
+ // 检测 @playcraft/adsdk(运行时 SDK)- 使用平台内置的 devkit 构建
29
+ // 或旧的内部包名 @tencent/playable-sdk
30
+ match: (n) => n === '@playcraft/adsdk' || n === '@tencent/playable-sdk',
31
+ buildTool: 'playable-scripts',
32
+ impliedEngine: 'phaser', // adsdk 项目通常是 Phaser 游戏
33
+ priority: 200, // 高于引擎检测优先级(200 > 100)
34
+ },
35
+ {
36
+ // 向后兼容:检测旧的 devkit 包名(用户项目可能还在使用)
37
+ match: (n) => n === '@playcraft/devkit' || n === '@tencent/playable-scripts',
38
+ buildTool: 'playable-scripts',
39
+ impliedEngine: 'generic',
40
+ priority: 200,
41
+ },
42
+ ];
18
43
  /**
19
44
  * 引擎检测器
20
- * 根据项目文件结构自动检测引擎类型
45
+ * 根据项目文件结构自动检测引擎类型和构建工具
21
46
  */
22
47
  export class EngineDetector {
23
48
  /**
24
- * 检测项目的引擎类型
49
+ * 完整检测:返回引擎类型 + 构建工具的二维检测结果
25
50
  * @param projectDir 项目目录
26
- * @returns 引擎类型
27
- * @throws 如果无法识别引擎,抛出错误
51
+ * @returns 包含 engine 和 buildTool 的检测结果
28
52
  */
29
- static async detect(projectDir) {
53
+ static async detectFull(projectDir) {
30
54
  // 1. 优先检测 PlayCanvas / PlayCraft 特有文件(最快路径)
31
55
  const isPlayCanvas = await this.detectPlayCanvasFiles(projectDir);
32
56
  if (isPlayCanvas) {
33
- return 'playcanvas';
57
+ return { engine: 'playcanvas', buildTool: 'playcanvas-native' };
34
58
  }
35
59
  // 2. 检测 package.json
36
60
  const pkg = await this.readPackageJson(projectDir);
@@ -39,15 +63,30 @@ export class EngineDetector {
39
63
  `项目目录: ${projectDir}\n` +
40
64
  `请确保这是一个有效的项目目录。`);
41
65
  }
42
- // 3. package.json 检测引擎
66
+ // 3. 优先检测构建工具(BUILD_TOOL_DETECTION_MAP,优先级 200)
67
+ const buildToolResult = this.detectBuildToolFromPackageJson(pkg);
68
+ if (buildToolResult) {
69
+ console.log(`[EngineDetector] 检测到构建工具: ${buildToolResult.buildTool}(隐含引擎: ${buildToolResult.engine})`);
70
+ return buildToolResult;
71
+ }
72
+ // 4. 检测引擎类型(ENGINE_DETECTION_MAP,优先级 100)
43
73
  const engine = this.detectFromPackageJson(pkg);
44
74
  if (engine) {
45
75
  console.log(`[EngineDetector] 检测到引擎: ${engine}(通过 package.json)`);
46
- return engine;
76
+ return { engine, buildTool: 'generic' };
47
77
  }
48
- // 4. 兜底:generic(任何有 npm build 的项目)
78
+ // 5. 兜底
49
79
  console.log('[EngineDetector] 无法识别具体引擎,使用 generic 类型');
50
- return 'generic';
80
+ return { engine: 'generic', buildTool: 'generic' };
81
+ }
82
+ /**
83
+ * 检测项目的引擎类型(向后兼容)
84
+ * @param projectDir 项目目录
85
+ * @returns 引擎类型
86
+ */
87
+ static async detect(projectDir) {
88
+ const result = await this.detectFull(projectDir);
89
+ return result.engine;
51
90
  }
52
91
  /**
53
92
  * 检测 PlayCanvas 特有文件
@@ -122,6 +161,31 @@ export class EngineDetector {
122
161
  return null;
123
162
  }
124
163
  }
164
+ /**
165
+ * 从 package.json 检测构建工具(优先级 200,高于引擎检测)
166
+ */
167
+ static detectBuildToolFromPackageJson(pkg) {
168
+ const allDeps = {
169
+ ...pkg.dependencies,
170
+ ...pkg.devDependencies,
171
+ };
172
+ // 收集所有匹配的构建工具,按优先级排序后返回最高优先级的项
173
+ const matches = [];
174
+ for (const [depName] of Object.entries(allDeps)) {
175
+ for (const { match, buildTool, impliedEngine, priority } of BUILD_TOOL_DETECTION_MAP) {
176
+ if (match(depName)) {
177
+ matches.push({ engine: impliedEngine, buildTool, priority });
178
+ }
179
+ }
180
+ }
181
+ if (matches.length === 0) {
182
+ return null;
183
+ }
184
+ // 按优先级排序(数字越大优先级越高),返回最高优先级的项
185
+ matches.sort((a, b) => b.priority - a.priority);
186
+ const bestMatch = matches[0];
187
+ return { engine: bestMatch.engine, buildTool: bestMatch.buildTool };
188
+ }
125
189
  /**
126
190
  * 从 package.json 检测引擎类型
127
191
  */
@@ -13,18 +13,24 @@ export declare class GenericAdapter {
13
13
  * 执行通用基础构建
14
14
  * 流程:
15
15
  * 1. 检测包管理器
16
- * 2. 执行 npm install(超时 3 分钟)
17
- * 3. 检测 build script
18
- * 4. 执行 npm run build(超时 5 分钟)
19
- * 5. 查找构建产物目录
20
- * 6. 复制构建产物到 outputDir
21
- * 7. 写入 .build-metadata.json
16
+ * 2. 检查 node_modules(存在则跳过安装)
17
+ * 3. 执行 npm install(超时 3 分钟)
18
+ * 4. 检测 build script
19
+ * 5. 执行 npm run build(超时 5 分钟)
20
+ * 6. 查找构建产物目录
21
+ * 7. 复制构建产物到 outputDir
22
+ * 8. 写入 .build-metadata.json
22
23
  */
23
24
  baseBuild(): Promise<BaseBuildOutput>;
24
25
  /**
25
26
  * 检测包管理器
26
27
  */
27
28
  private detectPackageManager;
29
+ /**
30
+ * 检查 node_modules 是否存在且不为空
31
+ * 用于判断是否需要执行安装
32
+ */
33
+ private checkNodeModules;
28
34
  /**
29
35
  * 执行 npm install
30
36
  * 超时:3 分钟