@playcraft/devkit 1.0.27 → 1.0.29

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 (55) hide show
  1. package/cli/bin/playable-scripts.js +6 -44
  2. package/cli/commands/build.js +41 -170
  3. package/cli/commands/builds.js +2 -13
  4. package/cli/commands/dev.js +1 -7
  5. package/cli/commands/pack.js +39 -308
  6. package/cli/commands/vite-dev.js +1 -7
  7. package/core/batch-build.js +185 -1121
  8. package/core/constants.js +2 -16
  9. package/core/cos-uploader.js +24 -286
  10. package/core/index.d.ts +12 -5
  11. package/core/index.js +7 -76
  12. package/core/loaders/gltf-loader.js +28 -74
  13. package/core/options.js +15 -310
  14. package/core/plugins/AdikteevInjectorPlugin.js +4 -26
  15. package/core/plugins/BigoAdsInjectorPlugin.js +4 -21
  16. package/core/plugins/DAPIInjectorPlugin.js +2 -25
  17. package/core/plugins/DebuggerInjectionPlugin.js +3 -31
  18. package/core/plugins/ExitAPIInjectorPlugin.js +4 -58
  19. package/core/plugins/FflateCompressionPlugin.js +40 -231
  20. package/core/plugins/FontInjectionWebpackPlugin.js +4 -46
  21. package/core/plugins/LiftoffInjectorPlugin.js +4 -26
  22. package/core/plugins/MRAIDInjectorPlugin.js +2 -24
  23. package/core/plugins/MintegralInjectorPlugin.js +2 -25
  24. package/core/plugins/PangleInjectorPlugin.js +2 -24
  25. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +7 -0
  26. package/core/plugins/SnapchatInjectorPlugin.js +4 -26
  27. package/core/plugins/SocialMediaInjectorPlugin.js +7 -119
  28. package/core/plugins/TikTokInjectorPlugin.js +2 -24
  29. package/core/plugins/UnityInjectorPlugin.js +8 -37
  30. package/core/plugins/ZipPlugin.js +20 -138
  31. package/core/plugins/socialMediaVitePlugin.js +2 -108
  32. package/core/utils/buildDefines.js +3 -29
  33. package/core/utils/buildTemplateString.js +5 -40
  34. package/core/utils/date.js +1 -16
  35. package/core/utils/fontRegistry.js +13 -81
  36. package/core/utils/fontResolver.js +37 -335
  37. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +7 -42
  38. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +3 -30
  39. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +7 -43
  40. package/core/utils/getPlaycraftOrientationSuffix.js +6 -0
  41. package/core/utils/injectSDKPlugins.js +14 -69
  42. package/core/utils/logOptions.js +5 -37
  43. package/core/utils/mergeOptions.js +2 -19
  44. package/core/utils/parseArgvOptions.js +5 -111
  45. package/core/utils/resolveChannelFold2Zip.js +5 -17
  46. package/core/utils/validateThemeData.js +26 -227
  47. package/core/validators/pre-build-checker.js +26 -286
  48. package/core/validators/tracking-validator.js +40 -358
  49. package/core/vite.dev.js +17 -322
  50. package/core/webpack.build.js +65 -489
  51. package/core/webpack.common.js +11 -177
  52. package/core/webpack.dev.js +6 -82
  53. package/defines.d.ts +2 -1
  54. package/package.json +3 -4
  55. package/README.md +0 -262
