ilha 0.4.1 → 0.4.3

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 +232 -110
  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";
@@ -262,81 +275,137 @@ function batch(fn) {
262
275
  endBatch();
263
276
  }
264
277
  }
265
- function buildDerivedSignals(entries, state, input, derivedSnapshot) {
278
+ function createDerivedProxy(entries, state, input, derivedSnapshot) {
266
279
  const envelopes = /* @__PURE__ */ new Map();
267
- const stops = [];
268
280
  for (const entry of entries) {
269
- const env = signal(derivedSnapshot?.[entry.key] ?? {
281
+ let initialEnvelope;
282
+ if (derivedSnapshot != null && entry.key in derivedSnapshot) initialEnvelope = { ...derivedSnapshot[entry.key] };
283
+ else if (!(entry.fn.constructor.name === "AsyncFunction" || entry.fn.constructor.name === "AsyncGeneratorFunction")) {
284
+ const ac = new AbortController();
285
+ try {
286
+ const result = entry.fn({
287
+ state,
288
+ input,
289
+ signal: ac.signal
290
+ });
291
+ if (!(result instanceof Promise)) initialEnvelope = {
292
+ loading: false,
293
+ value: result,
294
+ error: void 0
295
+ };
296
+ else {
297
+ initialEnvelope = {
298
+ loading: true,
299
+ value: void 0,
300
+ error: void 0
301
+ };
302
+ result.catch(() => {});
303
+ }
304
+ } catch (err) {
305
+ initialEnvelope = {
306
+ loading: false,
307
+ value: void 0,
308
+ error: err instanceof Error ? err : new Error(String(err))
309
+ };
310
+ } finally {
311
+ ac.abort();
312
+ }
313
+ } else initialEnvelope = {
270
314
  loading: true,
271
315
  value: void 0,
272
316
  error: void 0
273
- });
317
+ };
318
+ const env = signal(initialEnvelope);
274
319
  envelopes.set(entry.key, env);
275
- let ac = new AbortController();
276
- let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
277
- const stopEffect = effect(() => {
278
- ac.abort();
279
- ac = new AbortController();
280
- const currentAc = ac;
281
- const result = entry.fn({
282
- state,
283
- input,
284
- signal: currentAc.signal
285
- });
286
- if (skipFirst) {
287
- skipFirst = false;
288
- if (result instanceof Promise) result.catch(() => {});
289
- return;
290
- }
291
- if (!(result instanceof Promise)) {
320
+ }
321
+ const proxy = new Proxy({}, { get(_, key) {
322
+ const env = envelopes.get(key);
323
+ if (!env) return {
324
+ loading: false,
325
+ value: void 0,
326
+ error: void 0
327
+ };
328
+ return env();
329
+ } });
330
+ const setup = () => {
331
+ const stops = [];
332
+ for (const entry of entries) {
333
+ const env = envelopes.get(entry.key);
334
+ let ac = new AbortController();
335
+ let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
336
+ const stopEffect = effect(() => {
337
+ ac.abort();
338
+ ac = new AbortController();
339
+ const currentAc = ac;
340
+ let result;
341
+ try {
342
+ result = entry.fn({
343
+ state,
344
+ input,
345
+ signal: currentAc.signal
346
+ });
347
+ } catch (err) {
348
+ if (skipFirst) {
349
+ skipFirst = false;
350
+ return;
351
+ }
352
+ const prevSub = setActiveSub(void 0);
353
+ env({
354
+ loading: false,
355
+ value: void 0,
356
+ error: err instanceof Error ? err : new Error(String(err))
357
+ });
358
+ setActiveSub(prevSub);
359
+ return;
360
+ }
361
+ if (skipFirst) {
362
+ skipFirst = false;
363
+ if (result instanceof Promise) result.catch(() => {});
364
+ return;
365
+ }
366
+ if (!(result instanceof Promise)) {
367
+ const prevSub = setActiveSub(void 0);
368
+ env({
369
+ loading: false,
370
+ value: result,
371
+ error: void 0
372
+ });
373
+ setActiveSub(prevSub);
374
+ return;
375
+ }
292
376
  const prevSub = setActiveSub(void 0);
293
377
  env({
294
- loading: false,
295
- value: result,
378
+ loading: true,
379
+ value: env().value,
296
380
  error: void 0
297
381
  });
298
382
  setActiveSub(prevSub);
299
- return;
300
- }
301
- const prevSub = setActiveSub(void 0);
302
- env({
303
- loading: true,
304
- value: env().value,
305
- error: void 0
306
- });
307
- setActiveSub(prevSub);
308
- result.then((value) => {
309
- if (currentAc.signal.aborted) return;
310
- env({
311
- loading: false,
312
- value,
313
- error: void 0
314
- });
315
- }).catch((err) => {
316
- if (currentAc.signal.aborted) return;
317
- env({
318
- loading: false,
319
- value: void 0,
320
- error: err instanceof Error ? err : new Error(String(err))
383
+ result.then((value) => {
384
+ if (currentAc.signal.aborted) return;
385
+ env({
386
+ loading: false,
387
+ value,
388
+ error: void 0
389
+ });
390
+ }).catch((err) => {
391
+ if (currentAc.signal.aborted) return;
392
+ env({
393
+ loading: false,
394
+ value: void 0,
395
+ error: err instanceof Error ? err : new Error(String(err))
396
+ });
321
397
  });
322
398
  });
323
- });
324
- stops.push(() => {
325
- stopEffect();
326
- ac.abort();
327
- });
328
- }
399
+ stops.push(() => {
400
+ stopEffect();
401
+ ac.abort();
402
+ });
403
+ }
404
+ return () => stops.forEach((s) => s());
405
+ };
329
406
  return {
330
- proxy: new Proxy({}, { get(_, key) {
331
- const env = envelopes.get(key);
332
- if (!env) return {
333
- loading: false,
334
- value: void 0,
335
- error: void 0
336
- };
337
- return env();
338
- } }),
339
- stop: () => stops.forEach((s) => s())
407
+ proxy,
408
+ setup
340
409
  };
341
410
  }
342
411
  function resolveBindConfig(el) {
@@ -685,11 +754,18 @@ var IlhaBuilder = class IlhaBuilder {
685
754
  });
686
755
  }
687
756
  function mountIsland(host, props) {
757
+ return mountIslandInternal(host, props).unmount;
758
+ }
759
+ function mountIslandInternal(host, props) {
760
+ const noop = {
761
+ unmount: () => {},
762
+ updateProps: () => {}
763
+ };
688
764
  if (__DEV__ && _mountedHosts) {
689
765
  if (_mountedHosts.has(host)) {
690
766
  warn(`mount(): this element is already mounted. Call the previous unmount() first to avoid memory leaks and duplicate event listeners.
691
767
  Element: ${host.outerHTML.slice(0, 120)}`);
692
- return () => {};
768
+ return noop;
693
769
  }
694
770
  _mountedHosts.add(host);
695
771
  }
@@ -701,7 +777,31 @@ Element: ${host.outerHTML.slice(0, 120)}`);
701
777
  warn("Failed to parse data-ilha-props — invalid JSON, falling back to empty props.");
702
778
  }
703
779
  }
704
- const input = resolveInput(props);
780
+ const inputSignal = signal(resolveInput(props));
781
+ const input = new Proxy({}, {
782
+ get(_t, key) {
783
+ return inputSignal()[key];
784
+ },
785
+ has(_t, key) {
786
+ return key in inputSignal();
787
+ },
788
+ ownKeys() {
789
+ const prevSub = setActiveSub(void 0);
790
+ try {
791
+ return Reflect.ownKeys(inputSignal());
792
+ } finally {
793
+ setActiveSub(prevSub);
794
+ }
795
+ },
796
+ getOwnPropertyDescriptor(_t, key) {
797
+ const prevSub = setActiveSub(void 0);
798
+ try {
799
+ return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
800
+ } finally {
801
+ setActiveSub(prevSub);
802
+ }
803
+ }
804
+ });
705
805
  let snapshotRaw;
706
806
  const rawState = host.getAttribute(STATE_ATTR);
707
807
  if (rawState) try {
@@ -730,8 +830,8 @@ Element: ${host.outerHTML.slice(0, 120)}`);
730
830
  const result = transition.enter(host);
731
831
  if (result instanceof Promise) result.catch(console.error);
732
832
  }
733
- const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input, derivedSnapshot);
734
- cleanups.push(stopDerived);
833
+ const { proxy: derived, setup: setupDerived } = createDerivedProxy(deriveds, state, input, derivedSnapshot);
834
+ let stopDerived = () => {};
735
835
  function reportError(err, source) {
736
836
  const error = err instanceof Error ? err : new Error(String(err));
737
837
  if (onErrors.length === 0) {
@@ -771,7 +871,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
771
871
  mountedSlots.delete(id);
772
872
  }
773
873
  for (const [id, { island: childIsland, props }] of slotMap) {
774
- if (mountedSlots.has(id)) continue;
874
+ const existing = mountedSlots.get(id);
875
+ if (existing) {
876
+ existing.updateProps(props);
877
+ continue;
878
+ }
775
879
  const slotEl = findSlot(id);
776
880
  if (!slotEl) continue;
777
881
  let slotProps = props;
@@ -784,15 +888,19 @@ Element: ${host.outerHTML.slice(0, 120)}`);
784
888
  }
785
889
  }
786
890
  const prevSub = setActiveSub(void 0);
787
- let unmount;
891
+ let handle;
788
892
  try {
789
- unmount = childIsland.mount(slotEl, slotProps);
893
+ const internal = childIsland[ISLAND_MOUNT_INTERNAL];
894
+ handle = internal ? internal(slotEl, slotProps) : {
895
+ unmount: childIsland.mount(slotEl, slotProps),
896
+ updateProps: () => {}
897
+ };
790
898
  } finally {
791
899
  setActiveSub(prevSub);
792
900
  }
793
901
  mountedSlots.set(id, {
794
902
  el: slotEl,
795
- unmount
903
+ ...handle
796
904
  });
797
905
  }
798
906
  }
@@ -895,7 +1003,49 @@ Element: ${host.outerHTML.slice(0, 120)}`);
895
1003
  }
896
1004
  if (userCleanup) cleanups.push(userCleanup);
897
1005
  }
