From 62607f0f0b073ec74c6c15d796e7c1dc84fbf227 Mon Sep 17 00:00:00 2001 From: Hellings Date: Fri, 23 Oct 2026 08:53:07 -0400 Subject: [PATCH] Initial commit --- README.md | 1 + index.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 17 ++++++++++ 3 files changed, 106 insertions(+) create mode 100644 README.md create mode 100644 index.ts create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..da43184 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Build optimization \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..6e42db8 --- /dev/null +++ b/index.ts @@ -0,0 +1,88 @@ +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 }; + }, + }); + }, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..175d648 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "@muddle/optimize", + "version": "0.1.0", + "type": "module", + "exports": "./index.ts", + "dependencies": { + "terser": "^5.51.2", + "svgo": "^4.1.0", + "csso": "^5.0.5", + "html-minifier-terser": "^7.2.0", + "@types/csso": "^5.0.5", + "@types/html-minifier-terser": "^7.0.2" + }, + "peerDependencies": { + "muddle": "*" + } +}