Files
WooNooW/scripts/check-schema-drift.mjs
Dwindi Ramadhana 396ca25be4 feat: Page Editor v1.0 - canonical schema, SSR parity, and migration
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
2026-05-30 13:02:08 +07:00

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();