From 00c90f9d2c509f33ff68a7424d65428b5eec7d98 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:02:02 +0700 Subject: [PATCH 01/45] Install fdir --- package-lock.json | 59 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30fe329..e8e8f02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "commander": "^12.1.0", "exec-buffer": "^3.2.0", "fast-glob": "^3.3.2", + "fdir": "^6.3.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", "guetzli": "^5.0.0", @@ -1865,6 +1866,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -4342,6 +4355,19 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", + "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6294,6 +6320,18 @@ "node": ">=8" } }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -6854,6 +6892,17 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.53.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", @@ -7404,11 +7453,13 @@ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "optional": true, + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" diff --git a/package.json b/package.json index 96669eb..bf5bb74 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "commander": "^12.1.0", "exec-buffer": "^3.2.0", "fast-glob": "^3.3.2", + "fdir": "^6.3.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", "guetzli": "^5.0.0", From b20869e5739be2761667eba8ab9025580e27e8f1 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:03:28 +0700 Subject: [PATCH 02/45] Replace fast-glob with fdir Improve prepareFilePaths function --- index.js | 9 +++- lib/prepare-file-paths.js | 70 ++++++++++++++++++++------------ tests/prepare-file-paths.test.js | 18 ++++---- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index 8012c14..105ff24 100755 --- a/index.js +++ b/index.js @@ -12,12 +12,17 @@ export default async function optimizt({ paths, avif, webp, force, lossless, ver const configFilepath = pathToFileURL(config ? checkConfigPath(config) : findConfig()); const configData = await import(configFilepath); + const supportedFileTypes = { + convert: ['gif', 'jpeg', 'jpg', 'png'], + optimize: ['gif', 'jpeg', 'jpg', 'png', 'svg'], + }; + if (verbose) { enableVerbose(); } await (avif || webp ? convert({ - paths: prepareFilePaths(paths, ['gif', 'jpeg', 'jpg', 'png']), + paths: await prepareFilePaths(paths, supportedFileTypes.convert), lossless, avif, webp, @@ -25,7 +30,7 @@ export default async function optimizt({ paths, avif, webp, force, lossless, ver output: prepareOutputPath(output), config: configData.default.convert, }) : optimize({ - paths: prepareFilePaths(paths, ['gif', 'jpeg', 'jpg', 'png', 'svg']), + paths: await prepareFilePaths(paths, supportedFileTypes.optimize), lossless, output: prepareOutputPath(output), config: configData.default.optimize, diff --git a/lib/prepare-file-paths.js b/lib/prepare-file-paths.js index 68b625c..b30868f 100644 --- a/lib/prepare-file-paths.js +++ b/lib/prepare-file-paths.js @@ -1,33 +1,53 @@ import fs from 'node:fs'; import path from 'node:path'; -import fg from 'fast-glob'; +import { fdir } from 'fdir'; -export default function prepareFilePaths(paths, extensions) { - const fgExtensions = extensions.join('|'); - const replacePath = `${process.cwd()}${path.sep}`; - const filePaths = [...new Set(paths.reduce((accumulator, filePath) => { - if (fs.existsSync(filePath)) { - // Search for files recursively inside the dir - if (fs.lstatSync(filePath).isDirectory()) { - accumulator = [ - ...accumulator, - ...fg.sync( - `${path.resolve(filePath).replaceAll('\\', '/')}/**/*.+(${fgExtensions})`, - { caseSensitiveMatch: false }, - ), - ]; - } - - // Filter files by extension - if (extensions.includes(path.extname(filePath).toLowerCase().slice(1))) { - accumulator.push(filePath); - } +export default async function prepareFilePaths(paths, extensions) { + const files = new Set(); + const directories = new Set(); + + const pathsSet = new Set(paths); + + for (const currentPath of pathsSet) { + if (!fs.existsSync(currentPath)) { + continue; + } + + const lstat = fs.lstatSync(currentPath); + + if (lstat.isDirectory()) { + directories.add(currentPath); + } else if (lstat.isFile() && checkFileType(currentPath, extensions)) { + files.add(getRelativePath(currentPath)); } + } + + const crawler = new fdir() // eslint-disable-line new-cap + .withFullPaths() + .filter(currentPath => checkFileType(currentPath, extensions)); + + const crawlerPromises = [...directories].map(currentPath => crawler.crawl(currentPath).withPromise()); + const crawledPaths = await Promise.all(crawlerPromises); - return accumulator; - }, []))]; + for (const crawledPath of crawledPaths.flat()) { + files.add(getRelativePath(crawledPath)); + } + + const filteredPaths = [...files]; + + return filteredPaths; +} + +function getRelativePath(filePath) { + const replacePath = `${process.cwd()}${path.sep}`; + + return filePath.startsWith(replacePath) + ? filePath.slice(replacePath.length) + : filePath; +} - // Use relative paths when it's possible - return filePaths.map(p => p.replace(replacePath, '')); +function checkFileType(filePath, extensions) { + const extension = path.extname(filePath).toLowerCase().slice(1); + return extensions.includes(extension); } diff --git a/tests/prepare-file-paths.test.js b/tests/prepare-file-paths.test.js index bc8ceca..c6f0ac0 100644 --- a/tests/prepare-file-paths.test.js +++ b/tests/prepare-file-paths.test.js @@ -8,26 +8,26 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_IMAGE_PATH = resolvePath(['images']); const DEFAULT_EXTENSIONS = ['gif', 'jpeg', 'jpg', 'png', 'svg']; -test('Non-existent file paths are ignored', () => { +test('Non-existent file paths are ignored', async () => { const paths = [ resolvePath(['not+exists']), resolvePath(['not+exists.svg']), ]; - expect(prepareFilePaths(paths, DEFAULT_EXTENSIONS)).toStrictEqual([]); + expect(await prepareFilePaths(paths, DEFAULT_EXTENSIONS)).toStrictEqual([]); }); -test('Files from subdirectories are processed', () => { - expect(prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).toEqual( +test('Files from subdirectories are processed', async () => { + expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).toEqual( expect.arrayContaining([ expect.stringMatching(/file-in-subdirectory.jpg$/), ]), ); }); -test('Files are filtered by extension', () => { +test('Files are filtered by extension', async () => { const extensions = ['gif', 'jpeg', 'png', 'svg']; - expect(prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).toEqual( + expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).toEqual( expect.arrayContaining([ expect.stringMatching(/\.gif$/), expect.stringMatching(/\.png$/), @@ -35,15 +35,15 @@ test('Files are filtered by extension', () => { ]), ); - expect(prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).not.toEqual( + expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).not.toEqual( expect.arrayContaining([ expect.stringMatching(/\.jpg$/), ]), ); }); -test('Only relative file paths are generated', () => { - expect(prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).not.toEqual( +test('Only relative file paths are generated', async () => { + expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).not.toEqual( expect.arrayContaining([ expect.stringMatching(new RegExp(`^${dirname}`)), ]), From 2d5da0cae1331bf1890a009991118673089c7db8 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:04:56 +0700 Subject: [PATCH 03/45] Uninstall fast-glob --- package-lock.json | 50 ++++++++++++++--------------------------------- package.json | 1 - 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8e8f02..62ddfc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "cli-progress": "^3.11.0", "commander": "^12.1.0", "exec-buffer": "^3.2.0", - "fast-glob": "^3.3.2", "fdir": "^6.3.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", @@ -1576,6 +1575,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1588,6 +1588,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -1596,6 +1597,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2504,6 +2506,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -4292,32 +4295,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4334,6 +4311,7 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -4413,6 +4391,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5286,6 +5265,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5315,6 +5295,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -5343,6 +5324,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -6872,18 +6854,11 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -6896,6 +6871,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -7717,6 +7693,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -8017,6 +7994,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -8044,6 +8022,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -8820,6 +8799,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index bf5bb74..a13f073 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "cli-progress": "^3.11.0", "commander": "^12.1.0", "exec-buffer": "^3.2.0", - "fast-glob": "^3.3.2", "fdir": "^6.3.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", From 0e6dcde39d0462bd3ad091b839d5f692ddb2a62a Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:57:31 +0700 Subject: [PATCH 04/45] Improve index.js --- index.js | 76 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 105ff24..9d70ede 100755 --- a/index.js +++ b/index.js @@ -8,31 +8,63 @@ import optimize from './lib/optimize.js'; import prepareFilePaths from './lib/prepare-file-paths.js'; import prepareOutputPath from './lib/prepare-output-path.js'; -export default async function optimizt({ paths, avif, webp, force, lossless, verbose, output, config }) { - const configFilepath = pathToFileURL(config ? checkConfigPath(config) : findConfig()); - const configData = await import(configFilepath); +const MODE_NAME = { + convert: 'convert', + optimize: 'optimize', +}; - const supportedFileTypes = { - convert: ['gif', 'jpeg', 'jpg', 'png'], - optimize: ['gif', 'jpeg', 'jpg', 'png', 'svg'], - }; +const SUPPORTED_FILE_TYPES = { + convert: ['gif', 'jpeg', 'jpg', 'png'], + optimize: ['gif', 'jpeg', 'jpg', 'png', 'svg'], +}; - if (verbose) { +export default async function optimizt({ + paths: inputPaths, + output: outputDirectoryPath, + config: configFilePath, + + avif: shouldConvertToAvif, + webp: shouldConvertToWebp, + + force: isForced, + lossless: isLossless, + verbose: isVerbose, +}) { + const shouldConvert = shouldConvertToAvif || shouldConvertToWebp; + + const currentMode = shouldConvert + ? MODE_NAME.convert + : MODE_NAME.optimize; + + const preparedConfigFilePath = pathToFileURL( + configFilePath + ? checkConfigPath(configFilePath) + : findConfig(), + ); + const configData = await import(preparedConfigFilePath); + const config = configData.default[currentMode]; + + const preparedInputPaths = await prepareFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode]); + const preparedOutputDirectoryPath = prepareOutputPath(outputDirectoryPath); + + if (isVerbose) { enableVerbose(); } - await (avif || webp ? convert({ - paths: await prepareFilePaths(paths, supportedFileTypes.convert), - lossless, - avif, - webp, - force, - output: prepareOutputPath(output), - config: configData.default.convert, - }) : optimize({ - paths: await prepareFilePaths(paths, supportedFileTypes.optimize), - lossless, - output: prepareOutputPath(output), - config: configData.default.optimize, - })); + const process = shouldConvert + ? convert + : optimize; + + await process({ + paths: preparedInputPaths, + output: preparedOutputDirectoryPath, + lossless: isLossless, + config, + + ...shouldConvert && { + avif: shouldConvertToAvif, + webp: shouldConvertToWebp, + force: isForced, + }, + }); } From 29cfadba63511344b45b0ae087f70932d113ab9d Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:25:44 +0700 Subject: [PATCH 05/45] Rename prepareFilePaths to prepareInputFilePaths --- index.js | 6 +++--- ...are-file-paths.js => prepare-input-file-paths.js} | 2 +- tests/prepare-file-paths.test.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) rename lib/{prepare-file-paths.js => prepare-input-file-paths.js} (94%) diff --git a/index.js b/index.js index 9d70ede..7251a21 100755 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ import convert from './lib/convert.js'; import findConfig from './lib/find-config.js'; import { enableVerbose } from './lib/log.js'; import optimize from './lib/optimize.js'; -import prepareFilePaths from './lib/prepare-file-paths.js'; +import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; import prepareOutputPath from './lib/prepare-output-path.js'; const MODE_NAME = { @@ -44,7 +44,7 @@ export default async function optimizt({ const configData = await import(preparedConfigFilePath); const config = configData.default[currentMode]; - const preparedInputPaths = await prepareFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode]); + const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode]); const preparedOutputDirectoryPath = prepareOutputPath(outputDirectoryPath); if (isVerbose) { @@ -56,7 +56,7 @@ export default async function optimizt({ : optimize; await process({ - paths: preparedInputPaths, + paths: preparedInputFilePaths, output: preparedOutputDirectoryPath, lossless: isLossless, config, diff --git a/lib/prepare-file-paths.js b/lib/prepare-input-file-paths.js similarity index 94% rename from lib/prepare-file-paths.js rename to lib/prepare-input-file-paths.js index b30868f..70c990d 100644 --- a/lib/prepare-file-paths.js +++ b/lib/prepare-input-file-paths.js @@ -3,7 +3,7 @@ import path from 'node:path'; import { fdir } from 'fdir'; -export default async function prepareFilePaths(paths, extensions) { +export default async function prepareInputFilePaths(paths, extensions) { const files = new Set(); const directories = new Set(); diff --git a/tests/prepare-file-paths.test.js b/tests/prepare-file-paths.test.js index c6f0ac0..b01109b 100644 --- a/tests/prepare-file-paths.test.js +++ b/tests/prepare-file-paths.test.js @@ -1,7 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import prepareFilePaths from '../lib/prepare-file-paths.js'; +import prepareInputFilePaths from '../lib/prepare-input-file-paths.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -13,11 +13,11 @@ test('Non-existent file paths are ignored', async () => { resolvePath(['not+exists']), resolvePath(['not+exists.svg']), ]; - expect(await prepareFilePaths(paths, DEFAULT_EXTENSIONS)).toStrictEqual([]); + expect(await prepareInputFilePaths(paths, DEFAULT_EXTENSIONS)).toStrictEqual([]); }); test('Files from subdirectories are processed', async () => { - expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).toEqual( + expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).toEqual( expect.arrayContaining([ expect.stringMatching(/file-in-subdirectory.jpg$/), ]), @@ -27,7 +27,7 @@ test('Files from subdirectories are processed', async () => { test('Files are filtered by extension', async () => { const extensions = ['gif', 'jpeg', 'png', 'svg']; - expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).toEqual( + expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], extensions)).toEqual( expect.arrayContaining([ expect.stringMatching(/\.gif$/), expect.stringMatching(/\.png$/), @@ -35,7 +35,7 @@ test('Files are filtered by extension', async () => { ]), ); - expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], extensions)).not.toEqual( + expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], extensions)).not.toEqual( expect.arrayContaining([ expect.stringMatching(/\.jpg$/), ]), @@ -43,7 +43,7 @@ test('Files are filtered by extension', async () => { }); test('Only relative file paths are generated', async () => { - expect(await prepareFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).not.toEqual( + expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).not.toEqual( expect.arrayContaining([ expect.stringMatching(new RegExp(`^${dirname}`)), ]), From f93b8cb4d69d5534eefe659c00dd08add5ffb420 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:11:43 +0700 Subject: [PATCH 06/45] Rename prepareOutputPath to prepareOutputDirectoryPath --- index.js | 4 ++-- ...re-output-path.js => prepare-output-directory-path.js} | 6 +++--- tests/prepare-output-path.test.js | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) rename lib/{prepare-output-path.js => prepare-output-directory-path.js} (68%) diff --git a/index.js b/index.js index 7251a21..7fa4555 100755 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ import findConfig from './lib/find-config.js'; import { enableVerbose } from './lib/log.js'; import optimize from './lib/optimize.js'; import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; -import prepareOutputPath from './lib/prepare-output-path.js'; +import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; const MODE_NAME = { convert: 'convert', @@ -45,7 +45,7 @@ export default async function optimizt({ const config = configData.default[currentMode]; const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode]); - const preparedOutputDirectoryPath = prepareOutputPath(outputDirectoryPath); + const preparedOutputDirectoryPath = prepareOutputDirectoryPath(outputDirectoryPath); if (isVerbose) { enableVerbose(); diff --git a/lib/prepare-output-path.js b/lib/prepare-output-directory-path.js similarity index 68% rename from lib/prepare-output-path.js rename to lib/prepare-output-directory-path.js index d880e11..f54ba9b 100644 --- a/lib/prepare-output-path.js +++ b/lib/prepare-output-directory-path.js @@ -3,12 +3,12 @@ import path from 'node:path'; import { logErrorAndExit } from './log.js'; -export default function prepareOutputPath(outputPath) { - if (!outputPath) { +export default function prepareOutputDirectoryPath(outputDirectoryPath) { + if (!outputDirectoryPath) { return ''; } - const resolvedPath = path.resolve(outputPath); + const resolvedPath = path.resolve(outputDirectoryPath); if (!fs.existsSync(resolvedPath)) { logErrorAndExit('Output path does not exist'); diff --git a/tests/prepare-output-path.test.js b/tests/prepare-output-path.test.js index 494c011..0295966 100644 --- a/tests/prepare-output-path.test.js +++ b/tests/prepare-output-path.test.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { jest } from '@jest/globals'; -import prepareOutputPath from '../lib/prepare-output-path.js'; +import prepareOutputDirectoryPath from '../lib/prepare-output-directory-path.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -14,7 +14,7 @@ test('Exit if the path does not exist', () => { console.log = jest.fn(); - expect(() => prepareOutputPath('not+exists')).toThrow(); + expect(() => prepareOutputDirectoryPath('not+exists')).toThrow(); expect(processExitMock).toHaveBeenCalledWith(1); expect(console.log.mock.calls[0][1]).toBe('Output path does not exist'); @@ -29,7 +29,7 @@ test('Exit if specified path to file instead of directory', () => { console.log = jest.fn(); - expect(() => prepareOutputPath(path.resolve(dirname, 'images', 'svg-not-optimized.svg'))).toThrow(); + expect(() => prepareOutputDirectoryPath(path.resolve(dirname, 'images', 'svg-not-optimized.svg'))).toThrow(); expect(processExitMock).toHaveBeenCalledWith(1); expect(console.log.mock.calls[0][1]).toBe('Output path must be a directory'); @@ -38,5 +38,5 @@ test('Exit if specified path to file instead of directory', () => { }); test('Full path is generated', () => { - expect(prepareOutputPath('tests/images')).toBe(path.resolve(dirname, 'images')); + expect(prepareOutputDirectoryPath('tests/images')).toBe(path.resolve(dirname, 'images')); }); From 28e4e2c0ba72eb638ac4a0b32ed7d9db7129c8ea Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:09:03 +0700 Subject: [PATCH 07/45] Rename convert & optimize functions arguments --- index.js | 12 ++++++------ lib/convert.js | 41 ++++++++++++++++++++++------------------- lib/optimize.js | 21 +++++++++++++-------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index 7fa4555..ec4fb18 100755 --- a/index.js +++ b/index.js @@ -56,15 +56,15 @@ export default async function optimizt({ : optimize; await process({ - paths: preparedInputFilePaths, - output: preparedOutputDirectoryPath, - lossless: isLossless, + inputFilePaths: preparedInputFilePaths, + outputDirectoryPath: preparedOutputDirectoryPath, + isLossless, config, ...shouldConvert && { - avif: shouldConvertToAvif, - webp: shouldConvertToWebp, - force: isForced, + shouldConvertToAvif, + shouldConvertToWebp, + isForced, }, }); } diff --git a/lib/convert.js b/lib/convert.js index 23f6e9c..108019c 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -18,22 +18,22 @@ import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; export default async function convert({ - paths, - lossless, - avif, - webp, - force, - output, + inputFilePaths, + outputDirectoryPath, + isLossless, config, + shouldConvertToAvif, + shouldConvertToWebp, + isForced, }) { - const totalPaths = paths.length; + const totalPaths = inputFilePaths.length; if (!totalPaths) { return; } - log(`Converting ${totalPaths} ${getPlural(totalPaths, 'image', 'images')} (${lossless ? 'lossless' : 'lossy'})...`); - if (lossless) { + log(`Converting ${totalPaths} ${getPlural(totalPaths, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + if (isLossless) { log('Lossless conversion may take a long time'); } @@ -49,31 +49,33 @@ export default async function convert({ const tasksErrors = []; - const tasks = paths.reduce((accumulator, filePath) => { - if (avif) { + const tasks = inputFilePaths.reduce((accumulator, filePath) => { + if (shouldConvertToAvif) { accumulator.push(limit(() => processImage({ + // TODO: Rename arguments convertFunction: createAvif, format: 'AVIF', progressBar, filePath, - lossless, - force, + lossless: isLossless, + force: isForced, tasksErrors, - output, + output: outputDirectoryPath, config: config?.avif || {}, }))); } - if (webp) { + if (shouldConvertToWebp) { accumulator.push(limit(() => processImage({ + // TODO: Rename arguments convertFunction: createWebp, format: 'WebP', progressBar, filePath, - lossless, - force, + lossless: isLossless, + force: isForced, tasksErrors, - output, + output: outputDirectoryPath, config: { webp: config?.webp || {}, webpGif: config?.webpGif || {}, @@ -99,11 +101,12 @@ export default async function convert({ totalSize.after += isOptimized ? fileSize.after : fileSize.before; writeResultFile({ + // TODO: Rename arguments fileBuffer, filePath, fileSize, format, - output, + output: outputDirectoryPath, }); } diff --git a/lib/optimize.js b/lib/optimize.js index 26cf4ec..afe7683 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -19,8 +19,13 @@ import optionsToArguments from './options-to-arguments.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; -export default async function optimize({ paths, lossless: isLossless, output, config }) { - const totalPaths = paths.length; +export default async function optimize({ + inputFilePaths, + outputDirectoryPath, + isLossless, + config, +}) { + const totalPaths = inputFilePaths.length; if (!totalPaths) { return; @@ -41,16 +46,16 @@ export default async function optimize({ paths, lossless: isLossless, output, co const limit = pLimit( /* - Guetzli uses a large amount of memory and a significant amount of CPU time. - To reduce the processor load in lossless mode, we reduce the number - of simultaneous tasks by half. - */ + Guetzli uses a large amount of memory and a significant amount of CPU time. + To reduce the processor load in lossless mode, we reduce the number + of simultaneous tasks by half. + */ isLossless ? Math.round(os.cpus().length / 2) : os.cpus().length, ); const tasksErrors = []; - const tasks = paths.map(filePath => limit(() => fs.promises.readFile(filePath) + const tasks = inputFilePaths.map(filePath => limit(() => fs.promises.readFile(filePath) .then(fileBuffer => optimizeByType({ fileBuffer, filePath, @@ -83,7 +88,7 @@ export default async function optimize({ paths, lossless: isLossless, output, co totalSize.before += fileSize.before; totalSize.after += isOptimized ? fileSize.after : fileSize.before; - checkResult(fileBuffer, filePath, fileSize, output); + checkResult(fileBuffer, filePath, fileSize, outputDirectoryPath); } for (const error of tasksErrors) { From 375c6896e0356a7bf3b747cc9b698d4bb5b2d97b Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:15:11 +0700 Subject: [PATCH 08/45] Improve optimize function --- lib/optimize.js | 296 +++++++++++++++++++++++++++--------------------- 1 file changed, 166 insertions(+), 130 deletions(-) diff --git a/lib/optimize.js b/lib/optimize.js index afe7683..e5412b8 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -19,110 +19,108 @@ import optionsToArguments from './options-to-arguments.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; -export default async function optimize({ - inputFilePaths, - outputDirectoryPath, - isLossless, - config, -}) { - const totalPaths = inputFilePaths.length; - - if (!totalPaths) { +export default async function optimize({ inputFilePaths, outputDirectoryPath, isLossless, config }) { + const inputFilePathsCount = inputFilePaths.length; + + if (inputFilePathsCount <= 0) { return; } - log(`Optimizing ${totalPaths} ${getPlural(totalPaths, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + log(`Optimizing ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + if (isLossless) { log('Lossless optimization may take a long time'); } const progressBar = new CliProgress.SingleBar({ - format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(totalPaths, 'image', 'images')}`, + format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, clearOnComplete: true, stopOnComplete: true, }, CliProgress.Presets.shades_classic); - progressBar.start(totalPaths, 0); + progressBar.start(inputFilePathsCount, 0); - const limit = pLimit( + const simultaneousTasksLimit = pLimit( /* Guetzli uses a large amount of memory and a significant amount of CPU time. To reduce the processor load in lossless mode, we reduce the number of simultaneous tasks by half. */ - isLossless ? Math.round(os.cpus().length / 2) : os.cpus().length, + isLossless + ? Math.round(os.cpus().length / 2) + : os.cpus().length, ); - const tasksErrors = []; - - const tasks = inputFilePaths.map(filePath => limit(() => fs.promises.readFile(filePath) - .then(fileBuffer => optimizeByType({ - fileBuffer, - filePath, - isLossless, - config, - }))) - .then(fileBuffer => { - progressBar.increment(); - return { fileBuffer, filePath }; - }) - .catch(error => { - progressBar.increment(); - tasksErrors.push([filePath, { - type: 'error', - description: (error.message || '').trim(), - }]); - })); + const tasksPromises = inputFilePaths.map( + filePath => simultaneousTasksLimit( + () => processFile({ + filePath, + config, + isLossless, + progressBar, + tasksErrors, + }), + ), + ); const totalSize = { before: 0, after: 0 }; - const tasksResult = await Promise.all(tasks); - - for (const { fileBuffer, filePath } of tasksResult.filter(Boolean)) { - const fileSize = { - before: fs.statSync(filePath).size, - after: fileBuffer.length, - }; - - const isOptimized = fileSize.before > fileSize.after; - - totalSize.before += fileSize.before; - totalSize.after += isOptimized ? fileSize.after : fileSize.before; - - checkResult(fileBuffer, filePath, fileSize, outputDirectoryPath); + const tasksResults = await Promise.all(tasksPromises); + + for (const { fileBuffer, filePath } of tasksResults) { + if (fileBuffer) { + const fileSize = { + before: fs.statSync(filePath).size, + after: fileBuffer.length, + }; + + totalSize.before += fileSize.before; + totalSize.after += Math.min(fileSize.before, fileSize.after); + + checkResult({ + fileBuffer, + filePath, + fileSize, + outputDirectoryPath, + }); + } } - for (const error of tasksErrors) { - log(...error); - } + handleTasksErrors(tasksErrors); console.log(); showTotal(totalSize.before, totalSize.after); } -function checkResult(fileBuffer, filePath, fileSize, output) { +function handleTasksErrors(errors) { + for (const error of errors) { + log(...error); + } +} + +function checkResult({ fileBuffer, filePath, fileSize, outputDirectoryPath }) { if (!Buffer.isBuffer(fileBuffer) || typeof filePath !== 'string') { return; } - const fileExtension = path.extname(filePath).toLowerCase(); - const before = formatBytes(fileSize.before); - const after = formatBytes(fileSize.after); const ratio = calcRatio(fileSize.before, fileSize.after); - const successMessage = `${before} → ${after}. Ratio: ${ratio}%`; - - const writeFilePath = prepareWriteFilePath(filePath, output); - const isChanged = !fs.readFileSync(filePath).equals(fileBuffer); const isOptimized = ratio > 0; - const isSvg = fileExtension === '.svg'; + const isChanged = !fs.readFileSync(filePath).equals(fileBuffer); + const isSvg = path.extname(filePath).toLowerCase() === '.svg'; if (isOptimized || (isChanged && isSvg)) { try { - fs.writeFileSync(writeFilePath, fileBuffer); + fs.writeFileSync( + prepareWriteFilePath(filePath, outputDirectoryPath), + fileBuffer, + ); + + const before = formatBytes(fileSize.before); + const after = formatBytes(fileSize.after); log(filePath, { type: isOptimized ? 'success' : 'warning', - description: successMessage, + description: `${before} → ${after}. Ratio: ${ratio}%`, }); } catch (error) { if (error.message) { @@ -142,86 +140,124 @@ function checkResult(fileBuffer, filePath, fileSize, output) { } } -async function optimizeByType({ fileBuffer, filePath, isLossless, config }) { - const fileExtension = path.extname(filePath).toLowerCase(); - const imageFormat = await getImageFormat(fileBuffer); +async function processFile({ filePath, config, isLossless, progressBar, tasksErrors }) { + try { + const fileBuffer = await processFileByFormat({ + filePath, + config, + isLossless, + }); - switch (fileExtension) { - case '.jpg': - case '.jpeg': { - if (imageFormat !== 'jpeg') { - return fileBuffer; - } + return { + fileBuffer, + filePath, + }; + } catch (error) { + tasksErrors.push([ + filePath, + { + type: 'error', + description: (error.message || '').trim(), + }, + ]); - return isLossless - ? execBuffer({ - bin: guetzli, - args: [ - ...optionsToArguments({ - options: config?.jpeg?.lossless || {}, - }), - execBuffer.input, - execBuffer.output, - ], - input: await sharp(fileBuffer) - .toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB) - .rotate() // Rotate image using information from EXIF Orientation tag - .jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp - .toBuffer(), - }) - : sharp(fileBuffer) - .rotate() // Rotate image using information from EXIF Orientation tag - .jpeg(config?.jpeg?.lossy || {}) - .toBuffer(); - } + return { + fileBuffer: undefined, + filePath, + }; + } finally { + progressBar.increment(); + } +} - case '.png': { - if (imageFormat !== 'png') { - return fileBuffer; - } +async function processFileByFormat({ filePath, config, isLossless }) { + const fileBuffer = await fs.promises.readFile(filePath); + const imageFormat = await getImageFormat(fileBuffer); - return sharp(fileBuffer) - .png((isLossless ? config?.png?.lossless : config?.png?.lossy) || {}) - .toBuffer(); - } + if (!imageFormat) { + throw new Error('Unknown file format'); + } - case '.gif': { - if (imageFormat !== 'gif') { - return fileBuffer; - } + switch (imageFormat) { + case 'jpeg': { + return processJpeg({ fileBuffer, config, isLossless }); + } - return execBuffer({ - bin: gifsicle, - args: [ - ...optionsToArguments({ - options: (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}, - concat: true, - }), - `--threads=${os.cpus().length}`, - '--no-warnings', - '--output', - execBuffer.output, - execBuffer.input, - ], - input: fileBuffer, - }); + case 'png': { + return processPng({ fileBuffer, config, isLossless }); } - case '.svg': { - if (imageFormat !== 'svg') { - return fileBuffer; - } + case 'gif': { + return processGif({ fileBuffer, config, isLossless }); + } - return Buffer.from( - svgoOptimize( - fileBuffer, - config.svg, - ).data, - ); + case 'svg': { + return processSvg({ fileBuffer, config }); } default: { - throw new Error(`Unsupported file type: "${fileExtension}"`); + throw new Error(`Unsupported image format: "${imageFormat}"`); } } } + +async function processJpeg({ fileBuffer, config, isLossless }) { + const sharpImage = sharp(fileBuffer) + .rotate(); // Rotate image using information from EXIF Orientation tag + + if (isLossless) { + const inputBuffer = await sharpImage + .toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB) + .jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp + .toBuffer(); + + return execBuffer({ + bin: guetzli, + args: [ + ...optionsToArguments({ + options: config?.jpeg?.lossless || {}, + }), + execBuffer.input, + execBuffer.output, + ], + input: inputBuffer, + }); + } + + return sharpImage + .jpeg(config?.jpeg?.lossy || {}) + .toBuffer(); +} + +function processPng({ fileBuffer, config, isLossless }) { + return sharp(fileBuffer) + .png(isLossless ? config?.png?.lossless : config?.png?.lossy || {}) + .toBuffer(); +} + +function processGif({ fileBuffer, config, isLossless }) { + return execBuffer({ + bin: gifsicle, + args: [ + ...optionsToArguments({ + options: (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}, + concat: true, + }), + `--threads=${os.cpus().length}`, + '--no-warnings', + '--output', + execBuffer.output, + execBuffer.input, + ], + input: fileBuffer, + }); +} + +function processSvg({ fileBuffer, config }) { + return Buffer.from( + svgoOptimize( + fileBuffer, + config.svg, + ).data, + ); +} From ff954ce96174743694a24e1e44dfb158a21f6d98 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:42:20 +0700 Subject: [PATCH 09/45] Add lib/constants.js --- index.js | 18 +++++++----------- lib/constants.js | 4 ++++ 2 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 lib/constants.js diff --git a/index.js b/index.js index ec4fb18..8fe9427 100755 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ import { pathToFileURL } from 'node:url'; import checkConfigPath from './lib/check-config-path.js'; +import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; import findConfig from './lib/find-config.js'; import { enableVerbose } from './lib/log.js'; @@ -9,13 +10,8 @@ import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; const MODE_NAME = { - convert: 'convert', - optimize: 'optimize', -}; - -const SUPPORTED_FILE_TYPES = { - convert: ['gif', 'jpeg', 'jpg', 'png'], - optimize: ['gif', 'jpeg', 'jpg', 'png', 'svg'], + CONVERT: 'convert', + OPTIMIZE: 'optimize', }; export default async function optimizt({ @@ -33,8 +29,8 @@ export default async function optimizt({ const shouldConvert = shouldConvertToAvif || shouldConvertToWebp; const currentMode = shouldConvert - ? MODE_NAME.convert - : MODE_NAME.optimize; + ? MODE_NAME.CONVERT + : MODE_NAME.OPTIMIZE; const preparedConfigFilePath = pathToFileURL( configFilePath @@ -42,9 +38,9 @@ export default async function optimizt({ : findConfig(), ); const configData = await import(preparedConfigFilePath); - const config = configData.default[currentMode]; + const config = configData.default[currentMode.toLowerCase()]; - const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode]); + const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode.toUpperCase()]); const preparedOutputDirectoryPath = prepareOutputDirectoryPath(outputDirectoryPath); if (isVerbose) { diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..1767221 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,4 @@ +export const SUPPORTED_FILE_TYPES = { + CONVERT: ['gif', 'jpeg', 'jpg', 'png'], + OPTIMIZE: ['gif', 'jpeg', 'jpg', 'png', 'svg'], +}; From 74ce5c74e05d96f485a05f17dc3417025d3e675c Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:11:02 +0700 Subject: [PATCH 10/45] Improve convert function --- lib/convert.js | 261 ++++++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 131 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 108019c..5ac33c9 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -9,6 +9,7 @@ import pLimit from 'p-limit'; import sharp from 'sharp'; import calcRatio from './calc-ratio.js'; +import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; import getImageFormat from './get-image-format.js'; import getPlural from './get-plural.js'; @@ -17,77 +18,85 @@ import optionsToArguments from './options-to-arguments.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; -export default async function convert({ - inputFilePaths, - outputDirectoryPath, - isLossless, - config, - shouldConvertToAvif, - shouldConvertToWebp, - isForced, -}) { - const totalPaths = inputFilePaths.length; - - if (!totalPaths) { +export default async function convert({ inputFilePaths, outputDirectoryPath, isLossless, config, shouldConvertToAvif, shouldConvertToWebp, isForced }) { + const inputFilePathsCount = inputFilePaths.length; + + if (!inputFilePathsCount) { return; } - log(`Converting ${totalPaths} ${getPlural(totalPaths, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + log(`Converting ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + if (isLossless) { log('Lossless conversion may take a long time'); } const progressBar = new CliProgress.SingleBar({ - format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(totalPaths, 'image', 'images')}`, + format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, clearOnComplete: true, stopOnComplete: true, }, CliProgress.Presets.shades_classic); - progressBar.start(totalPaths, 0); - - const limit = pLimit(os.cpus().length); + progressBar.start(inputFilePathsCount, 0); + const simultaneousTasksLimit = pLimit(os.cpus().length); const tasksErrors = []; - const tasks = inputFilePaths.reduce((accumulator, filePath) => { + const avifConfig = isLossless + ? config?.avif?.lossless + : config?.avif?.lossy; + + const webpConfig = isLossless + ? config?.webp?.lossless + : config?.webp?.lossy; + + const webpGifConfig = isLossless + ? config?.webpGif?.lossless + : config?.webpGif?.lossy; + + const tasksPromises = inputFilePaths.reduce((accumulator, filePath) => { if (shouldConvertToAvif) { - accumulator.push(limit(() => processImage({ - // TODO: Rename arguments - convertFunction: createAvif, - format: 'AVIF', - progressBar, - filePath, - lossless: isLossless, - force: isForced, - tasksErrors, - output: outputDirectoryPath, - config: config?.avif || {}, - }))); + accumulator.push( + simultaneousTasksLimit( + () => processFile({ + filePath, + config: avifConfig || {}, + progressBar, + tasksErrors, + format: 'AVIF', + processFunction: processAvif, + outputDirectoryPath, + isForced, + }), + ), + ); } if (shouldConvertToWebp) { - accumulator.push(limit(() => processImage({ - // TODO: Rename arguments - convertFunction: createWebp, - format: 'WebP', - progressBar, - filePath, - lossless: isLossless, - force: isForced, - tasksErrors, - output: outputDirectoryPath, - config: { - webp: config?.webp || {}, - webpGif: config?.webpGif || {}, - }, - }))); + accumulator.push( + simultaneousTasksLimit( + () => processFile({ + filePath, + config: (path.extname(filePath).toLowerCase() === '.gif' + ? webpGifConfig + : webpConfig) + || {}, + progressBar, + tasksErrors, + format: 'WebP', + processFunction: processWebp, + outputDirectoryPath, + isForced, + }), + ), + ); } return accumulator; }, []); const totalSize = { before: 0, after: 0 }; - const tasksResult = await Promise.all(tasks); + const tasksResult = await Promise.all(tasksPromises); for (const { fileBuffer, filePath, format } of tasksResult.filter(Boolean)) { const fileSize = { @@ -95,41 +104,45 @@ export default async function convert({ after: fileBuffer.length, }; - const isOptimized = fileSize.before > fileSize.after; - totalSize.before += fileSize.before; - totalSize.after += isOptimized ? fileSize.after : fileSize.before; + totalSize.after += Math.min(fileSize.before, fileSize.after); - writeResultFile({ - // TODO: Rename arguments + writeResult({ fileBuffer, filePath, fileSize, + outputDirectoryPath, format, - output: outputDirectoryPath, }); } - for (const error of tasksErrors) { - log(...error); - } + handleTasksErrors(tasksErrors); console.log(); showTotal(totalSize.before, totalSize.after); } -function getOutputFilePath(filePath, format) { +function handleTasksErrors(errors) { + for (const error of errors) { + log(...error); + } +} + +function prepareOutputFilePath(filePath, format) { const { dir, name } = path.parse(filePath); return path.join(dir, `${name}.${format.toLowerCase()}`); } -function writeResultFile({ fileBuffer, filePath, fileSize, format, output }) { +function writeResult({ fileBuffer, filePath, fileSize, outputDirectoryPath, format }) { if (!Buffer.isBuffer(fileBuffer) || typeof filePath !== 'string') { return; } try { - const writeFilePath = prepareWriteFilePath(getOutputFilePath(filePath, format), output); + const writeFilePath = prepareWriteFilePath( + prepareOutputFilePath(filePath, format), + outputDirectoryPath, + ); fs.writeFileSync(writeFilePath, fileBuffer); @@ -137,11 +150,9 @@ function writeResultFile({ fileBuffer, filePath, fileSize, format, output }) { const after = formatBytes(fileSize.after); const ratio = calcRatio(fileSize.before, fileSize.after); - const successMessage = `${before} → ${format} ${after}. Ratio: ${ratio}%`; - log(filePath, { type: 'success', - description: successMessage, + description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, }); } catch (error) { if (error.message) { @@ -155,91 +166,79 @@ function writeResultFile({ fileBuffer, filePath, fileSize, format, output }) { } } -function processImage({ - filePath, - convertFunction, - lossless, - format, - force, - progressBar, - tasksErrors, - output, - config, -}) { - const writeFilePath = prepareWriteFilePath(getOutputFilePath(filePath, format), output); - - return fs.promises.readFile(filePath) - .then(fileBuffer => { - if (!force && fs.existsSync(writeFilePath)) { - throw new Error(`File already exists, '${writeFilePath}'`); - } - - return convertFunction({ - fileBuffer, - fileExt: path.extname(filePath).toLowerCase(), - lossless, - config, - }); - }) - .then(fileBuffer => { - progressBar.increment(); - return { fileBuffer, filePath, format }; - }) - .catch(error => { - progressBar.increment(); +async function processFile({ filePath, config, progressBar, tasksErrors, format, processFunction, outputDirectoryPath, isForced }) { + try { + const fileBuffer = await fs.promises.readFile(filePath); + + const writeFilePath = prepareWriteFilePath( + prepareOutputFilePath(filePath, format), + outputDirectoryPath, + ); + + if (!isForced && fs.existsSync(writeFilePath)) { + throw new Error(`File already exists, '${writeFilePath}'`); + } + + return { + fileBuffer: await processFunction({ fileBuffer, config }), + filePath, + format, + }; + } catch (error) { + if (error.message) { tasksErrors.push([filePath, { type: 'error', description: (error.message || '').trim(), }]); - }); + } else { + console.error(error); + } + } finally { + progressBar.increment(); + } } -async function createAvif({ fileBuffer, lossless, config }) { - const imageFormat = await getImageFormat(fileBuffer); - - if (!['jpeg', 'png', 'gif'].includes(imageFormat)) { - return fileBuffer; - } +async function processAvif({ fileBuffer, config }) { + checkImageFormat(await getImageFormat(fileBuffer)); return sharp(fileBuffer) .rotate() // Rotate image using information from EXIF Orientation tag - .avif((lossless ? config?.lossless : config?.lossy) || {}) + .avif(config) .toBuffer(); } -async function createWebp({ fileBuffer, fileExt, lossless, config }) { +async function processWebp({ fileBuffer, config }) { const imageFormat = await getImageFormat(fileBuffer); + checkImageFormat(imageFormat); + + if (imageFormat === 'gif') { + return execBuffer({ + bin: gif2webp, + args: [ + ...optionsToArguments({ + options: config, + prefix: '-', + }), + execBuffer.input, + '-o', + execBuffer.output, + ], + input: fileBuffer, + }); + } - switch (fileExt) { - case '.gif': { - if (imageFormat !== 'gif') { - return fileBuffer; - } - - return execBuffer({ - bin: gif2webp, - args: [ - ...optionsToArguments({ - options: (lossless ? config?.webpGif?.lossless : config?.webpGif?.lossy) || {}, - prefix: '-', - }), - execBuffer.input, - '-o', - execBuffer.output, - ], - input: fileBuffer, - }); - } + return sharp(fileBuffer) + .rotate() // Rotate image using information from EXIF Orientation tag + .webp(config) + .toBuffer(); +} - default: { - if (!['jpeg', 'png'].includes(imageFormat)) { - return fileBuffer; - } +function checkImageFormat(imageFormat) { + if (!imageFormat) { + throw new Error('Unknown file format'); + } - return sharp(fileBuffer) - .rotate() // Rotate image using information from EXIF Orientation tag - .webp((lossless ? config?.webp?.lossless : config?.webp?.lossy) || {}) - .toBuffer(); - } + if (!SUPPORTED_FILE_TYPES.CONVERT.includes(imageFormat)) { + throw new Error(`Unsupported image format: "${imageFormat}"`); } } From 063a285b99d9d82bcfd3da9325337e9bfe087fca Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:40:29 +0700 Subject: [PATCH 11/45] Move lossless optimization notice to index.js --- index.js | 6 +++++- lib/convert.js | 4 ---- lib/optimize.js | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 8fe9427..f3188aa 100755 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import checkConfigPath from './lib/check-config-path.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; import findConfig from './lib/find-config.js'; -import { enableVerbose } from './lib/log.js'; +import log, { enableVerbose } from './lib/log.js'; import optimize from './lib/optimize.js'; import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; @@ -47,6 +47,10 @@ export default async function optimizt({ enableVerbose(); } + if (isLossless) { + log('Lossless optimization may take a long time'); + } + const process = shouldConvert ? convert : optimize; diff --git a/lib/convert.js b/lib/convert.js index 5ac33c9..f12fdac 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -27,10 +27,6 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL log(`Converting ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); - if (isLossless) { - log('Lossless conversion may take a long time'); - } - const progressBar = new CliProgress.SingleBar({ format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, clearOnComplete: true, diff --git a/lib/optimize.js b/lib/optimize.js index e5412b8..791d523 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -28,10 +28,6 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is log(`Optimizing ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); - if (isLossless) { - log('Lossless optimization may take a long time'); - } - const progressBar = new CliProgress.SingleBar({ format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, clearOnComplete: true, From b5576c981e5f8cccb304bb884830f4fa08bdf93f Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:24:50 +0700 Subject: [PATCH 12/45] Add notice about Animated AVIF is not supported --- lib/convert.js | 17 +++++++++---- lib/get-image-format.js | 8 ------ lib/optimize.js | 10 ++++---- lib/parse-image-metadata.js | 12 +++++++++ tests/cli.test.js | 26 +++++++++++-------- tests/get-image-format.test.js | 32 ----------------------- tests/parse-image-metadata.test.js | 41 ++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 60 deletions(-) delete mode 100644 lib/get-image-format.js create mode 100644 lib/parse-image-metadata.js delete mode 100644 tests/get-image-format.test.js create mode 100644 tests/parse-image-metadata.test.js diff --git a/lib/convert.js b/lib/convert.js index f12fdac..c0a47fa 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -11,10 +11,10 @@ import sharp from 'sharp'; import calcRatio from './calc-ratio.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; -import getImageFormat from './get-image-format.js'; import getPlural from './get-plural.js'; import log from './log.js'; import optionsToArguments from './options-to-arguments.js'; +import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; @@ -195,7 +195,14 @@ async function processFile({ filePath, config, progressBar, tasksErrors, format, } async function processAvif({ fileBuffer, config }) { - checkImageFormat(await getImageFormat(fileBuffer)); + const imageMetadata = await parseImageMetadata(fileBuffer); + checkImageFormat(imageMetadata.format); + + const isAnimated = imageMetadata.pages > 1; + + if (isAnimated) { + throw new Error('Animated AVIF is not supported'); // See: https://github.com/strukturag/libheif/issues/377 + } return sharp(fileBuffer) .rotate() // Rotate image using information from EXIF Orientation tag @@ -204,10 +211,10 @@ async function processAvif({ fileBuffer, config }) { } async function processWebp({ fileBuffer, config }) { - const imageFormat = await getImageFormat(fileBuffer); - checkImageFormat(imageFormat); + const imageMetadata = await parseImageMetadata(fileBuffer); + checkImageFormat(imageMetadata.format); - if (imageFormat === 'gif') { + if (imageMetadata.format === 'gif') { return execBuffer({ bin: gif2webp, args: [ diff --git a/lib/get-image-format.js b/lib/get-image-format.js deleted file mode 100644 index bd75f13..0000000 --- a/lib/get-image-format.js +++ /dev/null @@ -1,8 +0,0 @@ -import sharp from 'sharp'; - -export default async function getImageFormat(buffer) { - try { - const metadata = await sharp(buffer).metadata(); - return metadata.format; - } catch {} -} diff --git a/lib/optimize.js b/lib/optimize.js index 791d523..5b985a0 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -12,10 +12,10 @@ import { optimize as svgoOptimize } from 'svgo'; import calcRatio from './calc-ratio.js'; import formatBytes from './format-bytes.js'; -import getImageFormat from './get-image-format.js'; import getPlural from './get-plural.js'; import log from './log.js'; import optionsToArguments from './options-to-arguments.js'; +import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; @@ -168,13 +168,13 @@ async function processFile({ filePath, config, isLossless, progressBar, tasksErr async function processFileByFormat({ filePath, config, isLossless }) { const fileBuffer = await fs.promises.readFile(filePath); - const imageFormat = await getImageFormat(fileBuffer); + const imageMetadata = await parseImageMetadata(fileBuffer); - if (!imageFormat) { + if (!imageMetadata.format) { throw new Error('Unknown file format'); } - switch (imageFormat) { + switch (imageMetadata.format) { case 'jpeg': { return processJpeg({ fileBuffer, config, isLossless }); } @@ -192,7 +192,7 @@ async function processFileByFormat({ filePath, config, isLossless }) { } default: { - throw new Error(`Unsupported image format: "${imageFormat}"`); + throw new Error(`Unsupported image format: "${imageMetadata.format}"`); } } } diff --git a/lib/parse-image-metadata.js b/lib/parse-image-metadata.js new file mode 100644 index 0000000..3167f8b --- /dev/null +++ b/lib/parse-image-metadata.js @@ -0,0 +1,12 @@ +import sharp from 'sharp'; + +export default async function parseImageMetadata(buffer) { + try { + // Fast access to (uncached) image metadata without decoding any compressed pixel data. + // This is read from the header of the input image. It does not take into consideration any operations to be applied to the output image, such as resize or rotate. + const metadata = await sharp(buffer).metadata(); + return metadata; + } catch { + return {}; + } +} diff --git a/tests/cli.test.js b/tests/cli.test.js index b8d3921..821a54c 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -147,14 +147,13 @@ describe('CLI', () => { expectFileNotModified(file); }); - test('GIF should be converted', () => { + test('GIF should not be converted', () => { const file = 'gif-not-optimized.gif'; const stdout = runCliWithParameters(`--avif ${workDirectory}${file}`); - expectFileRatio({ - stdout, file, maxRatio: 95, minRatio: 90, outputExt: 'avif', - }); + expectStringContains(stdout, 'Animated AVIF is not supported'); expectFileNotModified(file); + expectFileNotExists('gif-not-optimized.avif'); }); test('Files in provided directory should be converted', () => { @@ -183,25 +182,27 @@ describe('CLI', () => { expectFileNotModified(file); }); - test('GIF should be converted', () => { + test('GIF should not be converted', () => { const file = 'gif-not-optimized.gif'; const stdout = runCliWithParameters(`--avif --lossless ${workDirectory}${file}`); - expectFileRatio({ - stdout, file, maxRatio: 75, minRatio: 70, outputExt: 'avif', - }); + expectStringContains(stdout, 'Animated AVIF is not supported'); expectFileNotModified(file); + expectFileNotExists('gif-not-optimized.avif'); }); - test('Files in provided directory should be converted', () => { + test('Files in provided directory should be converted (except GIF)', () => { const fileBasename = 'png-not-optimized'; const stdout = runCliWithParameters(`--avif --lossless ${workDirectory}`); const stdoutRatio = grepTotalRatio(stdout); expectStringContains(stdout, 'Converting 6 images (lossless)...'); - expectRatio(stdoutRatio, 45, 50); + expectRatio(stdoutRatio, 27, 31); expectFileNotModified(`${fileBasename}.png`); expectFileExists(`${fileBasename}.avif`); + + expectStringContains(stdout, 'Animated AVIF is not supported'); + expectFileNotExists('gif-not-optimized.avif'); }); }); }); @@ -534,3 +535,8 @@ function expectFileExists(fileName) { const isFileExists = fs.existsSync(path.join(temporary, fileName)); expect(isFileExists).toBe(true); } + +function expectFileNotExists(fileName) { + const isFileExists = fs.existsSync(path.join(temporary, fileName)); + expect(isFileExists).toBe(false); +} diff --git a/tests/get-image-format.test.js b/tests/get-image-format.test.js deleted file mode 100644 index f19870e..0000000 --- a/tests/get-image-format.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import getImageFormat from '../lib/get-image-format.js'; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); - -function readFile(filePath) { - return fs.readFileSync(path.resolve(dirname, filePath)); -} - -const gifBuffer = readFile('images/gif-not-optimized.gif'); -const jpegBuffer = readFile('images/jpeg-one-pixel.jpg'); -const pngBuffer = readFile('images/png-not-optimized.png'); -const svgBuffer = readFile('images/svg-optimized.svg'); - -test('GIF should be detected as “gif”', async () => { - await expect(getImageFormat(gifBuffer)).resolves.toBe('gif'); -}); - -test('JPEG should be detected as “jpeg”', async () => { - await expect(getImageFormat(jpegBuffer)).resolves.toBe('jpeg'); -}); - -test('PNG should be detected as “png”', async () => { - await expect(getImageFormat(pngBuffer)).resolves.toBe('png'); -}); - -test('SVG should be detected as “svg”', async () => { - await expect(getImageFormat(svgBuffer)).resolves.toBe('svg'); -}); diff --git a/tests/parse-image-metadata.test.js b/tests/parse-image-metadata.test.js new file mode 100644 index 0000000..1c9672e --- /dev/null +++ b/tests/parse-image-metadata.test.js @@ -0,0 +1,41 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import parseImageMetadata from '../lib/parse-image-metadata.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +function readFile(filePath) { + return fs.readFileSync(path.resolve(dirname, filePath)); +} + +const gifBuffer = readFile('images/gif-not-optimized.gif'); +const jpegBuffer = readFile('images/jpeg-one-pixel.jpg'); +const pngBuffer = readFile('images/png-not-optimized.png'); +const svgBuffer = readFile('images/svg-optimized.svg'); + +const gifMetadata = await parseImageMetadata(gifBuffer); +const jpegMetadata = await parseImageMetadata(jpegBuffer); +const pngMetadata = await parseImageMetadata(pngBuffer); +const svgMetadata = await parseImageMetadata(svgBuffer); + +test('Format: GIF should be detected as “gif”', async () => { + expect(gifMetadata.format).toBe('gif'); +}); + +test('Format: JPEG should be detected as “jpeg”', async () => { + expect(jpegMetadata.format).toBe('jpeg'); +}); + +test('Format: PNG should be detected as “png”', async () => { + expect(pngMetadata.format).toBe('png'); +}); + +test('Format: SVG should be detected as “svg”', async () => { + expect(svgMetadata.format).toBe('svg'); +}); + +test('Pages: Frames count should be detected in animated GIF', async () => { + expect(gifMetadata.pages).toBe(10); +}); From 0dc02cc5328ebde36681116e91e5f7b0f4ae6ecd Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:52:05 +0700 Subject: [PATCH 13/45] Change image optimization workflow --- lib/optimize.js | 146 +++++++++++++++++------------------------------- 1 file changed, 52 insertions(+), 94 deletions(-) diff --git a/lib/optimize.js b/lib/optimize.js index 5b985a0..12cd889 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -36,138 +36,96 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is progressBar.start(inputFilePathsCount, 0); + const cpuCount = os.cpus().length; const simultaneousTasksLimit = pLimit( /* Guetzli uses a large amount of memory and a significant amount of CPU time. To reduce the processor load in lossless mode, we reduce the number of simultaneous tasks by half. */ - isLossless - ? Math.round(os.cpus().length / 2) - : os.cpus().length, + isLossless ? Math.round(cpuCount / 2) : cpuCount, ); - const tasksErrors = []; + + const totalSize = { before: 0, after: 0 }; + + const tasksLogs = []; const tasksPromises = inputFilePaths.map( filePath => simultaneousTasksLimit( () => processFile({ filePath, + outputDirectoryPath, config, isLossless, progressBar, - tasksErrors, + totalSize, + tasksLogs, }), ), ); - const totalSize = { before: 0, after: 0 }; - const tasksResults = await Promise.all(tasksPromises); - - for (const { fileBuffer, filePath } of tasksResults) { - if (fileBuffer) { - const fileSize = { - before: fs.statSync(filePath).size, - after: fileBuffer.length, - }; + await Promise.all(tasksPromises); - totalSize.before += fileSize.before; - totalSize.after += Math.min(fileSize.before, fileSize.after); - - checkResult({ - fileBuffer, - filePath, - fileSize, - outputDirectoryPath, - }); - } + for (const message of tasksLogs) { + log(...message); } - handleTasksErrors(tasksErrors); - console.log(); showTotal(totalSize.before, totalSize.after); } -function handleTasksErrors(errors) { - for (const error of errors) { - log(...error); - } -} +async function processFile({ filePath, outputDirectoryPath, config, isLossless, progressBar, totalSize, tasksLogs }) { + try { + const fileBuffer = await fs.promises.readFile(filePath); + const processedFileBuffer = await processFileByFormat({ fileBuffer, config, isLossless }); -function checkResult({ fileBuffer, filePath, fileSize, outputDirectoryPath }) { - if (!Buffer.isBuffer(fileBuffer) || typeof filePath !== 'string') { - return; - } + const fileSize = fileBuffer.length; + const processedFileSize = processedFileBuffer.length; + + totalSize.before += fileSize; + totalSize.after += Math.min(fileSize, processedFileSize); + + const ratio = calcRatio(fileSize, processedFileSize); + + const isOptimized = ratio > 0; + const isChanged = !fileBuffer.equals(processedFileBuffer); + const isSvg = path.extname(filePath).toLowerCase() === '.svg'; - const ratio = calcRatio(fileSize.before, fileSize.after); - - const isOptimized = ratio > 0; - const isChanged = !fs.readFileSync(filePath).equals(fileBuffer); - const isSvg = path.extname(filePath).toLowerCase() === '.svg'; - - if (isOptimized || (isChanged && isSvg)) { - try { - fs.writeFileSync( - prepareWriteFilePath(filePath, outputDirectoryPath), - fileBuffer, - ); - - const before = formatBytes(fileSize.before); - const after = formatBytes(fileSize.after); - - log(filePath, { - type: isOptimized ? 'success' : 'warning', - description: `${before} → ${after}. Ratio: ${ratio}%`, - }); - } catch (error) { - if (error.message) { - log(filePath, { - type: 'error', - description: error.message, - }); - } else { - console.error(error); - } + if (!isOptimized && !(isChanged && isSvg)) { + tasksLogs.push([filePath, { + description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, + verboseOnly: true, + }]); + + return; } - } else { - log(filePath, { - description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, - verboseOnly: true, - }); - } -} -async function processFile({ filePath, config, isLossless, progressBar, tasksErrors }) { - try { - const fileBuffer = await processFileByFormat({ - filePath, - config, - isLossless, - }); + await fs.promises.writeFile( + prepareWriteFilePath(filePath, outputDirectoryPath), + processedFileBuffer, + ); - return { - fileBuffer, - filePath, - }; + const before = formatBytes(fileSize); + const after = formatBytes(processedFileSize); + + tasksLogs.push([filePath, { + type: isOptimized ? 'success' : 'warning', + description: `${before} → ${after}. Ratio: ${ratio}%`, + }]); } catch (error) { - tasksErrors.push([ - filePath, - { + if (error.message) { + tasksLogs.push([filePath, { type: 'error', description: (error.message || '').trim(), - }, - ]); - - return { - fileBuffer: undefined, - filePath, - }; + }]); + } else { + console.error(error); + } } finally { progressBar.increment(); } } -async function processFileByFormat({ filePath, config, isLossless }) { - const fileBuffer = await fs.promises.readFile(filePath); +async function processFileByFormat({ fileBuffer, config, isLossless }) { const imageMetadata = await parseImageMetadata(fileBuffer); if (!imageMetadata.format) { From 1bb84e441399c891f0a70b51ea74f1f13d753512 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:30:37 +0700 Subject: [PATCH 14/45] Change image convert workflow --- lib/check-file-exists.js | 10 ++++ lib/convert.js | 121 +++++++++++++-------------------------- 2 files changed, 49 insertions(+), 82 deletions(-) create mode 100644 lib/check-file-exists.js diff --git a/lib/check-file-exists.js b/lib/check-file-exists.js new file mode 100644 index 0000000..8ff4c6b --- /dev/null +++ b/lib/check-file-exists.js @@ -0,0 +1,10 @@ +import { access } from 'node:fs/promises'; + +export default async function checkFileExists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} diff --git a/lib/convert.js b/lib/convert.js index c0a47fa..0350fe9 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -9,6 +9,7 @@ import pLimit from 'p-limit'; import sharp from 'sharp'; import calcRatio from './calc-ratio.js'; +import checkFileExists from './check-file-exists.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; @@ -36,7 +37,6 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL progressBar.start(inputFilePathsCount, 0); const simultaneousTasksLimit = pLimit(os.cpus().length); - const tasksErrors = []; const avifConfig = isLossless ? config?.avif?.lossless @@ -50,19 +50,23 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL ? config?.webpGif?.lossless : config?.webpGif?.lossy; + const totalSize = { before: 0, after: 0 }; + + const tasksLogs = []; const tasksPromises = inputFilePaths.reduce((accumulator, filePath) => { if (shouldConvertToAvif) { accumulator.push( simultaneousTasksLimit( () => processFile({ filePath, + outputDirectoryPath, config: avifConfig || {}, + isForced, progressBar, - tasksErrors, + totalSize, + tasksLogs, format: 'AVIF', processFunction: processAvif, - outputDirectoryPath, - isForced, }), ), ); @@ -73,16 +77,17 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL simultaneousTasksLimit( () => processFile({ filePath, + outputDirectoryPath, config: (path.extname(filePath).toLowerCase() === '.gif' ? webpGifConfig : webpConfig) || {}, + isForced, progressBar, - tasksErrors, + totalSize, + tasksLogs, format: 'WebP', processFunction: processWebp, - outputDirectoryPath, - isForced, }), ), ); @@ -91,98 +96,50 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL return accumulator; }, []); - const totalSize = { before: 0, after: 0 }; - const tasksResult = await Promise.all(tasksPromises); - - for (const { fileBuffer, filePath, format } of tasksResult.filter(Boolean)) { - const fileSize = { - before: fs.statSync(filePath).size, - after: fileBuffer.length, - }; - - totalSize.before += fileSize.before; - totalSize.after += Math.min(fileSize.before, fileSize.after); - - writeResult({ - fileBuffer, - filePath, - fileSize, - outputDirectoryPath, - format, - }); - } + await Promise.all(tasksPromises); - handleTasksErrors(tasksErrors); + for (const message of tasksLogs) { + log(...message); + } console.log(); showTotal(totalSize.before, totalSize.after); } -function handleTasksErrors(errors) { - for (const error of errors) { - log(...error); - } -} - -function prepareOutputFilePath(filePath, format) { - const { dir, name } = path.parse(filePath); - return path.join(dir, `${name}.${format.toLowerCase()}`); -} - -function writeResult({ fileBuffer, filePath, fileSize, outputDirectoryPath, format }) { - if (!Buffer.isBuffer(fileBuffer) || typeof filePath !== 'string') { - return; - } - +async function processFile({ filePath, outputDirectoryPath, config, isForced, progressBar, totalSize, tasksLogs, format, processFunction }) { try { - const writeFilePath = prepareWriteFilePath( - prepareOutputFilePath(filePath, format), - outputDirectoryPath, - ); - - fs.writeFileSync(writeFilePath, fileBuffer); + const { dir, name } = path.parse(filePath); + const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); + const preparedOutputFilePath = prepareWriteFilePath(outputFilePath, outputDirectoryPath); - const before = formatBytes(fileSize.before); - const after = formatBytes(fileSize.after); - const ratio = calcRatio(fileSize.before, fileSize.after); + const isFileExists = await checkFileExists(preparedOutputFilePath); - log(filePath, { - type: 'success', - description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, - }); - } catch (error) { - if (error.message) { - log(filePath, { - type: 'error', - description: error.message, - }); - } else { - console.error(error); + if (!isForced && isFileExists) { + throw new Error(`File already exists, '${preparedOutputFilePath}'`); } - } -} -async function processFile({ filePath, config, progressBar, tasksErrors, format, processFunction, outputDirectoryPath, isForced }) { - try { const fileBuffer = await fs.promises.readFile(filePath); + const processedFileBuffer = await processFunction({ fileBuffer, config }); - const writeFilePath = prepareWriteFilePath( - prepareOutputFilePath(filePath, format), - outputDirectoryPath, - ); + await fs.promises.writeFile(preparedOutputFilePath, processedFileBuffer); - if (!isForced && fs.existsSync(writeFilePath)) { - throw new Error(`File already exists, '${writeFilePath}'`); - } + const fileSize = fileBuffer.length; + const processedFileSize = processedFileBuffer.length; - return { - fileBuffer: await processFunction({ fileBuffer, config }), - filePath, - format, - }; + totalSize.before += fileSize; + totalSize.after += Math.min(fileSize, processedFileSize); + + const ratio = calcRatio(fileSize, processedFileSize); + const before = formatBytes(fileSize); + const after = formatBytes(processedFileSize); + + tasksLogs.push([filePath, { + type: 'success', + description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, + }]); } catch (error) { if (error.message) { - tasksErrors.push([filePath, { + tasksLogs.push([filePath, { type: 'error', description: (error.message || '').trim(), }]); From b02519b46bf3bad8b432f5a14935537f575465d5 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:18:55 +0700 Subject: [PATCH 15/45] Fix progress bar in convert mode --- lib/convert.js | 21 ++++++++++----------- lib/optimize.js | 15 +++++++-------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 0350fe9..6af0c2b 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -28,35 +28,32 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL log(`Converting ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + const progressBarTotal = shouldConvertToAvif && shouldConvertToWebp + ? inputFilePathsCount * 2 + : inputFilePathsCount; const progressBar = new CliProgress.SingleBar({ - format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, + format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(progressBarTotal, 'image', 'images')}`, clearOnComplete: true, - stopOnComplete: true, }, CliProgress.Presets.shades_classic); - progressBar.start(inputFilePathsCount, 0); - - const simultaneousTasksLimit = pLimit(os.cpus().length); + const totalSize = { before: 0, after: 0 }; const avifConfig = isLossless ? config?.avif?.lossless : config?.avif?.lossy; - const webpConfig = isLossless ? config?.webp?.lossless : config?.webp?.lossy; - const webpGifConfig = isLossless ? config?.webpGif?.lossless : config?.webpGif?.lossy; - const totalSize = { before: 0, after: 0 }; - const tasksLogs = []; + const tasksSimultaneousLimit = pLimit(os.cpus().length); const tasksPromises = inputFilePaths.reduce((accumulator, filePath) => { if (shouldConvertToAvif) { accumulator.push( - simultaneousTasksLimit( + tasksSimultaneousLimit( () => processFile({ filePath, outputDirectoryPath, @@ -74,7 +71,7 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL if (shouldConvertToWebp) { accumulator.push( - simultaneousTasksLimit( + tasksSimultaneousLimit( () => processFile({ filePath, outputDirectoryPath, @@ -96,7 +93,9 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL return accumulator; }, []); + progressBar.start(progressBarTotal, 0); await Promise.all(tasksPromises); + progressBar.stop(); for (const message of tasksLogs) { log(...message); diff --git a/lib/optimize.js b/lib/optimize.js index 12cd889..af9bf63 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -31,13 +31,14 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is const progressBar = new CliProgress.SingleBar({ format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, clearOnComplete: true, - stopOnComplete: true, }, CliProgress.Presets.shades_classic); - progressBar.start(inputFilePathsCount, 0); + const totalSize = { before: 0, after: 0 }; const cpuCount = os.cpus().length; - const simultaneousTasksLimit = pLimit( + + const tasksLogs = []; + const tasksSimultaneousLimit = pLimit( /* Guetzli uses a large amount of memory and a significant amount of CPU time. To reduce the processor load in lossless mode, we reduce the number @@ -45,12 +46,8 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is */ isLossless ? Math.round(cpuCount / 2) : cpuCount, ); - - const totalSize = { before: 0, after: 0 }; - - const tasksLogs = []; const tasksPromises = inputFilePaths.map( - filePath => simultaneousTasksLimit( + filePath => tasksSimultaneousLimit( () => processFile({ filePath, outputDirectoryPath, @@ -63,7 +60,9 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is ), ); + progressBar.start(inputFilePathsCount, 0); await Promise.all(tasksPromises); + progressBar.stop(); for (const message of tasksLogs) { log(...message); From 09bdb8ff9b956c9e2609754d482d9dc99a074bf6 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:29:28 +0700 Subject: [PATCH 16/45] Add interactive progress log --- index.js | 6 +-- lib/convert.js | 63 +++++++++++++++----------- lib/create-progress-bar-container.js | 10 +++++ lib/log.js | 51 +++++++++++++++++---- lib/optimize.js | 66 ++++++++++++++++------------ lib/show-total.js | 2 +- tests/log.test.js | 50 +++------------------ 7 files changed, 138 insertions(+), 110 deletions(-) create mode 100644 lib/create-progress-bar-container.js diff --git a/index.js b/index.js index f3188aa..718f252 100755 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import checkConfigPath from './lib/check-config-path.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; import findConfig from './lib/find-config.js'; -import log, { enableVerbose } from './lib/log.js'; +import { enableVerbose, log } from './lib/log.js'; import optimize from './lib/optimize.js'; import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; @@ -51,11 +51,11 @@ export default async function optimizt({ log('Lossless optimization may take a long time'); } - const process = shouldConvert + const processFunction = shouldConvert ? convert : optimize; - await process({ + await processFunction({ inputFilePaths: preparedInputFilePaths, outputDirectoryPath: preparedOutputDirectoryPath, isLossless, diff --git a/lib/convert.js b/lib/convert.js index 6af0c2b..9251fbc 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -2,7 +2,6 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import CliProgress from 'cli-progress'; import execBuffer from 'exec-buffer'; import gif2webp from 'gif2webp-bin'; import pLimit from 'p-limit'; @@ -10,16 +9,25 @@ import sharp from 'sharp'; import calcRatio from './calc-ratio.js'; import checkFileExists from './check-file-exists.js'; +import createProgressBarContainer from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; -import log from './log.js'; +import { log, logEmptyLine, logProgress } from './log.js'; import optionsToArguments from './options-to-arguments.js'; import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; -export default async function convert({ inputFilePaths, outputDirectoryPath, isLossless, config, shouldConvertToAvif, shouldConvertToWebp, isForced }) { +export default async function convert({ + inputFilePaths, + outputDirectoryPath, + isLossless, + config, + shouldConvertToAvif, + shouldConvertToWebp, + isForced, +}) { const inputFilePathsCount = inputFilePaths.length; if (!inputFilePathsCount) { @@ -31,10 +39,8 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL const progressBarTotal = shouldConvertToAvif && shouldConvertToWebp ? inputFilePathsCount * 2 : inputFilePathsCount; - const progressBar = new CliProgress.SingleBar({ - format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(progressBarTotal, 'image', 'images')}`, - clearOnComplete: true, - }, CliProgress.Presets.shades_classic); + const progressBarContainer = createProgressBarContainer(progressBarTotal); + const progressBar = progressBarContainer.create(progressBarTotal, 0); const totalSize = { before: 0, after: 0 }; @@ -48,7 +54,6 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL ? config?.webpGif?.lossless : config?.webpGif?.lossy; - const tasksLogs = []; const tasksSimultaneousLimit = pLimit(os.cpus().length); const tasksPromises = inputFilePaths.reduce((accumulator, filePath) => { if (shouldConvertToAvif) { @@ -58,10 +63,10 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL filePath, outputDirectoryPath, config: avifConfig || {}, - isForced, + progressBarContainer, progressBar, totalSize, - tasksLogs, + isForced, format: 'AVIF', processFunction: processAvif, }), @@ -79,10 +84,10 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL ? webpGifConfig : webpConfig) || {}, - isForced, + progressBarContainer, progressBar, totalSize, - tasksLogs, + isForced, format: 'WebP', processFunction: processWebp, }), @@ -93,19 +98,25 @@ export default async function convert({ inputFilePaths, outputDirectoryPath, isL return accumulator; }, []); - progressBar.start(progressBarTotal, 0); await Promise.all(tasksPromises); - progressBar.stop(); + progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 + progressBarContainer.stop(); - for (const message of tasksLogs) { - log(...message); - } - - console.log(); + logEmptyLine(); showTotal(totalSize.before, totalSize.after); } -async function processFile({ filePath, outputDirectoryPath, config, isForced, progressBar, totalSize, tasksLogs, format, processFunction }) { +async function processFile({ + filePath, + outputDirectoryPath, + config, + progressBarContainer, + progressBar, + totalSize, + isForced, + format, + processFunction, +}) { try { const { dir, name } = path.parse(filePath); const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); @@ -132,18 +143,20 @@ async function processFile({ filePath, outputDirectoryPath, config, isForced, pr const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - tasksLogs.push([filePath, { + logProgress(filePath, { type: 'success', description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, - }]); + progressBarContainer, + }); } catch (error) { if (error.message) { - tasksLogs.push([filePath, { + logProgress(filePath, { type: 'error', description: (error.message || '').trim(), - }]); + progressBarContainer, + }); } else { - console.error(error); + progressBarContainer.log(error); } } finally { progressBar.increment(); diff --git a/lib/create-progress-bar-container.js b/lib/create-progress-bar-container.js new file mode 100644 index 0000000..c43e4d6 --- /dev/null +++ b/lib/create-progress-bar-container.js @@ -0,0 +1,10 @@ +import CliProgress from 'cli-progress'; + +import getPlural from './get-plural.js'; + +export default function createCliProgressContainer(totalCount) { + return new CliProgress.MultiBar({ + format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(totalCount, 'image', 'images')}`, + clearOnComplete: true, + }, CliProgress.Presets.shades_classic); +} diff --git a/lib/log.js b/lib/log.js index 209a5f3..ecd1031 100644 --- a/lib/log.js +++ b/lib/log.js @@ -22,23 +22,56 @@ function enableVerbose() { isVerbose = true; } -function log(title = '', { type = 'info', description, verboseOnly } = {}) { - if (!isVerbose && verboseOnly && type === 'info') { - return; +function formatLogMessage(title, { type = 'info', description } = {}) { + if (!title) { + throw new Error('Title is required'); } - console.log( + /* + We use an array to create the message so that we can conveniently test + the content of `console.log` in tests + */ + return [ colorize(symbols[type][symbolIndex])[colors[type]], title, ...description ? [EOL, ' ', colorize(description).dim] : [], - ); + ]; +} + +function log(title, { type, description } = {}) { + console.log(...formatLogMessage(title, { type, description })); } function logErrorAndExit(title) { log(title, { type: 'error' }); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); + process.exit(1); // eslint-disable-line unicorn/no-process-exit +} + +function logEmptyLine() { + console.log(); } -export { enableVerbose, logErrorAndExit }; -export default log; +function logProgress(title, { type, description, progressBarContainer } = {}) { + if (process.stdout.isTTY) { + progressBarContainer.log( + `${formatLogMessage(title, { type, description }).join(' ')}${EOL}`, + ); + } else { + log(title, { type, description }); + } +} + +function logProgressVerbose(title, { type, description, progressBarContainer } = {}) { + if (isVerbose) { + logProgress(title, { type, description, progressBarContainer }); + } +} + +export { + enableVerbose, + log, + logEmptyLine, + logErrorAndExit, + logProgress, + logProgressVerbose, +}; diff --git a/lib/optimize.js b/lib/optimize.js index af9bf63..e68a31e 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -2,7 +2,6 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import CliProgress from 'cli-progress'; import execBuffer from 'exec-buffer'; import gifsicle from 'gifsicle'; import guetzli from 'guetzli'; @@ -11,15 +10,26 @@ import sharp from 'sharp'; import { optimize as svgoOptimize } from 'svgo'; import calcRatio from './calc-ratio.js'; +import createProgressBarContainer from './create-progress-bar-container.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; -import log from './log.js'; +import { + log, + logEmptyLine, + logProgress, + logProgressVerbose, +} from './log.js'; import optionsToArguments from './options-to-arguments.js'; import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; -export default async function optimize({ inputFilePaths, outputDirectoryPath, isLossless, config }) { +export default async function optimize({ + inputFilePaths, + outputDirectoryPath, + isLossless, + config, +}) { const inputFilePathsCount = inputFilePaths.length; if (inputFilePathsCount <= 0) { @@ -28,16 +38,12 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is log(`Optimizing ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); - const progressBar = new CliProgress.SingleBar({ - format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(inputFilePathsCount, 'image', 'images')}`, - clearOnComplete: true, - }, CliProgress.Presets.shades_classic); + const progressBarContainer = createProgressBarContainer(inputFilePathsCount); + const progressBar = progressBarContainer.create(inputFilePathsCount, 0); const totalSize = { before: 0, after: 0 }; const cpuCount = os.cpus().length; - - const tasksLogs = []; const tasksSimultaneousLimit = pLimit( /* Guetzli uses a large amount of memory and a significant amount of CPU time. @@ -52,27 +58,31 @@ export default async function optimize({ inputFilePaths, outputDirectoryPath, is filePath, outputDirectoryPath, config, - isLossless, + progressBarContainer, progressBar, totalSize, - tasksLogs, + isLossless, }), ), ); - progressBar.start(inputFilePathsCount, 0); await Promise.all(tasksPromises); - progressBar.stop(); + progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 + progressBarContainer.stop(); - for (const message of tasksLogs) { - log(...message); - } - - console.log(); + logEmptyLine(); showTotal(totalSize.before, totalSize.after); } -async function processFile({ filePath, outputDirectoryPath, config, isLossless, progressBar, totalSize, tasksLogs }) { +async function processFile({ + filePath, + outputDirectoryPath, + config, + progressBarContainer, + progressBar, + totalSize, + isLossless, +}) { try { const fileBuffer = await fs.promises.readFile(filePath); const processedFileBuffer = await processFileByFormat({ fileBuffer, config, isLossless }); @@ -90,10 +100,10 @@ async function processFile({ filePath, outputDirectoryPath, config, isLossless, const isSvg = path.extname(filePath).toLowerCase() === '.svg'; if (!isOptimized && !(isChanged && isSvg)) { - tasksLogs.push([filePath, { + logProgressVerbose(filePath, { description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, - verboseOnly: true, - }]); + progressBarContainer, + }); return; } @@ -106,18 +116,20 @@ async function processFile({ filePath, outputDirectoryPath, config, isLossless, const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - tasksLogs.push([filePath, { + logProgress(filePath, { type: isOptimized ? 'success' : 'warning', description: `${before} → ${after}. Ratio: ${ratio}%`, - }]); + progressBarContainer, + }); } catch (error) { if (error.message) { - tasksLogs.push([filePath, { + logProgress(filePath, { type: 'error', description: (error.message || '').trim(), - }]); + progressBarContainer, + }); } else { - console.error(error); + progressBarContainer.log(error); } } finally { progressBar.increment(); diff --git a/lib/show-total.js b/lib/show-total.js index 0d4fa3c..fbfd6a8 100644 --- a/lib/show-total.js +++ b/lib/show-total.js @@ -1,6 +1,6 @@ import calcRatio from './calc-ratio.js'; import formatBytes from './format-bytes.js'; -import log from './log.js'; +import { log } from './log.js'; export default function showTotal(before, after) { const ratio = calcRatio(before, after); diff --git a/tests/log.test.js b/tests/log.test.js index 6cd6878..00b90c6 100644 --- a/tests/log.test.js +++ b/tests/log.test.js @@ -1,7 +1,7 @@ import { jest } from '@jest/globals'; import colorize from '../lib/colorize.js'; -import log, { enableVerbose } from '../lib/log.js'; +import { log } from '../lib/log.js'; const colors = { info: 'blue', @@ -32,36 +32,6 @@ test('Description logged', () => { }); }); -describe('Verbose mode', () => { - test('Not logged if type = info & verboseOnly = true & isVerbose = false', () => { - expectLog({ - symbol: symbols.info[symbolIndex], - title: 'info', - type: 'info', - verboseOnly: true, - }); - }); - - test('Logged if type = info & verboseOnly = true & isVerbose = true', () => { - expectLog({ - symbol: symbols.info[symbolIndex], - title: 'info', - type: 'info', - verboseModeEnabled: true, - verboseOnly: true, - }); - }); - - test('Logged if type = error & verboseOnly = true & isVerbose = false', () => { - expectLog({ - symbol: symbols.error[symbolIndex], - title: 'error', - type: 'error', - verboseOnly: true, - }); - }); -}); - describe('Titles and symbols', () => { test('Logged “info” with symbol', () => { expectLog({ @@ -101,28 +71,18 @@ function expectLog({ symbol, title, type, - verboseModeEnabled, - verboseOnly, }) { const symbolColored = colorize(symbol)[colors[(type || 'info')]]; const descriptionColored = description ? colorize(description).dim : undefined; - if (verboseModeEnabled) { - enableVerbose(); - } - console.log = jest.fn(); - log(title, { type, description, verboseOnly }); + log(title, { type, description }); - if (type === 'info' && !verboseModeEnabled && verboseOnly) { - expect(console.log.mock.calls[0]).toBeUndefined(); - } else { - expect(console.log.mock.calls[0][0]).toBe(symbolColored); - expect(console.log.mock.calls[0][1]).toBe(title); - expect(console.log.mock.calls[0][4]).toBe(descriptionColored); - } + expect(console.log.mock.calls[0][0]).toBe(symbolColored); + expect(console.log.mock.calls[0][1]).toBe(title); + expect(console.log.mock.calls[0][4]).toBe(descriptionColored); console.log.mockRestore(); } From 126325ebb641a028cac77c060319836acd1e70e2 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:54:21 +0700 Subject: [PATCH 17/45] Change "File already exists" message type from "error" to "info" and make it verbose only --- lib/convert.js | 14 ++++++++++++-- tests/cli.test.js | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 9251fbc..5d2071c 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -13,7 +13,12 @@ import createProgressBarContainer from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; -import { log, logEmptyLine, logProgress } from './log.js'; +import { + log, + logEmptyLine, + logProgress, + logProgressVerbose, +} from './log.js'; import optionsToArguments from './options-to-arguments.js'; import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; @@ -125,7 +130,12 @@ async function processFile({ const isFileExists = await checkFileExists(preparedOutputFilePath); if (!isForced && isFileExists) { - throw new Error(`File already exists, '${preparedOutputFilePath}'`); + logProgressVerbose(preparedOutputFilePath, { + description: `File already exists, '${preparedOutputFilePath}'`, + progressBarContainer, + }); + + return; } const fileBuffer = await fs.promises.readFile(filePath); diff --git a/tests/cli.test.js b/tests/cli.test.js index 821a54c..a08f220 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -330,7 +330,7 @@ describe('CLI', () => { describe('Force rewrite AVIF or WebP (--force)', () => { test('Should not be overwritten', () => { const fileBasename = 'png-not-optimized'; - const parameters = `--avif --webp ${workDirectory}${fileBasename}.png`; + const parameters = `--verbose --avif --webp ${workDirectory}${fileBasename}.png`; runCliWithParameters(parameters); const stdout = runCliWithParameters(parameters); From 0be51df62e848034c7a67ca90377b5a54091bef9 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:15:33 +0700 Subject: [PATCH 18/45] Rename calcRatio to calculateRatio --- lib/{calc-ratio.js => calculate-ratio.js} | 2 +- lib/convert.js | 4 ++-- lib/optimize.js | 4 ++-- lib/show-total.js | 4 ++-- tests/{calc-ratio.test.js => calculate-ratio.test.js} | 6 +++--- tests/cli.test.js | 10 ++++------ 6 files changed, 14 insertions(+), 16 deletions(-) rename lib/{calc-ratio.js => calculate-ratio.js} (51%) rename tests/{calc-ratio.test.js => calculate-ratio.test.js} (50%) diff --git a/lib/calc-ratio.js b/lib/calculate-ratio.js similarity index 51% rename from lib/calc-ratio.js rename to lib/calculate-ratio.js index e9c8d9c..26f1e46 100644 --- a/lib/calc-ratio.js +++ b/lib/calculate-ratio.js @@ -1,3 +1,3 @@ -export default function calcRatio(from, to) { +export function calculateRatio(from, to) { return Math.round((from - to) / from * 100); } diff --git a/lib/convert.js b/lib/convert.js index 5d2071c..66f59ac 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -7,7 +7,7 @@ import gif2webp from 'gif2webp-bin'; import pLimit from 'p-limit'; import sharp from 'sharp'; -import calcRatio from './calc-ratio.js'; +import { calculateRatio } from './calculate-ratio.js'; import checkFileExists from './check-file-exists.js'; import createProgressBarContainer from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; @@ -149,7 +149,7 @@ async function processFile({ totalSize.before += fileSize; totalSize.after += Math.min(fileSize, processedFileSize); - const ratio = calcRatio(fileSize, processedFileSize); + const ratio = calculateRatio(fileSize, processedFileSize); const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); diff --git a/lib/optimize.js b/lib/optimize.js index e68a31e..62d4ebf 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -9,7 +9,7 @@ import pLimit from 'p-limit'; import sharp from 'sharp'; import { optimize as svgoOptimize } from 'svgo'; -import calcRatio from './calc-ratio.js'; +import { calculateRatio } from './calculate-ratio.js'; import createProgressBarContainer from './create-progress-bar-container.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; @@ -93,7 +93,7 @@ async function processFile({ totalSize.before += fileSize; totalSize.after += Math.min(fileSize, processedFileSize); - const ratio = calcRatio(fileSize, processedFileSize); + const ratio = calculateRatio(fileSize, processedFileSize); const isOptimized = ratio > 0; const isChanged = !fileBuffer.equals(processedFileBuffer); diff --git a/lib/show-total.js b/lib/show-total.js index fbfd6a8..492bb94 100644 --- a/lib/show-total.js +++ b/lib/show-total.js @@ -1,9 +1,9 @@ -import calcRatio from './calc-ratio.js'; +import { calculateRatio } from './calculate-ratio.js'; import formatBytes from './format-bytes.js'; import { log } from './log.js'; export default function showTotal(before, after) { - const ratio = calcRatio(before, after); + const ratio = calculateRatio(before, after); const saved = formatBytes(before - after); if (ratio > 0) { diff --git a/tests/calc-ratio.test.js b/tests/calculate-ratio.test.js similarity index 50% rename from tests/calc-ratio.test.js rename to tests/calculate-ratio.test.js index 05160f6..34ff365 100644 --- a/tests/calc-ratio.test.js +++ b/tests/calculate-ratio.test.js @@ -1,9 +1,9 @@ -import calcRatio from '../lib/calc-ratio.js'; +import { calculateRatio } from '../lib/calculate-ratio.js'; test('Ratio should be “50” if the file size has decreased by 50%', () => { - expect(calcRatio(1_000_000, 500_000)).toBe(50); + expect(calculateRatio(1_000_000, 500_000)).toBe(50); }); test('Ratio should be “-100” if the file size has increased by 100%', () => { - expect(calcRatio(500_000, 1_000_000)).toBe(-100); + expect(calculateRatio(500_000, 1_000_000)).toBe(-100); }); diff --git a/tests/cli.test.js b/tests/cli.test.js index a08f220..e46b0e0 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -4,6 +4,8 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { calculateRatio } from '../lib/calculate-ratio.js'; + const dirname = path.dirname(fileURLToPath(import.meta.url)); const cliPath = path.resolve('cli.js'); @@ -472,10 +474,6 @@ function calculateDirectorySize(directoryPath) { return totalSize; } -function calcRatio(from, to) { - return Math.round((from - to) / from * 100); -} - function runCliWithParameters(parameters) { return execSync(`node ${cliPath} ${parameters}`).toString(); } @@ -507,7 +505,7 @@ function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) { const sizeBefore = fs.statSync(path.join(images, file)).size; const sizeAfter = fs.statSync(path.join(temporary, outputFile)).size; - const calculatedRatio = calcRatio(sizeBefore, sizeAfter); + const calculatedRatio = calculateRatio(sizeBefore, sizeAfter); const stdoutRatio = grepTotalRatio(stdout); expect(stdoutRatio).toBe(calculatedRatio); @@ -517,7 +515,7 @@ function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) { function expectTotalRatio({ maxRatio, minRatio, stdout }) { const sizeBefore = calculateDirectorySize(images); const sizeAfter = calculateDirectorySize(temporary); - const calculatedRatio = calcRatio(sizeBefore, sizeAfter); + const calculatedRatio = calculateRatio(sizeBefore, sizeAfter); const stdoutRatio = grepTotalRatio(stdout); expect(stdoutRatio).toBe(calculatedRatio); From fba685e54a99617ed1b287fef3b320aa8e891db2 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:32:57 +0700 Subject: [PATCH 19/45] Remove lib/check-config-path.js --- index.js | 21 ++++++++++++++++++--- lib/check-config-path.js | 18 ------------------ 2 files changed, 18 insertions(+), 21 deletions(-) delete mode 100644 lib/check-config-path.js diff --git a/index.js b/index.js index 718f252..5a60395 100755 --- a/index.js +++ b/index.js @@ -1,10 +1,11 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import checkConfigPath from './lib/check-config-path.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; import findConfig from './lib/find-config.js'; -import { enableVerbose, log } from './lib/log.js'; +import { enableVerbose, log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; @@ -34,7 +35,7 @@ export default async function optimizt({ const preparedConfigFilePath = pathToFileURL( configFilePath - ? checkConfigPath(configFilePath) + ? resolveProvidedConfigPath(configFilePath) : findConfig(), ); const configData = await import(preparedConfigFilePath); @@ -68,3 +69,17 @@ export default async function optimizt({ }, }); } + +function resolveProvidedConfigPath(filepath = '') { + const resolvedPath = path.resolve(filepath); + + if (!fs.existsSync(resolvedPath)) { + logErrorAndExit('Provided config path does not exist'); + } + + if (!fs.statSync(resolvedPath).isFile()) { + logErrorAndExit('Provided config path must point to a file'); + } + + return resolvedPath; +} diff --git a/lib/check-config-path.js b/lib/check-config-path.js deleted file mode 100644 index 6f0f48a..0000000 --- a/lib/check-config-path.js +++ /dev/null @@ -1,18 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import { logErrorAndExit } from './log.js'; - -export default function checkConfigPath(filepath = '') { - const resolvedPath = path.resolve(filepath); - - if (!fs.existsSync(resolvedPath)) { - logErrorAndExit('Provided config path does not exist'); - } - - if (!fs.statSync(resolvedPath).isFile()) { - logErrorAndExit('Provided config path must point to a file'); - } - - return resolvedPath; -} From 1e5f4c3104948d9d400c01c1fbfbe26668c38c32 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:00:08 +0700 Subject: [PATCH 20/45] Rename checkFileExists in checkPathAccessibility --- lib/{check-file-exists.js => check-path-accessibility.js} | 2 +- lib/convert.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/{check-file-exists.js => check-path-accessibility.js} (68%) diff --git a/lib/check-file-exists.js b/lib/check-path-accessibility.js similarity index 68% rename from lib/check-file-exists.js rename to lib/check-path-accessibility.js index 8ff4c6b..f4788f7 100644 --- a/lib/check-file-exists.js +++ b/lib/check-path-accessibility.js @@ -1,6 +1,6 @@ import { access } from 'node:fs/promises'; -export default async function checkFileExists(filePath) { +export async function checkPathAccessibility(filePath) { try { await access(filePath); return true; diff --git a/lib/convert.js b/lib/convert.js index 66f59ac..fdea5d7 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -8,7 +8,7 @@ import pLimit from 'p-limit'; import sharp from 'sharp'; import { calculateRatio } from './calculate-ratio.js'; -import checkFileExists from './check-file-exists.js'; +import { checkPathAccessibility } from './check-path-accessibility.js'; import createProgressBarContainer from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; @@ -127,9 +127,9 @@ async function processFile({ const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); const preparedOutputFilePath = prepareWriteFilePath(outputFilePath, outputDirectoryPath); - const isFileExists = await checkFileExists(preparedOutputFilePath); + const isAccessible = await checkPathAccessibility(preparedOutputFilePath); - if (!isForced && isFileExists) { + if (!isForced && isAccessible) { logProgressVerbose(preparedOutputFilePath, { description: `File already exists, '${preparedOutputFilePath}'`, progressBarContainer, From c27a3e877eb0a709834fbdda76483f4811b8925e Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:12:14 +0700 Subject: [PATCH 21/45] Remove default export from colorize --- lib/colorize.js | 2 +- lib/log.js | 2 +- tests/colorize.test.js | 2 +- tests/log.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/colorize.js b/lib/colorize.js index 5ae5998..ff97f91 100644 --- a/lib/colorize.js +++ b/lib/colorize.js @@ -1,4 +1,4 @@ -export default function colorize(...arguments_) { +export function colorize(...arguments_) { const string_ = arguments_.join(' '); const isTTY = Boolean(process.stdout.isTTY); const buildColor = (start, end) => `${isTTY ? start : ''}${string_}${isTTY ? end : ''}`; diff --git a/lib/log.js b/lib/log.js index ecd1031..e7494dd 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,6 +1,6 @@ import { EOL } from 'node:os'; -import colorize from './colorize.js'; +import { colorize } from './colorize.js'; const colors = { info: 'blue', diff --git a/tests/colorize.test.js b/tests/colorize.test.js index 7db5445..2379ed2 100644 --- a/tests/colorize.test.js +++ b/tests/colorize.test.js @@ -1,4 +1,4 @@ -import colorize from '../lib/colorize.js'; +import { colorize } from '../lib/colorize.js'; const isTTY = Boolean(process.stdout.isTTY); diff --git a/tests/log.test.js b/tests/log.test.js index 00b90c6..9618594 100644 --- a/tests/log.test.js +++ b/tests/log.test.js @@ -1,6 +1,6 @@ import { jest } from '@jest/globals'; -import colorize from '../lib/colorize.js'; +import { colorize } from '../lib/colorize.js'; import { log } from '../lib/log.js'; const colors = { From 609727c45f7c99dff94483a003e8b4b8d0448c22 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:28:41 +0700 Subject: [PATCH 22/45] Remove default export from createProgressBarContainer --- lib/convert.js | 2 +- lib/create-progress-bar-container.js | 2 +- lib/optimize.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index fdea5d7..f78d87a 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -9,7 +9,7 @@ import sharp from 'sharp'; import { calculateRatio } from './calculate-ratio.js'; import { checkPathAccessibility } from './check-path-accessibility.js'; -import createProgressBarContainer from './create-progress-bar-container.js'; +import { createProgressBarContainer } from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; diff --git a/lib/create-progress-bar-container.js b/lib/create-progress-bar-container.js index c43e4d6..b1160e7 100644 --- a/lib/create-progress-bar-container.js +++ b/lib/create-progress-bar-container.js @@ -2,7 +2,7 @@ import CliProgress from 'cli-progress'; import getPlural from './get-plural.js'; -export default function createCliProgressContainer(totalCount) { +export function createProgressBarContainer(totalCount) { return new CliProgress.MultiBar({ format: `{bar} {percentage}% | Processed {value} of {total} ${getPlural(totalCount, 'image', 'images')}`, clearOnComplete: true, diff --git a/lib/optimize.js b/lib/optimize.js index 62d4ebf..aa46ccb 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -10,7 +10,7 @@ import sharp from 'sharp'; import { optimize as svgoOptimize } from 'svgo'; import { calculateRatio } from './calculate-ratio.js'; -import createProgressBarContainer from './create-progress-bar-container.js'; +import { createProgressBarContainer } from './create-progress-bar-container.js'; import formatBytes from './format-bytes.js'; import getPlural from './get-plural.js'; import { From e7b61dec15ec66efe30cae4b782980c1e1e4672b Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:31:38 +0700 Subject: [PATCH 23/45] Create DEFAULT_CONFIG_FILENAME constant --- lib/constants.js | 2 ++ lib/find-config.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index 1767221..7353533 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,3 +1,5 @@ +export const DEFAULT_CONFIG_FILENAME = '.optimiztrc.cjs'; + export const SUPPORTED_FILE_TYPES = { CONVERT: ['gif', 'jpeg', 'jpg', 'png'], OPTIMIZE: ['gif', 'jpeg', 'jpg', 'png', 'svg'], diff --git a/lib/find-config.js b/lib/find-config.js index 714a341..c1d3993 100644 --- a/lib/find-config.js +++ b/lib/find-config.js @@ -2,12 +2,13 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -const defaultFilename = '.optimiztrc.cjs'; +import { DEFAULT_CONFIG_FILENAME } from './constants'; + const defaultDirname = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const defaultPath = path.join(defaultDirname, defaultFilename); +const defaultPath = path.join(defaultDirname, DEFAULT_CONFIG_FILENAME); export default function findConfig(filepath = process.cwd()) { - const resolvedPath = path.resolve(filepath, defaultFilename); + const resolvedPath = path.resolve(filepath, DEFAULT_CONFIG_FILENAME); if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) { return resolvedPath; From bac2c12ade95b2054ce22a47b92260ee3384c742 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:35:03 +0700 Subject: [PATCH 24/45] Improve findConfig --- index.js | 4 ++-- lib/find-config.js | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 5a60395..fdfac2d 100755 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; -import findConfig from './lib/find-config.js'; +import { findConfig } from './lib/find-config.js'; import { enableVerbose, log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; @@ -36,7 +36,7 @@ export default async function optimizt({ const preparedConfigFilePath = pathToFileURL( configFilePath ? resolveProvidedConfigPath(configFilePath) - : findConfig(), + : await findConfig(), ); const configData = await import(preparedConfigFilePath); const config = configData.default[currentMode.toLowerCase()]; diff --git a/lib/find-config.js b/lib/find-config.js index c1d3993..2eeb907 100644 --- a/lib/find-config.js +++ b/lib/find-config.js @@ -1,23 +1,37 @@ -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { DEFAULT_CONFIG_FILENAME } from './constants'; +import { DEFAULT_CONFIG_FILENAME } from './constants.js'; -const defaultDirname = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const defaultPath = path.join(defaultDirname, DEFAULT_CONFIG_FILENAME); +const defaultDirectoryPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const defaultConfigPath = path.join(defaultDirectoryPath, DEFAULT_CONFIG_FILENAME); -export default function findConfig(filepath = process.cwd()) { - const resolvedPath = path.resolve(filepath, DEFAULT_CONFIG_FILENAME); +export async function findConfig() { + let currentDirectoryPath = path.resolve(process.cwd()); - if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) { - return resolvedPath; - } + while (true) { // eslint-disable-line no-constant-condition + const currentConfigPath = path.join(currentDirectoryPath, DEFAULT_CONFIG_FILENAME); + + try { + const stat = await fs.stat(currentConfigPath); // eslint-disable-line no-await-in-loop + + if (stat.isFile()) { + return currentConfigPath; + } + } catch { + // File not found, continue searching + } - const { root, dir } = path.parse(resolvedPath); - const isRootDirectory = dir === root; + const parentDirectoryPath = path.dirname(currentDirectoryPath); + + if (parentDirectoryPath === currentDirectoryPath) { + // Reached the root of the file system + break; + } + + currentDirectoryPath = parentDirectoryPath; + } - return isRootDirectory - ? defaultPath - : findConfig(path.dirname(dir)); + return defaultConfigPath; } From 4106ba17b791491691ef37d54ac72a16f1e8e077 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:07:27 +0700 Subject: [PATCH 25/45] Improve formatBytes --- lib/convert.js | 2 +- lib/format-bytes.js | 17 +++++++++-------- lib/optimize.js | 2 +- lib/show-total.js | 2 +- tests/format-bytes.test.js | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index f78d87a..e7302c3 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -11,7 +11,7 @@ import { calculateRatio } from './calculate-ratio.js'; import { checkPathAccessibility } from './check-path-accessibility.js'; import { createProgressBarContainer } from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; -import formatBytes from './format-bytes.js'; +import { formatBytes } from './format-bytes.js'; import getPlural from './get-plural.js'; import { log, diff --git a/lib/format-bytes.js b/lib/format-bytes.js index 0dd2a49..992e22b 100644 --- a/lib/format-bytes.js +++ b/lib/format-bytes.js @@ -1,14 +1,15 @@ -export default function formatBytes(bytes) { +const SIZES = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; +const DECIMALS = 3; +const K = 1024; +const LOG_K = Math.log(K); + +export function formatBytes(bytes) { if (bytes === 0) { return '0 Bytes'; } - const decimals = 3; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const index = Math.floor(Math.log(bytes) / Math.log(k)); + const index = Math.floor(Math.log(bytes) / LOG_K); + const value = Number.parseFloat((bytes / (K ** index)).toFixed(DECIMALS)); // Use parseFloat to show "1 KB" instead of "1.000 KB" - // Use parseFloat to show "1 KB" instead of "1.000 KB" - return `${Number.parseFloat((bytes / (k ** index)).toFixed(decimals))} ${sizes[index]}`; + return `${value} ${SIZES[index]}`; } diff --git a/lib/optimize.js b/lib/optimize.js index aa46ccb..ae24512 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -11,7 +11,7 @@ import { optimize as svgoOptimize } from 'svgo'; import { calculateRatio } from './calculate-ratio.js'; import { createProgressBarContainer } from './create-progress-bar-container.js'; -import formatBytes from './format-bytes.js'; +import { formatBytes } from './format-bytes.js'; import getPlural from './get-plural.js'; import { log, diff --git a/lib/show-total.js b/lib/show-total.js index 492bb94..59c996b 100644 --- a/lib/show-total.js +++ b/lib/show-total.js @@ -1,5 +1,5 @@ import { calculateRatio } from './calculate-ratio.js'; -import formatBytes from './format-bytes.js'; +import { formatBytes } from './format-bytes.js'; import { log } from './log.js'; export default function showTotal(before, after) { diff --git a/tests/format-bytes.test.js b/tests/format-bytes.test.js index 4db0be6..4db892c 100644 --- a/tests/format-bytes.test.js +++ b/tests/format-bytes.test.js @@ -1,4 +1,4 @@ -import formatBytes from '../lib/format-bytes.js'; +import { formatBytes } from '../lib/format-bytes.js'; test('1023 should be formatted as “1023 Bytes”', () => { expect(formatBytes(1023)).toBe('1023 Bytes'); From 49b6b363e526b5581f186cfea72e9e6a1d12dc62 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:31:01 +0700 Subject: [PATCH 26/45] Remove default export from getPlural --- lib/convert.js | 2 +- lib/create-progress-bar-container.js | 2 +- lib/get-plural.js | 2 +- lib/optimize.js | 2 +- tests/get-plural.test.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index e7302c3..435177e 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -12,7 +12,7 @@ import { checkPathAccessibility } from './check-path-accessibility.js'; import { createProgressBarContainer } from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import { formatBytes } from './format-bytes.js'; -import getPlural from './get-plural.js'; +import { getPlural } from './get-plural.js'; import { log, logEmptyLine, diff --git a/lib/create-progress-bar-container.js b/lib/create-progress-bar-container.js index b1160e7..ac36a28 100644 --- a/lib/create-progress-bar-container.js +++ b/lib/create-progress-bar-container.js @@ -1,6 +1,6 @@ import CliProgress from 'cli-progress'; -import getPlural from './get-plural.js'; +import { getPlural } from './get-plural.js'; export function createProgressBarContainer(totalCount) { return new CliProgress.MultiBar({ diff --git a/lib/get-plural.js b/lib/get-plural.js index 62b91d9..9fad0b7 100644 --- a/lib/get-plural.js +++ b/lib/get-plural.js @@ -1,3 +1,3 @@ -export default function getPlural(number_, one, many) { +export function getPlural(number_, one, many) { return Math.abs(number_) === 1 ? one : many; } diff --git a/lib/optimize.js b/lib/optimize.js index ae24512..2cd5276 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -12,7 +12,7 @@ import { optimize as svgoOptimize } from 'svgo'; import { calculateRatio } from './calculate-ratio.js'; import { createProgressBarContainer } from './create-progress-bar-container.js'; import { formatBytes } from './format-bytes.js'; -import getPlural from './get-plural.js'; +import { getPlural } from './get-plural.js'; import { log, logEmptyLine, diff --git a/tests/get-plural.test.js b/tests/get-plural.test.js index 7b88459..29ec363 100644 --- a/tests/get-plural.test.js +++ b/tests/get-plural.test.js @@ -1,4 +1,4 @@ -import getPlural from '../lib/get-plural.js'; +import { getPlural } from '../lib/get-plural.js'; test('Should return “image” if num equals 1', () => { expect(getPlural(1, 'image', 'images')).toBe('image'); From d07ef4a70bebaff67080c15cc9c5dc2a948f9408 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:54:57 +0700 Subject: [PATCH 27/45] Improve log --- lib/convert.js | 5 ++-- lib/log.js | 66 ++++++++++++++++++++++++----------------------- lib/optimize.js | 5 ++-- tests/log.test.js | 12 ++++----- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 435177e..a1f21ce 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -14,6 +14,7 @@ import { SUPPORTED_FILE_TYPES } from './constants.js'; import { formatBytes } from './format-bytes.js'; import { getPlural } from './get-plural.js'; import { + LOG_TYPES, log, logEmptyLine, logProgress, @@ -154,14 +155,14 @@ async function processFile({ const after = formatBytes(processedFileSize); logProgress(filePath, { - type: 'success', + type: LOG_TYPES.SUCCESS, description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { logProgress(filePath, { - type: 'error', + type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, }); diff --git a/lib/log.js b/lib/log.js index e7494dd..420e61b 100644 --- a/lib/log.js +++ b/lib/log.js @@ -2,27 +2,37 @@ import { EOL } from 'node:os'; import { colorize } from './colorize.js'; +export const LOG_TYPES = { + INFO: 'info', + SUCCESS: 'success', + WARNING: 'warning', + ERROR: 'error', +}; + const colors = { - info: 'blue', - success: 'green', - warning: 'yellow', - error: 'red', + [LOG_TYPES.INFO]: 'blue', + [LOG_TYPES.SUCCESS]: 'green', + [LOG_TYPES.WARNING]: 'yellow', + [LOG_TYPES.ERROR]: 'red', }; + const symbols = { - info: ['i', 'ℹ'], - success: ['√', '✔'], - warning: ['‼', '⚠'], - error: ['×', '✖'], + [LOG_TYPES.INFO]: ['i', 'ℹ'], + [LOG_TYPES.SUCCESS]: ['√', '✔'], + [LOG_TYPES.WARNING]: ['‼', '⚠'], + [LOG_TYPES.ERROR]: ['×', '✖'], }; -const symbolIndex = Number(process.platform !== 'win32' || process.env.TERM === 'xterm-256color'); + +const isUnicodeSupported = process.platform !== 'win32' || process.env.TERM === 'xterm-256color'; +const symbolIndex = isUnicodeSupported ? 1 : 0; let isVerbose = false; -function enableVerbose() { +export function enableVerbose() { isVerbose = true; } -function formatLogMessage(title, { type = 'info', description } = {}) { +function formatLogMessage(title, { type = LOG_TYPES.INFO, description } = {}) { if (!title) { throw new Error('Title is required'); } @@ -38,40 +48,32 @@ function formatLogMessage(title, { type = 'info', description } = {}) { ]; } -function log(title, { type, description } = {}) { +export function log(title, { type, description } = {}) { console.log(...formatLogMessage(title, { type, description })); } -function logErrorAndExit(title) { - log(title, { type: 'error' }); +export function logErrorAndExit(title) { + log(title, { type: LOG_TYPES.ERROR }); process.exit(1); // eslint-disable-line unicorn/no-process-exit } -function logEmptyLine() { +export function logEmptyLine() { console.log(); } -function logProgress(title, { type, description, progressBarContainer } = {}) { - if (process.stdout.isTTY) { - progressBarContainer.log( - `${formatLogMessage(title, { type, description }).join(' ')}${EOL}`, - ); - } else { - log(title, { type, description }); +export function logProgress(title, { type, description, progressBarContainer } = {}) { + if (process.stdout.isTTY && progressBarContainer) { + const message = `${formatLogMessage(title, { type, description }).join(' ')}${EOL}`; + progressBarContainer.log(message); + + return; } + + log(title, { type, description }); } -function logProgressVerbose(title, { type, description, progressBarContainer } = {}) { +export function logProgressVerbose(title, { type, description, progressBarContainer } = {}) { if (isVerbose) { logProgress(title, { type, description, progressBarContainer }); } } - -export { - enableVerbose, - log, - logEmptyLine, - logErrorAndExit, - logProgress, - logProgressVerbose, -}; diff --git a/lib/optimize.js b/lib/optimize.js index 2cd5276..08a232d 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -14,6 +14,7 @@ import { createProgressBarContainer } from './create-progress-bar-container.js'; import { formatBytes } from './format-bytes.js'; import { getPlural } from './get-plural.js'; import { + LOG_TYPES, log, logEmptyLine, logProgress, @@ -117,14 +118,14 @@ async function processFile({ const after = formatBytes(processedFileSize); logProgress(filePath, { - type: isOptimized ? 'success' : 'warning', + type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING, description: `${before} → ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { logProgress(filePath, { - type: 'error', + type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, }); diff --git a/tests/log.test.js b/tests/log.test.js index 9618594..db4492c 100644 --- a/tests/log.test.js +++ b/tests/log.test.js @@ -1,7 +1,7 @@ import { jest } from '@jest/globals'; import { colorize } from '../lib/colorize.js'; -import { log } from '../lib/log.js'; +import { LOG_TYPES, log } from '../lib/log.js'; const colors = { info: 'blue', @@ -37,7 +37,7 @@ describe('Titles and symbols', () => { expectLog({ symbol: symbols.info[symbolIndex], title: 'info', - type: 'info', + type: LOG_TYPES.INFO, }); }); @@ -45,7 +45,7 @@ describe('Titles and symbols', () => { expectLog({ symbol: symbols.success[symbolIndex], title: 'success', - type: 'success', + type: LOG_TYPES.SUCCESS, }); }); @@ -53,7 +53,7 @@ describe('Titles and symbols', () => { expectLog({ symbol: symbols.warning[symbolIndex], title: 'warning', - type: 'warning', + type: LOG_TYPES.WARNING, }); }); @@ -61,7 +61,7 @@ describe('Titles and symbols', () => { expectLog({ symbol: symbols.error[symbolIndex], title: 'error', - type: 'error', + type: LOG_TYPES.ERROR, }); }); }); @@ -72,7 +72,7 @@ function expectLog({ title, type, }) { - const symbolColored = colorize(symbol)[colors[(type || 'info')]]; + const symbolColored = colorize(symbol)[colors[(type || LOG_TYPES.INFO)]]; const descriptionColored = description ? colorize(description).dim : undefined; From 6be4f6dd5fe294b621704cd8a1e17352c16c9658 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 29 Sep 2024 16:32:31 +0700 Subject: [PATCH 28/45] Improve optionsToArguments --- lib/convert.js | 2 +- lib/optimize.js | 2 +- lib/options-to-arguments.js | 30 +++++++++++++++++------------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index a1f21ce..6a547be 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -20,7 +20,7 @@ import { logProgress, logProgressVerbose, } from './log.js'; -import optionsToArguments from './options-to-arguments.js'; +import { optionsToArguments } from './options-to-arguments.js'; import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; diff --git a/lib/optimize.js b/lib/optimize.js index 08a232d..5f9d57c 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -20,7 +20,7 @@ import { logProgress, logProgressVerbose, } from './log.js'; -import optionsToArguments from './options-to-arguments.js'; +import { optionsToArguments } from './options-to-arguments.js'; import parseImageMetadata from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; diff --git a/lib/options-to-arguments.js b/lib/options-to-arguments.js index 9571492..85d21a3 100644 --- a/lib/options-to-arguments.js +++ b/lib/options-to-arguments.js @@ -1,17 +1,21 @@ -export default function optionsToArguments({ options, prefix = '--', concat = false }) { - return Object.keys(options).reduce((accumulator, key) => { - const value = options[key]; - const shouldAddKey = value !== false; - const shouldAddValue = value !== true; +export function optionsToArguments({ options, prefix = '--', concat = false }) { + const arguments_ = []; - if (!shouldAddKey) { - return accumulator; + for (const [key, value] of Object.entries(options)) { + if (value === false) { + continue; } - return [ - ...accumulator, - `${prefix}${key}${(shouldAddValue && concat ? `=${value}` : '')}`, - ...shouldAddValue && !concat ? [value] : [], - ]; - }, []); + const option = `${prefix}${key}`; + + if (value === true) { + arguments_.push(option); + } else if (concat) { + arguments_.push(`${option}=${value}`); + } else { + arguments_.push(option, String(value)); + } + } + + return arguments_; } From 3d992ad4b940f424ad4893093da55b3a7f57bad0 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 29 Sep 2024 16:35:04 +0700 Subject: [PATCH 29/45] Remove default export from parseImageMetadata --- lib/convert.js | 2 +- lib/optimize.js | 2 +- lib/parse-image-metadata.js | 2 +- tests/parse-image-metadata.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 6a547be..be15a2a 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -21,7 +21,7 @@ import { logProgressVerbose, } from './log.js'; import { optionsToArguments } from './options-to-arguments.js'; -import parseImageMetadata from './parse-image-metadata.js'; +import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; diff --git a/lib/optimize.js b/lib/optimize.js index 5f9d57c..e8d4638 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -21,7 +21,7 @@ import { logProgressVerbose, } from './log.js'; import { optionsToArguments } from './options-to-arguments.js'; -import parseImageMetadata from './parse-image-metadata.js'; +import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import showTotal from './show-total.js'; diff --git a/lib/parse-image-metadata.js b/lib/parse-image-metadata.js index 3167f8b..a7a4b35 100644 --- a/lib/parse-image-metadata.js +++ b/lib/parse-image-metadata.js @@ -1,6 +1,6 @@ import sharp from 'sharp'; -export default async function parseImageMetadata(buffer) { +export async function parseImageMetadata(buffer) { try { // Fast access to (uncached) image metadata without decoding any compressed pixel data. // This is read from the header of the input image. It does not take into consideration any operations to be applied to the output image, such as resize or rotate. diff --git a/tests/parse-image-metadata.test.js b/tests/parse-image-metadata.test.js index 1c9672e..6ceb5f4 100644 --- a/tests/parse-image-metadata.test.js +++ b/tests/parse-image-metadata.test.js @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import parseImageMetadata from '../lib/parse-image-metadata.js'; +import { parseImageMetadata } from '../lib/parse-image-metadata.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); From 8f677721099ae9b13b9ce43bcde71dd15f6a80a7 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:57:46 +0700 Subject: [PATCH 30/45] Improve prepareInputFilePaths --- index.js | 2 +- lib/prepare-input-file-paths.js | 20 ++++++++++---------- tests/prepare-file-paths.test.js | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index fdfac2d..14df5ea 100755 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ import convert from './lib/convert.js'; import { findConfig } from './lib/find-config.js'; import { enableVerbose, log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; -import prepareInputFilePaths from './lib/prepare-input-file-paths.js'; +import { prepareInputFilePaths } from './lib/prepare-input-file-paths.js'; import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; const MODE_NAME = { diff --git a/lib/prepare-input-file-paths.js b/lib/prepare-input-file-paths.js index 70c990d..50c5398 100644 --- a/lib/prepare-input-file-paths.js +++ b/lib/prepare-input-file-paths.js @@ -3,24 +3,24 @@ import path from 'node:path'; import { fdir } from 'fdir'; -export default async function prepareInputFilePaths(paths, extensions) { +export async function prepareInputFilePaths(paths, extensions) { const files = new Set(); const directories = new Set(); const pathsSet = new Set(paths); for (const currentPath of pathsSet) { - if (!fs.existsSync(currentPath)) { + try { + const stat = await fs.promises.stat(currentPath); // eslint-disable-line no-await-in-loop + + if (stat.isDirectory()) { + directories.add(currentPath); + } else if (stat.isFile() && checkFileType(currentPath, extensions)) { + files.add(getRelativePath(currentPath)); + } + } catch { continue; } - - const lstat = fs.lstatSync(currentPath); - - if (lstat.isDirectory()) { - directories.add(currentPath); - } else if (lstat.isFile() && checkFileType(currentPath, extensions)) { - files.add(getRelativePath(currentPath)); - } } const crawler = new fdir() // eslint-disable-line new-cap diff --git a/tests/prepare-file-paths.test.js b/tests/prepare-file-paths.test.js index b01109b..1bd0457 100644 --- a/tests/prepare-file-paths.test.js +++ b/tests/prepare-file-paths.test.js @@ -1,7 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import prepareInputFilePaths from '../lib/prepare-input-file-paths.js'; +import { prepareInputFilePaths } from '../lib/prepare-input-file-paths.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); From e66ef40acd2e66fbbc582ad9325a6b8810ace941 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Sun, 29 Sep 2024 18:35:58 +0700 Subject: [PATCH 31/45] Improve prepareOutputDirectoryPath --- index.js | 4 ++-- lib/prepare-output-directory-path.js | 14 ++++++++------ tests/prepare-output-path.test.js | 14 +++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 14df5ea..320f82f 100755 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ import { findConfig } from './lib/find-config.js'; import { enableVerbose, log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; import { prepareInputFilePaths } from './lib/prepare-input-file-paths.js'; -import prepareOutputDirectoryPath from './lib/prepare-output-directory-path.js'; +import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; const MODE_NAME = { CONVERT: 'convert', @@ -42,7 +42,7 @@ export default async function optimizt({ const config = configData.default[currentMode.toLowerCase()]; const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode.toUpperCase()]); - const preparedOutputDirectoryPath = prepareOutputDirectoryPath(outputDirectoryPath); + const preparedOutputDirectoryPath = await prepareOutputDirectoryPath(outputDirectoryPath); if (isVerbose) { enableVerbose(); diff --git a/lib/prepare-output-directory-path.js b/lib/prepare-output-directory-path.js index f54ba9b..bb9ab0a 100644 --- a/lib/prepare-output-directory-path.js +++ b/lib/prepare-output-directory-path.js @@ -3,19 +3,21 @@ import path from 'node:path'; import { logErrorAndExit } from './log.js'; -export default function prepareOutputDirectoryPath(outputDirectoryPath) { +export async function prepareOutputDirectoryPath(outputDirectoryPath) { if (!outputDirectoryPath) { return ''; } const resolvedPath = path.resolve(outputDirectoryPath); - if (!fs.existsSync(resolvedPath)) { - logErrorAndExit('Output path does not exist'); - } + try { + const stat = await fs.promises.stat(resolvedPath); - if (!fs.lstatSync(resolvedPath).isDirectory()) { - logErrorAndExit('Output path must be a directory'); + if (!stat.isDirectory()) { + logErrorAndExit('Output path must be a directory'); + } + } catch { + logErrorAndExit('Output path does not exist'); } return resolvedPath; diff --git a/tests/prepare-output-path.test.js b/tests/prepare-output-path.test.js index 0295966..331fba3 100644 --- a/tests/prepare-output-path.test.js +++ b/tests/prepare-output-path.test.js @@ -3,18 +3,18 @@ import { fileURLToPath } from 'node:url'; import { jest } from '@jest/globals'; -import prepareOutputDirectoryPath from '../lib/prepare-output-directory-path.js'; +import { prepareOutputDirectoryPath } from '../lib/prepare-output-directory-path.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); -test('Exit if the path does not exist', () => { +test('Exit if the path does not exist', async () => { const processExitMock = jest.spyOn(process, 'exit').mockImplementation(exitCode => { throw new Error(`Process exit with status code: ${exitCode}`); }); console.log = jest.fn(); - expect(() => prepareOutputDirectoryPath('not+exists')).toThrow(); + await expect(() => prepareOutputDirectoryPath('not+exists')).rejects.toThrow(); expect(processExitMock).toHaveBeenCalledWith(1); expect(console.log.mock.calls[0][1]).toBe('Output path does not exist'); @@ -22,14 +22,14 @@ test('Exit if the path does not exist', () => { processExitMock.mockRestore(); }); -test('Exit if specified path to file instead of directory', () => { +test('Exit if specified path to file instead of directory', async () => { const processExitMock = jest.spyOn(process, 'exit').mockImplementation(exitCode => { throw new Error(`Process exit with status code: ${exitCode}`); }); console.log = jest.fn(); - expect(() => prepareOutputDirectoryPath(path.resolve(dirname, 'images', 'svg-not-optimized.svg'))).toThrow(); + await expect(() => prepareOutputDirectoryPath(path.resolve(dirname, 'images', 'svg-not-optimized.svg'))).rejects.toThrow(); expect(processExitMock).toHaveBeenCalledWith(1); expect(console.log.mock.calls[0][1]).toBe('Output path must be a directory'); @@ -37,6 +37,6 @@ test('Exit if specified path to file instead of directory', () => { processExitMock.mockRestore(); }); -test('Full path is generated', () => { - expect(prepareOutputDirectoryPath('tests/images')).toBe(path.resolve(dirname, 'images')); +test('Full path is generated', async () => { + expect(await prepareOutputDirectoryPath('tests/images')).toBe(path.resolve(dirname, 'images')); }); From 111320699d6dd88e929eb43858e1702a0b2c2e66 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:54:58 +0700 Subject: [PATCH 32/45] Add programOptions helper --- cli.js | 16 ++++++++++++++-- index.js | 33 +++++++++++---------------------- lib/convert.js | 12 ++++++++---- lib/log.js | 9 ++------- lib/optimize.js | 4 +++- lib/program-options.js | 12 ++++++++++++ 6 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 lib/program-options.js diff --git a/cli.js b/cli.js index 156420b..cfa07d7 100755 --- a/cli.js +++ b/cli.js @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url'; import { program } from 'commander'; import optimizt from './index.js'; +import { setProgramOptions } from './lib/program-options.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); const packageJson = JSON.parse(await fs.readFile(path.join(dirname, 'package.json'))); @@ -29,9 +30,20 @@ program if (program.args.length === 0) { program.help(); } else { + const { avif, webp, force, lossless, verbose, config, output } = program.opts(); + + setProgramOptions({ + shouldConvertToAvif: Boolean(avif), + shouldConvertToWebp: Boolean(webp), + isForced: Boolean(force), + isLossless: Boolean(lossless), + isVerbose: Boolean(verbose), + }); + optimizt({ - paths: program.args, - ...program.opts(), + inputPaths: program.args, + outputDirectoryPath: output, + configFilePath: config, }); } diff --git a/index.js b/index.js index 320f82f..3f92e14 100755 --- a/index.js +++ b/index.js @@ -5,10 +5,11 @@ import { pathToFileURL } from 'node:url'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; import { findConfig } from './lib/find-config.js'; -import { enableVerbose, log, logErrorAndExit } from './lib/log.js'; +import { log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; import { prepareInputFilePaths } from './lib/prepare-input-file-paths.js'; import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; +import { programOptions } from './lib/program-options.js'; const MODE_NAME = { CONVERT: 'convert', @@ -16,17 +17,16 @@ const MODE_NAME = { }; export default async function optimizt({ - paths: inputPaths, - output: outputDirectoryPath, - config: configFilePath, - - avif: shouldConvertToAvif, - webp: shouldConvertToWebp, - - force: isForced, - lossless: isLossless, - verbose: isVerbose, + inputPaths, + outputDirectoryPath, + configFilePath, }) { + const { + isLossless, + shouldConvertToAvif, + shouldConvertToWebp, + } = programOptions; + const shouldConvert = shouldConvertToAvif || shouldConvertToWebp; const currentMode = shouldConvert @@ -44,10 +44,6 @@ export default async function optimizt({ const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode.toUpperCase()]); const preparedOutputDirectoryPath = await prepareOutputDirectoryPath(outputDirectoryPath); - if (isVerbose) { - enableVerbose(); - } - if (isLossless) { log('Lossless optimization may take a long time'); } @@ -59,14 +55,7 @@ export default async function optimizt({ await processFunction({ inputFilePaths: preparedInputFilePaths, outputDirectoryPath: preparedOutputDirectoryPath, - isLossless, config, - - ...shouldConvert && { - shouldConvertToAvif, - shouldConvertToWebp, - isForced, - }, }); } diff --git a/lib/convert.js b/lib/convert.js index be15a2a..9e2c528 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -23,17 +23,21 @@ import { import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; +import { programOptions } from './program-options.js'; import showTotal from './show-total.js'; export default async function convert({ inputFilePaths, outputDirectoryPath, - isLossless, config, - shouldConvertToAvif, - shouldConvertToWebp, - isForced, }) { + const { + isForced, + isLossless, + shouldConvertToAvif, + shouldConvertToWebp, + } = programOptions; + const inputFilePathsCount = inputFilePaths.length; if (!inputFilePathsCount) { diff --git a/lib/log.js b/lib/log.js index 420e61b..248dd4b 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,6 +1,7 @@ import { EOL } from 'node:os'; import { colorize } from './colorize.js'; +import { programOptions } from './program-options.js'; export const LOG_TYPES = { INFO: 'info', @@ -26,12 +27,6 @@ const symbols = { const isUnicodeSupported = process.platform !== 'win32' || process.env.TERM === 'xterm-256color'; const symbolIndex = isUnicodeSupported ? 1 : 0; -let isVerbose = false; - -export function enableVerbose() { - isVerbose = true; -} - function formatLogMessage(title, { type = LOG_TYPES.INFO, description } = {}) { if (!title) { throw new Error('Title is required'); @@ -73,7 +68,7 @@ export function logProgress(title, { type, description, progressBarContainer } = } export function logProgressVerbose(title, { type, description, progressBarContainer } = {}) { - if (isVerbose) { + if (programOptions.isVerbose) { logProgress(title, { type, description, progressBarContainer }); } } diff --git a/lib/optimize.js b/lib/optimize.js index e8d4638..3a9c632 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -23,14 +23,16 @@ import { import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; +import { programOptions } from './program-options.js'; import showTotal from './show-total.js'; export default async function optimize({ inputFilePaths, outputDirectoryPath, - isLossless, config, }) { + const { isLossless } = programOptions; + const inputFilePathsCount = inputFilePaths.length; if (inputFilePathsCount <= 0) { diff --git a/lib/program-options.js b/lib/program-options.js new file mode 100644 index 0000000..b50874f --- /dev/null +++ b/lib/program-options.js @@ -0,0 +1,12 @@ +export const programOptions = { + shouldConvertToAvif: false, + shouldConvertToWebp: false, + isForced: false, + isLossless: false, + isVerbose: false, +}; + +export function setProgramOptions(options) { + Object.assign(programOptions, options); + Object.freeze(programOptions); +} From 2d3ce929f8a33f98cc40796c907a6361ea183981 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:13:55 +0700 Subject: [PATCH 33/45] Improve showTotal --- lib/convert.js | 4 +--- lib/optimize.js | 4 +--- lib/show-total.js | 14 +++++++------- tests/show-total.test.js | 6 +++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 9e2c528..aec00db 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -16,7 +16,6 @@ import { getPlural } from './get-plural.js'; import { LOG_TYPES, log, - logEmptyLine, logProgress, logProgressVerbose, } from './log.js'; @@ -24,7 +23,7 @@ import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import { programOptions } from './program-options.js'; -import showTotal from './show-total.js'; +import { showTotal } from './show-total.js'; export default async function convert({ inputFilePaths, @@ -112,7 +111,6 @@ export default async function convert({ progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 progressBarContainer.stop(); - logEmptyLine(); showTotal(totalSize.before, totalSize.after); } diff --git a/lib/optimize.js b/lib/optimize.js index 3a9c632..89ccd4c 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -16,7 +16,6 @@ import { getPlural } from './get-plural.js'; import { LOG_TYPES, log, - logEmptyLine, logProgress, logProgressVerbose, } from './log.js'; @@ -24,7 +23,7 @@ import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; import prepareWriteFilePath from './prepare-write-file-path.js'; import { programOptions } from './program-options.js'; -import showTotal from './show-total.js'; +import { showTotal } from './show-total.js'; export default async function optimize({ inputFilePaths, @@ -73,7 +72,6 @@ export default async function optimize({ progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 progressBarContainer.stop(); - logEmptyLine(); showTotal(totalSize.before, totalSize.after); } diff --git a/lib/show-total.js b/lib/show-total.js index 59c996b..1aef226 100644 --- a/lib/show-total.js +++ b/lib/show-total.js @@ -1,14 +1,14 @@ import { calculateRatio } from './calculate-ratio.js'; import { formatBytes } from './format-bytes.js'; -import { log } from './log.js'; +import { log, logEmptyLine } from './log.js'; -export default function showTotal(before, after) { +export function showTotal(before, after) { const ratio = calculateRatio(before, after); const saved = formatBytes(before - after); - if (ratio > 0) { - log(`Yay! You saved ${saved} (${ratio}%)`); - } else { - log('Done!'); - } + logEmptyLine(); + log(ratio > 0 + ? `Yay! You saved ${saved} (${ratio}%)` + : 'Done!', + ); } diff --git a/tests/show-total.test.js b/tests/show-total.test.js index ba7602a..0c4cd5e 100644 --- a/tests/show-total.test.js +++ b/tests/show-total.test.js @@ -1,6 +1,6 @@ import { jest } from '@jest/globals'; -import showTotal from '../lib/show-total.js'; +import { showTotal } from '../lib/show-total.js'; test('Savings size and compression ratio are displayed', () => { const fileSize = 1_048_576; @@ -8,7 +8,7 @@ test('Savings size and compression ratio are displayed', () => { console.log = jest.fn(); showTotal(fileSize, fileSize / 2); - expect(console.log.mock.calls[0][1]).toBe('Yay! You saved 512 KB (50%)'); + expect(console.log.mock.calls[1][1]).toBe('Yay! You saved 512 KB (50%)'); console.log.mockRestore(); }); @@ -19,7 +19,7 @@ test('Savings size and compression ratio are not displayed', () => { console.log = jest.fn(); showTotal(fileSize, fileSize * 2); - expect(console.log.mock.calls[0][1]).toBe('Done!'); + expect(console.log.mock.calls[1][1]).toBe('Done!'); console.log.mockRestore(); }); From b56e899e99137af45d478c88a0942f8c2a8dc37b Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:16:48 +0700 Subject: [PATCH 34/45] Fix output to custom directory --- index.js | 12 +++--- lib/convert.js | 43 ++++++++----------- lib/get-relative-path.js | 9 ++++ lib/optimize.js | 38 +++++++---------- lib/prepare-file-paths.js | 73 ++++++++++++++++++++++++++++++++ lib/prepare-input-file-paths.js | 53 ----------------------- tests/cli.test.js | 12 +++--- tests/prepare-file-paths.test.js | 35 ++++++++++----- 8 files changed, 152 insertions(+), 123 deletions(-) create mode 100644 lib/get-relative-path.js create mode 100644 lib/prepare-file-paths.js delete mode 100644 lib/prepare-input-file-paths.js diff --git a/index.js b/index.js index 3f92e14..1edc383 100755 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ import convert from './lib/convert.js'; import { findConfig } from './lib/find-config.js'; import { log, logErrorAndExit } from './lib/log.js'; import optimize from './lib/optimize.js'; -import { prepareInputFilePaths } from './lib/prepare-input-file-paths.js'; +import { prepareFilePaths } from './lib/prepare-file-paths.js'; import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; import { programOptions } from './lib/program-options.js'; @@ -41,8 +41,11 @@ export default async function optimizt({ const configData = await import(preparedConfigFilePath); const config = configData.default[currentMode.toLowerCase()]; - const preparedInputFilePaths = await prepareInputFilePaths(inputPaths, SUPPORTED_FILE_TYPES[currentMode.toUpperCase()]); - const preparedOutputDirectoryPath = await prepareOutputDirectoryPath(outputDirectoryPath); + const filePaths = await prepareFilePaths({ + inputPaths, + outputDirectoryPath: await prepareOutputDirectoryPath(outputDirectoryPath), + extensions: SUPPORTED_FILE_TYPES[currentMode.toUpperCase()], + }); if (isLossless) { log('Lossless optimization may take a long time'); @@ -53,8 +56,7 @@ export default async function optimizt({ : optimize; await processFunction({ - inputFilePaths: preparedInputFilePaths, - outputDirectoryPath: preparedOutputDirectoryPath, + filePaths, config, }); } diff --git a/lib/convert.js b/lib/convert.js index aec00db..f247350 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -13,6 +13,7 @@ import { createProgressBarContainer } from './create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './constants.js'; import { formatBytes } from './format-bytes.js'; import { getPlural } from './get-plural.js'; +import { getRelativePath } from './get-relative-path.js'; import { LOG_TYPES, log, @@ -21,15 +22,10 @@ import { } from './log.js'; import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; -import prepareWriteFilePath from './prepare-write-file-path.js'; import { programOptions } from './program-options.js'; import { showTotal } from './show-total.js'; -export default async function convert({ - inputFilePaths, - outputDirectoryPath, - config, -}) { +export default async function convert({ filePaths, config }) { const { isForced, isLossless, @@ -37,17 +33,17 @@ export default async function convert({ shouldConvertToWebp, } = programOptions; - const inputFilePathsCount = inputFilePaths.length; + const filePathsCount = filePaths.length; - if (!inputFilePathsCount) { + if (!filePathsCount) { return; } - log(`Converting ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + log(`Converting ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); const progressBarTotal = shouldConvertToAvif && shouldConvertToWebp - ? inputFilePathsCount * 2 - : inputFilePathsCount; + ? filePathsCount * 2 + : filePathsCount; const progressBarContainer = createProgressBarContainer(progressBarTotal); const progressBar = progressBarContainer.create(progressBarTotal, 0); @@ -64,13 +60,12 @@ export default async function convert({ : config?.webpGif?.lossy; const tasksSimultaneousLimit = pLimit(os.cpus().length); - const tasksPromises = inputFilePaths.reduce((accumulator, filePath) => { + const tasksPromises = filePaths.reduce((accumulator, filePath) => { if (shouldConvertToAvif) { accumulator.push( tasksSimultaneousLimit( () => processFile({ filePath, - outputDirectoryPath, config: avifConfig || {}, progressBarContainer, progressBar, @@ -88,8 +83,7 @@ export default async function convert({ tasksSimultaneousLimit( () => processFile({ filePath, - outputDirectoryPath, - config: (path.extname(filePath).toLowerCase() === '.gif' + config: (path.extname(filePath.input).toLowerCase() === '.gif' ? webpGifConfig : webpConfig) || {}, @@ -116,7 +110,6 @@ export default async function convert({ async function processFile({ filePath, - outputDirectoryPath, config, progressBarContainer, progressBar, @@ -126,25 +119,25 @@ async function processFile({ processFunction, }) { try { - const { dir, name } = path.parse(filePath); + const { dir, name } = path.parse(filePath.output); const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); - const preparedOutputFilePath = prepareWriteFilePath(outputFilePath, outputDirectoryPath); - const isAccessible = await checkPathAccessibility(preparedOutputFilePath); + const isAccessible = await checkPathAccessibility(outputFilePath); if (!isForced && isAccessible) { - logProgressVerbose(preparedOutputFilePath, { - description: `File already exists, '${preparedOutputFilePath}'`, + logProgressVerbose(getRelativePath(outputFilePath), { + description: `File already exists, '${outputFilePath}'`, progressBarContainer, }); return; } - const fileBuffer = await fs.promises.readFile(filePath); + const fileBuffer = await fs.promises.readFile(filePath.input); const processedFileBuffer = await processFunction({ fileBuffer, config }); - await fs.promises.writeFile(preparedOutputFilePath, processedFileBuffer); + await fs.promises.mkdir(path.dirname(outputFilePath), { recursive: true }); + await fs.promises.writeFile(outputFilePath, processedFileBuffer); const fileSize = fileBuffer.length; const processedFileSize = processedFileBuffer.length; @@ -156,14 +149,14 @@ async function processFile({ const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - logProgress(filePath, { + logProgress(getRelativePath(filePath.input), { type: LOG_TYPES.SUCCESS, description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { - logProgress(filePath, { + logProgress(getRelativePath(filePath.input), { type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, diff --git a/lib/get-relative-path.js b/lib/get-relative-path.js new file mode 100644 index 0000000..348d301 --- /dev/null +++ b/lib/get-relative-path.js @@ -0,0 +1,9 @@ +import path from 'node:path'; + +export function getRelativePath(filePath) { + const replacePath = `${process.cwd()}${path.sep}`; + + return filePath.startsWith(replacePath) + ? filePath.slice(replacePath.length) + : filePath; +} diff --git a/lib/optimize.js b/lib/optimize.js index 89ccd4c..9bdd2f2 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -13,6 +13,7 @@ import { calculateRatio } from './calculate-ratio.js'; import { createProgressBarContainer } from './create-progress-bar-container.js'; import { formatBytes } from './format-bytes.js'; import { getPlural } from './get-plural.js'; +import { getRelativePath } from './get-relative-path.js'; import { LOG_TYPES, log, @@ -21,27 +22,22 @@ import { } from './log.js'; import { optionsToArguments } from './options-to-arguments.js'; import { parseImageMetadata } from './parse-image-metadata.js'; -import prepareWriteFilePath from './prepare-write-file-path.js'; import { programOptions } from './program-options.js'; import { showTotal } from './show-total.js'; -export default async function optimize({ - inputFilePaths, - outputDirectoryPath, - config, -}) { +export default async function optimize({ filePaths, config }) { const { isLossless } = programOptions; - const inputFilePathsCount = inputFilePaths.length; + const filePathsCount = filePaths.length; - if (inputFilePathsCount <= 0) { + if (filePathsCount <= 0) { return; } - log(`Optimizing ${inputFilePathsCount} ${getPlural(inputFilePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); + log(`Optimizing ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); - const progressBarContainer = createProgressBarContainer(inputFilePathsCount); - const progressBar = progressBarContainer.create(inputFilePathsCount, 0); + const progressBarContainer = createProgressBarContainer(filePathsCount); + const progressBar = progressBarContainer.create(filePathsCount, 0); const totalSize = { before: 0, after: 0 }; @@ -54,11 +50,10 @@ export default async function optimize({ */ isLossless ? Math.round(cpuCount / 2) : cpuCount, ); - const tasksPromises = inputFilePaths.map( + const tasksPromises = filePaths.map( filePath => tasksSimultaneousLimit( () => processFile({ filePath, - outputDirectoryPath, config, progressBarContainer, progressBar, @@ -77,7 +72,6 @@ export default async function optimize({ async function processFile({ filePath, - outputDirectoryPath, config, progressBarContainer, progressBar, @@ -85,7 +79,7 @@ async function processFile({ isLossless, }) { try { - const fileBuffer = await fs.promises.readFile(filePath); + const fileBuffer = await fs.promises.readFile(filePath.input); const processedFileBuffer = await processFileByFormat({ fileBuffer, config, isLossless }); const fileSize = fileBuffer.length; @@ -98,10 +92,10 @@ async function processFile({ const isOptimized = ratio > 0; const isChanged = !fileBuffer.equals(processedFileBuffer); - const isSvg = path.extname(filePath).toLowerCase() === '.svg'; + const isSvg = path.extname(filePath.input).toLowerCase() === '.svg'; if (!isOptimized && !(isChanged && isSvg)) { - logProgressVerbose(filePath, { + logProgressVerbose(getRelativePath(filePath.input), { description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, progressBarContainer, }); @@ -109,22 +103,20 @@ async function processFile({ return; } - await fs.promises.writeFile( - prepareWriteFilePath(filePath, outputDirectoryPath), - processedFileBuffer, - ); + await fs.promises.mkdir(path.dirname(filePath.output), { recursive: true }); + await fs.promises.writeFile(filePath.output, processedFileBuffer); const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - logProgress(filePath, { + logProgress(getRelativePath(filePath.input), { type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING, description: `${before} → ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { - logProgress(filePath, { + logProgress(getRelativePath(filePath.input), { type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, diff --git a/lib/prepare-file-paths.js b/lib/prepare-file-paths.js new file mode 100644 index 0000000..e4e07e6 --- /dev/null +++ b/lib/prepare-file-paths.js @@ -0,0 +1,73 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { fdir } from 'fdir'; + +export async function prepareFilePaths({ + inputPaths, + outputDirectoryPath, + extensions, +}) { + const files = new Set(); + const directories = new Set(); + const inputPathsSet = new Set(inputPaths); + + await Promise.all( + [...inputPathsSet].map(async currentPath => { + try { + const stat = await fs.promises.stat(currentPath); + const resolvedPath = path.resolve(currentPath); + + if (stat.isDirectory()) { + directories.add(resolvedPath); + } else if (stat.isFile() && checkFileType(resolvedPath, extensions)) { + files.add(resolvedPath); + } + } catch {} + }), + ); + + const crawler = new fdir() // eslint-disable-line new-cap + .withFullPaths() + .withDirs() + .filter(currentPath => checkFileType(currentPath, extensions)); + + const crawledPaths = await Promise.all( + [...directories].map(currentPath => crawler.crawl(currentPath).withPromise()), + ); + + for (const crawledPath of crawledPaths.flat()) { + files.add(crawledPath); + } + + const hasDirectories = directories.size > 0; + + const result = [...files].map(filePath => { + let outputPath = filePath; + + if (outputDirectoryPath) { + if (hasDirectories) { + for (const directory of directories) { + if (filePath.startsWith(directory)) { + outputPath = path.join(outputDirectoryPath, filePath.slice(directory.length)); + break; + } + } + } else { + outputPath = path.join(outputDirectoryPath, filePath.slice(path.dirname(filePath).length)); + } + } + + return { + input: filePath, + output: outputPath, + }; + }); + + return result; +} + +function checkFileType(filePath, extensions) { + const extension = path.extname(filePath).toLowerCase().slice(1); + return extensions.includes(extension); +} diff --git a/lib/prepare-input-file-paths.js b/lib/prepare-input-file-paths.js deleted file mode 100644 index 50c5398..0000000 --- a/lib/prepare-input-file-paths.js +++ /dev/null @@ -1,53 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import { fdir } from 'fdir'; - -export async function prepareInputFilePaths(paths, extensions) { - const files = new Set(); - const directories = new Set(); - - const pathsSet = new Set(paths); - - for (const currentPath of pathsSet) { - try { - const stat = await fs.promises.stat(currentPath); // eslint-disable-line no-await-in-loop - - if (stat.isDirectory()) { - directories.add(currentPath); - } else if (stat.isFile() && checkFileType(currentPath, extensions)) { - files.add(getRelativePath(currentPath)); - } - } catch { - continue; - } - } - - const crawler = new fdir() // eslint-disable-line new-cap - .withFullPaths() - .filter(currentPath => checkFileType(currentPath, extensions)); - - const crawlerPromises = [...directories].map(currentPath => crawler.crawl(currentPath).withPromise()); - const crawledPaths = await Promise.all(crawlerPromises); - - for (const crawledPath of crawledPaths.flat()) { - files.add(getRelativePath(crawledPath)); - } - - const filteredPaths = [...files]; - - return filteredPaths; -} - -function getRelativePath(filePath) { - const replacePath = `${process.cwd()}${path.sep}`; - - return filePath.startsWith(replacePath) - ? filePath.slice(replacePath.length) - : filePath; -} - -function checkFileType(filePath, extensions) { - const extension = path.extname(filePath).toLowerCase().slice(1); - return extensions.includes(extension); -} diff --git a/tests/cli.test.js b/tests/cli.test.js index e46b0e0..3835ea3 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -371,13 +371,13 @@ describe('CLI', () => { const fileName = 'png-not-optimized.png'; runCliWithParameters(`--output ${outputDirectory} ${workDirectory}${fileName}`); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, fileName))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, fileName))).toBeTruthy(); }); test('Should output list of files', () => { runCliWithParameters(`--output ${outputDirectory} ${workDirectory}*.jpg ${workDirectory}*.jpeg`); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, 'jpeg-low-quality.jpg'))).toBeTruthy(); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, 'jpeg-not-optimized.jpeg'))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, 'jpeg-low-quality.jpg'))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, 'jpeg-not-optimized.jpeg'))).toBeTruthy(); }); }); @@ -386,13 +386,13 @@ describe('CLI', () => { const fileBasename = 'png-not-optimized'; runCliWithParameters(`--avif --output ${outputDirectory} ${workDirectory}${fileBasename}.png`); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, `${fileBasename}.avif`))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, `${fileBasename}.avif`))).toBeTruthy(); }); test('Should output list of files', () => { runCliWithParameters(`--avif --output ${outputDirectory} ${workDirectory}*.jpg ${workDirectory}*.jpeg`); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, 'jpeg-low-quality.avif'))).toBeTruthy(); - expect(fs.existsSync(path.join(outputDirectory, workDirectory, 'jpeg-not-optimized.avif'))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, 'jpeg-low-quality.avif'))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, 'jpeg-not-optimized.avif'))).toBeTruthy(); }); }); }); diff --git a/tests/prepare-file-paths.test.js b/tests/prepare-file-paths.test.js index 1bd0457..bc3025e 100644 --- a/tests/prepare-file-paths.test.js +++ b/tests/prepare-file-paths.test.js @@ -1,7 +1,8 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { prepareInputFilePaths } from '../lib/prepare-input-file-paths.js'; +import { getRelativePath } from '../lib/get-relative-path.js'; +import { prepareFilePaths } from '../lib/prepare-file-paths.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -9,15 +10,20 @@ const DEFAULT_IMAGE_PATH = resolvePath(['images']); const DEFAULT_EXTENSIONS = ['gif', 'jpeg', 'jpg', 'png', 'svg']; test('Non-existent file paths are ignored', async () => { - const paths = [ - resolvePath(['not+exists']), - resolvePath(['not+exists.svg']), - ]; - expect(await prepareInputFilePaths(paths, DEFAULT_EXTENSIONS)).toStrictEqual([]); + const inputPaths = await generateInputPaths({ + inputPaths: [ + resolvePath(['not+exists']), + resolvePath(['not+exists.svg']), + ], + }); + + expect(inputPaths).toStrictEqual([]); }); test('Files from subdirectories are processed', async () => { - expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).toEqual( + const inputPaths = await generateInputPaths(); + + expect(inputPaths).toEqual( expect.arrayContaining([ expect.stringMatching(/file-in-subdirectory.jpg$/), ]), @@ -25,9 +31,9 @@ test('Files from subdirectories are processed', async () => { }); test('Files are filtered by extension', async () => { - const extensions = ['gif', 'jpeg', 'png', 'svg']; + const inputPaths = await generateInputPaths({ extensions: ['gif', 'jpeg', 'png', 'svg'] }); - expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], extensions)).toEqual( + expect(inputPaths).toEqual( expect.arrayContaining([ expect.stringMatching(/\.gif$/), expect.stringMatching(/\.png$/), @@ -35,7 +41,7 @@ test('Files are filtered by extension', async () => { ]), ); - expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], extensions)).not.toEqual( + expect(inputPaths).not.toEqual( expect.arrayContaining([ expect.stringMatching(/\.jpg$/), ]), @@ -43,7 +49,9 @@ test('Files are filtered by extension', async () => { }); test('Only relative file paths are generated', async () => { - expect(await prepareInputFilePaths([DEFAULT_IMAGE_PATH], DEFAULT_EXTENSIONS)).not.toEqual( + const inputPaths = await generateInputPaths(); + + expect(inputPaths).not.toEqual( expect.arrayContaining([ expect.stringMatching(new RegExp(`^${dirname}`)), ]), @@ -53,3 +61,8 @@ test('Only relative file paths are generated', async () => { function resolvePath(segments) { return path.resolve(dirname, ...segments); } + +async function generateInputPaths({ inputPaths = [DEFAULT_IMAGE_PATH], extensions = DEFAULT_EXTENSIONS } = {}) { + const result = await prepareFilePaths({ inputPaths, extensions }); + return result.map(item => getRelativePath(item.input)); +} From 081a8795ba86c4bf61ec0f32417c4b61e0a2c12d Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:56:58 +0700 Subject: [PATCH 35/45] Improve findConfig and rename to findConfigFilePath --- index.js | 28 +++---------------- ...ind-config.js => find-config-file-path.js} | 23 +++++++++++++-- 2 files changed, 24 insertions(+), 27 deletions(-) rename lib/{find-config.js => find-config-file-path.js} (61%) diff --git a/index.js b/index.js index 1edc383..121e60a 100755 --- a/index.js +++ b/index.js @@ -1,11 +1,9 @@ -import fs from 'node:fs'; -import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import convert from './lib/convert.js'; -import { findConfig } from './lib/find-config.js'; -import { log, logErrorAndExit } from './lib/log.js'; +import { findConfigFilePath } from './lib/find-config-file-path.js'; +import { log } from './lib/log.js'; import optimize from './lib/optimize.js'; import { prepareFilePaths } from './lib/prepare-file-paths.js'; import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; @@ -33,12 +31,8 @@ export default async function optimizt({ ? MODE_NAME.CONVERT : MODE_NAME.OPTIMIZE; - const preparedConfigFilePath = pathToFileURL( - configFilePath - ? resolveProvidedConfigPath(configFilePath) - : await findConfig(), - ); - const configData = await import(preparedConfigFilePath); + const foundConfigFilePath = pathToFileURL(await findConfigFilePath(configFilePath)); + const configData = await import(foundConfigFilePath); const config = configData.default[currentMode.toLowerCase()]; const filePaths = await prepareFilePaths({ @@ -60,17 +54,3 @@ export default async function optimizt({ config, }); } - -function resolveProvidedConfigPath(filepath = '') { - const resolvedPath = path.resolve(filepath); - - if (!fs.existsSync(resolvedPath)) { - logErrorAndExit('Provided config path does not exist'); - } - - if (!fs.statSync(resolvedPath).isFile()) { - logErrorAndExit('Provided config path must point to a file'); - } - - return resolvedPath; -} diff --git a/lib/find-config.js b/lib/find-config-file-path.js similarity index 61% rename from lib/find-config.js rename to lib/find-config-file-path.js index 2eeb907..1dcca7e 100644 --- a/lib/find-config.js +++ b/lib/find-config-file-path.js @@ -1,20 +1,37 @@ -import fs from 'node:fs/promises'; +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { DEFAULT_CONFIG_FILENAME } from './constants.js'; +import { logErrorAndExit } from './log.js'; const defaultDirectoryPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const defaultConfigPath = path.join(defaultDirectoryPath, DEFAULT_CONFIG_FILENAME); -export async function findConfig() { +export async function findConfigFilePath(providedConfigPath) { + if (providedConfigPath) { + const resolvedPath = path.resolve(providedConfigPath); + + try { + const stat = await fs.promises.stat(resolvedPath); + + if (!stat.isFile()) { + logErrorAndExit('Config path must point to a file'); + } + + return resolvedPath; + } catch { + logErrorAndExit(`Config file not exists: ${resolvedPath}`); + } + } + let currentDirectoryPath = path.resolve(process.cwd()); while (true) { // eslint-disable-line no-constant-condition const currentConfigPath = path.join(currentDirectoryPath, DEFAULT_CONFIG_FILENAME); try { - const stat = await fs.stat(currentConfigPath); // eslint-disable-line no-await-in-loop + const stat = await fs.promises.stat(currentConfigPath); // eslint-disable-line no-await-in-loop if (stat.isFile()) { return currentConfigPath; From 32048c6a709053ef22a7ac67c71c3ffcea8b975f Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:09:20 +0700 Subject: [PATCH 36/45] Remove prepareWriteFilePath --- lib/prepare-write-file-path.js | 16 ------------ tests/prepare-write-file-path.test.js | 36 --------------------------- 2 files changed, 52 deletions(-) delete mode 100644 lib/prepare-write-file-path.js delete mode 100644 tests/prepare-write-file-path.test.js diff --git a/lib/prepare-write-file-path.js b/lib/prepare-write-file-path.js deleted file mode 100644 index cfc0061..0000000 --- a/lib/prepare-write-file-path.js +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -export default function prepareWriteFilePath(filePath, output) { - if (!output) { - return filePath; - } - - const replacePath = `${process.cwd()}${path.sep}`; - const { base, dir } = path.parse(filePath); - const [, ...subDirectories] = dir.split(path.sep); - - fs.mkdirSync(path.join(output, ...subDirectories), { recursive: true }); - - return path.join(output, ...subDirectories, base).replace(replacePath, ''); -} diff --git a/tests/prepare-write-file-path.test.js b/tests/prepare-write-file-path.test.js deleted file mode 100644 index e86d4e3..0000000 --- a/tests/prepare-write-file-path.test.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import prepareWriteFilePath from '../lib/prepare-write-file-path.js'; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); - -const imageName = 'jpeg-one-pixel.jpg'; -const output = fs.mkdtempSync(path.join(os.tmpdir(), 'optimizt-test-')); - -afterAll(() => { - fs.rmSync(output, { recursive: true }); -}); - -test('Write path does not change', () => { - const filePath = path.resolve(dirname, 'images', imageName); - - expect(prepareWriteFilePath(filePath)).toBe(filePath); -}); - -test('Path changes when outputDir is specified', () => { - const filePath = path.join('path', imageName); - const outputFilePath = path.join(output, imageName); - - expect(prepareWriteFilePath(filePath, output)).toBe(outputFilePath); -}); - -test('Hierarchy is preserved', () => { - const filePath = path.join('path', 'with', 'subdirs'); - const outputFilePath = path.join(output, 'with', 'subdirs'); - - expect(prepareWriteFilePath(filePath, output)).toBe(outputFilePath); - expect(prepareWriteFilePath(path.join(filePath, imageName), output)).toBe(path.join(outputFilePath, imageName)); -}); From 59a2dba081daa05847b4e0bb669f6c7b593a3c95 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:16:48 +0700 Subject: [PATCH 37/45] Remove default export from convert --- index.js | 2 +- lib/convert.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 121e60a..57d511d 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ import { pathToFileURL } from 'node:url'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; -import convert from './lib/convert.js'; +import { convert } from './lib/convert.js'; import { findConfigFilePath } from './lib/find-config-file-path.js'; import { log } from './lib/log.js'; import optimize from './lib/optimize.js'; diff --git a/lib/convert.js b/lib/convert.js index f247350..b9ba529 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -25,7 +25,7 @@ import { parseImageMetadata } from './parse-image-metadata.js'; import { programOptions } from './program-options.js'; import { showTotal } from './show-total.js'; -export default async function convert({ filePaths, config }) { +export async function convert({ filePaths, config }) { const { isForced, isLossless, From 826496dbfd427903dd573ada7792815a3f162620 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:17:29 +0700 Subject: [PATCH 38/45] Remove default export from optimize --- index.js | 2 +- lib/optimize.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 57d511d..e6fc17e 100755 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import { convert } from './lib/convert.js'; import { findConfigFilePath } from './lib/find-config-file-path.js'; import { log } from './lib/log.js'; -import optimize from './lib/optimize.js'; +import { optimize } from './lib/optimize.js'; import { prepareFilePaths } from './lib/prepare-file-paths.js'; import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; import { programOptions } from './lib/program-options.js'; diff --git a/lib/optimize.js b/lib/optimize.js index 9bdd2f2..1a44cf2 100644 --- a/lib/optimize.js +++ b/lib/optimize.js @@ -25,7 +25,7 @@ import { parseImageMetadata } from './parse-image-metadata.js'; import { programOptions } from './program-options.js'; import { showTotal } from './show-total.js'; -export default async function optimize({ filePaths, config }) { +export async function optimize({ filePaths, config }) { const { isLossless } = programOptions; const filePathsCount = filePaths.length; From f60649bc81705b963df88e9dac0de14b8a85b447 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:24:38 +0700 Subject: [PATCH 39/45] Move convert.js to project root --- .dockerignore | 1 + lib/convert.js => convert.js | 24 ++++++++++++------------ index.js | 3 ++- package.json | 1 + 4 files changed, 16 insertions(+), 13 deletions(-) rename lib/convert.js => convert.js (88%) diff --git a/.dockerignore b/.dockerignore index 96b110d..a8f8c4c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ !lib/ !.optimiztrc.cjs !cli.js +!convert.js !index.js !LICENSE !package*.json diff --git a/lib/convert.js b/convert.js similarity index 88% rename from lib/convert.js rename to convert.js index b9ba529..c78d856 100644 --- a/lib/convert.js +++ b/convert.js @@ -7,23 +7,23 @@ import gif2webp from 'gif2webp-bin'; import pLimit from 'p-limit'; import sharp from 'sharp'; -import { calculateRatio } from './calculate-ratio.js'; -import { checkPathAccessibility } from './check-path-accessibility.js'; -import { createProgressBarContainer } from './create-progress-bar-container.js'; -import { SUPPORTED_FILE_TYPES } from './constants.js'; -import { formatBytes } from './format-bytes.js'; -import { getPlural } from './get-plural.js'; -import { getRelativePath } from './get-relative-path.js'; +import { calculateRatio } from './lib/calculate-ratio.js'; +import { checkPathAccessibility } from './lib/check-path-accessibility.js'; +import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; +import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; +import { formatBytes } from './lib/format-bytes.js'; +import { getPlural } from './lib/get-plural.js'; +import { getRelativePath } from './lib/get-relative-path.js'; import { LOG_TYPES, log, logProgress, logProgressVerbose, -} from './log.js'; -import { optionsToArguments } from './options-to-arguments.js'; -import { parseImageMetadata } from './parse-image-metadata.js'; -import { programOptions } from './program-options.js'; -import { showTotal } from './show-total.js'; +} from './lib/log.js'; +import { optionsToArguments } from './lib/options-to-arguments.js'; +import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { programOptions } from './lib/program-options.js'; +import { showTotal } from './lib/show-total.js'; export async function convert({ filePaths, config }) { const { diff --git a/index.js b/index.js index e6fc17e..6f960bc 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,8 @@ import { pathToFileURL } from 'node:url'; +import { convert } from './convert.js'; + import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; -import { convert } from './lib/convert.js'; import { findConfigFilePath } from './lib/find-config-file-path.js'; import { log } from './lib/log.js'; import { optimize } from './lib/optimize.js'; diff --git a/package.json b/package.json index a13f073..cb60370 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "files": [ "MIGRATION.md", "cli.js", + "convert.js", "index.js", "lib/", "svgo/", From c951ef4d531b7b7c375a704faa1df01b02c08e0b Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:33:35 +0700 Subject: [PATCH 40/45] Move optimize.js to project root --- .dockerignore | 1 + index.js | 2 +- lib/optimize.js => optimize.js | 20 ++++++++++---------- package.json | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) rename lib/optimize.js => optimize.js (90%) diff --git a/.dockerignore b/.dockerignore index a8f8c4c..6d54d6c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,5 @@ !convert.js !index.js !LICENSE +!optimize.js !package*.json diff --git a/index.js b/index.js index 6f960bc..5631248 100755 --- a/index.js +++ b/index.js @@ -1,11 +1,11 @@ import { pathToFileURL } from 'node:url'; import { convert } from './convert.js'; +import { optimize } from './optimize.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import { findConfigFilePath } from './lib/find-config-file-path.js'; import { log } from './lib/log.js'; -import { optimize } from './lib/optimize.js'; import { prepareFilePaths } from './lib/prepare-file-paths.js'; import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; import { programOptions } from './lib/program-options.js'; diff --git a/lib/optimize.js b/optimize.js similarity index 90% rename from lib/optimize.js rename to optimize.js index 1a44cf2..6c43a57 100644 --- a/lib/optimize.js +++ b/optimize.js @@ -9,21 +9,21 @@ import pLimit from 'p-limit'; import sharp from 'sharp'; import { optimize as svgoOptimize } from 'svgo'; -import { calculateRatio } from './calculate-ratio.js'; -import { createProgressBarContainer } from './create-progress-bar-container.js'; -import { formatBytes } from './format-bytes.js'; -import { getPlural } from './get-plural.js'; -import { getRelativePath } from './get-relative-path.js'; +import { calculateRatio } from './lib/calculate-ratio.js'; +import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; +import { formatBytes } from './lib/format-bytes.js'; +import { getPlural } from './lib/get-plural.js'; +import { getRelativePath } from './lib/get-relative-path.js'; import { LOG_TYPES, log, logProgress, logProgressVerbose, -} from './log.js'; -import { optionsToArguments } from './options-to-arguments.js'; -import { parseImageMetadata } from './parse-image-metadata.js'; -import { programOptions } from './program-options.js'; -import { showTotal } from './show-total.js'; +} from './lib/log.js'; +import { optionsToArguments } from './lib/options-to-arguments.js'; +import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { programOptions } from './lib/program-options.js'; +import { showTotal } from './lib/show-total.js'; export async function optimize({ filePaths, config }) { const { isLossless } = programOptions; diff --git a/package.json b/package.json index cb60370..91ed49e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "convert.js", "index.js", "lib/", - "svgo/", + "optimize.js", ".optimiztrc.cjs" ], "scripts": { From 2ed50e44706660ed422a3237f05208e34d33ffb0 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:17:16 +0700 Subject: [PATCH 41/45] Bump fdir from 6.3.0 to 6.4.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62ddfc3..4eb10df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "cli-progress": "^3.11.0", "commander": "^12.1.0", "exec-buffer": "^3.2.0", - "fdir": "^6.3.0", + "fdir": "^6.4.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", "guetzli": "^5.0.0", @@ -4334,9 +4334,9 @@ } }, "node_modules/fdir": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", - "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", "peerDependencies": { "picomatch": "^3 || ^4" }, diff --git a/package.json b/package.json index 91ed49e..366ef83 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "cli-progress": "^3.11.0", "commander": "^12.1.0", "exec-buffer": "^3.2.0", - "fdir": "^6.3.0", + "fdir": "^6.4.0", "gif2webp-bin": "^5.0.0", "gifsicle": "^7.0.0", "guetzli": "^5.0.0", From cf23f5ef2b95a9b8d52dc4bfa695de1197bc0dae Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:18:35 +0700 Subject: [PATCH 42/45] Bump lint-staged from 15.2.8 to 15.2.10 --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4eb10df..8cfc913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@343dev/eslint-config": "^1.0.0", "eslint": "^8.57.0", "jest": "^29.7.0", - "lint-staged": "^15.2.8", + "lint-staged": "^15.2.10", "simple-git-hooks": "^2.11.1" }, "engines": { @@ -6517,9 +6517,9 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz", - "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==", + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, "dependencies": { "chalk": "~5.3.0", @@ -6528,7 +6528,7 @@ "execa": "~8.0.1", "lilconfig": "~3.1.2", "listr2": "~8.2.4", - "micromatch": "~4.0.7", + "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", "yaml": "~2.5.0" @@ -6855,9 +6855,9 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", diff --git a/package.json b/package.json index 366ef83..df4f5d2 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@343dev/eslint-config": "^1.0.0", "eslint": "^8.57.0", "jest": "^29.7.0", - "lint-staged": "^15.2.8", + "lint-staged": "^15.2.10", "simple-git-hooks": "^2.11.1" }, "simple-git-hooks": { From 05ab71e26df1b657f48759ade2f13fee10fe3bdb Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:42:03 +0700 Subject: [PATCH 43/45] Update MIGRATION.md --- MIGRATION.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index ac6c20f..879cd23 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,16 @@ # Migration +## 8.0.1 → 9.0.0 + +The main change in the new version is how Optimizt handles file processing. Before, the result was stored in memory until all files were processed. This could cause the app to crash if it ran out of memory, leading to a loss of optimization results. Now, each file is processed one by one, and the result is saved to disk immediately. Logging events also happen in real-time, instead of waiting until all files are done. + +Previously, if you tried to create an AVIF or WebP file that already existed, an error message would appear in the log. Now, it shows an informational message, but only if you use the --verbose option. + +To handle AVIF files, Optimizt uses the sharp module, which uses the libheif library. However, libheif doesn't support converting to animated AVIF (AVIS). In earlier versions, Optimizt would quietly convert animated GIFs into static AVIF. But from this version, an error will show up if you try to do this. + +The logic for creating file structures when using the --output option has also changed. Now, the original file structure will be recreated inside the output folder, starting from the folder passed to the app. Who knows how this worked before, but hopefully, it works better now! + + ## 7.0.2 → 8.0.0 Optimizt now checks for the EXIF Orientation tag before processing images and rotates the resulting image accordingly. From c239efbf71e50a4642109791d2afa387af43c266 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:43:23 +0700 Subject: [PATCH 44/45] Update CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e5fb0..790a86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 9.0.0 (2024-10-03) + +Changes: + +- Improved image processing workflow. Check the [migration guide](./MIGRATION.md) for details. +- Added an interactive log for image processing. +- Changed the “File already exists” message from “error” to “info,” now only shown in verbose mode. +- Added a notice that animated AVIF is not supported (unfortunately). +- Fixed the progress indicator in conversion mode. It now correctly shows the total number of items. +- Fixed output to the user directory. The original folder structure is now preserved (i hope so!). + ## 8.0.0 (2024-08-05) ❤️ Thank you for using Optimizt. If you have any suggestions or feedback, please don't hesitate to [open an issue](https://github.com/343dev/optimizt/issues). From dfb1bd901aca2f288b2fde897f7f2ecddef82884 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:13:30 +0700 Subject: [PATCH 45/45] 9.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cfc913..46302d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@343dev/optimizt", - "version": "8.0.1", + "version": "9.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@343dev/optimizt", - "version": "8.0.1", + "version": "9.0.0", "license": "MIT", "dependencies": { "cli-progress": "^3.11.0", diff --git a/package.json b/package.json index df4f5d2..05715b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@343dev/optimizt", - "version": "8.0.1", + "version": "9.0.0", "description": "CLI image optimization tool", "keywords": [ "svg",