Update sigpro-ssr.js
This commit is contained in:
@@ -1,87 +1,234 @@
|
||||
/**
|
||||
* SigPro Core v1.1.13 - Full SSR & Hydration Support
|
||||
* SigPro Core v1.2.0 SSR + Hydration Support
|
||||
*/
|
||||
(() => {
|
||||
const isServer = typeof window === "undefined";
|
||||
|
||||
const isServer = typeof window === "undefined" || typeof document === "undefined";
|
||||
const _global = isServer ? global : window;
|
||||
|
||||
let activeEffect = null;
|
||||
let currentOwner = null;
|
||||
let SSR_MODE = false;
|
||||
let HYDRATE_PTR = null; // Puntero al nodo real en el DOM durante hidratación
|
||||
|
||||
// --- MOCK DOM PARA NODE.JS ---
|
||||
const _doc = !isServer ? document : {
|
||||
createElement: (tag) => ({ tagName: tag.toUpperCase(), childNodes: [], appendChild: () => {}, setAttribute: () => {}, style: {} }),
|
||||
createTextNode: (txt) => ({ nodeType: 3, textContent: txt }),
|
||||
createComment: (txt) => ({ nodeType: 8, textContent: txt })
|
||||
};
|
||||
|
||||
// --- REACTIVITY CORE ---
|
||||
const effectQueue = new Set();
|
||||
let isFlushing = false;
|
||||
const MOUNTED_NODES = new WeakMap();
|
||||
let SSR_MODE = false;
|
||||
let HYDRATE_PTR = null;
|
||||
|
||||
const _doc = !isServer ? document : {
|
||||
createElement: (tag) => ({
|
||||
tagName: tag.toUpperCase(),
|
||||
style: {},
|
||||
childNodes: [],
|
||||
_cleanups: new Set(),
|
||||
appendChild: () => { },
|
||||
setAttribute: () => { },
|
||||
removeAttribute: () => { },
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
insertBefore: () => { },
|
||||
remove: () => { }
|
||||
}),
|
||||
createTextNode: (txt) => ({ nodeType: 3, textContent: txt, remove: () => { } }),
|
||||
createComment: (txt) => ({ nodeType: 8, textContent: txt, remove: () => { } })
|
||||
};
|
||||
|
||||
/** Flushes all pending effects in the queue */
|
||||
const flush = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = true;
|
||||
for (const eff of Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0))) {
|
||||
if (!eff._deleted) eff();
|
||||
}
|
||||
while (effectQueue.size > 0) {
|
||||
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
||||
effectQueue.clear();
|
||||
for (const eff of sorted) if (!eff._deleted) eff();
|
||||
}
|
||||
isFlushing = false;
|
||||
};
|
||||
|
||||
/** Tracks the current active effect in the given subscriptions set */
|
||||
const track = (subs) => {
|
||||
if (SSR_MODE || !activeEffect || activeEffect._deleted) return;
|
||||
subs.add(activeEffect);
|
||||
activeEffect._deps.add(subs);
|
||||
};
|
||||
|
||||
/** Triggers all effects subscribed to the given set */
|
||||
const trigger = (subs) => {
|
||||
if (SSR_MODE) return;
|
||||
for (const eff of subs) {
|
||||
if (eff === activeEffect || eff._deleted) continue;
|
||||
eff._isComputed ? (eff.markDirty(), eff._subs && trigger(eff._subs)) : effectQueue.add(eff);
|
||||
if (eff._isComputed) {
|
||||
eff.markDirty();
|
||||
if (eff._subs) trigger(eff._subs);
|
||||
} else {
|
||||
effectQueue.add(eff);
|
||||
}
|
||||
}
|
||||
if (!isFlushing) queueMicrotask(flush);
|
||||
};
|
||||
|
||||
// --- SIGNALS ($) ---
|
||||
/** Recursively sweeps and cleans up a node and its children */
|
||||
const sweep = (node) => {
|
||||
if (node && node._cleanups) {
|
||||
node._cleanups.forEach((f) => f());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
if (node && node.childNodes) {
|
||||
node.childNodes.forEach(sweep);
|
||||
}
|
||||
};
|
||||
|
||||
/** Internal view factory that creates reactive views */
|
||||
const _view = (fn) => {
|
||||
const cleanups = new Set();
|
||||
const prev = currentOwner;
|
||||
currentOwner = { cleanups };
|
||||
|
||||
try {
|
||||
const res = fn({ onCleanup: (f) => cleanups.add(f) });
|
||||
|
||||
if (SSR_MODE || isServer) {
|
||||
const toString = (n) => {
|
||||
if (!n && n !== 0) return '';
|
||||
if (n._isRuntime) return n._ssrString || '';
|
||||
if (Array.isArray(n)) return n.map(toString).join('');
|
||||
if (n && typeof n === 'object' && n.ssr) return n.ssr;
|
||||
if (n && typeof n === 'object' && n.nodeType) {
|
||||
if (n.tagName) return `<${n.tagName.toLowerCase()}>${n.textContent || ''}</${n.tagName.toLowerCase()}>`;
|
||||
return n.textContent || '';
|
||||
}
|
||||
return String(n);
|
||||
};
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
_ssrString: toString(res),
|
||||
destroy: () => { }
|
||||
};
|
||||
}
|
||||
|
||||
const container = _doc.createElement("div");
|
||||
container.style.display = "contents";
|
||||
|
||||
const process = (n) => {
|
||||
if (!n && n !== 0) return;
|
||||
if (n._isRuntime) {
|
||||
cleanups.add(n.destroy);
|
||||
container.appendChild(n.container);
|
||||
} else if (Array.isArray(n)) {
|
||||
n.forEach(process);
|
||||
} else if (n && typeof n === 'object' && n.nodeType) {
|
||||
container.appendChild(n);
|
||||
} else {
|
||||
container.appendChild(_doc.createTextNode(String(n)));
|
||||
}
|
||||
};
|
||||
process(res);
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
_ssrString: null,
|
||||
destroy: () => {
|
||||
cleanups.forEach((f) => f());
|
||||
sweep(container);
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
currentOwner = prev;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a reactive Signal or a Computed Value.
|
||||
*
|
||||
* @param {any|Function} initial - Initial value or a getter function for computed state.
|
||||
* @param {string} [key] - Optional. Key for automatic persistence in localStorage.
|
||||
* @returns {Function} Signal getter/setter. Use `sig()` to read and `sig(val)` to write.
|
||||
*
|
||||
* @example
|
||||
* // Simple signal
|
||||
* const count = $(0);
|
||||
* @example
|
||||
* // Computed signal
|
||||
* const double = $(() => count() * 2);
|
||||
* @example
|
||||
* // Persisted signal
|
||||
* const name = $("John", "user-name");
|
||||
*/
|
||||
const $ = (initial, key = null) => {
|
||||
if (typeof initial === "function") {
|
||||
const subs = new Set();
|
||||
let cached, dirty = true;
|
||||
|
||||
const effect = () => {
|
||||
if (effect._deleted) return;
|
||||
effect._deps.forEach(s => s.delete(effect));
|
||||
effect._deps.forEach((s) => s.delete(effect));
|
||||
effect._deps.clear();
|
||||
const prev = activeEffect;
|
||||
activeEffect = effect;
|
||||
try {
|
||||
const val = initial();
|
||||
if (!Object.is(cached, val) || dirty) { cached = val; dirty = false; if (!SSR_MODE) trigger(subs); }
|
||||
if (!Object.is(cached, val) || dirty) {
|
||||
cached = val;
|
||||
dirty = false;
|
||||
if (!SSR_MODE && !isServer) trigger(subs);
|
||||
}
|
||||
} finally { activeEffect = prev; }
|
||||
};
|
||||
|
||||
effect._deps = new Set();
|
||||
effect._isComputed = true;
|
||||
effect._subs = subs;
|
||||
effect._deleted = false;
|
||||
effect.markDirty = () => (dirty = true);
|
||||
effect.stop = () => { effect._deleted = true; effect._deps.forEach(s => s.delete(effect)); };
|
||||
if (currentOwner && !SSR_MODE) currentOwner.cleanups.add(effect.stop);
|
||||
effect(); // Primera ejecución
|
||||
return () => { if (dirty && !SSR_MODE) effect(); track(subs); return cached; };
|
||||
effect.stop = () => {
|
||||
effect._deleted = true;
|
||||
effect._deps.forEach((s) => s.delete(effect));
|
||||
subs.clear();
|
||||
};
|
||||
|
||||
if (currentOwner && !SSR_MODE && !isServer) {
|
||||
currentOwner.cleanups.add(effect.stop);
|
||||
}
|
||||
|
||||
if (SSR_MODE || isServer) effect();
|
||||
|
||||
return (...args) => {
|
||||
if (args.length && !SSR_MODE && !isServer) {
|
||||
const next = typeof args[0] === "function" ? args[0](cached) : args[0];
|
||||
if (!Object.is(cached, next)) {
|
||||
cached = next;
|
||||
if (key) {
|
||||
try { localStorage.setItem(key, JSON.stringify(cached)); } catch (e) { }
|
||||
}
|
||||
trigger(subs);
|
||||
}
|
||||
}
|
||||
track(subs);
|
||||
return cached;
|
||||
};
|
||||
}
|
||||
|
||||
let value = initial;
|
||||
if (key && !isServer) {
|
||||
if (key && !SSR_MODE && !isServer) {
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) try { value = JSON.parse(saved); } catch { value = saved; }
|
||||
if (saved !== null) {
|
||||
try { value = JSON.parse(saved); } catch { value = saved; }
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
const subs = new Set();
|
||||
return (...args) => {
|
||||
if (args.length && !SSR_MODE) {
|
||||
if (args.length && !SSR_MODE && !isServer) {
|
||||
const next = typeof args[0] === "function" ? args[0](value) : args[0];
|
||||
if (!Object.is(value, next)) {
|
||||
value = next;
|
||||
if (key && !isServer) localStorage.setItem(key, JSON.stringify(value));
|
||||
if (key) {
|
||||
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { }
|
||||
}
|
||||
trigger(subs);
|
||||
}
|
||||
}
|
||||
@@ -90,213 +237,555 @@
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Watches for signal changes and executes a side effect.
|
||||
* Handles automatic cleanup of previous effects.
|
||||
*
|
||||
* @param {Function|Array} target - Function to execute or Array of signals for explicit dependency tracking.
|
||||
* @param {Function} [fn] - If the first parameter is an Array, this is the callback function.
|
||||
* @returns {Function} Function to manually stop the watcher.
|
||||
* @example
|
||||
* // Automatic dependency tracking
|
||||
* $watch(() => console.log("Count is:", count()));
|
||||
* @example
|
||||
* // Explicit dependency tracking
|
||||
* $watch([count], () => console.log("Only runs when count changes"));
|
||||
*/
|
||||
const $watch = (target, fn) => {
|
||||
if (SSR_MODE) return () => {};
|
||||
const isArr = Array.isArray(target);
|
||||
const cb = isArr ? fn : target;
|
||||
if (SSR_MODE || isServer) return () => { };
|
||||
|
||||
const isExplicit = Array.isArray(target);
|
||||
const callback = isExplicit ? fn : target;
|
||||
const depsInput = isExplicit ? target : null;
|
||||
|
||||
if (typeof callback !== "function") return () => { };
|
||||
|
||||
const owner = currentOwner;
|
||||
const runner = () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deps.forEach(s => s.delete(runner)); runner._deps.clear();
|
||||
runner._cleanups.forEach(c => c()); runner._cleanups.clear();
|
||||
const prevEff = activeEffect, prevOwn = currentOwner;
|
||||
activeEffect = runner; currentOwner = { cleanups: runner._cleanups };
|
||||
runner.depth = prevEff ? prevEff.depth + 1 : 0;
|
||||
try { isArr ? (cb(), target.forEach(d => typeof d === "function" && d())) : cb(); }
|
||||
finally { activeEffect = prevEff; currentOwner = prevOwn; }
|
||||
runner._deps.forEach((s) => s.delete(runner));
|
||||
runner._deps.clear();
|
||||
runner._cleanups.forEach((c) => c());
|
||||
runner._cleanups.clear();
|
||||
|
||||
const prevEffect = activeEffect;
|
||||
const prevOwner = currentOwner;
|
||||
activeEffect = runner;
|
||||
currentOwner = { cleanups: runner._cleanups };
|
||||
runner.depth = prevEffect ? prevEffect.depth + 1 : 0;
|
||||
|
||||
try {
|
||||
if (isExplicit) {
|
||||
activeEffect = null;
|
||||
callback();
|
||||
activeEffect = runner;
|
||||
depsInput.forEach(d => typeof d === "function" && d());
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
} finally {
|
||||
activeEffect = prevEffect;
|
||||
currentOwner = prevOwner;
|
||||
}
|
||||
};
|
||||
runner._deps = new Set(); runner._cleanups = new Set();
|
||||
runner.stop = () => { runner._deleted = true; runner._deps.forEach(s => s.delete(runner)); runner._cleanups.forEach(c => c()); };
|
||||
if (currentOwner) currentOwner.cleanups.add(runner.stop);
|
||||
|
||||
runner._deps = new Set();
|
||||
runner._cleanups = new Set();
|
||||
runner._deleted = false;
|
||||
runner.stop = () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach((s) => s.delete(runner));
|
||||
runner._cleanups.forEach((c) => c());
|
||||
if (owner) owner.cleanups.delete(runner.stop);
|
||||
};
|
||||
|
||||
if (owner) owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
};
|
||||
|
||||
// --- VIRTUAL VIEW / HYDRATION ENGINE ---
|
||||
const _view = (fn) => {
|
||||
const cleanups = new Set();
|
||||
const prev = currentOwner;
|
||||
currentOwner = { cleanups };
|
||||
try {
|
||||
const res = fn({ onCleanup: f => cleanups.add(f) });
|
||||
if (SSR_MODE) {
|
||||
const toStr = (n) => {
|
||||
if (Array.isArray(n)) return n.map(toStr).join('');
|
||||
return n?._isRuntime ? n._ssrString : (n?.ssr || String(n ?? ''));
|
||||
};
|
||||
return { _isRuntime: true, _ssrString: toStr(res) };
|
||||
}
|
||||
const container = _doc.createElement("div");
|
||||
container.style.display = "contents";
|
||||
const process = (n) => {
|
||||
if (!n) return;
|
||||
if (n._isRuntime) container.appendChild(n.container);
|
||||
else if (Array.isArray(n)) n.forEach(process);
|
||||
else container.appendChild(n.nodeType ? n : _doc.createTextNode(String(n)));
|
||||
};
|
||||
process(res);
|
||||
return { _isRuntime: true, container, destroy: () => { cleanups.forEach(f => f()); container.remove(); } };
|
||||
} finally { currentOwner = prev; }
|
||||
};
|
||||
|
||||
// --- HTML TAG ENGINE ---
|
||||
/**
|
||||
* DOM element rendering engine with built-in reactivity.
|
||||
*
|
||||
* @param {string} tag - HTML tag name (e.g., 'div', 'span').
|
||||
* @param {Object} [props] - Attributes, events (onEvent), or two-way bindings (value, checked).
|
||||
* @param {Array|any} [content] - Children: text, other nodes, or reactive signals.
|
||||
* @returns {HTMLElement|Object} The configured reactive DOM element (or SSR object).
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* const button = Button({ onClick: () => alert("Clicked!") }, "Click me");
|
||||
* @example
|
||||
* // With reactive content
|
||||
* const counter = Div(
|
||||
* Span(() => `Count: ${count()}`),
|
||||
* Button({ onClick: () => count(count() + 1) }, "Increment")
|
||||
* );
|
||||
* @example
|
||||
* // Two-way binding
|
||||
* const input = Input({ value: name, onInput: (e) => name(e.target.value) });
|
||||
*/
|
||||
const $html = (tag, props = {}, content = []) => {
|
||||
if (props.nodeType || Array.isArray(props) || typeof props !== "object") { content = props; props = {}; }
|
||||
// Normalize arguments
|
||||
if (props && (props.nodeType || Array.isArray(props) || (typeof props !== "object"))) {
|
||||
content = props;
|
||||
props = {};
|
||||
}
|
||||
|
||||
if (SSR_MODE) {
|
||||
if (SSR_MODE || isServer) {
|
||||
let attrs = '';
|
||||
for (let [k, v] of Object.entries(props)) {
|
||||
let hasDynamic = false;
|
||||
|
||||
for (let [k, v] of Object.entries(props || {})) {
|
||||
if (k === "ref" || k.startsWith("on")) continue;
|
||||
const val = typeof v === "function" ? v() : v;
|
||||
if (val !== false && val != null) attrs += ` ${k === "class" ? "class" : k}="${val}"`;
|
||||
|
||||
if (typeof v === "function") {
|
||||
hasDynamic = true;
|
||||
const val = v();
|
||||
if (k === "class" && val) attrs += ` class="${val}"`;
|
||||
else if (val != null && val !== false) attrs += ` ${k}="${val}"`;
|
||||
} else if (v !== undefined && v !== null && v !== false) {
|
||||
if (k === "class") attrs += ` class="${v}"`;
|
||||
else attrs += ` ${k}="${v}"`;
|
||||
}
|
||||
const children = [].concat(content).map(c => {
|
||||
const v = typeof c === "function" ? c() : c;
|
||||
return v?._isRuntime ? v._ssrString : (v?.ssr || String(v ?? ''));
|
||||
}).join('');
|
||||
return { ssr: `<${tag}${attrs}>${children}</${tag}>` };
|
||||
}
|
||||
|
||||
// CLIENT / HYDRATION
|
||||
const processChild = (c) => {
|
||||
if (!c && c !== 0) return '';
|
||||
if (typeof c === "function") {
|
||||
hasDynamic = true;
|
||||
const res = c();
|
||||
if (res && res._isRuntime) return res._ssrString || '';
|
||||
if (Array.isArray(res)) return res.map(processChild).join('');
|
||||
return String(res ?? '');
|
||||
}
|
||||
if (c && c._isRuntime) return c._ssrString || '';
|
||||
if (Array.isArray(c)) return c.map(processChild).join('');
|
||||
if (c && typeof c === 'object' && c.ssr) return c.ssr;
|
||||
return String(c ?? '');
|
||||
};
|
||||
|
||||
const children = Array.isArray(content)
|
||||
? content.map(processChild).join('')
|
||||
: processChild(content);
|
||||
|
||||
const result = `<${tag}${attrs}>${children}</${tag}>`;
|
||||
|
||||
return { ssr: result, _isRuntime: false };
|
||||
}
|
||||
|
||||
let el;
|
||||
if (HYDRATE_PTR && HYDRATE_PTR.tagName === tag.toUpperCase()) {
|
||||
el = HYDRATE_PTR;
|
||||
HYDRATE_PTR = el.firstChild; // Entramos al primer hijo para la recursión
|
||||
HYDRATE_PTR = el.firstChild;
|
||||
} else {
|
||||
el = _doc.createElement(tag);
|
||||
}
|
||||
|
||||
el._cleanups = el._cleanups || new Set();
|
||||
for (let [k, v] of Object.entries(props)) {
|
||||
if (k === "ref") { typeof v === "function" ? v(el) : (v.current = el); continue; }
|
||||
if (k.startsWith("on")) {
|
||||
const name = k.slice(2).toLowerCase();
|
||||
el.addEventListener(name, v);
|
||||
el._cleanups.add(() => el.removeEventListener(name, v));
|
||||
} else if (typeof v === "function") {
|
||||
|
||||
for (let [k, v] of Object.entries(props || {})) {
|
||||
if (k === "ref") {
|
||||
typeof v === "function" ? v(el) : (v.current = el);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isSignal = typeof v === "function";
|
||||
const isInput = ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
|
||||
const isBindAttr = (k === "value" || k === "checked");
|
||||
|
||||
if (isInput && isBindAttr && isSignal) {
|
||||
el._cleanups.add($watch(() => {
|
||||
const val = v();
|
||||
if (el[k] !== val) el[k] = val;
|
||||
}));
|
||||
const evt = k === "checked" ? "change" : "input";
|
||||
const handler = (e) => v(e.target[k]);
|
||||
el.addEventListener(evt, handler);
|
||||
el._cleanups.add(() => el.removeEventListener(evt, handler));
|
||||
} else if (k.startsWith("on")) {
|
||||
const name = k.slice(2).toLowerCase().split(".")[0];
|
||||
const handler = (e) => v(e);
|
||||
el.addEventListener(name, handler);
|
||||
el._cleanups.add(() => el.removeEventListener(name, handler));
|
||||
} else if (isSignal) {
|
||||
el._cleanups.add($watch(() => {
|
||||
const val = v();
|
||||
if (k === "class") el.className = val || "";
|
||||
else val == null ? el.removeAttribute(k) : el.setAttribute(k, val);
|
||||
else if (val == null) el.removeAttribute(k);
|
||||
else el.setAttribute(k, val);
|
||||
}));
|
||||
} else if (!el.hasAttribute(k)) el.setAttribute(k, v);
|
||||
} else if (v !== undefined && v !== null && v !== false) {
|
||||
if (!el.hasAttribute(k)) el.setAttribute(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
const append = (c) => {
|
||||
if (Array.isArray(c)) return c.forEach(append);
|
||||
if (typeof c === "function") {
|
||||
const marker = _doc.createTextNode("");
|
||||
if (!HYDRATE_PTR) el.appendChild(marker);
|
||||
|
||||
if (HYDRATE_PTR) {
|
||||
el.insertBefore(marker, HYDRATE_PTR);
|
||||
} else {
|
||||
el.appendChild(marker);
|
||||
}
|
||||
|
||||
let nodes = [];
|
||||
el._cleanups.add($watch(() => {
|
||||
const res = c();
|
||||
const next = (Array.isArray(res) ? res : [res]).map(i => i?._isRuntime ? i.container : (i instanceof Node ? i : _doc.createTextNode(i ?? "")));
|
||||
nodes.forEach(n => n.remove());
|
||||
next.forEach(n => marker.parentNode?.insertBefore(n, marker));
|
||||
const next = (Array.isArray(res) ? res : [res]).map((i) => {
|
||||
if (i && i._isRuntime) return i.container;
|
||||
if (i && i.nodeType) return i;
|
||||
return _doc.createTextNode(i ?? "");
|
||||
});
|
||||
|
||||
nodes.forEach((n) => { sweep(n); n.remove(); });
|
||||
|
||||
next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
|
||||
nodes = next;
|
||||
}));
|
||||
} else if (c && c._isRuntime) {
|
||||
if (!HYDRATE_PTR) el.appendChild(c.container);
|
||||
} else if (c && c.nodeType) {
|
||||
if (!HYDRATE_PTR) el.appendChild(c);
|
||||
} else {
|
||||
const child = c?._isRuntime ? c.container : (c instanceof Node ? c : _doc.createTextNode(String(c ?? "")));
|
||||
if (!HYDRATE_PTR) el.appendChild(child);
|
||||
else if (HYDRATE_PTR.nodeType === 3) HYDRATE_PTR = HYDRATE_PTR.nextSibling; // Saltar texto ya hidratado
|
||||
if (HYDRATE_PTR && HYDRATE_PTR.nodeType === 3) {
|
||||
const textNode = HYDRATE_PTR;
|
||||
HYDRATE_PTR = textNode.nextSibling;
|
||||
} else {
|
||||
const textNode = _doc.createTextNode(c ?? "");
|
||||
if (!HYDRATE_PTR) el.appendChild(textNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
append(content);
|
||||
|
||||
if (el === HYDRATE_PTR?.parentNode) HYDRATE_PTR = el.nextSibling; // Volvemos al nivel superior
|
||||
if (el === HYDRATE_PTR?.parentNode) HYDRATE_PTR = el.nextSibling;
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
// --- CONTROL FLOW ---
|
||||
const $if = (cond, t, f = null) => {
|
||||
if (SSR_MODE) {
|
||||
const b = (typeof cond === "function" ? cond() : cond) ? t : f;
|
||||
return b ? (typeof b === "function" ? b() : b) : '';
|
||||
/**
|
||||
* Conditional rendering component.
|
||||
*
|
||||
* @param {Function|boolean} condition - Reactive signal or boolean value.
|
||||
* @param {Function|HTMLElement} thenVal - Content to show if true.
|
||||
* @param {Function|HTMLElement} [otherwiseVal] - Content to show if false (optional).
|
||||
* @returns {HTMLElement|string} A reactive container (or SSR string).
|
||||
* @example
|
||||
* $if(show, () => Div("Visible"), () => Div("Hidden"))
|
||||
*/
|
||||
const $if = (condition, thenVal, otherwiseVal = null) => {
|
||||
if (SSR_MODE || isServer) {
|
||||
const state = !!(typeof condition === "function" ? condition() : condition);
|
||||
const branch = state ? thenVal : otherwiseVal;
|
||||
if (!branch) return '';
|
||||
const result = typeof branch === "function" ? branch() : branch;
|
||||
if (result && result._isRuntime) return result._ssrString || '';
|
||||
if (result && result.ssr) return result.ssr;
|
||||
if (typeof result === 'string') return result;
|
||||
return String(result);
|
||||
}
|
||||
|
||||
const marker = _doc.createTextNode("");
|
||||
const container = $html("div", { style: "display:contents" }, [marker]);
|
||||
let curr = null, last = null;
|
||||
let current = null, last = null;
|
||||
|
||||
$watch(() => {
|
||||
const s = !!(typeof cond === "function" ? cond() : cond);
|
||||
if (s !== last) {
|
||||
last = s; if (curr) curr.destroy();
|
||||
const b = s ? t : f;
|
||||
if (b) { curr = _view(() => typeof b === "function" ? b() : b); container.insertBefore(curr.container, marker); }
|
||||
const state = !!(typeof condition === "function" ? condition() : condition);
|
||||
if (state !== last) {
|
||||
last = state;
|
||||
if (current) current.destroy();
|
||||
const branch = state ? thenVal : otherwiseVal;
|
||||
if (branch) {
|
||||
current = _view(() => typeof branch === "function" ? branch() : branch);
|
||||
container.insertBefore(current.container, marker);
|
||||
}
|
||||
}
|
||||
});
|
||||
return container;
|
||||
};
|
||||
|
||||
const $for = (src, itemFn, keyFn) => {
|
||||
if (SSR_MODE) {
|
||||
return ((typeof src === "function" ? src() : src) || []).map((item, i) => {
|
||||
const r = itemFn(item, i);
|
||||
return r?._isRuntime ? r._ssrString : (r?.ssr || String(r));
|
||||
/**
|
||||
* Negated conditional rendering.
|
||||
* @param {Function|boolean} condition - Reactive signal or boolean value.
|
||||
* @param {Function|HTMLElement} thenVal - Content to show if false.
|
||||
* @param {Function|HTMLElement} [otherwiseVal] - Content to show if true (optional).
|
||||
*/
|
||||
$if.not = (condition, thenVal, otherwiseVal) =>
|
||||
$if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal);
|
||||
|
||||
/**
|
||||
* Optimized reactive loop with key-based reconciliation.
|
||||
*
|
||||
* @param {Function|Array} source - Signal containing an Array of data.
|
||||
* @param {Function} render - Function receiving (item, index) and returning a node.
|
||||
* @param {Function} keyFn - Function to extract a unique key from the item.
|
||||
* @returns {HTMLElement|string} A reactive container (or SSR string).
|
||||
* @example
|
||||
* $for(items, (item) => Li(item.name), (item) => item.id)
|
||||
*/
|
||||
const $for = (source, render, keyFn) => {
|
||||
if (SSR_MODE || isServer) {
|
||||
const items = (typeof source === "function" ? source() : source) || [];
|
||||
return items.map((item, index) => {
|
||||
const result = render(item, index);
|
||||
if (result && result._isRuntime) return result._ssrString || '';
|
||||
if (result && result.ssr) return result.ssr;
|
||||
if (typeof result === 'string') return result;
|
||||
return String(result);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const marker = _doc.createTextNode("");
|
||||
const container = $html("div", { style: "display:contents" }, [marker]);
|
||||
const cache = new Map();
|
||||
|
||||
$watch(() => {
|
||||
const items = (typeof src === "function" ? src() : src) || [];
|
||||
const items = (typeof source === "function" ? source() : source) || [];
|
||||
const newKeys = new Set();
|
||||
items.forEach((item, i) => {
|
||||
const k = keyFn(item, i); newKeys.add(k);
|
||||
let run = cache.get(k);
|
||||
if (!run) { run = _view(() => itemFn(item, i)); cache.set(k, run); }
|
||||
items.forEach((item, index) => {
|
||||
const key = keyFn(item, index);
|
||||
newKeys.add(key);
|
||||
let run = cache.get(key);
|
||||
if (!run) {
|
||||
run = _view(() => render(item, index));
|
||||
cache.set(key, run);
|
||||
}
|
||||
container.insertBefore(run.container, marker);
|
||||
});
|
||||
cache.forEach((r, k) => { if (!newKeys.has(k)) { r.destroy(); cache.delete(k); }});
|
||||
cache.forEach((run, key) => {
|
||||
if (!newKeys.has(key)) { run.destroy(); cache.delete(key); }
|
||||
});
|
||||
});
|
||||
return container;
|
||||
};
|
||||
|
||||
// --- ROUTER ---
|
||||
/**
|
||||
* Hash-based (#) routing system with parameter support.
|
||||
*
|
||||
* @param {Array<{path: string, component: Function}>} routes - Route definitions.
|
||||
* @returns {HTMLElement|string} The router outlet container (or SSR string).
|
||||
* @example
|
||||
* const routes = [
|
||||
* { path: "/", component: Home },
|
||||
* { path: "/user/:id", component: UserProfile },
|
||||
* { path: "*", component: NotFound }
|
||||
* ];
|
||||
* $router(routes);
|
||||
*/
|
||||
const $router = (routes) => {
|
||||
const getPath = () => SSR_MODE ? $router._ssrPath : (window.location.hash.replace(/^#/, "") || "/");
|
||||
if (SSR_MODE) {
|
||||
const getPath = () => {
|
||||
if (SSR_MODE) return $router._ssrPath;
|
||||
return window.location.hash.replace(/^#/, "") || "/";
|
||||
};
|
||||
|
||||
if (SSR_MODE || isServer) {
|
||||
const path = getPath();
|
||||
const r = routes.find(rt => rt.path === path || rt.path === "*") || routes[0];
|
||||
const res = r.component({});
|
||||
return typeof res === "function" ? $ssr(res) : (res?.ssr || String(res));
|
||||
const route = routes.find(r => {
|
||||
const rp = r.path.split("/").filter(Boolean);
|
||||
const pp = path.split("/").filter(Boolean);
|
||||
return rp.length === pp.length && rp.every((p, i) => p.startsWith(":") || p === pp[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||
if (p.startsWith(":")) params[p.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||
});
|
||||
const result = route.component(params);
|
||||
if (typeof result === "function") return $ssr(result);
|
||||
if (result && result._isRuntime) return result._ssrString || '';
|
||||
if (result && result.ssr) return result.ssr;
|
||||
return String(result);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const sPath = $(getPath());
|
||||
window.addEventListener("hashchange", () => sPath(getPath()));
|
||||
const outlet = $html("div", { class: "router-outlet" });
|
||||
let curr = null;
|
||||
const outlet = Div({ class: "router-outlet" });
|
||||
let current = null;
|
||||
|
||||
$watch([sPath], () => {
|
||||
if (curr) curr.destroy();
|
||||
const r = routes.find(rt => rt.path === sPath() || rt.path === "*") || routes[0];
|
||||
curr = _view(() => r.component({}));
|
||||
outlet.appendChild(curr.container);
|
||||
if (current) current.destroy();
|
||||
const path = sPath();
|
||||
const route = routes.find(r => {
|
||||
const rp = r.path.split("/").filter(Boolean);
|
||||
const pp = path.split("/").filter(Boolean);
|
||||
return rp.length === pp.length && rp.every((p, i) => p.startsWith(":") || p === pp[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||
if (p.startsWith(":")) params[p.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||
});
|
||||
if ($router.params) $router.params(params);
|
||||
current = _view(() => {
|
||||
const res = route.component(params);
|
||||
return typeof res === "function" ? res() : res;
|
||||
});
|
||||
outlet.appendChild(current.container);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
};
|
||||
|
||||
/** Reactive route parameters signal */
|
||||
$router.params = $({});
|
||||
|
||||
/** Navigate to a route */
|
||||
$router.to = (path) => {
|
||||
if (SSR_MODE || isServer) return;
|
||||
window.location.hash = path.replace(/^#?\/?/, "#/");
|
||||
};
|
||||
|
||||
/** Go back in history */
|
||||
$router.back = () => {
|
||||
if (SSR_MODE || isServer) return;
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
/** Get current route path */
|
||||
$router.path = () => {
|
||||
if (SSR_MODE || isServer) return $router._ssrPath || "/";
|
||||
return window.location.hash.replace(/^#/, "") || "/";
|
||||
};
|
||||
|
||||
/** Internal SSR path storage */
|
||||
$router._ssrPath = "/";
|
||||
|
||||
// --- PUBLIC API ---
|
||||
_global.$ssr = (comp, path = "/") => {
|
||||
const prev = SSR_MODE; SSR_MODE = true; $router._ssrPath = path;
|
||||
try { return _view(typeof comp === "function" ? comp : () => comp)._ssrString; }
|
||||
finally { SSR_MODE = prev; }
|
||||
};
|
||||
/**
|
||||
* Mounts a component or node into a DOM target element.
|
||||
* It automatically handles the cleanup of any previously mounted SigPro instances
|
||||
* in that target to prevent memory leaks and duplicate renders.
|
||||
*
|
||||
* @param {Function|HTMLElement} component - The component function to render or a pre-built DOM node.
|
||||
* @param {string|HTMLElement} target - A CSS selector string or a direct DOM element to mount into.
|
||||
* @returns {Object|undefined} The view instance containing the `container` and `destroy` method, or undefined if target is not found.
|
||||
* @example
|
||||
* // Mount using a component function
|
||||
* $mount(() => Div({ class: "app" }, "Hello World"), "#root");
|
||||
* @example
|
||||
* // Mount using a direct element
|
||||
* const myApp = Div("Hello");
|
||||
* $mount(myApp, document.getElementById("app"));
|
||||
*/
|
||||
const $mount = (component, target) => {
|
||||
if (SSR_MODE || isServer) return;
|
||||
|
||||
_global.$mount = (comp, target) => {
|
||||
const el = typeof target === "string" ? document.querySelector(target) : target;
|
||||
if (!el) return;
|
||||
if (el.firstChild) {
|
||||
|
||||
if (el.firstChild && el.firstChild.nodeType === 1) {
|
||||
HYDRATE_PTR = el.firstChild;
|
||||
const inst = _view(typeof comp === "function" ? comp : () => comp);
|
||||
const instance = _view(typeof component === "function" ? component : () => component);
|
||||
HYDRATE_PTR = null;
|
||||
return inst;
|
||||
return instance;
|
||||
}
|
||||
const inst = _view(typeof comp === "function" ? comp : () => comp);
|
||||
el.replaceChildren(inst.container);
|
||||
return inst;
|
||||
|
||||
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
|
||||
const instance = _view(typeof component === "function" ? component : () => component);
|
||||
el.replaceChildren(instance.container);
|
||||
MOUNTED_NODES.set(el, instance);
|
||||
return instance;
|
||||
};
|
||||
|
||||
_global.$ = $; _global.$watch = $watch; _global.$html = $html; _global.$if = $if; _global.$for = $for; _global.$router = $router;
|
||||
// ============================================================================
|
||||
// SSR API ($ssr)
|
||||
// ============================================================================
|
||||
|
||||
const tags = 'div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong i b u mark code form label input textarea select button table tr th td img video audio svg'.split(' ');
|
||||
tags.forEach(t => _global[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $html(t, p, c));
|
||||
/**
|
||||
* Server-Side Rendering: Converts a component to an HTML string.
|
||||
*
|
||||
* @param {Function|HTMLElement} component - The component to render.
|
||||
* @param {string} [path="/"] - Optional route path for SSR routing.
|
||||
* @returns {string} The rendered HTML string.
|
||||
*
|
||||
* @example
|
||||
* // In Node.js
|
||||
* const html = $ssr(App, "/users/123");
|
||||
* res.send(`<!DOCTYPE html><body><div id="app">${html}</div></body>`);
|
||||
*/
|
||||
const $ssr = (component, path = "/") => {
|
||||
const prev = SSR_MODE;
|
||||
SSR_MODE = true;
|
||||
$router._ssrPath = path;
|
||||
try {
|
||||
const result = _view(typeof component === "function" ? component : () => component);
|
||||
return result._ssrString || '';
|
||||
} catch (err) {
|
||||
console.error('SSR Error:', err);
|
||||
return `<div>Error rendering component: ${err.message}</div>`;
|
||||
} finally {
|
||||
SSR_MODE = prev;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// GLOBAL API INJECTION
|
||||
// ============================================================================
|
||||
|
||||
const core = { $, $if, $for, $watch, $mount, $router, $html, $ssr };
|
||||
|
||||
for (const [name, fn] of Object.entries(core)) {
|
||||
Object.defineProperty(_global, name, {
|
||||
value: fn,
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTML TAG HELPERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Auto-generated HTML tag helpers (Div, Span, Button, etc.)
|
||||
* These provide a JSX-like experience without compilation.
|
||||
*
|
||||
* @example
|
||||
* Div({ class: "container" },
|
||||
* H1("Title"),
|
||||
* Button({ onClick: handleClick }, "Click")
|
||||
* )
|
||||
*/
|
||||
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address
|
||||
ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog
|
||||
form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption
|
||||
img video audio canvas svg iframe picture source progress meter`.split(/\s+/);
|
||||
|
||||
tags.forEach((tagName) => {
|
||||
const helperName = tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
||||
Object.defineProperty(_global, helperName, {
|
||||
value: (props, content) => $html(tagName, props, content),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
// ============================================================================
|
||||
// MODULE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
$: global.$,
|
||||
$watch: global.$watch,
|
||||
$html: global.$html,
|
||||
$if: global.$if,
|
||||
$for: global.$for,
|
||||
$router: global.$router,
|
||||
$mount: global.$mount,
|
||||
$ssr: global.$ssr
|
||||
};
|
||||
}
|
||||
|
||||
export const { $, $watch, $html, $if, $for, $router, $mount, $ssr } = typeof window !== 'undefined' ? window : global;
|
||||
|
||||
Reference in New Issue
Block a user