1
Files
2025/tools/lib/external_links_report.js

199 lines
5.5 KiB
JavaScript

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<string>} 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<string, unknown>} 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,
};