@playcraft/devkit 1.0.25 → 1.0.26

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';
@@ -7,7 +7,15 @@
7
7
  const buildsConfigPath=path.join(projectRoot,"builds.config.js");let themeConfig={sourceDir:"src/theme",entryFile:"src/theme/index.ts"};if(fs.existsSync(buildsConfigPath)){try{delete require.cache[buildsConfigPath];const config=require(buildsConfigPath);if(config.themes){themeConfig={...themeConfig,...config.themes}}}catch(e){// 忽略错误
8
8
  }}const entryPath=path.join(projectRoot,themeConfig.entryFile);if(!fs.existsSync(entryPath)){return{themeName:null,themeConfig}}const content=fs.readFileSync(entryPath,"utf8");// 匹配: export { default } from './themeName' 或 import ... from './themeName'
9
9
  const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(match){return{themeName:match[1],themeConfig}}return{themeName:null,themeConfig}}// --- 主流程 ---
10
- const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(projectRoot);let schemaPathToRestore=null;// ============================================================
10
+ const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(projectRoot);let schemaPathToRestore=null;let isExiting=false;// ============================================================
11
+ // 📌 中断信号处理(Ctrl+C / kill)
12
+ // ============================================================
13
+ // 单主题构建过程中,theme.schema.json5 可能已被 runThemeValidation()
14
+ // 覆写为合并后的数据。若此时进程被中断,需要在退出前恢复原始 schema,
15
+ // 避免文件被强制中断而遗留在“已覆写”状态。
16
+ // 注意:SIGKILL(kill -9)无法被捕获,属于无法规避的极端情况,
17
+ // 此时依赖 validateThemeData.js 中的兜底检测(下次构建自动识别并降级处理)。
18
+ function handleInterrupt(){if(isExiting)return;isExiting=true;console.log("\n\x1B[33m\u6536\u5230\u4E2D\u65AD\u4FE1\u53F7\uFF0C\u6B63\u5728\u6E05\u7406...\x1B[0m");if(schemaPathToRestore){try{restoreSchema(schemaPathToRestore);console.log("\x1B[36m\u5DF2\u6062\u590D theme.schema.json5\x1B[0m")}catch(e){console.error(`\x1b[31m恢复 theme.schema.json5 失败: ${e.message}\x1b[0m`)}schemaPathToRestore=null}process.exit(1)}process.on("SIGINT",handleInterrupt);process.on("SIGTERM",handleInterrupt);// ============================================================
11
19
  // 📌 编译前检查(埋点验证等)
12
20
  // ============================================================
13
21
  // 支持通过环境变量控制是否跳过检查
@@ -15,7 +23,8 @@ const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(
15
23
  // - PLAYABLE_BATCH_MODE=1: 批量构建模式,子进程自动跳过检查(主进程已执行)
16
24
  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
25
  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
26
+ continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u51FA\u9519:",err);process.exit(1)});function continueWithBuild(){// 当前广告渠道(用于决定是否启用 social_media 的 OG meta 注入)
27
+ const adNetwork=options["network"]||"";// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
19
28
  // executed runThemeValidation and overwritten theme.schema.json5 with merged data.
20
29
  // Running it again in child processes would cause a race condition:
21
30
  // 1. Child reads the already-overwritten schema (pure data, no "properties"/"default")
@@ -23,5 +32,7 @@ continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68
23
32
  // 3. Child overwrites schema with {} → file becomes empty object
24
33
  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
34
  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)})}
35
+ const fontPlugin=themeName?new FontInjectionWebpackPlugin({projectRoot,themeName,themeConfig}):null;// social_media 渠道:注入 OG / Twitter meta 标签,让 Facebook / LinkedIn
36
+ // 等抓取预览时拿到正确的标题/描述/图片。无 shareMeta 时空跑,不影响构建
37
+ const socialMediaPlugin=themeName&&adNetwork==="social_media"?new SocialMediaInjectorPlugin({projectRoot,themeName,themeConfig}):null;// 执行构建,无论成功或失败都恢复 schema
38
+ 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)})}
@@ -56,8 +56,8 @@ const headMatch=html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);if(!headMatch)retur
56
56
  const metaRegex=/<meta\s+[^>]*?\/?>/gi;let match;while((match=metaRegex.exec(headHtml))!==null){const tag=match[0];// 排除 charset 和 viewport(自解压模板已包含)
57
57
  if(/charset\s*=/i.test(tag))continue;if(/name\s*=\s*["']viewport["']/i.test(tag))continue;elements.push(tag)}// 提取 title 标签
58
58
  const titleMatch=headHtml.match(/<title[^>]*>[\s\S]*?<\/title>/i);if(titleMatch){elements.push(titleMatch[0])}// 提取外部 script 标签(有 src 属性的)
59
- const scriptRegex=/<script\s+[^>]*src\s*=\s*["'][^"']+["'][^>]*>[\s\S]*?<\/script>/gi;while((match=scriptRegex.exec(headHtml))!==null){elements.push(match[0])}// 提取 link 标签
60
- const linkRegex=/<link\s+[^>]*?\/?>/gi;while((match=linkRegex.exec(headHtml))!==null){elements.push(match[0])}return elements.join("\n ")}/**
59
+ const scriptRegex=/<script\s+[^>]*src\s*=\s*["'][^"']+["'][^>]*>[\s\S]*?<\/script>/gi;while((match=scriptRegex.exec(headHtml))!==null){elements.push(match[0])}// 提取 link 标签(排除 icon,模板已内置空 favicon,避免重复声明)
60
+ const linkRegex=/<link\s+[^>]*?\/?>/gi;while((match=linkRegex.exec(headHtml))!==null){const tag=match[0];if(/rel\s*=\s*["'][^"']*icon[^"']*["']/i.test(tag))continue;elements.push(tag)}return elements.join("\n ")}/**
61
61
  * 生成自解压 HTML 模板(极简压缩版本,无需 module)
62
62
  * @param {string} compressedBase64 - 压缩后的 Base64 字符串
63
63
  * @param {string} fflateLibCode - fflate 库代码(已压缩)
@@ -152,6 +152,7 @@ const htmlTemplate=`
152
152
  <head>
153
153
  <meta charset="UTF-8">
154
154
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
155
+ <link rel="icon" href="data:,">
155
156
  ${headContent}
156
157
  <style>
157
158
  body {
@@ -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;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/devkit",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
5
5
  "main": "core/index.js",
6
6
  "module": "core/index.js",