@playcraft/devkit 1.0.16 → 1.0.18

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 (57) hide show
  1. package/README.md +259 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +130 -17
  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 +1091 -124
  9. package/core/cos-uploader.js +264 -20
  10. package/core/index.js +71 -4
  11. package/core/loaders/gltf-loader.js +74 -28
  12. package/core/options.js +310 -15
  13. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  14. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  15. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  16. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  17. package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
  18. package/core/plugins/FflateCompressionPlugin.js +225 -37
  19. package/core/plugins/FontInjectionWebpackPlugin.js +59 -0
  20. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  21. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  22. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  23. package/core/plugins/PangleInjectorPlugin.js +24 -2
  24. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  25. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  26. package/core/plugins/UnityInjectorPlugin.js +37 -8
  27. package/core/plugins/ZipPlugin.js +138 -19
  28. package/core/resources/fonts/README.md +31 -0
  29. package/core/resources/fonts/maoken-regular.woff2 +0 -0
  30. package/core/resources/fonts/maoken-regular.woff2.chars +1 -0
  31. package/core/resources/fonts/maoken-zhuyuan.ttf +0 -0
  32. package/core/resources/fonts/noto-sans-sc-regular.woff2 +0 -0
  33. package/core/resources/fonts/pangmen-biaoti.ttf +0 -0
  34. package/core/resources/fonts/pangmen-regular.woff2 +0 -0
  35. package/core/resources/fonts/pangmen-regular.woff2.chars +1 -0
  36. package/core/resources/fonts/poppins-regular.woff2 +0 -0
  37. package/core/utils/buildDefines.js +29 -2
  38. package/core/utils/buildTemplateString.js +40 -5
  39. package/core/utils/date.js +16 -1
  40. package/core/utils/fontRegistry.js +97 -0
  41. package/core/utils/fontResolver.js +398 -0
  42. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  43. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  44. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  45. package/core/utils/injectSDKPlugins.js +58 -11
  46. package/core/utils/logOptions.js +37 -4
  47. package/core/utils/mergeOptions.js +19 -2
  48. package/core/utils/parseArgvOptions.js +110 -5
  49. package/core/utils/resolveChannelFold2Zip.js +15 -3
  50. package/core/utils/validateThemeData.js +220 -25
  51. package/core/validators/pre-build-checker.js +201 -21
  52. package/core/validators/tracking-validator.js +358 -40
  53. package/core/vite.dev.js +332 -16
  54. package/core/webpack.build.js +494 -52
  55. package/core/webpack.common.js +177 -11
  56. package/core/webpack.dev.js +82 -5
  57. package/package.json +3 -2
