first commit
This commit is contained in:
@@ -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 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/<slug>.png`.
|
||||
* 2. Sinon, image dans `ressources/public/<slug>.<ext>`.
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user