UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

710 lines (625 loc) 18.1 kB
/** * @fileoverview OrdoJS Component System - Modern component architecture * @author OrdoJS Framework Team */ import type { Props } from '../types/index.js'; import { signal, type EffectCleanup, type Signal } from './reactivity.js'; /** * Component lifecycle phases */ export enum ComponentLifecycle { CREATED = 'created', MOUNTED = 'mounted', UPDATED = 'updated', UNMOUNTED = 'unmounted', ERROR = 'error' } /** * Component context for dependency injection */ export interface ComponentContext { /** Parent component */ parent?: ComponentInstance; /** Injected dependencies */ injected: Map<string | symbol, any>; /** Provided dependencies */ provided: Map<string | symbol, any>; /** Component registry */ registry: ComponentRegistry; } /** * Component definition interface */ export interface ComponentDefinition<P extends Props = Props, S = any> { /** Component name */ name: string; /** Props validation schema */ props?: PropSchema<P>; /** Setup function */ setup?: (props: P, context: SetupContext) => S | Promise<S>; /** Render function */ render?: (state: S, props: P) => VNode | string; /** Lifecycle hooks */ lifecycle?: Partial<LifecycleHooks>; /** Component styles */ styles?: string | (() => string); /** Component metadata */ meta?: ComponentMeta; } /** * Component metadata */ export interface ComponentMeta { /** Component version */ version?: string; /** Component description */ description?: string; /** Component tags */ tags?: string[]; /** Is component async */ async?: boolean; /** Component dependencies */ dependencies?: string[]; } /** * Props validation schema */ export interface PropSchema<P = any> { [key: string]: PropDefinition<any>; } /** * Prop definition */ export interface PropDefinition<T = any> { /** Prop type */ type: PropType<T>; /** Is required */ required?: boolean; /** Default value */ default?: T | (() => T); /** Validation function */ validator?: (value: T) => boolean; /** Prop description */ description?: string; } /** * Prop types */ export type PropType<T> = | 'string' | 'number' | 'boolean' | 'object' | 'array' | 'function' | ((value: any) => value is T); /** * Setup context */ export interface SetupContext { /** Component props */ props: Signal<Props>; /** Emit events */ emit: (event: string, payload?: any) => void; /** Expose public API */ expose: (api: Record<string, any>) => void; /** Access parent context */ parent?: ComponentInstance; /** Inject dependency */ inject: <T>(key: string | symbol, defaultValue?: T) => T; /** Provide dependency */ provide: <T>(key: string | symbol, value: T) => void; /** Component slots */ slots: Record<string, () => VNode[]>; } /** * Lifecycle hooks */ export interface LifecycleHooks { /** Before component creation */ beforeCreate?: () => void; /** After component creation */ created?: () => void; /** Before component mount */ beforeMount?: () => void; /** After component mount */ mounted?: () => void; /** Before component update */ beforeUpdate?: () => void; /** After component update */ updated?: () => void; /** Before component unmount */ beforeUnmount?: () => void; /** After component unmount */ unmounted?: () => void; /** Error handler */ errorCaptured?: (error: Error, instance: ComponentInstance) => boolean | void; } /** * Virtual DOM node */ export interface VNode { /** Node type */ type: string | ComponentDefinition; /** Node props */ props?: Props; /** Child nodes */ children?: VNode[]; /** Node key */ key?: string | number; /** Node ref */ ref?: (el: Element | ComponentInstance | null) => void; /** Raw HTML */ innerHTML?: string; } /** * Component instance */ export interface ComponentInstance<P extends Props = Props, S = any> { /** Component ID */ id: string; /** Component definition */ definition: ComponentDefinition<P, S>; /** Component props */ props: Signal<P>; /** Component state */ state: S; /** Component context */ context: ComponentContext; /** Current lifecycle phase */ lifecycle: Signal<ComponentLifecycle>; /** DOM element */ element: Element | null; /** Child components */ children: ComponentInstance[]; /** Event emitter */ emit: (event: string, payload?: any) => void; /** Exposed API */ exposed: Record<string, any>; /** Cleanup functions */ cleanups: EffectCleanup[]; /** Mount component */ mount: (target: Element) => Promise<void>; /** Unmount component */ unmount: () => Promise<void>; /** Update component */ update: (newProps?: Partial<P>) => Promise<void>; /** Force re-render */ forceUpdate: () => Promise<void>; } /** * Component registry */ export interface ComponentRegistry { /** Register component */ register: (definition: ComponentDefinition) => void; /** Get component */ get: (name: string) => ComponentDefinition | undefined; /** Unregister component */ unregister: (name: string) => boolean; /** List all components */ list: () => ComponentDefinition[]; } /** * Component factory options */ export interface ComponentFactoryOptions { /** Global component registry */ registry?: ComponentRegistry; /** Global error handler */ errorHandler?: (error: Error, instance: ComponentInstance) => void; /** Development mode */ devMode?: boolean; } /** * Component registry implementation */ class ComponentRegistryImpl implements ComponentRegistry { private components = new Map<string, ComponentDefinition>(); register(definition: ComponentDefinition): void { if (this.components.has(definition.name)) { console.warn(`Component "${definition.name}" is already registered`); } this.components.set(definition.name, definition); } get(name: string): ComponentDefinition | undefined { return this.components.get(name); } unregister(name: string): boolean { return this.components.delete(name); } list(): ComponentDefinition[] { return Array.from(this.components.values()); } } /** * Component instance implementation */ class ComponentInstanceImpl<P extends Props = Props, S = any> implements ComponentInstance<P, S> { public id: string; public props: Signal<P>; public state: S; public context: ComponentContext; public lifecycle: Signal<ComponentLifecycle>; public element: Element | null = null; public children: ComponentInstance[] = []; public exposed: Record<string, any> = {}; public cleanups: EffectCleanup[] = []; private eventListeners = new Map<string, Set<(payload: any) => void>>(); constructor( public definition: ComponentDefinition<P, S>, initialProps: P, parentContext?: ComponentContext ) { this.id = generateComponentId(); this.props = signal(initialProps); this.lifecycle = signal(ComponentLifecycle.CREATED); this.context = { parent: parentContext?.parent, injected: new Map(parentContext?.injected), provided: new Map(), registry: parentContext?.registry || new ComponentRegistryImpl() }; this.state = {} as S; this.initializeComponent(); } emit = (event: string, payload?: any): void => { const listeners = this.eventListeners.get(event); if (listeners) { for (const listener of listeners) { try { listener(payload); } catch (error) { console.error(`Error in event listener for "${event}":`, error); } } } // Bubble to parent if (this.context.parent) { this.context.parent.emit(`child:${event}`, { source: this, payload }); } }; async mount(target: Element): Promise<void> { if (this.element) { throw new Error('Component is already mounted'); } try { this.runLifecycleHook('beforeMount'); // Create element this.element = await this.render(); target.appendChild(this.element); this.lifecycle.set(ComponentLifecycle.MOUNTED); this.runLifecycleHook('mounted'); } catch (error) { this.lifecycle.set(ComponentLifecycle.ERROR); this.handleError(error as Error); } } async unmount(): Promise<void> { if (!this.element) { return; } try { this.runLifecycleHook('beforeUnmount'); // Unmount children for (const child of this.children) { await child.unmount(); } // Remove element if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } this.element = null; // Cleanup effects for (const cleanup of this.cleanups) { cleanup(); } this.cleanups = []; this.lifecycle.set(ComponentLifecycle.UNMOUNTED); this.runLifecycleHook('unmounted'); } catch (error) { this.handleError(error as Error); } } async update(newProps?: Partial<P>): Promise<void> { if (newProps) { this.props.update(current => ({ ...current, ...newProps })); } try { this.runLifecycleHook('beforeUpdate'); if (this.element) { const newElement = await this.render(); if (this.element.parentNode) { this.element.parentNode.replaceChild(newElement, this.element); } this.element = newElement; } this.lifecycle.set(ComponentLifecycle.UPDATED); this.runLifecycleHook('updated'); } catch (error) { this.handleError(error as Error); } } async forceUpdate(): Promise<void> { await this.update(); } private async initializeComponent(): Promise<void> { try { this.runLifecycleHook('beforeCreate'); // Validate props if (this.definition.props) { this.validateProps(this.props.value, this.definition.props); } // Setup component if (this.definition.setup) { const setupContext = this.createSetupContext(); const result = await this.definition.setup(this.props.value, setupContext); if (result) { this.state = result; } } this.runLifecycleHook('created'); } catch (error) { this.handleError(error as Error); } } private createSetupContext(): SetupContext { return { props: this.props, emit: this.emit, expose: (api: Record<string, any>) => { Object.assign(this.exposed, api); }, parent: this.context.parent, inject: <T>(key: string | symbol, defaultValue?: T): T => { return this.context.injected.get(key) ?? defaultValue; }, provide: <T>(key: string | symbol, value: T) => { this.context.provided.set(key, value); // Propagate to children for (const child of this.children) { child.context.injected.set(key, value); } }, slots: {} // TODO: Implement slots }; } private async render(): Promise<Element> { if (this.definition.render) { const vnode = this.definition.render(this.state, this.props.value); return this.renderVNode(vnode); } // Default render const div = document.createElement('div'); div.className = `ordojs-component ordojs-${this.definition.name}`; div.textContent = `Component: ${this.definition.name}`; return div; } private renderVNode(vnode: VNode | string): Element { if (typeof vnode === 'string') { const div = document.createElement('div'); div.textContent = vnode; return div; } if (typeof vnode.type === 'string') { // HTML element const element = document.createElement(vnode.type); // Set props as attributes if (vnode.props) { for (const [key, value] of Object.entries(vnode.props)) { if (key.startsWith('on') && typeof value === 'function') { // Event listener const eventName = key.slice(2).toLowerCase(); element.addEventListener(eventName, value); } else if (key === 'className' || key === 'class') { element.className = String(value); } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else { element.setAttribute(key, String(value)); } } } // Render children if (vnode.children) { for (const child of vnode.children) { element.appendChild(this.renderVNode(child)); } } // Set innerHTML if provided if (vnode.innerHTML) { element.innerHTML = vnode.innerHTML; } return element; } else { // Component const childInstance = createComponent(vnode.type, vnode.props || {}, this.context); this.children.push(childInstance); const childElement = document.createElement('div'); childInstance.mount(childElement).then(() => { // Component mounted }); return childElement; } } private validateProps(props: P, schema: PropSchema<P>): void { for (const [key, definition] of Object.entries(schema)) { const value = props[key as keyof P]; // Check required if (definition.required && (value === undefined || value === null)) { throw new Error(`Required prop "${key}" is missing`); } // Check type if (value !== undefined && !this.validatePropType(value, definition.type)) { throw new Error(`Prop "${key}" has invalid type`); } // Custom validator if (value !== undefined && definition.validator && !definition.validator(value)) { throw new Error(`Prop "${key}" failed validation`); } } } private validatePropType(value: any, type: PropType<any>): boolean { if (typeof type === 'function') { return type(value); } switch (type) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number'; case 'boolean': return typeof value === 'boolean'; case 'object': return typeof value === 'object' && value !== null; case 'array': return Array.isArray(value); case 'function': return typeof value === 'function'; default: return true; } } private runLifecycleHook(hook: keyof LifecycleHooks): void { if (hook === 'errorCaptured') { return; // errorCaptured is handled separately } const hookFn = this.definition.lifecycle?.[hook] as (() => void) | undefined; if (hookFn) { try { hookFn.call(this); } catch (error) { this.handleError(error as Error); } } } private handleError(error: Error): void { console.error(`Error in component "${this.definition.name}":`, error); // Try error boundary if (this.definition.lifecycle?.errorCaptured) { const handled = this.definition.lifecycle.errorCaptured(error, this as any); if (handled) return; } // Bubble to parent if (this.context.parent?.definition.lifecycle?.errorCaptured) { const handled = this.context.parent.definition.lifecycle.errorCaptured(error, this as any); if (handled) return; } // Global error handler would be called here throw error; } } /** * Generate unique component ID */ function generateComponentId(): string { return `ordojs-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Create component instance */ export function createComponent<P extends Props = Props, S = any>( definition: ComponentDefinition<P, S>, props: P, context?: ComponentContext ): ComponentInstance<P, S> { return new ComponentInstanceImpl(definition, props, context); } /** * Define a component */ export function defineComponent<P extends Props = Props, S = any>( definition: ComponentDefinition<P, S> ): ComponentDefinition<P, S> { return definition; } /** * Create virtual DOM node */ export function h( type: string | ComponentDefinition, props?: Props, ...children: (VNode | string)[] ): VNode { return { type, props, children: children.flat().map(child => typeof child === 'string' ? { type: 'text', props: { textContent: child } } as VNode : child ) }; } /** * Fragment component for multiple root nodes */ export const Fragment = defineComponent({ name: 'Fragment', render: (_, props) => { return h('div', { style: { display: 'contents' } }, ...(props.children || [])); } }); /** * Component factory */ export class ComponentFactory { private registry: ComponentRegistry; private options: ComponentFactoryOptions; constructor(options: ComponentFactoryOptions = {}) { this.registry = options.registry || new ComponentRegistryImpl(); this.options = { devMode: false, ...options }; } /** * Register component */ register(definition: ComponentDefinition): void { this.registry.register(definition); } /** * Create component instance */ create<P extends Props = Props>( nameOrDefinition: string | ComponentDefinition<P>, props: P ): ComponentInstance<P> { const definition = typeof nameOrDefinition === 'string' ? this.registry.get(nameOrDefinition) : nameOrDefinition; if (!definition) { throw new Error(`Component "${nameOrDefinition}" not found`); } const context: ComponentContext = { injected: new Map(), provided: new Map(), registry: this.registry }; return createComponent(definition as ComponentDefinition<P>, props, context); } /** * Get registry */ getRegistry(): ComponentRegistry { return this.registry; } } /** * Default component factory */ export const componentFactory = new ComponentFactory(); /** * Global component registration */ export function registerComponent(definition: ComponentDefinition): void { componentFactory.register(definition); } /** * Create component from name */ export function createComponentByName<P extends Props = Props>( name: string, props: P ): ComponentInstance<P> { return componentFactory.create(name, props); }