@playcraft/devkit 1.0.16 → 1.0.17

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 (45) hide show
  1. package/README.md +259 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +122 -17
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +7 -1
  6. package/cli/commands/pack.js +308 -39
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/batch-build.js +1091 -124
  9. package/core/cos-uploader.js +264 -20
  10. package/core/index.js +58 -4
  11. package/core/loaders/gltf-loader.js +74 -28
  12. package/core/options.js +310 -15
  13. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  14. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  15. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  16. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  17. package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
  18. package/core/plugins/FflateCompressionPlugin.js +225 -37
  19. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  20. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  21. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  22. package/core/plugins/PangleInjectorPlugin.js +24 -2
  23. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  24. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  25. package/core/plugins/UnityInjectorPlugin.js +37 -8
  26. package/core/plugins/ZipPlugin.js +138 -19
  27. package/core/utils/buildDefines.js +29 -2
  28. package/core/utils/buildTemplateString.js +40 -5
  29. package/core/utils/date.js +16 -1
  30. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  31. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  32. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  33. package/core/utils/injectSDKPlugins.js +58 -11
  34. package/core/utils/logOptions.js +37 -4
  35. package/core/utils/mergeOptions.js +19 -2
  36. package/core/utils/parseArgvOptions.js +110 -5
  37. package/core/utils/resolveChannelFold2Zip.js +15 -3
  38. package/core/utils/validateThemeData.js +220 -25
  39. package/core/validators/pre-build-checker.js +201 -21
  40. package/core/validators/tracking-validator.js +358 -40
  41. package/core/vite.dev.js +181 -15
  42. package/core/webpack.build.js +446 -52
  43. package/core/webpack.common.js +177 -11
  44. package/core/webpack.dev.js +82 -5
  45. package/package.json +3 -2
