@playcraft/devkit 1.0.23 → 1.0.25-beta.1.0

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.
@@ -1,4 +1,4 @@
1
- "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const fs=require("fs");const path=require("path");const{runBuild}=require("../../core/webpack.build.js");const{runThemeValidation,restoreSchema}=require("../../core/utils/validateThemeData");const{runPreBuildChecks}=require("../../core/validators/pre-build-checker.js");const{FontInjectionWebpackPlugin}=require("../../core/plugins/FontInjectionWebpackPlugin.js");/**
1
+ "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const fs=require("fs");const path=require("path");const{runBuild}=require("../../core/webpack.build.js");const{runThemeValidation,restoreSchema}=require("../../core/utils/validateThemeData");const{runPreBuildChecks}=require("../../core/validators/pre-build-checker.js");const{FontInjectionWebpackPlugin}=require("../../core/plugins/FontInjectionWebpackPlugin.js");const{SocialMediaInjectorPlugin}=require("../../core/plugins/SocialMediaInjectorPlugin.js");const{options}=require("../../core/options.js");/**
2
2
  * 从主题入口文件中检测当前主题名称。
3
3
  * 支持以下模式:
4
4
  * export { default } from './themeName';
@@ -15,7 +15,8 @@ const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(
15
15
  // - PLAYABLE_BATCH_MODE=1: 批量构建模式,子进程自动跳过检查(主进程已执行)
16
16
  const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1"||isBatchMode;async function runPreChecks(){if(shouldSkipChecks){console.log("\n\u26A0\uFE0F \u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09\n");return}const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false});if(!checkResult.success){console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6784\u5EFA\u3002");console.error("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1\n");process.exit(1)}}// 执行编译前检查(异步)
17
17
  runPreChecks().then(()=>{// 检查通过后继续原有流程
18
- continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u51FA\u9519:",err);process.exit(1)});function continueWithBuild(){// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
18
+ continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u51FA\u9519:",err);process.exit(1)});function continueWithBuild(){// 当前广告渠道(用于决定是否启用 social_media 的 OG meta 注入)
19
+ const adNetwork=options["network"]||"";// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
19
20
  // executed runThemeValidation and overwritten theme.schema.json5 with merged data.
20
21
  // Running it again in child processes would cause a race condition:
21
22
  // 1. Child reads the already-overwritten schema (pure data, no "properties"/"default")
@@ -23,5 +24,7 @@ continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68
23
24
  // 3. Child overwrites schema with {} → file becomes empty object
24
25
  const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";if(themeName&&!isBatchMode){const buildLog=(msg,level)=>{const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}[build] ${msg}${reset}`)};const{valid,schemaPath}=runThemeValidation(themeName,projectRoot,themeConfig,{log:buildLog});schemaPathToRestore=schemaPath;if(!valid){// 校验失败时恢复 schema(备份可能已创建)
25
26
  if(schemaPathToRestore)restoreSchema(schemaPathToRestore);process.exit(1)}}// 注入字体插件(build 模式 base64 内联)
