thunderous 2.3.13 → 2.4.2

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 ident = 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 (ident !== null) {
59
+ subscribers.add(ident);
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((sym) => effects.get(sym)),
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 sym of subscribers) {
100
+ const effectRef = effects.get(sym);
101
+ if (effectRef !== void 0) {
102
+ try {
103
+ effectRef.fn({
104
+ lastValue: effectRef.value,
105
+ destroy: () => {
106
+ effects.delete(sym);
107
+ queueMicrotask(() => subscribers.delete(sym));
108
+ }
109
+ });
110
+ } catch (error) {
111
+ console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
112
+ }
113
+ } else {
114
+ queueMicrotask(() => subscribers.delete(sym));
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((sym) => effects.get(sym)),
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 privateIdent = ident = {};
150
+ effects.set(ident, { fn, value });
126
151
  try {
127
- fn();
152
+ fn({
153
+ lastValue: value,
154
+ destroy: () => {
155
+ effects.delete(privateIdent);
156
+ }
157
+ });
128
158
  } catch (error) {
129
159
  console.error("Error in effect:", { error, fn });
130
160
  }
131
- subscriber = null;
161
+ ident = 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,135 @@ 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
+ const newChildren = asNodeList(result, element);
463
+ const firstChild = newChildren[0];
464
+ if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
465
+ destroy();
466
+ bindFragment(signal2);
467
+ return;
468
+ }
469
+ if (newChildren.length === 1 && firstChild instanceof Text) {
470
+ destroy();
471
+ bindText(firstChild, signal2);
472
+ return;
473
+ }
474
+ while (startAnchor.nextSibling !== endAnchor) {
475
+ startAnchor.nextSibling?.remove();
476
+ }
477
+ startAnchor.after(...newChildren);
478
+ if (oldChildren === null) return newChildren;
479
+ for (const persistedChild of oldChildren) {
480
+ if (persistedChild instanceof Element) {
481
+ const key = persistedChild.getAttribute("key");
482
+ if (key === null) continue;
483
+ const newChild = queryChildren(newChildren, `[key="${key}"]`);
484
+ if (newChild === null) {
485
+ persistedChild.remove();
486
+ continue;
487
+ }
488
+ for (const attr of [...persistedChild.attributes]) {
489
+ if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
490
+ }
491
+ for (const newAttr of [...newChild.attributes]) {
492
+ const oldAttrValue = persistedChild.getAttribute(newAttr.name);
493
+ if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
494
+ persistedChild.setAttribute(newAttr.name, newAttr.value);
495
+ }
496
+ newChild.replaceWith(persistedChild);
497
+ }
419
498
  }
420
- matchingNode.replaceChildren(...child2.childNodes);
421
- anchor = matchingNode.nextSibling;
422
- child2.replaceWith(matchingNode);
499
+ return newChildren;
500
+ },
501
+ null
502
+ );
503
+ };
504
+ const bindFragment = (signal2) => {
505
+ const initialFragment = signal2();
506
+ renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
507
+ createEffect(({ destroy }) => {
508
+ const result = signal2();
509
+ const cachedChildren = renderState.childrenMap.get(initialFragment);
510
+ const children = cachedChildren ?? asNodeList(result, element);
511
+ if (Array.isArray(result)) {
512
+ destroy();
513
+ bindArray(signal2);
514
+ return;
515
+ }
516
+ if (result instanceof Text) {
517
+ const children2 = asNodeList(result, element);
518
+ const text2 = children2[0];
519
+ destroy();
520
+ bindText(text2, signal2);
521
+ return;
522
+ }
523
+ while (startAnchor.nextSibling !== endAnchor) {
524
+ startAnchor.nextSibling?.remove();
423
525
  }
424
- const nextAnchor = queryComment(nextNode, uniqueKey);
425
- nextAnchor?.remove();
426
- element.insertBefore(nextNode, anchor);
427
- if (!init) init = true;
526
+ startAnchor.after(...children);
428
527
  });
528
+ };
529
+ if (signal !== void 0) {
530
+ if (Array.isArray(newValue)) {
531
+ bindArray(signal);
532
+ } else if (initialChildren instanceof DocumentFragment) {
533
+ bindFragment(signal);
534
+ } else {
535
+ const initialChild = initialChildren[0];
536
+ bindText(initialChild, signal);
537
+ }
429
538
  }
430
539
  });
431
540
  }
@@ -510,6 +619,7 @@ var evaluateBindings = (element, fragment) => {
510
619
  );
511
620
  return;
512
621
  }
622
+ if (!(propName in child)) logPropertyWarning(propName, child);
513
623
  child[propName] = child.__customCallbackFns.get(uniqueKey);
514
624
  }
515
625
  });
@@ -524,6 +634,7 @@ var evaluateBindings = (element, fragment) => {
524
634
  );
525
635
  return;
526
636
  }
637
+ if (!(propName in child)) logPropertyWarning(propName, child);
527
638
  child[propName] = attr.value;
