image-guard 5.2.5 → 5.3.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 +26 -14
- package/package.json +13 -6
- package/src/index.js +29 -5
- package/.editorconfig +0 -12
- package/.github/FUNDING.yml +0 -3
- package/.github/dependabot.yml +0 -12
- package/.github/workflows/npm.yml +0 -21
- package/.github/workflows/tests.yml +0 -54
- package/CHANGELOG.md +0 -55
- package/SECURITY.md +0 -9
- package/bin/image-guard.test.js +0 -553
- package/eslint.config.js +0 -28
- package/media/output.png +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Image Guard
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/image-guard) [](https://github.com/j9t/image-guard/actions) [](https://socket.dev/npm/package/image-guard)
|
|
3
|
+
[](https://www.npmjs.com/package/image-guard) [](https://github.com/j9t/image-guard/actions) [](https://socket.dev/npm/package/image-guard) [](https://github.com/sponsors/j9t)
|
|
4
4
|
|
|
5
5
|
(This project was based on [sum.cumo’s imagemin-merlin](https://github.com/sumcumo/imagemin-merlin). [Changes are documented](https://github.com/sumcumo/imagemin-merlin/compare/master...j9t:master), and include this README. Image Guard supports additional file formats—WebP, AVIF, and HEIC/HEIF—, comes with improved code and documentation, and is being maintained. For this reason, it’s [not based on any Imagemin packages](https://meiert.com/blog/image-guard-4/) anymore.)
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ It’s as safe as possible because compression happens losslessly (near-lossless
|
|
|
20
20
|
|
|
21
21
|
You can use Image Guard right away, without installation, by running
|
|
22
22
|
|
|
23
|
-
```
|
|
23
|
+
```shell
|
|
24
24
|
npx image-guard
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -28,13 +28,13 @@ npx image-guard
|
|
|
28
28
|
|
|
29
29
|
Install Image Guard in your project:
|
|
30
30
|
|
|
31
|
-
```
|
|
31
|
+
```shell
|
|
32
32
|
npm i -D image-guard
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Run Image Guard by calling
|
|
36
36
|
|
|
37
|
-
```
|
|
37
|
+
```shell
|
|
38
38
|
npx image-guard
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -44,13 +44,13 @@ To make sure that _all_ images are being compressed, it’s recommended to run I
|
|
|
44
44
|
|
|
45
45
|
Install Image Guard in your project:
|
|
46
46
|
|
|
47
|
-
```
|
|
47
|
+
```shell
|
|
48
48
|
npm i -D image-guard
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
To compress images already in the codebase, run Image Guard once by calling
|
|
52
52
|
|
|
53
|
-
```
|
|
53
|
+
```shell
|
|
54
54
|
npx image-guard
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -60,7 +60,7 @@ For automated use, Image Guard should be triggered through a [Git hook](https://
|
|
|
60
60
|
|
|
61
61
|
Native Git hooks are simpler to set up and don’t require additional dependencies. Run these commands from your project root:
|
|
62
62
|
|
|
63
|
-
```
|
|
63
|
+
```shell
|
|
64
64
|
mkdir -p .githooks;\
|
|
65
65
|
cat > .githooks/pre-commit << 'EOF'
|
|
66
66
|
#!/bin/sh
|
|
@@ -81,7 +81,7 @@ chmod +x .githooks/pre-commit && git config core.hooksPath .githooks"
|
|
|
81
81
|
|
|
82
82
|
If you already use [Husky](https://typicode.github.io/husky/), run the following commands in your project root (you can copy and execute them at once):
|
|
83
83
|
|
|
84
|
-
```
|
|
84
|
+
```shell
|
|
85
85
|
grep -qxF "./node_modules/.bin/image-guard --staged" .husky/pre-commit || printf '\n./node_modules/.bin/image-guard --staged' >> .husky/pre-commit;\
|
|
86
86
|
git add .husky/pre-commit;\
|
|
87
87
|
git commit -m "feat: add Husky pre-commit hook for Image Guard";\
|
|
@@ -90,7 +90,7 @@ npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --st
|
|
|
90
90
|
|
|
91
91
|
If you don’t yet but want to use Husky, run the following commands from your project root:
|
|
92
92
|
|
|
93
|
-
```
|
|
93
|
+
```shell
|
|
94
94
|
npm i -D husky;\
|
|
95
95
|
npx husky init;\
|
|
96
96
|
echo "./node_modules/.bin/image-guard --staged" > .husky/pre-commit;\
|
|
@@ -101,21 +101,25 @@ npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --st
|
|
|
101
101
|
|
|
102
102
|
(The `postprepare` script ensures that the hook is added to the repository whenever someone installs the package.)
|
|
103
103
|
|
|
104
|
+
If you give the hook options like `--ignore`, add them to the `postprepare` script as well—the hook command is the only place to configure Image Guard, and the next install rewrites that line.
|
|
105
|
+
|
|
104
106
|
**Important:** When you commit images that have not yet been compressed, the automated compression process (triggered by the pre-commit hook) will modify those image files to reduce their size. As a result, after your initial commit attempt, you will see these images appear as changed files in Git. To include the optimized images in your repository, you need to stage and commit them again. In rare cases, if further compression is possible, you may need to repeat this process until no further changes are detected. This workflow is intentional and ensures that only optimally compressed images are committed. Many editors can display diffs for images, helping you review these changes.
|
|
105
107
|
|
|
106
108
|
### Parameters
|
|
107
109
|
|
|
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
|
+
|
|
108
112
|
* `--dry` allows you to run Image Guard in “dry mode.” All changes are shown in the terminal.
|
|
109
113
|
|
|
110
|
-
* `--ignore` allows you to specify paths to be ignored (as in `--ignore=example,test`). Multiple paths must be separated by commas. The option supports glob patterns (e.g., `assets/**`, `**/*.png`); matching is case
|
|
114
|
+
* `--ignore` 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.
|
|
111
115
|
|
|
112
|
-
* `--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)
|
|
116
|
+
* `--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.
|
|
113
117
|
|
|
114
118
|
* `--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.)
|
|
115
119
|
|
|
116
120
|
- `--keep-heic` (used with `--heic-to-avif`) preserves the original HEIC/HEIF files instead of deleting them after conversion.
|
|
117
121
|
|
|
118
|
-
* `--quiet` suppresses per
|
|
122
|
+
* `--quiet` 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
123
|
|
|
120
124
|
## What Does the Output Look Like?
|
|
121
125
|
|
|
@@ -128,7 +132,7 @@ Roughly like this:
|
|
|
128
132
|
* White (light gray): The image file has not been changed.
|
|
129
133
|
* Blue: The image file had already been compressed more effectively than the new result, and was therefore skipped, too.
|
|
130
134
|
|
|
131
|
-
Tip: Use `--quiet` to suppress these per
|
|
135
|
+
Tip: Use `--quiet` to suppress these per-file lines and keep only the final summary.
|
|
132
136
|
|
|
133
137
|
## How Does Image Guard Work?
|
|
134
138
|
|
|
@@ -165,4 +169,12 @@ Licensed under the Apache License, Version 2.0 (the “License”); you may not
|
|
|
165
169
|
|
|
166
170
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
167
171
|
|
|
168
|
-
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
172
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
173
|
+
|
|
174
|
+
***
|
|
175
|
+
|
|
176
|
+
You might like some of my other work:
|
|
177
|
+
|
|
178
|
+
* Optimization tools: [hihtml](https://github.com/j9t/hihtml) · [HTML Minifier Next](https://github.com/j9t/html-minifier-next) · [ObsoHTML](https://github.com/j9t/obsohtml) · [CSS Dedup](https://github.com/j9t/css-dedup) · Image Guard · [Compressor.js Next](https://github.com/j9t/compressorjs-next) · [.htaccess Punk](https://github.com/j9t/htaccess-punk)
|
|
179
|
+
* Defense tools: [IA Defensa](https://iadefensa.com/solutions/)
|
|
180
|
+
* Resources for quality web development: [Articles](https://meiert.com/topics/development/) · [Books](https://meiert.com/topics/books/) (including [_On Web Development_](https://meiert.com/blog/on-web-development-2/)) · [News](https://frontenddogma.com/) · [Terminology](https://webglossary.info/)
|
package/package.json
CHANGED
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
"imagemin-merlin": "bin/image-guard.sh"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"globby": "^16.
|
|
9
|
+
"globby": "^16.2.3",
|
|
10
10
|
"heic-decode": "^2.1.0",
|
|
11
|
-
"sharp": "^0.
|
|
12
|
-
"simple-git": "^3.
|
|
11
|
+
"sharp": "^0.35.3",
|
|
12
|
+
"simple-git": "^3.36.0"
|
|
13
13
|
},
|
|
14
14
|
"description": "Near-lossless image compressor that’s easy to use and easy to automate",
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@eslint/js": "^10.0.
|
|
17
|
-
"eslint": "^10.
|
|
16
|
+
"@eslint/js": "^10.0.1",
|
|
17
|
+
"eslint": "^10.8.1"
|
|
18
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/image-guard.js",
|
|
21
|
+
"bin/image-guard.sh",
|
|
22
|
+
"NOTICE.md",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
19
25
|
"funding": "https://github.com/j9t/image-guard?sponsor=1",
|
|
20
26
|
"homepage": "https://github.com/j9t/image-guard",
|
|
21
27
|
"keywords": [
|
|
@@ -44,8 +50,9 @@
|
|
|
44
50
|
"scripts": {
|
|
45
51
|
"lint": "eslint .",
|
|
46
52
|
"lint:fix": "eslint . --fix",
|
|
53
|
+
"prepare": "git config core.hooksPath .githooks || true",
|
|
47
54
|
"test": "node --test bin/image-guard.test.js"
|
|
48
55
|
},
|
|
49
56
|
"type": "module",
|
|
50
|
-
"version": "5.
|
|
57
|
+
"version": "5.3.0"
|
|
51
58
|
}
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { globby, convertPathToPattern } from 'globby'
|
|
4
4
|
import simpleGit from 'simple-git'
|
|
5
5
|
import { parseArgs, styleText } from 'node:util'
|
|
6
|
+
import fsSync from 'node:fs'
|
|
6
7
|
import os from 'node:os'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import sharp from 'sharp'
|
|
@@ -23,7 +24,21 @@ export async function runImageGuard() {
|
|
|
23
24
|
staged: { type: 'boolean', default: false },
|
|
24
25
|
quiet: { type: 'boolean', default: false }
|
|
25
26
|
}
|
|
26
|
-
const { values: argv } = parseArgs({ options })
|
|
27
|
+
const { values: argv, positionals } = parseArgs({ options, allowPositionals: true })
|
|
28
|
+
|
|
29
|
+
if (positionals.length > 1) {
|
|
30
|
+
throw new Error(`Expected at most one path, got ${positionals.length}: ${positionals.join(', ')}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dir = positionals[0] || '.'
|
|
34
|
+
|
|
35
|
+
if (positionals.length && argv.staged) {
|
|
36
|
+
throw new Error('`--staged` takes its files from Git, not from a path—pass one or the other.')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!fsSync.existsSync(dir) || !fsSync.statSync(dir).isDirectory()) {
|
|
40
|
+
throw new Error(`No such directory: ${dir}`)
|
|
41
|
+
}
|
|
27
42
|
|
|
28
43
|
// Share status
|
|
29
44
|
const summary = (run, includesConversion = false) => {
|
|
@@ -42,7 +57,8 @@ export async function runImageGuard() {
|
|
|
42
57
|
|
|
43
58
|
const allTypes = argv['heic-to-avif'] ? [...fileTypes, ...convertTypes] : fileTypes
|
|
44
59
|
if (!argv.quiet) {
|
|
45
|
-
|
|
60
|
+
const where = dir === '.' ? '' : `, in ${dir}`
|
|
61
|
+
console.log(`(Search pattern: ${allTypes.join(', ')}${where})\n`)
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
let savedKB = 0
|
|
@@ -129,12 +145,15 @@ export async function runImageGuard() {
|
|
|
129
145
|
return hadFailures
|
|
130
146
|
}
|
|
131
147
|
|
|
148
|
+
// Plain patterns also get a “/**” variant so directories are excluded with
|
|
149
|
+
// their contents: “dir/” resolves only where globby can stat it, and a bare
|
|
150
|
+
// “dir” only covers what the walk descends into
|
|
132
151
|
const getIgnorePatterns = (ignore) => {
|
|
133
152
|
return (ignore || '')
|
|
134
153
|
.split(',')
|
|
135
|
-
.map(s => s.trim())
|
|
154
|
+
.map(s => s.trim().replace(/^!/, '').replace(/\/+$/, ''))
|
|
136
155
|
.filter(Boolean)
|
|
137
|
-
.
|
|
156
|
+
.flatMap(p => (/[*?[\]{}]/.test(p) ? [`!${p}`] : [`!${p}`, `!${p}/**`]))
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
const getFilePattern = (ignore, types = fileTypes) => {
|
|
@@ -152,13 +171,18 @@ export async function runImageGuard() {
|
|
|
152
171
|
return patterns
|
|
153
172
|
}
|
|
154
173
|
|
|
174
|
+
// Globbing runs with `dir` as its base—so ignore patterns and `.gitignore`
|
|
175
|
+
// lookup are relative to the searched directory, not to the shell’s—and the
|
|
176
|
+
// results are rejoined for display and file access
|
|
155
177
|
const findFiles = async (patterns, options = {}) => {
|
|
156
|
-
|
|
178
|
+
const files = await globby(patterns, {
|
|
179
|
+
cwd: dir,
|
|
157
180
|
gitignore: true,
|
|
158
181
|
onlyFiles: true,
|
|
159
182
|
caseSensitiveMatch: false,
|
|
160
183
|
...options
|
|
161
184
|
})
|
|
185
|
+
return files.map(file => path.join(dir, file))
|
|
162
186
|
}
|
|
163
187
|
|
|
164
188
|
const filterStagedFiles = async (stagedFiles, types) => {
|
package/.editorconfig
DELETED
package/.github/FUNDING.yml
DELETED
package/.github/dependabot.yml
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference
|
|
2
|
-
|
|
3
|
-
version: 2
|
|
4
|
-
updates:
|
|
5
|
-
- package-ecosystem: "npm"
|
|
6
|
-
directory: "/"
|
|
7
|
-
schedule:
|
|
8
|
-
interval: "monthly"
|
|
9
|
-
- package-ecosystem: "github-actions"
|
|
10
|
-
directory: "/"
|
|
11
|
-
schedule:
|
|
12
|
-
interval: "monthly"
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: "npm Publishing"
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
|
|
6
|
-
jobs:
|
|
7
|
-
publish:
|
|
8
|
-
runs-on: ubuntu-latest
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
id-token: write
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v6
|
|
14
|
-
- uses: actions/setup-node@v6
|
|
15
|
-
with:
|
|
16
|
-
node-version: 'lts/*'
|
|
17
|
-
registry-url: 'https://registry.npmjs.org'
|
|
18
|
-
- run: npm ci
|
|
19
|
-
- run: npm publish --provenance
|
|
20
|
-
env:
|
|
21
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
name: "Tests"
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
|
|
11
|
-
concurrency:
|
|
12
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
-
cancel-in-progress: true
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
test:
|
|
17
|
-
strategy:
|
|
18
|
-
matrix:
|
|
19
|
-
platform: [ubuntu-latest]
|
|
20
|
-
node: ['lts/*', 'current']
|
|
21
|
-
name: "Node ${{ matrix.node }} (${{ matrix.platform }})"
|
|
22
|
-
runs-on: ${{ matrix.platform }}
|
|
23
|
-
steps:
|
|
24
|
-
- name: "Check out repository"
|
|
25
|
-
uses: actions/checkout@v6
|
|
26
|
-
- name: "Set up Node.js"
|
|
27
|
-
uses: actions/setup-node@v6
|
|
28
|
-
with:
|
|
29
|
-
node-version: ${{ matrix.node }}
|
|
30
|
-
cache: npm
|
|
31
|
-
- name: "Install dependencies"
|
|
32
|
-
run: npm ci
|
|
33
|
-
- name: "Run tests"
|
|
34
|
-
run: npm test
|
|
35
|
-
|
|
36
|
-
test-macos:
|
|
37
|
-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
38
|
-
strategy:
|
|
39
|
-
matrix:
|
|
40
|
-
node: ['lts/*', 'current']
|
|
41
|
-
name: "Node ${{ matrix.node }} (macOS)"
|
|
42
|
-
runs-on: macos-latest
|
|
43
|
-
steps:
|
|
44
|
-
- name: "Check out repository"
|
|
45
|
-
uses: actions/checkout@v6
|
|
46
|
-
- name: "Set up Node.js"
|
|
47
|
-
uses: actions/setup-node@v6
|
|
48
|
-
with:
|
|
49
|
-
node-version: ${{ matrix.node }}
|
|
50
|
-
cache: npm
|
|
51
|
-
- name: "Install dependencies"
|
|
52
|
-
run: npm ci
|
|
53
|
-
- name: "Run tests"
|
|
54
|
-
run: npm test
|
package/CHANGELOG.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Starting with version 5.2.0, all notable changes to Image Guard are documented in this file, which is (mostly) AI-generated and (always) human-edited. Dependency updates may or may not be called out specifically.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
-
|
|
7
|
-
## [5.2.5] - 2026-03-17
|
|
8
|
-
|
|
9
|
-
### Changed
|
|
10
|
-
|
|
11
|
-
* Added recommended ESLint rules to config (and fixed resulting warnings)
|
|
12
|
-
* Promoted `no-unused-vars` and `eqeqeq` from warnings to errors in ESLint config
|
|
13
|
-
|
|
14
|
-
## [5.2.4] - 2026-03-04
|
|
15
|
-
|
|
16
|
-
### Fixed
|
|
17
|
-
|
|
18
|
-
* Changed bin entry from a Node.js script to a shell wrapper that locates node from common version managers (nvm, Volta, asdf, mise) and package managers (Homebrew) before invocation, resolving hook failures in GUI Git clients that don’t inherit the shell’s PATH
|
|
19
|
-
- **Migration:** If you set up hooks, replace `npx image-guard --staged` with `./node_modules/.bin/image-guard --staged` in your `.githooks/pre-commit` or `.husky/pre-commit`.
|
|
20
|
-
|
|
21
|
-
## [5.2.3] - 2026-02-21
|
|
22
|
-
|
|
23
|
-
### Fixed
|
|
24
|
-
|
|
25
|
-
* Improved corrupt-file error detection to cover WebP and AVIF formats, normalize message casing before matching, and avoid false positives from the overly broad `Invalid` string match
|
|
26
|
-
* Warned when a `.bak` file is left behind after a failed file replacement
|
|
27
|
-
* Removed a dead code branch in result processing
|
|
28
|
-
|
|
29
|
-
### Changed
|
|
30
|
-
|
|
31
|
-
* Moved to compression and conversion running in parallel (non-overlapping file sets), reducing wall-clock time when `--heic-to-avif` is active
|
|
32
|
-
* Reduced directory traversals from two to one in non-staged mode when `--heic-to-avif` is active
|
|
33
|
-
* Extracted shared `MAX_FILE_SIZE` constant; aligned `node:fs` and `node:path` import style in `utils.js`
|
|
34
|
-
* Extended test coverage for corrupt file detection and reporting
|
|
35
|
-
|
|
36
|
-
## [5.2.2] - 2026-02-17
|
|
37
|
-
|
|
38
|
-
### Fixed
|
|
39
|
-
|
|
40
|
-
* Updated HEIC dry run to report actual file sizes instead of omitting size data
|
|
41
|
-
|
|
42
|
-
## [5.2.1] - 2026-02-17
|
|
43
|
-
|
|
44
|
-
### Changed
|
|
45
|
-
|
|
46
|
-
* Adjusted HEIC-to-AVIF conversion to use lossy instead of lossless encoding (quality 80), to produce files smaller than the HEIC originals but still at high quality
|
|
47
|
-
* Removed redundant lossless compression pass after HEIC conversion
|
|
48
|
-
* Documented Display P3 to sRGB color space mapping during HEIC conversion
|
|
49
|
-
|
|
50
|
-
## [5.2.0] - 2026-02-17
|
|
51
|
-
|
|
52
|
-
### Added
|
|
53
|
-
|
|
54
|
-
* Added opt-in HEIC/HEIF-to-AVIF conversion via `--heic-to-avif` (with `--keep-heic` to preserve originals)
|
|
55
|
-
* Introduced changelog
|
package/SECURITY.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Supported Versions
|
|
4
|
-
|
|
5
|
-
Only the latest and therefore current version of Image Guard is supported. It’s advised to update older versions to the latest version.
|
|
6
|
-
|
|
7
|
-
## Reporting a Vulnerability
|
|
8
|
-
|
|
9
|
-
To report a vulnerability, please [file an issue](https://github.com/j9t/image-guard/issues/new) or email info@meiert.com.
|
package/bin/image-guard.test.js
DELETED
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import os from 'os'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { execSync } from 'child_process'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
import { test, describe, before, after } from 'node:test'
|
|
7
|
-
import assert from 'node:assert'
|
|
8
|
-
import simpleGit from 'simple-git'
|
|
9
|
-
import { fileTypes as allowedFileTypes } from '../src/index.js'
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
-
const __dirname = path.dirname(__filename)
|
|
13
|
-
const testFolder = path.join(__dirname, '../media/test')
|
|
14
|
-
const testFolderGit = path.join(__dirname, '../media/test-git')
|
|
15
|
-
const imageGuardScript = path.join(__dirname, '../bin/image-guard.js')
|
|
16
|
-
|
|
17
|
-
// Function to copy files
|
|
18
|
-
function copyFiles(srcDir, destDir) {
|
|
19
|
-
if (!fs.existsSync(destDir)) {
|
|
20
|
-
fs.mkdirSync(destDir, { recursive: true })
|
|
21
|
-
}
|
|
22
|
-
fs.readdirSync(srcDir).forEach(file => {
|
|
23
|
-
const srcFile = path.join(srcDir, file)
|
|
24
|
-
const destFile = path.join(destDir, file)
|
|
25
|
-
fs.copyFileSync(srcFile, destFile)
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Function to check if images are compressed
|
|
30
|
-
const ignoreFiles = ['test#corrupt.gif']
|
|
31
|
-
|
|
32
|
-
function areImagesCompressed(dir, originalDir = testFolder) {
|
|
33
|
-
const uncompressedFiles = []
|
|
34
|
-
const allCompressed = fs.readdirSync(dir).every(file => {
|
|
35
|
-
if (ignoreFiles.includes(file)) {
|
|
36
|
-
// console.info(`Ignoring file: ${file}`)
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
39
|
-
const ext = path.extname(file).slice(1).toLowerCase()
|
|
40
|
-
if (!allowedFileTypes.includes(ext)) return true
|
|
41
|
-
const filePath = path.join(dir, file)
|
|
42
|
-
const originalFilePath = path.join(originalDir, file)
|
|
43
|
-
try {
|
|
44
|
-
const originalStats = fs.statSync(originalFilePath)
|
|
45
|
-
const compressedStats = fs.statSync(filePath)
|
|
46
|
-
const isCompressed = compressedStats.size < originalStats.size
|
|
47
|
-
if (!isCompressed) {
|
|
48
|
-
uncompressedFiles.push(file)
|
|
49
|
-
}
|
|
50
|
-
return isCompressed
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.warn(`Skipping possibly corrupt file: ${file} (${err.message})`)
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
return { allCompressed, uncompressedFiles }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Function to check if images are already compressed
|
|
60
|
-
function areImagesAlreadyCompressed(dir) {
|
|
61
|
-
return fs.readdirSync(dir).some(file => {
|
|
62
|
-
const ext = path.extname(file).slice(1).toLowerCase()
|
|
63
|
-
if (!allowedFileTypes.includes(ext)) return false
|
|
64
|
-
const filePath = path.join(dir, file)
|
|
65
|
-
const originalFilePath = path.join(testFolder, file)
|
|
66
|
-
const originalStats = fs.statSync(originalFilePath)
|
|
67
|
-
const compressedStats = fs.statSync(filePath)
|
|
68
|
-
return compressedStats.size >= originalStats.size
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe('Image Guard', () => {
|
|
73
|
-
before(() => {
|
|
74
|
-
// Back up original images
|
|
75
|
-
copyFiles(testFolder, testFolderGit)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
after(() => {
|
|
79
|
-
// Clean up temporary directory
|
|
80
|
-
if (fs.existsSync(testFolderGit)) {
|
|
81
|
-
fs.rmSync(testFolderGit, { recursive: true, force: true })
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('Compress images', () => {
|
|
86
|
-
// Ensure images in temp folder are not already compressed
|
|
87
|
-
assert.strictEqual(areImagesAlreadyCompressed(testFolderGit), true)
|
|
88
|
-
|
|
89
|
-
// Run the script in a completely isolated temporary directory
|
|
90
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-test-'))
|
|
91
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
92
|
-
|
|
93
|
-
// Copy test files to isolated temp directory
|
|
94
|
-
copyFiles(testFolder, tempTestFolder)
|
|
95
|
-
|
|
96
|
-
// Run image-guard from temp directory—only files in “tempDir” will be processed
|
|
97
|
-
const originalCwd = process.cwd()
|
|
98
|
-
try {
|
|
99
|
-
process.chdir(tempDir)
|
|
100
|
-
execSync(`node "${imageGuardScript}"`)
|
|
101
|
-
} finally {
|
|
102
|
-
process.chdir(originalCwd)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check results from the isolated temp files
|
|
106
|
-
const { allCompressed, uncompressedFiles } = areImagesCompressed(tempTestFolder, testFolder)
|
|
107
|
-
|
|
108
|
-
// Clean up
|
|
109
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
110
|
-
|
|
111
|
-
if (uncompressedFiles.length > 0) {
|
|
112
|
-
console.log('The following files were not compressed:', uncompressedFiles.join(', '))
|
|
113
|
-
}
|
|
114
|
-
assert.strictEqual(allCompressed, true)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('Compress only staged images', async () => {
|
|
118
|
-
const git = simpleGit(testFolderGit)
|
|
119
|
-
|
|
120
|
-
// Ensure the temp folder exists
|
|
121
|
-
if (!fs.existsSync(testFolderGit)) {
|
|
122
|
-
fs.mkdirSync(testFolderGit, { recursive: true })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Initialize a temporary Git repository
|
|
126
|
-
await git.init()
|
|
127
|
-
await git.addConfig('user.name', 'Test User')
|
|
128
|
-
await git.addConfig('user.email', 'test@example.com')
|
|
129
|
-
|
|
130
|
-
// Stage files
|
|
131
|
-
await git.add('.')
|
|
132
|
-
|
|
133
|
-
// Run image-guard script with “--staged” option
|
|
134
|
-
execSync(`node "${imageGuardScript}" --staged`, { cwd: testFolderGit })
|
|
135
|
-
|
|
136
|
-
// Verify images are compressed
|
|
137
|
-
const { allCompressed, uncompressedFiles } = areImagesCompressed(testFolderGit)
|
|
138
|
-
if (uncompressedFiles.length > 0) {
|
|
139
|
-
console.log('The following files were not compressed:', uncompressedFiles.join(', '))
|
|
140
|
-
}
|
|
141
|
-
assert.strictEqual(allCompressed, true)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
test('Ensure files are not modified in dry run', () => {
|
|
145
|
-
const originalStats = fs.readdirSync(testFolderGit).sort().map(file => {
|
|
146
|
-
const filePath = path.join(testFolderGit, file)
|
|
147
|
-
return { file, stats: fs.statSync(filePath) }
|
|
148
|
-
})
|
|
149
|
-
execSync(`node "${imageGuardScript}" --dry`, { cwd: testFolderGit, stdio: 'pipe' })
|
|
150
|
-
const newStats = fs.readdirSync(testFolderGit).sort().map(file => {
|
|
151
|
-
const filePath = path.join(testFolderGit, file)
|
|
152
|
-
return { file, stats: fs.statSync(filePath) }
|
|
153
|
-
})
|
|
154
|
-
originalStats.forEach((original, index) => {
|
|
155
|
-
const newFile = newStats[index]
|
|
156
|
-
assert.strictEqual(newFile.file, original.file)
|
|
157
|
-
assert.strictEqual(newFile.stats.size, original.stats.size)
|
|
158
|
-
assert.strictEqual(newFile.stats.mtime.getTime(), original.stats.mtime.getTime())
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
test('Ignore parity: single file (non-staged vs. staged)', async () => {
|
|
163
|
-
// Prepare isolated temp directory
|
|
164
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-ignore-one-'))
|
|
165
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
166
|
-
copyFiles(testFolder, tempTestFolder)
|
|
167
|
-
|
|
168
|
-
// Pick a known file from fixture folder
|
|
169
|
-
const entries = fs.readdirSync(tempTestFolder).filter(n => /\.(png|jpe?g|gif|webp|avif)$/i.test(n))
|
|
170
|
-
if (entries.length === 0) {
|
|
171
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
const target = entries[0]
|
|
175
|
-
const tempPath = path.join(tempTestFolder, target)
|
|
176
|
-
// Snapshot the temp copy before running the CLI to ensure equality checks reflect true non-mutation
|
|
177
|
-
const before = fs.statSync(tempPath)
|
|
178
|
-
|
|
179
|
-
// Prepare a pre-run snapshot for non-ignored candidates to verify at least one gets compressed
|
|
180
|
-
const preSnapshot = new Map()
|
|
181
|
-
fs.readdirSync(tempTestFolder).sort().forEach(name => {
|
|
182
|
-
if (name === target) return // excluded: explicitly ignored
|
|
183
|
-
if (ignoreFiles.includes(name)) return // excluded: known corrupt fixture
|
|
184
|
-
const ext = path.extname(name).slice(1).toLowerCase()
|
|
185
|
-
if (!allowedFileTypes.includes(ext)) return
|
|
186
|
-
const p = path.join(tempTestFolder, name)
|
|
187
|
-
preSnapshot.set(name, fs.statSync(p))
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// Non-staged: Run with `--ignore=<file>`
|
|
191
|
-
const originalCwd = process.cwd()
|
|
192
|
-
try {
|
|
193
|
-
process.chdir(tempDir)
|
|
194
|
-
execSync(`node "${imageGuardScript}" --ignore=${path.posix.join('test', target)}`, { stdio: 'pipe' })
|
|
195
|
-
} finally {
|
|
196
|
-
process.chdir(originalCwd)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Verify the ignored file was not modified (size and mtime unchanged vs. pre-run snapshot)
|
|
200
|
-
const tempStats = fs.statSync(tempPath)
|
|
201
|
-
assert.strictEqual(tempStats.size, before.size)
|
|
202
|
-
assert.strictEqual(tempStats.mtime.getTime(), before.mtime.getTime())
|
|
203
|
-
|
|
204
|
-
// Verify at least one non-ignored candidate was compressed in the non-staged run
|
|
205
|
-
let shrunkCount = 0
|
|
206
|
-
for (const [name, statBefore] of preSnapshot) {
|
|
207
|
-
const statAfter = fs.statSync(path.join(tempTestFolder, name))
|
|
208
|
-
if (statAfter.size < statBefore.size) shrunkCount++
|
|
209
|
-
}
|
|
210
|
-
assert.ok(shrunkCount >= 1, 'Expected at least one non-ignored file to be compressed')
|
|
211
|
-
|
|
212
|
-
// Staged: Init repo, stage only target and another file, ensure ignore prevents its processing
|
|
213
|
-
const git = simpleGit(tempTestFolder)
|
|
214
|
-
await git.init()
|
|
215
|
-
await git.addConfig('user.name', 'Test User')
|
|
216
|
-
await git.addConfig('user.email', 'test@example.com')
|
|
217
|
-
await git.add('.')
|
|
218
|
-
|
|
219
|
-
// Run staged with `ignore`
|
|
220
|
-
execSync(`node "${imageGuardScript}" --staged --ignore=${path.posix.join('test', target)}`, { cwd: tempTestFolder, stdio: 'pipe' })
|
|
221
|
-
|
|
222
|
-
// Check file still not modified compared to its current state (size should not shrink due to ignore)
|
|
223
|
-
const afterStats = fs.statSync(tempPath)
|
|
224
|
-
assert.strictEqual(afterStats.size, tempStats.size)
|
|
225
|
-
|
|
226
|
-
// Cleanup
|
|
227
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test('Ignore supports multiple patterns and directories; case-insensitive', () => {
|
|
231
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-ignore-multi-'))
|
|
232
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
233
|
-
copyFiles(testFolder, tempTestFolder)
|
|
234
|
-
|
|
235
|
-
// Create a subdirectory to simulate directory ignore
|
|
236
|
-
const subDir = path.join(tempTestFolder, 'Assets')
|
|
237
|
-
fs.mkdirSync(subDir, { recursive: true })
|
|
238
|
-
// Copy one file into subdir
|
|
239
|
-
const oneFile = fs.readdirSync(tempTestFolder).find(n => /\.(png|jpe?g|gif|webp|avif)$/i.test(n))
|
|
240
|
-
if (oneFile) fs.copyFileSync(path.join(tempTestFolder, oneFile), path.join(subDir, oneFile))
|
|
241
|
-
|
|
242
|
-
// Build ignore list: specific file (if available) and directory (case-insensitive path)
|
|
243
|
-
const ignoreArg = oneFile ? `--ignore=test/${oneFile},test/assets/` : `--ignore=test/assets/`
|
|
244
|
-
// Snapshot the file placed in ignored directory, before running the CLI
|
|
245
|
-
let preInside
|
|
246
|
-
if (oneFile) {
|
|
247
|
-
preInside = fs.statSync(path.join(subDir, oneFile))
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Snapshot before running CLI for file-level ignore check
|
|
251
|
-
let preIgnored
|
|
252
|
-
if (oneFile) {
|
|
253
|
-
preIgnored = fs.statSync(path.join(tempTestFolder, oneFile))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Build a pre-run snapshot of candidates that are not ignored
|
|
257
|
-
const preSnapshot = new Map()
|
|
258
|
-
fs.readdirSync(tempTestFolder).sort().forEach(name => {
|
|
259
|
-
// Exclude the explicitly ignored file (if any) and anything inside the ignored directory
|
|
260
|
-
if (oneFile && name === oneFile) return
|
|
261
|
-
if (ignoreFiles.includes(name)) return // exclude corrupt fixture
|
|
262
|
-
const ext = path.extname(name).slice(1).toLowerCase()
|
|
263
|
-
if (!allowedFileTypes.includes(ext)) return
|
|
264
|
-
const p = path.join(tempTestFolder, name)
|
|
265
|
-
preSnapshot.set(name, fs.statSync(p))
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
const originalCwd = process.cwd()
|
|
269
|
-
try {
|
|
270
|
-
process.chdir(tempDir)
|
|
271
|
-
execSync(`node "${imageGuardScript}" ${ignoreArg}`, { stdio: 'pipe' })
|
|
272
|
-
} finally {
|
|
273
|
-
process.chdir(originalCwd)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Assert ignored file unchanged (only if there was a file to ignore explicitly)
|
|
277
|
-
if (oneFile) {
|
|
278
|
-
const ignoredCopy = fs.statSync(path.join(tempTestFolder, oneFile))
|
|
279
|
-
assert.strictEqual(ignoredCopy.size, preIgnored.size)
|
|
280
|
-
assert.strictEqual(ignoredCopy.mtime.getTime(), preIgnored.mtime.getTime())
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Assert that at least one non-ignored file in the root `tempTestFolder` was compressed
|
|
284
|
-
let shrunkCount = 0
|
|
285
|
-
for (const [name, statBefore] of preSnapshot) {
|
|
286
|
-
const statAfter = fs.statSync(path.join(tempTestFolder, name))
|
|
287
|
-
if (statAfter.size < statBefore.size) shrunkCount++
|
|
288
|
-
}
|
|
289
|
-
assert.ok(shrunkCount >= 1, 'Expected at least one non-ignored file to be compressed')
|
|
290
|
-
|
|
291
|
-
// Assert file inside ignored directory unchanged (if created)
|
|
292
|
-
if (oneFile) {
|
|
293
|
-
const inside = fs.statSync(path.join(subDir, oneFile))
|
|
294
|
-
// Since original may change, assert that the file inside ignored directory remained unchanged, by comparing to its own pre-run snapshot
|
|
295
|
-
assert.strictEqual(inside.size, preInside.size)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test('Ensure quiet mode suppresses per-file logs but keeps summary', () => {
|
|
302
|
-
// Prepare isolated temp directory with test images
|
|
303
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-quiet-'))
|
|
304
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
305
|
-
copyFiles(testFolder, tempTestFolder)
|
|
306
|
-
|
|
307
|
-
const originalCwd = process.cwd()
|
|
308
|
-
let stdout
|
|
309
|
-
try {
|
|
310
|
-
process.chdir(tempDir)
|
|
311
|
-
stdout = execSync(`node "${imageGuardScript}" --quiet`, { encoding: 'utf8' })
|
|
312
|
-
} finally {
|
|
313
|
-
process.chdir(originalCwd)
|
|
314
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Summary should be present
|
|
318
|
-
assert.match(stdout, /Defensive base compression completed\./)
|
|
319
|
-
// Per-file lines like “Compressed <file>” or “Skipped <file>” should be suppressed
|
|
320
|
-
assert.strictEqual(!(/Compressed|Skipped/.test(stdout)), true)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
test('Ensure dry and quiet runs leave no artifacts and do not mutate files', () => {
|
|
324
|
-
// Use isolated temp directory
|
|
325
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-dry-quiet-'))
|
|
326
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
327
|
-
copyFiles(testFolder, tempTestFolder)
|
|
328
|
-
|
|
329
|
-
// Snapshot sizes/mtimes
|
|
330
|
-
const before = fs.readdirSync(tempTestFolder).sort().map(file => {
|
|
331
|
-
const filePath = path.join(tempTestFolder, file)
|
|
332
|
-
return { file, stats: fs.statSync(filePath) }
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
const originalCwd = process.cwd()
|
|
336
|
-
let stdout
|
|
337
|
-
try {
|
|
338
|
-
process.chdir(tempDir)
|
|
339
|
-
stdout = execSync(`node "${imageGuardScript}" --dry --quiet`, { encoding: 'utf8' })
|
|
340
|
-
} finally {
|
|
341
|
-
process.chdir(originalCwd)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Summary present; no per-file lines
|
|
345
|
-
assert.match(stdout, /There were no images to compress\.|Defensive base compression completed\./)
|
|
346
|
-
assert.strictEqual(!(/Compressed|Skipped/.test(stdout)), true)
|
|
347
|
-
|
|
348
|
-
// Verify no mutations
|
|
349
|
-
const after = fs.readdirSync(tempTestFolder).sort().map(file => {
|
|
350
|
-
const filePath = path.join(tempTestFolder, file)
|
|
351
|
-
return { file, stats: fs.statSync(filePath) }
|
|
352
|
-
})
|
|
353
|
-
before.forEach((b, i) => {
|
|
354
|
-
const a = after[i]
|
|
355
|
-
assert.strictEqual(a.file, b.file)
|
|
356
|
-
assert.strictEqual(a.stats.size, b.stats.size)
|
|
357
|
-
assert.strictEqual(a.stats.mtime.getTime(), b.stats.mtime.getTime())
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// Ensure no temp or backup artifacts present
|
|
361
|
-
const entries = fs.readdirSync(tempTestFolder)
|
|
362
|
-
const hasTemp = entries.some(name => name.startsWith('.image-guard-'))
|
|
363
|
-
const hasBak = entries.some(name => name.endsWith('.bak'))
|
|
364
|
-
assert.strictEqual(hasTemp, false)
|
|
365
|
-
assert.strictEqual(hasBak, false)
|
|
366
|
-
|
|
367
|
-
// Cleanup
|
|
368
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
test('Ensure no .bak files remain after normal compression', () => {
|
|
372
|
-
// Prepare isolated temp directory with test images
|
|
373
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-bak-'))
|
|
374
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
375
|
-
copyFiles(testFolder, tempTestFolder)
|
|
376
|
-
|
|
377
|
-
const originalCwd = process.cwd()
|
|
378
|
-
try {
|
|
379
|
-
process.chdir(tempDir)
|
|
380
|
-
execSync(`node "${imageGuardScript}"`, { stdio: 'pipe' })
|
|
381
|
-
} finally {
|
|
382
|
-
process.chdir(originalCwd)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const entries = fs.readdirSync(tempTestFolder)
|
|
386
|
-
const hasBak = entries.some(name => name.endsWith('.bak'))
|
|
387
|
-
assert.strictEqual(hasBak, false)
|
|
388
|
-
|
|
389
|
-
// Cleanup
|
|
390
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
test('Convert HEIC to AVIF with `--heic-to-avif`', () => {
|
|
394
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-'))
|
|
395
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
396
|
-
copyFiles(testFolder, tempTestFolder)
|
|
397
|
-
|
|
398
|
-
// Ensure HEIC test fixture exists
|
|
399
|
-
const heicFile = path.join(tempTestFolder, 'test.heic')
|
|
400
|
-
assert.strictEqual(fs.existsSync(heicFile), true, 'HEIC test fixture must exist')
|
|
401
|
-
|
|
402
|
-
// Remove pre-existing AVIF so we can verify the conversion actually creates it
|
|
403
|
-
const avifFile = path.join(tempTestFolder, 'test.avif')
|
|
404
|
-
if (fs.existsSync(avifFile)) {
|
|
405
|
-
fs.unlinkSync(avifFile)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const originalCwd = process.cwd()
|
|
409
|
-
try {
|
|
410
|
-
process.chdir(tempDir)
|
|
411
|
-
execSync(`node "${imageGuardScript}" --heic-to-avif`, { stdio: 'pipe' })
|
|
412
|
-
} finally {
|
|
413
|
-
process.chdir(originalCwd)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// AVIF should be created by the conversion
|
|
417
|
-
assert.strictEqual(fs.existsSync(avifFile), true, 'AVIF file should be created')
|
|
418
|
-
const avifSize = fs.statSync(avifFile).size
|
|
419
|
-
assert.ok(avifSize > 0, 'AVIF file should not be empty')
|
|
420
|
-
|
|
421
|
-
// Lossy AVIF should not be larger than the HEIC source
|
|
422
|
-
const heicSize = fs.statSync(path.join(testFolder, 'test.heic')).size
|
|
423
|
-
assert.ok(avifSize <= heicSize, `AVIF (${avifSize}) should not be larger than HEIC (${heicSize})`)
|
|
424
|
-
|
|
425
|
-
// Original HEIC should be deleted
|
|
426
|
-
assert.strictEqual(fs.existsSync(heicFile), false, 'Original HEIC should be deleted')
|
|
427
|
-
|
|
428
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
test('Ensure `--keep-heic` preserves original HEIC file', () => {
|
|
432
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-keep-heic-'))
|
|
433
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
434
|
-
copyFiles(testFolder, tempTestFolder)
|
|
435
|
-
|
|
436
|
-
const heicFile = path.join(tempTestFolder, 'test.heic')
|
|
437
|
-
assert.strictEqual(fs.existsSync(heicFile), true, 'HEIC test fixture must exist')
|
|
438
|
-
|
|
439
|
-
const originalCwd = process.cwd()
|
|
440
|
-
try {
|
|
441
|
-
process.chdir(tempDir)
|
|
442
|
-
execSync(`node "${imageGuardScript}" --heic-to-avif --keep-heic`, { stdio: 'pipe' })
|
|
443
|
-
} finally {
|
|
444
|
-
process.chdir(originalCwd)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Both files should exist
|
|
448
|
-
assert.strictEqual(fs.existsSync(path.join(tempTestFolder, 'test.avif')), true, 'AVIF file should be created')
|
|
449
|
-
assert.strictEqual(fs.existsSync(heicFile), true, 'HEIC file should be preserved with `--keep-heic`')
|
|
450
|
-
|
|
451
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
test('Ensure dry run does not convert HEIC files but reports sizes', () => {
|
|
455
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-dry-'))
|
|
456
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
457
|
-
copyFiles(testFolder, tempTestFolder)
|
|
458
|
-
|
|
459
|
-
// Remove pre-existing AVIF so the conversion path is exercised
|
|
460
|
-
const avifFile = path.join(tempTestFolder, 'test.avif')
|
|
461
|
-
if (fs.existsSync(avifFile)) {
|
|
462
|
-
fs.unlinkSync(avifFile)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const heicFile = path.join(tempTestFolder, 'test.heic')
|
|
466
|
-
const heicBefore = fs.statSync(heicFile)
|
|
467
|
-
const filesBefore = fs.readdirSync(tempTestFolder).sort()
|
|
468
|
-
|
|
469
|
-
const originalCwd = process.cwd()
|
|
470
|
-
let output
|
|
471
|
-
try {
|
|
472
|
-
process.chdir(tempDir)
|
|
473
|
-
output = execSync(`node "${imageGuardScript}" --heic-to-avif --dry`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
474
|
-
} finally {
|
|
475
|
-
process.chdir(originalCwd)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// HEIC should be unchanged
|
|
479
|
-
const heicAfter = fs.statSync(heicFile)
|
|
480
|
-
assert.strictEqual(heicAfter.size, heicBefore.size)
|
|
481
|
-
assert.strictEqual(heicAfter.mtime.getTime(), heicBefore.mtime.getTime())
|
|
482
|
-
|
|
483
|
-
// No files should be added or removed
|
|
484
|
-
const filesAfter = fs.readdirSync(tempTestFolder).sort()
|
|
485
|
-
assert.deepStrictEqual(filesAfter, filesBefore)
|
|
486
|
-
|
|
487
|
-
// Dry run should report size data for HEIC conversion
|
|
488
|
-
assert.match(output, /Converted.*test\.heic.*KB.*→.*KB/i, 'Dry run should report HEIC conversion sizes')
|
|
489
|
-
|
|
490
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
test('Ensure HEIC files are ignored without `--heic-to-avif` flag', () => {
|
|
494
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-noflag-'))
|
|
495
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
496
|
-
copyFiles(testFolder, tempTestFolder)
|
|
497
|
-
|
|
498
|
-
const heicFile = path.join(tempTestFolder, 'test.heic')
|
|
499
|
-
const heicBefore = fs.statSync(heicFile)
|
|
500
|
-
|
|
501
|
-
const originalCwd = process.cwd()
|
|
502
|
-
try {
|
|
503
|
-
process.chdir(tempDir)
|
|
504
|
-
execSync(`node "${imageGuardScript}"`, { stdio: 'pipe' })
|
|
505
|
-
} finally {
|
|
506
|
-
process.chdir(originalCwd)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// HEIC should be untouched
|
|
510
|
-
const heicAfter = fs.statSync(heicFile)
|
|
511
|
-
assert.strictEqual(heicAfter.size, heicBefore.size)
|
|
512
|
-
assert.strictEqual(heicAfter.mtime.getTime(), heicBefore.mtime.getTime())
|
|
513
|
-
|
|
514
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
test('Skip and report corrupt files', () => {
|
|
518
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-corrupt-'))
|
|
519
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
520
|
-
copyFiles(testFolder, tempTestFolder)
|
|
521
|
-
|
|
522
|
-
const originalCwd = process.cwd()
|
|
523
|
-
let output
|
|
524
|
-
try {
|
|
525
|
-
process.chdir(tempDir)
|
|
526
|
-
output = execSync(`node "${imageGuardScript}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
527
|
-
} finally {
|
|
528
|
-
process.chdir(originalCwd)
|
|
529
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
assert.match(output, /Skipped.*test#corrupt\.gif.*corrupt file/i)
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
test('Ensure `--keep-heic` without `--heic-to-avif` issues warning', () => {
|
|
536
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-keep-warn-'))
|
|
537
|
-
const tempTestFolder = path.join(tempDir, 'test')
|
|
538
|
-
copyFiles(testFolder, tempTestFolder)
|
|
539
|
-
|
|
540
|
-
const originalCwd = process.cwd()
|
|
541
|
-
let output
|
|
542
|
-
try {
|
|
543
|
-
process.chdir(tempDir)
|
|
544
|
-
output = execSync(`node "${imageGuardScript}" --keep-heic 2>&1`, { encoding: 'utf8' })
|
|
545
|
-
} finally {
|
|
546
|
-
process.chdir(originalCwd)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
assert.match(output, /`--keep-heic` has no effect without `--heic-to-avif`/)
|
|
550
|
-
|
|
551
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
552
|
-
})
|
|
553
|
-
})
|
package/eslint.config.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import js from '@eslint/js';
|
|
2
|
-
|
|
3
|
-
export default [
|
|
4
|
-
{
|
|
5
|
-
ignores: [
|
|
6
|
-
'media/',
|
|
7
|
-
'node_modules/**'
|
|
8
|
-
]
|
|
9
|
-
},
|
|
10
|
-
js.configs.recommended,
|
|
11
|
-
{
|
|
12
|
-
languageOptions: {
|
|
13
|
-
ecmaVersion: 'latest',
|
|
14
|
-
sourceType: 'module',
|
|
15
|
-
globals: {
|
|
16
|
-
console: 'readonly',
|
|
17
|
-
process: 'readonly',
|
|
18
|
-
setTimeout: 'readonly'
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
rules: {
|
|
22
|
-
'no-console': 'off', // CLI tool—allow console
|
|
23
|
-
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
24
|
-
eqeqeq: ['error', 'smart'],
|
|
25
|
-
curly: ['warn', 'multi-line']
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
]
|
package/media/output.png
DELETED
|
Binary file
|