/** * 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 d’ingré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 d’API). */ 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/.png`. * 2. Sinon, image dans `ressources/public/.`. * Copie le fichier dans `apps/web/public/recipe-covers/` et retourne l’URL 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 };