@playcraft/cli 0.0.1
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 +12 -0
- package/dist/build-config.js +26 -0
- package/dist/commands/build.js +363 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/init.js +86 -0
- package/dist/commands/inspect.js +209 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/start.js +284 -0
- package/dist/commands/status.js +106 -0
- package/dist/commands/stop.js +58 -0
- package/dist/config.js +31 -0
- package/dist/fs-handler.js +83 -0
- package/dist/index.js +200 -0
- package/dist/logger.js +122 -0
- package/dist/playable/base-builder.js +265 -0
- package/dist/playable/builder.js +1462 -0
- package/dist/playable/converter.js +150 -0
- package/dist/playable/index.js +3 -0
- package/dist/playable/platforms/base.js +12 -0
- package/dist/playable/platforms/facebook.js +37 -0
- package/dist/playable/platforms/index.js +24 -0
- package/dist/playable/platforms/snapchat.js +59 -0
- package/dist/playable/playable-builder.js +521 -0
- package/dist/playable/types.js +1 -0
- package/dist/playable/vite/config-builder.js +136 -0
- package/dist/playable/vite/platform-configs.js +102 -0
- package/dist/playable/vite/plugin-model-compression.js +63 -0
- package/dist/playable/vite/plugin-platform.js +65 -0
- package/dist/playable/vite/plugin-playcanvas.js +454 -0
- package/dist/playable/vite-builder.js +125 -0
- package/dist/port-utils.js +27 -0
- package/dist/process-manager.js +96 -0
- package/dist/server.js +128 -0
- package/dist/socket.js +117 -0
- package/dist/watcher.js +33 -0
- package/package.json +41 -0
- package/templates/playable-ad.html +59 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台配置映射
|
|
3
|
+
*/
|
|
4
|
+
export const PLATFORM_CONFIGS = {
|
|
5
|
+
facebook: {
|
|
6
|
+
sizeLimit: 2 * 1024 * 1024, // 2MB
|
|
7
|
+
outputFormat: 'html',
|
|
8
|
+
minifyCSS: true,
|
|
9
|
+
minifyJS: true,
|
|
10
|
+
compressImages: true,
|
|
11
|
+
compressModels: true,
|
|
12
|
+
injectScripts: ['fbPlayableAd'],
|
|
13
|
+
outputFileName: 'index.html',
|
|
14
|
+
includeSourcemap: false,
|
|
15
|
+
imageQuality: {
|
|
16
|
+
jpg: 75,
|
|
17
|
+
png: [0.7, 0.8],
|
|
18
|
+
webp: 75,
|
|
19
|
+
},
|
|
20
|
+
modelCompression: {
|
|
21
|
+
method: 'draco',
|
|
22
|
+
quality: 0.8,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
snapchat: {
|
|
26
|
+
sizeLimit: 5 * 1024 * 1024, // 5MB
|
|
27
|
+
outputFormat: 'zip',
|
|
28
|
+
minifyCSS: true,
|
|
29
|
+
minifyJS: true,
|
|
30
|
+
compressImages: true,
|
|
31
|
+
compressModels: true,
|
|
32
|
+
injectScripts: ['mraid', 'snapchatCta'],
|
|
33
|
+
outputFileName: 'ad.html',
|
|
34
|
+
includeSourcemap: false,
|
|
35
|
+
imageQuality: {
|
|
36
|
+
jpg: 80,
|
|
37
|
+
png: [0.75, 0.85],
|
|
38
|
+
webp: 80,
|
|
39
|
+
},
|
|
40
|
+
modelCompression: {
|
|
41
|
+
method: 'draco',
|
|
42
|
+
quality: 0.85,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
ironsource: {
|
|
46
|
+
sizeLimit: 3 * 1024 * 1024, // 3MB
|
|
47
|
+
outputFormat: 'html',
|
|
48
|
+
minifyCSS: true,
|
|
49
|
+
minifyJS: true,
|
|
50
|
+
compressImages: true,
|
|
51
|
+
compressModels: true,
|
|
52
|
+
outputFileName: 'index.html',
|
|
53
|
+
includeSourcemap: false,
|
|
54
|
+
imageQuality: {
|
|
55
|
+
jpg: 75,
|
|
56
|
+
png: [0.7, 0.8],
|
|
57
|
+
webp: 75,
|
|
58
|
+
},
|
|
59
|
+
modelCompression: {
|
|
60
|
+
method: 'draco',
|
|
61
|
+
quality: 0.8,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
applovin: {
|
|
65
|
+
sizeLimit: 3 * 1024 * 1024, // 3MB
|
|
66
|
+
outputFormat: 'html',
|
|
67
|
+
minifyCSS: true,
|
|
68
|
+
minifyJS: true,
|
|
69
|
+
compressImages: true,
|
|
70
|
+
compressModels: true,
|
|
71
|
+
outputFileName: 'index.html',
|
|
72
|
+
includeSourcemap: false,
|
|
73
|
+
imageQuality: {
|
|
74
|
+
jpg: 75,
|
|
75
|
+
png: [0.7, 0.8],
|
|
76
|
+
webp: 75,
|
|
77
|
+
},
|
|
78
|
+
modelCompression: {
|
|
79
|
+
method: 'draco',
|
|
80
|
+
quality: 0.8,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
google: {
|
|
84
|
+
sizeLimit: 2 * 1024 * 1024, // 2MB
|
|
85
|
+
outputFormat: 'html',
|
|
86
|
+
minifyCSS: true,
|
|
87
|
+
minifyJS: true,
|
|
88
|
+
compressImages: true,
|
|
89
|
+
compressModels: true,
|
|
90
|
+
outputFileName: 'index.html',
|
|
91
|
+
includeSourcemap: false,
|
|
92
|
+
imageQuality: {
|
|
93
|
+
jpg: 75,
|
|
94
|
+
png: [0.7, 0.8],
|
|
95
|
+
webp: 75,
|
|
96
|
+
},
|
|
97
|
+
modelCompression: {
|
|
98
|
+
method: 'draco',
|
|
99
|
+
quality: 0.8,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NodeIO } from '@gltf-transform/core';
|
|
2
|
+
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
|
3
|
+
import { draco, textureCompress } from '@gltf-transform/functions';
|
|
4
|
+
import draco3d from 'draco3dgltf';
|
|
5
|
+
/**
|
|
6
|
+
* 3D 模型压缩 Vite 插件
|
|
7
|
+
* 使用 gltf-transform 压缩 GLB/GLTF 模型
|
|
8
|
+
*/
|
|
9
|
+
export function viteModelCompressionPlugin(options = {}) {
|
|
10
|
+
const { quality = 0.8, method = 'draco', enabled = true } = options;
|
|
11
|
+
if (!enabled) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'vite-plugin-model-compression',
|
|
14
|
+
// 返回空插件
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
name: 'vite-plugin-model-compression',
|
|
19
|
+
async transform(code, id) {
|
|
20
|
+
// 只处理 GLB/GLTF 文件
|
|
21
|
+
if (!id.match(/\.(glb|gltf)$/i)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
// 初始化 gltf-transform
|
|
26
|
+
const io = new NodeIO()
|
|
27
|
+
.registerExtensions(ALL_EXTENSIONS)
|
|
28
|
+
.registerDependencies({
|
|
29
|
+
'draco3d.decoder': await draco3d.createDecoderModule(),
|
|
30
|
+
'draco3d.encoder': await draco3d.createEncoderModule(),
|
|
31
|
+
});
|
|
32
|
+
// 读取模型
|
|
33
|
+
const document = await io.read(id);
|
|
34
|
+
// 应用压缩
|
|
35
|
+
if (method === 'draco') {
|
|
36
|
+
await document.transform(draco({
|
|
37
|
+
quantizePosition: 14,
|
|
38
|
+
quantizeNormal: 10,
|
|
39
|
+
quantizeTexcoord: 12,
|
|
40
|
+
quantizeColor: 8,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
// 纹理压缩
|
|
44
|
+
await document.transform(textureCompress({
|
|
45
|
+
targetFormat: 'webp',
|
|
46
|
+
quality: quality,
|
|
47
|
+
}));
|
|
48
|
+
// 转换为 Base64 data URL
|
|
49
|
+
const glb = await io.writeBinary(document);
|
|
50
|
+
const base64 = Buffer.from(glb).toString('base64');
|
|
51
|
+
return {
|
|
52
|
+
code: `export default "data:model/gltf-binary;base64,${base64}"`,
|
|
53
|
+
map: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.warn(`警告: 无法压缩模型文件 ${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
// 如果压缩失败,返回原始内容
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import archiver from 'archiver';
|
|
4
|
+
import { createWriteStream } from 'fs';
|
|
5
|
+
import { PLATFORM_CONFIGS } from './platform-configs.js';
|
|
6
|
+
/**
|
|
7
|
+
* 平台 Vite 插件
|
|
8
|
+
* 注入平台特定代码和处理输出格式
|
|
9
|
+
*/
|
|
10
|
+
export function vitePlatformPlugin(options) {
|
|
11
|
+
return {
|
|
12
|
+
name: 'vite-plugin-platform',
|
|
13
|
+
enforce: 'post',
|
|
14
|
+
transformIndexHtml: {
|
|
15
|
+
order: 'post',
|
|
16
|
+
handler(html) {
|
|
17
|
+
// 使用平台适配器修改 HTML
|
|
18
|
+
return options.adapter.modifyHTML(html, []);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
closeBundle() {
|
|
22
|
+
// 如果需要 ZIP 格式,在构建完成后打包
|
|
23
|
+
const platformConfig = PLATFORM_CONFIGS[options.platform];
|
|
24
|
+
if (platformConfig.outputFormat === 'zip') {
|
|
25
|
+
// 使用 Promise 处理异步操作
|
|
26
|
+
createZipOutput(options.outputDir, platformConfig.outputFileName).catch((err) => {
|
|
27
|
+
console.error('创建 ZIP 文件失败:', err);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 创建 ZIP 输出
|
|
35
|
+
*/
|
|
36
|
+
function createZipOutput(outDir, htmlFileName) {
|
|
37
|
+
return new Promise(async (resolve, reject) => {
|
|
38
|
+
const zipPath = path.join(outDir, 'playable.zip');
|
|
39
|
+
const output = createWriteStream(zipPath);
|
|
40
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
41
|
+
output.on('close', () => {
|
|
42
|
+
console.log(`✅ ZIP 文件已创建: ${zipPath} (${archive.pointer()} bytes)`);
|
|
43
|
+
resolve();
|
|
44
|
+
});
|
|
45
|
+
archive.on('error', (err) => {
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
archive.pipe(output);
|
|
49
|
+
// 添加 HTML 文件
|
|
50
|
+
const htmlPath = path.join(outDir, htmlFileName);
|
|
51
|
+
archive.file(htmlPath, { name: htmlFileName });
|
|
52
|
+
// 添加其他资源文件(如果有)
|
|
53
|
+
const filesDir = path.join(outDir, 'files');
|
|
54
|
+
try {
|
|
55
|
+
const filesDirStat = await fs.stat(filesDir);
|
|
56
|
+
if (filesDirStat.isDirectory()) {
|
|
57
|
+
archive.directory(filesDir, 'files');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// files 目录可能不存在
|
|
62
|
+
}
|
|
63
|
+
archive.finalize();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* PlayCanvas Vite 插件
|
|
5
|
+
* 处理 PlayCanvas 特定的资源转换和内联
|
|
6
|
+
*/
|
|
7
|
+
export function vitePlayCanvasPlugin(options) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'vite-plugin-playcanvas',
|
|
10
|
+
enforce: 'pre',
|
|
11
|
+
async transformIndexHtml(html) {
|
|
12
|
+
// 1. 内联 PlayCanvas Engine
|
|
13
|
+
html = await inlineEngineScript(html, options.baseBuildDir);
|
|
14
|
+
// 2. 内联并转换 __settings__.js
|
|
15
|
+
html = await inlineAndConvertSettings(html, options.baseBuildDir);
|
|
16
|
+
// 3. 内联 __modules__.js
|
|
17
|
+
html = await inlineModulesScript(html, options.baseBuildDir);
|
|
18
|
+
// 4. 内联 __start__.js
|
|
19
|
+
html = await inlineStartScript(html, options.baseBuildDir);
|
|
20
|
+
// 5. 内联 __loading__.js
|
|
21
|
+
html = await inlineLoadingScript(html, options.baseBuildDir);
|
|
22
|
+
// 6. 内联 __game-scripts__.js
|
|
23
|
+
html = await inlineGameScripts(html, options.baseBuildDir);
|
|
24
|
+
// 7. 内联 CSS
|
|
25
|
+
html = await inlineCSS(html, options.baseBuildDir);
|
|
26
|
+
// 8. 内联 manifest.json
|
|
27
|
+
html = await inlineManifest(html, options.baseBuildDir);
|
|
28
|
+
return html;
|
|
29
|
+
},
|
|
30
|
+
async transform(code, id) {
|
|
31
|
+
// 转换 __settings__.js 中的资源路径为 data URLs
|
|
32
|
+
if (id.endsWith('__settings__.js')) {
|
|
33
|
+
return await convertSettingsToDataUrls(code, options.baseBuildDir);
|
|
34
|
+
}
|
|
35
|
+
return code;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 内联 PlayCanvas Engine
|
|
41
|
+
*/
|
|
42
|
+
async function inlineEngineScript(html, baseBuildDir) {
|
|
43
|
+
const engineNames = [
|
|
44
|
+
'playcanvas-stable.min.js',
|
|
45
|
+
'playcanvas.min.js',
|
|
46
|
+
'__lib__.js',
|
|
47
|
+
];
|
|
48
|
+
for (const engineName of engineNames) {
|
|
49
|
+
const enginePath = path.join(baseBuildDir, engineName);
|
|
50
|
+
try {
|
|
51
|
+
await fs.access(enginePath);
|
|
52
|
+
const engineCode = await fs.readFile(enginePath, 'utf-8');
|
|
53
|
+
// 替换 script 标签
|
|
54
|
+
const scriptPattern = new RegExp(`<script[^>]*src=["']${engineName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*></script>`, 'i');
|
|
55
|
+
html = html.replace(scriptPattern, `<script>${engineCode}</script>`);
|
|
56
|
+
return html;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// 继续尝试下一个
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return html;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 内联并转换 __settings__.js
|
|
66
|
+
*/
|
|
67
|
+
async function inlineAndConvertSettings(html, baseBuildDir) {
|
|
68
|
+
const settingsPath = path.join(baseBuildDir, '__settings__.js');
|
|
69
|
+
try {
|
|
70
|
+
await fs.access(settingsPath);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// __settings__.js 不存在,尝试从 config.json 生成
|
|
74
|
+
return await generateAndInlineSettings(html, baseBuildDir);
|
|
75
|
+
}
|
|
76
|
+
let settingsCode = await fs.readFile(settingsPath, 'utf-8');
|
|
77
|
+
// 转换资源URL为data URLs
|
|
78
|
+
settingsCode = await convertSettingsToDataUrls(settingsCode, baseBuildDir);
|
|
79
|
+
// 替换 script 标签
|
|
80
|
+
const scriptPattern = /<script[^>]*src=["']__settings__\.js["'][^>]*><\/script>/i;
|
|
81
|
+
html = html.replace(scriptPattern, `<script>${settingsCode}</script>`);
|
|
82
|
+
return html;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 生成并内联 settings(如果 __settings__.js 不存在)
|
|
86
|
+
*/
|
|
87
|
+
async function generateAndInlineSettings(html, baseBuildDir) {
|
|
88
|
+
const configPath = path.join(baseBuildDir, 'config.json');
|
|
89
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
90
|
+
const configJson = JSON.parse(configContent);
|
|
91
|
+
// 生成 config data URL
|
|
92
|
+
const configDataUrl = `data:application/json;base64,${Buffer.from(configContent).toString('base64')}`;
|
|
93
|
+
// 生成 scene data URL
|
|
94
|
+
let sceneDataUrl = '';
|
|
95
|
+
if (configJson.scenes && configJson.scenes.length > 0) {
|
|
96
|
+
const sceneUrl = configJson.scenes[0].url;
|
|
97
|
+
if (sceneUrl && !sceneUrl.startsWith('data:')) {
|
|
98
|
+
const scenePath = path.join(baseBuildDir, sceneUrl);
|
|
99
|
+
try {
|
|
100
|
+
const sceneContent = await fs.readFile(scenePath, 'utf-8');
|
|
101
|
+
sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.warn(`警告: 场景文件不存在: ${sceneUrl}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
sceneDataUrl = sceneUrl;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const appProps = configJson.application_properties || {};
|
|
112
|
+
const scripts = appProps.scripts || [];
|
|
113
|
+
const preloadModules = extractPreloadModules(configJson);
|
|
114
|
+
const settingsCode = `
|
|
115
|
+
window.ASSET_PREFIX = "";
|
|
116
|
+
window.SCRIPT_PREFIX = "";
|
|
117
|
+
window.SCENE_PATH = "${sceneDataUrl}";
|
|
118
|
+
window.CONTEXT_OPTIONS = {
|
|
119
|
+
'antialias': ${appProps.antiAlias !== false},
|
|
120
|
+
'alpha': ${appProps.transparentCanvas === true},
|
|
121
|
+
'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
|
|
122
|
+
'deviceTypes': ['webgl2', 'webgl1'],
|
|
123
|
+
'powerPreference': "default"
|
|
124
|
+
};
|
|
125
|
+
window.SCRIPTS = [${scripts.join(', ')}];
|
|
126
|
+
window.CONFIG_FILENAME = "${configDataUrl}";
|
|
127
|
+
window.INPUT_SETTINGS = {
|
|
128
|
+
useKeyboard: ${appProps.useKeyboard !== false},
|
|
129
|
+
useMouse: ${appProps.useMouse !== false},
|
|
130
|
+
useGamepads: ${appProps.useGamepads === true},
|
|
131
|
+
useTouch: ${appProps.useTouch !== false}
|
|
132
|
+
};
|
|
133
|
+
pc.script.legacy = ${appProps.useLegacyScripts === true};
|
|
134
|
+
window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
|
|
135
|
+
`;
|
|
136
|
+
// 在 </head> 之前插入
|
|
137
|
+
html = html.replace('</head>', `<script>${settingsCode}</script>\n</head>`);
|
|
138
|
+
return html;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 转换 settings 代码中的资源URL为 data URLs
|
|
142
|
+
*/
|
|
143
|
+
async function convertSettingsToDataUrls(settingsCode, baseBuildDir) {
|
|
144
|
+
// 1. 转换 config.json
|
|
145
|
+
settingsCode = await convertConfigUrl(settingsCode, baseBuildDir);
|
|
146
|
+
// 2. 转换场景文件
|
|
147
|
+
settingsCode = await convertSceneUrl(settingsCode, baseBuildDir);
|
|
148
|
+
// 3. 转换 PRELOAD_MODULES(优先使用 JS fallback)
|
|
149
|
+
settingsCode = await convertPreloadModules(settingsCode, baseBuildDir);
|
|
150
|
+
return settingsCode;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 转换 CONFIG_FILENAME 为 data URL
|
|
154
|
+
*/
|
|
155
|
+
async function convertConfigUrl(settingsCode, baseBuildDir) {
|
|
156
|
+
const configMatch = settingsCode.match(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)"/);
|
|
157
|
+
if (!configMatch) {
|
|
158
|
+
return settingsCode;
|
|
159
|
+
}
|
|
160
|
+
const configPath = configMatch[1];
|
|
161
|
+
if (configPath.startsWith('data:')) {
|
|
162
|
+
return settingsCode; // 已经是 data URL
|
|
163
|
+
}
|
|
164
|
+
const fullConfigPath = path.join(baseBuildDir, configPath);
|
|
165
|
+
try {
|
|
166
|
+
const configContent = await fs.readFile(fullConfigPath, 'utf-8');
|
|
167
|
+
const configDataUrl = `data:application/json;base64,${Buffer.from(configContent).toString('base64')}`;
|
|
168
|
+
return settingsCode.replace(/window\.CONFIG_FILENAME\s*=\s*"[^"]+"/, `window.CONFIG_FILENAME = "${configDataUrl}"`);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.warn(`警告: 无法读取配置文件: ${configPath}`);
|
|
172
|
+
return settingsCode;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 转换 SCENE_PATH 为 data URL
|
|
177
|
+
*/
|
|
178
|
+
async function convertSceneUrl(settingsCode, baseBuildDir) {
|
|
179
|
+
const sceneMatch = settingsCode.match(/window\.SCENE_PATH\s*=\s*"([^"]+)"/);
|
|
180
|
+
if (!sceneMatch) {
|
|
181
|
+
return settingsCode;
|
|
182
|
+
}
|
|
183
|
+
const scenePath = sceneMatch[1];
|
|
184
|
+
if (scenePath.startsWith('data:') || !scenePath) {
|
|
185
|
+
return settingsCode; // 已经是 data URL 或为空
|
|
186
|
+
}
|
|
187
|
+
const fullScenePath = path.join(baseBuildDir, scenePath);
|
|
188
|
+
try {
|
|
189
|
+
const sceneContent = await fs.readFile(fullScenePath, 'utf-8');
|
|
190
|
+
const sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
|
|
191
|
+
return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.warn(`警告: 无法读取场景文件: ${scenePath}`);
|
|
195
|
+
return settingsCode;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 转换 PRELOAD_MODULES 中的资源URL
|
|
200
|
+
* 对于 Playable Ads,优先使用 fallback JS 版本(跳过 WASM)
|
|
201
|
+
*/
|
|
202
|
+
async function convertPreloadModules(settingsCode, baseBuildDir) {
|
|
203
|
+
// 匹配 PRELOAD_MODULES 数组(支持单引号和双引号)
|
|
204
|
+
const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
|
|
205
|
+
if (!modulesMatch) {
|
|
206
|
+
return settingsCode;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
// 使用 Function 构造函数安全地解析 JavaScript 对象字面量
|
|
210
|
+
const modulesStr = modulesMatch[1];
|
|
211
|
+
const modules = new Function(`return ${modulesStr}`)();
|
|
212
|
+
// 转换每个模块的URL为data URL
|
|
213
|
+
for (const module of modules) {
|
|
214
|
+
// 对于 Playable Ads,优先使用 fallback JS 版本,跳过 WASM
|
|
215
|
+
if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
|
|
216
|
+
const fallbackPath = path.join(baseBuildDir, module.fallbackUrl);
|
|
217
|
+
try {
|
|
218
|
+
const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
|
|
219
|
+
module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
|
|
220
|
+
// 清空 WASM 相关 URL,强制使用 fallback
|
|
221
|
+
module.glueUrl = '';
|
|
222
|
+
module.wasmUrl = '';
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.warn(`警告: 无法读取 fallback 文件: ${module.fallbackUrl}`);
|
|
226
|
+
// 如果 fallback 读取失败,尝试读取 WASM 相关文件
|
|
227
|
+
if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
|
|
228
|
+
const gluePath = path.join(baseBuildDir, module.glueUrl);
|
|
229
|
+
try {
|
|
230
|
+
const glueCode = await fs.readFile(gluePath, 'utf-8');
|
|
231
|
+
module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
|
|
238
|
+
const wasmPath = path.join(baseBuildDir, module.wasmUrl);
|
|
239
|
+
try {
|
|
240
|
+
const wasmBinary = await fs.readFile(wasmPath);
|
|
241
|
+
module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// 如果没有 fallback,尝试转换 WASM 相关文件
|
|
251
|
+
if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
|
|
252
|
+
const gluePath = path.join(baseBuildDir, module.glueUrl);
|
|
253
|
+
try {
|
|
254
|
+
const glueCode = await fs.readFile(gluePath, 'utf-8');
|
|
255
|
+
module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
|
|
262
|
+
const wasmPath = path.join(baseBuildDir, module.wasmUrl);
|
|
263
|
+
try {
|
|
264
|
+
const wasmBinary = await fs.readFile(wasmPath);
|
|
265
|
+
module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// 重新生成 PRELOAD_MODULES 配置(使用双引号 JSON 格式)
|
|
274
|
+
const newModulesStr = JSON.stringify(modules, null, 4);
|
|
275
|
+
return settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
console.warn(`警告: 无法解析 PRELOAD_MODULES: ${error instanceof Error ? error.message : String(error)}`);
|
|
279
|
+
return settingsCode;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 从 config.json 提取 PRELOAD_MODULES
|
|
284
|
+
*/
|
|
285
|
+
function extractPreloadModules(configJson) {
|
|
286
|
+
const modules = [];
|
|
287
|
+
if (configJson.assets) {
|
|
288
|
+
for (const [id, asset] of Object.entries(configJson.assets)) {
|
|
289
|
+
const assetData = asset;
|
|
290
|
+
if (assetData.type === 'wasm' && assetData.data) {
|
|
291
|
+
const moduleData = assetData.data;
|
|
292
|
+
const fallbackAssetId = moduleData.fallbackScriptId;
|
|
293
|
+
let fallbackUrl = '';
|
|
294
|
+
if (fallbackAssetId && configJson.assets[fallbackAssetId]) {
|
|
295
|
+
const fallbackAsset = configJson.assets[fallbackAssetId];
|
|
296
|
+
fallbackUrl = fallbackAsset.file?.url || '';
|
|
297
|
+
}
|
|
298
|
+
if (moduleData.moduleName && fallbackUrl) {
|
|
299
|
+
modules.push({
|
|
300
|
+
moduleName: moduleData.moduleName,
|
|
301
|
+
glueUrl: '',
|
|
302
|
+
wasmUrl: '',
|
|
303
|
+
fallbackUrl: fallbackUrl,
|
|
304
|
+
preload: true,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return modules;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* 内联 __modules__.js
|
|
314
|
+
*/
|
|
315
|
+
async function inlineModulesScript(html, baseBuildDir) {
|
|
316
|
+
const modulesPath = path.join(baseBuildDir, '__modules__.js');
|
|
317
|
+
try {
|
|
318
|
+
await fs.access(modulesPath);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
// __modules__.js 不存在,跳过
|
|
322
|
+
return html;
|
|
323
|
+
}
|
|
324
|
+
const modulesCode = await fs.readFile(modulesPath, 'utf-8');
|
|
325
|
+
// 替换 script 标签
|
|
326
|
+
const scriptPattern = /<script[^>]*src=["']__modules__\.js["'][^>]*><\/script>/i;
|
|
327
|
+
html = html.replace(scriptPattern, `<script>${modulesCode}</script>`);
|
|
328
|
+
// 如果 HTML 中没有找到 script 标签,在 <body> 开头添加
|
|
329
|
+
if (!scriptPattern.test(html)) {
|
|
330
|
+
html = html.replace('<body>', `<body>\n<script>${modulesCode}</script>\n`);
|
|
331
|
+
}
|
|
332
|
+
return html;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 内联 __game-scripts.js
|
|
336
|
+
*/
|
|
337
|
+
async function inlineGameScripts(html, baseBuildDir) {
|
|
338
|
+
const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
|
|
339
|
+
try {
|
|
340
|
+
await fs.access(gameScriptsPath);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
// __game-scripts.js 不存在,跳过
|
|
344
|
+
return html;
|
|
345
|
+
}
|
|
346
|
+
const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
|
|
347
|
+
// 在 </head> 之前或第一个 <script> 标签之后插入游戏脚本
|
|
348
|
+
if (html.includes('</head>')) {
|
|
349
|
+
html = html.replace('</head>', `<script>${gameScriptsCode}</script>\n</head>`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// 如果没有 </head>,在第一个 <body> 标签之后插入
|
|
353
|
+
html = html.replace('<body>', `<body>\n<script>${gameScriptsCode}</script>\n`);
|
|
354
|
+
}
|
|
355
|
+
return html;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 内联 __start__.js
|
|
359
|
+
*/
|
|
360
|
+
async function inlineStartScript(html, baseBuildDir) {
|
|
361
|
+
const startPath = path.join(baseBuildDir, '__start__.js');
|
|
362
|
+
const startCode = await fs.readFile(startPath, 'utf-8');
|
|
363
|
+
// 替换 script 标签
|
|
364
|
+
const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
|
|
365
|
+
if (scriptPattern.test(html)) {
|
|
366
|
+
html = html.replace(scriptPattern, `<script>${startCode}</script>`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// 如果 HTML 中没有找到 script 标签,在 </body> 之前插入
|
|
370
|
+
html = html.replace('</body>', `<script>${startCode}</script>\n</body>`);
|
|
371
|
+
}
|
|
372
|
+
return html;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 内联 __loading__.js
|
|
376
|
+
*/
|
|
377
|
+
async function inlineLoadingScript(html, baseBuildDir) {
|
|
378
|
+
const loadingPath = path.join(baseBuildDir, '__loading__.js');
|
|
379
|
+
const scriptPattern = /<script[^>]*src=["']__loading__\.js["'][^>]*><\/script>/i;
|
|
380
|
+
try {
|
|
381
|
+
await fs.access(loadingPath);
|
|
382
|
+
// 文件存在,内联它
|
|
383
|
+
const loadingCode = await fs.readFile(loadingPath, 'utf-8');
|
|
384
|
+
if (scriptPattern.test(html)) {
|
|
385
|
+
html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// 如果 HTML 中没有找到 script 标签,在 </body> 之前添加
|
|
389
|
+
html = html.replace('</body>', `<script>${loadingCode}</script>\n</body>`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
// __loading__.js 不存在,移除 script 标签
|
|
394
|
+
if (scriptPattern.test(html)) {
|
|
395
|
+
html = html.replace(scriptPattern, '');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return html;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 内联 CSS 文件
|
|
402
|
+
*/
|
|
403
|
+
async function inlineCSS(html, baseBuildDir) {
|
|
404
|
+
// 匹配 <link rel="stylesheet" href="...">
|
|
405
|
+
const cssPattern = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
|
|
406
|
+
const matches = Array.from(html.matchAll(cssPattern));
|
|
407
|
+
for (const match of matches) {
|
|
408
|
+
const cssPath = match[1];
|
|
409
|
+
if (cssPath.startsWith('data:') || cssPath.startsWith('http://') || cssPath.startsWith('https://')) {
|
|
410
|
+
continue; // 跳过已经是 data URL 或外部 URL
|
|
411
|
+
}
|
|
412
|
+
const fullCssPath = path.join(baseBuildDir, cssPath);
|
|
413
|
+
try {
|
|
414
|
+
await fs.access(fullCssPath);
|
|
415
|
+
const cssContent = await fs.readFile(fullCssPath, 'utf-8');
|
|
416
|
+
// 替换 link 标签为 style 标签
|
|
417
|
+
html = html.replace(match[0], `<style>${cssContent}</style>`);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
console.warn(`警告: CSS 文件不存在: ${cssPath},移除引用`);
|
|
421
|
+
// 移除不存在的 CSS 引用
|
|
422
|
+
html = html.replace(match[0], '');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return html;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 内联 manifest.json
|
|
429
|
+
*/
|
|
430
|
+
async function inlineManifest(html, baseBuildDir) {
|
|
431
|
+
// 匹配 <link rel="manifest" href="...">
|
|
432
|
+
const manifestPattern = /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
|
|
433
|
+
const matches = Array.from(html.matchAll(manifestPattern));
|
|
434
|
+
for (const match of matches) {
|
|
435
|
+
const manifestPath = match[1];
|
|
436
|
+
if (manifestPath.startsWith('data:') || manifestPath.startsWith('http://') || manifestPath.startsWith('https://')) {
|
|
437
|
+
continue; // 跳过已经是 data URL 或外部 URL
|
|
438
|
+
}
|
|
439
|
+
const fullManifestPath = path.join(baseBuildDir, manifestPath);
|
|
440
|
+
try {
|
|
441
|
+
await fs.access(fullManifestPath);
|
|
442
|
+
const manifestContent = await fs.readFile(fullManifestPath, 'utf-8');
|
|
443
|
+
// 将 manifest 转换为 data URL 并内联到 meta 标签
|
|
444
|
+
const manifestDataUrl = `data:application/manifest+json;base64,${Buffer.from(manifestContent).toString('base64')}`;
|
|
445
|
+
html = html.replace(match[0], `<link rel="manifest" href="${manifestDataUrl}">`);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.warn(`警告: manifest 文件不存在: ${manifestPath},移除引用`);
|
|
449
|
+
// 移除不存在的 manifest 引用
|
|
450
|
+
html = html.replace(match[0], '');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return html;
|
|
454
|
+
}
|