UNPKG

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
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 };