vite-plugin-singlefile-compression
Version:
Compress all assets and embeds them into dist/index.html, making it convenient to share as a single HTML file.
459 lines (458 loc) • 18.8 kB
JavaScript
import pc from "picocolors";
import { minify } from "html-minifier-terser";
import { JSDOM } from "jsdom";
import path from "path";
import fs from "fs";
import { pathToFileURL } from "url";
import base128 from "base128-ascii";
import zlib from "zlib";
import svgToTinyDataUri from "mini-svg-data-uri";
import { lookup } from "mrmime";
//#region package.json
var name = "vite-plugin-singlefile-compression";
var version = "2.4.8";
//#endregion
//#region src/compress.ts
const compressors = {
"deflate-raw"(script) {
return zlib.deflateRawSync(script, { level: zlib.constants.Z_BEST_COMPRESSION });
},
deflate(script) {
return zlib.deflateSync(script, { level: zlib.constants.Z_BEST_COMPRESSION });
},
gzip(script) {
return zlib.gzipSync(script, { level: zlib.constants.Z_BEST_COMPRESSION });
},
brotli: zlib.brotliCompressSync,
zstd: zlib.zstdCompressSync && function(script) {
return zlib.zstdCompressSync(script, { params: { [zlib.constants.ZSTD_c_compressionLevel]: 19 } });
}
};
const compressFormatAlias = {
deflateRaw: "deflate-raw",
gz: "gzip",
br: "brotli",
brotliCompress: "brotli",
zstandard: "zstd",
zst: "zstd"
};
function switchCompressor(format) {
if (compressors.hasOwnProperty(format)) {
const f = compressors[format];
if (f) return f;
throw Error(`Could not get compressor: Please upgrade node.js or set your compressor function.`);
}
{
const _format = format.replace(/-([a-zA-Z])/g, (m, a) => a.toUpperCase());
for (const funcName of [_format + "CompressSync", _format + "Sync"]) if (Object.prototype.hasOwnProperty.call(zlib, funcName)) {
const f = zlib[funcName];
if (typeof f == "function") return f;
}
}
try {
const cs = new CompressionStream(format);
return (script) => new Response(new Response(script).body.pipeThrough(cs)).bytes();
} catch {}
throw Error(`Could not get compressor: Unknown compress format '${format}', please set your compressor function.`);
}
function compress(format, script, compressor) {
if (typeof compressor != "function") compressor = switchCompressor(format);
return compressor(script);
}
//#endregion
//#region _dist/templateRaw.ts
const files = {
"assets": ["{let e=", ";for(let l in e)for(let r of document.querySelectorAll(`[src=\"data:${l}\"]`))r.src=e[l]}"],
"base128": [
"{let e=Response,_=document,n=_.createElement(\"script\"),t=",
",r=",
",o=new Uint8Array(",
"),p=0,d=0,T,a,h=e=>(a=t.charCodeAt(p++))>>7?a=0:a;for(n.type=\"module\";p<",
";o[d++]=a<<8-T|h()>>--T)T||h(T=7);new e(new e(o).body.pipeThrough(new DecompressionStream(",
"))).text().then(e=>{n.innerHTML=e,_.head.appendChild(n)})}"
],
"base64": [
"fetch(\"data:;base64,",
"\").then(e=>new Response(e.body.pipeThrough(new DecompressionStream(",
"))).text()).then(e=>{var t=document,n=t.createElement(\"script\");n.type=\"module\",n.innerHTML=e,t.head.appendChild(n)})"
],
"importmeta": ["{let e=import.meta,t=e.resolve,r=e.url=new URL(", ",location).href;e.resolve=function(e){return/^\\.{0,2}\\//.test(e)?new URL(e,r).href:t.apply(this,arguments)}}"]
};
//#endregion
//#region src/to-base64.ts
const toBase64 = typeof Uint8Array.prototype.toBase64 == "function" ? (bytes) => Uint8Array.prototype.toBase64.call(bytes) : (bytes) => Buffer.prototype.base64Slice.call(bytes, 0, bytes.length);
//#endregion
//#region src/getTemplate.ts
/** base128 const inputLen string */
const base128ConstInputLenStr = files.base128[0].slice(-2, -1) + ".length";
/** base128 inputLen variable name */
const base128InputLenVarName = files.base128[1].slice(-2, -1);
/** base128 const outLen string */
const base128ConstOutLenStr = base128InputLenVarName + "/8*7";
const template = {
async base(script, format, useBase128, compressor) {
const compressedBytes = await compress(format, script, compressor);
const _format_ = JSON.stringify(format);
if (useBase128) {
const t = files.base128;
const b128Result = base128.encode(compressedBytes);
const b128Str = b128Result.toJSTemplateLiterals();
/** input length string */
const inputLenStr = b128Result.bytes.length.toString();
/** out length string */
const outLenStr = compressedBytes.length.toString();
if (inputLenStr.length + outLenStr.length > 17) return t[0].concat(b128Str, t[1], base128ConstInputLenStr, t[2], base128ConstOutLenStr, t[3], base128InputLenVarName, t[4], _format_, t[5]);
return t[0].concat(b128Str, t[2], outLenStr, t[3], inputLenStr, t[4], _format_, t[5]);
}
const t = files.base64;
const b64 = toBase64(compressedBytes);
return t[0].concat(b64, t[1], _format_, t[2]);
},
assets(assetsJSON) {
const t = files.assets;
return t[0].concat(JSON.stringify(assetsJSON), t[1]);
},
importmeta(p) {
const t = files.importmeta;
return t[0].concat(JSON.stringify(p), t[1]);
}
};
//#endregion
//#region src/encoding-api.ts
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
//#endregion
//#region src/dataurl.ts
function toDataURL(name, source) {
if (/\.svg$/i.test(name)) {
if (typeof source != "string") source = textDecoder.decode(source);
return svgToTinyDataUri(source);
}
if (typeof source == "string") source = textEncoder.encode(source);
return `data:${lookup(name)};base64,${toBase64(source)}`;
}
function toDataURL_andGetByteLength(name, source) {
if (/\.svg$/i.test(name)) {
if (typeof source == "string") return {
dataURL: svgToTinyDataUri(source),
byteLength: Buffer.byteLength(source)
};
return {
dataURL: svgToTinyDataUri(textDecoder.decode(source)),
byteLength: source.byteLength
};
}
if (typeof source == "string") source = textEncoder.encode(source);
return {
dataURL: `data:${lookup(name)};base64,${toBase64(source)}`,
byteLength: source.byteLength
};
}
//#endregion
//#region src/kB.ts
function kB(size) {
const s = String(size).padStart(4, "0");
return `${s.slice(0, -3)}.${s.slice(-3)} kB`;
}
//#endregion
//#region src/options.ts
const defaultHtmlMinifierTerserOptions = {
removeAttributeQuotes: true,
removeComments: true,
collapseWhitespace: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
minifyJS: false
};
function getInnerOptions(opt) {
opt ||= {};
const enableCompress = opt.enableCompress ?? true;
return {
rename: opt.rename == null ? void 0 : String(opt.rename),
enableCompress,
useBase128: opt.useBase128 ?? true,
compressFormat: opt.compressFormat ? compressFormatAlias.hasOwnProperty(opt.compressFormat) ? compressFormatAlias[opt.compressFormat] : String(opt.compressFormat) : "deflate-raw",
compressor: typeof opt.compressor == "function" ? opt.compressor : void 0,
htmlMinifierTerser: opt.htmlMinifierTerser == null || opt.htmlMinifierTerser === true ? defaultHtmlMinifierTerserOptions : opt.htmlMinifierTerser,
tryInlineHtmlAssets: opt.tryInlineHtmlAssets ?? true,
removeInlinedAssetFiles: opt.removeInlinedAssetFiles ?? true,
tryInlineHtmlPublicIcon: opt.tryInlineHtmlPublicIcon ?? true,
removeInlinedPublicIconFiles: opt.removeInlinedPublicIconFiles ?? true,
enableCompressInlinedIcon: enableCompress && (opt.enableCompressInlinedIcon ?? false),
useImportMetaPolyfill: opt.useImportMetaPolyfill ?? false,
quiet: opt.quiet ?? false
};
}
//#endregion
//#region src/cutPrefix.ts
function cutPrefix(str, prefix) {
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
}
//#endregion
//#region src/source-to-string.ts
function sourceToString(source) {
return typeof source == "string" ? source : textDecoder.decode(source);
}
//#endregion
//#region src/has-substring.ts
function hasSubstring(str, sub) {
const i = str.indexOf(sub);
if (i === -1) return 0;
if (i === str.lastIndexOf(sub)) return 1;
return 2;
}
//#endregion
//#region src/index.ts
function singleFileCompression(opt) {
let conf;
const innerOptions = getInnerOptions(opt);
return {
name,
enforce: "post",
config(...args) {
return setConfig.call(this, innerOptions, ...args);
},
configResolved(c) {
conf = c;
},
generateBundle(outputOptions, bundle, isWrite) {
return generateBundle.call(this, bundle, conf, innerOptions);
}
};
}
function setConfig(opt, config, env) {
config.base ??= "./";
const build = config.build ??= {};
build.cssCodeSplit ??= false;
build.assetsInlineLimit ??= () => true;
build.modulePreload ?? build.polyfillModulePreload ?? (build.modulePreload = { polyfill: false });
if (this.meta.rolldownVersion) {
const rolldownOptions = build.rolldownOptions ?? build.rollupOptions ?? (build.rolldownOptions = {});
for (const output of [rolldownOptions.output ??= {}].flat(1)) output.codeSplitting ?? output.inlineDynamicImports ?? (output.codeSplitting = false);
} else {
const rollupOptions = build.rollupOptions ??= {};
for (const output of [rollupOptions.output ??= {}].flat(1)) output.inlineDynamicImports ??= true;
}
}
async function generateBundle(bundle, config, options) {
if (!options.quiet) console.log(pc.reset("\n\n") + pc.cyan(name + " " + version) + " " + (options.enableCompress ? pc.green(options.compressFormat + " " + (options.useBase128 ? "base128-ascii" : "base64")) : pc.red("disable-compress")));
if (options.rename && options.rename != "index.html" && Object.prototype.hasOwnProperty.call(bundle, "index.html") && !Object.prototype.hasOwnProperty.call(bundle, options.rename)) {
bundle[options.rename] = bundle["index.html"];
bundle[options.rename].fileName = options.rename;
delete bundle["index.html"];
}
let fakeScript = "";
const regenerateFakeScript = () => {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$0123456789";
fakeScript = alphabet.charAt(Math.random() * 54);
for (let i = 1; i < 8; i++) fakeScript += alphabet.charAt(Math.random() * 64);
fakeScript += "()";
};
regenerateFakeScript();
const distURL = pathToFileURL(config.build.outDir).href + "/";
/** "assets/" */
const assetsDir = path.posix.join(config.build.assetsDir, "/");
/** "./assets/" */
const assetsDirWithBase = config.base + assetsDir;
/** '[href^="./assets/"]' */
const assetsHrefSelector = `[href^="${assetsDirWithBase}"]`;
/** '[src^="./assets/"]' */
const assetsSrcSelector = `[src^="${assetsDirWithBase}"]`;
const globalDelete = /* @__PURE__ */ new Set();
const globalDoNotDelete = /* @__PURE__ */ new Set();
const globalRemoveDistFileNames = /* @__PURE__ */ new Set();
const globalAssetsDataURLCache = {};
const globalPublicFilesCache = {};
/** format: ["assets/index-XXXXXXXX.js"] */
const bundleAssetsNames = [];
/** format: ["index.html"] */
const bundleHTMLNames = [];
for (const name in bundle) if (name.startsWith(assetsDir)) bundleAssetsNames.push(name);
else if (/\.html$/i.test(name)) bundleHTMLNames.push(name);
for (const htmlFileName of bundleHTMLNames) {
if (!options.quiet) console.log("\n " + pc.underline(pc.cyan(distURL) + pc.greenBright(bundle[htmlFileName].fileName)));
const htmlChunk = bundle[htmlFileName], oldHTML = sourceToString(htmlChunk.source), dom = new JSDOM(oldHTML), document = dom.window.document, thisDel = /* @__PURE__ */ new Set(), newJSCode = [], scriptElement = document.querySelector(`script[type=module]${assetsSrcSelector}`), scriptName = scriptElement ? cutPrefix(scriptElement.getAttribute("src"), config.base) : "", compressHeadElements = [];
let oldSize = options.quiet ? NaN : Buffer.byteLength(oldHTML);
scriptElement?.remove();
let allCSS = "";
const linkStylesheet = document.querySelectorAll(`link[rel=stylesheet]${assetsHrefSelector}`);
for (const element of linkStylesheet) {
const name = cutPrefix(element.href, config.base);
thisDel.add(name);
const css = bundle[name];
const cssSource = sourceToString(css.source);
if (cssSource) {
if (!options.quiet) oldSize += Buffer.byteLength(cssSource);
for (const name of bundleAssetsNames) if (cssSource.includes(name.slice(assetsDir.length))) globalDoNotDelete.add(name);
allCSS += cssSource.replace(/(\s*\/\*([^*]|\*(?!\/))*\*\/)*\s*$/, "");
}
if (options.enableCompress) element.remove();
}
if (allCSS) {
const e = document.createElement("style");
e.innerHTML = allCSS;
if (options.enableCompress) compressHeadElements.push(e);
else {
linkStylesheet[0].before(e);
for (const e of linkStylesheet) e.remove();
}
}
const assetsDataURL = {};
if (options.tryInlineHtmlAssets) for (const element of document.querySelectorAll(assetsSrcSelector)) {
const name = cutPrefix(element.src, assetsDirWithBase);
if (/\.js$/i.test(name)) continue;
if (!options.enableCompress || !Object.prototype.hasOwnProperty.call(assetsDataURL, name)) {
const bundleName = assetsDir + name;
const a = bundle[bundleName];
if (!a) continue;
thisDel.add(bundleName);
let dataURL;
if (Object.prototype.hasOwnProperty.call(globalAssetsDataURLCache, name)) {
const cache = globalAssetsDataURLCache[name];
if (!options.quiet) oldSize += cache.byteLength;
dataURL = cache.dataURL;
} else if (options.quiet) {
dataURL = toDataURL(name, a.source);
globalAssetsDataURLCache[name] = {
dataURL,
byteLength: NaN
};
} else {
const result = toDataURL_andGetByteLength(name, a.source);
oldSize += result.byteLength;
dataURL = result.dataURL;
globalAssetsDataURLCache[name] = result;
}
if (options.enableCompress) assetsDataURL[name] = dataURL;
else element.src = dataURL;
}
if (options.enableCompress) element.src = `data:${name}`;
}
const createIconElement = (href) => {
const e = document.createElement("link");
e.rel = "icon";
if (href != null) e.href = href;
return e;
};
const getPublicIcon = (faviconName) => {
if (Object.prototype.hasOwnProperty.call(globalPublicFilesCache, faviconName)) return globalPublicFilesCache[faviconName];
let _path = path.join(config.build.outDir, faviconName);
if (fs.existsSync(_path)) globalRemoveDistFileNames.add(faviconName);
else {
_path = path.join(config.publicDir, faviconName);
if (!fs.existsSync(_path)) return null;
}
const b = fs.readFileSync(_path);
return globalPublicFilesCache[faviconName] = {
buffer: b,
dataURL: toDataURL(faviconName, b),
size: b.length
};
};
const linkFaviconAll = document.querySelectorAll(`link[rel=icon][href]:not([href=""]),link[rel="shortcut icon"][href]:not([href=""])`);
if (linkFaviconAll.length == 0) {
if (options.tryInlineHtmlPublicIcon) {
const fileCache = getPublicIcon("favicon.ico");
if (fileCache) {
if (!options.quiet) oldSize += fileCache.size;
const e = createIconElement(fileCache.dataURL);
if (options.enableCompressInlinedIcon) compressHeadElements.push(e);
else document.head.appendChild(e);
}
}
} else for (const linkFavicon of linkFaviconAll) {
let faviconName = linkFavicon.href;
const faviconIsDataURL = /^data:/i.test(faviconName);
if (!faviconIsDataURL) faviconName = cutPrefix(faviconName, config.base);
const setFaviconDataURL = (dataURL) => {
if (options.enableCompressInlinedIcon) if (linkFavicon) {
linkFavicon.remove();
linkFavicon.href = dataURL;
compressHeadElements.push(linkFavicon);
} else compressHeadElements.push(createIconElement(dataURL));
else if (linkFavicon) linkFavicon.href = dataURL;
else document.head.appendChild(createIconElement(dataURL));
};
if (faviconIsDataURL) {
if (options.enableCompressInlinedIcon) {
linkFavicon.remove();
compressHeadElements.push(linkFavicon);
}
} else if (bundleAssetsNames.includes(faviconName)) {
const asset = bundle[faviconName];
if (asset) {
setFaviconDataURL(toDataURL(faviconName, asset.source));
thisDel.add(faviconName);
}
} else if (options.tryInlineHtmlPublicIcon) {
const fileCache = getPublicIcon(faviconName);
if (fileCache) {
if (!options.quiet) oldSize += fileCache.size;
setFaviconDataURL(fileCache.dataURL);
}
}
}
if (compressHeadElements.length) {
const html = compressHeadElements.map((v) => v.outerHTML).join("");
newJSCode.push(`document.head.insertAdjacentHTML("beforeend",${JSON.stringify(html)})`);
}
let newHTML = "";
const fakeScriptElement = document.body.appendChild(document.createElement("script"));
for (;;) {
fakeScriptElement.innerHTML = fakeScript;
newHTML = dom.serialize();
const has = hasSubstring(newHTML, fakeScript);
if (has === 0) throw Error(`Failed to inject fakeScript`);
if (has === 1) {
if (!options.htmlMinifierTerser) break;
newHTML = await minify(newHTML, options.htmlMinifierTerser);
const has = hasSubstring(newHTML, fakeScript);
if (has === 0) throw Error(`Failed to inject fakeScript`);
if (has === 1) break;
}
regenerateFakeScript();
}
function inlineHtmlAssets() {
if (options.tryInlineHtmlAssets) {
const assetsJSON = JSON.stringify(assetsDataURL);
if (assetsJSON !== "{}") newJSCode.push(template.assets(assetsJSON));
}
}
if (scriptElement) {
thisDel.add(scriptName);
let { code } = bundle[scriptName];
if (!options.quiet) oldSize += Buffer.byteLength(code);
code = code.replace(/;?\s*$/, "");
for (const name of bundleAssetsNames) {
const assetName = name.slice(assetsDir.length);
if (code.includes(assetName)) globalDoNotDelete.add(name);
}
inlineHtmlAssets();
if (options.useImportMetaPolyfill) newJSCode.push(template.importmeta(scriptName));
if (/\b__VITE_PRELOAD__\b/.test(code)) if (code.startsWith("var ")) code = "var __VITE_PRELOAD__," + code.slice(4);
else newJSCode.push("var __VITE_PRELOAD__");
newJSCode.push(code);
} else inlineHtmlAssets();
let outputScript = newJSCode.join(";").replaceAll("<\/script", "<\\/script");
if (options.enableCompress) outputScript = await template.base(outputScript, options.compressFormat, options.useBase128, options.compressor);
htmlChunk.source = newHTML = newHTML.replace(fakeScript, () => outputScript);
if (!options.quiet) console.log(" " + pc.gray(kB(oldSize) + " -> ") + pc.cyanBright(kB(Buffer.byteLength(newHTML))) + "\n");
for (const name of thisDel) globalDelete.add(name);
}
if (options.removeInlinedAssetFiles) {
for (const name of globalDelete) if (!globalDoNotDelete.has(name)) delete bundle[name];
}
if (options.removeInlinedPublicIconFiles) {
const { outDir } = config.build;
const mustStartsWith = path.resolve(outDir) + path.sep;
for (const name of globalRemoveDistFileNames) try {
const _path = path.resolve(outDir, name);
if (_path.startsWith(mustStartsWith)) fs.rmSync(_path, { force: true });
} catch (e) {
if (!options.quiet) console.error(e);
}
}
if (!options.quiet) console.log(pc.green("Finish.") + pc.reset("\n"));
}
//#endregion
export { singleFileCompression as default, singleFileCompression, defaultHtmlMinifierTerserOptions };