1006
+ for (const entry of effects) {
1007
+ let userCleanup;
1008
+ let runController = null;
1009
+ const stopEffect = effect(() => {
1010
+ if (userCleanup) {
1011
+ try {
1012
+ userCleanup();
1013
+ } catch (err) {
1014
+ reportError(err, "effect");
1015
+ }
1016
+ userCleanup = void 0;
1017
+ }
1018
+ if (runController) runController.abort();
1019
+ runController = new AbortController();
1020
+ const runSignal = anySignal([unmountController.signal, runController.signal]);
1021
+ startBatch();
1022
+ try {
1023
+ userCleanup = entry.fn({
1024
+ state,
1025
+ input,
1026
+ host,
1027
+ signal: runSignal
1028
+ });
1029
+ } catch (err) {
1030
+ reportError(err, "effect");
1031
+ } finally {
1032
+ endBatch();
1033
+ }
1034
+ });
1035
+ cleanups.push(() => {
1036
+ stopEffect();
1037
+ if (userCleanup) try {
1038
+ userCleanup();
1039
+ } catch (err) {
1040
+ reportError(err, "effect");
1041
+ }
1042
+ if (runController) runController.abort();
1043
+ });
1044
+ }
1045
+ stopDerived = setupDerived();
1046
+ cleanups.push(stopDerived);
898
1047
  let initialized = false;