528
639
  }
529
640
  }
@@ -597,7 +708,7 @@ var css = (strings, ...values) => {
597
708
  }
598
709
  const newCSSText = newCSSTextList.join("");
599
710
  if (isCSSStyleSheet(stylesheet)) {
600
- stylesheet.replace(newCSSText).catch(console.error);
711
+ stylesheet.replaceSync(newCSSText);
601
712
  } else {
602
713
  stylesheet.textContent = newCSSText;
603
714
  }
@@ -626,6 +737,7 @@ var customElement = (render, options) => {
626
737
  if (shadowRootOptions.registry !== void 0 && "scoped" in shadowRootOptions.registry && shadowRootOptions.registry.scoped) {
627
738
  return shadowRootOptions.registry;
628
739
  }
740
+ return void 0;
629
741
  })();
630
742
  return {
631
743
  define(tagName) {
@@ -685,8 +797,8 @@ var customElement = (render, options) => {
685
797
  #formResetCallbackFns = /* @__PURE__ */ new Set();
686
798
  #formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
687
799
  #clientOnlyCallbackFns = /* @__PURE__ */ new Set();
688
- #shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
689
800
  #internals = this.attachInternals();
801
+ #shadowRoot = attachShadow ? this.#internals.shadowRoot ?? this.attachShadow(shadowRootOptions) : null;
690
802
  #observer = options?.observedAttributes !== void 0 ? null : new MutationObserver((mutations) => {
691
803
  for (const mutation of mutations) {
692
804
  const attrName = mutation.attributeName;
@@ -739,7 +851,7 @@ Element: <${this.tagName.toLowerCase()}>
739
851
  this[prop] = newValue;
740
852
  _setter(newValue, options2);
741
853
  };
742
- const getter = (options2) => {
854
+ const getter = ((options2) => {
743
855
  const value = _getter(options2);
744
856
  if (value === void 0 && !allowUndefined) {
745
857
  const error = new Error(
@@ -751,7 +863,7 @@ You must set an initial value before calling a property signal's getter.
751
863
  throw error;
752
864
  }
753
865
  return value;
754
- };
866
+ });
755
867
  getter.getter = true;
756
868
  const publicSignal = [getter, setter];
757
869
  publicSignal.init = (value) => {
package/dist/index.d.cts CHANGED
@@ -107,11 +107,13 @@ type Signal<T = unknown> = [SignalGetter<T>, SignalSetter<T>];
107
107
  type SignalWithInit<T = unknown> = Signal<T> & { init: (value: T) => Signal<T> };
108
108
 
109
109
  // Flexible typing is necessary to support generic functions
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
110
  type AnyFn = (...args: any[]) => any;
112
111
 
113
112
  type HTMLCustomElement<T extends Record<PropertyKey, unknown>> = Omit<HTMLElement, keyof T> & T;
114
113
 
114
+ // Again, flexible typing is necessary to support these generics
115
+ type Effect<T = any> = (args: { lastValue: T; destroy: () => void }) => T | void;
116
+
115
117
  /**
116
118
  * Create a custom element that can be defined for use in the DOM.
117
119
  * @example
@@ -174,7 +176,7 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
174
176
  * });
175
177
  * ```
176
178
  */
177
- declare const createEffect: (fn: () => void) => void;
179
+ declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
178
180
 
179
181
  /**
180
182
  * A tagged template function for creating DocumentFragment instances.
@@ -182,4 +184,4 @@ declare const createEffect: (fn: () => void) => void;
182
184
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
183
185
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
184
186
 
185
- export { type HTMLCustomElement, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
187
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
package/dist/index.d.ts CHANGED
@@ -107,11 +107,13 @@ type Signal<T = unknown> = [SignalGetter<T>, SignalSetter<T>];
107
107
  type SignalWithInit<T = unknown> = Signal<T> & { init: (value: T) => Signal<T> };
108
108
 
109
109
  // Flexible typing is necessary to support generic functions
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
110
  type AnyFn = (...args: any[]) => any;
112
111
 
113
112
  type HTMLCustomElement<T extends Record<PropertyKey, unknown>> = Omit<HTMLElement, keyof T> & T;
114
113
 
114
+ // Again, flexible typing is necessary to support these generics
115
+ type Effect<T = any> = (args: { lastValue: T; destroy: () => void }) => T | void;
116
+
115
117
  /**
116
118
  * Create a custom element that can be defined for use in the DOM.
117
119
  * @example
@@ -174,7 +176,7 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
174
176
  * });
175
177
  * ```
176
178
  */
177
- declare const createEffect: (fn: () => void) => void;
179
+ declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
178
180
 
179
181
  /**
180
182
  * A tagged template function for creating DocumentFragment instances.
@@ -182,4 +184,4 @@ declare const createEffect: (fn: () => void) => void;
182
184
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
183
185
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
184
186
 
185
- export { type HTMLCustomElement, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
187
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };