Major improvements to WooNooW Page Editor system: Schema & Architecture: - Canonical section schema with unified sectionSchema.ts - Normalized feature-grid to use items (not features) - Standardized default values across all section types - Schema versioning with automatic migration on read Backend (PHP): - Enhanced PlaceholderRenderer with typed output contracts - Added fallback behavior for empty/invalid dynamic sources - Added caching support for post data resolution - New SchemaMigration class for backward compatibility - New Features class for feature flags - Enhanced PageSSR with full style support - Removed controller-level special-casing for related_posts Frontend (Admin SPA): - Updated CanvasRenderer with schema-aware transformation - Enhanced InspectorPanel with canonical schema metadata - Added new section renderers Frontend (Customer SPA): - New section components: BentoCategoryGrid, MarqueeBanner, ProductCarousel, ShoppableImage - Updated FeatureGridSection for items prop contract Testing: - Add PHP tests: SchemaMigrationTest, PlaceholderRendererTest, PageSSRTest - Add TypeScript tests: schema-integration, feature-grid-regression - Add parity tests for React vs SSR content matching - Add CI script: check-schema-drift.mjs - Add VERIFICATION_CHECKLIST.md Documentation: - RELEASE_NOTES-v1.0.md with full release notes - docs/PAGE_EDITOR_SECTION_SCHEMA_V1.md - docs/PAGE_EDITOR_SSR_COVERAGE_AUDIT.md
228 lines
7.7 KiB
JavaScript
228 lines
7.7 KiB
JavaScript
/**
|
|
* 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<string, string[]> = {
|
|
'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<string, string[]> = {
|
|
'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(); |