Files
ARIA df7d3b3a38 Fixes #13: CSS performance optimizations
- Add CSS minification support to build script (--minify flag)
  - Pure JS minifier: removes comments, whitespace, redundant chars
  - Reports size reduction percentage
  - Preserves CSS custom properties, data URIs, strings

- Refactor _08_themes.css: extend CSS custom properties pattern
  - Define theme-specific properties per theme (--rpg-content-bg,
    --rpg-content-box-shadow, --rpg-header-text-shadow, etc.)
  - Replace triple-theme selector duplication with single [data-theme]
    selectors — children inherit variables automatically
  - Reduces themes.css from 379 to ~220 lines
  - Reduces style.css from 305KB to 294KB (3.8% reduction)

- Add build:css:min npm script for production builds
- Add 9 tests for CSS build: minification, size reduction, CSS
  variable preservation, data URI preservation, theme refactoring
2026-07-12 14:55:36 +02:00

142 lines
4.4 KiB
JavaScript

#!/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);
});