vite-plugin-single-image-format
Version:
vite-plugin-single-image-format is a Vite/Rollup plugin that converts every raster asset in your build to a single output format – webp, png or avif. It can optionally re‑compress images that are already in the target format and automatically rewrites all
263 lines (262 loc) • 9.45 kB
JavaScript
import { posix as v } from "path";
import { createHash as ee } from "crypto";
import N from "sharp";
function ne(T = {}) {
const {
format: b = "webp",
reencode: H = !1,
webp: _ = {},
png: L = {},
avif: D = {},
htmlSizeMode: S = "add-only",
hashInName: P = !1,
hashLength: R = 8
} = T, I = {
quality: 88,
alphaQuality: 90,
smartSubsample: !0
}, K = {
quality: 80,
compressionLevel: 9,
palette: !0,
adaptiveFiltering: !0
}, Q = {
quality: 60,
lossless: !1,
speed: 5
}, G = { ...I, ..._ }, V = { ...K, ...L }, C = { ...Q, ...D }, x = /\.(png|jpe?g|webp|gif|avif|heif|heic|tiff?|bmp|jp2)$/i, W = [".html", ".css", ".js", ".mjs", ".ts", ".jsx", ".tsx"], F = "imgfmt=keep", d = /* @__PURE__ */ new Map();
function M(r, t) {
return new RegExp(`\\b${t}\\s*=`, "i").test(r);
}
function O(r) {
return r.replace(/\s+(?:width|height)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
}
function J(r) {
return r.split(/[?#]/)[0];
}
function U(r) {
const t = J(r);
for (const s of d.keys())
if (t.endsWith(s) || t.endsWith("./" + s) || t.endsWith("/" + s))
return s;
return null;
}
function X(r, t, s, m, p, e) {
const i = U(m);
if (!i) return r;
const n = d.get(i);
if (!n) return r;
if (S === "overwrite") {
const h = O(t), l = O(p);
return `<img ${h}src=${s}${m}${s}${l} width="${n.width}" height="${n.height}"${e}>`;
}
const c = M(t + p, "width"), f = M(t + p, "height");
if (c && f) return r;
const a = c ? "" : ` width="${n.width}"`, o = f ? "" : ` height="${n.height}"`;
return `<img ${t}src=${s}${m}${s}${p}${a}${o}${e}>`;
}
function j(r) {
return r.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function Y(r) {
const [t, s = ""] = r.split("#", 2), m = t.startsWith("?"), p = m ? t.slice(1) : t;
if (!p)
return (m ? "?" : "") + (s ? "#" + s : "");
const e = p.split("&").filter(Boolean).filter((n) => {
const [c, f = ""] = n.split("=", 2);
return !(c === "imgfmt" && f === "keep");
});
return (e.length ? "?" + e.join("&") : "") + (s ? "#" + s : "");
}
function Z(r, t) {
return new RegExp(`\\b${t}\\s*=`, "i").test(r);
}
function k(r, t) {
const s = new RegExp(
String.raw`(^|\s+)${t}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)`,
"gi"
);
return r.replace(s, "$1");
}
function A(r) {
const t = r.toLowerCase();
return t === "webp" ? "image/webp" : t === "png" ? "image/png" : t === "avif" ? "image/avif" : t === "jpg" || t === "jpeg" ? "image/jpeg" : t === "gif" ? "image/gif" : t === "svg" ? "image/svg+xml" : t === "bmp" ? "image/bmp" : t === "tif" || t === "tiff" ? "image/tiff" : t === "heif" ? "image/heif" : t === "heic" ? "image/heic" : t === "jp2" ? "image/jp2" : null;
}
function q(r) {
const t = r.split(",").map((s) => s.trim()).filter(Boolean);
for (const s of t) {
const e = (s.split(/\s+/)[0] || "").split(/[?#]/)[0].match(/\.([a-z0-9]+)$/i);
if (e && e[1]) {
const i = A(e[1]);
if (i) return i;
}
}
return null;
}
function B(r, t) {
const s = ee("sha256").update(r).digest("hex"), m = Math.max(1, Math.min(t, s.length));
return s.slice(0, m);
}
function z(r, t, s = "-") {
const m = v.parse(r), p = `${m.name}${s}${t}${m.ext}`;
return m.dir ? `${m.dir}/${p}` : p;
}
return {
name: "vite-plugin-single-image-format",
apply: "build",
enforce: "post",
async generateBundle(r, t) {
const s = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Set(), p = [];
for (const e of Object.values(t))
e.type === "asset" && W.some((i) => e.fileName.endsWith(i)) && p.push({ fileName: e.fileName, code: e.source.toString() });
if (p.length > 0)
for (const [e, i] of Object.entries(t)) {
if (i.type !== "asset" || !x.test(e)) continue;
let n = !1;
for (const c of p) {
const f = v.dirname(c.fileName), a = v.relative(f, e), o = /* @__PURE__ */ new Set();
o.add(e), o.add(a), !a.startsWith(".") && !a.startsWith("/") && o.add("./" + a);
for (const h of o) {
const l = `${h}?${F}`;
if (c.code.includes(l)) {
n = !0;
break;
}
}
if (n) break;
}
n && m.add(e);
}
for (const [e, i] of Object.entries(t)) {
if (i.type !== "asset" || !x.test(e)) continue;
const n = (typeof i.source == "string", Buffer.from(i.source));
if (m.has(e)) {
try {
const u = await N(n).metadata();
u.width && u.height && d.set(e, { width: u.width, height: u.height });
} catch (u) {
process?.env?.NODE_ENV === "development" && console.debug("[singleImageFormat] metadata probe failed (keep)", u);
}
continue;
}
const c = e.toLowerCase().endsWith(`.${b}`);
if (c && !H) {
let u;
try {
const g = await N(n).metadata();
g.width && g.height && (u = { width: g.width, height: g.height });
} catch (g) {
process?.env?.NODE_ENV === "development" && console.debug("[singleImageFormat] metadata probe failed (passthrough)", g);
}
if (!P) {
u && d.set(e, u);
continue;
}
const w = B(n, R), $ = z(e, w);
if (t[$]) {
u && d.set(e, u);
continue;
}
this.emitFile({ type: "asset", fileName: $, source: n }), s.set(e, $), u && d.set($, u), delete t[e];
continue;
}
const f = b === "webp" ? await N(n).webp(G).toBuffer() : b === "png" ? await N(n).png(V).toBuffer() : await N(n).avif(C).toBuffer(), a = await N(f).metadata(), o = a.width && a.height ? { width: a.width, height: a.height } : void 0;
if (c) {
i.source = f, o && d.set(e, o);
continue;
}
const h = e.replace(x, `.${b}`), l = P ? z(h, B(f, R)) : h;
if (t[l]) {
o && d.set(e, o), i.source = f;
continue;
}
this.emitFile({ type: "asset", fileName: l, source: f }), s.set(e, l), o && d.set(l, o), delete t[e];
}
if (s.size > 0)
for (const e of Object.values(t)) {
if (e.type !== "asset" || !W.some((c) => e.fileName.endsWith(c))) continue;
const i = v.dirname(e.fileName);
let n = e.source.toString();
for (const [c, f] of s) {
const a = v.relative(i, c), o = v.relative(i, f), h = [
[c, f],
[a, o]
];
!a.startsWith(".") && !a.startsWith("/") && h.push([`./${a}`, `./${o}`]);
for (const [l, u] of h) {
const w = new RegExp(
`(${j(l)})(\\?[^"'\\s)><#]*?)?(#[^"'\\s)><]*)?`,
"g"
);
n = n.replace(
w,
($, g, y, E) => `${u}${y ?? ""}${E ?? ""}`
);
}
}
e.source = n;
}
{
const e = Array.from(m);
if (e.length > 0)
for (const i of Object.values(t)) {
if (i.type !== "asset" || !W.some((f) => i.fileName.endsWith(f))) continue;
const n = v.dirname(i.fileName);
let c = i.source.toString();
for (const f of e) {
const a = v.relative(n, f), o = /* @__PURE__ */ new Set();
o.add(f), o.add(a), !a.startsWith(".") && !a.startsWith("/") && o.add("./" + a);
for (const h of o) {
const l = new RegExp(
`(${j(h)})(\\?[^"'\\s)><#]*?)?(#[^"'\\s)><]*)?`,
"g"
);
c = c.replace(
l,
(w, $, g, y) => {
if (!g) return $ + (y ?? "");
const E = Y((g || "") + (y || ""));
return $ + E;
}
);
const u = new RegExp(
`(${j(h)})\\?${F}(?![^"'\\s)><#])`,
"g"
);
c = c.replace(u, "$1");
}
}
i.source = c;
}
}
for (const e of Object.values(t)) {
if (e.type !== "asset" || !e.fileName.endsWith(".html")) continue;
const i = e.source.toString(), n = /<source\s+([^>]*?)srcset=(["'])([^"']+)\2([^>]*?)(\/?)>/gi, c = i.replace(
n,
(f, a, o, h, l, u) => {
const w = q(h);
if (!w) return f;
if (Z(a + l, "type")) {
const g = k(a, "type"), y = k(l, "type");
return `<source ${g}srcset=${o}${h}${o}${y} type="${w}"${u}>`;
}
return `<source ${a}srcset=${o}${h}${o}${l} type="${w}"${u}>`;
}
);
e.source = c;
}
if (S !== "off" && d.size > 0)
for (const e of Object.values(t)) {
if (e.type !== "asset" || !e.fileName.endsWith(".html")) continue;
const i = e.source.toString(), n = /<img\s+([^>]*?)src=(["'])([^"']+)\2([^>]*?)(\/?)>/gi, c = i.replace(
n,
(f, a, o, h, l, u) => X(f, a, o, h, l, u)
);
e.source = c;
}
}
};
}
export {
ne as default
};