Files
Keyboard-Vagabond-Web/minify-build.js

299 lines
11 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Enhanced Build Script with Minification for Keyboard Vagabond
*
* This script:
* 1. Optimizes CSS with PurgeCSS
* 2. Minifies CSS with cssnano
* 3. Bundles and minifies JavaScript with Terser
* 4. Minifies HTML with html-minifier-terser
* 5. Creates a production-ready dist/ folder
*/
const { PurgeCSS } = require('purgecss');
const cssnano = require('cssnano');
const postcss = require('postcss');
const { minify: htmlMinify } = require('html-minifier-terser');
const { minify: jsMinify } = require('terser');
const fs = require('fs').promises;
const path = require('path');
async function createOptimizedBuild() {
console.log('🚀 Starting enhanced build with minification...');
// Clean and create dist directory
await fs.rm('dist', { recursive: true, force: true });
await fs.mkdir('dist', { recursive: true });
await fs.mkdir('dist/css', { recursive: true });
await fs.mkdir('dist/site-styles', { recursive: true });
await fs.mkdir('dist/assets', { recursive: true });
try {
// Step 1: CSS Optimization with PurgeCSS
console.log('📝 Step 1: Optimizing CSS with PurgeCSS...');
const purgeResults = await new PurgeCSS().purge({
content: [
'public/index.html',
'public/about.html',
'index.html'
],
css: [
'public/css/pico.green.min.css',
'public/css/pico.colors.min.css'
],
safelist: {
standard: [
'container',
'banner-container',
'banner',
'banner-title',
'banner-subtitle',
'video-container',
'photo-gallery',
'photo-item'
],
deep: [
/^--pico-/,
/\[data-theme/,
/data-theme/,
/:hover/,
/:focus/,
/:active/,
/:visited/
],
// Keep base typography selectors that are essential for proper font rendering
greedy: [
/^:where\(:root\)$/,
/^:where\(:host\)$/,
/^:root$/,
/^:host$/,
/^body$/,
/^html$/,
/^h[1-6]$/,
/^p$/,
/^ul$/,
/^li$/,
/^strong$/,
/^a$/
]
},
variables: true,
keyframes: true
});
// Step 1.5: Add essential font rule that PurgeCSS removes too aggressively
const fontRule = `:where(:host),:where(:root){background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);}`;
// Step 2: CSS Minification with cssnano
console.log('🗜️ Step 2: Minifying CSS with cssnano...');
const combinedCSS = purgeResults.map(result => result.css).join('\n\n') + '\n' + fontRule;
const minifiedCSS = await postcss([
cssnano({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
minifySelectors: true,
minifyParams: true,
mergeLonghand: true,
mergeRules: true
}]
})
]).process(combinedCSS, { from: undefined });
await fs.writeFile('dist/css/styles.css', minifiedCSS.css);
// Also minify custom site styles
const customCSS = await fs.readFile('public/site-styles/style.css', 'utf8');
const minifiedCustomCSS = await postcss([
cssnano({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true
}]
})
]).process(customCSS, { from: undefined });
await fs.writeFile('dist/site-styles/style.css', minifiedCustomCSS.css);
// Step 3: JavaScript Bundling and Minification
console.log('📦 Step 3: Bundling and minifying JavaScript...');
await bundleAndMinifyJS();
// Step 4: HTML Minification
console.log('📄 Step 4: Minifying HTML...');
await minifyHTMLFile('public/index.html', 'dist/index.html');
await minifyHTMLFile('public/about.html', 'dist/about.html');
// Step 5: Copy assets
console.log('📁 Step 5: Copying assets...');
try {
const assets = await fs.readdir('public/assets');
for (const asset of assets) {
await fs.copyFile(
path.join('public/assets', asset),
path.join('dist/assets', asset)
);
}
} catch (err) {
console.log(' No assets directory found, skipping...');
}
// Calculate compression results
const originalCSSSize = (await fs.stat('public/css/pico.green.min.css')).size +
(await fs.stat('public/css/pico.colors.min.css')).size;
const optimizedCSSSize = (await fs.stat('dist/css/styles.css')).size;
const cssReduction = ((originalCSSSize - optimizedCSSSize) / originalCSSSize * 100).toFixed(1);
const originalHTMLSize = (await fs.stat('public/index.html')).size +
(await fs.stat('public/about.html')).size;
const minifiedHTMLSize = (await fs.stat('dist/index.html')).size +
(await fs.stat('dist/about.html')).size;
const htmlReduction = ((originalHTMLSize - minifiedHTMLSize) / originalHTMLSize * 100).toFixed(1);
// Calculate JavaScript size if bundled
let jsInfo = '';
try {
const bundledJSSize = (await fs.stat('dist/scripts/scripts.js')).size;
jsInfo = `\n JavaScript: Bundled into ${(bundledJSSize / 1024).toFixed(1)}KB`;
} catch (err) {
// No JavaScript files were bundled
}
console.log('\n✨ Build complete with minification!');
console.log('\n📊 Optimization Results:');
console.log(` CSS: ${(originalCSSSize / 1024).toFixed(1)}KB → ${(optimizedCSSSize / 1024).toFixed(1)}KB (${cssReduction}% reduction)`);
console.log(` HTML: ${(originalHTMLSize / 1024).toFixed(1)}KB → ${(minifiedHTMLSize / 1024).toFixed(1)}KB (${htmlReduction}% reduction)`);
if (jsInfo) console.log(jsInfo);
console.log(` Total CSS+HTML savings: ${((originalCSSSize + originalHTMLSize - optimizedCSSSize - minifiedHTMLSize) / 1024).toFixed(1)}KB`);
console.log('\n📂 Production files created in dist/');
console.log('🚀 Ready for deployment!');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
async function bundleAndMinifyJS() {
// Create scripts directory
await fs.mkdir('dist/scripts', { recursive: true });
// Find all JavaScript files in site-scripts directory
const jsFiles = [];
try {
const siteScriptsDir = 'public/site-scripts';
const files = await fs.readdir(siteScriptsDir);
for (const file of files) {
if (file.endsWith('.js')) {
jsFiles.push(path.join(siteScriptsDir, file));
}
}
} catch (err) {
console.log(' No site-scripts directory found, skipping...');
return;
}
if (jsFiles.length === 0) {
console.log(' No JavaScript files found to bundle');
return;
}
console.log(` Found ${jsFiles.length} JavaScript file(s) to bundle:`);
jsFiles.forEach(file => console.log(` - ${path.basename(file)}`));
// Read and concatenate all JavaScript files
let combinedJS = '';
for (const jsFile of jsFiles) {
const content = await fs.readFile(jsFile, 'utf8');
combinedJS += `\n// File: ${path.basename(jsFile)}\n`;
combinedJS += content;
combinedJS += '\n';
}
// Minify the combined JavaScript
try {
const minified = await jsMinify(combinedJS, {
compress: {
drop_console: false, // Keep console.log for debugging if needed
drop_debugger: true,
dead_code: true,
conditionals: true,
evaluate: true,
booleans: true,
loops: true,
unused: true,
hoist_funs: true,
keep_fargs: false,
hoist_vars: false,
if_return: true,
join_vars: true,
cascade: true,
side_effects: true
},
mangle: {
reserved: ['theme', 'toggle', 'icon'] // Preserve important function names
},
format: {
comments: false
}
});
await fs.writeFile('dist/scripts/bundle.js', minified.code);
// Calculate compression ratio
const originalSize = Buffer.byteLength(combinedJS, 'utf8');
const minifiedSize = Buffer.byteLength(minified.code, 'utf8');
const reduction = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
console.log(` ✅ JavaScript bundled: ${(originalSize / 1024).toFixed(1)}KB → ${(minifiedSize / 1024).toFixed(1)}KB (${reduction}% reduction)`);
} catch (error) {
console.error(' ❌ JavaScript minification failed:', error);
// Fallback: write unminified version
await fs.writeFile('dist/scripts/bundle.js', combinedJS);
console.log(' ✅ JavaScript bundled (unminified fallback)');
}
}
async function minifyHTMLFile(inputPath, outputPath) {
const html = await fs.readFile(inputPath, 'utf8');
// Update CSS references for production and add script reference
let updatedHTML = html
.replace(/<link rel="stylesheet" href="css\/pico\.green\.min\.css">/g, '')
.replace(/<link rel="stylesheet" href="css\/pico\.colors\.min\.css">/g, '')
.replace(/<link rel="stylesheet" href="site-styles\/style\.css">/g,
'<link rel="stylesheet" href="css/styles.css"><link rel="stylesheet" href="site-styles/style.css">')
.replace(/<\/body>/g, '<script src="scripts/scripts.js"></script></body>');
const minified = await htmlMinify(updatedHTML, {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: true,
useShortDoctype: true,
removeAttributeQuotes: false, // Keep for readability
removeEmptyAttributes: true,
sortAttributes: true,
sortClassName: true
});
await fs.writeFile(outputPath, minified);
console.log(`${path.basename(inputPath)}${path.basename(outputPath)}`);
}
// Run if called directly
if (require.main === module) {
createOptimizedBuild();
}
module.exports = { createOptimizedBuild };