@playcraft/build 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/analyzers/scene-asset-collector.js +99 -9
  2. package/dist/base-builder.d.ts +15 -78
  3. package/dist/base-builder.js +34 -741
  4. package/dist/engines/engine-detector.d.ts +38 -0
  5. package/dist/engines/engine-detector.js +201 -0
  6. package/dist/engines/generic-adapter.d.ts +71 -0
  7. package/dist/engines/generic-adapter.js +378 -0
  8. package/dist/engines/index.d.ts +7 -0
  9. package/dist/engines/index.js +7 -0
  10. package/dist/engines/playcanvas-adapter.d.ts +85 -0
  11. package/dist/engines/playcanvas-adapter.js +813 -0
  12. package/dist/generators/config-generator.js +59 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/loaders/playcraft-loader.js +240 -5
  16. package/dist/platforms/adikteev.d.ts +1 -1
  17. package/dist/platforms/adikteev.js +30 -36
  18. package/dist/platforms/applovin.d.ts +1 -1
  19. package/dist/platforms/applovin.js +31 -36
  20. package/dist/platforms/base.d.ts +27 -5
  21. package/dist/platforms/base.js +79 -181
  22. package/dist/platforms/bigo.d.ts +1 -1
  23. package/dist/platforms/bigo.js +28 -28
  24. package/dist/platforms/facebook.d.ts +1 -1
  25. package/dist/platforms/facebook.js +21 -10
  26. package/dist/platforms/google.d.ts +1 -1
  27. package/dist/platforms/google.js +28 -21
  28. package/dist/platforms/index.d.ts +1 -0
  29. package/dist/platforms/index.js +4 -0
  30. package/dist/platforms/inmobi.d.ts +1 -1
  31. package/dist/platforms/inmobi.js +27 -34
  32. package/dist/platforms/ironsource.d.ts +1 -1
  33. package/dist/platforms/ironsource.js +37 -40
  34. package/dist/platforms/liftoff.d.ts +1 -1
  35. package/dist/platforms/liftoff.js +22 -30
  36. package/dist/platforms/mintegral.d.ts +10 -0
  37. package/dist/platforms/mintegral.js +65 -0
  38. package/dist/platforms/moloco.d.ts +1 -1
  39. package/dist/platforms/moloco.js +18 -20
  40. package/dist/platforms/playcraft.d.ts +1 -1
  41. package/dist/platforms/playcraft.js +2 -2
  42. package/dist/platforms/remerge.d.ts +1 -1
  43. package/dist/platforms/remerge.js +19 -20
  44. package/dist/platforms/snapchat.d.ts +1 -1
  45. package/dist/platforms/snapchat.js +32 -26
  46. package/dist/platforms/tiktok.d.ts +1 -1
  47. package/dist/platforms/tiktok.js +28 -24
  48. package/dist/platforms/unity.d.ts +1 -1
  49. package/dist/platforms/unity.js +30 -36
  50. package/dist/playable-builder.d.ts +1 -0
  51. package/dist/playable-builder.js +16 -2
  52. package/dist/templates/__loading__.js +100 -0
  53. package/dist/templates/__modules__.js +47 -0
  54. package/dist/templates/__settings__.template.js +20 -0
  55. package/dist/templates/__start__.js +332 -0
  56. package/dist/templates/index.html +18 -0
  57. package/dist/templates/logo.png +0 -0
  58. package/dist/templates/manifest.json +1 -0
  59. package/dist/templates/patches/cannon.min.js +28 -0
  60. package/dist/templates/patches/lz4.js +10 -0
  61. package/dist/templates/patches/one-page-http-get.js +20 -0
  62. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  63. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  64. package/dist/templates/patches/p2.min.js +27 -0
  65. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  66. package/dist/templates/playcanvas-stable.min.js +16363 -0
  67. package/dist/templates/styles.css +43 -0
  68. package/dist/types.d.ts +113 -1
  69. package/dist/types.js +77 -1
  70. package/dist/utils/ammo-detector.d.ts +9 -0
  71. package/dist/utils/ammo-detector.js +76 -0
  72. package/dist/utils/build-mode-detector.js +2 -0
  73. package/dist/utils/minify.d.ts +32 -0
  74. package/dist/utils/minify.js +82 -0
  75. package/dist/vite/config-builder-generic.d.ts +70 -0
  76. package/dist/vite/config-builder-generic.js +251 -0
  77. package/dist/vite/config-builder.d.ts +8 -0
  78. package/dist/vite/config-builder.js +53 -16
  79. package/dist/vite/platform-configs.js +29 -1
  80. package/dist/vite/plugin-compress-js.d.ts +21 -0
  81. package/dist/vite/plugin-compress-js.js +213 -0
  82. package/dist/vite/plugin-esm-html-generator.js +5 -1
  83. package/dist/vite/plugin-platform.d.ts +5 -0
  84. package/dist/vite/plugin-platform.js +499 -35
  85. package/dist/vite/plugin-playcanvas.js +21 -68
  86. package/dist/vite/plugin-source-builder.js +102 -21
  87. package/dist/vite-builder.d.ts +25 -7
  88. package/dist/vite-builder.js +141 -52
  89. package/package.json +4 -2
  90. package/physics/cannon-rigidbody-adapter.js +243 -22
  91. package/templates/__loading__.js +0 -12
  92. package/templates/index.esm.mjs +0 -11
  93. package/templates/patches/playcraft-cta-adapter.js +129 -31
  94. package/templates/patches/scene-physics-defaults.js +49 -0
  95. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  96. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -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
