Create sigpro-ssr.js
This commit is contained in:
302
sigpro/sigpro-ssr.js
Normal file
302
sigpro/sigpro-ssr.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
/**
|
||||||
|
* SigPro Core v1.1.13 - Full SSR & Hydration Support
|
||||||
|
*/
|
||||||
|
(() => {
|
||||||
|
const isServer = typeof window === "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 flush = () => {
|
||||||
|
isFlushing = true;
|
||||||
|
for (const eff of Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0))) {
|
||||||
|
if (!eff._deleted) eff();
|
||||||
|
}
|
||||||
|
effectQueue.clear();
|
||||||
|
isFlushing = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const track = (subs) => {
|
||||||
|
if (SSR_MODE || !activeEffect || activeEffect._deleted) return;
|
||||||
|
subs.add(activeEffect);
|
||||||
|
activeEffect._deps.add(subs);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (!isFlushing) queueMicrotask(flush);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SIGNALS ($) ---
|
||||||
|
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.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); }
|
||||||
|
} finally { activeEffect = prev; }
|
||||||
|
};
|
||||||
|
effect._deps = new Set();
|
||||||
|
effect._isComputed = true;
|
||||||
|
effect._subs = subs;
|
||||||
|
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; };
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = initial;
|
||||||
|
if (key && !isServer) {
|
||||||
|
const saved = localStorage.getItem(key);
|
||||||
|
if (saved) try { value = JSON.parse(saved); } catch { value = saved; }
|
||||||
|
}
|
||||||
|
const subs = new Set();
|
||||||
|
return (...args) => {
|
||||||
|
if (args.length && !SSR_MODE) {
|
||||||
|
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));
|
||||||
|
trigger(subs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track(subs);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const $watch = (target, fn) => {
|
||||||
|
if (SSR_MODE) return () => {};
|
||||||
|
const isArr = Array.isArray(target);
|
||||||
|
const cb = isArr ? fn : target;
|
||||||
|
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 = 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();
|
||||||
|
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 ---
|
||||||
|
const $html = (tag, props = {}, content = []) => {
|
||||||
|
if (props.nodeType || Array.isArray(props) || typeof props !== "object") { content = props; props = {}; }
|
||||||
|
|
||||||
|
if (SSR_MODE) {
|
||||||
|
let attrs = '';
|
||||||
|
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}"`;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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
|
||||||
|
} 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") {
|
||||||
|
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 (!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);
|
||||||
|
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));
|
||||||
|
nodes = next;
|
||||||
|
}));
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
append(content);
|
||||||
|
|
||||||
|
if (el === HYDRATE_PTR?.parentNode) HYDRATE_PTR = el.nextSibling; // Volvemos al nivel superior
|
||||||
|
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) : '';
|
||||||
|
}
|
||||||
|
const marker = _doc.createTextNode("");
|
||||||
|
const container = $html("div", { style: "display:contents" }, [marker]);
|
||||||
|
let curr = 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); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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));
|
||||||
|
}).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 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); }
|
||||||
|
container.insertBefore(run.container, marker);
|
||||||
|
});
|
||||||
|
cache.forEach((r, k) => { if (!newKeys.has(k)) { r.destroy(); cache.delete(k); }});
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ROUTER ---
|
||||||
|
const $router = (routes) => {
|
||||||
|
const getPath = () => SSR_MODE ? $router._ssrPath : (window.location.hash.replace(/^#/, "") || "/");
|
||||||
|
if (SSR_MODE) {
|
||||||
|
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 sPath = $(getPath());
|
||||||
|
window.addEventListener("hashchange", () => sPath(getPath()));
|
||||||
|
const outlet = $html("div", { class: "router-outlet" });
|
||||||
|
let curr = 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);
|
||||||
|
});
|
||||||
|
return outlet;
|
||||||
|
};
|
||||||
|
$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; }
|
||||||
|
};
|
||||||
|
|
||||||
|
_global.$mount = (comp, target) => {
|
||||||
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
||||||
|
if (!el) return;
|
||||||
|
if (el.firstChild) {
|
||||||
|
HYDRATE_PTR = el.firstChild;
|
||||||
|
const inst = _view(typeof comp === "function" ? comp : () => comp);
|
||||||
|
HYDRATE_PTR = null;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
const inst = _view(typeof comp === "function" ? comp : () => comp);
|
||||||
|
el.replaceChildren(inst.container);
|
||||||
|
return inst;
|
||||||
|
};
|
||||||
|
|
||||||
|
_global.$ = $; _global.$watch = $watch; _global.$html = $html; _global.$if = $if; _global.$for = $for; _global.$router = $router;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user