26
- const fontPlugin=themeName?new FontInjectionWebpackPlugin({projectRoot,themeName,themeConfig}):null;// 执行构建,无论成功或失败都恢复 schema
27
- runBuild(undefined,undefined,undefined,{plugins:fontPlugin?[fontPlugin]:[]}).then(()=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore)}).catch(err=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore);console.error(err);process.exit(1)})}
27
+ const fontPlugin=themeName?new FontInjectionWebpackPlugin({projectRoot,themeName,themeConfig}):null;// social_media 渠道:注入 OG / Twitter meta 标签,让 Facebook / LinkedIn
28
+ // 等抓取预览时拿到正确的标题/描述/图片。无 shareMeta 时空跑,不影响构建
29
+ const socialMediaPlugin=themeName&&adNetwork==="social_media"?new SocialMediaInjectorPlugin({projectRoot,themeName,themeConfig}):null;// 执行构建,无论成功或失败都恢复 schema
30
+ runBuild(undefined,undefined,undefined,{plugins:[...(fontPlugin?[fontPlugin]:[]),...(socialMediaPlugin?[socialMediaPlugin]:[])]}).then(()=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore)}).catch(err=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore);console.error(err);process.exit(1)})}
@@ -94,6 +94,12 @@ const hasHeadContent=headContent.length>0;const decompressCodeReadable=`
94
94
  // 提取并放在模板 head 中,这里只需追加未被模板覆盖的内联脚本和样式
95
95
  // 如果原始 HTML 是片段(__hasHeadContent=false),如 Adikteev 格式,
96
96
  // 所有资源引用都在压缩数据中,需要完整恢复——包括外部脚本和样式表
97
+ //
98
+ // 重要:动态注入的外部脚本在页面解析完成后才异步加载执行,
99
+ // 此时 DOMContentLoaded 已触发,脚本内的事件监听器将永远无法执行。
100
+ // 因此在外部脚本加载完成后手动 dispatch DOMContentLoaded,确保游戏等代码正常初始化。
101
+ var __extScriptCount = 0;
102
+ var __extScriptLoaded = 0;
97
103
  Array.from(doc.head.children).forEach(function(child) {
98
104
  if (child.tagName === 'SCRIPT') {
99
105
  if (!child.src && child.textContent) {
@@ -105,11 +111,18 @@ const hasHeadContent=headContent.length>0;const decompressCodeReadable=`
105
111
  } else if (child.src && !__hasHeadContent) {
106
112
  // 外部 script(有 src 属性,如 <script src="creative.js">)
107
113
  // 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
114
+ __extScriptCount++;
108
115
  var extScript = document.createElement('script');
109
116
  extScript.src = child.src;
110
117
  if (child.type) extScript.type = child.type;
111
118
  if (child.async) extScript.async = true;
112
119
  if (child.defer) extScript.defer = true;
120
+ extScript.onload = function() {
121
+ __extScriptLoaded++;
122
+ if (__extScriptLoaded >= __extScriptCount) {
123
+ document.dispatchEvent(new Event('DOMContentLoaded'));
124
+ }
125
+ };
113
126
  document.head.appendChild(extScript);
114
127
  }
