@localnerve/gulp-images 0.1.1
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/LICENSE.md +660 -0
- package/README.md +150 -0
- package/package.json +56 -0
- package/src/index.js +20 -0
- package/src/optimize/index.js +14 -0
- package/src/optimize/jpeg.js +48 -0
- package/src/optimize/png.js +48 -0
- package/src/optimize/svg.js +49 -0
- package/src/responsive/index.js +65 -0
- package/src/transform/index.js +12 -0
- package/src/transform/toWebp.js +86 -0
- package/src/utils.js +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @localnerve/gulp-images
|
|
2
|
+
|
|
3
|
+
> Portable (wasm & sharp), minimal dependency, streaming image processing for javascript builds. Optionally outputs image processing metadata to allow the images themselves to drive css generation and/or other work.
|
|
4
|
+
|
|
5
|
+
Reusable Gulp image-processing transforms extracted from [jam-build](https://github.com/localnerve/jam-build).
|
|
6
|
+
|
|
7
|
+
Three functional groups — **optimize**, **responsive**, and **transform** — each independently importable and easily extensible.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @localnerve/gulp-images
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires `gulp` as a peer dependency.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Initialize WASM codecs first
|
|
24
|
+
|
|
25
|
+
All JPEG, PNG, and WebP operations rely on WASM modules that must be initialized before any pipeline runs.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { initWasmModules } from '@localnerve/gulp-images';
|
|
29
|
+
|
|
30
|
+
// Use process.cwd() (default) or pass an explicit base path:
|
|
31
|
+
await initWasmModules(); // looks for node_modules relative to cwd
|
|
32
|
+
await initWasmModules('/path/to/monorepo/root'); // explicit base path
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Full pipeline example
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import gulp from 'gulp';
|
|
39
|
+
import { initWasmModules, optimize, responsive, transform } from '@localnerve/gulp-images';
|
|
40
|
+
|
|
41
|
+
await initWasmModules();
|
|
42
|
+
|
|
43
|
+
const settings = {
|
|
44
|
+
prod: true,
|
|
45
|
+
svgoOptions: { /* svgo options */ },
|
|
46
|
+
mozjpegOptions: { quality: 80 },
|
|
47
|
+
oxipngOptions: { level: 2 },
|
|
48
|
+
webpOptions: { quality: 80 },
|
|
49
|
+
responsiveConfig: { /* gulp-responsive config */ }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const output = {}; // optional — receives image metadata from responsive + toWebp
|
|
53
|
+
|
|
54
|
+
gulp.series(
|
|
55
|
+
function optimizeAndResponsive () {
|
|
56
|
+
return gulp.src('dist/images/**', { encoding: false })
|
|
57
|
+
.pipe(responsive.responsive(settings, output))
|
|
58
|
+
.pipe(optimize.svg(settings))
|
|
59
|
+
.pipe(optimize.jpeg(settings))
|
|
60
|
+
.pipe(optimize.png(settings))
|
|
61
|
+
.pipe(gulp.dest('dist/images'));
|
|
62
|
+
},
|
|
63
|
+
function convertToWebp () {
|
|
64
|
+
return gulp.src('dist/images/**', { encoding: false })
|
|
65
|
+
.pipe(transform.toWebp(settings, output))
|
|
66
|
+
.pipe(gulp.dest('dist/images'));
|
|
67
|
+
}
|
|
68
|
+
)();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Groups
|
|
74
|
+
|
|
75
|
+
### `optimize` — `@localnerve/gulp-images/optimize`
|
|
76
|
+
|
|
77
|
+
| Export | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `svg(settings)` | Optimizes SVG files via [svgo](https://github.com/svg/svgo). No-op unless `settings.prod === true`. |
|
|
80
|
+
| `jpeg(settings)` | Re-encodes JPEGs via mozjpeg ([`@jsquash/jpeg`](https://github.com/jamsinclair/jSquash)). No-op unless `settings.prod === true`. |
|
|
81
|
+
| `png(settings)` | Optimizes PNGs via oxipng ([`@jsquash/oxipng`](https://github.com/jamsinclair/jSquash)). No-op unless `settings.prod === true`. |
|
|
82
|
+
|
|
83
|
+
**Extend the group:**
|
|
84
|
+
```js
|
|
85
|
+
import * as optimize from '@localnerve/gulp-images/optimize';
|
|
86
|
+
const myOptimize = { ...optimize, avif: myAvifOptimizer };
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**`settings` keys used by optimize:**
|
|
90
|
+
|
|
91
|
+
| Key | Type | Used by |
|
|
92
|
+
|-----|------|---------|
|
|
93
|
+
| `prod` | `boolean` | all — enables optimization |
|
|
94
|
+
| `svgoOptions` | `object` | `svg` |
|
|
95
|
+
| `mozjpegOptions` | `object` | `jpeg` |
|
|
96
|
+
| `oxipngOptions` | `object` | `png` |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### `responsive` — `@localnerve/gulp-images/responsive`
|
|
101
|
+
|
|
102
|
+
| Export | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `responsive(settings, output?)` | Generates responsive image variants via [`@localnerve/gulp-responsive`](https://github.com/localnerve/gulp-responsive). |
|
|
105
|
+
|
|
106
|
+
`settings` - The @localnerve/gulp-responsive config defined in the [full configuration details](https://github.com/localnerve/gulp-responsive/blob/public-package/README.md#config).
|
|
107
|
+
|
|
108
|
+
`output` (optional) — if supplied, variant metadata is written as:
|
|
109
|
+
```js
|
|
110
|
+
output[originalName][width] = { basename, mimeType }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**`settings` keys used:**
|
|
114
|
+
|
|
115
|
+
| Key | Type | Description |
|
|
116
|
+
|-----|------|-------------|
|
|
117
|
+
| `responsiveConfig` | `object` | Forwarded directly to `gulp-responsive` |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `transform` — `@localnerve/gulp-images/transform`
|
|
122
|
+
|
|
123
|
+
| Export | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `toWebp(settings, output?)` | Converts `.jpg`, `.jpeg`, and `.png` files to `.webp` using [`@jsquash/webp`](https://github.com/jamsinclair/jSquash). |
|
|
126
|
+
|
|
127
|
+
`output` (optional) — if supplied, converted file metadata is updated:
|
|
128
|
+
```js
|
|
129
|
+
output[key][width].basename = 'file.webp';
|
|
130
|
+
output[key][width].mimeType = 'image/webp';
|
|
131
|
+
```
|
|
132
|
+
The `key` and `width` are derived from the filename convention `<key1>-<key2>-<width>.<ext>` (same convention as `gulp-responsive` output).
|
|
133
|
+
|
|
134
|
+
**Extend the group:**
|
|
135
|
+
```js
|
|
136
|
+
import * as transform from '@localnerve/gulp-images/transform';
|
|
137
|
+
const myTransform = { ...transform, toAvif: myAvifConverter };
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`settings` keys used:**
|
|
141
|
+
|
|
142
|
+
| Key | Type | Description |
|
|
143
|
+
|-----|------|-------------|
|
|
144
|
+
| `webpOptions` | `object` | Forwarded to the WebP encoder |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
AGPL-3.0-or-later — Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@localnerve/gulp-images",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Reusable gulp image processing transforms: optimize (svg, jpeg, png), responsive, and transform (toWebp).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "eslint src/ test/",
|
|
8
|
+
"test": "node --test --experimental-test-coverage test/*.test.js",
|
|
9
|
+
"test:fixture:generate": "node test/fixtures/generate.js",
|
|
10
|
+
"check": "npm run lint && npm run test"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.js",
|
|
14
|
+
"./optimize": "./src/optimize/index.js",
|
|
15
|
+
"./responsive": "./src/responsive/index.js",
|
|
16
|
+
"./transform": "./src/transform/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"gulp",
|
|
23
|
+
"images",
|
|
24
|
+
"optimize",
|
|
25
|
+
"responsive",
|
|
26
|
+
"webp",
|
|
27
|
+
"svg",
|
|
28
|
+
"jpeg",
|
|
29
|
+
"png",
|
|
30
|
+
"wasm"
|
|
31
|
+
],
|
|
32
|
+
"author": "Alex Grant <info@localnerve.com> (https://www.localnerve.com)",
|
|
33
|
+
"license": "AGPL-3.0-or-later",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"gulp": ">=5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@jsquash/jpeg": "^1.4.0",
|
|
39
|
+
"@jsquash/oxipng": "^2.2.0",
|
|
40
|
+
"@jsquash/png": "^3.1.0",
|
|
41
|
+
"@jsquash/webp": "^1.5.0",
|
|
42
|
+
"@localnerve/gulp-responsive": ">=1.0.0",
|
|
43
|
+
"plugin-error": "^2.0.1",
|
|
44
|
+
"svgo": "^3.3.2",
|
|
45
|
+
"wasm-feature-detect": "^1.8.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
52
|
+
"eslint": "^10.0.3",
|
|
53
|
+
"gulp": "^5.0.1",
|
|
54
|
+
"vinyl": "^3.0.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — top-level entry point
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*
|
|
7
|
+
* Re-exports all three functional groups and the shared WASM initializer.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { initWasmModules, optimize, responsive, transform } from '@localnerve/gulp-images';
|
|
11
|
+
*
|
|
12
|
+
* Or import individual groups directly:
|
|
13
|
+
* import { svg, jpeg, png } from '@localnerve/gulp-images/optimize';
|
|
14
|
+
* import { responsive } from '@localnerve/gulp-images/responsive';
|
|
15
|
+
* import { toWebp } from '@localnerve/gulp-images/transform';
|
|
16
|
+
*/
|
|
17
|
+
export * as optimize from './optimize/index.js';
|
|
18
|
+
export * as responsive from './responsive/index.js';
|
|
19
|
+
export * as transform from './transform/index.js';
|
|
20
|
+
export { initWasmModules } from './utils.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — optimize group entry point
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*
|
|
7
|
+
* Exports the built-in optimizers. Callers can extend this group by spreading:
|
|
8
|
+
*
|
|
9
|
+
* import * as optimize from '@localnerve/gulp-images/optimize';
|
|
10
|
+
* const myOptimize = { ...optimize, avif: myAvifFn };
|
|
11
|
+
*/
|
|
12
|
+
export { svg } from './svg.js';
|
|
13
|
+
export { jpeg } from './jpeg.js';
|
|
14
|
+
export { png } from './png.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — optimize/jpeg
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { decodeJpeg, encodeJpeg, checkSkip, log, handleError, passThrough } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
const pluginName = '@localnerve/optimize-jpeg';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gulp Transform that losslessly re-encodes JPEG files using mozjpeg (via @jsquash/jpeg).
|
|
14
|
+
* When `settings.prod` is false, files pass through unchanged.
|
|
15
|
+
*
|
|
16
|
+
* mozjpeg options reference:
|
|
17
|
+
* https://github.com/jamsinclair/jSquash/blob/main/packages/jpeg/meta.ts
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} settings - Build settings
|
|
20
|
+
* @param {Object} [settings.mozjpegOptions] - Options forwarded to the mozjpeg encoder
|
|
21
|
+
* @param {boolean} settings.prod - Enable optimization only when true
|
|
22
|
+
* @returns {Transform} A Node.js Transform stream in object mode
|
|
23
|
+
*/
|
|
24
|
+
export function jpeg (settings) {
|
|
25
|
+
const { prod, mozjpegOptions } = settings;
|
|
26
|
+
|
|
27
|
+
if (prod) {
|
|
28
|
+
return new Transform({
|
|
29
|
+
objectMode: true,
|
|
30
|
+
transform: async (file, encoding, next) => {
|
|
31
|
+
if (checkSkip(file, ['.jpg', '.jpeg'])) { return next(null, file); }
|
|
32
|
+
if (file.isBuffer()) {
|
|
33
|
+
try {
|
|
34
|
+
const imageData = await decodeJpeg(file.contents);
|
|
35
|
+
file.contents = Buffer.from(await encodeJpeg(imageData, mozjpegOptions));
|
|
36
|
+
|
|
37
|
+
log(pluginName, file, `${file.extname.slice(1)} optimized`);
|
|
38
|
+
next(null, file);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
handleError(pluginName, file, next, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return passThrough();
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — optimize/png
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { decodePng, encodePng, checkSkip, log, handleError, passThrough } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
const pluginName = '@localnerve/optimize-png';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gulp Transform that optimizes PNG files using oxipng (via @jsquash/oxipng).
|
|
14
|
+
* When `settings.prod` is false, files pass through unchanged.
|
|
15
|
+
*
|
|
16
|
+
* oxipng options reference:
|
|
17
|
+
* https://github.com/jamsinclair/jSquash/blob/main/packages/oxipng/meta.ts
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} settings - Build settings
|
|
20
|
+
* @param {Object} [settings.oxipngOptions] - Options forwarded to the oxipng optimizer
|
|
21
|
+
* @param {boolean} settings.prod - Enable optimization only when true
|
|
22
|
+
* @returns {Transform} A Node.js Transform stream in object mode
|
|
23
|
+
*/
|
|
24
|
+
export function png (settings) {
|
|
25
|
+
const { prod, oxipngOptions } = settings;
|
|
26
|
+
|
|
27
|
+
if (prod) {
|
|
28
|
+
return new Transform({
|
|
29
|
+
objectMode: true,
|
|
30
|
+
transform: async (file, encoding, next) => {
|
|
31
|
+
if (checkSkip(file, ['.png'])) { return next(null, file); }
|
|
32
|
+
if (file.isBuffer()) {
|
|
33
|
+
try {
|
|
34
|
+
const imageData = await decodePng(file.contents);
|
|
35
|
+
file.contents = Buffer.from(await encodePng(imageData, oxipngOptions));
|
|
36
|
+
|
|
37
|
+
log(pluginName, file, `${file.extname.slice(1)} optimized`);
|
|
38
|
+
next(null, file);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
handleError(pluginName, file, next, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return passThrough();
|
|
48
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — optimize/svg
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { optimize as svgOptimize } from 'svgo';
|
|
9
|
+
import { checkSkip, log, handleError, passThrough } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
const pluginName = '@localnerve/optimize-svg';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gulp Transform that optimizes SVG files using svgo.
|
|
15
|
+
* When `settings.prod` is false (non-production), files pass through unchanged.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} settings - Build settings
|
|
18
|
+
* @param {Object} [settings.svgoOptions] - Options forwarded to svgo `optimize()`
|
|
19
|
+
* @param {boolean} settings.prod - Enable optimization only when true
|
|
20
|
+
* @returns {Transform} A Node.js Transform stream in object mode
|
|
21
|
+
*/
|
|
22
|
+
export function svg (settings) {
|
|
23
|
+
const { prod, svgoOptions } = settings;
|
|
24
|
+
|
|
25
|
+
if (prod) {
|
|
26
|
+
return new Transform({
|
|
27
|
+
objectMode: true,
|
|
28
|
+
transform: async (file, encoding, next) => {
|
|
29
|
+
if (checkSkip(file, ['.svg'])) { return next(null, file); }
|
|
30
|
+
if (file.isBuffer()) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await svgOptimize(file.contents.toString('utf8'), {
|
|
33
|
+
...svgoOptions,
|
|
34
|
+
path: file.path
|
|
35
|
+
});
|
|
36
|
+
file.contents = Buffer.from(result.data);
|
|
37
|
+
|
|
38
|
+
log(pluginName, file, 'svg optimized');
|
|
39
|
+
next(null, file);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
handleError(pluginName, file, next, error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return passThrough();
|
|
49
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — responsive
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { Transform } from 'node:stream';
|
|
9
|
+
import gulpResponsive from '@localnerve/gulp-responsive';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gulp Transform that generates responsive image variants using @localnerve/gulp-responsive.
|
|
13
|
+
*
|
|
14
|
+
* When `settings.responsiveConfig` is non-empty the transform produces resized/reformatted
|
|
15
|
+
* derivatives and—if `output` is provided—records each variant's metadata into that object
|
|
16
|
+
* (keyed by original filename without extension, then by target width):
|
|
17
|
+
*
|
|
18
|
+
* output[name][width] = { basename, mimeType }
|
|
19
|
+
*
|
|
20
|
+
* When `settings.responsiveConfig` is empty the transform is a no-op pass-through.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} settings - Build settings
|
|
23
|
+
* @param {Object} settings.responsiveConfig - Config object forwarded to gulp-responsive
|
|
24
|
+
* @param {Object} [output] - Optional object to receive image metadata.
|
|
25
|
+
* Treated the same as `data.images` in the original implementation.
|
|
26
|
+
* @returns {Transform} A Node.js Transform stream in object mode
|
|
27
|
+
*/
|
|
28
|
+
export function responsive (settings, output) {
|
|
29
|
+
const { responsiveConfig } = settings;
|
|
30
|
+
|
|
31
|
+
if (Object.keys(responsiveConfig).length > 0) {
|
|
32
|
+
const mimeTypes = {
|
|
33
|
+
'.avif': 'image/avif',
|
|
34
|
+
'.jpg': 'image/jpeg',
|
|
35
|
+
'.jpeg': 'image/jpeg',
|
|
36
|
+
'.gif': 'image/gif',
|
|
37
|
+
'.png': 'image/png',
|
|
38
|
+
'.svg': 'image/svg+xml',
|
|
39
|
+
'.webp': 'image/webp'
|
|
40
|
+
// add here as needed
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return gulpResponsive(responsiveConfig, {
|
|
44
|
+
errorOnUnusedConfig: false,
|
|
45
|
+
errorOnUnusedImage: false,
|
|
46
|
+
passThroughUnused: true,
|
|
47
|
+
postprocess: (originalFile, config, newFile) => {
|
|
48
|
+
if (!output) { return; }
|
|
49
|
+
const key = path.parse(originalFile.relative).name;
|
|
50
|
+
if (!output[key]) {
|
|
51
|
+
output[key] = {};
|
|
52
|
+
}
|
|
53
|
+
output[key][config.width] = {
|
|
54
|
+
basename: newFile.basename,
|
|
55
|
+
mimeType: mimeTypes[newFile.extname]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Transform({
|
|
62
|
+
objectMode: true,
|
|
63
|
+
transform: (file, enc, next) => next(null, file)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — transform group entry point
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*
|
|
7
|
+
* Exports the built-in image transforms. Callers can extend this group by spreading:
|
|
8
|
+
*
|
|
9
|
+
* import * as transform from '@localnerve/gulp-images/transform';
|
|
10
|
+
* const myTransform = { ...transform, toAvif: myAvifFn };
|
|
11
|
+
*/
|
|
12
|
+
export { toWebp } from './toWebp.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — transform/toWebp
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { Transform } from 'node:stream';
|
|
9
|
+
import { decodeJpeg, decodePng, encodeWebp, checkSkip, log, handleError } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
const pluginName = '@localnerve/to-webp';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gulp Transform that converts raster images (jpg/jpeg/png) to WebP.
|
|
15
|
+
*
|
|
16
|
+
* Converted files have their extension replaced with `.webp`. When the
|
|
17
|
+
* filename follows the `<key>-<sub>-<width>` naming convention used by
|
|
18
|
+
* gulp-responsive output AND `output` is provided, the transform also
|
|
19
|
+
* updates `output[key][width]` with the new basename and mimeType—mirroring
|
|
20
|
+
* the `data.images` updates in the original implementation.
|
|
21
|
+
*
|
|
22
|
+
* webp encoder options reference:
|
|
23
|
+
* https://github.com/jamsinclair/jSquash/blob/main/packages/webp/meta.ts
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} settings - Build settings
|
|
26
|
+
* @param {Object} [settings.webpOptions] - Options forwarded to the WebP encoder
|
|
27
|
+
* @param {Object} [output] - Optional object to receive updated image metadata
|
|
28
|
+
* (same shape written by `responsive`): output[key][width] = { basename, mimeType }
|
|
29
|
+
* @returns {Transform} A Node.js Transform stream in object mode
|
|
30
|
+
*/
|
|
31
|
+
export function toWebp(settings, output) {
|
|
32
|
+
const { webpOptions } = settings;
|
|
33
|
+
const exts = ['.jpg', '.jpeg', '.png'];
|
|
34
|
+
const decoders = [decodeJpeg, decodeJpeg, decodePng];
|
|
35
|
+
|
|
36
|
+
return new Transform({
|
|
37
|
+
objectMode: true,
|
|
38
|
+
transform: async (file, encoding, next) => {
|
|
39
|
+
if (checkSkip(file, exts)) {
|
|
40
|
+
return next(null, file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (file.isBuffer()) {
|
|
44
|
+
try {
|
|
45
|
+
const decoderIndex = exts.indexOf(file.extname.toLowerCase());
|
|
46
|
+
const originalFile = file.path;
|
|
47
|
+
const originalExt = file.extname;
|
|
48
|
+
|
|
49
|
+
// derive metadata keys from filename convention: <key-parts>-<width>
|
|
50
|
+
const name = path.parse(file.relative).name;
|
|
51
|
+
const nameParts = name.split('-');
|
|
52
|
+
const key = nameParts.slice(0, 2).join('-');
|
|
53
|
+
const width = nameParts.slice(2, 3)[0];
|
|
54
|
+
|
|
55
|
+
const imageData = await decoders[decoderIndex](file.contents);
|
|
56
|
+
file.contents = Buffer.from(await encodeWebp(imageData, webpOptions));
|
|
57
|
+
|
|
58
|
+
for (const ext of exts) {
|
|
59
|
+
file.path = file.path.replace(ext, '.webp');
|
|
60
|
+
|
|
61
|
+
if (originalFile !== file.path) {
|
|
62
|
+
// update caller-supplied output metadata if provided
|
|
63
|
+
if (output) {
|
|
64
|
+
const val = output?.[key]?.[width];
|
|
65
|
+
if (val) {
|
|
66
|
+
val.basename = file.basename;
|
|
67
|
+
val.mimeType = 'image/webp';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break; // a file won't be both .jpg and .jpeg
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (file.stat) {
|
|
75
|
+
file.stat.atime = file.stat.mtime = file.stat.ctime = new Date();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log(pluginName, file, `${originalExt.slice(1)} converted to webp`);
|
|
79
|
+
next(null, file);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
handleError(pluginName, file, next, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|