/** * Schema Drift Guard * CI tool to detect schema drift between TypeScript (admin-spa) and PHP (SSR) * * This script runs as part of CI to ensure the section schema is consistent * across both the editor (TypeScript) and the SSR renderer (PHP). * * Usage: * npx ts-node scripts/check-schema-drift.ts * or * node scripts/check-schema-drift.mjs */ import { readFileSync } from 'fs'; import { resolve } from 'path'; // Section types that must have matching renderers in PHP const SECTION_TYPES = [ 'hero', 'content', 'image-text', 'feature-grid', 'cta-banner', 'contact-form', 'bento-category-grid', 'product-carousel', 'shoppable-image', 'marquee-banner', ]; // Required props per section type (canonical schema) const REQUIRED_PROPS: Record = { 'hero': ['title', 'subtitle', 'image', 'cta_text', 'cta_url'], 'content': ['content', 'cta_text', 'cta_url'], 'image-text': ['title', 'text', 'image', 'cta_text', 'cta_url'], 'feature-grid': ['heading', 'items'], 'cta-banner': ['title', 'text', 'button_text', 'button_url'], 'contact-form': ['title', 'webhook_url', 'redirect_url'], 'bento-category-grid': ['title', 'items'], 'product-carousel': ['title', 'subtitle', 'cta_text', 'cta_url', 'source', 'limit'], 'shoppable-image': ['title', 'subtitle', 'image', 'alt', 'hotspots'], 'marquee-banner': ['text', 'separator', 'speed'], }; // Section styles that must be supported by SSR const REQUIRED_STYLES = [ 'backgroundType', 'backgroundColor', 'gradientFrom', 'gradientTo', 'gradientAngle', 'backgroundImage', 'backgroundOverlay', 'paddingTop', 'paddingBottom', 'contentWidth', 'heightPreset', ]; // Element styles per section type const ELEMENT_STYLES: Record = { 'hero': ['title', 'subtitle', 'cta_text'], 'content': ['heading', 'text', 'link', 'button', 'content'], 'image-text': ['title', 'text', 'image', 'button'], 'feature-grid': ['heading', 'feature_item'], 'cta-banner': ['title', 'text', 'button_text'], 'contact-form': ['title', 'button', 'fields'], 'bento-category-grid': ['title'], 'product-carousel': ['title', 'subtitle'], 'shoppable-image': ['title'], 'marquee-banner': [], }; interface ValidationResult { passed: boolean; errors: string[]; warnings: string[]; } function validateSchemaConsistency(): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // 1. Validate that all section types have required props SECTION_TYPES.forEach((type) => { const required = REQUIRED_PROPS[type] || []; if (required.length === 0) { warnings.push(`Section type '${type}' has no required props defined`); } }); // 2. Validate PHP renderer file exists and has all renderers const phpSSRPath = resolve(__dirname, '../includes/Frontend/PageSSR.php'); try { const phpContent = readFileSync(phpSSRPath, 'utf-8'); // Check for render methods SECTION_TYPES.forEach((type) => { const methodName = `render_${type.replace(/-/g, '_')}`; const hasMethod = phpContent.includes(`public static function ${methodName}`); if (!hasMethod) { // Check if it falls back to generic const fallbackCheck = phpContent.includes(`render_${type.replace(/-/g, '_')}`); if (!fallbackCheck) { warnings.push(`PHP SSR: No explicit renderer for '${type}', using generic fallback`); } } }); // Check for required style handling in render methods const hasBackgroundType = phpContent.includes('$section_styles[\'backgroundType\']'); const hasPaddingTop = phpContent.includes('$pt = $section_styles[\'paddingTop\']'); const hasContentWidth = phpContent.includes('$content_width = $section_styles[\'contentWidth\']'); if (!hasBackgroundType) { errors.push('PHP SSR: Missing backgroundType handling in section styles'); } if (!hasPaddingTop) { errors.push('PHP SSR: Missing paddingTop handling in section styles'); } if (!hasContentWidth) { warnings.push('PHP SSR: Missing contentWidth handling in section styles'); } } catch (err) { errors.push(`Failed to read PHP SSR file: ${err}`); } // 3. Validate TypeScript schema file const tsSchemaPath = resolve(__dirname, '../admin-spa/src/routes/Appearance/Pages/schema/sectionSchema.ts'); try { const tsContent = readFileSync(tsSchemaPath, 'utf-8'); // Check that all section types are defined SECTION_TYPES.forEach((type) => { const schemaCheck = tsContent.includes(`'${type}':`); if (!schemaCheck) { errors.push(`TypeScript Schema: Missing definition for '${type}'`); } }); // Check that feature-grid uses 'items' not 'features' const hasFeatureGridItems = tsContent.includes("items: { type: 'static', value: [] }"); const hasFeatureGridFeatures = tsContent.includes("features:"); if (hasFeatureGridFeatures && !hasFeatureGridItems) { errors.push('TypeScript Schema: feature-grid uses "features" instead of "items"'); } // Check for defaultStyles with contentWidth const hasDefaultStyles = tsContent.includes('defaultStyles'); const hasContentWidthDefault = tsContent.includes('contentWidth'); if (!hasDefaultStyles) { warnings.push('TypeScript Schema: Missing defaultStyles definition'); } if (!hasContentWidthDefault) { warnings.push('TypeScript Schema: Missing contentWidth in defaultStyles'); } } catch (err) { errors.push(`Failed to read TypeScript Schema file: ${err}`); } // 4. Validate that props match between TS and PHP const propMismatches: string[] = []; SECTION_TYPES.forEach((type) => { const requiredProps = REQUIRED_PROPS[type] || []; requiredProps.forEach((prop) => { // Check PHP renders the prop const phpHasProp = phpContent?.includes(`$props['${prop}']`) || phpContent?.includes(`$props[\"${prop}\"]`); if (!phpHasProp) { propMismatches.push(`PHP render_${type}: Missing prop '${prop}' handling`); } }); }); if (propMismatches.length > 0) { warnings.push(...propMismatches); } return { passed: errors.length === 0, errors, warnings, }; } function main() { console.log('🔍 WooNooW Schema Drift Check\n'); console.log('========================================\n'); const result = validateSchemaConsistency(); if (result.warnings.length > 0) { console.log('⚠️ Warnings:'); result.warnings.forEach((w) => console.log(` - ${w}`)); console.log(''); } if (result.errors.length > 0) { console.log('❌ Errors (CI will fail):'); result.errors.forEach((e) => console.log(` - ${e}`)); console.log(''); console.log('❌ Schema drift detected! Please fix the errors above.'); process.exit(1); } if (result.passed && result.warnings.length === 0) { console.log('✅ All checks passed! No schema drift detected.'); } else if (result.passed) { console.log('✅ Core checks passed (warnings above are non-blocking).'); } console.log('\n========================================'); console.log('Schema Version: 1'); console.log(`Section Types: ${SECTION_TYPES.length}`); console.log(`Required Styles: ${REQUIRED_STYLES.length}`); } // Run if called directly main();