115
128
  } else if (child.tagName === 'STYLE') {
@@ -0,0 +1,27 @@
1
+ "use strict";/**
2
+ * Webpack 插件:social_media 渠道专用 — 在构建产物 HTML 的 </head> 之前
3
+ * 注入 OG / Twitter / Facebook 分享卡片 meta 标签。
4
+ *
5
+ * 设计动机:
6
+ * Facebook sharer.php 自 2014 起废弃了 t/title 等参数,
7
+ * 完全靠爬取目标页面 <head> 的 og:* meta 标签生成预览卡。
8
+ * 我们的 index.html 模板只含基础 meta,必须在构建时把
9
+ * gamePlay.shareMeta 中的 title/description/imageUrl/pageUrl 注入。
10
+ *
11
+ * 数据源(按优先级):
12
+ * 1. opts.mergedData.gamePlay.shareMeta (显式传入,批量构建子进程用)
13
+ * 2. 从 <projectRoot>/<themeName>/theme.data.json5 读取
14
+ * 3. 都读不到 → 跳过注入(不报错,让用户后续补)
15
+ *
16
+ * 配合 injectSDKPlugins.js 的 social_media 分支在 build 时挂载。
17
+ */const HtmlWebpackPlugin=require("html-webpack-plugin");const fs=require("fs");const path=require("path");const JSON5=require("json5");class SocialMediaInjectorPlugin{/**
18
+ * @param {object} [opts]
19
+ * @param {string} [opts.projectRoot] - 项目根目录
20
+ * @param {string} [opts.themeName] - 当前主题目录名(不含前缀斜线)
21
+ * @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
22
+ * @param {object} [opts.mergedData] - 预合并数据(可选)
23
+ */constructor(opts={}){this.opts=opts}/** 从 mergedData / theme.data.json5 读出 shareMeta 块 */_loadShareMeta(){// 1) 优先用已传入的 mergedData
24
+ let merged=this.opts.mergedData;if(merged&&merged.gamePlay&&merged.gamePlay.shareMeta){return merged.gamePlay.shareMeta}// 2) 退化为读 theme.data.json5
25
+ const root=this.opts.projectRoot||process.cwd();const name=this.opts.themeName;if(!name)return null;const dataPath=path.join(root,this.opts.themeConfig&&this.opts.themeConfig.sourceDir||"src/theme",name,"theme.data.json5");if(!fs.existsSync(dataPath))return null;try{const raw=fs.readFileSync(dataPath,"utf8");const parsed=JSON5.parse(raw);return parsed&&parsed.gamePlay&&parsed.gamePlay.shareMeta?parsed.gamePlay.shareMeta:null}catch(e){console.warn("[SocialMediaInjectorPlugin] Failed to read theme.data.json5:",e.message);return null}}/** 把 shareMeta 渲染成 <meta> 标签字符串 */_buildMetaTags(meta){if(!meta)return"";const esc=s=>String(s).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");const lines=[];const title=(meta.title||"").trim();const description=(meta.description||"").trim();const image=(meta.imageUrl||"").trim();const url=(meta.pageUrl||"").trim();const siteName=(meta.siteName||"").trim();// Open Graph
26
+ if(title){lines.push(`<meta property="og:title" content="${esc(title)}" />`)}if(description){lines.push(`<meta property="og:description" content="${esc(description)}" />`)}if(image){lines.push(`<meta property="og:image" content="${esc(image)}" />`)}if(url){lines.push(`<meta property="og:url" content="${esc(url)}" />`)}if(siteName){lines.push(`<meta property="og:site_name" content="${esc(siteName)}" />`)}lines.push(`<meta property="og:type" content="website" />`);// Twitter Card
27
+ lines.push(`<meta name="twitter:card" content="summary_large_image" />`);if(title){lines.push(`<meta name="twitter:title" content="${esc(title)}" />`)}if(description){lines.push(`<meta name="twitter:description" content="${esc(description)}" />`)}if(image){lines.push(`<meta name="twitter:image" content="${esc(image)}" />`)}return lines.join("\n ")}apply(compiler){const pluginName="SocialMediaInjectorPlugin";compiler.hooks.compilation.tap(pluginName,compilation=>{HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(pluginName,(data,cb)=>{const shareMeta=this._loadShareMeta();if(!shareMeta){return cb(null,data)}const tags=this._buildMetaTags(shareMeta);if(tags){data.html=data.html.replace("</head>",` ${tags}\n </head>`)}cb(null,data)})})}}module.exports={SocialMediaInjectorPlugin};
@@ -0,0 +1,4 @@
1
+ "use strict";/**
2
+ * Vite dev 模式:social_media 渠道的 OG / Twitter meta 注入。
3
+ * 与 SocialMediaInjectorPlugin.js 输出完全一致,仅 hook 形态不同。
4
+ */const path=require("path");const fs=require("fs");const JSON5=require("json5");/** 把 shareMeta 渲染成 <meta> 标签字符串(与 webpack plugin 完全一致) */function buildMetaTags(meta){if(!meta)return[];const esc=s=>String(s).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");const tags=[];const{title,description,imageUrl,pageUrl,siteName}=meta;const t=(title||"").trim();const d=(description||"").trim();const i=(imageUrl||"").trim();const u=(pageUrl||"").trim();const s=(siteName||"").trim();if(t)tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:title",content:t}});if(d)tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:description",content:d}});if(i)tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:image",content:i}});if(u)tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:url",content:u}});if(s)tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:site_name",content:s}});tags.push({tag:"meta",injectTo:"head",attrs:{property:"og:type",content:"website"}});tags.push({tag:"meta",injectTo:"head",attrs:{name:"twitter:card",content:"summary_large_image"}});if(t)tags.push({tag:"meta",injectTo:"head",attrs:{name:"twitter:title",content:t}});if(d)tags.push({tag:"meta",injectTo:"head",attrs:{name:"twitter:description",content:d}});if(i)tags.push({tag:"meta",injectTo:"head",attrs:{name:"twitter:image",content:i}});return tags}/** 检测主题名(与 fontFamilyPlugin 逻辑保持一致) */function detectTheme(){const root=process.cwd();const buildsConfigPath=path.join(root,"builds.config.js");let config={sourceDir:"src/theme",entryFile:"src/theme/index.ts"};if(fs.existsSync(buildsConfigPath)){try{delete require.cache[require.resolve(buildsConfigPath)];const bc=require(buildsConfigPath);if(bc.themes)config={...config,...bc.themes}}catch(_){/* ignore */}}let name=null;const entryPath=path.join(root,config.entryFile);if(fs.existsSync(entryPath)){const content=fs.readFileSync(entryPath,"utf8");const m=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(m)name=m[1]}return{themeName:name,themeConfig:config}}/** 读取 shareMeta,缓存以避免 transformIndexHtml 多次触发时反复读盘 */let _cache={key:null,value:null};function loadShareMeta(){const{themeName,themeConfig}=detectTheme();if(!themeName)return null;const root=process.cwd();const dataPath=path.join(root,themeConfig.sourceDir||"src/theme",themeName,"theme.data.json5");if(!fs.existsSync(dataPath))return null;const stat=fs.statSync(dataPath);const key=`${dataPath}::${stat.mtimeMs}`;if(_cache.key===key)return _cache.value;try{const raw=fs.readFileSync(dataPath,"utf8");const parsed=JSON5.parse(raw);const value=parsed&&parsed.gamePlay&&parsed.gamePlay.shareMeta?parsed.gamePlay.shareMeta:null;_cache={key,value};return value}catch(e){console.warn("[socialMediaVitePlugin] Failed to read theme.data.json5:",e.message);return null}}function socialMediaVitePlugin(){return{name:"vite-plugin-social-media",apply:"serve",transformIndexHtml:{order:"pre",handler(){const meta=loadShareMeta();return buildMetaTags(meta)}}}}module.exports={socialMediaVitePlugin,buildMetaTags};
@@ -1,14 +1,18 @@
1
1
  "use strict";/**
2
2
  * 根据 AD_NETWORK 和 AD_PROTOCOL 注入对应的渠道 SDK 脚本插件
3
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");/**
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");const{SocialMediaInjectorPlugin}=require("../plugins/SocialMediaInjectorPlugin.js");/**
5
5
  * 注入渠道 SDK 插件到 webpack 配置中
6
6
  * @param {import('webpack').Configuration} webpackConfig - webpack 配置对象
7
7
  * @param {Object} options - 配置选项
8
8
  * @param {AD_NETWORK} options.network - 广告网络
9
9
  * @param {AD_PROTOCOL} options.protocol - 广告协议
10
10
  * @param {string} [options.orientation] - 屏幕方向(google 渠道需要)
11
- */function injectSDKPlugins(webpackConfig,options){const{network:adNetwork,protocol:adProtocol,orientation}=options;// 协议层注入
11
+ * @param {string} [options.projectRoot] - 项目根目录(social_media 渠道读取 theme.data.json5 用)
12
+ * @param {string} [options.themeName] - 当前主题名
13
+ * @param {object} [options.themeConfig] - builds.config.js 中的 themes 配置
14
+ * @param {object} [options.mergedData] - 预合并的主题数据(可选)
15
+ */function injectSDKPlugins(webpackConfig,options){const{network:adNetwork,protocol:adProtocol,orientation,projectRoot,themeName,themeConfig,mergedData}=options;// 协议层注入
12
16
  if("dapi"===adProtocol){webpackConfig.plugins.push(new DAPIInjectorPlugin)}else if("mraid"===adProtocol){// 只有 ironsource 才注入 MRAIDInjectorPlugin
13
17
  // Unity 使用专门的 UnityInjectorPlugin(在渠道特定注入中处理)
14
18
  if(adNetwork==="ironsource"){webpackConfig.plugins.push(new MRAIDInjectorPlugin)}}// 渠道特定注入
@@ -16,4 +20,6 @@ if(adNetwork==="google"){webpackConfig.plugins.push(new ExitAPIInjectorPlugin(or
16
20
  webpackConfig.plugins.push(new UnityInjectorPlugin)}else if(adNetwork==="pangle"){webpackConfig.plugins.push(new PangleInjectorPlugin)}else if(adNetwork==="tiktok"){webpackConfig.plugins.push(new TikTokInjectorPlugin)}else if(adNetwork==="mintegral"){webpackConfig.plugins.push(new MintegralInjectorPlugin)}else if(adNetwork==="bigoads"){// BigoAds 在 dev 模式下注入模拟配置
17
21
  webpackConfig.plugins.push(new BigoAdsInjectorPlugin)}else if(adNetwork==="liftoff"){webpackConfig.plugins.push(new LiftoffInjectorPlugin)}else if(adNetwork==="snapchat"){// Snapchat 注入 ScPlayableAd SDK
18
22
  webpackConfig.plugins.push(new SnapchatInjectorPlugin)}else if(adNetwork==="adikteev"){// Adikteev 注入 MRAID mock 和占位符变量
19
- webpackConfig.plugins.push(new AdikteevInjectorPlugin)}}exports.injectSDKPlugins=injectSDKPlugins;
23
+ webpackConfig.plugins.push(new AdikteevInjectorPlugin)}else if(adNetwork==="social_media"){// social_media 渠道:注入 OG / Twitter meta 标签,让 Facebook / LinkedIn 爬取时
24
+ // 能拿到正确的标题/描述/预览图。注意 Telegram / Slack / Discord 也读 OG meta
25
+ webpackConfig.plugins.push(new SocialMediaInjectorPlugin({projectRoot,themeName,themeConfig,mergedData}))}}exports.injectSDKPlugins=injectSDKPlugins;
@@ -1,4 +1,4 @@
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"];/**
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","social_media"];/** @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
2
  * Parses command line arguments based on provided options configuration
3
3
  * @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
4
4
  * @returns {Record<string, any>} Parsed options object with values from command line arguments
@@ -43,7 +43,8 @@ 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}}/**
46
+ const ajv=new Ajv({allErrors:true,strict:false});let validate;try{validate=ajv.compile(schemaContent)}catch(schemaErr){// schema 文件被之前的构建中断污染(mergedData 覆盖了 schema)→ 按纯数据恢复
47
+ console.warn(`[validateThemeData] schema 编译失败(可能被覆盖),降级为纯数据: ${schemaErr.message}`);return{defaultData:merged,mergedData:merged,isPlainData:true,errors:null}}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
48
  * 深度合并两个普通对象。`override` 中的值优先。
48
49
  * 数组会被整体替换(不会拼接)。
49
50
  *
package/core/vite.dev.js CHANGED
@@ -1,4 +1,4 @@
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");const{getOrGenerateFontCSS}=require("./utils/fontResolver.js");// ── 公共 Vite 插件(供项目 vite.config.ts 直接引用) ──
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");const{getOrGenerateFontCSS}=require("./utils/fontResolver.js");const{socialMediaVitePlugin}=require("./plugins/socialMediaVitePlugin.js");// ── 公共 Vite 插件(供项目 vite.config.ts 直接引用) ──
2
2
  /** JSON5 文件加载插件 */function json5Plugin(){return{name:"vite-plugin-json5",transform(code,id){if(id.endsWith(".json5")){const JSON5=require("json5");const raw=fs.readFileSync(id,"utf-8");const parsed=JSON5.parse(raw);return{code:`export default ${JSON.stringify(parsed)};`,map:null}}}}}/** 资源内联插件(模拟 webpack 的 asset/inline 行为) */function assetInlinePlugin(){return{name:"vite-plugin-asset-inline",enforce:"pre",transform(code,id){// .atlas 文件 -> base64 data URL
3
3
  if(id.endsWith(".atlas")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/atlas;base64,${base64}";`,map:null}}// .fnt 文件 -> base64 data URL
4
4
  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 文件 -> 原始文本
@@ -15,7 +15,7 @@ const defines={...buildDefines(),...devOptions.defines,__DEV__:JSON.stringify(de
15
15
  const tsConfigPath=path.resolve(devOptions.tsConfig||"tsconfig.json");const jsConfigPath=path.resolve(devOptions.jsConfig||"jsconfig.json");let aliasConfig={};// 尝试从 tsconfig/jsconfig 中读取 paths 配置
16
16
  let configFilePath=null;if(fs.existsSync(tsConfigPath)){configFilePath=tsConfigPath}else if(fs.existsSync(jsConfigPath)){configFilePath=jsConfigPath}if(configFilePath){try{var _config$compilerOptio;const JSON5=require("json5");const configContent=fs.readFileSync(configFilePath,"utf-8");const config=JSON5.parse(configContent);const paths=((_config$compilerOptio=config.compilerOptions)===null||_config$compilerOptio===void 0?void 0:_config$compilerOptio.paths)||{};for(const[alias,targets]of Object.entries(paths)){var _targets$;// 将 tsconfig paths 格式 "foo/*" -> ["src/foo/*"] 转换为 vite alias
17
17
  const cleanAlias=alias.replace("/*","");const cleanTarget=((_targets$=targets[0])===null||_targets$===void 0?void 0:_targets$.replace("/*",""))||"";if(cleanAlias&&cleanTarget){aliasConfig[cleanAlias]=path.resolve(cleanTarget)}}}catch(err){console.log("\x1B[33m\u26A0\uFE0F \u8BFB\u53D6\u8DEF\u5F84\u914D\u7F6E\u5931\u8D25: "+err.message+"\x1B[0m")}}// 默认别名
18
- aliasConfig={assets:path.resolve("assets"),"@":path.resolve("src"),...aliasConfig};/** @type {import('vite').UserConfig} */const viteConfig={root:".",resolve:{alias:aliasConfig},plugins:[json5Plugin(),assetInlinePlugin(),fontFamilyPlugin()],define:defines,server:{port:devOptions["port"]||3000,open:devOptions["open"]||false},build:{outDir:devOptions["outDir"]||"dist",sourcemap:true}};// 合并自定义配置
18
+ aliasConfig={assets:path.resolve("assets"),"@":path.resolve("src"),...aliasConfig};/** @type {import('vite').UserConfig} */const viteConfig={root:".",resolve:{alias:aliasConfig},plugins:[json5Plugin(),assetInlinePlugin(),fontFamilyPlugin(),socialMediaVitePlugin()],define:defines,server:{port:devOptions["port"]||3000,open:devOptions["open"]||false},build:{outDir:devOptions["outDir"]||"dist",sourcemap:true}};// 合并自定义配置
19
19
  if(viteCustomConfig){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
20
  * 深度合并两个 Vite 配置对象。override 覆盖 base。
21
21
  * 数组会拼接(plugins),其余字段浅覆盖。
package/defines.d.ts CHANGED
@@ -38,7 +38,8 @@ declare const AD_NETWORK:
38
38
  | 'adikteev'
39
39
  | 'bigabid'
40
40
  | 'inmobi'
41
- | 'bigoads';
41
+ | 'bigoads'
42
+ | 'social_media';
42
43
 
43
44
  declare const AD_PROTOCOL: 'mraid' | 'dapi' | 'nucleo' | 'none';
44
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/devkit",
3
- "version": "1.0.23",
3
+ "version": "1.0.25-beta.1.0",
4
4
  "description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
5
5
  "main": "core/index.js",
6
6
  "module": "core/index.js",