sigpro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Readme.md +1544 -0
- package/SigProRouterPlugin/Readme.md +139 -0
- package/SigProRouterPlugin/vite-plugin-sigpro.js +141 -0
- package/index.js +8 -0
- package/package.json +11 -0
- package/sigpro-1.0.0.tgz +0 -0
- package/sigpro.js +474 -0
package/sigpro.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// Global state for tracking the current reactive effect
|
|
2
|
+
let activeEffect = null;
|
|
3
|
+
|
|
4
|
+
// Queue for batched effect updates
|
|
5
|
+
const effectQueue = new Set();
|
|
6
|
+
let isFlushScheduled = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Flushes all pending effects in the queue
|
|
10
|
+
* Executes all queued jobs and clears the queue
|
|
11
|
+
*/
|
|
12
|
+
const flushEffectQueue = () => {
|
|
13
|
+
isFlushScheduled = false;
|
|
14
|
+
try {
|
|
15
|
+
for (const effect of effectQueue) {
|
|
16
|
+
effect.run();
|
|
17
|
+
}
|
|
18
|
+
effectQueue.clear();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("SigPro Flush Error:", error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a reactive signal
|
|
26
|
+
* @param {any} initialValue - Initial value or getter function
|
|
27
|
+
* @returns {Function} Signal getter/setter function
|
|
28
|
+
*/
|
|
29
|
+
export const $ = (initialValue) => {
|
|
30
|
+
const subscribers = new Set();
|
|
31
|
+
|
|
32
|
+
if (typeof initialValue === "function") {
|
|
33
|
+
// Computed signal case
|
|
34
|
+
let isDirty = true;
|
|
35
|
+
let cachedValue;
|
|
36
|
+
|
|
37
|
+
const computedEffect = {
|
|
38
|
+
dependencies: new Set(),
|
|
39
|
+
cleanupHandlers: new Set(),
|
|
40
|
+
markDirty: () => {
|
|
41
|
+
if (!isDirty) {
|
|
42
|
+
isDirty = true;
|
|
43
|
+
subscribers.forEach((subscriber) => {
|
|
44
|
+
if (subscriber.markDirty) subscriber.markDirty();
|
|
45
|
+
effectQueue.add(subscriber);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
run: () => {
|
|
50
|
+
// Clear old dependencies
|
|
51
|
+
computedEffect.dependencies.forEach((dependencySet) => dependencySet.delete(computedEffect));
|
|
52
|
+
computedEffect.dependencies.clear();
|
|
53
|
+
|
|
54
|
+
const previousEffect = activeEffect;
|
|
55
|
+
activeEffect = computedEffect;
|
|
56
|
+
try {
|
|
57
|
+
cachedValue = initialValue();
|
|
58
|
+
} finally {
|
|
59
|
+
activeEffect = previousEffect;
|
|
60
|
+
isDirty = false;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
if (activeEffect) {
|
|
67
|
+
subscribers.add(activeEffect);
|
|
68
|
+
activeEffect.dependencies.add(subscribers);
|
|
69
|
+
}
|
|
70
|
+
if (isDirty) computedEffect.run();
|
|
71
|
+
return cachedValue;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Regular signal case
|
|
76
|
+
return (...args) => {
|
|
77
|
+
if (args.length) {
|
|
78
|
+
const nextValue = typeof args[0] === "function" ? args[0](initialValue) : args[0];
|
|
79
|
+
if (!Object.is(initialValue, nextValue)) {
|
|
80
|
+
initialValue = nextValue;
|
|
81
|
+
subscribers.forEach((subscriber) => {
|
|
82
|
+
if (subscriber.markDirty) subscriber.markDirty();
|
|
83
|
+
effectQueue.add(subscriber);
|
|
84
|
+
});
|
|
85
|
+
if (!isFlushScheduled && effectQueue.size) {
|
|
86
|
+
isFlushScheduled = true;
|
|
87
|
+
queueMicrotask(flushEffectQueue);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (activeEffect) {
|
|
92
|
+
subscribers.add(activeEffect);
|
|
93
|
+
activeEffect.dependencies.add(subscribers);
|
|
94
|
+
}
|
|
95
|
+
return initialValue;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a reactive effect that runs when dependencies change
|
|
101
|
+
* @param {Function} effectFn - The effect function to run
|
|
102
|
+
* @returns {Function} Cleanup function to stop the effect
|
|
103
|
+
*/
|
|
104
|
+
export const $$ = (effectFn) => {
|
|
105
|
+
const effect = {
|
|
106
|
+
dependencies: new Set(),
|
|
107
|
+
cleanupHandlers: new Set(),
|
|
108
|
+
run() {
|
|
109
|
+
// Run cleanup handlers
|
|
110
|
+
this.cleanupHandlers.forEach((handler) => handler());
|
|
111
|
+
this.cleanupHandlers.clear();
|
|
112
|
+
|
|
113
|
+
// Clear old dependencies
|
|
114
|
+
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
|
|
115
|
+
this.dependencies.clear();
|
|
116
|
+
|
|
117
|
+
const previousEffect = activeEffect;
|
|
118
|
+
activeEffect = this;
|
|
119
|
+
try {
|
|
120
|
+
const result = effectFn();
|
|
121
|
+
if (typeof result === "function") this.cleanupFunction = result;
|
|
122
|
+
} finally {
|
|
123
|
+
activeEffect = previousEffect;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
stop() {
|
|
127
|
+
this.cleanupHandlers.forEach((handler) => handler());
|
|
128
|
+
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
|
|
129
|
+
this.cleanupFunction?.();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());
|
|
134
|
+
effect.run();
|
|
135
|
+
return () => effect.stop();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tagged template literal for creating reactive HTML
|
|
140
|
+
* @param {string[]} strings - Template strings
|
|
141
|
+
* @param {...any} values - Dynamic values
|
|
142
|
+
* @returns {DocumentFragment} Reactive document fragment
|
|
143
|
+
*/
|
|
144
|
+
export const html = (strings, ...values) => {
|
|
145
|
+
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets a node by path from root
|
|
149
|
+
* @param {Node} root - Root node
|
|
150
|
+
* @param {number[]} path - Path indices
|
|
151
|
+
* @returns {Node} Target node
|
|
152
|
+
*/
|
|
153
|
+
const getNodeByPath = (root, path) =>
|
|
154
|
+
path.reduce((node, index) => node?.childNodes?.[index], root);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Applies reactive text content to a node
|
|
158
|
+
* @param {Node} node - Target node
|
|
159
|
+
* @param {any[]} values - Values to insert
|
|
160
|
+
*/
|
|
161
|
+
const applyTextContent = (node, values) => {
|
|
162
|
+
const parts = node.textContent.split("{{part}}");
|
|
163
|
+
const parent = node.parentNode;
|
|
164
|
+
let valueIndex = 0;
|
|
165
|
+
|
|
166
|
+
parts.forEach((part, index) => {
|
|
167
|
+
if (part) parent.insertBefore(document.createTextNode(part), node);
|
|
168
|
+
if (index < parts.length - 1) {
|
|
169
|
+
const currentValue = values[valueIndex++];
|
|
170
|
+
const startMarker = document.createComment("s");
|
|
171
|
+
const endMarker = document.createComment("e");
|
|
172
|
+
parent.insertBefore(startMarker, node);
|
|
173
|
+
parent.insertBefore(endMarker, node);
|
|
174
|
+
|
|
175
|
+
let lastResult;
|
|
176
|
+
$$(() => {
|
|
177
|
+
let result = typeof currentValue === "function" ? currentValue() : currentValue;
|
|
178
|
+
if (result === lastResult) return;
|
|
179
|
+
lastResult = result;
|
|
180
|
+
|
|
181
|
+
if (typeof result !== "object" && !Array.isArray(result)) {
|
|
182
|
+
const textNode = startMarker.nextSibling;
|
|
183
|
+
if (textNode !== endMarker && textNode?.nodeType === 3) {
|
|
184
|
+
textNode.textContent = result ?? "";
|
|
185
|
+
} else {
|
|
186
|
+
while (startMarker.nextSibling !== endMarker)
|
|
187
|
+
parent.removeChild(startMarker.nextSibling);
|
|
188
|
+
parent.insertBefore(document.createTextNode(result ?? ""), endMarker);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle arrays or objects
|
|
194
|
+
while (startMarker.nextSibling !== endMarker)
|
|
195
|
+
parent.removeChild(startMarker.nextSibling);
|
|
196
|
+
|
|
197
|
+
const items = Array.isArray(result) ? result : [result];
|
|
198
|
+
const fragment = document.createDocumentFragment();
|
|
199
|
+
items.forEach(item => {
|
|
200
|
+
if (item == null || item === false) return;
|
|
201
|
+
const nodeItem = item instanceof Node ? item : document.createTextNode(item);
|
|
202
|
+
fragment.appendChild(nodeItem);
|
|
203
|
+
});
|
|
204
|
+
parent.insertBefore(fragment, endMarker);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
node.remove();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Get or create template from cache
|
|
212
|
+
let cachedTemplate = templateCache.get(strings);
|
|
213
|
+
if (!cachedTemplate) {
|
|
214
|
+
const template = document.createElement("template");
|
|
215
|
+
template.innerHTML = strings.join("{{part}}");
|
|
216
|
+
|
|
217
|
+
const dynamicNodes = [];
|
|
218
|
+
const treeWalker = document.createTreeWalker(template.content, 133); // NodeFilter.SHOW_ALL
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Gets path indices for a node
|
|
222
|
+
* @param {Node} node - Target node
|
|
223
|
+
* @returns {number[]} Path indices
|
|
224
|
+
*/
|
|
225
|
+
const getNodePath = (node) => {
|
|
226
|
+
const path = [];
|
|
227
|
+
while (node && node !== template.content) {
|
|
228
|
+
let index = 0;
|
|
229
|
+
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling)
|
|
230
|
+
index++;
|
|
231
|
+
path.push(index);
|
|
232
|
+
node = node.parentNode;
|
|
233
|
+
}
|
|
234
|
+
return path.reverse();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let currentNode;
|
|
238
|
+
while ((currentNode = treeWalker.nextNode())) {
|
|
239
|
+
let isDynamic = false;
|
|
240
|
+
const nodeInfo = {
|
|
241
|
+
type: currentNode.nodeType,
|
|
242
|
+
path: getNodePath(currentNode),
|
|
243
|
+
parts: []
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (currentNode.nodeType === 1) { // Element node
|
|
247
|
+
for (let i = 0; i < currentNode.attributes.length; i++) {
|
|
248
|
+
const attribute = currentNode.attributes[i];
|
|
249
|
+
if (attribute.value.includes("{{part}}")) {
|
|
250
|
+
nodeInfo.parts.push({ name: attribute.name });
|
|
251
|
+
isDynamic = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
|
|
255
|
+
// Text node
|
|
256
|
+
isDynamic = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isDynamic) dynamicNodes.push(nodeInfo);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
templateCache.set(strings, (cachedTemplate = { template, dynamicNodes }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const fragment = cachedTemplate.template.content.cloneNode(true);
|
|
266
|
+
let valueIndex = 0;
|
|
267
|
+
|
|
268
|
+
// Get target nodes before applyTextContent modifies the DOM
|
|
269
|
+
const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
|
|
270
|
+
node: getNodeByPath(fragment, nodeInfo.path),
|
|
271
|
+
info: nodeInfo
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
targets.forEach(({ node, info }) => {
|
|
275
|
+
if (!node) return;
|
|
276
|
+
|
|
277
|
+
if (info.type === 1) { // Element node
|
|
278
|
+
info.parts.forEach((part) => {
|
|
279
|
+
const currentValue = values[valueIndex++];
|
|
280
|
+
const attributeName = part.name;
|
|
281
|
+
const firstChar = attributeName[0];
|
|
282
|
+
|
|
283
|
+
if (firstChar === "@") {
|
|
284
|
+
// Event listener
|
|
285
|
+
node.addEventListener(attributeName.slice(1), currentValue);
|
|
286
|
+
} else if (firstChar === ":") {
|
|
287
|
+
// Two-way binding
|
|
288
|
+
const propertyName = attributeName.slice(1);
|
|
289
|
+
const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";
|
|
290
|
+
|
|
291
|
+
$$(() => {
|
|
292
|
+
const value = typeof currentValue === "function" ? currentValue() : currentValue;
|
|
293
|
+
if (node[propertyName] !== value) node[propertyName] = value;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
node.addEventListener(eventType, () => {
|
|
297
|
+
const value = eventType === "change" ? node.checked : node.value;
|
|
298
|
+
if (typeof currentValue === "function") currentValue(value);
|
|
299
|
+
});
|
|
300
|
+
} else if (firstChar === "?") {
|
|
301
|
+
// Boolean attribute
|
|
302
|
+
const attrName = attributeName.slice(1);
|
|
303
|
+
$$(() => {
|
|
304
|
+
const result = typeof currentValue === "function" ? currentValue() : currentValue;
|
|
305
|
+
node.toggleAttribute(attrName, !!result);
|
|
306
|
+
});
|
|
307
|
+
} else if (firstChar === ".") {
|
|
308
|
+
// Property binding
|
|
309
|
+
const propertyName = attributeName.slice(1);
|
|
310
|
+
$$(() => {
|
|
311
|
+
let result = typeof currentValue === "function" ? currentValue() : currentValue;
|
|
312
|
+
node[propertyName] = result;
|
|
313
|
+
if (result != null && typeof result !== "object" && typeof result !== "boolean") {
|
|
314
|
+
node.setAttribute(propertyName, result);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
// Regular attribute
|
|
319
|
+
if (typeof currentValue === "function") {
|
|
320
|
+
$$(() => node.setAttribute(attributeName, currentValue()));
|
|
321
|
+
} else {
|
|
322
|
+
node.setAttribute(attributeName, currentValue);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
} else if (info.type === 3) { // Text node
|
|
327
|
+
const placeholderCount = node.textContent.split("{{part}}").length - 1;
|
|
328
|
+
applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
|
|
329
|
+
valueIndex += placeholderCount;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return fragment;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Creates a custom web component with reactive properties
|
|
338
|
+
* @param {string} tagName - Custom element tag name
|
|
339
|
+
* @param {Function} setupFunction - Component setup function
|
|
340
|
+
* @param {string[]} observedAttributes - Array of observed attributes
|
|
341
|
+
*/
|
|
342
|
+
export const $component = (tagName, setupFunction, observedAttributes = []) => {
|
|
343
|
+
if (customElements.get(tagName)) return;
|
|
344
|
+
|
|
345
|
+
customElements.define(
|
|
346
|
+
tagName,
|
|
347
|
+
class extends HTMLElement {
|
|
348
|
+
static get observedAttributes() {
|
|
349
|
+
return observedAttributes;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
constructor() {
|
|
353
|
+
super();
|
|
354
|
+
this._propertySignals = {};
|
|
355
|
+
this.cleanupFunctions = [];
|
|
356
|
+
observedAttributes.forEach((attr) => (this._propertySignals[attr] = $(undefined)));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
connectedCallback() {
|
|
360
|
+
const frozenChildren = [...this.childNodes];
|
|
361
|
+
this.innerHTML = "";
|
|
362
|
+
|
|
363
|
+
observedAttributes.forEach((attr) => {
|
|
364
|
+
const initialValue = this.hasOwnProperty(attr) ? this[attr] : this.getAttribute(attr);
|
|
365
|
+
|
|
366
|
+
Object.defineProperty(this, attr, {
|
|
367
|
+
get: () => this._propertySignals[attr](),
|
|
368
|
+
set: (value) => {
|
|
369
|
+
const processedValue = value === "false" ? false : value === "" && attr !== "value" ? true : value;
|
|
370
|
+
this._propertySignals[attr](processedValue);
|
|
371
|
+
},
|
|
372
|
+
configurable: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (initialValue !== null && initialValue !== undefined) this[attr] = initialValue;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const context = {
|
|
379
|
+
select: (selector) => this.querySelector(selector),
|
|
380
|
+
slot: (name) =>
|
|
381
|
+
frozenChildren.filter((node) => {
|
|
382
|
+
const slotName = node.nodeType === 1 ? node.getAttribute("slot") : null;
|
|
383
|
+
return name ? slotName === name : !slotName;
|
|
384
|
+
}),
|
|
385
|
+
emit: (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })),
|
|
386
|
+
host: this,
|
|
387
|
+
onUnmount: (cleanupFn) => this.cleanupFunctions.push(cleanupFn),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const result = setupFunction(this._propertySignals, context);
|
|
391
|
+
if (result instanceof Node) this.appendChild(result);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
395
|
+
if (this[name] !== newValue) this[name] = newValue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
disconnectedCallback() {
|
|
399
|
+
this.cleanupFunctions.forEach((cleanupFn) => cleanupFn());
|
|
400
|
+
this.cleanupFunctions = [];
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Creates a router for hash-based navigation
|
|
408
|
+
* @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
|
|
409
|
+
* @returns {HTMLDivElement} Router container element
|
|
410
|
+
*/
|
|
411
|
+
export const $router = (routes) => {
|
|
412
|
+
/**
|
|
413
|
+
* Gets current path from hash
|
|
414
|
+
* @returns {string} Current path
|
|
415
|
+
*/
|
|
416
|
+
const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
|
|
417
|
+
|
|
418
|
+
const currentPath = $(getCurrentPath());
|
|
419
|
+
const container = document.createElement("div");
|
|
420
|
+
container.style.display = "contents";
|
|
421
|
+
|
|
422
|
+
window.addEventListener("hashchange", () => {
|
|
423
|
+
const nextPath = getCurrentPath();
|
|
424
|
+
if (currentPath() !== nextPath) currentPath(nextPath);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
$$(() => {
|
|
428
|
+
const path = currentPath();
|
|
429
|
+
let matchedRoute = null;
|
|
430
|
+
let routeParams = {};
|
|
431
|
+
|
|
432
|
+
for (const route of routes) {
|
|
433
|
+
if (route.path instanceof RegExp) {
|
|
434
|
+
const match = path.match(route.path);
|
|
435
|
+
if (match) {
|
|
436
|
+
matchedRoute = route;
|
|
437
|
+
routeParams = match.groups || { id: match[1] };
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
} else if (route.path === path) {
|
|
441
|
+
matchedRoute = route;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const previousEffect = activeEffect;
|
|
447
|
+
activeEffect = null;
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const view = matchedRoute
|
|
451
|
+
? matchedRoute.component(routeParams)
|
|
452
|
+
: html`
|
|
453
|
+
<h1>404</h1>
|
|
454
|
+
`;
|
|
455
|
+
|
|
456
|
+
container.replaceChildren(
|
|
457
|
+
view instanceof Node ? view : document.createTextNode(view ?? "")
|
|
458
|
+
);
|
|
459
|
+
} finally {
|
|
460
|
+
activeEffect = previousEffect;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return container;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Navigates to a specific route
|
|
469
|
+
* @param {string} path - Target path
|
|
470
|
+
*/
|
|
471
|
+
$router.go = (path) => {
|
|
472
|
+
const targetPath = path.startsWith("/") ? path : `/${path}`;
|
|
473
|
+
if (window.location.hash !== `#${targetPath}`) window.location.hash = targetPath;
|
|
474
|
+
};
|