image-guard 5.2.4 → 5.2.6
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 +21 -13
- package/package.json +12 -5
- package/src/index.js +1 -1
- package/src/utils.js +1 -1
- 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 -48
- package/SECURITY.md +0 -9
- package/bin/image-guard.test.js +0 -553
- package/eslint.config.js +0 -26
- 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/j9t/image-guard?sponsor=1)
|
|
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;\
|
|
@@ -107,7 +107,7 @@ npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --st
|
|
|
107
107
|
|
|
108
108
|
* `--dry` allows you to run Image Guard in “dry mode.” All changes are shown in the terminal.
|
|
109
109
|
|
|
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
|
|
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-insensitive and honors `.gitignore`.
|
|
111
111
|
|
|
112
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) in `git diff` and only processes those files—that approach makes Image Guard more efficient in operation.
|
|
113
113
|
|
|
@@ -115,7 +115,7 @@ npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --st
|
|
|
115
115
|
|
|
116
116
|
- `--keep-heic` (used with `--heic-to-avif`) preserves the original HEIC/HEIF files instead of deleting them after conversion.
|
|
117
117
|
|
|
118
|
-
* `--quiet` suppresses per
|
|
118
|
+
* `--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
119
|
|
|
120
120
|
## What Does the Output Look Like?
|
|
121
121
|
|
|
@@ -128,7 +128,7 @@ Roughly like this:
|
|
|
128
128
|
* White (light gray): The image file has not been changed.
|
|
129
129
|
* Blue: The image file had already been compressed more effectively than the new result, and was therefore skipped, too.
|
|
130
130
|
|
|
131
|
-
Tip: Use `--quiet` to suppress these per
|
|
131
|
+
Tip: Use `--quiet` to suppress these per-file lines and keep only the final summary.
|
|
132
132
|
|
|
133
133
|
## How Does Image Guard Work?
|
|
134
134
|
|
|
@@ -165,4 +165,12 @@ Licensed under the Apache License, Version 2.0 (the “License”); you may not
|
|
|
165
165
|
|
|
166
166
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
167
167
|
|
|
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.
|
|
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.
|
|
169
|
+
|
|
170
|
+
***
|
|
171
|
+
|
|
172
|
+
You might like some of my other work:
|
|
173
|
+
|
|
174
|
+
* 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)
|
|
175
|
+
* Defense tools: [IA Defensa](https://iadefensa.com/solutions/)
|
|
176
|
+
* 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,15 +6,22 @@
|
|
|
6
6
|
"imagemin-merlin": "bin/image-guard.sh"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"globby": "^16.
|
|
9
|
+
"globby": "^16.2.2",
|
|
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": "^10.0.
|
|
16
|
+
"@eslint/js": "^10.0.1",
|
|
17
|
+
"eslint": "^10.7.0"
|
|
17
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/image-guard.js",
|
|
21
|
+
"bin/image-guard.sh",
|
|
22
|
+
"NOTICE.md",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
18
25
|
"funding": "https://github.com/j9t/image-guard?sponsor=1",
|
|
19
26
|
"homepage": "https://github.com/j9t/image-guard",
|
|
20
27
|
"keywords": [
|
|
@@ -46,5 +53,5 @@
|
|
|
46
53
|
"test": "node --test bin/image-guard.test.js"
|
|
47
54
|
},
|
|
48
55
|
"type": "module",
|
|
49
|
-
"version": "5.2.
|
|
56
|
+
"version": "5.2.6"
|
|
50
57
|
}
|
package/src/index.js
CHANGED
package/src/utils.js
CHANGED
|
@@ -283,6 +283,6 @@ const size = async (file) => {
|
|
|
283
283
|
return stats.size
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
const sizeReadable = (size) => `${(size / 1024).toFixed(2)} KB`
|
|
286
|
+
const sizeReadable = (size) => `${(size / 1024).toFixed(2)} KB` // eslint-disable-line no-irregular-whitespace
|
|
287
287
|
|
|
288
288
|
export const utils = { compression, conversion, sizeReadable }
|
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,48 +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.4] - 2026-03-04
|
|
8
|
-
|
|
9
|
-
### Fixed
|
|
10
|
-
|
|
11
|
-
* 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
|
|
12
|
-
- **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`.
|
|
13
|
-
|
|
14
|
-
## [5.2.3] - 2026-02-21
|
|
15
|
-
|
|
16
|
-
### Fixed
|
|
17
|
-
|
|
18
|
-
* 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
|
|
19
|
-
* Warned when a `.bak` file is left behind after a failed file replacement
|
|
20
|
-
* Removed a dead code branch in result processing
|
|
21
|
-
|
|
22
|
-
### Changed
|
|
23
|
-
|
|
24
|
-
* Moved to compression and conversion running in parallel (non-overlapping file sets), reducing wall-clock time when `--heic-to-avif` is active
|
|
25
|
-
* Reduced directory traversals from two to one in non-staged mode when `--heic-to-avif` is active
|
|
26
|
-
* Extracted shared `MAX_FILE_SIZE` constant; aligned `node:fs` and `node:path` import style in `utils.js`
|
|
27
|
-
* Extended test coverage for corrupt file detection and reporting
|
|
28
|
-
|
|
29
|
-
## [5.2.2] - 2026-02-17
|
|
30
|
-
|
|
31
|
-
### Fixed
|
|
32
|
-
|
|
33
|
-
* Updated HEIC dry run to report actual file sizes instead of omitting size data
|
|
34
|
-
|
|
35
|
-
## [5.2.1] - 2026-02-17
|
|
36
|
-
|
|
37
|
-
### Changed
|
|
38
|
-
|
|
39
|
-
* 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
|
|
40
|
-
* Removed redundant lossless compression pass after HEIC conversion
|
|
41
|
-
* Documented Display P3 to sRGB color space mapping during HEIC conversion
|
|
42
|
-
|
|
43
|
-
## [5.2.0] - 2026-02-17
|
|
44
|
-
|
|
45
|
-
### Added
|
|
46
|
-
|
|
47
|
-
* Added opt-in HEIC/HEIF-to-AVIF conversion via `--heic-to-avif` (with `--keep-heic` to preserve originals)
|
|
48
|
-
* 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,26 +0,0 @@
|
|
|
1
|
-
export default [
|
|
2
|
-
{
|
|
3
|
-
ignores: [
|
|
4
|
-
'media/',
|
|
5
|
-
'node_modules/**'
|
|
6
|
-
]
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
languageOptions: {
|
|
10
|
-
ecmaVersion: 'latest',
|
|
11
|
-
sourceType: 'module',
|
|
12
|
-
globals: {
|
|
13
|
-
console: 'readonly',
|
|
14
|
-
process: 'readonly',
|
|
15
|
-
setTimeout: 'readonly'
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
rules: {
|
|
19
|
-
'no-console': 'off', // CLI tool—allow console
|
|
20
|
-
'no-undef': 'error', // Catch undefined vars
|
|
21
|
-
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
22
|
-
eqeqeq: ['warn', 'smart'],
|
|
23
|
-
curly: ['warn', 'multi-line']
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
]
|
package/media/output.png
DELETED
|
Binary file
|