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