metalsmith-optimize-images 0.10.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,15 +6,15 @@ Metalsmith plugin for generating responsive images with optimal formats
6
6
  [![npm: version][npm-badge]][npm-url]
7
7
  [![license: MIT][license-badge]][license-url]
8
8
  [![test coverage][coverage-badge]][coverage-url]
9
- [![ESM/CommonJS][modules-badge]][npm-url]
10
- [![Known Vulnerabilities](https://snyk.io/test/npm/metalsmith-optimize-images/badge.svg)](https://snyk.io/test/npm/metalsmith-optimize-images)
9
+ [![ESM][modules-badge]][npm-url]
11
10
 
12
- > This Metalsmith plugin is under active development. The API is stable, but breaking changes may occur before reaching 1.0.0.
11
+ > **Breaking change in 0.12.0**: When the persistent cache is enabled (`cache: true`), this plugin must now run **before** `metalsmith-static-files` in the pipeline, not after. The plugin writes variants to the source tree and the static-files plugin copies them to the build. See [Usage](#usage) for details.
13
12
 
14
13
  ## Features
15
14
 
16
15
  - **Multiple image formats**: Generates AVIF and WebP variants with JPEG/PNG fallbacks
17
16
  - **Responsive sizes**: Creates different image sizes for various device widths
17
+ - **Persistent cache**: Writes variants to a source-tree directory so subsequent builds (and CI) skip Sharp entirely
18
18
  - **Background image support**: Automatically processes unused images for CSS `image-set()` backgrounds
19
19
  - **Progressive loading**: Optional progressive image loading with low-quality placeholders
20
20
  - **Lazy loading**: Uses native browser lazy loading
@@ -23,9 +23,6 @@ Metalsmith plugin for generating responsive images with optimal formats
23
23
  - **Parallel processing**: Processes images in parallel
24
24
  - **Metadata generation**: Creates a JSON manifest with image information and variants
25
25
  - **Configurable compression**: Customize compression settings per format
26
- - **ESM and CommonJS support**:
27
- - ESM: `import optimizeImages from 'metalsmith-optimize-images'`
28
- - CommonJS: `const optimizeImages = require('metalsmith-optimize-images')`
29
26
 
30
27
  ## Installation
31
28
 
@@ -33,47 +30,125 @@ Metalsmith plugin for generating responsive images with optimal formats
33
30
  npm install metalsmith-optimize-images
34
31
  ```
35
32
 
33
+ This package is **ESM-only** and requires Node >= 22. CommonJS projects on Node 22 can still load it, because Node 22 lets `require` import ESM packages.
34
+
36
35
  ## Usage
37
36
 
38
- > This plugin **must** be run after assets are copied but before any final HTML processing.
37
+ When the persistent cache is enabled the plugin should run **before** the static-files copy so that newly generated variants land in the cache directory and get picked up by the copy step. When the cache is disabled the plugin should run **after** assets are copied, since it needs the images to already be in the Metalsmith files object.
38
+
39
+ ### With persistent cache (recommended)
39
40
 
40
41
  ```javascript
41
42
  metalsmith
43
+ .use(
44
+ optimizeImages({
45
+ cache: true,
46
+ widths: [320, 640, 960, 1280, 1920],
47
+ formats: ['avif', 'webp', 'original']
48
+
49
+ })
50
+ )
42
51
  .use(
43
52
  assets({
44
- source: 'lib/assets/', // Where to find assets
45
- destination: 'assets/' // Where to copy assets
53
+ source: 'lib/assets/',
54
+ destination: 'assets/'
55
+ })
56
+ );
57
+ ```
58
+
59
+ ### Without cache (original behaviour)
60
+
61
+ ```javascript
62
+ metalsmith
63
+ .use(
64
+ assets({
65
+ source: 'lib/assets/',
66
+ destination: 'assets/'
46
67
  })
47
68
  )
48
69
  .use(
49
70
  optimizeImages({
50
- // configuration options
51
71
  widths: [320, 640, 960, 1280, 1920],
52
72
  formats: ['avif', 'webp', 'original']
53
73
  })
54
74
  );
55
75
  ```
56
76
 
77
+ ## Theory of Operation
78
+
79
+ Understanding the full lifecycle helps explain why the plugin is structured the way it is and how the cache eliminates redundant work.
80
+
81
+ ### The problem
82
+
83
+ Sharp-based image processing is expensive. A typical site with 20 source images, five responsive widths, and three output formats generates 300 variant files. On a cold build this can take 30 seconds or more. On a CI host like Netlify the build starts from a clean checkout every time, so without intervention that cost is paid on every deploy even when no images changed.
84
+
85
+ ### How the cache solves it
86
+
87
+ The plugin can persist generated variants into a directory inside the source tree, for example `lib/assets/images/responsive/`. Because this directory is committed to git, CI clones already contain every variant that was generated on a previous build. The plugin checks the cache before calling Sharp: if a variant file already exists it is read from disk instead. Only genuinely new or changed images trigger Sharp processing.
88
+
89
+ ### Build pipeline flow
90
+
91
+ The cache changes where the plugin sits in the Metalsmith pipeline. Without the cache the plugin runs after the static-files copy because it needs images to be in the Metalsmith files object. With the cache enabled the plugin runs **before** the static-files copy:
92
+
93
+ ```
94
+ Source images on disk (lib/assets/images/)
95
+
96
+
97
+ ┌──────────────────────┐
98
+ │ optimize-images │ Reads source images from disk via sourcePrefix.
99
+ │ (runs first) │ Checks cache dir for existing variants.
100
+ │ │ Generates missing variants with Sharp.
101
+ │ │ Writes new variants to cache dir.
102
+ │ │ Rewrites HTML: <img> → <picture>.
103
+ │ │ Does NOT add variants to files object.
104
+ └──────────────────────┘
105
+
106
+
107
+ ┌──────────────────────┐
108
+ │ metalsmith-static │ Copies lib/assets/ → build/assets/.
109
+ │ (runs second) │ This includes the responsive/ cache dir,
110
+ │ │ so all variants end up in the build.
111
+ └──────────────────────┘
112
+
113
+
114
+ Final build output
115
+ ```
116
+
117
+ ### Source image discovery
118
+
119
+ When the plugin runs before the static-files copy, source images are not yet in the Metalsmith files object. The plugin derives a `sourcePrefix` from the cache path to locate them on disk. For example, if `cache` resolves to `lib/assets/images/responsive` and `outputDir` is `assets/images/responsive`, the prefix is `lib/`. An HTML reference to `assets/images/hero.jpg` maps to `lib/assets/images/hero.jpg` on disk.
120
+
121
+ ### Cache invalidation
122
+
123
+ HTML images include a content hash in their filenames (e.g., `hero-640w-a1b2c3d4.webp`). When a source image changes, its hash changes, the expected filename differs from anything on disk, and the cache misses naturally. Old variants with the previous hash remain in the cache directory but are harmless — they simply stop being referenced in HTML.
124
+
125
+ Background images use deterministic filenames without hashes (e.g., `hero-960w.webp`) for easier CSS authoring. This means the cache cannot detect content changes for background images automatically. If a background source image changes content without changing its filename, delete the cache directory to force regeneration.
126
+
127
+ ### What gets committed to git
128
+
129
+ The cache directory (e.g., `lib/assets/images/responsive/`) should be committed to the repository. It contains only generated variant files — binary images that are a deterministic function of the source images and plugin configuration. Committing them trades repository size for build speed. A typical site adds 50-100 MB to the repo but saves 30+ seconds on every CI build.
130
+
57
131
  ## Options
58
132
 
59
- | Option | Type | Default | Description |
60
- | --------------------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------ |
61
- | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
62
- | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
63
- | `formatOptions` | `object` | See below | Format-specific compression settings |
64
- | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
65
- | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
66
- | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
67
- | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
68
- | `skipLarger` | `boolean` | `true` | Don't upscale images |
69
- | `lazy` | `boolean` | `true` | Use native lazy loading |
70
- | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
71
- | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
72
- | `concurrency` | `number` | `5` | Process N images at a time |
73
- | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file at `{outputDir}/responsive-images-manifest.json` |
74
- | `isProgressive` | `boolean` | `false` | Enable progressive image loading |
75
- | `placeholder` | `object` | See below | Placeholder image settings |
76
- | `processUnusedImages` | `boolean` | `true` | Process unused images for background use |
133
+ | Option | Type | Default | Description |
134
+ | --------------------- | ------------------ | ------------------------------------- | ------------------------------------------------------------------------------ |
135
+ | `cache` | `boolean\|string` | `false` | Persistent cache. `true` uses `lib/<outputDir>`, string sets a custom path |
136
+ | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
137
+ | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
138
+ | `formatOptions` | `object` | See below | Format-specific compression settings |
139
+ | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
140
+ | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
141
+ | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
142
+ | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
143
+ | `skipLarger` | `boolean` | `true` | Don't upscale images |
144
+ | `lazy` | `boolean` | `true` | Use native lazy loading |
145
+ | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
146
+ | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
147
+ | `concurrency` | `number` | `5` | Process N images at a time |
148
+ | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file at `{outputDir}/responsive-images-manifest.json` |
149
+ | `isProgressive` | `boolean` | `false` | Enable progressive image loading |
150
+ | `placeholder` | `object` | See below | Placeholder image settings |
151
+ | `processUnusedImages` | `boolean` | `true` | Process unused images for background use |
77
152
 
78
153
  ### Default Format Options
79
154
 
@@ -104,9 +179,9 @@ The plugin operates in two phases:
104
179
 
105
180
  **Phase 1: HTML-Referenced Images**
106
181
 
107
- 1. Scans HTML files for image tags
108
- 2. Processes each image to create multiple sizes and formats
109
- 3. Creates a content hash for each image for efficient caching
182
+ 1. Scans HTML files for image tags matching the configured selector
183
+ 2. Processes each image to create multiple sizes and formats using Sharp
184
+ 3. Creates a content hash for each image for cache-busting filenames
110
185
  4. Replaces `<img>` tags with responsive `<picture>` elements
111
186
  5. Adds width/height attributes to prevent layout shifts
112
187
  6. Implements native lazy loading for better performance
@@ -116,7 +191,8 @@ The plugin operates in two phases:
116
191
  1. Finds images that weren't processed in Phase 1
117
192
  2. Generates 1x/2x variants (half size and original size) for retina displays
118
193
  3. Creates all configured formats (AVIF, WebP, original)
119
- 4. Suitable for use with CSS `image-set()` for background images
194
+ 4. Uses deterministic filenames without hashes for easier CSS authoring
195
+ 5. Suitable for use with CSS `image-set()` for background images
120
196
 
121
197
  ### Progressive Mode (experimental)
122
198
 
@@ -138,6 +214,28 @@ When `isProgressive: true` is enabled:
138
214
  metalsmith.use(optimizeImages());
139
215
  ```
140
216
 
217
+ ### With persistent cache
218
+
219
+ ```javascript
220
+ metalsmith.use(
221
+ optimizeImages({
222
+ cache: true
223
+ })
224
+ );
225
+ ```
226
+
227
+ This writes variants to `lib/assets/images/responsive/` (derived from `lib/` + the default `outputDir`). Commit this directory to git so CI builds skip Sharp entirely.
228
+
229
+ ### Custom cache path
230
+
231
+ ```javascript
232
+ metalsmith.use(
233
+ optimizeImages({
234
+ cache: 'lib/assets/images/responsive'
235
+ })
236
+ );
237
+ ```
238
+
141
239
  ### Custom configuration
142
240
 
143
241
  ```javascript
@@ -203,28 +301,19 @@ Add the `data-no-responsive` attribute to any image you don't want processed:
203
301
 
204
302
  The plugin automatically processes raster images and skips vector graphics:
205
303
 
206
- ### Processed
304
+ ### Processed
207
305
  - **JPEG** (`.jpg`, `.jpeg`)
208
306
  - **PNG** (`.png`)
209
307
  - **GIF** (`.gif`)
210
308
  - **WebP** (`.webp`)
211
309
  - **AVIF** (`.avif`)
212
310
 
213
- ### Automatically Skipped
214
- - **SVG** (`.svg`) - Vector graphics don't need responsive raster variants
311
+ ### Automatically Skipped
312
+ - **SVG** (`.svg`) vector graphics that scale perfectly at any resolution
215
313
  - **External URLs** (http/https)
216
314
  - **Data URLs** (`data:image/...`)
217
315
  - **Images with `data-no-responsive` attribute**
218
316
 
219
- ### Why SVGs are skipped
220
-
221
- SVG files are vector graphics that scale perfectly at any resolution without quality loss. Creating multiple raster sizes and formats from SVGs would:
222
- - Increase build time unnecessarily
223
- - Generate larger file sizes than the original vector
224
- - Lose the scalability benefits of SVG format
225
-
226
- If you have SVG logos or icons, they will remain untouched and work perfectly as-is.
227
-
228
317
  ## Background Images
229
318
 
230
319
  The plugin automatically processes images that aren't referenced in HTML for use as CSS background images. This feature is enabled by default (`processUnusedImages: true`).
@@ -233,13 +322,13 @@ The plugin automatically processes images that aren't referenced in HTML for use
233
322
 
234
323
  After processing HTML-referenced images, the plugin:
235
324
 
236
- 1. **Scans the Metalsmith files object** for all images
237
- 2. **Excludes already-processed images** (those found during HTML scanning)
238
- 3. **Excludes responsive variants** (generated images in the outputDir)
239
- 4. **Generates 1x/2x variants** using actual image dimensions:
325
+ 1. Scans the Metalsmith files object and filesystem for all images
326
+ 2. Excludes already-processed images (those found during HTML scanning)
327
+ 3. Excludes responsive variants (generated images in the outputDir)
328
+ 4. Generates 1x/2x variants using actual image dimensions:
240
329
  - **1x variant**: Half the original size for regular displays
241
330
  - **2x variant**: Original image size for retina displays (sharper on high-DPI screens)
242
- 5. **Creates all formats** (AVIF, WebP, original) for optimal browser support
331
+ 5. Creates all formats (AVIF, WebP, original) for optimal browser support
243
332
 
244
333
  ### Using Background Images with CSS
245
334
 
@@ -254,7 +343,7 @@ assets/images/responsive/hero-960w.jpg (1x - half 960px width for regular dis
254
343
  assets/images/responsive/hero-1920w.jpg (2x - original 1920px width, sharper on retina)
255
344
  ```
256
345
 
257
- **Note**: Background images are generated **without hashes** for easier CSS authoring. HTML images still include hashes for cache-busting.
346
+ Background images are generated without hashes for easier CSS authoring. HTML images still include hashes for cache-busting.
258
347
 
259
348
  Use them in CSS with `image-set()`:
260
349
 
@@ -278,36 +367,24 @@ Use them in CSS with `image-set()`:
278
367
  ```javascript
279
368
  metalsmith.use(
280
369
  optimizeImages({
281
- // Standard HTML image processing
282
370
  widths: [320, 640, 960, 1280, 1920],
283
371
  formats: ['avif', 'webp', 'original'],
284
-
285
- // Background image processing
286
- processUnusedImages: true, // Enable background processing
287
-
288
- // Generate metadata to see all variants
372
+ processUnusedImages: true,
289
373
  generateMetadata: true
290
374
  })
291
375
  );
292
376
  ```
293
377
 
294
- ### Benefits of Background Image Processing
295
-
296
- - **Automatic format optimization** - Browser selects best supported format
297
- - **Retina display support** - 2x variants provide crisp images on high-DPI screens
298
- - **No manual work** - Plugin automatically finds and processes unused images in Metalsmith files object
299
- - **Consistent workflow** - Same formats and quality settings as HTML images
300
-
301
378
  ## Progressive Loading
302
379
 
303
380
  ### Overview
304
381
 
305
382
  Progressive loading provides a smooth user experience by:
306
383
 
307
- 1. **Immediate display**: Shows a low-quality placeholder instantly
308
- 2. **Smooth transitions**: Fades from placeholder to high-quality image
309
- 3. **Lazy loading**: Only loads high-resolution images when they enter the viewport
310
- 4. **Format optimization**: Automatically serves the best supported format
384
+ 1. Showing a low-quality placeholder instantly
385
+ 2. Fading from placeholder to high-quality image
386
+ 3. Only loading high-resolution images when they enter the viewport
387
+ 4. Automatically serving the best supported format
311
388
 
312
389
  ### Implementation
313
390
 
@@ -448,6 +525,10 @@ metalsmith.env('DEBUG', 'metalsmith-optimize-images*');
448
525
  }
449
526
  ```
450
527
 
528
+ ## Test Coverage
529
+
530
+ 88 tests covering all major functionality including unit tests for utilities, integration tests with real Metalsmith instances, cache persistence tests, and edge case coverage.
531
+
451
532
  ## License
452
533
 
453
534
  MIT
@@ -468,6 +549,6 @@ All AI-assisted code has been reviewed and tested to ensure it meets project sta
468
549
  [metalsmith-url]: https://metalsmith.io
469
550
  [license-badge]: https://img.shields.io/github/license/wernerglinka/metalsmith-optimize-images
470
551
  [license-url]: LICENSE
471
- [coverage-badge]: https://img.shields.io/badge/test%20coverage-95%25-brightgreen
552
+ [coverage-badge]: https://img.shields.io/badge/test%20coverage-93%25-brightgreen
472
553
  [coverage-url]: https://github.com/wernerglinka/metalsmith-optimize-images/actions/workflows/test.yml
473
- [modules-badge]: https://img.shields.io/badge/modules-ESM%2FCJS-blue
554
+ [modules-badge]: https://img.shields.io/badge/module-ESM-blue
package/package.json CHANGED
@@ -1,38 +1,28 @@
1
1
  {
2
2
  "name": "metalsmith-optimize-images",
3
- "version": "0.10.3",
3
+ "version": "1.0.0",
4
4
  "description": "Metalsmith plugin for generating responsive images with optimal formats",
5
5
  "type": "module",
6
- "main": "./lib/index.cjs",
7
- "module": "./lib/index.js",
8
- "exports": {
9
- "import": "./lib/index.js",
10
- "require": "./lib/index.cjs",
11
- "default": "./lib/index.js"
12
- },
6
+ "exports": "./src/index.js",
13
7
  "engines": {
14
- "node": ">= 18.0.0"
8
+ "node": ">= 22.0.0"
15
9
  },
16
10
  "scripts": {
17
- "build": "microbundle --entry src/index.js --output lib/index.js --target node -f esm,cjs --strict --generateTypes=false",
18
11
  "changelog": "auto-changelog -u --commit-limit false --ignore-commit-pattern '^((dev|chore|ci):|Release)'",
19
- "coverage": "c8 --include=src/**/*.js --reporter=lcov --reporter=text-summary mocha 'test/index.js' 'test/cjs.test.cjs' 'test/unit/**/*.js' -t 15000",
20
- "format": "prettier --write \"**/*.{yml,md,js,json}\"",
21
- "format:check": "prettier --list-different \"**/*.{yml,md,js,json}\"",
22
- "lint": "eslint --fix .",
23
- "lint:check": "eslint --fix-dry-run .",
24
- "prepublishOnly": "npm run build",
12
+ "test": "node --test --test-timeout=60000 'test/**/*.test.js'",
13
+ "coverage": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-timeout=60000 'test/**/*.test.js'",
14
+ "test:coverage": "npm run coverage",
15
+ "format": "biome format --write .",
16
+ "format:check": "biome format .",
17
+ "lint": "biome check --write .",
18
+ "lint:check": "biome check .",
19
+ "pre-release": "npm run lint:check && npm run format:check && npm test && npx metalsmith-plugin-mcp-server validate .",
25
20
  "release:patch": "./scripts/release.sh patch --ci",
26
21
  "release:minor": "./scripts/release.sh minor --ci",
27
22
  "release:major": "./scripts/release.sh major --ci",
28
- "release:check": "npm run lint:check && ./scripts/release.sh patch --dry-run",
29
- "test": "c8 --include=src/**/*.js mocha 'test/index.js' 'test/cjs.test.cjs' 'test/unit/**/*.js' -t 15000",
30
- "test:coverage": "c8 npm test",
31
- "test:unit": "mocha 'test/unit/**/*.js' -t 5000",
32
- "test:esm": "c8 --include=src/**/*.js mocha test/index.js -t 15000",
33
- "test:cjs": "c8 --include=src/**/*.js mocha test/cjs.test.cjs -t 15000",
34
- "audit": "npm audit",
35
- "depcheck": "depcheck"
23
+ "release:check": "npm run lint:check && release-it --dry-run",
24
+ "depcheck": "depcheck",
25
+ "audit": "npm audit"
36
26
  },
37
27
  "repository": {
38
28
  "type": "git",
@@ -48,7 +38,8 @@
48
38
  "avif"
49
39
  ],
50
40
  "files": [
51
- "lib",
41
+ "src/**/*.js",
42
+ "LICENSE",
52
43
  "README.md"
53
44
  ],
54
45
  "author": "Werner Glinka <werner@glinka.co>",
@@ -58,28 +49,18 @@
58
49
  },
59
50
  "homepage": "https://github.com/wernerglinka/metalsmith-optimize-images",
60
51
  "dependencies": {
61
- "cheerio": "^1.1.2",
52
+ "cheerio": "^1.2.0",
62
53
  "mkdirp": "^3.0.1",
63
54
  "sharp": "^0.34.5"
64
55
  },
65
56
  "devDependencies": {
57
+ "@biomejs/biome": "^2.4.0",
66
58
  "auto-changelog": "^2.5.0",
67
- "c8": "^10.1.3",
68
59
  "depcheck": "^1.4.7",
69
- "eslint": "^9.39.2",
70
- "eslint-config-prettier": "^10.1.8",
71
- "eslint-plugin-prettier": "^5.5.5",
72
- "metalsmith": "^2.6.3",
73
- "microbundle": "^0.15.1",
74
- "mocha": "^11.7.5",
75
- "prettier": "^3.8.0",
76
- "release-it": "^19.2.3"
77
- },
78
- "peerDependencies": {
79
- "metalsmith": "^2.5.0"
60
+ "metalsmith": "^2.5.0",
61
+ "release-it": "^20.0.0"
80
62
  },
81
63
  "publishConfig": {
82
64
  "access": "public"
83
- },
84
- "// Note": "The warning about deprecated lodash.get comes from release-it, which is only a dev dependency"
65
+ }
85
66
  }