@@ -1,62 +1,456 @@
1
- "use strict";const{merge}=require("webpack-merge");const{webpackCommonConfig}=require("./webpack.common.js");const webpack=require("webpack");const TerserPlugin=require("terser-webpack-plugin");const CopyWebpackPlugin=require("copy-webpack-plugin");const HtmlInlineScriptPlugin=require("html-inline-script-webpack-plugin");const HtmlWebpackPlugin=require("html-webpack-plugin");const MiniCssExtractPlugin=require("mini-css-extract-plugin");const{ZipPlugin}=require("./plugins/ZipPlugin.js");const{FflateCompressionPlugin}=require("./plugins/FflateCompressionPlugin.js");const{generateAdikteevHtmlWebpackPluginConfig}=require("./utils/generateAdikteevHtmlWebpackPluginConfig.js");const{generateBigabidHtmlWebpackPluginConfig}=require("./utils/generateBigabidHtmlWebpackPluginConfig.js");const{generateInMobiHtmlWebpackPluginConfig}=require("./utils/generateInMobiHtmlWebpackPluginConfig.js");const path=require("path");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{buildDefines}=require("./utils/buildDefines.js");const{buildTemplateString}=require("./utils/buildTemplateString.js");const{logOptions}=require("./utils/logOptions.js");const{injectSDKPlugins}=require("./utils/injectSDKPlugins.js");const{resolveChannelFold2Zip}=require("./utils/resolveChannelFold2Zip.js");/** @type {AD_NETWORK[]} *//** @type {AD_NETWORK[]} */const zipOutputNetworks=["google","pangle","tiktok","vungle","mytarget","mintegral","adikteev","bigabid","inmobi",// InMobi 支持 ZIP 格式(包含 index.html + 资源文件)
2
- "snapchat","bigoads","liftoff"];/** @type {AD_NETWORK[]} */const zipOutputAllowedNetworks=["facebook","moloco"];/**
1
+ const { merge } = require('webpack-merge');
2
+ const { webpackCommonConfig } = require('./webpack.common.js');
3
+ const webpack = require('webpack');
4
+ const TerserPlugin = require('terser-webpack-plugin');
5
+ const CopyWebpackPlugin = require('copy-webpack-plugin');
6
+ const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');
7
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
8
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
9
+ const { ZipPlugin } = require('./plugins/ZipPlugin.js');
10
+ const { FflateCompressionPlugin } = require('./plugins/FflateCompressionPlugin.js');
11
+ const { generateAdikteevHtmlWebpackPluginConfig } = require('./utils/generateAdikteevHtmlWebpackPluginConfig.js');
12
+ const { generateBigabidHtmlWebpackPluginConfig } = require('./utils/generateBigabidHtmlWebpackPluginConfig.js');
13
+ const { generateInMobiHtmlWebpackPluginConfig } = require('./utils/generateInMobiHtmlWebpackPluginConfig.js');
14
+ const path = require('path');
15
+ const { mergeOptions } = require('./utils/mergeOptions.js');
16
+ const { options } = require('./options.js');
17
+ const { buildDefines } = require('./utils/buildDefines.js');
18
+ const { buildTemplateString } = require('./utils/buildTemplateString.js');
19
+ const { logOptions } = require('./utils/logOptions.js');
20
+ const { injectSDKPlugins } = require('./utils/injectSDKPlugins.js');
21
+ const { resolveChannelFold2Zip } = require('./utils/resolveChannelFold2Zip.js');
22
+
23
+
24
+ /** @type {AD_NETWORK[]} */
25
+ /** @type {AD_NETWORK[]} */
26
+ const zipOutputNetworks = [
27
+ 'google',
28
+ 'pangle',
29
+ 'tiktok',
30
+ 'vungle',
31
+ 'mytarget',
32
+ 'mintegral',
33
+ 'adikteev',
34
+ 'bigabid',
35
+ 'inmobi', // InMobi 支持 ZIP 格式(包含 index.html + 资源文件)
36
+ 'snapchat',
37
+ 'bigoads',
38
+ 'liftoff'
39
+ ];
40
+ /** @type {AD_NETWORK[]} */
41
+ const zipOutputAllowedNetworks = ['facebook', 'moloco'];
42
+
43
+ /**
3
44
  * Creates webpack configuration for production build
4
45
  * @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
5
46
  * @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
6
47
  * @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
7
48
  * @returns {import('webpack').Configuration} Final webpack production configuration
8
- */function makeWebpackBuildConfig(customOptions,customDefines,webpackCustomConfig){const buildOptions=mergeOptions(options,customOptions);logOptions(buildOptions);customDefines=customDefines||{};webpackCustomConfig=webpackCustomConfig||{};/** @type {AD_NETWORK} */const adNetwork=buildOptions["network"];/** @type {AD_PROTOCOL} */const adProtocol=buildOptions["protocol"];let outDir=buildTemplateString(buildOptions.outDir,buildOptions);function getFileName(){let filename=buildTemplateString(buildOptions.filename,buildOptions);if(adNetwork==="mintegral")return filename.replace(/\s+/g,"").replace(/[^a-zA-Z0-9]/g,"_").replace("_fullhash_6_","[fullhash:6]");return filename}const isZipOutput=zipOutputNetworks.includes(adNetwork)||buildOptions.zip&&zipOutputAllowedNetworks.includes(adNetwork);const{isChannelFold2Zip,isZipOutputFinal}=resolveChannelFold2Zip({isChannelFold2ZipOption:buildOptions.isChannelFold2Zip,adNetwork,isZipOutput});let htmlFileName="";if(adNetwork==="mintegral")htmlFileName="index.html";// 修改为 index.html
9
- else if(isZipOutputFinal)htmlFileName="index.html";else htmlFileName=`${getFileName()}.html`;const metaTags={viewport:"width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1.0,user-scalable=no"};if(adNetwork==="mintegral"){metaTags["viewport"]="width=device-width,user-scalable=no,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0"}if(!buildOptions.skipRecommendedMeta){metaTags["HandheldFriendly"]="True";metaTags["cleartype"]={"http-equiv":"cleartype",content:"on"};metaTags["apple-mobile-web-app-capable"]="yes";metaTags["mobile-web-app-capable"]="yes";metaTags["X-UA-Compatible"]={"http-equiv":"X-UA-Compatible",content:"IE=10"}}let htmlWebpackPluginConfig={template:path.resolve("src/index.html"),filename:htmlFileName,title:`${buildOptions.name} - ${buildOptions.app}`,inlineSource:".(js|css|png|jpg|webp|svg|xml|atlas|mp3|gif|glb|fbx|obj)$",meta:metaTags};if(adNetwork==="adikteev")htmlWebpackPluginConfig=generateAdikteevHtmlWebpackPluginConfig("src/index.html");else if(adNetwork==="bigabid")htmlWebpackPluginConfig=generateBigabidHtmlWebpackPluginConfig("src/index.html");else if(adNetwork==="inmobi")htmlWebpackPluginConfig=generateInMobiHtmlWebpackPluginConfig("src/index.html",buildOptions);const webpackConfig=merge(webpackCommonConfig,{mode:"production",stats:"errors-only",optimization:{usedExports:true,minimize:(buildOptions.obfuscateLevel!==undefined?buildOptions.obfuscateLevel:2)>=1,minimizer:[new TerserPlugin({extractComments:false,terserOptions:{safari10:true,mangle:{reserved:["$"],// ⚠️ 不要开启 properties: true,会混淆 Phaser 等框架的属性名导致运行时错误
10
- // properties: true,
11
- toplevel:true},compress:{drop_console:true,arrows:false,drop_debugger:true,unused:true,dead_code:true,// 🔥 增加优化轮数
12
- passes:3,// 🔥 激进压缩选项
13
- inline:3,// 最大内联
14
- collapse_vars:true,// 折叠变量
15
- reduce_vars:true,// 减少变量
16
- hoist_vars:true,// 提升变量
17
- hoist_funs:true,// 提升函数
18
- sequences:20,// 更长的序列化
19
- properties:true,// 优化属性访问
20
- conditionals:true,// 优化条件
21
- comparisons:true,// 优化比较
22
- booleans:true,// 优化布尔
23
- loops:true,// 优化循环
24
- if_return:true,// 优化 if-return
25
- join_vars:true,// 合并变量
26
- negate_iife:true,// 否定 IIFE
27
- side_effects:true,// 移除副作用
28
- switches:true,// 优化 switch
29
- typeofs:true,// 优化 typeof
30
- unsafe:false,// 不使用 unsafe(避免破坏框架)
31
- keep_fargs:false,// 移除未使用参数
32
- drop_console:true},output:{comments:false,quote_style:3,beautify:false,wrap_iife:true,ecma:2015},toplevel:true,keep_classnames:false,keep_fnames:false}})]},plugins:[new HtmlWebpackPlugin(htmlWebpackPluginConfig),new webpack.DefinePlugin({...buildDefines(),...buildOptions.defines,...customDefines}),// For some reason needed for PIXI v8
33
- new webpack.optimize.LimitChunkCountPlugin({maxChunks:1})]},webpackCustomConfig);webpackConfig.output.path=path.resolve(outDir);// 根据混淆等级注入 obfuscator loader
34
- const obfuscateLevel=buildOptions.obfuscateLevel!==undefined?buildOptions.obfuscateLevel:2;// 获取需要排除压缩的目录列表
35
- const excludeCompressDirs=buildOptions.excludeCompressDirs||[];if(obfuscateLevel>=2){// 构建各等级的 obfuscator 配置
36
- const obfuscatorOptions={// 通用基础配置
37
- target:"browser",sourceMap:false,selfDefending:false,renameProperties:false,disableConsoleOutput:true,splitStrings:false,unicodeEscapeSequence:false};if(obfuscateLevel===2){// 等级2:基础混淆(控制流平坦化 + 字符串数组)
38
- Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:0.75,deadCodeInjection:false,stringArray:true,stringArrayThreshold:0.75,stringArrayEncoding:["base64"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:2,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:0.75,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal"})}else if(obfuscateLevel===3){// 等级3:在等级2基础上增加死代码注入
39
- Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:0.75,deadCodeInjection:true,deadCodeInjectionThreshold:0.4,stringArray:true,stringArrayThreshold:0.75,stringArrayEncoding:["base64"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:2,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:0.75,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal"})}else if(obfuscateLevel===4){// 等级4:最高强度混淆
40
- Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:1,deadCodeInjection:true,deadCodeInjectionThreshold:0.5,stringArray:true,stringArrayThreshold:1,stringArrayEncoding:["rc4"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:3,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:1,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal",splitStrings:true,splitStringsChunkLength:5,unicodeEscapeSequence:true})}console.log(`Obfuscation level: ${obfuscateLevel} (${["","terser-only","basic","dead-code","maximum"][obfuscateLevel]})`);// 构建exclude数组,包含node_modules和用户指定的排除目录
41
- const excludePatterns=[];if(excludeCompressDirs.length>0){excludeCompressDirs.forEach(dir=>{excludePatterns.push(path.resolve(dir))});console.log(`ℹ️ Excluding directories from compression: ${excludeCompressDirs.join(", ")}`)}webpackConfig.module.rules.push({test:/\.(js|ts)$/,include:[path.resolve("src")],exclude:excludePatterns,enforce:"post",use:{loader:require.resolve("webpack-obfuscator/dist/loader"),options:obfuscatorOptions}})}else if(obfuscateLevel===1){console.log("Obfuscation level: 1 (terser-only)")}else{console.log("Obfuscation level: 0 (no minimize/obfuscate)")}// Adikteev 需要单独提取 CSS 为 style.css,替换默认的 style-loader
42
- if(adNetwork==="adikteev"){const cssRuleIndex=webpackConfig.module.rules.findIndex(rule=>rule.test&&rule.test.toString().includes("\\.css"));if(cssRuleIndex!==-1){webpackConfig.module.rules[cssRuleIndex]={test:/\.css$/,use:[MiniCssExtractPlugin.loader,"css-loader"]};console.log("\u2705 Adikteev: CSS will be extracted to style.css")}}// 决定是否内联 JS 到 HTML
43
- // 以下渠道需要分离 JS 文件,不使用 HtmlInlineScriptPlugin:
44
- // - mintegral: 需要独立的 JS 文件
45
- // - adikteev: 需要 creative.js 上传到 CDN
46
- // - bigabid: 需要 main.js 上传到 CDN
47
- // - facebook (ZIP 模式): Facebook ZIP 格式需要分离文件
48
- if(adNetwork!=="mintegral"&&adNetwork!=="adikteev"&&adNetwork!=="bigabid"&&!(isZipOutputFinal&&adNetwork==="facebook"))webpackConfig.plugins.push(new HtmlInlineScriptPlugin);// 注入渠道 SDK 插件(公共逻辑)
49
- injectSDKPlugins(webpackConfig,{network:adNetwork,protocol:adProtocol,orientation:buildOptions.orientation});if(isZipOutputFinal){if(adNetwork==="tiktok"){webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","tiktok-config.json"),to:"config.json"}]}))}else if(adNetwork==="snapchat"){const tmpConfigPath=path.join(__dirname,"resources","snapchat-config.json");webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:tmpConfigPath,to:"config.json"}]}))}else if(adNetwork==="bigoads"){// BigoAds 要求输出 ZIP 包,包含 index.html 和 config.json
50
- // SDK 注入已在 injectSDKPlugins 中处理
51
- webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","bigoads-config.json"),to:"config.json"}]}))}webpackConfig.plugins.push(new ZipPlugin({filename:isChannelFold2Zip?adNetwork:getFileName(),path:path.resolve(outDir),cleanTempFolder:true// 🗑️ 生成 zip 后删除临时文件夹
52
- }));webpackConfig.output.path=path.resolve(outDir,adNetwork);if(adNetwork==="adikteev"){webpackConfig.output.filename="creative.js";// Adikteev 需要将 CSS 单独提取为 style.css 文件
53
- webpackConfig.plugins.push(new MiniCssExtractPlugin({filename:"style.css"}))}else if(adNetwork==="bigabid")webpackConfig.output.filename="main.js";else if(adNetwork==="inmobi")webpackConfig.output.filename="main.js"}// 添加 fflate 压缩插件(仅在打正式包时生效)
54
- if(buildOptions.fflateCompression){console.log("\u2705 fflate compression enabled (production build only)");webpackConfig.plugins.push(new FflateCompressionPlugin({enabled:true,level:9,keepOriginal:false,logStats:true,googlePlayUrl:buildOptions.googlePlayUrl,appStoreUrl:buildOptions.appStoreUrl}))}return webpackConfig}/**
49
+ */
50
+ function makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfig) {
51
+ const buildOptions = mergeOptions(options, customOptions);
52
+ logOptions(buildOptions);
53
+ customDefines = customDefines || {};
54
+ webpackCustomConfig = webpackCustomConfig || {};
55
+
56
+ /** @type {AD_NETWORK} */
57
+ const adNetwork = buildOptions['network'];
58
+
59
+ /** @type {AD_PROTOCOL} */
60
+ const adProtocol = buildOptions['protocol'];
61
+
62
+ let outDir = buildTemplateString(buildOptions.outDir, buildOptions);
63
+
64
+ function getFileName() {
65
+ let filename = buildTemplateString(buildOptions.filename, buildOptions);
66
+
67
+ if (adNetwork === 'mintegral') return filename.replace(/\s+/g, '').replace(/[^a-zA-Z0-9]/g, '_').replace('_fullhash_6_', '[fullhash:6]');
68
+ return filename;
69
+ }
70
+
71
+ const isZipOutput =
72
+ zipOutputNetworks.includes(adNetwork) || (buildOptions.zip && zipOutputAllowedNetworks.includes(adNetwork));
73
+
74
+ const { isChannelFold2Zip, isZipOutputFinal } = resolveChannelFold2Zip({
75
+ isChannelFold2ZipOption: buildOptions.isChannelFold2Zip,
76
+ adNetwork,
77
+ isZipOutput
78
+ });
79
+
80
+ let htmlFileName = '';
81
+ if (adNetwork === 'mintegral') htmlFileName = 'index.html'; // 修改为 index.html
82
+ else if (isZipOutputFinal) htmlFileName = 'index.html';
83
+ else htmlFileName = `${getFileName()}.html`;
84
+
85
+ const metaTags = {
86
+ viewport: 'width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1.0,user-scalable=no'
87
+ };
88
+
89
+ if (adNetwork === 'mintegral') {
90
+ metaTags['viewport'] = 'width=device-width,user-scalable=no,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0';
91
+ }
92
+
93
+ if (!buildOptions.skipRecommendedMeta) {
94
+ metaTags['HandheldFriendly'] = 'True';
95
+ metaTags['cleartype'] = { 'http-equiv': 'cleartype', content: 'on' };
96
+ metaTags['apple-mobile-web-app-capable'] = 'yes';
97
+ metaTags['mobile-web-app-capable'] = 'yes';
98
+ metaTags['X-UA-Compatible'] = { 'http-equiv': 'X-UA-Compatible', content: 'IE=10' };
99
+ }
100
+
101
+ let htmlWebpackPluginConfig = {
102
+ template: path.resolve('src/index.html'),
103
+ filename: htmlFileName,
104
+ title: `${buildOptions.name} - ${buildOptions.app}`,
105
+ inlineSource: '.(js|css|png|jpg|webp|svg|xml|atlas|mp3|gif|glb|fbx|obj)$',
106
+ meta: metaTags
107
+ };
108
+
109
+ if (adNetwork === 'adikteev') htmlWebpackPluginConfig = generateAdikteevHtmlWebpackPluginConfig('src/index.html');
110
+ else if (adNetwork === 'bigabid') htmlWebpackPluginConfig = generateBigabidHtmlWebpackPluginConfig('src/index.html');
111
+ else if (adNetwork === 'inmobi')
112
+ htmlWebpackPluginConfig = generateInMobiHtmlWebpackPluginConfig('src/index.html', buildOptions);
113
+
114
+ const webpackConfig = merge(
115
+ webpackCommonConfig,
116
+ {
117
+ mode: 'production',
118
+ stats: 'errors-only',
119
+ optimization: {
120
+ usedExports: true,
121
+ minimize: (buildOptions.obfuscateLevel !== undefined ? buildOptions.obfuscateLevel : 2) >= 1,
122
+ minimizer: [
123
+ new TerserPlugin({
124
+ extractComments: false,
125
+ terserOptions: {
126
+ safari10: true,
127
+ mangle: {
128
+ reserved: ['$'],
129
+ // ⚠️ 不要开启 properties: true,会混淆 Phaser 等框架的属性名导致运行时错误
130
+ // properties: true,
131
+ toplevel: true
132
+ },
133
+ compress: {
134
+ drop_console: true,
135
+ arrows: false,
136
+ drop_debugger: true,
137
+ unused: true,
138
+ dead_code: true,
139
+ // 🔥 增加优化轮数
140
+ passes: 3,
141
+ // 🔥 激进压缩选项
142
+ inline: 3, // 最大内联
143
+ collapse_vars: true, // 折叠变量
144
+ reduce_vars: true, // 减少变量
145
+ hoist_vars: true, // 提升变量
146
+ hoist_funs: true, // 提升函数
147
+ sequences: 20, // 更长的序列化
148
+ properties: true, // 优化属性访问
149
+ conditionals: true, // 优化条件
150
+ comparisons: true, // 优化比较
151
+ booleans: true, // 优化布尔
152
+ loops: true, // 优化循环
153
+ if_return: true, // 优化 if-return
154
+ join_vars: true, // 合并变量
155
+ negate_iife: true, // 否定 IIFE
156
+ side_effects: true, // 移除副作用
157
+ switches: true, // 优化 switch
158
+ typeofs: true, // 优化 typeof
159
+ unsafe: false, // 不使用 unsafe(避免破坏框架)
160
+ keep_fargs: false, // 移除未使用参数
161
+ drop_console: true
162
+ },
163
+ output: {
164
+ comments: false,
165
+ quote_style: 3,
166
+ beautify: false,
167
+ wrap_iife: true,
168
+ ecma: 2015
169
+ },
170
+ toplevel: true,
171
+ keep_classnames: false,
172
+ keep_fnames: false
173
+ }
174
+ })
175
+ ]
176
+ },
177
+ plugins: [
178
+ new HtmlWebpackPlugin(htmlWebpackPluginConfig),
179
+
180
+ new webpack.DefinePlugin({
181
+ ...buildDefines(),
182
+ ...buildOptions.defines,
183
+ ...customDefines
184
+ }),
185
+
186
+ // For some reason needed for PIXI v8
187
+ new webpack.optimize.LimitChunkCountPlugin({
188
+ maxChunks: 1
189
+ })
190
+ ]
191
+ },
192
+ webpackCustomConfig
193
+ );
194
+
195
+ webpackConfig.output.path = path.resolve(outDir);
196
+
197
+ // 根据混淆等级注入 obfuscator loader
198
+ const obfuscateLevel = buildOptions.obfuscateLevel !== undefined ? buildOptions.obfuscateLevel : 2;
199
+
200
+ // 获取需要排除压缩的目录列表
201
+ const excludeCompressDirs = buildOptions.excludeCompressDirs || [];
202
+
203
+ if (obfuscateLevel >= 2) {
204
+ // 构建各等级的 obfuscator 配置
205
+ const obfuscatorOptions = {
206
+ // 通用基础配置
207
+ target: 'browser',
208
+ sourceMap: false,
209
+ selfDefending: false,
210
+ renameProperties: false,
211
+ disableConsoleOutput: true,
212
+ splitStrings: false,
213
+ unicodeEscapeSequence: false
214
+ };
215
+
216
+ if (obfuscateLevel === 2) {
217
+ // 等级2:基础混淆(控制流平坦化 + 字符串数组)
218
+ Object.assign(obfuscatorOptions, {
219
+ controlFlowFlattening: true,
220
+ controlFlowFlatteningThreshold: 0.75,
221
+ deadCodeInjection: false,
222
+ stringArray: true,
223
+ stringArrayThreshold: 0.75,
224
+ stringArrayEncoding: ['base64'],
225
+ stringArrayIndexShift: true,
226
+ stringArrayRotate: true,
227
+ stringArrayShuffle: true,
228
+ stringArrayWrappersCount: 2,
229
+ stringArrayWrappersChainedCalls: true,
230
+ stringArrayWrappersType: 'function',
231
+ stringArrayCallsTransform: true,
232
+ stringArrayCallsTransformThreshold: 0.75,
233
+ transformObjectKeys: true,
234
+ identifierNamesGenerator: 'hexadecimal'
235
+ });
236
+ } else if (obfuscateLevel === 3) {
237
+ // 等级3:在等级2基础上增加死代码注入
238
+ Object.assign(obfuscatorOptions, {
239
+ controlFlowFlattening: true,
240
+ controlFlowFlatteningThreshold: 0.75,
241
+ deadCodeInjection: true,
242
+ deadCodeInjectionThreshold: 0.4,
243
+ stringArray: true,
244
+ stringArrayThreshold: 0.75,
245
+ stringArrayEncoding: ['base64'],
246
+ stringArrayIndexShift: true,
247
+ stringArrayRotate: true,
248
+ stringArrayShuffle: true,
249
+ stringArrayWrappersCount: 2,
250
+ stringArrayWrappersChainedCalls: true,
251
+ stringArrayWrappersType: 'function',
252
+ stringArrayCallsTransform: true,
253
+ stringArrayCallsTransformThreshold: 0.75,
254
+ transformObjectKeys: true,
255
+ identifierNamesGenerator: 'hexadecimal'
256
+ });
257
+ } else if (obfuscateLevel === 4) {
258
+ // 等级4:最高强度混淆
259
+ Object.assign(obfuscatorOptions, {
260
+ controlFlowFlattening: true,
261
+ controlFlowFlatteningThreshold: 1,
262
+ deadCodeInjection: true,
263
+ deadCodeInjectionThreshold: 0.5,
264
+ stringArray: true,
265
+ stringArrayThreshold: 1,
266
+ stringArrayEncoding: ['rc4'],
267
+ stringArrayIndexShift: true,
268
+ stringArrayRotate: true,
269
+ stringArrayShuffle: true,
270
+ stringArrayWrappersCount: 3,
271
+ stringArrayWrappersChainedCalls: true,
272
+ stringArrayWrappersType: 'function',
273
+ stringArrayCallsTransform: true,
274
+ stringArrayCallsTransformThreshold: 1,
275
+ transformObjectKeys: true,
276
+ identifierNamesGenerator: 'hexadecimal',
277
+ splitStrings: true,
278
+ splitStringsChunkLength: 5,
279
+ unicodeEscapeSequence: true,
280
+ });
281
+ }
282
+
283
+ console.log(`Obfuscation level: ${obfuscateLevel} (${['', 'terser-only', 'basic', 'dead-code', 'maximum'][obfuscateLevel]})`);
284
+
285
+ // 构建exclude数组,包含node_modules和用户指定的排除目录
286
+ const excludePatterns = [];
287
+ if (excludeCompressDirs.length > 0) {
288
+ excludeCompressDirs.forEach(dir => {
289
+ excludePatterns.push(path.resolve(dir));
290
+ });
291
+ console.log(`ℹ️ Excluding directories from compression: ${excludeCompressDirs.join(', ')}`);
292
+ }
293
+
294
+ webpackConfig.module.rules.push({
295
+ test: /\.(js|ts)$/,
296
+ include: [path.resolve('src')],
297
+ exclude: excludePatterns,
298
+ enforce: 'post',
299
+ use: {
300
+ loader: require.resolve('webpack-obfuscator/dist/loader'),
301
+ options: obfuscatorOptions
302
+ }
303
+ });
304
+ } else if (obfuscateLevel === 1) {
305
+ console.log('Obfuscation level: 1 (terser-only)');
306
+ } else {
307
+ console.log('Obfuscation level: 0 (no minimize/obfuscate)');
308
+ }
309
+
310
+ // Adikteev 需要单独提取 CSS 为 style.css,替换默认的 style-loader
311
+ if (adNetwork === 'adikteev') {
312
+ const cssRuleIndex = webpackConfig.module.rules.findIndex(rule =>
313
+ rule.test && rule.test.toString().includes('\\.css')
314
+ );
315
+
316
+ if (cssRuleIndex !== -1) {
317
+ webpackConfig.module.rules[cssRuleIndex] = {
318
+ test: /\.css$/,
319
+ use: [MiniCssExtractPlugin.loader, 'css-loader']
320
+ };
321
+ console.log('✅ Adikteev: CSS will be extracted to style.css');
322
+ }
323
+ }
324
+
325
+ // 决定是否内联 JS 到 HTML
326
+ // 以下渠道需要分离 JS 文件,不使用 HtmlInlineScriptPlugin:
327
+ // - mintegral: 需要独立的 JS 文件
328
+ // - adikteev: 需要 creative.js 上传到 CDN
329
+ // - bigabid: 需要 main.js 上传到 CDN
330
+ // - facebook (ZIP 模式): Facebook ZIP 格式需要分离文件
331
+ if (
332
+ adNetwork !== 'mintegral' &&
333
+ adNetwork !== 'adikteev' &&
334
+ adNetwork !== 'bigabid' &&
335
+ !(isZipOutputFinal && adNetwork === 'facebook')
336
+ )
337
+ webpackConfig.plugins.push(new HtmlInlineScriptPlugin());
338
+
339
+ // 注入渠道 SDK 插件(公共逻辑)
340
+ injectSDKPlugins(webpackConfig, {
341
+ network: adNetwork,
342
+ protocol: adProtocol,
343
+ orientation: buildOptions.orientation
344
+ });
345
+
346
+ if (isZipOutputFinal) {
347
+ if (adNetwork === 'tiktok') {
348
+ webpackConfig.plugins.push(
349
+ new CopyWebpackPlugin({
350
+ patterns: [{ from: path.join(__dirname, 'resources', 'tiktok-config.json'), to: 'config.json' }]
351
+ })
352
+ );
353
+ } else if (adNetwork === 'snapchat') {
354
+ const tmpConfigPath = path.join(__dirname, 'resources', 'snapchat-config.json');
355
+
356
+ webpackConfig.plugins.push(
357
+ new CopyWebpackPlugin({
358
+ patterns: [{ from: tmpConfigPath, to: 'config.json' }]
359
+ })
360
+ );
361
+ } else if (adNetwork === 'bigoads') {
362
+ // BigoAds 要求输出 ZIP 包,包含 index.html 和 config.json
363
+ // SDK 注入已在 injectSDKPlugins 中处理
364
+ webpackConfig.plugins.push(
365
+ new CopyWebpackPlugin({
366
+ patterns: [{ from: path.join(__dirname, 'resources', 'bigoads-config.json'), to: 'config.json' }]
367
+ })
368
+ );
369
+ }
370
+
371
+ webpackConfig.plugins.push(
372
+ new ZipPlugin({
373
+ filename: isChannelFold2Zip ? adNetwork : getFileName(),
374
+ path: path.resolve(outDir),
375
+ cleanTempFolder: true // 🗑️ 生成 zip 后删除临时文件夹
376
+ })
377
+ );
378
+
379
+ webpackConfig.output.path = path.resolve(outDir, adNetwork);
380
+ if (adNetwork === 'adikteev') {
381
+ webpackConfig.output.filename = 'creative.js';
382
+ // Adikteev 需要将 CSS 单独提取为 style.css 文件
383
+ webpackConfig.plugins.push(
384
+ new MiniCssExtractPlugin({
385
+ filename: 'style.css'
386
+ })
387
+ );
388
+ } else if (adNetwork === 'bigabid') webpackConfig.output.filename = 'main.js';
389
+ else if (adNetwork === 'inmobi') webpackConfig.output.filename = 'main.js';
390
+ }
391
+
392
+ // 添加 fflate 压缩插件(仅在打正式包时生效)
393
+ if (buildOptions.fflateCompression) {
394
+ console.log('✅ fflate compression enabled (production build only)');
395
+ webpackConfig.plugins.push(
396
+ new FflateCompressionPlugin({
397
+ enabled: true,
398
+ level: 9,
399
+ keepOriginal: false,
400
+ logStats: true,
401
+ googlePlayUrl: buildOptions.googlePlayUrl,
402
+ appStoreUrl: buildOptions.appStoreUrl,
403
+ })
404
+ );
405
+ }
406
+
407
+ return webpackConfig;
408
+ }
409
+
410
+ /**
55
411
  * Runs webpack production build
56
412
  * @param {import('webpack').Configuration} [webpackConfig] - Webpack configuration to use, creates default if not provided
57
413
  * @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
58
414
  * @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
59
415
  * @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
60
- */function runBuild(webpackConfig,customOptions,customDefines,webpackCustomConfig){return new Promise((resolve,reject)=>{if(!webpackConfig)webpackConfig=makeWebpackBuildConfig(customOptions,customDefines,webpackCustomConfig);const compiler=webpack(webpackConfig);compiler.run((err,stats)=>{if(err){console.error("Build failed:",err.stack||err);if(err.details){console.error("Error details:",err.details)}return reject(err)}if(stats.hasErrors()){const errors=stats.compilation.errors;console.error(`\n❌ Build finished with ${errors.length} error(s):\n`);// 打印每个错误的详细信息
61
- errors.forEach((error,index)=>{console.error(`\n[Error ${index+1}/${errors.length}]`);console.error(error.message||error);if(error.stack){console.error(error.stack)}});// 拒绝 Promise 时传递格式化的错误消息
62
- const errorMessage=errors.map((e,i)=>`[${i+1}] ${e.message||e}`).join("\n");reject(new Error(`Webpack build failed with ${errors.length} error(s):\n${errorMessage}`))}else{console.log(`Build successful!`);resolve()}})})}exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runBuild=runBuild;
416
+ */
417
+ function runBuild(webpackConfig, customOptions, customDefines, webpackCustomConfig) {
418
+ return new Promise((resolve, reject) => {
419
+ if (!webpackConfig) webpackConfig = makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfig);
420
+
421
+ const compiler = webpack(webpackConfig);
422
+ compiler.run((err, stats) => {
423
+ if (err) {
424
+ console.error('Build failed:', err.stack || err);
425
+ if (err.details) {
426
+ console.error('Error details:', err.details);
427
+ }
428
+ return reject(err);
429
+ }
430
+
431
+ if (stats.hasErrors()) {
432
+ const errors = stats.compilation.errors;
433
+ console.error(`\n❌ Build finished with ${errors.length} error(s):\n`);
434
+
435
+ // 打印每个错误的详细信息
436
+ errors.forEach((error, index) => {
437
+ console.error(`\n[Error ${index + 1}/${errors.length}]`);
438
+ console.error(error.message || error);
439
+ if (error.stack) {
440
+ console.error(error.stack);
441
+ }
442
+ });
443
+
444
+ // 拒绝 Promise 时传递格式化的错误消息
445
+ const errorMessage = errors.map((e, i) => `[${i + 1}] ${e.message || e}`).join('\n');
446
+ reject(new Error(`Webpack build failed with ${errors.length} error(s):\n${errorMessage}`));
447
+ } else {
448
+ console.log(`Build successful!`);
449
+ resolve();
450
+ }
451
+ });
452
+ });
453
+ }
454
+
455
+ exports.makeWebpackBuildConfig = makeWebpackBuildConfig;
456
+ exports.runBuild = runBuild;