@playcraft/build 0.0.3 → 0.0.4
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/dist/base-builder.d.ts +2 -0
- package/dist/base-builder.js +19 -0
- package/dist/vite/config-builder.js +0 -14
- package/dist/vite/platform-configs.js +1 -1
- package/dist/vite/plugin-playcanvas.js +95 -22
- package/dist/vite/plugin-source-builder.js +36 -6
- package/package.json +1 -1
- package/templates/patches/one-page-inline-game-scripts.js +8 -0
package/dist/base-builder.d.ts
CHANGED
package/dist/base-builder.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { build as viteBuild } from 'vite';
|
|
5
|
+
import { visualizer } from 'rollup-plugin-visualizer';
|
|
5
6
|
import { viteSourceBuilderPlugin } from './vite/plugin-source-builder.js';
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -304,6 +305,11 @@ export class BaseBuilder {
|
|
|
304
305
|
entryFileNames: '__[name].js',
|
|
305
306
|
chunkFileNames: '__[name]-[hash].js',
|
|
306
307
|
assetFileNames: 'files/assets/[name].[ext]',
|
|
308
|
+
format: 'iife', // 使用 IIFE 格式,避免 ES 模块的 import 语句
|
|
309
|
+
globals: {
|
|
310
|
+
'playcanvas': 'pc',
|
|
311
|
+
'pc': 'pc',
|
|
312
|
+
},
|
|
307
313
|
},
|
|
308
314
|
// 外部化 PlayCanvas Engine(不打包)
|
|
309
315
|
external: ['pc', 'playcanvas'],
|
|
@@ -318,6 +324,19 @@ export class BaseBuilder {
|
|
|
318
324
|
outputDir: this.options.outputDir,
|
|
319
325
|
selectedScenes: this.options.selectedScenes,
|
|
320
326
|
}),
|
|
327
|
+
// 打包分析报告(如果启用)
|
|
328
|
+
...(this.options.analyze ? [
|
|
329
|
+
visualizer({
|
|
330
|
+
filename: this.options.analyzeReportPath
|
|
331
|
+
? path.join(this.options.outputDir, this.options.analyzeReportPath)
|
|
332
|
+
: path.join(this.options.outputDir, 'base-bundle-report.html'),
|
|
333
|
+
template: 'treemap',
|
|
334
|
+
gzipSize: true,
|
|
335
|
+
brotliSize: true,
|
|
336
|
+
open: false,
|
|
337
|
+
sourcemap: false,
|
|
338
|
+
}),
|
|
339
|
+
] : []),
|
|
321
340
|
],
|
|
322
341
|
};
|
|
323
342
|
// 2. 执行 Vite 构建
|
|
@@ -6,7 +6,6 @@ import viteImagemin from '@vheemstra/vite-plugin-imagemin';
|
|
|
6
6
|
import imageminMozjpeg from 'imagemin-mozjpeg';
|
|
7
7
|
import imageminPngquant from 'imagemin-pngquant';
|
|
8
8
|
import imageminWebp from 'imagemin-webp';
|
|
9
|
-
import { visualizer } from 'rollup-plugin-visualizer';
|
|
10
9
|
import { PLATFORM_CONFIGS } from './platform-configs.js';
|
|
11
10
|
import { vitePlayCanvasPlugin } from './plugin-playcanvas.js';
|
|
12
11
|
import { vitePlatformPlugin } from './plugin-platform.js';
|
|
@@ -163,19 +162,6 @@ export class ViteConfigBuilder {
|
|
|
163
162
|
removeViteModuleLoader: true,
|
|
164
163
|
}));
|
|
165
164
|
}
|
|
166
|
-
// 6. 打包分析报告
|
|
167
|
-
if (this.options.analyze) {
|
|
168
|
-
const reportPath = this.options.analyzeReportPath
|
|
169
|
-
? this.options.analyzeReportPath
|
|
170
|
-
: path.join(outputDir, 'bundle-report.html');
|
|
171
|
-
plugins.push(visualizer({
|
|
172
|
-
filename: reportPath,
|
|
173
|
-
template: 'treemap',
|
|
174
|
-
gzipSize: true,
|
|
175
|
-
brotliSize: true,
|
|
176
|
-
open: false,
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
179
165
|
return plugins.filter(Boolean);
|
|
180
166
|
}
|
|
181
167
|
getPlayableOptions(config) {
|
|
@@ -23,17 +23,21 @@ export function vitePlayCanvasPlugin(options) {
|
|
|
23
23
|
if (options.outputFormat === 'html') {
|
|
24
24
|
// 1. 内联 PlayCanvas Engine + 引擎补丁
|
|
25
25
|
html = await inlineEngineScript(html, options.baseBuildDir, options);
|
|
26
|
-
// 2.
|
|
26
|
+
// 2. 内联 __game-scripts.js(用户脚本)
|
|
27
|
+
if (options.inlineGameScripts) {
|
|
28
|
+
html = await inlineGameScripts(html, options.baseBuildDir);
|
|
29
|
+
}
|
|
30
|
+
// 3. 内联并转换 __settings__.js(传递插件上下文)
|
|
27
31
|
html = await inlineAndConvertSettings(html, options.baseBuildDir, options, pluginContext);
|
|
28
|
-
//
|
|
32
|
+
// 4. 内联 __modules__.js
|
|
29
33
|
html = await inlineModulesScript(html, options.baseBuildDir);
|
|
30
|
-
//
|
|
34
|
+
// 5. 内联 __start__.js
|
|
31
35
|
html = await inlineStartScript(html, options.baseBuildDir, options);
|
|
32
|
-
//
|
|
36
|
+
// 6. 内联 __loading__.js
|
|
33
37
|
html = await inlineLoadingScript(html, options.baseBuildDir);
|
|
34
|
-
//
|
|
38
|
+
// 7. 内联 CSS
|
|
35
39
|
html = await inlineCSS(html, options.baseBuildDir, options);
|
|
36
|
-
//
|
|
40
|
+
// 8. 处理 manifest.json
|
|
37
41
|
html = await inlineManifest(html, options.baseBuildDir, options);
|
|
38
42
|
}
|
|
39
43
|
return html;
|
|
@@ -375,6 +379,8 @@ async function inlineModulesScript(html, baseBuildDir) {
|
|
|
375
379
|
}
|
|
376
380
|
/**
|
|
377
381
|
* 内联 __game-scripts.js
|
|
382
|
+
* 注意:脚本需要在 pc.AppBase 创建后执行,所以插入到 </body> 之前
|
|
383
|
+
* 同时包装成一个函数,在 DOMContentLoaded 后执行,确保 PlayCanvas 引擎已完全初始化
|
|
378
384
|
*/
|
|
379
385
|
async function inlineGameScripts(html, baseBuildDir) {
|
|
380
386
|
const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
|
|
@@ -386,13 +392,36 @@ async function inlineGameScripts(html, baseBuildDir) {
|
|
|
386
392
|
return html;
|
|
387
393
|
}
|
|
388
394
|
const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
395
|
+
// 将脚本包装,确保在 pc.script 初始化后执行
|
|
396
|
+
// pc.createScript 需要 pc.script._scripts 已初始化
|
|
397
|
+
const wrappedCode = `
|
|
398
|
+
<script>
|
|
399
|
+
(function() {
|
|
400
|
+
// 等待 pc.script 准备好
|
|
401
|
+
function initGameScripts() {
|
|
402
|
+
if (typeof pc === 'undefined' || !pc.script) {
|
|
403
|
+
setTimeout(initGameScripts, 10);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
${gameScriptsCode}
|
|
407
|
+
}
|
|
408
|
+
// 延迟执行,确保引擎初始化完成
|
|
409
|
+
if (document.readyState === 'loading') {
|
|
410
|
+
document.addEventListener('DOMContentLoaded', initGameScripts);
|
|
411
|
+
} else {
|
|
412
|
+
setTimeout(initGameScripts, 0);
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
</script>`;
|
|
416
|
+
// 在 </body> 之前插入,确保在其他脚本之后执行
|
|
417
|
+
if (html.includes('</body>')) {
|
|
418
|
+
html = html.replace('</body>', `${wrappedCode}\n</body>`);
|
|
419
|
+
}
|
|
420
|
+
else if (html.includes('</html>')) {
|
|
421
|
+
html = html.replace('</html>', `${wrappedCode}\n</html>`);
|
|
392
422
|
}
|
|
393
423
|
else {
|
|
394
|
-
|
|
395
|
-
html = html.replace('<body>', `<body>\n<script>${gameScriptsCode}</script>\n`);
|
|
424
|
+
html += wrappedCode;
|
|
396
425
|
}
|
|
397
426
|
return html;
|
|
398
427
|
}
|
|
@@ -513,16 +542,44 @@ async function injectPhysicsLibrary(html, options) {
|
|
|
513
542
|
if (!library) {
|
|
514
543
|
return html;
|
|
515
544
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
545
|
+
let scriptsToInject = [];
|
|
546
|
+
if (library === 'p2') {
|
|
547
|
+
// Inject p2.js (2D physics)
|
|
548
|
+
const p2Code = await readPatchFile('p2.min.js');
|
|
549
|
+
if (p2Code) {
|
|
550
|
+
scriptsToInject.push(`<script>${p2Code}</script>`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (library === 'cannon') {
|
|
554
|
+
// Inject Cannon.js (3D physics) + adapter
|
|
555
|
+
// Get __dirname equivalent in ES modules
|
|
556
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
557
|
+
const __dirname = path.dirname(__filename);
|
|
558
|
+
const physicsDir = path.join(__dirname, '../../physics');
|
|
559
|
+
try {
|
|
560
|
+
// 1. Cannon.js engine
|
|
561
|
+
const cannonPath = path.join(physicsDir, 'cannon-es-bundle.js');
|
|
562
|
+
const cannonCode = await fs.readFile(cannonPath, 'utf-8');
|
|
563
|
+
scriptsToInject.push(`<script>${cannonCode}</script>`);
|
|
564
|
+
// 2. Rigidbody adapter (compatibility layer)
|
|
565
|
+
const adapterPath = path.join(physicsDir, 'cannon-rigidbody-adapter.js');
|
|
566
|
+
const adapterCode = await fs.readFile(adapterPath, 'utf-8');
|
|
567
|
+
scriptsToInject.push(`<script>${adapterCode}</script>`);
|
|
568
|
+
console.log('[PlayCanvas Plugin] Injected Cannon.js physics engine with rigidbody adapter');
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
console.warn('[PlayCanvas Plugin] Failed to load Cannon.js files:', error);
|
|
572
|
+
return html;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (scriptsToInject.length === 0) {
|
|
519
576
|
return html;
|
|
520
577
|
}
|
|
521
|
-
const
|
|
578
|
+
const injectedScripts = scriptsToInject.join('\n');
|
|
522
579
|
if (html.includes('</head>')) {
|
|
523
|
-
return html.replace('</head>', `${
|
|
580
|
+
return html.replace('</head>', `${injectedScripts}\n</head>`);
|
|
524
581
|
}
|
|
525
|
-
return `${
|
|
582
|
+
return `${injectedScripts}\n${html}`;
|
|
526
583
|
}
|
|
527
584
|
function isAmmoModule(module) {
|
|
528
585
|
const moduleName = module.moduleName?.toLowerCase() ?? '';
|
|
@@ -541,6 +598,7 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
541
598
|
const assets = configJson.assets;
|
|
542
599
|
const skippedAssets = [];
|
|
543
600
|
const optimizedAssets = [];
|
|
601
|
+
const skippedScripts = [];
|
|
544
602
|
const SIZE_LIMIT = 1 * 1024 * 1024; // 1MB - 跳过超过这个大小的文件
|
|
545
603
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
546
604
|
const file = asset?.file;
|
|
@@ -553,6 +611,13 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
553
611
|
}
|
|
554
612
|
const cleanUrl = url.split('?')[0];
|
|
555
613
|
const fileName = cleanUrl.toLowerCase();
|
|
614
|
+
// 跳过指向 __game-scripts.js 的脚本资源
|
|
615
|
+
// 这些资源已经被打包到 __game-scripts.js 中,不需要单独内联
|
|
616
|
+
// __game-scripts.js 会通过 transformIndexHtml 钩子内联到 HTML 中
|
|
617
|
+
if (cleanUrl === '__game-scripts.js' || asset.type === 'script') {
|
|
618
|
+
skippedScripts.push(asset.name || assetId);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
556
621
|
// 跳过物理引擎缓存文件和大型文本文件
|
|
557
622
|
if (fileName.endsWith('.pma.txt') ||
|
|
558
623
|
fileName.includes('browsermetrics') ||
|
|
@@ -627,6 +692,16 @@ async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
|
|
|
627
692
|
console.log(` ... 还有 ${skippedAssets.length - 10} 个文件`);
|
|
628
693
|
}
|
|
629
694
|
}
|
|
695
|
+
if (skippedScripts.length > 0) {
|
|
696
|
+
console.log(`\n📦 跳过了 ${skippedScripts.length} 个脚本资源(已打包到 __game-scripts.js)`);
|
|
697
|
+
if (skippedScripts.length <= 10) {
|
|
698
|
+
skippedScripts.forEach(name => console.log(` - ${name}`));
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
skippedScripts.slice(0, 5).forEach(name => console.log(` - ${name}`));
|
|
702
|
+
console.log(` ... 还有 ${skippedScripts.length - 5} 个脚本`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
630
705
|
return configJson;
|
|
631
706
|
}
|
|
632
707
|
async function inlineLogoInStartScript(startCode, baseBuildDir) {
|
|
@@ -830,11 +905,9 @@ async function buildCompressedEngineScript(engineCode) {
|
|
|
830
905
|
const base64 = Buffer.from(compressed).toString('base64');
|
|
831
906
|
// 遵循 PlayCanvas 官方 one-page.js 的实现方式
|
|
832
907
|
// 参考: https://github.com/playcanvas/playcanvas-rest-api-tools/blob/main/one-page.js
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
// 4. 插入到当前脚本之前(保持执行顺序)
|
|
837
|
-
const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.innerText=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
|
|
908
|
+
// 官方使用 new Buffer() 因为 lz4.js 提供了 Buffer polyfill
|
|
909
|
+
// 使用 textContent 而不是 innerText 以避免浏览器对内容的处理
|
|
910
|
+
const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.textContent=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
|
|
838
911
|
return `<script>${wrapper}</script>`;
|
|
839
912
|
}
|
|
840
913
|
async function loadLz4Module() {
|
|
@@ -95,7 +95,11 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
95
95
|
await fs.writeFile(path.join(options.outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
96
96
|
console.log('[SourceBuilder] 生成 config.json');
|
|
97
97
|
// 生成 __settings__.js
|
|
98
|
-
|
|
98
|
+
// 如果 config.scenes 中有场景,使用第一个场景的 URL
|
|
99
|
+
const firstScenePath = config.scenes && config.scenes.length > 0
|
|
100
|
+
? config.scenes[0].url
|
|
101
|
+
: undefined;
|
|
102
|
+
const settings = generateSettings(projectConfig, firstScenePath);
|
|
99
103
|
await fs.writeFile(path.join(options.outputDir, '__settings__.js'), settings, 'utf-8');
|
|
100
104
|
console.log('[SourceBuilder] 生成 __settings__.js');
|
|
101
105
|
// 复制模板文件
|
|
@@ -104,8 +108,8 @@ export function viteSourceBuilderPlugin(options) {
|
|
|
104
108
|
// 复制资源文件
|
|
105
109
|
await copyAssets(projectConfig, options.projectDir, options.outputDir, config.assets);
|
|
106
110
|
console.log('[SourceBuilder] 复制资源文件');
|
|
107
|
-
//
|
|
108
|
-
await generateSceneFiles(projectConfig, options.projectDir, options.outputDir);
|
|
111
|
+
// 生成场景文件(只生成 config 中包含的场景)
|
|
112
|
+
await generateSceneFiles(projectConfig, options.projectDir, options.outputDir, config.scenes);
|
|
109
113
|
console.log('[SourceBuilder] 生成场景文件');
|
|
110
114
|
},
|
|
111
115
|
};
|
|
@@ -275,12 +279,18 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
275
279
|
const assets = assetsOverride || (projectConfig.format === 'playcanvas'
|
|
276
280
|
? projectConfig.assets
|
|
277
281
|
: projectConfig.assets);
|
|
282
|
+
// 使用 Set 去重,避免重复复制同一个文件
|
|
283
|
+
const copiedUrls = new Set();
|
|
278
284
|
// 遍历 assets
|
|
279
285
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
280
286
|
const assetData = asset;
|
|
281
287
|
if (!assetData.file?.url || assetData.file.url.startsWith('data:')) {
|
|
282
288
|
continue;
|
|
283
289
|
}
|
|
290
|
+
// 跳过已经复制过的 URL
|
|
291
|
+
if (copiedUrls.has(assetData.file.url)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
284
294
|
const sourcePath = path.join(projectDir, assetData.file.url);
|
|
285
295
|
const targetPath = path.join(outputDir, assetData.file.url);
|
|
286
296
|
try {
|
|
@@ -291,6 +301,8 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
291
301
|
await fs.mkdir(targetDir, { recursive: true });
|
|
292
302
|
// 复制文件
|
|
293
303
|
await fs.copyFile(sourcePath, targetPath);
|
|
304
|
+
// 标记为已复制
|
|
305
|
+
copiedUrls.add(assetData.file.url);
|
|
294
306
|
}
|
|
295
307
|
catch (error) {
|
|
296
308
|
// 资源文件可能不存在(可能是内联的)
|
|
@@ -301,14 +313,23 @@ async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride)
|
|
|
301
313
|
/**
|
|
302
314
|
* 生成场景文件
|
|
303
315
|
*/
|
|
304
|
-
async function generateSceneFiles(projectConfig, projectDir, outputDir) {
|
|
316
|
+
async function generateSceneFiles(projectConfig, projectDir, outputDir, selectedScenes) {
|
|
305
317
|
if (projectConfig.format === 'playcanvas') {
|
|
306
318
|
const pcProject = projectConfig;
|
|
307
319
|
// PlayCanvas 格式:scenes.json 中已包含场景数据
|
|
308
320
|
if (pcProject.scenes) {
|
|
309
|
-
|
|
321
|
+
let scenes = Array.isArray(pcProject.scenes)
|
|
310
322
|
? pcProject.scenes
|
|
311
323
|
: Object.entries(pcProject.scenes).map(([id, scene]) => ({ id, ...scene }));
|
|
324
|
+
// 如果提供了 selectedScenes,只生成这些场景
|
|
325
|
+
if (selectedScenes && selectedScenes.length > 0) {
|
|
326
|
+
const selectedUrls = new Set(selectedScenes.map(s => s.url));
|
|
327
|
+
scenes = scenes.filter((scene) => {
|
|
328
|
+
const sceneId = scene.id || scene.name || 'scene';
|
|
329
|
+
const sceneUrl = `${sceneId}.json`;
|
|
330
|
+
return selectedUrls.has(sceneUrl);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
312
333
|
for (const scene of scenes) {
|
|
313
334
|
const sceneData = scene;
|
|
314
335
|
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
@@ -321,7 +342,16 @@ async function generateSceneFiles(projectConfig, projectDir, outputDir) {
|
|
|
321
342
|
else {
|
|
322
343
|
// PlayCraft 格式:从 scenes/ 目录读取
|
|
323
344
|
const pcProject = projectConfig;
|
|
324
|
-
|
|
345
|
+
let scenes = pcProject.scenes;
|
|
346
|
+
// 如果提供了 selectedScenes,只生成这些场景
|
|
347
|
+
if (selectedScenes && selectedScenes.length > 0) {
|
|
348
|
+
const selectedNames = new Set(selectedScenes.map(s => s.name));
|
|
349
|
+
scenes = scenes.filter((scene) => {
|
|
350
|
+
const sceneName = scene.name || scene.id || 'scene';
|
|
351
|
+
return selectedNames.has(sceneName);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const scene of scenes) {
|
|
325
355
|
const sceneData = scene;
|
|
326
356
|
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
327
357
|
const scenePath = path.join(outputDir, `${sceneId}.json`);
|
package/package.json
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
pc.ScriptHandler.prototype._loadScript = function (url, callback) {
|
|
3
|
+
// 如果 URL 不是 data URL(如 __game-scripts.js),则跳过加载
|
|
4
|
+
// 因为脚本已经在 HTML 中作为 <script> 标签执行过了
|
|
5
|
+
if (!url.startsWith('data:')) {
|
|
6
|
+
// 直接调用回调,不创建新的脚本元素
|
|
7
|
+
callback(null, url, null);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
var head = document.head;
|
|
4
12
|
var element = document.createElement('script');
|
|
5
13
|
this._cache[url] = element;
|