UNPKG

@memberjunction/react-runtime

Version:

Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.

367 lines (327 loc) 10.9 kB
/** * @fileoverview Component hierarchy registration utilities for MemberJunction React Runtime. * Provides functionality to register a hierarchy of components from Skip component specifications. * @module @memberjunction/react-runtime/hierarchy */ import { CompilationResult, CompileOptions, RuntimeContext } from '../types'; import { ComponentCompiler } from '../compiler'; import { ComponentRegistry } from '../registry'; import { ComponentSpec, ComponentStyles } from '@memberjunction/interactive-component-types'; import { UserInfo } from '@memberjunction/core'; import { ComponentLibraryEntity } from '@memberjunction/core-entities'; /** * Result of a hierarchy registration operation */ export interface HierarchyRegistrationResult { success: boolean; registeredComponents: string[]; errors: ComponentRegistrationError[]; warnings: string[]; } /** * Error information for component registration */ export interface ComponentRegistrationError { componentName: string; error: string; phase: 'compilation' | 'registration' | 'validation'; } /** * Options for hierarchy registration */ export interface HierarchyRegistrationOptions { /** Component styles to apply to all components */ styles?: ComponentStyles; /** Namespace for component registration */ namespace?: string; /** Version for component registration */ version?: string; /** Whether to continue on errors */ continueOnError?: boolean; /** Whether to override existing components */ allowOverride?: boolean; /** * Required, metadata for all possible libraries allowed by the system */ allLibraries: ComponentLibraryEntity[]; } /** * Utility class for registering component hierarchies */ export class ComponentHierarchyRegistrar { constructor( private compiler: ComponentCompiler, private registry: ComponentRegistry, private runtimeContext: RuntimeContext ) {} /** * Registers a complete component hierarchy from a root specification * @param rootSpec - The root component specification * @param options - Registration options * @returns Registration result with details about success/failures */ async registerHierarchy( rootSpec: ComponentSpec, options: HierarchyRegistrationOptions ): Promise<HierarchyRegistrationResult> { const { styles, namespace = 'Global', version = 'v1', continueOnError = true, allowOverride = true } = options; console.log('🌳 ComponentHierarchyRegistrar.registerHierarchy:', { rootComponent: rootSpec.name, hasLibraries: !!(rootSpec.libraries && rootSpec.libraries.length > 0), libraryCount: rootSpec.libraries?.length || 0, libraries: rootSpec.libraries?.map(l => l.name) }); const registeredComponents: string[] = []; const errors: ComponentRegistrationError[] = []; const warnings: string[] = []; // Register the root component const rootResult = await this.registerSingleComponent( rootSpec, { styles, namespace, version, allowOverride, allLibraries: options.allLibraries } ); if (rootResult.success) { registeredComponents.push(rootSpec.name); } else { errors.push(rootResult.error!); if (!continueOnError) { return { success: false, registeredComponents, errors, warnings }; } } // Register child components recursively const childComponents = rootSpec.dependencies || []; if (childComponents.length > 0) { const childResult = await this.registerChildComponents( childComponents, { styles, namespace, version, continueOnError, allowOverride, allLibraries: options.allLibraries }, registeredComponents, errors, warnings ); } return { success: errors.length === 0, registeredComponents, errors, warnings }; } /** * Registers a single component from a specification * @param spec - Component specification * @param options - Registration options * @returns Registration result for this component */ async registerSingleComponent( spec: ComponentSpec, options: { styles?: ComponentStyles; namespace?: string; version?: string; allowOverride?: boolean; allLibraries: ComponentLibraryEntity[]; } ): Promise<{ success: boolean; error?: ComponentRegistrationError }> { const { styles, namespace = 'Global', version = 'v1', allowOverride = true } = options; try { // Skip if no component code if (!spec.code) { return { success: true, error: undefined }; } // Check if component already exists const existingComponent = this.registry.get(spec.name, namespace, version); if (existingComponent && !allowOverride) { return { success: false, error: { componentName: spec.name, error: `Component already registered in ${namespace}/${version}`, phase: 'registration' } }; } // Compile the component const compileOptions: CompileOptions = { componentName: spec.name, componentCode: spec.code, styles, libraries: spec.libraries, // Pass along library dependencies from the spec allLibraries: options.allLibraries }; console.log(`🔧 Compiling component ${spec.name} with libraries:`, { libraryCount: spec.libraries?.length || 0, libraries: spec.libraries?.map(l => ({ name: l.name, globalVariable: l.globalVariable })) }); const compilationResult = await this.compiler.compile(compileOptions); if (!compilationResult.success) { return { success: false, error: { componentName: spec.name, error: compilationResult.error?.message || 'Unknown compilation error', phase: 'compilation' } }; } // Create component factory const componentFactory = compilationResult.component!.component(this.runtimeContext, styles); // Register the component this.registry.register( spec.name, componentFactory.component, namespace, version ); return { success: true }; } catch (error) { return { success: false, error: { componentName: spec.name, error: error instanceof Error ? error.message : String(error), phase: 'registration' } }; } } /** * Recursively registers child components * @param children - Array of child component specifications * @param options - Registration options * @param registeredComponents - Array to track registered components * @param errors - Array to collect errors * @param warnings - Array to collect warnings */ private async registerChildComponents( children: ComponentSpec[], options: HierarchyRegistrationOptions, registeredComponents: string[], errors: ComponentRegistrationError[], warnings: string[] ): Promise<void> { for (const child of children) { // Register this child const childResult = await this.registerSingleComponent(child, { styles: options.styles, namespace: options.namespace, version: options.version, allowOverride: options.allowOverride, allLibraries: options.allLibraries }); if (childResult.success) { if (child.code) { registeredComponents.push(child.name); } } else { errors.push(childResult.error!); if (!options.continueOnError) { return; } } // Register nested children recursively const nestedChildren = child.dependencies || []; if (nestedChildren.length > 0) { await this.registerChildComponents( nestedChildren, options, registeredComponents, errors, warnings ); } } } } /** * Convenience function to register a component hierarchy * @param rootSpec - The root component specification * @param compiler - Component compiler instance * @param registry - Component registry instance * @param runtimeContext - Runtime context with React and other libraries * @param options - Registration options * @returns Registration result */ export async function registerComponentHierarchy( rootSpec: ComponentSpec, compiler: ComponentCompiler, registry: ComponentRegistry, runtimeContext: RuntimeContext, options: HierarchyRegistrationOptions ): Promise<HierarchyRegistrationResult> { const registrar = new ComponentHierarchyRegistrar(compiler, registry, runtimeContext); return registrar.registerHierarchy(rootSpec, options); } /** * Validates a component specification before registration * @param spec - Component specification to validate * @returns Array of validation errors (empty if valid) */ export function validateComponentSpec(spec: ComponentSpec): string[] { const errors: string[] = []; if (!spec.name) { errors.push('Component specification must have a name'); } // If componentCode is provided, do basic validation if (spec.code) { if (typeof spec.code !== 'string') { errors.push(`Component code for ${spec.name} must be a string`); } if (spec.code.trim().length === 0) { errors.push(`Component code for ${spec.name} cannot be empty`); } } // Validate child components recursively const children = spec.dependencies || []; children.forEach((child, index) => { const childErrors = validateComponentSpec(child); childErrors.forEach(error => { errors.push(`Child ${index} (${child.name || 'unnamed'}): ${error}`); }); }); return errors; } /** * Flattens a component hierarchy into a list of all components * @param rootSpec - The root component specification * @returns Array of all component specifications in the hierarchy */ export function flattenComponentHierarchy(rootSpec: ComponentSpec): ComponentSpec[] { const components: ComponentSpec[] = [rootSpec]; const children = rootSpec.dependencies || []; children.forEach(child => { components.push(...flattenComponentHierarchy(child)); }); return components; } /** * Counts the total number of components in a hierarchy * @param rootSpec - The root component specification * @param includeEmpty - Whether to include components without code * @returns Total component count */ export function countComponentsInHierarchy( rootSpec: ComponentSpec, includeEmpty: boolean = false ): number { let count = 0; if (includeEmpty || rootSpec.code) { count = 1; } const children = rootSpec.dependencies || []; children.forEach(child => { count += countComponentsInHierarchy(child, includeEmpty); }); return count; }