thunderous 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -578,8 +578,18 @@ var serverDefineFns = /* @__PURE__ */ new Set();
578
578
  var onServerDefine = (fn) => {
579
579
  serverDefineFns.add(fn);
580
580
  };
581
- var serverDefine = ({ tagName, serverRender, options, scopedRegistry, parentRegistry }) => {
581
+ var serverDefine = ({
582
+ tagName,
583
+ serverRender,
584
+ options,
585
+ elementResult,
586
+ scopedRegistry,
587
+ parentRegistry
588
+ }) => {
582
589
  if (parentRegistry !== void 0) {
590
+ if (parentRegistry.getTagName(elementResult) !== tagName.toUpperCase()) {
591
+ parentRegistry.define(tagName, elementResult);
592
+ }
583
593
  parentRegistry.__serverRenderOpts.set(tagName, { serverRender, ...options });
584
594
  if (parentRegistry.scoped) return;
585
595
  }
@@ -712,21 +722,82 @@ var logValueError = (value) => {
712
722
  value
713
723
  );
714
724
  };
725
+ var arrayToDocumentFragment = (array, parent) => {
726
+ const documentFragment = new DocumentFragment();
727
+ let count = 0;
728
+ const keys = /* @__PURE__ */ new Set();
729
+ for (const item of array) {
730
+ const node = getNewNode(item, parent).cloneNode(true);
731
+ if (node instanceof DocumentFragment) {
732
+ const child = node.firstElementChild;
733
+ if (node.children.length > 1) {
734
+ console.error(
735
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
736
+ parent
737
+ );
738
+ }
739
+ if (child === null) continue;
740
+ let key = child.getAttribute("key");
741
+ if (key === null) {
742
+ console.warn(
743
+ "When rendering arrays, a `key` attribute should be provided on each child element. An index was automatically applied, but this could result in unexpected behavior:",
744
+ child
745
+ );
746
+ key = String(count);
747
+ child.setAttribute("key", key);
748
+ }
749
+ if (keys.has(key)) {
750
+ console.warn(
751
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
752
+ child
753
+ );
754
+ }
755
+ keys.add(key);
756
+ count++;
757
+ }
758
+ documentFragment.append(node);
759
+ }
760
+ return documentFragment;
761
+ };
762
+ var getNewNode = (value, parent) => {
763
+ if (typeof value === "string") return new Text(value);
764
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
765
+ if (value instanceof DocumentFragment) return value;
766
+ return new Text("");
767
+ };
715
768
  var html = (strings, ...values) => {
716
769
  let innerHTML = "";
717
770
  const signalMap = /* @__PURE__ */ new Map();
718
- strings.forEach((string, i) => {
719
- let value = values[i] ?? "";
771
+ const processValue = (value) => {
772
+ if (!isServer && value instanceof DocumentFragment) {
773
+ const tempDiv = document.createElement("div");
774
+ tempDiv.append(value.cloneNode(true));
775
+ return tempDiv.innerHTML;
776
+ }
720
777
  if (typeof value === "function") {
778
+ const getter = value;
721
779
  const uniqueKey = crypto.randomUUID();
722
- signalMap.set(uniqueKey, value);
723
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
780
+ signalMap.set(uniqueKey, getter);
781
+ let result = getter();
782
+ if (Array.isArray(result)) {
783
+ result = result.map((item) => processValue(item)).join("");
784
+ }
785
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
724
786
  }
725
787
  if (typeof value === "object" && value !== null) {
726
788
  logValueError(value);
727
- value = "";
789
+ return "";
790
+ }
791
+ return String(value);
792
+ };
793
+ strings.forEach((string, i) => {
794
+ let value = values[i] ?? "";
795
+ if (Array.isArray(value)) {
796
+ value = value.map((item) => processValue(item)).join("");
797
+ } else {
798
+ value = processValue(value);
728
799
  }
729
- innerHTML += string + String(value);
800
+ innerHTML += string + String(value === null ? "" : value);
730
801
  });
731
802
  if (isServer) {
732
803
  return innerHTML;
@@ -738,24 +809,40 @@ var html = (strings, ...values) => {
738
809
  for (const child of element.childNodes) {
739
810
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
740
811
  const textList = child.data.split(signalBindingRegex);
812
+ const sibling = child.nextSibling;
741
813
  textList.forEach((text, i) => {
742
814
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
743
815
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
744
816
  const newValue = signal !== void 0 ? signal() : text;
745
- const newNode = (() => {
746
- if (typeof newValue === "string") return new Text(newValue);
747
- if (newValue instanceof DocumentFragment) return newValue;
748
- return new Text("");
749
- })();
817
+ const newNode = getNewNode(newValue, element);
750
818
  if (i === 0) {
751
819
  child.replaceWith(newNode);
752
820
  } else {
753
- element.insertBefore(newNode, child.nextSibling);
821
+ element.insertBefore(newNode, sibling);
754
822
  }
755
823
  if (signal !== void 0 && newNode instanceof Text) {
756
824
  createEffect(() => {
757
825
  newNode.data = signal();
758
826
  });
827
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
828
+ createEffect(() => {
829
+ const result = signal();
830
+ const nextNode = getNewNode(result, element);
831
+ if (nextNode instanceof Text) {
832
+ throw new TypeError(
833
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
834
+ );
835
+ }
836
+ let lastSibling = element.lastChild;
837
+ for (const child2 of nextNode.children) {
838
+ const key = child2.getAttribute("key");
839
+ const matchingNode = element.querySelector(`[key="${key}"]`);
840
+ if (matchingNode === null) continue;
841
+ lastSibling = matchingNode.nextSibling;
842
+ child2.replaceWith(matchingNode);
843
+ }
844
+ element.insertBefore(nextNode, lastSibling);
845
+ });
759
846
  }
760
847
  });
761
848
  }
@@ -870,7 +957,8 @@ var customElement = (render, options) => {
870
957
  serverRender,
871
958
  options: allOptions,
872
959
  scopedRegistry,
873
- parentRegistry: _registry2
960
+ parentRegistry: _registry2,
961
+ elementResult: this
874
962
  });
875
963
  return this;
876
964
  },
@@ -1177,6 +1265,7 @@ You must set an initial value before calling a property signal's getter.
1177
1265
  var createRegistry = (args) => {
1178
1266
  const { scoped = false } = args ?? {};
1179
1267
  const customElementMap = /* @__PURE__ */ new Map();
1268
+ const elementResultMap = /* @__PURE__ */ new Map();
1180
1269
  const customElementTags = /* @__PURE__ */ new Set();
1181
1270
  const nativeRegistry = (() => {
1182
1271
  if (isServer) return;
@@ -1188,30 +1277,42 @@ var createRegistry = (args) => {
1188
1277
  __serverRenderOpts: /* @__PURE__ */ new Map(),
1189
1278
  define(tagName, ElementResult, options) {
1190
1279
  const isResult = "eject" in ElementResult;
1191
- if (isServer) {
1192
- if (isResult) ElementResult.register(this).define(tagName, options);
1280
+ const upperCaseTagName = tagName.toUpperCase();
1281
+ if (customElementTags.has(upperCaseTagName)) {
1282
+ console.warn(`Custom element tag name "${upperCaseTagName}" was already defined. Skipping...`);
1193
1283
  return this;
1194
1284
  }
1195
- const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1196
- if (customElementMap.has(CustomElement)) {
1197
- console.warn(`Custom element class "${CustomElement.constructor.name}" was already defined. Skipping...`);
1198
- return this;
1285
+ if (isResult) {
1286
+ if (elementResultMap.has(ElementResult)) {
1287
+ console.warn(`${upperCaseTagName} was already defined. Skipping...`);
1288
+ return this;
1289
+ }
1199
1290
  }
1200
- if (customElementTags.has(tagName)) {
1201
- console.warn(`Custom element tag name "${tagName}" was already defined. Skipping...`);
1202
- return this;
1291
+ if (!isServer) {
1292
+ const CustomElement2 = isResult ? ElementResult.eject() : ElementResult;
1293
+ if (customElementMap.has(CustomElement2)) {
1294
+ console.warn(`Custom element class "${CustomElement2.constructor.name}" was already defined. Skipping...`);
1295
+ return this;
1296
+ }
1297
+ customElementMap.set(CustomElement2, upperCaseTagName);
1203
1298
  }
1204
- customElementMap.set(CustomElement, tagName.toUpperCase());
1205
- customElementTags.add(tagName);
1206
- if (CustomElement === void 0) {
1207
- console.error(`Custom element class for tag name "${tagName}" was not found. You must register it first.`);
1299
+ if (isResult) elementResultMap.set(ElementResult, upperCaseTagName);
1300
+ customElementTags.add(upperCaseTagName);
1301
+ if (isServer) {
1302
+ if (isResult) ElementResult.register(this).define(tagName, options);
1208
1303
  return this;
1209
1304
  }
1305
+ const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1210
1306
  nativeRegistry?.define(tagName, CustomElement, options);
1211
1307
  return this;
1212
1308
  },
1213
1309
  getTagName: (ElementResult) => {
1214
- const CustomElement = "eject" in ElementResult ? ElementResult.eject() : ElementResult;
1310
+ const isResult = "eject" in ElementResult;
1311
+ if (isServer) {
1312
+ if (isResult) return elementResultMap.get(ElementResult);
1313
+ return;
1314
+ }
1315
+ const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1215
1316
  return customElementMap.get(CustomElement);
1216
1317
  },
1217
1318
  getAllTagNames: () => Array.from(customElementTags),
package/dist/index.d.cts CHANGED
@@ -645,18 +645,18 @@ type ServerRenderFunction = (args: RenderArgs<CustomElementProps>) => string;
645
645
 
646
646
  type ServerRenderOptions = { serverRender: ServerRenderFunction } & RenderOptions;
647
647
 
648
- type ServerDefineFn = (tagName: string, htmlString: string) => void;
648
+ type ServerDefineFn = (tagName: TagName, htmlString: string) => void;
649
649
 
650
650
  type RegistryResult = {
651
- __serverCss: Map<string, string[]>;
652
- __serverRenderOpts: Map<string, ServerRenderOptions>;
651
+ __serverCss: Map<TagName, string[]>;
652
+ __serverRenderOpts: Map<TagName, ServerRenderOptions>;
653
653
  define: (
654
654
  tagName: TagName,
655
655
  CustomElement: CustomElementConstructor | ElementResult,
656
656
  options?: ElementDefinitionOptions,
657
657
  ) => RegistryResult;
658
- getTagName: (CustomElement: CustomElementConstructor | ElementResult) => string | undefined;
659
- getAllTagNames: () => string[];
658
+ getTagName: (CustomElement: CustomElementConstructor | ElementResult) => TagName | undefined;
659
+ getAllTagNames: () => TagName[];
660
660
  eject: () => CustomElementRegistry;
661
661
  scoped: boolean;
662
662
  };
package/dist/index.d.ts CHANGED
@@ -645,18 +645,18 @@ type ServerRenderFunction = (args: RenderArgs<CustomElementProps>) => string;
645
645
 
646
646
  type ServerRenderOptions = { serverRender: ServerRenderFunction } & RenderOptions;
647
647
 
648
- type ServerDefineFn = (tagName: string, htmlString: string) => void;
648
+ type ServerDefineFn = (tagName: TagName, htmlString: string) => void;
649
649
 
650
650
  type RegistryResult = {
651
- __serverCss: Map<string, string[]>;
652
- __serverRenderOpts: Map<string, ServerRenderOptions>;
651
+ __serverCss: Map<TagName, string[]>;
652
+ __serverRenderOpts: Map<TagName, ServerRenderOptions>;
653
653
  define: (
654
654
  tagName: TagName,
655
655
  CustomElement: CustomElementConstructor | ElementResult,
656
656
  options?: ElementDefinitionOptions,
657
657
  ) => RegistryResult;
658
- getTagName: (CustomElement: CustomElementConstructor | ElementResult) => string | undefined;
659
- getAllTagNames: () => string[];
658
+ getTagName: (CustomElement: CustomElementConstructor | ElementResult) => TagName | undefined;
659
+ getAllTagNames: () => TagName[];
660
660
  eject: () => CustomElementRegistry;
661
661
  scoped: boolean;
662
662
  };
package/dist/index.js CHANGED
@@ -543,8 +543,18 @@ var serverDefineFns = /* @__PURE__ */ new Set();
543
543
  var onServerDefine = (fn) => {
544
544
  serverDefineFns.add(fn);
545
545
  };
546
- var serverDefine = ({ tagName, serverRender, options, scopedRegistry, parentRegistry }) => {
546
+ var serverDefine = ({
547
+ tagName,
548
+ serverRender,
549
+ options,
550
+ elementResult,
551
+ scopedRegistry,
552
+ parentRegistry
553
+ }) => {
547
554
  if (parentRegistry !== void 0) {
555
+ if (parentRegistry.getTagName(elementResult) !== tagName.toUpperCase()) {
556
+ parentRegistry.define(tagName, elementResult);
557
+ }
548
558
  parentRegistry.__serverRenderOpts.set(tagName, { serverRender, ...options });
549
559
  if (parentRegistry.scoped) return;
550
560
  }
@@ -677,21 +687,82 @@ var logValueError = (value) => {
677
687
  value
678
688
  );
679
689
  };
690
+ var arrayToDocumentFragment = (array, parent) => {
691
+ const documentFragment = new DocumentFragment();
692
+ let count = 0;
693
+ const keys = /* @__PURE__ */ new Set();
694
+ for (const item of array) {
695
+ const node = getNewNode(item, parent).cloneNode(true);
696
+ if (node instanceof DocumentFragment) {
697
+ const child = node.firstElementChild;
698
+ if (node.children.length > 1) {
699
+ console.error(
700
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
701
+ parent
702
+ );
703
+ }
704
+ if (child === null) continue;
705
+ let key = child.getAttribute("key");
706
+ if (key === null) {
707
+ console.warn(
708
+ "When rendering arrays, a `key` attribute should be provided on each child element. An index was automatically applied, but this could result in unexpected behavior:",
709
+ child
710
+ );
711
+ key = String(count);
712
+ child.setAttribute("key", key);
713
+ }
714
+ if (keys.has(key)) {
715
+ console.warn(
716
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
717
+ child
718
+ );
719
+ }
720
+ keys.add(key);
721
+ count++;
722
+ }
723
+ documentFragment.append(node);
724
+ }
725
+ return documentFragment;
726
+ };
727
+ var getNewNode = (value, parent) => {
728
+ if (typeof value === "string") return new Text(value);
729
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
730
+ if (value instanceof DocumentFragment) return value;
731
+ return new Text("");
732
+ };
680
733
  var html = (strings, ...values) => {
681
734
  let innerHTML = "";
682
735
  const signalMap = /* @__PURE__ */ new Map();
683
- strings.forEach((string, i) => {
684
- let value = values[i] ?? "";
736
+ const processValue = (value) => {
737
+ if (!isServer && value instanceof DocumentFragment) {
738
+ const tempDiv = document.createElement("div");
739
+ tempDiv.append(value.cloneNode(true));
740
+ return tempDiv.innerHTML;
741
+ }
685
742
  if (typeof value === "function") {
743
+ const getter = value;
686
744
  const uniqueKey = crypto.randomUUID();
687
- signalMap.set(uniqueKey, value);
688
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
745
+ signalMap.set(uniqueKey, getter);
746
+ let result = getter();
747
+ if (Array.isArray(result)) {
748
+ result = result.map((item) => processValue(item)).join("");
749
+ }
750
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
689
751
  }
690
752
  if (typeof value === "object" && value !== null) {
691
753
  logValueError(value);
692
- value = "";
754
+ return "";
755
+ }
756
+ return String(value);
757
+ };
758
+ strings.forEach((string, i) => {
759
+ let value = values[i] ?? "";
760
+ if (Array.isArray(value)) {
761
+ value = value.map((item) => processValue(item)).join("");
762
+ } else {
763
+ value = processValue(value);
693
764
  }
694
- innerHTML += string + String(value);
765
+ innerHTML += string + String(value === null ? "" : value);
695
766
  });
696
767
  if (isServer) {
697
768
  return innerHTML;
@@ -703,24 +774,40 @@ var html = (strings, ...values) => {
703
774
  for (const child of element.childNodes) {
704
775
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
705
776
  const textList = child.data.split(signalBindingRegex);
777
+ const sibling = child.nextSibling;
706
778
  textList.forEach((text, i) => {
707
779
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
708
780
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
709
781
  const newValue = signal !== void 0 ? signal() : text;
710
- const newNode = (() => {
711
- if (typeof newValue === "string") return new Text(newValue);
712
- if (newValue instanceof DocumentFragment) return newValue;
713
- return new Text("");
714
- })();
782
+ const newNode = getNewNode(newValue, element);
715
783
  if (i === 0) {
716
784
  child.replaceWith(newNode);
717
785
  } else {
718
- element.insertBefore(newNode, child.nextSibling);
786
+ element.insertBefore(newNode, sibling);
719
787
  }
720
788
  if (signal !== void 0 && newNode instanceof Text) {
721
789
  createEffect(() => {
722
790
  newNode.data = signal();
723
791
  });
792
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
793
+ createEffect(() => {
794
+ const result = signal();
795
+ const nextNode = getNewNode(result, element);
796
+ if (nextNode instanceof Text) {
797
+ throw new TypeError(
798
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
799
+ );
800
+ }
801
+ let lastSibling = element.lastChild;
802
+ for (const child2 of nextNode.children) {
803
+ const key = child2.getAttribute("key");
804
+ const matchingNode = element.querySelector(`[key="${key}"]`);
805
+ if (matchingNode === null) continue;
806
+ lastSibling = matchingNode.nextSibling;
807
+ child2.replaceWith(matchingNode);
808
+ }
809
+ element.insertBefore(nextNode, lastSibling);
810
+ });
724
811
  }
725
812
  });
726
813
  }
@@ -835,7 +922,8 @@ var customElement = (render, options) => {
835
922
  serverRender,
836
923
  options: allOptions,
837
924
  scopedRegistry,
838
- parentRegistry: _registry2
925
+ parentRegistry: _registry2,
926
+ elementResult: this
839
927
  });
840
928
  return this;
841
929
  },
@@ -1142,6 +1230,7 @@ You must set an initial value before calling a property signal's getter.
1142
1230
  var createRegistry = (args) => {
1143
1231
  const { scoped = false } = args ?? {};
1144
1232
  const customElementMap = /* @__PURE__ */ new Map();
1233
+ const elementResultMap = /* @__PURE__ */ new Map();
1145
1234
  const customElementTags = /* @__PURE__ */ new Set();
1146
1235
  const nativeRegistry = (() => {
1147
1236
  if (isServer) return;
@@ -1153,30 +1242,42 @@ var createRegistry = (args) => {
1153
1242
  __serverRenderOpts: /* @__PURE__ */ new Map(),
1154
1243
  define(tagName, ElementResult, options) {
1155
1244
  const isResult = "eject" in ElementResult;
1156
- if (isServer) {
1157
- if (isResult) ElementResult.register(this).define(tagName, options);
1245
+ const upperCaseTagName = tagName.toUpperCase();
1246
+ if (customElementTags.has(upperCaseTagName)) {
1247
+ console.warn(`Custom element tag name "${upperCaseTagName}" was already defined. Skipping...`);
1158
1248
  return this;
1159
1249
  }
1160
- const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1161
- if (customElementMap.has(CustomElement)) {
1162
- console.warn(`Custom element class "${CustomElement.constructor.name}" was already defined. Skipping...`);
1163
- return this;
1250
+ if (isResult) {
1251
+ if (elementResultMap.has(ElementResult)) {
1252
+ console.warn(`${upperCaseTagName} was already defined. Skipping...`);
1253
+ return this;
1254
+ }
1164
1255
  }
1165
- if (customElementTags.has(tagName)) {
1166
- console.warn(`Custom element tag name "${tagName}" was already defined. Skipping...`);
1167
- return this;
1256
+ if (!isServer) {
1257
+ const CustomElement2 = isResult ? ElementResult.eject() : ElementResult;
1258
+ if (customElementMap.has(CustomElement2)) {
1259
+ console.warn(`Custom element class "${CustomElement2.constructor.name}" was already defined. Skipping...`);
1260
+ return this;
1261
+ }
1262
+ customElementMap.set(CustomElement2, upperCaseTagName);
1168
1263
  }
1169
- customElementMap.set(CustomElement, tagName.toUpperCase());
1170
- customElementTags.add(tagName);
1171
- if (CustomElement === void 0) {
1172
- console.error(`Custom element class for tag name "${tagName}" was not found. You must register it first.`);
1264
+ if (isResult) elementResultMap.set(ElementResult, upperCaseTagName);
1265
+ customElementTags.add(upperCaseTagName);
1266
+ if (isServer) {
1267
+ if (isResult) ElementResult.register(this).define(tagName, options);
1173
1268
  return this;
1174
1269
  }
1270
+ const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1175
1271
  nativeRegistry?.define(tagName, CustomElement, options);
1176
1272
  return this;
1177
1273
  },
1178
1274
  getTagName: (ElementResult) => {
1179
- const CustomElement = "eject" in ElementResult ? ElementResult.eject() : ElementResult;
1275
+ const isResult = "eject" in ElementResult;
1276
+ if (isServer) {
1277
+ if (isResult) return elementResultMap.get(ElementResult);
1278
+ return;
1279
+ }
1280
+ const CustomElement = isResult ? ElementResult.eject() : ElementResult;
1180
1281
  return customElementMap.get(CustomElement);
1181
1282
  },
1182
1283
  getAllTagNames: () => Array.from(customElementTags),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,10 +31,11 @@
31
31
  "license": "MIT",
32
32
  "scripts": {
33
33
  "demo": "cd demo && npm start",
34
+ "demo:ssr": "cd demo && npm run ssr",
34
35
  "www": "cd www && npm run dev",
35
36
  "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
36
37
  "test": "find src -name '*.test.ts' | xargs c8 node --import tsx --test",
37
- "lint": "eslint ."
38
+ "lint": "tsc && eslint ."
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/dompurify": "^3.2.0",