jelenjs
Version:
Core runtime library for JelenJS - an experimental UI framework with fine-grained reactivity
1,426 lines (1,406 loc) • 49.1 kB
JavaScript
/**
* 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