@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.
- package/dist/analyzers/scene-asset-collector.js +99 -9
- package/dist/base-builder.d.ts +15 -78
- package/dist/base-builder.js +34 -741
- package/dist/engines/engine-detector.d.ts +38 -0
- package/dist/engines/engine-detector.js +201 -0
- package/dist/engines/generic-adapter.d.ts +71 -0
- package/dist/engines/generic-adapter.js +378 -0
- package/dist/engines/index.d.ts +7 -0
- package/dist/engines/index.js +7 -0
- package/dist/engines/playcanvas-adapter.d.ts +85 -0
- package/dist/engines/playcanvas-adapter.js +813 -0
- package/dist/generators/config-generator.js +59 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/loaders/playcraft-loader.js +240 -5
- package/dist/platforms/adikteev.d.ts +1 -1
- package/dist/platforms/adikteev.js +30 -36
- package/dist/platforms/applovin.d.ts +1 -1
- package/dist/platforms/applovin.js +31 -36
- package/dist/platforms/base.d.ts +27 -5
- package/dist/platforms/base.js +79 -181
- package/dist/platforms/bigo.d.ts +1 -1
- package/dist/platforms/bigo.js +28 -28
- package/dist/platforms/facebook.d.ts +1 -1
- package/dist/platforms/facebook.js +21 -10
- package/dist/platforms/google.d.ts +1 -1
- package/dist/platforms/google.js +28 -21
- package/dist/platforms/index.d.ts +1 -0
- package/dist/platforms/index.js +4 -0
- package/dist/platforms/inmobi.d.ts +1 -1
- package/dist/platforms/inmobi.js +27 -34
- package/dist/platforms/ironsource.d.ts +1 -1
- package/dist/platforms/ironsource.js +37 -40
- package/dist/platforms/liftoff.d.ts +1 -1
- package/dist/platforms/liftoff.js +22 -30
- package/dist/platforms/mintegral.d.ts +10 -0
- package/dist/platforms/mintegral.js +65 -0
- package/dist/platforms/moloco.d.ts +1 -1
- package/dist/platforms/moloco.js +18 -20
- package/dist/platforms/playcraft.d.ts +1 -1
- package/dist/platforms/playcraft.js +2 -2
- package/dist/platforms/remerge.d.ts +1 -1
- package/dist/platforms/remerge.js +19 -20
- package/dist/platforms/snapchat.d.ts +1 -1
- package/dist/platforms/snapchat.js +32 -26
- package/dist/platforms/tiktok.d.ts +1 -1
- package/dist/platforms/tiktok.js +28 -24
- package/dist/platforms/unity.d.ts +1 -1
- package/dist/platforms/unity.js +30 -36
- package/dist/playable-builder.d.ts +1 -0
- package/dist/playable-builder.js +16 -2
- package/dist/types.d.ts +113 -1
- package/dist/types.js +77 -1
- package/dist/utils/ammo-detector.d.ts +9 -0
- package/dist/utils/ammo-detector.js +76 -0
- package/dist/utils/build-mode-detector.js +2 -0
- package/dist/utils/minify.d.ts +32 -0
- package/dist/utils/minify.js +82 -0
- package/dist/utils/obfuscate.d.ts +42 -0
- package/dist/utils/obfuscate.js +216 -0
- package/dist/vite/config-builder-generic.d.ts +70 -0
- package/dist/vite/config-builder-generic.js +251 -0
- package/dist/vite/config-builder.d.ts +8 -0
- package/dist/vite/config-builder.js +53 -16
- package/dist/vite/platform-configs.js +29 -1
- package/dist/vite/plugin-compress-js.d.ts +21 -0
- package/dist/vite/plugin-compress-js.js +213 -0
- package/dist/vite/plugin-esm-html-generator.js +5 -1
- package/dist/vite/plugin-obfuscate.d.ts +22 -0
- package/dist/vite/plugin-obfuscate.js +52 -0
- package/dist/vite/plugin-platform.d.ts +5 -0
- package/dist/vite/plugin-platform.js +499 -35
- package/dist/vite/plugin-playcanvas.js +21 -68
- package/dist/vite/plugin-source-builder.js +102 -21
- package/dist/vite-builder.d.ts +25 -7
- package/dist/vite-builder.js +141 -52
- package/package.json +4 -2
- package/physics/cannon-rigidbody-adapter.js +243 -22
- package/templates/__loading__.js +0 -12
- package/templates/index.esm.mjs +0 -11
- package/templates/patches/playcraft-cta-adapter.js +129 -31
- package/templates/patches/scene-physics-defaults.js +49 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码混淆 + fflate 压缩工具
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. javascript-obfuscator 混淆(字符串加密、控制流扁平化等)
|
|
6
|
+
* 2. fflate deflate 压缩
|
|
7
|
+
* 3. base64 编码
|
|
8
|
+
* 4. 包装为自解压 bootstrap 脚本
|
|
9
|
+
*/
|
|
10
|
+
import JavaScriptObfuscator from 'javascript-obfuscator';
|
|
11
|
+
import { deflateSync } from 'fflate';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
/**
|
|
16
|
+
* 混淆级别预设配置
|
|
17
|
+
*/
|
|
18
|
+
const ObfuscatorPresets = {
|
|
19
|
+
// 轻量级:仅字符串加密 + 标识符混淆,体积膨胀 ~30%
|
|
20
|
+
light: {
|
|
21
|
+
compact: true,
|
|
22
|
+
controlFlowFlattening: false,
|
|
23
|
+
deadCodeInjection: false,
|
|
24
|
+
stringArray: true,
|
|
25
|
+
stringArrayEncoding: ['base64'],
|
|
26
|
+
stringArrayThreshold: 0.5,
|
|
27
|
+
stringArrayCallsTransform: true,
|
|
28
|
+
identifierNamesGenerator: 'hexadecimal',
|
|
29
|
+
renameGlobals: false,
|
|
30
|
+
selfDefending: false,
|
|
31
|
+
splitStrings: false,
|
|
32
|
+
transformObjectKeys: false,
|
|
33
|
+
},
|
|
34
|
+
// 中等:字符串加密 + 控制流扁平化,体积膨胀 ~60%
|
|
35
|
+
medium: {
|
|
36
|
+
compact: true,
|
|
37
|
+
controlFlowFlattening: true,
|
|
38
|
+
controlFlowFlatteningThreshold: 0.5,
|
|
39
|
+
deadCodeInjection: false,
|
|
40
|
+
stringArray: true,
|
|
41
|
+
stringArrayEncoding: ['base64'],
|
|
42
|
+
stringArrayThreshold: 0.75,
|
|
43
|
+
stringArrayCallsTransform: true,
|
|
44
|
+
identifierNamesGenerator: 'hexadecimal',
|
|
45
|
+
renameGlobals: false,
|
|
46
|
+
selfDefending: false,
|
|
47
|
+
splitStrings: true,
|
|
48
|
+
splitStringsChunkLength: 10,
|
|
49
|
+
transformObjectKeys: true,
|
|
50
|
+
},
|
|
51
|
+
// 重度:全部开启,体积膨胀 ~100%+
|
|
52
|
+
heavy: {
|
|
53
|
+
compact: true,
|
|
54
|
+
controlFlowFlattening: true,
|
|
55
|
+
controlFlowFlatteningThreshold: 0.75,
|
|
56
|
+
deadCodeInjection: true,
|
|
57
|
+
deadCodeInjectionThreshold: 0.3,
|
|
58
|
+
stringArray: true,
|
|
59
|
+
stringArrayEncoding: ['rc4'],
|
|
60
|
+
stringArrayThreshold: 1,
|
|
61
|
+
stringArrayCallsTransform: true,
|
|
62
|
+
identifierNamesGenerator: 'hexadecimal',
|
|
63
|
+
renameGlobals: false,
|
|
64
|
+
selfDefending: false,
|
|
65
|
+
splitStrings: true,
|
|
66
|
+
splitStringsChunkLength: 5,
|
|
67
|
+
transformObjectKeys: true,
|
|
68
|
+
unicodeEscapeSequence: true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* fflate inflate 运行时代码(浏览器端)
|
|
73
|
+
* 从 fflate 库的 UMD 构建中动态加载,确保与 Node 端使用的 fflate 版本一致
|
|
74
|
+
* 运行后在 window 上暴露 __fflate_inflate 函数
|
|
75
|
+
*/
|
|
76
|
+
let _fflateRuntimeCache = null;
|
|
77
|
+
export function getFflateInflateRuntime() {
|
|
78
|
+
if (_fflateRuntimeCache)
|
|
79
|
+
return _fflateRuntimeCache;
|
|
80
|
+
// 找到 fflate 包的 UMD 构建文件
|
|
81
|
+
// 从当前文件位置向上查找 node_modules/fflate/umd/index.js
|
|
82
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
83
|
+
// 向上查找 node_modules 目录(支持 pnpm 的嵌套 node_modules 结构)
|
|
84
|
+
let searchDir = currentDir;
|
|
85
|
+
let fflateUmdPath = '';
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
const candidate = path.join(searchDir, 'node_modules', 'fflate', 'umd', 'index.js');
|
|
88
|
+
if (fs.existsSync(candidate)) {
|
|
89
|
+
fflateUmdPath = candidate;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const parent = path.dirname(searchDir);
|
|
93
|
+
if (parent === searchDir)
|
|
94
|
+
break;
|
|
95
|
+
searchDir = parent;
|
|
96
|
+
}
|
|
97
|
+
if (!fflateUmdPath) {
|
|
98
|
+
throw new Error('[fflate] 无法找到 fflate UMD 构建文件,请确保 fflate 已安装');
|
|
99
|
+
}
|
|
100
|
+
const fflateUmd = fs.readFileSync(fflateUmdPath, 'utf-8');
|
|
101
|
+
// 包装 UMD 使其在浏览器环境中执行,并将 inflateSync 暴露为 window.__fflate_inflate
|
|
102
|
+
// UMD 的外层包装会检测环境:typeof self !== 'undefined' ? self : this
|
|
103
|
+
// 通过 var self=window 确保 fflate 挂载到 window.fflate
|
|
104
|
+
_fflateRuntimeCache = `!function(){var self=window;${fflateUmd};window.__fflate_inflate=window.fflate.inflateSync}();`;
|
|
105
|
+
return _fflateRuntimeCache;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 对 JS 代码进行混淆 + fflate 压缩,生成自解压 bootstrap
|
|
109
|
+
*/
|
|
110
|
+
export async function obfuscateAndCompress(jsCode, level = 'medium') {
|
|
111
|
+
const originalSize = Buffer.from(jsCode).length;
|
|
112
|
+
console.log(`[Obfuscator] 开始混淆... 级别: ${level}, 原始大小: ${formatBytes(originalSize)}`);
|
|
113
|
+
console.log(`[Obfuscator] 混淆中(这可能需要较长时间)...`);
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
// ① javascript-obfuscator 混淆
|
|
116
|
+
const obfuscated = JavaScriptObfuscator.obfuscate(jsCode, {
|
|
117
|
+
...ObfuscatorPresets[level],
|
|
118
|
+
target: 'browser',
|
|
119
|
+
seed: Date.now(),
|
|
120
|
+
// 不使用 sourceMap
|
|
121
|
+
sourceMap: false,
|
|
122
|
+
// 禁用 console 输出
|
|
123
|
+
disableConsoleOutput: false,
|
|
124
|
+
// 不使用 domain lock
|
|
125
|
+
domainLock: [],
|
|
126
|
+
domainLockRedirectUrl: 'about:blank',
|
|
127
|
+
// 排除一些 PlayCanvas 关键标识符不被混淆
|
|
128
|
+
reservedNames: [
|
|
129
|
+
'pc', 'PlayCanvasESMBundle', 'PRELOAD_MODULES',
|
|
130
|
+
'__fflate_inflate',
|
|
131
|
+
],
|
|
132
|
+
reservedStrings: [],
|
|
133
|
+
}).getObfuscatedCode();
|
|
134
|
+
const obfuscatedSize = Buffer.from(obfuscated).length;
|
|
135
|
+
const obfuscateTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
136
|
+
console.log(`[Obfuscator] 混淆完成 (${obfuscateTime}s), 大小: ${formatBytes(obfuscatedSize)} (膨胀 ${((obfuscatedSize / originalSize - 1) * 100).toFixed(0)}%)`);
|
|
137
|
+
// ② fflate deflate 压缩
|
|
138
|
+
const textEncoder = new TextEncoder();
|
|
139
|
+
const codeBytes = textEncoder.encode(obfuscated);
|
|
140
|
+
const compressed = deflateSync(codeBytes, { level: 9 });
|
|
141
|
+
const compressedSize = compressed.length;
|
|
142
|
+
console.log(`[Obfuscator] fflate 压缩完成, 大小: ${formatBytes(compressedSize)} (压缩率 ${((1 - compressedSize / obfuscatedSize) * 100).toFixed(0)}%)`);
|
|
143
|
+
// ③ 转 base64
|
|
144
|
+
const base64 = Buffer.from(compressed).toString('base64');
|
|
145
|
+
// ④ 生成 bootstrap 自解压脚本
|
|
146
|
+
// 运行时:base64 解码 → fflate inflate 解压 → 创建 <script> 执行
|
|
147
|
+
const bootstrapCode = `!function(){var b=atob("${base64}"),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));var e=document.createElement("script");e.textContent=decodeURIComponent(escape(s));document.currentScript.parentNode.insertBefore(e,document.currentScript)}();`;
|
|
148
|
+
const runtime = `<script>${getFflateInflateRuntime()}</script>`;
|
|
149
|
+
const bootstrap = `<script>${bootstrapCode}</script>`;
|
|
150
|
+
const finalSize = runtime.length + bootstrap.length;
|
|
151
|
+
console.log(`[Obfuscator] 最终大小: ${formatBytes(finalSize)} (原始 ${formatBytes(originalSize)} → 节省 ${((1 - finalSize / originalSize) * 100).toFixed(0)}%)`);
|
|
152
|
+
return {
|
|
153
|
+
runtime,
|
|
154
|
+
bootstrap,
|
|
155
|
+
originalSize,
|
|
156
|
+
obfuscatedSize,
|
|
157
|
+
compressedSize,
|
|
158
|
+
finalSize,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 使用 fflate 压缩引擎代码(替换 LZ4)
|
|
163
|
+
* 不做混淆,只做压缩
|
|
164
|
+
*/
|
|
165
|
+
export function compressWithFflate(code) {
|
|
166
|
+
const originalSize = Buffer.from(code).length;
|
|
167
|
+
// fflate deflate 压缩
|
|
168
|
+
const textEncoder = new TextEncoder();
|
|
169
|
+
const codeBytes = textEncoder.encode(code);
|
|
170
|
+
const compressed = deflateSync(codeBytes, { level: 9 });
|
|
171
|
+
const compressedSize = compressed.length;
|
|
172
|
+
// 转 base64
|
|
173
|
+
const base64 = Buffer.from(compressed).toString('base64');
|
|
174
|
+
// 生成自解压脚本
|
|
175
|
+
const wrapper = `!function(){var b=atob("${base64}"),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));var e=document.createElement("script");e.textContent=decodeURIComponent(escape(s));document.currentScript.parentNode.insertBefore(e,document.currentScript)}();`;
|
|
176
|
+
const runtime = `<script>${getFflateInflateRuntime()}</script>`;
|
|
177
|
+
const compressedScript = `<script>${wrapper}</script>`;
|
|
178
|
+
console.log(`[fflate] 引擎压缩: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (压缩率 ${((1 - compressedSize / originalSize) * 100).toFixed(0)}%)`);
|
|
179
|
+
return {
|
|
180
|
+
runtime,
|
|
181
|
+
compressed: compressedScript,
|
|
182
|
+
originalSize,
|
|
183
|
+
compressedSize,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 使用 fflate 压缩 config.json(替换 LZ4)
|
|
188
|
+
*/
|
|
189
|
+
export function compressConfigJsonWithFflate(jsonString) {
|
|
190
|
+
const originalSize = Buffer.from(jsonString).length;
|
|
191
|
+
const textEncoder = new TextEncoder();
|
|
192
|
+
const jsonBytes = textEncoder.encode(jsonString);
|
|
193
|
+
const compressed = deflateSync(jsonBytes, { level: 9 });
|
|
194
|
+
const compressedSize = compressed.length;
|
|
195
|
+
const compressedBase64 = Buffer.from(compressed).toString('base64');
|
|
196
|
+
// 使用 fflate-json 前缀标识格式
|
|
197
|
+
const dataUrl = `data:application/x-fflate-json;base64,${compressedBase64}`;
|
|
198
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
|
|
199
|
+
console.log(`[fflate] config.json 压缩: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (减少 ${ratio}%)`);
|
|
200
|
+
return { dataUrl, originalSize, compressedSize };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 构建 config.json 解压 patch(fflate 版本)
|
|
204
|
+
* 拦截 pc.Http.prototype.get,检测 fflate 压缩的 JSON 并解压
|
|
205
|
+
*/
|
|
206
|
+
export function buildFflateConfigDecompressPatch() {
|
|
207
|
+
return `!function(){function p(){if(!window.pc||!pc.Http)return!1;var o=pc.Http.prototype.get;return pc.Http.prototype.get=function(e,t,n){"function"==typeof t&&(n=t,t={});if("string"==typeof e&&e.startsWith("data:application/x-fflate-json;base64,")){try{var r=e.replace("data:application/x-fflate-json;base64,",""),b=atob(r),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);var d=__fflate_inflate(a),s="";for(var i=0;i<d.length;i+=8192)s+=String.fromCharCode.apply(null,d.subarray(i,i+8192));s=decodeURIComponent(escape(s));var j=JSON.parse(s);n(null,j)}catch(x){console.error("[fflate Config] Decompression error:",x);n(x)}return}o.call(this,e,t,n)},!0}if(!p()){var c=0,i=setInterval(function(){(p()||++c>100)&&clearInterval(i)},10)}}();`;
|
|
208
|
+
}
|
|
209
|
+
function formatBytes(bytes) {
|
|
210
|
+
if (bytes === 0)
|
|
211
|
+
return '0 B';
|
|
212
|
+
const k = 1024;
|
|
213
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
214
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
215
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
216
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type UserConfig } from 'vite';
|
|
2
|
+
import type { Platform, BuildOptions, BaseBuildMetadata } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 通用 Vite 配置构建器(外部引擎专用)
|
|
5
|
+
*
|
|
6
|
+
* 与 PlayCanvas 专用配置构建器不同,通用版本:
|
|
7
|
+
* - 不使用 vitePlayCanvasPlugin(不需要处理 config.json 等专有格式)
|
|
8
|
+
* - 不使用 viteESMBundlePlugin 和 viteModelCompressionPlugin(Draco/Meshopt 是 PlayCanvas 特有的)
|
|
9
|
+
* - 仅使用通用的插件:
|
|
10
|
+
* - viteSingleFile:HTML 内联
|
|
11
|
+
* - vitePlatformPlugin:平台 SDK 注入
|
|
12
|
+
* - viteImagemin:图片压缩
|
|
13
|
+
*/
|
|
14
|
+
export declare class GenericViteConfigBuilder {
|
|
15
|
+
private baseBuildDir;
|
|
16
|
+
private platform;
|
|
17
|
+
private options;
|
|
18
|
+
private baseBuildMetadata;
|
|
19
|
+
private finalOutputDir;
|
|
20
|
+
private viteOutputDir;
|
|
21
|
+
constructor(baseBuildDir: string, platform: Platform, options: BuildOptions, baseBuildMetadata: BaseBuildMetadata);
|
|
22
|
+
/**
|
|
23
|
+
* 创建 Vite 配置
|
|
24
|
+
*/
|
|
25
|
+
create(): Promise<UserConfig>;
|
|
26
|
+
/**
|
|
27
|
+
* 获取 Vite 的临时输出目录
|
|
28
|
+
*/
|
|
29
|
+
getViteOutputDir(): string;
|
|
30
|
+
/**
|
|
31
|
+
* 获取最终输出目录
|
|
32
|
+
*/
|
|
33
|
+
getFinalOutputDir(): string;
|
|
34
|
+
/**
|
|
35
|
+
* 创建通用配置
|
|
36
|
+
*/
|
|
37
|
+
private createConfig;
|
|
38
|
+
/**
|
|
39
|
+
* 创建插件列表
|
|
40
|
+
*/
|
|
41
|
+
private createPlugins;
|
|
42
|
+
/**
|
|
43
|
+
* 获取平台配置
|
|
44
|
+
*/
|
|
45
|
+
private getPlatformConfig;
|
|
46
|
+
/**
|
|
47
|
+
* 获取 Playable 选项
|
|
48
|
+
*/
|
|
49
|
+
private getPlayableOptions;
|
|
50
|
+
/**
|
|
51
|
+
* 解析外部文件配置
|
|
52
|
+
*/
|
|
53
|
+
private resolveExternFiles;
|
|
54
|
+
/**
|
|
55
|
+
* 是否应该压缩 CSS
|
|
56
|
+
*/
|
|
57
|
+
private shouldMinifyCSS;
|
|
58
|
+
/**
|
|
59
|
+
* 是否应该压缩 JS
|
|
60
|
+
*/
|
|
61
|
+
private shouldMinifyJS;
|
|
62
|
+
/**
|
|
63
|
+
* 创建自定义 Logger
|
|
64
|
+
*/
|
|
65
|
+
private createLogger;
|
|
66
|
+
/**
|
|
67
|
+
* 创建 Rollup 警告处理器
|
|
68
|
+
*/
|
|
69
|
+
private createRollupWarnHandler;
|
|
70
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createLogger, defineConfig } from 'vite';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { viteSingleFile } from 'vite-plugin-singlefile';
|
|
5
|
+
import viteImagemin from '@vheemstra/vite-plugin-imagemin';
|
|
6
|
+
import imageminMozjpeg from 'imagemin-mozjpeg';
|
|
7
|
+
import imageminPngquant from 'imagemin-pngquant';
|
|
8
|
+
import imageminWebp from 'imagemin-webp';
|
|
9
|
+
import { PLATFORM_CONFIGS } from './platform-configs.js';
|
|
10
|
+
import { vitePlatformPlugin } from './plugin-platform.js';
|
|
11
|
+
import { createPlatformAdapter } from '../platforms/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* 通用 Vite 配置构建器(外部引擎专用)
|
|
14
|
+
*
|
|
15
|
+
* 与 PlayCanvas 专用配置构建器不同,通用版本:
|
|
16
|
+
* - 不使用 vitePlayCanvasPlugin(不需要处理 config.json 等专有格式)
|
|
17
|
+
* - 不使用 viteESMBundlePlugin 和 viteModelCompressionPlugin(Draco/Meshopt 是 PlayCanvas 特有的)
|
|
18
|
+
* - 仅使用通用的插件:
|
|
19
|
+
* - viteSingleFile:HTML 内联
|
|
20
|
+
* - vitePlatformPlugin:平台 SDK 注入
|
|
21
|
+
* - viteImagemin:图片压缩
|
|
22
|
+
*/
|
|
23
|
+
export class GenericViteConfigBuilder {
|
|
24
|
+
constructor(baseBuildDir, platform, options, baseBuildMetadata) {
|
|
25
|
+
this.baseBuildDir = baseBuildDir;
|
|
26
|
+
this.platform = platform;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.baseBuildMetadata = baseBuildMetadata;
|
|
29
|
+
this.finalOutputDir = '';
|
|
30
|
+
this.viteOutputDir = '';
|
|
31
|
+
// 统一规范化 baseBuildDir(与 ViteConfigBuilder 一致)
|
|
32
|
+
// realpathSync.native 能在 Windows 上正确解析 8.3 短路径名(如 HOMKER~1 → homkerliu)
|
|
33
|
+
// macOS 上解析符号链接。Vite 内部也使用 native realpath,必须对齐。
|
|
34
|
+
this.baseBuildDir = fs.realpathSync.native(baseBuildDir).replace(/\\/g, '/');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 创建 Vite 配置
|
|
38
|
+
*/
|
|
39
|
+
async create() {
|
|
40
|
+
console.log('[GenericViteConfigBuilder] 创建通用配置...');
|
|
41
|
+
console.log(` - 引擎: ${this.baseBuildMetadata.engine}`);
|
|
42
|
+
const platformConfig = this.getPlatformConfig();
|
|
43
|
+
const outputDir = this.options.outputDir || './dist';
|
|
44
|
+
const playableOptions = this.getPlayableOptions(platformConfig);
|
|
45
|
+
const rootDir = this.baseBuildDir;
|
|
46
|
+
const viteOutputDir = '__dist__';
|
|
47
|
+
const viteOutputDirAbsolute = path.join(rootDir, viteOutputDir);
|
|
48
|
+
const finalOutputDir = path.isAbsolute(outputDir)
|
|
49
|
+
? outputDir
|
|
50
|
+
: path.resolve(process.cwd(), outputDir);
|
|
51
|
+
this.finalOutputDir = finalOutputDir;
|
|
52
|
+
this.viteOutputDir = viteOutputDirAbsolute;
|
|
53
|
+
console.log(`[GenericViteConfigBuilder] 路径配置:`);
|
|
54
|
+
console.log(` - root: ${rootDir}`);
|
|
55
|
+
console.log(` - viteOutputDir: ${viteOutputDirAbsolute}`);
|
|
56
|
+
console.log(` - finalOutputDir: ${finalOutputDir}`);
|
|
57
|
+
return this.createConfig(platformConfig, viteOutputDir, playableOptions, rootDir);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 获取 Vite 的临时输出目录
|
|
61
|
+
*/
|
|
62
|
+
getViteOutputDir() {
|
|
63
|
+
return this.viteOutputDir || '';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 获取最终输出目录
|
|
67
|
+
*/
|
|
68
|
+
getFinalOutputDir() {
|
|
69
|
+
return this.finalOutputDir || '';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 创建通用配置
|
|
73
|
+
*/
|
|
74
|
+
createConfig(platformConfig, outputDir, playableOptions, rootDir) {
|
|
75
|
+
const logger = this.createLogger();
|
|
76
|
+
return defineConfig({
|
|
77
|
+
root: rootDir,
|
|
78
|
+
base: './',
|
|
79
|
+
customLogger: logger,
|
|
80
|
+
build: {
|
|
81
|
+
outDir: outputDir,
|
|
82
|
+
emptyOutDir: true,
|
|
83
|
+
cssMinify: this.shouldMinifyCSS(platformConfig) ? 'lightningcss' : false,
|
|
84
|
+
minify: this.shouldMinifyJS(platformConfig) ? 'terser' : false,
|
|
85
|
+
terserOptions: {
|
|
86
|
+
compress: {
|
|
87
|
+
drop_console: true,
|
|
88
|
+
drop_debugger: true,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
// 所有资源内联(生成单文件 HTML)
|
|
92
|
+
assetsInlineLimit: Infinity,
|
|
93
|
+
sourcemap: platformConfig.includeSourcemap,
|
|
94
|
+
rollupOptions: {
|
|
95
|
+
onwarn: this.createRollupWarnHandler(),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
plugins: this.createPlugins(platformConfig, playableOptions),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 创建插件列表
|
|
103
|
+
*/
|
|
104
|
+
createPlugins(platformConfig, playableOptions) {
|
|
105
|
+
const plugins = [];
|
|
106
|
+
// 1. 平台特定插件(注入平台 SDK:MRAID、CTA 等)
|
|
107
|
+
const platformAdapter = createPlatformAdapter(this.options);
|
|
108
|
+
const absoluteOutputDir = this.viteOutputDir || path.resolve(this.baseBuildDir, '__dist__');
|
|
109
|
+
plugins.push(vitePlatformPlugin({
|
|
110
|
+
platform: this.platform,
|
|
111
|
+
adapter: platformAdapter,
|
|
112
|
+
outputDir: absoluteOutputDir,
|
|
113
|
+
baseBuildDir: this.baseBuildDir,
|
|
114
|
+
outputFormat: platformConfig.outputFormat,
|
|
115
|
+
externFiles: playableOptions.externFiles,
|
|
116
|
+
mraidSupport: playableOptions.mraidSupport,
|
|
117
|
+
}));
|
|
118
|
+
// 2. 图片压缩插件
|
|
119
|
+
if (platformConfig.compressImages) {
|
|
120
|
+
const imageQuality = platformConfig.imageQuality || {
|
|
121
|
+
jpg: 75,
|
|
122
|
+
png: [0.7, 0.8],
|
|
123
|
+
webp: 75,
|
|
124
|
+
};
|
|
125
|
+
const jpegQuality = this.options.imageQuality ?? imageQuality.jpg ?? 75;
|
|
126
|
+
const pngQuality = Array.isArray(imageQuality.png) && imageQuality.png.length >= 2
|
|
127
|
+
? [imageQuality.png[0], imageQuality.png[1]]
|
|
128
|
+
: [0.7, 0.8];
|
|
129
|
+
const webpQuality = imageQuality.webp ?? 75;
|
|
130
|
+
const imageminPlugin = viteImagemin({
|
|
131
|
+
plugins: {
|
|
132
|
+
jpg: [imageminMozjpeg({ quality: jpegQuality })],
|
|
133
|
+
jpeg: [imageminMozjpeg({ quality: jpegQuality })],
|
|
134
|
+
png: [imageminPngquant({ quality: pngQuality })],
|
|
135
|
+
},
|
|
136
|
+
makeWebp: this.options.convertToWebP !== false
|
|
137
|
+
? {
|
|
138
|
+
plugins: {
|
|
139
|
+
jpg: [imageminWebp({ quality: webpQuality })],
|
|
140
|
+
jpeg: [imageminWebp({ quality: webpQuality })],
|
|
141
|
+
png: [imageminWebp({ quality: webpQuality })],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: undefined,
|
|
145
|
+
});
|
|
146
|
+
if (imageminPlugin) {
|
|
147
|
+
plugins.push(imageminPlugin);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 3. 单文件输出插件(仅 HTML 格式)
|
|
151
|
+
if (platformConfig.outputFormat === 'html') {
|
|
152
|
+
plugins.push(viteSingleFile({
|
|
153
|
+
removeViteModuleLoader: true,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
return plugins.filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 获取平台配置
|
|
160
|
+
*/
|
|
161
|
+
getPlatformConfig() {
|
|
162
|
+
const baseConfig = PLATFORM_CONFIGS[this.platform];
|
|
163
|
+
const resolvedOutputFormat = this.options.format ?? baseConfig.outputFormat;
|
|
164
|
+
return {
|
|
165
|
+
...baseConfig,
|
|
166
|
+
outputFormat: resolvedOutputFormat,
|
|
167
|
+
minifyCSS: this.options.cssMinify ?? baseConfig.minifyCSS,
|
|
168
|
+
minifyJS: this.options.jsMinify ?? baseConfig.minifyJS,
|
|
169
|
+
compressImages: this.options.compressImages ?? baseConfig.compressImages,
|
|
170
|
+
// 外部引擎不支持模型压缩
|
|
171
|
+
compressModels: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 获取 Playable 选项
|
|
176
|
+
*/
|
|
177
|
+
getPlayableOptions(config) {
|
|
178
|
+
const playable = config.playable ?? {};
|
|
179
|
+
const externFiles = this.resolveExternFiles(playable.externFiles);
|
|
180
|
+
return {
|
|
181
|
+
outputFormat: config.outputFormat,
|
|
182
|
+
patchXhrOut: this.options.patchXhrOut ?? playable.patchXhrOut ?? false,
|
|
183
|
+
inlineGameScripts: false, // 外部引擎不使用此选项
|
|
184
|
+
compressEngine: false, // 外部引擎不支持引擎压缩
|
|
185
|
+
compressConfigJson: false, // 外部引擎没有 config.json
|
|
186
|
+
configJsonInline: false,
|
|
187
|
+
externFiles,
|
|
188
|
+
mraidSupport: this.options.mraidSupport ?? playable.mraidSupport ?? false,
|
|
189
|
+
snapchatCta: this.options.snapchatCta ?? playable.snapchatCta ?? false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 解析外部文件配置
|
|
194
|
+
*/
|
|
195
|
+
resolveExternFiles(defaultValue) {
|
|
196
|
+
const optionValue = this.options.externFiles;
|
|
197
|
+
const raw = optionValue ?? defaultValue;
|
|
198
|
+
if (!raw) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
if (typeof raw === 'object') {
|
|
202
|
+
return raw;
|
|
203
|
+
}
|
|
204
|
+
const folderName = this.options.snapchat?.folderName;
|
|
205
|
+
const externalUrlPrefix = this.options.snapchat?.externalUrlPrefix;
|
|
206
|
+
return {
|
|
207
|
+
enabled: true,
|
|
208
|
+
folderName,
|
|
209
|
+
externalUrlPrefix,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 是否应该压缩 CSS
|
|
214
|
+
*/
|
|
215
|
+
shouldMinifyCSS(config) {
|
|
216
|
+
return config.minifyCSS;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 是否应该压缩 JS
|
|
220
|
+
*/
|
|
221
|
+
shouldMinifyJS(config) {
|
|
222
|
+
return config.minifyJS;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 创建自定义 Logger
|
|
226
|
+
*/
|
|
227
|
+
createLogger() {
|
|
228
|
+
const logger = createLogger();
|
|
229
|
+
return {
|
|
230
|
+
...logger,
|
|
231
|
+
warn(msg, options) {
|
|
232
|
+
if (typeof msg === 'string' && msg.includes('can\'t be bundled without type="module"')) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
logger.warn(msg, options);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 创建 Rollup 警告处理器
|
|
241
|
+
*/
|
|
242
|
+
createRollupWarnHandler() {
|
|
243
|
+
return (warning, warn) => {
|
|
244
|
+
const message = typeof warning === 'string' ? warning : warning.message;
|
|
245
|
+
if (message && message.includes('can\'t be bundled without type="module"')) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
warn(warning);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -37,6 +37,14 @@ export declare class ViteConfigBuilder {
|
|
|
37
37
|
private getPlatformConfig;
|
|
38
38
|
private shouldMinifyCSS;
|
|
39
39
|
private shouldMinifyJS;
|
|
40
|
+
/**
|
|
41
|
+
* 统一的 Terser 混淆配置
|
|
42
|
+
* 方案:开启顶层变量混淆 + 多轮压缩(不做属性混淆,避免破坏引擎运行时)
|
|
43
|
+
*
|
|
44
|
+
* 注意:当 compressJS 启用时,javascript-obfuscator 会做深度混淆,
|
|
45
|
+
* Terser 仍然负责基础的压缩和变量名混淆。
|
|
46
|
+
*/
|
|
47
|
+
private getTerserOptions;
|
|
40
48
|
private createPlugins;
|
|
41
49
|
private getPlayableOptions;
|
|
42
50
|
private resolveExternFiles;
|
|
@@ -12,6 +12,7 @@ import { vitePlatformPlugin } from './plugin-platform.js';
|
|
|
12
12
|
import { viteModelCompressionPlugin } from './plugin-model-compression.js';
|
|
13
13
|
import { viteESMBundlePlugin, bundleESMToIIFE } from './plugin-esm-html-generator.js';
|
|
14
14
|
import { viteBuildStatePlugin } from './plugin-build-state.js';
|
|
15
|
+
import { viteCompressJSPlugin } from './plugin-compress-js.js';
|
|
15
16
|
import { createPlatformAdapter } from '../platforms/index.js';
|
|
16
17
|
import { detectBuildMode, importMapToAlias } from '../utils/build-mode-detector.js';
|
|
17
18
|
export class ViteConfigBuilder {
|
|
@@ -22,6 +23,11 @@ export class ViteConfigBuilder {
|
|
|
22
23
|
this.esmBundledScript = ''; // 预先打包的 ESM 脚本
|
|
23
24
|
this.finalOutputDir = ''; // 用户指定的最终输出目录
|
|
24
25
|
this.viteOutputDir = ''; // Vite 的临时输出目录(在 root 下)
|
|
26
|
+
// 统一规范化 baseBuildDir:解析符号链接(macOS)和 Windows 8.3 短路径名
|
|
27
|
+
// 必须使用 realpathSync.native —— 只有它能在 Windows 上将 HOMKER~1 → homkerliu
|
|
28
|
+
// (fs.realpathSync 在 Windows 上不会解析 8.3 短路径)
|
|
29
|
+
// Vite 内部也使用 native realpath,所以这里必须对齐,否则 path.relative 会算错
|
|
30
|
+
this.baseBuildDir = fs.realpathSync.native(baseBuildDir).replace(/\\/g, '/');
|
|
25
31
|
}
|
|
26
32
|
async create() {
|
|
27
33
|
// 1. 检测 Base Build 模式
|
|
@@ -33,7 +39,8 @@ export class ViteConfigBuilder {
|
|
|
33
39
|
const platformConfig = this.getPlatformConfig();
|
|
34
40
|
const outputDir = this.options.outputDir || './dist';
|
|
35
41
|
const playableOptions = this.getPlayableOptions(platformConfig);
|
|
36
|
-
|
|
42
|
+
// baseBuildDir 已在构造函数中通过 realpathSync 规范化
|
|
43
|
+
const rootDir = this.baseBuildDir;
|
|
37
44
|
// 为了避免 Vite build-html 插件的路径问题,使用相对于 root 的输出目录
|
|
38
45
|
// 注意:必须使用相对路径字符串,不能用绝对路径
|
|
39
46
|
const viteOutputDir = '__dist__'; // 相对于 root 的路径
|
|
@@ -131,12 +138,7 @@ export class ViteConfigBuilder {
|
|
|
131
138
|
emptyOutDir: true,
|
|
132
139
|
cssMinify: this.shouldMinifyCSS(platformConfig) ? 'lightningcss' : false,
|
|
133
140
|
minify: this.shouldMinifyJS(platformConfig) ? 'terser' : false,
|
|
134
|
-
terserOptions:
|
|
135
|
-
compress: {
|
|
136
|
-
drop_console: true,
|
|
137
|
-
drop_debugger: true,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
141
|
+
terserOptions: this.getTerserOptions(),
|
|
140
142
|
assetsInlineLimit: Infinity,
|
|
141
143
|
sourcemap: platformConfig.includeSourcemap,
|
|
142
144
|
rollupOptions: {
|
|
@@ -166,12 +168,7 @@ export class ViteConfigBuilder {
|
|
|
166
168
|
cssMinify: this.shouldMinifyCSS(platformConfig) ? 'lightningcss' : false,
|
|
167
169
|
// JS 压缩
|
|
168
170
|
minify: this.shouldMinifyJS(platformConfig) ? 'terser' : false,
|
|
169
|
-
terserOptions:
|
|
170
|
-
compress: {
|
|
171
|
-
drop_console: true,
|
|
172
|
-
drop_debugger: true,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
171
|
+
terserOptions: this.getTerserOptions(),
|
|
175
172
|
// 资源内联阈值(所有资源都内联)
|
|
176
173
|
assetsInlineLimit: Infinity,
|
|
177
174
|
// 不生成 sourcemap(Playable Ads 不需要)
|
|
@@ -228,6 +225,28 @@ export class ViteConfigBuilder {
|
|
|
228
225
|
shouldMinifyJS(config) {
|
|
229
226
|
return config.minifyJS;
|
|
230
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* 统一的 Terser 混淆配置
|
|
230
|
+
* 方案:开启顶层变量混淆 + 多轮压缩(不做属性混淆,避免破坏引擎运行时)
|
|
231
|
+
*
|
|
232
|
+
* 注意:当 compressJS 启用时,javascript-obfuscator 会做深度混淆,
|
|
233
|
+
* Terser 仍然负责基础的压缩和变量名混淆。
|
|
234
|
+
*/
|
|
235
|
+
getTerserOptions() {
|
|
236
|
+
return {
|
|
237
|
+
compress: {
|
|
238
|
+
drop_console: true,
|
|
239
|
+
drop_debugger: true,
|
|
240
|
+
passes: 3,
|
|
241
|
+
},
|
|
242
|
+
mangle: {
|
|
243
|
+
toplevel: true,
|
|
244
|
+
},
|
|
245
|
+
format: {
|
|
246
|
+
comments: false,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
231
250
|
createPlugins(platformConfig, outputDir, playableOptions, isESMBundle = false) {
|
|
232
251
|
const plugins = [];
|
|
233
252
|
// 0. ESM Bundle 插件(如果是 ESM 模式且需要转换为 IIFE)
|
|
@@ -263,6 +282,12 @@ export class ViteConfigBuilder {
|
|
|
263
282
|
outputFormat: platformConfig.outputFormat,
|
|
264
283
|
externFiles: playableOptions.externFiles,
|
|
265
284
|
mraidSupport: playableOptions.mraidSupport,
|
|
285
|
+
// Google Ads ZIP 验证器只允许 .css/.js/.html/.gif/.png/.jpeg/.svg
|
|
286
|
+
// 将不受支持的文件类型(如 .glb)内联为 base64 到 config.json 中
|
|
287
|
+
inlineUnsupportedAssets: this.platform === 'google',
|
|
288
|
+
// ZIP 格式输出时,对复制的 JS 文件进行 Terser 压缩混淆
|
|
289
|
+
minifyJSInZip: platformConfig.outputFormat === 'zip' && platformConfig.minifyJS,
|
|
290
|
+
ammoReplacement: this.options.ammoReplacement,
|
|
266
291
|
}));
|
|
267
292
|
// 3. 图片压缩插件
|
|
268
293
|
if (platformConfig.compressImages) {
|
|
@@ -315,19 +340,31 @@ export class ViteConfigBuilder {
|
|
|
315
340
|
removeViteModuleLoader: true,
|
|
316
341
|
}));
|
|
317
342
|
}
|
|
343
|
+
// 7. JS 压缩保护插件(在单文件输出之后,对最终 HTML 中的所有 JS 进行 LZ4 压缩)
|
|
344
|
+
if (playableOptions.compressJS) {
|
|
345
|
+
plugins.push(viteCompressJSPlugin({
|
|
346
|
+
enabled: true,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
318
349
|
return plugins.filter(Boolean);
|
|
319
350
|
}
|
|
320
351
|
getPlayableOptions(config) {
|
|
321
352
|
const playable = config.playable ?? {};
|
|
322
353
|
const externFiles = this.resolveExternFiles(playable.externFiles);
|
|
354
|
+
// 当输出格式为 HTML 时(尤其是平台默认 ZIP 但用户选了 HTML),
|
|
355
|
+
// 需要自动启用 HTML 单文件所必需的选项
|
|
356
|
+
const isHtmlOutput = config.outputFormat === 'html';
|
|
357
|
+
// HTML 单文件格式必须内联游戏脚本,否则脚本 URL 无法加载
|
|
358
|
+
const htmlRequiresInlineScripts = isHtmlOutput;
|
|
323
359
|
return {
|
|
324
360
|
outputFormat: config.outputFormat,
|
|
325
361
|
patchXhrOut: this.options.patchXhrOut ?? playable.patchXhrOut ?? false,
|
|
326
|
-
inlineGameScripts: this.options.inlineGameScripts ?? playable.inlineGameScripts ?? false,
|
|
362
|
+
inlineGameScripts: this.options.inlineGameScripts ?? (htmlRequiresInlineScripts || (playable.inlineGameScripts ?? false)),
|
|
327
363
|
compressEngine: this.options.compressEngine ?? playable.compressEngine ?? false,
|
|
328
364
|
compressConfigJson: this.options.compressConfigJson ?? playable.compressConfigJson ?? false,
|
|
329
|
-
|
|
330
|
-
|
|
365
|
+
compressJS: this.options.compressJS ?? false,
|
|
366
|
+
configJsonInline: playable.configJsonInline ?? isHtmlOutput,
|
|
367
|
+
externFiles: isHtmlOutput ? undefined : externFiles,
|
|
331
368
|
mraidSupport: this.options.mraidSupport ?? playable.mraidSupport ?? false,
|
|
332
369
|
snapchatCta: this.options.snapchatCta ?? playable.snapchatCta ?? false,
|
|
333
370
|
};
|