first commit

This commit is contained in:
2026-07-21 16:50:58 +02:00
commit 256599626e
407 changed files with 30489 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
/**
* Assets IO — lecture des métadonnées PNG, copie des fichiers ingrédients
* et branding vers `apps/web/public`, sélection d'une cover recette.
*
* Aucun parsing : ces fonctions touchent le système de fichiers. Le parsing
* bentext / sprite vit dans `parse-bentext.mjs` et `sprites.mjs`.
*/
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
/** Fichiers dingrédients copiés tels quels vers `apps/web/public`. */
const INGREDIENT_ASSET_FILES = ["ingredients.png", "ingredient-sprites.csv", "ingredient-unknown.png"];
/** Fichiers de branding copiés tels quels vers `apps/web/public/branding`. */
export const BRANDING_FILES = ["onigiri-logo.png", "og.jpg"];
export const PATHS = {
repoRoot,
recipesDir: path.join(repoRoot, "ressources/bentext"),
outDir: path.join(repoRoot, "ressources/generated"),
webPublicDir: path.join(repoRoot, "apps/web/public/recipes"),
webCoverDir: path.join(repoRoot, "apps/web/public/recipe-covers"),
webBrandingDir: path.join(repoRoot, "apps/web/public/branding"),
webPublicRoot: path.join(repoRoot, "apps/web/public"),
publicAssetsDir: path.join(repoRoot, "ressources/public"),
placeholderCoversDir: path.join(repoRoot, "ressources/recipe-placeholder-covers"),
/** Grille des tuiles : coordonnées et alias listés dans ce fichier (plus dAPI). */
ingredientSpritesBentextPath: path.join(repoRoot, "ressources/public/ingredient-sprites.bentext"),
ingredientsPngPath: path.join(repoRoot, "ressources/public/ingredients.png")
};
const COVER_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"];
const readPngDimensions = async (filePath) => {
try {
const buf = await fs.readFile(filePath);
if (buf.length < 24 || buf[0] !== 0x89) return null;
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
return { width, height };
} catch {
return null;
}
};
const copyIngredientAssetsToWeb = async () => {
await fs.mkdir(PATHS.webPublicRoot, { recursive: true });
for (const name of INGREDIENT_ASSET_FILES) {
const src = path.join(PATHS.publicAssetsDir, name);
const dest = path.join(PATHS.webPublicRoot, name);
try {
await fs.copyFile(src, dest);
} catch (error) {
if (error && error.code === "ENOENT") continue;
throw error;
}
}
};
const copyBrandingAssets = async () => {
await fs.mkdir(PATHS.webBrandingDir, { recursive: true });
for (const name of BRANDING_FILES) {
const src = path.join(PATHS.publicAssetsDir, name);
const dest = path.join(PATHS.webBrandingDir, name);
try {
await fs.copyFile(src, dest);
} catch (error) {
if (error && error.code === "ENOENT") {
console.warn(`Branding asset missing, skip: ${name}`);
continue;
}
throw error;
}
}
};
/**
* Sélectionne une cover pour `slug` :
* 1. Image HD dans `ressources/recipe-placeholder-covers/<slug>.png`.
* 2. Sinon, image dans `ressources/public/<slug>.<ext>`.
* Copie le fichier dans `apps/web/public/recipe-covers/` et retourne lURL publique.
*/
const pickCoverImage = async (slug) => {
await fs.mkdir(PATHS.webCoverDir, { recursive: true });
/* Priorité aux visuels HD du dépôt ; sinon ressources/public (override legacy). */
const placeholderPng = path.join(PATHS.placeholderCoversDir, `${slug}.png`);
try {
await fs.access(placeholderPng);
const publicName = `${slug}.png`;
const target = path.join(PATHS.webCoverDir, publicName);
await fs.copyFile(placeholderPng, target);
return `/recipe-covers/${publicName}`;
} catch {
// continue
}
for (const extension of COVER_EXTENSIONS) {
const candidate = path.join(PATHS.publicAssetsDir, `${slug}${extension}`);
try {
await fs.access(candidate);
const publicName = `${slug}${extension}`;
const target = path.join(PATHS.webCoverDir, publicName);
await fs.copyFile(candidate, target);
return `/recipe-covers/${publicName}`;
} catch {
// continue
}
}
return null;
};
export { readPngDimensions, copyIngredientAssetsToWeb, copyBrandingAssets, pickCoverImage };