Ajoute un outil de gestion interactive des liens morts
This commit is contained in:
198
tools/lib/external_links_report.js
Normal file
198
tools/lib/external_links_report.js
Normal file
@@ -0,0 +1,198 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user