- 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
142 lines
4.4 KiB
JavaScript
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);
|
|
});
|