@playcraft/build 0.0.13 → 0.0.15

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 (82) hide show
  1. package/dist/analyzers/scene-asset-collector.js +99 -9
  2. package/dist/base-builder.d.ts +15 -78
  3. package/dist/base-builder.js +34 -741
  4. package/dist/engines/engine-detector.d.ts +38 -0
  5. package/dist/engines/engine-detector.js +201 -0
  6. package/dist/engines/generic-adapter.d.ts +71 -0
  7. package/dist/engines/generic-adapter.js +378 -0
  8. package/dist/engines/index.d.ts +7 -0
  9. package/dist/engines/index.js +7 -0
  10. package/dist/engines/playcanvas-adapter.d.ts +85 -0
  11. package/dist/engines/playcanvas-adapter.js +813 -0
  12. package/dist/generators/config-generator.js +59 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/loaders/playcraft-loader.js +240 -5
  16. package/dist/platforms/adikteev.d.ts +1 -1
  17. package/dist/platforms/adikteev.js +30 -36
  18. package/dist/platforms/applovin.d.ts +1 -1
  19. package/dist/platforms/applovin.js +31 -36
  20. package/dist/platforms/base.d.ts +27 -5
  21. package/dist/platforms/base.js +79 -181
  22. package/dist/platforms/bigo.d.ts +1 -1
  23. package/dist/platforms/bigo.js +28 -28
  24. package/dist/platforms/facebook.d.ts +1 -1
  25. package/dist/platforms/facebook.js +21 -10
  26. package/dist/platforms/google.d.ts +1 -1
  27. package/dist/platforms/google.js +28 -21
  28. package/dist/platforms/index.d.ts +1 -0
  29. package/dist/platforms/index.js +4 -0
  30. package/dist/platforms/inmobi.d.ts +1 -1
  31. package/dist/platforms/inmobi.js +27 -34
  32. package/dist/platforms/ironsource.d.ts +1 -1
  33. package/dist/platforms/ironsource.js +37 -40
  34. package/dist/platforms/liftoff.d.ts +1 -1
  35. package/dist/platforms/liftoff.js +22 -30
  36. package/dist/platforms/mintegral.d.ts +10 -0
  37. package/dist/platforms/mintegral.js +65 -0
  38. package/dist/platforms/moloco.d.ts +1 -1
  39. package/dist/platforms/moloco.js +18 -20
  40. package/dist/platforms/playcraft.d.ts +1 -1
  41. package/dist/platforms/playcraft.js +2 -2
  42. package/dist/platforms/remerge.d.ts +1 -1
  43. package/dist/platforms/remerge.js +19 -20
  44. package/dist/platforms/snapchat.d.ts +1 -1
  45. package/dist/platforms/snapchat.js +32 -26
  46. package/dist/platforms/tiktok.d.ts +1 -1
  47. package/dist/platforms/tiktok.js +28 -24
  48. package/dist/platforms/unity.d.ts +1 -1
  49. package/dist/platforms/unity.js +30 -36
  50. package/dist/playable-builder.d.ts +1 -0
  51. package/dist/playable-builder.js +16 -2
  52. package/dist/types.d.ts +113 -1
  53. package/dist/types.js +77 -1
  54. package/dist/utils/ammo-detector.d.ts +9 -0
  55. package/dist/utils/ammo-detector.js +76 -0
  56. package/dist/utils/build-mode-detector.js +2 -0
  57. package/dist/utils/minify.d.ts +32 -0
  58. package/dist/utils/minify.js +82 -0
  59. package/dist/utils/obfuscate.d.ts +42 -0
  60. package/dist/utils/obfuscate.js +216 -0
  61. package/dist/vite/config-builder-generic.d.ts +70 -0
  62. package/dist/vite/config-builder-generic.js +251 -0
  63. package/dist/vite/config-builder.d.ts +8 -0
  64. package/dist/vite/config-builder.js +53 -16
  65. package/dist/vite/platform-configs.js +29 -1
  66. package/dist/vite/plugin-compress-js.d.ts +21 -0
  67. package/dist/vite/plugin-compress-js.js +213 -0
  68. package/dist/vite/plugin-esm-html-generator.js +5 -1
  69. package/dist/vite/plugin-obfuscate.d.ts +22 -0
  70. package/dist/vite/plugin-obfuscate.js +52 -0
  71. package/dist/vite/plugin-platform.d.ts +5 -0
  72. package/dist/vite/plugin-platform.js +499 -35
  73. package/dist/vite/plugin-playcanvas.js +21 -68
  74. package/dist/vite/plugin-source-builder.js +102 -21
  75. package/dist/vite-builder.d.ts +25 -7
  76. package/dist/vite-builder.js +141 -52
  77. package/package.json +4 -2
  78. package/physics/cannon-rigidbody-adapter.js +243 -22
  79. package/templates/__loading__.js +0 -12
  80. package/templates/index.esm.mjs +0 -11
  81. package/templates/patches/playcraft-cta-adapter.js +129 -31
  82. package/templates/patches/scene-physics-defaults.js +49 -0
