@playcraft/devkit 1.0.25 → 1.0.27

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 (52) hide show
  1. package/README.md +262 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +177 -18
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +7 -1
  6. package/cli/commands/pack.js +308 -39
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/batch-build.js +1119 -126
  9. package/core/constants.js +16 -2
  10. package/core/cos-uploader.js +286 -23
  11. package/core/index.js +76 -7
  12. package/core/loaders/gltf-loader.js +74 -28
  13. package/core/options.js +310 -15
  14. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  15. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  16. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  17. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  18. package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
  19. package/core/plugins/FflateCompressionPlugin.js +232 -40
  20. package/core/plugins/FontInjectionWebpackPlugin.js +46 -4
  21. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  22. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  23. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  24. package/core/plugins/PangleInjectorPlugin.js +24 -2
  25. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  26. package/core/plugins/SocialMediaInjectorPlugin.js +139 -0
  27. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  28. package/core/plugins/UnityInjectorPlugin.js +37 -8
  29. package/core/plugins/ZipPlugin.js +138 -19
  30. package/core/plugins/socialMediaVitePlugin.js +110 -0
  31. package/core/utils/buildDefines.js +29 -2
  32. package/core/utils/buildTemplateString.js +40 -5
  33. package/core/utils/date.js +16 -1
  34. package/core/utils/fontRegistry.js +81 -13
  35. package/core/utils/fontResolver.js +335 -37
  36. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  37. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  38. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  39. package/core/utils/injectSDKPlugins.js +72 -11
  40. package/core/utils/logOptions.js +37 -4
  41. package/core/utils/mergeOptions.js +19 -2
  42. package/core/utils/parseArgvOptions.js +111 -5
  43. package/core/utils/resolveChannelFold2Zip.js +15 -3
  44. package/core/utils/validateThemeData.js +227 -26
  45. package/core/validators/index.d.ts +4 -0
  46. package/core/validators/pre-build-checker.js +297 -21
  47. package/core/validators/tracking-validator.js +358 -40
  48. package/core/vite.dev.js +322 -17
  49. package/core/webpack.build.js +489 -56
  50. package/core/webpack.common.js +177 -11
  51. package/core/webpack.dev.js +82 -5
  52. package/package.json +3 -2
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webpack 插件:social_media 渠道专用 — 在构建产物 HTML 的 </head> 之前
5
+ * 注入 OG / Twitter / Facebook 分享卡片 meta 标签。
6
+ *
7
+ * 设计动机:
8
+ * Facebook sharer.php 自 2014 起废弃了 t/title 等参数,
9
+ * 完全靠爬取目标页面 <head> 的 og:* meta 标签生成预览卡。
10
+ * 我们的 index.html 模板只含基础 meta,必须在构建时把
11
+ * gamePlay.shareMeta 中的 title/description/imageUrl/pageUrl 注入。
12
+ *
13
+ * 数据源(按优先级):
14
+ * 1. opts.mergedData.gamePlay.shareMeta (显式传入,批量构建子进程用)
15
+ * 2. 从 <projectRoot>/<themeName>/theme.data.json5 读取
16
+ * 3. 都读不到 → 跳过注入(不报错,让用户后续补)
17
+ *
18
+ * 配合 injectSDKPlugins.js 的 social_media 分支在 build 时挂载。
19
+ */
20
+
21
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const JSON5 = require('json5');
25
+
26
+ class SocialMediaInjectorPlugin {
27
+ /**
28
+ * @param {object} [opts]
29
+ * @param {string} [opts.projectRoot] - 项目根目录
30
+ * @param {string} [opts.themeName] - 当前主题目录名(不含前缀斜线)
31
+ * @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
32
+ * @param {object} [opts.mergedData] - 预合并数据(可选)
33
+ */
34
+ constructor(opts = {}) {
35
+ this.opts = opts;
36
+ }
37
+
38
+ /** 从 mergedData / theme.data.json5 读出 shareMeta 块 */
39
+ _loadShareMeta() {
40
+ // 1) 优先用已传入的 mergedData
41
+ let merged = this.opts.mergedData;
42
+ if (merged && merged.gamePlay && merged.gamePlay.shareMeta) {
43
+ return merged.gamePlay.shareMeta;
44
+ }
45
+
46
+ // 2) 退化为读 theme.data.json5
47
+ const root = this.opts.projectRoot || process.cwd();
48
+ const name = this.opts.themeName;
49
+ if (!name) return null;
50
+ const dataPath = path.join(
51
+ root,
52
+ (this.opts.themeConfig && this.opts.themeConfig.sourceDir) || 'src/theme',
53
+ name,
54
+ 'theme.data.json5'
55
+ );
56
+ if (!fs.existsSync(dataPath)) return null;
57
+ try {
58
+ const raw = fs.readFileSync(dataPath, 'utf8');
59
+ const parsed = JSON5.parse(raw);
60
+ return parsed && parsed.gamePlay && parsed.gamePlay.shareMeta
61
+ ? parsed.gamePlay.shareMeta
62
+ : null;
63
+ } catch (e) {
64
+ console.warn('[SocialMediaInjectorPlugin] Failed to read theme.data.json5:', e.message);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /** 把 shareMeta 渲染成 <meta> 标签字符串 */
70
+ _buildMetaTags(meta) {
71
+ if (!meta) return '';
72
+ const esc = (s) => String(s)
73
+ .replace(/&/g, '&amp;')
74
+ .replace(/"/g, '&quot;')
75
+ .replace(/</g, '&lt;')
76
+ .replace(/>/g, '&gt;');
77
+
78
+ const lines = [];
79
+ const title = (meta.title || '').trim();
80
+ const description = (meta.description || '').trim();
81
+ const image = (meta.imageUrl || '').trim();
82
+ const url = (meta.pageUrl || '').trim();
83
+ const siteName = (meta.siteName || '').trim();
84
+
85
+ // Open Graph
86
+ if (title) {
87
+ lines.push(`<meta property="og:title" content="${esc(title)}" />`);
88
+ }
89
+ if (description) {
90
+ lines.push(`<meta property="og:description" content="${esc(description)}" />`);
91
+ }
92
+ if (image) {
93
+ lines.push(`<meta property="og:image" content="${esc(image)}" />`);
94
+ }
95
+ if (url) {
96
+ lines.push(`<meta property="og:url" content="${esc(url)}" />`);
97
+ }
98
+ if (siteName) {
99
+ lines.push(`<meta property="og:site_name" content="${esc(siteName)}" />`);
100
+ }
101
+ lines.push(`<meta property="og:type" content="website" />`);
102
+
103
+ // Twitter Card
104
+ lines.push(`<meta name="twitter:card" content="summary_large_image" />`);
105
+ if (title) {
106
+ lines.push(`<meta name="twitter:title" content="${esc(title)}" />`);
107
+ }
108
+ if (description) {
109
+ lines.push(`<meta name="twitter:description" content="${esc(description)}" />`);
110
+ }
111
+ if (image) {
112
+ lines.push(`<meta name="twitter:image" content="${esc(image)}" />`);
113
+ }
114
+
115
+ return lines.join('\n ');
116
+ }
117
+
118
+ apply(compiler) {
119
+ const pluginName = 'SocialMediaInjectorPlugin';
120
+ compiler.hooks.compilation.tap(pluginName, (compilation) => {
121
+ HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
122
+ pluginName,
123
+ (data, cb) => {
124
+ const shareMeta = this._loadShareMeta();
125
+ if (!shareMeta) {
126
+ return cb(null, data);
127
+ }
128
+ const tags = this._buildMetaTags(shareMeta);
129
+ if (tags) {
130
+ data.html = data.html.replace('</head>', ` ${tags}\n </head>`);
131
+ }
132
+ cb(null, data);
133
+ }
134
+ );
135
+ });
136
+ }
137
+ }
138
+
139
+ module.exports = { SocialMediaInjectorPlugin };
@@ -1,4 +1,26 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ /**
2
4
  * Webpack plugin that injects TikTok Playable SDK script into the HTML head.
3
5
  * @implements {import('webpack').WebpackPluginInstance}
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;
6
+ */
7
+ class TikTokInjectorPlugin {
8
+ apply(compiler) {
9
+ compiler.hooks.compilation.tap('TikTokInjectorPlugin', (compilation) => {
10
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('TikTokInjectorPlugin', (data, callback) => {
11
+ data.headTags.splice(0, 0, {
12
+ tagName: 'script',
13
+ voidTag: false,
14
+ meta: { plugin: 'html-inline-script-webpack-plugin' },
15
+ attributes: {
16
+ src: 'https://sf16-muse-va.ibytedtos.com/obj/union-fe-nc-i18n/playable/sdk/playable-sdk.js',
17
+ type: 'text/javascript'
18
+ }
19
+ });
20
+ callback(null, data);
21
+ });
22
+ });
23
+ }
24
+ }
25
+
26
+ exports.TikTokInjectorPlugin = TikTokInjectorPlugin;
@@ -1,4 +1,6 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ /**
2
4
  * Unity Ads SDK 注入插件
3
5
  * 根据 Unity Ads Playable 标准,注入 MRAID SDK 引用
4
6
  *
@@ -11,10 +13,37 @@
11
13
  * 4. 必须调用 mraid.close() 关闭广告
12
14
  * 5. 使用 mraid.fireTrackingEvent() 上报游戏事件
13
15
  * 6. 不允许使用 window.top(Unity 平台限制)
14
- */class UnityInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("UnityInjectorPlugin",compilation=>{// 1. 注入 mraid.js 标签
15
- HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("UnityInjectorPlugin",(data,callback)=>{// Unity Ads 使用标准 MRAID 协议
16
- // 在生产环境中,mraid.js 由 Unity Ads 平台提供
17
- data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"unity-injector-plugin"},attributes:{src:"mraid.js",type:"text/javascript"}});callback(null,data)});// 2. 替换 window.top 为 window(Unity 不允许使用 window.top)
18
- HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync("UnityInjectorPlugin",(data,callback)=>{// 替换所有 window.top 为 window
19
- // 这是 Phaser 框架的代码,Unity 平台不允许访问 window.top
20
- data.html=data.html.replace(/window\.top\b/g,"window");callback(null,data)})})}}exports.UnityInjectorPlugin=UnityInjectorPlugin;
16
+ */
17
+ class UnityInjectorPlugin {
18
+ apply(compiler) {
19
+ compiler.hooks.compilation.tap('UnityInjectorPlugin', (compilation) => {
20
+ // 1. 注入 mraid.js 标签
21
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('UnityInjectorPlugin', (data, callback) => {
22
+ // Unity Ads 使用标准 MRAID 协议
23
+ // 在生产环境中,mraid.js 由 Unity Ads 平台提供
24
+ data.headTags.splice(0, 0, {
25
+ tagName: 'script',
26
+ voidTag: false,
27
+ meta: { plugin: 'unity-injector-plugin' },
28
+ attributes: {
29
+ src: 'mraid.js',
30
+ type: 'text/javascript'
31
+ }
32
+ });
33
+
34
+ callback(null, data);
35
+ });
36
+
37
+ // 2. 替换 window.top 为 window(Unity 不允许使用 window.top)
38
+ HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('UnityInjectorPlugin', (data, callback) => {
39
+ // 替换所有 window.top 为 window
40
+ // 这是 Phaser 框架的代码,Unity 平台不允许访问 window.top
41
+ data.html = data.html.replace(/window\.top\b/g, 'window');
42
+
43
+ callback(null, data);
44
+ });
45
+ });
46
+ }
47
+ }
48
+
49
+ exports.UnityInjectorPlugin = UnityInjectorPlugin;
@@ -1,19 +1,138 @@
1
- "use strict";const path=require("path");const fs=require("fs");const ModuleFilenameHelpers=require("webpack/lib/ModuleFilenameHelpers");const webpack=require("webpack");const webpackSources=webpack.sources||require("webpack-sources");const yazl=require("yazl");function ZipPlugin(options){this.options=options||{}}ZipPlugin.prototype.apply=function(compiler){const options=this.options;const isWebpack4=webpack.version.startsWith("4.");// Prefer `compiler.webpack` instead of webpack or webpack-sources import (rspack compatibility hack)
2
- const{RawSource}=compiler.webpack&&compiler.webpack.sources||webpackSources;if(options.pathPrefix&&path.isAbsolute(options.pathPrefix)){throw new Error("\"pathPrefix\" must be a relative path")}// 保存临时文件夹路径,用于后续删除
3
- let tempFolderToClean=null;const process=function(compilation,callback){// assets from child compilers will be included in the parent
4
- // so we should not run in child compilers
5
- if(compilation.compiler.isChild()){callback();return}const zipFile=new yazl.ZipFile;const pathPrefix=options.pathPrefix||"";const pathMapper=options.pathMapper||function(x){return x};// populate the zip file with each asset
6
- for(const nameAndPath of Object.keys(compilation.assets)){// match against include and exclude, which may be strings, regexes, arrays of the previous or omitted
7
- if(!ModuleFilenameHelpers.matchObject({include:options.include,exclude:options.exclude},nameAndPath))continue;const source=compilation.assets[nameAndPath].source();zipFile.addBuffer(Buffer.isBuffer(source)?source:Buffer.from?Buffer.from(source):new Buffer(source),path.join(pathPrefix,pathMapper(nameAndPath)),options.fileOptions)}zipFile.end(options.zipOptions);// accumulate each buffer containing a part of the zip file
8
- const bufs=[];zipFile.outputStream.on("data",function(buf){bufs.push(buf)});zipFile.outputStream.on("end",function(){// default to webpack's root output path if no path provided
9
- const outputPath=options.path||compilation.options.output.path;// default to webpack root filename if no filename provided, else the basename of the output path
10
- let outputFilename=options.filename||compilation.options.output.filename||path.basename(outputPath);const fullHash=compilation.fullHash||compilation.hash;outputFilename=outputFilename.replace("[fullhash:6]",fullHash.slice(0,6));const extension="."+(options.extension||"zip");// combine the output path and filename
11
- const outputPathAndFilename=path.resolve(compilation.options.output.path,// ...supporting both absolute and relative paths
12
- outputPath,path.basename(outputFilename,".zip")+extension// ...and filenames with and without a .zip extension
13
- );// resolve a relative output path with respect to webpack's root output path
14
- // since only relative paths are permitted for keys in `compilation.assets`
15
- const relativeOutputPath=path.relative(compilation.options.output.path,outputPathAndFilename);// add our zip file to the assets
16
- const zipFileSource=new RawSource(Buffer.concat(bufs));if(isWebpack4){compilation.assets[relativeOutputPath]=zipFileSource}else{compilation.emitAsset(relativeOutputPath,zipFileSource)}// 🗑️ 保存临时文件夹路径,用于后续删除
17
- if(options.cleanTempFolder!==false){const tempFolder=compilation.options.output.path;const zipOutputPath=options.path||compilation.options.output.path;// 确保临时文件夹存在且不是 zip 输出目录
18
- if(tempFolder&&tempFolder!==zipOutputPath){tempFolderToClean=tempFolder}}callback()})};if(isWebpack4){compiler.hooks.emit.tapAsync(ZipPlugin.name,process)}else{compiler.hooks.thisCompilation.tap(ZipPlugin.name,compilation=>{compilation.hooks.processAssets.tapPromise({name:ZipPlugin.name,stage:webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER},()=>new Promise(resolve=>process(compilation,resolve)))})}// 🗑️ 在所有文件写入完成后删除临时文件夹
19
- compiler.hooks.done.tap(ZipPlugin.name,()=>{if(tempFolderToClean&&fs.existsSync(tempFolderToClean)){try{fs.rmSync(tempFolderToClean,{recursive:true,force:true});console.log(`✅ 已删除临时文件夹: ${tempFolderToClean}`)}catch(err){console.warn(`⚠️ 删除临时文件夹失败: ${tempFolderToClean}`,err.message)}}})};exports.ZipPlugin=ZipPlugin;
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers');
6
+ const webpack = require('webpack');
7
+ const webpackSources = webpack.sources || require('webpack-sources');
8
+ const yazl = require('yazl');
9
+
10
+ function ZipPlugin(options) {
11
+ this.options = options || {};
12
+ }
13
+
14
+ ZipPlugin.prototype.apply = function (compiler) {
15
+ const options = this.options;
16
+ const isWebpack4 = webpack.version.startsWith('4.');
17
+ // Prefer `compiler.webpack` instead of webpack or webpack-sources import (rspack compatibility hack)
18
+ const { RawSource } = (compiler.webpack && compiler.webpack.sources) || webpackSources;
19
+
20
+ if (options.pathPrefix && path.isAbsolute(options.pathPrefix)) {
21
+ throw new Error('"pathPrefix" must be a relative path');
22
+ }
23
+
24
+ // 保存临时文件夹路径,用于后续删除
25
+ let tempFolderToClean = null;
26
+
27
+ const process = function (compilation, callback) {
28
+ // assets from child compilers will be included in the parent
29
+ // so we should not run in child compilers
30
+ if (compilation.compiler.isChild()) {
31
+ callback();
32
+ return;
33
+ }
34
+
35
+ const zipFile = new yazl.ZipFile();
36
+
37
+ const pathPrefix = options.pathPrefix || '';
38
+ const pathMapper =
39
+ options.pathMapper ||
40
+ function (x) {
41
+ return x;
42
+ };
43
+
44
+ // populate the zip file with each asset
45
+ for (const nameAndPath of Object.keys(compilation.assets)) {
46
+ // match against include and exclude, which may be strings, regexes, arrays of the previous or omitted
47
+ if (!ModuleFilenameHelpers.matchObject({ include: options.include, exclude: options.exclude }, nameAndPath)) continue;
48
+
49
+ const source = compilation.assets[nameAndPath].source();
50
+
51
+ zipFile.addBuffer(
52
+ Buffer.isBuffer(source) ? source : Buffer.from ? Buffer.from(source) : new Buffer(source),
53
+ path.join(pathPrefix, pathMapper(nameAndPath)),
54
+ options.fileOptions
55
+ );
56
+ }
57
+
58
+ zipFile.end(options.zipOptions);
59
+
60
+ // accumulate each buffer containing a part of the zip file
61
+ const bufs = [];
62
+
63
+ zipFile.outputStream.on('data', function (buf) {
64
+ bufs.push(buf);
65
+ });
66
+
67
+ zipFile.outputStream.on('end', function () {
68
+ // default to webpack's root output path if no path provided
69
+ const outputPath = options.path || compilation.options.output.path;
70
+ // default to webpack root filename if no filename provided, else the basename of the output path
71
+ let outputFilename = options.filename || compilation.options.output.filename || path.basename(outputPath);
72
+
73
+ const fullHash = compilation.fullHash || compilation.hash;
74
+ outputFilename = outputFilename.replace('[fullhash:6]', fullHash.slice(0, 6));
75
+
76
+ const extension = '.' + (options.extension || 'zip');
77
+
78
+ // combine the output path and filename
79
+ const outputPathAndFilename = path.resolve(
80
+ compilation.options.output.path, // ...supporting both absolute and relative paths
81
+ outputPath,
82
+ path.basename(outputFilename, '.zip') + extension // ...and filenames with and without a .zip extension
83
+ );
84
+
85
+ // resolve a relative output path with respect to webpack's root output path
86
+ // since only relative paths are permitted for keys in `compilation.assets`
87
+ const relativeOutputPath = path.relative(compilation.options.output.path, outputPathAndFilename);
88
+
89
+ // add our zip file to the assets
90
+ const zipFileSource = new RawSource(Buffer.concat(bufs));
91
+ if (isWebpack4) {
92
+ compilation.assets[relativeOutputPath] = zipFileSource;
93
+ } else {
94
+ compilation.emitAsset(relativeOutputPath, zipFileSource);
95
+ }
96
+
97
+ // 🗑️ 保存临时文件夹路径,用于后续删除
98
+ if (options.cleanTempFolder !== false) {
99
+ const tempFolder = compilation.options.output.path;
100
+ const zipOutputPath = options.path || compilation.options.output.path;
101
+ // 确保临时文件夹存在且不是 zip 输出目录
102
+ if (tempFolder && tempFolder !== zipOutputPath) {
103
+ tempFolderToClean = tempFolder;
104
+ }
105
+ }
106
+
107
+ callback();
108
+ });
109
+ };
110
+
111
+ if (isWebpack4) {
112
+ compiler.hooks.emit.tapAsync(ZipPlugin.name, process);
113
+ } else {
114
+ compiler.hooks.thisCompilation.tap(ZipPlugin.name, (compilation) => {
115
+ compilation.hooks.processAssets.tapPromise(
116
+ {
117
+ name: ZipPlugin.name,
118
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER
119
+ },
120
+ () => new Promise((resolve) => process(compilation, resolve))
121
+ );
122
+ });
123
+ }
124
+
125
+ // 🗑️ 在所有文件写入完成后删除临时文件夹
126
+ compiler.hooks.done.tap(ZipPlugin.name, () => {
127
+ if (tempFolderToClean && fs.existsSync(tempFolderToClean)) {
128
+ try {
129
+ fs.rmSync(tempFolderToClean, { recursive: true, force: true });
130
+ console.log(`✅ 已删除临时文件夹: ${tempFolderToClean}`);
131
+ } catch (err) {
132
+ console.warn(`⚠️ 删除临时文件夹失败: ${tempFolderToClean}`, err.message);
133
+ }
134
+ }
135
+ });
136
+ };
137
+
138
+ exports.ZipPlugin = ZipPlugin;
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Vite dev 模式:social_media 渠道的 OG / Twitter meta 注入。
5
+ * 与 SocialMediaInjectorPlugin.js 输出完全一致,仅 hook 形态不同。
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const JSON5 = require('json5');
11
+
12
+ /** 把 shareMeta 渲染成 <meta> 标签字符串(与 webpack plugin 完全一致) */
13
+ function buildMetaTags(meta) {
14
+ if (!meta) return [];
15
+ const esc = (s) => String(s)
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;');
20
+
21
+ const tags = [];
22
+ const { title, description, imageUrl, pageUrl, siteName } = meta;
23
+ const t = (title || '').trim();
24
+ const d = (description || '').trim();
25
+ const i = (imageUrl || '').trim();
26
+ const u = (pageUrl || '').trim();
27
+ const s = (siteName || '').trim();
28
+
29
+ if (t) tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:title', content: t } });
30
+ if (d) tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:description', content: d } });
31
+ if (i) tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:image', content: i } });
32
+ if (u) tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:url', content: u } });
33
+ if (s) tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:site_name', content: s } });
34
+ tags.push({ tag: 'meta', injectTo: 'head', attrs: { property: 'og:type', content: 'website' } });
35
+
36
+ tags.push({ tag: 'meta', injectTo: 'head', attrs: { name: 'twitter:card', content: 'summary_large_image' } });
37
+ if (t) tags.push({ tag: 'meta', injectTo: 'head', attrs: { name: 'twitter:title', content: t } });
38
+ if (d) tags.push({ tag: 'meta', injectTo: 'head', attrs: { name: 'twitter:description', content: d } });
39
+ if (i) tags.push({ tag: 'meta', injectTo: 'head', attrs: { name: 'twitter:image', content: i } });
40
+ return tags;
41
+ }
42
+
43
+ /** 检测主题名(与 fontFamilyPlugin 逻辑保持一致) */
44
+ function detectTheme() {
45
+ const root = process.cwd();
46
+ const buildsConfigPath = path.join(root, 'builds.config.js');
47
+ let config = { sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' };
48
+ if (fs.existsSync(buildsConfigPath)) {
49
+ try {
50
+ delete require.cache[require.resolve(buildsConfigPath)];
51
+ const bc = require(buildsConfigPath);
52
+ if (bc.themes) config = { ...config, ...bc.themes };
53
+ } catch (_) { /* ignore */ }
54
+ }
55
+ let name = null;
56
+ const entryPath = path.join(root, config.entryFile);
57
+ if (fs.existsSync(entryPath)) {
58
+ const content = fs.readFileSync(entryPath, 'utf8');
59
+ const m = content.match(/from\s+['"]\.\/([^'"]+)['"]/);
60
+ if (m) name = m[1];
61
+ }
62
+ return { themeName: name, themeConfig: config };
63
+ }
64
+
65
+ /** 读取 shareMeta,缓存以避免 transformIndexHtml 多次触发时反复读盘 */
66
+ let _cache = { key: null, value: null };
67
+
68
+ function loadShareMeta() {
69
+ const { themeName, themeConfig } = detectTheme();
70
+ if (!themeName) return null;
71
+ const root = process.cwd();
72
+ const dataPath = path.join(
73
+ root,
74
+ themeConfig.sourceDir || 'src/theme',
75
+ themeName,
76
+ 'theme.data.json5'
77
+ );
78
+ if (!fs.existsSync(dataPath)) return null;
79
+ const stat = fs.statSync(dataPath);
80
+ const key = `${dataPath}::${stat.mtimeMs}`;
81
+ if (_cache.key === key) return _cache.value;
82
+ try {
83
+ const raw = fs.readFileSync(dataPath, 'utf8');
84
+ const parsed = JSON5.parse(raw);
85
+ const value = parsed && parsed.gamePlay && parsed.gamePlay.shareMeta
86
+ ? parsed.gamePlay.shareMeta
87
+ : null;
88
+ _cache = { key, value };
89
+ return value;
90
+ } catch (e) {
91
+ console.warn('[socialMediaVitePlugin] Failed to read theme.data.json5:', e.message);
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function socialMediaVitePlugin() {
97
+ return {
98
+ name: 'vite-plugin-social-media',
99
+ apply: 'serve',
100
+ transformIndexHtml: {
101
+ order: 'pre',
102
+ handler() {
103
+ const meta = loadShareMeta();
104
+ return buildMetaTags(meta);
105
+ },
106
+ },
107
+ };
108
+ }
109
+
110
+ module.exports = { socialMediaVitePlugin, buildMetaTags };
@@ -1,3 +1,30 @@
1
- "use strict";const{options}=require("../options");/**
1
+ const { options } = require('../options');
2
+
3
+ /**
2
4
  * Build's default defines list for dev & prod builds
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};
5
+ */
6
+ exports.buildDefines = function buildDefines() {
7
+ const defines = {
8
+ __DEV__: JSON.stringify(options['dev'] === undefined ? false : options['dev']),
9
+ GOOGLE_PLAY_URL: JSON.stringify(options.googlePlayUrl),
10
+ APP_STORE_URL: JSON.stringify(options.appStoreUrl),
11
+ AD_NETWORK: JSON.stringify(options['network']),
12
+ AD_PROTOCOL: JSON.stringify(options['protocol']),
13
+ APP: JSON.stringify(options.app),
14
+ NAME: JSON.stringify(options.name),
15
+ VERSION: JSON.stringify(options.version),
16
+ BUILD_HASH: JSON.stringify((+new Date()).toString(36)),
17
+ LANGUAGE: JSON.stringify(options.language),
18
+ ORIENTATION: JSON.stringify(options.orientation)
19
+ };
20
+
21
+ /** @type {AD_NETWORK} */
22
+ let adNetworkDefine = options['network'];
23
+
24
+ if (adNetworkDefine === 'pangle') adNetworkDefine = 'tiktok';
25
+ if (adNetworkDefine === 'mytarget' || adNetworkDefine === 'moloco') adNetworkDefine = 'facebook';
26
+
27
+ defines['AD_NETWORK'] = JSON.stringify(adNetworkDefine);
28
+
29
+ return defines;
30
+ };
@@ -1,6 +1,41 @@
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"};/**
1
+ const { options } = require('../options');
2
+ const { getCurrentDateFormatted } = require('./date');
3
+ const path = require('path');
4
+
5
+ /** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */
6
+ const adNetworkFileNameMap = {
7
+ preview: 'Preview',
8
+ bigoads: 'bigo'
9
+ };
10
+
11
+ /**
2
12
  * Build's result string from template. Used for outDir or filename
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
- let networkName=customOptions.adNetworkNames[adNetwork]||adNetworkFileNameMap[adNetwork]||adNetwork;// if (adProtocol === 'dapi') networkName += '_DAPI';
5
- // 如果是绝对路径,直接返回,不做模板替换
6
- if(path.isAbsolute(template)){return template}template=template.replaceAll("{app}",customOptions.app);template=template.replaceAll("{name}",customOptions.name);template=template.replaceAll("{version}",customOptions.version);template=template.replaceAll("{date}",getCurrentDateFormatted());template=template.replaceAll("{language}",customOptions.language);template=template.replaceAll("{orientation}",customOptions.orientation);template=template.replaceAll("{network}",networkName.toLowerCase());template=template.replaceAll("{hash}","[fullhash:6]");return template};
13
+ */
14
+ exports.buildTemplateString = function buildTemplateString(template, customOptions) {
15
+ customOptions = customOptions || options;
16
+
17
+ /** @type {AD_NETWORK} */
18
+ const adNetwork = customOptions['network'];
19
+
20
+ /** @type {AD_PROTOCOL} */
21
+ // const adProtocol = customOptions['protocol'];
22
+
23
+ let networkName = customOptions.adNetworkNames[adNetwork] || adNetworkFileNameMap[adNetwork] || adNetwork;
24
+ // if (adProtocol === 'dapi') networkName += '_DAPI';
25
+
26
+ // 如果是绝对路径,直接返回,不做模板替换
27
+ if (path.isAbsolute(template)) {
28
+ return template;
29
+ }
30
+
31
+ template = template.replaceAll('{app}', customOptions.app);
32
+ template = template.replaceAll('{name}', customOptions.name);
33
+ template = template.replaceAll('{version}', customOptions.version);
34
+ template = template.replaceAll('{date}', getCurrentDateFormatted());
35
+ template = template.replaceAll('{language}', customOptions.language);
36
+ template = template.replaceAll('{orientation}', customOptions.orientation);
37
+ template = template.replaceAll('{network}', networkName.toLowerCase());
38
+ template = template.replaceAll('{hash}', '[fullhash:6]');
39
+
40
+ return template;
41
+ };
@@ -1 +1,16 @@
1
- "use strict";exports.getCurrentDateFormatted=function getCurrentDateFormatted(includeTime){const date=new Date;const year=date.getFullYear();const month=(date.getMonth()+1).toString().padStart(2,"0");const day=date.getDate().toString().padStart(2,"0");if(includeTime){const hour=date.getHours().toString().padStart(2,"0");const minute=date.getMinutes().toString().padStart(2,"0");const second=date.getSeconds().toString().padStart(2,"0");return`${year}${month}${day}-${hour}${minute}${second}`}return`${year}${month}${day}`};
1
+ exports.getCurrentDateFormatted = function getCurrentDateFormatted(includeTime) {
2
+ const date = new Date();
3
+ const year = date.getFullYear();
4
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
5
+ const day = date.getDate().toString().padStart(2, '0');
6
+
7
+ if (includeTime) {
8
+ const hour = date.getHours().toString().padStart(2, '0');
9
+ const minute = date.getMinutes().toString().padStart(2, '0');
10
+ const second = date.getSeconds().toString().padStart(2, '0');
11
+
12
+ return `${year}${month}${day}-${hour}${minute}${second}`;
13
+ }
14
+
15
+ return `${year}${month}${day}`;
16
+ };