import type { APIRoute } from "astro"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { parse as parseYAML } from "yaml"; const CWD = process.cwd(); const ANNO_DIR = path.join(CWD, "src", "annotations"); // Strict en CI (ou override explicite) const STRICT = process.env.ANNOTATIONS_STRICT === "1" || process.env.CI === "1" || process.env.CI === "true"; async function exists(p: string): Promise { try { await fs.access(p); return true; } catch { return false; } } async function walk(dir: string): Promise { const out: string[] = []; 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 isPlainObject(x: unknown): x is Record { return !!x && typeof x === "object" && !Array.isArray(x); } function normalizePageKey(s: unknown): string { return String(s ?? "") .replace(/^\/+/, "") .replace(/\/+$/, "") .trim(); } function inferPageKeyFromFile(inDirAbs: string, fileAbs: string): string { const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/"); return rel.replace(/\.(ya?ml|json)$/i, ""); } function parseDoc(raw: string, fileAbs: string): unknown { if (/\.json$/i.test(fileAbs)) return JSON.parse(raw); return parseYAML(raw); } function hardFailOrCollect(errors: string[], msg: string): void { if (STRICT) throw new Error(msg); errors.push(msg); } function sanitizeEntry( fileRel: string, paraId: string, entry: unknown, errors: string[] ): Record { if (entry == null) return {}; if (!isPlainObject(entry)) { hardFailOrCollect(errors, `${fileRel}: paras.${paraId} must be an object`); return {}; } const e: Record = { ...entry }; const arrayFields = [ "refs", "authors", "quotes", "media", "comments_editorial", ] as const; for (const k of arrayFields) { if (e[k] == null) continue; if (!Array.isArray(e[k])) { errors.push(`${fileRel}: paras.${paraId}.${k} must be an array (coerced to [])`); e[k] = []; } } return e; } export const GET: APIRoute = async () => { if (!(await exists(ANNO_DIR))) { const out = { schema: 1, generatedAt: new Date().toISOString(), pages: {}, stats: { pages: 0, paras: 0, errors: 0 }, errors: [] as string[], }; return new Response(JSON.stringify(out), { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }, }); } const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p)); const pages: Record> }> = Object.create(null); const errors: string[] = []; let paraCount = 0; for (const f of files) { const fileRel = path.relative(CWD, f).replace(/\\/g, "/"); const pageKey = normalizePageKey(inferPageKeyFromFile(ANNO_DIR, f)); if (!pageKey) { hardFailOrCollect(errors, `${fileRel}: cannot infer page key`); continue; } let doc: unknown; try { const raw = await fs.readFile(f, "utf8"); doc = parseDoc(raw, f); } catch (e) { hardFailOrCollect(errors, `${fileRel}: parse failed: ${String((e as any)?.message ?? e)}`); continue; } if (!isPlainObject(doc) || (doc as any).schema !== 1) { hardFailOrCollect(errors, `${fileRel}: schema must be 1`); continue; } if ((doc as any).page != null) { const declared = normalizePageKey((doc as any).page); if (declared !== pageKey) { hardFailOrCollect( errors, `${fileRel}: page mismatch (page="${declared}" vs path="${pageKey}")` ); } } const parasAny = (doc as any).paras; if (!isPlainObject(parasAny)) { hardFailOrCollect(errors, `${fileRel}: missing object key "paras"`); continue; } if (pages[pageKey]) { hardFailOrCollect(errors, `${fileRel}: duplicate page "${pageKey}" (only one file per page)`); continue; } const parasOut: Record> = Object.create(null); for (const [paraId, entry] of Object.entries(parasAny)) { if (!/^p-\d+-/i.test(paraId)) { hardFailOrCollect(errors, `${fileRel}: invalid para id "${paraId}"`); continue; } parasOut[paraId] = sanitizeEntry(fileRel, paraId, entry, errors); } pages[pageKey] = { paras: parasOut }; paraCount += Object.keys(parasOut).length; } const out = { schema: 1, generatedAt: new Date().toISOString(), pages, stats: { pages: Object.keys(pages).length, paras: paraCount, errors: errors.length, }, errors, }; return new Response(JSON.stringify(out), { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }, }); };