@playcraft/devkit 1.0.31-beta.11 → 1.0.31-beta.14
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.
- package/cli/bin/playable-scripts.js +6 -53
- package/cli/commands/build.js +32 -241
- package/cli/commands/builds.js +2 -13
- package/cli/commands/dev.js +1 -22
- package/cli/commands/pack.js +40 -320
- package/cli/commands/vite-dev.js +1 -7
- package/core/artifacts/emit-fonts.test.js +1 -0
- package/core/artifacts/emit.js +4 -98
- package/core/artifacts/fflate.js +1 -31
- package/core/artifacts/zip.js +1 -39
- package/core/batch-build.js +147 -1261
- package/core/constants.js +2 -20
- package/core/cos-uploader.js +23 -289
- package/core/engines/cocos/adapter-vfs-path.test.js +1 -0
- package/core/engines/cocos/clean-cache.js +5 -179
- package/core/engines/cocos/clean-cache.test.js +1 -75
- package/core/engines/cocos/cli-build.js +2 -116
- package/core/engines/cocos/cli.js +11 -38
- package/core/engines/cocos/constants.js +1 -21
- package/core/engines/cocos/creator-cli.js +1 -108
- package/core/engines/cocos/dev-server.js +4 -412
- package/core/engines/cocos/fs.js +1 -105
- package/core/engines/cocos/injects/3x/init.js +19 -438
- package/core/engines/cocos/injects/3x/main.js +2 -52
- package/core/engines/cocos/injects/libs/pako.js +1 -2
- package/core/engines/cocos/mcp-refresh.js +2 -76
- package/core/engines/cocos/merge-inline.test.js +1 -0
- package/core/engines/cocos/merge.js +9 -148
- package/core/engines/cocos/omit-playable-weight.js +10 -0
- package/core/engines/cocos/omit-playable-weight.test.js +2 -0
- package/core/engines/cocos/pack.js +1 -187
- package/core/engines/cocos/preview-cache.js +2 -48
- package/core/engines/cocos/preview-cache.test.js +1 -32
- package/core/engines/cocos/preview-concat.js +6 -189
- package/core/engines/cocos/preview-concat.test.js +1 -73
- package/core/engines/cocos/preview-json-hook.js +1 -200
- package/core/engines/cocos/preview-json-pack.js +3 -376
- package/core/engines/cocos/preview-json-pack.test.js +2 -226
- package/core/engines/cocos/preview-proxy.js +1 -132
- package/core/engines/cocos/rebundle-preview.js +2 -54
- package/core/engines/cocos/strip-social-share.js +3 -115
- package/core/engines/cocos/theme-assets.js +2 -161
- package/core/engines/cocos/theme-levels.js +1 -0
- package/core/engines/cocos/writable-cli.js +2 -248
- package/core/engines/cocos/writable-cli.test.js +1 -112
- package/core/engines/web/index.js +1 -20
- package/core/engines/web/vite.dev.js +17 -322
- package/core/engines/web/webpack.build.js +37 -522
- package/core/engines/web/webpack.common.js +11 -177
- package/core/engines/web/webpack.dev.js +6 -88
- package/core/index.js +7 -101
- package/core/injectors/apply.js +1 -94
- package/core/injectors/check-channel-matrix.js +2 -130
- package/core/injectors/spec.js +10 -217
- package/core/loaders/gltf-loader.js +28 -74
- package/core/options.js +16 -353
- package/core/plugins/AdikteevInjectorPlugin.js +4 -26
- package/core/plugins/BigoAdsInjectorPlugin.js +4 -21
- package/core/plugins/DAPIInjectorPlugin.js +2 -25
- package/core/plugins/DebuggerInjectionPlugin.js +3 -31
- package/core/plugins/ExitAPIInjectorPlugin.js +3 -56
- package/core/plugins/FflateCompressionPlugin.js +160 -307
- package/core/plugins/FflateCompressionPlugin.test.js +13 -0
- package/core/plugins/FontInjectionWebpackPlugin.js +4 -46
- package/core/plugins/LiftoffInjectorPlugin.js +4 -26
- package/core/plugins/MRAIDInjectorPlugin.js +2 -24
- package/core/plugins/MintegralInjectorPlugin.js +2 -25
- package/core/plugins/PangleInjectorPlugin.js +2 -24
- package/core/plugins/PlaycraftOrientationInjectorPlugin.js +3 -29
- package/core/plugins/SnapchatInjectorPlugin.js +4 -26
- package/core/plugins/SocialMediaInjectorPlugin.js +7 -119
- package/core/plugins/TikTokInjectorPlugin.js +2 -24
- package/core/plugins/UnityInjectorPlugin.js +8 -37
- package/core/plugins/ZipPlugin.js +20 -154
- package/core/plugins/socialMediaVitePlugin.js +2 -108
- package/core/utils/buildDefines.js +2 -30
- package/core/utils/buildTemplateString.js +5 -39
- package/core/utils/date.js +1 -16
- package/core/utils/fontRegistry.js +13 -81
- package/core/utils/fontResolver.js +37 -335
- package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +8 -42
- package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +3 -30
- package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +7 -43
- package/core/utils/getPlaycraftOrientationSuffix.js +2 -9
- package/core/utils/injectSDKPlugins.js +13 -68
- package/core/utils/logOptions.js +5 -46
- package/core/utils/mergeOptions.js +2 -21
- package/core/utils/obfuscateLevels.js +14 -0
- package/core/utils/obfuscateLevels.test.js +10 -0
- package/core/utils/parseArgvOptions.js +5 -125
- package/core/utils/resolveChannelFold2Zip.js +3 -15
- package/core/utils/validateThemeData.js +26 -227
- package/core/validators/pre-build-checker.js +25 -287
- package/core/validators/tracking-validator.js +40 -441
- package/core/validators/tracking-validator.test.js +2 -84
- package/core/vite.dev.js +1 -1
- package/core/webpack.build.js +1 -1
- package/core/webpack.common.js +1 -1
- package/core/webpack.dev.js +1 -1
- package/package.json +2 -3
- package/README.md +0 -262
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";/**
|
|
2
2
|
* Webpack 插件:使用 fflate 压缩 HTML 文件并生成自解压版本
|
|
3
3
|
*
|
|
4
4
|
* 工作原理:
|
|
@@ -21,15 +21,11 @@
|
|
|
21
21
|
* - 仅在打正式包(生产环境)时使用
|
|
22
22
|
* - 压缩等级固定为 9(最高压缩)
|
|
23
23
|
* - 不保留原始文件
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const { minify } = require('terser');
|
|
30
|
-
|
|
31
|
-
class FflateCompressionPlugin {
|
|
32
|
-
/**
|
|
24
|
+
*/const fflate=require("fflate");const fs=require("fs");const path=require("path");const{minify}=require("terser");/**
|
|
25
|
+
* Collapse HTML whitespace without touching script bodies.
|
|
26
|
+
* Stripping newlines inside a script turns `// comment` into a comment that
|
|
27
|
+
* eats the rest of that script (and, after Adikteev concat, the next files).
|
|
28
|
+
*/function minifyHtmlKeepingScripts(html){const scripts=[];const without=String(html).replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,block=>{scripts.push(block);return`%%PLAYABLE_SCRIPT_${scripts.length-1}%%`});const minified=without.replace(/\n\s*/g,"").replace(/>\s+</g,"><").replace(/\s{2,}/g," ").trim();return minified.replace(/%%PLAYABLE_SCRIPT_(\d+)%%/g,(_,i)=>scripts[Number(i)])}class FflateCompressionPlugin{/**
|
|
33
29
|
* @param {Object} options - 插件配置选项
|
|
34
30
|
* @param {boolean} [options.enabled=true] - 是否启用压缩
|
|
35
31
|
* @param {number} [options.level=9] - 压缩等级(固定为 9)
|
|
@@ -37,298 +33,174 @@ class FflateCompressionPlugin {
|
|
|
37
33
|
* @param {boolean} [options.logStats=true] - 是否输出压缩统计
|
|
38
34
|
* @param {string} [options.googlePlayUrl] - Google Play 商店链接
|
|
39
35
|
* @param {string} [options.appStoreUrl] - App Store 商店链接
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
/**
|
|
36
|
+
*/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 之前执行
|
|
37
|
+
// 阶段顺序:OPTIMIZE_INLINE (700) → SUMMARIZE (1000) → OPTIMIZE_TRANSFER (3000)
|
|
38
|
+
// HtmlWebpackPlugin + HtmlInlineScriptPlugin 在 OPTIMIZE_INLINE 阶段完成 HTML 生成
|
|
39
|
+
// FflateCompressionPlugin 在 SUMMARIZE 阶段压缩 HTML
|
|
40
|
+
// ZipPlugin 在 OPTIMIZE_TRANSFER 阶段打包 ZIP
|
|
41
|
+
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 文件
|
|
42
|
+
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
|
|
43
|
+
for(const htmlFilename of htmlFiles){const asset=compilation.assets[htmlFilename];// 读取原始 HTML 内容
|
|
44
|
+
const originalHtml=asset.source();const originalSize=Buffer.byteLength(originalHtml,"utf8");// 压缩
|
|
45
|
+
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
|
|
46
|
+
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 库代码并压缩(使用浏览器版本)
|
|
47
|
+
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 库代码
|
|
48
|
+
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 内容,保留到自解压模板中
|
|
49
|
+
const headContent=this.extractHeadContent(originalHtml);// 生成自解压 HTML
|
|
50
|
+
const decompressedHtml=await this.generateDecompressHtml(base64String,fflateLibContent,headContent);const finalSize=Buffer.byteLength(decompressedHtml,"utf8");// 输出统计信息
|
|
51
|
+
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)
|
|
52
|
+
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}}/**
|
|
175
53
|
* 从原始 HTML 中提取 <head> 内容
|
|
176
54
|
* 保留 meta 标签、外部 script 引用、link 标签、title 等
|
|
177
55
|
* 排除内联 style 和内联 script(这些会在解压后恢复)
|
|
178
56
|
* @param {string} html - 原始 HTML 内容
|
|
179
57
|
* @returns {string} 提取的 head 内容(HTML 字符串)
|
|
180
|
-
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 标签(排除 icon,模板已内置空 favicon,避免重复声明)
|
|
213
|
-
const linkRegex = /<link\s+[^>]*?\/?>/gi;
|
|
214
|
-
while ((match = linkRegex.exec(headHtml)) !== null) {
|
|
215
|
-
const tag = match[0];
|
|
216
|
-
if (/rel\s*=\s*["'][^"']*icon[^"']*["']/i.test(tag)) continue;
|
|
217
|
-
elements.push(tag);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return elements.join('\n ');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
58
|
+
*/extractHeadContent(html){// 提取 <head>...</head> 之间的内容
|
|
59
|
+
const headMatch=html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);if(!headMatch)return"";const headHtml=headMatch[1];const elements=[];// 提取所有 meta 标签(自闭合,排除 charset 和 viewport,因为模板已包含)
|
|
60
|
+
const metaRegex=/<meta\s+[^>]*?\/?>/gi;let match;while((match=metaRegex.exec(headHtml))!==null){const tag=match[0];// 排除 charset 和 viewport(自解压模板已包含)
|
|
61
|
+
if(/charset\s*=/i.test(tag))continue;if(/name\s*=\s*["']viewport["']/i.test(tag))continue;elements.push(tag)}// 提取 title 标签
|
|
62
|
+
const titleMatch=headHtml.match(/<title[^>]*>[\s\S]*?<\/title>/i);if(titleMatch){elements.push(titleMatch[0])}// 提取外部 script 标签(有 src 属性的)
|
|
63
|
+
const scriptRegex=/<script\s+[^>]*src\s*=\s*["'][^"']+["'][^>]*>[\s\S]*?<\/script>/gi;while((match=scriptRegex.exec(headHtml))!==null){elements.push(match[0])}// 提取 link 标签(排除 icon,模板已内置空 favicon,避免重复声明)
|
|
64
|
+
const linkRegex=/<link\s+[^>]*?\/?>/gi;while((match=linkRegex.exec(headHtml))!==null){const tag=match[0];if(/rel\s*=\s*["'][^"']*icon[^"']*["']/i.test(tag))continue;elements.push(tag)}return elements.join("\n ")}/**
|
|
224
65
|
* 生成自解压 HTML 模板(极简压缩版本,无需 module)
|
|
225
66
|
* @param {string} compressedBase64 - 压缩后的 Base64 字符串
|
|
226
67
|
* @param {string} fflateLibCode - fflate 库代码(已压缩)
|
|
227
68
|
* @param {string} headContent - 从原始 HTML 提取的 head 内容
|
|
228
69
|
* @returns {Promise<string>} 完整的自解压 HTML
|
|
229
|
-
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// - false: 原始 HTML 是片段(如 Adikteev),外部资源在压缩数据中,需要完整恢复
|
|
235
|
-
const hasHeadContent = headContent.length > 0;
|
|
236
|
-
const decompressCodeReadable = `
|
|
70
|
+
*/async generateDecompressHtml(compressedBase64,fflateLibCode,headContent=""){// 可读的解压代码(保持格式以便维护)
|
|
71
|
+
// hasHeadContent: 标记 extractHeadContent 是否从原始 HTML 提取了外部资源引用
|
|
72
|
+
// - true: 原始 HTML 有 <head>,外部资源已提取到模板 head,避免解压时重复
|
|
73
|
+
// - false: 原始 HTML 是片段(如 Adikteev),外部资源在压缩数据中,需要完整恢复
|
|
74
|
+
const hasHeadContent=headContent.length>0;const decompressCodeReadable=`
|
|
237
75
|
(function() {
|
|
238
76
|
var compressedData = '${compressedBase64}';
|
|
239
77
|
var __hasHeadContent = ${hasHeadContent};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
var
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// 替换 body
|
|
256
|
-
document.body.replaceWith(doc.body);
|
|
257
|
-
|
|
258
|
-
// 恢复 head 中的资源引用和内联脚本
|
|
259
|
-
// 如果原始 HTML 有 <head>(__hasHeadContent=true),外部资源已由 extractHeadContent
|
|
260
|
-
// 提取并放在模板 head 中,这里只需追加未被模板覆盖的内联脚本和样式
|
|
261
|
-
// 如果原始 HTML 是片段(__hasHeadContent=false),如 Adikteev 格式,
|
|
262
|
-
// 所有资源引用都在压缩数据中,需要完整恢复——包括外部脚本和样式表
|
|
263
|
-
//
|
|
264
|
-
// 重要:动态注入的外部脚本在页面解析完成后才异步加载执行,
|
|
265
|
-
// 此时 DOMContentLoaded 已触发,脚本内的事件监听器将永远无法执行。
|
|
266
|
-
// 因此在外部脚本加载完成后手动 dispatch DOMContentLoaded,确保游戏等代码正常初始化。
|
|
267
|
-
var __extScriptCount = 0;
|
|
268
|
-
var __extScriptLoaded = 0;
|
|
269
|
-
Array.from(doc.head.children).forEach(function(child) {
|
|
270
|
-
if (child.tagName === 'SCRIPT') {
|
|
271
|
-
if (!child.src && child.textContent) {
|
|
272
|
-
// 内联 script(有内容但无 src)——始终需要恢复
|
|
273
|
-
var script = document.createElement('script');
|
|
274
|
-
script.textContent = child.textContent;
|
|
275
|
-
if (child.type) script.type = child.type;
|
|
276
|
-
document.head.appendChild(script);
|
|
277
|
-
} else if (child.src && !__hasHeadContent) {
|
|
278
|
-
// 外部 script(有 src 属性,如 <script src="creative.js">)
|
|
279
|
-
// 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
|
|
280
|
-
__extScriptCount++;
|
|
281
|
-
var extScript = document.createElement('script');
|
|
282
|
-
extScript.src = child.src;
|
|
283
|
-
if (child.type) extScript.type = child.type;
|
|
284
|
-
if (child.async) extScript.async = true;
|
|
285
|
-
if (child.defer) extScript.defer = true;
|
|
286
|
-
extScript.onload = function() {
|
|
287
|
-
__extScriptLoaded++;
|
|
288
|
-
if (__extScriptLoaded >= __extScriptCount) {
|
|
289
|
-
document.dispatchEvent(new Event('DOMContentLoaded'));
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
document.head.appendChild(extScript);
|
|
293
|
-
}
|
|
294
|
-
} else if (child.tagName === 'STYLE') {
|
|
295
|
-
// 内联 style——始终需要恢复
|
|
296
|
-
document.head.appendChild(child.cloneNode(true));
|
|
297
|
-
} else if (child.tagName === 'LINK' && /stylesheet/i.test(child.rel) && !__hasHeadContent) {
|
|
298
|
-
// 外部样式表(如 <link rel="stylesheet" href="style.css">)
|
|
299
|
-
// 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
|
|
300
|
-
var link = document.createElement('link');
|
|
301
|
-
link.rel = 'stylesheet';
|
|
302
|
-
link.href = child.href;
|
|
303
|
-
document.head.appendChild(link);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
} catch (error) {
|
|
78
|
+
|
|
79
|
+
function ensureBody() {
|
|
80
|
+
if (document.body) return document.body;
|
|
81
|
+
var root = document.documentElement;
|
|
82
|
+
if (!root) {
|
|
83
|
+
root = document.createElement('html');
|
|
84
|
+
document.appendChild(root);
|
|
85
|
+
}
|
|
86
|
+
var body = document.createElement('body');
|
|
87
|
+
root.appendChild(body);
|
|
88
|
+
return body;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function showDecompressError(error) {
|
|
307
92
|
console.error('Decompress failed:', error);
|
|
308
|
-
|
|
309
|
-
|
|
93
|
+
try {
|
|
94
|
+
ensureBody().innerHTML = '<div style="padding:20px;color:#fff;background:#000">' +
|
|
95
|
+
'<h2>Error</h2><p>' + error.message + '</p></div>';
|
|
96
|
+
} catch (e) {}
|
|
310
97
|
}
|
|
311
|
-
})();
|
|
312
|
-
`;
|
|
313
98
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
99
|
+
function apply() {
|
|
100
|
+
try {
|
|
101
|
+
// Base64 解码为 Uint8Array
|
|
102
|
+
var compressedArray = Uint8Array.from(atob(compressedData), function(c) {
|
|
103
|
+
return c.charCodeAt(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 使用 fflate 解压
|
|
107
|
+
var decompressedArray = fflate.unzlibSync(compressedArray);
|
|
108
|
+
var htmlString = new TextDecoder().decode(decompressedArray);
|
|
109
|
+
|
|
110
|
+
// 解析 HTML
|
|
111
|
+
var parser = new DOMParser();
|
|
112
|
+
var doc = parser.parseFromString(htmlString, 'text/html');
|
|
113
|
+
if (!doc.body) {
|
|
114
|
+
throw new Error('Decompressed HTML has no body');
|
|
325
115
|
}
|
|
326
|
-
});
|
|
327
116
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
117
|
+
// Adikteev / Bigabid 会把解压脚本抽到外部 JS。渠道 HTML 是片段,
|
|
118
|
+
// 脚本常在隐式 head 里同步执行,此时 document.body 还是 null。
|
|
119
|
+
ensureBody().replaceWith(doc.body);
|
|
120
|
+
|
|
121
|
+
// 恢复 head 中的资源引用和内联脚本
|
|
122
|
+
// 如果原始 HTML 有 <head>(__hasHeadContent=true),外部资源已由 extractHeadContent
|
|
123
|
+
// 提取并放在模板 head 中,这里只需追加未被模板覆盖的内联脚本和样式
|
|
124
|
+
// 如果原始 HTML 是片段(__hasHeadContent=false),如 Adikteev 格式,
|
|
125
|
+
// 所有资源引用都在压缩数据中,需要完整恢复——包括外部脚本和样式表
|
|
126
|
+
//
|
|
127
|
+
// 重要:动态注入的外部脚本在页面解析完成后才异步加载执行,
|
|
128
|
+
// 此时 DOMContentLoaded 已触发,脚本内的事件监听器将永远无法执行。
|
|
129
|
+
// 因此在外部脚本加载完成后手动 dispatch DOMContentLoaded,确保游戏等代码正常初始化。
|
|
130
|
+
var __extScriptCount = 0;
|
|
131
|
+
var __extScriptLoaded = 0;
|
|
132
|
+
var headChildren = doc.head ? Array.from(doc.head.children) : [];
|
|
133
|
+
headChildren.forEach(function(child) {
|
|
134
|
+
if (child.tagName === 'SCRIPT') {
|
|
135
|
+
if (!child.src && child.textContent) {
|
|
136
|
+
// 内联 script(有内容但无 src)——始终需要恢复
|
|
137
|
+
var script = document.createElement('script');
|
|
138
|
+
script.textContent = child.textContent;
|
|
139
|
+
if (child.type) script.type = child.type;
|
|
140
|
+
(document.head || document.documentElement).appendChild(script);
|
|
141
|
+
} else if (child.src && !__hasHeadContent) {
|
|
142
|
+
// 外部 script(有 src 属性,如 <script src="creative.js">)
|
|
143
|
+
// 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
|
|
144
|
+
__extScriptCount++;
|
|
145
|
+
var extScript = document.createElement('script');
|
|
146
|
+
extScript.src = child.src;
|
|
147
|
+
if (child.type) extScript.type = child.type;
|
|
148
|
+
if (child.async) extScript.async = true;
|
|
149
|
+
if (child.defer) extScript.defer = true;
|
|
150
|
+
extScript.onload = function() {
|
|
151
|
+
__extScriptLoaded++;
|
|
152
|
+
if (__extScriptLoaded >= __extScriptCount) {
|
|
153
|
+
document.dispatchEvent(new Event('DOMContentLoaded'));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
(document.head || document.documentElement).appendChild(extScript);
|
|
157
|
+
}
|
|
158
|
+
} else if (child.tagName === 'STYLE') {
|
|
159
|
+
// 内联 style——始终需要恢复
|
|
160
|
+
(document.head || document.documentElement).appendChild(child.cloneNode(true));
|
|
161
|
+
} else if (child.tagName === 'LINK' && /stylesheet/i.test(child.rel) && !__hasHeadContent) {
|
|
162
|
+
// 外部样式表(如 <link rel="stylesheet" href="style.css">)
|
|
163
|
+
// 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
|
|
164
|
+
var link = document.createElement('link');
|
|
165
|
+
link.rel = 'stylesheet';
|
|
166
|
+
link.href = child.href;
|
|
167
|
+
(document.head || document.documentElement).appendChild(link);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// DOMParser 解析出的 script 被 replaceWith 进当前文档后不会执行。
|
|
172
|
+
// head 内联脚本上面已经重建;body 里的资源表和 adapter 也必须重建,
|
|
173
|
+
// 否则引擎脚本不跑,页面会停在空白画布。
|
|
174
|
+
Array.from(document.body.querySelectorAll('script')).forEach(function(oldScript) {
|
|
175
|
+
var script = document.createElement('script');
|
|
176
|
+
Array.from(oldScript.attributes).forEach(function(attr) {
|
|
177
|
+
script.setAttribute(attr.name, attr.value);
|
|
178
|
+
});
|
|
179
|
+
var src = oldScript.getAttribute('src');
|
|
180
|
+
if (!src) {
|
|
181
|
+
script.textContent = oldScript.textContent;
|
|
182
|
+
} else if (!oldScript.hasAttribute('async') && !oldScript.hasAttribute('defer')) {
|
|
183
|
+
script.async = false;
|
|
184
|
+
}
|
|
185
|
+
oldScript.replaceWith(script);
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
showDecompressError(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (document.body) {
|
|
193
|
+
apply();
|
|
194
|
+
} else if (document.readyState === 'loading') {
|
|
195
|
+
document.addEventListener('DOMContentLoaded', apply);
|
|
196
|
+
} else {
|
|
197
|
+
apply();
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
`;// 使用 Terser 压缩解压代码
|
|
201
|
+
const minifiedDecompressCode=await minify(decompressCodeReadable,{compress:{passes:2,dead_code:true,drop_console:false,// 保留 console.error 用于调试
|
|
202
|
+
drop_debugger:true},mangle:true,format:{comments:false}});const decompressCode=minifiedDecompressCode.code;// 可读的 HTML 模板(保持格式以便维护)
|
|
203
|
+
const htmlTemplate=`
|
|
332
204
|
<!DOCTYPE html>
|
|
333
205
|
<html>
|
|
334
206
|
<head>
|
|
@@ -378,8 +250,8 @@ class FflateCompressionPlugin {
|
|
|
378
250
|
<script>${decompressCode}</script>
|
|
379
251
|
<script>
|
|
380
252
|
(function() {
|
|
381
|
-
|
|
382
|
-
|
|
253
|
+
/* 渠道静态扫描:FbPlayableAd.onCTAClick(); ExitApi.exit();window.BGY_MRAID.open() mraid.open(destinationUrl); dapi.openStoreUrl(); NUC.trigger.convert(destinationUrl) */
|
|
254
|
+
/* ScPlayableAd.onCTAClick(); smxTracking.redirect(); window.install();window.TJ_API.click(); window.openAppStore(); window.open(destinationUrl); */
|
|
383
255
|
var FbPlayableAd = {
|
|
384
256
|
onCTAClick: function() {}
|
|
385
257
|
};
|
|
@@ -389,7 +261,6 @@ class FflateCompressionPlugin {
|
|
|
389
261
|
};
|
|
390
262
|
ExitApi.exit();
|
|
391
263
|
|
|
392
|
-
// 根据 UA 判断商店链接
|
|
393
264
|
var GOOGLE_PLAY_URL = '${this.options.googlePlayUrl}';
|
|
394
265
|
var APP_STORE_URL = '${this.options.appStoreUrl}';
|
|
395
266
|
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
@@ -404,14 +275,12 @@ class FflateCompressionPlugin {
|
|
|
404
275
|
* 实际的 MRAID 调用在解压后的业务代码中执行
|
|
405
276
|
*/
|
|
406
277
|
if (false) {
|
|
407
|
-
|
|
278
|
+
/* MRAID 事件监听 */
|
|
408
279
|
mraid.addEventListener("ready", function() {});
|
|
409
280
|
mraid.addEventListener("viewableChange", function(isViewable) {
|
|
410
281
|
if (isViewable) {
|
|
411
|
-
// 广告可见,开始播放游戏和音频
|
|
412
282
|
game.sound.resumeAll();
|
|
413
283
|
} else {
|
|
414
|
-
// 广告不可见,暂停游戏和音频
|
|
415
284
|
game.sound.pauseAll();
|
|
416
285
|
}
|
|
417
286
|
});
|
|
@@ -420,14 +289,11 @@ class FflateCompressionPlugin {
|
|
|
420
289
|
mraid.addEventListener("orientationChange", function() {});
|
|
421
290
|
mraid.addEventListener("resize", function() {});
|
|
422
291
|
|
|
423
|
-
// 音频控制
|
|
424
292
|
var audio = new Audio();
|
|
425
293
|
audio.play();
|
|
426
294
|
audio.pause();
|
|
427
295
|
audio.muted = true;
|
|
428
296
|
audio.volume = 0;
|
|
429
|
-
|
|
430
|
-
// 商店跳转
|
|
431
297
|
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
432
298
|
var destinationUrl = isIOS
|
|
433
299
|
? '${this.options.appStoreUrl}'
|
|
@@ -437,17 +303,4 @@ class FflateCompressionPlugin {
|
|
|
437
303
|
</script>
|
|
438
304
|
</body>
|
|
439
305
|
</html>
|
|
440
|
-
`;
|
|
441
|
-
|
|
442
|
-
// 手动压缩 HTML(去除多余空格和换行,但保留必要的空格)
|
|
443
|
-
const minifiedHtml = htmlTemplate
|
|
444
|
-
.replace(/\n\s*/g, '') // 去除换行和缩进
|
|
445
|
-
.replace(/>\s+</g, '><') // 去除标签间的空格
|
|
446
|
-
.replace(/\s{2,}/g, ' ') // 多个空格压缩为一个
|
|
447
|
-
.trim();
|
|
448
|
-
|
|
449
|
-
return minifiedHtml;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
module.exports = { FflateCompressionPlugin };
|
|
306
|
+
`;return minifyHtmlKeepingScripts(htmlTemplate)}}module.exports={FflateCompressionPlugin,minifyHtmlKeepingScripts};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const{minifyHtmlKeepingScripts}=require("./FflateCompressionPlugin.js");const{extractInlineScripts}=require("../injectors/apply.js");describe("minifyHtmlKeepingScripts",()=>{it("does not let // comments eat the rest of a script after newline collapse",()=>{const html=`
|
|
2
|
+
<!DOCTYPE html><html><body>
|
|
3
|
+
<script>
|
|
4
|
+
(function() {
|
|
5
|
+
// decoy scanner tokens mraid.open(destinationUrl)
|
|
6
|
+
var ExitApi = { exit: function() {} };
|
|
7
|
+
ExitApi.exit();
|
|
8
|
+
}());
|
|
9
|
+
</script>
|
|
10
|
+
<script>
|
|
11
|
+
if (false) { mraid.open("x"); }
|
|
12
|
+
</script>
|
|
13
|
+
</body></html>`;const minified=minifyHtmlKeepingScripts(html);const{scripts}=extractInlineScripts(minified);const joined=scripts.join("\n");assert.match(joined,/ExitApi\.exit\(\)/);assert.match(joined,/mraid\.open\("x"\)/);assert.doesNotThrow(()=>new Function(joined))})});describe("fflate decompress runtime",()=>{it("waits for document.body before replaceWith (Adikteev fragment / head script)",async()=>{const{FflateCompressionPlugin}=require("./FflateCompressionPlugin.js");const plugin=new FflateCompressionPlugin;const html=await plugin.generateDecompressHtml("YQ==","var fflate={};","");assert.match(html,/DOMContentLoaded/);assert.match(html,/ensureBody|document\.body/);const{scripts}=extractInlineScripts(html);const joined=scripts.join("\n");assert.match(joined,/DOMContentLoaded/);assert.doesNotThrow(()=>new Function(joined))})});
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
1
|
+
"use strict";/**
|
|
4
2
|
* Webpack 插件:在构建产物 HTML 的 </head> 之前注入字体 @font-face CSS。
|
|
5
3
|
*
|
|
6
4
|
* 用于 build 模式(playable-scripts build / builds)。
|
|
@@ -9,51 +7,11 @@
|
|
|
9
7
|
* 两种使用方式:
|
|
10
8
|
* 1. 单构建:在 cli/commands/build.js 中通过 webpackCustomConfig.plugins 注入
|
|
11
9
|
* 2. 批量构建:子进程同样走单构建逻辑(schema 已被覆写为纯数据,fontResolver 自动兼容)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
15
|
-
const { getOrGenerateFontCSS } = require('../utils/fontResolver');
|
|
16
|
-
|
|
17
|
-
class FontInjectionWebpackPlugin {
|
|
18
|
-
/**
|
|
10
|
+
*/const HtmlWebpackPlugin=require("html-webpack-plugin");const{getOrGenerateFontCSS}=require("../utils/fontResolver");class FontInjectionWebpackPlugin{/**
|
|
19
11
|
* @param {object} opts
|
|
20
12
|
* @param {string} opts.projectRoot - 项目根目录
|
|
21
13
|
* @param {string} opts.themeName - 当前主题名称
|
|
22
14
|
* @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
|
|
23
15
|
* @param {object} [opts.mergedData] - 预合并数据(可选,批量构建子进程会走自动检测)
|
|
24
|
-
*/
|
|
25
|
-
|
|
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 };
|
|
16
|
+
*/constructor(opts={}){this.opts=opts}apply(compiler){compiler.hooks.compilation.tap("FontInjectionWebpackPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync("FontInjectionWebpackPlugin",(data,cb)=>{const injectData={projectRoot:this.opts.projectRoot||process.cwd(),themeName:this.opts.themeName,themeConfig:this.opts.themeConfig,mergedData:this.opts.mergedData};if(!injectData.themeName){// 没有主题名 → 可能是非主题项目,跳过
|
|
17
|
+
cb(null,data);return}const css=getOrGenerateFontCSS(injectData);if(css){data.html=data.html.replace("</head>",`${css}\n</head>`)}cb(null,data)})})}}module.exports={FontInjectionWebpackPlugin};
|