import type { Integration, IntegrationContext } from "muddle/contracts"; import * as terser from "terser"; import * as svgo from "svgo"; import * as csso from "csso"; import * as htmlMinifier from "html-minifier-terser"; export function minifyCss(source: string): string { return csso.minify(source).css; } export async function minifyHtml(source: string): Promise { return await htmlMinifier.minify(source, { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeEmptyAttributes: true, removeOptionalTags: true, }); } export async function minifyJavaScript(source: string): Promise { return ( (await terser.minify(source, { format: { comments: false } })).code ?? source ); } export function optimizeSvg(source: string): string { return svgo.optimize(source, { multipass: true }).data; } export interface ImageOptimizer { optimize(source: Uint8Array, type: string): Promise | Uint8Array; } export function imageOptimization(optimizer: ImageOptimizer): ImageOptimizer { return optimizer; } function loaderIntegration( name: string, extension: string, transform: (source: string) => string | Promise, ): Integration { return { name, async transform(asset) { if (!asset.path.endsWith(extension)) return asset; const source = typeof asset.content === "string" ? asset.content : new TextDecoder().decode(asset.content); return { ...asset, content: await transform(source) }; }, setup(context: IntegrationContext) { context.loader(name, { name, extensions: [extension], async load(file) { return { data: {}, content: await transform(file.source) }; }, }); }, }; } export function terserIntegration(): Integration { return loaderIntegration("terser", ".js", minifyJavaScript); } export function cssMinifyIntegration(): Integration { return loaderIntegration("css-minify", ".css", minifyCss); } export function htmlMinifyIntegration(): Integration { return loaderIntegration("html-minify", ".html", minifyHtml); } export function svgoIntegration(): Integration { return loaderIntegration("svgo", ".svg", optimizeSvg); } export function imageOptimizationIntegration( optimizer: ImageOptimizer, ): Integration { return { name: "image-optimization", async transform(asset) { if (!/\.(png|jpe?g|webp|avif)$/i.test(asset.path)) return asset; const input = typeof asset.content === "string" ? new TextEncoder().encode(asset.content) : asset.content; return { ...asset, content: await optimizer.optimize(input, asset.contentType), }; }, setup(context: IntegrationContext) { context.loader("image", { name: "image", extensions: [".png", ".jpg", ".jpeg", ".webp", ".avif"], async load(file) { const optimized = await optimizer.optimize( new TextEncoder().encode(file.source), "image/*", ); return { data: { optimized }, content: file.source }; }, }); }, }; }