ilha 0.6.0 → 0.7.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/README.md +5 -2
- package/dist/index.d.ts +21 -15
- package/dist/index.js +323 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,11 +121,11 @@ ilha
|
|
|
121
121
|
.render(({ derived }) => {
|
|
122
122
|
if (derived.user.loading) return `<p>Loading…</p>`;
|
|
123
123
|
if (derived.user.error) return `<p>Error: ${derived.user.error.message}</p>`;
|
|
124
|
-
return `<p>${derived.user.
|
|
124
|
+
return `<p>${derived.user().name}</p>`;
|
|
125
125
|
});
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
Read with `derived.name()` like state. Each accessor also exposes `loading`, `value`, and `error` for async work. Write `derived.name(value)` for optimistic UI.
|
|
129
129
|
|
|
130
130
|
---
|
|
131
131
|
|
|
@@ -239,12 +239,15 @@ The handler receives an `EffectContext`:
|
|
|
239
239
|
```ts
|
|
240
240
|
{
|
|
241
241
|
state: IslandState;
|
|
242
|
+
derived: IslandDerived;
|
|
242
243
|
input: TInput;
|
|
243
244
|
host: Element;
|
|
244
245
|
signal: AbortSignal; // aborts when the effect re-runs OR the island unmounts
|
|
245
246
|
}
|
|
246
247
|
```
|
|
247
248
|
|
|
249
|
+
Reading `derived.name()` subscribes the effect. Writing `derived.name(value)` does not — use writes for optimistic UI without pinning the effect to every derived update.
|
|
250
|
+
|
|
248
251
|
**Cancelling async work with `ctx.signal`** — unlike `.on()`, race-cancellation is the **default** behaviour for effects (no opt-in modifier needed) because dependency changes invariably make the previous run stale. Pass `signal` to async work to bail out of stale invocations without needing a manual cleanup function:
|
|
249
252
|
|
|
250
253
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ interface IslandCall {
|
|
|
51
51
|
interface MarkedSignalAccessor<T> {
|
|
52
52
|
(): T;
|
|
53
53
|
(...args: [value: T]): void;
|
|
54
|
+
select<S>(selector: (state: T) => S): MarkedSignalAccessor<S>;
|
|
54
55
|
[SIGNAL_ACCESSOR]: true;
|
|
55
56
|
}
|
|
56
57
|
declare function ilhaRaw(value: string): RawHtml;
|
|
@@ -58,11 +59,11 @@ declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (str
|
|
|
58
59
|
type JsxChild = unknown;
|
|
59
60
|
type JsxProps = Record<string, unknown> | null | undefined;
|
|
60
61
|
type JsxType = string | ((props: Record<string, unknown>) => unknown);
|
|
61
|
-
declare function ilhaJsx(type: JsxType, props: JsxProps, ...
|
|
62
|
+
declare function ilhaJsx(type: JsxType, props: JsxProps, maybeKey?: JsxChild | string | number, ...restChildren: JsxChild[]): RawHtml;
|
|
62
63
|
declare function ilhaFragment(props: {
|
|
63
64
|
children?: JsxChild;
|
|
64
65
|
} | null, ...children: JsxChild[]): RawHtml;
|
|
65
|
-
declare function ilhaJsxDEV(type: JsxType, props: JsxProps): RawHtml;
|
|
66
|
+
declare function ilhaJsxDEV(type: JsxType, props: JsxProps, maybeKey?: string | number, _source?: unknown, _self?: unknown): RawHtml;
|
|
66
67
|
declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
|
|
67
68
|
type ContextSignal<T> = {
|
|
68
69
|
(): T;
|
|
@@ -80,7 +81,7 @@ declare function ilhaContext<T>(key: string, initial: T): ContextSignal<T>;
|
|
|
80
81
|
* or `.render()` automatically subscribes the surrounding reactive scope —
|
|
81
82
|
* so when the signal changes, dependents re-run as if it were local state.
|
|
82
83
|
*/
|
|
83
|
-
declare function ilhaSignal<T>(initial: T):
|
|
84
|
+
declare function ilhaSignal<T>(initial: T): SignalAccessor<T>;
|
|
84
85
|
/**
|
|
85
86
|
* Run `fn` with reactive tracking suspended. Reading signals inside `fn`
|
|
86
87
|
* returns their current value without subscribing the surrounding scope.
|
|
@@ -104,6 +105,13 @@ interface DerivedValue<T> {
|
|
|
104
105
|
value: T | undefined;
|
|
105
106
|
error: Error | undefined;
|
|
106
107
|
}
|
|
108
|
+
type DerivedAccessor<T> = {
|
|
109
|
+
readonly loading: boolean;
|
|
110
|
+
readonly value: T | undefined;
|
|
111
|
+
readonly error: Error | undefined;
|
|
112
|
+
(): T | undefined;
|
|
113
|
+
(value: T): void;
|
|
114
|
+
};
|
|
107
115
|
type DerivedFnContext<TInput, TStateMap extends Record<string, unknown>> = {
|
|
108
116
|
state: IslandState<TStateMap>;
|
|
109
117
|
input: TInput;
|
|
@@ -114,11 +122,8 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
|
|
|
114
122
|
key: string;
|
|
115
123
|
fn: DerivedFn<TInput, TStateMap, unknown>;
|
|
116
124
|
}
|
|
117
|
-
type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]:
|
|
118
|
-
|
|
119
|
-
(): T;
|
|
120
|
-
(value: T): void;
|
|
121
|
-
}
|
|
125
|
+
type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedAccessor<TDerivedMap[K]> };
|
|
126
|
+
type ExternalSignal<T = unknown> = SignalAccessor<T>;
|
|
122
127
|
type SignalAccessor<T> = MarkedSignalAccessor<T>;
|
|
123
128
|
type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
|
|
124
129
|
type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
|
|
@@ -149,8 +154,9 @@ type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMa
|
|
|
149
154
|
derived: IslandDerived<TDerivedMap>;
|
|
150
155
|
input: TInput;
|
|
151
156
|
};
|
|
152
|
-
type EffectContext<TInput, TStateMap extends Record<string, unknown>> = {
|
|
157
|
+
type EffectContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
|
|
153
158
|
state: IslandState<TStateMap>;
|
|
159
|
+
derived: IslandDerived<TDerivedMap>;
|
|
154
160
|
input: TInput;
|
|
155
161
|
host: Element;
|
|
156
162
|
/**
|
|
@@ -213,8 +219,8 @@ interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap
|
|
|
213
219
|
abortable: boolean;
|
|
214
220
|
handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>;
|
|
215
221
|
}
|
|
216
|
-
interface EffectEntry<TInput, TStateMap extends Record<string, unknown>> {
|
|
217
|
-
fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void;
|
|
222
|
+
interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
|
|
223
|
+
fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void;
|
|
218
224
|
}
|
|
219
225
|
/** Where the error originated. `"on"` covers sync throws and async rejections
|
|
220
226
|
* from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
|
|
@@ -243,14 +249,14 @@ interface MountOptions {
|
|
|
243
249
|
lazy?: boolean;
|
|
244
250
|
}
|
|
245
251
|
interface MountResult {
|
|
246
|
-
unmount: () => void
|
|
252
|
+
unmount: () => void | Promise<void>;
|
|
247
253
|
}
|
|
248
254
|
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
|
|
249
255
|
schema: StandardSchemaV1 | null;
|
|
250
256
|
states: StateEntry<TInput>[];
|
|
251
257
|
deriveds: DerivedEntry<TInput, TStateMap>[];
|
|
252
258
|
ons: OnEntry<TInput, TStateMap, TDerivedMap>[];
|
|
253
|
-
effects: EffectEntry<TInput, TStateMap>[];
|
|
259
|
+
effects: EffectEntry<TInput, TStateMap, TDerivedMap>[];
|
|
254
260
|
onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
|
|
255
261
|
onErrors: OnErrorEntry<TInput, TStateMap, TDerivedMap>[];
|
|
256
262
|
transition: TransitionOptions | null;
|
|
@@ -264,7 +270,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
|
|
|
264
270
|
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
|
|
265
271
|
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
|
|
266
272
|
on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
267
|
-
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
273
|
+
effect(fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
268
274
|
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
269
275
|
onError(fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap>) => void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
270
276
|
transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
@@ -299,4 +305,4 @@ declare const mount: typeof mountAll;
|
|
|
299
305
|
declare const from: typeof ilhaFrom;
|
|
300
306
|
declare const context: typeof ilhaContext;
|
|
301
307
|
//#endregion
|
|
302
|
-
export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
|
308
|
+
export { DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,76 @@ function shallowEqualInput(a, b) {
|
|
|
16
16
|
}
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
|
+
function extractDirectChildSlotIdsInOrder(html) {
|
|
20
|
+
if (typeof document === "undefined") return [];
|
|
21
|
+
const tpl = document.createElement("template");
|
|
22
|
+
tpl.innerHTML = html;
|
|
23
|
+
const root = tpl.content;
|
|
24
|
+
const ids = [];
|
|
25
|
+
for (const el of root.querySelectorAll("[data-ilha-slot]")) {
|
|
26
|
+
if (!(el instanceof Element)) continue;
|
|
27
|
+
let parent = el.parentElement ?? el.parentNode;
|
|
28
|
+
let nested = false;
|
|
29
|
+
while (parent && parent !== root) {
|
|
30
|
+
if (parent instanceof Element && parent.hasAttribute("data-ilha-slot")) {
|
|
31
|
+
nested = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
parent = parent instanceof Element ? parent.parentElement ?? parent.parentNode : parent.parentNode;
|
|
35
|
+
}
|
|
36
|
+
if (!nested) ids.push(el.getAttribute("data-ilha-slot"));
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
function extractSlotPropsAttr(html, slotId) {
|
|
41
|
+
const escaped = slotId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
|
+
const withProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"[^>]*data-ilha-props='([^']*)'`);
|
|
43
|
+
const m = html.match(withProps);
|
|
44
|
+
if (m) return m[1];
|
|
45
|
+
const withoutProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"`);
|
|
46
|
+
return html.match(withoutProps) ? "" : null;
|
|
47
|
+
}
|
|
48
|
+
function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
|
|
49
|
+
const initialOrder = extractDirectChildSlotIdsInOrder(initialHtml);
|
|
50
|
+
const renderedOrder = extractDirectChildSlotIdsInOrder(renderedHtml);
|
|
51
|
+
if (initialOrder.length !== renderedOrder.length || initialOrder.some((id, i) => id !== renderedOrder[i])) return false;
|
|
52
|
+
for (const id of slotIds) {
|
|
53
|
+
const initialProps = extractSlotPropsAttr(initialHtml, id);
|
|
54
|
+
const renderedProps = extractSlotPropsAttr(renderedHtml, id);
|
|
55
|
+
if (initialProps === null || renderedProps === null) return false;
|
|
56
|
+
if (initialProps !== renderedProps) return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function serializeSlotProps(props) {
|
|
61
|
+
return props === void 0 ? "" : JSON.stringify(props);
|
|
62
|
+
}
|
|
63
|
+
const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
|
|
64
|
+
const MORPH_CONTROLLER_ATTRS = new Set([
|
|
65
|
+
"data-checked",
|
|
66
|
+
"data-unchecked",
|
|
67
|
+
"data-indeterminate",
|
|
68
|
+
"aria-checked",
|
|
69
|
+
"data-open",
|
|
70
|
+
"data-closed",
|
|
71
|
+
"data-state",
|
|
72
|
+
"aria-expanded",
|
|
73
|
+
"aria-hidden",
|
|
74
|
+
"data-selected",
|
|
75
|
+
"data-panel-open"
|
|
76
|
+
]);
|
|
77
|
+
function shouldPreserveMorphAttr(el, name) {
|
|
78
|
+
return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
|
|
79
|
+
}
|
|
19
80
|
function syncAttributes(from, to) {
|
|
20
|
-
for (const { name, value } of to.attributes)
|
|
21
|
-
|
|
81
|
+
for (const { name, value } of to.attributes) {
|
|
82
|
+
if (shouldPreserveMorphAttr(from, name)) continue;
|
|
83
|
+
if (from.getAttribute(name) !== value) from.setAttribute(name, value);
|
|
84
|
+
}
|
|
85
|
+
for (const { name } of Array.from(from.attributes)) {
|
|
86
|
+
if (shouldPreserveMorphAttr(from, name)) continue;
|
|
87
|
+
if (!to.hasAttribute(name)) from.removeAttribute(name);
|
|
88
|
+
}
|
|
22
89
|
}
|
|
23
90
|
function morphChildren(fromParent, toParent) {
|
|
24
91
|
const fromNodes = Array.from(fromParent.childNodes);
|
|
@@ -88,14 +155,14 @@ function dedentString(str) {
|
|
|
88
155
|
const indent = Math.min(...lines.filter((l) => l.trim() !== "").map((l) => l.match(/^(\s*)/)[1].length));
|
|
89
156
|
return lines.map((l) => l.slice(indent)).join("\n");
|
|
90
157
|
}
|
|
91
|
-
const RAW = Symbol("ilha.raw");
|
|
92
|
-
const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
93
|
-
const ISLAND = Symbol("ilha.island");
|
|
94
|
-
const ISLAND_CALL = Symbol("ilha.islandCall");
|
|
158
|
+
const RAW = Symbol.for("ilha.raw");
|
|
159
|
+
const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
|
|
160
|
+
const ISLAND = Symbol.for("ilha.island");
|
|
161
|
+
const ISLAND_CALL = Symbol.for("ilha.islandCall");
|
|
95
162
|
/** @internal Internal hook used by a parent's mountSlots to mount a child island and
|
|
96
163
|
* retain a handle to push updated props into it on subsequent parent
|
|
97
164
|
* re-renders. Not part of the public surface. */
|
|
98
|
-
const ISLAND_MOUNT_INTERNAL = Symbol("ilha.islandMountInternal");
|
|
165
|
+
const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
|
|
99
166
|
const SLOT_ATTR = "data-ilha-slot";
|
|
100
167
|
const PROPS_ATTR = "data-ilha-props";
|
|
101
168
|
const STATE_ATTR = "data-ilha-state";
|
|
@@ -122,10 +189,19 @@ function currentRenderCtx() {
|
|
|
122
189
|
return renderCtxStack[renderCtxStack.length - 1];
|
|
123
190
|
}
|
|
124
191
|
function isIsland(v) {
|
|
125
|
-
|
|
192
|
+
if (typeof v !== "function") return false;
|
|
193
|
+
if (ISLAND in v) return true;
|
|
194
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.island");
|
|
126
195
|
}
|
|
127
196
|
function isIslandCall(v) {
|
|
128
|
-
|
|
197
|
+
if (v == null || typeof v !== "object" && typeof v !== "function") return false;
|
|
198
|
+
if (ISLAND_CALL in v) return true;
|
|
199
|
+
if (Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.islandCall")) return true;
|
|
200
|
+
if (typeof v === "object" && "island" in v) {
|
|
201
|
+
const island = v.island;
|
|
202
|
+
return typeof island === "function" && (ISLAND in island || Object.getOwnPropertySymbols(island).some((s) => s.description === "ilha.island"));
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
129
205
|
}
|
|
130
206
|
function emitIslandSlot(island, props, key) {
|
|
131
207
|
const ctx = currentRenderCtx();
|
|
@@ -171,10 +247,86 @@ async function resolveAsyncChildren(html, ctx) {
|
|
|
171
247
|
}
|
|
172
248
|
function markSignalAccessor(fn) {
|
|
173
249
|
fn[SIGNAL_ACCESSOR] = true;
|
|
174
|
-
|
|
250
|
+
const accessor = fn;
|
|
251
|
+
accessor.select = (selector) => createSelectAccessor(accessor, selector);
|
|
252
|
+
return accessor;
|
|
175
253
|
}
|
|
176
254
|
function isSignalAccessor(v) {
|
|
177
|
-
|
|
255
|
+
if (typeof v !== "function") return false;
|
|
256
|
+
if (SIGNAL_ACCESSOR in v) return true;
|
|
257
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.signalAccessor");
|
|
258
|
+
}
|
|
259
|
+
function getAtPath(obj, path) {
|
|
260
|
+
let cur = obj;
|
|
261
|
+
for (const seg of path) {
|
|
262
|
+
if (cur == null) return void 0;
|
|
263
|
+
cur = cur[seg];
|
|
264
|
+
}
|
|
265
|
+
return cur;
|
|
266
|
+
}
|
|
267
|
+
function setAtPath(obj, path, value) {
|
|
268
|
+
if (path.length === 0) return value;
|
|
269
|
+
const [head, ...rest] = path;
|
|
270
|
+
if (Array.isArray(obj)) {
|
|
271
|
+
const idx = head;
|
|
272
|
+
if (idx < 0 || idx >= obj.length) return obj;
|
|
273
|
+
const next = obj[idx];
|
|
274
|
+
const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
|
|
275
|
+
if (Object.is(next, updated)) return obj;
|
|
276
|
+
const copy = obj.slice();
|
|
277
|
+
copy[idx] = updated;
|
|
278
|
+
return copy;
|
|
279
|
+
}
|
|
280
|
+
if (obj !== null && typeof obj === "object") {
|
|
281
|
+
const record = obj;
|
|
282
|
+
const key = String(head);
|
|
283
|
+
const next = record[key];
|
|
284
|
+
const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
|
|
285
|
+
if (rest.length === 0) {
|
|
286
|
+
if (Object.is(next, value)) return obj;
|
|
287
|
+
} else if (Object.is(next, updated)) return obj;
|
|
288
|
+
return {
|
|
289
|
+
...record,
|
|
290
|
+
[key]: updated
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return rest.length === 0 ? value : setAtPath(void 0, rest, value);
|
|
294
|
+
}
|
|
295
|
+
function toPathSegment(prop) {
|
|
296
|
+
if (typeof prop === "symbol") return null;
|
|
297
|
+
if (prop === "length") return null;
|
|
298
|
+
if (/^\d+$/.test(prop)) return Number(prop);
|
|
299
|
+
return prop;
|
|
300
|
+
}
|
|
301
|
+
function trackSelectPath(rootState, selector) {
|
|
302
|
+
const path = [];
|
|
303
|
+
const track = (value) => {
|
|
304
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") return value;
|
|
305
|
+
return new Proxy(value, { get(target, prop, receiver) {
|
|
306
|
+
const seg = toPathSegment(prop);
|
|
307
|
+
if (seg != null) path.push(seg);
|
|
308
|
+
const next = Reflect.get(target, prop, receiver);
|
|
309
|
+
return seg != null && next !== null && typeof next === "object" ? track(next) : next;
|
|
310
|
+
} });
|
|
311
|
+
};
|
|
312
|
+
selector(track(rootState));
|
|
313
|
+
return path;
|
|
314
|
+
}
|
|
315
|
+
function createSelectAccessor(root, selector) {
|
|
316
|
+
const path = trackSelectPath(root(), selector);
|
|
317
|
+
const selected = selector(root());
|
|
318
|
+
const resolved = path.length === 0 ? root() : getAtPath(root(), path);
|
|
319
|
+
if (!Object.is(selected, resolved)) {
|
|
320
|
+
const msg = "select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.";
|
|
321
|
+
if (__DEV__) warn(msg);
|
|
322
|
+
throw new Error(msg);
|
|
323
|
+
}
|
|
324
|
+
if (__DEV__ && path.length === 0) warn("select(): selector did not traverse nested state — bind writes may replace the entire root value.");
|
|
325
|
+
return markSignalAccessor((...args) => {
|
|
326
|
+
if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
|
|
327
|
+
const next = path.length === 0 ? args[0] : setAtPath(root(), path, args[0]);
|
|
328
|
+
if (!Object.is(root(), next)) root(next);
|
|
329
|
+
});
|
|
178
330
|
}
|
|
179
331
|
function ilhaRaw(value) {
|
|
180
332
|
return {
|
|
@@ -229,6 +381,19 @@ function normalizeJsxChildren(props, children) {
|
|
|
229
381
|
const propChildren = props && "children" in props ? props.children : void 0;
|
|
230
382
|
return (children.length > 0 ? children : propChildren === void 0 ? [] : [propChildren]).flat(1);
|
|
231
383
|
}
|
|
384
|
+
function normalizeJsxSlotKey(raw) {
|
|
385
|
+
const key = String(raw);
|
|
386
|
+
if (key.trim().length === 0) throw new Error("jsx key requires a non-empty string.");
|
|
387
|
+
if (key.includes(":")) throw new Error(`jsx key cannot contain the slot separator ":" (got "${key}").`);
|
|
388
|
+
return key;
|
|
389
|
+
}
|
|
390
|
+
/** React-style `key` (props or 3rd jsx arg) on island components → `k:{key}` slot ids. */
|
|
391
|
+
function extractJsxSlotKey(props, keyArg) {
|
|
392
|
+
const fromProps = props?.key;
|
|
393
|
+
const raw = keyArg ?? (typeof fromProps === "string" || typeof fromProps === "number" ? fromProps : void 0);
|
|
394
|
+
if (raw == null) return void 0;
|
|
395
|
+
return normalizeJsxSlotKey(raw);
|
|
396
|
+
}
|
|
232
397
|
function serializeStyle(value) {
|
|
233
398
|
return Object.entries(value).map(([k, v]) => {
|
|
234
399
|
if (!SAFE_CSS_PROP_RE.test(k)) return "";
|
|
@@ -281,17 +446,25 @@ function renderJsxElement(type, props, children) {
|
|
|
281
446
|
} else if (children.length > 0 && __DEV__) warn(`Void element <${type}> should not have children. They will be ignored.`);
|
|
282
447
|
return ilhaHtml(chunks, ...values);
|
|
283
448
|
}
|
|
284
|
-
function ilhaJsx(type, props, ...
|
|
285
|
-
const
|
|
449
|
+
function ilhaJsx(type, props, maybeKey, ...restChildren) {
|
|
450
|
+
const hasKeyArg = typeof maybeKey === "string" || typeof maybeKey === "number";
|
|
451
|
+
const keyFromArg = hasKeyArg ? maybeKey : void 0;
|
|
452
|
+
const normalizedChildren = normalizeJsxChildren(props, hasKeyArg || maybeKey === void 0 ? restChildren : [maybeKey, ...restChildren]);
|
|
286
453
|
if (typeof type === "function") {
|
|
454
|
+
const slotKey = extractJsxSlotKey(props, typeof keyFromArg === "string" || typeof keyFromArg === "number" ? keyFromArg : void 0);
|
|
287
455
|
const componentProps = {
|
|
288
456
|
...props ?? {},
|
|
289
457
|
...normalizedChildren.length > 0 ? { children: normalizedChildren } : {}
|
|
290
458
|
};
|
|
291
459
|
delete componentProps["key"];
|
|
292
460
|
const out = type(Object.keys(componentProps).length ? componentProps : {});
|
|
461
|
+
if (isIslandCall(out)) {
|
|
462
|
+
if (slotKey !== void 0) out.key = slotKey;
|
|
463
|
+
return ilhaHtml`${out}`;
|
|
464
|
+
}
|
|
465
|
+
if (isRawHtml(out)) return out;
|
|
466
|
+
if (typeof out === "string" && isIsland(type)) return ilhaRaw(emitIslandSlot(type, componentProps, slotKey));
|
|
293
467
|
if (typeof out === "string") return ilhaHtml`${out}`;
|
|
294
|
-
if (out && typeof out === "object" && RAW in out) return out;
|
|
295
468
|
return ilhaHtml`${out}`;
|
|
296
469
|
}
|
|
297
470
|
return renderJsxElement(type, props, normalizedChildren);
|
|
@@ -300,13 +473,13 @@ function ilhaFragment(props, ...children) {
|
|
|
300
473
|
const normalizedChildren = normalizeJsxChildren(props, children);
|
|
301
474
|
return ilhaHtml(["", ...normalizedChildren.map(() => "")], ...normalizedChildren);
|
|
302
475
|
}
|
|
303
|
-
function ilhaJsxDEV(type, props) {
|
|
304
|
-
return ilhaJsx(type, props);
|
|
476
|
+
function ilhaJsxDEV(type, props, maybeKey, _source, _self) {
|
|
477
|
+
return ilhaJsx(type, props, maybeKey);
|
|
305
478
|
}
|
|
306
479
|
function interpolateValue(v) {
|
|
307
480
|
if (v == null || v === true || v === false) return "";
|
|
308
481
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
309
|
-
if (
|
|
482
|
+
if (isRawHtml(v)) return v.value;
|
|
310
483
|
if (isIslandCall(v)) {
|
|
311
484
|
const call = typeof v === "function" ? v() : v;
|
|
312
485
|
return emitIslandSlot(call.island, call.props, call.key);
|
|
@@ -432,8 +605,15 @@ function ilhaHtml(strings, ...values) {
|
|
|
432
605
|
value: dedentString(result)
|
|
433
606
|
};
|
|
434
607
|
}
|
|
608
|
+
/** Duck-type RawHtml — works across duplicate `ilha` bundles (distinct RAW symbols). */
|
|
609
|
+
function isRawHtml(v) {
|
|
610
|
+
if (typeof v !== "object" || v === null || !("value" in v)) return false;
|
|
611
|
+
if (typeof v.value !== "string") return false;
|
|
612
|
+
if (RAW in v) return true;
|
|
613
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.raw");
|
|
614
|
+
}
|
|
435
615
|
function unwrapHtml(v) {
|
|
436
|
-
return
|
|
616
|
+
return isRawHtml(v) ? v.value : v;
|
|
437
617
|
}
|
|
438
618
|
const contextRegistry = /* @__PURE__ */ new Map();
|
|
439
619
|
function ilhaContext(key, initial) {
|
|
@@ -496,6 +676,38 @@ function batch(fn) {
|
|
|
496
676
|
endBatch();
|
|
497
677
|
}
|
|
498
678
|
}
|
|
679
|
+
function createDerivedAccessor(read, write) {
|
|
680
|
+
const accessor = markSignalAccessor((...args) => {
|
|
681
|
+
if (args.length > 0) {
|
|
682
|
+
if (write) write(args[0]);
|
|
683
|
+
else if (__DEV__) warn("derived values are read-only");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
return read().value;
|
|
687
|
+
});
|
|
688
|
+
return new Proxy(accessor, { get(target, prop, receiver) {
|
|
689
|
+
if (prop === "loading" || prop === "value" || prop === "error") return read()[prop];
|
|
690
|
+
return Reflect.get(target, prop, receiver);
|
|
691
|
+
} });
|
|
692
|
+
}
|
|
693
|
+
function defaultDerivedAccessor() {
|
|
694
|
+
return createDerivedAccessor(() => ({
|
|
695
|
+
loading: false,
|
|
696
|
+
value: void 0,
|
|
697
|
+
error: void 0
|
|
698
|
+
}));
|
|
699
|
+
}
|
|
700
|
+
function buildDerivedAccessors(envelopes) {
|
|
701
|
+
const accessors = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const [key, envelope] of Object.entries(envelopes)) accessors.set(key, createDerivedAccessor(() => envelope, (value) => {
|
|
703
|
+
envelope.loading = false;
|
|
704
|
+
envelope.value = value;
|
|
705
|
+
envelope.error = void 0;
|
|
706
|
+
}));
|
|
707
|
+
return new Proxy({}, { get(_, key) {
|
|
708
|
+
return accessors.get(key) ?? defaultDerivedAccessor();
|
|
709
|
+
} });
|
|
710
|
+
}
|
|
499
711
|
function createDerivedProxy(entries, state, input, derivedSnapshot) {
|
|
500
712
|
const envelopes = /* @__PURE__ */ new Map();
|
|
501
713
|
for (const entry of entries) {
|
|
@@ -539,14 +751,21 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
|
|
|
539
751
|
const env = signal(initialEnvelope);
|
|
540
752
|
envelopes.set(entry.key, env);
|
|
541
753
|
}
|
|
754
|
+
const accessors = /* @__PURE__ */ new Map();
|
|
755
|
+
for (const entry of entries) {
|
|
756
|
+
const env = envelopes.get(entry.key);
|
|
757
|
+
accessors.set(entry.key, createDerivedAccessor(() => env(), (value) => {
|
|
758
|
+
const prevSub = setActiveSub(void 0);
|
|
759
|
+
env({
|
|
760
|
+
loading: false,
|
|
761
|
+
value,
|
|
762
|
+
error: void 0
|
|
763
|
+
});
|
|
764
|
+
setActiveSub(prevSub);
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
542
767
|
const proxy = new Proxy({}, { get(_, key) {
|
|
543
|
-
|
|
544
|
-
if (!env) return {
|
|
545
|
-
loading: false,
|
|
546
|
-
value: void 0,
|
|
547
|
-
error: void 0
|
|
548
|
-
};
|
|
549
|
-
return env();
|
|
768
|
+
return accessors.get(key) ?? defaultDerivedAccessor();
|
|
550
769
|
} });
|
|
551
770
|
const setup = () => {
|
|
552
771
|
const stops = [];
|
|
@@ -1003,17 +1222,18 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1003
1222
|
}
|
|
1004
1223
|
});
|
|
1005
1224
|
if (!results.some((r) => r.result instanceof Promise) || sync) {
|
|
1006
|
-
const
|
|
1007
|
-
for (const r of results) if (r.result instanceof Promise)
|
|
1225
|
+
const envelopes = {};
|
|
1226
|
+
for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
|
|
1008
1227
|
loading: true,
|
|
1009
1228
|
value: void 0,
|
|
1010
1229
|
error: void 0
|
|
1011
1230
|
};
|
|
1012
|
-
else
|
|
1231
|
+
else envelopes[r.key] = {
|
|
1013
1232
|
loading: false,
|
|
1014
1233
|
value: r.result,
|
|
1015
1234
|
error: void 0
|
|
1016
1235
|
};
|
|
1236
|
+
const derived = buildDerivedAccessors(envelopes);
|
|
1017
1237
|
const prevSub = setActiveSub(void 0);
|
|
1018
1238
|
try {
|
|
1019
1239
|
const { html } = renderWithCtx(() => fn({
|
|
@@ -1047,8 +1267,9 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1047
1267
|
};
|
|
1048
1268
|
}
|
|
1049
1269
|
})).then(async (resolved) => {
|
|
1050
|
-
const
|
|
1051
|
-
for (const r of resolved)
|
|
1270
|
+
const envelopes = {};
|
|
1271
|
+
for (const r of resolved) envelopes[r.key] = r.envelope;
|
|
1272
|
+
const derived = buildDerivedAccessors(envelopes);
|
|
1052
1273
|
const prevSub = setActiveSub(void 0);
|
|
1053
1274
|
try {
|
|
1054
1275
|
const { html } = await renderWithCtx(() => fn({
|
|
@@ -1161,10 +1382,38 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1161
1382
|
}
|
|
1162
1383
|
}
|
|
1163
1384
|
const mountedSlots = /* @__PURE__ */ new Map();
|
|
1385
|
+
const leavingSlots = /* @__PURE__ */ new Map();
|
|
1386
|
+
function teardownMountedSlot(id, entry) {
|
|
1387
|
+
mountedSlots.delete(id);
|
|
1388
|
+
const token = Symbol();
|
|
1389
|
+
const result = entry.unmount();
|
|
1390
|
+
const remove = (stub) => {
|
|
1391
|
+
const leaving = leavingSlots.get(id);
|
|
1392
|
+
if (!leaving || leaving.token !== token) return;
|
|
1393
|
+
leaving.el.remove();
|
|
1394
|
+
stub?.remove();
|
|
1395
|
+
leavingSlots.delete(id);
|
|
1396
|
+
};
|
|
1397
|
+
if (result instanceof Promise) {
|
|
1398
|
+
let stub;
|
|
1399
|
+
if (entry.el.isConnected) {
|
|
1400
|
+
stub = document.createElement("div");
|
|
1401
|
+
stub.setAttribute(SLOT_ATTR, id);
|
|
1402
|
+
entry.el.replaceWith(stub);
|
|
1403
|
+
}
|
|
1404
|
+
leavingSlots.set(id, {
|
|
1405
|
+
el: entry.el,
|
|
1406
|
+
token
|
|
1407
|
+
});
|
|
1408
|
+
result.finally(() => remove(stub));
|
|
1409
|
+
} else entry.el.remove();
|
|
1410
|
+
}
|
|
1164
1411
|
function findSlot(id) {
|
|
1412
|
+
const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
|
|
1165
1413
|
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
1166
1414
|
const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
|
|
1167
1415
|
for (const candidate of candidates) {
|
|
1416
|
+
if (leavingEls.has(candidate)) continue;
|
|
1168
1417
|
let el = candidate;
|
|
1169
1418
|
while (el && el !== host) {
|
|
1170
1419
|
if (el.hasAttribute("data-ilha")) break;
|
|
@@ -1175,10 +1424,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1175
1424
|
return null;
|
|
1176
1425
|
}
|
|
1177
1426
|
function mountSlots(slotMap) {
|
|
1178
|
-
for (const [id, entry] of mountedSlots) if (!slotMap.has(id))
|
|
1179
|
-
entry.unmount();
|
|
1180
|
-
mountedSlots.delete(id);
|
|
1181
|
-
}
|
|
1427
|
+
for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
|
|
1182
1428
|
for (const [id, { island: childIsland, props }] of slotMap) {
|
|
1183
1429
|
const existing = mountedSlots.get(id);
|
|
1184
1430
|
if (existing) {
|
|
@@ -1229,6 +1475,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1229
1475
|
listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
|
|
1230
1476
|
}
|
|
1231
1477
|
const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
|
|
1478
|
+
const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
|
|
1232
1479
|
let handlerSignal;
|
|
1233
1480
|
if (entry.abortable) {
|
|
1234
1481
|
let entryMap = invocationControllers.get(entry);
|
|
@@ -1250,7 +1497,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1250
1497
|
derived,
|
|
1251
1498
|
input,
|
|
1252
1499
|
host,
|
|
1253
|
-
target:
|
|
1500
|
+
target: handlerTarget,
|
|
1254
1501
|
event,
|
|
1255
1502
|
signal: handlerSignal
|
|
1256
1503
|
});
|
|
@@ -1291,11 +1538,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1291
1538
|
input
|
|
1292
1539
|
}), hasExistingContent ? host : void 0);
|
|
1293
1540
|
if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
|
|
1294
|
-
attachListeners();
|
|
1295
1541
|
let stopBindings = applyTemplateBindings(host, initial.binds);
|
|
1296
1542
|
cleanups.push(() => stopBindings());
|
|
1297
1543
|
mountSlots(initial.slots);
|
|
1298
1544
|
cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
|
|
1545
|
+
attachListeners();
|
|
1299
1546
|
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
1300
1547
|
const prevSub = setActiveSub(void 0);
|
|
1301
1548
|
let userCleanup;
|
|
@@ -1331,6 +1578,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1331
1578
|
try {
|
|
1332
1579
|
userCleanup = entry.fn({
|
|
1333
1580
|
state,
|
|
1581
|
+
derived,
|
|
1334
1582
|
input,
|
|
1335
1583
|
host,
|
|
1336
1584
|
signal: runSignal
|
|
@@ -1365,19 +1613,34 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1365
1613
|
if (!initialized) {
|
|
1366
1614
|
initialized = true;
|
|
1367
1615
|
if (rendered === initialRenderedHtml) return;
|
|
1616
|
+
if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
|
|
1368
1617
|
}
|
|
1369
1618
|
detachListeners();
|
|
1370
1619
|
stopBindings();
|
|
1620
|
+
const prevMountedCount = mountedSlots.size;
|
|
1621
|
+
const removedSlotIds = [];
|
|
1371
1622
|
for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
|
|
1372
|
-
entry
|
|
1373
|
-
|
|
1623
|
+
teardownMountedSlot(id, entry);
|
|
1624
|
+
removedSlotIds.push(id);
|
|
1625
|
+
}
|
|
1626
|
+
for (const id of removedSlotIds) {
|
|
1627
|
+
if (leavingSlots.has(id)) continue;
|
|
1628
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
1629
|
+
for (const el of host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`)) el.remove();
|
|
1374
1630
|
}
|
|
1375
1631
|
const preserved = /* @__PURE__ */ new Map();
|
|
1632
|
+
const listShrunk = newSlotMap.size < prevMountedCount;
|
|
1376
1633
|
for (const [id, entry] of mountedSlots) {
|
|
1377
1634
|
if (!newSlotMap.has(id)) continue;
|
|
1378
1635
|
if (!entry.el.isConnected) continue;
|
|
1636
|
+
if (listShrunk && id.startsWith("p:")) {
|
|
1637
|
+
teardownMountedSlot(id, entry);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1379
1640
|
const stub = document.createElement("div");
|
|
1380
1641
|
stub.setAttribute(SLOT_ATTR, id);
|
|
1642
|
+
const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
|
|
1643
|
+
if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
|
|
1381
1644
|
entry.el.replaceWith(stub);
|
|
1382
1645
|
preserved.set(id, entry.el);
|
|
1383
1646
|
}
|
|
@@ -1400,16 +1663,23 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1400
1663
|
if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
|
|
1401
1664
|
stopRender();
|
|
1402
1665
|
detachListeners();
|
|
1666
|
+
const pending = [];
|
|
1667
|
+
for (const [, entry] of mountedSlots) {
|
|
1668
|
+
const result = entry.unmount();
|
|
1669
|
+
if (result instanceof Promise) pending.push(result);
|
|
1670
|
+
}
|
|
1403
1671
|
if (transition?.leave) {
|
|
1404
1672
|
const result = transition.leave(host);
|
|
1405
|
-
if (result instanceof Promise)
|
|
1406
|
-
result.then(() => {
|
|
1407
|
-
for (const c of cleanups) c();
|
|
1408
|
-
}).catch(console.error);
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1673
|
+
if (result instanceof Promise) pending.push(result);
|
|
1411
1674
|
}
|
|
1412
|
-
|
|
1675
|
+
const finish = () => {
|
|
1676
|
+
for (const c of cleanups) c();
|
|
1677
|
+
};
|
|
1678
|
+
if (pending.length > 0) return Promise.all(pending).then(finish).catch((err) => {
|
|
1679
|
+
console.error(err);
|
|
1680
|
+
finish();
|
|
1681
|
+
});
|
|
1682
|
+
finish();
|
|
1413
1683
|
};
|
|
1414
1684
|
const updateProps = (nextProps) => {
|
|
1415
1685
|
if (tornDown) return;
|
|
@@ -1569,7 +1839,14 @@ function mountAll(registry, options = {}) {
|
|
|
1569
1839
|
io.disconnect();
|
|
1570
1840
|
});
|
|
1571
1841
|
} else els.forEach(activateEl);
|
|
1572
|
-
return { unmount: () =>
|
|
1842
|
+
return { unmount: () => {
|
|
1843
|
+
const pending = [];
|
|
1844
|
+
for (const u of unmounts) {
|
|
1845
|
+
const result = u();
|
|
1846
|
+
if (result instanceof Promise) pending.push(result);
|
|
1847
|
+
}
|
|
1848
|
+
if (pending.length > 0) return Promise.all(pending).then(() => {});
|
|
1849
|
+
} };
|
|
1573
1850
|
}
|
|
1574
1851
|
const rootBuilder = new IlhaBuilder({
|
|
1575
1852
|
schema: null,
|