@playcraft/build 0.0.2
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 +96 -0
- package/dist/base-builder.d.ts +66 -0
- package/dist/base-builder.js +415 -0
- package/dist/converter.d.ts +35 -0
- package/dist/converter.js +148 -0
- package/dist/generators/config-generator.d.ts +7 -0
- package/dist/generators/config-generator.js +122 -0
- package/dist/generators/settings-generator.d.ts +14 -0
- package/dist/generators/settings-generator.js +100 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/loaders/playcanvas-loader.d.ts +10 -0
- package/dist/loaders/playcanvas-loader.js +18 -0
- package/dist/loaders/playcraft-loader.d.ts +10 -0
- package/dist/loaders/playcraft-loader.js +51 -0
- package/dist/platforms/applovin.d.ts +10 -0
- package/dist/platforms/applovin.js +67 -0
- package/dist/platforms/base.d.ts +29 -0
- package/dist/platforms/base.js +11 -0
- package/dist/platforms/bigo.d.ts +15 -0
- package/dist/platforms/bigo.js +77 -0
- package/dist/platforms/facebook.d.ts +9 -0
- package/dist/platforms/facebook.js +37 -0
- package/dist/platforms/google.d.ts +10 -0
- package/dist/platforms/google.js +53 -0
- package/dist/platforms/index.d.ts +14 -0
- package/dist/platforms/index.js +47 -0
- package/dist/platforms/ironsource.d.ts +10 -0
- package/dist/platforms/ironsource.js +71 -0
- package/dist/platforms/liftoff.d.ts +10 -0
- package/dist/platforms/liftoff.js +56 -0
- package/dist/platforms/moloco.d.ts +10 -0
- package/dist/platforms/moloco.js +53 -0
- package/dist/platforms/snapchat.d.ts +10 -0
- package/dist/platforms/snapchat.js +59 -0
- package/dist/platforms/tiktok.d.ts +15 -0
- package/dist/platforms/tiktok.js +65 -0
- package/dist/platforms/unity.d.ts +10 -0
- package/dist/platforms/unity.js +69 -0
- package/dist/playable-builder.d.ts +97 -0
- package/dist/playable-builder.js +590 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +1 -0
- package/dist/vite/config-builder.d.ts +15 -0
- package/dist/vite/config-builder.js +212 -0
- package/dist/vite/platform-configs.d.ts +38 -0
- package/dist/vite/platform-configs.js +257 -0
- package/dist/vite/plugin-model-compression.d.ts +11 -0
- package/dist/vite/plugin-model-compression.js +63 -0
- package/dist/vite/plugin-platform.d.ts +17 -0
- package/dist/vite/plugin-platform.js +241 -0
- package/dist/vite/plugin-playcanvas.d.ts +18 -0
- package/dist/vite/plugin-playcanvas.js +711 -0
- package/dist/vite/plugin-source-builder.d.ts +15 -0
- package/dist/vite/plugin-source-builder.js +344 -0
- package/dist/vite-builder.d.ts +51 -0
- package/dist/vite-builder.js +122 -0
- package/package.json +51 -0
- package/templates/__loading__.js +100 -0
- package/templates/__modules__.js +47 -0
- package/templates/__settings__.template.js +20 -0
- package/templates/__start__.js +332 -0
- package/templates/index.html +18 -0
- package/templates/logo.png +0 -0
- package/templates/manifest.json +1 -0
- package/templates/patches/cannon.min.js +28 -0
- package/templates/patches/lz4.js +10 -0
- package/templates/patches/one-page-http-get.js +20 -0
- package/templates/patches/one-page-inline-game-scripts.js +20 -0
- package/templates/patches/one-page-mraid-resize-canvas.js +46 -0
- package/templates/patches/p2.min.js +27 -0
- package/templates/patches/playcraft-no-xhr.js +52 -0
- package/templates/playcanvas-stable.min.js +16363 -0
- package/templates/styles.css +43 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export interface SourceBuilderPluginOptions {
|
|
3
|
+
projectDir: string;
|
|
4
|
+
outputDir: string;
|
|
5
|
+
}
|
|
6
|
+
export interface UserScript {
|
|
7
|
+
id: string;
|
|
8
|
+
path: string;
|
|
9
|
+
code: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Vite 插件:从源代码构建 PlayCanvas 项目
|
|
14
|
+
*/
|
|
15
|
+
export declare function viteSourceBuilderPlugin(options: SourceBuilderPluginOptions): Plugin;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { loadPlayCanvasProject } from '../loaders/playcanvas-loader.js';
|
|
6
|
+
import { loadPlayCraftProject } from '../loaders/playcraft-loader.js';
|
|
7
|
+
import { generateConfig } from '../generators/config-generator.js';
|
|
8
|
+
import { generateSettings } from '../generators/settings-generator.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
/**
|
|
12
|
+
* Vite 插件:从源代码构建 PlayCanvas 项目
|
|
13
|
+
*/
|
|
14
|
+
export function viteSourceBuilderPlugin(options) {
|
|
15
|
+
let projectFormat = null;
|
|
16
|
+
let projectConfig = null;
|
|
17
|
+
let userScripts = [];
|
|
18
|
+
return {
|
|
19
|
+
name: 'vite-plugin-source-builder',
|
|
20
|
+
enforce: 'pre',
|
|
21
|
+
async buildStart() {
|
|
22
|
+
console.log('[SourceBuilder] 开始构建源代码项目...');
|
|
23
|
+
// 1. 检测项目格式
|
|
24
|
+
projectFormat = await detectFormat(options.projectDir);
|
|
25
|
+
console.log(`[SourceBuilder] 检测到项目格式: ${projectFormat}`);
|
|
26
|
+
// 2. 读取项目配置
|
|
27
|
+
if (projectFormat === 'playcanvas') {
|
|
28
|
+
projectConfig = await loadPlayCanvasProject(options.projectDir);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
projectConfig = await loadPlayCraftProject(options.projectDir);
|
|
32
|
+
}
|
|
33
|
+
// 2.1 规范化资源文件 URL(补齐 file.url)
|
|
34
|
+
await normalizeAssetUrls(projectConfig, options.projectDir);
|
|
35
|
+
// 3. 收集用户脚本
|
|
36
|
+
userScripts = await collectUserScripts(projectConfig, options.projectDir);
|
|
37
|
+
console.log(`[SourceBuilder] 收集到 ${userScripts.length} 个用户脚本`);
|
|
38
|
+
},
|
|
39
|
+
// 处理虚拟模块(用户脚本)
|
|
40
|
+
resolveId(id) {
|
|
41
|
+
if (id === 'virtual:game-scripts') {
|
|
42
|
+
return '\0virtual:game-scripts';
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
},
|
|
46
|
+
load(id) {
|
|
47
|
+
if (id === '\0virtual:game-scripts') {
|
|
48
|
+
// 合并所有用户脚本
|
|
49
|
+
const scriptCode = userScripts.length > 0
|
|
50
|
+
? userScripts
|
|
51
|
+
.map(s => wrapPlayCanvasScript(s.code, s.name, s))
|
|
52
|
+
.join('\n\n')
|
|
53
|
+
: '// No user scripts';
|
|
54
|
+
return scriptCode;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
async closeBundle() {
|
|
59
|
+
if (!projectConfig) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// 确保输出目录存在
|
|
63
|
+
await fs.mkdir(options.outputDir, { recursive: true });
|
|
64
|
+
// 生成 config.json
|
|
65
|
+
const config = generateConfig(projectConfig);
|
|
66
|
+
// 为脚本资产补齐 __game-scripts.js 信息
|
|
67
|
+
await patchScriptAssets(config, options.outputDir);
|
|
68
|
+
await fs.writeFile(path.join(options.outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
69
|
+
console.log('[SourceBuilder] 生成 config.json');
|
|
70
|
+
// 生成 __settings__.js
|
|
71
|
+
const settings = generateSettings(projectConfig);
|
|
72
|
+
await fs.writeFile(path.join(options.outputDir, '__settings__.js'), settings, 'utf-8');
|
|
73
|
+
console.log('[SourceBuilder] 生成 __settings__.js');
|
|
74
|
+
// 复制模板文件
|
|
75
|
+
await copyTemplates(options.outputDir);
|
|
76
|
+
console.log('[SourceBuilder] 复制模板文件');
|
|
77
|
+
// 复制资源文件
|
|
78
|
+
await copyAssets(projectConfig, options.projectDir, options.outputDir, config.assets);
|
|
79
|
+
console.log('[SourceBuilder] 复制资源文件');
|
|
80
|
+
// 生成场景文件
|
|
81
|
+
await generateSceneFiles(projectConfig, options.projectDir, options.outputDir);
|
|
82
|
+
console.log('[SourceBuilder] 生成场景文件');
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 检测项目格式
|
|
88
|
+
*/
|
|
89
|
+
async function detectFormat(projectDir) {
|
|
90
|
+
// 检查 manifest.json(PlayCraft)
|
|
91
|
+
const manifestPath = path.join(projectDir, 'manifest.json');
|
|
92
|
+
try {
|
|
93
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
94
|
+
const manifest = JSON.parse(content);
|
|
95
|
+
if (manifest.format === 'playcraft' || Array.isArray(manifest.assets)) {
|
|
96
|
+
return 'playcraft';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// manifest.json 不存在或格式不对
|
|
101
|
+
}
|
|
102
|
+
// 默认为 PlayCanvas
|
|
103
|
+
return 'playcanvas';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 收集用户脚本
|
|
107
|
+
*/
|
|
108
|
+
async function collectUserScripts(projectConfig, projectDir) {
|
|
109
|
+
const scripts = [];
|
|
110
|
+
if (projectConfig.format === 'playcanvas') {
|
|
111
|
+
const pcProject = projectConfig;
|
|
112
|
+
const scriptIds = pcProject.project.settings?.scripts || [];
|
|
113
|
+
// 从 assets.json 读取脚本资产
|
|
114
|
+
for (const scriptId of scriptIds) {
|
|
115
|
+
const asset = pcProject.assets[String(scriptId)];
|
|
116
|
+
if (asset && asset.type === 'script') {
|
|
117
|
+
const assetData = asset;
|
|
118
|
+
if (assetData.file?.url && !assetData.file.url.startsWith('data:')) {
|
|
119
|
+
const scriptPath = path.join(projectDir, assetData.file.url);
|
|
120
|
+
try {
|
|
121
|
+
const code = await fs.readFile(scriptPath, 'utf-8');
|
|
122
|
+
scripts.push({
|
|
123
|
+
id: String(scriptId),
|
|
124
|
+
path: assetData.file.url,
|
|
125
|
+
code,
|
|
126
|
+
name: assetData.name || `script_${scriptId}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.warn(`警告: 无法读取脚本文件: ${scriptPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// PlayCraft 格式
|
|
138
|
+
const pcProject = projectConfig;
|
|
139
|
+
const scriptIds = pcProject.manifest.settings?.scripts || [];
|
|
140
|
+
for (const scriptId of scriptIds) {
|
|
141
|
+
const asset = pcProject.assets[String(scriptId)];
|
|
142
|
+
if (asset && asset.type === 'script') {
|
|
143
|
+
const assetData = asset;
|
|
144
|
+
if (assetData.file?.url && !assetData.file.url.startsWith('data:')) {
|
|
145
|
+
const scriptPath = path.join(projectDir, assetData.file.url);
|
|
146
|
+
try {
|
|
147
|
+
const code = await fs.readFile(scriptPath, 'utf-8');
|
|
148
|
+
scripts.push({
|
|
149
|
+
id: String(scriptId),
|
|
150
|
+
path: assetData.file.url,
|
|
151
|
+
code,
|
|
152
|
+
name: assetData.name || `script_${scriptId}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.warn(`警告: 无法读取脚本文件: ${scriptPath}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return scripts;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 包装 PlayCanvas 脚本
|
|
166
|
+
*/
|
|
167
|
+
function wrapPlayCanvasScript(code, scriptName, script) {
|
|
168
|
+
// 清理脚本名称(移除特殊字符)
|
|
169
|
+
const cleanName = scriptName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
170
|
+
return `
|
|
171
|
+
var ${cleanName} = pc.createScript('${cleanName}');
|
|
172
|
+
${code}
|
|
173
|
+
`.trim();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 复制模板文件
|
|
177
|
+
*/
|
|
178
|
+
async function copyTemplates(outputDir) {
|
|
179
|
+
const templatesDir = path.resolve(__dirname, '../../templates');
|
|
180
|
+
const templateFiles = [
|
|
181
|
+
'__start__.js',
|
|
182
|
+
'__modules__.js',
|
|
183
|
+
'__loading__.js',
|
|
184
|
+
'playcanvas-stable.min.js',
|
|
185
|
+
'styles.css',
|
|
186
|
+
'logo.png',
|
|
187
|
+
'manifest.json',
|
|
188
|
+
];
|
|
189
|
+
for (const file of templateFiles) {
|
|
190
|
+
const sourcePath = path.join(templatesDir, file);
|
|
191
|
+
const targetPath = path.join(outputDir, file);
|
|
192
|
+
try {
|
|
193
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.warn(`警告: 无法复制模板文件 ${file}:`, error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// 生成 index.html
|
|
200
|
+
const projectName = 'PlayCanvas Project';
|
|
201
|
+
const htmlTemplate = await fs.readFile(path.join(templatesDir, 'index.html'), 'utf-8');
|
|
202
|
+
const htmlContent = htmlTemplate.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
203
|
+
await fs.writeFile(path.join(outputDir, 'index.html'), htmlContent, 'utf-8');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 复制资源文件
|
|
207
|
+
*/
|
|
208
|
+
async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride) {
|
|
209
|
+
const assetsOutputDir = path.join(outputDir, 'files');
|
|
210
|
+
await fs.mkdir(assetsOutputDir, { recursive: true });
|
|
211
|
+
const assets = assetsOverride || (projectConfig.format === 'playcanvas'
|
|
212
|
+
? projectConfig.assets
|
|
213
|
+
: projectConfig.assets);
|
|
214
|
+
// 遍历 assets
|
|
215
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
216
|
+
const assetData = asset;
|
|
217
|
+
if (!assetData.file?.url || assetData.file.url.startsWith('data:')) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const sourcePath = path.join(projectDir, assetData.file.url);
|
|
221
|
+
const targetPath = path.join(outputDir, assetData.file.url);
|
|
222
|
+
try {
|
|
223
|
+
// 检查源文件是否存在
|
|
224
|
+
await fs.access(sourcePath);
|
|
225
|
+
// 创建目标目录
|
|
226
|
+
const targetDir = path.dirname(targetPath);
|
|
227
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
228
|
+
// 复制文件
|
|
229
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// 资源文件可能不存在(可能是内联的)
|
|
233
|
+
// console.warn(`警告: 无法复制资源文件 ${assetData.file.url}:`, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 生成场景文件
|
|
239
|
+
*/
|
|
240
|
+
async function generateSceneFiles(projectConfig, projectDir, outputDir) {
|
|
241
|
+
if (projectConfig.format === 'playcanvas') {
|
|
242
|
+
const pcProject = projectConfig;
|
|
243
|
+
// PlayCanvas 格式:scenes.json 中已包含场景数据
|
|
244
|
+
if (pcProject.scenes) {
|
|
245
|
+
const scenes = Array.isArray(pcProject.scenes)
|
|
246
|
+
? pcProject.scenes
|
|
247
|
+
: Object.entries(pcProject.scenes).map(([id, scene]) => ({ id, ...scene }));
|
|
248
|
+
for (const scene of scenes) {
|
|
249
|
+
const sceneData = scene;
|
|
250
|
+
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
251
|
+
const scenePath = path.join(outputDir, `${sceneId}.json`);
|
|
252
|
+
// 写入场景 JSON
|
|
253
|
+
await fs.writeFile(scenePath, JSON.stringify(sceneData, null, 2), 'utf-8');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// PlayCraft 格式:从 scenes/ 目录读取
|
|
259
|
+
const pcProject = projectConfig;
|
|
260
|
+
for (const scene of pcProject.scenes) {
|
|
261
|
+
const sceneData = scene;
|
|
262
|
+
const sceneId = sceneData.id || sceneData.name || 'scene';
|
|
263
|
+
const scenePath = path.join(outputDir, `${sceneId}.json`);
|
|
264
|
+
// 写入场景 JSON
|
|
265
|
+
await fs.writeFile(scenePath, JSON.stringify(sceneData, null, 2), 'utf-8');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 规范化资源 URL(补齐 file.url)
|
|
271
|
+
*/
|
|
272
|
+
async function normalizeAssetUrls(projectConfig, projectDir) {
|
|
273
|
+
const assets = projectConfig.format === 'playcanvas'
|
|
274
|
+
? projectConfig.assets
|
|
275
|
+
: projectConfig.assets;
|
|
276
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
277
|
+
const assetData = asset;
|
|
278
|
+
if (!assetData.file || assetData.file.url) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const filename = assetData.file.filename;
|
|
282
|
+
if (!filename) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const revision = assetData.revision ?? assetData.file.revision ?? 1;
|
|
286
|
+
const candidates = [
|
|
287
|
+
path.join('files', 'assets', String(assetId), String(revision), filename),
|
|
288
|
+
path.join('files', 'assets', String(assetId), '1', filename),
|
|
289
|
+
path.join('files', 'assets', String(assetId), filename),
|
|
290
|
+
path.join('files', filename),
|
|
291
|
+
];
|
|
292
|
+
for (const rel of candidates) {
|
|
293
|
+
const abs = path.join(projectDir, rel);
|
|
294
|
+
try {
|
|
295
|
+
await fs.access(abs);
|
|
296
|
+
assetData.file.url = rel;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
// 继续尝试下一个
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 为脚本资产补齐 __game-scripts.js 信息
|
|
307
|
+
*/
|
|
308
|
+
async function patchScriptAssets(config, outputDir) {
|
|
309
|
+
if (!config?.assets) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const scriptIds = Array.isArray(config?.application_properties?.scripts)
|
|
313
|
+
? config.application_properties.scripts.map((id) => String(id))
|
|
314
|
+
: [];
|
|
315
|
+
if (scriptIds.length === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const gameScriptsPath = path.join(outputDir, '__game-scripts.js');
|
|
319
|
+
let stat = null;
|
|
320
|
+
let hash = null;
|
|
321
|
+
try {
|
|
322
|
+
const buffer = await fs.readFile(gameScriptsPath);
|
|
323
|
+
stat = { size: buffer.byteLength };
|
|
324
|
+
hash = crypto.createHash('md5').update(buffer).digest('hex');
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// __game-scripts.js 可能不存在
|
|
328
|
+
}
|
|
329
|
+
for (const [assetId, asset] of Object.entries(config.assets)) {
|
|
330
|
+
const assetData = asset;
|
|
331
|
+
if (!scriptIds.includes(String(assetId))) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (assetData.type !== 'script' || !assetData.file) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
assetData.file.url = '__game-scripts.js';
|
|
338
|
+
assetData.file.filename = '__game-scripts.js';
|
|
339
|
+
if (stat && hash) {
|
|
340
|
+
assetData.file.size = stat.size;
|
|
341
|
+
assetData.file.hash = hash;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BuildOptions, SizeReport } from './types.js';
|
|
2
|
+
export interface ViteBuildOutput {
|
|
3
|
+
outputPath: string;
|
|
4
|
+
size: number;
|
|
5
|
+
sizeLimit: number;
|
|
6
|
+
valid: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Vite 构建器 - 使用 Vite 构建 Playable Ads
|
|
10
|
+
*
|
|
11
|
+
* 职责:
|
|
12
|
+
* 1. 验证输入是有效的基础构建
|
|
13
|
+
* 2. 创建 Vite 配置
|
|
14
|
+
* 3. 执行 Vite 构建
|
|
15
|
+
* 4. 验证输出大小
|
|
16
|
+
* 5. 生成报告
|
|
17
|
+
*/
|
|
18
|
+
export declare class ViteBuilder {
|
|
19
|
+
private baseBuildDir;
|
|
20
|
+
private options;
|
|
21
|
+
private sizeReport;
|
|
22
|
+
constructor(baseBuildDir: string, options: BuildOptions);
|
|
23
|
+
/**
|
|
24
|
+
* 执行构建
|
|
25
|
+
*/
|
|
26
|
+
build(): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* 验证基础构建
|
|
29
|
+
*/
|
|
30
|
+
private validateBaseBuild;
|
|
31
|
+
/**
|
|
32
|
+
* 获取输出路径
|
|
33
|
+
*/
|
|
34
|
+
private getOutputPath;
|
|
35
|
+
/**
|
|
36
|
+
* 验证输出大小
|
|
37
|
+
*/
|
|
38
|
+
private validateSize;
|
|
39
|
+
/**
|
|
40
|
+
* 生成报告
|
|
41
|
+
*/
|
|
42
|
+
private generateReport;
|
|
43
|
+
/**
|
|
44
|
+
* 获取大小报告
|
|
45
|
+
*/
|
|
46
|
+
getSizeReport(): SizeReport;
|
|
47
|
+
/**
|
|
48
|
+
* 格式化字节数
|
|
49
|
+
*/
|
|
50
|
+
private formatBytes;
|
|
51
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { build as viteBuild } from 'vite';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ViteConfigBuilder } from './vite/config-builder.js';
|
|
5
|
+
import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
|
|
6
|
+
/**
|
|
7
|
+
* Vite 构建器 - 使用 Vite 构建 Playable Ads
|
|
8
|
+
*
|
|
9
|
+
* 职责:
|
|
10
|
+
* 1. 验证输入是有效的基础构建
|
|
11
|
+
* 2. 创建 Vite 配置
|
|
12
|
+
* 3. 执行 Vite 构建
|
|
13
|
+
* 4. 验证输出大小
|
|
14
|
+
* 5. 生成报告
|
|
15
|
+
*/
|
|
16
|
+
export class ViteBuilder {
|
|
17
|
+
constructor(baseBuildDir, options) {
|
|
18
|
+
this.baseBuildDir = baseBuildDir;
|
|
19
|
+
this.options = options;
|
|
20
|
+
const platformConfig = PLATFORM_CONFIGS[options.platform];
|
|
21
|
+
this.sizeReport = {
|
|
22
|
+
engine: 0,
|
|
23
|
+
assets: {},
|
|
24
|
+
total: 0,
|
|
25
|
+
limit: platformConfig.sizeLimit,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 执行构建
|
|
30
|
+
*/
|
|
31
|
+
async build() {
|
|
32
|
+
// 1. 验证输入
|
|
33
|
+
await this.validateBaseBuild();
|
|
34
|
+
// 2. 创建 Vite 配置
|
|
35
|
+
const configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
|
|
36
|
+
const viteConfig = configBuilder.create();
|
|
37
|
+
// 3. 执行 Vite 构建
|
|
38
|
+
await viteBuild(viteConfig);
|
|
39
|
+
// 4. 验证输出大小
|
|
40
|
+
const outputPath = this.getOutputPath();
|
|
41
|
+
await this.validateSize(outputPath);
|
|
42
|
+
// 5. 生成报告
|
|
43
|
+
this.generateReport(outputPath);
|
|
44
|
+
return outputPath;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 验证基础构建
|
|
48
|
+
*/
|
|
49
|
+
async validateBaseBuild() {
|
|
50
|
+
const requiredFiles = [
|
|
51
|
+
'index.html',
|
|
52
|
+
'config.json',
|
|
53
|
+
'__start__.js',
|
|
54
|
+
];
|
|
55
|
+
const missingFiles = [];
|
|
56
|
+
for (const file of requiredFiles) {
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(path.join(this.baseBuildDir, file));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
missingFiles.push(file);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (missingFiles.length > 0) {
|
|
65
|
+
throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
66
|
+
`请确保输入目录包含完整的多文件构建产物。`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 获取输出路径
|
|
71
|
+
*/
|
|
72
|
+
getOutputPath() {
|
|
73
|
+
const platformConfig = PLATFORM_CONFIGS[this.options.platform];
|
|
74
|
+
const outputDir = this.options.outputDir || './dist';
|
|
75
|
+
if (platformConfig.outputFormat === 'zip') {
|
|
76
|
+
return path.join(outputDir, 'playable.zip');
|
|
77
|
+
}
|
|
78
|
+
return path.join(outputDir, platformConfig.outputFileName);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 验证输出大小
|
|
82
|
+
*/
|
|
83
|
+
async validateSize(outputPath) {
|
|
84
|
+
try {
|
|
85
|
+
const stats = await fs.stat(outputPath);
|
|
86
|
+
this.sizeReport.total = stats.size;
|
|
87
|
+
this.sizeReport.assets[path.basename(outputPath)] = stats.size;
|
|
88
|
+
if (stats.size > this.sizeReport.limit) {
|
|
89
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
90
|
+
const limitMB = (this.sizeReport.limit / 1024 / 1024).toFixed(2);
|
|
91
|
+
console.warn(`⚠️ 警告: 文件大小 ${sizeMB} MB 超过限制 ${limitMB} MB`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.warn(`警告: 无法读取输出文件: ${outputPath}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 生成报告
|
|
100
|
+
*/
|
|
101
|
+
generateReport(outputPath) {
|
|
102
|
+
// 报告已在 validateSize 中生成
|
|
103
|
+
// 这里可以添加额外的报告逻辑
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 获取大小报告
|
|
107
|
+
*/
|
|
108
|
+
getSizeReport() {
|
|
109
|
+
return this.sizeReport;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 格式化字节数
|
|
113
|
+
*/
|
|
114
|
+
formatBytes(bytes) {
|
|
115
|
+
if (bytes === 0)
|
|
116
|
+
return '0 Bytes';
|
|
117
|
+
const k = 1024;
|
|
118
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
119
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
120
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
121
|
+
}
|
|
122
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@playcraft/build",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./types": {
|
|
14
|
+
"import": "./dist/types.js",
|
|
15
|
+
"types": "./dist/types.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"templates",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "tsc -w",
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"release": "node scripts/release.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@gltf-transform/core": "^4.0.0",
|
|
30
|
+
"@gltf-transform/extensions": "^4.0.0",
|
|
31
|
+
"@gltf-transform/functions": "^4.0.0",
|
|
32
|
+
"@vheemstra/vite-plugin-imagemin": "^2.0.0",
|
|
33
|
+
"archiver": "^7.0.1",
|
|
34
|
+
"draco3dgltf": "^1.5.7",
|
|
35
|
+
"imagemin-mozjpeg": "^10.0.0",
|
|
36
|
+
"imagemin-pngquant": "^10.0.0",
|
|
37
|
+
"imagemin-webp": "^8.0.0",
|
|
38
|
+
"lightningcss": "^1.27.0",
|
|
39
|
+
"mime-types": "^2.1.35",
|
|
40
|
+
"rollup-plugin-visualizer": "^6.0.5",
|
|
41
|
+
"terser": "^5.30.0",
|
|
42
|
+
"vite": "^6.0.0",
|
|
43
|
+
"vite-plugin-singlefile": "^2.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/archiver": "^6.0.4",
|
|
47
|
+
"@types/mime-types": "^2.1.4",
|
|
48
|
+
"@types/node": "^22.10.5",
|
|
49
|
+
"typescript": "^5.7.2"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
pc.script.createLoadingScreen((app) => {
|
|
2
|
+
const createCss = () => {
|
|
3
|
+
const css = `
|
|
4
|
+
body {
|
|
5
|
+
background-color: #283538;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#application-splash-wrapper {
|
|
9
|
+
position: absolute;
|
|
10
|
+
top: 0;
|
|
11
|
+
left: 0;
|
|
12
|
+
height: 100%;
|
|
13
|
+
width: 100%;
|
|
14
|
+
background-color: #283538;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#application-splash {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: calc(50% - 28px);
|
|
20
|
+
width: 264px;
|
|
21
|
+
left: calc(50% - 132px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#application-splash img {
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#progress-bar-container {
|
|
29
|
+
margin: 20px auto 0 auto;
|
|
30
|
+
height: 2px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
background-color: #1d292c;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#progress-bar {
|
|
36
|
+
width: 0%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
background-color: #f60;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (max-width: 480px) {
|
|
42
|
+
#application-splash {
|
|
43
|
+
width: 170px;
|
|
44
|
+
left: calc(50% - 85px);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const style = document.createElement('style');
|
|
50
|
+
style.textContent = css;
|
|
51
|
+
document.head.appendChild(style);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const showSplash = () => {
|
|
55
|
+
const wrapper = document.createElement('div');
|
|
56
|
+
wrapper.id = 'application-splash-wrapper';
|
|
57
|
+
document.body.appendChild(wrapper);
|
|
58
|
+
|
|
59
|
+
const splash = document.createElement('div');
|
|
60
|
+
splash.id = 'application-splash';
|
|
61
|
+
wrapper.appendChild(splash);
|
|
62
|
+
splash.style.display = 'none';
|
|
63
|
+
|
|
64
|
+
const logo = document.createElement('img');
|
|
65
|
+
logo.src = `${ASSET_PREFIX}logo.png`;
|
|
66
|
+
splash.appendChild(logo);
|
|
67
|
+
logo.onload = () => {
|
|
68
|
+
splash.style.display = 'block';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const container = document.createElement('div');
|
|
72
|
+
container.id = 'progress-bar-container';
|
|
73
|
+
splash.appendChild(container);
|
|
74
|
+
|
|
75
|
+
const bar = document.createElement('div');
|
|
76
|
+
bar.id = 'progress-bar';
|
|
77
|
+
container.appendChild(bar);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const setProgress = (value) => {
|
|
81
|
+
const bar = document.getElementById('progress-bar');
|
|
82
|
+
if (bar) {
|
|
83
|
+
value = Math.min(1, Math.max(0, value));
|
|
84
|
+
bar.style.width = `${value * 100}%`;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const hideSplash = () => {
|
|
89
|
+
document.getElementById('application-splash-wrapper').remove();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
createCss();
|
|
93
|
+
showSplash();
|
|
94
|
+
|
|
95
|
+
app.on('preload:end', () => {
|
|
96
|
+
app.off('preload:progress');
|
|
97
|
+
});
|
|
98
|
+
app.on('preload:progress', setProgress);
|
|
99
|
+
app.on('start', hideSplash);
|
|
100
|
+
});
|