176 lines
4.7 KiB
JavaScript
176 lines
4.7 KiB
JavaScript
// scripts/check-annotations.mjs
|
||
import fs from "node:fs/promises";
|
||
import path from "node:path";
|
||
import YAML from "yaml";
|
||
|
||
const CWD = process.cwd();
|
||
const ANNO_DIR = path.join(CWD, "src", "annotations");
|
||
const DIST_DIR = path.join(CWD, "dist");
|
||
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
|
||
|
||
async function exists(p) {
|
||
try { await fs.access(p); return true; } catch { return false; }
|
||
}
|
||
|
||
async function walk(dir) {
|
||
const out = [];
|
||
const ents = await fs.readdir(dir, { withFileTypes: true });
|
||
for (const e of ents) {
|
||
const p = path.join(dir, e.name);
|
||
if (e.isDirectory()) out.push(...(await walk(p)));
|
||
else out.push(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function escRe(s) {
|
||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
}
|
||
|
||
function inferPageKeyFromFile(fileAbs) {
|
||
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
|
||
return rel.replace(/\.(ya?ml|json)$/i, "");
|
||
}
|
||
|
||
function normalizePageKey(s) {
|
||
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
|
||
}
|
||
|
||
function isPlainObject(x) {
|
||
return !!x && typeof x === "object" && !Array.isArray(x);
|
||
}
|
||
|
||
async function loadAliases() {
|
||
if (!(await exists(ALIASES_PATH))) return {};
|
||
try {
|
||
const raw = await fs.readFile(ALIASES_PATH, "utf8");
|
||
const json = JSON.parse(raw);
|
||
return isPlainObject(json) ? json : {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function parseDoc(raw, fileAbs) {
|
||
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
|
||
return YAML.parse(raw);
|
||
}
|
||
|
||
function getAlias(aliases, pageKey, oldId) {
|
||
// supporte:
|
||
// 1) { "<pageKey>": { "<old>": "<new>" } }
|
||
// 2) { "<old>": "<new>" }
|
||
const k1 = String(pageKey || "");
|
||
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
|
||
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
|
||
if (a1) return String(a1);
|
||
const a2 = aliases?.[oldId];
|
||
if (a2) return String(a2);
|
||
return "";
|
||
}
|
||
|
||
async function main() {
|
||
if (!(await exists(ANNO_DIR))) {
|
||
console.log("✅ annotations: aucun dossier src/annotations — rien à vérifier.");
|
||
process.exit(0);
|
||
}
|
||
|
||
if (!(await exists(DIST_DIR))) {
|
||
console.error("FAIL: dist/ absent. Lance d’abord `npm run build` (ou `npm test`).");
|
||
process.exit(1);
|
||
}
|
||
|
||
const aliases = await loadAliases();
|
||
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
|
||
|
||
let pages = 0;
|
||
let checked = 0;
|
||
let failures = 0;
|
||
const notes = [];
|
||
|
||
for (const f of files) {
|
||
const rel = path.relative(CWD, f).replace(/\\/g, "/");
|
||
const raw = await fs.readFile(f, "utf8");
|
||
|
||
let doc;
|
||
try {
|
||
doc = parseDoc(raw, f);
|
||
} catch (e) {
|
||
failures++;
|
||
notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
|
||
continue;
|
||
}
|
||
|
||
if (!isPlainObject(doc) || doc.schema !== 1) {
|
||
failures++;
|
||
notes.push(`- INVALID: ${rel} (schema must be 1)`);
|
||
continue;
|
||
}
|
||
|
||
const pageKey = normalizePageKey(inferPageKeyFromFile(f));
|
||
|
||
if (doc.page != null && normalizePageKey(doc.page) !== pageKey) {
|
||
failures++;
|
||
notes.push(`- PAGE MISMATCH: ${rel} (page="${doc.page}" != path="${pageKey}")`);
|
||
continue;
|
||
}
|
||
|
||
if (!isPlainObject(doc.paras)) {
|
||
failures++;
|
||
notes.push(`- INVALID: ${rel} (missing object key "paras")`);
|
||
continue;
|
||
}
|
||
|
||
const distFile = path.join(DIST_DIR, pageKey, "index.html");
|
||
if (!(await exists(distFile))) {
|
||
failures++;
|
||
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
|
||
continue;
|
||
}
|
||
|
||
pages++;
|
||
const html = await fs.readFile(distFile, "utf8");
|
||
|
||
for (const paraId of Object.keys(doc.paras)) {
|
||
checked++;
|
||
|
||
if (!/^p-\d+-/i.test(paraId)) {
|
||
failures++;
|
||
notes.push(`- INVALID ID: ${rel} (${paraId})`);
|
||
continue;
|
||
}
|
||
|
||
const re = new RegExp(`\\bid=["']${escRe(paraId)}["']`, "g");
|
||
if (re.test(html)) continue;
|
||
|
||
const alias = getAlias(aliases, pageKey, paraId);
|
||
if (alias) {
|
||
const re2 = new RegExp(`\\bid=["']${escRe(alias)}["']`, "g");
|
||
if (re2.test(html)) {
|
||
notes.push(`- WARN alias used: ${pageKey} ${paraId} -> ${alias}`);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
failures++;
|
||
notes.push(`- MISSING ID: ${pageKey} (#${paraId})`);
|
||
}
|
||
}
|
||
|
||
const warns = notes.filter((x) => x.startsWith("- WARN"));
|
||
|
||
if (failures > 0) {
|
||
console.error(`FAIL: annotations invalid (pages=${pages} checked=${checked} failures=${failures})`);
|
||
for (const n of notes) console.error(n);
|
||
process.exit(1);
|
||
}
|
||
|
||
for (const w of warns) console.log(w);
|
||
console.log(`✅ annotations OK: pages=${pages} checked=${checked} warnings=${warns.length}`);
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error("FAIL: annotations check crashed:", e);
|
||
process.exit(1);
|
||
});
|