#!/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' ], css: [ 'public/css/pico.jade.css' // 'public/css/pico.colors.min.css' ], safelist: { standard: [ 'container', 'banner-container', 'banner', 'banner-title', 'banner-subtitle', 'video-container', 'photo-gallery', 'photo-item', 'dark' ], 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$/, /^a:/, // Add anchor pseudo-classes like a:hover, a:focus /^a\./, // Add anchor with classes like a.secondary /^a\[/ // Add anchor with attributes like a[role=button] ] }, variables: true, keyframes: true }); // Step 1.5: Add essential rules 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);}`; // Add missing anchor tag primary color rule const anchorRule = `a:not([role=button]){--pico-color:var(--pico-primary);--pico-underline:var(--pico-primary-underline);background-color:transparent;color:var(--pico-color);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:.125em;transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);}a:not([role=button]):is(:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);text-decoration:underline;}`; // 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 + '\n' + anchorRule; 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 }); // Combine with custom site styles const customCSS = await fs.readFile('public/site-styles/style.css', 'utf8'); const combinedWithCustomCSS = minifiedCSS.css + '\n' + customCSS; // Minify the combined CSS bundle const finalMinifiedCSS = await postcss([ cssnano({ preset: ['default', { discardComments: { removeAll: true }, normalizeWhitespace: true }] }) ]).process(combinedWithCustomCSS, { from: undefined }); await fs.writeFile('dist/css/bundle.css', finalMinifiedCSS.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.jade.min.css')).size + (await fs.stat('public/css/pico.min.css')).size; const optimizedCSSSize = (await fs.stat('dist/css/bundle.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/bundle.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(//g, '') .replace(//g, '') .replace(/<\/body>/g, ''); 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 };