rsbuild-plugin-sharp-image-optimizer
Version:
A Rsbuild plugin for image optimization using Sharp
200 lines (199 loc) • 10.6 kB
JavaScript
import * as __WEBPACK_EXTERNAL_MODULE_sharp__ from "sharp";
import * as __WEBPACK_EXTERNAL_MODULE_path__ from "path";
import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs";
import * as __WEBPACK_EXTERNAL_MODULE_crypto__ from "crypto";
function _define_property(obj, key, value) {
return key in obj ? Object.defineProperty(obj, key, {
value: value,
enumerable: !0,
configurable: !0,
writable: !0
}) : obj[key] = value, obj;
}
class CacheManager {
ensureCacheDir() {
!__WEBPACK_EXTERNAL_MODULE_fs__.default.existsSync(this.cacheDir) && __WEBPACK_EXTERNAL_MODULE_fs__.default.mkdirSync(this.cacheDir, {
recursive: !0
});
}
getCacheKey(inputBuffer, options) {
let contentHash = __WEBPACK_EXTERNAL_MODULE_crypto__.default.createHash('md5').update(inputBuffer).digest('hex'), optionsHash = __WEBPACK_EXTERNAL_MODULE_crypto__.default.createHash('md5').update(JSON.stringify(options)).digest('hex');
return `${contentHash}-${optionsHash}`;
}
getCacheFilePath(cacheKey) {
return __WEBPACK_EXTERNAL_MODULE_path__.default.join(this.cacheDir, `${cacheKey}.cache`);
}
parseSize(sizeStr) {
let match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B)$/i);
if (!match) return 1073741824;
let value = parseFloat(match[1]);
return value * (({
B: 1,
KB: 1024,
MB: 1048576,
GB: 1073741824
})[match[2].toUpperCase()] || 1);
}
async cleanupCache() {
try {
let cacheFiles = __WEBPACK_EXTERNAL_MODULE_fs__.default.readdirSync(this.cacheDir).filter((file)=>file.endsWith('.cache')).map((file)=>{
let filePath = __WEBPACK_EXTERNAL_MODULE_path__.default.join(this.cacheDir, file), stats = __WEBPACK_EXTERNAL_MODULE_fs__.default.statSync(filePath);
return {
name: file,
path: filePath,
stats,
timestamp: stats.mtime.getTime()
};
}).sort((a, b)=>a.timestamp - b.timestamp), maxSize = this.parseSize(this.options.maxSize), currentSize = 0, now = Date.now();
for (let file of cacheFiles)currentSize += file.stats.size;
for (let file of cacheFiles)now - file.timestamp > this.options.ttl && (__WEBPACK_EXTERNAL_MODULE_fs__.default.unlinkSync(file.path), currentSize -= file.stats.size);
if (currentSize > maxSize) {
for (let file of cacheFiles)if (__WEBPACK_EXTERNAL_MODULE_fs__.default.existsSync(file.path) && (__WEBPACK_EXTERNAL_MODULE_fs__.default.unlinkSync(file.path), (currentSize -= file.stats.size) <= maxSize)) break;
}
} catch (error) {
console.warn('Failed to cleanup cache:', error);
}
}
async get(inputBuffer, options) {
if (!this.options.enabled) return null;
try {
let cacheKey = this.getCacheKey(inputBuffer, options), cacheFilePath = this.getCacheFilePath(cacheKey);
if (__WEBPACK_EXTERNAL_MODULE_fs__.default.existsSync(cacheFilePath)) {
let stats = __WEBPACK_EXTERNAL_MODULE_fs__.default.statSync(cacheFilePath);
if (Date.now() - stats.mtime.getTime() > this.options.ttl) return __WEBPACK_EXTERNAL_MODULE_fs__.default.unlinkSync(cacheFilePath), null;
return __WEBPACK_EXTERNAL_MODULE_fs__.default.readFileSync(cacheFilePath);
}
} catch (error) {
console.warn('Failed to read cache:', error);
}
return null;
}
async set(inputBuffer, options, outputBuffer) {
if (!!this.options.enabled) try {
await this.cleanupCache();
let cacheKey = this.getCacheKey(inputBuffer, options), cacheFilePath = this.getCacheFilePath(cacheKey);
__WEBPACK_EXTERNAL_MODULE_fs__.default.writeFileSync(cacheFilePath, outputBuffer);
} catch (error) {
console.warn('Failed to write cache:', error);
}
}
constructor(options){
_define_property(this, "options", void 0), _define_property(this, "cacheDir", void 0), this.options = options, this.cacheDir = __WEBPACK_EXTERNAL_MODULE_path__.default.resolve(process.cwd(), options.dir), this.ensureCacheDir();
}
}
function rspack_plugins_define_property(obj, key, value) {
return key in obj ? Object.defineProperty(obj, key, {
value: value,
enumerable: !0,
configurable: !0,
writable: !0
}) : obj[key] = value, obj;
}
class SharpImageOptimizerPlugin {
async getOptimizationOptions(format) {
let baseOptions = {
quality: this.options.quality,
effort: this.options.effort
};
return 'jpeg' === format || 'jpg' === format ? {
quality: this.options.quality
} : baseOptions;
}
async optimizeImage(inputBuffer, format) {
let outputBuffer;
if (this.cacheManager) {
let cacheOptions = this.getOptimizationOptions(format), cached = await this.cacheManager.get(inputBuffer, cacheOptions);
if (cached) return cached;
}
let sharpInstance = (0, __WEBPACK_EXTERNAL_MODULE_sharp__.default)(inputBuffer), options = this.getOptimizationOptions(format), method = {
webp: ()=>sharpInstance.webp(options).toBuffer(),
avif: ()=>sharpInstance.avif(options).toBuffer(),
png: ()=>sharpInstance.png(options).toBuffer(),
jpeg: ()=>sharpInstance.jpeg(options).toBuffer(),
jpg: ()=>sharpInstance.jpeg(options).toBuffer()
}[format];
if (!method) throw Error(`Unsupported format: ${format}`);
return outputBuffer = await method(), this.cacheManager && await this.cacheManager.set(inputBuffer, options, outputBuffer), outputBuffer;
}
async optimize(compiler, compilation, assets) {
let { RawSource } = compiler.webpack.sources, processedAssets = new Map();
for (let [name, asset] of Object.entries(assets)){
if (!!this.options.test.test(name)) try {
let inputBuffer = asset.source(), ext = __WEBPACK_EXTERNAL_MODULE_path__.default.extname(name).toLowerCase(), originalFormat = ext.slice(1), newName = name;
if (this.options.format && this.options.format !== originalFormat && !('jpg' === this.options.format && 'jpeg' === originalFormat) && !('jpeg' === this.options.format && 'jpg' === originalFormat)) {
var _compilation_getAsset;
newName = name.replace(ext, `.${this.options.format}`);
let outputBuffer = await this.optimizeImage(inputBuffer, this.options.format);
compilation.emitAsset(newName, new RawSource(outputBuffer), {
...null === (_compilation_getAsset = compilation.getAsset(name)) || void 0 === _compilation_getAsset ? void 0 : _compilation_getAsset.info,
sourceFilename: name
}), processedAssets.set(name, newName);
} else {
let outputBuffer = await this.optimizeImage(inputBuffer, originalFormat);
compilation.updateAsset(name, new RawSource(outputBuffer), asset.info);
}
} catch (error) {
compilation.errors.push(new compiler.webpack.WebpackError(`Failed to process image ${name}: ${error instanceof Error ? error.message : String(error)}`));
}
}
if (processedAssets.size > 0) {
for (let [name, asset] of Object.entries(assets))if (name.endsWith('.js')) {
let source = asset.source().toString(), modified = !1;
for (let [oldName, newName] of processedAssets.entries()){
let oldPath = oldName.replace(/\\/g, '/'), newPath = newName.replace(/\\/g, '/');
source.includes(oldPath) && (source = source.replace(RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath), modified = !0);
}
modified && compilation.updateAsset(name, new RawSource(source), asset.info);
}
for (let [oldName] of processedAssets)compilation.deleteAsset(oldName);
}
}
apply(compiler) {
compiler.hooks.compilation.tap('SharpImageOptimizerPlugin', (compilation)=>{
compilation.hooks.processAssets.tapPromise({
name: 'SharpImageOptimizerPlugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE
}, async (assets)=>this.optimize(compiler, compilation, assets));
});
}
constructor(options = {}){
var _this_options_cache;
rspack_plugins_define_property(this, "options", void 0), rspack_plugins_define_property(this, "cacheManager", null), this.options = {
test: /\.(png|jpe?g)$/i,
quality: 85,
effort: 6,
...options
}, (null === (_this_options_cache = this.options.cache) || void 0 === _this_options_cache ? void 0 : _this_options_cache.enabled) && (this.cacheManager = new CacheManager(this.options.cache));
}
}
let sharpImageOptimizer = (options = {})=>{
let { test = /\.(jpe?g|png|webp|avif)$/i, quality = 80, effort = 4, cache: userCache = {}, ...pluginOptions } = options, cache = {
enabled: !0,
dir: '.image-cache',
maxSize: '1GB',
ttl: 604800000,
...userCache
};
return {
name: 'rsbuild-plugin-sharp-image-optimizer',
setup (api) {
api.modifyBundlerChain((chain, { isProd })=>{
if (isProd) {
var _config_output_distPath, _config_output;
let imagePath = (null === (_config_output = api.getNormalizedConfig().output) || void 0 === _config_output ? void 0 : null === (_config_output_distPath = _config_output.distPath) || void 0 === _config_output_distPath ? void 0 : _config_output_distPath.image) ?? 'static/image';
chain.plugin('sharp-image-optimizer-plugin').use(SharpImageOptimizerPlugin, [
{
test,
quality,
effort,
cache,
...pluginOptions,
imagePath
}
]);
}
});
}
};
};
export { sharpImageOptimizer };