This commit is contained in:
2026-04-08 14:15:34 +02:00
parent d053ba39a1
commit 7879aa463b

View File

@@ -1,195 +1,133 @@
/** /**
* SigPro - Fine-Grained Reactive UI Framework * SigPro v3.3 - Stable Integrated Engine
* Zero-Dependency Single-File Architecture
*/ */
const SigPro = (() => {
const doc = typeof document !== "undefined" ? document : null;
const isArr = Array.isArray, assign = Object.assign, isFunc = (f) => typeof f === "function", isObj = (o) => typeof o === "object" && o !== null;
const ensureNode = (n) => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(String(n ?? "")));
// --- 1. CORE HELPERS (DRY) --- // --- INTERNAL STATE & CLEANUP ---
const doc = typeof document !== "undefined" ? document : null; let activeEffect = null, currentOwner = null, isFlushing = false;
const isArr = Array.isArray; const effectQueue = new Set(), MOUNTED_NODES = new WeakMap();
const assign = Object.assign;
const isFunc = (f) => typeof f === "function";
const isObj = (o) => typeof o === "object" && o !== null;
const createEl = (t) => doc?.createElement(t); const runCleanups = (s) => { s?.forEach(f => f()); s?.clear(); };
const createText = (t) => doc?.createTextNode(String(t ?? "")); const clearDeps = (e) => { e._deps.forEach(d => d.delete(e)); e._deps.clear(); };
const ensureNode = (n) => n?._isRuntime ? n.container : (n instanceof Node ? n : createText(n)); const onUnmount = (fn) => currentOwner && currentOwner.cleanups.add(fn);
// Funciones DRY para reducir repetición en memoria y reactividad const cleanupNode = (node) => {
const runCleanups = (set) => { set?.forEach(fn => fn()); set?.clear(); }; if (node._cleanups) runCleanups(node._cleanups);
const clearDeps = (effect) => { effect._deps.forEach(d => d.delete(effect)); effect._deps.clear(); };
// --- 2. INTERNAL STATE ---
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
// --- 3. SCHEDULER & MEMORY ---
const runWithContext = (effect, callback) => {
const prev = activeEffect;
activeEffect = effect;
try { return callback(); }
finally { activeEffect = prev; }
};
const cleanupNode = (node) => {
runCleanups(node._cleanups);
node.childNodes?.forEach(cleanupNode); node.childNodes?.forEach(cleanupNode);
}; };
const flushEffects = () => { // --- SCHEDULER ---
if (isFlushing) return; const runWithContext = (e, cb) => {
isFlushing = true; const p = activeEffect; activeEffect = e;
try { return cb(); } finally { activeEffect = p; }
};
const flush = () => {
if (isFlushing) return; isFlushing = true;
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0)); const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear(); effectQueue.clear();
sorted.forEach(effect => !effect._deleted && effect()); sorted.forEach(e => !e._deleted && e());
isFlushing = false; isFlushing = false;
}; };
const trackUpdate = (subscribers, isTrigger = false) => { const trackUpdate = (subs, trigger = false) => {
if (!isTrigger && activeEffect && !activeEffect._deleted) { if (!trigger && activeEffect && !activeEffect._deleted) {
subscribers.add(activeEffect); subs.add(activeEffect); activeEffect._deps.add(subs);
activeEffect._deps.add(subscribers); } else if (trigger) {
} else if (isTrigger) { subs.forEach(e => {
subscribers.forEach((eff) => { if (e === activeEffect || e._deleted) return;
if (eff === activeEffect || eff._deleted) return; if (e._isComputed) { e.markDirty(); if (e._subs) trackUpdate(e._subs, true); }
if (eff._isComputed) { eff.markDirty(); if (eff._subs) trackUpdate(eff._subs, true); } else effectQueue.add(e);
else effectQueue.add(eff);
}); });
if (!isFlushing) queueMicrotask(flushEffects); if (!isFlushing) queueMicrotask(flush);
} }
}; };
// --- 4. REACTIVITY --- // --- CORE API ---
const $ = (initialValue, storageKey = null) => { const untrack = (fn) => {
const subscribers = new Set(); const p = activeEffect; activeEffect = null;
try { return fn(); } finally { activeEffect = p; }
};
if (isFunc(initialValue)) { // Computed const $ = (val, key = null) => {
let cachedValue, isDirty = true; const subs = new Set();
const effect = () => { if (isFunc(val)) {
if (effect._deleted) return; let cache, dirty = true;
clearDeps(effect); const e = () => {
runWithContext(effect, () => { if (e._deleted) return;
const newVal = initialValue(); clearDeps(e);
if (!Object.is(cachedValue, newVal) || isDirty) { runWithContext(e, () => {
cachedValue = newVal; isDirty = false; trackUpdate(subscribers, true); const next = val();
} if (!Object.is(cache, next) || dirty) { cache = next; dirty = false; trackUpdate(subs, true); }
}); });
}; };
assign(effect, { assign(e, { _deps: new Set(), _isComputed: true, _subs: subs, _deleted: false, markDirty: () => (dirty = true),
_deps: new Set(), _isComputed: true, _subs: subscribers, _deleted: false, stop: () => { e._deleted = true; clearDeps(e); subs.clear(); } });
markDirty: () => (isDirty = true), onUnmount(e.stop);
stop: () => { effect._deleted = true; clearDeps(effect); subscribers.clear(); } return () => { if (dirty) e(); trackUpdate(subs); return cache; };
});
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => { if (isDirty) effect(); trackUpdate(subscribers); return cachedValue; };
} }
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val; } catch(e){}
// Signal
let value = initialValue;
if (storageKey) try { value = JSON.parse(localStorage.getItem(storageKey) || "null") ?? value; } catch(e){}
return (...args) => { return (...args) => {
if (args.length) { if (args.length) {
const next = isFunc(args[0]) ? args[0](value) : args[0]; const next = isFunc(args[0]) ? args[0](val) : args[0];
if (!Object.is(value, next)) { if (!Object.is(val, next)) {
value = next; val = next; if (key) localStorage.setItem(key, JSON.stringify(val));
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value)); trackUpdate(subs, true);
trackUpdate(subscribers, true);
} }
} }
trackUpdate(subscribers); trackUpdate(subs); return val;
return value; };
}; };
};
const $$ = (object, cache = new WeakMap()) => { const $$ = (obj, cache = new WeakMap()) => {
if (!isObj(object)) return object; if (!isObj(obj)) return obj;
if (cache.has(object)) return cache.get(object); if (cache.has(obj)) return cache.get(obj);
const subs = {}; const subs = {};
const proxy = new Proxy(object, { const proxy = new Proxy(obj, {
get: (t, k) => { trackUpdate(subs[k] ??= new Set()); const v = t[k]; return isObj(v) ? $$(v, cache) : v; }, get: (t, k) => { trackUpdate(subs[k] ??= new Set()); return isObj(t[k]) ? $$(t[k], cache) : t[k]; },
set: (t, k, v) => { if (!Object.is(t[k], v)) { t[k] = v; if (subs[k]) trackUpdate(subs[k], true); } return true; } set: (t, k, v) => { if (!Object.is(t[k], v)) { t[k] = v; if (subs[k]) trackUpdate(subs[k], true); } return true; }
}); });
cache.set(object, proxy); return proxy; cache.set(obj, proxy); return proxy;
}; };
const Watch = (target, callbackFn) => { const Watch = (target, cb) => {
const isExplicit = isArr(target); const explicit = isArr(target), runner = () => {
const callback = isExplicit ? callbackFn : target;
if (!isFunc(callback)) return () => {};
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return; if (runner._deleted) return;
clearDeps(runner); runCleanups(runner._cleanups); clearDeps(runner); runCleanups(runner._cleanups);
runner.depth = activeEffect ? activeEffect.depth + 1 : 0; runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
runWithContext(runner, () => { runWithContext(runner, () => {
const prevOwner = currentOwner; const prev = currentOwner; currentOwner = { cleanups: runner._cleanups };
currentOwner = { cleanups: runner._cleanups }; explicit ? (untrack(cb), target.forEach(d => isFunc(d) && d())) : cb();
if (isExplicit) { runWithContext(null, callback); target.forEach(d => isFunc(d) && d()); } currentOwner = prev;
else callback();
currentOwner = prevOwner;
}); });
}; };
assign(runner, { _deps: new Set(), _cleanups: new Set(), _deleted: false,
assign(runner, { stop: () => { runner._deleted = true; clearDeps(runner); runCleanups(runner._cleanups); } });
_deps: new Set(), _cleanups: new Set(), _deleted: false, onUnmount(runner.stop);
stop: () => {
if (runner._deleted) return;
runner._deleted = true; effectQueue.delete(runner);
clearDeps(runner); runCleanups(runner._cleanups);
if (owner) owner.cleanups.delete(runner.stop);
}
});
if (owner) owner.cleanups.add(runner.stop);
runner(); return runner.stop; runner(); return runner.stop;
};
// --- 5. DOM & COMPONENTS ---
const Render = (renderFn) => {
const cleanups = new Set(), prev = currentOwner, container = createEl("div");
container.style.display = "contents";
currentOwner = { cleanups };
const process = (res) => {
if (!res) return;
if (res._isRuntime) { cleanups.add(res.destroy); container.appendChild(res.container); }
else if (isArr(res)) res.forEach(process);
else container.appendChild(ensureNode(res));
}; };
try { process(renderFn({ onCleanup: (fn) => cleanups.add(fn) })); } const Tag = (tag, props = {}, children = []) => {
finally { currentOwner = prev; }
return { _isRuntime: true, container, destroy: () => { runCleanups(cleanups); cleanupNode(container); container.remove(); } };
};
const Tag = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; } if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; }
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag); const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag); const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag);
el._cleanups = new Set(); el.onUnmount = (fn) => el._cleanups.add(fn); el._cleanups = new Set();
for (let [k, v] of Object.entries(props)) { for (let [k, v] of Object.entries(props)) {
if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; } if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; }
const isSig = isFunc(v);
if (k.startsWith("on")) { if (k.startsWith("on")) {
const evt = k.slice(2).toLowerCase().split(".")[0]; const ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v);
el.addEventListener(evt, v); el._cleanups.add(() => el.removeEventListener(evt, v)); el._cleanups.add(() => el.removeEventListener(ev, v));
} else if (isSig) { } else if (isFunc(v)) {
el._cleanups.add(Watch(() => { el._cleanups.add(Watch(() => {
const val = v(), clean = (k === 'src' || k === 'href') && String(val).toLowerCase().includes('javascript:') ? '#' : val; const val = v(), safe = (k === 'src' || k === 'href') && String(val).includes('javascript:') ? '#' : val;
k === "class" ? (el.className = clean || "") : clean == null || clean === false ? el.removeAttribute(k) : el.setAttribute(k, clean === true ? "" : clean); k === "class" ? (el.className = safe || "") : (safe == null || safe === false ? el.removeAttribute(k) : el.setAttribute(k, safe === true ? "" : safe));
})); }));
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) { if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
const evt = k === "checked" ? "change" : "input", handler = (e) => v(e.target[k]); el.addEventListener(k === "checked" ? "change" : "input", (e) => v(e.target[k]));
el.addEventListener(evt, handler); el._cleanups.add(() => el.removeEventListener(evt, handler));
} }
} else el.setAttribute(k, v); } else el.setAttribute(k, v);
} }
@@ -197,128 +135,113 @@ const Tag = (tag, props = {}, children = []) => {
const append = (c) => { const append = (c) => {
if (isArr(c)) return c.forEach(append); if (isArr(c)) return c.forEach(append);
if (isFunc(c)) { if (isFunc(c)) {
const marker = createText(""); el.appendChild(marker); const m = doc.createTextNode(""); el.appendChild(m); let curr = [];
let curr = [];
el._cleanups.add(Watch(() => { el._cleanups.add(Watch(() => {
const res = c(), next = (isArr(res) ? res : [res]).map(ensureNode); const res = c(), next = (isArr(res) ? res : [res]).map(ensureNode);
curr.forEach(n => { cleanupNode(n); n.remove(); }); curr.forEach(n => { if (n instanceof Node) { cleanupNode(n); n.remove(); } });
next.forEach(n => marker.parentNode?.insertBefore(n, marker)); next.forEach(n => m.parentNode?.insertBefore(n, m)); curr = next;
curr = next;
})); }));
} else el.appendChild(ensureNode(c)); } else el.appendChild(ensureNode(c));
}; };
append(children); return el; append(children); return el;
}; };
// --- 6. CONTROL FLOW --- const Render = (fn) => {
const If = (condition, thenVal, otherwiseVal = null, transition = null) => { const cleanups = new Set(), prev = currentOwner, container = doc.createElement("div");
const marker = createText(""), container = Tag("div", { style: "display:contents" }, [marker]); container.style.display = "contents"; currentOwner = { cleanups };
let currentView = null, lastState = null; const res = fn({ onCleanup: (f) => cleanups.add(f) });
(isArr(res) ? res : [res]).forEach(r => container.appendChild(ensureNode(r)));
currentOwner = prev;
return { _isRuntime: true, container, destroy: () => { runCleanups(cleanups); cleanupNode(container); container.remove(); } };
};
const If = (cond, t, f = null, trans = null) => {
const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]);
let view = null, last = null;
Watch(() => { Watch(() => {
const state = !!(isFunc(condition) ? condition() : condition); const s = !!(isFunc(cond) ? cond() : cond);
if (state === lastState) return; if (s === last) return; last = s;
lastState = state; const dispose = () => { if(view) { view.destroy(); view = null; } };
if (view && !s && trans?.out) trans.out(view.container, dispose); else dispose();
const dispose = () => { currentView?.destroy(); currentView = null; }; const b = s ? t : f;
if (currentView && !state && transition?.out) transition.out(currentView.container, dispose); else dispose(); if (b) {
view = Render(() => isFunc(b) ? b() : b);
const branch = state ? thenVal : otherwiseVal; root.insertBefore(view.container, m);
if (branch) { if (trans?.in) trans.in(view.container);
currentView = Render(() => isFunc(branch) ? branch() : branch);
container.insertBefore(currentView.container, marker);
if (state && transition?.in) transition.in(currentView.container);
} }
}); });
return container; return root;
}; };
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = createText(""), container = Tag(tag, props, [marker]);
let viewCache = new Map();
const For = (src, itemFn, keyFn) => {
const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]);
let cache = new Map();
Watch(() => { Watch(() => {
const items = (isFunc(source) ? source() : source) || [], nextCache = new Map(), order = []; const items = (isFunc(src) ? src() : src) || [], next = new Map(), order = [];
items.forEach((item, i) => { items.forEach((item, i) => {
const key = keyFn ? keyFn(item, i) : i; const k = keyFn ? keyFn(item, i) : i;
let view = viewCache.get(key); let v = cache.get(k) || Render(() => itemFn(item, i));
if (!view) { cache.delete(k); next.set(k, v); order.push(k);
const res = renderFn(item, i);
view = res instanceof Node ? { container: res, destroy: () => { cleanupNode(res); res.remove(); } } : Render(() => res);
}
viewCache.delete(key); nextCache.set(key, view); order.push(key);
}); });
cache.forEach(v => v.destroy());
viewCache.forEach(v => v.destroy()); let anchor = m;
let anchor = marker;
for (let i = order.length - 1; i >= 0; i--) { for (let i = order.length - 1; i >= 0; i--) {
const view = nextCache.get(order[i]); const v = next.get(order[i]);
if (view.container.nextSibling !== anchor) container.insertBefore(view.container, anchor); if (v.container.nextSibling !== anchor) root.insertBefore(v.container, anchor);
anchor = view.container; anchor = v.container;
} }
viewCache = nextCache; cache = next;
}); });
return container; return root;
}; };
// --- 7. ROUTER & MOUNT --- // --- ROUTER SYSTEM ---
const Router = (routes) => { const Router = (routes) => {
const path = $(window.location.hash.replace(/^#/, "") || "/"); const getHash = () => window.location.hash.replace(/^#/, "") || "/";
window.addEventListener("hashchange", () => path(window.location.hash.replace(/^#/, "") || "/")); const path = $(getHash());
const outlet = Tag("div", { class: "router-transition" }); window.addEventListener("hashchange", () => path(getHash()));
const outlet = Tag("div", { class: "router-outlet" });
let currentView = null; let currentView = null;
Watch([path], async () => { Watch([path], async () => {
const current = path(); const cur = path(), route = routes.find(r => {
const route = routes.find(r => { const p1 = r.path.split("/").filter(Boolean), p2 = cur.split("/").filter(Boolean);
const p1 = r.path.split("/").filter(Boolean), p2 = current.split("/").filter(Boolean); return p1.length === p2.length && p1.every((p, i) => p.startsWith(":") || p === p2[i]);
return p1.length === p2.length && p1.every((part, i) => part.startsWith(":") || part === p2[i]);
}) || routes.find(r => r.path === "*"); }) || routes.find(r => r.path === "*");
if (route) { if (route) {
let comp = route.component; let comp = route.component;
if (isFunc(comp) && comp.toString().includes('import')) comp = (await comp()).default || await comp(); if (isFunc(comp) && comp.toString().includes('import')) comp = (await comp()).default;
const params = {}; const params = {};
route.path.split("/").filter(Boolean).forEach((p, i) => { if (p.startsWith(":")) params[p.slice(1)] = current.split("/").filter(Boolean)[i]; }); route.path.split("/").filter(Boolean).forEach((p, i) => { if (p.startsWith(":")) params[p.slice(1)] = cur.split("/").filter(Boolean)[i]; });
currentView?.destroy(); currentView?.destroy();
if (Router.params) Router.params(params); Router.params(params);
currentView = Render(() => isFunc(comp) ? comp(params) : comp);
currentView = Render(() => { try { return isFunc(comp) ? comp(params) : comp; } catch (e) { return Tag("div", {}, "Error"); } });
outlet.appendChild(currentView.container); outlet.appendChild(currentView.container);
} }
}); });
return outlet; return outlet;
}; };
Router.params = $({}); // router utils
Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/"); Router.params = $({});
Router.back = () => window.history.back(); Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
Router.path = () => window.location.hash.replace(/^#/, "") || "/"; Router.back = () => window.history.back();
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
const Mount = (comp, target) => { const Mount = (comp, target) => {
const el = typeof target === "string" ? doc.querySelector(target) : target; const t = typeof target === "string" ? doc.querySelector(target) : target;
if (!el) return; if (!t) return; if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy();
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy(); const inst = Render(isFunc(comp) ? comp : () => comp);
const instance = Render(isFunc(comp) ? comp : () => comp); t.replaceChildren(inst.container); MOUNTED_NODES.set(t, inst); return inst;
el.replaceChildren(instance.container); };
MOUNTED_NODES.set(el, instance);
return instance;
};
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount }; return { $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onUnmount };
})();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
assign(window, SigPro); Object.assign(window, SigPro);
"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" "div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg"
.split(" ").forEach(t => { .split(" ").forEach(t => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c));
const h = t[0].toUpperCase() + t.slice(1);
if (!(h in window)) window[h] = (p, c) => Tag(t, p, c);
});
window.SigPro = Object.freeze(SigPro);
} }
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
export default SigPro; export default SigPro;