@playcraft/devkit 1.0.26 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +262 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +170 -22
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +7 -1
  6. package/cli/commands/pack.js +308 -39
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/batch-build.js +1119 -126
  9. package/core/constants.js +16 -2
  10. package/core/cos-uploader.js +286 -23
  11. package/core/index.js +76 -7
  12. package/core/loaders/gltf-loader.js +74 -28
  13. package/core/options.js +310 -15
  14. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  15. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  16. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  17. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  18. package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
  19. package/core/plugins/FflateCompressionPlugin.js +231 -40
  20. package/core/plugins/FontInjectionWebpackPlugin.js +46 -4
  21. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  22. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  23. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  24. package/core/plugins/PangleInjectorPlugin.js +24 -2
  25. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  26. package/core/plugins/SocialMediaInjectorPlugin.js +119 -7
  27. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  28. package/core/plugins/UnityInjectorPlugin.js +37 -8
  29. package/core/plugins/ZipPlugin.js +138 -19
  30. package/core/plugins/socialMediaVitePlugin.js +108 -2
  31. package/core/utils/buildDefines.js +29 -2
  32. package/core/utils/buildTemplateString.js +40 -5
  33. package/core/utils/date.js +16 -1
  34. package/core/utils/fontRegistry.js +81 -13
  35. package/core/utils/fontResolver.js +335 -37
  36. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  37. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  38. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  39. package/core/utils/injectSDKPlugins.js +68 -13
  40. package/core/utils/logOptions.js +37 -4
  41. package/core/utils/mergeOptions.js +19 -2
  42. package/core/utils/parseArgvOptions.js +111 -5
  43. package/core/utils/resolveChannelFold2Zip.js +15 -3
  44. package/core/utils/validateThemeData.js +227 -26
  45. package/core/validators/index.d.ts +4 -0
  46. package/core/validators/pre-build-checker.js +297 -21
  47. package/core/validators/tracking-validator.js +358 -40
  48. package/core/vite.dev.js +322 -17
  49. package/core/webpack.build.js +489 -56
  50. package/core/webpack.common.js +177 -11
  51. package/core/webpack.dev.js +82 -5
  52. package/package.json +3 -2
