#!/usr/bin/env node /** * CSS Build Script * Concatenates modular CSS files from src/styles/ into style.css * * Usage: * node scripts/build-css.js # Development build (readable) * node scripts/build-css.js --minify # Production build (minified) * * File order matters! Files are concatenated in alphabetical order. * Prefix files with numbers to control order: _01_variables.css, _02_panel.css, etc. */ import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, '..'); const stylesDir = path.join(rootDir, 'src', 'styles'); const outputFile = path.join(rootDir, 'style.css'); const shouldMinify = process.argv.includes('--minify'); /** * Minify CSS by removing comments, unnecessary whitespace, and redundant characters. * Safe for CSS custom properties, strings, and data URIs. */ function minifyCSS(css) { let result = css; // Remove CSS comments (/* ... */) — but not inside strings result = result.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove lines that are only whitespace after comment removal result = result.replace(/^\s*[\r\n]/g, '\n'); // Collapse multiple newlines into one result = result.replace(/\n{2,}/g, '\n'); // Remove spaces around structural characters outside of strings result = result.replace(/(\{|\}|;|:|,)/g, '$1'); // Remove space before { result = result.replace(/\s+\{/g, '{'); // Remove space after { result = result.replace(/\{\s+/g, '{'); // Remove space before } result = result.replace(/\s+\}/g, '}'); // Remove space after } result = result.replace(/\}\s+/g, '}'); // Remove space around : result = result.replace(/\s*:\s*/g, ':'); // Remove space around ; result = result.replace(/;\s*/g, ';'); // Remove space around , result = result.replace(/,\s*/g, ','); // Remove trailing semicolon before } result = result.replace(/;\}/g, '}'); // Collapse multiple spaces into one (preserves spaces inside strings roughly) result = result.replace(/[ \t]+/g, ' '); // Remove leading/trailing whitespace result = result.trim(); return result; } async function buildCSS() { console.log(`Building CSS from src/styles/...${shouldMinify ? ' (minified)' : ' (readable)'}`); // Read all CSS files from src/styles/ const files = await fs.readdir(stylesDir); const cssFiles = files .filter(f => f.endsWith('.css')) .sort() // Alphabetical order (use _01_, _02_ prefix for control) .map(f => path.join(stylesDir, f)); if (cssFiles.length === 0) { console.error('No CSS files found in src/styles/'); process.exit(1); } console.log(`Found ${cssFiles.length} CSS files:`); cssFiles.forEach(f => console.log(` - ${path.basename(f)}`)); // Read and concatenate all files const contents = []; for (const file of cssFiles) { const content = await fs.readFile(file, 'utf-8'); const basename = path.basename(file); if (shouldMinify) { // In minified mode, skip comment separators contents.push(content); } else { contents.push(`/* ============================================ */`); contents.push(`/* ${basename} */`); contents.push(`/* ============================================ */`); contents.push(''); contents.push(content); contents.push(''); } } let combined = contents.join('\n'); // Apply minification if requested if (shouldMinify) { const beforeSize = combined.length; combined = minifyCSS(combined); const afterSize = combined.length; const reduction = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1); console.log(`\nšŸ—œļø Minification: ${beforeSize.toLocaleString()} → ${afterSize.toLocaleString()} bytes (${reduction}% reduction)`); } // Write to style.css await fs.writeFile(outputFile, combined, 'utf-8'); const stats = await fs.stat(outputFile); const lines = combined.split('\n').length; const sizeKB = (stats.size / 1024).toFixed(1); console.log(`\nāœ… Built ${outputFile}`); console.log(` Lines: ${lines}`); console.log(` Size: ${sizeKB} KB`); } buildCSS().catch(err => { console.error('Build failed:', err); process.exit(1); });