refactor: clean up gallery tooling and document the workflow
All checks were successful
Deploy on push / deploy (push) Has been skipped
All checks were successful
Deploy on push / deploy (push) Has been skipped
This commit is contained in:
151
scripts/check.js
Normal file
151
scripts/check.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { getEloAlignmentReport, loadEloData } = require("./lib/elo");
|
||||
const {
|
||||
fullsDir,
|
||||
loadMeals,
|
||||
repoRoot,
|
||||
thumbsDir,
|
||||
validateMealAssets,
|
||||
} = require("./lib/meals");
|
||||
|
||||
const indexPath = path.join(repoRoot, "index.html");
|
||||
const rankingsPath = path.join(repoRoot, "rankings.html");
|
||||
|
||||
function listJpgIds(directoryPath) {
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(directoryPath, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) => entry.isFile() && path.extname(entry.name).toLowerCase() === ".jpg"
|
||||
)
|
||||
.map((entry) => path.basename(entry.name, ".jpg"))
|
||||
.sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
|
||||
}
|
||||
|
||||
function getUnexpectedIds(directoryPath, expectedIds) {
|
||||
return listJpgIds(directoryPath).filter((id) => !expectedIds.has(id));
|
||||
}
|
||||
|
||||
function countMatches(text, pattern) {
|
||||
return (text.match(pattern) || []).length;
|
||||
}
|
||||
|
||||
function parseRankingsSeedData(rankingsHtml) {
|
||||
const match = rankingsHtml.match(
|
||||
/<script id="rankings-seed-data" type="application\/json">([\s\S]*?)<\/script>/
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
throw new Error("Generated rankings.html is missing embedded rankings seed data");
|
||||
}
|
||||
|
||||
return JSON.parse(match[1]);
|
||||
}
|
||||
|
||||
function validateGeneratedPages(meals, eloData) {
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
throw new Error("Generated index.html is missing; run npm run build");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rankingsPath)) {
|
||||
throw new Error("Generated rankings.html is missing; run npm run build");
|
||||
}
|
||||
|
||||
const indexHtml = fs.readFileSync(indexPath, "utf8");
|
||||
const rankingsHtml = fs.readFileSync(rankingsPath, "utf8");
|
||||
const galleryArticleCount = countMatches(indexHtml, /<article>/g);
|
||||
const rankingCardCount = countMatches(rankingsHtml, /class="ranking-card"/g);
|
||||
|
||||
if (galleryArticleCount !== meals.length) {
|
||||
throw new Error(
|
||||
`Generated index.html is out of sync: expected ${meals.length} gallery entries, found ${galleryArticleCount}`
|
||||
);
|
||||
}
|
||||
|
||||
if (rankingCardCount !== meals.length) {
|
||||
throw new Error(
|
||||
`Generated rankings.html is out of sync: expected ${meals.length} ranking cards, found ${rankingCardCount}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!rankingsHtml.includes('src="assets/js/rankings.js"')) {
|
||||
throw new Error("Generated rankings.html is missing the interactive rankings script");
|
||||
}
|
||||
|
||||
const seedData = parseRankingsSeedData(rankingsHtml);
|
||||
|
||||
if (!Array.isArray(seedData.meals) || seedData.meals.length !== meals.length) {
|
||||
throw new Error("Generated rankings.html has stale embedded meal seed data");
|
||||
}
|
||||
|
||||
if (
|
||||
!seedData.elo ||
|
||||
!Array.isArray(seedData.elo.entries) ||
|
||||
seedData.elo.entries.length !== eloData.entries.length
|
||||
) {
|
||||
throw new Error("Generated rankings.html has stale embedded Elo seed data");
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const meals = loadMeals();
|
||||
const eloData = loadEloData();
|
||||
const expectedIds = new Set(meals.map((meal) => meal.id));
|
||||
const alignment = getEloAlignmentReport(meals, eloData);
|
||||
|
||||
validateMealAssets(meals);
|
||||
|
||||
if (alignment.missingEntryIds.length > 0 || alignment.unexpectedEntryIds.length > 0) {
|
||||
const messages = [];
|
||||
|
||||
if (alignment.missingEntryIds.length > 0) {
|
||||
messages.push(
|
||||
`Missing Elo entries for meal ids: ${alignment.missingEntryIds.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (alignment.unexpectedEntryIds.length > 0) {
|
||||
messages.push(
|
||||
`Unexpected Elo entries with no meal: ${alignment.unexpectedEntryIds.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(messages.join("\n"));
|
||||
}
|
||||
|
||||
const unexpectedFulls = getUnexpectedIds(fullsDir, expectedIds);
|
||||
|
||||
if (unexpectedFulls.length > 0) {
|
||||
throw new Error(
|
||||
`Unexpected full-size image files with no meal entry: ${unexpectedFulls.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
const unexpectedThumbs = getUnexpectedIds(thumbsDir, expectedIds);
|
||||
|
||||
if (unexpectedThumbs.length > 0) {
|
||||
throw new Error(
|
||||
`Unexpected thumbnail files with no meal entry: ${unexpectedThumbs.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
validateGeneratedPages(meals, eloData);
|
||||
|
||||
console.log(
|
||||
`Validation passed: ${meals.length} meals, ${eloData.entries.length} Elo entries, generated pages and image assets are in sync.`
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user