@valdiszz53/image-optimizer-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +46 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/optimizer.d.ts +2 -0
- package/dist/optimizer.js +247 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/scanner.d.ts +1 -0
- package/dist/scanner.js +12 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +173 -0
- package/dist/utils.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladislav Tregubov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# Image Optimizer CLI
|
|
2
|
+
|
|
3
|
+
A fast and flexible CLI tool for optimizing images, converting formats and resizing images based on filename instructions or project configuration.
|
|
4
|
+
|
|
5
|
+
- [Features](#features)
|
|
6
|
+
- [Installation](#installation)
|
|
7
|
+
- [Usage](#usage)
|
|
8
|
+
- [Configuration](#configuration)
|
|
9
|
+
- [CLI Options](#cli-options)
|
|
10
|
+
- [Supported Formats](#supported-formats)
|
|
11
|
+
- [Filename Instructions](#filename-instructions)
|
|
12
|
+
- [SVG Support](#svg-support)
|
|
13
|
+
- [Configuration Priority](#configuration-priority)
|
|
14
|
+
- [Folder Structure](#folder-structure)
|
|
15
|
+
- [Processing Statistics](#processing-statistics)
|
|
16
|
+
- [Example Workflow](#example-workflow)
|
|
17
|
+
- [Requirements](#requirements)
|
|
18
|
+
- [License](#license)
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- ๐ Optimize JPEG, PNG, WebP, AVIF and SVG images
|
|
23
|
+
- ๐ Recursive directory scanning
|
|
24
|
+
- ๐ Preserve folder structure
|
|
25
|
+
- ๐ผ Convert images to multiple formats
|
|
26
|
+
- ๐ Resize images directly from filename
|
|
27
|
+
- ๐ Configurable compression quality
|
|
28
|
+
- โ๏ธ Project configuration file
|
|
29
|
+
- ๐ป CLI arguments
|
|
30
|
+
- ๐ Dry-run mode
|
|
31
|
+
- ๐งน Clean output directory
|
|
32
|
+
- ๐ Processing statistics
|
|
33
|
+
- โก Powered by Sharp
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Global
|
|
39
|
+
npm install -g image-optimizer-cli
|
|
40
|
+
|
|
41
|
+
# Using npx
|
|
42
|
+
npx image-optimizer-cli ./images
|
|
43
|
+
|
|
44
|
+
# Local
|
|
45
|
+
npm install image-optimizer-cli
|
|
46
|
+
# or
|
|
47
|
+
npm install
|
|
48
|
+
npm link
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
image-optimizer [input-directory] [options]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The input directory can be provided either:
|
|
58
|
+
|
|
59
|
+
- as the first CLI argument;
|
|
60
|
+
- in the configuration file (`inputDir`).
|
|
61
|
+
|
|
62
|
+
### Examples
|
|
63
|
+
|
|
64
|
+
Optimize images:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
image-optimizer ./images
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Save into another directory:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
image-optimizer ./images \
|
|
74
|
+
--output ./dist
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Convert every image to WebP:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
image-optimizer ./images \
|
|
81
|
+
--format webp
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Change quality:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
image-optimizer ./images \
|
|
88
|
+
--quality 70
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Dry run:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
image-optimizer ./images \
|
|
95
|
+
--dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use custom configuration:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
image-optimizer \
|
|
102
|
+
--config ./configs/image-optimizer.config.ts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
Create a file:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
image-optimizer.config.ts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { defineConfig } from 'image-optimizer'
|
|
117
|
+
|
|
118
|
+
export default defineConfig({
|
|
119
|
+
inputDir: './assets',
|
|
120
|
+
outputDir: './public/assets',
|
|
121
|
+
quality: 80,
|
|
122
|
+
clean: true,
|
|
123
|
+
outputs: {
|
|
124
|
+
jpg: ['jpeg', 'webp'],
|
|
125
|
+
jpeg: ['jpeg', 'webp'],
|
|
126
|
+
png: ['png', 'webp'],
|
|
127
|
+
webp: ['webp'],
|
|
128
|
+
avif: ['avif'],
|
|
129
|
+
svg: ['svg'],
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
With this configuration:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
logo.png
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
becomes
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
logo.png
|
|
144
|
+
logo.webp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
without adding any filename instructions.
|
|
148
|
+
|
|
149
|
+
## CLI Options
|
|
150
|
+
|
|
151
|
+
| Option | Description |
|
|
152
|
+
| --------------- | ---------------------------------------- |
|
|
153
|
+
| `-c, --config` | Path to configuration file |
|
|
154
|
+
| `-o, --output` | Output directory |
|
|
155
|
+
| `-q, --quality` | Compression quality (1โ100) |
|
|
156
|
+
| `-f, --format` | Force output format |
|
|
157
|
+
| `--dry-run` | Show processing without writing files |
|
|
158
|
+
| `--clean` | Clean output directory before processing |
|
|
159
|
+
|
|
160
|
+
## Supported Formats
|
|
161
|
+
|
|
162
|
+
- JPG
|
|
163
|
+
- JPEG
|
|
164
|
+
- PNG
|
|
165
|
+
- WebP
|
|
166
|
+
- AVIF
|
|
167
|
+
- SVG
|
|
168
|
+
- TIFF
|
|
169
|
+
|
|
170
|
+
## Filename Instructions
|
|
171
|
+
|
|
172
|
+
Filename instructions always override both CLI arguments and configuration.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
logo[120].png
|
|
178
|
+
โ
|
|
179
|
+
logo.png
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Resize
|
|
183
|
+
|
|
184
|
+
| Instruction | Result |
|
|
185
|
+
| ------------------ | ---------------------------- |
|
|
186
|
+
| `logo[120].png` | width = 120px |
|
|
187
|
+
| `logo[120x80].png` | width = 120px, height = 80px |
|
|
188
|
+
| `logo[w=120].png` | width = 120px |
|
|
189
|
+
| `logo[h=80].png` | height = 80px |
|
|
190
|
+
| `logo[50%].png` | resize to 50% |
|
|
191
|
+
| `logo[2x].png` | scale ร2 |
|
|
192
|
+
|
|
193
|
+
### Format Conversion
|
|
194
|
+
|
|
195
|
+
Convert:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
photo[webp].png
|
|
199
|
+
โ
|
|
200
|
+
photo.webp
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Keep original:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
photo[+webp].png
|
|
207
|
+
โ
|
|
208
|
+
photo.png
|
|
209
|
+
photo.webp
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Generate multiple formats:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
photo[+webp,+avif].png
|
|
216
|
+
โ
|
|
217
|
+
photo.png
|
|
218
|
+
photo.webp
|
|
219
|
+
photo.avif
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Resize + convert:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
photo[320.webp].png
|
|
226
|
+
โ
|
|
227
|
+
photo.webp # width 320px
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Resize + keep original:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
photo[320.+webp].png
|
|
234
|
+
โ
|
|
235
|
+
photo.png
|
|
236
|
+
photo.webp
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Both images will be resized.
|
|
240
|
+
|
|
241
|
+
Per-image quality:
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
photo[q=60].jpg
|
|
245
|
+
or
|
|
246
|
+
photo[quality=60].jpg
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## SVG Support
|
|
250
|
+
|
|
251
|
+
SVG files are never rasterized.
|
|
252
|
+
|
|
253
|
+
Supported:
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
logo[24].svg
|
|
257
|
+
โ
|
|
258
|
+
<svg width="24px"/>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
logo[24x24].svg
|
|
263
|
+
โ
|
|
264
|
+
<svg
|
|
265
|
+
width="24px"
|
|
266
|
+
height="24px"/>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Configuration Priority
|
|
270
|
+
|
|
271
|
+
Image Optimizer resolves settings using the following priority:
|
|
272
|
+
|
|
273
|
+
1. **Filename instructions**
|
|
274
|
+
2. **CLI arguments**
|
|
275
|
+
3. **Configuration file**
|
|
276
|
+
4. **Default values**
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
|
|
280
|
+
Configuration:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
outputs: {
|
|
284
|
+
png: ['png', 'webp']
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Image:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
logo.png
|
|
292
|
+
โ
|
|
293
|
+
logo.png
|
|
294
|
+
logo.webp
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
But:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
logo[avif].png
|
|
301
|
+
โ
|
|
302
|
+
logo.avif
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
because filename instructions always have the highest priority.
|
|
306
|
+
|
|
307
|
+
## Folder Structure
|
|
308
|
+
|
|
309
|
+
Input:
|
|
310
|
+
|
|
311
|
+
```text
|
|
312
|
+
images
|
|
313
|
+
โโโ logo[120].png
|
|
314
|
+
โโโ hero.jpg
|
|
315
|
+
โโโ icons
|
|
316
|
+
โโโ menu[32].svg
|
|
317
|
+
โโโ close.png
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Configuration:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
export default defineConfig({
|
|
324
|
+
outputDir: './dist',
|
|
325
|
+
outputs: {
|
|
326
|
+
png: ['png', 'webp'],
|
|
327
|
+
jpg: ['jpeg'],
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Output:
|
|
333
|
+
|
|
334
|
+
```text
|
|
335
|
+
dist
|
|
336
|
+
โโโ logo.png
|
|
337
|
+
โโโ logo.webp
|
|
338
|
+
โโโ hero.jpeg
|
|
339
|
+
โโโ icons
|
|
340
|
+
โโโ menu.svg
|
|
341
|
+
โโโ close.png
|
|
342
|
+
โโโ close.webp
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The directory structure is always preserved.
|
|
346
|
+
|
|
347
|
+
## Processing Statistics
|
|
348
|
+
|
|
349
|
+
Example output:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
Found 42 image(s)
|
|
353
|
+
|
|
354
|
+
โ logo.png 420 KB โ 95 KB (-77%)
|
|
355
|
+
โ logo.webp 420 KB โ 62 KB (-85%)
|
|
356
|
+
โ hero.jpeg 1.8 MB โ 640 KB (-64%)
|
|
357
|
+
|
|
358
|
+
Summary
|
|
359
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
360
|
+
Processed: 63
|
|
361
|
+
Skipped: 0
|
|
362
|
+
Before: 24.7 MB
|
|
363
|
+
After: 10.2 MB
|
|
364
|
+
Saved: 14.5 MB
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Example Workflow
|
|
368
|
+
|
|
369
|
+
Configuration:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
export default defineConfig({
|
|
373
|
+
inputDir: './assets',
|
|
374
|
+
outputDir: './public/assets',
|
|
375
|
+
outputs: {
|
|
376
|
+
png: ['png', 'webp'],
|
|
377
|
+
jpg: ['jpeg', 'webp'],
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Assets:
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
assets
|
|
386
|
+
โโโ logo.png
|
|
387
|
+
โโโ hero.jpg
|
|
388
|
+
โโโ avatar[50%].png
|
|
389
|
+
โโโ icon[32].svg
|
|
390
|
+
โโโ banner[+avif].jpg
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Result:
|
|
394
|
+
|
|
395
|
+
```text
|
|
396
|
+
public/assets
|
|
397
|
+
โโโ logo.png
|
|
398
|
+
โโโ logo.webp
|
|
399
|
+
โโโ hero.jpeg
|
|
400
|
+
โโโ hero.webp
|
|
401
|
+
โโโ avatar.png
|
|
402
|
+
โโโ avatar.webp
|
|
403
|
+
โโโ icon.svg
|
|
404
|
+
โโโ banner.jpg
|
|
405
|
+
โโโ banner.avif
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Requirements
|
|
409
|
+
|
|
410
|
+
- Node.js 18+
|
|
411
|
+
- Sharp 0.33+
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[MIT](LICENSE)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loadConfig, mergeConfigWithCliOptions } from './config.js';
|
|
5
|
+
import { optimizeDirectory } from './index.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('image-optimizer')
|
|
9
|
+
.description('Optimize images from a directory')
|
|
10
|
+
.argument('[input]', 'Input directory with images')
|
|
11
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
12
|
+
.option('-o, --output <directory>', 'Output directory')
|
|
13
|
+
.option('-q, --quality <number>', 'Image quality from 1 to 100')
|
|
14
|
+
.option('-f, --format <format>', 'Convert all images to format: jpeg, png, webp, avif')
|
|
15
|
+
.option('--dry-run', 'Show what will be processed without writing files')
|
|
16
|
+
.option('--clean', 'Clean output directory before optimization')
|
|
17
|
+
.parse(process.argv);
|
|
18
|
+
const options = program.opts();
|
|
19
|
+
const input = program.args[0];
|
|
20
|
+
try {
|
|
21
|
+
const config = await loadConfig(options.config);
|
|
22
|
+
const finalOptions = mergeConfigWithCliOptions({
|
|
23
|
+
config,
|
|
24
|
+
cliOptions: {
|
|
25
|
+
inputDir: input,
|
|
26
|
+
outputDir: options.output,
|
|
27
|
+
quality: options.quality ? Number(options.quality) : undefined,
|
|
28
|
+
format: options.format,
|
|
29
|
+
dryRun: options.dryRun,
|
|
30
|
+
clean: options.clean,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!finalOptions.inputDir) {
|
|
34
|
+
throw new Error('Input directory is required. Pass it as CLI argument or define inputDir in config.');
|
|
35
|
+
}
|
|
36
|
+
await optimizeDirectory(finalOptions);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
console.error(chalk.red('Error:'), message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAG9C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACF,IAAI,CAAC,iBAAiB,CAAC;KACvB,WAAW,CAAC,kCAAkC,CAAC;KAC/C,QAAQ,CAAC,SAAS,EAAE,6BAA6B,CAAC;KAClD,MAAM,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;KACpD,MAAM,CAAC,0BAA0B,EAAE,kBAAkB,CAAC;KACtD,MAAM,CAAC,wBAAwB,EAAE,6BAA6B,CAAC;KAC/D,MAAM,CACH,uBAAuB,EACvB,qDAAqD,CACxD;KACA,MAAM,CAAC,WAAW,EAAE,mDAAmD,CAAC;KACxE,MAAM,CAAC,SAAS,EAAE,4CAA4C,CAAC;KAC/D,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAExB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAOxB,CAAA;AAEJ,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAE7B,IAAI,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAE/C,MAAM,YAAY,GAAG,yBAAyB,CAAC;QAC3C,MAAM;QACN,UAAU,EAAE;YACR,QAAQ,EAAE,KAAK;YACf,SAAS,EAAE,OAAO,CAAC,MAAM;YACzB,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;YAC9D,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,KAAK,EAAE,OAAO,CAAC,KAAK;SACvB;KACJ,CAAC,CAAA;IAEF,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACX,oFAAoF,CACvF,CAAA;IACL,CAAC;IAED,MAAM,iBAAiB,CAAC,YAAY,CAAC,CAAA;AACzC,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACb,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAEtE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACnB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ImageOptimizerConfig } from './types.js';
|
|
2
|
+
export declare function defineConfig(config: ImageOptimizerConfig): ImageOptimizerConfig;
|
|
3
|
+
export declare function loadConfig(configPath?: string): Promise<ImageOptimizerConfig>;
|
|
4
|
+
export declare function mergeConfigWithCliOptions({ config, cliOptions, }: {
|
|
5
|
+
config: ImageOptimizerConfig;
|
|
6
|
+
cliOptions: ImageOptimizerConfig;
|
|
7
|
+
}): ImageOptimizerConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
const DEFAULT_CONFIG_FILES = [
|
|
5
|
+
'image-optimizer.config.ts',
|
|
6
|
+
'image-optimizer.config.js',
|
|
7
|
+
'image-optimizer.config.mjs',
|
|
8
|
+
];
|
|
9
|
+
export function defineConfig(config) {
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
12
|
+
export async function loadConfig(configPath) {
|
|
13
|
+
const resolvedConfigPath = configPath
|
|
14
|
+
? path.resolve(configPath)
|
|
15
|
+
: await findConfigFile();
|
|
16
|
+
if (!resolvedConfigPath) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
const moduleUrl = pathToFileURL(resolvedConfigPath).href;
|
|
20
|
+
const configModule = await import(moduleUrl);
|
|
21
|
+
return configModule.default ?? configModule;
|
|
22
|
+
}
|
|
23
|
+
async function findConfigFile() {
|
|
24
|
+
for (const file of DEFAULT_CONFIG_FILES) {
|
|
25
|
+
const fullPath = path.resolve(file);
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(fullPath);
|
|
28
|
+
return fullPath;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
export function mergeConfigWithCliOptions({ config, cliOptions, }) {
|
|
37
|
+
return {
|
|
38
|
+
...config,
|
|
39
|
+
...removeUndefined(cliOptions),
|
|
40
|
+
outputs: cliOptions.outputs ?? config.outputs,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function removeUndefined(object) {
|
|
44
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,MAAM,oBAAoB,GAAG;IACzB,2BAA2B;IAC3B,2BAA2B;IAC3B,4BAA4B;CAC/B,CAAA;AAED,MAAM,UAAU,YAAY,CACxB,MAA4B;IAE5B,OAAO,MAAM,CAAA;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC5B,UAAmB;IAEnB,MAAM,kBAAkB,GAAG,UAAU;QACjC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;QAC1B,CAAC,CAAC,MAAM,cAAc,EAAE,CAAA;IAE5B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACtB,OAAO,EAAE,CAAA;IACb,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAA;IACxD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;IAE5C,OAAO,YAAY,CAAC,OAAO,IAAI,YAAY,CAAA;AAC/C,CAAC;AAED,KAAK,UAAU,cAAc;IACzB,KAAK,MAAM,IAAI,IAAI,oBAAoB,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAEnC,IAAI,CAAC;YACD,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YACzB,OAAO,QAAQ,CAAA;QACnB,CAAC;QAAC,MAAM,CAAC;YACL,SAAS;QACb,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAA;AACf,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,EACtC,MAAM,EACN,UAAU,GAIb;IACG,OAAO;QACH,GAAG,MAAM;QACT,GAAG,eAAe,CAAC,UAAU,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO;KAChD,CAAA;AACL,CAAC;AAED,SAAS,eAAe,CAAmB,MAAS;IAChD,OAAO,MAAM,CAAC,WAAW,CACrB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CACtD,CAAA;AACnB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { defineConfig } from './config.js';
|
|
2
|
+
export { optimizeDirectory } from './optimizer.js';
|
|
3
|
+
export type { ImageFormat, ImageInstructions, ImageOptimizerConfig, ImageOutput, InputImageFormat, OptimizeOptions, OutputImageFormat, OutputsConfig, OutputTask, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { scanImages } from './scanner.js';
|
|
6
|
+
import { cleanDirectory, ensureDirectory, formatBytes, getFileSize, getInputFormat, getOutputPath, normalizeFormat, normalizeOutputFormat, parseImageInstructions, pathExists, validateFormat, validateQuality, } from './utils.js';
|
|
7
|
+
export async function optimizeDirectory({ inputDir, outputDir, quality = 80, format, outputs, dryRun = false, clean = false, }) {
|
|
8
|
+
if (!inputDir) {
|
|
9
|
+
throw new Error('inputDir is required');
|
|
10
|
+
}
|
|
11
|
+
validateQuality(quality);
|
|
12
|
+
validateFormat(format);
|
|
13
|
+
const absoluteInputDir = path.resolve(inputDir);
|
|
14
|
+
const absoluteOutputDir = outputDir
|
|
15
|
+
? path.resolve(outputDir)
|
|
16
|
+
: absoluteInputDir;
|
|
17
|
+
if (!(await pathExists(absoluteInputDir))) {
|
|
18
|
+
throw new Error(`Input directory does not exist: ${absoluteInputDir}`);
|
|
19
|
+
}
|
|
20
|
+
if (clean && outputDir && !dryRun) {
|
|
21
|
+
await cleanDirectory(absoluteOutputDir);
|
|
22
|
+
}
|
|
23
|
+
const files = await scanImages(absoluteInputDir);
|
|
24
|
+
if (files.length === 0) {
|
|
25
|
+
console.log(chalk.yellow('No images found'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let totalBefore = 0;
|
|
29
|
+
let totalAfter = 0;
|
|
30
|
+
let processed = 0;
|
|
31
|
+
let skipped = 0;
|
|
32
|
+
console.log(chalk.cyan(`Found ${files.length} image(s)`));
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
const instructions = parseImageInstructions(file);
|
|
36
|
+
const beforeSize = await getFileSize(file);
|
|
37
|
+
const outputTasks = createOutputTasks({
|
|
38
|
+
inputDir: absoluteInputDir,
|
|
39
|
+
outputDir: absoluteOutputDir,
|
|
40
|
+
file,
|
|
41
|
+
cliFormat: format,
|
|
42
|
+
configOutputs: outputs,
|
|
43
|
+
instructions,
|
|
44
|
+
});
|
|
45
|
+
if (dryRun) {
|
|
46
|
+
for (const task of outputTasks) {
|
|
47
|
+
console.log(chalk.gray(`[dry-run] ${file} -> ${task.outputPath}`));
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const task of outputTasks) {
|
|
52
|
+
await ensureDirectory(path.dirname(task.outputPath));
|
|
53
|
+
await processImage({
|
|
54
|
+
file,
|
|
55
|
+
outputPath: task.outputPath,
|
|
56
|
+
outputFormat: task.format,
|
|
57
|
+
instructions,
|
|
58
|
+
quality: instructions.quality || quality,
|
|
59
|
+
});
|
|
60
|
+
const afterSize = await getFileSize(task.outputPath);
|
|
61
|
+
totalBefore += beforeSize;
|
|
62
|
+
totalAfter += afterSize;
|
|
63
|
+
processed++;
|
|
64
|
+
const percent = Math.round((1 - afterSize / beforeSize) * 100);
|
|
65
|
+
console.log(chalk.green('โ'), path.relative(process.cwd(), task.outputPath), chalk.gray(`${formatBytes(beforeSize)} โ ${formatBytes(afterSize)}`), percent >= 0
|
|
66
|
+
? chalk.green(`-${percent}%`)
|
|
67
|
+
: chalk.yellow(`+${Math.abs(percent)}%`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
skipped++;
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
console.log(chalk.red('โ'), path.relative(process.cwd(), file), chalk.gray(message));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(chalk.cyan('Summary'));
|
|
78
|
+
console.log(chalk.gray('โโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
79
|
+
console.log(`Processed: ${processed}`);
|
|
80
|
+
console.log(`Skipped: ${skipped}`);
|
|
81
|
+
console.log(`Before: ${formatBytes(totalBefore)}`);
|
|
82
|
+
console.log(`After: ${formatBytes(totalAfter)}`);
|
|
83
|
+
console.log(`Saved: ${formatBytes(totalBefore - totalAfter)}`);
|
|
84
|
+
}
|
|
85
|
+
function createOutputTasks({ inputDir, outputDir, file, cliFormat, configOutputs, instructions, }) {
|
|
86
|
+
const tasks = [];
|
|
87
|
+
const hasKeepOriginalOutput = instructions.outputs.some(output => output.keepOriginal);
|
|
88
|
+
if (hasKeepOriginalOutput) {
|
|
89
|
+
tasks.push({
|
|
90
|
+
format: null,
|
|
91
|
+
outputPath: getOutputPath({
|
|
92
|
+
inputDir,
|
|
93
|
+
outputDir,
|
|
94
|
+
filePath: file,
|
|
95
|
+
format: null,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (instructions.outputs.length > 0) {
|
|
100
|
+
for (const output of instructions.outputs) {
|
|
101
|
+
tasks.push({
|
|
102
|
+
format: output.format,
|
|
103
|
+
outputPath: getOutputPath({
|
|
104
|
+
inputDir,
|
|
105
|
+
outputDir,
|
|
106
|
+
filePath: file,
|
|
107
|
+
format: output.format,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return removeDuplicateTasks(tasks);
|
|
112
|
+
}
|
|
113
|
+
if (cliFormat) {
|
|
114
|
+
const normalizedFormat = normalizeFormat(cliFormat);
|
|
115
|
+
tasks.push({
|
|
116
|
+
format: normalizedFormat ?? null,
|
|
117
|
+
outputPath: getOutputPath({
|
|
118
|
+
inputDir,
|
|
119
|
+
outputDir,
|
|
120
|
+
filePath: file,
|
|
121
|
+
format: normalizedFormat,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
return removeDuplicateTasks(tasks);
|
|
125
|
+
}
|
|
126
|
+
const inputFormat = getInputFormat(file);
|
|
127
|
+
const configuredOutputs = configOutputs?.[inputFormat];
|
|
128
|
+
if (configuredOutputs?.length) {
|
|
129
|
+
for (const outputFormat of configuredOutputs) {
|
|
130
|
+
const normalizedOutputFormat = normalizeOutputFormat(outputFormat);
|
|
131
|
+
tasks.push({
|
|
132
|
+
format: normalizedOutputFormat ?? null,
|
|
133
|
+
outputPath: getOutputPath({
|
|
134
|
+
inputDir,
|
|
135
|
+
outputDir,
|
|
136
|
+
filePath: file,
|
|
137
|
+
format: normalizedOutputFormat,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return removeDuplicateTasks(tasks);
|
|
142
|
+
}
|
|
143
|
+
tasks.push({
|
|
144
|
+
format: null,
|
|
145
|
+
outputPath: getOutputPath({
|
|
146
|
+
inputDir,
|
|
147
|
+
outputDir,
|
|
148
|
+
filePath: file,
|
|
149
|
+
format: null,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
return tasks;
|
|
153
|
+
}
|
|
154
|
+
function removeDuplicateTasks(tasks) {
|
|
155
|
+
const map = new Map();
|
|
156
|
+
for (const task of tasks) {
|
|
157
|
+
map.set(task.outputPath, task);
|
|
158
|
+
}
|
|
159
|
+
return [...map.values()];
|
|
160
|
+
}
|
|
161
|
+
async function processImage({ file, outputPath, outputFormat, instructions, quality, }) {
|
|
162
|
+
const inputExt = path.extname(file).toLowerCase();
|
|
163
|
+
if (inputExt === '.svg' && (!outputFormat || outputFormat === 'svg')) {
|
|
164
|
+
await processSvgFile({
|
|
165
|
+
file,
|
|
166
|
+
outputPath,
|
|
167
|
+
instructions,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await processRasterFile({
|
|
172
|
+
file,
|
|
173
|
+
outputPath,
|
|
174
|
+
instructions,
|
|
175
|
+
outputFormat,
|
|
176
|
+
quality,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function processRasterFile({ file, outputPath, instructions, outputFormat, quality, }) {
|
|
180
|
+
const image = sharp(file, { failOn: 'none' });
|
|
181
|
+
const metadata = await image.metadata();
|
|
182
|
+
let width = instructions.width;
|
|
183
|
+
let height = instructions.height;
|
|
184
|
+
if (instructions.percent && metadata.width) {
|
|
185
|
+
width = Math.round((metadata.width * instructions.percent) / 100);
|
|
186
|
+
}
|
|
187
|
+
if (instructions.scale && metadata.width) {
|
|
188
|
+
width = Math.round(metadata.width * instructions.scale);
|
|
189
|
+
}
|
|
190
|
+
let pipeline = image.rotate();
|
|
191
|
+
if (width || height) {
|
|
192
|
+
pipeline = pipeline.resize({
|
|
193
|
+
width: width ?? undefined,
|
|
194
|
+
height: height ?? undefined,
|
|
195
|
+
fit: width && height ? 'cover' : 'inside',
|
|
196
|
+
withoutEnlargement: !instructions.scale,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const finalFormat = normalizeOutputFormat(outputFormat || metadata.format);
|
|
200
|
+
switch (finalFormat) {
|
|
201
|
+
case 'jpeg':
|
|
202
|
+
pipeline = pipeline.jpeg({
|
|
203
|
+
quality,
|
|
204
|
+
mozjpeg: true,
|
|
205
|
+
});
|
|
206
|
+
break;
|
|
207
|
+
case 'png':
|
|
208
|
+
pipeline = pipeline.png({
|
|
209
|
+
quality,
|
|
210
|
+
compressionLevel: 9,
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
case 'webp':
|
|
214
|
+
pipeline = pipeline.webp({
|
|
215
|
+
quality,
|
|
216
|
+
});
|
|
217
|
+
break;
|
|
218
|
+
case 'avif':
|
|
219
|
+
pipeline = pipeline.avif({
|
|
220
|
+
quality,
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
case 'svg':
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
const tempPath = `${outputPath}.tmp`;
|
|
227
|
+
await pipeline.toFile(tempPath);
|
|
228
|
+
await fs.rename(tempPath, outputPath);
|
|
229
|
+
}
|
|
230
|
+
async function processSvgFile({ file, outputPath, instructions, }) {
|
|
231
|
+
let svg = await fs.readFile(file, 'utf8');
|
|
232
|
+
if (instructions.width) {
|
|
233
|
+
svg = setSvgAttribute(svg, 'width', `${instructions.width}px`);
|
|
234
|
+
}
|
|
235
|
+
if (instructions.height) {
|
|
236
|
+
svg = setSvgAttribute(svg, 'height', `${instructions.height}px`);
|
|
237
|
+
}
|
|
238
|
+
await fs.writeFile(outputPath, svg);
|
|
239
|
+
}
|
|
240
|
+
function setSvgAttribute(svg, attribute, value) {
|
|
241
|
+
const regexp = new RegExp(`${attribute}="[^"]*"`, 'i');
|
|
242
|
+
if (regexp.test(svg)) {
|
|
243
|
+
return svg.replace(regexp, `${attribute}="${value}"`);
|
|
244
|
+
}
|
|
245
|
+
return svg.replace('<svg', `<svg ${attribute}="${value}"`);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=optimizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimizer.js","sourceRoot":"","sources":["../src/optimizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function scanImages(inputDir: string): Promise<string[]>;
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'tiff', 'svg'];
|
|
3
|
+
export async function scanImages(inputDir) {
|
|
4
|
+
const normalizedDir = inputDir.replace(/\\/g, '/');
|
|
5
|
+
const patterns = IMAGE_EXTENSIONS.map(ext => `${normalizedDir}/**/*.${ext}`);
|
|
6
|
+
return fg(patterns, {
|
|
7
|
+
onlyFiles: true,
|
|
8
|
+
dot: false,
|
|
9
|
+
unique: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,WAAW,CAAA;AAE1B,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;AAE9E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC7C,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAElD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,SAAS,GAAG,EAAE,CAAC,CAAA;IAE5E,OAAO,EAAE,CAAC,QAAQ,EAAE;QAChB,SAAS,EAAE,IAAI;QACf,GAAG,EAAE,KAAK;QACV,MAAM,EAAE,IAAI;KACf,CAAC,CAAA;AACN,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type ImageFormat = 'jpeg' | 'jpg' | 'png' | 'webp' | 'avif';
|
|
2
|
+
export type InputImageFormat = ImageFormat | 'svg' | 'tiff';
|
|
3
|
+
export type OutputImageFormat = ImageFormat | 'svg';
|
|
4
|
+
export type OutputsConfig = Partial<Record<InputImageFormat, OutputImageFormat[]>>;
|
|
5
|
+
export interface ImageOptimizerConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Directory with source images.
|
|
8
|
+
*/
|
|
9
|
+
inputDir?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Directory where processed images will be saved.
|
|
12
|
+
*/
|
|
13
|
+
outputDir?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Compression quality from 1 to 100.
|
|
16
|
+
*
|
|
17
|
+
* @default 80
|
|
18
|
+
*/
|
|
19
|
+
quality?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Force all images to be converted to this format.
|
|
22
|
+
*
|
|
23
|
+
* CLI `--format` has higher priority.
|
|
24
|
+
* Filename instructions have the highest priority.
|
|
25
|
+
*/
|
|
26
|
+
format?: ImageFormat;
|
|
27
|
+
/**
|
|
28
|
+
* Base output rules by input format.
|
|
29
|
+
*
|
|
30
|
+
* Example:
|
|
31
|
+
* png: ["png", "webp"]
|
|
32
|
+
*
|
|
33
|
+
* Means:
|
|
34
|
+
* image.png -> image.png + image.webp
|
|
35
|
+
*/
|
|
36
|
+
outputs?: OutputsConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Simulate processing without writing files.
|
|
39
|
+
*
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Clean output directory before processing.
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
clean?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface OptimizeOptions extends ImageOptimizerConfig {
|
|
51
|
+
/**
|
|
52
|
+
* Path to custom config file.
|
|
53
|
+
*/
|
|
54
|
+
config?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ImageOutput {
|
|
57
|
+
format: OutputImageFormat;
|
|
58
|
+
keepOriginal: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface ImageInstructions {
|
|
61
|
+
width: number | null;
|
|
62
|
+
height: number | null;
|
|
63
|
+
percent: number | null;
|
|
64
|
+
scale: number | null;
|
|
65
|
+
quality: number | null;
|
|
66
|
+
outputs: ImageOutput[];
|
|
67
|
+
}
|
|
68
|
+
export interface OutputTask {
|
|
69
|
+
format: OutputImageFormat | null;
|
|
70
|
+
outputPath: string;
|
|
71
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ImageFormat, ImageInstructions, InputImageFormat, OutputImageFormat } from './types.js';
|
|
2
|
+
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
3
|
+
export declare function ensureDirectory(dir: string): Promise<void>;
|
|
4
|
+
export declare function cleanDirectory(dir: string): Promise<void>;
|
|
5
|
+
export declare function getFileSize(file: string): Promise<number>;
|
|
6
|
+
export declare function formatBytes(bytes: number): string;
|
|
7
|
+
export declare function normalizeFormat(format?: string | null): ImageFormat | undefined;
|
|
8
|
+
export declare function normalizeOutputFormat(format?: string | null): OutputImageFormat | undefined;
|
|
9
|
+
export declare function getInputFormat(filePath: string): InputImageFormat;
|
|
10
|
+
export declare function validateQuality(q: number): void;
|
|
11
|
+
export declare function validateFormat(format?: string | null): void;
|
|
12
|
+
export declare function parseImageInstructions(filePath: string): ImageInstructions;
|
|
13
|
+
export declare function removeInstructionsFromName(filePath: string): string;
|
|
14
|
+
export declare function getCleanRelativePath(inputDir: string, filePath: string): string;
|
|
15
|
+
export declare function getOutputPath({ inputDir, outputDir, filePath, format, }: {
|
|
16
|
+
inputDir: string;
|
|
17
|
+
outputDir?: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
format?: OutputImageFormat | null;
|
|
20
|
+
}): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const SUPPORTED_OUTPUT_FORMATS = [
|
|
4
|
+
'jpeg',
|
|
5
|
+
'jpg',
|
|
6
|
+
'png',
|
|
7
|
+
'webp',
|
|
8
|
+
'avif',
|
|
9
|
+
'svg',
|
|
10
|
+
];
|
|
11
|
+
export async function pathExists(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(filePath);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function ensureDirectory(dir) {
|
|
21
|
+
await fs.mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
export async function cleanDirectory(dir) {
|
|
24
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
25
|
+
await ensureDirectory(dir);
|
|
26
|
+
}
|
|
27
|
+
export async function getFileSize(file) {
|
|
28
|
+
return (await fs.stat(file)).size;
|
|
29
|
+
}
|
|
30
|
+
export function formatBytes(bytes) {
|
|
31
|
+
if (!bytes)
|
|
32
|
+
return '0 B';
|
|
33
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
34
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
35
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
36
|
+
}
|
|
37
|
+
export function normalizeFormat(format) {
|
|
38
|
+
if (!format)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (format === 'jpg')
|
|
41
|
+
return 'jpeg';
|
|
42
|
+
return format;
|
|
43
|
+
}
|
|
44
|
+
export function normalizeOutputFormat(format) {
|
|
45
|
+
if (!format)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (format === 'jpg')
|
|
48
|
+
return 'jpeg';
|
|
49
|
+
return format;
|
|
50
|
+
}
|
|
51
|
+
export function getInputFormat(filePath) {
|
|
52
|
+
const ext = path.extname(filePath).replace('.', '').toLowerCase();
|
|
53
|
+
if (ext === 'jpg')
|
|
54
|
+
return 'jpeg';
|
|
55
|
+
return ext;
|
|
56
|
+
}
|
|
57
|
+
export function validateQuality(q) {
|
|
58
|
+
if (!Number.isFinite(q) || q < 1 || q > 100) {
|
|
59
|
+
throw new Error('Quality must be between 1 and 100');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function validateFormat(format) {
|
|
63
|
+
if (!format)
|
|
64
|
+
return;
|
|
65
|
+
if (!SUPPORTED_OUTPUT_FORMATS.includes(format)) {
|
|
66
|
+
throw new Error(`Unsupported format "${format}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function parseImageInstructions(filePath) {
|
|
70
|
+
const fileName = path.basename(filePath);
|
|
71
|
+
const match = fileName.match(/\[([^\]]+)]/);
|
|
72
|
+
const instructions = {
|
|
73
|
+
width: null,
|
|
74
|
+
height: null,
|
|
75
|
+
percent: null,
|
|
76
|
+
scale: null,
|
|
77
|
+
quality: null,
|
|
78
|
+
outputs: [],
|
|
79
|
+
};
|
|
80
|
+
if (!match) {
|
|
81
|
+
return instructions;
|
|
82
|
+
}
|
|
83
|
+
const parts = match[1]
|
|
84
|
+
.split(',')
|
|
85
|
+
.map(part => part.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
for (const part of parts) {
|
|
88
|
+
parseInstructionPart(part, instructions);
|
|
89
|
+
}
|
|
90
|
+
return instructions;
|
|
91
|
+
}
|
|
92
|
+
function parseInstructionPart(part, instructions) {
|
|
93
|
+
if (/^\d+$/.test(part)) {
|
|
94
|
+
instructions.width = Number(part);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (/^\d+x\d+$/.test(part)) {
|
|
98
|
+
const [width, height] = part.split('x').map(Number);
|
|
99
|
+
instructions.width = width;
|
|
100
|
+
instructions.height = height;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (/^w=\d+$/.test(part)) {
|
|
104
|
+
instructions.width = Number(part.replace('w=', ''));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (/^h=\d+$/.test(part)) {
|
|
108
|
+
instructions.height = Number(part.replace('h=', ''));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (/^\d+%$/.test(part)) {
|
|
112
|
+
instructions.percent = Number(part.replace('%', ''));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (/^\d+(\.\d+)?x$/.test(part)) {
|
|
116
|
+
instructions.scale = Number(part.replace('x', ''));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (/^(q|quality)=\d+$/.test(part)) {
|
|
120
|
+
const quality = Number(part.split('=')[1]);
|
|
121
|
+
validateQuality(quality);
|
|
122
|
+
instructions.quality = quality;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (/^\+\w+$/.test(part)) {
|
|
126
|
+
const format = normalizeOutputFormat(part.replace('+', ''));
|
|
127
|
+
validateFormat(format);
|
|
128
|
+
instructions.outputs.push({
|
|
129
|
+
format: format,
|
|
130
|
+
keepOriginal: true,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (/^(webp|avif|png|jpg|jpeg|svg)$/.test(part)) {
|
|
135
|
+
const format = normalizeOutputFormat(part);
|
|
136
|
+
instructions.outputs.push({
|
|
137
|
+
format: format,
|
|
138
|
+
keepOriginal: false,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (/^\d+\.\+?(webp|avif|png|jpg|jpeg)$/.test(part)) {
|
|
143
|
+
const [width, rawFormat] = part.split('.');
|
|
144
|
+
instructions.width = Number(width);
|
|
145
|
+
const keepOriginal = rawFormat.startsWith('+');
|
|
146
|
+
const format = normalizeOutputFormat(rawFormat.replace('+', ''));
|
|
147
|
+
validateFormat(format);
|
|
148
|
+
instructions.outputs.push({
|
|
149
|
+
format: format,
|
|
150
|
+
keepOriginal,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Unknown filename instruction: "${part}"`);
|
|
155
|
+
}
|
|
156
|
+
export function removeInstructionsFromName(filePath) {
|
|
157
|
+
const ext = path.extname(filePath);
|
|
158
|
+
const name = path.basename(filePath, ext);
|
|
159
|
+
return `${name.replace(/\[[^\]]+]/g, '')}${ext}`;
|
|
160
|
+
}
|
|
161
|
+
export function getCleanRelativePath(inputDir, filePath) {
|
|
162
|
+
const relative = path.relative(inputDir, filePath);
|
|
163
|
+
const dir = path.dirname(relative);
|
|
164
|
+
const cleanName = removeInstructionsFromName(relative);
|
|
165
|
+
return path.join(dir, cleanName);
|
|
166
|
+
}
|
|
167
|
+
export function getOutputPath({ inputDir, outputDir, filePath, format, }) {
|
|
168
|
+
const cleanRelativePath = getCleanRelativePath(inputDir, filePath);
|
|
169
|
+
const parsed = path.parse(cleanRelativePath);
|
|
170
|
+
const ext = format ? `.${normalizeOutputFormat(format)}` : parsed.ext;
|
|
171
|
+
return path.join(outputDir || inputDir, parsed.dir, `${parsed.name}${ext}`);
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@valdiszz53/image-optimizer-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI package for optimizing images from a directory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"image-optimizer": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Vladislav Tregubov"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsx src/cli.ts",
|
|
23
|
+
"start": "node dist/cli.js",
|
|
24
|
+
"playground": "npm run build && node dist/cli.js --config image-optimizer.config.js",
|
|
25
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"fast-glob": "^3.3.2",
|
|
32
|
+
"sharp": "^0.33.5"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"prettier": "^3.9.1",
|
|
37
|
+
"tsx": "^4.19.0",
|
|
38
|
+
"typescript": "^5.5.0"
|
|
39
|
+
}
|
|
40
|
+
}
|