@playcraft/devkit 1.0.16 → 1.0.17

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 (45) hide show
  1. package/README.md +259 -0
  2. package/cli/bin/playable-scripts.js +44 -1
  3. package/cli/commands/build.js +122 -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 +58 -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/LiftoffInjectorPlugin.js +26 -4
  20. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  21. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  22. package/core/plugins/PangleInjectorPlugin.js +24 -2
  23. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  24. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  25. package/core/plugins/UnityInjectorPlugin.js +37 -8
  26. package/core/plugins/ZipPlugin.js +138 -19
  27. package/core/utils/buildDefines.js +29 -2
  28. package/core/utils/buildTemplateString.js +40 -5
  29. package/core/utils/date.js +16 -1
  30. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  31. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  32. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  33. package/core/utils/injectSDKPlugins.js +58 -11
  34. package/core/utils/logOptions.js +37 -4
  35. package/core/utils/mergeOptions.js +19 -2
  36. package/core/utils/parseArgvOptions.js +110 -5
  37. package/core/utils/resolveChannelFold2Zip.js +15 -3
  38. package/core/utils/validateThemeData.js +220 -25
  39. package/core/validators/pre-build-checker.js +201 -21
  40. package/core/validators/tracking-validator.js +358 -40
  41. package/core/vite.dev.js +181 -15
  42. package/core/webpack.build.js +446 -52
  43. package/core/webpack.common.js +177 -11
  44. package/core/webpack.dev.js +82 -5
  45. package/package.json +3 -2
