fix: 更新提交

This commit is contained in:
han_han9
2025-11-26 22:57:07 +08:00
parent 8a6620cf8f
commit 4c16bec13f
640 changed files with 70914 additions and 13327 deletions

View File

@@ -0,0 +1,661 @@
/**
* Excel 配置生成脚本(增强版)
*
* 功能:
* - 将 /excels 目录下的 .xlsx 文件转换为 JSONassets/configs/generated/data
* - 依据表头(字段:类型 / 可选? / 默认=值 / //备注)生成 TS 数据类与解析函数assets/configs/generated/core
* - 从 __enums__.xlsx 生成 Enums.ts从 __beans__.xlsx 生成 Beans.ts 与解析函数
* - 生成 Manager.ts缺失时支持唯一索引 getByXxx
* - 生成时进行唯一约束校验与跨表引用校验
*
* 使用:
* - 在 extensions/max-studio/package.json 中暴露脚本:"gen:configs"
* - 运行npm run gen:configs
*/
import path from "path";
import fs from "fs";
import { ensureDirSync, writeFileSync, existsSync } from "fs-extra";
import xlsx from "xlsx";
type PrimitiveType = "number" | "string" | "boolean";
type CcType = "Color" | "Vec2" | "Vec3" | "Vec4" | "Size" | "Rect" | "Quat" | "Mat4";
interface FieldDef {
name: string;
type: string; // number|string|boolean|CcType|Enum|Bean
// 原始类型标识,用于从 Excel 表头保留 `int` / `float` 信息
rawType?: string;
isArray: boolean;
optional: boolean;
defaultValue?: string;
comment?: string;
unique?: boolean;
refTarget?: string; // 跨表引用目标(表名)
}
interface TableDef {
name: string; // e.g. PetConfig
fields: FieldDef[];
}
interface EnumItem {
name: string;
value: number;
comment?: string;
}
interface EnumDef {
name: string;
items: EnumItem[];
}
interface BeanDef {
name: string;
fields: FieldDef[];
}
const PROJECT_ROOT = path.resolve(__dirname, "../../..");
const EXCEL_DIR = path.join(PROJECT_ROOT, "excels");
const OUTPUT_CORE_DIR = path.join(PROJECT_ROOT, "assets/configs/generated/core");
const OUTPUT_DATA_DIR = path.join(PROJECT_ROOT, "assets/configs/generated/data");
const CC_COMPLEX_TYPES: ReadonlySet<string> = new Set([
"Color",
"Vec2",
"Vec3",
"Vec4",
"Size",
"Rect",
"Quat",
"Mat4",
]);
function log(...args: any[]) {
// 统一日志输出
console.log("[gen-configs]", ...args);
}
function parseHeaderCell(cell: string): FieldDef | null {
const raw = cell?.trim();
if (!raw) return null;
const [left, commentRaw] = raw.split("//");
const comment = commentRaw?.trim();
const [namePartRaw, typePartRaw = ""] = left.split(":").map((v) => v.trim());
const optional = /\?$/.test(namePartRaw);
const name = namePartRaw.replace(/\?$/, "");
const [typeIdentRaw, defaultRaw] = typePartRaw.split("=").map((v) => v.trim());
let typeIdent = typeIdentRaw || (name === "id" ? "number" : "string");
const isArray = /\[\]$/.test(typeIdent);
if (isArray) typeIdent = typeIdent.replace(/\[\]$/, "");
let refTarget: string | undefined;
if (/^Ref:/i.test(typeIdent)) {
refTarget = typeIdent.replace(/^Ref:/i, "");
typeIdent = "number"; // 引用用目标 id数值
}
// 规范 Cocos 原生类型:去掉 "cc." 前缀
if (/^cc\./i.test(typeIdent)) {
typeIdent = typeIdent.replace(/^cc\./i, "");
}
// 记录原始数值类型,并将 int/float 规范化为 TS 的 number
const rawType = /^(int|float)$/i.test(typeIdent) ? typeIdent.toLowerCase() : undefined;
if (rawType === "int" || rawType === "float") {
typeIdent = "number";
}
const unique = /\[\s*unique\s*\]/i.test(comment || "");
const refMatch = (comment || "").match(/\[\s*ref\s*=\s*([A-Za-z0-9_]+)\s*\]/i);
if (refMatch) refTarget = refMatch[1];
const def: FieldDef = {
name,
type: typeIdent,
rawType,
isArray,
optional,
defaultValue: defaultRaw || undefined,
comment,
unique,
refTarget,
};
return def;
}
function readWorkbookTables(filePath: string): { table: TableDef; rows: any[] } {
const workbook = xlsx.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
// 读取为二维数组,支持三行表头:第一行字段名、第二行类型、第三行备注
const rows2d = xlsx.utils.sheet_to_json(sheet, { header: 1, defval: "" }) as any[][];
const headerNames = (rows2d[0] || []).map((v) => String(v ?? "").trim());
const headerTypes = (rows2d[1] || []).map((v) => String(v ?? "").trim());
const headerComments = (rows2d[2] || []).map((v) => String(v ?? "").trim());
const fields: FieldDef[] = [];
for (let i = 0; i < headerNames.length; i++) {
const nameRaw = headerNames[i];
if (!nameRaw) continue;
const typeSpecRaw = headerTypes[i] || "";
const commentRaw = headerComments[i] || "";
// 解析类型行:支持 Ref:Table、数组[]、默认值=xxx、可选?(写在类型末尾)
let typeIdent = typeSpecRaw;
let defaultValue: string | undefined;
const optional = /\?$/.test(typeIdent);
if (optional) typeIdent = typeIdent.replace(/\?$/, "");
const isArray = /\[\]$/.test(typeIdent);
if (isArray) typeIdent = typeIdent.replace(/\[\]$/, "");
const parts = typeIdent.split("=");
if (parts.length > 1) {
typeIdent = parts[0].trim();
defaultValue = parts.slice(1).join("=").trim();
}
let refTarget: string | undefined;
if (/^Ref:/i.test(typeIdent)) {
refTarget = typeIdent.replace(/^Ref:/i, "");
typeIdent = "number"; // 引用以目标 id数值表达
}
// 规范 Cocos 原生类型:去掉 "cc." 前缀
if (/^cc\./i.test(typeIdent)) {
typeIdent = typeIdent.replace(/^cc\./i, "");
}
// 记录原始数值类型,并将 int/float 规范化为 TS 的 number
const rawType = /^(int|float)$/i.test(typeIdent) ? typeIdent.toLowerCase() : undefined;
if (rawType === "int" || rawType === "float") {
typeIdent = "number";
}
const unique = /\[\s*unique\s*\]/i.test(commentRaw);
const refMatch = commentRaw.match(/\[\s*ref\s*=\s*([A-Za-z0-9_]+)\s*\]/i);
if (refMatch) refTarget = refMatch[1];
const name = String(nameRaw).replace(/\?$/, "");
const type = typeIdent || (name === "id" ? "number" : "string");
fields.push({
name,
type,
rawType,
isArray,
optional,
defaultValue,
comment: commentRaw || undefined,
unique,
refTarget,
});
}
// 将第 4 行开始的数据行转换为对象数组(按第一行字段名映射)
const dataRows: any[] = [];
for (let r = 3; r < rows2d.length; r++) {
const rowArr = rows2d[r] || [];
// 空行跳过
if (rowArr.every((v: any) => String(v ?? "").trim() === "")) continue;
const obj: Record<string, any> = {};
for (let c = 0; c < headerNames.length; c++) {
const key = headerNames[c];
if (!key) continue;
obj[key.replace(/\?$/, "")] = rowArr[c];
}
dataRows.push(obj);
}
const tableName = path.basename(filePath, path.extname(filePath));
return { table: { name: tableName, fields }, rows: dataRows };
}
function writeFileSafe(filePath: string, content: string) {
ensureDirSync(path.dirname(filePath));
writeFileSync(filePath, content, { encoding: "utf-8" });
}
function generateEnumsTs(enums: EnumDef[]): string {
const parts: string[] = [];
parts.push("/** 自动生成的枚举定义 */");
for (const e of enums) {
parts.push(`export enum ${e.name} {`);
for (const item of e.items) {
const comment = item.comment ? ` // ${item.comment}` : "";
parts.push(` ${item.name} = ${item.value},${comment}`);
}
parts.push("}\n");
}
return parts.join("\n");
}
function readEnumsExcel(absPath: string): EnumDef[] {
const workbook = xlsx.readFile(absPath);
const results: EnumDef[] = [];
for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName];
const rows: any[] = xlsx.utils.sheet_to_json(sheet, { defval: "" });
// 检测是否为分组格式:包含 EnumName/ValueName/Value/Comment 列
const hasGrouped =
rows.length > 0 &&
Object.keys(rows[0]).some((k) => /^(EnumName|enumName)$/i.test(String(k))) &&
Object.keys(rows[0]).some((k) => /^(ValueName|valueName|Name|name)$/i.test(String(k))) &&
Object.keys(rows[0]).some((k) => /^(Value|value)$/i.test(String(k)));
if (hasGrouped) {
const enumMap = new Map<string, EnumItem[]>();
let currentEnum = "";
for (const row of rows) {
const enumNameRaw = String(row.EnumName ?? row.enumName ?? "").trim();
if (enumNameRaw) currentEnum = enumNameRaw;
if (!currentEnum) continue;
const valueName = String(row.ValueName ?? row.valueName ?? row.Name ?? row.name ?? "").trim();
if (!valueName) continue;
const valueRaw = row.Value ?? row.value ?? 0;
const value = Number(valueRaw);
const comment = String(row.Comment ?? row.comment ?? "").trim();
if (!enumMap.has(currentEnum)) enumMap.set(currentEnum, []);
enumMap.get(currentEnum)!.push({ name: valueName, value, comment });
}
for (const [name, items] of enumMap.entries()) {
results.push({ name, items });
}
} else {
// 兼容每个 Sheet 一个枚举:使用 Name/Value/Comment
const items: EnumItem[] = [];
for (const row of rows) {
const name = String(row.ValueName ?? row.valueName ?? row.Name ?? row.name ?? "").trim();
if (!name) continue;
const valueRaw = row.Value ?? row.value ?? 0;
const value = Number(valueRaw);
const comment = String(row.Comment ?? row.comment ?? "").trim();
items.push({ name, value, comment });
}
results.push({ name: sheetName, items });
}
}
return results;
}
function generateBeansTs(beans: BeanDef[]): string {
const usedCcTypes = new Set<string>();
for (const b of beans) {
for (const f of b.fields) {
const base = f.type;
if (CC_COMPLEX_TYPES.has(base)) usedCcTypes.add(base);
}
}
const ccImports = usedCcTypes.size > 0 ? `import { ${Array.from(usedCcTypes).join(", ")} } from 'cc';\n\n` : "";
const parts: string[] = [];
parts.push(`${ccImports}import { ConfigParseUtils } from './ConfigParseUtils';`);
for (const b of beans) {
parts.push(`\n/** Bean: ${b.name} */`);
const ifaceLines: string[] = [];
for (const f of b.fields) {
const tsType = `${f.type}${f.isArray ? "[]" : ""}`;
ifaceLines.push(` ${f.name}${f.optional ? "?" : ""}: ${tsType};`);
}
parts.push(`export interface ${b.name} {\n${ifaceLines.join("\n")}\n}`);
// 解析函数
const parseExprs: string[] = [];
for (const f of b.fields) {
const expr = buildParseExpression(f, new Set<string>(), new Set<string>(), true);
parseExprs.push(` ${f.name}: ${expr},`);
}
parts.push(
`export function parse${b.name}(data: any): ${b.name} {\n return {\n${parseExprs.join("\n")}\n };\n}`,
);
}
return parts.join("\n");
}
function readBeansExcel(absPath: string): BeanDef[] {
const workbook = xlsx.readFile(absPath);
const results: BeanDef[] = [];
for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName];
const headerRow = xlsx.utils.sheet_to_json(sheet, { header: 1 })[0] as string[];
const fields: FieldDef[] = [];
for (const cell of headerRow) {
const def = parseHeaderCell(String(cell || ""));
if (def) fields.push(def);
}
results.push({ name: sheetName, fields });
}
return results;
}
function isPrimitive(t: string): t is PrimitiveType {
return t === "number" || t === "string" || t === "boolean";
}
function isCcType(t: string): t is CcType {
return CC_COMPLEX_TYPES.has(t);
}
function buildParseExpression(
field: FieldDef,
enumNames: ReadonlySet<string>,
beanNames: ReadonlySet<string>,
inBean = false,
): string {
const source = inBean ? `data.${field.name}` : `data.${field.name}`;
const base = field.type;
if (!field.isArray) {
if (isPrimitive(base)) {
if (base === "number") {
// 根据原始类型选择解析函数int -> parseIntfloat -> parseFloat
return field.rawType === "float"
? `ConfigParseUtils.parseFloat(${source})`
: `ConfigParseUtils.parseInt(${source})`;
}
if (base === "boolean") return `ConfigParseUtils.parseBoolean(${source})`;
return `ConfigParseUtils.parseString(${source})`;
}
if (isCcType(base)) {
const fn =
base === "Color"
? "parseColor"
: base === "Vec2"
? "parseVec2"
: base === "Vec3"
? "parseVec3"
: base === "Vec4"
? "parseVec4"
: base === "Size"
? "parseSize"
: base === "Rect"
? "parseRect"
: base === "Quat"
? "parseQuat"
: base === "Mat4"
? "parseMat4"
: "parseString";
return `ConfigParseUtils.${fn}(${source})`;
}
if (enumNames.has(base)) {
// 支持 name 或 number两者均可
return `(typeof ${source} === 'number' ? ${source} : ((${"Enums"}).${base}[String(${source}) as keyof typeof ${"Enums"}.${base}] ?? 0))`;
}
if (beanNames.has(base)) {
return `parse${base}(${source})`;
}
// 未知类型按字符串解析
return `ConfigParseUtils.parseString(${source})`;
} else {
// 数组解析
if (isPrimitive(base)) {
if (base === "number") return `ConfigParseUtils.parseNumberArray(${source})`;
if (base === "string") return `ConfigParseUtils.parseStringArray(${source})`;
// boolean[]:先按字符串数组再逐项转布尔
return `ConfigParseUtils.parseStringArray(${source}).map((v) => ConfigParseUtils.parseBoolean(v))`;
}
if (isCcType(base)) {
const fn =
base === "Color"
? "parseColor"
: base === "Vec2"
? "parseVec2"
: base === "Vec3"
? "parseVec3"
: base === "Vec4"
? "parseVec4"
: base === "Size"
? "parseSize"
: base === "Rect"
? "parseRect"
: base === "Quat"
? "parseQuat"
: base === "Mat4"
? "parseMat4"
: "parseString";
// 使用 ';' 分隔元素,每个元素用解析函数处理
return `String(${source}).split(';').filter(Boolean).map((chunk) => ConfigParseUtils.${fn}(chunk))`;
}
if (enumNames.has(base)) {
return `ConfigParseUtils.parseStringArray(${source}).map((v) => (isNaN(Number(v)) ? ((${"Enums"}).${base}[String(v) as keyof typeof ${"Enums"}.${base}] ?? 0) : Number(v)))`;
}
if (beanNames.has(base)) {
return `String(${source}).split(';').filter(Boolean).map((json) => parse${base}(JSON.parse(json)))`;
}
return `ConfigParseUtils.parseStringArray(${source})`;
}
}
function generateDataJson(table: TableDef, rows: any[]): string {
return JSON.stringify(rows, null, 2);
}
function generateDataTs(table: TableDef, enumNames: ReadonlySet<string>, beanNames: ReadonlySet<string>): string {
const usedCcTypes = new Set<string>();
const usedEnums = new Set<string>();
const usedBeans = new Set<string>();
for (const f of table.fields) {
const base = f.type;
if (isCcType(base)) usedCcTypes.add(base);
if (enumNames.has(base)) usedEnums.add(base);
if (beanNames.has(base)) usedBeans.add(base);
}
const importLines: string[] = [];
if (usedCcTypes.size > 0) {
importLines.push(`import { ${Array.from(usedCcTypes).join(", ")} } from "cc";`);
}
if (usedEnums.size > 0) {
importLines.push(`import * as Enums from "./Enums";`);
}
if (usedBeans.size > 0) {
importLines.push(
`import { ${Array.from(usedBeans)
.map((b) => `${b}, parse${b}`)
.join(", ")} } from "./Beans";`,
);
}
importLines.push(`import { ConfigParseUtils } from "./ConfigParseUtils";`);
const imports = `${importLines.join("\n")}\n\n`;
const className = `${table.name}Data`;
const fieldsPrivate = table.fields
.map((f) => {
let value = `\n private _${f.name}: ${f.type}${f.isArray ? "[]" : ""};`;
value += `\n public get ${f.name}(): ${f.type}${f.isArray ? "[]" : ""} {`;
value += `\n return this._${f.name};`;
value += `\n }`;
return value;
})
.join("\n");
const getters = table.fields
.map((f) => {
const title = f.comment ? `${f.comment}` : f.name;
const tsType = `${f.type}${f.isArray ? "[]" : ""}`;
const wrap = isCcType(f.type)
? `ConfigParseUtils.createReadonlyProxy(ConfigParseUtils.deepFreeze(this._${f.name}), '${title}')`
: `this._${f.name}`;
return `\n\n /** ${title} */\n public get ${f.name}(): ${tsType} {\n return ${wrap};\n }`;
})
.join("\n");
const ctorParams = table.fields.map((f) => `${f.name}: ${f.type}${f.isArray ? "[]" : ""}`).join(",\n ");
const ctorAssigns = table.fields.map((f) => `\n this._${f.name} = ${f.name};`).join("");
const parseArgs = table.fields.map((f) => `\n ${buildParseExpression(f, enumNames, beanNames)}`).join(",\n");
const file = `${imports}/**\n * ${
table.name
}数据结构\n */\nexport class ${className} {\n${fieldsPrivate}\n\n public constructor(\n${
" " + ctorParams
}\n ) {${ctorAssigns}\n }\n}\n\n/**\n * 解析配置数据\n */\nexport function parse${
table.name
}Data(data: any): ${className} {\n return new ${className}(${parseArgs}\n );\n}\n`;
return file;
}
function generateManagerTs(table: TableDef): string {
const name = table.name;
const uniqueFields = table.fields.filter(
(f) => f.unique && !f.isArray && (isPrimitive(f.type) || isCcType(f.type) === false),
);
const idxDecls = uniqueFields
.map((f) => ` private by_${f.name} = new Map<${f.type}, ${name}Data>();`)
.join("\n");
const idxAssigns = uniqueFields
.map((f) => ` this.by_${f.name}.set((config as any).${f.name}, config);`)
.join("\n");
const idxGetters = uniqueFields
.map(
(f) =>
`\n public getBy${f.name.charAt(0).toUpperCase() + f.name.slice(1)}(key: ${
f.type
}): ${name}Data | null {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return null;\n }\n return this.by_${
f.name
}.get(key) || null;\n }\n`,
)
.join("");
return `import { JsonAsset } from "cc";\n\nimport { singleton, Singleton } from "@max-studio/core/Singleton";\nimport LogUtils from "@max-studio/core/utils/LogUtils";\n\nimport { ConfigParseUtils } from "./ConfigParseUtils";\nimport { ${name}Data, parse${name}Data } from "./${name}Data";\nimport ResManager from "@max-studio/core/res/ResManager";\n\n/**\n * ${name}配置管理器\n *\n * ⚠️ 此文件由配置表生成器自动生成,请勿手动修改!\n * 如需修改请编辑对应的Excel配置文件然后重新生成\n */\n@singleton({ auto: true })\nexport class ${name}Manager extends Singleton {\n private configList: readonly ${name}Data[] = [];\n private configMap = new Map<number, ${name}Data>();\n ${idxDecls}\n private isLoaded = false;\n\n protected async onInit(): Promise<void> {\n await this.loadConfig();\n }\n\n private async loadConfig(): Promise<void> {\n try {\n const {err, asset} = await ResManager.getInstance().loadAsset<JsonAsset>({\n path: "generated/data/${name}",\n type: JsonAsset,\n bundle: "configs",\n });\n this.parseConfig(<any>asset.json);\n this.isLoaded = true;\n } catch (err) {\n LogUtils.error("${name}Manager", "加载 ${name} 配置失败:", err);\n }\n }\n\n private parseConfig(data: any[]): void {\n this.configList = Object.freeze(\n data.map((item) => {\n const config = parse${name}Data(item);\n const frozenConfig = ConfigParseUtils.deepFreeze(config);\n return ConfigParseUtils.createReadonlyProxy(frozenConfig, "${name}Data配置");\n }),\n );\n this.configMap.clear();\n ${
uniqueFields.length > 0 ? uniqueFields.map((f) => `this.by_${f.name}.clear();`).join(" ") : ""
}\n for (const config of this.configList) {\n this.configMap.set((config as any).id, config);\n${
idxAssigns ? ` ${idxAssigns}\n` : ""
} }\n ConfigParseUtils.deepFreeze(this.configMap);\n }\n\n public getConfig(id: number): ${name}Data | null {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return null;\n }\n return this.configMap.get(id) || null;\n }\n\n public getAllConfigs(): ${name}Data[] {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return [];\n }\n return [...this.configList];\n }\n\n public isConfigLoaded(): boolean {\n return this.isLoaded;\n }\n ${idxGetters}
}
`;
}
function validateUnique(table: TableDef, rows: any[]): void {
for (const f of table.fields) {
if (!f.unique) continue;
const seen = new Set<string>();
let dupCount = 0;
for (let i = 0; i < rows.length; i++) {
const v = String(rows[i][f.name] ?? "");
if (!v) continue;
if (seen.has(v)) {
dupCount++;
log(`⚠️ [唯一约束] ${table.name}.${f.name} 在第 ${i + 2} 行出现重复值: ${v}`);
} else {
seen.add(v);
}
}
if (dupCount === 0) {
log(`✅ [唯一约束] ${table.name}.${f.name} 校验通过`);
}
}
}
function validateRefsAll(tables: { table: TableDef; rows: any[] }[]): void {
const idMap = new Map<string, Set<string>>();
for (const { table, rows } of tables) {
const ids = new Set<string>();
for (const r of rows) {
const id = String(r.id ?? "");
if (id) ids.add(id);
}
idMap.set(table.name, ids);
}
for (const { table, rows } of tables) {
for (const f of table.fields) {
if (!f.refTarget) continue;
const targetIds = idMap.get(f.refTarget);
if (!targetIds) {
log(`⚠️ [引用校验] ${table.name}.${f.name} 指向的表 ${f.refTarget} 未在本次生成中发现`);
continue;
}
for (let i = 0; i < rows.length; i++) {
const v = String(rows[i][f.name] ?? "");
if (!v) continue;
if (!targetIds.has(v)) {
log(`⚠️ [引用校验] ${table.name}.${f.name}${i + 2} 行引用不存在的 ${f.refTarget}.id=${v}`);
}
}
}
}
}
function genForExcel(file: string, enumNames: ReadonlySet<string>, beanNames: ReadonlySet<string>) {
const absPath = path.join(EXCEL_DIR, file);
log("处理 Excel:", absPath);
const { table, rows } = readWorkbookTables(absPath);
validateUnique(table, rows);
const jsonOut = path.join(OUTPUT_DATA_DIR, `${table.name}.json`);
writeFileSafe(jsonOut, generateDataJson(table, rows));
log("生成 JSON:", jsonOut);
const dataTsOut = path.join(OUTPUT_CORE_DIR, `${table.name}Data.ts`);
writeFileSafe(dataTsOut, generateDataTs(table, enumNames, beanNames));
log("生成 TS 数据类:", dataTsOut);
const mgrTsOut = path.join(OUTPUT_CORE_DIR, `${table.name}Manager.ts`);
if (!existsSync(mgrTsOut)) {
writeFileSafe(mgrTsOut, generateManagerTs(table));
log("生成 TS 管理器:", mgrTsOut);
} else {
log("检测到已有管理器,跳过生成:", mgrTsOut);
}
}
function main() {
try {
if (!existsSync(EXCEL_DIR)) {
throw new Error(`未找到 Excel 目录: ${EXCEL_DIR}`);
}
const files = fs.readdirSync(EXCEL_DIR).filter((f) => f.endsWith(".xlsx"));
if (files.length === 0) {
log("excels 目录下没有 .xlsx 文件,无需生成");
return;
}
const enumsFile = files.find((f) => f === "__enums__.xlsx");
const beansFile = files.find((f) => f === "__beans__.xlsx");
const tableFiles = files.filter((f) => f !== "__enums__.xlsx" && f !== "__beans__.xlsx");
// 1) 生成枚举
const enumDefs = enumsFile ? readEnumsExcel(path.join(EXCEL_DIR, enumsFile)) : [];
const enumNames = new Set<string>(enumDefs.map((e) => e.name));
if (enumDefs.length > 0) {
const enumsOut = path.join(OUTPUT_CORE_DIR, "Enums.ts");
writeFileSafe(enumsOut, generateEnumsTs(enumDefs));
log("生成枚举:", enumsOut);
}
// 2) 生成 Beans
const beanDefs = beansFile ? readBeansExcel(path.join(EXCEL_DIR, beansFile)) : [];
const beanNames = new Set<string>(beanDefs.map((b) => b.name));
if (beanDefs.length > 0) {
const beansOut = path.join(OUTPUT_CORE_DIR, "Beans.ts");
writeFileSafe(beansOut, generateBeansTs(beanDefs));
log("生成 Beans:", beansOut);
}
// 3) 读取所有表用于跨表引用校验
const parsedTables: { table: TableDef; rows: any[] }[] = [];
for (const f of tableFiles) {
const absPath = path.join(EXCEL_DIR, f);
const { table, rows } = readWorkbookTables(absPath);
parsedTables.push({ table, rows });
}
validateRefsAll(parsedTables);
// 4) 为每个表生成产物
for (const f of tableFiles) {
genForExcel(f, enumNames, beanNames);
}
log("配置生成完成 ✅");
} catch (err) {
console.error("[gen-configs] 生成失败:", err);
process.exitCode = 1;
}
}
main();