@@ -1,4 +1,6 @@
1
- "use strict";/**
1
+ 'use strict';
2
+
3
+ /**
2
4
  * 平台预置字体字典。
3
5
  *
4
6
  * 所有字体文件内置于 playable-scripts/core/resources/fonts/ 目录下。
@@ -9,21 +11,87 @@
9
11
  * - builtin=false: 需要从本地字体文件读取并 base64 内联到产物中
10
12
  *
11
13
  * key 命名规范:与 theme.schema.json5 中 fontFamily 的 enum 值一一对应。
12
- */const path=require("path");/** 字体资源目录(随 npm 包发布) */const FONTS_DIR=path.join(__dirname,"..","resources","fonts");const FONT_REGISTRY={// ===== 系统自带字体(无需注入) =====
13
- system:{builtin:true,fallback:"system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"},Arial:{builtin:true,fallback:"Arial, Helvetica, sans-serif"},monaco:{builtin:true,fallback:"Monaco, \"Courier New\", monospace"},// ===== 平台内置字体(base64 内联注入) =====
14
- // key 必须与 theme.schema.json5 fontFamily.enum 值完全一致
15
- // Google Fonts 字体:单文件 woff2
16
- Poppins:{builtin:false,fontFileName:"poppins-regular.woff2",cssFamily:"Poppins, sans-serif"},"Noto Sans SC":{builtin:false,fontFileName:"noto-sans-sc-regular.woff2",cssFamily:"\"Noto Sans SC\", sans-serif"},// 本地 ttf 种子字体(单文件 woff2,由 download-fonts.js 用 pyftsubset 生成)
17
- "Pangmen Zhengdao":{builtin:false,fontFileName:"pangmen-regular.woff2",// 由 pangmen-biaoti.ttf subset 生成
18
- cssFamily:"\"Pangmen Zhengdao\", sans-serif"},// ZeoSeven 字体:unicode-range 子集(build 时按需筛选)
19
- "Maoken Zhuyuan Ti":{builtin:false,subsetsDir:"maoken",fontFileName:"maoken-regular.woff2",cssFamily:"\"Maoken Zhuyuan Ti\", sans-serif"}};/**
14
+ */
15
+
16
+ const path = require('path');
17
+
18
+ /** 字体资源目录(随 npm 包发布) */
19
+ const FONTS_DIR = path.join(__dirname, '..', 'resources', 'fonts');
20
+
21
+ const FONT_REGISTRY = {
22
+ // ===== 系统自带字体(无需注入) =====
23
+ system: {
24
+ builtin: true,
25
+ fallback: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
26
+ },
27
+ Arial: {
28
+ builtin: true,
29
+ fallback: 'Arial, Helvetica, sans-serif',
30
+ },
31
+ monaco: {
32
+ builtin: true,
33
+ fallback: 'Monaco, "Courier New", monospace',
34
+ },
35
+
36
+ // ===== 平台内置字体(base64 内联注入) =====
37
+ // key 必须与 theme.schema.json5 fontFamily.enum 值完全一致
38
+
39
+ // Google Fonts 字体:单文件 woff2
40
+ Poppins: {
41
+ builtin: false,
42
+ fontFileName: 'poppins-regular.woff2',
43
+ cssFamily: 'Poppins, sans-serif',
44
+ },
45
+ 'Noto Sans SC': {
46
+ builtin: false,
47
+ fontFileName: 'noto-sans-sc-regular.woff2',
48
+ cssFamily: '"Noto Sans SC", sans-serif',
49
+ },
50
+
51
+ // 本地 ttf 种子字体(单文件 woff2,由 download-fonts.js 用 pyftsubset 生成)
52
+ 'Pangmen Zhengdao': {
53
+ builtin: false,
54
+ fontFileName: 'pangmen-regular.woff2', // 由 pangmen-biaoti.ttf subset 生成
55
+ cssFamily: '"Pangmen Zhengdao", sans-serif',
56
+ },
57
+
58
+ // ZeoSeven 字体:unicode-range 子集(build 时按需筛选)
59
+ 'Maoken Zhuyuan Ti': {
60
+ builtin: false,
61
+ subsetsDir: 'maoken',
62
+ fontFileName: 'maoken-regular.woff2',
63
+ cssFamily: '"Maoken Zhuyuan Ti", sans-serif',
64
+ },
65
+ };
66
+
67
+ /**
20
68
  * @returns {string[]} 所有已注册的字体 key 列表
21
- */function getRegisteredFontKeys(){return Object.keys(FONT_REGISTRY)}/**
69
+ */
70
+ function getRegisteredFontKeys() {
71
+ return Object.keys(FONT_REGISTRY);
72
+ }
73
+
74
+ /**
22
75
  * @param {string} key - 字体 key
23
76
  * @returns {boolean} 是否为系统自带字体
24
- */function isBuiltin(key){return!!(FONT_REGISTRY[key]&&FONT_REGISTRY[key].builtin)}/**
77
+ */
78
+ function isBuiltin(key) {
79
+ return !!(FONT_REGISTRY[key] && FONT_REGISTRY[key].builtin);
80
+ }
81
+
82
+ /**
25
83
  * @param {string} key - 字体 key
26
84
  * @returns {object|null} 字体配置对象,未注册返回 null
27
- */function getFontEntry(key){return FONT_REGISTRY[key]||null}/**
85
+ */
86
+ function getFontEntry(key) {
87
+ return FONT_REGISTRY[key] || null;
88
+ }
89
+
90
+ /**
28
91
  * @returns {string} 字体资源目录绝对路径
29
- */function getFontsDir(){return FONTS_DIR}module.exports={FONT_REGISTRY,getRegisteredFontKeys,isBuiltin,getFontEntry,getFontsDir};
92
+ */
93
+ function getFontsDir() {
94
+ return FONTS_DIR;
95
+ }
96
+
97
+ module.exports = { FONT_REGISTRY, getRegisteredFontKeys, isBuiltin, getFontEntry, getFontsDir };
@@ -1,4 +1,6 @@
1
- "use strict";/**
1
+ 'use strict';
2
+
3
+ /**
2
4
  * 字体解析工具。
3
5
  *
4
6
  * 负责:
@@ -9,9 +11,17 @@
9
11
  * 5. 读取字体内置文件,生成 base64 @font-face CSS
10
12
  *
11
13
  * dev / build 共用此模块。
12
- */const fs=require("fs");const path=require("path");const JSON5=require("json5");const{FONT_REGISTRY,isBuiltin,getFontsDir}=require("./fontRegistry");// ── 内存缓存(dev 模式多次 transformIndexHtml 时复用) ──
13
- let _cacheKey=null;// 'themeName::schemaMtime::dataMtime'
14
- let _cachedCSS=null;// 生成的 CSS 字符串 '' 表示无字体需要注入
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
+
15
25
  /**
16
26
  * 递归扫描 JSON Schema,收集所有名为 `fontFamily` 且有 `enum` 的字段节点。
17
27
  *
@@ -19,12 +29,64 @@ let _cachedCSS=null;// 生成的 CSS 字符串 '' 表示无字体需要注入
19
29
  * @param {Array<{trail: string[], enum: string[], default: string}>} nodes - 收集结果
20
30
  * @param {string[]} trail - 当前属性路径
21
31
  * @returns {Array}
22
- */function collectFontFamilySchemaNodes(schema,nodes=[],trail=[]){if(!schema||typeof schema!=="object")return nodes;if(schema.type==="object"&&schema.properties){for(const[key,sub]of Object.entries(schema.properties)){if(key==="fontFamily"&&sub&&Array.isArray(sub.enum)){nodes.push({trail:[...trail,key],enum:sub.enum,default:sub.default})}else{collectFontFamilySchemaNodes(sub,nodes,[...trail,key])}}}return nodes}/**
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
+ /**
23
52
  * 深度合并两个对象(同 validateThemeData.js 的 deepMerge 语义)。
24
53
  * override 中的值优先,数组整体替换不拼接。
25
- */function deepMerge(base,override){const result={...base};for(const key of Object.keys(override)){const bv=base[key];const ov=override[key];if(ov!==null&&typeof ov==="object"&&!Array.isArray(ov)&&bv!==null&&typeof bv==="object"&&!Array.isArray(bv)){result[key]=deepMerge(bv,ov)}else if(ov!==undefined){result[key]=ov}}return result}/**
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
+ /**
26
73
  * 从 JSON Schema 递归提取 default 值树(同 validateThemeData.js)。
27
- */function extractDefaults(node){if(!node||typeof node!=="object")return undefined;if(node.type==="object"&&node.properties){const r={};for(const[k,v]of Object.entries(node.properties)){const val=extractDefaults(v);if(val!==undefined)r[k]=val}return Object.keys(r).length?r:undefined}if("default"in node)return node.default;return undefined}/**
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
+ /**
28
90
  * 合并 schema 的 default 值与 theme.data.json5。
29
91
  * 如果 schemaContent 已经是纯数据(非 Schema 格式),直接作为数据使用。
30
92
  *
@@ -32,21 +94,64 @@ let _cachedCSS=null;// 生成的 CSS 字符串 '' 表示无字体需要注入
32
94
  * @param {object} themeData - 已解析的 theme.data.json5 内容
33
95
  * @param {string} themeName - 当前主题名(仅用于日志)
34
96
  * @returns {object}
35
- */function mergeThemeData(schemaContent,themeData,themeName){// schema 已经是纯数据(如批量构建子进程中 schema 已被覆写)
36
- if(!isJsonSchema(schemaContent)){// 直接使用 schema 作为 mergedData
37
- return schemaContent}// schema 是 JSON Schema 格式 → 提取 default + 合并 theme.data
38
- return deepMerge(extractDefaults(schemaContent)||{},themeData||{})}/**
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
+ /**
39
109
  * 判断对象是否为 JSON Schema 格式(有 type='object' 和 properties)。
40
- */function isJsonSchema(obj){return obj&&typeof obj==="object"&&obj.type==="object"&&typeof obj.properties==="object"}/**
110
+ */
111
+ function isJsonSchema(obj) {
112
+ return obj && typeof obj === 'object' && obj.type === 'object' && typeof obj.properties === 'object';
113
+ }
114
+
115
+ /**
41
116
  * 递归遍历 mergedData,提取所有 key 为 `fontFamily` 的字符串值。
42
117
  *
43
118
  * @param {object} mergedData
44
119
  * @param {string[]} values
45
120
  * @returns {string[]}
46
- */function collectUsedFontFamilies(mergedData,values=[]){if(!mergedData||typeof mergedData!=="object")return values;for(const[k,v]of Object.entries(mergedData)){if(k==="fontFamily"&&typeof v==="string"&&v){values.push(v)}else if(v&&typeof v==="object"&&!Array.isArray(v)){collectUsedFontFamilies(v,values)}}return values}/**
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
+ /**
47
135
  * 读取字体文件并返回 base64 编码内容。
48
136
  * 支持缓存,同进程内同一文件只读一次。
49
- */const _fontDataCache=new Map;function readFontBase64(fileName){if(_fontDataCache.has(fileName))return _fontDataCache.get(fileName);const fontPath=path.join(getFontsDir(),fileName);if(!fs.existsSync(fontPath)){console.warn(`[font] Font file not found: ${fontPath}, skipping`);_fontDataCache.set(fileName,null);return null}const data=fs.readFileSync(fontPath);const base64=data.toString("base64");_fontDataCache.set(fileName,base64);return base64}/**
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
+ /**
50
155
  * 主入口:根据项目根目录 + 主题名称,计算字体注入计划。
51
156
  *
52
157
  * @param {object} opts
@@ -60,41 +165,234 @@ return deepMerge(extractDefaults(schemaContent)||{},themeData||{})}/**
60
165
  * injectFonts: Array<{key: string, entry: object}>,
61
166
  * warnings: string[]
62
167
  * }}
63
- */function resolveFontPlans(opts){const{projectRoot,themeName,themeConfig,mergedData:preMerged}=opts;const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");if(!fs.existsSync(schemaPath)){return{usedFonts:[],builtinFonts:[],injectFonts:[],mergedData:{},warnings:[]}}const schemaContent=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));// 收集 schema 中所有 fontFamily 字段的 enum 合集作为白名单
64
- const nodes=collectFontFamilySchemaNodes(schemaContent);const schemaEnumSet=new Set;nodes.forEach(n=>n.enum.forEach(e=>schemaEnumSet.add(e)));// 合并数据:优先使用传入的预合并数据,其次 IO 自算
65
- let mergedData;if(preMerged){mergedData=preMerged}else{const themeData=fs.existsSync(dataPath)?JSON5.parse(fs.readFileSync(dataPath,"utf8")):{};mergedData=mergeThemeData(schemaContent,themeData,themeName)}// 收集使用过的 fontFamily(去重)
66
- const rawUsed=collectUsedFontFamilies(mergedData);const usedFonts=Array.from(new Set(rawUsed));// 分类 + 校验
67
- const builtinFonts=[];const injectFonts=[];const warnings=[];for(const key of usedFonts){if(!FONT_REGISTRY[key]){warnings.push(`[font] "${key}" not found in FONT_REGISTRY, will use as raw CSS font-family`);continue}if(schemaEnumSet.size>0&&!schemaEnumSet.has(key)){warnings.push(`[font] "${key}" used but not declared in any schema fontFamily.enum`)}if(isBuiltin(key)){builtinFonts.push(key)}else{injectFonts.push({key,entry:FONT_REGISTRY[key]})}}return{usedFonts,builtinFonts,injectFonts,mergedData,warnings}}/**
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
+ /**
68
224
  * 递归遍历配置对象,收集所有字符串值(用于提取文案字符集)。
69
- */function collectAllStrings(obj,strs=[]){if(!obj||typeof obj!=="object")return strs;for(const[k,v]of Object.entries(obj)){if(typeof v==="string"&&v){strs.push(v)}else if(v&&typeof v==="object"&&!Array.isArray(v)){collectAllStrings(v,strs)}}return strs}/**
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
+ /**
70
239
  * 解析单个 unicode-range 值(如 "U+4E00-9FFF, U+3400-4DBF"),判断是否覆盖 codePoint。
71
- */function rangeCoversCodePoint(rangeStr,cp){const parts=rangeStr.split(/\s*,\s*/);for(const part of parts){const m=part.match(/^U\+([0-9A-Fa-f]+)(?:-U\+([0-9A-Fa-f]+))?/);if(!m)continue;const lo=parseInt(m[1],16);const hi=m[2]?parseInt(m[2],16):lo;if(cp>=lo&&cp<=hi)return true}return false}/**
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
+ /**
72
254
  * 从 ZeoSeven 子集目录中,筛选出 covering 目标字符集的子集 woff2 文件。
73
255
  * @returns {{ css: string, usedFiles: string[] }} 或 null
74
- */function buildSubsetCSS(key,entry,targetChars){const subsDir=path.join(getFontsDir(),entry.subsetsDir);const indexPath=path.join(subsDir,"index.json");if(!fs.existsSync(indexPath))return null;const index=JSON.parse(fs.readFileSync(indexPath,"utf8"));// { "hash.woff2": "U+..." }
75
- // 找出覆盖目标字符的子集文件名(去重)
76
- const neededFiles=new Set;const matchedRanges={};// debug
77
- for(const ch of targetChars){const cp=ch.codePointAt(0);for(const[file,rangeStr]of Object.entries(index)){if(rangeCoversCodePoint(rangeStr,cp)){neededFiles.add(file);matchedRanges[ch]=rangeStr;// debug
78
- break}}}if(targetChars.length<=20){console.log(`[font] ${key} subset matching:`,matchedRanges)}console.log(`[font] ${key}: ${neededFiles.size} subsets for ${targetChars.length} unique chars`);if(neededFiles.size===0)return null;// 读取每个需要的子集文件并生成 @font-face 声明
79
- // 使用 cssFamily 中的主字体名(如 Poppins),而非内部 key(如 poppins)
80
- const familyName=entry.cssFamily?entry.cssFamily.split(",")[0].trim().replace(/^["']|["']$/g,""):key;const blocks=[];for(const file of neededFiles){const data=readFontBase64(path.join(entry.subsetsDir,file));if(!data)continue;const range=index[file];blocks.push(`@font-face{font-family:'${familyName}';src:url(data:font/woff2;base64,${data}) format('woff2');unicode-range:${range};font-display:swap}`)}return blocks.length>0?{css:blocks.join("\n"),usedFiles:[...neededFiles]}: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
+ /**
81
303
  * 生成要注入到 <head> 的 <style> 片段(base64 @font-face)。
82
304
  *
83
305
  * @param {Array<{key: string, entry: object}>} injectFonts
84
306
  * @param {object} mergedData - 合并后的主题数据,用于提取字符集
85
307
  * @returns {string}
86
- */function generateFontFaceStyle(injectFonts,mergedData){if(!injectFonts||injectFonts.length===0)return"";// 从配置中提取所有文案字符(用于动态 subset)
87
- const allStrings=collectAllStrings(mergedData||{});const targetChars=[...new Set(allStrings.join("").split(""))].join("");const blocks=[];for(const{key,entry}of injectFonts){// ZeoSeven 字体:按需筛选 unicode-range 子集
88
- if(entry.subsetsDir){const subset=buildSubsetCSS(key,entry,targetChars);if(subset){blocks.push(subset.css);console.log(`[font] ${key}: ${subset.usedFiles.length} subsets for ${targetChars.length} unique chars`);continue}// subsetsDir 存在但筛选失败 → 回退到单文件
89
- }// Google Fonts / 单文件回退
90
- const base64=readFontBase64(entry.fontFileName);if(!base64)continue;// 使用 cssFamily 的主字体名,与游戏代码中 fontFamily 值一致
91
- const familyName=entry.cssFamily?entry.cssFamily.split(",")[0].trim().replace(/^["']|["']$/g,""):key;blocks.push(`@font-face{font-family:'${familyName}';src:url(data:font/woff2;base64,${base64}) format('woff2');font-display:swap}`)}if(blocks.length===0)return"";return`<style data-playable-fonts>\n${blocks.join("\n")}\n</style>`}/**
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
+ /**
92
345
  * dev/build 统一入口:获取字体 CSS。
93
346
  * 内建基于 schema/theme.data 修改时间的缓存,dev 模式下避免重复 IO。
94
347
  *
95
348
  * @param {object} opts - 同 resolveFontPlans
96
349
  * @param {boolean} [opts.force] - 强制跳过缓存
97
350
  * @returns {string}
98
- */function getOrGenerateFontCSS(opts){const{projectRoot,themeName,themeConfig,force}=opts;const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");if(!fs.existsSync(schemaPath))return"";// 构建缓存 key:主题名 + 两个文件的修改时间
99
- const schemaMtime=fs.statSync(schemaPath).mtimeMs;const dataMtime=fs.existsSync(dataPath)?fs.statSync(dataPath).mtimeMs:0;const cacheKey=`${themeName}::${schemaMtime}::${dataMtime}`;if(!force&&_cacheKey===cacheKey&&_cachedCSS!==null){return _cachedCSS}const plan=resolveFontPlans({projectRoot,themeName,themeConfig,mergedData:opts.mergedData});// 打印 warn
100
- plan.warnings.forEach(w=>console.warn(w));const css=generateFontFaceStyle(plan.injectFonts,plan.mergedData);if(plan.injectFonts.length>0){const names=plan.injectFonts.map(f=>f.key).join(", ");console.log(`[font] Injecting fonts: ${names}`)}if(plan.builtinFonts.length>0){console.log(`[font] Using system fonts: ${plan.builtinFonts.join(", ")}`)}_cacheKey=cacheKey;_cachedCSS=css;return css}module.exports={collectFontFamilySchemaNodes,mergeThemeData,collectUsedFontFamilies,resolveFontPlans,generateFontFaceStyle,getOrGenerateFontCSS,isJsonSchema};
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
+ };