first template launch
This commit is contained in:
298
minify-build.js
Normal file
298
minify-build.js
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user