@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
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 字体解析工具。
5
+ *
6
+ * 负责:
7
+ * 1. 扫描 theme.schema.json5,收集所有 fontFamily 字段(含 enum 白名单)
8
+ * 2. 合并 schema 默认值与 theme.data.json5
9
+ * 3. 从合并数据中提取实际使用的 fontFamily 值(去重)
10
+ * 4. 对照 fontRegistry 生成注入计划
11
+ * 5. 读取字体内置文件,生成 base64 @font-face CSS
12
+ *
13
+ * dev / build 共用此模块。
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const JSON5 = require('json5');
19
+ const { FONT_REGISTRY, isBuiltin, getFontsDir } = require('./fontRegistry');
20
+
21
+ // ── 内存缓存(dev 模式多次 transformIndexHtml 时复用) ──
22
+ let _cacheKey = null; // 'themeName::schemaMtime::dataMtime'
23
+ let _cachedCSS = null; // 生成的 CSS 字符串 '' 表示无字体需要注入
24
+
25
+ /**
26
+ * 递归扫描 JSON Schema,收集所有名为 `fontFamily` 且有 `enum` 的字段节点。
27
+ *
28
+ * @param {object} schema - JSON Schema 对象
29
+ * @param {Array<{trail: string[], enum: string[], default: string}>} nodes - 收集结果
30
+ * @param {string[]} trail - 当前属性路径
31
+ * @returns {Array}
32
+ */
33
+ function collectFontFamilySchemaNodes(schema, nodes = [], trail = []) {
34
+ if (!schema || typeof schema !== 'object') return nodes;
35
+ if (schema.type === 'object' && schema.properties) {
36
+ for (const [key, sub] of Object.entries(schema.properties)) {
37
+ if (key === 'fontFamily' && sub && Array.isArray(sub.enum)) {
38
+ nodes.push({
39
+ trail: [...trail, key],
40
+ enum: sub.enum,
41
+ default: sub.default,
42
+ });
43
+ } else {
44
+ collectFontFamilySchemaNodes(sub, nodes, [...trail, key]);
45
+ }
46
+ }
47
+ }
48
+ return nodes;
49
+ }
50
+
51
+ /**
52
+ * 深度合并两个对象(同 validateThemeData.js 的 deepMerge 语义)。
53
+ * override 中的值优先,数组整体替换不拼接。
54
+ */
55
+ function deepMerge(base, override) {
56
+ const result = { ...base };
57
+ for (const key of Object.keys(override)) {
58
+ const bv = base[key];
59
+ const ov = override[key];
60
+ if (
61
+ ov !== null && typeof ov === 'object' && !Array.isArray(ov) &&
62
+ bv !== null && typeof bv === 'object' && !Array.isArray(bv)
63
+ ) {
64
+ result[key] = deepMerge(bv, ov);
65
+ } else if (ov !== undefined) {
66
+ result[key] = ov;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * 从 JSON Schema 递归提取 default 值树(同 validateThemeData.js)。
74
+ */
75
+ function extractDefaults(node) {
76
+ if (!node || typeof node !== 'object') return undefined;
77
+ if (node.type === 'object' && node.properties) {
78
+ const r = {};
79
+ for (const [k, v] of Object.entries(node.properties)) {
80
+ const val = extractDefaults(v);
81
+ if (val !== undefined) r[k] = val;
82
+ }
83
+ return Object.keys(r).length ? r : undefined;
84
+ }
85
+ if ('default' in node) return node.default;
86
+ return undefined;
87
+ }
88
+
89
+ /**
90
+ * 合并 schema 的 default 值与 theme.data.json5。
91
+ * 如果 schemaContent 已经是纯数据(非 Schema 格式),直接作为数据使用。
92
+ *
93
+ * @param {object} schemaContent - 已解析的 schema 文件内容
94
+ * @param {object} themeData - 已解析的 theme.data.json5 内容
95
+ * @param {string} themeName - 当前主题名(仅用于日志)
96
+ * @returns {object}
97
+ */
98
+ function mergeThemeData(schemaContent, themeData, themeName) {
99
+ // schema 已经是纯数据(如批量构建子进程中 schema 已被覆写)
100
+ if (!isJsonSchema(schemaContent)) {
101
+ // 直接使用 schema 作为 mergedData
102
+ return schemaContent;
103
+ }
104
+ // schema 是 JSON Schema 格式 → 提取 default + 合并 theme.data
105
+ return deepMerge(extractDefaults(schemaContent) || {}, themeData || {});
106
+ }
107
+
108
+ /**
109
+ * 判断对象是否为 JSON Schema 格式(有 type='object' 和 properties)。
110
+ */
111
+ function isJsonSchema(obj) {
112
+ return obj && typeof obj === 'object' && obj.type === 'object' && typeof obj.properties === 'object';
113
+ }
114
+
115
+ /**
116
+ * 递归遍历 mergedData,提取所有 key 为 `fontFamily` 的字符串值。
117
+ *
118
+ * @param {object} mergedData
119
+ * @param {string[]} values
120
+ * @returns {string[]}
121
+ */
122
+ function collectUsedFontFamilies(mergedData, values = []) {
123
+ if (!mergedData || typeof mergedData !== 'object') return values;
124
+ for (const [k, v] of Object.entries(mergedData)) {
125
+ if (k === 'fontFamily' && typeof v === 'string' && v) {
126
+ values.push(v);
127
+ } else if (v && typeof v === 'object' && !Array.isArray(v)) {
128
+ collectUsedFontFamilies(v, values);
129
+ }
130
+ }
131
+ return values;
132
+ }
133
+
134
+ /**
135
+ * 读取字体文件并返回 base64 编码内容。
136
+ * 支持缓存,同进程内同一文件只读一次。
137
+ */
138
+ const _fontDataCache = new Map();
139
+
140
+ function readFontBase64(fileName) {
141
+ if (_fontDataCache.has(fileName)) return _fontDataCache.get(fileName);
142
+ const fontPath = path.join(getFontsDir(), fileName);
143
+ if (!fs.existsSync(fontPath)) {
144
+ console.warn(`[font] Font file not found: ${fontPath}, skipping`);
145
+ _fontDataCache.set(fileName, null);
146
+ return null;
147
+ }
148
+ const data = fs.readFileSync(fontPath);
149
+ const base64 = data.toString('base64');
150
+ _fontDataCache.set(fileName, base64);
151
+ return base64;
152
+ }
153
+
154
+ /**
155
+ * 主入口:根据项目根目录 + 主题名称,计算字体注入计划。
156
+ *
157
+ * @param {object} opts
158
+ * @param {string} opts.projectRoot - 项目根目录绝对路径
159
+ * @param {string} opts.themeName - 主题目录名称
160
+ * @param {object} [opts.themeConfig] - builds.config.js 中的 themes 配置
161
+ * @param {object} [opts.mergedData] - 预合并数据(build 模式直接传,跳过 IO)
162
+ * @returns {{
163
+ * usedFonts: string[],
164
+ * builtinFonts: string[],
165
+ * injectFonts: Array<{key: string, entry: object}>,
166
+ * warnings: string[]
167
+ * }}
168
+ */
169
+ function resolveFontPlans(opts) {
170
+ const { projectRoot, themeName, themeConfig, mergedData: preMerged } = opts;
171
+ const sourceDir = (themeConfig && themeConfig.sourceDir) || 'src/theme';
172
+ const themeBaseDir = path.join(projectRoot, sourceDir);
173
+ const schemaPath = path.join(themeBaseDir, 'theme.schema.json5');
174
+ const dataPath = path.join(themeBaseDir, themeName, 'theme.data.json5');
175
+
176
+ if (!fs.existsSync(schemaPath)) {
177
+ return { usedFonts: [], builtinFonts: [], injectFonts: [], mergedData: {}, warnings: [] };
178
+ }
179
+
180
+ const schemaContent = JSON5.parse(fs.readFileSync(schemaPath, 'utf8'));
181
+
182
+ // 收集 schema 中所有 fontFamily 字段的 enum 合集作为白名单
183
+ const nodes = collectFontFamilySchemaNodes(schemaContent);
184
+ const schemaEnumSet = new Set();
185
+ nodes.forEach(n => n.enum.forEach(e => schemaEnumSet.add(e)));
186
+
187
+ // 合并数据:优先使用传入的预合并数据,其次 IO 自算
188
+ let mergedData;
189
+ if (preMerged) {
190
+ mergedData = preMerged;
191
+ } else {
192
+ const themeData = fs.existsSync(dataPath) ? JSON5.parse(fs.readFileSync(dataPath, 'utf8')) : {};
193
+ mergedData = mergeThemeData(schemaContent, themeData, themeName);
194
+ }
195
+
196
+ // 收集使用过的 fontFamily(去重)
197
+ const rawUsed = collectUsedFontFamilies(mergedData);
198
+ const usedFonts = Array.from(new Set(rawUsed));
199
+
200
+ // 分类 + 校验
201
+ const builtinFonts = [];
202
+ const injectFonts = [];
203
+ const warnings = [];
204
+
205
+ for (const key of usedFonts) {
206
+ if (!FONT_REGISTRY[key]) {
207
+ warnings.push(`[font] "${key}" not found in FONT_REGISTRY, will use as raw CSS font-family`);
208
+ continue;
209
+ }
210
+ if (schemaEnumSet.size > 0 && !schemaEnumSet.has(key)) {
211
+ warnings.push(`[font] "${key}" used but not declared in any schema fontFamily.enum`);
212
+ }
213
+ if (isBuiltin(key)) {
214
+ builtinFonts.push(key);
215
+ } else {
216
+ injectFonts.push({ key, entry: FONT_REGISTRY[key] });
217
+ }
218
+ }
219
+
220
+ return { usedFonts, builtinFonts, injectFonts, mergedData, warnings };
221
+ }
222
+
223
+ /**
224
+ * 递归遍历配置对象,收集所有字符串值(用于提取文案字符集)。
225
+ */
226
+ function collectAllStrings(obj, strs = []) {
227
+ if (!obj || typeof obj !== 'object') return strs;
228
+ for (const [k, v] of Object.entries(obj)) {
229
+ if (typeof v === 'string' && v) {
230
+ strs.push(v);
231
+ } else if (v && typeof v === 'object' && !Array.isArray(v)) {
232
+ collectAllStrings(v, strs);
233
+ }
234
+ }
235
+ return strs;
236
+ }
237
+
238
+ /**
239
+ * 解析单个 unicode-range 值(如 "U+4E00-9FFF, U+3400-4DBF"),判断是否覆盖 codePoint。
240
+ */
241
+ function rangeCoversCodePoint(rangeStr, cp) {
242
+ const parts = rangeStr.split(/\s*,\s*/);
243
+ for (const part of parts) {
244
+ const m = part.match(/^U\+([0-9A-Fa-f]+)(?:-U\+([0-9A-Fa-f]+))?/);
245
+ if (!m) continue;
246
+ const lo = parseInt(m[1], 16);
247
+ const hi = m[2] ? parseInt(m[2], 16) : lo;
248
+ if (cp >= lo && cp <= hi) return true;
249
+ }
250
+ return false;
251
+ }
252
+
253
+ /**
254
+ * 从 ZeoSeven 子集目录中,筛选出 covering 目标字符集的子集 woff2 文件。
255
+ * @returns {{ css: string, usedFiles: string[] }} 或 null
256
+ */
257
+ function buildSubsetCSS(key, entry, targetChars) {
258
+ const subsDir = path.join(getFontsDir(), entry.subsetsDir);
259
+ const indexPath = path.join(subsDir, 'index.json');
260
+
261
+ if (!fs.existsSync(indexPath)) return null;
262
+
263
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); // { "hash.woff2": "U+..." }
264
+
265
+ // 找出覆盖目标字符的子集文件名(去重)
266
+ const neededFiles = new Set();
267
+ const matchedRanges = {}; // debug
268
+ for (const ch of targetChars) {
269
+ const cp = ch.codePointAt(0);
270
+ for (const [file, rangeStr] of Object.entries(index)) {
271
+ if (rangeCoversCodePoint(rangeStr, cp)) {
272
+ neededFiles.add(file);
273
+ matchedRanges[ch] = rangeStr; // debug
274
+ break;
275
+ }
276
+ }
277
+ }
278
+
279
+ if (targetChars.length <= 20) {
280
+ console.log(`[font] ${key} subset matching:`, matchedRanges);
281
+ }
282
+ console.log(`[font] ${key}: ${neededFiles.size} subsets for ${targetChars.length} unique chars`);
283
+
284
+ if (neededFiles.size === 0) return null;
285
+
286
+ // 读取每个需要的子集文件并生成 @font-face 声明
287
+ // 使用 cssFamily 中的主字体名(如 Poppins),而非内部 key(如 poppins)
288
+ const familyName = entry.cssFamily ? entry.cssFamily.split(',')[0].trim().replace(/^["']|["']$/g, '') : key;
289
+ const blocks = [];
290
+ for (const file of neededFiles) {
291
+ const data = readFontBase64(path.join(entry.subsetsDir, file));
292
+ if (!data) continue;
293
+ const range = index[file];
294
+ blocks.push(
295
+ `@font-face{font-family:'${familyName}';src:url(data:font/woff2;base64,${data}) format('woff2');unicode-range:${range};font-display:swap}`
296
+ );
297
+ }
298
+
299
+ return blocks.length > 0 ? { css: blocks.join('\n'), usedFiles: [...neededFiles] } : null;
300
+ }
301
+
302
+ /**
303
+ * 生成要注入到 <head> 的 <style> 片段(base64 @font-face)。
304
+ *
305
+ * @param {Array<{key: string, entry: object}>} injectFonts
306
+ * @param {object} mergedData - 合并后的主题数据,用于提取字符集
307
+ * @returns {string}
308
+ */
309
+ function generateFontFaceStyle(injectFonts, mergedData) {
310
+ if (!injectFonts || injectFonts.length === 0) return '';
311
+
312
+ // 从配置中提取所有文案字符(用于动态 subset)
313
+ const allStrings = collectAllStrings(mergedData || {});
314
+ const targetChars = [...new Set(allStrings.join('').split(''))].join('');
315
+
316
+ const blocks = [];
317
+
318
+ for (const { key, entry } of injectFonts) {
319
+ // ZeoSeven 字体:按需筛选 unicode-range 子集
320
+ if (entry.subsetsDir) {
321
+ const subset = buildSubsetCSS(key, entry, targetChars);
322
+ if (subset) {
323
+ blocks.push(subset.css);
324
+ console.log(`[font] ${key}: ${subset.usedFiles.length} subsets for ${targetChars.length} unique chars`);
325
+ continue;
326
+ }
327
+ // subsetsDir 存在但筛选失败 → 回退到单文件
328
+ }
329
+ // Google Fonts / 单文件回退
330
+ const base64 = readFontBase64(entry.fontFileName);
331
+ if (!base64) continue;
332
+ // 使用 cssFamily 的主字体名,与游戏代码中 fontFamily 值一致
333
+ const familyName = entry.cssFamily ? entry.cssFamily.split(',')[0].trim().replace(/^["']|["']$/g, '') : key;
334
+ blocks.push(
335
+ `@font-face{font-family:'${familyName}';src:url(data:font/woff2;base64,${base64}) format('woff2');font-display:swap}`
336
+ );
337
+ }
338
+
339
+ if (blocks.length === 0) return '';
340
+
341
+ return `<style data-playable-fonts>\n${blocks.join('\n')}\n</style>`;
342
+ }
343
+
344
+ /**
345
+ * dev/build 统一入口:获取字体 CSS。
346
+ * 内建基于 schema/theme.data 修改时间的缓存,dev 模式下避免重复 IO。
347
+ *
348
+ * @param {object} opts - 同 resolveFontPlans
349
+ * @param {boolean} [opts.force] - 强制跳过缓存
350
+ * @returns {string}
351
+ */
352
+ function getOrGenerateFontCSS(opts) {
353
+ const { projectRoot, themeName, themeConfig, force } = opts;
354
+ const sourceDir = (themeConfig && themeConfig.sourceDir) || 'src/theme';
355
+ const themeBaseDir = path.join(projectRoot, sourceDir);
356
+ const schemaPath = path.join(themeBaseDir, 'theme.schema.json5');
357
+ const dataPath = path.join(themeBaseDir, themeName, 'theme.data.json5');
358
+
359
+ if (!fs.existsSync(schemaPath)) return '';
360
+
361
+ // 构建缓存 key:主题名 + 两个文件的修改时间
362
+ const schemaMtime = fs.statSync(schemaPath).mtimeMs;
363
+ const dataMtime = fs.existsSync(dataPath) ? fs.statSync(dataPath).mtimeMs : 0;
364
+ const cacheKey = `${themeName}::${schemaMtime}::${dataMtime}`;
365
+
366
+ if (!force && _cacheKey === cacheKey && _cachedCSS !== null) {
367
+ return _cachedCSS;
368
+ }
369
+
370
+ const plan = resolveFontPlans({ projectRoot, themeName, themeConfig, mergedData: opts.mergedData });
371
+
372
+ // 打印 warn
373
+ plan.warnings.forEach(w => console.warn(w));
374
+
375
+ const css = generateFontFaceStyle(plan.injectFonts, plan.mergedData);
376
+
377
+ if (plan.injectFonts.length > 0) {
378
+ const names = plan.injectFonts.map(f => f.key).join(', ');
379
+ console.log(`[font] Injecting fonts: ${names}`);
380
+ }
381
+ if (plan.builtinFonts.length > 0) {
382
+ console.log(`[font] Using system fonts: ${plan.builtinFonts.join(', ')}`);
383
+ }
384
+
385
+ _cacheKey = cacheKey;
386
+ _cachedCSS = css;
387
+ return css;
388
+ }
389
+
390
+ module.exports = {
391
+ collectFontFamilySchemaNodes,
392
+ mergeThemeData,
393
+ collectUsedFontFamilies,
394
+ resolveFontPlans,
395
+ generateFontFaceStyle,
396
+ getOrGenerateFontCSS,
397
+ isJsonSchema,
398
+ };
@@ -1,4 +1,7 @@
1
- "use strict";const fs=require("fs");const path=require("path");/**
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
2
5
  * Adikteev 平台要求的占位符变量
3
6
  * 这些变量会在生产环境中被 Adikteev 平台替换为实际值
4
7
  *
@@ -9,13 +12,16 @@
9
12
  * 2. CSS 必须单独提取为 style.css,通过 <link rel="stylesheet"> 引用
10
13
  * 3. JS 必须单独为 creative.js,通过 <script src> 引用
11
14
  * 4. JS 和 CSS 文件会被上传到 Adikteev CDN
12
- */const AdikteevPlaceholders=`<meta charset="UTF-8">
15
+ */
16
+ const AdikteevPlaceholders = `<meta charset="UTF-8">
13
17
  <link rel="stylesheet" href="style.css">
14
18
  <script type="application/javascript">
15
19
  var AK_CLICK_DESTINATION_URL = "PLACEHOLDER_CLICK_REDIRECT";
16
20
  var AK_CLICK_PIXEL_URL = "PLACEHOLDER_CLICK_PIXEL";
17
21
  </script>
18
- <script type="application/javascript" src="creative.js"></script>`;/**
22
+ <script type="application/javascript" src="creative.js"></script>`;
23
+
24
+ /**
19
25
  * 生成 Adikteev 平台所需的 HTML 配置
20
26
  *
21
27
  * Adikteev 特殊要求:
@@ -30,7 +36,36 @@
30
36
  * - style.css → 上传到 Adikteev CDN
31
37
  * - creative.js → 上传到 Adikteev CDN
32
38
  * - index.html → HTML 片段(引用上述文件)
33
- */exports.generateAdikteevHtmlWebpackPluginConfig=function generateAdikteevHtmlWebpackPluginConfig(originalHtmlContentPath){originalHtmlContentPath=originalHtmlContentPath||"src/index.html";let originalBody="";try{const content=fs.readFileSync(path.resolve(originalHtmlContentPath),"utf8");const match=content.match(/<body>([\s\S]*?)<\/body>/);if(match){originalBody=match[1].trim();// 移除缩进以获得更清晰的输出
34
- originalBody=originalBody.replaceAll(" ","").replaceAll("\t","")}}catch(e){console.warn("[Adikteev] Could not read original HTML body:",e.message)}return{filename:"index.html",inject:false,minify:false,templateContent:({htmlWebpackPlugin})=>{// 生成 HTML 代码片段(非完整文档)
35
- // 结构: CSS 引用 → 占位符变量 → JS 引用 → body 内容
36
- return[AdikteevPlaceholders,originalBody].join("\n")}}};
39
+ */
40
+ exports.generateAdikteevHtmlWebpackPluginConfig = function generateAdikteevHtmlWebpackPluginConfig(originalHtmlContentPath) {
41
+ originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
42
+ let originalBody = '';
43
+
44
+ try {
45
+ const content = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
46
+ const match = content.match(/<body>([\s\S]*?)<\/body>/);
47
+
48
+ if (match) {
49
+ originalBody = match[1].trim();
50
+ // 移除缩进以获得更清晰的输出
51
+ originalBody = originalBody.replaceAll(' ', '').replaceAll('\t', '');
52
+ }
53
+ } catch (e) {
54
+ console.warn('[Adikteev] Could not read original HTML body:', e.message);
55
+ }
56
+
57
+ return {
58
+ filename: 'index.html',
59
+ inject: false,
60
+ minify: false,
61
+ templateContent: ({ htmlWebpackPlugin }) => {
62
+ // 生成 HTML 代码片段(非完整文档)
63
+ // 结构: CSS 引用 → 占位符变量 → JS 引用 → body 内容
64
+ return [
65
+ AdikteevPlaceholders,
66
+ originalBody
67
+ ].join('\n');
68
+ }
69
+ };
70
+ };
71
+
@@ -1,4 +1,7 @@
1
- "use strict";const fs=require("fs");const path=require("path");const BigabidPlaceholders=`<meta charset="UTF-8">
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const BigabidPlaceholders = `<meta charset="UTF-8">
2
5
  <script src="mraid.js"></script>
3
6
  <script>window.BIGABID_BIDTIMEMACROS = {
4
7
  mraid_viewable: unescape("#IMP_TRACE_MRAID_VIEWABLE_ESC#"),
@@ -9,5 +12,29 @@
9
12
  final_url: unescape("#FINAL_LANDING_URL_ESC#")
10
13
  }</script>
11
14
  <script src="#BIGABID_PLAYABLE_CDN_URL#"></script>
12
- %{IMP_BEACON}`;exports.generateBigabidHtmlWebpackPluginConfig=function generateBigabidHtmlWebpackPluginConfig(originalHtmlContentPath){originalHtmlContentPath=originalHtmlContentPath||"src/index.html";let originalBody="";try{const content=fs.readFileSync(path.resolve(originalHtmlContentPath),"utf8");const match=content.match(/<body>([\s\S]*?)<\/body>/);if(match){originalBody=match[1].trim();// Remove tabs for a better look
13
- originalBody=originalBody.replaceAll(" ","").replaceAll("\t","")}}catch(e){}return{filename:"index.html",inject:false,minify:false,templateContent:({htmlWebpackPlugin})=>{return[BigabidPlaceholders,htmlWebpackPlugin.tags.headTags,originalBody].join("\n")}}};
15
+ %{IMP_BEACON}`;
16
+
17
+ exports.generateBigabidHtmlWebpackPluginConfig = function generateBigabidHtmlWebpackPluginConfig(originalHtmlContentPath) {
18
+ originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
19
+ let originalBody = '';
20
+
21
+ try {
22
+ const content = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
23
+ const match = content.match(/<body>([\s\S]*?)<\/body>/);
24
+
25
+ if (match) {
26
+ originalBody = match[1].trim();
27
+ // Remove tabs for a better look
28
+ originalBody = originalBody.replaceAll(' ', '').replaceAll('\t', '');
29
+ }
30
+ } catch (e) {}
31
+
32
+ return {
33
+ filename: 'index.html',
34
+ inject: false,
35
+ minify: false,
36
+ templateContent: ({ htmlWebpackPlugin }) => {
37
+ return [BigabidPlaceholders, htmlWebpackPlugin.tags.headTags, originalBody].join('\n');
38
+ }
39
+ };
40
+ };
@@ -1,4 +1,7 @@
1
- "use strict";const fs=require("fs");const path=require("path");/**
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
2
5
  * InMobi 追踪宏定义
3
6
  * 文档: https://support.inmobi.com/dsp/in/inmobi-dsp-india/creative-specifications-and-guidelines/
4
7
  *
@@ -7,7 +10,8 @@
7
10
  * 2. 必须包含 INMOBI_DSPMACROS 追踪变量
8
11
  * 3. 引用外部 mraid.js(由 InMobi 平台提供)
9
12
  * 4. 支持 MRAID 3.0 标准
10
- */const InMobiPlaceholders=`<script>
13
+ */
14
+ const InMobiPlaceholders = `<script>
11
15
  var INMOBI_DSPMACROS = {
12
16
  "Ad_Load_Start": ["$P_AD_LOAD_START"],
13
17
  "Ad_Viewable": ["$P_AD_READY"],
@@ -23,14 +27,46 @@
23
27
  "Spent_30_Seconds": ["$P_TIMESPENT_30"]
24
28
  };
25
29
  </script>
26
- <script src="mraid.js"></script>`;/**
30
+ <script src="mraid.js"></script>`;
31
+
32
+ /**
27
33
  * 生成 InMobi 平台所需的 HTML 配置
28
34
  *
29
35
  * 策略:
30
36
  * 1. 使用标准的 HtmlWebpackPlugin 配置(不是 templateContent)
31
37
  * 2. JS 会通过 HtmlInlineScriptPlugin 自动内联到 HTML 中
32
38
  * 3. 通过修改 template 文件注入 InMobi 追踪脚本
33
- */exports.generateInMobiHtmlWebpackPluginConfig=function generateInMobiHtmlWebpackPluginConfig(originalHtmlContentPath,buildOptions){originalHtmlContentPath=originalHtmlContentPath||"src/index.html";const placeholder=InMobiPlaceholders.replaceAll("{GOOGLE_PLAY_URL}",buildOptions.googlePlayUrl).replaceAll("{APP_STORE_URL}",buildOptions.appStoreUrl);// 读取原始 HTML 模板
34
- let htmlContent="";try{htmlContent=fs.readFileSync(path.resolve(originalHtmlContentPath),"utf8")}catch(e){console.warn("[InMobi] Could not read HTML template:",e.message)}// 在 </head> 之前注入 InMobi 追踪脚本
35
- htmlContent=htmlContent.replace("</head>",`${placeholder}\n</head>`);return{filename:"index.html",inject:"head",// webpack 会自动注入 JS,然后 HtmlInlineScriptPlugin 内联
36
- minify:false,templateContent:htmlContent,meta:{charset:"UTF-8",viewport:"width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"}}};
39
+ */
40
+ exports.generateInMobiHtmlWebpackPluginConfig = function generateInMobiHtmlWebpackPluginConfig(
41
+ originalHtmlContentPath,
42
+ buildOptions
43
+ ) {
44
+ originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
45
+
46
+ const placeholder = InMobiPlaceholders
47
+ .replaceAll('{GOOGLE_PLAY_URL}', buildOptions.googlePlayUrl)
48
+ .replaceAll('{APP_STORE_URL}', buildOptions.appStoreUrl);
49
+
50
+ // 读取原始 HTML 模板
51
+ let htmlContent = '';
52
+ try {
53
+ htmlContent = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
54
+ } catch (e) {
55
+ console.warn('[InMobi] Could not read HTML template:', e.message);
56
+ }
57
+
58
+ // 在 </head> 之前注入 InMobi 追踪脚本
59
+ htmlContent = htmlContent.replace('</head>', `${placeholder}\n</head>`);
60
+
61
+ return {
62
+ filename: 'index.html',
63
+ inject: 'head', // webpack 会自动注入 JS,然后 HtmlInlineScriptPlugin 内联
64
+ minify: false,
65
+ templateContent: htmlContent,
66
+ meta: {
67
+ charset: 'UTF-8',
68
+ viewport: 'width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no'
69
+ }
70
+ };
71
+ };
72
+
@@ -1,19 +1,66 @@
1
- "use strict";/**
1
+ /**
2
2
  * 根据 AD_NETWORK 和 AD_PROTOCOL 注入对应的渠道 SDK 脚本插件
3
3
  * 此函数被 webpack.dev.js 和 webpack.build.js 共用
4
- */const{ExitAPIInjectorPlugin}=require("../plugins/ExitAPIInjectorPlugin.js");const{DAPIInjectorPlugin}=require("../plugins/DAPIInjectorPlugin.js");const{PangleInjectorPlugin}=require("../plugins/PangleInjectorPlugin.js");const{TikTokInjectorPlugin}=require("../plugins/TikTokInjectorPlugin.js");const{MintegralInjectorPlugin}=require("../plugins/MintegralInjectorPlugin.js");const{MRAIDInjectorPlugin}=require("../plugins/MRAIDInjectorPlugin.js");const{BigoAdsInjectorPlugin}=require("../plugins/BigoAdsInjectorPlugin.js");const{LiftoffInjectorPlugin}=require("../plugins/LiftoffInjectorPlugin.js");const{SnapchatInjectorPlugin}=require("../plugins/SnapchatInjectorPlugin.js");const{AdikteevInjectorPlugin}=require("../plugins/AdikteevInjectorPlugin.js");const{UnityInjectorPlugin}=require("../plugins/UnityInjectorPlugin.js");/**
4
+ */
5
+
6
+ const { ExitAPIInjectorPlugin } = require('../plugins/ExitAPIInjectorPlugin.js');
7
+ const { DAPIInjectorPlugin } = require('../plugins/DAPIInjectorPlugin.js');
8
+ const { PangleInjectorPlugin } = require('../plugins/PangleInjectorPlugin.js');
9
+ const { TikTokInjectorPlugin } = require('../plugins/TikTokInjectorPlugin.js');
10
+ const { MintegralInjectorPlugin } = require('../plugins/MintegralInjectorPlugin.js');
11
+ const { MRAIDInjectorPlugin } = require('../plugins/MRAIDInjectorPlugin.js');
12
+ const { BigoAdsInjectorPlugin } = require('../plugins/BigoAdsInjectorPlugin.js');
13
+ const { LiftoffInjectorPlugin } = require('../plugins/LiftoffInjectorPlugin.js');
14
+ const { SnapchatInjectorPlugin } = require('../plugins/SnapchatInjectorPlugin.js');
15
+ const { AdikteevInjectorPlugin } = require('../plugins/AdikteevInjectorPlugin.js');
16
+ const { UnityInjectorPlugin } = require('../plugins/UnityInjectorPlugin.js');
17
+
18
+ /**
5
19
  * 注入渠道 SDK 插件到 webpack 配置中
6
20
  * @param {import('webpack').Configuration} webpackConfig - webpack 配置对象
7
21
  * @param {Object} options - 配置选项
8
22
  * @param {AD_NETWORK} options.network - 广告网络
9
23
  * @param {AD_PROTOCOL} options.protocol - 广告协议
10
24
  * @param {string} [options.orientation] - 屏幕方向(google 渠道需要)
11
- */function injectSDKPlugins(webpackConfig,options){const{network:adNetwork,protocol:adProtocol,orientation}=options;// 协议层注入
12
- if("dapi"===adProtocol){webpackConfig.plugins.push(new DAPIInjectorPlugin)}else if("mraid"===adProtocol){// 只有 ironsource 才注入 MRAIDInjectorPlugin
13
- // Unity 使用专门的 UnityInjectorPlugin(在渠道特定注入中处理)
14
- if(adNetwork==="ironsource"){webpackConfig.plugins.push(new MRAIDInjectorPlugin)}}// 渠道特定注入
15
- if(adNetwork==="google"){webpackConfig.plugins.push(new ExitAPIInjectorPlugin(orientation))}else if(adNetwork==="unity"){// Unity Ads 使用专门的 UnityInjectorPlugin
16
- webpackConfig.plugins.push(new UnityInjectorPlugin)}else if(adNetwork==="pangle"){webpackConfig.plugins.push(new PangleInjectorPlugin)}else if(adNetwork==="tiktok"){webpackConfig.plugins.push(new TikTokInjectorPlugin)}else if(adNetwork==="mintegral"){webpackConfig.plugins.push(new MintegralInjectorPlugin)}else if(adNetwork==="bigoads"){// BigoAds 在 dev 模式下注入模拟配置
17
- webpackConfig.plugins.push(new BigoAdsInjectorPlugin)}else if(adNetwork==="liftoff"){webpackConfig.plugins.push(new LiftoffInjectorPlugin)}else if(adNetwork==="snapchat"){// Snapchat 注入 ScPlayableAd SDK
18
- webpackConfig.plugins.push(new SnapchatInjectorPlugin)}else if(adNetwork==="adikteev"){// Adikteev 注入 MRAID mock 和占位符变量
19
- webpackConfig.plugins.push(new AdikteevInjectorPlugin)}}exports.injectSDKPlugins=injectSDKPlugins;
25
+ */
26
+ function injectSDKPlugins(webpackConfig, options) {
27
+ const { network: adNetwork, protocol: adProtocol, orientation } = options;
28
+
29
+ // 协议层注入
30
+ if ('dapi' === adProtocol) {
31
+ webpackConfig.plugins.push(new DAPIInjectorPlugin());
32
+ } else if ('mraid' === adProtocol) {
33
+ // 只有 ironsource 才注入 MRAIDInjectorPlugin
34
+ // Unity 使用专门的 UnityInjectorPlugin(在渠道特定注入中处理)
35
+ if (adNetwork === 'ironsource') {
36
+ webpackConfig.plugins.push(new MRAIDInjectorPlugin());
37
+ }
38
+ }
39
+
40
+ // 渠道特定注入
41
+ if (adNetwork === 'google') {
42
+ webpackConfig.plugins.push(new ExitAPIInjectorPlugin(orientation));
43
+ } else if (adNetwork === 'unity') {
44
+ // Unity Ads 使用专门的 UnityInjectorPlugin
45
+ webpackConfig.plugins.push(new UnityInjectorPlugin());
46
+ } else if (adNetwork === 'pangle') {
47
+ webpackConfig.plugins.push(new PangleInjectorPlugin());
48
+ } else if (adNetwork === 'tiktok') {
49
+ webpackConfig.plugins.push(new TikTokInjectorPlugin());
50
+ } else if (adNetwork === 'mintegral') {
51
+ webpackConfig.plugins.push(new MintegralInjectorPlugin());
52
+ } else if (adNetwork === 'bigoads') {
53
+ // BigoAds 在 dev 模式下注入模拟配置
54
+ webpackConfig.plugins.push(new BigoAdsInjectorPlugin());
55
+ } else if (adNetwork === 'liftoff') {
56
+ webpackConfig.plugins.push(new LiftoffInjectorPlugin());
57
+ } else if (adNetwork === 'snapchat') {
58
+ // Snapchat 注入 ScPlayableAd SDK
59
+ webpackConfig.plugins.push(new SnapchatInjectorPlugin());
60
+ } else if (adNetwork === 'adikteev') {
61
+ // Adikteev 注入 MRAID mock 和占位符变量
62
+ webpackConfig.plugins.push(new AdikteevInjectorPlugin());
63
+ }
64
+ }
65
+
66
+ exports.injectSDKPlugins = injectSDKPlugins;