@playcraft/devkit 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +259 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +130 -17
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +7 -1
  6. package/cli/commands/pack.js +308 -39
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/batch-build.js +1091 -124
  9. package/core/cos-uploader.js +264 -20
  10. package/core/index.js +71 -4
  11. package/core/loaders/gltf-loader.js +74 -28
  12. package/core/options.js +310 -15
  13. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  14. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  15. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  16. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  17. package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
  18. package/core/plugins/FflateCompressionPlugin.js +225 -37
  19. package/core/plugins/FontInjectionWebpackPlugin.js +59 -0
  20. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  21. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  22. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  23. package/core/plugins/PangleInjectorPlugin.js +24 -2
  24. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  25. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  26. package/core/plugins/UnityInjectorPlugin.js +37 -8
  27. package/core/plugins/ZipPlugin.js +138 -19
  28. package/core/resources/fonts/README.md +31 -0
  29. package/core/resources/fonts/maoken-regular.woff2 +0 -0
  30. package/core/resources/fonts/maoken-regular.woff2.chars +1 -0
  31. package/core/resources/fonts/maoken-zhuyuan.ttf +0 -0
  32. package/core/resources/fonts/noto-sans-sc-regular.woff2 +0 -0
  33. package/core/resources/fonts/pangmen-biaoti.ttf +0 -0
  34. package/core/resources/fonts/pangmen-regular.woff2 +0 -0
  35. package/core/resources/fonts/pangmen-regular.woff2.chars +1 -0
  36. package/core/resources/fonts/poppins-regular.woff2 +0 -0
  37. package/core/utils/buildDefines.js +29 -2
  38. package/core/utils/buildTemplateString.js +40 -5
  39. package/core/utils/date.js +16 -1
  40. package/core/utils/fontRegistry.js +97 -0
  41. package/core/utils/fontResolver.js +398 -0
  42. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  43. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  44. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  45. package/core/utils/injectSDKPlugins.js +58 -11
  46. package/core/utils/logOptions.js +37 -4
  47. package/core/utils/mergeOptions.js +19 -2
  48. package/core/utils/parseArgvOptions.js +110 -5
  49. package/core/utils/resolveChannelFold2Zip.js +15 -3
  50. package/core/utils/validateThemeData.js +220 -25
  51. package/core/validators/pre-build-checker.js +201 -21
  52. package/core/validators/tracking-validator.js +358 -40
  53. package/core/vite.dev.js +332 -16
  54. package/core/webpack.build.js +494 -52
  55. package/core/webpack.common.js +177 -11
  56. package/core/webpack.dev.js +82 -5
  57. package/package.json +3 -2
