image-guard 5.4.1 → 5.4.2
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 +16 -3
- package/package.json +12 -7
- package/src/gitignore.js +50 -0
- package/src/index.js +18 -10
package/README.md
CHANGED
|
@@ -158,11 +158,24 @@ Image Guard is no substitute for image fine-tuning and micro-optimization. That
|
|
|
158
158
|
|
|
159
159
|
That is, micro-optimization still needs to be taken care of through other means, whether manually or through tools. Image Guard just solves the problem that images are checked in or go live that are not compressed _at all_.
|
|
160
160
|
|
|
161
|
-
##
|
|
161
|
+
## Working on Image Guard
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
### Benchmarking
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
To check how a change affects speed, memory, and compression, run the benchmark on any local folder with images:
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
npm run benchmark -- ~/Projects
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The benchmark copies a deterministic sample of the images (honoring .gitignore) to a temporary folder, runs Image Guard on fresh copies of it several times, and reports the median time, peak memory, and bytes saved. Your images are not modified.
|
|
172
|
+
|
|
173
|
+
* `--runs <n>` sets the number of runs (default: 3).
|
|
174
|
+
* `--sample <n>` sets the approximate sample size (default: 300). Each format is sampled in proportion to its share, with at least 10 files per format where available.
|
|
175
|
+
* `--save` saves the result as the baseline (benchmark/baseline.json, ignored by Git), which later runs on the same sample compare against.
|
|
176
|
+
* `--profile` additionally compresses each image on its own, in dry mode, and reports the time per format and the slowest files.
|
|
177
|
+
|
|
178
|
+
To compare a branch with `main`, run the benchmark with `--save` on `main`, then without it on the branch. Differences of around 10% can be noise; use more runs, and repeat a run on `main`, before relying on smaller ones.
|
|
166
179
|
|
|
167
180
|
## License
|
|
168
181
|
|
package/package.json
CHANGED
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
"imagemin-merlin": "bin/image-guard.sh"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"globby": "^16.2.4",
|
|
10
9
|
"heic-decode": "^2.1.0",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"ignore": "^7.0.12",
|
|
11
|
+
"sharp": "^0.35.5",
|
|
12
|
+
"simple-git": "^4.0.2",
|
|
13
|
+
"tinyglobby": "^0.2.17"
|
|
13
14
|
},
|
|
14
15
|
"description": "Near-lossless image compressor that’s easy to use and easy to automate",
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"@eslint/js": "^10.0.1",
|
|
17
|
-
"eslint": "^10.
|
|
18
|
-
"globals": "^17.
|
|
18
|
+
"eslint": "^10.12.0",
|
|
19
|
+
"globals": "^17.13.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.13"
|
|
19
23
|
},
|
|
20
24
|
"files": [
|
|
21
25
|
"bin/image-guard.js",
|
|
@@ -49,11 +53,12 @@
|
|
|
49
53
|
"url": "git+https://github.com/j9t/image-guard.git"
|
|
50
54
|
},
|
|
51
55
|
"scripts": {
|
|
56
|
+
"benchmark": "node benchmark/benchmark.js",
|
|
52
57
|
"lint": "eslint .",
|
|
53
58
|
"lint:fix": "eslint . --fix",
|
|
54
59
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
55
|
-
"test": "node --test bin/image-guard.test.js"
|
|
60
|
+
"test": "node --test bin/image-guard.test.js benchmark/benchmark.test.js"
|
|
56
61
|
},
|
|
57
62
|
"type": "module",
|
|
58
|
-
"version": "5.4.
|
|
63
|
+
"version": "5.4.2"
|
|
59
64
|
}
|
package/src/gitignore.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import ignore from 'ignore'
|
|
4
|
+
|
|
5
|
+
function findGitRoot(dir) {
|
|
6
|
+
for (let current = dir; ; current = path.dirname(current)) {
|
|
7
|
+
if (fs.existsSync(path.join(current, '.git'))) return current
|
|
8
|
+
if (path.dirname(current) === current) return undefined
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Returns a predicate for absolute paths under `cwd` that applies .gitignore
|
|
13
|
+
// files from the repository root (or `cwd` outside a repository) down to each
|
|
14
|
+
// path, as Git does: Deeper rules take precedence, and nothing inside an
|
|
15
|
+
// ignored directory can be re-included
|
|
16
|
+
export function createGitignoreFilter(cwd) {
|
|
17
|
+
const base = path.resolve(cwd)
|
|
18
|
+
const top = findGitRoot(base) ?? base
|
|
19
|
+
const rulesByDir = new Map()
|
|
20
|
+
|
|
21
|
+
const getRules = (dir) => {
|
|
22
|
+
if (!rulesByDir.has(dir)) {
|
|
23
|
+
let rules
|
|
24
|
+
try {
|
|
25
|
+
rules = ignore().add(fs.readFileSync(path.join(dir, '.gitignore'), 'utf8'))
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') throw err
|
|
28
|
+
}
|
|
29
|
+
rulesByDir.set(dir, rules)
|
|
30
|
+
}
|
|
31
|
+
return rulesByDir.get(dir)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (file) => {
|
|
35
|
+
const segments = path.relative(top, file).split(path.sep)
|
|
36
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
37
|
+
const suffix = i < segments.length ? '/' : ''
|
|
38
|
+
let ignored = false
|
|
39
|
+
for (let j = 0; j < i; j++) {
|
|
40
|
+
const rules = getRules(path.join(top, ...segments.slice(0, j)))
|
|
41
|
+
if (!rules) continue
|
|
42
|
+
const result = rules.test(segments.slice(j, i).join('/') + suffix)
|
|
43
|
+
if (result.ignored) ignored = true
|
|
44
|
+
else if (result.unignored) ignored = false
|
|
45
|
+
}
|
|
46
|
+
if (ignored) return true
|
|
47
|
+
}
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// This file, which had been forked from imagemin-merlin, was modified for image-guard: https://github.com/sumcumo/imagemin-merlin/compare/master...j9t:master
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import simpleGit from 'simple-git'
|
|
3
|
+
import { glob, convertPathToPattern } from 'tinyglobby'
|
|
4
|
+
import { simpleGit } from 'simple-git'
|
|
5
5
|
import { parseArgs, styleText } from 'node:util'
|
|
6
6
|
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 { createGitignoreFilter } from './gitignore.js'
|
|
10
11
|
import { styleStderr, utils } from './utils.js'
|
|
11
12
|
|
|
12
13
|
// Files to be compressed
|
|
@@ -147,8 +148,14 @@ Options:
|
|
|
147
148
|
})
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
const
|
|
151
|
+
// Capped to bound memory, as each file in flight holds its decoded pixels
|
|
152
|
+
const desiredFileConcurrency = Math.min(os.availableParallelism(), 8)
|
|
153
|
+
const perTaskThreads = Math.max(1, Math.floor(os.availableParallelism() / desiredFileConcurrency))
|
|
154
|
+
|
|
155
|
+
// sharp runs on libuv’s thread pool (4 threads by default), which caps files
|
|
156
|
+
// in flight regardless of the limiter; this must happen before the first
|
|
157
|
+
// asynchronous I/O and respects a user setting
|
|
158
|
+
process.env.UV_THREADPOOL_SIZE ??= String(desiredFileConcurrency)
|
|
152
159
|
try {
|
|
153
160
|
sharp.concurrency(perTaskThreads)
|
|
154
161
|
} catch {
|
|
@@ -194,7 +201,7 @@ Options:
|
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
// Plain patterns also get a “/**” variant so directories are excluded with
|
|
197
|
-
// their contents: “dir/” resolves only where
|
|
204
|
+
// their contents: “dir/” resolves only where tinyglobby can stat it, and a bare
|
|
198
205
|
// “dir” only covers what the walk descends into
|
|
199
206
|
const getIgnorePatterns = (ignore) => {
|
|
200
207
|
return (ignore || '')
|
|
@@ -223,14 +230,14 @@ Options:
|
|
|
223
230
|
// lookup are relative to the searched directory, not to the shell’s—and the
|
|
224
231
|
// results are rejoined for display and file access
|
|
225
232
|
const findFiles = async (patterns, options = {}) => {
|
|
226
|
-
const files = await
|
|
233
|
+
const files = await glob(patterns, {
|
|
227
234
|
cwd: dir,
|
|
228
|
-
gitignore: true,
|
|
229
235
|
onlyFiles: true,
|
|
230
236
|
caseSensitiveMatch: false,
|
|
231
237
|
...options
|
|
232
238
|
})
|
|
233
|
-
|
|
239
|
+
const isGitignored = createGitignoreFilter(dir)
|
|
240
|
+
return files.map(file => path.join(dir, file)).filter(file => !isGitignored(path.resolve(file)))
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
const filterStagedFiles = async (stagedFiles, types) => {
|
|
@@ -240,12 +247,13 @@ Options:
|
|
|
240
247
|
|
|
241
248
|
if (ignoreList.length > 0) {
|
|
242
249
|
const escapedPaths = byExt.map(p => convertPathToPattern(p))
|
|
243
|
-
|
|
244
|
-
gitignore: true,
|
|
250
|
+
const files = await glob([...escapedPaths, ...ignoreList], {
|
|
245
251
|
expandDirectories: false,
|
|
246
252
|
onlyFiles: true,
|
|
247
253
|
caseSensitiveMatch: false
|
|
248
254
|
})
|
|
255
|
+
const isGitignored = createGitignoreFilter('.')
|
|
256
|
+
return files.filter(file => !isGitignored(path.resolve(file)))
|
|
249
257
|
}
|
|
250
258
|
return byExt
|
|
251
259
|
}
|