From 9588bcbce89c730ecf3c9696c459cfe25fd5b279 Mon Sep 17 00:00:00 2001 From: natxocc Date: Fri, 10 Apr 2026 18:37:58 +0200 Subject: [PATCH] up --- sigpro v1.2.1.js | 262 ----------------- sigpro v1.2.2.js | 543 ----------------------------------- sigpro v1.3.js => sigpro2.js | 48 ++-- sigworkPro.js | 263 ++++++++--------- 4 files changed, 154 insertions(+), 962 deletions(-) delete mode 100644 sigpro v1.2.1.js delete mode 100644 sigpro v1.2.2.js rename sigpro v1.3.js => sigpro2.js (91%) diff --git a/sigpro v1.2.1.js b/sigpro v1.2.1.js deleted file mode 100644 index 6ca129f..0000000 --- a/sigpro v1.2.1.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * SigPro v1.2.1 - */ -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 ?? ""))); - - // --- INTERNAL STATE & CLEANUP --- - let activeEffect = null, currentOwner = null, isFlushing = false; - const effectQueue = new Set(), MOUNTED_NODES = new WeakMap(); - - 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) => { - if (node._cleanups) runCleanups(node._cleanups); - node.childNodes?.forEach(cleanupNode); - }; - - // --- 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(e => !e._deleted && e()); - isFlushing = false; - }; - - 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(flush); - } - }; - - // --- CORE API --- - const untrack = (fn) => { - const p = activeEffect; activeEffect = null; - try { return fn(); } finally { activeEffect = p; } - }; - - 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(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; }; - } - if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val; } catch (e) { } - return (...args) => { - if (args.length) { - 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(subs); return val; - }; - }; - - const $$ = (obj, cache = new WeakMap()) => { - if (!isObj(obj)) return obj; - if (cache.has(obj)) return cache.get(obj); - const subs = {}; - 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(obj, proxy); return proxy; - }; - - // Watch for changes - 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 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: () => { runner._deleted = true; clearDeps(runner); runCleanups(runner._cleanups); } - }); - onUnmount(runner.stop); - runner(); return runner.stop; - }; - - // Create element with props and children - 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) : 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; } - if (k.startsWith("on")) { - 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(), 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")) { - el.addEventListener(k === "checked" ? "change" : "input", (e) => v(e.target[k])); - } - } else el.setAttribute(k, v); - } - - const append = (c) => { - if (isArr(c)) return c.forEach(append); - if (isFunc(c)) { - 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 => { 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; - }; - - // Render a function to a 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(); } }; - }; - - // Conditional rendering - 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 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; - }; - - // For loop - 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 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); - }); - cache.forEach(v => v.destroy()); - let anchor = m; - for (let i = order.length - 1; i >= 0; i--) { - const v = next.get(order[i]); - if (v.container.nextSibling !== anchor) root.insertBefore(v.container, anchor); - anchor = v.container; - } - cache = next; - }); - return root; - }; - - // Router SPA hash - const Router = (routes) => { - const getHash = () => window.location.hash.slice(1) || "/"; - const path = $(getHash()); - window.addEventListener("hashchange", () => path(getHash())); - - const outlet = Tag("div", { class: "router-outlet" }); - let currentView = null; - - Watch([path], () => { - const cur = path(); - const route = routes.find(r => { - const p1 = r.path.split("/").filter(Boolean); - const p2 = cur.split("/").filter(Boolean); - return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]); - }) || routes.find(r => r.path === "*"); - - if (route) { - currentView?.destroy(); - const params = {}; - route.path.split("/").filter(Boolean).forEach((p, i) => { - if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]; - }); - - Router.params(params); - currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component); - outlet.replaceChildren(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 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; - }; - - return { $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onUnmount }; -})(); - -// AutoRegister DX in window, remove if don't want a dirty window -if (typeof window !== "undefined") { - 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 default SigPro; \ No newline at end of file diff --git a/sigpro v1.2.2.js b/sigpro v1.2.2.js deleted file mode 100644 index 2a095c5..0000000 --- a/sigpro v1.2.2.js +++ /dev/null @@ -1,543 +0,0 @@ -/** - * SigPro v1.2.2 - ESM version with improved XSS blocking - */ - -// DOM reference -const documentReference = typeof document !== "undefined" ? document : null - -// Type checking helpers -const isArray = Array.isArray -const objectAssign = Object.assign -const isFunction = (value) => typeof value === "function" -const isObject = (value) => value && typeof value === "object" - -// Convert any value to a DOM node -const ensureNode = (nodeOrValue) => { - if (nodeOrValue && nodeOrValue._isRuntime) { - return nodeOrValue.container - } - if (nodeOrValue instanceof Node) { - return nodeOrValue - } - return documentReference.createTextNode(nodeOrValue ?? "") -} - -// --- REACTIVITY CORE --- -let currentActiveEffect = null -let currentOwnerEffect = null -let isFlushingEffects = false -const effectQueue = new Set() -const mountedNodesRegistry = new WeakMap() - -// Dispose an effect and its entire tree -const disposeEffect = (effect) => { - if (!effect || effect._disposed) { - return - } - effect._disposed = true - const stack = [effect] - while (stack.length) { - const currentEffect = stack.pop() - if (currentEffect._cleanups) { - for (const cleanupFunction of currentEffect._cleanups) { - cleanupFunction() - } - currentEffect._cleanups.clear() - } - if (currentEffect._children) { - for (const childEffect of currentEffect._children) { - stack.push(childEffect) - } - currentEffect._children.clear() - } - if (currentEffect._deps) { - for (const dependencySet of currentEffect._deps) { - dependencySet.delete(currentEffect) - } - currentEffect._deps.clear() - } - } -} - -// Create a reactive effect -const createReactiveEffect = (effectFunction, isComputed = false) => { - const reactiveEffect = () => { - if (reactiveEffect._disposed) { - return - } - if (reactiveEffect._deps) { - for (const dependencySet of reactiveEffect._deps) { - dependencySet.delete(reactiveEffect) - } - reactiveEffect._deps.clear() - } - if (reactiveEffect._cleanups) { - for (const cleanupFunction of reactiveEffect._cleanups) { - cleanupFunction() - } - reactiveEffect._cleanups.clear() - } - const previousActiveEffect = currentActiveEffect - const previousOwnerEffect = currentOwnerEffect - currentActiveEffect = reactiveEffect - currentOwnerEffect = reactiveEffect - try { - return effectFunction() - } finally { - currentActiveEffect = previousActiveEffect - currentOwnerEffect = previousOwnerEffect - } - } - objectAssign(reactiveEffect, { - _deps: null, - _cleanups: null, - _children: null, - _disposed: false, - _isComputed: isComputed, - _depth: currentActiveEffect ? currentActiveEffect._depth + 1 : 0, - _provisions: currentOwnerEffect ? currentOwnerEffect._provisions : {} - }) - if (currentOwnerEffect) { - if (!currentOwnerEffect._children) { - currentOwnerEffect._children = new Set() - } - currentOwnerEffect._children.add(reactiveEffect) - } - return reactiveEffect -} - -// --- LIFECYCLE AND CONTEXT --- -const onUnmount = (cleanupFunction) => { - if (currentOwnerEffect) { - if (!currentOwnerEffect._cleanups) { - currentOwnerEffect._cleanups = new Set() - } - currentOwnerEffect._cleanups.add(cleanupFunction) - } -} - -const onMount = (mountFunction) => { - if (currentOwnerEffect) { - if (!currentOwnerEffect._mounts) { - currentOwnerEffect._mounts = [] - } - currentOwnerEffect._mounts.push(mountFunction) - } -} - -const provide = (key, value) => { - if (currentOwnerEffect) { - currentOwnerEffect._provisions = { - ...currentOwnerEffect._provisions, - [key]: value - } - } -} - -const inject = (key, fallbackValue) => { - if (currentOwnerEffect && currentOwnerEffect._provisions[key]) { - return currentOwnerEffect._provisions[key] - } - return fallbackValue -} - -// --- SCHEDULER --- -const flushEffectQueue = () => { - if (isFlushingEffects) { - return - } - isFlushingEffects = true - const sortedEffects = Array.from(effectQueue).sort((effectA, effectB) => effectA._depth - effectB._depth) - effectQueue.clear() - for (let index = 0; index < sortedEffects.length; index++) { - const effect = sortedEffects[index] - if (!effect._disposed) { - effect() - } - } - isFlushingEffects = false -} - -const trackAndTrigger = (subscriberSet, shouldTrigger = false) => { - if (!shouldTrigger && currentActiveEffect && !currentActiveEffect._disposed) { - subscriberSet.add(currentActiveEffect) - if (!currentActiveEffect._deps) { - currentActiveEffect._deps = new Set() - } - currentActiveEffect._deps.add(subscriberSet) - } else if (shouldTrigger) { - let hasEffectsToQueue = false - for (const effect of subscriberSet) { - if (effect === currentActiveEffect || effect._disposed) { - continue - } - if (effect._isComputed) { - effect._dirty = true - if (effect._subs) { - trackAndTrigger(effect._subs, true) - } - } else { - effectQueue.add(effect) - hasEffectsToQueue = true - } - } - if (hasEffectsToQueue && !isFlushingEffects) { - queueMicrotask(flushEffectQueue) - } - } -} - -// --- SIGNALS AND COMPUTED --- -const $ = (initialValue, storageKey = null) => { - const subscribers = new Set() - // Computed case - if (isFunction(initialValue)) { - let cachedValue = undefined - let isDirty = true - const computedEffect = () => { - if (isDirty) { - const previousActiveEffect = currentActiveEffect - currentActiveEffect = computedEffect - try { - const newValue = initialValue() - if (!Object.is(cachedValue, newValue)) { - cachedValue = newValue - isDirty = false - trackAndTrigger(subscribers, true) - } - } finally { - currentActiveEffect = previousActiveEffect - } - } - trackAndTrigger(subscribers) - return cachedValue - } - objectAssign(computedEffect, { - _isComputed: true, - _subs: subscribers, - _dirty: true, - _deps: null, - _disposed: false, - stop: () => { - computedEffect._disposed = true - if (computedEffect._deps) { - for (const dependencySet of computedEffect._deps) { - dependencySet.delete(computedEffect) - } - } - subscribers.clear() - } - }) - onUnmount(computedEffect.stop) - return computedEffect - } - // Persistent $case - let currentValue = initialValue - if (storageKey) { - try { - const stored = localStorage.getItem(storageKey) - if (stored !== null) { - currentValue = JSON.parse(stored) - } - } catch (error) { - // ignore parse errors - } - } - // Signal function - const signalFunction = (...argumentsList) => { - if (argumentsList.length) { - const nextValue = isFunction(argumentsList[0]) ? argumentsList[0](currentValue) : argumentsList[0] - if (!Object.is(currentValue, nextValue)) { - currentValue = nextValue - if (storageKey) { - localStorage.setItem(storageKey, JSON.stringify(currentValue)) - } - trackAndTrigger(subscribers, true) - } - } - trackAndTrigger(subscribers) - return currentValue - } - return signalFunction -} - -// --- DOM UTILITIES WITH XSS PROTECTION --- -const setAttributeWithSecurity = (element, attributeName, attributeValue, isSvgElement) => { - let safeValue = attributeValue - if (attributeName === "src" || attributeName === "href") { - const stringValue = String(attributeValue).toLowerCase() - if (stringValue.startsWith("javascript:") || stringValue.startsWith("data:text/html")) { - safeValue = "#" - } - } - if (attributeName === "class" || attributeName === "className") { - element.className = safeValue || "" - } else if (safeValue == null || safeValue === false) { - element.removeAttribute(attributeName) - } else if (attributeName in element && !isSvgElement) { - element[attributeName] = safeValue - } else { - element.setAttribute(attributeName, safeValue === true ? "" : safeValue) - } -} - -// --- VIRTUAL DOM / TAG CREATION --- -const Tag = (tagName, properties = {}, childrenList = []) => { - // Overload: if properties is not an object, treat as children - if (properties instanceof Node || isArray(properties) || !isObject(properties)) { - childrenList = properties - properties = {} - } - const isSvgElement = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tagName) - const element = isSvgElement - ? documentReference.createElementNS("http://www.w3.org/2000/svg", tagName) - : documentReference.createElement(tagName) - - for (const [propertyName, propertyValue] of Object.entries(properties)) { - if (propertyName === "ref") { - if (isFunction(propertyValue)) { - propertyValue(element) - } else if (propertyValue && "current" in propertyValue) { - propertyValue.current = element - } - continue - } - if (propertyName.startsWith("on")) { - const eventName = propertyName.slice(2).toLowerCase() - element.addEventListener(eventName, propertyValue) - onUnmount(() => element.removeEventListener(eventName, propertyValue)) - continue - } - if (isFunction(propertyValue)) { - const effect = createReactiveEffect(() => setAttributeWithSecurity(element, propertyName, propertyValue(), isSvgElement)) - effect() - if (/^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName) && (propertyName === "value" || propertyName === "checked")) { - const inputEventName = propertyName === "checked" ? "change" : "input" - element.addEventListener(inputEventName, (event) => propertyValue(event.target[propertyName])) - } - } else { - setAttributeWithSecurity(element, propertyName, propertyValue, isSvgElement) - } - } - - const appendChildNode = (childNodeOrFunction) => { - if (isArray(childNodeOrFunction)) { - for (const nestedChild of childNodeOrFunction) { - appendChildNode(nestedChild) - } - return - } - if (isFunction(childNodeOrFunction)) { - const anchorNode = documentReference.createTextNode("") - element.appendChild(anchorNode) - let currentNodes = [] - const effect = createReactiveEffect(() => { - const result = childNodeOrFunction() - const nextNodes = (isArray(result) ? result : [result]).map(ensureNode) - for (let index = 0; index < currentNodes.length; index++) { - const existingNode = currentNodes[index] - if (existingNode._isRuntime) { - existingNode.destroy() - } else { - existingNode.remove() - } - } - let referenceNode = anchorNode - for (let index = nextNodes.length - 1; index >= 0; index--) { - const newNode = nextNodes[index] - element.insertBefore(newNode, referenceNode) - referenceNode = newNode - } - currentNodes = nextNodes - }) - effect() - return - } - element.appendChild(ensureNode(childNodeOrFunction)) - } - appendChildNode(childrenList) - return element -} - -// --- REACTIVE RENDER CONTAINER --- -const Render = (renderFunction) => { - const container = documentReference.createElement("div") - container.style.display = "contents" - const effect = createReactiveEffect(() => { - const result = renderFunction() - const nodes = (isArray(result) ? result : [result]).map(ensureNode) - for (const node of nodes) { - container.appendChild(node) - } - }) - effect() - if (effect._mounts) { - for (const mountFunction of effect._mounts) { - mountFunction() - } - } - return { - _isRuntime: true, - container: container, - destroy: () => { - disposeEffect(effect) - container.remove() - } - } -} - -// --- CONTROL FLOW COMPONENTS --- -const For = (source, itemRenderer, keyExtractor) => { - const anchorNode = documentReference.createTextNode("") - const rootContainer = Tag("div", { style: "display:contents" }, [anchorNode]) - let itemCache = new Map() - createReactiveEffect(() => { - const items = (isFunction(source) ? source() : source) || [] - const nextCache = new Map() - const order = [] - for (let index = 0; index < items.length; index++) { - const item = items[index] - const cacheKey = keyExtractor ? keyExtractor(item, index) : index - let cachedItem = itemCache.get(cacheKey) - if (!cachedItem) { - cachedItem = Render(() => itemRenderer(item, index)) - } - nextCache.set(cacheKey, cachedItem) - order.push(cacheKey) - itemCache.delete(cacheKey) - } - for (const oldItem of itemCache.values()) { - oldItem.destroy() - } - itemCache = nextCache - let referenceNode = anchorNode - for (let index = order.length - 1; index >= 0; index--) { - const key = order[index] - const view = nextCache.get(key) - if (view.container.nextSibling !== referenceNode) { - rootContainer.insertBefore(view.container, referenceNode) - } - referenceNode = view.container - } - }) - return rootContainer -} - -const If = (condition, trueBranch, falseBranch = null) => { - const anchorNode = documentReference.createTextNode("") - const rootContainer = Tag("div", { style: "display:contents" }, [anchorNode]) - let currentView = null - let lastCondition = null - createReactiveEffect(() => { - const conditionResult = !!(isFunction(condition) ? condition() : condition) - if (conditionResult === lastCondition) { - return - } - lastCondition = conditionResult - if (currentView) { - currentView.destroy() - currentView = null - } - const content = conditionResult ? trueBranch : falseBranch - if (content) { - currentView = Render(() => isFunction(content) ? content() : content) - rootContainer.insertBefore(currentView.container, anchorNode) - } - }) - return rootContainer -} - -// --- ROUTER --- -const getCurrentHashPath = () => { - return window.location.hash.slice(1) || "/" -} -const currentRoutePath = $(getCurrentHashPath()) -window.addEventListener("hashchange", () => currentRoutePath(getCurrentHashPath())) - -const Router = (routesDefinition) => { - const outletElement = Tag("div", { class: "router-outlet" }) - let currentView = null - createReactiveEffect(() => { - const path = currentRoutePath() - const pathSegments = path.split("/").filter(Boolean) - const matchedRoute = routesDefinition.find(route => { - const routeSegments = route.path.split("/").filter(Boolean) - return routeSegments.length === pathSegments.length && routeSegments.every((segment, index) => segment[0] === ":" || segment === pathSegments[index]) - }) || routesDefinition.find(route => route.path === "*") - if (matchedRoute) { - if (currentView) { - currentView.destroy() - } - const routeParams = {} - matchedRoute.path.split("/").filter(Boolean).forEach((segment, index) => { - if (segment[0] === ":") { - routeParams[segment.slice(1)] = pathSegments[index] - } - }) - Router.params(routeParams) - const componentToRender = isFunction(matchedRoute.component) ? matchedRoute.component(routeParams) : matchedRoute.component - currentView = Render(() => componentToRender) - outletElement.replaceChildren(currentView.container) - } - }) - return outletElement -} - -Router.params = $({}) -Router.to = (targetPath) => window.location.hash = targetPath.replace(/^#?\/?/, "#/") -Router.back = () => window.history.back() - -// --- MOUNT APPLICATION --- -const Mount = (component, targetSelector) => { - const targetElement = typeof targetSelector === "string" ? documentReference.querySelector(targetSelector) : targetSelector - if (!targetElement) { - return - } - const existingInstance = mountedNodesRegistry.get(targetElement) - if (existingInstance) { - existingInstance.destroy() - } - const instance = Render(isFunction(component) ? component : () => component) - targetElement.replaceChildren(instance.container) - mountedNodesRegistry.set(targetElement, instance) - return instance -} - -// --- WATCH UTILITY --- -const Watch = (sources, callback) => { - const isArraySource = isArray(sources) - const effect = createReactiveEffect(() => { - if (isArraySource) { - const previousActive = currentActiveEffect - currentActiveEffect = null - callback() - for (const dependency of sources) { - if (isFunction(dependency)) { - dependency() - } - } - currentActiveEffect = previousActive - } else { - callback() - } - }) - effect() - return () => disposeEffect(effect) -} - -// --- UNTRACK --- -const untrack = (untrackedFunction) => { - const previousActiveEffect = currentActiveEffect - currentActiveEffect = null - try { - return untrackedFunction() - } finally { - currentActiveEffect = previousActiveEffect - } -} - -// --- EXPORTS (ESM) --- -export { $, Watch, Tag, Render, If, For, Mount, onMount, onUnmount, provide, inject, Router, untrack } - -// Default export with all public members -export default { $, Watch, Tag, Render, If, For, Mount, onMount, onUnmount, provide, inject, Router, untrack } \ No newline at end of file diff --git a/sigpro v1.3.js b/sigpro2.js similarity index 91% rename from sigpro v1.3.js rename to sigpro2.js index dbc7ec1..22e5b29 100644 --- a/sigpro v1.3.js +++ b/sigpro2.js @@ -1,4 +1,4 @@ -/** SigPro 1.3 (Signals & Proxies) */ +/** SigPro - Signals & Proxies */ // Helpers const doc = typeof document !== "undefined" ? document : null; @@ -34,7 +34,7 @@ const createEffect = (fn, isComputed = false) => { activeEffect = activeOwner = effect; try { const res = isComputed ? fn() : (fn(), undefined); - if (!isComputed) effect._result = res; // ← ESTO ES LO NUEVO + if (!isComputed) effect._result = res; return res; } finally { activeEffect = prevEffect; @@ -141,7 +141,7 @@ export const $$ = (obj, cache = new WeakMap()) => { return proxy; }; -// Watcher +// Watchers export const Watch = (sources, cb) => { const isArr = Array.isArray(sources); const effect = createEffect(() => { @@ -150,7 +150,7 @@ export const Watch = (sources, cb) => { }); effect(); return () => dispose(effect); -} +}; export const watch = (source, callback) => { let oldValue, first = true; @@ -184,15 +184,18 @@ export const inject = (key, defaultValue) => { return defaultValue; }; +// --- Seguridad optimizada --- +const DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i; +const isDangerousAttr = key => key === 'src' || key === 'href' || key.startsWith('on'); + const validateAttr = (key, val) => { if (val == null || val === false) return null; - const sVal = String(val); - - // Bloqueo de protocolos peligrosos en atributos de carga o navegación - if ((key === 'src' || key === 'href' || key.startsWith('on')) && - /^\s*(javascript|data|vbscript):/i.test(sVal)) { - console.warn(`[Seguridad] Bloqueado protocolo peligroso en ${key}`); - return '#'; + if (isDangerousAttr(key)) { + const sVal = String(val); + if (DANGEROUS_PROTOCOL.test(sVal)) { + console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`); + return '#'; + } } return val; }; @@ -218,17 +221,19 @@ export const Tag = (tag, props = {}, children = []) => { ctx._mounts = effect._mounts || []; ctx._cleanups = effect._cleanups || new Set(); const result = effect._result; - const attachLifecycle = (node) => node && typeof node === 'object' && !node._isRuntime && (node._mounts = ctx._mounts, node._cleanups = ctx._cleanups, node._ownerEffect = effect); - Array.isArray(result) ? result.forEach(attachLifecycle) : attachLifecycle(result); + const attachLifecycle = node => node && typeof node === 'object' && !node._isRuntime && (node._mounts = ctx._mounts, node._cleanups = ctx._cleanups, node._ownerEffect = effect); + isArr(result) ? result.forEach(attachLifecycle) : attachLifecycle(result); if (result == null) return null; - if (isNode(result) || (Array.isArray(result) && result.every(isNode))) return result; + if (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node))) return result; return doc.createTextNode(String(result)); } 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) : doc.createElement(tag); el._cleanups = new Set(); - const validate = (k, v) => ((k === 'src' || k === 'href') && /^\s*(javascript|data|vbscript):/i.test(String(v))) ? '#' : v; - for (let [k, v] of Object.entries(props)) { + + for (let k in props) { + if (!props.hasOwnProperty(k)) continue; + let v = props[k]; if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; } if (k.startsWith("on")) { const ev = k.slice(2).toLowerCase(); @@ -238,9 +243,9 @@ export const Tag = (tag, props = {}, children = []) => { onUnmount(off); } else if (isFunc(v)) { const effect = createEffect(() => { - const val = validate(k, v()); + const val = validateAttr(k, v()); if (k === "class") el.className = val || ""; - else if (val == null || val === false) el.removeAttribute(k); + else if (val == null) el.removeAttribute(k); else if (k in el && !isSVG) el[k] = val; else el.setAttribute(k, val === true ? "" : val); }); @@ -252,13 +257,14 @@ export const Tag = (tag, props = {}, children = []) => { el.addEventListener(evType, ev => v(ev.target[k])); } } else { - const val = validate(k, v); - if (val != null && val !== false) { + const val = validateAttr(k, v); + if (val != null) { if (k in el && !isSVG) el[k] = val; else el.setAttribute(k, val === true ? "" : val); } } } + const append = c => { if (isArr(c)) return c.forEach(append); if (isFunc(c)) { @@ -398,7 +404,7 @@ export const Mount = (comp, target) => { return inst; }; -const SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onMount, onUnmount, provide, inject }); +const SigPro = Object.freeze({ $, $$, Watch, watch, Tag, Render, If, For, Router, Mount, untrack, onMount, onUnmount, provide, inject }); export const initDX = () => { if (typeof window === "undefined") return; diff --git a/sigworkPro.js b/sigworkPro.js index 4875d17..e241020 100644 --- a/sigworkPro.js +++ b/sigworkPro.js @@ -1,3 +1,7 @@ +/** * Sigwork 2.0 - Memoria Optimizada + * Soluciona fugas en Atributos, For/If, Router y Transiciones. + */ + const isFunction = (v) => typeof v === 'function'; const isNode = (v) => v instanceof Node; const doc = typeof document !== "undefined" ? document : null; @@ -6,6 +10,14 @@ let activeEffect = null; const pendingEffects = new Set(); let flushScheduled = false; +// Registro global para limpiezas vinculadas a nodos DOM nativos +const nodeDisposers = new WeakMap(); + +const registerNodeCleanup = (node, disposer) => { + if (!nodeDisposers.has(node)) nodeDisposers.set(node, []); + nodeDisposers.get(node).push(disposer); +}; + const flushEffects = () => { if (pendingEffects.size === 0) return; const all = Array.from(pendingEffects); @@ -80,8 +92,40 @@ const createEffect = (fn) => { }; export const Watch = createEffect; -export const effect = Watch; -export const scope = Watch; + +// --- RECTIFICACIÓN: removeNode DEEP & RECURSIVE --- +export const removeNode = (node) => { + if (!node) return; + + // 1. Limpieza recursiva de hijos (Fuga #6 y #8) + if (node.childNodes) { + node.childNodes.forEach(child => removeNode(child)); + } + + // 2. Limpiar efectos vinculados al nodo nativo (Fuga #2 y #3) + const disposers = nodeDisposers.get(node); + if (disposers) { + disposers.forEach(d => d()); + nodeDisposers.delete(node); + } + + // 3. Cancelar animaciones pendientes (Fuga #7) + if (node._raf) cancelAnimationFrame(node._raf); + + // 4. Limpiar contexto de componentes + if (node.componentStop) node.componentStop(); + if (node.componentContext) { + node.componentContext.unmount.forEach(fn => fn()); + node.componentContext.unmount = []; + } + + // 5. Salida con transición o eliminación directa + if (node.leaveTransition) { + node.leaveTransition(() => node.remove()); + } else { + node.remove(); + } +}; const track = (subs) => { if (activeEffect && !activeEffect.disposed) { @@ -112,8 +156,8 @@ export const $ = (initialValue) => { }, }; }; -export const signal = $; +// Utilidades reactivas (Fuga #1: Watch ahora se auto-asocia al componente actual) export const persistent = (initialValue, storageKey) => { let stored = initialValue; try { @@ -122,8 +166,7 @@ export const persistent = (initialValue, storageKey) => { } catch (e) {} const sig = $(stored); Watch(() => { - const val = sig.value; - try { localStorage.setItem(storageKey, JSON.stringify(val)); } catch (e) {} + localStorage.setItem(storageKey, JSON.stringify(sig.value)); }); return sig; }; @@ -134,12 +177,6 @@ export const computed = (fn) => { return { get value() { return s.value; } }; }; -export const untrack = (fn) => { - const prev = activeEffect; - activeEffect = null; - try { return fn(); } finally { activeEffect = prev; } -}; - export const watch = (source, callback) => { let first = true, oldVal; return Watch(() => { @@ -150,31 +187,18 @@ export const watch = (source, callback) => { }); }; +export const untrack = (fn) => { + const prev = activeEffect; + activeEffect = null; + try { return fn(); } finally { activeEffect = prev; } +}; + let currentComponentContext = null; -export const onMount = (fn) => { - if (currentComponentContext) currentComponentContext.mount.push(fn); -}; -export const onUnmount = (fn) => { - if (currentComponentContext) currentComponentContext.unmount.push(fn); -}; -export const provide = (key, val) => { - if (currentComponentContext) currentComponentContext.provisions[key] = val; -}; -export const inject = (key, def) => { - if (currentComponentContext && key in currentComponentContext.provisions) - return currentComponentContext.provisions[key]; - return def; -}; +export const onMount = (fn) => currentComponentContext?.mount.push(fn); +export const onUnmount = (fn) => currentComponentContext?.unmount.push(fn); const setProperty = (el, key, val, isSVG) => { - if ((key === 'src' || key === 'href') && typeof val === 'string') { - const lower = val.toLowerCase(); - if (lower.startsWith('javascript:') || lower.startsWith('data:text/html')) { - console.warn(`Bloqueado ${key}`); - val = '#'; - } - } if (key === 'class' || key === 'className') { el.className = val || ''; } else if (key === 'style' && typeof val === 'object') { @@ -194,27 +218,31 @@ const appendChildNode = (parent, child) => { const anchor = doc.createTextNode(''); parent.appendChild(anchor); let currentNodes = []; - Watch(() => { + + const stop = Watch(() => { const raw = child(); const next = (Array.isArray(raw) ? raw : [raw]) .flat(Infinity) .filter(v => v != null) .map(v => isNode(v) ? v : doc.createTextNode(String(v))); - for (let i = 0; i < currentNodes.length; i++) { - const n = currentNodes[i]; + + // RECTIFICACIÓN Fuga #6: Eliminación explícita mediante removeNode + for (const n of currentNodes) { if (!next.includes(n)) removeNode(n); } + let ref = anchor; for (let i = next.length - 1; i >= 0; i--) { const n = next[i]; if (n.parentNode !== parent) { parent.insertBefore(n, ref); - if (n.componentContext) n.componentContext.mount.forEach(fn => fn()); + if (n.componentContext) n.componentContext.mount.forEach(f => f()); } ref = n; } currentNodes = next; }); + registerNodeCleanup(anchor, stop); // Fuga #3 corregida } else if (isNode(child)) { parent.appendChild(child); } else { @@ -222,74 +250,63 @@ const appendChildNode = (parent, child) => { } }; -const removeNode = (node) => { - if (node.componentStop) node.componentStop(); - if (node.componentContext) node.componentContext.unmount.forEach(fn => fn()); - if (node.leaveTransition) { - node.leaveTransition(() => node.remove()); - } else { - node.remove(); - } -}; - -export const Tag = (tag, props, ...children) => { - props = props || {}; +export const Tag = (tag, props = {}, ...children) => { children = children.flat(Infinity); + if (isFunction(tag)) { const prevCtx = currentComponentContext; - const ctx = { - mount: [], - unmount: [], - provisions: { ...(prevCtx?.provisions || {}) }, - }; + const ctx = { mount: [], unmount: [], provisions: { ...(prevCtx?.provisions || {}) } }; currentComponentContext = ctx; + let rendered; - const stop = scope(() => { - rendered = tag(props, { - children, - emit: (ev, ...args) => { - const handler = props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]; - if (isFunction(handler)) handler(...args); - }, - }); + const stop = Watch(() => { + rendered = tag(props, { children, emit: (ev, ...args) => { + const h = props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]; + if (isFunction(h)) h(...args); + }}); }); + currentComponentContext = prevCtx; - if (isNode(rendered) || isFunction(rendered)) { + if (isNode(rendered)) { rendered.componentContext = ctx; rendered.componentStop = stop; } return rendered; } - if (!tag) return () => children; + const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|use)$/.test(tag); - const el = isSVG - ? doc.createElementNS('http://www.w3.org/2000/svg', tag) - : doc.createElement(tag); + const el = isSVG ? doc.createElementNS('http://www.w3.org/2000/svg', tag) : doc.createElement(tag); + for (const [k, v] of Object.entries(props)) { - if (k === 'ref') { - if (isFunction(v)) v(el); - else if (v && 'value' in v) v.value = el; - continue; - } if (k.startsWith('on')) { const ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v); onUnmount(() => el.removeEventListener(ev, v)); - continue; - } - if (isFunction(v)) { - Watch(() => setProperty(el, k, v(), isSVG)); + } else if (isFunction(v)) { + // Fuga #2 corregida: El efecto del atributo se registra en el nodo + const stopAttr = Watch(() => setProperty(el, k, v(), isSVG)); + registerNodeCleanup(el, stopAttr); } else { setProperty(el, k, v, isSVG); } } + for (const child of children) appendChildNode(el, child); return el; }; -export const h = Tag; export const If = ({ when, children }) => { - return () => (isFunction(when) ? when() : when) ? children[0] : children[1] || null; + let lastResult = null; + let node = null; + return () => { + const condition = !!(isFunction(when) ? when() : when); + if (condition === lastResult) return node; + + if (node) removeNode(node); // Limpieza de la rama anterior + lastResult = condition; + node = condition ? children[0] : (children[1] || null); + return node; + }; }; export const For = ({ each, key, children }) => { @@ -298,17 +315,21 @@ export const For = ({ each, key, children }) => { const items = isFunction(each) ? each() : each || []; const newCache = new Map(); const nodes = []; + for (let i = 0; i < items.length; i++) { const item = items[i]; const itemKey = key ? (isFunction(key) ? key(item, i) : item[key]) : i; let node = cache.get(itemKey); if (!node) { - const childFn = children[0]; - node = Tag(childFn, { item, index: i }); + node = Tag(children[0], { item, index: i }); } newCache.set(itemKey, node); nodes.push(node); + cache.delete(itemKey); } + + // Fuga #6 corregida: Los nodos que sobran se destruyen formalmente + for (const node of cache.values()) removeNode(node); cache = newCache; return nodes; }; @@ -317,11 +338,12 @@ export const For = ({ each, key, children }) => { export const Transition = ({ enter, leave, children }) => { const decorate = (el) => { if (!isNode(el)) return el; + if (enter) { const [from, active, to] = enter; - requestAnimationFrame(() => { + el._raf = requestAnimationFrame(() => { el.classList.add(active); - requestAnimationFrame(() => { + el._raf = requestAnimationFrame(() => { el.classList.add(from); el.classList.remove(active); el.classList.add(to); @@ -333,17 +355,17 @@ export const Transition = ({ enter, leave, children }) => { }); }); } + if (leave) { const [from, active, to] = leave; el.leaveTransition = (done) => { el.classList.add(active); - requestAnimationFrame(() => { + el._raf = requestAnimationFrame(() => { el.classList.add(from); el.classList.remove(active); el.classList.add(to); const onEnd = () => { el.classList.remove(to, from); - el.removeEventListener('transitionend', onEnd); done(); }; el.addEventListener('transitionend', onEnd, { once: true }); @@ -353,86 +375,55 @@ export const Transition = ({ enter, leave, children }) => { return el; }; const child = children[0]; - if (!child) return null; return isFunction(child) ? () => decorate(child()) : decorate(child); }; -const currentPath = $((window.location.hash.slice(1) || '/')); -window.addEventListener('hashchange', () => { - currentPath.value = window.location.hash.slice(1) || '/'; -}); - export const Router = ({ routes }) => { const outlet = Tag('div', { class: 'router-outlet' }); let currentView = null; + Watch(() => { const path = currentPath.value; - const segments = path.split('/').filter(Boolean); - const matched = routes.find(route => { - const rSeg = route.path.split('/').filter(Boolean); - return rSeg.length === segments.length && rSeg.every((s, i) => s[0] === ':' || s === segments[i]); + const matched = routes.find(r => { + const rSeg = r.path.split('/').filter(Boolean); + const pSeg = path.split('/').filter(Boolean); + return rSeg.length === pSeg.length && rSeg.every((s, i) => s[0] === ':' || s === pSeg[i]); }) || routes.find(r => r.path === '*'); + if (matched) { - if (currentView && currentView.componentStop) currentView.componentStop(); + // Fuga #8 corregida: Limpieza profunda de la vista anterior + while (outlet.firstChild) removeNode(outlet.firstChild); + const params = {}; - const rSeg = matched.path.split('/').filter(Boolean); - rSeg.forEach((s, i) => { if (s[0] === ':') params[s.slice(1)] = segments[i]; }); + matched.path.split('/').filter(Boolean).forEach((s, i) => { + if (s[0] === ':') params[s.slice(1)] = path.split('/').filter(Boolean)[i]; + }); + currentView = Tag(matched.component, { params }); - outlet.innerHTML = ''; outlet.appendChild(currentView); } }); return outlet; }; -export const navigate = (to) => { window.location.hash = to.replace(/^#?\/?/, '#/'); }; -export const back = () => window.history.back(); -export const getCurrentPath = () => currentPath.value; - -export const $$ = (obj, cache = new WeakMap()) => { - if (!obj || typeof obj !== 'object') return obj; - if (cache.has(obj)) return cache.get(obj); - const subs = {}; - const proxy = new Proxy(obj, { - get: (t, k) => { - track(subs[k] ??= new Set()); - const val = t[k]; - return (val && typeof val === 'object') ? $$(val, cache) : val; - }, - set: (t, k, v) => { - if (Object.is(t[k], v)) return true; - t[k] = v; - if (subs[k]) trigger(subs[k]); - return true; - }, - }); - cache.set(obj, proxy); - return proxy; -}; -export const reactive = $$; +// --- RESTO DE EXPORTS --- +export const currentPath = $((window.location.hash.slice(1) || '/')); +window.addEventListener('hashchange', () => currentPath.value = window.location.hash.slice(1) || '/'); export const createApp = (Root, rootProps = {}) => (selector) => { const target = typeof selector === 'string' ? doc.querySelector(selector) : selector; - if (!target) throw new Error(`No se encontró ${selector}`); if (target.appUnmount) target.appUnmount(); const app = Tag(Root, rootProps); target.appendChild(app); - if (app.componentContext) app.componentContext.mount.forEach(fn => fn()); + if (app.componentContext) app.componentContext.mount.forEach(f => f()); target.appUnmount = () => removeNode(app); return target.appUnmount; }; -const tags = 'div span p a button input form label ul li ol header footer main section article nav aside h1 h2 h3 h4 h5 h6 img svg path circle rect line polyline polygon g defs text use br hr pre code strong em table tr td th thead tbody tfoot select option textarea iframe video audio canvas'.split(' '); -tags.forEach(tag => { - const name = tag[0].toUpperCase() + tag.slice(1); - globalThis[name] = (props, ...children) => Tag(tag, props, ...children); +// Global Tags DX +'div span p a button input form label ul li ol header footer main section article nav aside h1 h2 h3 h4 h5 h6 img svg path circle rect line polyline polygon g defs text use br hr pre code strong em table tr td th thead tbody tfoot select option textarea iframe video audio canvas' +.split(' ').forEach(tag => { + globalThis[tag[0].toUpperCase() + tag.slice(1)] = (props, ...children) => Tag(tag, props, ...children); }); -export const createElement = Tag; -export const Fragment = (props) => props.children; - -export default { - $, signal, $$, reactive, persistent, computed, Watch, effect, scope, watch, untrack, - onMount, onUnmount, provide, inject, Tag, h, createElement, Fragment, If, For, Transition, - Router, navigate, back, getCurrentPath, createApp, -}; \ No newline at end of file +export default { $, Watch, Tag, If, For, Transition, Router, createApp, removeNode }; \ No newline at end of file