@@ -1,4 +1,4 @@
1
- "use strict";/**
1
+ /**
2
2
  * Webpack 插件:使用 fflate 压缩 HTML 文件并生成自解压版本
3
3
  *
4
4
  * 工作原理:
@@ -21,7 +21,15 @@
21
21
  * - 仅在打正式包(生产环境)时使用
22
22
  * - 压缩等级固定为 9(最高压缩)
23
23
  * - 不保留原始文件
24
- */const fflate=require("fflate");const fs=require("fs");const path=require("path");const{minify}=require("terser");class FflateCompressionPlugin{/**
24
+ */
25
+
26
+ const fflate = require('fflate');
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const { minify } = require('terser');
30
+
31
+ class FflateCompressionPlugin {
32
+ /**
25
33
  * @param {Object} options - 插件配置选项
26
34
  * @param {boolean} [options.enabled=true] - 是否启用压缩
27
35
  * @param {number} [options.level=9] - 压缩等级(固定为 9)
@@ -29,42 +37,197 @@
29
37
  * @param {boolean} [options.logStats=true] - 是否输出压缩统计
30
38
  * @param {string} [options.googlePlayUrl] - Google Play 商店链接
31
39
  * @param {string} [options.appStoreUrl] - App Store 商店链接
32
- */constructor(options={}){this.options={enabled:options.enabled!==false,level:options.level||9,keepOriginal:options.keepOriginal||false,logStats:options.logStats!==false,googlePlayUrl:options.googlePlayUrl||"https://play.google.com/store/games",appStoreUrl:options.appStoreUrl||"https://www.apple.com/app-store/"}}apply(compiler){if(!this.options.enabled){console.log("\u26A0\uFE0F fflate compression is disabled");return}const webpack=require("webpack");// 使用 processAssets 钩子(Webpack 5),在 ZipPlugin 之前执行
33
- // 阶段顺序:OPTIMIZE_INLINE (700) SUMMARIZE (1000) → OPTIMIZE_TRANSFER (3000)
34
- // HtmlWebpackPlugin + HtmlInlineScriptPlugin 在 OPTIMIZE_INLINE 阶段完成 HTML 生成
35
- // FflateCompressionPlugin SUMMARIZE 阶段压缩 HTML
36
- // ZipPlugin OPTIMIZE_TRANSFER 阶段打包 ZIP
37
- compiler.hooks.thisCompilation.tap("FflateCompressionPlugin",compilation=>{compilation.hooks.processAssets.tapPromise({name:"FflateCompressionPlugin",stage:webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE},async()=>{await this.processAssets(compilation)})})}async processAssets(compilation){const webpack=require("webpack");const{RawSource}=compilation.compiler.webpack&&compilation.compiler.webpack.sources||webpack.sources;try{// 查找所有 HTML 文件
38
- const htmlFiles=Object.keys(compilation.assets).filter(filename=>filename.endsWith(".html"));if(htmlFiles.length===0){console.log("\u26A0\uFE0F No HTML files found to compress");return}console.log(`\n🗜️ Compressing HTML files with fflate (level ${this.options.level})...`);// 使用 for...of 替代 forEach 以支持 async/await
39
- for(const htmlFilename of htmlFiles){const asset=compilation.assets[htmlFilename];// 读取原始 HTML 内容
40
- const originalHtml=asset.source();const originalSize=Buffer.byteLength(originalHtml,"utf8");// 压缩
41
- const startTime=Date.now();const htmlUint8Array=new TextEncoder().encode(originalHtml);const compressedUint8Array=fflate.zlibSync(htmlUint8Array,{level:this.options.level});const compressTime=Date.now()-startTime;// 转换为 Base64
42
- const base64String=Buffer.from(compressedUint8Array).toString("base64");const compressedSize=compressedUint8Array.length;const base64Size=Buffer.byteLength(base64String,"utf8");const compressionRatio=((1-compressedSize/originalSize)*100).toFixed(2);// 读取 fflate 库代码并压缩(使用浏览器版本)
43
- const fflatePackageRoot=path.join(path.dirname(require.resolve("fflate")),"..");const fflateLibPath=path.join(fflatePackageRoot,"umd","index.js");let fflateLibContent=fs.readFileSync(fflateLibPath,"utf8");// 使用 Terser 压缩 fflate 库代码
44
- const minifiedFflate=await minify(fflateLibContent,{compress:{passes:2,dead_code:true,drop_console:true,drop_debugger:true,pure_funcs:["console.log","console.info"]},mangle:true,format:{comments:false}});fflateLibContent=minifiedFflate.code;// 从原始 HTML 中提取 head 内容,保留到自解压模板中
45
- const headContent=this.extractHeadContent(originalHtml);// 生成自解压 HTML
46
- const decompressedHtml=await this.generateDecompressHtml(base64String,fflateLibContent,headContent);const finalSize=Buffer.byteLength(decompressedHtml,"utf8");// 输出统计信息
47
- if(this.options.logStats){console.log(`\n📄 ${htmlFilename}`);console.log(` 原始大小: ${(originalSize/1024).toFixed(2)} KB`);console.log(` 压缩后大小: ${(compressedSize/1024).toFixed(2)} KB`);console.log(` Base64 大小: ${(base64Size/1024).toFixed(2)} KB`);console.log(` 最终大小: ${(finalSize/1024).toFixed(2)} KB (含解压代码)`);console.log(` 压缩率: ${compressionRatio}%`);console.log(` 体积减少: ${originalSize-finalSize} bytes (${((1-finalSize/originalSize)*100).toFixed(2)}%)`);console.log(` 压缩耗时: ${compressTime}ms`);if(finalSize<originalSize){const saved=originalSize-finalSize;console.log(` ✅ 节省空间: ${(saved/1024).toFixed(2)} KB (${(saved/originalSize*100).toFixed(2)}%)`)}else{console.log(` ⚠️ 压缩后反而变大了 ${finalSize-originalSize} bytes`);console.log(` 💡 建议: 对于小文件(<50KB),压缩可能不划算`)}}// 更新 webpack 资源(使用 Webpack 5 API)
48
- compilation.updateAsset(htmlFilename,new RawSource(decompressedHtml))}console.log("\n\u2705 fflate compression completed!\n")}catch(error){console.error("\n\u274C fflate compression failed:",error);throw error}}/**
40
+ */
41
+ constructor(options = {}) {
42
+ this.options = {
43
+ enabled: options.enabled !== false,
44
+ level: options.level || 9,
45
+ keepOriginal: options.keepOriginal || false,
46
+ logStats: options.logStats !== false,
47
+ googlePlayUrl: options.googlePlayUrl || 'https://play.google.com/store/games',
48
+ appStoreUrl: options.appStoreUrl || 'https://www.apple.com/app-store/'
49
+ };
50
+ }
51
+
52
+ apply(compiler) {
53
+ if (!this.options.enabled) {
54
+ console.log('⚠️ fflate compression is disabled');
55
+ return;
56
+ }
57
+
58
+ const webpack = require('webpack');
59
+
60
+ // 使用 processAssets 钩子(Webpack 5),在 ZipPlugin 之前执行
61
+ // 阶段顺序:OPTIMIZE_INLINE (700) → SUMMARIZE (1000) → OPTIMIZE_TRANSFER (3000)
62
+ // HtmlWebpackPlugin + HtmlInlineScriptPlugin 在 OPTIMIZE_INLINE 阶段完成 HTML 生成
63
+ // FflateCompressionPlugin 在 SUMMARIZE 阶段压缩 HTML
64
+ // ZipPlugin 在 OPTIMIZE_TRANSFER 阶段打包 ZIP
65
+ compiler.hooks.thisCompilation.tap('FflateCompressionPlugin', (compilation) => {
66
+ compilation.hooks.processAssets.tapPromise(
67
+ {
68
+ name: 'FflateCompressionPlugin',
69
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
70
+ },
71
+ async () => {
72
+ await this.processAssets(compilation);
73
+ }
74
+ );
75
+ });
76
+ }
77
+
78
+ async processAssets(compilation) {
79
+ const webpack = require('webpack');
80
+ const { RawSource } = (compilation.compiler.webpack && compilation.compiler.webpack.sources) || webpack.sources;
81
+
82
+ try {
83
+ // 查找所有 HTML 文件
84
+ const htmlFiles = Object.keys(compilation.assets).filter(filename =>
85
+ filename.endsWith('.html')
86
+ );
87
+
88
+ if (htmlFiles.length === 0) {
89
+ console.log('⚠️ No HTML files found to compress');
90
+ return;
91
+ }
92
+
93
+ console.log(`\n🗜️ Compressing HTML files with fflate (level ${this.options.level})...`);
94
+
95
+ // 使用 for...of 替代 forEach 以支持 async/await
96
+ for (const htmlFilename of htmlFiles) {
97
+ const asset = compilation.assets[htmlFilename];
98
+
99
+ // 读取原始 HTML 内容
100
+ const originalHtml = asset.source();
101
+ const originalSize = Buffer.byteLength(originalHtml, 'utf8');
102
+
103
+ // 压缩
104
+ const startTime = Date.now();
105
+ const htmlUint8Array = new TextEncoder().encode(originalHtml);
106
+ const compressedUint8Array = fflate.zlibSync(htmlUint8Array, { level: this.options.level });
107
+ const compressTime = Date.now() - startTime;
108
+
109
+ // 转换为 Base64
110
+ const base64String = Buffer.from(compressedUint8Array).toString('base64');
111
+ const compressedSize = compressedUint8Array.length;
112
+ const base64Size = Buffer.byteLength(base64String, 'utf8');
113
+ const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
114
+
115
+ // 读取 fflate 库代码并压缩(使用浏览器版本)
116
+ const fflatePackageRoot = path.join(path.dirname(require.resolve('fflate')), '..');
117
+ const fflateLibPath = path.join(fflatePackageRoot, 'umd', 'index.js');
118
+ let fflateLibContent = fs.readFileSync(fflateLibPath, 'utf8');
119
+
120
+ // 使用 Terser 压缩 fflate 库代码
121
+ const minifiedFflate = await minify(fflateLibContent, {
122
+ compress: {
123
+ passes: 2,
124
+ dead_code: true,
125
+ drop_console: true,
126
+ drop_debugger: true,
127
+ pure_funcs: ['console.log', 'console.info']
128
+ },
129
+ mangle: true,
130
+ format: {
131
+ comments: false
132
+ }
133
+ });
134
+ fflateLibContent = minifiedFflate.code;
135
+
136
+ // 从原始 HTML 中提取 head 内容,保留到自解压模板中
137
+ const headContent = this.extractHeadContent(originalHtml);
138
+
139
+ // 生成自解压 HTML
140
+ const decompressedHtml = await this.generateDecompressHtml(base64String, fflateLibContent, headContent);
141
+ const finalSize = Buffer.byteLength(decompressedHtml, 'utf8');
142
+
143
+ // 输出统计信息
144
+ if (this.options.logStats) {
145
+ console.log(`\n📄 ${htmlFilename}`);
146
+ console.log(` 原始大小: ${(originalSize / 1024).toFixed(2)} KB`);
147
+ console.log(` 压缩后大小: ${(compressedSize / 1024).toFixed(2)} KB`);
148
+ console.log(` Base64 大小: ${(base64Size / 1024).toFixed(2)} KB`);
149
+ console.log(` 最终大小: ${(finalSize / 1024).toFixed(2)} KB (含解压代码)`);
150
+ console.log(` 压缩率: ${compressionRatio}%`);
151
+ console.log(` 体积减少: ${(originalSize - finalSize)} bytes (${((1 - finalSize / originalSize) * 100).toFixed(2)}%)`);
152
+ console.log(` 压缩耗时: ${compressTime}ms`);
153
+
154
+ if (finalSize < originalSize) {
155
+ const saved = originalSize - finalSize;
156
+ console.log(` ✅ 节省空间: ${(saved / 1024).toFixed(2)} KB (${((saved / originalSize) * 100).toFixed(2)}%)`);
157
+ } else {
158
+ console.log(` ⚠️ 压缩后反而变大了 ${(finalSize - originalSize)} bytes`);
159
+ console.log(` 💡 建议: 对于小文件(<50KB),压缩可能不划算`);
160
+ }
161
+ }
162
+
163
+ // 更新 webpack 资源(使用 Webpack 5 API)
164
+ compilation.updateAsset(htmlFilename, new RawSource(decompressedHtml));
165
+ }
166
+
167
+ console.log('\n✅ fflate compression completed!\n');
168
+ } catch (error) {
169
+ console.error('\n❌ fflate compression failed:', error);
170
+ throw error;
171
+ }
172
+ }
173
+
174
+ /**
49
175
  * 从原始 HTML 中提取 <head> 内容
50
176
  * 保留 meta 标签、外部 script 引用、link 标签、title 等
51
177
  * 排除内联 style 和内联 script(这些会在解压后恢复)
52
178
  * @param {string} html - 原始 HTML 内容
53
179
  * @returns {string} 提取的 head 内容(HTML 字符串)
54
- */extractHeadContent(html){// 提取 <head>...</head> 之间的内容
55
- const headMatch=html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);if(!headMatch)return"";const headHtml=headMatch[1];const elements=[];// 提取所有 meta 标签(自闭合,排除 charset 和 viewport,因为模板已包含)
56
- const metaRegex=/<meta\s+[^>]*?\/?>/gi;let match;while((match=metaRegex.exec(headHtml))!==null){const tag=match[0];// 排除 charset 和 viewport(自解压模板已包含)
57
- if(/charset\s*=/i.test(tag))continue;if(/name\s*=\s*["']viewport["']/i.test(tag))continue;elements.push(tag)}// 提取 title 标签
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 ")}/**
180
+ */
181
+ extractHeadContent(html) {
182
+ // 提取 <head>...</head> 之间的内容
183
+ const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
184
+ if (!headMatch) return '';
185
+
186
+ const headHtml = headMatch[1];
187
+ const elements = [];
188
+
189
+ // 提取所有 meta 标签(自闭合,排除 charset 和 viewport,因为模板已包含)
190
+ const metaRegex = /<meta\s+[^>]*?\/?>/gi;
191
+ let match;
192
+ while ((match = metaRegex.exec(headHtml)) !== null) {
193
+ const tag = match[0];
194
+ // 排除 charset 和 viewport(自解压模板已包含)
195
+ if (/charset\s*=/i.test(tag)) continue;
196
+ if (/name\s*=\s*["']viewport["']/i.test(tag)) continue;
197
+ elements.push(tag);
198
+ }
199
+
200
+ // 提取 title 标签
201
+ const titleMatch = headHtml.match(/<title[^>]*>[\s\S]*?<\/title>/i);
202
+ if (titleMatch) {
203
+ elements.push(titleMatch[0]);
204
+ }
205
+
206
+ // 提取外部 script 标签(有 src 属性的)
207
+ const scriptRegex = /<script\s+[^>]*src\s*=\s*["'][^"']+["'][^>]*>[\s\S]*?<\/script>/gi;
208
+ while ((match = scriptRegex.exec(headHtml)) !== null) {
209
+ elements.push(match[0]);
210
+ }
211
+
212
+ // 提取 link 标签
213
+ const linkRegex = /<link\s+[^>]*?\/?>/gi;
214
+ while ((match = linkRegex.exec(headHtml)) !== null) {
215
+ elements.push(match[0]);
216
+ }
217
+
218
+ return elements.join('\n ');
219
+ }
220
+
221
+ /**
61
222
  * 生成自解压 HTML 模板(极简压缩版本,无需 module)
62
223
  * @param {string} compressedBase64 - 压缩后的 Base64 字符串
63
224
  * @param {string} fflateLibCode - fflate 库代码(已压缩)
64
225
  * @param {string} headContent - 从原始 HTML 提取的 head 内容
65
226
  * @returns {Promise<string>} 完整的自解压 HTML
66
- */async generateDecompressHtml(compressedBase64,fflateLibCode,headContent=""){// 可读的解压代码(保持格式以便维护)
67
- const decompressCodeReadable=`
227
+ */
228
+ async generateDecompressHtml(compressedBase64, fflateLibCode, headContent = '') {
229
+ // 可读的解压代码(保持格式以便维护)
230
+ const decompressCodeReadable = `
68
231
  (function() {
69
232
  var compressedData = '${compressedBase64}';
70
233
 
@@ -107,10 +270,26 @@ const decompressCodeReadable=`
107
270
  '<h2>Error</h2><p>' + error.message + '</p></div>';
108
271
  }
109
272
  })();
110
- `;// 使用 Terser 压缩解压代码
111
- const minifiedDecompressCode=await minify(decompressCodeReadable,{compress:{passes:2,dead_code:true,drop_console:false,// 保留 console.error 用于调试
112
- drop_debugger:true},mangle:true,format:{comments:false}});const decompressCode=minifiedDecompressCode.code;// 可读的 HTML 模板(保持格式以便维护)
113
- const htmlTemplate=`
273
+ `;
274
+
275
+ // 使用 Terser 压缩解压代码
276
+ const minifiedDecompressCode = await minify(decompressCodeReadable, {
277
+ compress: {
278
+ passes: 2,
279
+ dead_code: true,
280
+ drop_console: false, // 保留 console.error 用于调试
281
+ drop_debugger: true
282
+ },
283
+ mangle: true,
284
+ format: {
285
+ comments: false
286
+ }
287
+ });
288
+
289
+ const decompressCode = minifiedDecompressCode.code;
290
+
291
+ // 可读的 HTML 模板(保持格式以便维护)
292
+ const htmlTemplate = `
114
293
  <!DOCTYPE html>
115
294
  <html>
116
295
  <head>
@@ -218,8 +397,17 @@ const htmlTemplate=`
218
397
  </script>
219
398
  </body>
220
399
  </html>
221
- `;// 手动压缩 HTML(去除多余空格和换行,但保留必要的空格)
222
- const minifiedHtml=htmlTemplate.replace(/\n\s*/g,"")// 去除换行和缩进
223
- .replace(/>\s+</g,"><")// 去除标签间的空格
224
- .replace(/\s{2,}/g," ")// 多个空格压缩为一个
225
- .trim();return minifiedHtml}}module.exports={FflateCompressionPlugin};
400
+ `;
401
+
402
+ // 手动压缩 HTML(去除多余空格和换行,但保留必要的空格)
403
+ const minifiedHtml = htmlTemplate
404
+ .replace(/\n\s*/g, '') // 去除换行和缩进
405
+ .replace(/>\s+</g, '><') // 去除标签间的空格
406
+ .replace(/\s{2,}/g, ' ') // 多个空格压缩为一个
407
+ .trim();
408
+
409
+ return minifiedHtml;
410
+ }
411
+ }
412
+
413
+ module.exports = { FflateCompressionPlugin };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webpack 插件:在构建产物 HTML 的 </head> 之前注入字体 @font-face CSS。
5
+ *
6
+ * 用于 build 模式(playable-scripts build / builds)。
7
+ * dev 模式走 vite.dev.js 中的 Vite 插件,不走此插件。
8
+ *
9
+ * 两种使用方式:
10
+ * 1. 单构建:在 cli/commands/build.js 中通过 webpackCustomConfig.plugins 注入
11
+ * 2. 批量构建:子进程同样走单构建逻辑(schema 已被覆写为纯数据,fontResolver 自动兼容)
12
+ */
13
+
14
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
15
+ const { getOrGenerateFontCSS } = require('../utils/fontResolver');
16
+
17
+ class FontInjectionWebpackPlugin {
18
+ /**
19
+ * @param {object} opts
20
+ * @param {string} opts.projectRoot - 项目根目录
21
+ * @param {string} opts.themeName - 当前主题名称
22
+ * @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
23
+ * @param {object} [opts.mergedData] - 预合并数据(可选,批量构建子进程会走自动检测)
24
+ */
25
+ constructor(opts = {}) {
26
+ this.opts = opts;
27
+ }
28
+
29
+ apply(compiler) {
30
+ compiler.hooks.compilation.tap('FontInjectionWebpackPlugin', (compilation) => {
31
+ HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
32
+ 'FontInjectionWebpackPlugin',
33
+ (data, cb) => {
34
+ const injectData = {
35
+ projectRoot: this.opts.projectRoot || process.cwd(),
36
+ themeName: this.opts.themeName,
37
+ themeConfig: this.opts.themeConfig,
38
+ mergedData: this.opts.mergedData,
39
+ };
40
+
41
+ if (!injectData.themeName) {
42
+ // 没有主题名 → 可能是非主题项目,跳过
43
+ cb(null, data);
44
+ return;
45
+ }
46
+
47
+ const css = getOrGenerateFontCSS(injectData);
48
+ if (css) {
49
+ data.html = data.html.replace('</head>', `${css}\n</head>`);
50
+ }
51
+
52
+ cb(null, data);
53
+ }
54
+ );
55
+ });
56
+ }
57
+ }
58
+
59
+ module.exports = { FontInjectionWebpackPlugin };
@@ -1,4 +1,6 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ /**
2
4
  * Liftoff SDK 注入插件
3
5
  * 根据 Liftoff v2 标准,注入 MRAID SDK 并添加必要的 meta 标签
4
6
  * 文档: https://app.liftoff.io/public/docs/api/creative/v2/index.html
@@ -9,6 +11,26 @@
9
11
  * 3. 禁止使用 iframe
10
12
  * 4. ZIP 包格式输出
11
13
  * 5. 预留左上角和右上角 50x50px 区域(关闭按钮)
12
- */class LiftoffInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("LiftoffInjectorPlugin",compilation=>{// 注入 MRAID SDK 到 head
13
- HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("LiftoffInjectorPlugin",(data,callback)=>{// Liftoff 使用标准 MRAID 协议
14
- data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"liftoff-injector-plugin"},attributes:{src:"mraid.js",type:"text/javascript"}});callback(null,data)})})}}exports.LiftoffInjectorPlugin=LiftoffInjectorPlugin;
14
+ */
15
+ class LiftoffInjectorPlugin {
16
+ apply(compiler) {
17
+ compiler.hooks.compilation.tap('LiftoffInjectorPlugin', (compilation) => {
18
+ // 注入 MRAID SDK 到 head
19
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('LiftoffInjectorPlugin', (data, callback) => {
20
+ // Liftoff 使用标准 MRAID 协议
21
+ data.headTags.splice(0, 0, {
22
+ tagName: 'script',
23
+ voidTag: false,
24
+ meta: { plugin: 'liftoff-injector-plugin' },
25
+ attributes: {
26
+ src: 'mraid.js',
27
+ type: 'text/javascript'
28
+ }
29
+ });
30
+ callback(null, data);
31
+ });
32
+ });
33
+ }
34
+ }
35
+
36
+ exports.LiftoffInjectorPlugin = LiftoffInjectorPlugin;
@@ -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 Pangle Playable SDK script into the HTML head.
3
5
  * @implements {import('webpack').WebpackPluginInstance}
4
- */class MRAIDInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("MRAIDInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("MRAIDInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"mraid.js",type:"text/javascript"}});callback(null,data)})})}}exports.MRAIDInjectorPlugin=MRAIDInjectorPlugin;
6
+ */
7
+ class MRAIDInjectorPlugin {
8
+ apply(compiler) {
9
+ compiler.hooks.compilation.tap('MRAIDInjectorPlugin', (compilation) => {
10
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('MRAIDInjectorPlugin', (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: 'mraid.js',
17
+ type: 'text/javascript'
18
+ }
19
+ });
20
+ callback(null, data);
21
+ });
22
+ });
23
+ }
24
+ }
25
+
26
+ exports.MRAIDInjectorPlugin = MRAIDInjectorPlugin;
@@ -1,5 +1,28 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");const mintegralSrc="function gameStart() {window.mintGameStart && window.mintGameStart()}; function gameClose() {window.mintGameClose && window.mintGameClose()}";/**
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ const mintegralSrc =
4
+ 'function gameStart() {window.mintGameStart && window.mintGameStart()}; function gameClose() {window.mintGameClose && window.mintGameClose()}';
5
+
6
+ /**
2
7
  * Webpack plugin that injects Mintegral required script into the HTML head.
3
8
  * Designed to use with @playcraft/adsdk package
4
9
  * @implements {import('webpack').WebpackPluginInstance}
5
- */class MintegralInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("MintegralInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("MintegralInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},innerHTML:mintegralSrc});callback(null,data)})})}}exports.MintegralInjectorPlugin=MintegralInjectorPlugin;
10
+ */
11
+ class MintegralInjectorPlugin {
12
+ apply(compiler) {
13
+ compiler.hooks.compilation.tap('MintegralInjectorPlugin', (compilation) => {
14
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('MintegralInjectorPlugin', (data, callback) => {
15
+ data.headTags.splice(0, 0, {
16
+ tagName: 'script',
17
+ voidTag: false,
18
+ meta: { plugin: 'html-inline-script-webpack-plugin' },
19
+ innerHTML: mintegralSrc
20
+ });
21
+
22
+ callback(null, data);
23
+ });
24
+ });
25
+ }
26
+ }
27
+
28
+ exports.MintegralInjectorPlugin = MintegralInjectorPlugin;
@@ -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 Pangle Playable SDK script into the HTML head.
3
5
  * @implements {import('webpack').WebpackPluginInstance}
