ilha 0.4.0 → 0.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.
Files changed (2) hide show
  1. package/dist/index.js +136 -37
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,6 +4,18 @@ const __DEV__ = typeof process !== "undefined" ? process.env?.["NODE_ENV"] !== "
4
4
  function warn(msg) {
5
5
  if (__DEV__) console.warn(`[ilha] ${msg}`);
6
6
  }
7
+ function shallowEqualInput(a, b) {
8
+ if (Object.is(a, b)) return true;
9
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
10
+ const ak = Object.keys(a);
11
+ const bk = Object.keys(b);
12
+ if (ak.length !== bk.length) return false;
13
+ for (const k of ak) {
14
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
15
+ if (!Object.is(a[k], b[k])) return false;
16
+ }
17
+ return true;
18
+ }
7
19
  function syncAttributes(from, to) {
8
20
  for (const { name, value } of to.attributes) if (from.getAttribute(name) !== value) from.setAttribute(name, value);
9
21
  for (const { name } of Array.from(from.attributes)) if (!to.hasAttribute(name)) from.removeAttribute(name);
@@ -80,6 +92,7 @@ const RAW = Symbol("ilha.raw");
80
92
  const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
81
93
  const ISLAND = Symbol("ilha.island");
82
94
  const ISLAND_CALL = Symbol("ilha.islandCall");
95
+ const ISLAND_MOUNT_INTERNAL = Symbol("ilha.islandMountInternal");
83
96
  const SLOT_ATTR = "data-ilha-slot";
84
97
  const PROPS_ATTR = "data-ilha-props";
85
98
  const STATE_ATTR = "data-ilha-state";
@@ -605,6 +618,7 @@ var IlhaBuilder = class IlhaBuilder {
605
618
  const input = resolveInput(props);
606
619
  const state = buildPlainState(input);
607
620
  const results = deriveds.map((entry) => {
621
+ const prevSub = setActiveSub(void 0);
608
622
  try {
609
623
  return {
610
624
  key: entry.key,
@@ -619,6 +633,8 @@ var IlhaBuilder = class IlhaBuilder {
619
633
  key: entry.key,
620
634
  result: Promise.reject(err)
621
635
  };
636
+ } finally {
637
+ setActiveSub(prevSub);
622
638
  }
623
639
  });
624
640
  if (!results.some((r) => r.result instanceof Promise) || sync) {
@@ -633,12 +649,17 @@ var IlhaBuilder = class IlhaBuilder {
633
649
  value: r.result,
634
650
  error: void 0
635
651
  };
636
- const { html } = renderWithCtx(() => fn({
637
- state,
638
- derived,
639
- input
640
- }));
641
- return stylePrefix + html;
652
+ const prevSub = setActiveSub(void 0);
653
+ try {
654
+ const { html } = renderWithCtx(() => fn({
655
+ state,
656
+ derived,
657
+ input
658
+ }));
659
+ return stylePrefix + html;
660
+ } finally {
661
+ setActiveSub(prevSub);
662
+ }
642
663
  }
643
664
  return Promise.all(results.map(async (r) => {
644
665
  try {
@@ -663,20 +684,32 @@ var IlhaBuilder = class IlhaBuilder {
663
684
  })).then(async (resolved) => {
664
685
  const derived = {};
665
686
  for (const r of resolved) derived[r.key] = r.envelope;
666
- const { html } = await renderWithCtx(() => fn({
667
- state,
668
- derived,
669
- input
670
- }), void 0, true);
671
- return stylePrefix + html;
687
+ const prevSub = setActiveSub(void 0);
688
+ try {
689
+ const { html } = await renderWithCtx(() => fn({
690
+ state,
691
+ derived,
692
+ input
693
+ }), void 0, true);
694
+ return stylePrefix + html;
695
+ } finally {
696
+ setActiveSub(prevSub);
697
+ }
672
698
  });
673
699
  }
674
700
  function mountIsland(host, props) {
701
+ return mountIslandInternal(host, props).unmount;
702
+ }
703
+ function mountIslandInternal(host, props) {
704
+ const noop = {
705
+ unmount: () => {},
706
+ updateProps: () => {}
707
+ };
675
708
  if (__DEV__ && _mountedHosts) {
676
709
  if (_mountedHosts.has(host)) {
677
710
  warn(`mount(): this element is already mounted. Call the previous unmount() first to avoid memory leaks and duplicate event listeners.
678
711
  Element: ${host.outerHTML.slice(0, 120)}`);
679
- return () => {};
712
+ return noop;
680
713
  }
681
714
  _mountedHosts.add(host);
682
715
  }
@@ -688,7 +721,31 @@ Element: ${host.outerHTML.slice(0, 120)}`);
688
721
  warn("Failed to parse data-ilha-props — invalid JSON, falling back to empty props.");
689
722
  }
690
723
  }
691
- const input = resolveInput(props);
724
+ const inputSignal = signal(resolveInput(props));
725
+ const input = new Proxy({}, {
726
+ get(_t, key) {
727
+ return inputSignal()[key];
728
+ },
729
+ has(_t, key) {
730
+ return key in inputSignal();
731
+ },
732
+ ownKeys() {
733
+ const prevSub = setActiveSub(void 0);
734
+ try {
735
+ return Reflect.ownKeys(inputSignal());
736
+ } finally {
737
+ setActiveSub(prevSub);
738
+ }
739
+ },
740
+ getOwnPropertyDescriptor(_t, key) {
741
+ const prevSub = setActiveSub(void 0);
742
+ try {
743
+ return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
744
+ } finally {
745
+ setActiveSub(prevSub);
746
+ }
747
+ }
748
+ });
692
749
  let snapshotRaw;
693
750
  const rawState = host.getAttribute(STATE_ATTR);
694
751
  if (rawState) try {
@@ -758,7 +815,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
758
815
  mountedSlots.delete(id);
759
816
  }
760
817
  for (const [id, { island: childIsland, props }] of slotMap) {
761
- if (mountedSlots.has(id)) continue;
818
+ const existing = mountedSlots.get(id);
819
+ if (existing) {
820
+ existing.updateProps(props);
821
+ continue;
822
+ }
762
823
  const slotEl = findSlot(id);
763
824
  if (!slotEl) continue;
764
825
  let slotProps = props;
@@ -771,15 +832,19 @@ Element: ${host.outerHTML.slice(0, 120)}`);
771
832
  }
772
833
  }
773
834
  const prevSub = setActiveSub(void 0);
774
- let unmount;
835
+ let handle;
775
836
  try {
776
- unmount = childIsland.mount(slotEl, slotProps);
837
+ const internal = childIsland[ISLAND_MOUNT_INTERNAL];
838
+ handle = internal ? internal(slotEl, slotProps) : {
839
+ unmount: childIsland.mount(slotEl, slotProps),
840
+ updateProps: () => {}
841
+ };
777
842
  } finally {
778
843
  setActiveSub(prevSub);
779
844
  }
780
845
  mountedSlots.set(id, {
781
846
  el: slotEl,
782
- unmount
847
+ ...handle
783
848
  });
784
849
  }
785
850
  }
