@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/src/utils.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @localnerve/gulp-images — shared utilities.
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 fs from 'node:fs/promises';
9
+ import { Transform } from 'node:stream';
10
+ import PluginError from 'plugin-error';
11
+ import { simd, relaxedSimd } from 'wasm-feature-detect';
12
+ import decodeJpeg, { init as initJpegDecode } from '@jsquash/jpeg/decode.js';
13
+ import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js';
14
+ import decodePng, { init as initPngDecode } from '@jsquash/png/decode.js';
15
+ import encodePng, { init as initPngEncode } from '@jsquash/oxipng/optimise.js';
16
+ import encodeWebp, { init as initWebpEncode } from '@jsquash/webp/encode.js';
17
+
18
+ export { decodeJpeg, encodeJpeg, decodePng, encodePng, encodeWebp };
19
+
20
+ /**
21
+ * Check skip condition for a vinyl stream object.
22
+ * Skip if: not the right extension, empty file, or a stream/null.
23
+ *
24
+ * @param {import('vinyl')} file - Vinyl file object passing through
25
+ * @param {string[]} exts - Array of dot-prefixed extensions to allow, e.g. ['.jpg']
26
+ * @returns {boolean} true if the file should be skipped
27
+ */
28
+ export function checkSkip(file, exts) {
29
+ return (
30
+ !exts.includes(file.extname.toLowerCase()) ||
31
+ !file.contents.toString('utf8') ||
32
+ file.isStream() ||
33
+ file.isNull()
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Colorized console logger.
39
+ *
40
+ * @param {string} owner - The plugin/function name
41
+ * @param {import('vinyl')} file - Vinyl file object
42
+ * @param {string} message - The log message
43
+ * @param {'log'|'error'} [method='log'] - console method to use
44
+ */
45
+ export function log(owner, file, message, method = 'log') {
46
+ const colors = {
47
+ magenta: '\x1b[35m',
48
+ yellow: '\x1b[33m',
49
+ red: '\x1b[31m',
50
+ green: '\x1b[32m',
51
+ reset: '\x1b[0m'
52
+ };
53
+ const filepath = path.relative(process.cwd(), file.path);
54
+ const now = new Date();
55
+ const TN = i => i < 10 ? `0${i}` : i;
56
+ const timestring = `${TN(now.getHours())}:${TN(now.getMinutes())}:${TN(now.getSeconds())}`;
57
+
58
+ console[method](
59
+ `[${colors.magenta}${timestring}${colors.reset}] \n${owner}: ${method === 'log' ? colors.green : colors.red}File ${filepath} - ${colors.yellow}${message}${colors.reset}`
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Handle an error inside a gulp transform stream.
65
+ *
66
+ * @param {string} owner - The plugin/function name
67
+ * @param {import('vinyl')} file - Vinyl file object
68
+ * @param {Function} next - The transform callback
69
+ * @param {Error|string} error - The error
70
+ */
71
+ export function handleError(owner, file, next, error) {
72
+ const colors = { reset: '\x1b[0m' };
73
+ const filepath = path.relative(process.cwd(), file.path);
74
+ let message = error.message || error;
75
+
76
+ if (message) {
77
+ message = message
78
+ .replace('Line:', `${colors.reset}File: ${filepath}\nLine:`)
79
+ .replace(/\n/g, '\n\t')
80
+ .trim();
81
+ log(owner, file, message, 'error');
82
+ }
83
+
84
+ next(new PluginError(owner, message));
85
+ }
86
+
87
+ /**
88
+ * Pass-through transform (no-op).
89
+ * Returned by optimize functions when not in production mode.
90
+ *
91
+ * @returns {Transform}
92
+ */
93
+ export function passThrough() {
94
+ return new Transform({
95
+ objectMode: true,
96
+ transform: (file, enc, next) => next(null, file)
97
+ });
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // WASM codec paths relative to a base directory
102
+ // ---------------------------------------------------------------------------
103
+ const WASM_PATHS = {
104
+ jpegDecode: 'node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm',
105
+ jpegEncode: 'node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm',
106
+ pngDecode: 'node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm',
107
+ pngEncode: 'node_modules/@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm',
108
+ webpEncode: 'node_modules/@jsquash/webp/codec/enc/webp_enc.wasm',
109
+ webpEncodeSIMD: 'node_modules/@jsquash/webp/codec/enc/webp_enc_simd.wasm'
110
+ };
111
+
112
+ /**
113
+ * Initialize all WASM codec modules.
114
+ * Call this once before running any image transforms.
115
+ *
116
+ * @param {string} [wasmBasePath] - Absolute path to the directory that contains
117
+ * the `node_modules` folder where the @jsquash packages are installed.
118
+ * Defaults to `process.cwd()`.
119
+ */
120
+ export async function initWasmModules(wasmBasePath = process.cwd()) {
121
+ const resolve = rel => path.join(wasmBasePath, rel);
122
+
123
+ const jpegDecWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.jpegDecode)));
124
+ await initJpegDecode(jpegDecWasmModule);
125
+
126
+ const jpegEncWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.jpegEncode)));
127
+ await initJpegEncode(jpegEncWasmModule);
128
+
129
+ const pngDecWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.pngDecode)));
130
+ await initPngDecode(pngDecWasmModule);
131
+
132
+ const oxipngWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.pngEncode)));
133
+ await initPngEncode(oxipngWasmModule);
134
+
135
+ // use SIMD variant if supported (~10 % faster)
136
+ const simdSupport = await Promise.allSettled([simd(), relaxedSimd()]);
137
+ const webpPath = simdSupport.some(r => r.status === 'fulfilled' && r.value)
138
+ ? WASM_PATHS.webpEncodeSIMD
139
+ : WASM_PATHS.webpEncode;
140
+
141
+ const webpEncWasmModule = await WebAssembly.compile(await fs.readFile(resolve(webpPath)));
142
+ await initWebpEncode(webpEncWasmModule);
143
+ }