thunderous 2.4.3 → 2.4.5-next.1776736743

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
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ clearRenderState: () => clearRenderState,
24
+ clearServerCss: () => clearServerCss,
23
25
  clientOnlyCallback: () => clientOnlyCallback,
24
26
  createEffect: () => createEffect,
25
27
  createRegistry: () => createRegistry,
@@ -100,13 +102,14 @@ var createSignal = (initVal, options) => {
100
102
  const effectRef = effects.get(sym);
101
103
  if (effectRef !== void 0) {
102
104
  try {
103
- effectRef.fn({
105
+ const result = effectRef.fn({
104
106
  lastValue: effectRef.value,
105
107
  destroy: () => {
106
108
  effects.delete(sym);
107
109
  queueMicrotask(() => subscribers.delete(sym));
108
110
  }
109
111
  });
112
+ if (result !== void 0) effectRef.value = result;
110
113
  } catch (error) {
111
114
  console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
112
115
  }
@@ -147,14 +150,16 @@ var derived = (fn, options) => {
147
150
  };
148
151
  var createEffect = (fn, value) => {
149
152
  const privateIdent = ident = {};
150
- effects.set(ident, { fn, value });
153
+ const effectRef = { fn, value };
154
+ effects.set(ident, effectRef);
151
155
  try {
152
- fn({
156
+ const result = fn({
153
157
  lastValue: value,
154
158
  destroy: () => {
155
159
  effects.delete(privateIdent);
156
160
  }
157
161
  });
162
+ if (result !== void 0) effectRef.value = result;
158
163
  } catch (error) {
159
164
  console.error("Error in effect:", { error, fn });
160
165
  }
@@ -163,12 +168,6 @@ var createEffect = (fn, value) => {
163
168
 
164
169
  // src/utilities.ts
165
170
  var NOOP = () => void 0;
166
- var queryComment = (node, 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
171
  var queryChildren = (children, selector) => {
173
172
  for (const child of children) {
174
173
  if (child instanceof Element && child.matches(selector)) {
@@ -184,6 +183,9 @@ var serverDefineFns = /* @__PURE__ */ new Set();
184
183
  var onServerDefine = (fn) => {
185
184
  serverDefineFns.add(fn);
186
185
  };
186
+ var clearServerCss = () => {
187
+ serverCss.clear();
188
+ };
187
189
  var serverDefine = ({
188
190
  tagName,
189
191
  serverRender,
@@ -321,7 +323,7 @@ var clientOnlyCallback = (fn) => {
321
323
  // src/render.ts
322
324
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
323
325
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
324
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
326
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
325
327
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
326
328
  var renderState = {
327
329
  currentShadowRoot: null,
@@ -332,6 +334,13 @@ var renderState = {
332
334
  propertyMap: /* @__PURE__ */ new Map(),
333
335
  registry: typeof customElements !== "undefined" ? customElements : {}
334
336
  };
337
+ var clearRenderState = () => {
338
+ renderState.signalMap.clear();
339
+ renderState.callbackMap.clear();
340
+ renderState.propertyMap.clear();
341
+ renderState.fragmentMap.clear();
342
+ renderState.childrenMap.clear();
343
+ };
335
344
  var logPropertyWarning = (propName, element) => {
336
345
  console.warn(
337
346
  `Property "${propName}" does not exist on element:`,
@@ -339,19 +348,34 @@ var logPropertyWarning = (propName, element) => {
339
348
  "\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."
340
349
  );
341
350
  };
342
- var asNodeList = (value, parent) => {
351
+ var asNodeList = (value, parent, autoKey) => {
352
+ if (value === null || value === void 0) return [];
343
353
  if (typeof value === "string") return [new Text(value)];
344
- if (value instanceof DocumentFragment) return [...value.children];
354
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
355
+ if (value instanceof DocumentFragment) {
356
+ const children = Array.from(value.children);
357
+ if (autoKey !== void 0 && children.length > 0) {
358
+ const child = children[0];
359
+ if (child instanceof Element && child.getAttribute("key") === null) {
360
+ child.setAttribute("key", String(autoKey));
361
+ }
362
+ }
363
+ return children;
364
+ }
345
365
  if (Array.isArray(value)) {
346
366
  const nodeList = [];
347
367
  let count = 0;
348
368
  const keys = /* @__PURE__ */ new Set();
349
369
  for (const item of value) {
350
370
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
351
- const children = cachedItem ?? asNodeList(item, parent);
371
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
352
372
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
353
373
  renderState.childrenMap.set(item, children);
354
374
  }
375
+ if (!(item instanceof DocumentFragment)) {
376
+ nodeList.push(...children);
377
+ continue;
378
+ }
355
379
  if (children.length > 1) {
356
380
  console.error(
357
381
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -404,73 +428,68 @@ var processValue = (value) => {
404
428
  renderState.callbackMap.set(uniqueKey, value);
405
429
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
406
430
  }
407
- return String(value);
431
+ return value === null || value === void 0 ? "" : String(value);
408
432
  };
409
433
  var evaluateBindings = (element, fragment) => {
410
- for (const child of [...element.childNodes]) {
434
+ for (const child of Array.from(element.childNodes)) {
411
435
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
412
436
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
413
- const nextSibling = child.nextSibling;
414
- const prevSibling = child.previousSibling;
415
- textList.forEach((text, i) => {
437
+ const allInitialChildren = [];
438
+ const signalEntries = [];
439
+ let signalIndex = 0;
440
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
441
+ textList.forEach((text) => {
416
442
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
417
443
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
418
444
  const newValue = signal !== void 0 ? signal() : text;
419
- const initialChildren = asNodeList(newValue, element);
420
- if (i === 0) {
421
- child.replaceWith(...initialChildren);
422
- } else {
423
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
424
- if (endAnchor2 !== null) {
425
- endAnchor2.before(...initialChildren);
426
- } else {
427
- element.append(...initialChildren);
428
- }
445
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
446
+ const initialChildren = asNodeList(newValue, element, autoKey);
447
+ allInitialChildren.push(...initialChildren);
448
+ if (uniqueKey !== void 0 && signal !== void 0) {
449
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
429
450
  }
430
- if (uniqueKey === void 0) return;
451
+ });
452
+ child.replaceWith(...allInitialChildren);
453
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
454
+ const firstChild = initialChildren[0];
455
+ const lastChild = initialChildren[initialChildren.length - 1];
456
+ if (firstChild === void 0) return;
431
457
  const startAnchor = document.createComment(`${uniqueKey}:start`);
432
- if (prevSibling !== null) {
433
- prevSibling.after(startAnchor);
434
- } else {
435
- element.prepend(startAnchor);
436
- }
458
+ firstChild.before(startAnchor);
437
459
  const endAnchor = document.createComment(`${uniqueKey}:end`);
438
- if (nextSibling !== null) {
439
- nextSibling.before(endAnchor);
440
- } else {
441
- element.append(endAnchor);
442
- }
460
+ lastChild.after(endAnchor);
443
461
  const bindText = (node, signal2) => {
444
462
  createEffect(({ destroy }) => {
445
463
  const result = signal2();
446
464
  if (Array.isArray(result)) {
447
465
  destroy();
448
- bindArray(signal2);
466
+ bindArray(signal2, autoKey);
449
467
  return;
450
468
  }
451
469
  if (result instanceof DocumentFragment) {
452
470
  destroy();
453
- bindFragment(signal2);
471
+ bindFragment(signal2, initialChildren, autoKey);
454
472
  return;
455
473
  }
456
- node.data = result === null ? "" : String(result);
474
+ node.data = result === null || result === void 0 ? "" : String(result);
457
475
  });
458
476
  };
459
- const bindArray = (signal2) => {
477
+ const bindArray = (signal2, autoKey2) => {
460
478
  createEffect(
461
479
  ({ lastValue: oldChildren, destroy }) => {
462
480
  const result = signal2();
463
- const newChildren = asNodeList(result, element);
464
- const firstChild = newChildren[0];
465
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
466
- destroy();
467
- bindFragment(signal2);
468
- return;
469
- }
470
- if (newChildren.length === 1 && firstChild instanceof Text) {
471
- destroy();
472
- bindText(firstChild, signal2);
473
- return;
481
+ const newChildren = asNodeList(result, element, autoKey2);
482
+ const firstChild2 = newChildren[0];
483
+ if (!Array.isArray(result)) {
484
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
485
+ while (startAnchor.nextSibling !== endAnchor) {
486
+ startAnchor.nextSibling?.remove();
487
+ }
488
+ startAnchor.after(firstChild2);
489
+ destroy();
490
+ bindText(firstChild2, signal2);
491
+ return;
492
+ }
474
493
  }
475
494
  while (startAnchor.nextSibling !== endAnchor) {
476
495
  startAnchor.nextSibling?.remove();
@@ -480,16 +499,15 @@ var evaluateBindings = (element, fragment) => {
480
499
  for (const persistedChild of oldChildren) {
481
500
  if (persistedChild instanceof Element) {
482
501
  const key = persistedChild.getAttribute("key");
483
- if (key === null) continue;
484
502
  const newChild = queryChildren(newChildren, `[key="${key}"]`);
485
503
  if (newChild === null) {
486
504
  persistedChild.remove();
487
505
  continue;
488
506
  }
489
- for (const attr of [...persistedChild.attributes]) {
507
+ for (const attr of Array.from(persistedChild.attributes)) {
490
508
  if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
491
509
  }
492
- for (const newAttr of [...newChild.attributes]) {
510
+ for (const newAttr of Array.from(newChild.attributes)) {
493
511
  const oldAttrValue = persistedChild.getAttribute(newAttr.name);
494
512
  if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
495
513
  persistedChild.setAttribute(newAttr.name, newAttr.value);
@@ -502,40 +520,52 @@ var evaluateBindings = (element, fragment) => {
502
520
  null
503
521
  );
504
522
  };
505
- const bindFragment = (signal2) => {
523
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
506
524
  const initialFragment = signal2();
507
- renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
525
+ const firstInitialChild = initialChildren2[0];
526
+ if (firstInitialChild instanceof Element) {
527
+ renderState.childrenMap.set(initialFragment, initialChildren2);
528
+ }
508
529
  createEffect(({ destroy }) => {
509
530
  const result = signal2();
510
- const cachedChildren = renderState.childrenMap.get(initialFragment);
511
- const children = cachedChildren ?? asNodeList(result, element);
531
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
532
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
533
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
534
+ renderState.childrenMap.set(result, children);
535
+ }
512
536
  if (Array.isArray(result)) {
513
537
  destroy();
514
- bindArray(signal2);
538
+ bindArray(signal2, autoKey2);
515
539
  return;
516
540
  }
517
- if (result instanceof Text) {
518
- const children2 = asNodeList(result, element);
519
- const text2 = children2[0];
541
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
542
+ while (startAnchor.nextSibling !== endAnchor) {
543
+ startAnchor.nextSibling?.remove();
544
+ }
545
+ const children2 = asNodeList(result, element, autoKey2);
546
+ const text = children2[0];
547
+ startAnchor.after(text);
520
548
  destroy();
521
- bindText(text2, signal2);
549
+ bindText(text, signal2);
522
550
  return;
523
551
  }
524
552
  while (startAnchor.nextSibling !== endAnchor) {
525
553
  startAnchor.nextSibling?.remove();
526
554
  }
555
+ if (result === null || result === void 0) {
556
+ return;
557
+ }
527
558
  startAnchor.after(...children);
528
559
  });
529
560
  };
530
- if (signal !== void 0) {
531
- if (Array.isArray(newValue)) {
532
- bindArray(signal);
533
- } else if (initialChildren instanceof DocumentFragment) {
534
- bindFragment(signal);
535
- } else {
536
- const initialChild = initialChildren[0];
537
- bindText(initialChild, signal);
538
- }
561
+ const currentValue = signal();
562
+ if (Array.isArray(currentValue)) {
563
+ bindArray(signal, autoKey);
564
+ } else if (currentValue instanceof DocumentFragment) {
565
+ bindFragment(signal, initialChildren, autoKey);
566
+ } else {
567
+ const initialChild = initialChildren[0];
568
+ bindText(initialChild, signal);
539
569
  }
540
570
  });
541
571
  }
@@ -546,7 +576,7 @@ var evaluateBindings = (element, fragment) => {
546
576
  child.replaceWith(childFragment);
547
577
  }
548
578
  } else if (child instanceof Element) {
549
- for (const attr of [...child.attributes]) {
579
+ for (const attr of Array.from(child.attributes)) {
550
580
  const attrName = attr.name;
551
581
  if (SIGNAL_BINDING_REGEX.test(attr.value)) {
552
582
  const textList = attr.value.split(SIGNAL_BINDING_REGEX);
@@ -572,7 +602,6 @@ var evaluateBindings = (element, fragment) => {
572
602
  if (newText !== prevText) child.setAttribute(attrName, newText);
573
603
  }
574
604
  if (attrName.startsWith("prop-id:")) {
575
- if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
576
605
  const propId = attrName.replace("prop-id:", "");
577
606
  const propName = renderState.propertyMap.get(propId);
578
607
  if (propName === void 0) {
@@ -607,9 +636,7 @@ var evaluateBindings = (element, fragment) => {
607
636
  child.__customCallbackFns.set(uniqueKey, callback);
608
637
  }
609
638
  }
610
- if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
611
- child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
612
- } else if (attrName.startsWith("prop-id:")) {
639
+ if (attrName.startsWith("prop-id:")) {
613
640
  child.removeAttribute(attrName);
614
641
  const propId = attrName.replace("prop-id:", "");
615
642
  const propName = renderState.propertyMap.get(propId);
@@ -622,6 +649,8 @@ var evaluateBindings = (element, fragment) => {
622
649
  }
623
650
  if (!(propName in child)) logPropertyWarning(propName, child);
624
651
  child[propName] = child.__customCallbackFns.get(uniqueKey);
652
+ } else {
653
+ child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
625
654
  }
626
655
  });
627
656
  } else if (attrName.startsWith("prop-id:")) {
@@ -651,7 +680,7 @@ var html = (strings, ...values) => {
651
680
  } else {
652
681
  value = processValue(value);
653
682
  }
654
- innerHTML2 += str + String(value === null ? "" : value);
683
+ innerHTML2 += str + String(value);
655
684
  return innerHTML2;
656
685
  }, "");
657
686
  if (isServer) return innerHTML;
@@ -672,7 +701,7 @@ var html = (strings, ...values) => {
672
701
  evaluateBindings(fragment, fragment);
673
702
  return fragment;
674
703
  };
675
- var adoptedStylesSupported = typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
704
+ var isAdoptedStylesSupported = () => typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
676
705
  var isCSSStyleSheet = (stylesheet) => {
677
706
  return typeof CSSStyleSheet !== "undefined" && stylesheet instanceof CSSStyleSheet;
678
707
  };
@@ -696,7 +725,7 @@ var css = (strings, ...values) => {
696
725
  if (isServer) {
697
726
  return cssText;
698
727
  }
699
- const stylesheet = adoptedStylesSupported ? new CSSStyleSheet() : document.createElement("style");
728
+ const stylesheet = isAdoptedStylesSupported() ? new CSSStyleSheet() : document.createElement("style");
700
729
  const textList = cssText.split(signalBindingRegex);
701
730
  createEffect(() => {
702
731
  const newCSSTextList = [];
@@ -803,8 +832,9 @@ var customElement = (render, options) => {
803
832
  #observer = options?.observedAttributes !== void 0 ? null : new MutationObserver((mutations) => {
804
833
  for (const mutation of mutations) {
805
834
  const attrName = mutation.attributeName;
806
- if (mutation.type !== "attributes" || attrName === null) continue;
807
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
835
+ if (!(attrName in this.#attrSignals)) {
836
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
837
+ }
808
838
  const [getter, setter] = this.#attrSignals[attrName];
809
839
  const oldValue = getter();
810
840
  const newValue = this.getAttribute(attrName);
@@ -904,7 +934,9 @@ You must set an initial value before calling a property signal's getter.
904
934
  {},
905
935
  {
906
936
  get: (_, prop) => {
907
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
937
+ if (!(prop in this.#attrSignals)) {
938
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
939
+ }
908
940
  const [getter] = this.#attrSignals[prop];
909
941
  const setter = (newValue) => this.setAttribute(prop, newValue);
910
942
  return [getter, setter];
@@ -973,9 +1005,7 @@ You must set an initial value before calling a property signal's getter.
973
1005
  constructor() {
974
1006
  try {
975
1007
  super();
976
- if (!Object.prototype.hasOwnProperty.call(this, "__customCallbackFns")) {
977
- this.__customCallbackFns = /* @__PURE__ */ new Map();
978
- }
1008
+ this.__customCallbackFns = /* @__PURE__ */ new Map();
979
1009
  for (const attr of this.attributes) {
980
1010
  this.#attrSignals[attr.name] = createSignal(attr.value);
981
1011
  }
@@ -986,12 +1016,13 @@ You must set an initial value before calling a property signal's getter.
986
1016
  { cause: error }
987
1017
  );
988
1018
  console.error(_error);
989
- throw _error;
990
1019
  }
991
1020
  }
992
1021
  connectedCallback() {
993
1022
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
994
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
1023
+ if (!(attrName in this.#attrSignals)) {
1024
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
1025
+ }
995
1026
  const propName = attr.prop;
996
1027
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
997
1028
  let busy = false;
@@ -1008,6 +1039,14 @@ You must set an initial value before calling a property signal's getter.
1008
1039
  busy = false;
1009
1040
  });
1010
1041
  }
1042
+ for (const attrName of Object.keys(this.#attrSignals)) {
1043
+ const signal = this.#attrSignals[attrName];
1044
+ const [getter, setter] = signal;
1045
+ const currentValue = this.getAttribute(attrName);
1046
+ if (getter() !== currentValue) {
1047
+ setter(currentValue);
1048
+ }
1049
+ }
1011
1050
  if (this.#observer !== null) {
1012
1051
  this.#observer.observe(this, { attributes: true });
1013
1052
  }
@@ -1176,6 +1215,8 @@ var createRegistry = (args) => {
1176
1215
  };
1177
1216
  // Annotate the CommonJS export names for ESM import in node:
1178
1217
  0 && (module.exports = {
1218
+ clearRenderState,
1219
+ clearServerCss,
1179
1220
  clientOnlyCallback,
1180
1221
  createEffect,
1181
1222
  createRegistry,
package/dist/index.d.cts CHANGED
@@ -94,7 +94,7 @@ type RegistryArgs = {
94
94
 
95
95
  type Styles = CSSStyleSheet | HTMLStyleElement;
96
96
 
97
- type SignalOptions = { debugMode: boolean; label?: string };
97
+ type SignalOptions = { debugMode?: boolean; label?: string };
98
98
  type SignalGetter<T> = {
99
99
  (options?: SignalOptions): T;
100
100
  getter: true;
@@ -143,7 +143,41 @@ declare const customElement: <Props extends CustomElementProps>(render: RenderFu
143
143
  */
144
144
  declare const createRegistry: (args?: RegistryArgs) => RegistryResult;
145
145
 
146
+ /**
147
+ * Add a callback to handle each call to `define()` on the server.
148
+ *
149
+ * This enables you to intercept those definitions and respond to them,
150
+ * for example to inject declarative shadow DOM templates.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * let response = originalResponse;
155
+ * onServerDefine((tagName, htmlString) => {
156
+ * // ...
157
+ * response = htmlString.replace(tagName, `my-${tagName}`);
158
+ * });
159
+ * ```
160
+ */
146
161
  declare const onServerDefine: (fn: ServerDefineFn) => void;
162
+ /**
163
+ * Thunderous tracks its state using several maps to associate values with
164
+ * their respective elements.
165
+ *
166
+ * This function clears the map that tracks CSS on the server side, to prevent
167
+ * memory leaks and purge stale data from previous renders.
168
+ *
169
+ * If you are building a framework or plugin that depends on Thunderous, you
170
+ * should call this function before every render. Otherwise, the map will
171
+ * accumulate stale data and may create significant performance issues.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { clearServerCss } from 'thunderous'
176
+ *
177
+ * clearServerCss();
178
+ * ```
179
+ */
180
+ declare const clearServerCss: () => void;
147
181
  declare const insertTemplates: (tagName: string, template: string, inputString: string) => string;
148
182
  declare const clientOnlyCallback: (fn: (() => void) | (() => Promise<void>)) => void | Promise<void>;
149
183
 
@@ -178,10 +212,29 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
178
212
  */
179
213
  declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
180
214
 
215
+ /**
216
+ * Thunderous tracks its state using several maps to associate values with
217
+ * their respective elements.
218
+ *
219
+ * This function clears the maps tracking render state, to prevent memory
220
+ * leaks and purge stale data from previous renders.
221
+ *
222
+ * If you are building a framework or plugin that depends on Thunderous, you
223
+ * should call this function before every render. Otherwise, the maps will
224
+ * accumulate stale data and may create significant performance issues.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { clearRenderState } from 'thunderous'
229
+ *
230
+ * clearRenderState();
231
+ * ```
232
+ */
233
+ declare const clearRenderState: () => void;
181
234
  /**
182
235
  * A tagged template function for creating DocumentFragment instances.
183
236
  */
184
237
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
185
238
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
186
239
 
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 };
240
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clearRenderState, clearServerCss, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
package/dist/index.d.ts CHANGED
@@ -94,7 +94,7 @@ type RegistryArgs = {
94
94
 
95
95
  type Styles = CSSStyleSheet | HTMLStyleElement;
96
96
 
97
- type SignalOptions = { debugMode: boolean; label?: string };
97
+ type SignalOptions = { debugMode?: boolean; label?: string };
98
98
  type SignalGetter<T> = {
99
99
  (options?: SignalOptions): T;
100
100
  getter: true;
@@ -143,7 +143,41 @@ declare const customElement: <Props extends CustomElementProps>(render: RenderFu
143
143
  */
144
144
  declare const createRegistry: (args?: RegistryArgs) => RegistryResult;
145
145
 
146
+ /**
147
+ * Add a callback to handle each call to `define()` on the server.
148
+ *
149
+ * This enables you to intercept those definitions and respond to them,
150
+ * for example to inject declarative shadow DOM templates.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * let response = originalResponse;
155
+ * onServerDefine((tagName, htmlString) => {
156
+ * // ...
157
+ * response = htmlString.replace(tagName, `my-${tagName}`);
158
+ * });
159
+ * ```
160
+ */
146
161
  declare const onServerDefine: (fn: ServerDefineFn) => void;
162
+ /**
163
+ * Thunderous tracks its state using several maps to associate values with
164
+ * their respective elements.
165
+ *
166
+ * This function clears the map that tracks CSS on the server side, to prevent
167
+ * memory leaks and purge stale data from previous renders.
168
+ *
169
+ * If you are building a framework or plugin that depends on Thunderous, you
170
+ * should call this function before every render. Otherwise, the map will
171
+ * accumulate stale data and may create significant performance issues.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { clearServerCss } from 'thunderous'
176
+ *
177
+ * clearServerCss();
178
+ * ```
179
+ */
180
+ declare const clearServerCss: () => void;
147
181
  declare const insertTemplates: (tagName: string, template: string, inputString: string) => string;
148
182
  declare const clientOnlyCallback: (fn: (() => void) | (() => Promise<void>)) => void | Promise<void>;
149
183
 
@@ -178,10 +212,29 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
178
212
  */
179
213
  declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
180
214
 
215
+ /**
216
+ * Thunderous tracks its state using several maps to associate values with
217
+ * their respective elements.
218
+ *
219
+ * This function clears the maps tracking render state, to prevent memory
220
+ * leaks and purge stale data from previous renders.
221
+ *
222
+ * If you are building a framework or plugin that depends on Thunderous, you
223
+ * should call this function before every render. Otherwise, the maps will
224
+ * accumulate stale data and may create significant performance issues.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { clearRenderState } from 'thunderous'
229
+ *
230
+ * clearRenderState();
231
+ * ```
232
+ */
233
+ declare const clearRenderState: () => void;
181
234
  /**
182
235
  * A tagged template function for creating DocumentFragment instances.
183
236
  */
184
237
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
185
238
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
186
239
 
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 };
240
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clearRenderState, clearServerCss, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
package/dist/index.js CHANGED
@@ -65,13 +65,14 @@ var createSignal = (initVal, options) => {
65
65
  const effectRef = effects.get(sym);
66
66
  if (effectRef !== void 0) {
67
67
  try {
68
- effectRef.fn({
68
+ const result = effectRef.fn({
69
69
  lastValue: effectRef.value,
70
70
  destroy: () => {
71
71
  effects.delete(sym);
72
72
  queueMicrotask(() => subscribers.delete(sym));
73
73
  }
74
74
  });
75
+ if (result !== void 0) effectRef.value = result;
75
76
  } catch (error) {
76
77
  console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
77
78
  }
@@ -112,14 +113,16 @@ var derived = (fn, options) => {
112
113
  };
113
114
  var createEffect = (fn, value) => {
114
115
  const privateIdent = ident = {};
115
- effects.set(ident, { fn, value });
116
+ const effectRef = { fn, value };
117
+ effects.set(ident, effectRef);
116
118
  try {
117
- fn({
119
+ const result = fn({
118
120
  lastValue: value,
119
121
  destroy: () => {
120
122
  effects.delete(privateIdent);
121
123
  }
122
124
  });
125
+ if (result !== void 0) effectRef.value = result;
123
126
  } catch (error) {
124
127
  console.error("Error in effect:", { error, fn });
125
128
  }
@@ -128,12 +131,6 @@ var createEffect = (fn, value) => {
128
131
 
129
132
  // src/utilities.ts
130
133
  var NOOP = () => void 0;
131
- var queryComment = (node, comment) => {
132
- const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT, {
133
- acceptNode: (n) => n.nodeValue === comment ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
134
- });
135
- return walker.nextNode();
136
- };
137
134
  var queryChildren = (children, selector) => {
138
135
  for (const child of children) {
139
136
  if (child instanceof Element && child.matches(selector)) {
@@ -149,6 +146,9 @@ var serverDefineFns = /* @__PURE__ */ new Set();
149
146
  var onServerDefine = (fn) => {
150
147
  serverDefineFns.add(fn);
151
148
  };
149
+ var clearServerCss = () => {
150
+ serverCss.clear();
151
+ };
152
152
  var serverDefine = ({
153
153
  tagName,
154
154
  serverRender,
@@ -286,7 +286,7 @@ var clientOnlyCallback = (fn) => {
286
286
  // src/render.ts
287
287
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
288
288
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
289
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
289
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
290
290
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
291
291
  var renderState = {
292
292
  currentShadowRoot: null,
@@ -297,6 +297,13 @@ var renderState = {
297
297
  propertyMap: /* @__PURE__ */ new Map(),
298
298
  registry: typeof customElements !== "undefined" ? customElements : {}
299
299
  };
300
+ var clearRenderState = () => {
301
+ renderState.signalMap.clear();
302
+ renderState.callbackMap.clear();
303
+ renderState.propertyMap.clear();
304
+ renderState.fragmentMap.clear();
305
+ renderState.childrenMap.clear();
306
+ };
300
307
  var logPropertyWarning = (propName, element) => {
301
308
  console.warn(
302
309
  `Property "${propName}" does not exist on element:`,
@@ -304,19 +311,34 @@ var logPropertyWarning = (propName, element) => {
304
311
  "\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."
305
312
  );
306
313
  };
307
- var asNodeList = (value, parent) => {
314
+ var asNodeList = (value, parent, autoKey) => {
315
+ if (value === null || value === void 0) return [];
308
316
  if (typeof value === "string") return [new Text(value)];
309
- if (value instanceof DocumentFragment) return [...value.children];
317
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
318
+ if (value instanceof DocumentFragment) {
319
+ const children = Array.from(value.children);
320
+ if (autoKey !== void 0 && children.length > 0) {
321
+ const child = children[0];
322
+ if (child instanceof Element && child.getAttribute("key") === null) {
323
+ child.setAttribute("key", String(autoKey));
324
+ }
325
+ }
326
+ return children;
327
+ }
310
328
  if (Array.isArray(value)) {
311
329
  const nodeList = [];
312
330
  let count = 0;
313
331
  const keys = /* @__PURE__ */ new Set();
314
332
  for (const item of value) {
315
333
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
316
- const children = cachedItem ?? asNodeList(item, parent);
334
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
317
335
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
318
336
  renderState.childrenMap.set(item, children);
319
337
  }
338
+ if (!(item instanceof DocumentFragment)) {
339
+ nodeList.push(...children);
340
+ continue;
341
+ }
320
342
  if (children.length > 1) {
321
343
  console.error(
322
344
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -369,73 +391,68 @@ var processValue = (value) => {
369
391
  renderState.callbackMap.set(uniqueKey, value);
370
392
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
371
393
  }
372
- return String(value);
394
+ return value === null || value === void 0 ? "" : String(value);
373
395
  };
374
396
  var evaluateBindings = (element, fragment) => {
375
- for (const child of [...element.childNodes]) {
397
+ for (const child of Array.from(element.childNodes)) {
376
398
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
377
399
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
378
- const nextSibling = child.nextSibling;
379
- const prevSibling = child.previousSibling;
380
- textList.forEach((text, i) => {
400
+ const allInitialChildren = [];
401
+ const signalEntries = [];
402
+ let signalIndex = 0;
403
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
404
+ textList.forEach((text) => {
381
405
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
382
406
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
383
407
  const newValue = signal !== void 0 ? signal() : text;
384
- const initialChildren = asNodeList(newValue, element);
385
- if (i === 0) {
386
- child.replaceWith(...initialChildren);
387
- } else {
388
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
389
- if (endAnchor2 !== null) {
390
- endAnchor2.before(...initialChildren);
391
- } else {
392
- element.append(...initialChildren);
393
- }
408
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
409
+ const initialChildren = asNodeList(newValue, element, autoKey);
410
+ allInitialChildren.push(...initialChildren);
411
+ if (uniqueKey !== void 0 && signal !== void 0) {
412
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
394
413
  }
395
- if (uniqueKey === void 0) return;
414
+ });
415
+ child.replaceWith(...allInitialChildren);
416
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
417
+ const firstChild = initialChildren[0];
418
+ const lastChild = initialChildren[initialChildren.length - 1];
419
+ if (firstChild === void 0) return;
396
420
  const startAnchor = document.createComment(`${uniqueKey}:start`);
397
- if (prevSibling !== null) {
398
- prevSibling.after(startAnchor);
399
- } else {
400
- element.prepend(startAnchor);
401
- }
421
+ firstChild.before(startAnchor);
402
422
  const endAnchor = document.createComment(`${uniqueKey}:end`);
403
- if (nextSibling !== null) {
404
- nextSibling.before(endAnchor);
405
- } else {
406
- element.append(endAnchor);
407
- }
423
+ lastChild.after(endAnchor);
408
424
  const bindText = (node, signal2) => {
409
425
  createEffect(({ destroy }) => {
410
426
  const result = signal2();
411
427
  if (Array.isArray(result)) {
412
428
  destroy();
413
- bindArray(signal2);
429
+ bindArray(signal2, autoKey);
414
430
  return;
415
431
  }
416
432
  if (result instanceof DocumentFragment) {
417
433
  destroy();
418
- bindFragment(signal2);
434
+ bindFragment(signal2, initialChildren, autoKey);
419
435
  return;
420
436
  }
421
- node.data = result === null ? "" : String(result);
437
+ node.data = result === null || result === void 0 ? "" : String(result);
422
438
  });
423
439
  };
424
- const bindArray = (signal2) => {
440
+ const bindArray = (signal2, autoKey2) => {
425
441
  createEffect(
426
442
  ({ lastValue: oldChildren, destroy }) => {
427
443
  const result = signal2();
428
- const newChildren = asNodeList(result, element);
429
- const firstChild = newChildren[0];
430
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
431
- destroy();
432
- bindFragment(signal2);
433
- return;
434
- }
435
- if (newChildren.length === 1 && firstChild instanceof Text) {
436
- destroy();
437
- bindText(firstChild, signal2);
438
- return;
444
+ const newChildren = asNodeList(result, element, autoKey2);
445
+ const firstChild2 = newChildren[0];
446
+ if (!Array.isArray(result)) {
447
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
448
+ while (startAnchor.nextSibling !== endAnchor) {
449
+ startAnchor.nextSibling?.remove();
450
+ }
451
+ startAnchor.after(firstChild2);
452
+ destroy();
453
+ bindText(firstChild2, signal2);
454
+ return;
455
+ }
439
456
  }
440
457
  while (startAnchor.nextSibling !== endAnchor) {
441
458
  startAnchor.nextSibling?.remove();
@@ -445,16 +462,15 @@ var evaluateBindings = (element, fragment) => {
445
462
  for (const persistedChild of oldChildren) {
446
463
  if (persistedChild instanceof Element) {
447
464
  const key = persistedChild.getAttribute("key");
448
- if (key === null) continue;
449
465
  const newChild = queryChildren(newChildren, `[key="${key}"]`);
450
466
  if (newChild === null) {
451
467
  persistedChild.remove();
452
468
  continue;
453
469
  }
454
- for (const attr of [...persistedChild.attributes]) {
470
+ for (const attr of Array.from(persistedChild.attributes)) {
455
471
  if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
456
472
  }
457
- for (const newAttr of [...newChild.attributes]) {
473
+ for (const newAttr of Array.from(newChild.attributes)) {
458
474
  const oldAttrValue = persistedChild.getAttribute(newAttr.name);
459
475
  if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
460
476
  persistedChild.setAttribute(newAttr.name, newAttr.value);
@@ -467,40 +483,52 @@ var evaluateBindings = (element, fragment) => {
467
483
  null
468
484
  );
469
485
  };
470
- const bindFragment = (signal2) => {
486
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
471
487
  const initialFragment = signal2();
472
- renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
488
+ const firstInitialChild = initialChildren2[0];
489
+ if (firstInitialChild instanceof Element) {
490
+ renderState.childrenMap.set(initialFragment, initialChildren2);
491
+ }
473
492
  createEffect(({ destroy }) => {
474
493
  const result = signal2();
475
- const cachedChildren = renderState.childrenMap.get(initialFragment);
476
- const children = cachedChildren ?? asNodeList(result, element);
494
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
495
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
496
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
497
+ renderState.childrenMap.set(result, children);
498
+ }
477
499
  if (Array.isArray(result)) {
478
500
  destroy();
479
- bindArray(signal2);
501
+ bindArray(signal2, autoKey2);
480
502
  return;
481
503
  }
482
- if (result instanceof Text) {
483
- const children2 = asNodeList(result, element);
484
- const text2 = children2[0];
504
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
505
+ while (startAnchor.nextSibling !== endAnchor) {
506
+ startAnchor.nextSibling?.remove();
507
+ }
508
+ const children2 = asNodeList(result, element, autoKey2);
509
+ const text = children2[0];
510
+ startAnchor.after(text);
485
511
  destroy();
486
- bindText(text2, signal2);
512
+ bindText(text, signal2);
487
513
  return;
488
514
  }
489
515
  while (startAnchor.nextSibling !== endAnchor) {
490
516
  startAnchor.nextSibling?.remove();
491
517
  }
518
+ if (result === null || result === void 0) {
519
+ return;
520
+ }
492
521
  startAnchor.after(...children);
493
522
  });
494
523
  };
495
- if (signal !== void 0) {
496
- if (Array.isArray(newValue)) {
497
- bindArray(signal);
498
- } else if (initialChildren instanceof DocumentFragment) {
499
- bindFragment(signal);
500
- } else {
501
- const initialChild = initialChildren[0];
502
- bindText(initialChild, signal);
503
- }
524
+ const currentValue = signal();
525
+ if (Array.isArray(currentValue)) {
526
+ bindArray(signal, autoKey);
527
+ } else if (currentValue instanceof DocumentFragment) {
528
+ bindFragment(signal, initialChildren, autoKey);
529
+ } else {
530
+ const initialChild = initialChildren[0];
531
+ bindText(initialChild, signal);
504
532
  }
505
533
  });
506
534
  }
@@ -511,7 +539,7 @@ var evaluateBindings = (element, fragment) => {
511
539
  child.replaceWith(childFragment);
512
540
  }
513
541
  } else if (child instanceof Element) {
514
- for (const attr of [...child.attributes]) {
542
+ for (const attr of Array.from(child.attributes)) {
515
543
  const attrName = attr.name;
516
544
  if (SIGNAL_BINDING_REGEX.test(attr.value)) {
517
545
  const textList = attr.value.split(SIGNAL_BINDING_REGEX);
@@ -537,7 +565,6 @@ var evaluateBindings = (element, fragment) => {
537
565
  if (newText !== prevText) child.setAttribute(attrName, newText);
538
566
  }
539
567
  if (attrName.startsWith("prop-id:")) {
540
- if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
541
568
  const propId = attrName.replace("prop-id:", "");
542
569
  const propName = renderState.propertyMap.get(propId);
543
570
  if (propName === void 0) {
@@ -572,9 +599,7 @@ var evaluateBindings = (element, fragment) => {
572
599
  child.__customCallbackFns.set(uniqueKey, callback);
573
600
  }
574
601
  }
575
- if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
576
- child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
577
- } else if (attrName.startsWith("prop-id:")) {
602
+ if (attrName.startsWith("prop-id:")) {
578
603
  child.removeAttribute(attrName);
579
604
  const propId = attrName.replace("prop-id:", "");
580
605
  const propName = renderState.propertyMap.get(propId);
@@ -587,6 +612,8 @@ var evaluateBindings = (element, fragment) => {
587
612
  }
588
613
  if (!(propName in child)) logPropertyWarning(propName, child);
589
614
  child[propName] = child.__customCallbackFns.get(uniqueKey);
615
+ } else {
616
+ child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
590
617
  }
591
618
  });
592
619
  } else if (attrName.startsWith("prop-id:")) {
@@ -616,7 +643,7 @@ var html = (strings, ...values) => {
616
643
  } else {
617
644
  value = processValue(value);
618
645
  }
619
- innerHTML2 += str + String(value === null ? "" : value);
646
+ innerHTML2 += str + String(value);
620
647
  return innerHTML2;
621
648
  }, "");
622
649
  if (isServer) return innerHTML;
@@ -637,7 +664,7 @@ var html = (strings, ...values) => {
637
664
  evaluateBindings(fragment, fragment);
638
665
  return fragment;
639
666
  };
640
- var adoptedStylesSupported = typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
667
+ var isAdoptedStylesSupported = () => typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
641
668
  var isCSSStyleSheet = (stylesheet) => {
642
669
  return typeof CSSStyleSheet !== "undefined" && stylesheet instanceof CSSStyleSheet;
643
670
  };
@@ -661,7 +688,7 @@ var css = (strings, ...values) => {
661
688
  if (isServer) {
662
689
  return cssText;
663
690
  }
664
- const stylesheet = adoptedStylesSupported ? new CSSStyleSheet() : document.createElement("style");
691
+ const stylesheet = isAdoptedStylesSupported() ? new CSSStyleSheet() : document.createElement("style");
665
692
  const textList = cssText.split(signalBindingRegex);
666
693
  createEffect(() => {
667
694
  const newCSSTextList = [];
@@ -768,8 +795,9 @@ var customElement = (render, options) => {
768
795
  #observer = options?.observedAttributes !== void 0 ? null : new MutationObserver((mutations) => {
769
796
  for (const mutation of mutations) {
770
797
  const attrName = mutation.attributeName;
771
- if (mutation.type !== "attributes" || attrName === null) continue;
772
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
798
+ if (!(attrName in this.#attrSignals)) {
799
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
800
+ }
773
801
  const [getter, setter] = this.#attrSignals[attrName];
774
802
  const oldValue = getter();
775
803
  const newValue = this.getAttribute(attrName);
@@ -869,7 +897,9 @@ You must set an initial value before calling a property signal's getter.
869
897
  {},
870
898
  {
871
899
  get: (_, prop) => {
872
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
900
+ if (!(prop in this.#attrSignals)) {
901
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
902
+ }
873
903
  const [getter] = this.#attrSignals[prop];
874
904
  const setter = (newValue) => this.setAttribute(prop, newValue);
875
905
  return [getter, setter];
@@ -938,9 +968,7 @@ You must set an initial value before calling a property signal's getter.
938
968
  constructor() {
939
969
  try {
940
970
  super();
941
- if (!Object.prototype.hasOwnProperty.call(this, "__customCallbackFns")) {
942
- this.__customCallbackFns = /* @__PURE__ */ new Map();
943
- }
971
+ this.__customCallbackFns = /* @__PURE__ */ new Map();
944
972
  for (const attr of this.attributes) {
945
973
  this.#attrSignals[attr.name] = createSignal(attr.value);
946
974
  }
@@ -951,12 +979,13 @@ You must set an initial value before calling a property signal's getter.
951
979
  { cause: error }
952
980
  );
953
981
  console.error(_error);
954
- throw _error;
955
982
  }
956
983
  }
957
984
  connectedCallback() {
958
985
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
959
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
986
+ if (!(attrName in this.#attrSignals)) {
987
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
988
+ }
960
989
  const propName = attr.prop;
961
990
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
962
991
  let busy = false;
@@ -973,6 +1002,14 @@ You must set an initial value before calling a property signal's getter.
973
1002
  busy = false;
974
1003
  });
975
1004
  }
1005
+ for (const attrName of Object.keys(this.#attrSignals)) {
1006
+ const signal = this.#attrSignals[attrName];
1007
+ const [getter, setter] = signal;
1008
+ const currentValue = this.getAttribute(attrName);
1009
+ if (getter() !== currentValue) {
1010
+ setter(currentValue);
1011
+ }
1012
+ }
976
1013
  if (this.#observer !== null) {
977
1014
  this.#observer.observe(this, { attributes: true });
978
1015
  }
@@ -1140,6 +1177,8 @@ var createRegistry = (args) => {
1140
1177
  };
1141
1178
  };
1142
1179
  export {
1180
+ clearRenderState,
1181
+ clearServerCss,
1143
1182
  clientOnlyCallback,
1144
1183
  createEffect,
1145
1184
  createRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "2.4.3",
3
+ "version": "2.4.5-next.1776736743",
4
4
  "description": "A lightweight, functional web components library that brings the power of signals to your UI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -15,7 +15,8 @@
15
15
  "author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/Thunder-Solutions/Thunderous"
18
+ "url": "https://github.com/Thunder-Solutions/Thunderous",
19
+ "directory": "packages/thunderous"
19
20
  },
20
21
  "keywords": [
21
22
  "thunderous",
@@ -27,7 +28,7 @@
27
28
  "bugs": {
28
29
  "url": "https://github.com/Thunder-Solutions/Thunderous/issues"
29
30
  },
30
- "homepage": "https://github.com/Thunder-Solutions/Thunderous#readme",
31
+ "homepage": "https://thunderous.dev",
31
32
  "license": "MIT",
32
33
  "peerDependencies": {
33
34
  "@webcomponents/scoped-custom-element-registry": "^0.0.10"
@@ -37,19 +38,29 @@
37
38
  "optional": true
38
39
  }
39
40
  },
41
+ "devDependencies": {
42
+ "@playwright/test": "^1.58.2",
43
+ "@vitest/browser": "^4.1.4",
44
+ "@vitest/browser-playwright": "^4.1.4",
45
+ "@vitest/coverage-istanbul": "^4.1.4",
46
+ "istanbul-merge": "^2.0.0",
47
+ "nyc": "^18.0.0",
48
+ "vitest": "^4.1.4"
49
+ },
40
50
  "scripts": {
41
51
  "demo": "cd demo && npm start",
42
- "demo:ssr": "cd demo && npm run ssr",
52
+ "demo:ssr": "cd demo && pnpm ssr",
43
53
  "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
44
- "test": "npm run test:server && npm run test:client",
45
- "test:server": "find src/__test__/server -name '*.test.ts' | xargs c8 tsx --test",
46
- "test:client": "PLAYWRIGHT_BROWSERS_PATH=../../.browsers playwright test",
54
+ "clean": "rm -rf dist",
55
+ "test": "pnpm test:server & pnpm test:client && pnpm coverage",
56
+ "test:server": "vitest run --config vitest.server.config.ts --coverage",
57
+ "test:client": "vitest run --config vitest.client.config.ts --coverage",
58
+ "coverage": "istanbul-merge --out ./coverage/merged/coverage-final.json ./coverage/server/coverage-final.json ./coverage/client/coverage-final.json && nyc report && nyc check-coverage",
47
59
  "typecheck": "tsc --noEmit",
48
60
  "lint": "eslint .",
49
61
  "lint:fix": "eslint . --fix",
50
62
  "format": "prettier --check . --ignore-path ../../.gitignore",
51
63
  "format:fix": "prettier --write . --ignore-path ../../.gitignore",
52
- "preversion": "npm run typecheck && npm run lint && npm test && npm run build",
53
64
  "version": "node postversion.js"
54
65
  }
55
66
  }