@@ -883,6 +948,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
883
948
  if (userCleanup) cleanups.push(userCleanup);
884
949
  }
885
950
  let initialized = false;
951
+ const initialRenderedHtml = initial.html;
886
952
  const stopRender = effect(() => {
887
953
  const { html: rendered, slots: newSlotMap } = renderWithCtx(() => fn({
888
954
  state,
@@ -892,7 +958,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
892
958
  const html = stylePrefix + rendered;
893
959
  if (!initialized) {
894
960
  initialized = true;
895
- return;
961
+ if (rendered === initialRenderedHtml) return;
896
962
  }
897
963
  detachListeners();
898
964
  stopBindings();
@@ -963,7 +1029,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
963
1029
  });
964
1030
  }
965
1031
  let tornDown = false;
966
- return () => {
1032
+ const unmount = () => {
967
1033
  if (tornDown) return;
968
1034
  tornDown = true;
969
1035
  if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
@@ -976,6 +1042,16 @@ Element: ${host.outerHTML.slice(0, 120)}`);
976
1042
  }
977
1043
  cleanups.forEach((c) => c());
978
1044
  };
1045
+ const updateProps = (nextProps) => {
1046
+ if (tornDown) return;
1047
+ const next = resolveInput(nextProps);
1048
+ if (shallowEqualInput(inputSignal(), next)) return;
1049
+ inputSignal(next);
1050
+ };
1051
+ return {
1052
+ unmount,
1053
+ updateProps
1054
+ };
979
1055
  }
980
1056
  /**
981
1057
  * Dual-mode island invocation:
@@ -999,6 +1075,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
999
1075
  };
1000
1076
  island.toString = (props) => renderToString(props, true);
1001
1077
  island.mount = (host, props) => mountIsland(host, props);
1078
+ island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
1002
1079
  island.key = (key) => {
1003
1080
  if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
1004
1081
  if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
@@ -1028,23 +1105,45 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1028
1105
  if (doState) for (const entry of states) snapshotData[entry.key] = plainState[entry.key]();
1029
1106
  if (doDerived) {
1030
1107
  const derivedResults = {};
1031
- for (const entry of deriveds) try {
1032
- const result = await Promise.resolve(entry.fn({
1033
- state: plainState,
1034
- input,
1035
- signal: new AbortController().signal
1036
- }));
1037
- derivedResults[entry.key] = {
1038
- loading: false,
1039
- value: result,
1040
- error: void 0
1041
- };
1042
- } catch (err) {
1043
- derivedResults[entry.key] = {
1044
- loading: false,
1045
- value: void 0,
1046
- error: err instanceof Error ? err.message : String(err)
1047
- };
1108
+ for (const entry of deriveds) {
1109
+ const prevSub = setActiveSub(void 0);
1110
+ let resultPromise;
1111
+ let syncError;
1112
+ let threw = false;
1113
+ try {
1114
+ resultPromise = entry.fn({
1115
+ state: plainState,
1116
+ input,
1117
+ signal: new AbortController().signal
1118
+ });
1119
+ } catch (err) {
1120
+ threw = true;
1121
+ syncError = err;
1122
+ } finally {
1123
+ setActiveSub(prevSub);
1124
+ }
1125
+ if (threw) {
1126
+ derivedResults[entry.key] = {
1127
+ loading: false,
1128
+ value: void 0,
1129
+ error: syncError instanceof Error ? syncError.message : String(syncError)
1130
+ };
1131
+ continue;
1132
+ }
1133
+ try {
1134
+ const result = await Promise.resolve(resultPromise);
1135
+ derivedResults[entry.key] = {
1136
+ loading: false,
1137
+ value: result,
1138
+ error: void 0
1139
+ };
1140
+ } catch (err) {
1141
+ derivedResults[entry.key] = {
1142
+ loading: false,
1143
+ value: void 0,
1144
+ error: err instanceof Error ? err.message : String(err)
1145
+ };
1146
+ }
1048
1147
  }
1049
1148
  snapshotData["_derived"] = derivedResults;
1050
1149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",
@@ -26,6 +26,6 @@
26
26
  "alien-signals": "3.1.2"
27
27
  },
28
28
  "devDependencies": {
29
- "zod": "^4.4.1"
29
+ "zod": "^4.4.3"
30
30
  }
31
31
  }