Modular router && remove $$
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
This commit is contained in:
@@ -3,7 +3,7 @@ Blazing fast, zero-overhead, vanilla JS renderer with atomic reactivity.
|
||||
# `SigPro`
|
||||
|
||||
[](https://www.npmjs.com/package/sigpro)
|
||||
[](https://bundlejs.com/?q=sigpro)
|
||||
[]
|
||||
[](https://github.com/natxocc/sigpro/blob/main/LICENSE)
|
||||
|
||||
[**Explore the Docs →**](https://sigpro.natxocc.com/#/)
|
||||
@@ -62,7 +62,7 @@ Create reactive, persistent components with a syntax that feels like Vanilla JS,
|
||||
```
|
||||
|
||||
```javascript
|
||||
import "sigpro";
|
||||
import { $, mount } from "sigpro";
|
||||
|
||||
const Counter = () => {
|
||||
// Simple signal
|
||||
@@ -89,7 +89,7 @@ mount(Counter, "#app");
|
||||
|
||||
| Feature | **SigPro** | React / Vue | Svelte |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Payload (Gzipped)** | **~3KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) |
|
||||
| **Payload (Gzipped)** | **<3KB** | ~30KB - 50KB | ~5KB (Compiled Runtime) |
|
||||
| **State Logic** | **Atomic Signals** | Virtual DOM Diffing | Compiler Dirty Bits |
|
||||
| **Update Speed** | **Direct Node Access** | Component Re-render | Block Reconciliation |
|
||||
| **Native Persistence** | **Included ($)** | Requires Plugins | Manual |
|
||||
|
||||
776
dist/router.js
vendored
Normal file
776
dist/router.js
vendored
Normal file
@@ -0,0 +1,776 @@
|
||||
// src/sigpro.js
|
||||
var isFunc = (f) => typeof f === "function";
|
||||
var isObj = (o) => o && typeof o === "object";
|
||||
var isArr = Array.isArray;
|
||||
var doc = typeof document !== "undefined" ? document : null;
|
||||
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
|
||||
var activeEffect = null;
|
||||
var activeOwner = null;
|
||||
var isFlushing = false;
|
||||
var batchDepth = 0;
|
||||
var effectQueue = new Set;
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
var dispose = (eff) => {
|
||||
if (!eff || eff._disposed)
|
||||
return;
|
||||
eff._disposed = true;
|
||||
const stack = [eff];
|
||||
while (stack.length) {
|
||||
const e = stack.pop();
|
||||
if (e._cleanups) {
|
||||
e._cleanups.forEach((fn) => fn());
|
||||
e._cleanups.clear();
|
||||
}
|
||||
if (e._children) {
|
||||
e._children.forEach((child) => stack.push(child));
|
||||
e._children.clear();
|
||||
}
|
||||
if (e._deps) {
|
||||
e._deps.forEach((depSet) => depSet.delete(e));
|
||||
e._deps.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
var onUnmount = (fn) => {
|
||||
if (activeOwner)
|
||||
(activeOwner._cleanups ||= new Set).add(fn);
|
||||
};
|
||||
var untrack = (fn) => {
|
||||
const p = activeEffect;
|
||||
activeEffect = null;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
activeEffect = p;
|
||||
}
|
||||
};
|
||||
var createEffect = (fn, isComputed = false) => {
|
||||
const effect = () => {
|
||||
if (effect._disposed)
|
||||
return;
|
||||
if (effect._deps)
|
||||
effect._deps.forEach((s) => s.delete(effect));
|
||||
if (effect._cleanups) {
|
||||
effect._cleanups.forEach((c) => c());
|
||||
effect._cleanups.clear();
|
||||
}
|
||||
const prevEffect = activeEffect;
|
||||
const prevOwner = activeOwner;
|
||||
activeEffect = activeOwner = effect;
|
||||
try {
|
||||
return effect._result = fn();
|
||||
} catch (e) {
|
||||
console.error("[SigPro]", e);
|
||||
} finally {
|
||||
activeEffect = prevEffect;
|
||||
activeOwner = prevOwner;
|
||||
}
|
||||
};
|
||||
effect._deps = effect._cleanups = effect._children = null;
|
||||
effect._disposed = false;
|
||||
effect._isComputed = isComputed;
|
||||
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
|
||||
effect._mounts = [];
|
||||
effect._parent = activeOwner;
|
||||
if (activeOwner)
|
||||
(activeOwner._children ||= new Set).add(effect);
|
||||
return effect;
|
||||
};
|
||||
var flush = () => {
|
||||
if (isFlushing)
|
||||
return;
|
||||
isFlushing = true;
|
||||
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
|
||||
effectQueue.clear();
|
||||
for (const e of sorted)
|
||||
if (!e._disposed)
|
||||
e();
|
||||
isFlushing = false;
|
||||
};
|
||||
var trackUpdate = (subs, trigger = false) => {
|
||||
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||
subs.add(activeEffect);
|
||||
(activeEffect._deps ||= new Set).add(subs);
|
||||
} else if (trigger && subs.size > 0) {
|
||||
let hasQueue = false;
|
||||
for (const e of subs) {
|
||||
if (e === activeEffect || e._disposed)
|
||||
continue;
|
||||
if (e._isComputed) {
|
||||
e._dirty = true;
|
||||
if (e._subs)
|
||||
trackUpdate(e._subs, true);
|
||||
} else {
|
||||
effectQueue.add(e);
|
||||
hasQueue = true;
|
||||
}
|
||||
}
|
||||
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
};
|
||||
var $ = (val, key = null) => {
|
||||
const subs = new Set;
|
||||
if (isFunc(val)) {
|
||||
let cache;
|
||||
const computed = () => {
|
||||
if (computed._dirty) {
|
||||
const prev = activeEffect;
|
||||
activeEffect = computed;
|
||||
try {
|
||||
const next = val();
|
||||
if (!Object.is(cache, next)) {
|
||||
cache = next;
|
||||
trackUpdate(subs, true);
|
||||
}
|
||||
} finally {
|
||||
activeEffect = prev;
|
||||
}
|
||||
computed._dirty = false;
|
||||
}
|
||||
trackUpdate(subs);
|
||||
return cache;
|
||||
};
|
||||
computed._isComputed = true;
|
||||
computed._subs = subs;
|
||||
computed._dirty = true;
|
||||
computed._deps = null;
|
||||
computed._disposed = false;
|
||||
return computed;
|
||||
}
|
||||
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;
|
||||
};
|
||||
};
|
||||
var watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect2 = createEffect(sources);
|
||||
effect2();
|
||||
return () => dispose(effect2);
|
||||
}
|
||||
const effect = createEffect(() => {
|
||||
const vals = isArr(sources) ? sources.map((s) => s()) : sources();
|
||||
untrack(() => cb(vals));
|
||||
});
|
||||
effect();
|
||||
return () => dispose(effect);
|
||||
};
|
||||
var cleanupNode = (node) => {
|
||||
if (!node)
|
||||
return;
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((fn) => fn());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
if (node._ownerEffect)
|
||||
dispose(node._ownerEffect);
|
||||
if (node.childNodes)
|
||||
node.childNodes.forEach((n) => cleanupNode(n));
|
||||
};
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal))
|
||||
return "#";
|
||||
}
|
||||
return val;
|
||||
};
|
||||
var h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
props = {};
|
||||
}
|
||||
if (isFunc(tag)) {
|
||||
const effect = createEffect(() => {
|
||||
const result2 = tag(props, {
|
||||
children,
|
||||
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||
});
|
||||
effect._result = result2;
|
||||
return result2;
|
||||
});
|
||||
effect();
|
||||
const result = effect._result;
|
||||
if (result == null)
|
||||
return null;
|
||||
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
|
||||
const attach = (n) => {
|
||||
if (isObj(n) && !n._isRuntime) {
|
||||
n._mounts = effect._mounts || [];
|
||||
n._cleanups = effect._cleanups || new Set;
|
||||
n._ownerEffect = effect;
|
||||
}
|
||||
};
|
||||
isArr(node) ? node.forEach(attach) : attach(node);
|
||||
return node;
|
||||
}
|
||||
const isSVG = SVG_TAGS.has(tag);
|
||||
const el = isSVG ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag);
|
||||
el._cleanups = new Set;
|
||||
for (const k of Object.keys(props)) {
|
||||
let v = props[k];
|
||||
if (k === "ref") {
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v);
|
||||
cleanVal == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, v);
|
||||
const off = () => el.removeEventListener(ev, v);
|
||||
el._cleanups.add(off);
|
||||
onUnmount(off);
|
||||
} else if (isFunc(v)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v());
|
||||
if (k === "class")
|
||||
el.className = val || "";
|
||||
else if (val == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k === "style" && typeof val === "string")
|
||||
el.setAttribute("style", val);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input";
|
||||
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v);
|
||||
if (val != null) {
|
||||
if (k === "style" && typeof val === "string")
|
||||
el.setAttribute("style", val);
|
||||
else 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)) {
|
||||
const anchor = doc.createTextNode("");
|
||||
el.appendChild(anchor);
|
||||
let currentNodes = [];
|
||||
const effect = createEffect(() => {
|
||||
const res = c();
|
||||
const next = (isArr(res) ? res : [res]).map(ensureNode);
|
||||
currentNodes.forEach((n) => {
|
||||
if (n._isRuntime)
|
||||
n.destroy();
|
||||
else
|
||||
cleanupNode(n);
|
||||
if (n.parentNode)
|
||||
n.remove();
|
||||
});
|
||||
let ref = anchor;
|
||||
for (let i = next.length - 1;i >= 0; i--) {
|
||||
const node = next[i];
|
||||
if (node.parentNode !== ref.parentNode)
|
||||
ref.parentNode?.insertBefore(node, ref);
|
||||
if (node._mounts)
|
||||
node._mounts.forEach((fn) => fn());
|
||||
ref = node;
|
||||
}
|
||||
currentNodes = next;
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
} else {
|
||||
const node = ensureNode(c);
|
||||
el.appendChild(node);
|
||||
if (node._mounts)
|
||||
node._mounts.forEach((fn) => fn());
|
||||
}
|
||||
};
|
||||
append(children);
|
||||
return el;
|
||||
};
|
||||
var render = (renderFn) => {
|
||||
const cleanups = new Set;
|
||||
const previousOwner = activeOwner;
|
||||
const previousEffect = activeEffect;
|
||||
const container = doc.createElement("div");
|
||||
container.style.display = "contents";
|
||||
container.setAttribute("role", "presentation");
|
||||
activeOwner = { _cleanups: cleanups };
|
||||
activeEffect = null;
|
||||
const processResult = (result) => {
|
||||
if (!result)
|
||||
return;
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy);
|
||||
container.appendChild(result.container);
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult);
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)));
|
||||
}
|
||||
};
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||
} finally {
|
||||
activeOwner = previousOwner;
|
||||
activeEffect = previousEffect;
|
||||
}
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((fn) => fn());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
}
|
||||
|
||||
// src/router.js
|
||||
var { default: fs} = (() => ({}));
|
||||
|
||||
// node:path
|
||||
function assertPath(path) {
|
||||
if (typeof path !== "string")
|
||||
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
||||
}
|
||||
function normalizeStringPosix(path, allowAboveRoot) {
|
||||
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
||||
for (var i = 0;i <= path.length; ++i) {
|
||||
if (i < path.length)
|
||||
code = path.charCodeAt(i);
|
||||
else if (code === 47)
|
||||
break;
|
||||
else
|
||||
code = 47;
|
||||
if (code === 47) {
|
||||
if (lastSlash === i - 1 || dots === 1)
|
||||
;
|
||||
else if (lastSlash !== i - 1 && dots === 2) {
|
||||
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
||||
if (res.length > 2) {
|
||||
var lastSlashIndex = res.lastIndexOf("/");
|
||||
if (lastSlashIndex !== res.length - 1) {
|
||||
if (lastSlashIndex === -1)
|
||||
res = "", lastSegmentLength = 0;
|
||||
else
|
||||
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
||||
lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
} else if (res.length === 2 || res.length === 1) {
|
||||
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
if (res.length > 0)
|
||||
res += "/..";
|
||||
else
|
||||
res = "..";
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0)
|
||||
res += "/" + path.slice(lastSlash + 1, i);
|
||||
else
|
||||
res = path.slice(lastSlash + 1, i);
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i, dots = 0;
|
||||
} else if (code === 46 && dots !== -1)
|
||||
++dots;
|
||||
else
|
||||
dots = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function _format(sep, pathObject) {
|
||||
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
||||
if (!dir)
|
||||
return base;
|
||||
if (dir === pathObject.root)
|
||||
return dir + base;
|
||||
return dir + sep + base;
|
||||
}
|
||||
function resolve() {
|
||||
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
||||
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
||||
var path;
|
||||
if (i >= 0)
|
||||
path = arguments[i];
|
||||
else {
|
||||
if (cwd === undefined)
|
||||
cwd = process.cwd();
|
||||
path = cwd;
|
||||
}
|
||||
if (assertPath(path), path.length === 0)
|
||||
continue;
|
||||
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
||||
}
|
||||
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
||||
if (resolvedPath.length > 0)
|
||||
return "/" + resolvedPath;
|
||||
else
|
||||
return "/";
|
||||
else if (resolvedPath.length > 0)
|
||||
return resolvedPath;
|
||||
else
|
||||
return ".";
|
||||
}
|
||||
function normalize(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
||||
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
||||
path = ".";
|
||||
if (path.length > 0 && trailingSeparator)
|
||||
path += "/";
|
||||
if (isAbsolute)
|
||||
return "/" + path;
|
||||
return path;
|
||||
}
|
||||
function isAbsolute(path) {
|
||||
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
||||
}
|
||||
function join() {
|
||||
if (arguments.length === 0)
|
||||
return ".";
|
||||
var joined;
|
||||
for (var i = 0;i < arguments.length; ++i) {
|
||||
var arg = arguments[i];
|
||||
if (assertPath(arg), arg.length > 0)
|
||||
if (joined === undefined)
|
||||
joined = arg;
|
||||
else
|
||||
joined += "/" + arg;
|
||||
}
|
||||
if (joined === undefined)
|
||||
return ".";
|
||||
return normalize(joined);
|
||||
}
|
||||
function relative(from, to) {
|
||||
if (assertPath(from), assertPath(to), from === to)
|
||||
return "";
|
||||
if (from = resolve(from), to = resolve(to), from === to)
|
||||
return "";
|
||||
var fromStart = 1;
|
||||
for (;fromStart < from.length; ++fromStart)
|
||||
if (from.charCodeAt(fromStart) !== 47)
|
||||
break;
|
||||
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
||||
for (;toStart < to.length; ++toStart)
|
||||
if (to.charCodeAt(toStart) !== 47)
|
||||
break;
|
||||
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
||||
for (;i <= length; ++i) {
|
||||
if (i === length) {
|
||||
if (toLen > length) {
|
||||
if (to.charCodeAt(toStart + i) === 47)
|
||||
return to.slice(toStart + i + 1);
|
||||
else if (i === 0)
|
||||
return to.slice(toStart + i);
|
||||
} else if (fromLen > length) {
|
||||
if (from.charCodeAt(fromStart + i) === 47)
|
||||
lastCommonSep = i;
|
||||
else if (i === 0)
|
||||
lastCommonSep = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
||||
if (fromCode !== toCode)
|
||||
break;
|
||||
else if (fromCode === 47)
|
||||
lastCommonSep = i;
|
||||
}
|
||||
var out = "";
|
||||
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
||||
if (i === fromEnd || from.charCodeAt(i) === 47)
|
||||
if (out.length === 0)
|
||||
out += "..";
|
||||
else
|
||||
out += "/..";
|
||||
if (out.length > 0)
|
||||
return out + to.slice(toStart + lastCommonSep);
|
||||
else {
|
||||
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
||||
++toStart;
|
||||
return to.slice(toStart);
|
||||
}
|
||||
}
|
||||
function _makeLong(path) {
|
||||
return path;
|
||||
}
|
||||
function dirname(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
||||
for (var i = path.length - 1;i >= 1; --i)
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else
|
||||
matchedSlash = false;
|
||||
if (end === -1)
|
||||
return hasRoot ? "/" : ".";
|
||||
if (hasRoot && end === 1)
|
||||
return "//";
|
||||
return path.slice(0, end);
|
||||
}
|
||||
function basename(path, ext) {
|
||||
if (ext !== undefined && typeof ext !== "string")
|
||||
throw TypeError('"ext" argument must be a string');
|
||||
assertPath(path);
|
||||
var start = 0, end = -1, matchedSlash = true, i;
|
||||
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
||||
if (ext.length === path.length && ext === path)
|
||||
return "";
|
||||
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
||||
for (i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (firstNonSlashEnd === -1)
|
||||
matchedSlash = false, firstNonSlashEnd = i + 1;
|
||||
if (extIdx >= 0)
|
||||
if (code === ext.charCodeAt(extIdx)) {
|
||||
if (--extIdx === -1)
|
||||
end = i;
|
||||
} else
|
||||
extIdx = -1, end = firstNonSlashEnd;
|
||||
}
|
||||
}
|
||||
if (start === end)
|
||||
end = firstNonSlashEnd;
|
||||
else if (end === -1)
|
||||
end = path.length;
|
||||
return path.slice(start, end);
|
||||
} else {
|
||||
for (i = path.length - 1;i >= 0; --i)
|
||||
if (path.charCodeAt(i) === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (end === -1)
|
||||
return "";
|
||||
return path.slice(start, end);
|
||||
}
|
||||
}
|
||||
function extname(path) {
|
||||
assertPath(path);
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
||||
for (var i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
return "";
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
function format(pathObject) {
|
||||
if (pathObject === null || typeof pathObject !== "object")
|
||||
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
||||
return _format("/", pathObject);
|
||||
}
|
||||
function parse(path) {
|
||||
assertPath(path);
|
||||
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
||||
if (path.length === 0)
|
||||
return ret;
|
||||
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
||||
if (isAbsolute2)
|
||||
ret.root = "/", start = 1;
|
||||
else
|
||||
start = 0;
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
||||
for (;i >= start; --i) {
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
||||
if (end !== -1)
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.base = ret.name = path.slice(1, end);
|
||||
else
|
||||
ret.base = ret.name = path.slice(startPart, end);
|
||||
} else {
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
||||
else
|
||||
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
||||
ret.ext = path.slice(startDot, end);
|
||||
}
|
||||
if (startPart > 0)
|
||||
ret.dir = path.slice(0, startPart - 1);
|
||||
else if (isAbsolute2)
|
||||
ret.dir = "/";
|
||||
return ret;
|
||||
}
|
||||
var sep = "/";
|
||||
var delimiter = ":";
|
||||
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
||||
var path_default = posix;
|
||||
|
||||
// src/router.js
|
||||
var router = (routes) => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
const handler = () => path(getHash());
|
||||
window.addEventListener("hashchange", handler);
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||
const hook = h("div", { class: "router-hook" });
|
||||
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);
|
||||
hook.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
return hook;
|
||||
};
|
||||
router.params = $({});
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
function sigproRouter() {
|
||||
const virtualModuleId = "virtual:sigpro-routes";
|
||||
const resolvedVirtualModuleId = "\x00" + virtualModuleId;
|
||||
const getFiles = (dir) => {
|
||||
if (!fs.existsSync(dir))
|
||||
return [];
|
||||
return fs.readdirSync(dir, { recursive: true }).filter((file) => /\.(js|jsx)$/.test(file) && !path_default.basename(file).startsWith("_")).map((file) => path_default.resolve(dir, file));
|
||||
};
|
||||
const pathToUrl = (pagesDir, filePath) => {
|
||||
let relative2 = path_default.relative(pagesDir, filePath).replace(/\\/g, "/").replace(/\.(js|jsx)$/, "").replace(/\/index$/, "").replace(/^index$/, "");
|
||||
return ("/" + relative2).replace(/\/+/g, "/").replace(/\[\.\.\.([^\]]+)\]/g, "*").replace(/\[([^\]]+)\]/g, ":$1").replace(/\/$/, "") || "/";
|
||||
};
|
||||
return {
|
||||
name: "sigpro-router",
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId)
|
||||
return resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id !== resolvedVirtualModuleId)
|
||||
return;
|
||||
const root = process.cwd();
|
||||
const pagesDir = path_default.resolve(root, "src/pages");
|
||||
const files = getFiles(pagesDir).sort((a, b) => {
|
||||
const urlA = pathToUrl(pagesDir, a);
|
||||
const urlB = pathToUrl(pagesDir, b);
|
||||
if (urlA.includes(":") && !urlB.includes(":"))
|
||||
return 1;
|
||||
if (!urlA.includes(":") && urlB.includes(":"))
|
||||
return -1;
|
||||
return urlB.length - urlA.length;
|
||||
});
|
||||
let routeEntries = "";
|
||||
files.forEach((fullPath) => {
|
||||
const urlPath = pathToUrl(pagesDir, fullPath);
|
||||
const relativeImport = "./" + path_default.relative(root, fullPath).replace(/\\/g, "/");
|
||||
routeEntries += ` { path: '${urlPath}', component: () => import('/${relativeImport}') },
|
||||
`;
|
||||
});
|
||||
if (!routeEntries.includes("path: '*'")) {
|
||||
routeEntries += ` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },
|
||||
`;
|
||||
}
|
||||
return `export const routes = [
|
||||
${routeEntries}];`;
|
||||
}
|
||||
};
|
||||
}
|
||||
export {
|
||||
sigproRouter,
|
||||
router
|
||||
};
|
||||
84
dist/sigpro.esm.js
vendored
84
dist/sigpro.esm.js
vendored
@@ -9,8 +9,6 @@ var activeOwner = null;
|
||||
var isFlushing = false;
|
||||
var batchDepth = 0;
|
||||
var effectQueue = new Set;
|
||||
var proxyCache = new WeakMap;
|
||||
var ITER = Symbol("iter");
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
@@ -171,52 +169,6 @@ var $ = (val, key = null) => {
|
||||
return val;
|
||||
};
|
||||
};
|
||||
var $$ = (target) => {
|
||||
if (!isObj(target))
|
||||
return target;
|
||||
const cached = proxyCache.get(target);
|
||||
if (cached)
|
||||
return cached;
|
||||
const subs = new Map;
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key);
|
||||
if (!set)
|
||||
subs.set(key, set = new Set);
|
||||
return set;
|
||||
};
|
||||
const proxy = new Proxy(target, {
|
||||
get(target2, key, receiver) {
|
||||
if (typeof key !== "symbol")
|
||||
trackUpdate(getSubs(key));
|
||||
return $$(Reflect.get(target2, key, receiver));
|
||||
},
|
||||
set(target2, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target2, key);
|
||||
const oldValue = Reflect.get(target2, key, receiver);
|
||||
const result = Reflect.set(target2, key, value, receiver);
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
if (!hadKey)
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deleteProperty(target2, key) {
|
||||
const result = Reflect.deleteProperty(target2, key);
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
ownKeys(target2) {
|
||||
trackUpdate(getSubs(ITER));
|
||||
return Reflect.ownKeys(target2);
|
||||
}
|
||||
});
|
||||
proxyCache.set(target, proxy);
|
||||
return proxy;
|
||||
};
|
||||
var watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect2 = createEffect(sources);
|
||||
@@ -466,39 +418,6 @@ var each = (src, itemFn, keyField) => {
|
||||
});
|
||||
return root;
|
||||
};
|
||||
var router = (routes) => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
const handler = () => path(getHash());
|
||||
window.addEventListener("hashchange", handler);
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||
const hook = h("div", { class: "router-hook" });
|
||||
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);
|
||||
hook.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
return hook;
|
||||
};
|
||||
router.params = $({});
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var Fragment = (props) => props.children;
|
||||
var mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
@@ -519,7 +438,7 @@ if (typeof window !== "undefined") {
|
||||
export {
|
||||
when,
|
||||
watch,
|
||||
router,
|
||||
render,
|
||||
onUnmount,
|
||||
mount,
|
||||
isObj,
|
||||
@@ -529,6 +448,5 @@ export {
|
||||
each,
|
||||
batch,
|
||||
Fragment,
|
||||
$$,
|
||||
$
|
||||
};
|
||||
|
||||
2
dist/sigpro.esm.min.js
vendored
2
dist/sigpro.esm.min.js
vendored
File diff suppressed because one or more lines are too long
447
dist/sigpro.js
vendored
447
dist/sigpro.js
vendored
@@ -1,4 +1,42 @@
|
||||
(() => {
|
||||
var __create = Object.create;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toESMCache_node;
|
||||
var __toESMCache_esm;
|
||||
var __toESM = (mod, isNodeMode, target) => {
|
||||
var canCache = mod != null && typeof mod === "object";
|
||||
if (canCache) {
|
||||
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
||||
var cached = cache.get(mod);
|
||||
if (cached)
|
||||
return cached;
|
||||
}
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
};
|
||||
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
||||
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
||||
}) : x)(function(x) {
|
||||
if (typeof require !== "undefined")
|
||||
return require.apply(this, arguments);
|
||||
throw Error('Dynamic require of "' + x + '" is not supported');
|
||||
});
|
||||
|
||||
// src/sigpro.js
|
||||
var isFunc = (f) => typeof f === "function";
|
||||
var isObj = (o) => o && typeof o === "object";
|
||||
@@ -10,8 +48,6 @@
|
||||
var isFlushing = false;
|
||||
var batchDepth = 0;
|
||||
var effectQueue = new Set;
|
||||
var proxyCache = new WeakMap;
|
||||
var ITER = Symbol("iter");
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
@@ -172,52 +208,6 @@
|
||||
return val;
|
||||
};
|
||||
};
|
||||
var $$ = (target) => {
|
||||
if (!isObj(target))
|
||||
return target;
|
||||
const cached = proxyCache.get(target);
|
||||
if (cached)
|
||||
return cached;
|
||||
const subs = new Map;
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key);
|
||||
if (!set)
|
||||
subs.set(key, set = new Set);
|
||||
return set;
|
||||
};
|
||||
const proxy = new Proxy(target, {
|
||||
get(target2, key, receiver) {
|
||||
if (typeof key !== "symbol")
|
||||
trackUpdate(getSubs(key));
|
||||
return $$(Reflect.get(target2, key, receiver));
|
||||
},
|
||||
set(target2, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target2, key);
|
||||
const oldValue = Reflect.get(target2, key, receiver);
|
||||
const result = Reflect.set(target2, key, value, receiver);
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
if (!hadKey)
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deleteProperty(target2, key) {
|
||||
const result = Reflect.deleteProperty(target2, key);
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
ownKeys(target2) {
|
||||
trackUpdate(getSubs(ITER));
|
||||
return Reflect.ownKeys(target2);
|
||||
}
|
||||
});
|
||||
proxyCache.set(target, proxy);
|
||||
return proxy;
|
||||
};
|
||||
var watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect2 = createEffect(sources);
|
||||
@@ -467,6 +457,348 @@
|
||||
});
|
||||
return root;
|
||||
};
|
||||
var Fragment = (props) => props.children;
|
||||
var 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;
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
}
|
||||
|
||||
// src/router.js
|
||||
var import_node_fs = (() => ({}));
|
||||
|
||||
// node:path
|
||||
function assertPath(path) {
|
||||
if (typeof path !== "string")
|
||||
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
||||
}
|
||||
function normalizeStringPosix(path, allowAboveRoot) {
|
||||
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
||||
for (var i = 0;i <= path.length; ++i) {
|
||||
if (i < path.length)
|
||||
code = path.charCodeAt(i);
|
||||
else if (code === 47)
|
||||
break;
|
||||
else
|
||||
code = 47;
|
||||
if (code === 47) {
|
||||
if (lastSlash === i - 1 || dots === 1)
|
||||
;
|
||||
else if (lastSlash !== i - 1 && dots === 2) {
|
||||
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
||||
if (res.length > 2) {
|
||||
var lastSlashIndex = res.lastIndexOf("/");
|
||||
if (lastSlashIndex !== res.length - 1) {
|
||||
if (lastSlashIndex === -1)
|
||||
res = "", lastSegmentLength = 0;
|
||||
else
|
||||
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
||||
lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
} else if (res.length === 2 || res.length === 1) {
|
||||
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
if (res.length > 0)
|
||||
res += "/..";
|
||||
else
|
||||
res = "..";
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0)
|
||||
res += "/" + path.slice(lastSlash + 1, i);
|
||||
else
|
||||
res = path.slice(lastSlash + 1, i);
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i, dots = 0;
|
||||
} else if (code === 46 && dots !== -1)
|
||||
++dots;
|
||||
else
|
||||
dots = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function _format(sep, pathObject) {
|
||||
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
||||
if (!dir)
|
||||
return base;
|
||||
if (dir === pathObject.root)
|
||||
return dir + base;
|
||||
return dir + sep + base;
|
||||
}
|
||||
function resolve() {
|
||||
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
||||
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
||||
var path;
|
||||
if (i >= 0)
|
||||
path = arguments[i];
|
||||
else {
|
||||
if (cwd === undefined)
|
||||
cwd = process.cwd();
|
||||
path = cwd;
|
||||
}
|
||||
if (assertPath(path), path.length === 0)
|
||||
continue;
|
||||
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
||||
}
|
||||
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
||||
if (resolvedPath.length > 0)
|
||||
return "/" + resolvedPath;
|
||||
else
|
||||
return "/";
|
||||
else if (resolvedPath.length > 0)
|
||||
return resolvedPath;
|
||||
else
|
||||
return ".";
|
||||
}
|
||||
function normalize(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
||||
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
||||
path = ".";
|
||||
if (path.length > 0 && trailingSeparator)
|
||||
path += "/";
|
||||
if (isAbsolute)
|
||||
return "/" + path;
|
||||
return path;
|
||||
}
|
||||
function isAbsolute(path) {
|
||||
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
||||
}
|
||||
function join() {
|
||||
if (arguments.length === 0)
|
||||
return ".";
|
||||
var joined;
|
||||
for (var i = 0;i < arguments.length; ++i) {
|
||||
var arg = arguments[i];
|
||||
if (assertPath(arg), arg.length > 0)
|
||||
if (joined === undefined)
|
||||
joined = arg;
|
||||
else
|
||||
joined += "/" + arg;
|
||||
}
|
||||
if (joined === undefined)
|
||||
return ".";
|
||||
return normalize(joined);
|
||||
}
|
||||
function relative(from, to) {
|
||||
if (assertPath(from), assertPath(to), from === to)
|
||||
return "";
|
||||
if (from = resolve(from), to = resolve(to), from === to)
|
||||
return "";
|
||||
var fromStart = 1;
|
||||
for (;fromStart < from.length; ++fromStart)
|
||||
if (from.charCodeAt(fromStart) !== 47)
|
||||
break;
|
||||
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
||||
for (;toStart < to.length; ++toStart)
|
||||
if (to.charCodeAt(toStart) !== 47)
|
||||
break;
|
||||
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
||||
for (;i <= length; ++i) {
|
||||
if (i === length) {
|
||||
if (toLen > length) {
|
||||
if (to.charCodeAt(toStart + i) === 47)
|
||||
return to.slice(toStart + i + 1);
|
||||
else if (i === 0)
|
||||
return to.slice(toStart + i);
|
||||
} else if (fromLen > length) {
|
||||
if (from.charCodeAt(fromStart + i) === 47)
|
||||
lastCommonSep = i;
|
||||
else if (i === 0)
|
||||
lastCommonSep = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
||||
if (fromCode !== toCode)
|
||||
break;
|
||||
else if (fromCode === 47)
|
||||
lastCommonSep = i;
|
||||
}
|
||||
var out = "";
|
||||
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
||||
if (i === fromEnd || from.charCodeAt(i) === 47)
|
||||
if (out.length === 0)
|
||||
out += "..";
|
||||
else
|
||||
out += "/..";
|
||||
if (out.length > 0)
|
||||
return out + to.slice(toStart + lastCommonSep);
|
||||
else {
|
||||
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
||||
++toStart;
|
||||
return to.slice(toStart);
|
||||
}
|
||||
}
|
||||
function _makeLong(path) {
|
||||
return path;
|
||||
}
|
||||
function dirname(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
||||
for (var i = path.length - 1;i >= 1; --i)
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else
|
||||
matchedSlash = false;
|
||||
if (end === -1)
|
||||
return hasRoot ? "/" : ".";
|
||||
if (hasRoot && end === 1)
|
||||
return "//";
|
||||
return path.slice(0, end);
|
||||
}
|
||||
function basename(path, ext) {
|
||||
if (ext !== undefined && typeof ext !== "string")
|
||||
throw TypeError('"ext" argument must be a string');
|
||||
assertPath(path);
|
||||
var start = 0, end = -1, matchedSlash = true, i;
|
||||
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
||||
if (ext.length === path.length && ext === path)
|
||||
return "";
|
||||
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
||||
for (i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (firstNonSlashEnd === -1)
|
||||
matchedSlash = false, firstNonSlashEnd = i + 1;
|
||||
if (extIdx >= 0)
|
||||
if (code === ext.charCodeAt(extIdx)) {
|
||||
if (--extIdx === -1)
|
||||
end = i;
|
||||
} else
|
||||
extIdx = -1, end = firstNonSlashEnd;
|
||||
}
|
||||
}
|
||||
if (start === end)
|
||||
end = firstNonSlashEnd;
|
||||
else if (end === -1)
|
||||
end = path.length;
|
||||
return path.slice(start, end);
|
||||
} else {
|
||||
for (i = path.length - 1;i >= 0; --i)
|
||||
if (path.charCodeAt(i) === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (end === -1)
|
||||
return "";
|
||||
return path.slice(start, end);
|
||||
}
|
||||
}
|
||||
function extname(path) {
|
||||
assertPath(path);
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
||||
for (var i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
return "";
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
function format(pathObject) {
|
||||
if (pathObject === null || typeof pathObject !== "object")
|
||||
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
||||
return _format("/", pathObject);
|
||||
}
|
||||
function parse(path) {
|
||||
assertPath(path);
|
||||
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
||||
if (path.length === 0)
|
||||
return ret;
|
||||
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
||||
if (isAbsolute2)
|
||||
ret.root = "/", start = 1;
|
||||
else
|
||||
start = 0;
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
||||
for (;i >= start; --i) {
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
||||
if (end !== -1)
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.base = ret.name = path.slice(1, end);
|
||||
else
|
||||
ret.base = ret.name = path.slice(startPart, end);
|
||||
} else {
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
||||
else
|
||||
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
||||
ret.ext = path.slice(startDot, end);
|
||||
}
|
||||
if (startPart > 0)
|
||||
ret.dir = path.slice(0, startPart - 1);
|
||||
else if (isAbsolute2)
|
||||
ret.dir = "/";
|
||||
return ret;
|
||||
}
|
||||
var sep = "/";
|
||||
var delimiter = ":";
|
||||
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
||||
|
||||
// src/router.js
|
||||
var router = (routes) => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
@@ -500,26 +832,9 @@
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var Fragment = (props) => props.children;
|
||||
var 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;
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
}
|
||||
|
||||
// src/build_umd.js
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, { $, $$, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
|
||||
Object.assign(window, { $, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
|
||||
}
|
||||
})();
|
||||
|
||||
2
dist/sigpro.min.js
vendored
2
dist/sigpro.min.js
vendored
File diff suppressed because one or more lines are too long
77
dist/vite.js
vendored
77
dist/vite.js
vendored
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* SigPro Vite Plugin - File-based Routing
|
||||
* @module sigpro/vite
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export default function sigproRouter() {
|
||||
const virtualModuleId = 'virtual:sigpro-routes';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
// Helper para escanear archivos
|
||||
const getFiles = (dir) => {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { recursive: true })
|
||||
.filter(file => /\.(js|jsx)$/.test(file) && !path.basename(file).startsWith('_'))
|
||||
.map(file => path.resolve(dir, file));
|
||||
};
|
||||
|
||||
// Transformador de ruta de archivo a URL de router
|
||||
const pathToUrl = (pagesDir, filePath) => {
|
||||
let relative = path.relative(pagesDir, filePath)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.(js|jsx)$/, '')
|
||||
.replace(/\/index$/, '')
|
||||
.replace(/^index$/, '');
|
||||
|
||||
return ('/' + relative)
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '*')
|
||||
.replace(/\[([^\]]+)\]/g, ':$1')
|
||||
.replace(/\/$/, '') || '/';
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'sigpro-router',
|
||||
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (id !== resolvedVirtualModuleId) return;
|
||||
|
||||
const root = process.cwd();
|
||||
const pagesDir = path.resolve(root, 'src/pages');
|
||||
|
||||
// Obtenemos y ordenamos archivos (rutas estáticas primero, luego dinámicas)
|
||||
const files = getFiles(pagesDir).sort((a, b) => {
|
||||
const urlA = pathToUrl(pagesDir, a);
|
||||
const urlB = pathToUrl(pagesDir, b);
|
||||
if (urlA.includes(':') && !urlB.includes(':')) return 1;
|
||||
if (!urlA.includes(':') && urlB.includes(':')) return -1;
|
||||
return urlB.length - urlA.length;
|
||||
});
|
||||
|
||||
let routeEntries = '';
|
||||
|
||||
files.forEach((fullPath) => {
|
||||
const urlPath = pathToUrl(pagesDir, fullPath);
|
||||
// Hacemos la ruta relativa al proyecto para que el import de Vite sea limpio
|
||||
const relativeImport = './' + path.relative(root, fullPath).replace(/\\/g, '/');
|
||||
|
||||
routeEntries += ` { path: '${urlPath}', component: async () => (await import('/${relativeImport}')).default },\n`;
|
||||
});
|
||||
|
||||
// Fallback 404 si no existe una ruta comodín
|
||||
if (!routeEntries.includes("path: '*'")) {
|
||||
routeEntries += ` { path: '*', component: () => document.createTextNode('404 - Not Found') },\n`;
|
||||
}
|
||||
|
||||
return `export const routes = [\n${routeEntries}];`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { sigproRouter };
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://git.natxocc.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">Gitea</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine‑grained signals update exactly what changes. No V‑DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA‑THIN</h3><p class="text-sm opacity-70">Sub‑3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER‑FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
|
||||
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Fine‑grained signals update exactly what changes. No V‑DOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRA‑THIN</h3><p class="text-sm opacity-70">less than 3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILER‑FREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>High‑Efficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>
|
||||
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
* **Introduction**
|
||||
* [Installation](install.md)
|
||||
* [Vite Plugin](vite/plugin.md)
|
||||
* [Router](router.md)
|
||||
|
||||
* **API Reference**
|
||||
* [Quick Start](api/quick.md)
|
||||
* [Signals & Proxies](api/signal.md)
|
||||
* [$ignal](api/signal.md)
|
||||
* [watch](api/watch.md)
|
||||
* [when](api/when.md)
|
||||
* [each](api/each.md)
|
||||
* [router](api/router.md)
|
||||
* [mount](api/mount.md)
|
||||
* [h](api/h.md)
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ batch(() => {
|
||||
Hash‑based SPA router. Returns a DOM node that renders the current route.
|
||||
|
||||
```javascript
|
||||
import { router } from 'sigpro'
|
||||
import { router } from 'sigpro/router' // import router
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => div({}, 'Home') },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Signal Function: `$( )`
|
||||
# The Signal Function: `$()`
|
||||
|
||||
The `$( )` function is the core constructor of SigPro. It defines how data is stored, computed, and persisted.
|
||||
The `$()` function is the **only** reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.
|
||||
|
||||
## Function Signature
|
||||
|
||||
@@ -99,175 +99,181 @@ When calling the setter, you can pass an **updater function** to access the curr
|
||||
|
||||
---
|
||||
|
||||
# The Reactive Object: `$$( )`
|
||||
## Composing Signals for Complex State
|
||||
|
||||
The `$$( )` function creates a reactive proxy for complex nested objects. Unlike `$()`, which tracks a single value, `$$()` tracks **every property access** automatically.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
$$<T extends object>(obj: T): T
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`obj`** | `object` | Yes | The object to make reactive. Properties are tracked recursively. |
|
||||
|
||||
**Returns:** A reactive proxy that behaves like the original object but triggers updates when any property changes.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
For nested objects, **compose signals** instead of using magic proxies. This gives you explicit control over reactivity and memory.
|
||||
|
||||
### 1. Simple Object
|
||||
|
||||
<div id="demo-dollar-simple"></div>
|
||||
<div id="demo-compose-simple"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const state = $$({ count: 0, name: "Juan" });
|
||||
watch(() => console.log(`Count is now ${state.count}`));
|
||||
const count = $(0);
|
||||
const name = $("Juan");
|
||||
|
||||
// Optionally create a derived combined state
|
||||
const state = $(() => ({ count: count(), name: name() }));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Count: ${state.count}, Name: ${state.name}`),
|
||||
button({ onClick: () => state.count++ }, "Increment count"),
|
||||
button({ onClick: () => state.name = state.name === "Juan" ? "Ana" : "Juan" }, "Toggle name")
|
||||
p(() => `Count: ${count()}, Name: ${name()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment count"),
|
||||
button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-simple'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deep Reactivity
|
||||
### 2. Deeply Nested State
|
||||
|
||||
<div id="demo-dollar-deep"></div>
|
||||
<div id="demo-compose-deep"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const user = $$({
|
||||
profile: {
|
||||
name: "Juan",
|
||||
address: { city: "Madrid", zip: "28001" }
|
||||
}
|
||||
});
|
||||
const profileName = $("Juan");
|
||||
const profileCity = $("Madrid");
|
||||
const profileZip = $("28001");
|
||||
|
||||
watch(() => user.profile.address.city, () => console.log("City changed"));
|
||||
// Computed derived values
|
||||
const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
|
||||
|
||||
watch(profileCity, () => console.log("City changed to:", profileCity()));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `City: ${user.profile.address.city}`),
|
||||
button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona")
|
||||
p(() => `Name: ${profileName()}`),
|
||||
p(() => `City: ${profileCity()}`),
|
||||
p(() => `Full address: ${fullAddress()}`),
|
||||
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-deep'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-deep'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Arrays
|
||||
|
||||
<div id="demo-dollar-array"></div>
|
||||
<div id="demo-compose-array"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const todos = $$([
|
||||
const todos = $([
|
||||
{ id: 1, text: "Learn SigPro", done: false },
|
||||
{ id: 2, text: "Build an app", done: false }
|
||||
]);
|
||||
|
||||
watch(() => todos.length, () => console.log(`You have ${todos.length} todos`));
|
||||
const todoCount = $(() => todos().length);
|
||||
|
||||
watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
|
||||
|
||||
const App = () => div([
|
||||
ul(() => todos.map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
|
||||
button({ onClick: () => todos.push({ id: Date.now(), text: "New todo", done: false }) }, "Add todo"),
|
||||
button({ onClick: () => todos[0].done = !todos[0].done }, "Toggle first todo")
|
||||
ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
|
||||
button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
|
||||
button({ onClick: () => {
|
||||
const updated = [...todos()];
|
||||
updated[0] = { ...updated[0], done: !updated[0].done };
|
||||
todos(updated);
|
||||
}}, "Toggle first todo")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-array'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-array'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Mixed with Signals
|
||||
### 4. Complete Form Example
|
||||
|
||||
<div id="demo-dollar-mixed"></div>
|
||||
<div id="demo-compose-form"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const form = $$({
|
||||
fields: { email: "", password: "" },
|
||||
isValid: $(false)
|
||||
});
|
||||
const email = $("");
|
||||
const password = $("");
|
||||
const isValid = $(() => email().includes("@") && password().length > 6);
|
||||
|
||||
const canSubmit = $(() =>
|
||||
form.fields.email.includes("@") &&
|
||||
form.fields.password.length > 6
|
||||
);
|
||||
|
||||
watch(canSubmit, valid => form.isValid(valid));
|
||||
watch(isValid, valid => console.log("Form valid:", valid));
|
||||
|
||||
const App = () => div([
|
||||
input({ type: "email", placeholder: "Email", value: () => form.fields.email, onInput: e => form.fields.email = e.target.value }),
|
||||
input({ type: "password", placeholder: "Password", value: () => form.fields.password, onInput: e => form.fields.password = e.target.value }),
|
||||
p(() => `Form valid: ${form.isValid() ? "Yes" : "No"}`)
|
||||
input({
|
||||
type: "email",
|
||||
placeholder: "Email",
|
||||
value: email,
|
||||
onInput: e => email(e.target.value)
|
||||
}),
|
||||
input({
|
||||
type: "password",
|
||||
placeholder: "Password",
|
||||
value: password,
|
||||
onInput: e => password(e.target.value)
|
||||
}),
|
||||
p(() => `Form valid: ${isValid() ? "Yes" : "No"}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-mixed'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-form'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences: `$()` vs `$$()`
|
||||
## Best Practices for Complex State
|
||||
|
||||
| Feature | `$()` Signal | `$$()` Reactive |
|
||||
| :--- | :--- | :--- |
|
||||
| **Primitives** | ✅ Works directly | ❌ Needs wrapper object |
|
||||
| **Objects** | Manual tracking | ✅ Automatic deep tracking |
|
||||
| **Nested properties** | ❌ Not reactive | ✅ Fully reactive |
|
||||
| **Arrays** | Requires reassignment | ✅ Methods (push, pop, etc.) work |
|
||||
| **Syntax** | `count()` / `count(5)` | `state.count = 5` |
|
||||
| **LocalStorage** | ✅ Built-in | ❌ (use `$()` for persistence) |
|
||||
| **Performance** | Lighter | Slightly heavier (Proxy) |
|
||||
| **Destructuring** | ✅ Safe | ❌ Breaks reactivity |
|
||||
|
||||
---
|
||||
|
||||
## When to Use Each
|
||||
|
||||
### Use `$()` when:
|
||||
|
||||
<div id="demo-use-dollar"></div>
|
||||
### ✅ DO: Compose signals explicitly
|
||||
|
||||
```javascript
|
||||
{
|
||||
const count = $(0);
|
||||
const firstName = $("John");
|
||||
const lastName = $("Doe");
|
||||
const fullName = $(() => `${firstName()} ${lastName()}`);
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Count up"),
|
||||
p(() => `Full name: ${fullName()}`),
|
||||
input({ value: firstName, placeholder: "First name" }),
|
||||
input({ value: lastName, placeholder: "Last name" })
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-use-dollar'), 50);
|
||||
// Clear, predictable, and memory-safe
|
||||
const user = {
|
||||
name: $("Juan"),
|
||||
email: $("juan@example.com"),
|
||||
preferences: {
|
||||
theme: $("dark"),
|
||||
notifications: $(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Computed values derived from composition
|
||||
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
|
||||
```
|
||||
|
||||
### Use `$$()` when:
|
||||
|
||||
<div id="demo-use-dollar-dollar"></div>
|
||||
### ✅ DO: Create store patterns
|
||||
|
||||
```javascript
|
||||
{
|
||||
const form = $$({ email: "", password: "" });
|
||||
const settings = $$({ theme: "dark", notifications: true });
|
||||
const createUserStore = () => {
|
||||
const name = $("")
|
||||
const email = $("")
|
||||
|
||||
const App = () => div([
|
||||
input({ placeholder: "Email", onInput: e => form.email = e.target.value }),
|
||||
input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }),
|
||||
p(() => `Email: ${form.email}, Password: ${form.password}`),
|
||||
button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"),
|
||||
p(() => `Current theme: ${settings.theme}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50);
|
||||
const isValid = $(() => name().length > 0 && email().includes("@"))
|
||||
|
||||
const actions = {
|
||||
setName: (value) => name(value),
|
||||
setEmail: (value) => email(value),
|
||||
reset: () => {
|
||||
name("")
|
||||
email("")
|
||||
}
|
||||
}
|
||||
|
||||
return { name, email, isValid, ...actions }
|
||||
}
|
||||
|
||||
const userStore = createUserStore()
|
||||
```
|
||||
|
||||
### ❌ DON'T: Try to wrap objects with signals
|
||||
|
||||
```javascript
|
||||
// Wrong - loses reactivity on nested properties
|
||||
const user = $({ name: "Juan", email: "..." })
|
||||
user().name = "Ana" // ❌ Not reactive!
|
||||
|
||||
// Correct - each property its own signal
|
||||
const userName = $("Juan")
|
||||
const userEmail = $("...")
|
||||
```
|
||||
|
||||
### ❌ DON'T: Destructure signals in reactive contexts
|
||||
|
||||
```javascript
|
||||
// Wrong - breaks tracking
|
||||
const { name, email } = user
|
||||
watch(() => name(), ...) // ❌ 'name' is not tracked properly
|
||||
|
||||
// Correct - use the original signal
|
||||
watch(() => user.name(), ...) // ✅
|
||||
```
|
||||
|
||||
---
|
||||
@@ -276,115 +282,121 @@ $$<T extends object>(obj: T): T
|
||||
|
||||
### ✅ DO:
|
||||
```javascript
|
||||
// Access properties directly
|
||||
state.count = 10;
|
||||
state.user.name = "Ana";
|
||||
todos.push(newItem);
|
||||
// Update by recreating objects for arrays
|
||||
todos(prev => [...prev, newTodo])
|
||||
|
||||
// Track in effects
|
||||
watch(() => state.count, () => {});
|
||||
watch(() => state.user.name, () => {});
|
||||
// Update objects immutably
|
||||
const current = user()
|
||||
user({ ...current, name: "Ana" })
|
||||
|
||||
// Track individual signals
|
||||
watch(() => user.name(), () => {})
|
||||
watch(() => user.email(), () => {})
|
||||
```
|
||||
|
||||
### ❌ DON'T:
|
||||
```javascript
|
||||
// Destructuring breaks reactivity
|
||||
const { count, user } = state; // ❌ count and user are not reactive
|
||||
// Mutate objects directly
|
||||
user().name = "Ana" // ❌ Not reactive
|
||||
|
||||
// Reassigning the whole object
|
||||
state = { count: 10 }; // ❌ Loses reactivity
|
||||
// Mutate arrays in place
|
||||
todos().push(newTodo) // ❌ Not reactive
|
||||
|
||||
// Using primitive directly
|
||||
const count = $$(0); // ❌ Doesn't work (use $() instead)
|
||||
// Destructure in component bodies
|
||||
const { name, email } = user // ❌ Breaks reactivity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
|
||||
All signals integrate with the cleanup system:
|
||||
|
||||
- Effects tracking reactive properties are automatically disposed
|
||||
- No manual cleanup needed
|
||||
- Works with `watch`, `when`, and `each`
|
||||
```javascript
|
||||
// Effects are automatically disposed when components unmount
|
||||
const name = $("Juan")
|
||||
watch(name, () => console.log("Name changed"))
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Aspect | `$()` | `$$()` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Implementation** | Closure with Set | Proxy with WeakMap |
|
||||
| **Tracking** | Explicit (function call) | Implicit (property access) |
|
||||
| **Memory** | Minimal | Slightly more (WeakMap cache) |
|
||||
| **Use Case** | Simple state | Complex state |
|
||||
| **Learning Curve** | Low | Low (feels like plain JS) |
|
||||
// Manual cleanup if needed
|
||||
const stop = watch(name, callback)
|
||||
stop() // Clean up manually
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
<div id="demo-complete"></div>
|
||||
<div id="demo-complete-final"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const app = {
|
||||
theme: $("dark", "theme_complete"),
|
||||
sidebarOpen: $(true),
|
||||
user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }),
|
||||
isLoggedIn: $(() => !!app.user.name),
|
||||
login(name, email) {
|
||||
app.user.name = name;
|
||||
app.user.email = email;
|
||||
},
|
||||
logout() {
|
||||
app.user.name = "";
|
||||
app.user.email = "";
|
||||
app.user.preferences.notifications = true;
|
||||
}
|
||||
};
|
||||
// All state as explicit signals
|
||||
const theme = $("dark", "theme_complete")
|
||||
const sidebarOpen = $(true)
|
||||
const userName = $("")
|
||||
const userEmail = $("")
|
||||
const notifications = $(true)
|
||||
const language = $("es")
|
||||
|
||||
// Computed signals
|
||||
const isLoggedIn = $(() => !!userName() && !!userEmail())
|
||||
|
||||
// Actions as plain functions
|
||||
const login = (name, email) => {
|
||||
userName(name)
|
||||
userEmail(email)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userName("")
|
||||
userEmail("")
|
||||
notifications(true) // Reset on logout
|
||||
}
|
||||
|
||||
// Components using signals directly
|
||||
const LoginForm = () => div([
|
||||
input({ placeholder: "Name", onInput: e => app.user.name = e.target.value }),
|
||||
input({ placeholder: "Email", onInput: e => app.user.email = e.target.value }),
|
||||
button({ onClick: () => app.login(app.user.name, app.user.email) }, "Login")
|
||||
]);
|
||||
input({
|
||||
placeholder: "Name",
|
||||
onInput: e => userName(e.target.value)
|
||||
}),
|
||||
input({
|
||||
placeholder: "Email",
|
||||
onInput: e => userEmail(e.target.value)
|
||||
}),
|
||||
button({
|
||||
onClick: () => login(userName(), userEmail())
|
||||
}, "Login")
|
||||
])
|
||||
|
||||
const UserProfile = () => div([
|
||||
h2(() => `Welcome ${app.user.name}`),
|
||||
p(() => `Email: ${app.user.email}`),
|
||||
p(() => `Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
|
||||
button({ onClick: () => app.user.preferences.notifications = !app.user.preferences.notifications }, "Toggle Notifications"),
|
||||
button({ onClick: app.logout }, "Logout")
|
||||
]);
|
||||
h2(() => `Welcome ${userName()}`),
|
||||
p(() => `Email: ${userEmail()}`),
|
||||
p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
|
||||
p(() => `Language: ${language()}`),
|
||||
button({
|
||||
onClick: () => notifications(!notifications())
|
||||
}, "Toggle Notifications"),
|
||||
button({ onClick: logout }, "Logout")
|
||||
])
|
||||
|
||||
const App = () => div({ class: "complete-example" }, [
|
||||
when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
]);
|
||||
when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
])
|
||||
|
||||
setTimeout(() => mount(App, '#demo-complete'), 50);
|
||||
setTimeout(() => mount(App, '#demo-complete-final'), 50)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from `$()` to `$$()`
|
||||
## Summary
|
||||
|
||||
If you have code using nested signals:
|
||||
With **only `$()`** as your reactive primitive:
|
||||
|
||||
```javascript
|
||||
// Before - Manual nesting
|
||||
const user = $({
|
||||
name: $(""),
|
||||
email: $("")
|
||||
});
|
||||
user().name("Juan"); // Need to call inner signal
|
||||
|
||||
// After - Automatic nesting
|
||||
const user = $$({
|
||||
name: "",
|
||||
email: ""
|
||||
});
|
||||
user.name = "Juan"; // Direct assignment
|
||||
```
|
||||
- ✅ **Explicit** - You know exactly what's reactive
|
||||
- ✅ **Memory safe** - No hidden proxies or WeakMap caches
|
||||
- ✅ **Predictable** - No magic, just signals
|
||||
- ✅ **Performant** - Minimal overhead
|
||||
- ✅ **Debuggable** - Clear data flow
|
||||
|
||||
Complex state is built by **composing signals**, not by wrapping objects. This gives you the same power as reactive proxies but with better control and fewer surprises.
|
||||
@@ -182,7 +182,7 @@ SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhea
|
||||
|
||||
| Feature | **SigPro** | **SolidJS** | **Svelte** | **React** | **Vue** |
|
||||
| :----------------- | :--------------- | :----------- | :----------- | :---------- | :---------- |
|
||||
| **Bundle Size** | **~3KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
|
||||
| **Bundle Size** | **<3KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
|
||||
| **DOM Strategy** | **Direct DOM** | Direct DOM | Compiled DOM | Virtual DOM | Virtual DOM |
|
||||
| **Reactivity** | **Fine-grained** | Fine-grained | Compiled | Re-renders | Proxies |
|
||||
| **Build Step** | **Optional** | Required | Required | Required | Optional |
|
||||
|
||||
@@ -17,7 +17,7 @@ router(routes: Route[]): HTMLElement
|
||||
|
||||
**Returns:** A `div` element (with class `"router-hook"`) that acts as the router outlet. The router automatically destroys the previous view and mounts the matched component when the hash changes.
|
||||
|
||||
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro'`). In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
|
||||
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro/router'`). In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
|
||||
|
||||
---
|
||||
|
||||
@@ -187,3 +187,152 @@ mount(App, "#app");
|
||||
| `router.back()` | Goes back in history. |
|
||||
| `router.path()` | Returns the current path without `#`. |
|
||||
| `router.params()` | Reactive signal of the current route parameters. |
|
||||
|
||||
|
||||
# Vite Plugin: File-based Routing
|
||||
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
|
||||
## 1. Project Structure
|
||||
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
|
||||
<pre><code>my-sigpro-app/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── index.js → #/
|
||||
│ │ ├── about.js → #/about
|
||||
│ │ ├── users/
|
||||
│ │ │ └── [id].js → #/users/:id
|
||||
│ │ └── blog/
|
||||
│ │ ├── index.js → #/blog
|
||||
│ │ └── [slug].js → #/blog/:slug
|
||||
│ ├── App.js (Main Layout)
|
||||
│ └── main.js (Entry Point)
|
||||
├── vite.config.js
|
||||
└── package.json</code></pre>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup & Configuration
|
||||
|
||||
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sigproRouter } from 'sigpro/router';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sigproRouter()]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { mount } from 'sigpro';
|
||||
import { router } from 'sigpro/router';
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
// The Core already has Router ready
|
||||
mount(router(routes), '#app');
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/App.js
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
export default () => div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => Router.go('/') }, "Home"),
|
||||
button({ onclick: () => Router.go('/blog') }, "Blog")
|
||||
])
|
||||
]),
|
||||
// Only the content inside <main> will be swapped reactively
|
||||
main(Router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Mapping Reference
|
||||
|
||||
The plugin follows a simple convention to transform your file system into a routing map.
|
||||
|
||||
<div class="overflow-x-auto my-8">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>File Path</th>
|
||||
<th>Generated Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/</td>
|
||||
<td>The application root.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>about.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/about</td>
|
||||
<td>A static page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[id].js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/:id</td>
|
||||
<td>Dynamic parameter (passed to the component).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blog/index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/blog</td>
|
||||
<td>Folder index page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>_utils.js</code></td>
|
||||
<td class="italic opacity-50 text-error">Ignored</td>
|
||||
<td>Files starting with <code>_</code> are excluded from routing.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. How it Works (Vite Virtual Module)
|
||||
|
||||
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
|
||||
|
||||
```javascript
|
||||
// Internal representation generated by the plugin
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('/src/pages/index.js') },
|
||||
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.
|
||||
447
docs/sigpro.js
447
docs/sigpro.js
@@ -1,4 +1,42 @@
|
||||
(() => {
|
||||
var __create = Object.create;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toESMCache_node;
|
||||
var __toESMCache_esm;
|
||||
var __toESM = (mod, isNodeMode, target) => {
|
||||
var canCache = mod != null && typeof mod === "object";
|
||||
if (canCache) {
|
||||
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
||||
var cached = cache.get(mod);
|
||||
if (cached)
|
||||
return cached;
|
||||
}
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
};
|
||||
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
||||
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
||||
}) : x)(function(x) {
|
||||
if (typeof require !== "undefined")
|
||||
return require.apply(this, arguments);
|
||||
throw Error('Dynamic require of "' + x + '" is not supported');
|
||||
});
|
||||
|
||||
// src/sigpro.js
|
||||
var isFunc = (f) => typeof f === "function";
|
||||
var isObj = (o) => o && typeof o === "object";
|
||||
@@ -10,8 +48,6 @@
|
||||
var isFlushing = false;
|
||||
var batchDepth = 0;
|
||||
var effectQueue = new Set;
|
||||
var proxyCache = new WeakMap;
|
||||
var ITER = Symbol("iter");
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
@@ -172,52 +208,6 @@
|
||||
return val;
|
||||
};
|
||||
};
|
||||
var $$ = (target) => {
|
||||
if (!isObj(target))
|
||||
return target;
|
||||
const cached = proxyCache.get(target);
|
||||
if (cached)
|
||||
return cached;
|
||||
const subs = new Map;
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key);
|
||||
if (!set)
|
||||
subs.set(key, set = new Set);
|
||||
return set;
|
||||
};
|
||||
const proxy = new Proxy(target, {
|
||||
get(target2, key, receiver) {
|
||||
if (typeof key !== "symbol")
|
||||
trackUpdate(getSubs(key));
|
||||
return $$(Reflect.get(target2, key, receiver));
|
||||
},
|
||||
set(target2, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target2, key);
|
||||
const oldValue = Reflect.get(target2, key, receiver);
|
||||
const result = Reflect.set(target2, key, value, receiver);
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
if (!hadKey)
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deleteProperty(target2, key) {
|
||||
const result = Reflect.deleteProperty(target2, key);
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
ownKeys(target2) {
|
||||
trackUpdate(getSubs(ITER));
|
||||
return Reflect.ownKeys(target2);
|
||||
}
|
||||
});
|
||||
proxyCache.set(target, proxy);
|
||||
return proxy;
|
||||
};
|
||||
var watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect2 = createEffect(sources);
|
||||
@@ -467,6 +457,348 @@
|
||||
});
|
||||
return root;
|
||||
};
|
||||
var Fragment = (props) => props.children;
|
||||
var 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;
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
}
|
||||
|
||||
// src/router.js
|
||||
var import_node_fs = (() => ({}));
|
||||
|
||||
// node:path
|
||||
function assertPath(path) {
|
||||
if (typeof path !== "string")
|
||||
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
||||
}
|
||||
function normalizeStringPosix(path, allowAboveRoot) {
|
||||
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
||||
for (var i = 0;i <= path.length; ++i) {
|
||||
if (i < path.length)
|
||||
code = path.charCodeAt(i);
|
||||
else if (code === 47)
|
||||
break;
|
||||
else
|
||||
code = 47;
|
||||
if (code === 47) {
|
||||
if (lastSlash === i - 1 || dots === 1)
|
||||
;
|
||||
else if (lastSlash !== i - 1 && dots === 2) {
|
||||
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
||||
if (res.length > 2) {
|
||||
var lastSlashIndex = res.lastIndexOf("/");
|
||||
if (lastSlashIndex !== res.length - 1) {
|
||||
if (lastSlashIndex === -1)
|
||||
res = "", lastSegmentLength = 0;
|
||||
else
|
||||
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
||||
lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
} else if (res.length === 2 || res.length === 1) {
|
||||
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
if (res.length > 0)
|
||||
res += "/..";
|
||||
else
|
||||
res = "..";
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0)
|
||||
res += "/" + path.slice(lastSlash + 1, i);
|
||||
else
|
||||
res = path.slice(lastSlash + 1, i);
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i, dots = 0;
|
||||
} else if (code === 46 && dots !== -1)
|
||||
++dots;
|
||||
else
|
||||
dots = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function _format(sep, pathObject) {
|
||||
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
||||
if (!dir)
|
||||
return base;
|
||||
if (dir === pathObject.root)
|
||||
return dir + base;
|
||||
return dir + sep + base;
|
||||
}
|
||||
function resolve() {
|
||||
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
||||
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
||||
var path;
|
||||
if (i >= 0)
|
||||
path = arguments[i];
|
||||
else {
|
||||
if (cwd === undefined)
|
||||
cwd = process.cwd();
|
||||
path = cwd;
|
||||
}
|
||||
if (assertPath(path), path.length === 0)
|
||||
continue;
|
||||
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
||||
}
|
||||
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
||||
if (resolvedPath.length > 0)
|
||||
return "/" + resolvedPath;
|
||||
else
|
||||
return "/";
|
||||
else if (resolvedPath.length > 0)
|
||||
return resolvedPath;
|
||||
else
|
||||
return ".";
|
||||
}
|
||||
function normalize(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
||||
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
||||
path = ".";
|
||||
if (path.length > 0 && trailingSeparator)
|
||||
path += "/";
|
||||
if (isAbsolute)
|
||||
return "/" + path;
|
||||
return path;
|
||||
}
|
||||
function isAbsolute(path) {
|
||||
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
||||
}
|
||||
function join() {
|
||||
if (arguments.length === 0)
|
||||
return ".";
|
||||
var joined;
|
||||
for (var i = 0;i < arguments.length; ++i) {
|
||||
var arg = arguments[i];
|
||||
if (assertPath(arg), arg.length > 0)
|
||||
if (joined === undefined)
|
||||
joined = arg;
|
||||
else
|
||||
joined += "/" + arg;
|
||||
}
|
||||
if (joined === undefined)
|
||||
return ".";
|
||||
return normalize(joined);
|
||||
}
|
||||
function relative(from, to) {
|
||||
if (assertPath(from), assertPath(to), from === to)
|
||||
return "";
|
||||
if (from = resolve(from), to = resolve(to), from === to)
|
||||
return "";
|
||||
var fromStart = 1;
|
||||
for (;fromStart < from.length; ++fromStart)
|
||||
if (from.charCodeAt(fromStart) !== 47)
|
||||
break;
|
||||
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
||||
for (;toStart < to.length; ++toStart)
|
||||
if (to.charCodeAt(toStart) !== 47)
|
||||
break;
|
||||
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
||||
for (;i <= length; ++i) {
|
||||
if (i === length) {
|
||||
if (toLen > length) {
|
||||
if (to.charCodeAt(toStart + i) === 47)
|
||||
return to.slice(toStart + i + 1);
|
||||
else if (i === 0)
|
||||
return to.slice(toStart + i);
|
||||
} else if (fromLen > length) {
|
||||
if (from.charCodeAt(fromStart + i) === 47)
|
||||
lastCommonSep = i;
|
||||
else if (i === 0)
|
||||
lastCommonSep = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
||||
if (fromCode !== toCode)
|
||||
break;
|
||||
else if (fromCode === 47)
|
||||
lastCommonSep = i;
|
||||
}
|
||||
var out = "";
|
||||
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
||||
if (i === fromEnd || from.charCodeAt(i) === 47)
|
||||
if (out.length === 0)
|
||||
out += "..";
|
||||
else
|
||||
out += "/..";
|
||||
if (out.length > 0)
|
||||
return out + to.slice(toStart + lastCommonSep);
|
||||
else {
|
||||
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
||||
++toStart;
|
||||
return to.slice(toStart);
|
||||
}
|
||||
}
|
||||
function _makeLong(path) {
|
||||
return path;
|
||||
}
|
||||
function dirname(path) {
|
||||
if (assertPath(path), path.length === 0)
|
||||
return ".";
|
||||
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
||||
for (var i = path.length - 1;i >= 1; --i)
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else
|
||||
matchedSlash = false;
|
||||
if (end === -1)
|
||||
return hasRoot ? "/" : ".";
|
||||
if (hasRoot && end === 1)
|
||||
return "//";
|
||||
return path.slice(0, end);
|
||||
}
|
||||
function basename(path, ext) {
|
||||
if (ext !== undefined && typeof ext !== "string")
|
||||
throw TypeError('"ext" argument must be a string');
|
||||
assertPath(path);
|
||||
var start = 0, end = -1, matchedSlash = true, i;
|
||||
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
||||
if (ext.length === path.length && ext === path)
|
||||
return "";
|
||||
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
||||
for (i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (firstNonSlashEnd === -1)
|
||||
matchedSlash = false, firstNonSlashEnd = i + 1;
|
||||
if (extIdx >= 0)
|
||||
if (code === ext.charCodeAt(extIdx)) {
|
||||
if (--extIdx === -1)
|
||||
end = i;
|
||||
} else
|
||||
extIdx = -1, end = firstNonSlashEnd;
|
||||
}
|
||||
}
|
||||
if (start === end)
|
||||
end = firstNonSlashEnd;
|
||||
else if (end === -1)
|
||||
end = path.length;
|
||||
return path.slice(start, end);
|
||||
} else {
|
||||
for (i = path.length - 1;i >= 0; --i)
|
||||
if (path.charCodeAt(i) === 47) {
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (end === -1)
|
||||
return "";
|
||||
return path.slice(start, end);
|
||||
}
|
||||
}
|
||||
function extname(path) {
|
||||
assertPath(path);
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
||||
for (var i = path.length - 1;i >= 0; --i) {
|
||||
var code = path.charCodeAt(i);
|
||||
if (code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
return "";
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
function format(pathObject) {
|
||||
if (pathObject === null || typeof pathObject !== "object")
|
||||
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
||||
return _format("/", pathObject);
|
||||
}
|
||||
function parse(path) {
|
||||
assertPath(path);
|
||||
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
||||
if (path.length === 0)
|
||||
return ret;
|
||||
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
||||
if (isAbsolute2)
|
||||
ret.root = "/", start = 1;
|
||||
else
|
||||
start = 0;
|
||||
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
||||
for (;i >= start; --i) {
|
||||
if (code = path.charCodeAt(i), code === 47) {
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1)
|
||||
matchedSlash = false, end = i + 1;
|
||||
if (code === 46) {
|
||||
if (startDot === -1)
|
||||
startDot = i;
|
||||
else if (preDotState !== 1)
|
||||
preDotState = 1;
|
||||
} else if (startDot !== -1)
|
||||
preDotState = -1;
|
||||
}
|
||||
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
||||
if (end !== -1)
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.base = ret.name = path.slice(1, end);
|
||||
else
|
||||
ret.base = ret.name = path.slice(startPart, end);
|
||||
} else {
|
||||
if (startPart === 0 && isAbsolute2)
|
||||
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
||||
else
|
||||
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
||||
ret.ext = path.slice(startDot, end);
|
||||
}
|
||||
if (startPart > 0)
|
||||
ret.dir = path.slice(0, startPart - 1);
|
||||
else if (isAbsolute2)
|
||||
ret.dir = "/";
|
||||
return ret;
|
||||
}
|
||||
var sep = "/";
|
||||
var delimiter = ":";
|
||||
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
||||
|
||||
// src/router.js
|
||||
var router = (routes) => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
@@ -500,26 +832,9 @@
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var Fragment = (props) => props.children;
|
||||
var 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;
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
}
|
||||
|
||||
// src/build_umd.js
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, { $, $$, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
|
||||
Object.assign(window, { $, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Vite Plugin: File-based Routing
|
||||
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
|
||||
## 1. Project Structure
|
||||
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
|
||||
<pre><code>my-sigpro-app/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── index.js → #/
|
||||
│ │ ├── about.js → #/about
|
||||
│ │ ├── users/
|
||||
│ │ │ └── [id].js → #/users/:id
|
||||
│ │ └── blog/
|
||||
│ │ ├── index.js → #/blog
|
||||
│ │ └── [slug].js → #/blog/:slug
|
||||
│ ├── App.js (Main Layout)
|
||||
│ └── main.js (Entry Point)
|
||||
├── vite.config.js
|
||||
└── package.json</code></pre>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup & Configuration
|
||||
|
||||
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sigproRouter } from 'sigpro/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sigproRouter()]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { mount, router } from 'sigpro';
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
// The Core already has Router ready
|
||||
mount(router(routes), '#app');
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// src/App.js
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
export default () => div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => Router.go('/') }, "Home"),
|
||||
button({ onclick: () => Router.go('/blog') }, "Blog")
|
||||
])
|
||||
]),
|
||||
// Only the content inside <main> will be swapped reactively
|
||||
main(Router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Mapping Reference
|
||||
|
||||
The plugin follows a simple convention to transform your file system into a routing map.
|
||||
|
||||
<div class="overflow-x-auto my-8">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>File Path</th>
|
||||
<th>Generated Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/</td>
|
||||
<td>The application root.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>about.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/about</td>
|
||||
<td>A static page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[id].js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/:id</td>
|
||||
<td>Dynamic parameter (passed to the component).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blog/index.js</code></td>
|
||||
<td class="font-mono text-primary font-bold">/blog</td>
|
||||
<td>Folder index page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>_utils.js</code></td>
|
||||
<td class="italic opacity-50 text-error">Ignored</td>
|
||||
<td>Files starting with <code>_</code> are excluded from routing.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. How it Works (Vite Virtual Module)
|
||||
|
||||
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
|
||||
|
||||
```javascript
|
||||
// Internal representation generated by the plugin
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('/src/pages/index.js') },
|
||||
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.
|
||||
@@ -19,8 +19,8 @@
|
||||
"script": "./dist/sigpro.min.js",
|
||||
"types": "./sigpro.d.ts"
|
||||
},
|
||||
"./vite": {
|
||||
"import": "./dist/vite.js"
|
||||
"./router": {
|
||||
"import": "./dist/router.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -46,8 +46,9 @@
|
||||
"build:iife:min": "bun build ./src/build_umd.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=sp --minify",
|
||||
"build:esm": "bun build ./src/sigpro.js --bundle --outfile=./dist/sigpro.esm.js --format=esm",
|
||||
"build:esm:min": "bun build ./src/sigpro.js --bundle --outfile=./dist/sigpro.esm.min.js --format=esm --minify",
|
||||
"build:copy": "cp ./src/vite.js ./dist/vite.js && cp ./dist/sigpro.js ./docs/sigpro.js",
|
||||
"build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:copy",
|
||||
"build:router": "bun build ./src/router.js --bundle --outfile=./dist/router.js --format=esm --external:fs --external:path",
|
||||
"build:copy": "cp ./dist/sigpro.js ./docs/sigpro.js",
|
||||
"build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:router && bun run build:copy",
|
||||
"docs": "bun x serve docs"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj } from "./sigpro.js"
|
||||
import { $, watch, batch, h, Fragment, mount, when, each, onUnmount, isArr, isFunc, isObj } from "./sigpro.js"
|
||||
import { router } from "./router.js"
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, { $, $$, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj })
|
||||
Object.assign(window, { $, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj })
|
||||
}
|
||||
101
src/router.js
Normal file
101
src/router.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// src/router.js
|
||||
import { h, watch, $, render, onUnmount, isFunc } from './sigpro.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const router = routes => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
const handler = () => path(getHash());
|
||||
window.addEventListener("hashchange", handler);
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||
|
||||
const hook = h("div", { class: "router-hook" });
|
||||
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);
|
||||
hook.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
return hook;
|
||||
};
|
||||
|
||||
router.params = $({});
|
||||
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
|
||||
function sigproRouter() {
|
||||
const virtualModuleId = 'virtual:sigpro-routes';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
const getFiles = (dir) => {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { recursive: true })
|
||||
.filter(file => /\.(js|jsx)$/.test(file) && !path.basename(file).startsWith('_'))
|
||||
.map(file => path.resolve(dir, file));
|
||||
};
|
||||
|
||||
const pathToUrl = (pagesDir, filePath) => {
|
||||
let relative = path.relative(pagesDir, filePath)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.(js|jsx)$/, '')
|
||||
.replace(/\/index$/, '')
|
||||
.replace(/^index$/, '');
|
||||
return ('/' + relative)
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '*')
|
||||
.replace(/\[([^\]]+)\]/g, ':$1')
|
||||
.replace(/\/$/, '') || '/';
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'sigpro-router',
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id !== resolvedVirtualModuleId) return;
|
||||
const root = process.cwd();
|
||||
const pagesDir = path.resolve(root, 'src/pages');
|
||||
const files = getFiles(pagesDir).sort((a, b) => {
|
||||
const urlA = pathToUrl(pagesDir, a);
|
||||
const urlB = pathToUrl(pagesDir, b);
|
||||
if (urlA.includes(':') && !urlB.includes(':')) return 1;
|
||||
if (!urlA.includes(':') && urlB.includes(':')) return -1;
|
||||
return urlB.length - urlA.length;
|
||||
});
|
||||
|
||||
let routeEntries = '';
|
||||
files.forEach((fullPath) => {
|
||||
const urlPath = pathToUrl(pagesDir, fullPath);
|
||||
const relativeImport = './' + path.relative(root, fullPath).replace(/\\/g, '/');
|
||||
routeEntries += ` { path: '${urlPath}', component: () => import('/${relativeImport}') },\n`;
|
||||
});
|
||||
if (!routeEntries.includes("path: '*'")) {
|
||||
routeEntries += ` { path: '*', component: () => ({ default: () => document.createTextNode('404 - Not Found') }) },\n`;
|
||||
}
|
||||
return `export const routes = [\n${routeEntries}];`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Exportación final (lo que se consume desde 'sigpro/router')
|
||||
// ============================================================
|
||||
export { router, sigproRouter };
|
||||
@@ -9,8 +9,6 @@ let activeOwner = null
|
||||
let isFlushing = false
|
||||
let batchDepth = 0
|
||||
const effectQueue = new Set()
|
||||
const proxyCache = new WeakMap()
|
||||
const ITER = Symbol('iter')
|
||||
const MOUNTED_NODES = new WeakMap()
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg"
|
||||
@@ -161,51 +159,6 @@ const $ = (val, key = null) => {
|
||||
}
|
||||
}
|
||||
|
||||
const $$ = (target) => {
|
||||
if (!isObj(target)) return target
|
||||
const cached = proxyCache.get(target)
|
||||
if (cached) return cached
|
||||
|
||||
const subs = new Map()
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key)
|
||||
if (!set) subs.set(key, set = new Set())
|
||||
return set
|
||||
}
|
||||
|
||||
const proxy = new Proxy(target, {
|
||||
get(target, key, receiver) {
|
||||
if (typeof key !== 'symbol') trackUpdate(getSubs(key))
|
||||
return $$(Reflect.get(target, key, receiver))
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target, key)
|
||||
const oldValue = Reflect.get(target, key, receiver)
|
||||
const result = Reflect.set(target, key, value, receiver)
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true)
|
||||
if (!hadKey) trackUpdate(getSubs(ITER), true)
|
||||
}
|
||||
return result
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const result = Reflect.deleteProperty(target, key)
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true)
|
||||
trackUpdate(getSubs(ITER), true)
|
||||
}
|
||||
return result
|
||||
},
|
||||
ownKeys(target) {
|
||||
trackUpdate(getSubs(ITER))
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
})
|
||||
|
||||
proxyCache.set(target, proxy)
|
||||
return proxy
|
||||
}
|
||||
|
||||
const watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect = createEffect(sources)
|
||||
@@ -459,39 +412,6 @@ const each = (src, itemFn, keyField) => {
|
||||
return root
|
||||
}
|
||||
|
||||
const router = routes => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/"
|
||||
const path = $(getHash())
|
||||
const handler = () => path(getHash())
|
||||
window.addEventListener("hashchange", handler)
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler))
|
||||
const hook = h("div", { class: "router-hook" })
|
||||
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)
|
||||
hook.replaceChildren(currentView.container)
|
||||
}
|
||||
})
|
||||
return hook
|
||||
}
|
||||
router.params = $({})
|
||||
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
|
||||
router.back = () => window.history.back()
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/"
|
||||
|
||||
const Fragment = (props) => props.children;
|
||||
|
||||
const mount = (comp, target) => {
|
||||
@@ -510,4 +430,4 @@ if (typeof window !== "undefined") {
|
||||
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
|
||||
}
|
||||
|
||||
export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj }
|
||||
export { $, watch, batch, h, Fragment, render, mount, when, each, onUnmount, isArr, isFunc, isObj }
|
||||
1
src/sigpro.min.js
vendored
Normal file
1
src/sigpro.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user