thunderous 2.3.12 → 2.4.1

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/dist/index.cjs CHANGED
@@ -49,13 +49,14 @@ var DEFAULT_RENDER_OPTIONS = {
49
49
  };
50
50
 
51
51
  // src/signals.ts
52
- var subscriber = null;
52
+ var sym = null;
53
+ var effects = /* @__PURE__ */ new WeakMap();
53
54
  var createSignal = (initVal, options) => {
54
55
  const subscribers = /* @__PURE__ */ new Set();
55
56
  let value = initVal;
56
57
  const getter = (getterOptions) => {
57
- if (subscriber !== null) {
58
- subscribers.add(subscriber);
58
+ if (sym !== null) {
59
+ subscribers.add(sym);
59
60
  }
60
61
  if (options?.debugMode === true || getterOptions?.debugMode === true) {
61
62
  let label = "anonymous signal";
@@ -67,7 +68,11 @@ var createSignal = (initVal, options) => {
67
68
  } else if (getterOptions?.label !== void 0) {
68
69
  label = getterOptions.label;
69
70
  }
70
- console.log("Signal retrieved:", { value, subscribers, label });
71
+ console.log("Signal retrieved:", {
72
+ value,
73
+ subscribers: Array.from(subscribers).map((sym2) => effects.get(sym2)),
74
+ label
75
+ });
71
76
  }
72
77
  return value;
73
78
  };
@@ -84,15 +89,29 @@ var createSignal = (initVal, options) => {
84
89
  const isObject = typeof newValue === "object" && newValue !== null;
85
90
  if (!isObject && value === newValue) return;
86
91
  if (isObject && typeof value === "object" && value !== null) {
87
- if (JSON.stringify(value) === JSON.stringify(newValue)) return;
92
+ const isPlainObject = (obj) => typeof obj === "object" && obj !== null && Object.getPrototypeOf(obj) === Object.prototype;
93
+ if (isPlainObject(value) && isPlainObject(newValue)) {
94
+ if (JSON.stringify(value) === JSON.stringify(newValue)) return;
95
+ }
88
96
  }
89
97
  const oldValue = value;
90
98
  value = newValue;
91
- for (const fn of subscribers) {
92
- try {
93
- fn();
94
- } catch (error) {
95
- console.error("Error in subscriber:", { error, oldValue, newValue, fn });
99
+ for (const sym2 of subscribers) {
100
+ const effectRef = effects.get(sym2);
101
+ if (effectRef !== void 0) {
102
+ try {
103
+ effectRef.fn({
104
+ lastValue: effectRef.value,
105
+ destroy: () => {
106
+ effects.delete(sym2);
107
+ queueMicrotask(() => subscribers.delete(sym2));
108
+ }
109
+ });
110
+ } catch (error) {
111
+ console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
112
+ }
113
+ } else {
114
+ queueMicrotask(() => subscribers.delete(sym2));
96
115
  }
97
116
  }
98
117
  if (options?.debugMode === true || setterOptions?.debugMode === true) {
@@ -105,7 +124,12 @@ var createSignal = (initVal, options) => {
105
124
  } else if (setterOptions?.label !== void 0) {
106
125
  label = setterOptions.label;
107
126
  }
108
- console.log("Signal set:", { oldValue, newValue, subscribers, label });
127
+ console.log("Signal set:", {
128
+ oldValue,
129
+ newValue,
130
+ subscribers: Array.from(subscribers).map((sym2) => effects.get(sym2)),
131
+ label
132
+ });
109
133
  }
110
134
  };
111
135
  return [getter, setter];
@@ -121,21 +145,33 @@ var derived = (fn, options) => {
121
145
  });
122
146
  return getter;
123
147
  };
124
- var createEffect = (fn) => {
125
- subscriber = fn;
148
+ var createEffect = (fn, value) => {
149
+ const privateSym = sym = Symbol();
150
+ effects.set(sym, { fn, value });
126
151
  try {
127
- fn();
152
+ fn({
153
+ lastValue: value,
154
+ destroy: () => {
155
+ effects.delete(privateSym);
156
+ }
157
+ });
128
158
  } catch (error) {
129
159
  console.error("Error in effect:", { error, fn });
130
160
  }
131
- subscriber = null;
161
+ sym = null;
132
162
  };
133
163
 
134
164
  // src/utilities.ts
135
165
  var NOOP = () => void 0;
136
166
  var queryComment = (node, comment) => {
137
- for (const child of node.childNodes) {
138
- if (child.nodeType === Node.COMMENT_NODE && child.nodeValue === comment) {
167
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT, {
168
+ acceptNode: (n) => n.nodeValue === comment ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
169
+ });
170
+ return walker.nextNode();
171
+ };
172
+ var queryChildren = (children, selector) => {
173
+ for (const child of children) {
174
+ if (child instanceof Element && child.matches(selector)) {
139
175
  return child;
140
176
  }
141
177
  }
@@ -291,6 +327,8 @@ var renderState = {
291
327
  signalMap: /* @__PURE__ */ new Map(),
292
328
  callbackMap: /* @__PURE__ */ new Map(),
293
329
  fragmentMap: /* @__PURE__ */ new Map(),
330
+ childrenMap: /* @__PURE__ */ new Map(),
331
+ propertyMap: /* @__PURE__ */ new Map(),
294
332
  registry: typeof customElements !== "undefined" ? customElements : {}
295
333
  };
296
334
  var logPropertyWarning = (propName, element) => {
@@ -300,21 +338,27 @@ var logPropertyWarning = (propName, element) => {
300
338
  "\n\nThunderous will attempt to set the property anyway, but this may result in unexpected behavior. Please make sure the property exists on the element prior to setting it."
301
339
  );
302
340
  };
303
- var arrayToDocumentFragment = (array, parent, uniqueKey) => {
304
- const documentFragment = new DocumentFragment();
305
- let count = 0;
306
- const keys = /* @__PURE__ */ new Set();
307
- for (const item of array) {
308
- const node = createNewNode(item, parent, uniqueKey);
309
- if (node instanceof DocumentFragment) {
310
- const child = node.firstElementChild;
311
- if (node.children.length > 1) {
341
+ var asNodeList = (value, parent) => {
342
+ if (typeof value === "string") return [new Text(value)];
343
+ if (value instanceof DocumentFragment) return [...value.children];
344
+ if (Array.isArray(value)) {
345
+ const nodeList = [];
346
+ let count = 0;
347
+ const keys = /* @__PURE__ */ new Set();
348
+ for (const item of value) {
349
+ const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
350
+ const children = cachedItem ?? asNodeList(item, parent);
351
+ if (cachedItem === void 0 && item instanceof DocumentFragment) {
352
+ renderState.childrenMap.set(item, children);
353
+ }
354
+ if (children.length > 1) {
312
355
  console.error(
313
356
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
314
357
  parent
315
358
  );
316
359
  }
317
- if (child === null) continue;
360
+ const child = children[0];
361
+ if (child === null || !(child instanceof Element)) continue;
318
362
  let key = child.getAttribute("key");
319
363
  if (key === null) {
320
364
  console.warn(
@@ -332,18 +376,11 @@ var arrayToDocumentFragment = (array, parent, uniqueKey) => {
332
376
  }
333
377
  keys.add(key);
334
378
  count++;
379
+ nodeList.push(...children);
335
380
  }
336
- documentFragment.append(node);
381
+ return nodeList;
337
382
  }
338
- const comment = document.createComment(uniqueKey);
339
- documentFragment.append(comment);
340
- return documentFragment;
341
- };
342
- var createNewNode = (value, parent, uniqueKey) => {
343
- if (typeof value === "string") return new Text(value);
344
- if (Array.isArray(value)) return arrayToDocumentFragment(value, parent, uniqueKey);
345
- if (value instanceof DocumentFragment) return value;
346
- return new Text("");
383
+ return [new Text()];
347
384
  };
348
385
  var processValue = (value) => {
349
386
  if (!isServer && value instanceof DocumentFragment) {
@@ -369,62 +406,139 @@ var processValue = (value) => {
369
406
  return String(value);
370
407
  };
371
408
  var evaluateBindings = (element, fragment) => {
372
- for (const child of element.childNodes) {
409
+ for (const child of [...element.childNodes]) {
373
410
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
374
411
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
375
- const sibling = child.nextSibling;
412
+ const nextSibling = child.nextSibling;
413
+ const prevSibling = child.previousSibling;
376
414
  textList.forEach((text, i) => {
377
- const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
378
- const signal = uniqueKey !== text ? renderState.signalMap.get(uniqueKey) : void 0;
415
+ const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
416
+ const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
379
417
  const newValue = signal !== void 0 ? signal() : text;
380
- const newNode = createNewNode(newValue, element, uniqueKey);
418
+ const initialChildren = asNodeList(newValue, element);
381
419
  if (i === 0) {
382
- child.replaceWith(newNode);
420
+ child.replaceWith(...initialChildren);
383
421
  } else {
384
- element.insertBefore(newNode, sibling);
422
+ const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
423
+ if (endAnchor2 !== null) {
424
+ endAnchor2.before(...initialChildren);
425
+ } else {
426
+ element.append(...initialChildren);
427
+ }
385
428
  }
386
- if (signal !== void 0 && newNode instanceof Text) {
387
- createEffect(() => {
388
- newNode.data = signal();
389
- });
390
- } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
391
- let init = false;
392
- createEffect(() => {
393
- const result = signal();
394
- const nextNode = createNewNode(result, element, uniqueKey);
395
- if (nextNode instanceof Text) {
396
- const error = new TypeError(
397
- "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
398
- );
399
- console.error(error);
400
- throw error;
429
+ if (uniqueKey === void 0) return;
430
+ const startAnchor = document.createComment(`${uniqueKey}:start`);
431
+ if (prevSibling !== null) {
432
+ prevSibling.after(startAnchor);
433
+ } else {
434
+ element.prepend(startAnchor);
435
+ }
436
+ const endAnchor = document.createComment(`${uniqueKey}:end`);
437
+ if (nextSibling !== null) {
438
+ nextSibling.before(endAnchor);
439
+ } else {
440
+ element.append(endAnchor);
441
+ }
442
+ const bindText = (node, signal2) => {
443
+ createEffect(({ destroy }) => {
444
+ const result = signal2();
445
+ if (Array.isArray(result)) {
446
+ destroy();
447
+ bindArray(signal2);
448
+ return;
401
449
  }
402
- for (const child2 of element.children) {
403
- const key = child2.getAttribute("key");
404
- if (key === null) continue;
405
- const matchingNode = nextNode.querySelector(`[key="${key}"]`);
406
- if (init && matchingNode === null) {
407
- child2.remove();
408
- }
450
+ if (result instanceof DocumentFragment) {
451
+ destroy();
452
+ bindFragment(signal2);
453
+ return;
409
454
  }
410
- let anchor = queryComment(element, uniqueKey);
411
- for (const child2 of nextNode.children) {
412
- const key = child2.getAttribute("key");
413
- const matchingNode = element.querySelector(`[key="${key}"]`);
414
- if (matchingNode === null) continue;
415
- matchingNode.__customCallbackFns = child2.__customCallbackFns;
416
- for (const attr of child2.attributes) {
417
- matchingNode.setAttribute(attr.name, attr.value);
455
+ node.data = result === null ? "" : String(result);
456
+ });
457
+ };
458
+ const bindArray = (signal2) => {
459
+ createEffect(
460
+ ({ lastValue: oldChildren, destroy }) => {
461
+ const result = signal2();
462
+ console.trace("Binding array:", {
463
+ result: result.map((node) => node.cloneNode(true)),
464
+ oldChildren
465
+ });
466
+ const newChildren = asNodeList(result, element);
467
+ const firstChild = newChildren[0];
468
+ if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
469
+ destroy();
470
+ bindFragment(signal2);
471
+ return;
472
+ }
473
+ if (newChildren.length === 1 && firstChild instanceof Text) {
474
+ destroy();
475
+ bindText(firstChild, signal2);
476
+ return;
418
477
  }
419
- matchingNode.replaceChildren(...child2.childNodes);
420
- anchor = matchingNode.nextSibling;
421
- child2.replaceWith(matchingNode);
478
+ while (startAnchor.nextSibling !== endAnchor) {
479
+ startAnchor.nextSibling?.remove();
480
+ }
481
+ startAnchor.after(...newChildren);
482
+ if (oldChildren === null) return newChildren;
483
+ for (const persistedChild of oldChildren) {
484
+ if (persistedChild instanceof Element) {
485
+ const key = persistedChild.getAttribute("key");
486
+ if (key === null) continue;
487
+ const newChild = queryChildren(newChildren, `[key="${key}"]`);
488
+ if (newChild === null) {
489
+ persistedChild.remove();
490
+ continue;
491
+ }
492
+ for (const attr of [...persistedChild.attributes]) {
493
+ if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
494
+ }
495
+ for (const newAttr of [...newChild.attributes]) {
496
+ const oldAttrValue = persistedChild.getAttribute(newAttr.name);
497
+ if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
498
+ persistedChild.setAttribute(newAttr.name, newAttr.value);
499
+ }
500
+ newChild.replaceWith(persistedChild);
501
+ }
502
+ }
503
+ return newChildren;
504
+ },
505
+ null
506
+ );
507
+ };
508
+ const bindFragment = (signal2) => {
509
+ const initialFragment = signal2();
510
+ renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
511
+ createEffect(({ destroy }) => {
512
+ const result = signal2();
513
+ const cachedChildren = renderState.childrenMap.get(initialFragment);
514
+ const children = cachedChildren ?? asNodeList(result, element);
515
+ if (Array.isArray(result)) {
516
+ destroy();
517
+ bindArray(signal2);
518
+ return;
422
519
  }
423
- const nextAnchor = queryComment(nextNode, uniqueKey);
424
- nextAnchor?.remove();
425
- element.insertBefore(nextNode, anchor);
426
- if (!init) init = true;
520
+ if (result instanceof Text) {
521
+ const children2 = asNodeList(result, element);
522
+ const text2 = children2[0];
523
+ destroy();
524
+ bindText(text2, signal2);
525
+ return;
526
+ }
527
+ while (startAnchor.nextSibling !== endAnchor) {
528
+ startAnchor.nextSibling?.remove();
529
+ }
530
+ startAnchor.after(...children);
427
531
  });
532
+ };
533
+ if (signal !== void 0) {
534
+ if (Array.isArray(newValue)) {
535
+ bindArray(signal);
536
+ } else if (initialChildren instanceof DocumentFragment) {
537
+ bindFragment(signal);
538
+ } else {
539
+ const initialChild = initialChildren[0];
540
+ bindText(initialChild, signal);
541
+ }
428
542
  }
429
543
  });
430
544
  }
@@ -455,14 +569,22 @@ var evaluateBindings = (element, fragment) => {
455
569
  newText += text;
456
570
  }
457
571
  }
458
- if (hasNull && newText === "null" || attrName.startsWith("prop:")) {
572
+ if (hasNull && newText === "null" || attrName.startsWith("prop-id:")) {
459
573
  if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
460
574
  } else {
461
575
  if (newText !== prevText) child.setAttribute(attrName, newText);
462
576
  }
463
- if (attrName.startsWith("prop:")) {
577
+ if (attrName.startsWith("prop-id:")) {
464
578
  if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
465
- const propName = attrName.replace("prop:", "");
579
+ const propId = attrName.replace("prop-id:", "");
580
+ const propName = renderState.propertyMap.get(propId);
581
+ if (propName === void 0) {
582
+ console.error(
583
+ `BRANCH:SIGNAL; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
584
+ child
585
+ );
586
+ return;
587
+ }
466
588
  const newValue = hasNull && newText === "null" ? null : newText;
467
589
  if (!(propName in child)) logPropertyWarning(propName, child);
468
590
  child[propName] = signal !== void 0 ? signal() : newValue;
@@ -488,18 +610,34 @@ var evaluateBindings = (element, fragment) => {
488
610
  child.__customCallbackFns.set(uniqueKey, callback);
489
611
  }
490
612
  }
491
- if (uniqueKey !== "" && !attrName.startsWith("prop:")) {
613
+ if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
492
614
  child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
493
- } else if (attrName.startsWith("prop:")) {
615
+ } else if (attrName.startsWith("prop-id:")) {
494
616
  child.removeAttribute(attrName);
495
- const propName = attrName.replace("prop:", "");
617
+ const propId = attrName.replace("prop-id:", "");
618
+ const propName = renderState.propertyMap.get(propId);
619
+ if (propName === void 0) {
620
+ console.error(
621
+ `BRANCH:CALLBACK; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
622
+ child
623
+ );
624
+ return;
625
+ }
496
626
  if (!(propName in child)) logPropertyWarning(propName, child);
497
627
  child[propName] = child.__customCallbackFns.get(uniqueKey);
498
628
  }
499
629
  });
500
- } else if (attrName.startsWith("prop:")) {
630
+ } else if (attrName.startsWith("prop-id:")) {
501
631
  child.removeAttribute(attrName);
502
- const propName = attrName.replace("prop:", "");
632
+ const propId = attrName.replace("prop-id:", "");
633
+ const propName = renderState.propertyMap.get(propId);
634
+ if (propName === void 0) {
635
+ console.error(
636
+ `BRANCH:PROP; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
637
+ child
638
+ );
639
+ return;
640
+ }
503
641
  if (!(propName in child)) logPropertyWarning(propName, child);
504
642
  child[propName] = attr.value;
505
643
  }
@@ -509,7 +647,7 @@ var evaluateBindings = (element, fragment) => {
509
647
  }
510
648
  };
511
649
  var html = (strings, ...values) => {
512
- const innerHTML = strings.reduce((innerHTML2, str, i) => {
650
+ let innerHTML = strings.reduce((innerHTML2, str, i) => {
513
651
  let value = values[i] ?? "";
514
652
  if (Array.isArray(value)) {
515
653
  value = value.map((item) => processValue(item)).join("");
@@ -520,6 +658,16 @@ var html = (strings, ...values) => {
520
658
  return innerHTML2;
521
659
  }, "");
522
660
  if (isServer) return innerHTML;
661
+ const props = innerHTML.match(/prop:([^=]+)/g);
662
+ if (props !== null) {
663
+ for (const prop of props) {
664
+ const name = prop.split(":")[1].trim();
665
+ const id = crypto.randomUUID();
666
+ const newProp = `prop-id:${id}`;
667
+ renderState.propertyMap.set(id, name);
668
+ innerHTML = innerHTML.replace(`prop:${name}`, newProp);
669
+ }
670
+ }
523
671
  const template = document.createElement("template");
524
672
  template.innerHTML = innerHTML;
525
673
  const fragment = renderState.currentShadowRoot?.importNode?.(template.content, true) ?? document.importNode(template.content, true);
@@ -633,7 +781,7 @@ var customElement = (render, options) => {
633
781
  observedAttributesSet.add(attrName);
634
782
  attributesAsPropertiesMap.set(attrName, {
635
783
  // convert kebab-case attribute names to camelCase property names
636
- prop: attrName.replace(/^([A-Z]+)/, (_, letter) => letter.toLowerCase()).replace(/(-|_| )([a-zA-Z])/g, (_, letter) => letter.toUpperCase()),
784
+ prop: attrName.replace(/(?<=-|_)([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/(-|_)/g, ""),
637
785
  coerce,
638
786
  value: null
639
787
  });
package/dist/index.d.cts CHANGED
@@ -112,6 +112,10 @@ type AnyFn = (...args: any[]) => any;
112
112
 
113
113
  type HTMLCustomElement<T extends Record<PropertyKey, unknown>> = Omit<HTMLElement, keyof T> & T;
114
114
 
115
+ // Again, flexible typing is necessary to support these generics
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ type Effect<T = any> = (args: { lastValue: T; destroy: () => void }) => T | void;
118
+
115
119
  /**
116
120
  * Create a custom element that can be defined for use in the DOM.
117
121
  * @example
@@ -174,7 +178,7 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
174
178
  * });
175
179
  * ```
176
180
  */
177
- declare const createEffect: (fn: () => void) => void;
181
+ declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
178
182
 
179
183
  /**
180
184
  * A tagged template function for creating DocumentFragment instances.
package/dist/index.d.ts CHANGED
@@ -112,6 +112,10 @@ type AnyFn = (...args: any[]) => any;
112
112
 
113
113
  type HTMLCustomElement<T extends Record<PropertyKey, unknown>> = Omit<HTMLElement, keyof T> & T;
114
114
 
115
+ // Again, flexible typing is necessary to support these generics
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ type Effect<T = any> = (args: { lastValue: T; destroy: () => void }) => T | void;
118
+
115
119
  /**
116
120
  * Create a custom element that can be defined for use in the DOM.
117
121
  * @example
@@ -174,7 +178,7 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
174
178
  * });
175
179
  * ```
176
180
  */
177
- declare const createEffect: (fn: () => void) => void;
181
+ declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
178
182
 
179
183
  /**
180
184
  * A tagged template function for creating DocumentFragment instances.