4
- */class PangleInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("PangleInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("PangleInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"https://sf3-ttcdn-tos.pstatp.com/obj/union-fe-nc/playable/sdk/playable-sdk.js",type:"text/javascript"}});callback(null,data)})})}}exports.PangleInjectorPlugin=PangleInjectorPlugin;
6
+ */
7
+ class PangleInjectorPlugin {
8
+ apply(compiler) {
9
+ compiler.hooks.compilation.tap('PangleInjectorPlugin', (compilation) => {
10
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('PangleInjectorPlugin', (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://sf3-ttcdn-tos.pstatp.com/obj/union-fe-nc/playable/sdk/playable-sdk.js',
17
+ type: 'text/javascript'
18
+ }
19
+ });
20
+ callback(null, data);
21
+ });
22
+ });
23
+ }
24
+ }
25
+
26
+ exports.PangleInjectorPlugin = PangleInjectorPlugin;
@@ -1,9 +1,23 @@
1
- "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ /**
2
4
  * Snapchat SDK 注入插件
3
5
  * 在 HTML 头部注入 Snapchat Playable SDK 脚本
4
6
  * 文档: https://businesshelp.snapchat.com/s/article/app-playables
5
- */class SnapchatInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("SnapchatInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("SnapchatInjectorPlugin",(data,callback)=>{// 在头部注入 Snapchat SDK 脚本标签
6
- data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{type:"text/javascript"},innerHTML:`
7
+ */
8
+ class SnapchatInjectorPlugin {
9
+ apply(compiler) {
10
+ compiler.hooks.compilation.tap('SnapchatInjectorPlugin', (compilation) => {
11
+ HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('SnapchatInjectorPlugin', (data, callback) => {
12
+ // 在头部注入 Snapchat SDK 脚本标签
13
+ data.headTags.splice(0, 0, {
14
+ tagName: 'script',
15
+ voidTag: false,
16
+ meta: { plugin: 'html-inline-script-webpack-plugin' },
17
+ attributes: {
18
+ type: 'text/javascript'
19
+ },
20
+ innerHTML: `
7
21
  // Snapchat Playable Ad SDK Mock (开发环境)
8
22
  if (typeof ScPlayableAd === 'undefined') {
9
23
  window.ScPlayableAd = {
@@ -17,4 +31,12 @@ if (typeof ScPlayableAd === 'undefined') {
17
31
  };
18
32
  console.log('[Snapchat] SDK initialized (development mode)');
19
33
  }
20
- `.trim()});callback(null,data)})})}}exports.SnapchatInjectorPlugin=SnapchatInjectorPlugin;
34
+ `.trim()
35
+ });
36
+ callback(null, data);
37
+ });
38
+ });
39
+ }
40
+ }
41
+
42
+ exports.SnapchatInjectorPlugin = SnapchatInjectorPlugin;
@@ -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;