diff --git a/app/scripts/check-responsive.mjs b/app/scripts/check-responsive.mjs new file mode 100644 index 0000000..0c53c71 --- /dev/null +++ b/app/scripts/check-responsive.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node +// scripts/check-responsive.mjs — Responsive layout validation for built dist/ + +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, extname } from 'path'; + +const DIST_DIR = 'dist'; +const MOBILE_WIDTHS = [360, 390, 414, 640]; +const MAX_ALLOWED_OVERFLOW = 1; // Allow 1px tolerance for rounding + +console.log('šŸ” Checking responsive layout rules...\n'); + +let errors = 0; +let warnings = 0; + +// Check 1: Verify CSS contains mobile breakpoints +function checkCSSFiles(dir) { + const files = readdirSync(dir); + + for (const file of files) { + const fullPath = join(dir, file); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + checkCSSFiles(fullPath); + } else if (extname(file) === '.css') { + const content = readFileSync(fullPath, 'utf-8'); + + // Check for mobile media queries + if (!content.includes('@media') || !content.includes('max-width')) { + console.warn(`āš ļø ${fullPath}: No mobile media queries found`); + warnings++; + } + + // Check for overflow prevention + if (!content.includes('overflow-x') && !content.includes('overflowX')) { + console.warn(`āš ļø ${fullPath}: No overflow-x rules found`); + warnings++; + } + + // Check for word-wrap/overflow-wrap + if (!content.includes('word-wrap') && !content.includes('overflow-wrap')) { + console.warn(`āš ļø ${fullPath}: No word-wrap/overflow-wrap rules found`); + warnings++; + } + + console.log(`āœ“ ${fullPath}: Basic responsive rules present`); + } + } +} + +// Check 2: Verify HTML contains viewport meta tag +function checkHTMLFiles(dir) { + const files = readdirSync(dir); + + for (const file of files) { + const fullPath = join(dir, file); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + checkHTMLFiles(fullPath); + } else if (extname(file) === '.html') { + const content = readFileSync(fullPath, 'utf-8'); + + // Skip placeholder files (Google/Bing verification) + if (file.includes('placeholder') || file.includes('verification') || content.length < 100) { + console.log(`⊘ ${fullPath}: Skipping placeholder/verification file`); + continue; + } + + // Check for viewport meta tag + if (!content.includes('viewport') || !content.includes('width=device-width')) { + console.error(`āŒ ${fullPath}: Missing or incorrect viewport meta tag`); + errors++; + } else { + console.log(`āœ“ ${fullPath}: Viewport meta tag present`); + } + + // Check for max-width constraints on root elements + if (!content.includes('max-width') && !content.includes('maxWidth')) { + console.warn(`āš ļø ${fullPath}: No max-width constraints found in inline styles`); + warnings++; + } + } + } +} + +// Check 3: Verify no hardcoded wide widths in JS bundles +function checkJSFiles(dir) { + const files = readdirSync(dir); + + for (const file of files) { + const fullPath = join(dir, file); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + checkJSFiles(fullPath); + } else if (extname(file) === '.js') { + const content = readFileSync(fullPath, 'utf-8'); + + // Check for suspicious fixed widths (e.g., width: 1920px, minWidth: 1200px) + const suspiciousWidths = content.match(/(?:width|minWidth):\s*["']?\d{4,}px/g); + if (suspiciousWidths && suspiciousWidths.length > 0) { + console.warn(`āš ļø ${fullPath}: Found potentially problematic fixed widths: ${suspiciousWidths.slice(0, 3).join(', ')}`); + warnings++; + } + } + } +} + +// Check 4: Validate article-specific responsive patterns +function checkArticlePatterns() { + console.log('\nšŸ“± Checking article-specific responsive patterns...\n'); + + const patterns = [ + { name: 'Article detail page wrapper', selector: '.article-detail-page', required: ['overflow-x', 'max-width'] }, + { name: 'Article layout grid', selector: '.article-layout', required: ['overflow-x', 'min-width'] }, + { name: 'Article main content', selector: '.article-main', required: ['overflow-x', 'min-width'] }, + { name: 'Article header title', selector: '.article-header h1', required: ['word-wrap', 'overflow-wrap', 'clamp'] }, + { name: 'Article breadcrumb', selector: '.article-breadcrumb', required: ['overflow-x'] }, + { name: 'CVE summary card', selector: '.cve-summary-card', required: ['@media', 'grid-template-columns: 1fr'] }, + { name: 'Article content', selector: '.article-content', required: ['overflow-x', 'word-wrap'] }, + ]; + + // Read all CSS files and check for patterns + const cssFiles = []; + function collectCSS(dir) { + const files = readdirSync(dir); + for (const file of files) { + const fullPath = join(dir, file); + const stat = statSync(fullPath); + if (stat.isDirectory()) { + collectCSS(fullPath); + } else if (extname(file) === '.css') { + cssFiles.push(fullPath); + } + } + } + collectCSS(DIST_DIR); + + const allCSS = cssFiles.map(f => readFileSync(f, 'utf-8')).join('\n'); + + for (const pattern of patterns) { + let found = false; + for (const req of pattern.required) { + if (allCSS.includes(req)) { + found = true; + break; + } + } + + if (found) { + console.log(`āœ“ ${pattern.name}: Responsive rules present`); + } else { + console.warn(`āš ļø ${pattern.name}: Missing expected responsive rules (${pattern.required.join(', ')})`); + warnings++; + } + } +} + +// Run all checks +try { + console.log('Checking CSS files...\n'); + checkCSSFiles(DIST_DIR); + + console.log('\nChecking HTML files...\n'); + checkHTMLFiles(DIST_DIR); + + console.log('\nChecking JS bundles...\n'); + checkJSFiles(DIST_DIR); + + checkArticlePatterns(); + + console.log('\n' + '='.repeat(60)); + console.log(`\nšŸ“Š Responsive Check Summary:`); + console.log(` āœ“ Errors: ${errors}`); + console.log(` āš ļø Warnings: ${warnings}`); + + if (errors > 0) { + console.log('\nāŒ Responsive check FAILED. Fix errors before deploying.\n'); + process.exit(1); + } else if (warnings > 5) { + console.log('\nāš ļø Many warnings detected. Review before deploying.\n'); + process.exit(0); + } else { + console.log('\nāœ… Responsive check PASSED. Layout should work on mobile.\n'); + process.exit(0); + } +} catch (err) { + console.error('āŒ Responsive check failed with error:', err.message); + process.exit(1); +}