380 lines
13 KiB
Plaintext
380 lines
13 KiB
Plaintext
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
const fs = require("fs");
|
|
const fsp = require("fs/promises");
|
|
const pathe = require("pathe");
|
|
const ansi = require("ansi-colors");
|
|
const SLASH_RE = /[/\\]/;
|
|
function filename(path) {
|
|
const base = path.split(SLASH_RE).pop();
|
|
if (!base) {
|
|
return void 0;
|
|
}
|
|
const separatorIndex = base.lastIndexOf(".");
|
|
if (separatorIndex <= 0) {
|
|
return base;
|
|
}
|
|
return base.slice(0, separatorIndex);
|
|
}
|
|
function isRegex(src) {
|
|
return Object.prototype.toString.call(src) === "[object RegExp]";
|
|
}
|
|
function isString(src) {
|
|
return Object.prototype.toString.call(src) === "[object String]";
|
|
}
|
|
function isArray(src) {
|
|
return Array.isArray(src);
|
|
}
|
|
function merge(src, target) {
|
|
const deepClone = (src2) => {
|
|
if (typeof src2 !== "object" || isRegex(src2) || src2 === null) return src2;
|
|
const target2 = Array.isArray(src2) ? [] : {};
|
|
for (const key in src2) {
|
|
const value = src2[key];
|
|
target2[key] = deepClone(value);
|
|
}
|
|
return target2;
|
|
};
|
|
const clone = deepClone(src);
|
|
for (const key in target) {
|
|
if (clone[key] === void 0) {
|
|
clone[key] = target[key];
|
|
}
|
|
}
|
|
return clone;
|
|
}
|
|
function readAllFiles(root) {
|
|
let resultArr = [];
|
|
try {
|
|
if (fs.existsSync(root)) {
|
|
const stat = fs.lstatSync(root);
|
|
if (stat.isDirectory()) {
|
|
const files = fs.readdirSync(root);
|
|
files.forEach(function(file) {
|
|
const t = readAllFiles(pathe.join(root, "/", file));
|
|
resultArr = resultArr.concat(t);
|
|
});
|
|
} else {
|
|
resultArr.push(root);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
return resultArr;
|
|
}
|
|
function areFilesMatching(fileName, filePath, matcher) {
|
|
if (isString(matcher)) return fileName === matcher;
|
|
if (isRegex(matcher)) return matcher.test(filePath);
|
|
if (isArray(matcher)) return matcher.includes(fileName);
|
|
return false;
|
|
}
|
|
function decideStyle(text, enableColors) {
|
|
return enableColors ? text : ansi.unstyle(text);
|
|
}
|
|
function getLogger(rootConfig) {
|
|
return (rootConfig == null ? void 0 : rootConfig.logger) ?? console;
|
|
}
|
|
function getOutDir(rootConfig) {
|
|
var _a;
|
|
return ((_a = rootConfig == null ? void 0 : rootConfig.build) == null ? void 0 : _a.outDir) ?? "dist";
|
|
}
|
|
function logErrors(rootConfig, errorsMap, ansiColors) {
|
|
const logger = getLogger(rootConfig);
|
|
const outDir = getOutDir(rootConfig);
|
|
logger.info(decideStyle(`
|
|
🚨 ${ansi.red("[vite-plugin-image-optimizer]")} - errors during optimization: `, ansiColors));
|
|
const keyLengths = Array.from(errorsMap.keys(), (name) => name.length);
|
|
const maxKeyLength = Math.max(...keyLengths);
|
|
errorsMap.forEach((message, name) => {
|
|
logger.error(
|
|
decideStyle(
|
|
`${ansi.dim(pathe.basename(outDir))}/${ansi.blueBright(name)}${" ".repeat(2 + maxKeyLength - name.length)} ${ansi.red(message)}`,
|
|
ansiColors
|
|
)
|
|
);
|
|
});
|
|
logger.info("\n");
|
|
}
|
|
function logOptimizationStats(rootConfig, sizesMap, ansiColors) {
|
|
const logger = getLogger(rootConfig);
|
|
const outDir = getOutDir(rootConfig);
|
|
logger.info(decideStyle(`
|
|
✨ ${ansi.cyan("[vite-plugin-image-optimizer]")} - optimized images successfully: `, ansiColors));
|
|
const keyLengths = Array.from(sizesMap.keys(), (name) => name.length);
|
|
const valueLengths = Array.from(sizesMap.values(), (value) => `${Math.floor(100 * value.ratio)}`.length);
|
|
const maxKeyLength = Math.max(...keyLengths);
|
|
const valueKeyLength = Math.max(...valueLengths);
|
|
let totalOriginalSize = 0;
|
|
let totalSavedSize = 0;
|
|
sizesMap.forEach((value, name) => {
|
|
const { size, oldSize, ratio, skipWrite, isCached } = value;
|
|
let percentChange;
|
|
if (ratio > 0) {
|
|
percentChange = ansi.red(`+${ratio}%`);
|
|
} else if (ratio <= 0) {
|
|
percentChange = ansi.green(`${ratio}%`);
|
|
} else {
|
|
percentChange = "";
|
|
}
|
|
let sizeText;
|
|
if (skipWrite) {
|
|
sizeText = `${ansi.yellow.bold("skipped")} ${ansi.dim(`original: ${oldSize.toFixed(2)} kB <= optimized: ${size.toFixed(2)} kB`)}`;
|
|
} else if (isCached) {
|
|
sizeText = `${ansi.yellow.bold("cached")} ${ansi.dim(`original: ${oldSize.toFixed(2)} kB; cached: ${size.toFixed(2)} kB`)}`;
|
|
} else {
|
|
sizeText = ansi.dim(`${oldSize.toFixed(2)} kB ⭢ ${size.toFixed(2)} kB`);
|
|
}
|
|
logger.info(
|
|
decideStyle(
|
|
ansi.dim(pathe.basename(outDir)) + "/" + ansi.blueBright(name) + " ".repeat(2 + maxKeyLength - name.length) + ansi.gray(`${percentChange} ${" ".repeat(valueKeyLength - `${ratio}`.length)}`) + " " + sizeText,
|
|
ansiColors
|
|
)
|
|
);
|
|
if (!skipWrite) {
|
|
totalOriginalSize += oldSize;
|
|
totalSavedSize += oldSize - size;
|
|
}
|
|
});
|
|
if (totalSavedSize > 0) {
|
|
const savedText = `${totalSavedSize.toFixed(2)}kB`;
|
|
const originalText = `${totalOriginalSize.toFixed(2)}kB`;
|
|
const savingsPercent = `${Math.round(totalSavedSize / totalOriginalSize * 100)}%`;
|
|
logger.info(
|
|
decideStyle(`
|
|
💰 total savings = ${ansi.green(savedText)}/${ansi.green(originalText)} ≈ ${ansi.green(savingsPercent)}`, ansiColors)
|
|
);
|
|
}
|
|
logger.info("\n");
|
|
}
|
|
const VITE_PLUGIN_NAME = "vite-plugin-image-optimizer";
|
|
const SVGO_CONFIG = {
|
|
multipass: true,
|
|
plugins: [
|
|
{
|
|
name: "preset-default",
|
|
params: {
|
|
overrides: {
|
|
cleanupNumericValues: false,
|
|
cleanupIds: {
|
|
minify: false,
|
|
remove: false
|
|
},
|
|
convertPathData: false
|
|
}
|
|
}
|
|
},
|
|
"sortAttrs",
|
|
{
|
|
name: "addAttributesToSVGElement",
|
|
params: {
|
|
attributes: [{ xmlns: "http://www.w3.org/2000/svg" }]
|
|
}
|
|
}
|
|
]
|
|
};
|
|
const DEFAULT_OPTIONS = {
|
|
logStats: true,
|
|
ansiColors: true,
|
|
includePublic: true,
|
|
exclude: void 0,
|
|
include: void 0,
|
|
test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i,
|
|
svg: SVGO_CONFIG,
|
|
png: {
|
|
// https://sharp.pixelplumbing.com/api-output#png
|
|
quality: 100
|
|
},
|
|
jpeg: {
|
|
// https://sharp.pixelplumbing.com/api-output#jpeg
|
|
quality: 100
|
|
},
|
|
jpg: {
|
|
// https://sharp.pixelplumbing.com/api-output#jpeg
|
|
quality: 100
|
|
},
|
|
tiff: {
|
|
// https://sharp.pixelplumbing.com/api-output#tiff
|
|
quality: 100
|
|
},
|
|
// gif does not support lossless compression
|
|
// https://sharp.pixelplumbing.com/api-output#gif
|
|
gif: {},
|
|
webp: {
|
|
// https://sharp.pixelplumbing.com/api-output#webp
|
|
lossless: true
|
|
},
|
|
avif: {
|
|
// https://sharp.pixelplumbing.com/api-output#avif
|
|
lossless: true
|
|
},
|
|
cache: false,
|
|
cacheLocation: void 0
|
|
};
|
|
function ViteImageOptimizer(optionsParam = {}) {
|
|
const options = merge(optionsParam, DEFAULT_OPTIONS);
|
|
let outputPath;
|
|
let publicDir;
|
|
let rootConfig;
|
|
const sizesMap = /* @__PURE__ */ new Map();
|
|
const mtimeCache = /* @__PURE__ */ new Map();
|
|
const errorsMap = /* @__PURE__ */ new Map();
|
|
const applySVGO = async (filePath, buffer) => {
|
|
const optimize = (await import("svgo")).optimize;
|
|
return Buffer.from(
|
|
optimize(buffer.toString(), {
|
|
path: filePath,
|
|
...options.svg
|
|
}).data
|
|
);
|
|
};
|
|
const applySharp = async (filePath, buffer) => {
|
|
const sharp = (await import("sharp")).default;
|
|
const extName = pathe.extname(filePath).replace(".", "").toLowerCase();
|
|
return await sharp(buffer, { animated: extName === "gif" }).toFormat(
|
|
extName,
|
|
options[extName]
|
|
).toBuffer();
|
|
};
|
|
const processFile = async (filePath, buffer) => {
|
|
try {
|
|
let newBuffer;
|
|
let isCached;
|
|
const cachedFilePath = pathe.join(options.cacheLocation ?? "", filePath);
|
|
if (options.cache === true && fs.existsSync(cachedFilePath)) {
|
|
newBuffer = await fsp.readFile(cachedFilePath);
|
|
isCached = true;
|
|
} else {
|
|
const engine = /\.svg$/.test(filePath) ? applySVGO : applySharp;
|
|
newBuffer = await engine(filePath, buffer);
|
|
isCached = false;
|
|
}
|
|
if (options.cache === true && !isCached) {
|
|
if (!fs.existsSync(pathe.dirname(cachedFilePath))) {
|
|
await fsp.mkdir(pathe.dirname(cachedFilePath), { recursive: true });
|
|
}
|
|
await fsp.writeFile(cachedFilePath, newBuffer);
|
|
}
|
|
const newSize = newBuffer.byteLength;
|
|
const oldSize = buffer.byteLength;
|
|
const skipWrite = newSize >= oldSize;
|
|
sizesMap.set(filePath, {
|
|
size: newSize / 1024,
|
|
oldSize: oldSize / 1024,
|
|
ratio: Math.floor(100 * (newSize / oldSize - 1)),
|
|
skipWrite,
|
|
isCached
|
|
});
|
|
return { content: newBuffer, skipWrite };
|
|
} catch (error) {
|
|
errorsMap.set(filePath, error.message);
|
|
return {};
|
|
}
|
|
};
|
|
const getFilesToProcess = (allFiles, getFileName) => {
|
|
if (options.include) {
|
|
return allFiles.reduce((acc, filePath) => {
|
|
const fileName = getFileName(filePath);
|
|
if (areFilesMatching(fileName, filePath, options.include)) {
|
|
acc.push(filePath);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
}
|
|
return allFiles.reduce((acc, filePath) => {
|
|
var _a;
|
|
if ((_a = options.test) == null ? void 0 : _a.test(filePath)) {
|
|
const fileName = getFileName(filePath);
|
|
if (!areFilesMatching(fileName, filePath, options.exclude)) {
|
|
acc.push(filePath);
|
|
}
|
|
}
|
|
return acc;
|
|
}, []);
|
|
};
|
|
const ensureCacheDirectoryExists = async function() {
|
|
if (options.cache === true && options.cacheLocation && !fs.existsSync(options.cacheLocation)) {
|
|
await fsp.mkdir(options.cacheLocation, { recursive: true });
|
|
}
|
|
};
|
|
return {
|
|
name: VITE_PLUGIN_NAME,
|
|
enforce: "post",
|
|
apply: "build",
|
|
configResolved(c) {
|
|
rootConfig = c;
|
|
outputPath = c.build.outDir;
|
|
if (typeof c.publicDir === "string") {
|
|
publicDir = c.publicDir.replace(/\\/g, "/");
|
|
}
|
|
},
|
|
generateBundle: async (_, bundler) => {
|
|
const allFiles = Object.keys(bundler);
|
|
const files = getFilesToProcess(allFiles, (path) => bundler[path].name);
|
|
if (files.length > 0) {
|
|
await ensureCacheDirectoryExists();
|
|
const handles = files.map(async (filePath) => {
|
|
const source = bundler[filePath].source;
|
|
const { content, skipWrite } = await processFile(filePath, source);
|
|
if (content && content.length > 0 && !skipWrite) {
|
|
bundler[filePath].source = content;
|
|
}
|
|
});
|
|
await Promise.all(handles);
|
|
}
|
|
},
|
|
async closeBundle() {
|
|
if (publicDir && options.includePublic) {
|
|
const allFiles = readAllFiles(publicDir);
|
|
const files = getFilesToProcess(allFiles, (path) => filename(path) + pathe.extname(path));
|
|
if (files.length > 0) {
|
|
await ensureCacheDirectoryExists();
|
|
const handles = files.map(async (publicFilePath) => {
|
|
const filePath = publicFilePath.replace(publicDir + pathe.sep, "");
|
|
const fullFilePath = pathe.resolve(rootConfig.root, outputPath, filePath);
|
|
if (fs.existsSync(fullFilePath) === false) return;
|
|
const { mtimeMs } = await fsp.stat(fullFilePath);
|
|
if (mtimeMs <= (mtimeCache.get(filePath) || 0)) return;
|
|
const buffer = await fsp.readFile(fullFilePath);
|
|
const { content, skipWrite } = await processFile(filePath, buffer);
|
|
if (content && (content == null ? void 0 : content.length) > 0 && !skipWrite) {
|
|
await fsp.writeFile(fullFilePath, content);
|
|
mtimeCache.set(filePath, Date.now());
|
|
}
|
|
});
|
|
await Promise.all(handles);
|
|
}
|
|
}
|
|
if (sizesMap.size > 0 && options.logStats) {
|
|
logOptimizationStats(rootConfig, sizesMap, options.ansiColors ?? true);
|
|
}
|
|
if (errorsMap.size > 0) {
|
|
logErrors(rootConfig, errorsMap, options.ansiColors ?? true);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
exports.ViteImageOptimizer = ViteImageOptimizer;
|