@@ -1,19 +1,138 @@
1
- "use strict";const path=require("path");const fs=require("fs");const ModuleFilenameHelpers=require("webpack/lib/ModuleFilenameHelpers");const webpack=require("webpack");const webpackSources=webpack.sources||require("webpack-sources");const yazl=require("yazl");function ZipPlugin(options){this.options=options||{}}ZipPlugin.prototype.apply=function(compiler){const options=this.options;const isWebpack4=webpack.version.startsWith("4.");// Prefer `compiler.webpack` instead of webpack or webpack-sources import (rspack compatibility hack)
2
- const{RawSource}=compiler.webpack&&compiler.webpack.sources||webpackSources;if(options.pathPrefix&&path.isAbsolute(options.pathPrefix)){throw new Error("\"pathPrefix\" must be a relative path")}// 保存临时文件夹路径,用于后续删除
3
- let tempFolderToClean=null;const process=function(compilation,callback){// assets from child compilers will be included in the parent
4
- // so we should not run in child compilers
5
- if(compilation.compiler.isChild()){callback();return}const zipFile=new yazl.ZipFile;const pathPrefix=options.pathPrefix||"";const pathMapper=options.pathMapper||function(x){return x};// populate the zip file with each asset
6
- for(const nameAndPath of Object.keys(compilation.assets)){// match against include and exclude, which may be strings, regexes, arrays of the previous or omitted
7
- if(!ModuleFilenameHelpers.matchObject({include:options.include,exclude:options.exclude},nameAndPath))continue;const source=compilation.assets[nameAndPath].source();zipFile.addBuffer(Buffer.isBuffer(source)?source:Buffer.from?Buffer.from(source):new Buffer(source),path.join(pathPrefix,pathMapper(nameAndPath)),options.fileOptions)}zipFile.end(options.zipOptions);// accumulate each buffer containing a part of the zip file
8
- const bufs=[];zipFile.outputStream.on("data",function(buf){bufs.push(buf)});zipFile.outputStream.on("end",function(){// default to webpack's root output path if no path provided
9
- const outputPath=options.path||compilation.options.output.path;// default to webpack root filename if no filename provided, else the basename of the output path
10
- let outputFilename=options.filename||compilation.options.output.filename||path.basename(outputPath);const fullHash=compilation.fullHash||compilation.hash;outputFilename=outputFilename.replace("[fullhash:6]",fullHash.slice(0,6));const extension="."+(options.extension||"zip");// combine the output path and filename
11
- const outputPathAndFilename=path.resolve(compilation.options.output.path,// ...supporting both absolute and relative paths
12
- outputPath,path.basename(outputFilename,".zip")+extension// ...and filenames with and without a .zip extension
13
- );// resolve a relative output path with respect to webpack's root output path
14
- // since only relative paths are permitted for keys in `compilation.assets`
15
- const relativeOutputPath=path.relative(compilation.options.output.path,outputPathAndFilename);// add our zip file to the assets
16
- const zipFileSource=new RawSource(Buffer.concat(bufs));if(isWebpack4){compilation.assets[relativeOutputPath]=zipFileSource}else{compilation.emitAsset(relativeOutputPath,zipFileSource)}// 🗑️ 保存临时文件夹路径,用于后续删除
17
- if(options.cleanTempFolder!==false){const tempFolder=compilation.options.output.path;const zipOutputPath=options.path||compilation.options.output.path;// 确保临时文件夹存在且不是 zip 输出目录
18
- if(tempFolder&&tempFolder!==zipOutputPath){tempFolderToClean=tempFolder}}callback()})};if(isWebpack4){compiler.hooks.emit.tapAsync(ZipPlugin.name,process)}else{compiler.hooks.thisCompilation.tap(ZipPlugin.name,compilation=>{compilation.hooks.processAssets.tapPromise({name:ZipPlugin.name,stage:webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER},()=>new Promise(resolve=>process(compilation,resolve)))})}// 🗑️ 在所有文件写入完成后删除临时文件夹
19
- compiler.hooks.done.tap(ZipPlugin.name,()=>{if(tempFolderToClean&&fs.existsSync(tempFolderToClean)){try{fs.rmSync(tempFolderToClean,{recursive:true,force:true});console.log(`✅ 已删除临时文件夹: ${tempFolderToClean}`)}catch(err){console.warn(`⚠️ 删除临时文件夹失败: ${tempFolderToClean}`,err.message)}}})};exports.ZipPlugin=ZipPlugin;
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers');
6
+ const webpack = require('webpack');
7
+ const webpackSources = webpack.sources || require('webpack-sources');
8
+ const yazl = require('yazl');
9
+
10
+ function ZipPlugin(options) {
11
+ this.options = options || {};
12
+ }
13
+
14
+ ZipPlugin.prototype.apply = function (compiler) {
15
+ const options = this.options;
16
+ const isWebpack4 = webpack.version.startsWith('4.');
17
+ // Prefer `compiler.webpack` instead of webpack or webpack-sources import (rspack compatibility hack)
18
+ const { RawSource } = (compiler.webpack && compiler.webpack.sources) || webpackSources;
19
+
20
+ if (options.pathPrefix && path.isAbsolute(options.pathPrefix)) {
21
+ throw new Error('"pathPrefix" must be a relative path');
22
+ }
23
+
24
+ // 保存临时文件夹路径,用于后续删除
25
+ let tempFolderToClean = null;
26
+
27
+ const process = function (compilation, callback) {
28
+ // assets from child compilers will be included in the parent
29
+ // so we should not run in child compilers
30
+ if (compilation.compiler.isChild()) {
31
+ callback();
32
+ return;
33
+ }
34
+
35
+ const zipFile = new yazl.ZipFile();
36
+
37
+ const pathPrefix = options.pathPrefix || '';
38
+ const pathMapper =
39
+ options.pathMapper ||
40
+ function (x) {
41
+ return x;
42
+ };
43
+
44
+ // populate the zip file with each asset
45
+ for (const nameAndPath of Object.keys(compilation.assets)) {
46
+ // match against include and exclude, which may be strings, regexes, arrays of the previous or omitted
47
+ if (!ModuleFilenameHelpers.matchObject({ include: options.include, exclude: options.exclude }, nameAndPath)) continue;
48
+
49
+ const source = compilation.assets[nameAndPath].source();
50
+
51
+ zipFile.addBuffer(
52
+ Buffer.isBuffer(source) ? source : Buffer.from ? Buffer.from(source) : new Buffer(source),
53
+ path.join(pathPrefix, pathMapper(nameAndPath)),
54
+ options.fileOptions
55
+ );
56
+ }
57
+
58
+ zipFile.end(options.zipOptions);
59
+
60
+ // accumulate each buffer containing a part of the zip file
61
+ const bufs = [];
62
+
63
+ zipFile.outputStream.on('data', function (buf) {
64
+ bufs.push(buf);
65
+ });
66
+
67
+ zipFile.outputStream.on('end', function () {
68
+ // default to webpack's root output path if no path provided
69
+ const outputPath = options.path || compilation.options.output.path;
70
+ // default to webpack root filename if no filename provided, else the basename of the output path
71
+ let outputFilename = options.filename || compilation.options.output.filename || path.basename(outputPath);
72
+
73
+ const fullHash = compilation.fullHash || compilation.hash;
74
+ outputFilename = outputFilename.replace('[fullhash:6]', fullHash.slice(0, 6));
75
+
76
+ const extension = '.' + (options.extension || 'zip');
77
+
78
+ // combine the output path and filename
79
+ const outputPathAndFilename = path.resolve(
80
+ compilation.options.output.path, // ...supporting both absolute and relative paths
81
+ outputPath,
82
+ path.basename(outputFilename, '.zip') + extension // ...and filenames with and without a .zip extension
83
+ );
84
+
85
+ // resolve a relative output path with respect to webpack's root output path
86
+ // since only relative paths are permitted for keys in `compilation.assets`
87
+ const relativeOutputPath = path.relative(compilation.options.output.path, outputPathAndFilename);
88
+
89
+ // add our zip file to the assets
90
+ const zipFileSource = new RawSource(Buffer.concat(bufs));
91
+ if (isWebpack4) {
92
+ compilation.assets[relativeOutputPath] = zipFileSource;
93
+ } else {
94
+ compilation.emitAsset(relativeOutputPath, zipFileSource);
95
+ }
96
+
97
+ // 🗑️ 保存临时文件夹路径,用于后续删除
98
+ if (options.cleanTempFolder !== false) {
99
+ const tempFolder = compilation.options.output.path;
100
+ const zipOutputPath = options.path || compilation.options.output.path;
101
+ // 确保临时文件夹存在且不是 zip 输出目录
102
+ if (tempFolder && tempFolder !== zipOutputPath) {
103
+ tempFolderToClean = tempFolder;
104
+ }
105
+ }
106
+
107
+ callback();
108
+ });
109
+ };
110
+
111
+ if (isWebpack4) {
112
+ compiler.hooks.emit.tapAsync(ZipPlugin.name, process);
113
+ } else {
114
+ compiler.hooks.thisCompilation.tap(ZipPlugin.name, (compilation) => {
115
+ compilation.hooks.processAssets.tapPromise(
116
+ {
117
+ name: ZipPlugin.name,
118
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER
119
+ },
120
+ () => new Promise((resolve) => process(compilation, resolve))
121
+ );
122
+ });
123
+ }
124
+
125
+ // 🗑️ 在所有文件写入完成后删除临时文件夹
126
+ compiler.hooks.done.tap(ZipPlugin.name, () => {
127
+ if (tempFolderToClean && fs.existsSync(tempFolderToClean)) {
128
+ try {
129
+ fs.rmSync(tempFolderToClean, { recursive: true, force: true });
130
+ console.log(`✅ 已删除临时文件夹: ${tempFolderToClean}`);
131
+ } catch (err) {
132
+ console.warn(`⚠️ 删除临时文件夹失败: ${tempFolderToClean}`, err.message);
133
+ }
134
+ }
135
+ });
136
+ };
137
+
138
+ exports.ZipPlugin = ZipPlugin;
@@ -0,0 +1,31 @@
1
+ # 平台预置字体资源
2
+
3
+ 本目录存放可玩广告使用的字体内置资源文件(woff2 格式)。
4
+
5
+ ## 字体列表
6
+
7
+ | key | 文件名 | 说明 | 下载来源 |
8
+ |-----|--------|------|----------|
9
+ | `poppins` | `poppins-regular.woff2` | 英文字体 Poppins Regular | [Google Fonts](https://fonts.google.com/specimen/Poppins) |
10
+ | `noto-sans-sc` | `noto-sans-sc-regular.woff2` | 简体中文 Noto Sans SC | [Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+SC) |
11
+ | `pangmen` | `pangmen-regular.woff2` | 庞门正道标题体 | 字体网站下载后 subset |
12
+ | `maoken` | `maoken-regular.woff2` | 猫啃珠圆体 | 字体网站下载后 subset |
13
+
14
+ ## 下载字体
15
+
16
+ ```bash
17
+ # 在 playable-scripts 目录下运行
18
+ node scripts/download-fonts.js
19
+ ```
20
+
21
+ 该脚本会自动下载 Google Fonts 中的开源字体(poppins、noto-sans-sc),
22
+ 并转换为 woff2 格式存入本目录。
23
+
24
+ 对于 pangmen / maoken 等非 Google Fonts 字体,需手动下载后放入本目录。
25
+
26
+ ## 字体文件要求
27
+
28
+ - 格式:woff2
29
+ - 文件名:与上表中的 key 对应
30
+ - 中文字体需做 subset(仅保留常用字符),建议控制在 100KB 以内
31
+ - 会被内联为 base64 写入 HTML 产物,请控制体积
@@ -0,0 +1 @@
1
+ 5434fbcb2072e4a7760465dd0f37cffa
@@ -0,0 +1 @@
1
+ 5434fbcb2072e4a7760465dd0f37cffa
@@ -1,3 +1,30 @@
1
- "use strict";const{options}=require("../options");/**
1
+ const { options } = require('../options');
2
+
3
+ /**
2
4
  * Build's default defines list for dev & prod builds
3
- */exports.buildDefines=function buildDefines(){const defines={__DEV__:JSON.stringify(options["dev"]===undefined?false:options["dev"]),GOOGLE_PLAY_URL:JSON.stringify(options.googlePlayUrl),APP_STORE_URL:JSON.stringify(options.appStoreUrl),AD_NETWORK:JSON.stringify(options["network"]),AD_PROTOCOL:JSON.stringify(options["protocol"]),APP:JSON.stringify(options.app),NAME:JSON.stringify(options.name),VERSION:JSON.stringify(options.version),BUILD_HASH:JSON.stringify((+new Date).toString(36)),LANGUAGE:JSON.stringify(options.language),ORIENTATION:JSON.stringify(options.orientation)};/** @type {AD_NETWORK} */let adNetworkDefine=options["network"];if(adNetworkDefine==="pangle")adNetworkDefine="tiktok";if(adNetworkDefine==="mytarget"||adNetworkDefine==="moloco")adNetworkDefine="facebook";defines["AD_NETWORK"]=JSON.stringify(adNetworkDefine);return defines};
5
+ */
6
+ exports.buildDefines = function buildDefines() {
7
+ const defines = {
8
+ __DEV__: JSON.stringify(options['dev'] === undefined ? false : options['dev']),
9
+ GOOGLE_PLAY_URL: JSON.stringify(options.googlePlayUrl),
10
+ APP_STORE_URL: JSON.stringify(options.appStoreUrl),
11
+ AD_NETWORK: JSON.stringify(options['network']),
12
+ AD_PROTOCOL: JSON.stringify(options['protocol']),
13
+ APP: JSON.stringify(options.app),
14
+ NAME: JSON.stringify(options.name),
15
+ VERSION: JSON.stringify(options.version),
16
+ BUILD_HASH: JSON.stringify((+new Date()).toString(36)),
17
+ LANGUAGE: JSON.stringify(options.language),
18
+ ORIENTATION: JSON.stringify(options.orientation)
19
+ };
20
+
21
+ /** @type {AD_NETWORK} */
22
+ let adNetworkDefine = options['network'];
23
+
24
+ if (adNetworkDefine === 'pangle') adNetworkDefine = 'tiktok';
25
+ if (adNetworkDefine === 'mytarget' || adNetworkDefine === 'moloco') adNetworkDefine = 'facebook';
26
+
27
+ defines['AD_NETWORK'] = JSON.stringify(adNetworkDefine);
28
+
29
+ return defines;
30
+ };
@@ -1,6 +1,41 @@
1
- "use strict";const{options}=require("../options");const{getCurrentDateFormatted}=require("./date");const path=require("path");/** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */const adNetworkFileNameMap={preview:"Preview",bigoads:"bigo"};/**
1
+ const { options } = require('../options');
2
+ const { getCurrentDateFormatted } = require('./date');
3
+ const path = require('path');
4
+
5
+ /** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */
6
+ const adNetworkFileNameMap = {
7
+ preview: 'Preview',
8
+ bigoads: 'bigo'
9
+ };
10
+
11
+ /**
2
12
  * Build's result string from template. Used for outDir or filename
3
- */exports.buildTemplateString=function buildTemplateString(template,customOptions){customOptions=customOptions||options;/** @type {AD_NETWORK} */const adNetwork=customOptions["network"];/** @type {AD_PROTOCOL} */// const adProtocol = customOptions['protocol'];
4
- let networkName=customOptions.adNetworkNames[adNetwork]||adNetworkFileNameMap[adNetwork]||adNetwork;// if (adProtocol === 'dapi') networkName += '_DAPI';
5
- // 如果是绝对路径,直接返回,不做模板替换
6
- if(path.isAbsolute(template)){return template}template=template.replaceAll("{app}",customOptions.app);template=template.replaceAll("{name}",customOptions.name);template=template.replaceAll("{version}",customOptions.version);template=template.replaceAll("{date}",getCurrentDateFormatted());template=template.replaceAll("{language}",customOptions.language);template=template.replaceAll("{orientation}",customOptions.orientation);template=template.replaceAll("{network}",networkName.toLowerCase());template=template.replaceAll("{hash}","[fullhash:6]");return template};
13
+ */
14
+ exports.buildTemplateString = function buildTemplateString(template, customOptions) {
15
+ customOptions = customOptions || options;
16
+
17
+ /** @type {AD_NETWORK} */
18
+ const adNetwork = customOptions['network'];
19
+
20
+ /** @type {AD_PROTOCOL} */
21
+ // const adProtocol = customOptions['protocol'];
22
+
23
+ let networkName = customOptions.adNetworkNames[adNetwork] || adNetworkFileNameMap[adNetwork] || adNetwork;
24
+ // if (adProtocol === 'dapi') networkName += '_DAPI';
25
+
26
+ // 如果是绝对路径,直接返回,不做模板替换
27
+ if (path.isAbsolute(template)) {
28
+ return template;
29
+ }
30
+
31
+ template = template.replaceAll('{app}', customOptions.app);
32
+ template = template.replaceAll('{name}', customOptions.name);
33
+ template = template.replaceAll('{version}', customOptions.version);
34
+ template = template.replaceAll('{date}', getCurrentDateFormatted());
35
+ template = template.replaceAll('{language}', customOptions.language);
36
+ template = template.replaceAll('{orientation}', customOptions.orientation);
37
+ template = template.replaceAll('{network}', networkName.toLowerCase());
38
+ template = template.replaceAll('{hash}', '[fullhash:6]');
39
+
40
+ return template;
41
+ };
@@ -1 +1,16 @@
1
- "use strict";exports.getCurrentDateFormatted=function getCurrentDateFormatted(includeTime){const date=new Date;const year=date.getFullYear();const month=(date.getMonth()+1).toString().padStart(2,"0");const day=date.getDate().toString().padStart(2,"0");if(includeTime){const hour=date.getHours().toString().padStart(2,"0");const minute=date.getMinutes().toString().padStart(2,"0");const second=date.getSeconds().toString().padStart(2,"0");return`${year}${month}${day}-${hour}${minute}${second}`}return`${year}${month}${day}`};
1
+ exports.getCurrentDateFormatted = function getCurrentDateFormatted(includeTime) {
2
+ const date = new Date();
3
+ const year = date.getFullYear();
4
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
5
+ const day = date.getDate().toString().padStart(2, '0');
6
+
7
+ if (includeTime) {
8
+ const hour = date.getHours().toString().padStart(2, '0');
9
+ const minute = date.getMinutes().toString().padStart(2, '0');
10
+ const second = date.getSeconds().toString().padStart(2, '0');
11
+
12
+ return `${year}${month}${day}-${hour}${minute}${second}`;
13
+ }
14
+
15
+ return `${year}${month}${day}`;
16
+ };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 平台预置字体字典。
5
+ *
6
+ * 所有字体文件内置于 playable-scripts/core/resources/fonts/ 目录下。
7
+ * 首次使用前需运行 `node scripts/download-fonts.js` 下载字体文件。
8
+ *
9
+ * 分类:
10
+ * - builtin=true: 系统自带字体,无需注入文件,运行时走 CSS fallback 链
11
+ * - builtin=false: 需要从本地字体文件读取并 base64 内联到产物中
12
+ *
13
+ * key 命名规范:与 theme.schema.json5 中 fontFamily 的 enum 值一一对应。
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
+ /**
68
+ * @returns {string[]} 所有已注册的字体 key 列表
69
+ */
70
+ function getRegisteredFontKeys() {
71
+ return Object.keys(FONT_REGISTRY);
72
+ }
73
+
74
+ /**
75
+ * @param {string} key - 字体 key
76
+ * @returns {boolean} 是否为系统自带字体
77
+ */
78
+ function isBuiltin(key) {
79
+ return !!(FONT_REGISTRY[key] && FONT_REGISTRY[key].builtin);
80
+ }
81
+
82
+ /**
83
+ * @param {string} key - 字体 key
84
+ * @returns {object|null} 字体配置对象,未注册返回 null
85
+ */
86
+ function getFontEntry(key) {
87
+ return FONT_REGISTRY[key] || null;
88
+ }
89
+
90
+ /**
91
+ * @returns {string} 字体资源目录绝对路径
92
+ */
93
+ function getFontsDir() {
94
+ return FONTS_DIR;
95
+ }
96
+
97
+ module.exports = { FONT_REGISTRY, getRegisteredFontKeys, isBuiltin, getFontEntry, getFontsDir };