@@ -0,0 +1,813 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { build as viteBuild } from 'vite';
4
+ import { visualizer } from 'rollup-plugin-visualizer';
5
+ import { viteSourceBuilderPlugin } from '../vite/plugin-source-builder.js';
6
+ import { loadPlayCanvasProject } from '../loaders/playcanvas-loader.js';
7
+ import { BuildStateManager } from '../state/index.js';
8
+ import { BuildAnalyzer } from '../analyzers/build-analyzer.js';
9
+ /**
10
+ * PlayCanvas 引擎适配器
11
+ * 负责 PlayCanvas 项目的 Base Build(生成可运行的多文件构建产物)
12
+ */
13
+ export class PlayCanvasAdapter {
14
+ constructor(projectDir, options) {
15
+ this.projectDir = projectDir;
16
+ this.options = options;
17
+ this.stateManager = new BuildStateManager(projectDir, options.outputDir);
18
+ console.log(`[PlayCanvasAdapter] 初始化: projectDir=${projectDir}, outputDir=${options.outputDir}`);
19
+ }
20
+ /**
21
+ * 执行 PlayCanvas 基础构建
22
+ */
23
+ async baseBuild() {
24
+ // 开始构建阶段
25
+ this.stateManager.startStage('base-build', {
26
+ selectedScenes: this.options.selectedScenes,
27
+ analyze: this.options.analyze,
28
+ clean: this.options.clean,
29
+ engine: 'playcanvas',
30
+ });
31
+ // 检测项目类型
32
+ const projectType = await this.detectProjectType();
33
+ let result;
34
+ if (projectType === 'official-build') {
35
+ // 官方构建产物 - 直接复制并验证
36
+ result = await this.buildFromOfficial();
37
+ }
38
+ else {
39
+ // 源代码格式(PlayCanvas 或 PlayCraft)- 使用 Vite 生成构建产物
40
+ const formatName = projectType === 'playcraft' ? 'PlayCraft' : 'PlayCanvas';
41
+ console.log(`[PlayCanvasAdapter] 检测到 ${formatName} 源代码格式,使用 Vite 构建...`);
42
+ result = await this.buildFromSource();
43
+ }
44
+ // 保存构建元数据(包含引擎类型)
45
+ await this.saveBuildMetadata(result.metadata);
46
+ // 记录构建产物
47
+ await this.recordBuildOutput(result);
48
+ // 结束构建阶段
49
+ this.stateManager.endStage();
50
+ // 生成分析报告(如果启用)
51
+ if (this.options.analyze) {
52
+ await this.stateManager.saveState();
53
+ await this.generateAnalysisReport();
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * 保存构建元数据到输出目录
59
+ */
60
+ async saveBuildMetadata(metadata) {
61
+ const metadataPath = path.join(this.options.outputDir, '.build-metadata.json');
62
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
63
+ console.log('[PlayCanvasAdapter] 构建元数据已保存(引擎: playcanvas)');
64
+ }
65
+ /**
66
+ * 检测项目类型
67
+ */
68
+ async detectProjectType() {
69
+ // 1. 检查是否是官方构建产物
70
+ const buildIndicators = [
71
+ path.join(this.projectDir, 'index.html'),
72
+ path.join(this.projectDir, 'config.json'),
73
+ ];
74
+ try {
75
+ await fs.access(buildIndicators[0]);
76
+ await fs.access(buildIndicators[1]);
77
+ return 'official-build';
78
+ }
79
+ catch {
80
+ // 2. 检查是否是 PlayCraft 项目格式
81
+ const manifestPath = path.join(this.projectDir, 'manifest.json');
82
+ try {
83
+ await fs.access(manifestPath);
84
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
85
+ const manifest = JSON.parse(manifestContent);
86
+ if (manifest.format === 'playcraft' || Array.isArray(manifest.assets)) {
87
+ return 'playcraft';
88
+ }
89
+ }
90
+ catch {
91
+ // manifest.json 不存在或格式不对
92
+ }
93
+ // 3. 默认视为源代码项目
94
+ return 'source';
95
+ }
96
+ }
97
+ /**
98
+ * 检测是否为 ESM 格式项目
99
+ */
100
+ async detectESMFormat() {
101
+ const esmIndicators = [
102
+ path.join(this.projectDir, 'js/index.mjs'),
103
+ path.join(this.projectDir, 'esm-scripts'),
104
+ ];
105
+ for (const indicator of esmIndicators) {
106
+ try {
107
+ await fs.access(indicator);
108
+ return true;
109
+ }
110
+ catch {
111
+ // 继续检查下一个
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ /**
117
+ * 从 PlayCraft 项目中加载 Import Map
118
+ * PlayCraft 的 import map 存储在 assets/Import Map.json 中,
119
+ * 其 asset ID 在 manifest.settings.importMap 中引用
120
+ */
121
+ async loadPlayCraftImportMap() {
122
+ try {
123
+ // 直接尝试读取 assets/Import Map.json
124
+ const importMapPath = path.join(this.projectDir, 'assets', 'Import Map.json');
125
+ const content = await fs.readFile(importMapPath, 'utf-8');
126
+ const importMapContent = JSON.parse(content);
127
+ if (importMapContent.imports) {
128
+ console.log(`[PlayCanvasAdapter] 加载了 PlayCraft Import Map: ${Object.keys(importMapContent.imports).length} 个映射`);
129
+ return {
130
+ id: 'playcraft-import-map',
131
+ content: importMapContent,
132
+ };
133
+ }
134
+ }
135
+ catch {
136
+ // Import Map 文件不存在或格式不对
137
+ }
138
+ // 尝试从 manifest 中读取 importMap asset ID,再从 assets.json 查找
139
+ try {
140
+ const manifestContent = await fs.readFile(path.join(this.projectDir, 'manifest.json'), 'utf-8');
141
+ const manifest = JSON.parse(manifestContent);
142
+ const importMapId = manifest.settings?.importMap;
143
+ if (importMapId) {
144
+ const assetsJsonPath = path.join(this.projectDir, 'assets', 'assets.json');
145
+ const assetsContent = await fs.readFile(assetsJsonPath, 'utf-8');
146
+ const assets = JSON.parse(assetsContent);
147
+ const importMapAsset = assets[String(importMapId)];
148
+ if (importMapAsset?.file?.url) {
149
+ // 尝试从本地路径读取
150
+ const url = importMapAsset.file.url;
151
+ if (!url.startsWith('/api/') && !url.startsWith('http')) {
152
+ const filePath = path.join(this.projectDir, url);
153
+ const content = await fs.readFile(filePath, 'utf-8');
154
+ const importMapContent = JSON.parse(content);
155
+ if (importMapContent.imports) {
156
+ console.log(`[PlayCanvasAdapter] 从 assets.json 加载 Import Map: ${Object.keys(importMapContent.imports).length} 个映射`);
157
+ return {
158
+ id: String(importMapId),
159
+ content: importMapContent,
160
+ };
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // 无法从 manifest/assets.json 加载
168
+ }
169
+ return undefined;
170
+ }
171
+ /**
172
+ * 从官方构建产物构建
173
+ */
174
+ async buildFromOfficial() {
175
+ await this.validateOfficialBuild();
176
+ await fs.mkdir(this.options.outputDir, { recursive: true });
177
+ const files = await this.copyBuildFiles();
178
+ const metadata = await this.detectBuildModeFromOutput();
179
+ return {
180
+ outputDir: this.options.outputDir,
181
+ metadata,
182
+ files,
183
+ };
184
+ }
185
+ /**
186
+ * 从输出目录检测构建模式
187
+ */
188
+ async detectBuildModeFromOutput() {
189
+ const indexPath = path.join(this.options.outputDir, 'index.html');
190
+ try {
191
+ const html = await fs.readFile(indexPath, 'utf-8');
192
+ const hasImportMap = html.includes('<script type="importmap">');
193
+ if (hasImportMap) {
194
+ const importMapMatch = html.match(/<script type="importmap">\s*(\{[\s\S]*?\})\s*<\/script>/);
195
+ const importMapContent = importMapMatch ? JSON.parse(importMapMatch[1]) : { imports: {} };
196
+ return {
197
+ mode: 'esm',
198
+ engine: 'playcanvas',
199
+ importMap: {
200
+ id: 'detected',
201
+ imports: importMapContent.imports || {},
202
+ },
203
+ entryPoint: 'js/index.mjs',
204
+ };
205
+ }
206
+ }
207
+ catch {
208
+ // 忽略错误,返回 Classic 模式
209
+ }
210
+ return {
211
+ mode: 'classic',
212
+ engine: 'playcanvas',
213
+ };
214
+ }
215
+ /**
216
+ * 验证官方构建产物
217
+ */
218
+ async validateOfficialBuild() {
219
+ const isESM = await this.detectESMFormat();
220
+ const requiredFiles = isESM
221
+ ? ['index.html', 'config.json']
222
+ : ['index.html', 'config.json', '__start__.js'];
223
+ const missingFiles = [];
224
+ for (const file of requiredFiles) {
225
+ try {
226
+ await fs.access(path.join(this.projectDir, file));
227
+ }
228
+ catch {
229
+ missingFiles.push(file);
230
+ }
231
+ }
232
+ if (missingFiles.length > 0) {
233
+ const formatType = isESM ? 'ESM' : 'Classic';
234
+ throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
235
+ `检测到 ${formatType} 格式,请确保项目目录包含完整的构建产物。`);
236
+ }
237
+ }
238
+ /**
239
+ * 复制构建文件到输出目录
240
+ */
241
+ async copyBuildFiles() {
242
+ const files = {
243
+ html: '',
244
+ engine: null,
245
+ config: '',
246
+ settings: null,
247
+ modules: null,
248
+ start: '',
249
+ scenes: [],
250
+ assets: [],
251
+ };
252
+ // 复制 index.html
253
+ const htmlPath = path.join(this.projectDir, 'index.html');
254
+ const outputHtmlPath = path.join(this.options.outputDir, 'index.html');
255
+ await fs.copyFile(htmlPath, outputHtmlPath);
256
+ files.html = outputHtmlPath;
257
+ // 复制 config.json
258
+ const configPath = path.join(this.projectDir, 'config.json');
259
+ const outputConfigPath = path.join(this.options.outputDir, 'config.json');
260
+ await fs.copyFile(configPath, outputConfigPath);
261
+ files.config = outputConfigPath;
262
+ const configContent = await fs.readFile(configPath, 'utf-8');
263
+ const configJson = JSON.parse(configContent);
264
+ const isESM = await this.detectESMFormat();
265
+ // 复制 __start__.js(Classic 格式必需)
266
+ if (!isESM) {
267
+ const startPath = path.join(this.projectDir, '__start__.js');
268
+ const outputStartPath = path.join(this.options.outputDir, '__start__.js');
269
+ try {
270
+ await fs.copyFile(startPath, outputStartPath);
271
+ files.start = outputStartPath;
272
+ }
273
+ catch {
274
+ console.warn('[PlayCanvasAdapter] 警告: 无法复制 __start__.js');
275
+ }
276
+ }
277
+ // 复制 __settings__.js
278
+ const settingsPath = path.join(this.projectDir, '__settings__.js');
279
+ try {
280
+ await fs.access(settingsPath);
281
+ const outputSettingsPath = path.join(this.options.outputDir, '__settings__.js');
282
+ await fs.copyFile(settingsPath, outputSettingsPath);
283
+ files.settings = outputSettingsPath;
284
+ }
285
+ catch {
286
+ // __settings__.js 不是必需的
287
+ }
288
+ // 复制 __modules__.js
289
+ const modulesPath = path.join(this.projectDir, '__modules__.js');
290
+ try {
291
+ await fs.access(modulesPath);
292
+ const outputModulesPath = path.join(this.options.outputDir, '__modules__.js');
293
+ await fs.copyFile(modulesPath, outputModulesPath);
294
+ files.modules = outputModulesPath;
295
+ }
296
+ catch {
297
+ // __modules__.js 不是必需的
298
+ }
299
+ // 复制 PlayCanvas Engine
300
+ const engineNames = [
301
+ 'playcanvas-stable.min.js',
302
+ 'playcanvas.min.js',
303
+ '__lib__.js',
304
+ ];
305
+ for (const engineName of engineNames) {
306
+ const enginePath = path.join(this.projectDir, engineName);
307
+ try {
308
+ await fs.access(enginePath);
309
+ const outputEnginePath = path.join(this.options.outputDir, engineName);
310
+ await fs.copyFile(enginePath, outputEnginePath);
311
+ files.engine = outputEnginePath;
312
+ break;
313
+ }
314
+ catch {
315
+ // 继续尝试下一个
316
+ }
317
+ }
318
+ // 复制场景文件
319
+ if (configJson.scenes && Array.isArray(configJson.scenes)) {
320
+ for (const scene of configJson.scenes) {
321
+ if (scene.url && !scene.url.startsWith('data:')) {
322
+ const scenePath = path.join(this.projectDir, scene.url);
323
+ try {
324
+ await fs.access(scenePath);
325
+ const sceneDir = path.dirname(scene.url);
326
+ if (sceneDir && sceneDir !== '.') {
327
+ const outputSceneDir = path.join(this.options.outputDir, sceneDir);
328
+ await fs.mkdir(outputSceneDir, { recursive: true });
329
+ }
330
+ const outputScenePath = path.join(this.options.outputDir, scene.url);
331
+ await fs.copyFile(scenePath, outputScenePath);
332
+ files.scenes.push(outputScenePath);
333
+ }
334
+ catch {
335
+ console.warn(`[PlayCanvasAdapter] 警告: 场景文件不存在: ${scene.url}`);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ // 复制资产文件
341
+ if (configJson.assets) {
342
+ const assetsDir = path.join(this.options.outputDir, 'files');
343
+ await fs.mkdir(assetsDir, { recursive: true });
344
+ for (const [, assetData] of Object.entries(configJson.assets)) {
345
+ const asset = assetData;
346
+ if (asset.file && asset.file.url && !asset.file.url.startsWith('data:')) {
347
+ const assetPath = path.join(this.projectDir, asset.file.url);
348
+ try {
349
+ await fs.access(assetPath);
350
+ const assetDir = path.dirname(asset.file.url);
351
+ if (assetDir && assetDir !== '.') {
352
+ const outputAssetDir = path.join(this.options.outputDir, assetDir);
353
+ await fs.mkdir(outputAssetDir, { recursive: true });
354
+ }
355
+ const outputAssetPath = path.join(this.options.outputDir, asset.file.url);
356
+ await fs.copyFile(assetPath, outputAssetPath);
357
+ files.assets.push(outputAssetPath);
358
+ }
359
+ catch {
360
+ // 资产文件可能不存在
361
+ }
362
+ }
363
+ }
364
+ }
365
+ // 复制其他目录
366
+ await this.copyAdditionalDirectories(files);
367
+ return files;
368
+ }
369
+ /**
370
+ * 复制其他必要的目录
371
+ */
372
+ async copyAdditionalDirectories(files) {
373
+ // files/ 目录
374
+ const filesDir = path.join(this.projectDir, 'files');
375
+ try {
376
+ const stat = await fs.stat(filesDir);
377
+ if (stat.isDirectory()) {
378
+ const outputFilesDir = path.join(this.options.outputDir, 'files');
379
+ await this.copyDirectory(filesDir, outputFilesDir);
380
+ }
381
+ }
382
+ catch {
383
+ // files/ 目录可能不存在
384
+ }
385
+ // styles.css
386
+ const stylesPath = path.join(this.projectDir, 'styles.css');
387
+ try {
388
+ await fs.access(stylesPath);
389
+ const outputStylesPath = path.join(this.options.outputDir, 'styles.css');
390
+ await fs.copyFile(stylesPath, outputStylesPath);
391
+ }
392
+ catch {
393
+ // styles.css 可能不存在
394
+ }
395
+ // manifest.json
396
+ const manifestPath = path.join(this.projectDir, 'manifest.json');
397
+ try {
398
+ await fs.access(manifestPath);
399
+ const outputManifestPath = path.join(this.options.outputDir, 'manifest.json');
400
+ await fs.copyFile(manifestPath, outputManifestPath);
401
+ }
402
+ catch {
403
+ // manifest.json 可能不存在
404
+ }
405
+ // js/ 目录(ESM)
406
+ const jsDir = path.join(this.projectDir, 'js');
407
+ try {
408
+ const stat = await fs.stat(jsDir);
409
+ if (stat.isDirectory()) {
410
+ const outputJsDir = path.join(this.options.outputDir, 'js');
411
+ await this.copyDirectory(jsDir, outputJsDir);
412
+ }
413
+ }
414
+ catch {
415
+ // js/ 目录可能不存在
416
+ }
417
+ // esm-scripts/ 目录(ESM)
418
+ const esmScriptsDir = path.join(this.projectDir, 'esm-scripts');
419
+ try {
420
+ const stat = await fs.stat(esmScriptsDir);
421
+ if (stat.isDirectory()) {
422
+ const outputEsmScriptsDir = path.join(this.options.outputDir, 'esm-scripts');
423
+ await this.copyDirectory(esmScriptsDir, outputEsmScriptsDir);
424
+ }
425
+ }
426
+ catch {
427
+ // esm-scripts/ 目录可能不存在
428
+ }
429
+ }
430
+ /**
431
+ * 递归复制目录
432
+ */
433
+ async copyDirectory(src, dest) {
434
+ await fs.mkdir(dest, { recursive: true });
435
+ const entries = await fs.readdir(src, { withFileTypes: true });
436
+ for (const entry of entries) {
437
+ const srcPath = path.join(src, entry.name);
438
+ const destPath = path.join(dest, entry.name);
439
+ if (entry.isDirectory()) {
440
+ await this.copyDirectory(srcPath, destPath);
441
+ }
442
+ else {
443
+ await fs.copyFile(srcPath, destPath);
444
+ }
445
+ }
446
+ }
447
+ /**
448
+ * 从源代码构建(使用 Vite)
449
+ */
450
+ async buildFromSource() {
451
+ console.log('[PlayCanvasAdapter] 使用 Vite 从源代码构建...');
452
+ let buildMode = 'classic';
453
+ let importMap = undefined;
454
+ try {
455
+ console.log('[PlayCanvasAdapter] 正在加载项目配置...');
456
+ // 先检查是否是 PlayCraft 格式(根目录没有 project.json)
457
+ const manifestPath = path.join(this.projectDir, 'manifest.json');
458
+ let isPlayCraft = false;
459
+ try {
460
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
461
+ const manifest = JSON.parse(manifestContent);
462
+ isPlayCraft = manifest.format === 'playcraft' || Array.isArray(manifest.assets);
463
+ }
464
+ catch {
465
+ // manifest.json 不存在
466
+ }
467
+ if (!isPlayCraft) {
468
+ // PlayCanvas 原生格式:从 project.json 加载以检测 ESM 模式
469
+ const projectConfig = await loadPlayCanvasProject(this.projectDir);
470
+ buildMode = projectConfig.importMap ? 'esm' : 'classic';
471
+ importMap = projectConfig.importMap;
472
+ }
473
+ else {
474
+ // PlayCraft 格式:先加载 Import Map,再决定构建模式
475
+ // Import Map 存在意味着项目使用 ESM 模式(与 PlayCanvas 格式检测逻辑一致)
476
+ importMap = await this.loadPlayCraftImportMap();
477
+ if (importMap) {
478
+ // 有 Import Map → ESM 模式(脚本使用 import/export 语法和裸模块别名)
479
+ buildMode = 'esm';
480
+ console.log('[PlayCanvasAdapter] PlayCraft 项目检测到 Import Map,使用 ESM 模式');
481
+ }
482
+ else {
483
+ // 没有 Import Map → 回退到文件检测
484
+ const isESM = await this.detectESMFormat();
485
+ buildMode = isESM ? 'esm' : 'classic';
486
+ }
487
+ }
488
+ console.log(`[PlayCanvasAdapter] 构建模式: ${buildMode}`);
489
+ }
490
+ catch (error) {
491
+ console.log('[PlayCanvasAdapter] 无法加载项目配置,使用传统模式:', error.message);
492
+ }
493
+ const shouldEmptyOutDir = this.options.clean === true;
494
+ const viteConfig = buildMode === 'esm'
495
+ ? this.createESMConfig(importMap, shouldEmptyOutDir)
496
+ : this.createClassicConfig(importMap, shouldEmptyOutDir);
497
+ await viteBuild(viteConfig);
498
+ const files = await this.scanOutputFiles();
499
+ const metadata = {
500
+ mode: buildMode,
501
+ engine: 'playcanvas',
502
+ importMap: importMap ? {
503
+ id: importMap.id,
504
+ imports: importMap.content.imports,
505
+ } : undefined,
506
+ entryPoint: buildMode === 'esm' ? 'js/index.mjs' : undefined,
507
+ };
508
+ return {
509
+ outputDir: this.options.outputDir,
510
+ metadata,
511
+ files,
512
+ };
513
+ }
514
+ /**
515
+ * 创建 ESM 模式的 Vite 配置
516
+ */
517
+ createESMConfig(importMap, emptyOutDir) {
518
+ return {
519
+ root: this.projectDir,
520
+ base: './',
521
+ build: {
522
+ outDir: this.options.outputDir,
523
+ emptyOutDir,
524
+ write: this.options.analyze ? true : false,
525
+ rollupOptions: {
526
+ input: {
527
+ 'esm-entry': 'virtual:esm-entry',
528
+ },
529
+ },
530
+ },
531
+ plugins: [
532
+ {
533
+ name: 'virtual-esm-entry',
534
+ resolveId(id) {
535
+ if (id === 'virtual:esm-entry') {
536
+ return '\0virtual:esm-entry';
537
+ }
538
+ return null;
539
+ },
540
+ load(id) {
541
+ if (id === '\0virtual:esm-entry') {
542
+ return '// ESM mode - no bundling needed';
543
+ }
544
+ return null;
545
+ },
546
+ },
547
+ viteSourceBuilderPlugin({
548
+ projectDir: this.projectDir,
549
+ outputDir: this.options.outputDir,
550
+ selectedScenes: this.options.selectedScenes,
551
+ buildMode: 'esm',
552
+ importMap,
553
+ }),
554
+ ...(this.options.analyze ? [
555
+ visualizer({
556
+ filename: this.options.analyzeReportPath
557
+ ? path.join(this.options.outputDir, this.options.analyzeReportPath)
558
+ : path.join(this.options.outputDir, 'base-bundle-report.html'),
559
+ template: 'treemap',
560
+ gzipSize: true,
561
+ brotliSize: true,
562
+ open: false,
563
+ sourcemap: false,
564
+ }),
565
+ ] : []),
566
+ ],
567
+ };
568
+ }
569
+ /**
570
+ * 创建 Classic 模式的 Vite 配置
571
+ */
572
+ createClassicConfig(importMap, emptyOutDir) {
573
+ return {
574
+ root: this.projectDir,
575
+ base: './',
576
+ build: {
577
+ outDir: this.options.outputDir,
578
+ emptyOutDir,
579
+ assetsInlineLimit: 0,
580
+ minify: false,
581
+ cssMinify: false,
582
+ sourcemap: false,
583
+ rollupOptions: {
584
+ input: {
585
+ 'game-scripts': 'virtual:game-scripts',
586
+ },
587
+ output: {
588
+ entryFileNames: '__[name].js',
589
+ chunkFileNames: '__[name]-[hash].js',
590
+ assetFileNames: 'files/assets/[name].[ext]',
591
+ format: 'iife',
592
+ globals: {
593
+ 'playcanvas': 'pc',
594
+ 'pc': 'pc',
595
+ },
596
+ },
597
+ external: ['pc', 'playcanvas'],
598
+ treeshake: true,
599
+ },
600
+ },
601
+ plugins: [
602
+ viteSourceBuilderPlugin({
603
+ projectDir: this.projectDir,
604
+ outputDir: this.options.outputDir,
605
+ selectedScenes: this.options.selectedScenes,
606
+ buildMode: 'classic',
607
+ importMap,
608
+ }),
609
+ ...(this.options.analyze ? [
610
+ visualizer({
611
+ filename: this.options.analyzeReportPath
612
+ ? path.join(this.options.outputDir, this.options.analyzeReportPath)
613
+ : path.join(this.options.outputDir, 'base-bundle-report.html'),
614
+ template: 'treemap',
615
+ gzipSize: true,
616
+ brotliSize: true,
617
+ open: false,
618
+ }),
619
+ ] : []),
620
+ ],
621
+ };
622
+ }
623
+ /**
624
+ * 扫描输出文件
625
+ */
626
+ async scanOutputFiles() {
627
+ const files = {
628
+ html: path.join(this.options.outputDir, 'index.html'),
629
+ engine: null,
630
+ config: path.join(this.options.outputDir, 'config.json'),
631
+ settings: path.join(this.options.outputDir, '__settings__.js'),
632
+ modules: null,
633
+ start: path.join(this.options.outputDir, '__start__.js'),
634
+ scenes: [],
635
+ assets: [],
636
+ };
637
+ // 检查 engine
638
+ const enginePath = path.join(this.options.outputDir, 'playcanvas-stable.min.js');
639
+ try {
640
+ await fs.access(enginePath);
641
+ files.engine = enginePath;
642
+ }
643
+ catch {
644
+ // engine 可能不存在
645
+ }
646
+ // 检查 modules
647
+ const modulesPath = path.join(this.options.outputDir, '__modules__.js');
648
+ try {
649
+ await fs.access(modulesPath);
650
+ files.modules = modulesPath;
651
+ }
652
+ catch {
653
+ // modules 可能不存在
654
+ }
655
+ // 读取 config.json 获取场景信息
656
+ try {
657
+ const configContent = await fs.readFile(files.config, 'utf-8');
658
+ const configJson = JSON.parse(configContent);
659
+ if (configJson.scenes && Array.isArray(configJson.scenes)) {
660
+ for (const scene of configJson.scenes) {
661
+ if (scene.url && !scene.url.startsWith('data:')) {
662
+ const scenePath = path.join(this.options.outputDir, scene.url);
663
+ try {
664
+ await fs.access(scenePath);
665
+ files.scenes.push(scenePath);
666
+ }
667
+ catch {
668
+ // 场景文件可能不存在
669
+ }
670
+ }
671
+ }
672
+ }
673
+ // 扫描资产文件
674
+ const filesDir = path.join(this.options.outputDir, 'files');
675
+ try {
676
+ const stat = await fs.stat(filesDir);
677
+ if (stat.isDirectory()) {
678
+ await this.scanDirectory(filesDir, files.assets);
679
+ }
680
+ }
681
+ catch {
682
+ // files/ 目录可能不存在
683
+ }
684
+ }
685
+ catch (error) {
686
+ console.warn('[PlayCanvasAdapter] 警告: 无法读取 config.json:', error);
687
+ }
688
+ return files;
689
+ }
690
+ /**
691
+ * 递归扫描目录
692
+ */
693
+ async scanDirectory(dir, files) {
694
+ const entries = await fs.readdir(dir, { withFileTypes: true });
695
+ for (const entry of entries) {
696
+ const fullPath = path.join(dir, entry.name);
697
+ if (entry.isDirectory()) {
698
+ await this.scanDirectory(fullPath, files);
699
+ }
700
+ else {
701
+ files.push(fullPath);
702
+ }
703
+ }
704
+ }
705
+ /**
706
+ * 记录构建产物信息
707
+ */
708
+ async recordBuildOutput(result) {
709
+ console.log('[PlayCanvasAdapter] 记录构建产物信息...');
710
+ try {
711
+ if (result.files.html) {
712
+ const stat = await fs.stat(result.files.html);
713
+ this.stateManager.recordAsset('index.html', 'index.html', result.files.html, stat.size, 'html');
714
+ this.stateManager.updateAsset('index.html', 'base-build', 'index.html', 'index.html', stat.size);
715
+ }
716
+ if (result.files.engine) {
717
+ const stat = await fs.stat(result.files.engine);
718
+ const engineName = path.basename(result.files.engine);
719
+ this.stateManager.recordAsset('playcanvas-engine', engineName, result.files.engine, stat.size, 'script');
720
+ this.stateManager.updateAsset('playcanvas-engine', 'base-build', engineName, engineName, stat.size);
721
+ }
722
+ if (result.files.config) {
723
+ const stat = await fs.stat(result.files.config);
724
+ this.stateManager.recordAsset('config.json', 'config.json', result.files.config, stat.size, 'json');
725
+ this.stateManager.updateAsset('config.json', 'base-build', 'config.json', 'config.json', stat.size);
726
+ }
727
+ const scriptFiles = [
728
+ { path: result.files.start, id: '__start__.js' },
729
+ { path: result.files.settings, id: '__settings__.js' },
730
+ { path: result.files.modules, id: '__modules__.js' },
731
+ ];
732
+ for (const { path: filePath, id } of scriptFiles) {
733
+ if (filePath) {
734
+ try {
735
+ const stat = await fs.stat(filePath);
736
+ const fileName = path.basename(filePath);
737
+ this.stateManager.recordAsset(id, fileName, filePath, stat.size, 'script');
738
+ this.stateManager.updateAsset(id, 'base-build', fileName, fileName, stat.size);
739
+ }
740
+ catch {
741
+ // 文件可能不存在
742
+ }
743
+ }
744
+ }
745
+ for (const scenePath of result.files.scenes) {
746
+ try {
747
+ const stat = await fs.stat(scenePath);
748
+ const sceneName = path.basename(scenePath);
749
+ const sceneId = `scene-${sceneName}`;
750
+ this.stateManager.recordAsset(sceneId, sceneName, scenePath, stat.size, 'json');
751
+ this.stateManager.updateAsset(sceneId, 'base-build', sceneName, path.relative(this.options.outputDir, scenePath), stat.size);
752
+ }
753
+ catch {
754
+ // 忽略错误
755
+ }
756
+ }
757
+ for (const assetPath of result.files.assets) {
758
+ try {
759
+ const stat = await fs.stat(assetPath);
760
+ const assetName = path.basename(assetPath);
761
+ const assetId = `asset-${assetName}`;
762
+ const ext = path.extname(assetName).toLowerCase();
763
+ let assetType = 'other';
764
+ if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext)) {
765
+ assetType = 'texture';
766
+ }
767
+ else if (['.mp3', '.wav', '.ogg'].includes(ext)) {
768
+ assetType = 'audio';
769
+ }
770
+ else if (['.glb', '.gltf', '.obj', '.fbx'].includes(ext)) {
771
+ assetType = 'model';
772
+ }
773
+ this.stateManager.recordAsset(assetId, assetName, assetPath, stat.size, assetType);
774
+ this.stateManager.updateAsset(assetId, 'base-build', assetName, path.relative(this.options.outputDir, assetPath), stat.size);
775
+ }
776
+ catch {
777
+ // 忽略错误
778
+ }
779
+ }
780
+ console.log('[PlayCanvasAdapter] 构建产物信息已记录');
781
+ }
782
+ catch (error) {
783
+ console.error('[PlayCanvasAdapter] 记录构建产物信息失败:', error);
784
+ }
785
+ }
786
+ /**
787
+ * 生成构建分析报告
788
+ */
789
+ async generateAnalysisReport() {
790
+ console.log('\n[PlayCanvasAdapter] 生成构建分析报告...');
791
+ try {
792
+ const analyzer = new BuildAnalyzer(this.options.outputDir, this.options.outputDir);
793
+ const report = await analyzer.analyze();
794
+ const reportPath = await analyzer.generateHTMLReport(report);
795
+ // 删除旧的报告
796
+ const oldReportPath = path.join(this.options.outputDir, 'base-bundle-report.html');
797
+ try {
798
+ await fs.unlink(oldReportPath);
799
+ }
800
+ catch {
801
+ // 文件可能不存在,忽略
802
+ }
803
+ console.log(`\n📊 构建分析报告:`);
804
+ console.log(` 文件总数: ${report.totalFiles}`);
805
+ console.log(` 当前总大小: ${report.totalSizeFormatted}`);
806
+ console.log(` 预估单 HTML 大小: ${report.estimatedHtmlSizeFormatted}`);
807
+ console.log(` 报告路径: ${reportPath}\n`);
808
+ }
809
+ catch (error) {
810
+ console.error('[PlayCanvasAdapter] 生成分析报告失败:', error);
811
+ }
812
+ }
813
+ }