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.
- package/dist/index.js +232 -110
- 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
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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:
|
|
295
|
-
value:
|
|
378
|
+
loading: true,
|
|
379
|
+
value: env().value,
|
|
296
380
|
error: void 0
|
|
297
381
|
});
|
|
298
382
|
setActiveSub(prevSub);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
399
|
+
stops.push(() => {
|
|
400
|
+
stopEffect();
|
|
401
|
+
ac.abort();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return () => stops.forEach((s) => s());
|
|
405
|
+
};
|
|
329
406
|
return {
|
|
330
|
-
proxy
|
|
331
|
-
|
|
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
|
|
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,
|
|
734
|
-
|
|
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
|
-
|
|
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
|
|
891
|
+
let handle;
|
|
788
892
|
try {
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
26
|
+
"alien-signals": "3.2.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"zod": "^4.4.3"
|