@@ -1,6 +1,4 @@
1
- 'use strict';
2
-
3
- /**
1
+ "use strict";/**
4
2
  * Webpack 插件:social_media 渠道专用 — 在构建产物 HTML 的 </head> 之前
5
3
  * 注入 OG / Twitter / Facebook 分享卡片 meta 标签。
6
4
  *
@@ -16,124 +14,14 @@
16
14
  * 3. 都读不到 → 跳过注入(不报错,让用户后续补)
17
15
  *
18
16
  * 配合 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
- /**
17
+ */const HtmlWebpackPlugin=require("html-webpack-plugin");const fs=require("fs");const path=require("path");const JSON5=require("json5");class SocialMediaInjectorPlugin{/**
28
18
  * @param {object} [opts]
29
19
  * @param {string} [opts.projectRoot] - 项目根目录
30
20
  * @param {string} [opts.themeName] - 当前主题目录名(不含前缀斜线)
31
21
  * @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
32
22
  * @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 };
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};
@@ -1,26 +1,4 @@
1
- const HtmlWebpackPlugin = require('html-webpack-plugin');
2
-
3
- /**
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
4
2
  * Webpack plugin that injects TikTok Playable SDK script into the HTML head.
5
3
  * @implements {import('webpack').WebpackPluginInstance}
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;
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,6 +1,4 @@
1
- const HtmlWebpackPlugin = require('html-webpack-plugin');
2
-
3
- /**
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
4
2
  * Unity Ads SDK 注入插件
5
3
  * 根据 Unity Ads Playable 标准,注入 MRAID SDK 引用
6
4
  *
@@ -13,37 +11,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
13
11
  * 4. 必须调用 mraid.close() 关闭广告
14
12
  * 5. 使用 mraid.fireTrackingEvent() 上报游戏事件
15
13
  * 6. 不允许使用 window.top(Unity 平台限制)
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;
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;
@@ -1,138 +1,20 @@
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;
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");const{MAX_PLAYABLE_PACKAGE_BYTES}=require("../constants.js");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();// 使用 posix 路径,保证 zip 内是「文件在根目录」而不是再套一层文件夹
8
+ const entryName=path.posix.join(pathPrefix.replace(/\\/g,"/"),pathMapper(nameAndPath).replace(/\\/g,"/"));zipFile.addBuffer(Buffer.isBuffer(source)?source:Buffer.from?Buffer.from(source):new Buffer(source),entryName,options.fileOptions)}zipFile.end(options.zipOptions);// accumulate each buffer containing a part of the zip file
9
+ 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
10
+ const outputPath=options.path||compilation.options.output.path;// default to webpack root filename if no filename provided, else the basename of the output path
11
+ 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
12
+ const outputPathAndFilename=path.resolve(compilation.options.output.path,// ...supporting both absolute and relative paths
13
+ outputPath,path.basename(outputFilename,".zip")+extension// ...and filenames with and without a .zip extension
14
+ );// resolve a relative output path with respect to webpack's root output path
15
+ // since only relative paths are permitted for keys in `compilation.assets`
16
+ const relativeOutputPath=path.relative(compilation.options.output.path,outputPathAndFilename);// add our zip file to the assets
17
+ const zipBuffer=Buffer.concat(bufs);const zipSize=zipBuffer.length;const zipSizeMb=(zipSize/(1024*1024)).toFixed(2);const maxSizeMb=(MAX_PLAYABLE_PACKAGE_BYTES/(1024*1024)).toFixed(0);console.log(`📦 ZIP size: ${zipSizeMb}MB (limit: ${maxSizeMb}MB)`);if(zipSize>MAX_PLAYABLE_PACKAGE_BYTES){const sizeError=new Error(`ZIP 产物超过 ${maxSizeMb}MB 上限: ${zipSizeMb}MB (${path.basename(outputPathAndFilename)})`);compilation.errors.push(sizeError)}const zipFileSource=new RawSource(zipBuffer);if(isWebpack4){compilation.assets[relativeOutputPath]=zipFileSource}else{compilation.emitAsset(relativeOutputPath,zipFileSource)}// 🗑️ 保存临时文件夹路径,用于后续删除
18
+ if(options.cleanTempFolder!==false){const tempFolder=compilation.options.output.path;const zipOutputPath=options.path||compilation.options.output.path;// 确保临时文件夹存在且不是 zip 输出目录
19
+ 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)))})}// 🗑️ 在所有文件写入完成后删除临时文件夹
20
+ 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,110 +1,4 @@
1
- 'use strict';
2
-
3
- /**
1
+ "use strict";/**
4
2
  * Vite dev 模式:social_media 渠道的 OG / Twitter meta 注入。
5
3
  * 与 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 };
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,30 +1,4 @@
1
- const { options } = require('../options');
2
-
3
- /**
1
+ "use strict";const{options}=require("../options");/**
4
2
  * Build's default defines list for dev & prod builds
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
- };
3
+ * @param {import('../index').CLIOptions} [customOptions] - 合并后的当前构建选项(用于读取会随单次构建变化的字段,如 playcraftOrientationValue);未提供时回退到全局 options
4
+ */exports.buildDefines=function buildDefines(customOptions){const opts=customOptions||options;const defines={__DEV__:JSON.stringify(opts["dev"]===undefined?false:opts["dev"]),GOOGLE_PLAY_URL:JSON.stringify(opts.googlePlayUrl),APP_STORE_URL:JSON.stringify(opts.appStoreUrl),AD_NETWORK:JSON.stringify(opts["network"]),AD_PROTOCOL:JSON.stringify(opts["protocol"]),APP:JSON.stringify(opts.app),NAME:JSON.stringify(opts.name),VERSION:JSON.stringify(opts.version),BUILD_HASH:JSON.stringify((+new Date).toString(36)),LANGUAGE:JSON.stringify(opts.language),PLAYCRAFT_ORIENTATION:JSON.stringify(opts.playcraftOrientationValue||"both")};/** @type {AD_NETWORK} */let adNetworkDefine=opts["network"];if(adNetworkDefine==="pangle")adNetworkDefine="tiktok";if(adNetworkDefine==="mytarget"||adNetworkDefine==="moloco")adNetworkDefine="facebook";defines["AD_NETWORK"]=JSON.stringify(adNetworkDefine);return defines};
@@ -1,41 +1,6 @@
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
- /**
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"};/**
12
2
  * Build's result string from template. Used for outDir or filename
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
- };
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("{network}",networkName.toLowerCase());template=template.replaceAll("{hash}","[fullhash:6]");return template};
@@ -1,16 +1 @@
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
- };
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}`};