import fs from "fs"; import path from "path"; import { createRequire } from "module"; import { fileURLToPath } from "url"; const require = createRequire(import.meta.url); const Ajv = require("ajv"); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); const contentRoot = path.join(repoRoot, "content"); const schemaRoot = path.join(contentRoot, "schema"); const ajv = new Ajv({ allErrors: true, schemaId: "auto", jsonPointers: true, }); function normalizePath(filePath) { return filePath.split(path.sep).join("/"); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } function listFilesRecursive(dirPath) { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { files.push(...listFilesRecursive(fullPath)); } else { files.push(fullPath); } } return files; } const jobs = [ { schema: "abilities.schema.json", files: [path.join(contentRoot, "abilities.json")], }, { schema: "dialogues.schema.json", files: [path.join(contentRoot, "dialogues.json")], }, { schema: "factions.schema.json", files: [path.join(contentRoot, "factions.json")], }, { schema: "images.schema.json", files: [path.join(contentRoot, "images.json")], }, { schema: "items.schema.json", files: [path.join(contentRoot, "items.json")], }, { schema: "loot_tables.schema.json", files: [path.join(contentRoot, "loot_tables.json")], }, { schema: "monsters.schema.json", files: [path.join(contentRoot, "monsters.json")], }, { schema: "npc_templates.schema.json", files: [path.join(contentRoot, "npc_templates.json")], }, { schema: "quests.schema.json", files: [path.join(contentRoot, "quests.json")], }, { schema: "worlds.schema.json", files: [path.join(contentRoot, "worlds.json")], }, { schema: "world.schema.json", files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => path.basename(filePath) === "world.json"), }, { schema: "world-bookmarks.schema.json", files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => path.basename(filePath) === "bookmarks.json"), }, { schema: "world-chunk.schema.json", files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => /^-?\d+_-?\d+\.json$/i.test(path.basename(filePath))), }, { schema: "dev_config.schema.json", files: [path.join(contentRoot, "dev_config.json")], }, { schema: "npcs.schema.json", files: fs.existsSync(path.join(contentRoot, "npcs.json")) ? [path.join(contentRoot, "npcs.json")] : [], }, ]; let hasErrors = false; let validatedCount = 0; for (const job of jobs) { const schemaPath = path.join(schemaRoot, job.schema); const schema = readJson(schemaPath); let validate; try { validate = ajv.compile(schema); } catch (error) { hasErrors = true; console.error(`Schema compile failed: ${normalizePath(path.relative(repoRoot, schemaPath))}`); console.error(String(error)); continue; } for (const filePath of job.files) { const payload = readJson(filePath); const valid = validate(payload); validatedCount += 1; if (!valid) { hasErrors = true; console.error(`Validation failed: ${normalizePath(path.relative(repoRoot, filePath))}`); for (const error of validate.errors || []) { const instancePath = error.dataPath || "/"; console.error(` ${instancePath} ${error.message}`); } } } } if (hasErrors) { process.exitCode = 1; } else { console.log(`Validated ${validatedCount} content file(s) successfully.`); }