thunderous 2.3.13 → 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,7 @@ 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(),
294
331
  propertyMap: /* @__PURE__ */ new Map(),
295
332
  registry: typeof customElements !== "undefined" ? customElements : {}
296
333
  };
@@ -301,21 +338,27 @@ var logPropertyWarning = (propName, element) => {
301
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."
302
339
  );
303
340
  };
304
- var arrayToDocumentFragment = (array, parent, uniqueKey) => {
305
- const documentFragment = new DocumentFragment();
306
- let count = 0;
307
- const keys = /* @__PURE__ */ new Set();
308
- for (const item of array) {
309
- const node = createNewNode(item, parent, uniqueKey);
310
- if (node instanceof DocumentFragment) {
311
- const child = node.firstElementChild;
312
- 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) {
313
355
  console.error(
314
356
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
315
357
  parent
316
358
  );
317
359
  }
318
- if (child === null) continue;
360
+ const child = children[0];
361
+ if (child === null || !(child instanceof Element)) continue;
319
362
  let key = child.getAttribute("key");
320
363
  if (key === null) {
321
364
  console.warn(
@@ -333,18 +376,11 @@ var arrayToDocumentFragment = (array, parent, uniqueKey) => {
333
376
  }
334
377
  keys.add(key);
335
378
  count++;
379
+ nodeList.push(...children);
336
380
  }
337
- documentFragment.append(node);
381
+ return nodeList;
338
382
  }
339
- const comment = document.createComment(uniqueKey);
340
- documentFragment.append(comment);
341
- return documentFragment;
342
- };
343
- var createNewNode = (value, parent, uniqueKey) => {
344
- if (typeof value === "string") return new Text(value);
345
- if (Array.isArray(value)) return arrayToDocumentFragment(value, parent, uniqueKey);
346
- if (value instanceof DocumentFragment) return value;
347
- return new Text("");
383
+ return [new Text()];
348
384
  };
349
385
  var processValue = (value) => {
350
386
  if (!isServer && value instanceof DocumentFragment) {
@@ -370,62 +406,139 @@ var processValue = (value) => {
370
406
  return String(value);
371
407
  };
372
408
  var evaluateBindings = (element, fragment) => {
373
- for (const child of element.childNodes) {
409
+ for (const child of [...element.childNodes]) {
374
410
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
375
411
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
376
- const sibling = child.nextSibling;
412
+ const nextSibling = child.nextSibling;
413
+ const prevSibling = child.previousSibling;
377
414
  textList.forEach((text, i) => {
378
- const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
379
- 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;
380
417
  const newValue = signal !== void 0 ? signal() : text;
381
- const newNode = createNewNode(newValue, element, uniqueKey);
418
+ const initialChildren = asNodeList(newValue, element);
382
419
  if (i === 0) {
383
- child.replaceWith(newNode);
420
+ child.replaceWith(...initialChildren);
384
421
  } else {
385
- 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
+ }
386
428
  }
387
- if (signal !== void 0 && newNode instanceof Text) {
388
- createEffect(() => {
389
- newNode.data = signal();
390
- });
391
- } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
392
- let init = false;
393
- createEffect(() => {
394
- const result = signal();
395
- const nextNode = createNewNode(result, element, uniqueKey);
396
- if (nextNode instanceof Text) {
397
- const error = new TypeError(
398
- "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
399
- );
400
- console.error(error);
401
- 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;
402
449
  }
403
- for (const child2 of element.children) {
404
- const key = child2.getAttribute("key");
405
- if (key === null) continue;
406
- const matchingNode = nextNode.querySelector(`[key="${key}"]`);
407
- if (init && matchingNode === null) {
408
- child2.remove();
409
- }
450
+ if (result instanceof DocumentFragment) {
451
+ destroy();
452
+ bindFragment(signal2);
453
+ return;
410
454
  }
411
- let anchor = queryComment(element, uniqueKey);
412
- for (const child2 of nextNode.children) {
413
- const key = child2.getAttribute("key");
414
- const matchingNode = element.querySelector(`[key="${key}"]`);
415
- if (matchingNode === null) continue;
416
- matchingNode.__customCallbackFns = child2.__customCallbackFns;
417
- for (const attr of child2.attributes) {
418
- 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;
419
477
  }
420
- matchingNode.replaceChildren(...child2.childNodes);
421
- anchor = matchingNode.nextSibling;
422
- 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;
519
+ }
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();
423
529
  }
424
- const nextAnchor = queryComment(nextNode, uniqueKey);
425
- nextAnchor?.remove();
426
- element.insertBefore(nextNode, anchor);
427
- if (!init) init = true;
530
+ startAnchor.after(...children);
428
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
+ }
429
542
  }
430
543
  });
431
544
  }
@@ -510,6 +623,7 @@ var evaluateBindings = (element, fragment) => {
510
623
  );
511
624
  return;
512
625
  }
626
+ if (!(propName in child)) logPropertyWarning(propName, child);
513
627
  child[propName] = child.__customCallbackFns.get(uniqueKey);
514
628
  }
515
629
  });
@@ -524,6 +638,7 @@ var evaluateBindings = (element, fragment) => {
524
638
  );
525
639
  return;
526
640
  }
641
+ if (!(propName in child)) logPropertyWarning(propName, child);
527
642
  child[propName] = attr.value;
528
643
  }
529
644
  }
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.