- const rootDir = fs.realpathSync(this.baseBuildDir);
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
- configJsonInline: playable.configJsonInline ?? (config.outputFormat === 'html'),
330
- externFiles,
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
  };
@@ -22,7 +22,7 @@ export const PLATFORM_CONFIGS = {
22
22
  },
23
23
  esmSupport: {
24
24
  enabled: true,
25
- preferIIFE: false, // 通用构建支持现代 ESM
25
+ preferIIFE: true, // 使用 IIFE 确保 pc 全局可用,避免 ESM 模块作用域问题
26
26
  },
27
27
  playable: {
28
28
  patchXhrOut: false,
@@ -415,4 +415,32 @@ export const PLATFORM_CONFIGS = {
415
415
  configJsonInline: true,
416
416
  },
417
417
  },
418
+ mintegral: {
419
+ sizeLimit: 5 * 1024 * 1024, // 5MB (ZIP)
420
+ outputFormat: 'zip',
421
+ minifyCSS: true,
422
+ minifyJS: true,
423
+ compressImages: false, // 禁用图片压缩,原图输出效果更好
424
+ compressModels: true,
425
+ injectScripts: ['mintegralSdk'],
426
+ outputFileName: 'index.html',
427
+ includeSourcemap: false,
428
+ imageQuality: {
429
+ jpg: 80,
430
+ png: [0.75, 0.85],
431
+ webp: 80,
432
+ },
433
+ modelCompression: {
434
+ method: 'draco',
435
+ quality: 0.85,
436
+ },
437
+ esmSupport: {
438
+ enabled: true,
439
+ preferIIFE: true, // Mintegral 推荐 IIFE 确保兼容性
440
+ },
441
+ playable: {
442
+ inlineGameScripts: true,
443
+ externFiles: true, // Mintegral ZIP 支持外部文件(但非 JS/HTML 资源需 base64)
444
+ },
445
+ },
418
446
  };
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface CompressJSPluginOptions {
3
+ /** 是否启用 JS 代码混淆保护 */
4
+ enabled: boolean;
5
+ }
6
+ /**
7
+ * JS 代码混淆保护插件
8
+ *
9
+ * 使用 javascript-obfuscator 对 HTML 中所有内联 <script> 标签的 JS 代码
10
+ * 进行深度混淆,包括:
11
+ * - 变量名混淆
12
+ * - 字符串混淆(字面量转为十六进制/unicode 编码)
13
+ * - 控制流平坦化
14
+ * - 死代码注入
15
+ *
16
+ * 混淆后的代码在浏览器中无法直接阅读理解。
17
+ *
18
+ * 注意:这个插件必须在 vite-plugin-singlefile 之后执行,
19
+ * 确保所有资源已内联到 HTML 中。
20
+ */
21
+ export declare function viteCompressJSPlugin(options: CompressJSPluginOptions): Plugin | null;
@@ -0,0 +1,213 @@
1
+ import JavaScriptObfuscator from 'javascript-obfuscator';
2
+ import { minify } from 'terser';
3
+ /**
4
+ * JS 代码混淆保护插件
5
+ *
6
+ * 使用 javascript-obfuscator 对 HTML 中所有内联 <script> 标签的 JS 代码
7
+ * 进行深度混淆,包括:
8
+ * - 变量名混淆
9
+ * - 字符串混淆(字面量转为十六进制/unicode 编码)
10
+ * - 控制流平坦化
11
+ * - 死代码注入
12
+ *
13
+ * 混淆后的代码在浏览器中无法直接阅读理解。
14
+ *
15
+ * 注意:这个插件必须在 vite-plugin-singlefile 之后执行,
16
+ * 确保所有资源已内联到 HTML 中。
17
+ */
18
+ export function viteCompressJSPlugin(options) {
19
+ if (!options.enabled) {
20
+ return null;
21
+ }
22
+ return {
23
+ name: 'vite-plugin-compress-js',
24
+ enforce: 'post',
25
+ async generateBundle(_outputOptions, bundle) {
26
+ for (const [fileName, asset] of Object.entries(bundle)) {
27
+ if (!fileName.endsWith('.html'))
28
+ continue;
29
+ if (asset.type !== 'asset')
30
+ continue;
31
+ const html = typeof asset.source === 'string'
32
+ ? asset.source
33
+ : new TextDecoder().decode(asset.source);
34
+ console.log('[CompressJS] 开始混淆 HTML 中的 JS 代码...');
35
+ const result = await obfuscateAllInlineScripts(html);
36
+ if (result.obfuscatedCount > 0) {
37
+ asset.source = result.html;
38
+ console.log(`[CompressJS] 混淆完成: ${result.obfuscatedCount} 个脚本块`);
39
+ console.log(`[CompressJS] 原始大小: ${formatBytes(result.originalSize)}`);
40
+ console.log(`[CompressJS] 混淆后大小: ${formatBytes(result.obfuscatedSize)}`);
41
+ }
42
+ else {
43
+ console.log('[CompressJS] 未找到需要混淆的脚本块');
44
+ }
45
+ }
46
+ },
47
+ };
48
+ }
49
+ /**
50
+ * 检测一个 <script> 标签内容是否是 lz4.js 运行时
51
+ */
52
+ function isLz4Runtime(content) {
53
+ return content.includes('window.lz4') && content.includes('window.Buffer') && content.includes('lz4js');
54
+ }
55
+ /**
56
+ * 检测一个 <script> 标签内容是否是 LZ4 自解压 bootstrap(由 compressEngine 生成)
57
+ */
58
+ function isLz4Bootstrap(content) {
59
+ return content.includes('lz4.decompress') && content.includes('new Buffer(') && content.includes('textContent');
60
+ }
61
+ /**
62
+ * 混淆 HTML 中所有内联 <script> 标签的 JS 代码
63
+ */
64
+ async function obfuscateAllInlineScripts(html) {
65
+ // 阶段 1:收集所有需要混淆的脚本块及其位置
66
+ const scriptRegex = /<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/gi;
67
+ const entries = [];
68
+ let match;
69
+ while ((match = scriptRegex.exec(html)) !== null) {
70
+ const fullMatch = match[0];
71
+ const content = match[1];
72
+ const tagAttributes = fullMatch.substring(0, fullMatch.indexOf('>'));
73
+ let skip = false;
74
+ // 跳过有 src 属性的外部脚本
75
+ if (/\bsrc\s*=/i.test(tagAttributes))
76
+ skip = true;
77
+ // 跳过 type 不是 javascript 的
78
+ if (!skip) {
79
+ const typeMatch = tagAttributes.match(/\btype\s*=\s*["']([^"']+)["']/i);
80
+ if (typeMatch) {
81
+ const type = typeMatch[1].toLowerCase();
82
+ if (type !== 'text/javascript' && type !== 'application/javascript' && type !== '') {
83
+ skip = true;
84
+ }
85
+ }
86
+ }
87
+ // 跳过空脚本
88
+ if (!skip && !content.trim())
89
+ skip = true;
90
+ // 跳过 lz4.js 运行时和 bootstrap
91
+ if (!skip && (isLz4Runtime(content) || isLz4Bootstrap(content)))
92
+ skip = true;
93
+ // 跳过太短的脚本
94
+ if (!skip && content.trim().length < 100)
95
+ skip = true;
96
+ entries.push({
97
+ fullMatch,
98
+ content,
99
+ index: match.index,
100
+ skip,
101
+ replacement: fullMatch,
102
+ });
103
+ }
104
+ // 阶段 2:混淆 + 二次压缩(async)
105
+ let obfuscatedCount = 0;
106
+ let originalSize = 0;
107
+ let obfuscatedSize = 0;
108
+ for (const entry of entries) {
109
+ if (entry.skip)
110
+ continue;
111
+ try {
112
+ const originalLen = Buffer.from(entry.content).length;
113
+ originalSize += originalLen;
114
+ console.log(`[CompressJS] 混淆脚本 (${formatBytes(originalLen)})...`);
115
+ // javascript-obfuscator:标识符混淆(关闭 stringArray 避免体积膨胀和运行时冲突)
116
+ const obfuscatedRaw = JavaScriptObfuscator.obfuscate(entry.content, {
117
+ compact: true,
118
+ controlFlowFlattening: false,
119
+ deadCodeInjection: false,
120
+ identifierNamesGenerator: 'hexadecimal',
121
+ stringArray: false,
122
+ splitStrings: false,
123
+ numbersToExpressions: false,
124
+ transformObjectKeys: false,
125
+ unicodeEscapeSequence: false,
126
+ disableConsoleOutput: false,
127
+ selfDefending: false,
128
+ target: 'browser',
129
+ // ========== 保留名单 ==========
130
+ reservedNames: [
131
+ '^pc$', '^app$', '^entity$', '^pcBootstrap$',
132
+ '^ASSET_PREFIX$', '^SCRIPT_PREFIX$', '^SCENE_PATH$',
133
+ '^CONTEXT_OPTIONS$', '^SCRIPTS$', '^INPUT_SETTINGS$',
134
+ '^PRELOAD_MODULES$', '^CONFIG_FILENAME$',
135
+ '^PlayCraftCTA$', '^jump2AppStore$', '^loadModules$',
136
+ '^__esmScriptClasses$', '^__esmScriptSchemas$',
137
+ '^__deferredESMScripts$', '^__pendingScriptRegistrations$',
138
+ '^Application$', '^Entity$', '^ScriptHandler$',
139
+ '^ScriptType$', '^Http$', '^Asset$', '^AssetRegistry$',
140
+ '^GraphNode$', '^Vec2$', '^Vec3$', '^Vec4$', '^Quat$',
141
+ '^Mat4$', '^Color$', '^Curve$', '^CurveSet$',
142
+ '^BoundingBox$', '^BoundingSphere$', '^Ray$',
143
+ '^Texture$', '^Material$', '^StandardMaterial$',
144
+ '^Mesh$', '^MeshInstance$', '^Model$',
145
+ '^AnimComponent$', '^RigidBodyComponent$',
146
+ '^CollisionComponent$', '^ElementComponent$',
147
+ '^SpriteComponent$', '^ParticleSystemComponent$',
148
+ '^ScreenComponent$', '^LayoutGroupComponent$',
149
+ '^ScrollViewComponent$', '^ButtonComponent$',
150
+ '^CameraComponent$', '^LightComponent$',
151
+ '^RenderComponent$', '^SoundComponent$',
152
+ '^ScriptComponent$',
153
+ '^GameplaySystem$', '^GameRule$',
154
+ '^FbPlayableAd$', '^mraid$', '^dapi$', '^ExitApi$',
155
+ '^BGY_MRAID$', '^snapchatCta$', '^openAppStore$',
156
+ '^install$', '^TJ_API$', '^NUC$', '^smxTracking$',
157
+ ],
158
+ }).getObfuscatedCode();
159
+ // Terser 二次压缩:只做 compress,不做 mangle
160
+ let obfuscated = obfuscatedRaw;
161
+ try {
162
+ const terserResult = await minify(obfuscatedRaw, {
163
+ compress: {
164
+ passes: 2,
165
+ drop_console: true,
166
+ drop_debugger: true,
167
+ },
168
+ mangle: false,
169
+ format: {
170
+ comments: false,
171
+ },
172
+ });
173
+ if (terserResult.code) {
174
+ obfuscated = terserResult.code;
175
+ console.log(`[CompressJS] 二次压缩: ${formatBytes(Buffer.from(obfuscatedRaw).length)} → ${formatBytes(Buffer.from(obfuscated).length)}`);
176
+ }
177
+ }
178
+ catch (e) {
179
+ console.warn(`[CompressJS] 二次压缩失败,使用混淆后原始代码: ${e.message}`);
180
+ }
181
+ const obfuscatedLen = Buffer.from(obfuscated).length;
182
+ obfuscatedSize += obfuscatedLen;
183
+ obfuscatedCount++;
184
+ const tagOpen = entry.fullMatch.substring(0, entry.fullMatch.indexOf('>') + 1);
185
+ entry.replacement = `${tagOpen}${obfuscated}</script>`;
186
+ }
187
+ catch (error) {
188
+ console.warn(`[CompressJS] 混淆脚本失败: ${error.message}`);
189
+ }
190
+ }
191
+ // 阶段 3:从后往前替换,避免偏移量变化
192
+ let newHtml = html;
193
+ for (let i = entries.length - 1; i >= 0; i--) {
194
+ const entry = entries[i];
195
+ if (entry.skip)
196
+ continue;
197
+ newHtml = newHtml.substring(0, entry.index) + entry.replacement + newHtml.substring(entry.index + entry.fullMatch.length);
198
+ }
199
+ return {
200
+ html: newHtml,
201
+ obfuscatedCount,
202
+ originalSize,
203
+ obfuscatedSize,
204
+ };
205
+ }
206
+ function formatBytes(bytes) {
207
+ if (bytes === 0)
208
+ return '0 B';
209
+ const k = 1024;
210
+ const sizes = ['B', 'KB', 'MB', 'GB'];
211
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
212
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
213
+ }