/** * Build Bootstrap Subset Script * * Genera un subset de Bootstrap con SOLO las clases usadas en el tema. * * USO: * node build-bootstrap-subset.js * * OUTPUT: * Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css */ const { PurgeCSS } = require('purgecss'); const { globSync } = require('glob'); const fs = require('fs'); const path = require('path'); async function buildBootstrapSubset() { console.log('='.repeat(60)); console.log('Building Bootstrap Subset for ROI Theme'); console.log('='.repeat(60)); const themeDir = __dirname; const inputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap.min.css'); const outputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css'); // Verificar que existe el archivo de entrada if (!fs.existsSync(inputFile)) { console.error('ERROR: bootstrap.min.css not found at:', inputFile); process.exit(1); } const inputSize = fs.statSync(inputFile).size; console.log(`Input: bootstrap.min.css (${(inputSize / 1024).toFixed(2)} KB)`); // Encontrar archivos PHP y JS manualmente console.log('\nScanning for PHP and JS files...'); const patterns = [ '*.php', 'Public/**/*.php', 'Admin/**/*.php', 'Inc/**/*.php', 'Shared/**/*.php', 'template-parts/**/*.php', 'Assets/js/**/*.js', ]; let contentFiles = []; for (const pattern of patterns) { const files = globSync(pattern, { cwd: themeDir, absolute: true }); contentFiles = contentFiles.concat(files); } console.log(`Found ${contentFiles.length} files to analyze`); if (contentFiles.length === 0) { console.error('ERROR: No content files found. Check glob patterns.'); process.exit(1); } // Mostrar algunos archivos encontrados console.log('\nSample files:'); contentFiles.slice(0, 5).forEach(f => console.log(' -', path.relative(themeDir, f))); if (contentFiles.length > 5) { console.log(` ... and ${contentFiles.length - 5} more`); } try { const purgeCSSResult = await new PurgeCSS().purge({ css: [inputFile], content: contentFiles, // Safelist: Clases que SIEMPRE deben incluirse safelist: { standard: [ // Estados de navbar scroll (JavaScript) 'scrolled', 'navbar-scrolled', // Bootstrap Collapse (JavaScript) 'show', 'showing', 'hiding', 'collapse', 'collapsing', // Estados de dropdown 'dropdown-menu', 'dropdown-item', 'dropdown-toggle', // Estados de form 'is-valid', 'is-invalid', 'was-validated', // Visually hidden (accesibilidad) 'visually-hidden', 'visually-hidden-focusable', // Screen reader 'sr-only', // Container 'container', 'container-fluid', // Row 'row', // Display 'd-flex', 'd-none', 'd-block', 'd-inline-block', 'd-inline', 'd-grid', // Common spacing 'mb-0', 'mb-1', 'mb-2', 'mb-3', 'mb-4', 'mb-5', 'mt-0', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mt-5', 'me-0', 'me-1', 'me-2', 'me-3', 'me-4', 'me-5', 'ms-0', 'ms-1', 'ms-2', 'ms-3', 'ms-4', 'ms-5', 'mx-auto', 'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5', 'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5', 'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5', 'g-0', 'g-1', 'g-2', 'g-3', 'g-4', 'g-5', // Flex 'flex-wrap', 'flex-nowrap', 'flex-column', 'flex-row', 'justify-content-center', 'justify-content-between', 'justify-content-start', 'justify-content-end', 'align-items-center', 'align-items-start', 'align-items-end', // Text 'text-center', 'text-start', 'text-end', 'text-white', 'text-muted', 'fw-bold', 'fw-normal', 'small', // Images 'img-fluid', // Border/rounded 'rounded', 'rounded-circle', 'border', 'border-0', // Shadow 'shadow', 'shadow-sm', 'shadow-lg', // Width 'w-100', 'w-auto', 'h-100', 'h-auto', // Toast classes (plugin IP View Limit) 'toast-container', 'toast', 'toast-body', 'position-fixed', 'bottom-0', 'end-0', 'start-50', 'translate-middle-x', 'text-dark', 'bg-warning', 'btn-close', 'm-auto', ], deep: [ // Grid responsive /^col-/, /^col$/, // Display responsive /^d-[a-z]+-/, // Navbar responsive /^navbar-expand/, /^navbar-/, // Responsive margins/padding /^m[tbsexy]?-[a-z]+-/, /^p[tbsexy]?-[a-z]+-/, // Text responsive /^text-[a-z]+-/, // Flex responsive /^flex-[a-z]+-/, /^justify-content-[a-z]+-/, /^align-items-[a-z]+-/, ], greedy: [ // Form controls /form-/, /input-/, // Buttons /btn/, // Cards /card/, // Navbar /navbar/, /nav-/, // Tables /table/, // Alerts /alert/, // Toast /toast/, // Badges /badge/, // Lists /list-/, ], }, // Mantener variables CSS de Bootstrap variables: true, // Mantener keyframes keyframes: true, // Mantener font-face fontFace: true, }); if (purgeCSSResult.length === 0 || !purgeCSSResult[0].css) { console.error('ERROR: PurgeCSS returned empty result'); process.exit(1); } // Agregar header al CSS generado const header = `/** * Bootstrap 5.3.2 Subset - ROI Theme * * Generado automáticamente con PurgeCSS * Contiene SOLO las clases Bootstrap usadas en el tema. * * Original: ${(inputSize / 1024).toFixed(2)} KB * Subset: ${(purgeCSSResult[0].css.length / 1024).toFixed(2)} KB * Reduccion: ${(100 - (purgeCSSResult[0].css.length / inputSize * 100)).toFixed(1)}% * * Generado: ${new Date().toISOString()} * * Para regenerar: * node build-bootstrap-subset.js */ `; const outputCSS = header + purgeCSSResult[0].css; // Escribir archivo fs.writeFileSync(outputFile, outputCSS); const outputSize = fs.statSync(outputFile).size; const reduction = ((1 - outputSize / inputSize) * 100).toFixed(1); console.log(''); console.log('SUCCESS!'); console.log('-'.repeat(60)); console.log(`Output: bootstrap-subset.min.css (${(outputSize / 1024).toFixed(2)} KB)`); console.log(`Reduction: ${reduction}% smaller`); console.log('-'.repeat(60)); console.log(''); console.log('Next steps:'); console.log('1. Update Inc/enqueue-scripts.php to use bootstrap-subset.min.css'); console.log('2. Test the theme thoroughly'); console.log('3. Run PageSpeed Insights to verify improvement'); } catch (error) { console.error('ERROR:', error.message); console.error(error.stack); process.exit(1); } } buildBootstrapSubset();