#!/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, "\\$&"); } async function exists(p) { try { await fs.access(p); return true; } catch { return false; } } 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; } 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) { 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; return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag); } function injectBeforeId(html, newId, injectHtml) { 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>} */ 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, ""); 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); // ✅ déjà injecté => idempotent 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; } // ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé if (oldCount > 0) { const found = findStartTagWithId(html, oldId); const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`; const msg = `⚠️ alias inutile/inversé: oldId déjà présent (${where}). ` + `Supprime ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`; if (STRICT) throw new Error(msg); console.log(msg); warnCount++; continue; } // newId doit exister UNE fois (sinon injection ambiguë) 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; } const aliasSpan = ``; 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); });