switch to bundle.css Co-authored-by: Michael DiLeo <michael.dileo@oakstreethealth.com> Reviewed-on: #13
305 lines
12 KiB
JavaScript
305 lines
12 KiB
JavaScript
#!/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(/<link rel="stylesheet" href="css\/pico\.jade\.min\.css">/g, '')
|
|
.replace(/<link rel="stylesheet" href="site-styles\/style\.css">/g,
|
|
'<link rel="stylesheet" href="css/bundle.css">')
|
|
.replace(/<\/body>/g, '<script src="scripts/bundle.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 };
|