image-guard 5.3.1 → 5.4.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/README.md +10 -6
- package/bin/image-guard.js +6 -1
- package/package.json +4 -3
- package/src/index.js +55 -12
- package/src/utils.js +11 -6
package/README.md
CHANGED
|
@@ -109,17 +109,21 @@ If you give the hook options like `--ignore`, add them to the `postprepare` scri
|
|
|
109
109
|
|
|
110
110
|
Image Guard takes an optional path to the directory to process—`npx image-guard assets`—and works on the current directory when that path is omitted.
|
|
111
111
|
|
|
112
|
-
* `--
|
|
112
|
+
* `--heic-to-avif` converts HEIC/HEIF files to AVIF. Because HEIC sources are already lossy-compressed (HEVC), the conversion uses lossy AVIF encoding (quality 80) to produce files smaller than the originals at near-identical visual quality. Original HEIC/HEIF files are deleted after conversion by default. (Note: The conversion maps Display P3 colors—common on iPhones—to sRGB, which may result in slightly less vivid colors.)
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
- `--keep-heic` (used with `--heic-to-avif`) preserves the original HEIC/HEIF files instead of deleting them after conversion.
|
|
115
115
|
|
|
116
|
-
* `--
|
|
116
|
+
* `--ignore`/`-i` allows you to specify paths to be ignored (as in `--ignore=example,test`). Multiple paths must be separated by commas. Naming a directory excludes everything inside it. The option supports glob patterns (e.g., `assets/**`, `**/*.png`); matching is case-insensitive and honors .gitignore. Patterns are relative to the directory being processed—with `--staged`, to the repository root.
|
|
117
117
|
|
|
118
|
-
* `--
|
|
118
|
+
* `--quiet`/`-q` suppresses per-file logs and prints only the final summary (plus errors). This reduces console noise and speeds up runs in CI and Git hooks.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
* `--dry`/`-d` allows you to run Image Guard in “dry mode.” All changes are shown in the terminal (unless combined with `--quiet`, which leaves just the summary).
|
|
121
|
+
|
|
122
|
+
* `--staged` (recommended with automated use) triggers a mode that watches PNG, JPG, GIF, WebP, and AVIF files (and HEIC/HEIF, when `--heic-to-avif` is used) from staged changes (`git diff --cached`) and only processes those files—that approach makes Image Guard more efficient in operation. Because the file set comes from Git, `--staged` can’t be combined with a path.
|
|
123
|
+
|
|
124
|
+
* `--help`/`-h` lists the arguments and options.
|
|
121
125
|
|
|
122
|
-
* `--
|
|
126
|
+
* `--version`/`-V` prints the version number.
|
|
123
127
|
|
|
124
128
|
## What Does the Output Look Like?
|
|
125
129
|
|
package/bin/image-guard.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { runImageGuard } from '../src/index.js'
|
|
4
|
+
import { styleStderr } from '../src/utils.js'
|
|
4
5
|
|
|
5
6
|
try {
|
|
6
7
|
await runImageGuard()
|
|
7
8
|
} catch (err) {
|
|
8
|
-
|
|
9
|
+
if (err.setupFailed) {
|
|
10
|
+
console.error(styleStderr('red', err.message))
|
|
11
|
+
} else {
|
|
12
|
+
console.error('Error running Image Guard:', err)
|
|
13
|
+
}
|
|
9
14
|
process.exit(1)
|
|
10
15
|
}
|
package/package.json
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"description": "Near-lossless image compressor that’s easy to use and easy to automate",
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@eslint/js": "^10.0.1",
|
|
17
|
-
"eslint": "^10.8.1"
|
|
17
|
+
"eslint": "^10.8.1",
|
|
18
|
+
"globals": "^17.11.0"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"bin/image-guard.js",
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"test": "node --test bin/image-guard.test.js"
|
|
55
56
|
},
|
|
56
57
|
"type": "module",
|
|
57
|
-
"version": "5.
|
|
58
|
-
}
|
|
58
|
+
"version": "5.4.0"
|
|
59
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import fsSync from 'node:fs'
|
|
|
7
7
|
import os from 'node:os'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import sharp from 'sharp'
|
|
10
|
-
import { utils } from './utils.js'
|
|
10
|
+
import { styleStderr, utils } from './utils.js'
|
|
11
11
|
|
|
12
12
|
// Files to be compressed
|
|
13
13
|
export const fileTypes = ['avif', 'gif', 'jpg', 'jpeg', 'png', 'webp'];
|
|
@@ -15,34 +15,77 @@ export const fileTypes = ['avif', 'gif', 'jpg', 'jpeg', 'png', 'webp'];
|
|
|
15
15
|
// Files to be converted (require explicit opt-in)
|
|
16
16
|
export const convertTypes = ['heic', 'heif'];
|
|
17
17
|
|
|
18
|
+
// Marks a failure as the user’s to fix rather than a bug
|
|
19
|
+
function setupError(message) {
|
|
20
|
+
const err = new Error(message)
|
|
21
|
+
err.setupFailed = true
|
|
22
|
+
return err
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export async function runImageGuard() {
|
|
19
26
|
const options = {
|
|
20
|
-
dry: { type: 'boolean', default: false },
|
|
21
27
|
'heic-to-avif': { type: 'boolean', default: false },
|
|
22
|
-
ignore: { type: 'string', multiple: false, default: '' },
|
|
23
28
|
'keep-heic': { type: 'boolean', default: false },
|
|
29
|
+
ignore: { type: 'string', short: 'i', multiple: false, default: '' },
|
|
30
|
+
quiet: { type: 'boolean', short: 'q', default: false },
|
|
31
|
+
dry: { type: 'boolean', short: 'd', default: false },
|
|
24
32
|
staged: { type: 'boolean', default: false },
|
|
25
|
-
|
|
33
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
34
|
+
version: { type: 'boolean', short: 'V', default: false }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let argv, positionals
|
|
38
|
+
try {
|
|
39
|
+
({ values: argv, positionals } = parseArgs({ options, allowPositionals: true }))
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// `parseArgs` appends guidance about `--` that only muddies a plain typo
|
|
42
|
+
const [summary] = err.message.split('. ')
|
|
43
|
+
throw setupError(`${summary}—run \`image-guard --help\` for the available options.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (argv.help) {
|
|
47
|
+
console.log(`Usage: image-guard [options] [directory]
|
|
48
|
+
|
|
49
|
+
Compress images in place, and optionally convert HEIC/HEIF files to AVIF.
|
|
50
|
+
|
|
51
|
+
Arguments:
|
|
52
|
+
directory Directory to process (default: current directory)
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--heic-to-avif Also convert HEIC/HEIF files to AVIF
|
|
56
|
+
--keep-heic Keep the original HEIC/HEIF files (only with \`--heic-to-avif\`)
|
|
57
|
+
-i, --ignore <paths> Comma-separated paths or glob patterns to exclude
|
|
58
|
+
-q, --quiet Print only the final summary
|
|
59
|
+
-d, --dry Show what would change without writing any files
|
|
60
|
+
--staged Process only images staged in Git (not combinable with a directory)
|
|
61
|
+
-h, --help Show this help
|
|
62
|
+
-V, --version Show the version number`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (argv.version) {
|
|
67
|
+
const pkg = JSON.parse(fsSync.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
68
|
+
console.log(pkg.version)
|
|
69
|
+
return
|
|
26
70
|
}
|
|
27
|
-
const { values: argv, positionals } = parseArgs({ options, allowPositionals: true })
|
|
28
71
|
|
|
29
72
|
if (positionals.length > 1) {
|
|
30
|
-
throw
|
|
73
|
+
throw setupError(`Expected at most one path, got ${positionals.length}: ${positionals.join(', ')}`)
|
|
31
74
|
}
|
|
32
75
|
|
|
33
76
|
const dir = positionals[0] || '.'
|
|
34
77
|
|
|
35
78
|
if (positionals.length && argv.staged) {
|
|
36
|
-
throw
|
|
79
|
+
throw setupError('`--staged` takes its files from Git, not from a path—pass one or the other.')
|
|
37
80
|
}
|
|
38
81
|
|
|
39
82
|
if (!fsSync.existsSync(dir)) {
|
|
40
|
-
throw
|
|
83
|
+
throw setupError(`No such directory: ${dir}`)
|
|
41
84
|
}
|
|
42
85
|
|
|
43
86
|
// A path that is there but isn’t a directory gets its own message
|
|
44
87
|
if (!fsSync.statSync(dir).isDirectory()) {
|
|
45
|
-
throw
|
|
88
|
+
throw setupError(`Not a directory: ${dir}`)
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
// Share status
|
|
@@ -57,7 +100,7 @@ export async function runImageGuard() {
|
|
|
57
100
|
}
|
|
58
101
|
|
|
59
102
|
if (argv['keep-heic'] && !argv['heic-to-avif']) {
|
|
60
|
-
console.warn(
|
|
103
|
+
console.warn(styleStderr('yellow', '`--keep-heic` has no effect without `--heic-to-avif`'))
|
|
61
104
|
}
|
|
62
105
|
|
|
63
106
|
const allTypes = argv['heic-to-avif'] ? [...fileTypes, ...convertTypes] : fileTypes
|
|
@@ -122,7 +165,7 @@ export async function runImageGuard() {
|
|
|
122
165
|
} else {
|
|
123
166
|
hadFailures = true
|
|
124
167
|
const reason = r.reason && r.reason.message ? r.reason.message : String(r.reason)
|
|
125
|
-
console.error(
|
|
168
|
+
console.error(styleStderr('red', 'Task failed:'), reason)
|
|
126
169
|
}
|
|
127
170
|
}
|
|
128
171
|
return { hadFailures, addedKB }
|
|
@@ -176,7 +219,7 @@ export async function runImageGuard() {
|
|
|
176
219
|
return patterns
|
|
177
220
|
}
|
|
178
221
|
|
|
179
|
-
// Globbing runs with `dir` as its base—so ignore patterns and
|
|
222
|
+
// Globbing runs with `dir` as its base—so ignore patterns and .gitignore
|
|
180
223
|
// lookup are relative to the searched directory, not to the shell’s—and the
|
|
181
224
|
// results are rejoined for display and file access
|
|
182
225
|
const findFiles = async (patterns, options = {}) => {
|
package/src/utils.js
CHANGED
|
@@ -8,6 +8,11 @@ import decode from 'heic-decode'
|
|
|
8
8
|
|
|
9
9
|
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100 MB
|
|
10
10
|
|
|
11
|
+
// `styleText` detects color on `process.stdout` by default, so anything bound
|
|
12
|
+
// for STDERR has to name that stream—otherwise a redirected STDERR collects
|
|
13
|
+
// escape codes whenever STDOUT happens to be a terminal
|
|
14
|
+
export const styleStderr = (format, text) => styleText(format, text, { stream: process.stderr })
|
|
15
|
+
|
|
11
16
|
const logMessage = (message, dry, color = 'yellow', quiet = false) => {
|
|
12
17
|
if (quiet) return
|
|
13
18
|
const prefix = dry ? 'Dry run: ' : ''
|
|
@@ -143,7 +148,7 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
if (fileSizeAfter === 0) {
|
|
146
|
-
console.error(
|
|
151
|
+
console.error(styleStderr('red', `Error compressing ${filename}: Compressed file size is 0`))
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
return fileSizeAfter < fileSizeBefore ? fileSizeBefore - fileSizeAfter : 0
|
|
@@ -163,7 +168,7 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
163
168
|
) {
|
|
164
169
|
logMessage(`Skipped ${filename} (corrupt file)`, dry, 'yellow', quiet)
|
|
165
170
|
} else {
|
|
166
|
-
console.error(
|
|
171
|
+
console.error(styleStderr('red', `Error compressing ${filename}:`), err)
|
|
167
172
|
}
|
|
168
173
|
return 0
|
|
169
174
|
|
|
@@ -184,14 +189,14 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
184
189
|
await retryFileOperation(() => fs.unlink(filenameBackup))
|
|
185
190
|
} catch (err) {
|
|
186
191
|
if (err.code !== 'ENOENT') {
|
|
187
|
-
console.warn(
|
|
192
|
+
console.warn(styleStderr('yellow', `Failed to delete backup file ${filenameBackup}:`), err)
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
} else if (!dry && !replacementSucceeded) {
|
|
191
196
|
// If a backup was created but replacement failed, warn so the user can recover it
|
|
192
197
|
try {
|
|
193
198
|
await fs.access(filenameBackup)
|
|
194
|
-
console.warn(
|
|
199
|
+
console.warn(styleStderr('yellow', `Replacement failed for ${filename}; backup preserved at ${filenameBackup}`))
|
|
195
200
|
} catch {
|
|
196
201
|
// No backup exists—nothing to warn about
|
|
197
202
|
}
|
|
@@ -262,7 +267,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
262
267
|
await retryFileOperation(() => fs.unlink(avifPath))
|
|
263
268
|
} catch (cleanupErr) {
|
|
264
269
|
if (cleanupErr.code !== 'ENOENT') {
|
|
265
|
-
console.warn(
|
|
270
|
+
console.warn(styleStderr('yellow', `Failed to clean up ${avifPath}:`), cleanupErr)
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -272,7 +277,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
272
277
|
)) {
|
|
273
278
|
logMessage(`Skipped ${filename} (corrupt or unsupported HEIC/HEIF file)`, dry, 'yellow', quiet)
|
|
274
279
|
} else {
|
|
275
|
-
console.error(
|
|
280
|
+
console.error(styleStderr('red', `Error converting ${filename}:`), err)
|
|
276
281
|
}
|
|
277
282
|
return 0
|
|
278
283
|
}
|