UNPKG

vite-plugin-vsharp

Version:

A Vite plugin that compresses static images after each builds by using sharp.js

316 lines (278 loc) 7.92 kB
import {normalizePath} from 'vite' import path from 'path' import fs from 'fs' import sharp from 'sharp' import chalk from 'chalk' import glob from 'glob' import walk from './utils/walk.js' import getViteVersion from './utils/getViteVersion.js' import bytesToSize from './utils/bytesToSize.js' let vite_version let config // default options const defaults = { scale: undefined, width: undefined, height: undefined, includePublic: [], excludePublic: [], exclude: [], '.jpg': { quality: 80 }, '.jpeg': { quality: 80 }, '.png': { quality: 80, palette: true }, '.webp': { lossless: true }, preserveMetadata: { orientation: false, icc: false, exif: false } } // specific function for each file extension // values are from sharp const extFunction = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png', '.gif': 'gif', '.webp': 'webp' } const supportedFileExt = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] /** * compress images during build process * @param {object} opts * @returns {{writeBundle(*, *): void, name: string, configResolved(*): void}} */ export default function vsharp(opts = {}) { const options = Object.assign({}, defaults, opts) return { name: 'vsharp', configResolved(res) { config = res vite_version = vite_version ? parseInt( getViteVersion(config.root).replace(/^\D+/, '').split('.')[0] ) : null }, writeBundle(op, bundle) { let outDir = op.dir let keys = Object.keys(bundle) keys = keys.map((el) => { return path.join(outDir, el) }) let bundle_images = getImgs(keys, options) bundle_images.forEach((el) => { vsharpIt(el, options) }) let public_images = [] walk(path.normalize(config.publicDir), function (err, filesRec) { if (err) { if (err.code === 'ENOENT') { // no public folder return null } console.log('walk error: ', err) return null } let excludedFiles = [] let includedFiles = [] let scanCount = 0 const processFiles = () => { let allImageFiles = filesRec.map(el => { let p = normalizePath(el) return p.replace(config.root + '/', '') }).filter(p => supportedFileExt.includes(path.extname(p))) let filteredFiles = allImageFiles.filter(file => { return !excludedFiles.some(pattern => file.startsWith(pattern.replace('*', '')) || file === pattern ) }) includedFiles.forEach(file => { if (!filteredFiles.includes(file)) { filteredFiles.push(file) } }) filteredFiles.forEach(img => { let publicDir = normalizePath(config.publicDir).replace( config.root + '/', '' ) img = img.replace(publicDir, config.build.outDir) vsharpIt(img, options) }) } const checkAndProcess = () => { scanCount++ if (scanCount === 2) { processFiles() } } if (options.excludePublic.length > 0) { options.excludePublic.forEach((rule, i) => { glob( rule, { root: options.root }, function (err, files) { if (err) { console.log(err) return false } files.forEach((file) => { excludedFiles.push(file) }) if (i === options.excludePublic.length - 1) { checkAndProcess() } } ) }) } else { scanCount++ } if (options.includePublic.length > 0) { options.includePublic.forEach((rule, i) => { glob( rule, { root: options.root }, function (err, files) { if (err) { console.log(err) return false } files.forEach((file) => { includedFiles.push(file) }) if (i === options.includePublic.length - 1) { checkAndProcess() } } ) }) } else { scanCount++ } }) } } } function getImgs(data, opts) { let _imgs = [] data.forEach((el) => { let thisExt = path.extname(el) let thisName if (vite_version < 4) { thisName = path.basename(el).split('.') } else { thisName = path.basename(el).split('-') } thisName = thisName.reverse() thisName.splice(0, 2) thisName = thisName.join('') if (opts.exclude.includes(thisName + thisExt)) { return false } if (supportedFileExt.includes(thisExt)) { el = normalizePath(el) el = el.replace(config.root + '/', '') _imgs.push(el) } }) return _imgs } /** * run with sharp * @param {string} img - image path * @param opts - options */ function vsharpIt(img, opts) { let extname = path.extname(img) let sharp_function = extFunction[extname] if (!fs.existsSync(img)) { console.log(`vsharp: [${chalk.red(img)}] File does not exist, skipping...`) return } let sharp_image = sharp(img, {animated: true}) sharp_image.metadata().then((metadata) => { let previousSize try { previousSize = fs.statSync(img).size } catch (err) { console.log(`vsharp: [${chalk.red(img)}] Cannot access file: ${err.message}`) return } let currentWidth = metadata.width let targetWidth = metadata.width let targetHeight = metadata.height if (opts.width !== undefined && opts.height !== undefined) { targetWidth = opts.width targetHeight = opts.height } else if (opts.width === undefined && opts.height === undefined) { targetWidth = metadata.width targetHeight = metadata.height } else if (opts.width !== undefined && opts.height === undefined) { targetWidth = opts.width targetHeight = null } else if (opts.width === undefined && opts.height !== undefined) { targetWidth = null targetHeight = opts.height } if (opts.scale !== undefined) { targetWidth = Math.floor(opts.scale * currentWidth) targetHeight = null } let outBuffer = sharp_image outBuffer = outBuffer.resize(targetWidth, targetHeight) outBuffer = outBuffer[sharp_function](opts[extname]) if (typeof opts.preserveMetadata === 'object') { let preservedMetadata = {} if (opts.preserveMetadata.orientation) { preservedMetadata.orientation = metadata.orientation } outBuffer = outBuffer.withMetadata(preservedMetadata) } return outBuffer.toBuffer((err, buffer, info) => { if (err) { console.log(err) } let currentSize = info.size if (previousSize < currentSize) { console.log( `vsharp: [${chalk.green(img)}], current size (${bytesToSize( currentSize )}) is bigger after <sharp> processed, skipping...` ) return false } fs.writeFile(img, buffer, {flag: 'w+'}, (err) => { if (err) { console.log(err) return } let shrinkRatio = ( -((previousSize - currentSize) / previousSize) * 100 ).toFixed(2) console.log( `vsharp: [${chalk.green(img)}] ${chalk.yellow( bytesToSize(previousSize) )} <<${chalk.green(shrinkRatio + '%')}>> ${chalk.yellow( bytesToSize(currentSize) )}` ) }) }) }) }