UNPKG

rsbuild-plugin-sharp-image-optimizer

Version:

A Rsbuild plugin for image optimization using Sharp

200 lines (199 loc) 10.6 kB
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 };