Seed from NAS prod snapshot 20260130-190531
All checks were successful
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (pull_request) Successful in 1m20s

This commit is contained in:
archicratia
2026-01-31 10:51:38 +00:00
commit 60d88939b0
142 changed files with 33443 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
const CWD = process.cwd();
const DIST_ROOT = path.join(CWD, "dist");
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
const argv = process.argv.slice(2);
const DRY_RUN = argv.includes("--dry-run");
const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.env.CI === "true";
function escRe(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function countIdAttr(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
let c = 0;
while (re.exec(html)) c++;
return c;
}
function findStartTagWithId(html, id) {
// 1er élément qui porte id="..."
const re = new RegExp(
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
"i"
);
const m = re.exec(html);
if (!m) return null;
return { tagName: String(m[1]).toLowerCase(), tag: m[0] };
}
function isInjectedAliasSpan(html, id) {
const found = findStartTagWithId(html, id);
if (!found) return false;
if (found.tagName !== "span") return false;
// class="... para-alias ..."
return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag);
}
function normalizeRoute(route) {
let r = String(route || "").trim();
if (!r.startsWith("/")) r = "/" + r;
if (!r.endsWith("/")) r = r + "/";
r = r.replace(/\/{2,}/g, "/");
return r;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function hasId(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "i");
return re.test(html);
}
function injectBeforeId(html, newId, injectHtml) {
// insère juste avant la balise qui porte id="newId"
const re = new RegExp(
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
"i"
);
const m = html.match(re);
if (!m || m.index == null) return { html, injected: false };
const i = m.index;
const out = html.slice(0, i) + injectHtml + "\n" + html.slice(i);
return { html: out, injected: true };
}
async function main() {
if (!(await exists(ALIASES_PATH))) {
console.log(" Aucun fichier d'aliases (src/anchors/anchor-aliases.json). Skip.");
return;
}
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
/** @type {Record<string, Record<string,string>>} */
let aliases;
try {
aliases = JSON.parse(raw);
} catch (e) {
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
}
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
}
const routes = Object.keys(aliases || {});
if (routes.length === 0) {
console.log(" Aliases vides. Rien à injecter.");
return;
}
let changedFiles = 0;
let injectedCount = 0;
let warnCount = 0;
for (const routeKey of routes) {
const route = normalizeRoute(routeKey);
const map = aliases[routeKey] || {};
const entries = Object.entries(map);
if (route !== routeKey) {
const msg = `⚠️ routeKey non normalisée: "${routeKey}" → "${route}" (corrige anchor-aliases.json)`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
}
if (entries.length === 0) continue;
const rel = route.replace(/^\/+|\/+$/g, ""); // sans slash
const htmlPath = path.join(DIST_ROOT, rel, "index.html");
if (!(await exists(htmlPath))) {
const msg = `⚠️ dist introuvable pour route=${route} (${htmlPath})`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
let html = await fs.readFile(htmlPath, "utf-8");
let fileChanged = false;
for (const [oldId, newId] of entries) {
if (!oldId || !newId) continue;
const oldCount = countIdAttr(html, oldId);
if (oldCount > 0) {
// ✅ déjà injecté (idempotent)
if (isInjectedAliasSpan(html, oldId)) continue;
// ⛔️ oldId existe déjà "en vrai" (ex: <p id="oldId">)
// => alias inutile / inversé / obsolète
const found = findStartTagWithId(html, oldId);
const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
const msg =
`⚠️ alias inutile/inversé: oldId déjà présent dans la page (${where}). ` +
`Supprime l'alias ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
// juste après avoir calculé oldCount
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
if (STRICT && oldCount !== 1) {
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
}
continue;
}
// avant l'injection, après hasId(newId)
const newCount = countIdAttr(html, newId);
if (newCount !== 1) {
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
if (!hasId(html, newId)) {
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
const aliasSpan = `<span id="${oldId}" class="para-alias" aria-hidden="true"></span>`;
const r = injectBeforeId(html, newId, aliasSpan);
if (!r.injected) {
const msg = `⚠️ injection impossible (pattern non trouvé) : ${route} new=${newId}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
html = r.html;
fileChanged = true;
injectedCount++;
}
if (fileChanged) {
changedFiles++;
if (!DRY_RUN) await fs.writeFile(htmlPath, html, "utf-8");
}
}
console.log(
`✅ inject-anchor-aliases: files_changed=${changedFiles} aliases_injected=${injectedCount} warnings=${warnCount}` +
(DRY_RUN ? " (dry-run)" : "")
);
if (STRICT && warnCount > 0) process.exit(2);
}
main().catch((e) => {
console.error("💥 inject-anchor-aliases:", e?.message || e);
process.exit(1);
});