@playcraft/devkit 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/batch-build.js +10 -6
- package/core/utils/validateThemeData.js +27 -8
- package/package.json +1 -1
package/core/batch-build.js
CHANGED
|
@@ -34,7 +34,8 @@ const parallel=Math.min(8,calculated);// 最多 8 个并发
|
|
|
34
34
|
return parallel}const DEFAULT_CONFIG={title:"Phaser Game",appName:"",// 用于从远程仓库拉取配置
|
|
35
35
|
channels:["google","facebook","tiktok","applovin","ironsource","unity","moloco","liftoff","bigoads"],themes:{enabled:true,sourceDir:"src/theme",entryFile:"src/theme/index.ts",whitelist:null},naming:{language:"EN",format:"\u65E0",supplier:"AIX",follower:"ZQL"},build:{outputDir:"dist-all",tempDirPrefix:"dist-temp",continueOnError:true,cleanTemp:true,parallel:getDefaultParallel()// 根据 CPU 核心数自动设置
|
|
36
36
|
}};// ==================== 工具函数 ====================
|
|
37
|
-
let isExiting=false;let backupContent=null;let backupPath=null;
|
|
37
|
+
let isExiting=false;let backupContent=null;let backupPath=null;let activeSchemaPath=null;// 跟踪当前活动的 schema 路径,用于中断时恢复
|
|
38
|
+
const activeTempDirs=new Set;// 跟踪所有活动的临时目录
|
|
38
39
|
function log(message,level="info"){const timestamp=new Date().toLocaleTimeString();const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m",warn:"\x1B[33m",debug:"\x1B[90m"};const reset="\x1B[0m";const prefix={info:"\uD83D\uDCE6",success:"\u2705",error:"\u274C",warn:"\u26A0\uFE0F",debug:"\uD83D\uDD0D"}[level]||"\u2139\uFE0F";console.log(`${colors[level]}[${timestamp}] ${prefix} ${message}${reset}`)}function getCurrentMonth(){const month=new Date().getMonth()+1;return`${month}月交付`}function getCurrentDate(){const date=new Date;const year=date.getFullYear();const month=String(date.getMonth()+1).padStart(2,"0");const day=String(date.getDate()).padStart(2,"0");return`${year}${month}${day}`}function defaultFileNameGenerator(config,themeName,channel){const parts=[themeName,config.naming.language,config.naming.format,getCurrentMonth(),getCurrentDate(),config.naming.supplier,config.naming.follower,channel.toLowerCase()].filter(item=>!!item);return parts.join("-")}function getAllThemes(config,projectRoot){const themesDir=path.join(projectRoot,config.themes.sourceDir);if(!fs.existsSync(themesDir)){log(`主题目录不存在: ${themesDir}`,"warn");return[]}const entries=fs.readdirSync(themesDir,{withFileTypes:true});let themes=entries.filter(entry=>entry.isDirectory()).map(entry=>entry.name).sort();if(config.themes.whitelist&&typeof config.themes.whitelist==="function"){themes=themes.filter(config.themes.whitelist);log(`应用主题白名单过滤: ${themes.length} 个主题`,"debug")}return themes}function backupEntryFile(config,projectRoot){const entryPath=path.join(projectRoot,config.themes.entryFile);if(!fs.existsSync(entryPath)){throw new Error(`入口文件不存在: ${entryPath}`)}backupPath=entryPath;backupContent=fs.readFileSync(entryPath,"utf8");log(`已备份入口文件: ${config.themes.entryFile}`,"debug")}function restoreEntryFile(){if(backupPath&&backupContent){fs.writeFileSync(backupPath,backupContent,"utf8");log(`已恢复入口文件`,"debug");backupPath=null;backupContent=null}}function switchTheme(config,themeName,projectRoot){const entryPath=path.join(projectRoot,config.themes.entryFile);// 使用配置文件中的 generateEntryFile 函数(如果提供)
|
|
39
40
|
let content;if(config.themes.switching&&typeof config.themes.switching.generateEntryFile==="function"){content=config.themes.switching.generateEntryFile(themeName)}else{// 兼容旧格式:简化的 export 语句
|
|
40
41
|
const themePath=`./${themeName}`;content=`export { default } from '${themePath}';\n`}fs.writeFileSync(entryPath,content,"utf8");log(`已切换主题: ${themeName}`,"debug")}/**
|
|
@@ -149,11 +150,12 @@ const outputDir=path.join(projectRoot,config.build.outputDir);const allResults=[
|
|
|
149
150
|
cleanTempDir(outputDir);// 获取临时凭证,用于所有主题的上传操作
|
|
150
151
|
let credentials=null;try{credentials=await fetchTemporaryCredentials();log("\u2713 \u83B7\u53D6\u4E34\u65F6\u51ED\u8BC1\u6210\u529F","success")}catch(error){log(`获取临时凭证失败: ${error.message},将跳过所有上传操作`,"warn")}for(const theme of themes){// 切换主题
|
|
151
152
|
if(config.themes.enabled){switchTheme(config,theme,projectRoot)}// Validate theme data and prepare schema (backup + overwrite with merged data)
|
|
152
|
-
const{valid,errors,schemaPath}=runThemeValidation(theme,projectRoot,config.themes,{log});
|
|
153
|
-
if(
|
|
153
|
+
const{valid,errors,schemaPath}=runThemeValidation(theme,projectRoot,config.themes,{log});activeSchemaPath=schemaPath;// Track for cleanup on interrupt
|
|
154
|
+
if(!valid){// Restore schema if backup was created
|
|
155
|
+
if(schemaPath)restoreSchema(schemaPath);activeSchemaPath=null;if(!config.build.continueOnError){throw new Error(`Theme data validation failed for "${theme}"`)}// Record failures for all channels of this theme and skip building
|
|
154
156
|
channels.forEach(channel=>{allResults.push({success:false,theme,channel,error:"Theme data validation failed"})});continue}// 并发构建该主题的所有渠道
|
|
155
157
|
let themeResults;try{themeResults=await buildChannelsInParallel(config,theme,channels,outputDir,projectRoot,isChannelFold2Zip)}finally{// Always restore schema after building this theme
|
|
156
|
-
if(schemaPath)restoreSchema(schemaPath)}allResults.push(...themeResults);// 上传 preview 渠道产物到 COS
|
|
158
|
+
if(schemaPath)restoreSchema(schemaPath);activeSchemaPath=null}allResults.push(...themeResults);// 上传 preview 渠道产物到 COS
|
|
157
159
|
const previewResult=themeResults.find(r=>r.channel==="preview"&&r.success);if(previewResult&&credentials){try{const previewDir=path.join(outputDir,theme,"preview");// 去掉 theme 这一级目录,直接使用 PathPrefix
|
|
158
160
|
const cosPrefix=config.PathPrefix||"";log(`开始上传 ${theme} 预览文件到 COS...`,"info");const uploadResults=await uploadDirectoryToCOS(null,// 不传递 cosConfig,使用传入的凭证
|
|
159
161
|
previewDir,cosPrefix,credentials// 传入凭证
|
|
@@ -168,6 +170,8 @@ const successBuilds=allResults.filter(r=>r.success).length;const failedBuilds=al
|
|
|
168
170
|
const duration=((Date.now()-startTime)/1000/60).toFixed(2);log("\n========================================");log("\uD83C\uDF89 \u6240\u6709\u6784\u5EFA\u4EFB\u52A1\u5B8C\u6210\uFF01","success");log(`✓ 成功构建: ${successBuilds}/${allResults.length}`);if(failedBuilds.length>0){log(`✗ 失败构建: ${failedBuilds.length}`,"warn");failedBuilds.forEach(({theme,channel,error})=>{log(` - ${theme}/${channel}: ${error}`,"error")})}log(`⏱️ 耗时: ${duration} 分钟`);log(`📁 输出目录: ${outputDir}`);// 打印预览链接
|
|
169
171
|
if(previewLinks.length>0){log("\n========================================");log("\uD83D\uDD17 \u9884\u89C8\u94FE\u63A5","info");log("========================================");// 按主题分组
|
|
170
172
|
const linksByTheme={};previewLinks.forEach(link=>{if(!linksByTheme[link.theme]){linksByTheme[link.theme]=[]}linksByTheme[link.theme].push(link)});// 打印链接
|
|
171
|
-
Object.keys(linksByTheme).sort().forEach(theme=>{log(`\n📦 ${theme}:`,"info");linksByTheme[theme].forEach(link=>{console.log(`${link.url}`)})});log("\n========================================")}log("========================================")}catch(error){log(`严重错误: ${error.message}`,"error");console.error(error)
|
|
172
|
-
|
|
173
|
+
Object.keys(linksByTheme).sort().forEach(theme=>{log(`\n📦 ${theme}:`,"info");linksByTheme[theme].forEach(link=>{console.log(`${link.url}`)})});log("\n========================================")}log("========================================")}catch(error){log(`严重错误: ${error.message}`,"error");console.error(error);// Restore schema if it was overwritten
|
|
174
|
+
if(activeSchemaPath){restoreSchema(activeSchemaPath);activeSchemaPath=null}restoreEntryFile();cleanAllTempDirs();process.exit(1)}}// ==================== 信号处理 ====================
|
|
175
|
+
function cleanup(){if(isExiting)return;isExiting=true;log("\n\u6536\u5230\u4E2D\u65AD\u4FE1\u53F7\uFF0C\u6B63\u5728\u6E05\u7406...","warn");// Restore schema if it was overwritten during theme validation
|
|
176
|
+
if(activeSchemaPath){try{restoreSchema(activeSchemaPath);log("\u5DF2\u6062\u590D theme.schema.json5","info")}catch(e){log(`恢复 theme.schema.json5 失败: ${e.message}`,"error")}activeSchemaPath=null}restoreEntryFile();cleanAllTempDirs();process.exit(1)}process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);// ==================== 导出 ====================
|
|
173
177
|
module.exports={buildBatch};
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* @returns {*} 提取的默认值树,如果没有则返回 undefined
|
|
9
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
10
|
if("default"in schema){return schema.default}return undefined}/**
|
|
11
|
+
* Check whether the parsed content of theme.schema.json5 is a real JSON Schema
|
|
12
|
+
* (has "type" and "properties" fields) or has already been overwritten with
|
|
13
|
+
* plain merged data (e.g. due to a previous interrupted build).
|
|
14
|
+
*
|
|
15
|
+
* @param {object} obj - Parsed content of theme.schema.json5
|
|
16
|
+
* @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"}/**
|
|
11
18
|
* 根据 schema 校验指定主题的主题数据。
|
|
12
19
|
*
|
|
13
20
|
* 项目中预期的文件结构:
|
|
@@ -17,18 +24,26 @@ if("default"in schema){return schema.default}return undefined}/**
|
|
|
17
24
|
* 通过 extractDefaultsFromSchema 从 schema 的 `default` 字段提取默认值,
|
|
18
25
|
* 然后与主题数据深度合并(主题数据优先)后再进行校验。
|
|
19
26
|
*
|
|
27
|
+
* 保险机制:如果 theme.schema.json5 已经是纯 JSON 数据(非 schema 格式),
|
|
28
|
+
* 说明上次构建中断后未能恢复,此时直接使用该数据,跳过校验。
|
|
29
|
+
*
|
|
20
30
|
* @param {string} themeName - 主题目录名称
|
|
21
31
|
* @param {string} projectRoot - 项目根目录的绝对路径
|
|
22
32
|
* @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
|
|
23
|
-
* @returns {{ defaultData: object, errors: string[] | null }}
|
|
24
|
-
* defaultData
|
|
25
|
-
*
|
|
33
|
+
* @returns {{ defaultData: object, mergedData: object, isPlainData: boolean, errors: string[] | null }}
|
|
34
|
+
* defaultData – 从 schema 提取的默认值
|
|
35
|
+
* mergedData – schema 默认值与主题数据合并后的完整数据
|
|
36
|
+
* isPlainData – 如果 schema 文件已经是纯数据(非 schema 格式)则为 true
|
|
37
|
+
* errors – 校验通过为 null,否则为错误信息数组
|
|
26
38
|
*/function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- 解析文件路径 ---
|
|
27
39
|
const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- 读取并解析文件 ---
|
|
28
|
-
if(!fs.existsSync(schemaPath)){return{defaultData:null,errors:[`未找到 Schema 文件: ${schemaPath}`]}}const
|
|
29
|
-
|
|
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 默认值),然后校验
|
|
30
45
|
const merged=deepMerge(defaultData,themeData);// --- 根据 schema 校验合并后的数据 ---
|
|
31
|
-
const ajv=new Ajv({allErrors:true,strict:false});const validate=ajv.compile(
|
|
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}}/**
|
|
32
47
|
* 深度合并两个普通对象。`override` 中的值优先。
|
|
33
48
|
* 数组会被整体替换(不会拼接)。
|
|
34
49
|
*
|
|
@@ -74,5 +89,9 @@ const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!
|
|
|
74
89
|
* errors – 校验通过为 null,否则为错误信息数组
|
|
75
90
|
* schemaPath – theme.schema.json5 的绝对路径(用于 restoreSchema 调用),或 null
|
|
76
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 文件 → 完全跳过校验
|
|
77
|
-
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"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}${msg}${reset}`)};logFn(`校验主题数据: ${themeName}`,"info");const{defaultData,errors}=validateThemeData(themeName,projectRoot,themeConfig)
|
|
78
|
-
|
|
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};
|