Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 6 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ program
.option('-l, --lossless', 'perform lossless optimizations')
.option('-v, --verbose', 'be verbose')
.option('-c, --config <path>', 'use this configuration, overriding default config options if present')
.option('-o, --output <path>', 'write output to directory');
.option('-o, --output <path>', 'write output to directory')
.option('-p, --prefix <text>', 'add prefix to optimized file names')
.option('-s, --suffix <text>', 'add suffix to optimized file names');

program
.allowExcessArguments()
Expand All @@ -31,14 +33,16 @@ 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),
shouldConvertToWebp: Boolean(webp),
isForced: Boolean(force),
isLossless: Boolean(lossless),
isVerbose: Boolean(verbose),
filePrefix: prefix || '',
fileSuffix: suffix || '',
});

optimizt({
Expand Down
10 changes: 5 additions & 5 deletions convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions lib/prepare-file-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions lib/program-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const programOptions = {
isForced: false,
isLossless: false,
isVerbose: false,
filePrefix: '',
fileSuffix: '',
};

export function setProgramOptions(options) {
Expand Down
6 changes: 3 additions & 3 deletions optimize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@343dev/optimizt",
"version": "12.0.0",
"version": "12.1.0",
"description": "CLI image optimization tool",
"keywords": [
"svg",
Expand Down
51 changes: 47 additions & 4 deletions tests/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -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`);
Expand Down Expand Up @@ -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 "<un:safe>" --suffix "<un:safe>" --output ${outputDirectory} ${workDirectory}${fileName}`);
expect(fs.existsSync(path.join(outputDirectory, expectedOutput))).toBeTruthy();
});
});

describe('Help (--help)', () => {
const helpString = `\
Usage: cli [options] <dir> <file ...>
Expand All @@ -428,6 +470,8 @@ Options:
-c, --config <path> use this configuration, overriding default config options
if present
-o, --output <path> write output to directory
-p, --prefix <text> add prefix to optimized file names
-s, --suffix <text> add suffix to optimized file names
-V, --version output the version number
-h, --help display help for command
`;
Expand Down Expand Up @@ -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;

Expand All @@ -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);
}
Expand Down