@forward-software/gulp-sharp
Version:
Custom plugin for gulp toolkit to convert images using sharp library
321 lines (301 loc) • 11 kB
JavaScript
import sharp from 'sharp';
import { Transform } from 'node:stream';
import tty from 'node:tty';
// eslint-disable-next-line no-warning-comments
// TODO: Use a better method when it's added to Node.js (https://github.com/nodejs/node/pull/40240)
// Lots of optionals here to support Deno.
const hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false;
const format = (open, close) => {
if (!hasColors) {
return input => input;
}
const openCode = `\u001B[${open}m`;
const closeCode = `\u001B[${close}m`;
return input => {
const string = input + ''; // eslint-disable-line no-implicit-coercion -- This is faster.
let index = string.indexOf(closeCode);
if (index === -1) {
// Note: Intentionally not using string interpolation for performance reasons.
return openCode + string + closeCode;
}
// Handle nested colors.
// We could have done this, but it's too slow (as of Node.js 22).
// return openCode + string.replaceAll(closeCode, openCode) + closeCode;
let result = openCode;
let lastIndex = 0;
while (index !== -1) {
result += string.slice(lastIndex, index) + openCode;
lastIndex = index + closeCode.length;
index = string.indexOf(closeCode, lastIndex);
}
result += string.slice(lastIndex) + closeCode;
return result;
};
};
const red = format(31, 39);
const cyan = format(36, 39);
/*
The MIT License (MIT)
Copyright (c) 2015, 2017-2018, 2022 Blaine Bublitz <blaine.bublitz@gmail.com> and Eric Schoffstall <yo@contra.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const nonEnum = ["message", "name", "stack"];
const ignored = new Set([...nonEnum, "__safety", "_stack", "plugin", "showProperties", "showStack", "domain", "domainEmitter", "domainThrown"]);
const properties = ["fileName", "lineNumber", "message", "name", "plugin", "showProperties", "showStack", "stack"];
class PluginError extends Error {
constructor(plugin, message, options) {
super();
const options_ = setDefaults(plugin, message, options);
Object.assign(this, options_);
if (typeof options_.error === "object") {
this.message = options_.error.message;
this.stack = options_.error.stack;
this.cause = options_.error.cause;
Object.assign(this, options_.error);
}
for (const property of properties) {
if (property in options_) {
this[property] = options_[property];
}
}
if (!this.stack) {
const safety = {
toString: this._messageWithDetails.bind(this) + "\nStack:"
};
Error.captureStackTrace(safety, this.constructor);
this.__safety = safety;
}
if (!this.plugin) {
throw new Error("Missing plugin name");
}
if (!this.message) {
throw new Error("Missing error message");
}
if (message instanceof Error && options_.error.isPresentable) {
this.showStack = false;
this.showProperties = false;
}
}
_messageWithDetails() {
let message_ = `Message:\n ${this.message}`;
const details = this._messageDetails();
if (details) {
message_ += `\n${details}`;
}
return message_;
}
_messageDetails() {
if (!this.showProperties) {
return "";
}
const relevantProperties = Object.keys(this).filter(key => !ignored.has(key));
return relevantProperties.length > 0 ? `Details:\n${relevantProperties.map(property => ` ${property}: ${this[property]}`).join("\n")}` : "";
}
toString() {
const message_ = this.showStack ? this.__safety ? this.__safety.stack : this.stack : this._messageWithDetails();
return formatMessage(message_, this);
}
}
function formatMessage(message, thisArgument) {
return `${red(thisArgument.name)} in plugin "${cyan(thisArgument.plugin)}"\n${message}`;
}
function setDefaults(plugin, message, options) {
if (typeof plugin === "object") {
return {
...plugin
};
}
if (message instanceof Error) {
options = {
...options,
error: message
};
} else if (typeof message === "object") {
options = {
...message
};
} else {
options = {
...options,
message
};
}
options.plugin = plugin;
return {
showStack: false,
showProperties: true,
...options
};
}
/**
* Safely calls a function with the provided arguments, catching and ignoring any errors that might occur.
*
* @param {Function} fn - The function to call.
* @param {...*} args - The arguments to pass to the function.
* @returns {*} The return value of the function, or undefined if an error occurred.
*/
function safeCall(fn, ...args) {
try {
return fn(...args);
} catch (error) {
// Ignore the error
}
}
/**
* Creates a TransformStream that applies a given transformer function to each chunk.
*
* @param {Function} transformer - The function to apply to each chunk. It should return a Promise that resolves to the transformed chunk.
* @returns {TransformStream} A new TransformStream instance.
*/
function transformStream(transformer) {
return new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
transformer(chunk, encoding, this).then(value => safeCall(callback, undefined, value)) // If the callback throws, we don't want to cause an infinite recursion.
.catch(callback);
},
flush(callback) {
callback();
}
});
}
/**
* Creates a gulp plugin that applies a given function to each file.
*
* @param {string} name - The name of the plugin.
* @param {Function} onFile - The function to apply to each file. It should return a Promise that resolves to the transformed file.
* @returns {NodeJS.ReadableStream} A function that can be used as a gulp plugin.
*/
function gulpPlugin(name, onFile) {
return transformStream(file => {
if (file.isNull() || file.isDirectory()) {
return Promise.resolve(file);
}
if (file.isStream()) {
return Promise.reject(new PluginError(name, "Streaming not supported"));
}
try {
return onFile(file);
} catch (error) {
return Promise.reject(new PluginError(name, error, {
fileName: file.path,
showStack: true
}));
}
});
}
const SUPPORTED_EXTENSIONS = ["avif", "gif", "jpg", "jpeg", "png", "svg", "tif", "tiff", "webp"];
function handleOptions(options) {
const {
extensions,
...sharpOptions
} = options || {};
const supportedExtensions = new Set(extensions ?? SUPPORTED_EXTENSIONS);
return {
supportedExtensions,
sharpOptions
};
}
/**
* Converts images to WebP format using sharp.
*
* @param {Object} options - Options for the conversion process.
* @param {Array<string>} options.extensions - Array of file extensions to convert, if not specified the following extensions will be converted: ["avif", "gif", "jpg", "jpeg", "png", "svg", "tif", "tiff", "webp"].
* @returns {Function} A function that can be used as a gulp plugin.
*/
function webp(options) {
const {
sharpOptions,
supportedExtensions
} = handleOptions(options);
return gulpPlugin("gulp-sharp-webp", async file => {
// DO NOT transform unsupported (or not requested) file extensions
if (!supportedExtensions.has(file.extname.slice(1).toLowerCase())) {
return file;
}
file.contents = await sharp(file.contents).webp(sharpOptions).toBuffer();
file.extname = ".webp";
return file;
});
}
/**
* Converts images to PNG format using sharp.
*
* @param {Object} options - Options for the conversion process.
* @param {Array<string>} options.extensions - Array of file extensions to convert, if not specified the following extensions will be converted: ["avif", "gif", "jpg", "jpeg", "png", "svg", "tif", "tiff", "webp"].
* @returns {Function} A function that can be used as a gulp plugin.
*/
function png(options) {
const {
sharpOptions,
supportedExtensions
} = handleOptions(options);
return gulpPlugin("gulp-sharp-png", async file => {
// DO NOT transform unsupported (or not requested) file extensions
if (!supportedExtensions.has(file.extname.slice(1).toLowerCase())) {
return file;
}
file.contents = await sharp(file.contents).png(sharpOptions).toBuffer();
file.extname = ".png";
return file;
});
}
/**
* Converts images to JPEG format using sharp.
*
* @param {Object} options - Options for the conversion process.
* @param {Array<string>} options.extensions - Array of file extensions to convert, if not specified the following extensions will be converted: ["avif", "gif", "jpg", "jpeg", "png", "svg", "tif", "tiff", "webp"].
* @returns {Function} A function that can be used as a gulp plugin.
*/
function jpeg(options) {
const {
sharpOptions,
supportedExtensions
} = handleOptions(options);
return gulpPlugin("gulp-sharp-jpeg", async file => {
// DO NOT transform unsupported (or not requested) file extensions
if (!supportedExtensions.has(file.extname.slice(1).toLowerCase())) {
return file;
}
file.contents = await sharp(file.contents).jpeg(sharpOptions).toBuffer();
file.extname = ".jpeg";
return file;
});
}
/**
* Converts images to TIFF format using sharp.
*
* @param {Object} options - Options for the conversion process.
* @param {Array<string>} options.extensions - Array of file extensions to convert, if not specified the following extensions will be converted: ["avif", "gif", "jpg", "jpeg", "png", "svg", "tif", "tiff", "webp"].
* @returns {Function} A function that can be used as a gulp plugin.
*/
function tiff(options) {
const {
sharpOptions,
supportedExtensions
} = handleOptions(options);
return gulpPlugin("gulp-sharp-tiff", async file => {
// DO NOT transform unsupported (or not requested) file extensions
if (!supportedExtensions.has(file.extname.slice(1).toLowerCase())) {
return file;
}
file.contents = await sharp(file.contents).tiff(sharpOptions).toBuffer();
file.extname = ".tiff";
return file;
});
}
export { jpeg, png, tiff, webp };