1048
+ const initialRenderedHtml = initial.html;
899
1049
  const stopRender = effect(() => {
900
1050
  const { html: rendered, slots: newSlotMap } = renderWithCtx(() => fn({
901
1051
  state,
@@ -905,7 +1055,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
905
1055
  const html = stylePrefix + rendered;
906
1056
  if (!initialized) {
907
1057
  initialized = true;
908
- return;
1058
+ if (rendered === initialRenderedHtml) return;
909
1059
  }
910
1060
  detachListeners();
911
1061
  stopBindings();
@@ -936,47 +1086,8 @@ Element: ${host.outerHTML.slice(0, 120)}`);
936
1086
  });
937
1087
  cleanups.push(stopRender);
938
1088
  cleanups.push(detachListeners);
939
- for (const entry of effects) {
940
- let userCleanup;
941
- let runController = null;
942
- const stopEffect = effect(() => {
943
- if (userCleanup) {
944
- try {
945
- userCleanup();
946
- } catch (err) {
947
- reportError(err, "effect");
948
- }
949
- userCleanup = void 0;
950
- }
951
- if (runController) runController.abort();
952
- runController = new AbortController();
953
- const runSignal = anySignal([unmountController.signal, runController.signal]);
954
- startBatch();
955
- try {
956
- userCleanup = entry.fn({
957
- state,
958
- input,
959
- host,
960
- signal: runSignal
961
- });
962
- } catch (err) {
963
- reportError(err, "effect");
964
- } finally {
965
- endBatch();
966
- }
967
- });
968
- cleanups.push(() => {
969
- stopEffect();
970
- if (userCleanup) try {
971
- userCleanup();
972
- } catch (err) {
973
- reportError(err, "effect");
974
- }
975
- if (runController) runController.abort();
976
- });
977
- }
978
1089
  let tornDown = false;
979
- return () => {
1090
+ const unmount = () => {
980
1091
  if (tornDown) return;
981
1092
  tornDown = true;
982
1093
  if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
@@ -989,6 +1100,16 @@ Element: ${host.outerHTML.slice(0, 120)}`);
989
1100
  }
990
1101
  cleanups.forEach((c) => c());
991
1102
  };
1103
+ const updateProps = (nextProps) => {
1104
+ if (tornDown) return;
1105
+ const next = resolveInput(nextProps);
1106
+ if (shallowEqualInput(inputSignal(), next)) return;
1107
+ inputSignal(next);
1108
+ };
1109
+ return {
1110
+ unmount,
1111
+ updateProps
1112
+ };
992
1113
  }
993
1114
  /**
994
1115
  * Dual-mode island invocation:
@@ -1012,6 +1133,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1012
1133
  };
1013
1134
  island.toString = (props) => renderToString(props, true);
1014
1135
  island.mount = (host, props) => mountIsland(host, props);
1136
+ island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
1015
1137
  island.key = (key) => {
1016
1138
  if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
1017
1139
  if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",
@@ -23,7 +23,7 @@
23
23
  "test": "bun test"
24
24
  },
25
25
  "dependencies": {
26
- "alien-signals": "3.1.2"
26
+ "alien-signals": "3.2.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "zod": "^4.4.3"