From 9318b0291e38df3f642756935e832a23a2977f0b Mon Sep 17 00:00:00 2001 From: "ramin.najarbashi" Date: Mon, 27 Jul 2026 15:55:11 +0330 Subject: [PATCH] build: stabilize image optimization against EAGAIN/EPIPE failures Process images in bounded batches with per-file fallback so npm start and npm run build no longer crash when pngquant/mozjpeg spawn limits are hit. Run assets before the dev server and after the parallel site/css/js build to reduce concurrent process pressure. Signed-off-by: ramin.najarbashi --- package.json | 4 +- scripts/images.js | 122 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 4870c4ec2..39860d865 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "site": "npx eleventy", "clean": "rimraf dist", "start": "npm run dev", - "dev": "cross-env NODE_ENV=development npm-run-all clean --parallel 'site -- --serve' 'css -- --watch' 'js -- --watch' assets", - "build": "npm-run-all clean --parallel site css js assets", + "dev": "cross-env NODE_ENV=development npm-run-all clean assets --parallel 'site -- --serve' 'css -- --watch' 'js -- --watch'", + "build": "npm-run-all clean --parallel site css js && npm run assets", "build:development": "cross-env NODE_ENV=development npm run build", "build:production": "cross-env NODE_ENV=production npm run build", "debug": "cross-env DEBUG=* npx eleventy", diff --git a/scripts/images.js b/scripts/images.js index ed7ee5937..4d2ca8b37 100644 --- a/scripts/images.js +++ b/scripts/images.js @@ -3,41 +3,101 @@ const imageminMozjpeg = require('imagemin-mozjpeg'); const imageminPngquant = require('imagemin-pngquant'); const imageminSvgo = require('imagemin-svgo'); const imageminGifsicle = require('imagemin-gifsicle'); +const chunk = require('lodash.chunk'); -const util = require('util'); const path = require('path'); -const fs = require('graceful-fs'); -const makeDir = require('make-dir'); -const writeFile = util.promisify(fs.writeFile); +const fs = require('fs/promises'); const srcDir = 'src'; const distDir = 'dist'; +const BATCH_SIZE = 8; +const imagePattern = /\.(jpg|jpeg|png|svg|gif)$/i; + +const allPlugins = [ + imageminMozjpeg({ quality: 75 }), + imageminPngquant({ quality: [0.6, 0.8] }), + imageminSvgo({ plugins: [{ removeViewBox: false }] }), + imageminGifsicle({ colors: 96, optimizationLevel: 2 }), +]; + +function destinationPathFor(imagePath) { + return path.join(distDir, path.relative(srcDir, imagePath)); +} + +async function collectImagePaths(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const paths = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + paths.push(...(await collectImagePaths(fullPath))); + } else if (imagePattern.test(entry.name)) { + paths.push(fullPath); + } + } + + return paths; +} + +async function writeOptimizedFile(file) { + const destinationPath = destinationPathFor(file.sourcePath); + await fs.mkdir(path.dirname(destinationPath), { recursive: true }); + await fs.writeFile(destinationPath, file.data); +} + +async function copyOriginalImage(imagePath) { + const destinationPath = destinationPathFor(imagePath); + await fs.mkdir(path.dirname(destinationPath), { recursive: true }); + await fs.copyFile(imagePath, destinationPath); +} + +async function optimizeImage(imagePath) { + try { + const files = await imagemin([imagePath], { plugins: allPlugins }); + + if (!files.length) { + throw new Error(`No output for ${imagePath}`); + } + + await writeOptimizedFile(files[0]); + } catch (error) { + console.warn( + `Warning: could not optimize ${imagePath}, copying original (${ + error.code || error.message + })` + ); + await copyOriginalImage(imagePath); + } +} + +async function optimizeBatch(batch) { + try { + const files = await imagemin(batch, { plugins: allPlugins }); + await Promise.all(files.map(writeOptimizedFile)); + } catch (error) { + console.warn( + `Warning: batch optimize failed (${ + error.code || error.message + }), retrying file-by-file` + ); + await Promise.all(batch.map(optimizeImage)); + } +} (async () => { - const files = await imagemin([srcDir + '/**/*.{jpg,jpeg,png,svg,gif}'], { - plugins: [ - imageminMozjpeg({ quality: 75 }), - imageminPngquant({ - quality: [0.6, 0.8], - }), - imageminSvgo({ - plugins: [{ removeViewBox: false }], - }), - imageminGifsicle({ - colors: 96, - optimizationLevel: 2, - }), - ], - }); - - const { length } = files; - files.forEach(async v => { - let source = path.parse(v.sourcePath); - v.destinationPath = `${source.dir.replace(srcDir, distDir)}/${source.name}${ - source.ext - }`; - await makeDir(path.dirname(v.destinationPath)); - await writeFile(v.destinationPath, v.data); - }); - console.log(`${length} image${length !== 1 ? 's' : ''} minified`); -})(); + const imagePaths = await collectImagePaths(srcDir); + const batches = chunk(imagePaths, BATCH_SIZE); + + for (const batch of batches) { + await optimizeBatch(batch); + } + + console.log( + `${imagePaths.length} image${imagePaths.length !== 1 ? 's' : ''} processed` + ); +})().catch(error => { + console.error(error); + process.exit(1); +});