#!/usr/bin/env node import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const definitionsDir = join(repositoryRoot, "src", "content", "definitions"); const sitesDir = join(definitionsDir, "sites"); const outputFile = join(definitionsDir, "default.json"); const checkOnly = process.argv.includes("--check"); function readJson(file) { try { return JSON.parse(readFileSync(file, "utf8")); } catch (error) { throw new Error(`Cannot read valid JSON from ${file}: ${error.message}`); } } function requireString(value, field, file) { if (typeof value !== "string" || !value.trim()) throw new Error(`${file}: ${field} must be a non-empty string.`); } function validateDefinition(definition, file) { if (!definition || typeof definition !== "object" || Array.isArray(definition)) throw new Error(`${file}: a site definition must be an object.`); requireString(definition.id, "id", file); requireString(definition.name, "name", file); if (!Array.isArray(definition.urlPatterns) || definition.urlPatterns.length === 0) throw new Error(`${file}: urlPatterns must be a non-empty array.`); if (!Array.isArray(definition.requiredSelectors) || definition.requiredSelectors.length === 0) throw new Error(`${file}: requiredSelectors must be a non-empty array.`); if (!definition.post || typeof definition.post !== "object" || Array.isArray(definition.post)) throw new Error(`${file}: post must be an object.`); } const collection = readJson(join(definitionsDir, "collection.json")); if (!collection || typeof collection !== "object" || Array.isArray(collection)) throw new Error("collection.json must be an object."); if (collection.schemaVersion !== 1) throw new Error("collection.json: schemaVersion must be 1."); requireString(collection.collectionId, "collectionId", "collection.json"); requireString(collection.name, "name", "collection.json"); const definitions = readdirSync(sitesDir) .filter((file) => file.endsWith(".json")) .sort() .map((file) => { const definition = readJson(join(sitesDir, file)); validateDefinition(definition, join("sites", file)); return definition; }); if (definitions.length === 0) throw new Error("No site definition JSON files were found."); const ids = new Set(); for (const definition of definitions) { if (ids.has(definition.id)) throw new Error(`Duplicate definition ID: ${definition.id}.`); ids.add(definition.id); } const generated = `${JSON.stringify({ ...collection, definitions }, null, 2)}\n`; if (checkOnly) { let current; try { current = readFileSync(outputFile, "utf8"); } catch { throw new Error(`${basename(outputFile)} is missing. Run npm run definitions:generate.`); } if (current !== generated) throw new Error(`${basename(outputFile)} is out of date. Run npm run definitions:generate and commit the result.`); console.log("Definition collection is up to date."); } else { writeFileSync(outputFile, generated); console.log(`Generated ${basename(outputFile)} from ${definitions.length} site definition(s).`); }