const fs = require("node:fs"); const path = require("node:path"); const yaml = require("js-yaml"); const { loadToolsConfig } = require("./config"); const DEFAULT_CACHE_DIR = "tools/cache"; const DEFAULT_CACHE_FILE = "external_links.yaml"; /** * Resout le chemin du rapport des liens externes a partir de la configuration. * @param {string} siteRoot Racine du projet. * @returns {Promise} Chemin absolu du rapport YAML. */ async function resolveExternalLinksReportPath(siteRoot) { const rootDir = path.resolve(siteRoot); const configPath = path.join(rootDir, "tools", "config", "config.json"); const config = await loadToolsConfig(configPath); let cacheDir = DEFAULT_CACHE_DIR; const externalLinks = config.externalLinks; if (externalLinks && typeof externalLinks.cacheDir === "string" && externalLinks.cacheDir.trim()) { cacheDir = externalLinks.cacheDir.trim(); } let cacheFile = DEFAULT_CACHE_FILE; if (externalLinks && typeof externalLinks.cacheFile === "string" && externalLinks.cacheFile.trim()) { cacheFile = externalLinks.cacheFile.trim(); } let resolvedCacheDir = cacheDir; if (!path.isAbsolute(resolvedCacheDir)) { resolvedCacheDir = path.join(rootDir, resolvedCacheDir); } if (path.isAbsolute(cacheFile)) { return cacheFile; } return path.join(resolvedCacheDir, cacheFile); } /** * Normalise la liste des emplacements associes a un lien. * @param {unknown[]} rawLocations Emplacements bruts. * @returns {Array<{ file: string, line: number|null, page: string|null }>} */ function normalizeLocations(rawLocations) { if (!Array.isArray(rawLocations)) { return []; } const locations = []; for (const rawLocation of rawLocations) { if (!rawLocation || typeof rawLocation !== "object") { continue; } let file = null; if (typeof rawLocation.file === "string" && rawLocation.file.trim()) { file = rawLocation.file.trim(); } if (!file) { continue; } let line = null; if (typeof rawLocation.line === "number" && Number.isFinite(rawLocation.line)) { line = rawLocation.line; } let page = null; if (typeof rawLocation.page === "string" && rawLocation.page.trim()) { page = rawLocation.page.trim(); } locations.push({ file, line, page }); } return locations; } /** * Normalise une entree du rapport. * @param {unknown} rawLink Entree brute. * @returns {{ url: string, status: number|null, locations: Array<{ file: string, line: number|null, page: string|null }> }|null} */ function normalizeLink(rawLink) { if (!rawLink || typeof rawLink !== "object") { return null; } if (typeof rawLink.url !== "string" || !rawLink.url.trim()) { return null; } let status = null; if (typeof rawLink.status === "number" && Number.isFinite(rawLink.status)) { status = rawLink.status; } if (typeof rawLink.status === "string" && rawLink.status.trim()) { const parsedStatus = Number.parseInt(rawLink.status, 10); if (!Number.isNaN(parsedStatus)) { status = parsedStatus; } } return { url: rawLink.url.trim(), status, locations: normalizeLocations(rawLink.locations), }; } /** * Reconstitue une liste de liens a partir de la section entries du cache. * @param {Record} entries Entrees brutes. * @returns {Array<{ url: string, status: number|null, locations: Array<{ file: string, line: number|null, page: string|null }> }>} */ function buildLinksFromEntries(entries) { const links = []; for (const [url, rawEntry] of Object.entries(entries)) { let status = null; let locations = null; if (rawEntry && typeof rawEntry === "object") { status = rawEntry.status; locations = rawEntry.locations; } const normalized = normalizeLink({ url, status, locations, }); if (normalized) { links.push(normalized); } } return links; } /** * Charge le rapport des liens externes. * @param {string} reportPath Chemin absolu ou relatif du rapport YAML. * @returns {{ generatedAt: string|null, links: Array<{ url: string, status: number|null, locations: Array<{ file: string, line: number|null, page: string|null }> }> }} */ function loadExternalLinksReport(reportPath) { const resolvedPath = path.resolve(reportPath); if (!fs.existsSync(resolvedPath)) { return { generatedAt: null, links: [] }; } const raw = yaml.load(fs.readFileSync(resolvedPath, "utf8")) || {}; let links = []; if (Array.isArray(raw.links)) { for (const rawLink of raw.links) { const normalized = normalizeLink(rawLink); if (normalized) { links.push(normalized); } } } else if (raw.entries && typeof raw.entries === "object") { links = buildLinksFromEntries(raw.entries); } return { generatedAt: raw.generatedAt || null, links, }; } /** * Filtre les liens du rapport par code de statut HTTP. * @param {{ links?: Array<{ status: number|null }> }} report Rapport charge. * @param {number} statusCode Code a retenir. * @returns {Array<{ url: string, status: number|null, locations: Array<{ file: string, line: number|null, page: string|null }> }>} */ function getLinksByStatus(report, statusCode) { if (!report || !Array.isArray(report.links)) { return []; } const links = []; for (const link of report.links) { if (!link || typeof link !== "object") { continue; } if (link.status !== statusCode) { continue; } links.push(link); } return links; } module.exports = { resolveExternalLinksReportPath, loadExternalLinksReport, getLinksByStatus, };