@playcraft/devkit 1.0.12 → 1.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 (39) hide show
  1. package/LICENSE +21 -21
  2. package/cli/bin/playable-scripts.js +0 -0
  3. package/cli/commands/build.js +5 -5
  4. package/cli/commands/builds.js +14 -14
  5. package/cli/commands/pack.js +47 -47
  6. package/core/batch-build.js +72 -72
  7. package/core/cos-uploader.js +39 -47
  8. package/core/index.d.ts +185 -185
  9. package/core/loaders/gltf-loader.js +19 -19
  10. package/core/plugins/AdikteevInjectorPlugin.js +11 -11
  11. package/core/plugins/BigoAdsInjectorPlugin.js +4 -4
  12. package/core/plugins/DAPIInjectorPlugin.js +4 -4
  13. package/core/plugins/DebuggerInjectionPlugin.js +5 -5
  14. package/core/plugins/ExitAPIInjectorPlugin.js +9 -9
  15. package/core/plugins/FflateCompressionPlugin.js +43 -43
  16. package/core/plugins/LiftoffInjectorPlugin.js +11 -11
  17. package/core/plugins/MRAIDInjectorPlugin.js +3 -3
  18. package/core/plugins/MintegralInjectorPlugin.js +4 -4
  19. package/core/plugins/PangleInjectorPlugin.js +3 -3
  20. package/core/plugins/SnapchatInjectorPlugin.js +4 -4
  21. package/core/plugins/TikTokInjectorPlugin.js +3 -3
  22. package/core/plugins/UnityInjectorPlugin.js +13 -13
  23. package/core/resources/snapchat-config.json +1 -1
  24. package/core/resources/tiktok-config.json +1 -1
  25. package/core/utils/buildDefines.js +2 -2
  26. package/core/utils/buildTemplateString.js +2 -2
  27. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +26 -26
  28. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +16 -16
  29. package/core/utils/injectSDKPlugins.js +10 -10
  30. package/core/utils/logOptions.js +2 -2
  31. package/core/utils/mergeOptions.js +5 -5
  32. package/core/utils/parseArgvOptions.js +4 -4
  33. package/core/utils/resolveChannelFold2Zip.js +12 -12
  34. package/core/utils/validateThemeData.js +80 -80
  35. package/core/vite.dev.js +12 -12
  36. package/core/webpack.build.js +12 -12
  37. package/core/webpack.dev.js +12 -12
  38. package/defines.d.ts +54 -54
  39. package/package.json +1 -1
@@ -1,14 +1,14 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Liftoff SDK 注入插件
3
- * 根据 Liftoff v2 标准,注入 MRAID SDK 并添加必要的 meta 标签
4
- * 文档: https://app.liftoff.io/public/docs/api/creative/v2/index.html
5
- *
6
- * Liftoff 要求:
7
- * 1. 使用 mraid.open() 处理点击跳转
8
- * 2. 禁止使用 window.location
9
- * 3. 禁止使用 iframe
10
- * 4. ZIP 包格式输出
11
- * 5. 预留左上角和右上角 50x50px 区域(关闭按钮)
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Liftoff SDK 注入插件
3
+ * 根据 Liftoff v2 标准,注入 MRAID SDK 并添加必要的 meta 标签
4
+ * 文档: https://app.liftoff.io/public/docs/api/creative/v2/index.html
5
+ *
6
+ * Liftoff 要求:
7
+ * 1. 使用 mraid.open() 处理点击跳转
8
+ * 2. 禁止使用 window.location
9
+ * 3. 禁止使用 iframe
10
+ * 4. ZIP 包格式输出
11
+ * 5. 预留左上角和右上角 50x50px 区域(关闭按钮)
12
12
  */class LiftoffInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("LiftoffInjectorPlugin",compilation=>{// 注入 MRAID SDK 到 head
13
13
  HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("LiftoffInjectorPlugin",(data,callback)=>{// Liftoff 使用标准 MRAID 协议
14
14
  data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"liftoff-injector-plugin"},attributes:{src:"mraid.js",type:"text/javascript"}});callback(null,data)})})}}exports.LiftoffInjectorPlugin=LiftoffInjectorPlugin;
@@ -1,4 +1,4 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Webpack plugin that injects Pangle Playable SDK script into the HTML head.
3
- * @implements {import('webpack').WebpackPluginInstance}
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Webpack plugin that injects Pangle Playable SDK script into the HTML head.
3
+ * @implements {import('webpack').WebpackPluginInstance}
4
4
  */class MRAIDInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("MRAIDInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("MRAIDInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"mraid.js",type:"text/javascript"}});callback(null,data)})})}}exports.MRAIDInjectorPlugin=MRAIDInjectorPlugin;
@@ -1,5 +1,5 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");const mintegralSrc="function gameStart() {window.mintGameStart && window.mintGameStart()}; function gameClose() {window.mintGameClose && window.mintGameClose()}";/**
2
- * Webpack plugin that injects Mintegral required script into the HTML head.
3
- * Designed to use with @playcraft/adsdk package
4
- * @implements {import('webpack').WebpackPluginInstance}
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");const mintegralSrc="function gameStart() {window.mintGameStart && window.mintGameStart()}; function gameClose() {window.mintGameClose && window.mintGameClose()}";/**
2
+ * Webpack plugin that injects Mintegral required script into the HTML head.
3
+ * Designed to use with @playcraft/adsdk package
4
+ * @implements {import('webpack').WebpackPluginInstance}
5
5
  */class MintegralInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("MintegralInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("MintegralInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},innerHTML:mintegralSrc});callback(null,data)})})}}exports.MintegralInjectorPlugin=MintegralInjectorPlugin;
@@ -1,4 +1,4 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Webpack plugin that injects Pangle Playable SDK script into the HTML head.
3
- * @implements {import('webpack').WebpackPluginInstance}
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Webpack plugin that injects Pangle Playable SDK script into the HTML head.
3
+ * @implements {import('webpack').WebpackPluginInstance}
4
4
  */class PangleInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("PangleInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("PangleInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"https://sf3-ttcdn-tos.pstatp.com/obj/union-fe-nc/playable/sdk/playable-sdk.js",type:"text/javascript"}});callback(null,data)})})}}exports.PangleInjectorPlugin=PangleInjectorPlugin;