@@ -1,8 +1,113 @@
1
- "use strict";/** @type {string[]} List of supported advertising networks */const allowedAdNetworks=["preview","applovin","unity","google","ironsource","facebook","moloco","adcolony","mintegral","vungle","tapjoy","snapchat","tiktok","appreciate","chartboost","pangle","mytarget","liftoff","smadex","adikteev","bigabid","inmobi","bigoads"];/** @type {AD_PROTOCOL[]} List of supported advertising protocols */const allowedAdProtocols=["none","mraid","dapi"];/** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */const mraidPartners=["ironsource","applovin","unity","appreciate","chartboost","mytarget","liftoff","adcolony","adikteev","bigabid","inmobi"];/** @type {LANGUAGE[]} */const allowedLanguages=["auto","en","es","zh","hi","ar","fr","de","ja","pt"];/** @type {ORIENTATION[]} */const allowedOrientations=["both","portrait","landscape","square"];/**
1
+ /** @type {string[]} List of supported advertising networks */
2
+ const allowedAdNetworks = [
3
+ 'preview',
4
+ 'applovin',
5
+ 'unity',
6
+ 'google',
7
+ 'ironsource',
8
+ 'facebook',
9
+ 'moloco',
10
+ 'adcolony',
11
+ 'mintegral',
12
+ 'vungle',
13
+ 'tapjoy',
14
+ 'snapchat',
15
+ 'tiktok',
16
+ 'appreciate',
17
+ 'chartboost',
18
+ 'pangle',
19
+ 'mytarget',
20
+ 'liftoff',
21
+ 'smadex',
22
+ 'adikteev',
23
+ 'bigabid',
24
+ 'inmobi',
25
+ 'bigoads'
26
+ ];
27
+ /** @type {AD_PROTOCOL[]} List of supported advertising protocols */
28
+ const allowedAdProtocols = ['none', 'mraid', 'dapi'];
29
+
30
+ /** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */
31
+ const mraidPartners = [
32
+ 'ironsource',
33
+ 'applovin',
34
+ 'unity',
35
+ 'appreciate',
36
+ 'chartboost',
37
+ 'mytarget',
38
+ 'liftoff',
39
+ 'adcolony',
40
+ 'adikteev',
41
+ 'bigabid',
42
+ 'inmobi'
43
+ ];
44
+
45
+ /** @type {LANGUAGE[]} */
46
+ const allowedLanguages = ['auto', 'en', 'es', 'zh', 'hi', 'ar', 'fr', 'de', 'ja', 'pt'];
47
+
48
+ /** @type {ORIENTATION[]} */
49
+ const allowedOrientations = ['both', 'portrait', 'landscape', 'square'];
50
+
51
+ /**
2
52
  * Parses command line arguments based on provided options configuration
3
53
  * @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
4
54
  * @returns {Record<string, any>} Parsed options object with values from command line arguments
5
- */exports.parseArgvOptions=function parseArgvOptions(posiibleOptions){const argvOptions={};// Track which options were explicitly set via CLI args
6
- const cliExplicitKeys=new Set;for(let possibleOption of posiibleOptions){if(possibleOption.defaultValue!==undefined)argvOptions[possibleOption.alias||possibleOption.name]=possibleOption.defaultValue}let adNetwork=null;for(let i=2;i<process.argv.length;i++){let key=process.argv[i];let inlineValue=null;// 支持 --key=value 格式
7
- if(key.includes("=")){const eqIndex=key.indexOf("=");inlineValue=key.slice(eqIndex+1);key=key.slice(0,eqIndex)}const allowedOption=posiibleOptions.find(e=>"--"+e.name===key);if(allowedOption){let value=true;if(allowedOption.hasValue){// 优先使用内联值(--key=value),否则使用下一个参数
8
- value=inlineValue!==null?inlineValue:process.argv[++i];if(allowedOption.parser)value=allowedOption.parser(value)}const optionKey=allowedOption.alias||allowedOption.name;argvOptions[optionKey]=value;cliExplicitKeys.add(optionKey)}else if(!adNetwork)adNetwork=key}if(posiibleOptions.find(e=>e.name==="network")){if(adNetwork&&allowedAdNetworks.includes(adNetwork)){argvOptions["network"]=adNetwork}if(posiibleOptions.find(e=>e.name==="protocol")){if(mraidPartners.includes(argvOptions["network"])&&argvOptions["protocol"]==="none"){argvOptions["protocol"]="mraid"}}}argvOptions._cliExplicitKeys=cliExplicitKeys;return argvOptions};exports.allowedAdNetworks=allowedAdNetworks;exports.allowedAdProtocols=allowedAdProtocols;exports.mraidPartners=mraidPartners;exports.allowedLanguages=allowedLanguages;exports.allowedOrientations=allowedOrientations;
55
+ */
56
+ exports.parseArgvOptions = function parseArgvOptions(posiibleOptions) {
57
+ const argvOptions = {};
58
+ // Track which options were explicitly set via CLI args
59
+ const cliExplicitKeys = new Set();
60
+
61
+ for (let possibleOption of posiibleOptions) {
62
+ if (possibleOption.defaultValue !== undefined)
63
+ argvOptions[possibleOption.alias || possibleOption.name] = possibleOption.defaultValue;
64
+ }
65
+
66
+ let adNetwork = null;
67
+
68
+ for (let i = 2; i < process.argv.length; i++) {
69
+ let key = process.argv[i];
70
+ let inlineValue = null;
71
+
72
+ // 支持 --key=value 格式
73
+ if (key.includes('=')) {
74
+ const eqIndex = key.indexOf('=');
75
+ inlineValue = key.slice(eqIndex + 1);
76
+ key = key.slice(0, eqIndex);
77
+ }
78
+
79
+ const allowedOption = posiibleOptions.find((e) => '--' + e.name === key);
80
+
81
+ if (allowedOption) {
82
+ let value = true;
83
+ if (allowedOption.hasValue) {
84
+ // 优先使用内联值(--key=value),否则使用下一个参数
85
+ value = inlineValue !== null ? inlineValue : process.argv[++i];
86
+ if (allowedOption.parser) value = allowedOption.parser(value);
87
+ }
88
+ const optionKey = allowedOption.alias || allowedOption.name;
89
+ argvOptions[optionKey] = value;
90
+ cliExplicitKeys.add(optionKey);
91
+ } else if (!adNetwork) adNetwork = key;
92
+ }
93
+
94
+ if (posiibleOptions.find((e) => e.name === 'network')) {
95
+ if (adNetwork && allowedAdNetworks.includes(adNetwork)) {
96
+ argvOptions['network'] = adNetwork;
97
+ }
98
+ if (posiibleOptions.find((e) => e.name === 'protocol')) {
99
+ if (mraidPartners.includes(argvOptions['network']) && argvOptions['protocol'] === 'none') {
100
+ argvOptions['protocol'] = 'mraid';
101
+ }
102
+ }
103
+ }
104
+
105
+ argvOptions._cliExplicitKeys = cliExplicitKeys;
106
+ return argvOptions;
107
+ };
108
+
109
+ exports.allowedAdNetworks = allowedAdNetworks;
110
+ exports.allowedAdProtocols = allowedAdProtocols;
111
+ exports.mraidPartners = mraidPartners;
112
+ exports.allowedLanguages = allowedLanguages;
113
+ exports.allowedOrientations = allowedOrientations;
@@ -1,4 +1,4 @@
1
- "use strict";/**
1
+ /**
2
2
  * 解析 isChannelFold2Zip 相关标志
3
3
  *
4
4
  * isChannelFold2Zip:所有渠道(排除 preview)输出 dist/渠道名称.zip
@@ -10,5 +10,17 @@
10
10
  * @param {string} params.adNetwork - 当前渠道名称
11
11
  * @param {boolean} params.isZipOutput - 当前渠道是否本身就走 ZIP 输出流程
12
12
  * @returns {{ isChannelFold2Zip: boolean, isZipOutputFinal: boolean }}
13
- */function resolveChannelFold2Zip({isChannelFold2ZipOption,adNetwork,isZipOutput}){const isChannelFold2Zip=!!(isChannelFold2ZipOption&&adNetwork!=="preview");// 非 ZIP 渠道启用 isChannelFold2Zip 时,需要强制走 ZIP 输出流程
14
- const isZipOutputFinal=isZipOutput||isChannelFold2Zip;if(isChannelFold2Zip){console.log(`✅ isChannelFold2Zip enabled: output will be dist/${adNetwork}.zip`)}return{isChannelFold2Zip,isZipOutputFinal}}exports.resolveChannelFold2Zip=resolveChannelFold2Zip;
13
+ */
14
+ function resolveChannelFold2Zip({ isChannelFold2ZipOption, adNetwork, isZipOutput }) {
15
+ const isChannelFold2Zip = !!(isChannelFold2ZipOption && adNetwork !== 'preview');
16
+ // 非 ZIP 渠道启用 isChannelFold2Zip 时,需要强制走 ZIP 输出流程
17
+ const isZipOutputFinal = isZipOutput || isChannelFold2Zip;
18
+
19
+ if (isChannelFold2Zip) {
20
+ console.log(`✅ isChannelFold2Zip enabled: output will be dist/${adNetwork}.zip`);
21
+ }
22
+
23
+ return { isChannelFold2Zip, isZipOutputFinal };
24
+ }
25
+
26
+ exports.resolveChannelFold2Zip = resolveChannelFold2Zip;
@@ -1,4 +1,17 @@
1
- "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");const SCHEMA_BACKUP_SUFFIX=".backup";const SCHEMA_BACKUP_COMMENT="// [\u81EA\u52A8\u751F\u6210\u7684\u5907\u4EFD] \u6B64\u6587\u4EF6\u7531\u6253\u5305\u6D41\u7A0B\u521B\u5EFA\u3002\n"+"// \u5982\u679C\u6253\u5305\u4E2D\u65AD\uFF0C\u53EF\u4EE5\u79FB\u9664 \".backup\" \u540E\u7F00\uFF0C\n"+"// \u5E76\u7528\u6B64\u6587\u4EF6\u66FF\u6362\u539F\u59CB\u7684 \"theme.schema.json5\" \u6765\u6062\u590D\u3002\n";/**
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const JSON5 = require('json5');
6
+ const Ajv = require('ajv');
7
+
8
+ const SCHEMA_BACKUP_SUFFIX = '.backup';
9
+ const SCHEMA_BACKUP_COMMENT =
10
+ '// [自动生成的备份] 此文件由打包流程创建。\n' +
11
+ '// 如果打包中断,可以移除 ".backup" 后缀,\n' +
12
+ '// 并用此文件替换原始的 "theme.schema.json5" 来恢复。\n';
13
+
14
+ /**
2
15
  * 递归提取 JSON Schema 定义中的 `default` 值。
3
16
  *
4
17
  * - 对于 "object" 类型:递归遍历 `properties` 并构建嵌套对象。
@@ -6,15 +19,42 @@
6
19
  *
7
20
  * @param {object} schema - JSON Schema(或子 schema)对象
8
21
  * @returns {*} 提取的默认值树,如果没有则返回 undefined
9
- */function extractDefaultsFromSchema(schema){if(!schema||typeof schema!=="object")return undefined;if(schema.type==="object"&&schema.properties){const result={};for(const[key,propSchema]of Object.entries(schema.properties)){const val=extractDefaultsFromSchema(propSchema);if(val!==undefined){result[key]=val}}return Object.keys(result).length>0?result:undefined}// 基本类型或数组类型,带有默认值
10
- if("default"in schema){return schema.default}return undefined}/**
22
+ */
23
+ function extractDefaultsFromSchema(schema) {
24
+ if (!schema || typeof schema !== 'object') return undefined;
25
+
26
+ if (schema.type === 'object' && schema.properties) {
27
+ const result = {};
28
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
29
+ const val = extractDefaultsFromSchema(propSchema);
30
+ if (val !== undefined) {
31
+ result[key] = val;
32
+ }
33
+ }
34
+ return Object.keys(result).length > 0 ? result : undefined;
35
+ }
36
+
37
+ // 基本类型或数组类型,带有默认值
38
+ if ('default' in schema) {
39
+ return schema.default;
40
+ }
41
+
42
+ return undefined;
43
+ }
44
+
45
+ /**
11
46
  * Check whether the parsed content of theme.schema.json5 is a real JSON Schema
12
47
  * (has "type" and "properties" fields) or has already been overwritten with
13
48
  * plain merged data (e.g. due to a previous interrupted build).
14
49
  *
15
50
  * @param {object} obj - Parsed content of theme.schema.json5
16
51
  * @returns {boolean} true if it looks like a valid JSON Schema definition
17
- */function isJsonSchema(obj){return obj&&typeof obj==="object"&&obj.type==="object"&&typeof obj.properties==="object"}/**
52
+ */
53
+ function isJsonSchema(obj) {
54
+ return obj && typeof obj === 'object' && obj.type === 'object' && typeof obj.properties === 'object';
55
+ }
56
+
57
+ /**
18
58
  * 根据 schema 校验指定主题的主题数据。
19
59
  *
20
60
  * 项目中预期的文件结构:
@@ -35,39 +75,144 @@ if("default"in schema){return schema.default}return undefined}/**
35
75
  * mergedData – schema 默认值与主题数据合并后的完整数据
36
76
  * isPlainData – 如果 schema 文件已经是纯数据(非 schema 格式)则为 true
37
77
  * errors – 校验通过为 null,否则为错误信息数组
38
- */function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- 解析文件路径 ---
39
- const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- 读取并解析文件 ---
40
- if(!fs.existsSync(schemaPath)){return{defaultData:null,mergedData:null,isPlainData:false,errors:[`未找到 Schema 文件: ${schemaPath}`]}}const schemaContent=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));// Safety check: if the file is already plain data (not a JSON Schema),
41
- // it was likely left over from a previous interrupted build.
42
- // Use it directly without validation or overwriting.
43
- if(!isJsonSchema(schemaContent)){return{defaultData:schemaContent,mergedData:schemaContent,isPlainData:true,errors:null}}// schema 提取默认值
44
- const defaultData=extractDefaultsFromSchema(schemaContent)||{};let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}// 深度合并(主题数据覆盖 schema 默认值),然后校验
45
- const merged=deepMerge(defaultData,themeData);// --- 根据 schema 校验合并后的数据 ---
46
- const ajv=new Ajv({allErrors:true,strict:false});const validate=ajv.compile(schemaContent);const valid=validate(merged);if(!valid){const errors=validate.errors.map(err=>{const field=err.instancePath||"(root)";return`${field} ${err.message}`});return{defaultData,mergedData:merged,isPlainData:false,errors}}return{defaultData,mergedData:merged,isPlainData:false,errors:null}}/**
78
+ */
79
+ function validateThemeData(themeName, projectRoot, themeConfig) {
80
+ const sourceDir = (themeConfig && themeConfig.sourceDir) || 'src/theme';
81
+ const themeBaseDir = path.join(projectRoot, sourceDir);
82
+
83
+ // --- 解析文件路径 ---
84
+ const schemaPath = path.join(themeBaseDir, 'theme.schema.json5');
85
+ const dataPath = path.join(themeBaseDir, themeName, 'theme.data.json5');
86
+
87
+ // --- 读取并解析文件 ---
88
+ if (!fs.existsSync(schemaPath)) {
89
+ return {
90
+ defaultData: null,
91
+ mergedData: null,
92
+ isPlainData: false,
93
+ errors: [`未找到 Schema 文件: ${schemaPath}`],
94
+ };
95
+ }
96
+
97
+ const schemaContent = JSON5.parse(fs.readFileSync(schemaPath, 'utf8'));
98
+
99
+ // Safety check: if the file is already plain data (not a JSON Schema),
100
+ // it was likely left over from a previous interrupted build.
101
+ // Use it directly without validation or overwriting.
102
+ if (!isJsonSchema(schemaContent)) {
103
+ return {
104
+ defaultData: schemaContent,
105
+ mergedData: schemaContent,
106
+ isPlainData: true,
107
+ errors: null,
108
+ };
109
+ }
110
+
111
+ // 从 schema 提取默认值
112
+ const defaultData = extractDefaultsFromSchema(schemaContent) || {};
113
+
114
+ let themeData = {};
115
+ if (fs.existsSync(dataPath)) {
116
+ themeData = JSON5.parse(fs.readFileSync(dataPath, 'utf8'));
117
+ }
118
+
119
+ // 深度合并(主题数据覆盖 schema 默认值),然后校验
120
+ const merged = deepMerge(defaultData, themeData);
121
+
122
+ // --- 根据 schema 校验合并后的数据 ---
123
+ const ajv = new Ajv({ allErrors: true, strict: false });
124
+ const validate = ajv.compile(schemaContent);
125
+ const valid = validate(merged);
126
+
127
+ if (!valid) {
128
+ const errors = validate.errors.map(err => {
129
+ const field = err.instancePath || '(root)';
130
+ return `${field} ${err.message}`;
131
+ });
132
+ return { defaultData, mergedData: merged, isPlainData: false, errors };
133
+ }
134
+
135
+ return { defaultData, mergedData: merged, isPlainData: false, errors: null };
136
+ }
137
+
138
+ /**
47
139
  * 深度合并两个普通对象。`override` 中的值优先。
48
140
  * 数组会被整体替换(不会拼接)。
49
141
  *
50
142
  * @param {object} base
51
143
  * @param {object} override
52
144
  * @returns {object}
53
- */function deepMerge(base,override){const result={...base};for(const key of Object.keys(override)){const baseVal=base[key];const overVal=override[key];if(overVal!==null&&typeof overVal==="object"&&!Array.isArray(overVal)&&baseVal!==null&&typeof baseVal==="object"&&!Array.isArray(baseVal)){result[key]=deepMerge(baseVal,overVal)}else{result[key]=overVal}}return result}/**
145
+ */
146
+ function deepMerge(base, override) {
147
+ const result = { ...base };
148
+
149
+ for (const key of Object.keys(override)) {
150
+ const baseVal = base[key];
151
+ const overVal = override[key];
152
+
153
+ if (
154
+ overVal !== null &&
155
+ typeof overVal === 'object' &&
156
+ !Array.isArray(overVal) &&
157
+ baseVal !== null &&
158
+ typeof baseVal === 'object' &&
159
+ !Array.isArray(baseVal)
160
+ ) {
161
+ result[key] = deepMerge(baseVal, overVal);
162
+ } else {
163
+ result[key] = overVal;
164
+ }
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
54
171
  * 备份原始的 theme.schema.json5 文件。
55
172
  * 备份文件包含注释头,说明如何手动恢复。
56
173
  *
57
174
  * @param {string} schemaPath - theme.schema.json5 的绝对路径
58
175
  * @returns {string} 备份文件路径
59
- */function backupSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;const originalContent=fs.readFileSync(schemaPath,"utf8");fs.writeFileSync(backupPath,SCHEMA_BACKUP_COMMENT+originalContent,"utf8");return backupPath}/**
176
+ */
177
+ function backupSchema(schemaPath) {
178
+ const backupPath = schemaPath + SCHEMA_BACKUP_SUFFIX;
179
+ const originalContent = fs.readFileSync(schemaPath, 'utf8');
180
+ fs.writeFileSync(backupPath, SCHEMA_BACKUP_COMMENT + originalContent, 'utf8');
181
+ return backupPath;
182
+ }
183
+
184
+ /**
60
185
  * 从备份文件恢复 theme.schema.json5 并删除备份。
61
186
  *
62
187
  * @param {string} schemaPath - theme.schema.json5 的绝对路径
63
- */function restoreSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;if(!fs.existsSync(backupPath))return;const backupContent=fs.readFileSync(backupPath,"utf8");// 去除自动生成的注释头(以 "//" 开头的行)
64
- const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!line.startsWith("//"));const originalContent=lines.slice(contentStart>=0?contentStart:0).join("\n");fs.writeFileSync(schemaPath,originalContent,"utf8");fs.unlinkSync(backupPath)}/**
188
+ */
189
+ function restoreSchema(schemaPath) {
190
+ const backupPath = schemaPath + SCHEMA_BACKUP_SUFFIX;
191
+ if (!fs.existsSync(backupPath)) return;
192
+
193
+ const backupContent = fs.readFileSync(backupPath, 'utf8');
194
+ // 去除自动生成的注释头(以 "//" 开头的行)
195
+ const lines = backupContent.split('\n');
196
+ const contentStart = lines.findIndex(line => !line.startsWith('//'));
197
+ const originalContent = lines.slice(contentStart >= 0 ? contentStart : 0).join('\n');
198
+
199
+ fs.writeFileSync(schemaPath, originalContent, 'utf8');
200
+ fs.unlinkSync(backupPath);
201
+ }
202
+
203
+ /**
65
204
  * 用合并后的数据(schema 默认值 + 主题覆盖值)覆写 theme.schema.json5,
66
205
  * 以便打包工具在构建时可以直接使用。
67
206
  *
68
207
  * @param {string} schemaPath - theme.schema.json5 的绝对路径
69
208
  * @param {object} mergedData - 要写入的合并后的主题数据
70
- */function overwriteSchemaWithMerged(schemaPath,mergedData){const content=JSON.stringify(mergedData,null,2);fs.writeFileSync(schemaPath,content,"utf8")}/**
209
+ */
210
+ function overwriteSchemaWithMerged(schemaPath, mergedData) {
211
+ const content = JSON.stringify(mergedData, null, 2);
212
+ fs.writeFileSync(schemaPath, content, 'utf8');
213
+ }
214
+
215
+ /**
71
216
  * 执行主题校验并输出标准日志。
72
217
  * 单次构建(build.js)和批量构建(batch-build.js)共用此函数。
73
218
  *
@@ -88,10 +233,60 @@ const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!
88
233
  * valid – schema 文件不存在(跳过)或校验通过时为 true
89
234
  * errors – 校验通过为 null,否则为错误信息数组
90
235
  * schemaPath – theme.schema.json5 的绝对路径(用于 restoreSchema 调用),或 null
91
- */function runThemeValidation(themeName,projectRoot,themeConfig,options){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");// 没有 schema 文件 → 完全跳过校验
92
- if(!fs.existsSync(schemaPath)){return{valid:true,errors:null,schemaPath:null}}const logFn=options&&options.log||function defaultLog(msg,level){const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m",warn:"\x1B[33m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}${msg}${reset}`)};logFn(`校验主题数据: ${themeName}`,"info");const{defaultData,mergedData,isPlainData,errors}=validateThemeData(themeName,projectRoot,themeConfig);// Safety: schema file is already plain data (previous build was interrupted)
93
- // Use it directly, no need to backup/overwrite
94
- if(isPlainData){logFn(`⚠️ theme.schema.json5 已经是纯数据格式(可能上次构建中断未恢复),直接使用`,"warn");// schemaPath is returned as null so that restoreSchema won't be called
95
- // (there's nothing to restore — the file is already data, not a schema)
96
- return{valid:true,errors:null,schemaPath:null}}if(errors){logFn(`主题 "${themeName}" 数据校验失败:`,"error");errors.forEach(e=>logFn(` - ${e}`,"error"));return{valid:false,errors,schemaPath}}logFn(`主题 "${themeName}" 数据校验通过`,"success");// 备份原始 schema,然后用合并后的数据覆写以供构建使用
97
- backupSchema(schemaPath);overwriteSchemaWithMerged(schemaPath,mergedData);logFn(`已备份 schema 并写入合并后的主题数据`,"info");return{valid:true,errors:null,schemaPath}}module.exports={validateThemeData,deepMerge,extractDefaultsFromSchema,isJsonSchema,runThemeValidation,backupSchema,restoreSchema,overwriteSchemaWithMerged};
236
+ */
237
+ function runThemeValidation(themeName, projectRoot, themeConfig, options) {
238
+ const sourceDir = (themeConfig && themeConfig.sourceDir) || 'src/theme';
239
+ const themeBaseDir = path.join(projectRoot, sourceDir);
240
+ const schemaPath = path.join(themeBaseDir, 'theme.schema.json5');
241
+
242
+ // 没有 schema 文件 → 完全跳过校验
243
+ if (!fs.existsSync(schemaPath)) {
244
+ return { valid: true, errors: null, schemaPath: null };
245
+ }
246
+
247
+ const logFn = (options && options.log) || function defaultLog(msg, level) {
248
+ const colors = { info: '\x1b[36m', success: '\x1b[32m', error: '\x1b[31m', warn: '\x1b[33m' };
249
+ const reset = '\x1b[0m';
250
+ const color = colors[level] || '';
251
+ console.log(`${color}${msg}${reset}`);
252
+ };
253
+
254
+ logFn(`校验主题数据: ${themeName}`, 'info');
255
+
256
+ const { defaultData, mergedData, isPlainData, errors } = validateThemeData(themeName, projectRoot, themeConfig);
257
+
258
+ // Safety: schema file is already plain data (previous build was interrupted)
259
+ // Use it directly, no need to backup/overwrite
260
+ if (isPlainData) {
261
+ logFn(`⚠️ theme.schema.json5 已经是纯数据格式(可能上次构建中断未恢复),直接使用`, 'warn');
262
+ // schemaPath is returned as null so that restoreSchema won't be called
263
+ // (there's nothing to restore — the file is already data, not a schema)
264
+ return { valid: true, errors: null, schemaPath: null };
265
+ }
266
+
267
+ if (errors) {
268
+ logFn(`主题 "${themeName}" 数据校验失败:`, 'error');
269
+ errors.forEach(e => logFn(` - ${e}`, 'error'));
270
+ return { valid: false, errors, schemaPath };
271
+ }
272
+
273
+ logFn(`主题 "${themeName}" 数据校验通过`, 'success');
274
+
275
+ // 备份原始 schema,然后用合并后的数据覆写以供构建使用
276
+ backupSchema(schemaPath);
277
+ overwriteSchemaWithMerged(schemaPath, mergedData);
278
+ logFn(`已备份 schema 并写入合并后的主题数据`, 'info');
279
+
280
+ return { valid: true, errors: null, schemaPath };
281
+ }
282
+
283
+ module.exports = {
284
+ validateThemeData,
285
+ deepMerge,
286
+ extractDefaultsFromSchema,
287
+ isJsonSchema,
288
+ runThemeValidation,
289
+ backupSchema,
290
+ restoreSchema,
291
+ overwriteSchemaWithMerged,
292
+ };