jelenjs
Version:
Core runtime library for JelenJS - an experimental UI framework with fine-grained reactivity
725 lines (716 loc) • 25.3 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 = () => { };
function debugEffect(effectId, 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;
globalState.microtaskPending;
globalState.globalEffectId;
// Effect states using bitmasks (inspired by SolidJS)
const CLEAN = 0; // Effect is clean (no updates needed)
// 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;
}
/**
* Signal implementation for reactive state management
*/
// 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);
}
/**
* 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;
}
/**
* Shared utility functions for JelenJS
*/
/**
* 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;
}
/**
* 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;
}
/**
* 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(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));
}
}
}
/**
* 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;
}
/**
* 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);
}
}
/**
* 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(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;
};
export { Fragment, jsx, jsxs };
//# sourceMappingURL=jsx-runtime.js.map