feat(F-055): completed feature

This commit is contained in:
chattie
2026-08-19 13:48:32 +02:00
parent de41a42d88
commit cc84f24658
44 changed files with 473 additions and 236 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
// Generate cached thumbnails (40px and 200px) for every uploaded product image.
// Pre-generates thumbnails in each app's public/uploads/thumbs/<size>/ directory
// so Next.js serves them as static files.
import { readdir, stat, mkdir, writeFile, readFile } from 'node:fs/promises';
import { dirname, extname, join } from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const sharp = require(process.env.SHARP_PATH || '/Users/chattie/git/mercadodevida/project/apps/admin/node_modules/sharp');
const PROJECT_DIR = join(import.meta.dirname, '..');
const SIZES = [40, 200];
const APP_DIRS = [
join(PROJECT_DIR, 'apps', 'admin'),
join(PROJECT_DIR, 'frontend'),
join(PROJECT_DIR, 'storefront'),
];
const SRC_DIR = join(PROJECT_DIR, 'apps', 'admin', 'public', 'uploads');
const VALID_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif']);
async function main() {
let files;
try {
files = await readdir(SRC_DIR);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('[INFO] No uploads dir yet, nothing to do');
return;
}
throw err;
}
const images = files.filter((f) => VALID_EXTS.has(extname(f).toLowerCase()));
console.log(`[INFO] Found ${images.length} source images`);
let generated = 0;
for (const filename of images) {
const src = join(SRC_DIR, filename);
const srcStat = await stat(src);
if (!srcStat.isFile()) continue;
for (const appDir of APP_DIRS) {
// Ensure peer upload exists so the thumbnail is reachable
const peerPath = join(appDir, 'public', 'uploads', filename);
try {
await stat(peerPath);
} catch {
// Copy from source if missing (mirror admin's uploads)
await mkdir(dirname(peerPath), { recursive: true });
await writeFile(peerPath, await import('node:fs/promises').then((fs) => fs.readFile(src)));
}
for (const size of SIZES) {
// Store as <app>/public/uploads/<size>/<filename> so Next.js serves
// them as plain static files via URLs like /uploads/40/foo.jpg
const thumbDir = join(appDir, 'public', 'uploads', String(size));
const thumbPath = join(thumbDir, filename);
// Skip if already cached
try {
const s = await stat(thumbPath);
if (s.isFile() && s.size > 0) continue;
} catch {}
await mkdir(thumbDir, { recursive: true });
await sharp(src)
.resize({ width: size, withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(thumbPath);
generated += 1;
}
}
}
console.log(`[OK] Processed ${images.length} images, generated ${generated} new thumbnails`);
}
main().catch((err) => {
console.error('[FAIL]', err.message);
process.exit(1);
});

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Generate cached thumbnails (40px and 200px) for every uploaded product image.
#
# Pre-generates thumbnails in each app's public/uploads/thumbs/<size>/ directory
# so Next.js serves them as static files with Cache-Control: public, max-age=31536000.
#
# Run after every upload, as part of sync_uploads in monolith.sh, or on demand.
#
# Usage:
# ./scripts/generate-thumbnails.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Find a node_modules that contains sharp
SHARP_DIR=""
for dir in "$PROJECT_DIR/apps/admin" "$PROJECT_DIR/frontend" "$PROJECT_DIR/storefront"; do
if [[ -d "$dir/node_modules/sharp" ]]; then
SHARP_DIR="$dir"
break
fi
done
if [[ -z "$SHARP_DIR" ]]; then
echo "[FAIL] sharp not found; install with: cd project/apps/admin && npm install sharp" >&2
exit 1
fi
cd "$SHARP_DIR"
exec node "$SCRIPT_DIR/generate-thumbnails.mjs"

View File

@@ -210,24 +210,30 @@ build_prod() {
sync_uploads() {
# Mirror admin's public/uploads to frontend and storefront so that product
# images are reachable from any of the three apps.
# images are reachable from any of the three apps. Also mirrors every
# generated thumbnail subdir (40/, 200/, ...) so the static cached files
# reach all three apps in lockstep.
local src="$PROJECT_DIR/apps/admin/public/uploads"
local peer1="$PROJECT_DIR/frontend/public/uploads"
local peer2="$PROJECT_DIR/storefront/public/uploads"
local peers=(
"$PROJECT_DIR/frontend/public/uploads"
"$PROJECT_DIR/storefront/public/uploads"
)
if [[ ! -d "$src" ]]; then
return 0
fi
for peer in "$peer1" "$peer2"; do
for peer in "${peers[@]}"; do
mkdir -p "$peer"
# copy every file present in source but missing in peer
while IFS= read -r -d '' f; do
local name
name="$(basename "$f")"
if [[ ! -f "$peer/$name" ]]; then
cp "$f" "$peer/$name" 2>/dev/null || true
find "$src" -maxdepth 2 -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
local rel
rel="${f#$src/}"
if [[ ! -f "$peer/$rel" ]]; then
mkdir -p "$(dirname "$peer/$rel")"
cp "$f" "$peer/$rel" 2>/dev/null || true
fi
done < <(find "$src" -maxdepth 1 -type f -print0 2>/dev/null)
done
done
# Ensure thumbnails are up to date for the current image set.
"$PROJECT_DIR/scripts/generate-thumbnails.sh" --quiet
}
spawn_service() {