105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Minify all public JS, CSS, and HTML assets in-place.
|
|
* Uses terser (JS), clean-css (CSS), html-minifier-terser (HTML).
|
|
*
|
|
* Usage: bun run scripts/minify-public.ts [--dir public]
|
|
*/
|
|
import { readdir, stat } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { minify as terserMinify } from "terser";
|
|
import CleanCSS from "clean-css";
|
|
import { minify as htmlMinify } from "html-minifier-terser";
|
|
|
|
const args = process.argv.slice(2);
|
|
let publicDir = path.resolve("public");
|
|
const dirIdx = args.indexOf("--dir");
|
|
if (dirIdx !== -1 && args[dirIdx + 1]) {
|
|
publicDir = path.resolve(args[dirIdx + 1]);
|
|
}
|
|
|
|
const cleanCss = new CleanCSS({ level: 2 });
|
|
|
|
const htmlOpts = {
|
|
collapseWhitespace: true,
|
|
removeComments: true,
|
|
removeRedundantAttributes: true,
|
|
removeEmptyAttributes: true,
|
|
minifyCSS: true,
|
|
minifyJS: true,
|
|
sortAttributes: true,
|
|
sortClassName: true,
|
|
};
|
|
|
|
type Stats = { js: number; css: number; html: number; savedBytes: number };
|
|
const stats: Stats = { js: 0, css: 0, html: 0, savedBytes: 0 };
|
|
|
|
async function collectFiles(dir: string, exts: Set<string>): Promise<string[]> {
|
|
const results: string[] = [];
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
for (const e of entries) {
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) {
|
|
// Skip vendor — already distributed minified
|
|
if (e.name === "vendor") continue;
|
|
results.push(...(await collectFiles(full, exts)));
|
|
} else if (exts.has(path.extname(e.name).toLowerCase())) {
|
|
results.push(full);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async function minifyJS(filePath: string) {
|
|
const src = await Bun.file(filePath).text();
|
|
const result = await terserMinify(src, {
|
|
compress: { passes: 2, drop_console: false, ecma: 2020 },
|
|
mangle: { toplevel: false },
|
|
format: { ecma: 2020 },
|
|
module: true,
|
|
});
|
|
if (result.code && result.code.length < src.length) {
|
|
stats.savedBytes += src.length - result.code.length;
|
|
await Bun.write(filePath, result.code);
|
|
}
|
|
stats.js++;
|
|
}
|
|
|
|
async function minifyCSS(filePath: string) {
|
|
const src = await Bun.file(filePath).text();
|
|
const result = cleanCss.minify(src);
|
|
if (result.styles && result.styles.length < src.length) {
|
|
stats.savedBytes += src.length - result.styles.length;
|
|
await Bun.write(filePath, result.styles);
|
|
}
|
|
stats.css++;
|
|
}
|
|
|
|
async function minifyHTML(filePath: string) {
|
|
const src = await Bun.file(filePath).text();
|
|
const result = await htmlMinify(src, htmlOpts);
|
|
if (result.length < src.length) {
|
|
stats.savedBytes += src.length - result.length;
|
|
await Bun.write(filePath, result);
|
|
}
|
|
stats.html++;
|
|
}
|
|
|
|
// Collect all files
|
|
const jsFiles = await collectFiles(path.join(publicDir, "assets"), new Set([".js"]));
|
|
const cssFiles = await collectFiles(path.join(publicDir, "assets"), new Set([".css"]));
|
|
const htmlFiles = await collectFiles(publicDir, new Set([".html"]));
|
|
|
|
console.log(`Minifying ${jsFiles.length} JS, ${cssFiles.length} CSS, ${htmlFiles.length} HTML files...`);
|
|
|
|
// Process in parallel batches
|
|
await Promise.all([
|
|
...jsFiles.map((f) => minifyJS(f).catch((e) => console.error(`JS error ${f}: ${e.message}`))),
|
|
...cssFiles.map((f) => minifyCSS(f).catch((e) => console.error(`CSS error ${f}: ${e.message}`))),
|
|
...htmlFiles.map((f) => minifyHTML(f).catch((e) => console.error(`HTML error ${f}: ${e.message}`))),
|
|
]);
|
|
|
|
console.log(
|
|
`Done: ${stats.js} JS, ${stats.css} CSS, ${stats.html} HTML — saved ${(stats.savedBytes / 1024).toFixed(1)} KB`,
|
|
);
|