diff --git a/CHANGELOG.md b/CHANGELOG.md index 142c6c0..8eeec12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [12.1.0] - 2025-12-24 + +### Added + +- Added `--prefix` and `--suffix` flags to add custom prefixes and suffixes to optimized file names. + +### Changed + +- Updated log output to display output filenames instead of input filenames. + ## [12.0.0] - 2025-12-23 ### Added diff --git a/README.md b/README.md index a012925..3598b4f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ optimizt path/to/picture.jpg - `-v, --verbose` — show detailed output (e.g. skipped files). - `-c, --config` — use a custom configuration file instead of the default. - `-o, --output` — write results to the specified directory. +- `-p, --prefix` — add prefix to optimized file names. +- `-s, --suffix` — add suffix to optimized file names. - `-V, --version` — display the tool version. - `-h, --help` — show help message. diff --git a/cli.js b/cli.js index ca419a9..14f2c27 100755 --- a/cli.js +++ b/cli.js @@ -19,7 +19,9 @@ program .option('-l, --lossless', 'perform lossless optimizations') .option('-v, --verbose', 'be verbose') .option('-c, --config ', 'use this configuration, overriding default config options if present') - .option('-o, --output ', 'write output to directory'); + .option('-o, --output ', 'write output to directory') + .option('-p, --prefix ', 'add prefix to optimized file names') + .option('-s, --suffix ', 'add suffix to optimized file names'); program .allowExcessArguments() @@ -31,7 +33,7 @@ program if (program.args.length === 0) { program.help(); } else { - const { avif, webp, force, lossless, verbose, config, output } = program.opts(); + const { avif, webp, force, lossless, verbose, config, output, prefix, suffix } = program.opts(); setProgramOptions({ shouldConvertToAvif: Boolean(avif), @@ -39,6 +41,8 @@ if (program.args.length === 0) { isForced: Boolean(force), isLossless: Boolean(lossless), isVerbose: Boolean(verbose), + filePrefix: prefix || '', + fileSuffix: suffix || '', }); optimizt({ diff --git a/convert.js b/convert.js index d1ecc0c..0037893 100644 --- a/convert.js +++ b/convert.js @@ -110,10 +110,10 @@ async function processFile({ format, processFunction, }) { - try { - const { dir, name } = path.parse(filePath.output); - const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); + const { dir, name } = path.parse(filePath.output); + const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); + try { const isAccessible = await checkPathAccessibility(outputFilePath); if (!isForced && isAccessible) { @@ -141,14 +141,14 @@ async function processFile({ const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - logProgress(getRelativePath(filePath.input), { + logProgress(getRelativePath(outputFilePath), { type: LOG_TYPES.SUCCESS, description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { - logProgress(getRelativePath(filePath.input), { + logProgress(getRelativePath(outputFilePath), { type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, diff --git a/lib/prepare-file-paths.js b/lib/prepare-file-paths.js index e4e07e6..79a47ed 100644 --- a/lib/prepare-file-paths.js +++ b/lib/prepare-file-paths.js @@ -3,6 +3,13 @@ import path from 'node:path'; import { fdir } from 'fdir'; +import { programOptions } from './program-options.js'; + +function sanitizeFilenamePart(part) { + // Remove characters that are forbidden in filenames across platforms + return part.replaceAll(/[<>:"|?*\\/]/g, ''); +} + export async function prepareFilePaths({ inputPaths, outputDirectoryPath, @@ -58,6 +65,15 @@ export async function prepareFilePaths({ } } + // Apply prefix and suffix to the basename + const { dir, base } = path.parse(outputPath); + const { name, ext } = path.parse(base); + const sanitizedPrefix = sanitizeFilenamePart(programOptions.filePrefix); + const sanitizedSuffix = sanitizeFilenamePart(programOptions.fileSuffix); + const newName = sanitizedPrefix + name + sanitizedSuffix; + const newBase = newName + ext; + outputPath = path.join(dir, newBase); + return { input: filePath, output: outputPath, diff --git a/lib/program-options.js b/lib/program-options.js index b50874f..63ce870 100644 --- a/lib/program-options.js +++ b/lib/program-options.js @@ -4,6 +4,8 @@ export const programOptions = { isForced: false, isLossless: false, isVerbose: false, + filePrefix: '', + fileSuffix: '', }; export function setProgramOptions(options) { diff --git a/optimize.js b/optimize.js index ba626ed..333c5d4 100644 --- a/optimize.js +++ b/optimize.js @@ -96,7 +96,7 @@ async function processFile({ const isSvg = path.extname(filePath.input).toLowerCase() === '.svg'; if (!isOptimized && (!isChanged || !isSvg)) { - logProgressVerbose(getRelativePath(filePath.input), { + logProgressVerbose(getRelativePath(filePath.output), { description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, progressBarContainer, }); @@ -110,14 +110,14 @@ async function processFile({ const before = formatBytes(fileSize); const after = formatBytes(processedFileSize); - logProgress(getRelativePath(filePath.input), { + logProgress(getRelativePath(filePath.output), { type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING, description: `${before} → ${after}. Ratio: ${ratio}%`, progressBarContainer, }); } catch (error) { if (error.message) { - logProgress(getRelativePath(filePath.input), { + logProgress(getRelativePath(filePath.output), { type: LOG_TYPES.ERROR, description: (error.message || '').trim(), progressBarContainer, diff --git a/package-lock.json b/package-lock.json index d8cd7e8..e2f4d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@343dev/optimizt", - "version": "12.0.0", + "version": "12.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@343dev/optimizt", - "version": "12.0.0", + "version": "12.1.0", "license": "MIT", "dependencies": { "@343dev/gifsicle": "1.0.0", diff --git a/package.json b/package.json index 04e32a4..2bb36b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@343dev/optimizt", - "version": "12.0.0", + "version": "12.1.0", "description": "CLI image optimization tool", "keywords": [ "svg", diff --git a/tests/cli.test.js b/tests/cli.test.js index c99f93f..04ef15c 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -309,7 +309,8 @@ describe('CLI', () => { const stdoutRatio = grepTotalRatio(stdout); expectStringContains(stdout, 'Converting 1 image (lossy)...'); - expectStringContains(stdout, path.join(temporary, `${fileBasename}.png`)); + expectStringContains(stdout, path.join(temporary, `${fileBasename}.avif`)); + expectStringContains(stdout, path.join(temporary, `${fileBasename}.webp`)); expectRatio(stdoutRatio, 85, 90); expectFileNotModified(`${fileBasename}.png`); expectFileExists(`${fileBasename}.avif`); @@ -324,7 +325,8 @@ describe('CLI', () => { const stdoutRatio = grepTotalRatio(stdout); expectStringContains(stdout, 'Converting 1 image (lossless)...'); - expectStringContains(stdout, path.join(temporary, `${fileBasename}.png`)); + expectStringContains(stdout, path.join(temporary, `${fileBasename}.avif`)); + expectStringContains(stdout, path.join(temporary, `${fileBasename}.webp`)); expectRatio(stdoutRatio, 35, 40); expectFileNotModified(`${fileBasename}.png`); expectFileExists(`${fileBasename}.avif`); @@ -413,6 +415,46 @@ describe('CLI', () => { }); }); + describe('Prefix and Suffix (--prefix --suffix)', () => { + let outputDirectory; + + beforeEach(() => { + outputDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'optimizt-test-')); + }); + + afterEach(() => { + if (outputDirectory) { + fs.rmSync(outputDirectory, { recursive: true }); + } + }); + + test('Should add prefix and suffix to optimized filenames', () => { + const fileName = 'png-not-optimized.png'; + const expectedOutput = 'prepng-not-optimizedsuf.png'; + + runCliWithParameters(`--prefix pre --suffix suf --output ${outputDirectory} ${workDirectory}${fileName}`); + expect(fs.existsSync(path.join(outputDirectory, expectedOutput))).toBeTruthy(); + }); + + test('Should add prefix and suffix to converted filenames', () => { + const fileBasename = 'png-not-optimized'; + const expectedAvif = 'prepng-not-optimizedsuf.avif'; + const expectedWebp = 'prepng-not-optimizedsuf.webp'; + + runCliWithParameters(`--avif --webp --prefix pre --suffix suf --output ${outputDirectory} ${workDirectory}${fileBasename}.png`); + expect(fs.existsSync(path.join(outputDirectory, expectedAvif))).toBeTruthy(); + expect(fs.existsSync(path.join(outputDirectory, expectedWebp))).toBeTruthy(); + }); + + test('Should sanitize forbidden characters in prefix and suffix', () => { + const fileName = 'png-not-optimized.png'; + const expectedOutput = 'unsafepng-not-optimizedunsafe.png'; + + runCliWithParameters(`--prefix "" --suffix "" --output ${outputDirectory} ${workDirectory}${fileName}`); + expect(fs.existsSync(path.join(outputDirectory, expectedOutput))).toBeTruthy(); + }); + }); + describe('Help (--help)', () => { const helpString = `\ Usage: cli [options] @@ -428,6 +470,8 @@ Options: -c, --config use this configuration, overriding default config options if present -o, --output write output to directory + -p, --prefix add prefix to optimized file names + -s, --suffix add suffix to optimized file names -V, --version output the version number -h, --help display help for command `; @@ -501,8 +545,6 @@ function expectRatio(current, min, max) { } function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) { - expectStringContains(stdout, path.join(temporary, file)); - const fileBasename = path.basename(file, path.extname(file)); const outputFile = outputExt ? `${fileBasename}.${outputExt}` : file; @@ -512,6 +554,7 @@ function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) { const calculatedRatio = calculateRatio(sizeBefore, sizeAfter); const stdoutRatio = grepTotalRatio(stdout); + expectStringContains(stdout, path.join(temporary, outputFile)); expect(stdoutRatio).toBe(calculatedRatio); expectRatio(stdoutRatio, minRatio, maxRatio); }