1

Nettoyage des images

This commit is contained in:
2026-03-05 11:55:41 +01:00
parent 5b355aca6a
commit f352dfd5ee
90 changed files with 595 additions and 47 deletions

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,7 +1,7 @@
--- ---
cover: images/xegNty.jpg cover: images/xegNty.jpg
date: "2024-04-28 11:36:06" date: "2024-04-28 11:36:06"
title: 'Atrociraptor Dinosaur: Bike Chase' title: "Atrociraptor Dinosaur: Bike Chase"
links: links:
- name: Page du catalogue LEGO - name: Page du catalogue LEGO
url: https://www.lego.com/fr-fr/product/atrociraptor-dinosaur-bike-chase-76945 url: https://www.lego.com/fr-fr/product/atrociraptor-dinosaur-bike-chase-76945
@@ -46,3 +46,9 @@ En outre, le set inclut un bien bel _Atrociraptor_.
Parmi les petites touches sympathiques de ce set, on notera le crâne de dinosaure — vraisemblablement un _Triceratops_, la cage qui peut contenir un bébé _Velociraptor_, l'arène qui utilise une roue pour ajouter un peu d'interaction, ainsi que la moto d'Owen. Parmi les petites touches sympathiques de ce set, on notera le crâne de dinosaure — vraisemblablement un _Triceratops_, la cage qui peut contenir un bébé _Velociraptor_, l'arène qui utilise une roue pour ajouter un peu d'interaction, ainsi que la moto d'Owen.
Avec 169 pièces, il étoffera un peu le marché de Malte commencé avec le set [_30390_](/collections/lego/jurassic-world/30390/). Avec 169 pièces, il étoffera un peu le marché de Malte commencé avec le set [_30390_](/collections/lego/jurassic-world/30390/).
## Images complémentaires
![](images/103678.jpg)
![](images/110688.jpg)

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -116,3 +116,15 @@ Je conclurai en disant qu'il m'a fallut une soirée pour terminer le montage (à
Un très beau set, dont les dimensions sont parfaites pour qu'il siège au-dessus de mon bureau, où il sera en permanence dans mon champs de vision. Un très beau set, dont les dimensions sont parfaites pour qu'il siège au-dessus de mon bureau, où il sera en permanence dans mon champs de vision.
Un très beau set pour démarrer la commémoration des 30 ans de mon film préféré depuis que j'en ai 10. Un très beau set pour démarrer la commémoration des 30 ans de mon film préféré depuis que j'en ai 10.
Un très beau set, enfin, pour le passionné que je suis. Un très beau set, enfin, pour le passionné que je suis.
## Images complémentaires
![](images/107061.jpg)
![](images/107063.jpg)
![](images/107064.jpg)
![](images/107067.jpg)
![](images/142003.jpg)

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

View File

@@ -59,3 +59,9 @@ La minifig de Dennis Nedry diffère un peu de celle fournie par le colossal set
Certes, les différences ne sont pas évidentes, et il ne faudra rien de moins qu'un oeil attentif pour voir que la tenue n'est pas la même, ou les subtiles mimiques des quatre visages présentés par les deux minifigs. Certes, les différences ne sont pas évidentes, et il ne faudra rien de moins qu'un oeil attentif pour voir que la tenue n'est pas la même, ou les subtiles mimiques des quatre visages présentés par les deux minifigs.
Dernier clin d'oeil au film, la fameuse canette de Barbasol, réceptacle réfrigéré contenant les embryons qu'il a tenté de dérober. Dernier clin d'oeil au film, la fameuse canette de Barbasol, réceptacle réfrigéré contenant les embryons qu'il a tenté de dérober.
## Images complémentaires
![](images/128936.jpg)
![](images/129866.jpg)

View File

@@ -62,3 +62,11 @@ Notons seulement que, possédant les deux sets, il est agréable de se retrouver
![4ZXYgy](images/4ZXYgy.webp) ![4ZXYgy](images/4ZXYgy.webp)
Encore une fois, LEGO fait mouche en proposant un set agréable à construire, satisfaisant, et assurant le _fan-service_ en remémorant l'une des scènes majeures du film. Encore une fois, LEGO fait mouche en proposant un set agréable à construire, satisfaisant, et assurant le _fan-service_ en remémorant l'une des scènes majeures du film.
## Images complémentaires
![](images/129861.jpg)
![](images/137712.jpg)
![](images/137714.jpg)

View File

@@ -70,3 +70,13 @@ Enfin, le set nous propose les minifigs de John Hammond (équipé de sa canne re
Un bien beau set, qui permet à la fois d'ouvrir sur la découverte des dinosaures par nos héros, mais aussi de revivre la scène du refuge d'Alan et des enfants après l'incident causé par Dennis Nedry. Un bien beau set, qui permet à la fois d'ouvrir sur la découverte des dinosaures par nos héros, mais aussi de revivre la scène du refuge d'Alan et des enfants après l'incident causé par Dennis Nedry.
De quoi ranimer ou cultiver plein de souvenirs ! De quoi ranimer ou cultiver plein de souvenirs !
## Images complémentaires
![](images/129836.jpg)
![](images/139699.jpg)
![](images/139700.jpg)
![](images/139701.jpg)

View File

@@ -75,3 +75,19 @@ Mais là encore, ce serait pour moi contre-nature...
Voilà, j'ai acquis puis assemblé l'intégralité des sets proposés par LEGO à l'occasion des 30 ans du film. Voilà, j'ai acquis puis assemblé l'intégralité des sets proposés par LEGO à l'occasion des 30 ans du film.
Je vous proposerai une photo de l'étagère que j'ai dédiée à leur exposition dans les jours qui viennent, dès que je me serai décidé concernant l'ordre dans lequel je vais les disposer 😋 Je vous proposerai une photo de l'étagère que j'ai dédiée à leur exposition dans les jours qui viennent, dès que je me serai décidé concernant l'ordre dans lequel je vais les disposer 😋
## Images complémentaires
![](images/129841.jpg)
![](images/139692.jpg)
![](images/139693.jpg)
![](images/139735.jpg)
![](images/139736.jpg)
![](images/139737.jpg)
![](images/139738.jpg)

View File

@@ -1 +0,0 @@
attribution: "© [Rebrickable](https://rebrickable.com/)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

View File

@@ -1 +0,0 @@
file: videos/pGDYWA.mov

View File

@@ -1,4 +0,0 @@
#title: ""
attribution: "Richard Dern"
description: "Rexou vous souhaite un joyeux Noël en avance"
#prompt: ""

View File

@@ -1,2 +0,0 @@
file: sounds/Rz7pM4.mp3
title: Pic vert, à une trentaine de mètres

View File

@@ -1,2 +0,0 @@
file: sounds/giHuWL.mp3
title: Pie, dans le champ voisin

View File

@@ -1,2 +0,0 @@
file: sounds/lLgMhQ.mp3
title: Corneille noire, à une vingtaine de mètres

View File

@@ -1,2 +0,0 @@
file: sounds/pMWFOQ.mp3
title: Moineau, deux mètres au-dessus du micro

View File

@@ -1 +0,0 @@
file: videos/Zf8iAR.mp4

View File

@@ -1 +0,0 @@
file: videos/fubVNP.mp4

View File

@@ -1 +0,0 @@
file: videos/glsyvj.mp4

View File

@@ -1 +0,0 @@
file: videos/oOg5AS.mp4

View File

@@ -1 +0,0 @@
attribution: https://commons.wikimedia.org/wiki/File:404_not_found.png

View File

@@ -1 +0,0 @@
attribution: https://commons.wikimedia.org/wiki/File:500_Internal_Server_error.png

View File

@@ -1,4 +0,0 @@
#title: ""
#attribution: ""
#description: ""
#prompt: ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,2 +0,0 @@
title: "Interface du logiciel Swift Imaging"
file: images/screenshot.png

View File

@@ -1,3 +0,0 @@
title: "Cluster d'organismes"
description: "Un cluster de quatre organismes. Couleurs naturelles."
file: videos/cluster.mp4

View File

@@ -1,3 +0,0 @@
title: "Cluster d'organismes colorés à l'éosine"
description: 'Un cluster de quatre organismes colorés à l''éosine. La membrane partagée est mieux visible. On peut également distinguer les "antennes".'
file: videos/cluster_eosine.mp4

View File

@@ -1,3 +0,0 @@
title: "Un organisme bien vigoureux !"
description: "Dur de suivre ce petit organisme ! La coloration à l'éosine ne semble pas l'avoir affecté. Notez que, outre ma difficulté à le garder dans le cadre, il faut garder à l'esprit que dans une lame concave, il peut aussi se balader sur le plan vertical : ça fait trois molettes avec lesquelles jouer. Vivement que je puisse m'équiper d'une platine motorisée !"
file: videos/solo.mp4

View File

@@ -0,0 +1,536 @@
#!/usr/bin/env node
const fs = require("node:fs/promises");
const path = require("node:path");
const readline = require("node:readline");
const { collectBundles } = require("./lib/content");
const CONTENT_DIR = path.resolve("content");
const IMAGE_EXTENSIONS = new Set([
".apng",
".avif",
".bmp",
".gif",
".heic",
".heif",
".jpeg",
".jpg",
".png",
".svg",
".tif",
".tiff",
".webp",
]);
let promptInterface = null;
process.stdout.on("error", handleStdoutError);
main().then(
() => {
closePromptInterface();
process.exit(0);
},
(error) => {
closePromptInterface();
console.error(`❌ Vérification interrompue : ${error.message}`);
process.exit(1);
}
);
/**
* Parcourt tous les bundles et affiche uniquement ceux qui contiennent
* au moins une image non référencée dans leur fichier index.md.
*/
async function main() {
const bundles = await resolveBundlesToScan();
for (const bundle of bundles) {
const unusedImages = await findUnusedBundleImages(bundle);
if (unusedImages.length === 0) {
continue;
}
console.log(`${toRelativePath(bundle.dir)}:`);
for (const imagePath of unusedImages) {
console.log(`- ${toRelativePath(imagePath)}`);
}
const action = await askBundleAction();
if (action === "1") {
await appendUnusedImagesToMarkdown(bundle, unusedImages);
console.log("✔ Images ajoutées au markdown.");
continue;
}
await deleteUnusedImages(unusedImages);
console.log("✔ Images non utilisées supprimées.");
}
await cleanupBundleMetadataFiles(bundles);
}
/**
* Demande le mode de recherche puis retourne les bundles à parcourir.
* @returns {Promise<Array<{dir: string, indexPath: string}>>} Bundles à analyser.
*/
async function resolveBundlesToScan() {
const prompt = [
"Choisissez le mode de recherche:",
"1) Dernier article écrit (défaut)",
"2) Recherche globale",
"Votre choix [1/2]: ",
].join("\n");
const answer = await askQuestion(prompt);
if (answer === "" || answer === "1") {
const latestBundle = await findLatestBundle(CONTENT_DIR);
if (latestBundle) {
return [latestBundle];
}
return [];
}
if (answer === "2") {
return collectBundles(CONTENT_DIR);
}
throw new Error('Choix invalide. Veuillez saisir "1" ou "2".');
}
/**
* Retourne le bundle le plus récent selon la date de modification du dossier.
* @param {string} rootDir Racine du contenu.
* @returns {Promise<{dir: string, indexPath: string}|null>} Bundle le plus récent.
*/
async function findLatestBundle(rootDir) {
const bundles = await collectBundles(rootDir);
let latestBundle = null;
let latestTime = 0;
for (const bundle of bundles) {
const stat = await fs.stat(bundle.dir);
if (stat.mtimeMs > latestTime) {
latestBundle = bundle;
latestTime = stat.mtimeMs;
}
}
return latestBundle;
}
/**
* Pose une question interactive et retourne la réponse saisie.
* @param {string} query Question à afficher.
* @returns {Promise<string>} Réponse sans espaces superflus.
*/
function askQuestion(query) {
return new Promise((resolve) => {
const rl = getPromptInterface();
let resolved = false;
const onClose = () => {
if (resolved) {
return;
}
resolved = true;
resolve("");
};
rl.once("close", onClose);
rl.question(query, (answer) => {
if (resolved) {
return;
}
resolved = true;
rl.removeListener("close", onClose);
resolve(answer.trim());
});
});
}
/**
* Retourne l'interface readline réutilisable pour toute l'exécution.
* @returns {readline.Interface} Interface interactive.
*/
function getPromptInterface() {
if (promptInterface !== null) {
return promptInterface;
}
promptInterface = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return promptInterface;
}
/**
* Ferme proprement l'interface readline si elle est ouverte.
*/
function closePromptInterface() {
if (promptInterface === null) {
return;
}
promptInterface.close();
promptInterface = null;
}
/**
* Demande l'action à appliquer au bundle courant.
* @returns {Promise<"1"|"2">} Action choisie.
*/
async function askBundleAction() {
const prompt = [
"Action pour ce bundle:",
"1) Ajouter les images au markdown",
"2) Supprimer les images non utilisées",
"Votre choix [1/2]: ",
].join("\n");
const answer = await askQuestion(prompt);
if (answer === "1") {
return "1";
}
if (answer === "2") {
return "2";
}
throw new Error('Choix invalide. Veuillez saisir "1" ou "2".');
}
/**
* Ajoute les images manquantes à la section "Images complémentaires".
* @param {{dir: string, indexPath: string}} bundle Bundle à modifier.
* @param {string[]} unusedImages Chemins absolus des images à ajouter.
*/
async function appendUnusedImagesToMarkdown(bundle, unusedImages) {
const articleText = await fs.readFile(bundle.indexPath, "utf8");
const imageLines = buildImageLines(bundle.dir, unusedImages);
const updatedText = insertComplementaryImages(articleText, imageLines);
await fs.writeFile(bundle.indexPath, updatedText, "utf8");
}
/**
* Supprime les images non utilisées du disque.
* @param {string[]} imagePaths Chemins absolus des images à supprimer.
*/
async function deleteUnusedImages(imagePaths) {
for (const imagePath of imagePaths) {
await fs.unlink(imagePath);
}
}
/**
* Construit les lignes Markdown d'inclusion d'images.
* @param {string} bundleDir Dossier du bundle.
* @param {string[]} imagePaths Chemins absolus des images.
* @returns {string[]} Lignes de type ![](images/...)
*/
function buildImageLines(bundleDir, imagePaths) {
const lines = [];
for (const imagePath of imagePaths) {
const relativePath = toPosixPath(path.relative(bundleDir, imagePath));
lines.push(`![](${relativePath})`);
}
return lines;
}
/**
* Insère les images dans la section "## Images complémentaires".
* @param {string} articleText Contenu de index.md.
* @param {string[]} imageLines Lignes Markdown des images à ajouter.
* @returns {string} Nouveau contenu Markdown.
*/
function insertComplementaryImages(articleText, imageLines) {
const headingRegex = /^##\s+Images complémentaires\s*$/m;
const headingMatch = headingRegex.exec(articleText);
const imageBlock = imageLines.join("\n\n");
if (headingMatch === null) {
const trimmed = articleText.trimEnd();
return `${trimmed}\n\n## Images complémentaires\n\n${imageBlock}\n`;
}
const headingEnd = headingMatch.index + headingMatch[0].length;
const afterHeading = articleText.slice(headingEnd);
const nextHeadingRegex = /\n##\s+/;
const nextHeadingMatch = nextHeadingRegex.exec(afterHeading);
let sectionEnd = articleText.length;
if (nextHeadingMatch !== null) {
sectionEnd = headingEnd + nextHeadingMatch.index;
}
const sectionPrefix = articleText.slice(0, sectionEnd).trimEnd();
const sectionSuffix = articleText.slice(sectionEnd);
return `${sectionPrefix}\n\n${imageBlock}\n${sectionSuffix}`;
}
/**
* Retourne la liste des images d'un bundle absentes du texte de l'article.
* @param {{dir: string, indexPath: string}} bundle Bundle à analyser.
* @returns {Promise<string[]>} Chemins absolus des images non utilisées.
*/
async function findUnusedBundleImages(bundle) {
const imagesDir = path.join(bundle.dir, "images");
const hasImagesDir = await directoryExists(imagesDir);
if (hasImagesDir === false) {
return [];
}
const imageFiles = await collectImageFiles(imagesDir);
if (imageFiles.length === 0) {
return [];
}
imageFiles.sort((left, right) => left.localeCompare(right));
const articleText = await fs.readFile(bundle.indexPath, "utf8");
const searchableText = stripMarkdownCode(articleText);
const unusedImages = [];
for (const imagePath of imageFiles) {
const relativePath = toPosixPath(path.relative(bundle.dir, imagePath));
const candidatePaths = buildCandidatePaths(relativePath);
const isReferenced = hasAnyReference(searchableText, candidatePaths);
if (isReferenced === false) {
unusedImages.push(imagePath);
}
}
return unusedImages;
}
/**
* Supprime les fichiers .yaml orphelins dans data/ pour chaque bundle.
* Un fichier est conservé si une image du bundle partage le même nom de base.
* @param {Array<{dir: string}>} bundles Bundles à nettoyer.
*/
async function cleanupBundleMetadataFiles(bundles) {
for (const bundle of bundles) {
await cleanupMetadataFilesForBundle(bundle);
}
}
/**
* Nettoie les fichiers .yaml orphelins dans le dossier data/ d'un bundle.
* @param {{dir: string}} bundle Bundle à nettoyer.
*/
async function cleanupMetadataFilesForBundle(bundle) {
const imageBaseNames = await collectImageBaseNames(bundle.dir);
const dataDir = path.join(bundle.dir, "data");
const hasDataDir = await directoryExists(dataDir);
if (hasDataDir === false) {
return;
}
const yamlFiles = await collectYamlFiles(dataDir);
for (const yamlPath of yamlFiles) {
const yamlBaseName = path.parse(yamlPath).name;
if (imageBaseNames.has(yamlBaseName)) {
continue;
}
await fs.unlink(yamlPath);
}
}
/**
* Retourne les noms de base des images du bundle (sans extension).
* @param {string} bundleDir Dossier du bundle.
* @returns {Promise<Set<string>>} Ensemble des noms de base.
*/
async function collectImageBaseNames(bundleDir) {
const imageBaseNames = new Set();
const imagesDir = path.join(bundleDir, "images");
const hasImagesDir = await directoryExists(imagesDir);
if (hasImagesDir === false) {
return imageBaseNames;
}
const imageFiles = await collectImageFiles(imagesDir);
for (const imagePath of imageFiles) {
imageBaseNames.add(path.parse(imagePath).name);
}
return imageBaseNames;
}
/**
* Indique si au moins une variante de chemin est trouvée dans le texte.
* @param {string} articleText Contenu de index.md.
* @param {string[]} candidatePaths Variantes à tester.
* @returns {boolean} true si une référence est détectée.
*/
function hasAnyReference(articleText, candidatePaths) {
for (const candidatePath of candidatePaths) {
if (isPathReferenced(articleText, candidatePath)) {
return true;
}
}
return false;
}
/**
* Supprime les zones de code Markdown pour éviter les faux positifs.
* @param {string} text Texte brut du fichier Markdown.
* @returns {string} Texte nettoyé des portions de code.
*/
function stripMarkdownCode(text) {
const withoutFencedBackticks = text.replace(/```[\s\S]*?```/g, "\n");
const withoutFencedTildes = withoutFencedBackticks.replace(/~~~[\s\S]*?~~~/g, "\n");
return withoutFencedTildes.replace(/`[^`\n]*`/g, "");
}
/**
* Construit les variantes de chemin pour gérer les chemins encodés.
* @param {string} relativePath Chemin relatif au bundle.
* @returns {string[]} Variantes uniques du chemin.
*/
function buildCandidatePaths(relativePath) {
const values = [relativePath];
const encoded = encodeURI(relativePath);
if (encoded !== relativePath) {
values.push(encoded);
}
return values;
}
/**
* Vérifie si un chemin est référencé comme unité complète dans le texte.
* @param {string} articleText Contenu de index.md.
* @param {string} pathValue Chemin relatif à rechercher.
* @returns {boolean} true si le chemin est présent.
*/
function isPathReferenced(articleText, pathValue) {
const escapedPath = escapeRegExp(pathValue);
const before = "(^|[\\s\"'`(=:/])";
const after = "($|[\\s\"'`)#?&,:;.!\\]}<>])";
const pattern = new RegExp(`${before}${escapedPath}${after}`, "u");
return pattern.test(articleText);
}
/**
* Échappe une chaîne pour un usage littéral dans une expression régulière.
* @param {string} value Valeur à échapper.
* @returns {string} Valeur échappée.
*/
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Collecte récursivement les fichiers image d'un dossier.
* @param {string} rootDir Dossier à parcourir.
* @returns {Promise<string[]>} Liste des fichiers image.
*/
async function collectImageFiles(rootDir) {
const files = [];
const entries = await fs.readdir(rootDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
const nestedFiles = await collectImageFiles(fullPath);
files.push(...nestedFiles);
continue;
}
if (entry.isFile() === false) {
continue;
}
const extension = path.extname(entry.name).toLowerCase();
if (IMAGE_EXTENSIONS.has(extension) === false) {
continue;
}
files.push(fullPath);
}
return files;
}
/**
* Collecte récursivement les fichiers .yaml d'un dossier.
* @param {string} rootDir Dossier à parcourir.
* @returns {Promise<string[]>} Liste des chemins de fichiers .yaml.
*/
async function collectYamlFiles(rootDir) {
const files = [];
const entries = await fs.readdir(rootDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
const nestedFiles = await collectYamlFiles(fullPath);
files.push(...nestedFiles);
continue;
}
if (entry.isFile() === false) {
continue;
}
const extension = path.extname(entry.name).toLowerCase();
if (extension !== ".yaml") {
continue;
}
files.push(fullPath);
}
return files;
}
/**
* Vérifie l'existence d'un dossier.
* @param {string} dirPath Dossier à vérifier.
* @returns {Promise<boolean>} true si le dossier existe.
*/
async function directoryExists(dirPath) {
const entries = await fs.readdir(path.dirname(dirPath), { withFileTypes: true });
for (const entry of entries) {
if (entry.name !== path.basename(dirPath)) {
continue;
}
if (entry.isDirectory()) {
return true;
}
return false;
}
return false;
}
/**
* Convertit un chemin en représentation relative à la racine du dépôt.
* @param {string} absolutePath Chemin absolu.
* @returns {string} Chemin relatif normalisé en POSIX.
*/
function toRelativePath(absolutePath) {
return toPosixPath(path.relative(process.cwd(), absolutePath));
}
/**
* Normalise les séparateurs de chemin au format POSIX.
* @param {string} value Chemin à normaliser.
* @returns {string} Chemin avec séparateurs "/".
*/
function toPosixPath(value) {
return value.split(path.sep).join("/");
}
/**
* Interrompt proprement le script si la sortie standard est fermée.
* @param {NodeJS.ErrnoException} error Erreur émise par stdout.
*/
function handleStdoutError(error) {
if (error.code === "EPIPE") {
process.exit(0);
}
throw error;
}