UNPKG

jelenjs

Version:

Core runtime library for JelenJS - an experimental UI framework with fine-grained reactivity

1,426 lines (1,406 loc) 49.1 kB
/** * Debug configuration for JelenJS reactive system * Uses environment variables for conditional compilation */ // Main debug flag - will be replaced by bundler const DEBUG_MODE = false ; // Debug utility functions that will be completely removed in production const debugLog = () => { }; // Conditional debug wrapper - will be optimized away in production function debugSignal(signalId, message, ...args) { } function debugEffect(effectId, message, ...args) { } function debugQueue(message, ...args) { } function debugConstants(message, ...args) { } /** * Constants and global state for reactivity system */ // Create a global singleton to ensure all modules share the same state const GLOBAL_KEY = '__JELENJS_REACTIVE_STATE__'; // Get or create global state function getGlobalState() { if (typeof globalThis !== 'undefined') { if (!globalThis[GLOBAL_KEY]) { globalThis[GLOBAL_KEY] = { currentEffect: null, currentContext: null, microtaskPending: false, globalEffectId: 0 }; } return globalThis[GLOBAL_KEY]; } // Fallback for environments without globalThis if (typeof window !== 'undefined') { if (!window[GLOBAL_KEY]) { window[GLOBAL_KEY] = { currentEffect: null, currentContext: null, microtaskPending: false, globalEffectId: 0 }; } return window[GLOBAL_KEY]; } // Final fallback - create local state (not ideal but better than nothing) return { currentEffect: null, currentContext: null, microtaskPending: false, globalEffectId: 0 }; } const globalState = getGlobalState(); // Export getters and setters that use the global state let currentEffect = globalState.currentEffect; let currentContext = globalState.currentContext; let microtaskPending = globalState.microtaskPending; globalState.globalEffectId; // Effect states using bitmasks (inspired by SolidJS) const CLEAN = 0; // Effect is clean (no updates needed) const STALE = 1; // Effect is stale (needs to be run) const PENDING = 2; // Effect is pending (in queue or running) // Execution counter for batching (inspired by SolidJS) let execCount = 0; // Default equality function const defaultEquals = Object.is; // Updater functions function setCurrentEffect(value) { debugConstants(`Current effect changing from #${globalState.currentEffect?.id ?? 'null'} to #${value?.id ?? 'null'}`); globalState.currentEffect = value; currentEffect = value; // Keep local variable in sync return globalState.currentEffect; } function incrementEffectId() { globalState.globalEffectId++; globalState.globalEffectId; // Keep local variable in sync debugConstants(`Created new effect #${globalState.globalEffectId}`); return globalState.globalEffectId; } function setMicrotaskPending(value) { if (globalState.microtaskPending !== value) { debugConstants(); } globalState.microtaskPending = value; microtaskPending = value; // Keep local variable in sync return globalState.microtaskPending; } function incrementExecCount() { globalState.globalEffectId++; // Reuse existing counter execCount = globalState.globalEffectId; debugConstants(); return execCount; } /** * Queue system for managing effect execution */ // Simple queue for pending effects const pendingEffects = new Set(); /** * Enqueue an effect to be executed */ function enqueueEffect(effect) { // Only enqueue if not already queued if ((effect.state & PENDING) === 0) { // Mark as queued effect.state |= PENDING; // Add to pending effects pendingEffects.add(effect); debugQueue(`Enqueued effect #${effect.id}, queue size now: ${pendingEffects.size}`); } } /** * Schedule a microtask to flush the effect queue */ function scheduleFlush() { if (!microtaskPending) { setMicrotaskPending(true); debugQueue(`Scheduling flush of ${pendingEffects.size} effects`); queueMicrotask(flushPendingEffects); } else { debugQueue(`Flush already scheduled, ${pendingEffects.size} effects pending`); } } /** * Flush all pending effects */ function flushPendingEffects() { setMicrotaskPending(false); // Skip if no effects if (pendingEffects.size === 0) { debugQueue(); return; } // Increment execution counter for batching (SolidJS-inspired) const execTime = incrementExecCount(); debugQueue(`Flushing ${pendingEffects.size} effects at execTime ${execTime}`); // Process all effects const effectsToRun = Array.from(pendingEffects); pendingEffects.clear(); // Run each effect for (const effect of effectsToRun) { // Clear PENDING state but keep STALE state if set effect.state &= -3; // Only run the effect if it's dirty and not disposed if (!effect.disposed && (effect.state & STALE)) { // Clear dirty state effect.state &= -2; debugQueue(`Running effect #${effect.id}`); // Run the effect effect(); } else { if (effect.disposed) { debugQueue(`Skipping disposed effect #${effect.id}`); } else if (!(effect.state & STALE)) { debugQueue(`Skipping clean effect #${effect.id}`); } } } // If new effects were created during flush, schedule another flush if (pendingEffects.size > 0) { debugQueue(`${pendingEffects.size} new effects added during flush, scheduling another flush`); scheduleFlush(); } else { debugQueue(); } } /** * Signal implementation for reactive state management */ // Import global state access const GLOBAL_STATE_KEY = '__JELENJS_REACTIVE_STATE__'; function getCurrentEffectFromGlobal() { if (typeof globalThis !== 'undefined' && globalThis[GLOBAL_STATE_KEY]) { return globalThis[GLOBAL_STATE_KEY].currentEffect; } if (typeof window !== 'undefined' && window[GLOBAL_STATE_KEY]) { return window[GLOBAL_STATE_KEY].currentEffect; } return currentEffect; // fallback } // Debug flag is now imported from debug.ts // Global signal registry to ensure all modules share the same signal tracking const SIGNAL_REGISTRY_KEY = '__JELENJS_SIGNAL_REGISTRY__'; function getGlobalSignalRegistry() { if (typeof globalThis !== 'undefined') { if (!globalThis[SIGNAL_REGISTRY_KEY]) { globalThis[SIGNAL_REGISTRY_KEY] = new Set(); } return globalThis[SIGNAL_REGISTRY_KEY]; } if (typeof window !== 'undefined') { if (!window[SIGNAL_REGISTRY_KEY]) { window[SIGNAL_REGISTRY_KEY] = new Set(); } return window[SIGNAL_REGISTRY_KEY]; } // Fallback return new Set(); } const globalSignalRegistry = getGlobalSignalRegistry(); // Symbol to mark signal getter functions (more reliable than WeakSet) const SIGNAL_GETTER_SYMBOL = Symbol('signalGetter'); /** * Check if a function is a signal getter */ function isSignalGetter(fn) { if (typeof fn !== 'function') return false; // Check Symbol first (most reliable) if (fn[SIGNAL_GETTER_SYMBOL] === true) { return true; } // Check global registry as fallback return globalSignalRegistry.has(fn); } /** * Create a reactive signal with optimized getter/setter * * @param initialValue The initial value of the signal * @param equals Optional equality function to determine when to trigger updates * @returns A tuple of [getter, setter] functions */ function createSignal(initialValue, equals = defaultEquals) { // Generate a unique ID for this signal for debugging const signalId = Math.random().toString(36).substr(2, 5); debugSignal(signalId, 'Created with initial value:', initialValue); // Simple structure to store signal state (SolidJS-inspired) const node = { value: initialValue, // Current value subscribers: new Set(), // Set of dependent effects (legacy) observers: null, // Array of observers (SolidJS-style) observerSlots: null // Slots for O(1) removal }; // Getter function - tracks dependencies during effect execution function getter() { const globalCurrentEffect = getCurrentEffectFromGlobal(); debugSignal(signalId, 'Getter called, currentEffect:', globalCurrentEffect); // If we're inside an effect, add it as a subscriber if (globalCurrentEffect !== null) { debugSignal(signalId, `Adding effect #${globalCurrentEffect.id} as subscriber`); // Add current effect to this signal's subscribers (legacy Set-based) node.subscribers.add(globalCurrentEffect); // Add this signal to effect's sources (new array-based) const sourceIndex = globalCurrentEffect.sources.length; globalCurrentEffect.sources.push(node); globalCurrentEffect.sourceSlots.push(node.subscribers.size - 1); debugSignal(signalId, `Tracked by effect #${globalCurrentEffect.id} at index ${sourceIndex}`); } else { debugSignal(); } // Return the current value return node.value; } // Mark the getter function as a signal getter for proper detection getter[SIGNAL_GETTER_SYMBOL] = true; globalSignalRegistry.add(getter); debugSignal(signalId, 'Marked getter with Symbol:', getter[SIGNAL_GETTER_SYMBOL]); debugSignal(signalId, 'Added to global registry, size:', globalSignalRegistry.size); // Setter function - updates value and notifies subscribers function setter(newValue) { // Handle updater functions if (typeof newValue === 'function') { // Cast to correct type to handle updater function newValue = newValue(node.value); } // Only update if value changed (using provided equality function) if (!equals(node.value, newValue)) { debugSignal(signalId, 'Value changing from', node.value, 'to', newValue); // Update the value node.value = newValue; // Skip notification if no subscribers if (node.subscribers.size === 0) { debugSignal(); return node.value; } debugSignal(signalId, `Notifying ${node.subscribers.size} subscribers`); // Create a copy of subscribers to avoid issues if the set changes during iteration const subscribers = Array.from(node.subscribers); // Notify all subscribers by marking them stale and enqueueing for (const effect of subscribers) { if (effect && !effect.disposed) { // Mark as stale effect.state |= STALE; // Add to the queue for execution enqueueEffect(effect); debugSignal(signalId, `Marked effect #${effect.id} as stale`); } } // Schedule a flush to run all pending effects scheduleFlush(); } else { debugSignal(); } return node.value; } // Return the getter/setter tuple return [getter, setter]; } /** * Effect implementation for reactive computations */ /** * Creates an effect that tracks reactive dependencies * * @param fn The function to run as an effect * @returns A dispose function that can be called to clean up the effect */ function createEffect(fn) { // Create the effect function const effect = () => { // Skip if already disposed if (effect.disposed) { debugEffect(effect.id); return; } debugEffect(effect.id); // Run cleanup functions if any if (effect.cleanupFns && effect.cleanupFns.length > 0) { debugEffect(effect.id, `Running ${effect.cleanupFns.length} cleanup functions`); for (let i = 0; i < effect.cleanupFns.length; i++) { effect.cleanupFns[i](); } effect.cleanupFns = []; } // Clear previous dependencies if (effect.sources && effect.sources.length > 0) { debugEffect(effect.id, `Clearing ${effect.sources.length} sources`); // Remove this effect from all previous sources for (let i = 0; i < effect.sources.length; i++) { const sourceNode = effect.sources[i]; sourceNode.subscribers.delete(effect); } effect.sources.length = 0; // Clear array effect.sourceSlots.length = 0; // Clear slots } // Prevent recursive effect execution if (effect.running) { debugEffect(effect.id); return; } effect.running = true; // Set as current effect during execution to track dependencies const prevEffect = currentEffect; setCurrentEffect(effect); debugEffect(effect.id, 'Set as currentEffect:', currentEffect); try { // Run the effect function - this will track dependencies via signal getters const result = fn(); // Handle cleanup function returned from effect if (typeof result === 'function') { debugEffect(effect.id, 'Registered cleanup function'); effect.cleanupFns.push(result); } } catch (error) { console.error(`[EFFECT #${effect.id}] Error in effect:`, error); } finally { // Restore previous effect setCurrentEffect(prevEffect); effect.running = false; debugEffect(effect.id, 'Restored currentEffect to:', currentEffect); } }; // Initialize effect properties effect.id = incrementEffectId(); effect.disposed = false; effect.running = false; effect.state = CLEAN; effect.cleanupFns = []; effect.sources = []; // Array instead of Set effect.sourceSlots = []; // Track positions for O(1) removal effect.updatedAt = null; // Track when last updated effect.parentContext = currentContext; debugEffect(effect.id); // Register with parent context if available if (currentContext && currentContext.effects) { currentContext.effects.add(effect); debugEffect(effect.id); } // Dispose function to clean up the effect effect.dispose = () => { if (effect.disposed) { debugEffect(effect.id); return; } debugEffect(effect.id); effect.disposed = true; // Run cleanup functions if (effect.cleanupFns.length > 0) { debugEffect(effect.id, `Running ${effect.cleanupFns.length} cleanup functions before disposal`); for (let i = 0; i < effect.cleanupFns.length; i++) { effect.cleanupFns[i](); } effect.cleanupFns = []; } // Remove from all sources if (effect.sources && effect.sources.length > 0) { debugEffect(effect.id, `Removing from ${effect.sources.length} sources`); for (let i = 0; i < effect.sources.length; i++) { const sourceNode = effect.sources[i]; sourceNode.subscribers.delete(effect); } effect.sources.length = 0; // Clear array effect.sourceSlots.length = 0; // Clear slots } // Remove from parent context if (effect.parentContext && effect.parentContext.effects) { effect.parentContext.effects.delete(effect); debugEffect(effect.id); } }; // Initial execution to establish dependencies debugEffect(effect.id); effect(); // Return the dispose function return effect.dispose; } /** * Memo implementation for derived values */ /** * Creates a memoized value that recalculates only when its dependencies change * * @param fn The computation function to memoize * @returns A getter function for the memoized value */ function createMemo(fn) { // Create a signal to store the result const [value, setValue] = createSignal(undefined); // Use an effect to update the value when dependencies change createEffect(() => { setValue(fn()); // Return void to satisfy the effect type return; }); // Return the getter return value; } /** * Batching system for grouping reactive updates */ // Batching state let batchDepth = 0; /** * Run a function in batch mode, where updates are applied only after * the function completes * * @param fn Function to execute in batch mode * @returns The result of the executed function */ function batch(fn) { // Increment batch depth counter batchDepth++; try { // Execute the function return fn(); } finally { // Decrement batch depth when done batchDepth--; } } /** * Check if we're currently in batch mode */ function isBatching() { return batchDepth > 0; } /** * Reactive system for JelenJS * * This file exports the reactive primitives for the framework */ // Export signal functionality var index = /*#__PURE__*/Object.freeze({ __proto__: null, CLEAN: CLEAN, PENDING: PENDING, STALE: STALE, batch: batch, createEffect: createEffect, createMemo: createMemo, createSignal: createSignal, get currentEffect () { return currentEffect; }, flushPendingEffects: flushPendingEffects, isBatching: isBatching, scheduleFlush: scheduleFlush, setCurrentEffect: setCurrentEffect }); /** * Shared utility functions for JelenJS */ // Re-export DEBUG for backward compatibility const DEBUG = DEBUG_MODE; /** * Helper to detect if an object is a signal */ function isSignal(value) { // Signal detection for the new implementation if (value === null || value === undefined) { return false; } // Check if it's a signal tuple [getter, setter] if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'function' && typeof value[1] === 'function') { return true; } // Check if it's a getter function marked as a signal getter if (isSignalGetter(value)) { return true; } return false; } /** * Generate a unique ID */ function generateId(prefix = '') { return prefix + Math.random().toString(36).substr(2, 9); } /** * Helper to safely get value from a signal * Works with both getter functions and signal tuples * For class attributes, converts boolean value to string ('active' or '') */ function getSignalValue(signal, isClassAttr = false) { let rawValue; if (typeof signal === 'function') { try { // Try to get the value by calling the function rawValue = signal(); if (DEBUG_MODE) ; } catch (e) { rawValue = signal; } } else if (Array.isArray(signal) && typeof signal[0] === 'function') { try { // Try to get the value from the first element of the tuple rawValue = signal[0](); if (DEBUG_MODE) ; } catch (e) { rawValue = signal; } } else { rawValue = signal; } // For class attributes, convert boolean to string value if (isClassAttr && typeof rawValue === 'boolean') { return (rawValue ? 'active' : ''); } return rawValue; } /** * Helper for using signals directly in JSX * This is a convenience function that ensures signals are properly tracked in JSX * * Usage: <div>{useSignal(count)}</div> instead of <div>{count}</div> */ function useSignal(signal) { if (typeof signal === 'function') { try { const result = signal(); if (DEBUG_MODE) ; return result; } catch (e) { return signal; } } else if (Array.isArray(signal) && typeof signal[0] === 'function') { try { const result = signal[0](); if (DEBUG_MODE) ; return result; } catch (e) { return signal; } } return signal; } var utils = /*#__PURE__*/Object.freeze({ __proto__: null, DEBUG: DEBUG, generateId: generateId, getSignalValue: getSignalValue, isSignal: isSignal, useSignal: useSignal }); /** * Simplified DOM handling utilities */ // Store of elements that have signal attributes const signalElementRegistry = new Map(); /** * Create an HTML element with the given tag name, props, and children */ function h$1(tag, props, ...children) { const element = document.createElement(tag); // Set properties and attributes on the element if (props) { for (const [key, value] of Object.entries(props)) { if (key === 'style' && typeof value === 'object') { // Handle style object Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { // Handle event listeners const eventName = key.slice(2).toLowerCase(); element.addEventListener(eventName, value); } else if (isSignal(value)) { // Handle signals const initialValue = getSignalValue(value); updateProperty(element, key, initialValue); // Track signals for this element if (!signalElementRegistry.has(element)) { signalElementRegistry.set(element, new Map()); } signalElementRegistry.get(element).set(key, value); // Create an effect to keep the DOM updated createEffect(() => { const signalValue = getSignalValue(value); updateProperty(element, key, signalValue); }); } else { // Handle regular properties and attributes if (key === 'class' || key === 'className') { element.className = String(value); } else if (key.startsWith('data-')) { element.setAttribute(key, String(value)); } else { element[key] = value; } } } } // Add children for (const child of children) { appendChildToElement(element, child); } return element; } /** * Check if a value is a signal or a function (which might be a signal accessor) */ function isSignalOrFunction(value) { // Skip specific values that we know are not reactive if (value === null || value === undefined || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return false; } // Check if it's a signal (tuple or marked getter function) if (isSignal(value)) { return true; } // Check if it's a function that might be a signal getter if (typeof value === 'function') { // If it's marked as a signal getter, it's definitely reactive if (isSignalGetter(value)) { return true; } // For other functions, we'll treat them as potentially reactive // This handles cases where functions might be signal getters return true; } return false; } /** * Append a child to an element, with special handling for different types */ function appendChildToElement(element, child) { if (child == null) return; // Handle arrays (multiple children or from conditional expressions) if (Array.isArray(child)) { for (const subChild of child) { appendChildToElement(element, subChild); } return; } // Handle DOM nodes directly if (child instanceof Node) { element.appendChild(child); return; } // Handle signals and functions (both {count} and {count()} syntaxes) if (isSignalOrFunction(child)) { const container = document.createElement('span'); container.style.display = 'contents'; // Make it invisible in the layout element.appendChild(container); // Create an effect to update when signal changes createEffect(() => { if (DEBUG_MODE) ; // FIXED: Call the signal getter INSIDE the effect to establish dependency let value; if (typeof child === 'function') { if (DEBUG_MODE) ; value = child(); // This will track the dependency } else if (Array.isArray(child) && typeof child[0] === 'function') { if (DEBUG_MODE) ; value = child[0](); // This will track the dependency } else if (isSignal(child)) { if (DEBUG_MODE) ; value = getSignalValue(child); // This will track the dependency } else { value = child; } if (DEBUG_MODE) ; // Clear previous content container.innerHTML = ''; // Handle different types of values if (value instanceof Node) { container.appendChild(value); } else if (value instanceof Element) { container.appendChild(value); } else if (Array.isArray(value)) { for (const item of value) { appendChildToElement(container, item); } } else if (value === false || value === null || value === undefined) { // For falsy values in conditional rendering, don't render anything } else if (value != null) { container.textContent = String(value); } }); return; } // Handle primitive values element.appendChild(document.createTextNode(String(child))); } /** * Helper to update a property or attribute on an element */ function updateProperty(element, key, value) { if (key === 'class' || key === 'className') { element.className = String(value); } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key === 'disabled' || key === 'checked' || key === 'required') { // Handle boolean attributes if (value) { element.setAttribute(key, ''); } else { element.removeAttribute(key); } } else if (key.startsWith('data-')) { element.setAttribute(key, String(value)); } else { // For other properties, set them directly element[key] = value; // Also set attribute for visibility in DOM if (typeof value !== 'object' && value !== undefined) { element.setAttribute(key, String(value)); } } } /** * Mount a component to a DOM element */ function mount$1(component, container) { const element = component(); container.appendChild(element); return element; } /** * Query selector helper that returns an element */ function query$1(selector) { return document.querySelector(selector); } /** * Query selector that returns multiple elements */ function queryAll$1(selector) { return Array.from(document.querySelectorAll(selector)); } /** * Creates a container element and initializes the app inside it */ function createApp$1(rootComponent, containerSelector = '#app') { const container = query$1(containerSelector); if (!container) { return; } mount$1(rootComponent, container); } var dom = /*#__PURE__*/Object.freeze({ __proto__: null, createApp: createApp$1, h: h$1, mount: mount$1, query: query$1, queryAll: queryAll$1 }); var runtime = /*#__PURE__*/Object.freeze({ __proto__: null }); /** * Component Lifecycle for JelenJS */ // Global state key for component lifecycle const GLOBAL_COMPONENT_STATE_KEY = '__JELENJS_COMPONENT_STATE__'; // Initialize global component state function getGlobalComponentState() { if (typeof globalThis !== 'undefined') { if (!globalThis[GLOBAL_COMPONENT_STATE_KEY]) { globalThis[GLOBAL_COMPONENT_STATE_KEY] = { currentComponent: null, mountCallbacks: new WeakMap(), unmountCallbacks: new WeakMap() }; } return globalThis[GLOBAL_COMPONENT_STATE_KEY]; } if (typeof window !== 'undefined') { if (!window[GLOBAL_COMPONENT_STATE_KEY]) { window[GLOBAL_COMPONENT_STATE_KEY] = { currentComponent: null, mountCallbacks: new WeakMap(), unmountCallbacks: new WeakMap() }; } return window[GLOBAL_COMPONENT_STATE_KEY]; } // Fallback for non-browser environments return { currentComponent: null, mountCallbacks: new WeakMap(), unmountCallbacks: new WeakMap() }; } const globalComponentState = getGlobalComponentState(); /** * Set the current component context (used internally) */ function setCurrentComponent(component) { console.log('[jelenjs-lifecycle] setCurrentComponent called, component:', component); globalComponentState.currentComponent = component; } /** * Get the current component context */ function getCurrentComponent() { return globalComponentState.currentComponent; } /** * Register a callback to be executed when the component is mounted to the DOM * @param callback Function to execute after mounting */ function onMount$1(callback) { console.log('[jelenjs-lifecycle] onMount called, currentComponent:', globalComponentState.currentComponent); if (typeof callback !== 'function') return; // If we have a current component context, register the callback if (globalComponentState.currentComponent) { if (!globalComponentState.mountCallbacks.has(globalComponentState.currentComponent)) { globalComponentState.mountCallbacks.set(globalComponentState.currentComponent, []); } globalComponentState.mountCallbacks.get(globalComponentState.currentComponent).push(callback); } else { // If no component context, execute immediately (fallback behavior) // This ensures compatibility with existing code setTimeout(callback, 0); } } /** * Register a callback to be executed when the component is unmounted from the DOM * @param callback Function to execute before unmounting */ function onUnmount$1(callback) { console.log('[jelenjs-lifecycle] onUnmount called, currentComponent:', globalComponentState.currentComponent); if (typeof callback !== 'function') return; if (globalComponentState.currentComponent) { if (!globalComponentState.unmountCallbacks.has(globalComponentState.currentComponent)) { globalComponentState.unmountCallbacks.set(globalComponentState.currentComponent, []); } globalComponentState.unmountCallbacks.get(globalComponentState.currentComponent).push(callback); } } /** * Register a callback to be executed when the component updates * @param callback Function to execute on update */ function onUpdate$1(callback) { // For updates, we can use createEffect as it tracks dependencies if (typeof callback === 'function') { createEffect(() => { callback(); }); } } /** * Similar to React's useEffect, combines onMount, onUpdate and onUnmount * @param callback Effect function that can return a cleanup function * @param deps Optional dependency array for selective updates */ function useEffect$1(callback, deps) { if (typeof callback === 'function') { createEffect(() => { const cleanup = callback(); // If callback returns a cleanup function, register it for unmount if (typeof cleanup === 'function') { onUnmount$1(cleanup); } }); } } /** * Get the current element being rendered */ function getCurrentElement() { return null; } /** * Set the current element being rendered (for internal use) */ function setCurrentElement(element) { // Simplified implementation, no-op } /** * Mark a component as mounted and run mount callbacks * @param component The component instance */ function markAsMounted(component) { console.log('[jelenjs-lifecycle] markAsMounted called, component:', component); const callbacks = globalComponentState.mountCallbacks.get(component); if (callbacks) { callbacks.forEach((callback) => { try { callback(); } catch (error) { console.error('Error in onMount callback:', error); } }); // Clear callbacks after execution globalComponentState.mountCallbacks.delete(component); } } /** * Handle component unmount and run cleanup * @param component The component instance */ function handleUnmount(component) { const callbacks = globalComponentState.unmountCallbacks.get(component); if (callbacks) { callbacks.forEach((callback) => { try { callback(); } catch (error) { console.error('Error in onUnmount callback:', error); } }); // Clear callbacks after execution globalComponentState.unmountCallbacks.delete(component); } } var component = /*#__PURE__*/Object.freeze({ __proto__: null, getCurrentComponent: getCurrentComponent, getCurrentElement: getCurrentElement, handleUnmount: handleUnmount, markAsMounted: markAsMounted, onMount: onMount$1, onUnmount: onUnmount$1, onUpdate: onUpdate$1, setCurrentComponent: setCurrentComponent, setCurrentElement: setCurrentElement, useEffect: useEffect$1 }); /** * Simplified JSX implementation for JelenJS */ /** * Handle reactive expressions in JSX props and children */ function processReactiveValue(value) { // If it's a function (which might be a signal accessor like count()), // try to call it and get the current value if (typeof value === 'function') { try { const result = value(); return result; } catch (e) { // If it fails, it's not a signal accessor function return value; } } // For regular signals, use getSignalValue if (isSignal(value)) { const result = getSignalValue(value); return result; } return value; } /** * Simple check for conditional expressions in runtime * This is a lightweight runtime check, not a full AST analysis */ function looksLikeConditionalExpression(expr) { // Simple pattern matching for common conditional patterns return /\?\s*.*\s*:/.test(expr) || /&&/.test(expr) || /\|\|/.test(expr); } /** * Auto-wrap conditional expressions for reactivity */ function autoWrapConditional(expr) { // If it's already a function, return it as-is if (typeof expr === 'function') { return expr; } // If it's a string that looks like a conditional expression with signals if (typeof expr === 'string' && looksLikeConditionalExpression(expr)) { // Create a function that evaluates the expression in the current scope // This is experimental and requires the signals to be in global scope try { return new Function('count', `return ${expr}`); } catch (e) { return () => expr; } } // Return a simple function that returns the value return () => expr; } /** * Process JSX children and make them reactive when needed */ function processJSXChildren(children) { if (children == null) return []; // Handle arrays of children if (Array.isArray(children)) { return children.flatMap(processJSXChildren); } // Handle conditional expressions - the key part for automatic conditional rendering if (typeof children === 'object' && children !== null && (('operator' in children && children.operator === '&&') || ('test' in children && 'consequent' in children && 'alternate' in children))) { // Create a reactive function that will evaluate the condition const reactiveCondition = () => { if ('operator' in children && children.operator === '&&') { // For logical AND expressions (condition && <Element/>) const testValue = processReactiveValue(children.left); return testValue ? children.right : null; } else if ('test' in children) { // For ternary expressions (condition ? <TrueElement/> : <FalseElement/>) const testValue = processReactiveValue(children.test); return testValue ? children.consequent : children.alternate; } return null; }; return [reactiveCondition]; } // EXPERIMENTAL: Check if this might be a conditional expression string if (typeof children === 'string' && looksLikeConditionalExpression(children)) { // Auto-wrap the conditional expression return [autoWrapConditional(children)]; } // Check if this is a simple boolean result of a condition if (typeof children === 'boolean') { return [children]; } // Check if this might be a signal or a function that should be reactive if (typeof children === 'function' || isSignal(children)) { return [children]; // Pass the original signal directly, don't wrap it } return [children]; } /** * Factory function for JSX elements */ function jsx(tag, props = {}) { // Extract children from props const { children, ...restProps } = props; // Handle function components if (typeof tag === 'function') { // Set component context for lifecycle hooks const componentInstance = tag; setCurrentComponent(componentInstance); try { const result = tag({ ...restProps, children }); // Schedule mount callbacks to run after the component is rendered setTimeout(() => { markAsMounted(componentInstance); }, 0); return result; } finally { // Clear component context setCurrentComponent(null); } } // Process children const childrenArray = children === undefined ? [] : processJSXChildren(children); // Create element with hyperscript function return h$1(tag, restProps, ...childrenArray); } // Alias for JSX with spread children const jsxs = jsx; // JSX Fragment implementation const Fragment = (props) => { if (!props.children) { return document.createDocumentFragment(); } const fragment = document.createDocumentFragment(); // Process children const childrenArray = processJSXChildren(props.children); for (const child of childrenArray) { if (child instanceof Node || child instanceof Element) { fragment.appendChild(child); } else if (typeof child === 'function') { // Handle reactive functions (from conditional expressions) const container = document.createElement('span'); container.style.display = 'contents'; fragment.appendChild(container); // Set up effect to reactively update the content createEffect(() => { try { const result = child(); // Clear container container.innerHTML = ''; // Update with new content if (result instanceof Node) { container.appendChild(result); } else if (result != null) { container.textContent = String(result); } } catch (e) { // Handle errors silently } }); } else if (child != null) { fragment.appendChild(document.createTextNode(String(child))); } } return fragment; }; /** * JSX Debug utilities for JelenJS */ /** * Debug function to show JSX transformation results * Use this in your templates to see what JSX produces */ function debugJSX(value, label) { const debugElement = document.createElement('pre'); debugElement.style.cssText = ` background: #f0f0f0; border: 1px solid #ccc; padding: 10px; margin: 5px 0; border-radius: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; overflow-x: auto; `; const updateDebugContent = () => { let content = label ? `${label}:\n` : 'JSX Debug:\n'; try { // If it's a function, try to call it and show both the function and result if (typeof value === 'function') { content += `Type: Function\n`; content += `Function: ${value.toString().substring(0, 200)}${value.toString().length > 200 ? '...' : ''}\n`; try { const result = value(); content += `Result: ${JSON.stringify(result, null, 2)}\n`; content += `Result Type: ${typeof result}\n`; if (result instanceof Element) { content += `Element Tag: ${result.tagName}\n`; content += `Element HTML: ${result.outerHTML.substring(0, 200)}${result.outerHTML.length > 200 ? '...' : ''}\n`; } } catch (e) { content += `Function Call Error: ${e.message}\n`; } } else if (isSignal(value)) { content += `Type: Signal\n`; content += `Signal Value: ${JSON.stringify(getSignalValue(value), null, 2)}\n`; } else if (value instanceof Element) { content += `Type: Element\n`; content += `Tag: ${value.tagName}\n`; content += `HTML: ${value.outerHTML.substring(0, 300)}${value.outerHTML.length > 300 ? '...' : ''}\n`; } else { content += `Type: ${typeof value}\n`; content += `Value: ${JSON.stringify(value, null, 2)}\n`; } } catch (e) { content += `Debug Error: ${e.message}\n`; } debugElement.textContent = content; }; // Initial update updateDebugContent(); // If the value is reactive, update when it changes if (typeof value === 'function' || isSignal(value)) { createEffect(() => { updateDebugContent(); }); } return debugElement; } /** * Show the raw JSX transformation result without rendering */ function showJSXTransform(jsxExpression, label) { const container = document.createElement('div'); container.style.cssText = ` background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; margin: 5px 0; border-radius: 4px; `; const title = document.createElement('h4'); title.textContent = label || 'JSX Transform Result'; title.style.margin = '0 0 10px 0'; container.appendChild(title); const pre = document.createElement('pre'); pre.style.cssText = ` background: #f8f9fa; padding: 8px; border-radius: 3px; font-family: monospace; font-size: 11px; overflow-x: auto; margin: 0; `; // Show the structure of what JSX created const analyzeJSX = (expr, depth = 0) => { const indent = ' '.repeat(depth); if (expr === null || expr === undefined) { return `${indent}${expr}\n`; } if (typeof expr === 'string' || typeof expr === 'number' || typeof expr === 'boolean') { return `${indent}"${expr}" (${typeof expr})\n`; } if (typeof expr === 'function') { const funcStr = expr.toString(); const preview = funcStr.substring(0, 100).replace(/\n/g, ' '); return `${indent}Function: ${preview}${funcStr.length > 100 ? '...' : ''}\n`; } if (Array.isArray(expr)) { let result = `${indent}Array[${expr.length}]:\n`; expr.forEach((item, i) => { result += `${indent} [${i}]: `; result += analyzeJSX(item, depth + 2).trim() + '\n'; }); return result; } if (expr instanceof Element) { let result = `${indent}Element: <${expr.tagName.toLowerCase()}`; if (expr.attributes.length > 0) { result += ' '; for (let i = 0; i < expr.attributes.length; i++) { const attr = expr.attributes[i]; result += `${attr.name}="${attr.value}" `; } } result += `>\n`; if (expr.children.length > 0) { result += `${indent} children[${expr.children.length}]:\n`; for (let i = 0; i < expr.children.length; i++) { result += analyzeJSX(expr.children[i], depth + 2); } } return result; } if (typeof expr === 'object') { let result = `${indent}Object:\n`; for (const [key, value] of Object.entries(expr)) { result += `${indent} ${key}: `; result += analyzeJSX(value, depth + 2).trim() + '\n'; } return result; } return `${indent}${typeof expr}: ${String(expr)}\n`; }; pre.textContent = analyzeJSX(jsxExpression); container.appendChild(pre); return container; } /** * Show detailed information about reactive values and their current state */ function debugReactive(value, label) { const container = document.createElement('div'); container.style.cssText = ` background: #e8f5e8; border: 1px solid #4caf50; padding: 10px; margin: 5px 0; border-radius: 4px; `; const title = document.createElement('h4'); title.textContent = label || 'Reactive Debug'; title.style.margin = '0 0 10px 0'; container.appendChild(title); const content = document.createElement('div'); content.style.fontFamily = 'monospace'; content.style.fontSize = '12px'; const updateContent = () => { let info = ''; if (isSignal(value)) { info += `Signal detected\n`; info += `Current value: ${JSON.stringify(getSignalValue(value))}\n`; info += `Value type: ${typeof getSignalValue(value)}\n`; } else if (typeof value === 'function') { info += `Function detected\n`; try { const result = value(); info += `Function result: ${JSON.stringify(result)}\n`; info += `Result type: ${typeof result}\n`; } catch (e) { info += `Function error: ${e.message}\n`; } } else { info += `Static value\n`; info += `Value: ${JSON.stringify(value)}\n`; info += `Type: ${typeof value}\n`; } content.textContent = info; }; updateContent(); // Update reactively if needed if (typeof value === 'function' || isSignal(value)) { createEffect(() => { updateContent(); }); } container.appendChild(content); return container; } /** * JelenJS Framework Core - Runtime Only * Reactive UI framework with fine-grained reactivity */ // Import core modules // Re-export with more convenient names const signal = createSignal; const effect = createEffect; const computed = createMemo; // Re-export specific functions from dom module const { createApp, mount, h, query, queryAll } = dom; // Export component lifecycle functions const { onMount, onUnmount, onUpdate, useEffect } = component; // Export the version const VERSION = '0.1.6'; export { Fragment, VERSION, batch, component, computed, createApp, createEffect, createMemo, createSignal, debugJSX, debugReactive, dom, effect, h, jsx, jsxs, mount, onMount, onUnmount, onUpdate, query, queryAll, index as reactive, runtime, showJSXTransform, signal, useEffect, utils }; //# sourceMappingURL=index.es.js.map