UNPKG

@hadyfayed/filament-react-wrapper

Version:

Enterprise React integration for Laravel/Filament - Smart asset loading, 90%+ React-PHP function mapping, no-plugin Filament integration

631 lines (539 loc) 17 kB
/** * Component Versioning System for React Wrapper * Handles versioning, compatibility, migrations, and updates */ import React from 'react'; import { devTools } from './DevTools'; interface ComponentVersion { version: string; component: React.ComponentType<Record<string, unknown>>; metadata: ComponentMetadata; deprecated?: boolean; deprecationMessage?: string; migrationPath?: string; breakingChanges?: BreakingChange[]; dependencies?: ComponentDependency[]; } interface ComponentMetadata { name: string; description: string; author: string; createdAt: string; updatedAt: string; changelog: ChangelogEntry[]; tags: string[]; category: string; apiVersion: string; dependencies?: ComponentDependency[]; } interface BreakingChange { type: 'prop' | 'method' | 'event' | 'structure'; description: string; migration: string; affectedProps?: string[]; replacedWith?: string; } interface ComponentDependency { name: string; version: string; optional?: boolean; reason?: string; } interface ChangelogEntry { version: string; date: string; type: 'major' | 'minor' | 'patch'; changes: string[]; author: string; } interface MigrationResult { success: boolean; fromVersion: string; toVersion: string; migratedProps: Record<string, unknown>; warnings: string[]; errors: string[]; manualStepsRequired: boolean; } interface CompatibilityCheck { compatible: boolean; issues: CompatibilityIssue[]; recommendations: string[]; autoMigrationAvailable: boolean; } interface CompatibilityIssue { severity: 'error' | 'warning' | 'info'; type: string; description: string; affectedProps?: string[]; solution?: string; } interface VersionConstraint { min?: string; max?: string; exact?: string; exclude?: string[]; } class ComponentVersioningService { private versions: Map<string, Map<string, ComponentVersion>> = new Map(); private aliases: Map<string, string> = new Map(); // component@alias -> component@version private migrations: Map<string, MigrationFunction[]> = new Map(); private defaultVersions: Map<string, string> = new Map(); private compatibilityRules: Map<string, CompatibilityRule[]> = new Map(); constructor() { this.setupDefaultAliases(); } /** * Register a new component version */ registerVersion( componentName: string, version: string, component: unknown, metadata: Partial<ComponentMetadata> = {} ): void { if (!this.versions.has(componentName)) { this.versions.set(componentName, new Map()); } const componentVersions = this.versions.get(componentName)!; const fullMetadata: ComponentMetadata = { name: componentName, description: metadata.description || '', author: metadata.author || 'Unknown', createdAt: metadata.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), changelog: metadata.changelog || [], tags: metadata.tags || [], category: metadata.category || 'general', apiVersion: metadata.apiVersion || '1.0.0', }; const componentVersion: ComponentVersion = { version, component: component as React.ComponentType<Record<string, unknown>>, metadata: fullMetadata, deprecated: false, dependencies: metadata.dependencies || [], }; componentVersions.set(version, componentVersion); // Set as default if it's the first version or marked as latest if (!this.defaultVersions.has(componentName) || version === 'latest') { this.defaultVersions.set(componentName, version); } devTools.log(`Component version registered: ${componentName}@${version}`); } /** * Get version string for a component */ getVersion(componentName: string): string | undefined { return this.defaultVersions.get(componentName); } /** * Get a specific component version object */ getComponentVersion(componentName: string, version?: string): ComponentVersion | null { const componentVersions = this.versions.get(componentName); if (!componentVersions) { return null; } // Resolve version const resolvedVersion = this.resolveVersion(componentName, version); if (!resolvedVersion) { return null; } return componentVersions.get(resolvedVersion!) || null; } /** * Get all versions of a component */ getAllVersions(componentName: string): ComponentVersion[] { const componentVersions = this.versions.get(componentName); if (!componentVersions) { return []; } return Array.from(componentVersions.values()).sort((a, b) => this.compareVersions(b.version, a.version) ); // Latest first } /** * Get latest version of a component */ getLatestVersion(componentName: string): ComponentVersion | null { const versions = this.getAllVersions(componentName); return versions.length > 0 ? versions[0] || null : null; } /** * Check if component version exists */ hasVersion(componentName: string, version?: string): boolean { return this.getComponentVersion(componentName, version) !== null; } /** * Register version alias */ registerAlias(componentName: string, alias: string, version: string): void { const key = `${componentName}@${alias}`; this.aliases.set(key, `${componentName}@${version}`); devTools.log(`Version alias registered: ${key} -> ${componentName}@${version}`); } /** * Deprecate a component version */ deprecateVersion( componentName: string, version: string, message?: string, migrationPath?: string ): void { const componentVersion = this.getComponentVersion(componentName, version); if (componentVersion) { componentVersion.deprecated = true; componentVersion.deprecationMessage = message; componentVersion.migrationPath = migrationPath; devTools.warn(`Component version deprecated: ${componentName}@${version}`, { message, migrationPath, }); } } /** * Add migration function between versions */ addMigration( componentName: string, fromVersion: string, toVersion: string, migrationFn: MigrationFunction ): void { const key = `${componentName}:${fromVersion}->${toVersion}`; if (!this.migrations.has(key)) { this.migrations.set(key, []); } this.migrations.get(key)!.push(migrationFn); devTools.log(`Migration registered: ${key}`); } /** * Migrate props from one version to another */ async migrateProps( componentName: string, fromVersion: string, toVersion: string, props: Record<string, unknown> ): Promise<MigrationResult> { const result: MigrationResult = { success: false, fromVersion, toVersion, migratedProps: { ...props }, warnings: [], errors: [], manualStepsRequired: false, }; try { // Find migration path const migrationPath = this.findMigrationPath(componentName, fromVersion, toVersion); if (migrationPath.length === 0) { result.errors.push(`No migration path found from ${fromVersion} to ${toVersion}`); return result; } // Apply migrations step by step let currentProps = { ...props }; for (const step of migrationPath) { const migrations = this.migrations.get(step) || []; for (const migration of migrations) { try { const [fromPart, toPart] = step.split('->'); const fromVersion = fromPart?.split(':')[1]; const toVersion = toPart; if (!fromVersion || !toVersion) { result.errors.push(`Invalid migration step format: ${step}`); return result; } const migrationResult = await migration(currentProps, { componentName, fromVersion, toVersion, }); currentProps = migrationResult.props; result.warnings.push(...(migrationResult.warnings || [])); if (migrationResult.manualStepsRequired) { result.manualStepsRequired = true; } } catch (error) { result.errors.push(`Migration failed at step ${step}: ${error}`); return result; } } } result.migratedProps = currentProps; result.success = result.errors.length === 0; return result; } catch (error) { result.errors.push(`Migration error: ${error}`); return result; } } /** * Check compatibility between versions */ checkCompatibility( componentName: string, fromVersion: string, toVersion: string ): CompatibilityCheck { const result: CompatibilityCheck = { compatible: true, issues: [], recommendations: [], autoMigrationAvailable: false, }; const fromVersionObj = this.getComponentVersion(componentName, fromVersion); const toVersionObj = this.getComponentVersion(componentName, toVersion); if (!fromVersionObj || !toVersionObj) { result.compatible = false; result.issues.push({ severity: 'error', type: 'version_not_found', description: 'One or both versions not found', }); return result; } // Check breaking changes if ( (toVersionObj as ComponentVersion & { breakingChanges?: BreakingChange[] }).breakingChanges ) { for (const change of ( toVersionObj as ComponentVersion & { breakingChanges: BreakingChange[] } ).breakingChanges) { result.issues.push({ severity: 'warning', type: 'breaking_change', description: change.description, affectedProps: change.affectedProps, solution: change.migration, }); } } // Check if migration is available const migrationPath = this.findMigrationPath(componentName, fromVersion, toVersion); result.autoMigrationAvailable = migrationPath.length > 0; if (!result.autoMigrationAvailable && result.issues.length > 0) { result.compatible = false; result.recommendations.push('Manual migration required due to breaking changes'); } // Check compatibility rules const rules = this.compatibilityRules.get(componentName) || []; for (const rule of rules) { const ruleResult = rule(fromVersionObj as ComponentVersion, toVersionObj as ComponentVersion); if (ruleResult) { result.issues.push(...ruleResult.issues); result.recommendations.push(...ruleResult.recommendations); if (ruleResult.issues.some(issue => issue.severity === 'error')) { result.compatible = false; } } } return result; } /** * Get version constraints satisfaction */ satisfiesConstraint(version: string, constraint: VersionConstraint): boolean { if (constraint.exact) { return version === constraint.exact; } if (constraint.exclude && constraint.exclude.includes(version)) { return false; } if (constraint.min && this.compareVersions(version, constraint.min) < 0) { return false; } if (constraint.max && this.compareVersions(version, constraint.max) > 0) { return false; } return true; } /** * Find best matching version for constraint */ findBestVersion(componentName: string, constraint: VersionConstraint): string | null { const versions = this.getAllVersions(componentName); for (const versionObj of versions) { if (this.satisfiesConstraint(versionObj.version, constraint)) { return versionObj.version; } } return null; } /** * Add compatibility rule */ addCompatibilityRule(componentName: string, rule: CompatibilityRule): void { if (!this.compatibilityRules.has(componentName)) { this.compatibilityRules.set(componentName, []); } this.compatibilityRules.get(componentName)!.push(rule); } /** * Get component changelog */ getChangelog(componentName: string): ChangelogEntry[] { const versions = this.getAllVersions(componentName); const changelog: ChangelogEntry[] = []; for (const version of versions) { changelog.push(...version.metadata.changelog); } return changelog.sort((a, b) => this.compareVersions(b.version, a.version)); } /** * Get version statistics */ getVersionStats(componentName?: string): { componentName?: string; totalVersions: number; latestVersion?: string; deprecatedVersions: number; totalComponents?: number; deprecated?: ComponentVersion[]; componentsWithMultipleVersions?: string[]; hasBreakingChanges?: boolean; dependencies?: ComponentDependency[]; } { if (componentName) { const versions = this.getAllVersions(componentName); return { componentName, totalVersions: versions.length, latestVersion: versions[0]?.version, deprecatedVersions: versions.filter(v => v.deprecated).length, hasBreakingChanges: versions.some(v => v.breakingChanges && v.breakingChanges.length > 0), dependencies: versions[0]?.dependencies || [], }; } // Global stats const allComponents = Array.from(this.versions.keys()); return { totalComponents: allComponents.length, totalVersions: allComponents.reduce((sum, name) => sum + this.getAllVersions(name).length, 0), componentsWithMultipleVersions: allComponents.filter( name => this.getAllVersions(name).length > 1 ), deprecatedVersions: allComponents.reduce( (sum, name) => sum + this.getAllVersions(name).filter(v => v.deprecated).length, 0 ), }; } private resolveVersion(componentName: string, version?: string): string | null { if (!version || version === 'latest') { return this.defaultVersions.get(componentName) || null; } // Check if it's an alias const aliasKey = `${componentName}@${version}`; const aliasTarget = this.aliases.get(aliasKey); if (aliasTarget) { return aliasTarget.split('@')[1] || null; } return version; } private compareVersions(a: string, b: string): number { const aParts = a.split('.').map(Number); const bParts = b.split('.').map(Number); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aPart = aParts[i] || 0; const bPart = bParts[i] || 0; if (aPart !== bPart) { return aPart - bPart; } } return 0; } private findMigrationPath( componentName: string, fromVersion: string, toVersion: string ): string[] { // Simple direct path check first const directPath = `${componentName}:${fromVersion}->${toVersion}`; if (this.migrations.has(directPath)) { return [directPath]; } // Return empty if no direct path return []; } private setupDefaultAliases(): void { // Common version aliases this.registerAlias('*', 'stable', 'latest'); this.registerAlias('*', 'beta', 'latest'); } /** * Set version for a component */ setVersion(componentName: string, version: string): void { this.defaultVersions.set(componentName, version); } /** * Check if component version is compatible */ isCompatible(componentName: string, requiredVersion: string): boolean { const currentVersion = this.getVersion(componentName); if (!currentVersion) { return false; } // Simple semantic version compatibility check const current = currentVersion.split('.').map(Number); const required = requiredVersion.split('.').map(Number); // Major version must match if (current[0] !== required[0]) { return false; } // Minor version must be >= required if ((current[1] || 0) < (required[1] || 0)) { return false; } // Patch version must be >= required if minor versions are equal if (current[1] === required[1] && (current[2] || 0) < (required[2] || 0)) { return false; } return true; } } // Migration function type type MigrationFunction = ( props: Record<string, unknown>, context: { componentName: string; fromVersion: string; toVersion: string; } ) => Promise<{ props: Record<string, unknown>; warnings?: string[]; manualStepsRequired?: boolean; }>; // Compatibility rule type type CompatibilityRule = ( fromVersion: ComponentVersion, toVersion: ComponentVersion ) => { issues: CompatibilityIssue[]; recommendations: string[]; } | null; // Export singleton instance export const componentVersioningService = new ComponentVersioningService(); // Export types export type { ComponentVersion, ComponentMetadata, BreakingChange, ComponentDependency, ChangelogEntry, MigrationResult, CompatibilityCheck, CompatibilityIssue, VersionConstraint, MigrationFunction, CompatibilityRule, }; // Default export export default componentVersioningService;