Files
Max-Cocos-Demo/extensions/max-studio/scripts/generate-configs.ts
2025-11-26 22:57:07 +08:00

662 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();