Skip to content

Commit 063d274

Browse files
authored
Add prefix and suffix options for optimized filenames (#38)
- Added `--prefix` and `--suffix` flags to add custom prefixes and suffixes to optimized file names - Updated log output to display output filenames instead of input filenames - Added sanitization for filename parts to remove forbidden characters - Updated tests to verify new functionality
2 parents 1b26612 + 34a5914 commit 063d274

10 files changed

Lines changed: 94 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [12.1.0] - 2025-12-24
9+
10+
### Added
11+
12+
- Added `--prefix` and `--suffix` flags to add custom prefixes and suffixes to optimized file names.
13+
14+
### Changed
15+
16+
- Updated log output to display output filenames instead of input filenames.
17+
818
## [12.0.0] - 2025-12-23
919

1020
### Added

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ optimizt path/to/picture.jpg
3838
- `-v, --verbose` — show detailed output (e.g. skipped files).
3939
- `-c, --config` — use a custom configuration file instead of the default.
4040
- `-o, --output` — write results to the specified directory.
41+
- `-p, --prefix` — add prefix to optimized file names.
42+
- `-s, --suffix` — add suffix to optimized file names.
4143
- `-V, --version` — display the tool version.
4244
- `-h, --help` — show help message.
4345

cli.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ program
1919
.option('-l, --lossless', 'perform lossless optimizations')
2020
.option('-v, --verbose', 'be verbose')
2121
.option('-c, --config <path>', 'use this configuration, overriding default config options if present')
22-
.option('-o, --output <path>', 'write output to directory');
22+
.option('-o, --output <path>', 'write output to directory')
23+
.option('-p, --prefix <text>', 'add prefix to optimized file names')
24+
.option('-s, --suffix <text>', 'add suffix to optimized file names');
2325

2426
program
2527
.allowExcessArguments()
@@ -31,14 +33,16 @@ program
3133
if (program.args.length === 0) {
3234
program.help();
3335
} else {
34-
const { avif, webp, force, lossless, verbose, config, output } = program.opts();
36+
const { avif, webp, force, lossless, verbose, config, output, prefix, suffix } = program.opts();
3537

3638
setProgramOptions({
3739
shouldConvertToAvif: Boolean(avif),
3840
shouldConvertToWebp: Boolean(webp),
3941
isForced: Boolean(force),
4042
isLossless: Boolean(lossless),
4143
isVerbose: Boolean(verbose),
44+
filePrefix: prefix || '',
45+
fileSuffix: suffix || '',
4246
});
4347

4448
optimizt({

convert.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ async function processFile({
110110
format,
111111
processFunction,
112112
}) {
113-
try {
114-
const { dir, name } = path.parse(filePath.output);
115-
const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`);
113+
const { dir, name } = path.parse(filePath.output);
114+
const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`);
116115

116+
try {
117117
const isAccessible = await checkPathAccessibility(outputFilePath);
118118

119119
if (!isForced && isAccessible) {
@@ -141,14 +141,14 @@ async function processFile({
141141
const before = formatBytes(fileSize);
142142
const after = formatBytes(processedFileSize);
143143

144-
logProgress(getRelativePath(filePath.input), {
144+
logProgress(getRelativePath(outputFilePath), {
145145
type: LOG_TYPES.SUCCESS,
146146
description: `${before}${format} ${after}. Ratio: ${ratio}%`,
147147
progressBarContainer,
148148
});
149149
} catch (error) {
150150
if (error.message) {
151-
logProgress(getRelativePath(filePath.input), {
151+
logProgress(getRelativePath(outputFilePath), {
152152
type: LOG_TYPES.ERROR,
153153
description: (error.message || '').trim(),
154154
progressBarContainer,

lib/prepare-file-paths.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import path from 'node:path';
33

44
import { fdir } from 'fdir';
55

6+
import { programOptions } from './program-options.js';
7+
8+
function sanitizeFilenamePart(part) {
9+
// Remove characters that are forbidden in filenames across platforms
10+
return part.replaceAll(/[<>:"|?*\\/]/g, '');
11+
}
12+
613
export async function prepareFilePaths({
714
inputPaths,
815
outputDirectoryPath,
@@ -58,6 +65,15 @@ export async function prepareFilePaths({
5865
}
5966
}
6067

68+
// Apply prefix and suffix to the basename
69+
const { dir, base } = path.parse(outputPath);
70+
const { name, ext } = path.parse(base);
71+
const sanitizedPrefix = sanitizeFilenamePart(programOptions.filePrefix);
72+
const sanitizedSuffix = sanitizeFilenamePart(programOptions.fileSuffix);
73+
const newName = sanitizedPrefix + name + sanitizedSuffix;
74+
const newBase = newName + ext;
75+
outputPath = path.join(dir, newBase);
76+
6177
return {
6278
input: filePath,
6379
output: outputPath,

lib/program-options.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const programOptions = {
44
isForced: false,
55
isLossless: false,
66
isVerbose: false,
7+
filePrefix: '',
8+
fileSuffix: '',
79
};
810

911
export function setProgramOptions(options) {

optimize.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ async function processFile({
9696
const isSvg = path.extname(filePath.input).toLowerCase() === '.svg';
9797

9898
if (!isOptimized && (!isChanged || !isSvg)) {
99-
logProgressVerbose(getRelativePath(filePath.input), {
99+
logProgressVerbose(getRelativePath(filePath.output), {
100100
description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`,
101101
progressBarContainer,
102102
});
@@ -110,14 +110,14 @@ async function processFile({
110110
const before = formatBytes(fileSize);
111111
const after = formatBytes(processedFileSize);
112112

113-
logProgress(getRelativePath(filePath.input), {
113+
logProgress(getRelativePath(filePath.output), {
114114
type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING,
115115
description: `${before}${after}. Ratio: ${ratio}%`,
116116
progressBarContainer,
117117
});
118118
} catch (error) {
119119
if (error.message) {
120-
logProgress(getRelativePath(filePath.input), {
120+
logProgress(getRelativePath(filePath.output), {
121121
type: LOG_TYPES.ERROR,
122122
description: (error.message || '').trim(),
123123
progressBarContainer,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@343dev/optimizt",
3-
"version": "12.0.0",
3+
"version": "12.1.0",
44
"description": "CLI image optimization tool",
55
"keywords": [
66
"svg",

tests/cli.test.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ describe('CLI', () => {
309309
const stdoutRatio = grepTotalRatio(stdout);
310310

311311
expectStringContains(stdout, 'Converting 1 image (lossy)...');
312-
expectStringContains(stdout, path.join(temporary, `${fileBasename}.png`));
312+
expectStringContains(stdout, path.join(temporary, `${fileBasename}.avif`));
313+
expectStringContains(stdout, path.join(temporary, `${fileBasename}.webp`));
313314
expectRatio(stdoutRatio, 85, 90);
314315
expectFileNotModified(`${fileBasename}.png`);
315316
expectFileExists(`${fileBasename}.avif`);
@@ -324,7 +325,8 @@ describe('CLI', () => {
324325
const stdoutRatio = grepTotalRatio(stdout);
325326

326327
expectStringContains(stdout, 'Converting 1 image (lossless)...');
327-
expectStringContains(stdout, path.join(temporary, `${fileBasename}.png`));
328+
expectStringContains(stdout, path.join(temporary, `${fileBasename}.avif`));
329+
expectStringContains(stdout, path.join(temporary, `${fileBasename}.webp`));
328330
expectRatio(stdoutRatio, 35, 40);
329331
expectFileNotModified(`${fileBasename}.png`);
330332
expectFileExists(`${fileBasename}.avif`);
@@ -413,6 +415,46 @@ describe('CLI', () => {
413415
});
414416
});
415417

418+
describe('Prefix and Suffix (--prefix --suffix)', () => {
419+
let outputDirectory;
420+
421+
beforeEach(() => {
422+
outputDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'optimizt-test-'));
423+
});
424+
425+
afterEach(() => {
426+
if (outputDirectory) {
427+
fs.rmSync(outputDirectory, { recursive: true });
428+
}
429+
});
430+
431+
test('Should add prefix and suffix to optimized filenames', () => {
432+
const fileName = 'png-not-optimized.png';
433+
const expectedOutput = 'prepng-not-optimizedsuf.png';
434+
435+
runCliWithParameters(`--prefix pre --suffix suf --output ${outputDirectory} ${workDirectory}${fileName}`);
436+
expect(fs.existsSync(path.join(outputDirectory, expectedOutput))).toBeTruthy();
437+
});
438+
439+
test('Should add prefix and suffix to converted filenames', () => {
440+
const fileBasename = 'png-not-optimized';
441+
const expectedAvif = 'prepng-not-optimizedsuf.avif';
442+
const expectedWebp = 'prepng-not-optimizedsuf.webp';
443+
444+
runCliWithParameters(`--avif --webp --prefix pre --suffix suf --output ${outputDirectory} ${workDirectory}${fileBasename}.png`);
445+
expect(fs.existsSync(path.join(outputDirectory, expectedAvif))).toBeTruthy();
446+
expect(fs.existsSync(path.join(outputDirectory, expectedWebp))).toBeTruthy();
447+
});
448+
449+
test('Should sanitize forbidden characters in prefix and suffix', () => {
450+
const fileName = 'png-not-optimized.png';
451+
const expectedOutput = 'unsafepng-not-optimizedunsafe.png';
452+
453+
runCliWithParameters(`--prefix "<un:safe>" --suffix "<un:safe>" --output ${outputDirectory} ${workDirectory}${fileName}`);
454+
expect(fs.existsSync(path.join(outputDirectory, expectedOutput))).toBeTruthy();
455+
});
456+
});
457+
416458
describe('Help (--help)', () => {
417459
const helpString = `\
418460
Usage: cli [options] <dir> <file ...>
@@ -428,6 +470,8 @@ Options:
428470
-c, --config <path> use this configuration, overriding default config options
429471
if present
430472
-o, --output <path> write output to directory
473+
-p, --prefix <text> add prefix to optimized file names
474+
-s, --suffix <text> add suffix to optimized file names
431475
-V, --version output the version number
432476
-h, --help display help for command
433477
`;
@@ -501,8 +545,6 @@ function expectRatio(current, min, max) {
501545
}
502546

503547
function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) {
504-
expectStringContains(stdout, path.join(temporary, file));
505-
506548
const fileBasename = path.basename(file, path.extname(file));
507549
const outputFile = outputExt ? `${fileBasename}.${outputExt}` : file;
508550

@@ -512,6 +554,7 @@ function expectFileRatio({ file, maxRatio, minRatio, stdout, outputExt }) {
512554
const calculatedRatio = calculateRatio(sizeBefore, sizeAfter);
513555
const stdoutRatio = grepTotalRatio(stdout);
514556

557+
expectStringContains(stdout, path.join(temporary, outputFile));
515558
expect(stdoutRatio).toBe(calculatedRatio);
516559
expectRatio(stdoutRatio, minRatio, maxRatio);
517560
}

0 commit comments

Comments
 (0)