@@ -1,7 +1,7 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Snapchat SDK 注入插件
3
- * 在 HTML 头部注入 Snapchat Playable SDK 脚本
4
- * 文档: https://businesshelp.snapchat.com/s/article/app-playables
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Snapchat SDK 注入插件
3
+ * 在 HTML 头部注入 Snapchat Playable SDK 脚本
4
+ * 文档: https://businesshelp.snapchat.com/s/article/app-playables
5
5
  */class SnapchatInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("SnapchatInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("SnapchatInjectorPlugin",(data,callback)=>{// 在头部注入 Snapchat SDK 脚本标签
6
6
  data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{type:"text/javascript"},innerHTML:`
7
7
  // Snapchat Playable Ad SDK Mock (开发环境)
@@ -1,4 +1,4 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Webpack plugin that injects TikTok Playable SDK script into the HTML head.
3
- * @implements {import('webpack').WebpackPluginInstance}
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Webpack plugin that injects TikTok Playable SDK script into the HTML head.
3
+ * @implements {import('webpack').WebpackPluginInstance}
4
4
  */class TikTokInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("TikTokInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("TikTokInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"https://sf16-muse-va.ibytedtos.com/obj/union-fe-nc-i18n/playable/sdk/playable-sdk.js",type:"text/javascript"}});callback(null,data)})})}}exports.TikTokInjectorPlugin=TikTokInjectorPlugin;
@@ -1,16 +1,16 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
- * Unity Ads SDK 注入插件
3
- * 根据 Unity Ads Playable 标准,注入 MRAID SDK 引用
4
- *
5
- * 文档: https://storage.googleapis.com/unity-ads-aui-prod-deployments/external-app/UnityAds_Playable_guide.pdf
6
- *
7
- * Unity Ads 要求:
8
- * 1. 使用 MRAID 2.0 标准
9
- * 2. 必须等待 mraid ready 事件
10
- * 3. 使用 mraid.open() 处理点击跳转
11
- * 4. 必须调用 mraid.close() 关闭广告
12
- * 5. 使用 mraid.fireTrackingEvent() 上报游戏事件
13
- * 6. 不允许使用 window.top(Unity 平台限制)
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Unity Ads SDK 注入插件
3
+ * 根据 Unity Ads Playable 标准,注入 MRAID SDK 引用
4
+ *
5
+ * 文档: https://storage.googleapis.com/unity-ads-aui-prod-deployments/external-app/UnityAds_Playable_guide.pdf
6
+ *
7
+ * Unity Ads 要求:
8
+ * 1. 使用 MRAID 2.0 标准
9
+ * 2. 必须等待 mraid ready 事件
10
+ * 3. 使用 mraid.open() 处理点击跳转
11
+ * 4. 必须调用 mraid.close() 关闭广告
12
+ * 5. 使用 mraid.fireTrackingEvent() 上报游戏事件
13
+ * 6. 不允许使用 window.top(Unity 平台限制)
14
14
  */class UnityInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("UnityInjectorPlugin",compilation=>{// 1. 注入 mraid.js 标签
15
15
  HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("UnityInjectorPlugin",(data,callback)=>{// Unity Ads 使用标准 MRAID 协议
16
16
  // 在生产环境中,mraid.js 由 Unity Ads 平台提供
@@ -1 +1 @@
1
- { "orientation": 1 }
1
+ { "orientation": 1 }
@@ -1 +1 @@
1
- { "playable_orientation": 0 }
1
+ { "playable_orientation": 0 }
@@ -1,3 +1,3 @@
1
- "use strict";const{options}=require("../options");/**
2
- * Build's default defines list for dev & prod builds
1
+ "use strict";const{options}=require("../options");/**
2
+ * Build's default defines list for dev & prod builds
3
3
  */exports.buildDefines=function buildDefines(){const defines={__DEV__:JSON.stringify(options["dev"]===undefined?false:options["dev"]),GOOGLE_PLAY_URL:JSON.stringify(options.googlePlayUrl),APP_STORE_URL:JSON.stringify(options.appStoreUrl),AD_NETWORK:JSON.stringify(options["network"]),AD_PROTOCOL:JSON.stringify(options["protocol"]),APP:JSON.stringify(options.app),NAME:JSON.stringify(options.name),VERSION:JSON.stringify(options.version),BUILD_HASH:JSON.stringify((+new Date).toString(36)),LANGUAGE:JSON.stringify(options.language),ORIENTATION:JSON.stringify(options.orientation)};/** @type {AD_NETWORK} */let adNetworkDefine=options["network"];if(adNetworkDefine==="pangle")adNetworkDefine="tiktok";if(adNetworkDefine==="mytarget"||adNetworkDefine==="moloco")adNetworkDefine="facebook";defines["AD_NETWORK"]=JSON.stringify(adNetworkDefine);return defines};
@@ -1,5 +1,5 @@
1
- "use strict";const{options}=require("../options");const{getCurrentDateFormatted}=require("./date");const path=require("path");/** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */const adNetworkFileNameMap={preview:"Preview",bigoads:"bigo"};/**
2
- * Build's result string from template. Used for outDir or filename
1
+ "use strict";const{options}=require("../options");const{getCurrentDateFormatted}=require("./date");const path=require("path");/** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */const adNetworkFileNameMap={preview:"Preview",bigoads:"bigo"};/**
2
+ * Build's result string from template. Used for outDir or filename
3
3
  */exports.buildTemplateString=function buildTemplateString(template,customOptions){customOptions=customOptions||options;/** @type {AD_NETWORK} */const adNetwork=customOptions["network"];/** @type {AD_PROTOCOL} */// const adProtocol = customOptions['protocol'];
4
4
  let networkName=customOptions.adNetworkNames[adNetwork]||adNetworkFileNameMap[adNetwork]||adNetwork;// if (adProtocol === 'dapi') networkName += '_DAPI';
5
5
  // 如果是绝对路径,直接返回,不做模板替换
@@ -1,35 +1,35 @@
1
- "use strict";const fs=require("fs");const path=require("path");/**
2
- * Adikteev 平台要求的占位符变量
3
- * 这些变量会在生产环境中被 Adikteev 平台替换为实际值
4
- *
5
- * 文档: https://help.adikteev.com/hc/en-us/articles/10549028250130
6
- *
7
- * 官方要求:
8
- * 1. HTML 片段(不是完整 HTML 文档)
9
- * 2. CSS 必须单独提取为 style.css,通过 <link rel="stylesheet"> 引用
10
- * 3. JS 必须单独为 creative.js,通过 <script src> 引用
11
- * 4. JS 和 CSS 文件会被上传到 Adikteev CDN
1
+ "use strict";const fs=require("fs");const path=require("path");/**
2
+ * Adikteev 平台要求的占位符变量
3
+ * 这些变量会在生产环境中被 Adikteev 平台替换为实际值
4
+ *
5
+ * 文档: https://help.adikteev.com/hc/en-us/articles/10549028250130
6
+ *
7
+ * 官方要求:
8
+ * 1. HTML 片段(不是完整 HTML 文档)
9
+ * 2. CSS 必须单独提取为 style.css,通过 <link rel="stylesheet"> 引用
10
+ * 3. JS 必须单独为 creative.js,通过 <script src> 引用
11
+ * 4. JS 和 CSS 文件会被上传到 Adikteev CDN
12
12
  */const AdikteevPlaceholders=`<meta charset="UTF-8">
13
13
  <link rel="stylesheet" href="style.css">
14
14
  <script type="application/javascript">
15
15
  var AK_CLICK_DESTINATION_URL = "PLACEHOLDER_CLICK_REDIRECT";
16
16
  var AK_CLICK_PIXEL_URL = "PLACEHOLDER_CLICK_PIXEL";
17
17
  </script>
18
- <script type="application/javascript" src="creative.js"></script>`;/**
19
- * 生成 Adikteev 平台所需的 HTML 配置
20
- *
21
- * Adikteev 特殊要求:
22
- * 1. 必须是 HTML 代码片段,不是完整 HTML 文档
23
- * 2. CSS 必须单独提取为 style.css(通过 MiniCssExtractPlugin)
24
- * 3. JS 必须单独为 creative.js
25
- * 4. 必须包含 AK_CLICK_DESTINATION_URL 和 AK_CLICK_PIXEL_URL 占位符
26
- * 5. 使用 <link> 和 <script> 标签引用外部文件
27
- * 6. 禁止固定宽高,必须响应式布局
28
- *
29
- * 文件上传:
30
- * - style.css → 上传到 Adikteev CDN
31
- * - creative.js → 上传到 Adikteev CDN
32
- * - index.html → HTML 片段(引用上述文件)
18
+ <script type="application/javascript" src="creative.js"></script>`;/**
19
+ * 生成 Adikteev 平台所需的 HTML 配置
20
+ *
21
+ * Adikteev 特殊要求:
22
+ * 1. 必须是 HTML 代码片段,不是完整 HTML 文档
23
+ * 2. CSS 必须单独提取为 style.css(通过 MiniCssExtractPlugin)
24
+ * 3. JS 必须单独为 creative.js
25
+ * 4. 必须包含 AK_CLICK_DESTINATION_URL 和 AK_CLICK_PIXEL_URL 占位符
26
+ * 5. 使用 <link> 和 <script> 标签引用外部文件
27
+ * 6. 禁止固定宽高,必须响应式布局
28
+ *
29
+ * 文件上传:
30
+ * - style.css → 上传到 Adikteev CDN
31
+ * - creative.js → 上传到 Adikteev CDN
32
+ * - index.html → HTML 片段(引用上述文件)
33
33
  */exports.generateAdikteevHtmlWebpackPluginConfig=function generateAdikteevHtmlWebpackPluginConfig(originalHtmlContentPath){originalHtmlContentPath=originalHtmlContentPath||"src/index.html";let originalBody="";try{const content=fs.readFileSync(path.resolve(originalHtmlContentPath),"utf8");const match=content.match(/<body>([\s\S]*?)<\/body>/);if(match){originalBody=match[1].trim();// 移除缩进以获得更清晰的输出
34
34
  originalBody=originalBody.replaceAll(" ","").replaceAll("\t","")}}catch(e){console.warn("[Adikteev] Could not read original HTML body:",e.message)}return{filename:"index.html",inject:false,minify:false,templateContent:({htmlWebpackPlugin})=>{// 生成 HTML 代码片段(非完整文档)
35
35
  // 结构: CSS 引用 → 占位符变量 → JS 引用 → body 内容
@@ -1,12 +1,12 @@
1
- "use strict";const fs=require("fs");const path=require("path");/**
2
- * InMobi 追踪宏定义
3
- * 文档: https://support.inmobi.com/dsp/in/inmobi-dsp-india/creative-specifications-and-guidelines/
4
- *
5
- * InMobi 要求:
6
- * 1. 单个 index.html 文件(所有资源内联)或 ZIP 包
7
- * 2. 必须包含 INMOBI_DSPMACROS 追踪变量
8
- * 3. 引用外部 mraid.js(由 InMobi 平台提供)
9
- * 4. 支持 MRAID 3.0 标准
1
+ "use strict";const fs=require("fs");const path=require("path");/**
2
+ * InMobi 追踪宏定义
3
+ * 文档: https://support.inmobi.com/dsp/in/inmobi-dsp-india/creative-specifications-and-guidelines/
4
+ *
5
+ * InMobi 要求:
6
+ * 1. 单个 index.html 文件(所有资源内联)或 ZIP 包
7
+ * 2. 必须包含 INMOBI_DSPMACROS 追踪变量
8
+ * 3. 引用外部 mraid.js(由 InMobi 平台提供)
9
+ * 4. 支持 MRAID 3.0 标准
10
10
  */const InMobiPlaceholders=`<script>
11
11
  var INMOBI_DSPMACROS = {
12
12
  "Ad_Load_Start": ["$P_AD_LOAD_START"],
@@ -23,13 +23,13 @@
23
23
  "Spent_30_Seconds": ["$P_TIMESPENT_30"]
24
24
  };
25
25
  </script>
26
- <script src="mraid.js"></script>`;/**
27
- * 生成 InMobi 平台所需的 HTML 配置
28
- *
29
- * 策略:
30
- * 1. 使用标准的 HtmlWebpackPlugin 配置(不是 templateContent)
31
- * 2. JS 会通过 HtmlInlineScriptPlugin 自动内联到 HTML 中
32
- * 3. 通过修改 template 文件注入 InMobi 追踪脚本
26
+ <script src="mraid.js"></script>`;/**
27
+ * 生成 InMobi 平台所需的 HTML 配置
28
+ *
29
+ * 策略:
30
+ * 1. 使用标准的 HtmlWebpackPlugin 配置(不是 templateContent)
31
+ * 2. JS 会通过 HtmlInlineScriptPlugin 自动内联到 HTML 中
32
+ * 3. 通过修改 template 文件注入 InMobi 追踪脚本
33
33
  */exports.generateInMobiHtmlWebpackPluginConfig=function generateInMobiHtmlWebpackPluginConfig(originalHtmlContentPath,buildOptions){originalHtmlContentPath=originalHtmlContentPath||"src/index.html";const placeholder=InMobiPlaceholders.replaceAll("{GOOGLE_PLAY_URL}",buildOptions.googlePlayUrl).replaceAll("{APP_STORE_URL}",buildOptions.appStoreUrl);// 读取原始 HTML 模板
34
34
  let htmlContent="";try{htmlContent=fs.readFileSync(path.resolve(originalHtmlContentPath),"utf8")}catch(e){console.warn("[InMobi] Could not read HTML template:",e.message)}// 在 </head> 之前注入 InMobi 追踪脚本
35
35
  htmlContent=htmlContent.replace("</head>",`${placeholder}\n</head>`);return{filename:"index.html",inject:"head",// webpack 会自动注入 JS,然后 HtmlInlineScriptPlugin 内联
@@ -1,13 +1,13 @@
1
- "use strict";/**
2
- * 根据 AD_NETWORK 和 AD_PROTOCOL 注入对应的渠道 SDK 脚本插件
3
- * 此函数被 webpack.dev.js 和 webpack.build.js 共用
4
- */const{ExitAPIInjectorPlugin}=require("../plugins/ExitAPIInjectorPlugin.js");const{DAPIInjectorPlugin}=require("../plugins/DAPIInjectorPlugin.js");const{PangleInjectorPlugin}=require("../plugins/PangleInjectorPlugin.js");const{TikTokInjectorPlugin}=require("../plugins/TikTokInjectorPlugin.js");const{MintegralInjectorPlugin}=require("../plugins/MintegralInjectorPlugin.js");const{MRAIDInjectorPlugin}=require("../plugins/MRAIDInjectorPlugin.js");const{BigoAdsInjectorPlugin}=require("../plugins/BigoAdsInjectorPlugin.js");const{LiftoffInjectorPlugin}=require("../plugins/LiftoffInjectorPlugin.js");const{SnapchatInjectorPlugin}=require("../plugins/SnapchatInjectorPlugin.js");const{AdikteevInjectorPlugin}=require("../plugins/AdikteevInjectorPlugin.js");const{UnityInjectorPlugin}=require("../plugins/UnityInjectorPlugin.js");/**
5
- * 注入渠道 SDK 插件到 webpack 配置中
6
- * @param {import('webpack').Configuration} webpackConfig - webpack 配置对象
7
- * @param {Object} options - 配置选项
8
- * @param {AD_NETWORK} options.network - 广告网络
9
- * @param {AD_PROTOCOL} options.protocol - 广告协议
10
- * @param {string} [options.orientation] - 屏幕方向(google 渠道需要)
1
+ "use strict";/**
2
+ * 根据 AD_NETWORK 和 AD_PROTOCOL 注入对应的渠道 SDK 脚本插件
3
+ * 此函数被 webpack.dev.js 和 webpack.build.js 共用
4
+ */const{ExitAPIInjectorPlugin}=require("../plugins/ExitAPIInjectorPlugin.js");const{DAPIInjectorPlugin}=require("../plugins/DAPIInjectorPlugin.js");const{PangleInjectorPlugin}=require("../plugins/PangleInjectorPlugin.js");const{TikTokInjectorPlugin}=require("../plugins/TikTokInjectorPlugin.js");const{MintegralInjectorPlugin}=require("../plugins/MintegralInjectorPlugin.js");const{MRAIDInjectorPlugin}=require("../plugins/MRAIDInjectorPlugin.js");const{BigoAdsInjectorPlugin}=require("../plugins/BigoAdsInjectorPlugin.js");const{LiftoffInjectorPlugin}=require("../plugins/LiftoffInjectorPlugin.js");const{SnapchatInjectorPlugin}=require("../plugins/SnapchatInjectorPlugin.js");const{AdikteevInjectorPlugin}=require("../plugins/AdikteevInjectorPlugin.js");const{UnityInjectorPlugin}=require("../plugins/UnityInjectorPlugin.js");/**
5
+ * 注入渠道 SDK 插件到 webpack 配置中
6
+ * @param {import('webpack').Configuration} webpackConfig - webpack 配置对象
7
+ * @param {Object} options - 配置选项
8
+ * @param {AD_NETWORK} options.network - 广告网络
9
+ * @param {AD_PROTOCOL} options.protocol - 广告协议
10
+ * @param {string} [options.orientation] - 屏幕方向(google 渠道需要)
11
11
  */function injectSDKPlugins(webpackConfig,options){const{network:adNetwork,protocol:adProtocol,orientation}=options;// 协议层注入
12
12
  if("dapi"===adProtocol){webpackConfig.plugins.push(new DAPIInjectorPlugin)}else if("mraid"===adProtocol){// 只有 ironsource 才注入 MRAIDInjectorPlugin
13
13
  // Unity 使用专门的 UnityInjectorPlugin(在渠道特定注入中处理)
@@ -1,5 +1,5 @@
1
- "use strict";var prettyjson=require("prettyjson");/**
2
- * Log provided options into console output
1
+ "use strict";var prettyjson=require("prettyjson");/**
2
+ * Log provided options into console output
3
3
  */exports.logOptions=function logOptions(options){let logOptions={mode:process.env.NODE_ENV,...options};let isProduction=false;if(process.env.NODE_ENV==="production"){isProduction=true;delete logOptions.port;delete logOptions.open}else if(process.env.NODE_ENV==="development"){delete logOptions.outDir}if(Object.keys(logOptions.compilation).length===0||!isProduction)delete logOptions.compilation;if(Object.keys(logOptions.adNetworkNames).length===0||!isProduction)delete logOptions.adNetworkNames;if(Object.keys(logOptions.defines).length===0)delete logOptions.defines;if(logOptions.zip===false||!isProduction)delete logOptions.zip;if(logOptions.tsConfig==="tsconfig.json")delete logOptions.tsConfig;if(logOptions.jsConfig==="jsconfig.json")delete logOptions.jsConfig;if(logOptions.buildConfig==="build.json")delete logOptions.buildConfig;if(logOptions.language==="en")delete logOptions.language;if(logOptions.orientation==="both")delete logOptions.orientation;// 在 dev 模式下,如果 network 为默认值 preview 则不显示
4
4
  if(!isProduction&&logOptions.network==="preview")delete logOptions.network;// 在 dev 模式下,如果 protocol 为默认值 none 则不显示
5
5
  if(!isProduction&&logOptions.protocol==="none")delete logOptions.protocol;delete logOptions.filename;delete logOptions.app;delete logOptions.name;delete logOptions.googlePlayUrl;delete logOptions.appStoreUrl;if(logOptions.obfuscateLevel===2)delete logOptions.obfuscateLevel;console.log(prettyjson.render(logOptions,{},2))};
@@ -1,6 +1,6 @@
1
- "use strict";/**
2
- * Merges two option objects with deep merge for 'defines' and 'build' properties
3
- * @param {import('../index').CLIOptions} options1 - First options object
4
- * @param {Partial<import('../index').CLIOptions>} [options2] - Second options object
5
- * @returns {import('../index').CLIOptions} Merged options object with combined defines and build properties
1
+ "use strict";/**
2
+ * Merges two option objects with deep merge for 'defines' and 'build' properties
3
+ * @param {import('../index').CLIOptions} options1 - First options object
4
+ * @param {Partial<import('../index').CLIOptions>} [options2] - Second options object
5
+ * @returns {import('../index').CLIOptions} Merged options object with combined defines and build properties
6
6
  */exports.mergeOptions=function mergeOptions(options1,options2){if(!options1)return{};if(!options2)return options1;const defines=Object.assign({},options1.defines,options2.defines);const build=Object.assign({},options1.build,options2.build);const compilation=Object.assign({},options1.compilation,options2.compilation);const adNetworkNames=Object.assign({},options1.adNetworkNames,options2.adNetworkNames);const options=Object.assign({},options1,options2);options.defines=defines;options.compilation=compilation;options.adNetworkNames=adNetworkNames;options.build=build;return options};
@@ -1,7 +1,7 @@
1
- "use strict";/** @type {string[]} List of supported advertising networks */const allowedAdNetworks=["preview","applovin","unity","google","ironsource","facebook","moloco","adcolony","mintegral","vungle","tapjoy","snapchat","tiktok","appreciate","chartboost","pangle","mytarget","liftoff","smadex","adikteev","bigabid","inmobi","bigoads"];/** @type {AD_PROTOCOL[]} List of supported advertising protocols */const allowedAdProtocols=["none","mraid","dapi"];/** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */const mraidPartners=["ironsource","applovin","unity","appreciate","chartboost","mytarget","liftoff","adcolony","adikteev","bigabid","inmobi"];/** @type {LANGUAGE[]} */const allowedLanguages=["auto","en","es","zh","hi","ar","fr","de","ja","pt"];/** @type {ORIENTATION[]} */const allowedOrientations=["both","portrait","landscape","square"];/**
2
- * Parses command line arguments based on provided options configuration
3
- * @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
4
- * @returns {Record<string, any>} Parsed options object with values from command line arguments
1
+ "use strict";/** @type {string[]} List of supported advertising networks */const allowedAdNetworks=["preview","applovin","unity","google","ironsource","facebook","moloco","adcolony","mintegral","vungle","tapjoy","snapchat","tiktok","appreciate","chartboost","pangle","mytarget","liftoff","smadex","adikteev","bigabid","inmobi","bigoads"];/** @type {AD_PROTOCOL[]} List of supported advertising protocols */const allowedAdProtocols=["none","mraid","dapi"];/** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */const mraidPartners=["ironsource","applovin","unity","appreciate","chartboost","mytarget","liftoff","adcolony","adikteev","bigabid","inmobi"];/** @type {LANGUAGE[]} */const allowedLanguages=["auto","en","es","zh","hi","ar","fr","de","ja","pt"];/** @type {ORIENTATION[]} */const allowedOrientations=["both","portrait","landscape","square"];/**
2
+ * Parses command line arguments based on provided options configuration
3
+ * @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
4
+ * @returns {Record<string, any>} Parsed options object with values from command line arguments
5
5
  */exports.parseArgvOptions=function parseArgvOptions(posiibleOptions){const argvOptions={};// Track which options were explicitly set via CLI args
6
6
  const cliExplicitKeys=new Set;for(let possibleOption of posiibleOptions){if(possibleOption.defaultValue!==undefined)argvOptions[possibleOption.alias||possibleOption.name]=possibleOption.defaultValue}let adNetwork=null;for(let i=2;i<process.argv.length;i++){let key=process.argv[i];let inlineValue=null;// 支持 --key=value 格式
7
7
  if(key.includes("=")){const eqIndex=key.indexOf("=");inlineValue=key.slice(eqIndex+1);key=key.slice(0,eqIndex)}const allowedOption=posiibleOptions.find(e=>"--"+e.name===key);if(allowedOption){let value=true;if(allowedOption.hasValue){// 优先使用内联值(--key=value),否则使用下一个参数
@@ -1,14 +1,14 @@
1
- "use strict";/**
2
- * 解析 isChannelFold2Zip 相关标志
3
- *
4
- * isChannelFold2Zip:所有渠道(排除 preview)输出 dist/渠道名称.zip
5
- * - ZIP 渠道(google/tiktok 等):正常 ZIP 流程,输出路径改为 dist/渠道名称.zip
6
- * - 非 ZIP 渠道(facebook 默认等):强制将 HTML 打包成 zip,输出 dist/渠道名称.zip
7
- *
8
- * @param {object} params
9
- * @param {boolean} params.isChannelFold2ZipOption - buildOptions.isChannelFold2Zip 的值
10
- * @param {string} params.adNetwork - 当前渠道名称
11
- * @param {boolean} params.isZipOutput - 当前渠道是否本身就走 ZIP 输出流程
12
- * @returns {{ isChannelFold2Zip: boolean, isZipOutputFinal: boolean }}
1
+ "use strict";/**
2
+ * 解析 isChannelFold2Zip 相关标志
3
+ *
4
+ * isChannelFold2Zip:所有渠道(排除 preview)输出 dist/渠道名称.zip
5
+ * - ZIP 渠道(google/tiktok 等):正常 ZIP 流程,输出路径改为 dist/渠道名称.zip
6
+ * - 非 ZIP 渠道(facebook 默认等):强制将 HTML 打包成 zip,输出 dist/渠道名称.zip
7
+ *
8
+ * @param {object} params
9
+ * @param {boolean} params.isChannelFold2ZipOption - buildOptions.isChannelFold2Zip 的值
10
+ * @param {string} params.adNetwork - 当前渠道名称
11
+ * @param {boolean} params.isZipOutput - 当前渠道是否本身就走 ZIP 输出流程
12
+ * @returns {{ isChannelFold2Zip: boolean, isZipOutputFinal: boolean }}
13
13
  */function resolveChannelFold2Zip({isChannelFold2ZipOption,adNetwork,isZipOutput}){const isChannelFold2Zip=!!(isChannelFold2ZipOption&&adNetwork!=="preview");// 非 ZIP 渠道启用 isChannelFold2Zip 时,需要强制走 ZIP 输出流程
14
14
  const isZipOutputFinal=isZipOutput||isChannelFold2Zip;if(isChannelFold2Zip){console.log(`✅ isChannelFold2Zip enabled: output will be dist/${adNetwork}.zip`)}return{isChannelFold2Zip,isZipOutputFinal}}exports.resolveChannelFold2Zip=resolveChannelFold2Zip;
@@ -1,40 +1,40 @@
1
- "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");const SCHEMA_BACKUP_SUFFIX=".backup";const SCHEMA_BACKUP_COMMENT="// [\u81EA\u52A8\u751F\u6210\u7684\u5907\u4EFD] \u6B64\u6587\u4EF6\u7531\u6253\u5305\u6D41\u7A0B\u521B\u5EFA\u3002\n"+"// \u5982\u679C\u6253\u5305\u4E2D\u65AD\uFF0C\u53EF\u4EE5\u79FB\u9664 \".backup\" \u540E\u7F00\uFF0C\n"+"// \u5E76\u7528\u6B64\u6587\u4EF6\u66FF\u6362\u539F\u59CB\u7684 \"theme.schema.json5\" \u6765\u6062\u590D\u3002\n";/**
2
- * 递归提取 JSON Schema 定义中的 `default` 值。
3
- *
4
- * - 对于 "object" 类型:递归遍历 `properties` 并构建嵌套对象。
5
- * - 对于基本类型:如果存在 `default` 值则返回。
6
- *
7
- * @param {object} schema - JSON Schema(或子 schema)对象
8
- * @returns {*} 提取的默认值树,如果没有则返回 undefined
1
+ "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");const SCHEMA_BACKUP_SUFFIX=".backup";const SCHEMA_BACKUP_COMMENT="// [\u81EA\u52A8\u751F\u6210\u7684\u5907\u4EFD] \u6B64\u6587\u4EF6\u7531\u6253\u5305\u6D41\u7A0B\u521B\u5EFA\u3002\n"+"// \u5982\u679C\u6253\u5305\u4E2D\u65AD\uFF0C\u53EF\u4EE5\u79FB\u9664 \".backup\" \u540E\u7F00\uFF0C\n"+"// \u5E76\u7528\u6B64\u6587\u4EF6\u66FF\u6362\u539F\u59CB\u7684 \"theme.schema.json5\" \u6765\u6062\u590D\u3002\n";/**
2
+ * 递归提取 JSON Schema 定义中的 `default` 值。
3
+ *
4
+ * - 对于 "object" 类型:递归遍历 `properties` 并构建嵌套对象。
5
+ * - 对于基本类型:如果存在 `default` 值则返回。
6
+ *
7
+ * @param {object} schema - JSON Schema(或子 schema)对象
8
+ * @returns {*} 提取的默认值树,如果没有则返回 undefined
9
9
  */function extractDefaultsFromSchema(schema){if(!schema||typeof schema!=="object")return undefined;if(schema.type==="object"&&schema.properties){const result={};for(const[key,propSchema]of Object.entries(schema.properties)){const val=extractDefaultsFromSchema(propSchema);if(val!==undefined){result[key]=val}}return Object.keys(result).length>0?result:undefined}// 基本类型或数组类型,带有默认值
10
- if("default"in schema){return schema.default}return undefined}/**
11
- * Check whether the parsed content of theme.schema.json5 is a real JSON Schema
12
- * (has "type" and "properties" fields) or has already been overwritten with
13
- * plain merged data (e.g. due to a previous interrupted build).
14
- *
15
- * @param {object} obj - Parsed content of theme.schema.json5
16
- * @returns {boolean} true if it looks like a valid JSON Schema definition
17
- */function isJsonSchema(obj){return obj&&typeof obj==="object"&&obj.type==="object"&&typeof obj.properties==="object"}/**
18
- * 根据 schema 校验指定主题的主题数据。
19
- *
20
- * 项目中预期的文件结构:
21
- * src/theme/theme.schema.json5 – JSON Schema(JSON5 格式)
22
- * src/theme/{themeName}/theme.data.json5 – 各主题的数据
23
- *
24
- * 通过 extractDefaultsFromSchema 从 schema 的 `default` 字段提取默认值,
25
- * 然后与主题数据深度合并(主题数据优先)后再进行校验。
26
- *
27
- * 保险机制:如果 theme.schema.json5 已经是纯 JSON 数据(非 schema 格式),
28
- * 说明上次构建中断后未能恢复,此时直接使用该数据,跳过校验。
29
- *
30
- * @param {string} themeName - 主题目录名称
31
- * @param {string} projectRoot - 项目根目录的绝对路径
32
- * @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
33
- * @returns {{ defaultData: object, mergedData: object, isPlainData: boolean, errors: string[] | null }}
34
- * defaultData – 从 schema 提取的默认值
35
- * mergedData – schema 默认值与主题数据合并后的完整数据
36
- * isPlainData – 如果 schema 文件已经是纯数据(非 schema 格式)则为 true
37
- * errors – 校验通过为 null,否则为错误信息数组
10
+ if("default"in schema){return schema.default}return undefined}/**
11
+ * Check whether the parsed content of theme.schema.json5 is a real JSON Schema
12
+ * (has "type" and "properties" fields) or has already been overwritten with
13
+ * plain merged data (e.g. due to a previous interrupted build).
14
+ *
15
+ * @param {object} obj - Parsed content of theme.schema.json5
16
+ * @returns {boolean} true if it looks like a valid JSON Schema definition
17
+ */function isJsonSchema(obj){return obj&&typeof obj==="object"&&obj.type==="object"&&typeof obj.properties==="object"}/**
18
+ * 根据 schema 校验指定主题的主题数据。
19
+ *
20
+ * 项目中预期的文件结构:
21
+ * src/theme/theme.schema.json5 – JSON Schema(JSON5 格式)
22
+ * src/theme/{themeName}/theme.data.json5 – 各主题的数据
23
+ *
24
+ * 通过 extractDefaultsFromSchema 从 schema 的 `default` 字段提取默认值,
25
+ * 然后与主题数据深度合并(主题数据优先)后再进行校验。
26
+ *
27
+ * 保险机制:如果 theme.schema.json5 已经是纯 JSON 数据(非 schema 格式),
28
+ * 说明上次构建中断后未能恢复,此时直接使用该数据,跳过校验。
29
+ *
30
+ * @param {string} themeName - 主题目录名称
31
+ * @param {string} projectRoot - 项目根目录的绝对路径
32
+ * @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
33
+ * @returns {{ defaultData: object, mergedData: object, isPlainData: boolean, errors: string[] | null }}
34
+ * defaultData – 从 schema 提取的默认值
35
+ * mergedData – schema 默认值与主题数据合并后的完整数据
36
+ * isPlainData – 如果 schema 文件已经是纯数据(非 schema 格式)则为 true
37
+ * errors – 校验通过为 null,否则为错误信息数组
38
38
  */function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- 解析文件路径 ---
39
39
  const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- 读取并解析文件 ---
40
40
  if(!fs.existsSync(schemaPath)){return{defaultData:null,mergedData:null,isPlainData:false,errors:[`未找到 Schema 文件: ${schemaPath}`]}}const schemaContent=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));// Safety check: if the file is already plain data (not a JSON Schema),
@@ -43,51 +43,51 @@ if(!fs.existsSync(schemaPath)){return{defaultData:null,mergedData:null,isPlainDa
43
43
  if(!isJsonSchema(schemaContent)){return{defaultData:schemaContent,mergedData:schemaContent,isPlainData:true,errors:null}}// 从 schema 提取默认值
44
44
  const defaultData=extractDefaultsFromSchema(schemaContent)||{};let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}// 深度合并(主题数据覆盖 schema 默认值),然后校验
45
45
  const merged=deepMerge(defaultData,themeData);// --- 根据 schema 校验合并后的数据 ---
46
- const ajv=new Ajv({allErrors:true,strict:false});const validate=ajv.compile(schemaContent);const valid=validate(merged);if(!valid){const errors=validate.errors.map(err=>{const field=err.instancePath||"(root)";return`${field} ${err.message}`});return{defaultData,mergedData:merged,isPlainData:false,errors}}return{defaultData,mergedData:merged,isPlainData:false,errors:null}}/**
47
- * 深度合并两个普通对象。`override` 中的值优先。
48
- * 数组会被整体替换(不会拼接)。
49
- *
50
- * @param {object} base
51
- * @param {object} override
52
- * @returns {object}
53
- */function deepMerge(base,override){const result={...base};for(const key of Object.keys(override)){const baseVal=base[key];const overVal=override[key];if(overVal!==null&&typeof overVal==="object"&&!Array.isArray(overVal)&&baseVal!==null&&typeof baseVal==="object"&&!Array.isArray(baseVal)){result[key]=deepMerge(baseVal,overVal)}else{result[key]=overVal}}return result}/**
54
- * 备份原始的 theme.schema.json5 文件。
55
- * 备份文件包含注释头,说明如何手动恢复。
56
- *
57
- * @param {string} schemaPath - theme.schema.json5 的绝对路径
58
- * @returns {string} 备份文件路径
59
- */function backupSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;const originalContent=fs.readFileSync(schemaPath,"utf8");fs.writeFileSync(backupPath,SCHEMA_BACKUP_COMMENT+originalContent,"utf8");return backupPath}/**
60
- * 从备份文件恢复 theme.schema.json5 并删除备份。
61
- *
62
- * @param {string} schemaPath - theme.schema.json5 的绝对路径
46
+ const ajv=new Ajv({allErrors:true,strict:false});const validate=ajv.compile(schemaContent);const valid=validate(merged);if(!valid){const errors=validate.errors.map(err=>{const field=err.instancePath||"(root)";return`${field} ${err.message}`});return{defaultData,mergedData:merged,isPlainData:false,errors}}return{defaultData,mergedData:merged,isPlainData:false,errors:null}}/**
47
+ * 深度合并两个普通对象。`override` 中的值优先。
48
+ * 数组会被整体替换(不会拼接)。
49
+ *
50
+ * @param {object} base
51
+ * @param {object} override
52
+ * @returns {object}
53
+ */function deepMerge(base,override){const result={...base};for(const key of Object.keys(override)){const baseVal=base[key];const overVal=override[key];if(overVal!==null&&typeof overVal==="object"&&!Array.isArray(overVal)&&baseVal!==null&&typeof baseVal==="object"&&!Array.isArray(baseVal)){result[key]=deepMerge(baseVal,overVal)}else{result[key]=overVal}}return result}/**
54
+ * 备份原始的 theme.schema.json5 文件。
55
+ * 备份文件包含注释头,说明如何手动恢复。
56
+ *
57
+ * @param {string} schemaPath - theme.schema.json5 的绝对路径
58
+ * @returns {string} 备份文件路径
59
+ */function backupSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;const originalContent=fs.readFileSync(schemaPath,"utf8");fs.writeFileSync(backupPath,SCHEMA_BACKUP_COMMENT+originalContent,"utf8");return backupPath}/**
60
+ * 从备份文件恢复 theme.schema.json5 并删除备份。
61
+ *
62
+ * @param {string} schemaPath - theme.schema.json5 的绝对路径
63
63
  */function restoreSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;if(!fs.existsSync(backupPath))return;const backupContent=fs.readFileSync(backupPath,"utf8");// 去除自动生成的注释头(以 "//" 开头的行)
64
- const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!line.startsWith("//"));const originalContent=lines.slice(contentStart>=0?contentStart:0).join("\n");fs.writeFileSync(schemaPath,originalContent,"utf8");fs.unlinkSync(backupPath)}/**
65
- * 用合并后的数据(schema 默认值 + 主题覆盖值)覆写 theme.schema.json5,
66
- * 以便打包工具在构建时可以直接使用。
67
- *
68
- * @param {string} schemaPath - theme.schema.json5 的绝对路径
69
- * @param {object} mergedData - 要写入的合并后的主题数据
70
- */function overwriteSchemaWithMerged(schemaPath,mergedData){const content=JSON.stringify(mergedData,null,2);fs.writeFileSync(schemaPath,content,"utf8")}/**
71
- * 执行主题校验并输出标准日志。
72
- * 单次构建(build.js)和批量构建(batch-build.js)共用此函数。
73
- *
74
- * 流程:
75
- * 1. 根据 theme.schema.json5 校验 theme.data.json5
76
- * 2. 备份 theme.schema.json5(包含恢复说明)
77
- * 3. 通过 extractDefaultsFromSchema 从 schema 提取默认值
78
- * 4. 用默认数据覆写 theme.schema.json5 以供构建使用
79
- *
80
- * 构建完成后(无论成功或失败),调用 restoreSchema() 恢复原始文件。
81
- *
82
- * @param {string} themeName - 主题目录名称
83
- * @param {string} projectRoot - 项目根目录的绝对路径
84
- * @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
85
- * @param {object} [options]
86
- * @param {function} [options.log] - 自定义日志函数(message, level)。默认使用 console.log。
87
- * @returns {{ valid: boolean, errors: string[]|null, schemaPath: string|null }}
88
- * valid – schema 文件不存在(跳过)或校验通过时为 true
89
- * errors – 校验通过为 null,否则为错误信息数组
90
- * schemaPath – theme.schema.json5 的绝对路径(用于 restoreSchema 调用),或 null
64
+ const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!line.startsWith("//"));const originalContent=lines.slice(contentStart>=0?contentStart:0).join("\n");fs.writeFileSync(schemaPath,originalContent,"utf8");fs.unlinkSync(backupPath)}/**
65
+ * 用合并后的数据(schema 默认值 + 主题覆盖值)覆写 theme.schema.json5,
66
+ * 以便打包工具在构建时可以直接使用。
67
+ *
68
+ * @param {string} schemaPath - theme.schema.json5 的绝对路径
69
+ * @param {object} mergedData - 要写入的合并后的主题数据
70
+ */function overwriteSchemaWithMerged(schemaPath,mergedData){const content=JSON.stringify(mergedData,null,2);fs.writeFileSync(schemaPath,content,"utf8")}/**
71
+ * 执行主题校验并输出标准日志。
72
+ * 单次构建(build.js)和批量构建(batch-build.js)共用此函数。
73
+ *
74
+ * 流程:
75
+ * 1. 根据 theme.schema.json5 校验 theme.data.json5
76
+ * 2. 备份 theme.schema.json5(包含恢复说明)
77
+ * 3. 通过 extractDefaultsFromSchema 从 schema 提取默认值
78
+ * 4. 用默认数据覆写 theme.schema.json5 以供构建使用
79
+ *
80
+ * 构建完成后(无论成功或失败),调用 restoreSchema() 恢复原始文件。
81
+ *
82
+ * @param {string} themeName - 主题目录名称
83
+ * @param {string} projectRoot - 项目根目录的绝对路径
84
+ * @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
85
+ * @param {object} [options]
86
+ * @param {function} [options.log] - 自定义日志函数(message, level)。默认使用 console.log。
87
+ * @returns {{ valid: boolean, errors: string[]|null, schemaPath: string|null }}
88
+ * valid – schema 文件不存在(跳过)或校验通过时为 true
89
+ * errors – 校验通过为 null,否则为错误信息数组
90
+ * schemaPath – theme.schema.json5 的绝对路径(用于 restoreSchema 调用),或 null
91
91
  */function runThemeValidation(themeName,projectRoot,themeConfig,options){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");// 没有 schema 文件 → 完全跳过校验
92
92
  if(!fs.existsSync(schemaPath)){return{valid:true,errors:null,schemaPath:null}}const logFn=options&&options.log||function defaultLog(msg,level){const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m",warn:"\x1B[33m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}${msg}${reset}`)};logFn(`校验主题数据: ${themeName}`,"info");const{defaultData,mergedData,isPlainData,errors}=validateThemeData(themeName,projectRoot,themeConfig);// Safety: schema file is already plain data (previous build was interrupted)
93
93
  // Use it directly, no need to backup/overwrite
package/core/vite.dev.js CHANGED
@@ -1,9 +1,9 @@
1
- "use strict";const path=require("path");const fs=require("fs");const{options}=require("./options.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{buildDefines}=require("./utils/buildDefines.js");const{logOptions}=require("./utils/logOptions.js");/**
2
- * 创建 Vite 开发配置
3
- * @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项,会与默认选项合并
4
- * @param {Record<string, any>} [customDefines] - 额外的 define 常量
5
- * @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置,会与默认配置合并
6
- * @returns {import('vite').UserConfig} 最终的 Vite 开发配置
1
+ "use strict";const path=require("path");const fs=require("fs");const{options}=require("./options.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{buildDefines}=require("./utils/buildDefines.js");const{logOptions}=require("./utils/logOptions.js");/**
2
+ * 创建 Vite 开发配置
3
+ * @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项,会与默认选项合并
4
+ * @param {Record<string, any>} [customDefines] - 额外的 define 常量
5
+ * @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置,会与默认配置合并
6
+ * @returns {import('vite').UserConfig} 最终的 Vite 开发配置
7
7
  */function makeViteDevConfig(customOptions,customDefines,viteCustomConfig){const devOptions=mergeOptions(options,customOptions);logOptions(devOptions);customDefines=customDefines||{};viteCustomConfig=viteCustomConfig||{};// 构建 define 常量(复用 webpack 的 buildDefines 逻辑)
8
8
  const defines={...buildDefines(),...devOptions.defines,__DEV__:JSON.stringify(devOptions["dev"]===undefined?true:devOptions["dev"]),...customDefines};// 解析 tsconfig/jsconfig 路径别名
9
9
  const tsConfigPath=path.resolve(devOptions.tsConfig||"tsconfig.json");const jsConfigPath=path.resolve(devOptions.jsConfig||"jsconfig.json");let aliasConfig={};// 尝试从 tsconfig/jsconfig 中读取 paths 配置
@@ -16,10 +16,10 @@ if(id.endsWith(".atlas")){const content=fs.readFileSync(id);const base64=content
16
16
  if(id.endsWith(".fnt")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/plain;base64,${base64}";`,map:null}}// .xml 文件 -> 原始文本
17
17
  if(id.endsWith(".xml")){const raw=fs.readFileSync(id,"utf-8");return{code:`export default ${JSON.stringify(raw)};`,map:null}}}}}/** @type {import('vite').UserConfig} */const viteConfig={root:".",resolve:{alias:aliasConfig},plugins:[json5Plugin(),assetInlinePlugin()],define:defines,server:{port:devOptions["port"]||3000,open:devOptions["open"]||false},build:{outDir:devOptions["outDir"]||"dist",sourcemap:true}};// 合并自定义配置
18
18
  if(viteCustomConfig){// 简单合并:自定义配置中的字段会覆盖默认配置
19
- const{plugins:customPlugins,define:customDefine,resolve:customResolve,server:customServer,build:customBuild,...rest}=viteCustomConfig;if(customPlugins){viteConfig.plugins=[...viteConfig.plugins,...customPlugins]}if(customDefine){viteConfig.define={...viteConfig.define,...customDefine}}if(customResolve){viteConfig.resolve={...viteConfig.resolve,...customResolve,alias:{...viteConfig.resolve.alias,...(customResolve.alias||{})}}}if(customServer){viteConfig.server={...viteConfig.server,...customServer}}if(customBuild){viteConfig.build={...viteConfig.build,...customBuild}}Object.assign(viteConfig,rest)}return viteConfig}/**
20
- * 启动 Vite 开发服务器
21
- * @param {import('vite').UserConfig} [viteConfig] - Vite 配置,不传则自动创建
22
- * @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项
23
- * @param {Record<string, any>} [customDefines] - 额外的 define 常量
24
- * @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置
19
+ const{plugins:customPlugins,define:customDefine,resolve:customResolve,server:customServer,build:customBuild,...rest}=viteCustomConfig;if(customPlugins){viteConfig.plugins=[...viteConfig.plugins,...customPlugins]}if(customDefine){viteConfig.define={...viteConfig.define,...customDefine}}if(customResolve){viteConfig.resolve={...viteConfig.resolve,...customResolve,alias:{...viteConfig.resolve.alias,...(customResolve.alias||{})}}}if(customServer){viteConfig.server={...viteConfig.server,...customServer}}if(customBuild){viteConfig.build={...viteConfig.build,...customBuild}}Object.assign(viteConfig,rest)}return viteConfig}/**
20
+ * 启动 Vite 开发服务器
21
+ * @param {import('vite').UserConfig} [viteConfig] - Vite 配置,不传则自动创建
22
+ * @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项
23
+ * @param {Record<string, any>} [customDefines] - 额外的 define 常量
24
+ * @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置
25
25
  */async function runViteDev(viteConfig,customOptions,customDefines,viteCustomConfig){if(!viteConfig){viteConfig=makeViteDevConfig(customOptions,customDefines,viteCustomConfig)}const{createServer}=require("vite");const server=await createServer(viteConfig);await server.listen();server.printUrls()}exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;