ilha 0.13.0 → 0.14.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.js CHANGED
@@ -1,3223 +1,65 @@
1
- import { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, serializeServerManifest, setJsxRuntimeBridge, setServerActionBinder, setTemplateRenderer } from "./internal.js";
2
- import { effect as effect$1, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
1
+ import { a as atom, c as isAtomHandle, d as makeFiber, i as interpret, l as closeFiber, n as renderToString, o as handleOwner, r as paint, s as instr, t as mount, u as getFiber } from "./mount-Dcy4teaf.js";
2
+ import { i as Fragment, t as h } from "./vnode-DHN2EFHi.js";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Stream from "effect/Stream";
5
+ import * as Scope from "effect/Scope";
6
+ import * as Deferred from "effect/Deferred";
3
7
 
4
- //#region src/index.ts
5
- const __DEV__ = false;
6
- const MAX_SNAPSHOT_CHARS = 262144;
7
- const MAX_SNAPSHOT_DEPTH = 32;
8
- function exceedsMaxDepth(value, depth) {
9
- if (depth > MAX_SNAPSHOT_DEPTH) return true;
10
- if (value === null || typeof value !== "object") return false;
11
- if (Array.isArray(value)) {
12
- for (const item of value) if (exceedsMaxDepth(item, depth + 1)) return true;
13
- return false;
14
- }
15
- for (const key in value) {
16
- if (!Object.hasOwn(value, key)) continue;
17
- if (exceedsMaxDepth(value[key], depth + 1)) return true;
18
- }
19
- return false;
20
- }
21
- const UNSAFE_SNAPSHOT_KEYS = /* @__PURE__ */ new Set([
22
- "__proto__",
23
- "constructor",
24
- "prototype"
25
- ]);
26
- function stripUnsafeKeys(value) {
27
- if (value === null || typeof value !== "object") return;
28
- if (Array.isArray(value)) {
29
- for (const item of value) stripUnsafeKeys(item);
30
- return;
31
- }
32
- for (const key of Object.getOwnPropertyNames(value)) if (UNSAFE_SNAPSHOT_KEYS.has(key)) delete value[key];
33
- else stripUnsafeKeys(value[key]);
34
- }
35
- function safeParseSnapshot(raw, label) {
36
- if (raw.length > MAX_SNAPSHOT_CHARS) {
37
- `${label}`;
38
- return;
39
- }
40
- let parsed;
41
- try {
42
- parsed = JSON.parse(raw);
43
- } catch {
44
- `${label}`;
45
- return;
46
- }
47
- if (exceedsMaxDepth(parsed, 1)) {
48
- `${label}`;
49
- return;
50
- }
51
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
52
- `${label}`;
53
- return;
54
- }
55
- stripUnsafeKeys(parsed);
56
- return parsed;
57
- }
58
- function shallowEqualInput(a, b) {
59
- if (Object.is(a, b)) return true;
60
- if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
61
- const ak = Object.keys(a);
62
- const bk = Object.keys(b);
63
- if (ak.length !== bk.length) return false;
64
- for (const k of ak) {
65
- if (!Object.hasOwn(b, k)) return false;
66
- if (!Object.is(a[k], b[k])) return false;
67
- }
68
- return true;
69
- }
70
- function parseHtmlFragment(html) {
71
- const tpl = document.createElement("template");
72
- tpl.innerHTML = html;
73
- return tpl.content;
74
- }
75
- function extractDirectChildSlotIdsInOrder(root) {
76
- const ids = [];
77
- for (const el of root.querySelectorAll("[data-ilha-slot]")) {
78
- if (!(el instanceof Element)) continue;
79
- let parent = el.parentElement ?? el.parentNode;
80
- let nested = false;
81
- while (parent && parent !== root) {
82
- if (parent instanceof Element && parent.hasAttribute("data-ilha-slot")) {
83
- nested = true;
84
- break;
85
- }
86
- parent = parent instanceof Element ? parent.parentElement ?? parent.parentNode : parent.parentNode;
87
- }
88
- if (!nested) ids.push(el.getAttribute("data-ilha-slot"));
89
- }
90
- return ids;
91
- }
92
- function extractSlotPropsAttr(root, slotId) {
93
- for (const el of root.querySelectorAll(`[${SLOT_ATTR}]`)) if (el.getAttribute(SLOT_ATTR) === slotId) return el.getAttribute(PROPS_ATTR) ?? "";
94
- return null;
95
- }
96
- const SLOT_TAG_NAME_RE = /^[a-z][a-z0-9-]*$/i;
97
- function assertValidSlotTagName(tag) {
98
- const trimmed = tag.trim();
99
- if (trimmed.length === 0) throw new Error("The { as } option requires a non-empty HTML tag name.");
100
- if (!SLOT_TAG_NAME_RE.test(trimmed)) throw new Error(`The { as } option tag must be a valid HTML element name (got "${tag}"). Use names like "span", "div", or "li".`);
101
- return trimmed.toLowerCase();
102
- }
103
- function getIslandSlotTag(island) {
104
- const tag = island[ISLAND_SLOT_TAG];
105
- if (typeof tag === "string" && tag.length > 0) return tag;
106
- return "div";
107
- }
108
- function wrapIslandSlotHtml({ tag, id, propsAttr, inner }) {
109
- return `<${tag} ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${inner}</${tag}>`;
110
- }
111
- function isStableInlineSlotMount({ initialHtml, renderedHtml, slotIds }) {
112
- if (typeof document === "undefined") return false;
113
- const initialRoot = parseHtmlFragment(initialHtml);
114
- const renderedRoot = parseHtmlFragment(renderedHtml);
115
- const initialOrder = extractDirectChildSlotIdsInOrder(initialRoot);
116
- const renderedOrder = extractDirectChildSlotIdsInOrder(renderedRoot);
117
- if (initialOrder.length !== renderedOrder.length || initialOrder.some((id, i) => id !== renderedOrder[i])) return false;
118
- for (const id of slotIds) {
119
- const initialProps = extractSlotPropsAttr(initialRoot, id);
120
- const renderedProps = extractSlotPropsAttr(renderedRoot, id);
121
- if (initialProps === null || renderedProps === null) return false;
122
- if (initialProps !== renderedProps) return false;
123
- }
124
- return true;
125
- }
126
- /**
127
- * Props safe to embed in `data-ilha-props`.
128
- * Functions cannot round-trip through JSON; children are owned by the live slot
129
- * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
130
- * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
131
- */
132
- function slotPropsForAttr(props, captureActions = false) {
133
- if (props === void 0) return void 0;
134
- let out;
135
- for (const key of Object.keys(props)) {
136
- if (key === "children") continue;
137
- const value = props[key];
138
- if (typeof value === "function") {
139
- const id = captureActions && !isRawHtmlValue(value) ? actionManifestId(value) : void 0;
140
- if (id) {
141
- const a = actionBoundArgs(value);
142
- (out ??= {})[key] = Array.isArray(a) ? {
143
- __ilha: "action",
144
- k: id,
145
- a
146
- } : {
147
- __ilha: "action",
148
- k: id
149
- };
150
- }
151
- continue;
152
- }
153
- if (typeof value === "symbol") continue;
154
- const encoded = encodeSlotPropValue(value);
155
- if (encoded === void 0 && value !== void 0 && value !== null) continue;
156
- (out ??= {})[key] = encoded;
157
- }
158
- return out;
159
- }
160
- const SLOT_RAW = Symbol.for("ilha.raw");
161
- function isRawHtmlValue(v) {
162
- return !!(v && typeof v === "object" && SLOT_RAW in v);
163
- }
164
- function encodeSlotPropValue(value, seen) {
165
- if (value == null) return value;
166
- if (typeof value === "function" || typeof value === "symbol") return void 0;
167
- if (typeof value !== "object") return value;
168
- if (isRawHtmlValue(value)) return {
169
- __ilha: "raw",
170
- value: value.value
171
- };
172
- const visited = seen ?? /* @__PURE__ */ new WeakSet();
173
- if (visited.has(value)) throw new TypeError("encodeSlotPropValue: circular reference in slot props");
174
- visited.add(value);
175
- if (Array.isArray(value)) return value.map((item) => encodeSlotPropValue(item, visited)).filter((item) => item !== void 0);
176
- if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
177
- const obj = value;
178
- const encoded = {};
179
- for (const key of Object.keys(obj)) {
180
- const next = encodeSlotPropValue(obj[key], visited);
181
- if (next !== void 0 || obj[key] === null) encoded[key] = next;
182
- }
183
- return encoded;
184
- }
185
- function makeRawHtml(value) {
186
- return {
187
- [SLOT_RAW]: true,
188
- value
189
- };
190
- }
191
- function reviveSlotPropValue(value) {
192
- if (value == null || typeof value !== "object") return value;
193
- if (Array.isArray(value)) return value.map(reviveSlotPropValue);
194
- const obj = value;
195
- if (obj.__ilha === "raw" && typeof obj.value === "string") return makeRawHtml(obj.value);
196
- const out = {};
197
- for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
198
- return out;
199
- }
200
- /** Revive attr-parsed props, restoring tagged `__ilha:raw` RawHtml values. */
201
- function reviveSlotProps(props) {
202
- if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
203
- return reviveSlotPropValue(props);
204
- }
205
- /** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
206
- function serializeSlotProps(props) {
207
- const safe = slotPropsForAttr(props);
208
- return safe === void 0 ? "" : JSON.stringify(safe);
209
- }
210
- const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
211
- const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
212
- "data-checked",
213
- "data-unchecked",
214
- "data-indeterminate",
215
- "aria-checked",
216
- "data-open",
217
- "data-closed",
218
- "data-state",
219
- "aria-expanded",
220
- "aria-hidden",
221
- "data-selected",
222
- "data-panel-open"
223
- ]);
224
- /**
225
- * Opt-in contract for controller-owned attributes: an element carrying
226
- * `data-morph-preserve="attr-a attr-b class"` keeps those attributes exactly
227
- * as they are in the live DOM — the morph neither overwrites nor removes them.
228
- * The Areia list above stays as a built-in default for `[data-slot]` elements.
229
- */
230
- const MORPH_PRESERVE_ATTR = "data-morph-preserve";
231
- function shouldPreserveMorphAttr(el, name) {
232
- if (name === MORPH_PRESERVE_ATTR) return el.hasAttribute(MORPH_PRESERVE_ATTR);
233
- const custom = el.getAttribute(MORPH_PRESERVE_ATTR);
234
- if (custom !== null) {
235
- for (const token of custom.split(/\s+/)) if (token === name) return true;
236
- }
237
- return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
238
- }
239
- function syncAttributes(from, to) {
240
- for (const { name, value } of to.attributes) {
241
- if (shouldPreserveMorphAttr(from, name)) continue;
242
- if (from.getAttribute(name) !== value) from.setAttribute(name, value);
243
- }
244
- for (const { name } of Array.from(from.attributes)) {
245
- if (shouldPreserveMorphAttr(from, name)) continue;
246
- if (!to.hasAttribute(name)) from.removeAttribute(name);
247
- }
248
- }
249
- /**
250
- * Morph identity key: explicit `data-key`, or the child-island slot id.
251
- * Slot hosts are matched by id like keyed list items so a mounted child
252
- * island's host element is reconciled IN PLACE on parent re-renders — never
253
- * detached and reinserted, which would blur a focused element inside it.
254
- */
255
- function morphKeyOf(el) {
256
- const k = el.getAttribute("data-key");
257
- if (k !== null) return `k:${k}`;
258
- const s = el.getAttribute(SLOT_ATTR);
259
- return s === null ? null : `s:${s}`;
260
- }
261
- function morphChildren(fromParent, toParent) {
262
- const toNodes = Array.from(toParent.childNodes);
263
- let fromKeyed = null;
264
- for (const child of fromParent.children) {
265
- const k = morphKeyOf(child);
266
- if (k !== null && !(fromKeyed ??= /* @__PURE__ */ new Map()).has(k)) fromKeyed.set(k, child);
267
- }
268
- let toKeys = null;
269
- if (fromKeyed !== null) {
270
- toKeys = /* @__PURE__ */ new Set();
271
- for (const child of toParent.children) {
272
- const k = morphKeyOf(child);
273
- if (k !== null) toKeys.add(k);
274
- }
275
- }
276
- for (let i = 0; i < toNodes.length; i++) {
277
- const toNode = toNodes[i];
278
- let fromNode = fromParent.childNodes[i];
279
- if (fromKeyed !== null) {
280
- const toKey = toNode.nodeType === 1 ? morphKeyOf(toNode) : null;
281
- if (toKey !== null) {
282
- const match = fromKeyed.get(toKey);
283
- if (match) {
284
- fromKeyed.delete(toKey);
285
- if (match !== fromNode) {
286
- fromParent.insertBefore(match, fromNode ?? null);
287
- fromNode = match;
288
- }
289
- }
290
- }
291
- if (fromNode instanceof Element) {
292
- const fromKey = morphKeyOf(fromNode);
293
- if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
294
- fromParent.insertBefore(toNode.cloneNode(true), fromNode);
295
- continue;
296
- }
297
- }
298
- }
299
- if (!fromNode) {
300
- fromParent.appendChild(toNode.cloneNode(true));
301
- continue;
302
- }
303
- if (fromNode.nodeType !== toNode.nodeType) {
304
- fromParent.replaceChild(toNode.cloneNode(true), fromNode);
305
- continue;
306
- }
307
- if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
308
- if (fromNode.nodeValue !== toNode.nodeValue) fromNode.nodeValue = toNode.nodeValue;
309
- continue;
310
- }
311
- if (fromNode.nodeType === 1) {
312
- const fromEl = fromNode;
313
- const toEl = toNode;
314
- if (fromEl.localName !== toEl.localName || fromEl.namespaceURI !== toEl.namespaceURI) {
315
- fromParent.replaceChild(toEl.cloneNode(true), fromEl);
316
- continue;
317
- }
318
- if (fromEl.hasAttribute(SLOT_ATTR) && !fromEl.isConnected) {
319
- fromParent.replaceChild(toEl.cloneNode(true), fromEl);
320
- continue;
321
- }
322
- {
323
- const slotId = toEl.getAttribute(SLOT_ATTR);
324
- if (slotId !== null && fromEl.getAttribute(SLOT_ATTR) === slotId) {
325
- const props = toEl.getAttribute(PROPS_ATTR);
326
- if (props !== null && fromEl.getAttribute(PROPS_ATTR) !== props) fromEl.setAttribute(PROPS_ATTR, props);
327
- continue;
328
- }
329
- }
330
- if (fromEl.localName === "input" && fromEl.type !== toEl.type) {
331
- fromParent.replaceChild(toEl.cloneNode(true), fromEl);
332
- continue;
333
- }
334
- if (fromEl.localName === "input") {
335
- const hadChecked = fromEl.hasAttribute("checked");
336
- const hadValue = fromEl.getAttribute("value");
337
- syncAttributes(fromEl, toEl);
338
- const hasChecked = toEl.hasAttribute("checked");
339
- if (hasChecked !== hadChecked) fromEl.checked = hasChecked;
340
- const newValue = toEl.getAttribute("value");
341
- if (newValue !== hadValue) fromEl.value = newValue ?? "";
342
- continue;
343
- }
344
- if (fromEl.localName === "select") {
345
- const before = /* @__PURE__ */ new Map();
346
- for (const o of fromEl.options) before.set(o, {
347
- attr: o.hasAttribute("selected"),
348
- live: o.selected
349
- });
350
- syncAttributes(fromEl, toEl);
351
- morphChildren(fromEl, toEl);
352
- const options = Array.from(fromEl.options);
353
- if (options.some((o) => o.hasAttribute("selected") !== (before.get(o)?.attr ?? false))) for (const o of options) o.selected = o.hasAttribute("selected");
354
- else for (const o of options) {
355
- const prev = before.get(o);
356
- if (prev && o.selected !== prev.live) o.selected = prev.live;
357
- }
358
- continue;
359
- }
360
- syncAttributes(fromEl, toEl);
361
- if (fromEl.localName === "textarea") {
362
- const newText = toEl.textContent ?? "";
363
- if (fromEl.textContent !== newText) {
364
- fromEl.textContent = newText;
365
- fromEl.value = newText;
366
- }
367
- } else morphChildren(fromEl, toEl);
368
- }
369
- }
370
- while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild.remove();
371
- }
372
- /**
373
- * Invariant: the morph must never lose focus for an element that survives it.
374
- * Surviving slot hosts and keyed elements are patched in place, but a genuine
375
- * reorder still detaches-and-reinserts an ancestor of `document.activeElement`,
376
- * which blurs it in real engines — snapshot before, restore after.
377
- */
378
- function snapshotFocusForMorph() {
379
- if (typeof document === "undefined") return null;
380
- const active = document.activeElement;
381
- if (!(active instanceof HTMLElement) || active === document.body) return null;
382
- let selection = null;
383
- let range = null;
384
- try {
385
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) selection = {
386
- start: active.selectionStart,
387
- end: active.selectionEnd,
388
- dir: active.selectionDirection
389
- };
390
- else if (active.isContentEditable) {
391
- const sel = window.getSelection();
392
- if (sel && sel.rangeCount > 0) range = sel.getRangeAt(0).cloneRange();
393
- }
394
- } catch {}
395
- return {
396
- active,
397
- selection,
398
- range
399
- };
400
- }
401
- function restoreFocusAfterMorph(snapshot) {
402
- if (!snapshot) return;
403
- const { active, selection, range } = snapshot;
404
- if (!active.isConnected) return;
405
- try {
406
- if (document.activeElement !== active) active.focus({ preventScroll: true });
407
- if (selection && selection.start !== null) {
408
- const el = active;
409
- if (el.selectionStart !== selection.start || el.selectionEnd !== selection.end) el.setSelectionRange(selection.start, selection.end, selection.dir ?? "none");
410
- }
411
- if (range && range.startContainer.isConnected) {
412
- const sel = window.getSelection();
413
- if (sel) {
414
- const current = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
415
- if (!(current !== null && current.startContainer === range.startContainer && current.startOffset === range.startOffset && current.endContainer === range.endContainer && current.endOffset === range.endOffset)) {
416
- sel.removeAllRanges();
417
- sel.addRange(range);
418
- }
419
- }
420
- }
421
- } catch {}
422
- }
423
- function morphInner(from, to) {
424
- if (from.localName !== to.localName || from.namespaceURI !== to.namespaceURI) throw new Error("[ilha] morph: elements must match");
425
- const focus = snapshotFocusForMorph();
426
- morphChildren(from, to);
427
- restoreFocusAfterMorph(focus);
428
- }
429
- function isStandardSchema(value) {
430
- if (value == null || typeof value !== "object") return false;
431
- const std = value["~standard"];
432
- return std != null && typeof std.validate === "function" && std.version === 1;
433
- }
434
- function validateSchema(schema, value) {
435
- const result = schema["~standard"].validate(value);
436
- if (result instanceof Promise) throw new Error("[ilha] Async schemas are not supported.");
437
- if (result.issues) throw new Error(`[ilha] Validation failed:\n${result.issues.map((i) => ` - ${i.message}`).join("\n")}`);
438
- return result.value;
439
- }
440
- const ESC = {
441
- "&": "&amp;",
442
- "<": "&lt;",
443
- ">": "&gt;",
444
- "\"": "&quot;",
445
- "'": "&#39;"
446
- };
447
- function escapeHtml(value) {
448
- return String(value).replace(/[&<>"']/g, (c) => ESC[c]);
449
- }
450
- const SAFE_CSS_PROP_RE = /^(-{2}[a-zA-Z][a-zA-Z0-9-]*|-?[a-zA-Z][a-zA-Z0-9-]*)$/;
451
- const URL_ATTRS = /* @__PURE__ */ new Set([
452
- "href",
453
- "src",
454
- "srcset",
455
- "imagesrcset",
456
- "action",
457
- "formaction",
458
- "cite",
459
- "data",
460
- "poster"
461
- ]);
462
- const SAFE_DATA_IMAGE_RE = /^data:image\/(png|jpe?g|gif|webp|avif)[;,]/i;
463
- const UNSAFE_SCHEME_RE = /^(?:javascript|vbscript):/i;
464
- const DATA_IMAGE_CONTEXTS = /* @__PURE__ */ new Set([
465
- "img:src",
466
- "source:srcset",
467
- "link:imagesrcset"
468
- ]);
469
- function normalizeUrl(value) {
470
- return value.replace(/[\u0000-\u0020]/g, "");
471
- }
472
- function checkUrl(normalized, allowDataImage) {
473
- if (UNSAFE_SCHEME_RE.test(normalized)) return false;
474
- if (/^data:/i.test(normalized)) return allowDataImage && SAFE_DATA_IMAGE_RE.test(normalized);
475
- return true;
476
- }
477
- /** Strict single-URL check — all data: URLs rejected. */
478
- function isSafeUrl(value) {
479
- return checkUrl(normalizeUrl(value), false);
480
- }
481
- /** Element- and attribute-aware URL policy for rendered attributes.
482
- * `srcset`/`imagesrcset` validate each comma-separated candidate URL. */
483
- function isSafeUrlAttrValue(tagName, attrName, value) {
484
- const attr = attrName.toLowerCase();
485
- const allowDataImage = DATA_IMAGE_CONTEXTS.has(`${tagName.toLowerCase()}:${attr}`) && !attr.startsWith("srcset") && attr !== "imagesrcset";
486
- if (/^(?:srcset|imagesrcset)$/i.test(attr)) return value.split(",").every((candidate) => {
487
- const url = candidate.trim().split(/\s+/)[0] ?? "";
488
- return url === "" || checkUrl(normalizeUrl(url), false);
489
- });
490
- return checkUrl(normalizeUrl(value), allowDataImage);
491
- }
492
- function isUrlAttributeName(name) {
493
- const lower = name.toLowerCase();
494
- return URL_ATTRS.has(lower) || /:(href|src|srcset|imagesrcset|action|formaction|cite|data|poster)$/.test(lower);
495
- }
496
- function isPlainObject(v) {
497
- return v !== null && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
498
- }
499
- function serializeStyle(value) {
500
- return Object.entries(value).map(([k, v]) => {
501
- if (!SAFE_CSS_PROP_RE.test(k)) return "";
502
- const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
503
- const str = String(v);
504
- if (/[<>{};]/.test(str) || /expression\(/i.test(str) || /javascript:/i.test(str)) return "";
505
- return `${prop}:${str}`;
506
- }).filter(Boolean).join(";");
507
- }
508
- function serializeScriptJson(value) {
509
- return JSON.stringify(value).replace(/</g, "\\u003C");
510
- }
511
- function escapeStyleText(text) {
512
- return text.replace(/</g, "\\3C ");
513
- }
514
- const RAW = Symbol.for("ilha.raw");
515
- const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
516
- const ISLAND = Symbol.for("ilha.island");
517
- const ISLAND_CALL = Symbol.for("ilha.islandCall");
518
- const ASTRO_RENDERER_GLOBAL = Symbol.for("ilha.astroRenderer");
519
- const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
520
- const ISLAND_CLIENT_REF = Symbol.for("ilha.clientRef");
521
- /** @internal Sync re-render of an island from a state snapshot. Attached by
522
- * render(); used by server-frame endpoints to produce HTML for streamed
523
- * updates without a live mount. Not part of the public surface. */
524
- const ISLAND_RENDER_STATE = Symbol.for("ilha.renderState");
525
- /** @internal Async SSR render that records the event→action manifest into the
526
- * provided map. Used by emitIslandSlot so nested server islands ship their
527
- * interactive surface alongside their inlined HTML. */
528
- const ISLAND_SSR_MANIFEST = Symbol.for("ilha.ssrManifest");
529
- const SLOT_ATTR = "data-ilha-slot";
530
- const PROPS_ATTR = "data-ilha-props";
531
- const CLIENT_REF_ATTR = "data-ilha-client-ref";
532
- const STATE_ATTR = "data-ilha-state";
533
- function normalizeElseOutput(value) {
534
- if (value == null || value === false) return value;
535
- return Array.isArray(value) ? value : [value];
536
- }
537
- function createEachResult(items, mapFn) {
538
- const mapped = items.length === 0 ? [] : items.map(mapFn);
539
- const result = mapped;
540
- Object.defineProperty(result, "else", {
541
- value(fallback) {
542
- if (items.length === 0) return normalizeElseOutput(typeof fallback === "function" ? fallback(items) : fallback);
543
- return mapped;
544
- },
545
- enumerable: false
546
- });
547
- return result;
548
- }
549
- function createEachBuilder(items) {
550
- return {
551
- key(keyFn) {
552
- return { as(mapFn) {
553
- return createEachResult(items, (item, index) => mapFn(item, index, keyFn(item, index)));
554
- } };
555
- },
556
- as(mapFn) {
557
- return createEachResult(items, mapFn);
558
- }
559
- };
560
- }
561
- /**
562
- * Svelte-style `{#each}` helper for mapping collections to rendered output
563
- * with an optional empty fallback.
564
- *
565
- * @example — plain list (empty → [])
566
- * each(items).as((item) => html`<li>${item.name}</li>`)
567
- *
568
- * @example — keyed list feeding island.key(), with empty fallback
569
- * each(items)
570
- * .key((item) => item.id)
571
- * .as((item, _i, id) => Row.key(id)({ item }))
572
- * .else(() => html`<EmptyState />`)
573
- */
574
- function each(items) {
575
- if (typeof items === "function") throw new TypeError("[ilha] each() expected an array but received a function. Call accessors first (each(state.items())) or pass a snapshot from a reactive render.");
576
- return createEachBuilder(Array.isArray(items) ? items : []);
577
- }
578
- /** Marks SSR action stubs so event registration can recognize action
579
- * references without ever invoking them (executing a handler body during
580
- * render would run its side effects server-side). Symbol.for brands keep
581
- * this recognizable across duplicate ilha copies and external action
582
- * implementations (e.g. oxidejs) in the same realm. */
583
- const SSR_ACTION_STUB = Symbol.for("ilha.ssrActionStub");
584
- const ACTION_MANIFEST_ID = Symbol.for("ilha.actionManifestId");
585
- const ACTION_BOUND_ARGS = Symbol.for("ilha.actionBoundArgs");
586
- /** Serialized size ceiling for `.with()` payloads (JSON chars). */
587
- const MAX_BOUND_ARGS_CHARS = 8192;
588
- /** Manifest id for a brand-recognized action reference, else undefined.
589
- * Anything that isn't a branded action stub is rejected outright — event
590
- * handlers are never executed during SSR to discover what they call. */
591
- function actionManifestId(fn) {
592
- if (typeof fn !== "function" || !(SSR_ACTION_STUB in fn)) return void 0;
593
- return fn[ACTION_MANIFEST_ID];
594
- }
595
- /** Bound args recorded by `.with()` on an action reference, else undefined. */
596
- function actionBoundArgs(fn) {
597
- if (typeof fn !== "function" || !(ACTION_BOUND_ARGS in fn)) return void 0;
598
- const args = fn[ACTION_BOUND_ARGS];
599
- return Array.isArray(args) ? args : void 0;
600
- }
601
- /** Attach `.with(...args)` to an action accessor. Returns an explicit,
602
- * serializable action REFERENCE — never a closure to inspect or probe.
603
- * Arguments are validated JSON-safe at bind time; the hydration manifest
604
- * stores `{ actionId, args }` directly and the client RPC path sends them
605
- * only after a real user event. On the client, calling the bound reference
606
- * invokes the accessor with the bound payload. */
607
- /** Keys that must never appear in a bound payload — they become dangerous
608
- * if an RPC handler or downstream library merges the object into another. */
609
- const UNSAFE_KEYS = /* @__PURE__ */ new Set([
610
- "__proto__",
611
- "constructor",
612
- "prototype"
613
- ]);
614
- /** Strict JSON-safety validator: accept, or throw — never transform.
615
- * Rejects undefined, functions, symbols, bigint, non-finite numbers,
616
- * Dates/custom prototypes/Map/Set, circular values, and unsafe keys so a
617
- * bound payload is byte-identical to what the author wrote. */
618
- function assertJsonSafe(value, seen = /* @__PURE__ */ new WeakSet()) {
619
- if (value === null) return;
620
- switch (typeof value) {
621
- case "string":
622
- case "boolean": return;
623
- case "number":
624
- if (Number.isFinite(value)) return;
625
- throw new TypeError("with() rejects non-finite numbers.");
626
- case "object": break;
627
- default: throw new TypeError(`with() rejects ${typeof value} values.`);
628
- }
629
- if (seen.has(value)) throw new TypeError("with() rejects circular values.");
630
- seen.add(value);
631
- if (Array.isArray(value)) {
632
- for (const item of value) assertJsonSafe(item, seen);
633
- return;
634
- }
635
- if (Object.getPrototypeOf(value) !== Object.prototype) throw new TypeError("with() accepts plain objects only.");
636
- for (const [key, child] of Object.entries(value)) {
637
- if (UNSAFE_KEYS.has(key)) throw new TypeError(`with() rejects unsafe key "${key}".`);
638
- assertJsonSafe(child, seen);
639
- }
640
- }
641
- /** Deep-clone an already-validated JSON-safe value. */
642
- function cloneJsonSafe(value) {
643
- return structuredClone(value);
644
- }
645
- function attachWith(accessor) {
646
- Object.defineProperty(accessor, "with", {
647
- enumerable: false,
648
- value: (...args) => {
649
- for (const arg of args) assertJsonSafe(arg);
650
- const serialized = cloneJsonSafe(args);
651
- if (JSON.stringify(serialized).length > MAX_BOUND_ARGS_CHARS) throw new RangeError(`with() payload exceeds ${MAX_BOUND_ARGS_CHARS} JSON characters.`);
652
- const manifestId = actionManifestId(accessor);
653
- const bound = (...runtimeArgs) => {
654
- return accessor(serialized.length > 0 ? serialized.length === 1 ? serialized[0] : serialized : runtimeArgs.length === 1 ? runtimeArgs[0] : runtimeArgs);
655
- };
656
- const brand = bound;
657
- brand[SSR_ACTION_STUB] = true;
658
- brand[ACTION_MANIFEST_ID] = manifestId;
659
- brand[ACTION_BOUND_ARGS] = serialized;
660
- return bound;
661
- }
662
- });
663
- }
664
- setServerActionBinder(function bindExternalAction(fn, manifestId) {
665
- const action = fn;
666
- action[SSR_ACTION_STUB] = true;
667
- action[ACTION_MANIFEST_ID] = manifestId;
668
- attachWith(action);
669
- return action;
670
- });
671
- const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
672
- function renderCtxStack() {
673
- const g = globalThis;
674
- return g[RENDER_CTX_STACK] ??= [];
675
- }
676
- function pushRenderCtx(liveHost, asyncChildren, manifest) {
677
- const ctx = {
678
- slots: /* @__PURE__ */ new Map(),
679
- positional: 0,
680
- liveHost,
681
- pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
682
- binds: [],
683
- events: [],
684
- manifest
685
- };
686
- renderCtxStack().push(ctx);
687
- return ctx;
688
- }
689
- function popRenderCtx() {
690
- renderCtxStack().pop();
691
- }
692
- function currentRenderCtx() {
693
- const stack = renderCtxStack();
694
- return stack[stack.length - 1];
695
- }
696
- function isIsland(v) {
697
- return typeof v === "function" && ISLAND in v;
698
- }
699
- function isIslandCall(v) {
700
- if (v == null || typeof v !== "object" && typeof v !== "function") return false;
701
- if (ISLAND_CALL in v) return true;
702
- return typeof v === "object" && "island" in v && isIsland(v.island);
703
- }
704
- function emitIslandSlot({ island, props, key }) {
705
- const ctx = currentRenderCtx();
706
- let id;
707
- if (key === void 0) id = ctx ? `p:${ctx.positional++}` : "p:0";
708
- else id = `k:${key}`;
709
- if (ctx) ctx.slots.set(id, {
710
- island,
711
- props
712
- });
713
- const slotTag = getIslandSlotTag(island);
714
- const clientRef = island[ISLAND_CLIENT_REF];
715
- const attrProps = slotPropsForAttr(props, typeof clientRef === "string" && ctx?.manifest === true);
716
- const propsAttr = (attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "") + (typeof clientRef === "string" ? ` ${CLIENT_REF_ATTR}="${escapeHtml(clientRef)}"` : "");
717
- if (ctx?.liveHost) return wrapIslandSlotHtml({
718
- tag: slotTag,
719
- id,
720
- propsAttr,
721
- inner: ""
722
- });
723
- if (ctx?.pending) {
724
- popRenderCtx();
725
- try {
726
- const manifest = /* @__PURE__ */ new Map();
727
- const ssrWithManifest = island[ISLAND_SSR_MANIFEST];
728
- const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island.toStringAsync(props);
729
- const prefix = () => serializeServerManifest(manifest);
730
- if (result instanceof Promise) {
731
- ctx.pending.set(id, result.then(String).then((html) => prefix() + html));
732
- return wrapIslandSlotHtml({
733
- tag: slotTag,
734
- id,
735
- propsAttr,
736
- inner: asyncSlotMarker(id)
737
- });
738
- }
739
- return wrapIslandSlotHtml({
740
- tag: slotTag,
741
- id,
742
- propsAttr,
743
- inner: prefix() + String(result)
744
- });
745
- } finally {
746
- renderCtxStack().push(ctx);
747
- }
748
- }
749
- const inner = island.toString(props);
750
- return wrapIslandSlotHtml({
751
- tag: slotTag,
752
- id,
753
- propsAttr,
754
- inner
755
- });
756
- }
757
- function asyncSlotMarker(id) {
758
- return `<!--ilha-async:${escapeHtml(id)}-->`;
759
- }
760
- async function resolveAsyncChildren(html, pending) {
761
- for (const [id, promise] of pending) {
762
- const inner = await promise;
763
- html = html.split(asyncSlotMarker(id)).join(inner);
764
- }
765
- return html;
766
- }
767
- function markSignalAccessor(read, write) {
768
- read[SIGNAL_ACCESSOR] = true;
769
- const accessor = read;
770
- accessor.set = (value) => write(value);
771
- accessor.update = (fn) => {
772
- const prevSub = setActiveSub(void 0);
773
- try {
774
- write(fn(read()));
775
- } finally {
776
- setActiveSub(prevSub);
777
- }
778
- };
779
- accessor.select = ((...args) => createSelectAccessor(accessor, args));
780
- return accessor;
781
- }
782
- function isSignalAccessor(v) {
783
- return typeof v === "function" && SIGNAL_ACCESSOR in v;
784
- }
785
- function getAtPath(obj, path) {
786
- let cur = obj;
787
- for (const seg of path) {
788
- if (cur == null) return void 0;
789
- cur = cur[seg];
790
- }
791
- return cur;
792
- }
793
- function setAtPath({ object, path, value }) {
794
- const obj = object;
795
- if (path.length === 0) return value;
796
- const [head, ...rest] = path;
797
- if (Array.isArray(obj)) {
798
- const idx = head;
799
- if (idx < 0 || idx >= obj.length) return obj;
800
- const next = obj[idx];
801
- const updated = rest.length === 0 ? value : setAtPath({
802
- object: next,
803
- path: rest,
804
- value
805
- });
806
- if (Object.is(next, updated)) return obj;
807
- const copy = obj.slice();
808
- copy[idx] = updated;
809
- return copy;
810
- }
811
- if (obj !== null && typeof obj === "object") {
812
- const record = obj;
813
- const key = String(head);
814
- const next = record[key];
815
- const updated = rest.length === 0 ? value : setAtPath({
816
- object: next,
817
- path: rest,
818
- value
819
- });
820
- if (rest.length === 0) {
821
- if (Object.is(next, value)) return obj;
822
- } else if (Object.is(next, updated)) return obj;
823
- return {
824
- ...record,
825
- [key]: updated
826
- };
827
- }
828
- return rest.length === 0 ? value : setAtPath({
829
- object: void 0,
830
- path: rest,
831
- value
832
- });
833
- }
834
- function toPathSegment(prop) {
835
- if (typeof prop === "symbol") return null;
836
- if (prop === "length") return null;
837
- if (/^\d+$/.test(prop)) return Number(prop);
838
- return prop;
839
- }
840
- function trackSelectPath(rootState, selector) {
841
- const path = [];
842
- const track = (value) => {
843
- if (value === null || typeof value !== "object" && typeof value !== "function") return value;
844
- return new Proxy(value, { get(target, prop, receiver) {
845
- const seg = toPathSegment(prop);
846
- if (seg != null) path.push(seg);
847
- const next = Reflect.get(target, prop, receiver);
848
- return seg != null && next !== null && typeof next === "object" ? track(next) : next;
8
+ //#region src/when.ts
9
+ function when(stream, body) {
10
+ return instr(Effect.gen(function* () {
11
+ const parent = getFiber();
12
+ let child;
13
+ parent.holes.push({ dispose() {
14
+ if (child) closeFiber(child);
15
+ child = void 0;
849
16
  } });
850
- };
851
- selector(track(rootState));
852
- return path;
853
- }
854
- function createSelectAccessor(root, input) {
855
- const selector = typeof input[0] === "function" ? input[0] : void 0;
856
- let path;
857
- if (selector) {
858
- path = trackSelectPath(root(), selector);
859
- const selected = selector(root());
860
- const resolved = path.length === 0 ? root() : getAtPath(root(), path);
861
- if (!Object.is(selected, resolved)) throw new Error("select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.");
862
- } else path = input;
863
- const read = (from) => selector ? path.length === 0 ? selector(from) : getAtPath(from, path) : getAtPath(from, path);
864
- return markSignalAccessor(() => read(root()), (value) => {
865
- const previousRoot = root();
866
- const next = path.length === 0 && selector ? value : setAtPath({
867
- object: previousRoot,
868
- path,
869
- value
870
- });
871
- if (!Object.is(previousRoot, next)) root.set(next);
872
- });
873
- }
874
- function ilhaRaw(value) {
875
- return {
876
- [RAW]: true,
877
- value
878
- };
879
- }
880
- /**
881
- * Serialize a JSON-safe value into executable `<script>` content safely. `<`
882
- * is escaped as `\u003C`, so a value can never close the script block or open
883
- * the `<!--` escape hatch. Use it for script-element data:
884
- *
885
- * ```ts
886
- * html`<script>const d = ${json(payload)};<\/script>`
887
- * ```
888
- *
889
- * Like `raw()`, `json()` is for element *content* only — interpolating it into
890
- * an attribute is author error. It does not protect against values that are
891
- * not valid JSON (functions, symbols, undefined, cyclic objects are dropped
892
- * or throw, matching `JSON.stringify`).
893
- */
894
- function json(value) {
895
- return ilhaRaw(serializeScriptJson(value));
896
- }
897
- /**
898
- * Escape text for a `<style>` element body. `<` becomes the CSS hex escape
899
- * `\3C `, so the value can never close the style block or open the `<!--`
900
- * escape hatch while surviving CSS parsing:
901
- *
902
- * ```ts
903
- * html`<style>${css(cssSource)}</style>`
904
- * ```
905
- *
906
- * Like `raw()`, `css()` is for element *content* only — interpolating it into
907
- * an attribute is author error.
908
- */
909
- function css(value) {
910
- return ilhaRaw(escapeStyleText(value));
911
- }
912
- function interpolateValue(v) {
913
- if (v == null || v === true || v === false) return "";
914
- if (Array.isArray(v)) return v.map(interpolateValue).join("");
915
- if (isRawHtml(v)) return v.value;
916
- if (isIslandCall(v)) {
917
- const call = typeof v === "function" ? v() : v;
918
- return emitIslandSlot({
919
- island: call.island,
920
- props: call.props,
921
- key: call.key
922
- });
923
- }
924
- if (isIsland(v)) return emitIslandSlot({
925
- island: v,
926
- props: void 0,
927
- key: void 0
928
- });
929
- if (isSignalAccessor(v)) return escapeHtml(v());
930
- if (typeof v === "function") return escapeHtml(v());
931
- return escapeHtml(v);
932
- }
933
- const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
934
- "value",
935
- "checked",
936
- "files",
937
- "open",
938
- "group",
939
- "this"
940
- ]);
941
- const NATIVE_EVENT_MODIFIERS = /* @__PURE__ */ new Set([
942
- "abortable",
943
- "once",
944
- "capture",
945
- "passive"
946
- ]);
947
- function resolveTemplateValue(v) {
948
- if (isSignalAccessor(v)) return v();
949
- if (typeof v === "function") return v();
950
- return v;
951
- }
952
- function emitBindSSR({ kind, index, accessor, optionValue }) {
953
- const spec = `${kind}:${index}`;
954
- let v;
955
- try {
956
- v = accessor();
957
- } catch {
958
- v = void 0;
959
- }
960
- switch (kind) {
961
- case "value": return [` value="${escapeHtml(v ?? "")}"`, spec];
962
- case "checked": return [v ? ` checked` : ``, spec];
963
- case "open": return [v ? ` open` : ``, spec];
964
- case "files": return [``, spec];
965
- case "this": return [``, spec];
966
- case "group":
967
- if (optionValue == null) return [``, spec];
968
- return [(Array.isArray(v) ? v.map(String).includes(optionValue) : v != null && String(v) === optionValue) ? ` checked` : ``, spec];
969
- }
970
- }
971
- const TEMPLATE_ATTRIBUTE = Symbol.for("ilha.templateAttribute");
972
- const TEMPLATE_PARTS = Symbol.for("ilha.templateParts");
973
- const TEMPLATE_SLOT_START = "ilha:";
974
- const TEMPLATE_SLOT_END = ":";
975
- const TEMPLATE_SLOT_RE = /\uE000ilha:(\d+):\uE001/g;
976
- const templateBlueprintCache = /* @__PURE__ */ new WeakMap();
977
- const TEMPLATE_PARSE_VOID_TAGS = /* @__PURE__ */ new Set([
978
- "area",
979
- "base",
980
- "br",
981
- "col",
982
- "embed",
983
- "hr",
984
- "img",
985
- "input",
986
- "link",
987
- "meta",
988
- "param",
989
- "source",
990
- "track",
991
- "wbr"
992
- ]);
993
- const TEMPLATE_PARSE_RAW_TEXT_TAGS = /* @__PURE__ */ new Set([
994
- "script",
995
- "style",
996
- "textarea",
997
- "title"
998
- ]);
999
- function templateSkipWs(s, i) {
1000
- while (i < s.length && /[\t\n\f\r ]/.test(s[i])) i++;
1001
- return i;
1002
- }
1003
- function templateAsciiLower(source) {
1004
- return source.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
1005
- }
1006
- function templateClosingTagAt(lower, index, tag) {
1007
- const prefix = `</${tag}`;
1008
- if (!lower.startsWith(prefix, index)) return false;
1009
- const boundary = lower[index + prefix.length];
1010
- return boundary === ">" || boundary === "/" || /[\t\n\f\r ]/.test(boundary ?? "");
1011
- }
1012
- function templateFindClosingTag(lower, tag, from) {
1013
- const needle = `</${tag}`;
1014
- let index = lower.indexOf(needle, from);
1015
- while (index !== -1) {
1016
- if (templateClosingTagAt(lower, index, tag)) return index;
1017
- index = lower.indexOf(needle, index + 1);
1018
- }
1019
- return -1;
1020
- }
1021
- function templateParseFragment(source) {
1022
- const children = [];
1023
- templateParseNodes(source, templateAsciiLower(source), 0, null, children);
1024
- return {
1025
- nodeName: "#document-fragment",
1026
- childNodes: children
1027
- };
1028
- }
1029
- function templateParseNodes(s, lower, i, stop, out) {
1030
- while (i < s.length) {
1031
- if (stop && templateClosingTagAt(lower, i, stop)) {
1032
- const gt = s.indexOf(">", i);
1033
- return gt === -1 ? s.length : gt + 1;
1034
- }
1035
- if (s.startsWith("<!--", i)) {
1036
- const end = s.indexOf("-->", i + 4);
1037
- const close = end === -1 ? s.length : end + 3;
1038
- out.push({
1039
- nodeName: "#comment",
1040
- data: s.slice(i + 4, end === -1 ? s.length : end)
1041
- });
1042
- i = close;
1043
- continue;
1044
- }
1045
- if (stop === null && s[i] === "<" && s[i + 1] === "/") {
1046
- const gt = s.indexOf(">", i);
1047
- i = gt === -1 ? s.length : gt + 1;
1048
- continue;
1049
- }
1050
- if (s[i] === "<" && /[A-Za-z]/.test(s[i + 1] ?? "")) {
1051
- const start = i;
1052
- i++;
1053
- const tagStart = i;
1054
- while (i < s.length && /[A-Za-z0-9:-]/.test(s[i])) i++;
1055
- const tagOrig = s.slice(tagStart, i);
1056
- const tag = tagOrig.toLowerCase();
1057
- const attrs = [];
1058
- const attrLocs = {};
1059
- i = templateSkipWs(s, i);
1060
- while (i < s.length && s[i] !== ">" && s[i] !== "/") {
1061
- const attrStart = i;
1062
- while (i < s.length && /[^\s=>/]/.test(s[i])) i++;
1063
- const rawName = s.slice(attrStart, i);
1064
- if (!rawName) break;
1065
- const name = rawName;
1066
- i = templateSkipWs(s, i);
1067
- let value = "";
1068
- if (s[i] === "=") {
1069
- i++;
1070
- i = templateSkipWs(s, i);
1071
- if (s[i] === "\"" || s[i] === "'") {
1072
- const q = s[i];
1073
- i++;
1074
- const vs = i;
1075
- while (i < s.length && s[i] !== q) i++;
1076
- value = s.slice(vs, i);
1077
- if (s[i] === q) i++;
1078
- } else {
1079
- const vs = i;
1080
- while (i < s.length && /[^\s>]/.test(s[i])) {
1081
- if (s[i] === "/") {
1082
- let j = i + 1;
1083
- while (j < s.length && /\s/.test(s[j])) j++;
1084
- if (j >= s.length || s[j] === ">") break;
1085
- }
1086
- i++;
1087
- }
1088
- value = s.slice(vs, i);
1089
- }
1090
- }
1091
- attrs.push({
1092
- name,
1093
- value
1094
- });
1095
- attrLocs[name] = {
1096
- startOffset: attrStart,
1097
- endOffset: i
1098
- };
1099
- i = templateSkipWs(s, i);
1100
- }
1101
- if (s[i] === "/") {
1102
- i++;
1103
- i = templateSkipWs(s, i);
1104
- }
1105
- if (s[i] === ">") i++;
1106
- const el = {
1107
- nodeName: tagOrig,
1108
- tagName: tagOrig,
1109
- attrs,
1110
- childNodes: [],
1111
- sourceCodeLocation: {
1112
- attrs: attrLocs,
1113
- startTag: {
1114
- startOffset: start,
1115
- endOffset: i
1116
- }
1117
- }
1118
- };
1119
- if (!TEMPLATE_PARSE_VOID_TAGS.has(tag)) {
1120
- if (TEMPLATE_PARSE_RAW_TEXT_TAGS.has(tag)) {
1121
- const ci = templateFindClosingTag(lower, tag, i);
1122
- const textEnd = ci === -1 ? s.length : ci;
1123
- if (textEnd > i) el.childNodes.push({
1124
- nodeName: "#text",
1125
- value: s.slice(i, textEnd)
1126
- });
1127
- i = ci === -1 ? s.length : s.indexOf(">", ci) === -1 ? s.length : s.indexOf(">", ci) + 1;
1128
- } else i = templateParseNodes(s, lower, i, tag, el.childNodes);
1129
- }
1130
- out.push(el);
1131
- continue;
1132
- }
1133
- const next = s.indexOf("<", i);
1134
- const end = next === -1 ? s.length : next;
1135
- if (end > i) {
1136
- out.push({
1137
- nodeName: "#text",
1138
- value: s.slice(i, end)
1139
- });
1140
- i = end;
1141
- continue;
1142
- }
1143
- out.push({
1144
- nodeName: "#text",
1145
- value: s[i]
1146
- });
1147
- i++;
1148
- }
1149
- return i;
1150
- }
1151
- function templateSegments(value) {
1152
- const out = [];
1153
- let offset = 0;
1154
- for (const match of value.matchAll(TEMPLATE_SLOT_RE)) {
1155
- if (match.index > offset) out.push(value.slice(offset, match.index));
1156
- out.push({ slot: Number(match[1]) });
1157
- offset = match.index + match[0].length;
1158
- }
1159
- if (offset < value.length) out.push(value.slice(offset));
1160
- return out;
1161
- }
1162
- function templateBlueprint(node, source) {
1163
- if (node.nodeName === "#text") return {
1164
- kind: "text",
1165
- value: templateSegments(node.value)
1166
- };
1167
- if (node.nodeName === "#comment") return {
1168
- kind: "comment",
1169
- value: templateSegments(node.data)
1170
- };
1171
- if (node.nodeName === "#document-fragment") return {
1172
- kind: "fragment",
1173
- children: node.childNodes.map((child) => templateBlueprint(child, source)).filter(Boolean)
1174
- };
1175
- if (typeof node.tagName === "string") {
1176
- const props = {};
1177
- for (const attr of node.attrs ?? []) {
1178
- const location = node.sourceCodeLocation?.attrs?.[attr.name];
1179
- const sourceAttr = location ? source.slice(location.startOffset, location.endOffset) : attr.name;
1180
- const parts = templateSegments(attr.value);
1181
- parts.quoted = /=\s*["']/.test(sourceAttr);
1182
- parts.bare = !sourceAttr.includes("=");
1183
- props[attr.name] = parts;
1184
- }
1185
- return {
1186
- kind: "element",
1187
- tag: node.tagName,
1188
- props,
1189
- children: (node.childNodes ?? []).map((child) => templateBlueprint(child, source)).filter(Boolean),
1190
- selfClosing: node.sourceCodeLocation?.startTag ? /\/\s*>$/.test(source.slice(node.sourceCodeLocation.startTag.startOffset, node.sourceCodeLocation.startTag.endOffset)) : false
1191
- };
1192
- }
1193
- return null;
1194
- }
1195
- function templateCompile(strings) {
1196
- let source = strings[0] ?? "";
1197
- for (let i = 0; i < strings.length - 1; i++) source += `${TEMPLATE_SLOT_START}${i}${TEMPLATE_SLOT_END}${strings[i + 1] ?? ""}`;
1198
- return templateBlueprint(templateParseFragment(source), source) ?? {
1199
- kind: "fragment",
1200
- children: []
1201
- };
1202
- }
1203
- function templateBindSegments(parts, values) {
1204
- let value;
1205
- if (parts.length === 1 && typeof parts[0] !== "string") value = values[parts[0].slot];
1206
- else if (parts.some((part) => typeof part !== "string")) value = {
1207
- [TEMPLATE_PARTS]: true,
1208
- values: parts.map((part) => typeof part === "string" ? part : values[part.slot])
1209
- };
1210
- else value = parts.join("");
1211
- return {
1212
- [TEMPLATE_ATTRIBUTE]: true,
1213
- value,
1214
- quoted: parts.quoted ?? true,
1215
- bare: parts.bare ?? false
1216
- };
1217
- }
1218
- function templateBind(node, values) {
1219
- if (node.kind === "text") {
1220
- const out = [];
1221
- for (const part of node.value) if (typeof part === "string") {
1222
- if (part) out.push({
1223
- kind: "text",
1224
- value: part
1225
- });
1226
- } else out.push({
1227
- kind: "dynamic",
1228
- value: values[part.slot]
1229
- });
1230
- return out;
1231
- }
1232
- if (node.kind === "comment") {
1233
- const parts = node.value;
1234
- if (parts.length === 1 && typeof parts[0] === "string") return {
1235
- kind: "comment",
1236
- value: parts[0]
1237
- };
1238
- if (parts.every((part) => typeof part === "string")) return {
1239
- kind: "comment",
1240
- value: parts.join("")
1241
- };
1242
- return {
1243
- kind: "comment",
1244
- value: {
1245
- [TEMPLATE_PARTS]: true,
1246
- values: parts.map((part) => typeof part === "string" ? part : values[part.slot])
1247
- }
1248
- };
1249
- }
1250
- if (node.kind === "element") {
1251
- const props = {};
1252
- for (const [name, parts] of Object.entries(node.props)) props[name] = templateBindSegments(parts, values);
1253
- return {
1254
- kind: "element",
1255
- tag: node.tag,
1256
- props,
1257
- children: node.children.flatMap((child) => templateBind(child, values)),
1258
- selfClosing: node.selfClosing
1259
- };
1260
- }
1261
- return {
1262
- kind: "fragment",
1263
- children: node.children.flatMap((child) => templateBind(child, values))
1264
- };
1265
- }
1266
- function htmlTemplate(strings, values) {
1267
- let compiled = templateBlueprintCache.get(strings);
1268
- if (!compiled) {
1269
- compiled = templateCompile(strings);
1270
- templateBlueprintCache.set(strings, compiled);
1271
- }
1272
- const result = templateBind(compiled, values);
1273
- return Array.isArray(result) ? {
1274
- kind: "fragment",
1275
- children: result
1276
- } : result;
1277
- }
1278
- function templateAttribute(value) {
1279
- return value && typeof value === "object" && TEMPLATE_ATTRIBUTE in value ? value : null;
1280
- }
1281
- function templateParts(value) {
1282
- return value && typeof value === "object" && TEMPLATE_PARTS in value ? value.values : null;
1283
- }
1284
- const TEMPLATE_ATTRIBUTE_ALIASES = {
1285
- className: "class",
1286
- htmlFor: "for",
1287
- acceptCharset: "accept-charset",
1288
- httpEquiv: "http-equiv",
1289
- accentHeight: "accent-height",
1290
- alignmentBaseline: "alignment-baseline",
1291
- baselineShift: "baseline-shift",
1292
- clipPath: "clip-path",
1293
- clipRule: "clip-rule",
1294
- dominantBaseline: "dominant-baseline",
1295
- fillOpacity: "fill-opacity",
1296
- fillRule: "fill-rule",
1297
- floodColor: "flood-color",
1298
- floodOpacity: "flood-opacity",
1299
- fontFamily: "font-family",
1300
- fontSize: "font-size",
1301
- fontStyle: "font-style",
1302
- fontWeight: "font-weight",
1303
- markerEnd: "marker-end",
1304
- markerMid: "marker-mid",
1305
- markerStart: "marker-start",
1306
- stopColor: "stop-color",
1307
- stopOpacity: "stop-opacity",
1308
- strokeDasharray: "stroke-dasharray",
1309
- strokeDashoffset: "stroke-dashoffset",
1310
- strokeLinecap: "stroke-linecap",
1311
- strokeLinejoin: "stroke-linejoin",
1312
- strokeMiterlimit: "stroke-miterlimit",
1313
- strokeOpacity: "stroke-opacity",
1314
- strokeWidth: "stroke-width",
1315
- textAnchor: "text-anchor",
1316
- vectorEffect: "vector-effect",
1317
- xlinkHref: "xlink:href"
1318
- };
1319
- const TEMPLATE_VOID_ELEMENTS = /* @__PURE__ */ new Set([
1320
- "area",
1321
- "base",
1322
- "br",
1323
- "col",
1324
- "embed",
1325
- "hr",
1326
- "img",
1327
- "input",
1328
- "link",
1329
- "meta",
1330
- "param",
1331
- "source",
1332
- "track",
1333
- "wbr"
1334
- ]);
1335
- const TEMPLATE_STRING_BOOLEANS = /* @__PURE__ */ new Set([
1336
- "contenteditable",
1337
- "draggable",
1338
- "spellcheck"
1339
- ]);
1340
- const TEMPLATE_SAFE_NAME = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
1341
- const TEMPLATE_EVENT = /^on([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?$/;
1342
- function normalizeTemplateClass(value) {
1343
- if (Array.isArray(value)) return value.filter(Boolean).join(" ");
1344
- if (isPlainObject(value)) return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([name]) => name).join(" ");
1345
- return String(value);
1346
- }
1347
- function resolvedAttrText(rawSourceValue) {
1348
- const attribute = templateAttribute(rawSourceValue);
1349
- const sourceValue = attribute ? attribute.value : rawSourceValue;
1350
- if (sourceValue == null) return null;
1351
- const parts = templateParts(sourceValue);
1352
- const value = parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : resolveTemplateValue(sourceValue);
1353
- if (value == null || value === false) return null;
1354
- return String(value);
1355
- }
1356
- function renderTemplateElement(node) {
1357
- const ctx = currentRenderCtx();
1358
- let attrs = "";
1359
- const eventSpecs = [];
1360
- const bindSpecs = [];
1361
- for (const [sourceName, rawSourceValue] of Object.entries(node.props)) {
1362
- const attribute = templateAttribute(rawSourceValue);
1363
- const sourceValue = attribute ? attribute.value : rawSourceValue;
1364
- if (sourceName === "children" || sourceName === "key" || sourceValue == null) continue;
1365
- if (sourceName === "__proto__" || sourceName === "constructor" || sourceName === "prototype") continue;
1366
- const name = TEMPLATE_ATTRIBUTE_ALIASES[sourceName] ?? (/^on[A-Z]/.test(sourceName) ? sourceName.toLowerCase() : sourceName);
1367
- if (!TEMPLATE_SAFE_NAME.test(name)) continue;
1368
- if (name.startsWith("bind:")) {
1369
- const kind = name.slice(5);
1370
- if (!BIND_VALID_KINDS.has(kind) || !isSignalAccessor(sourceValue)) continue;
1371
- if (!ctx) {
1372
- const value = sourceValue();
1373
- if (kind === "value") attrs += ` value="${escapeHtml(value ?? "")}"`;
1374
- else if ((kind === "checked" || kind === "open") && value) attrs += ` ${kind}`;
1375
- continue;
1376
- }
1377
- const index = ctx.binds.length;
1378
- ctx.binds.push({
1379
- kind,
1380
- accessor: sourceValue
1381
- });
1382
- const [reflected, spec] = emitBindSSR({
1383
- kind,
1384
- index,
1385
- accessor: sourceValue,
1386
- optionValue: resolvedAttrText(node.props.value)
1387
- });
1388
- attrs += reflected;
1389
- bindSpecs.push(spec);
1390
- continue;
1391
- }
1392
- if (/^on/i.test(name)) {
1393
- const match = TEMPLATE_EVENT.exec(name.toLowerCase());
1394
- if (templateParts(sourceValue)?.some((part) => typeof part === "function")) throw new Error(`[ilha] ${name} handler must occupy the entire attribute value. Write ${name}=\${handler}.`);
1395
- if (!match || typeof sourceValue !== "function") continue;
1396
- const eventName = match[1];
1397
- const rawModifier = match[2];
1398
- const modifier = rawModifier !== void 0 && NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
1399
- if (rawModifier && !modifier) continue;
1400
- const index = __ilhaJsxEvent({
1401
- type: eventName,
1402
- handler: sourceValue,
1403
- modifier
1404
- });
1405
- if (index !== void 0) eventSpecs.push(`${eventName}:${index}`);
1406
- continue;
1407
- }
1408
- const lower = name.toLowerCase();
1409
- if (lower === "srcdoc") continue;
1410
- const parts = templateParts(sourceValue);
1411
- let value = parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : resolveTemplateValue(sourceValue);
1412
- if ((lower.startsWith("aria-") || TEMPLATE_STRING_BOOLEANS.has(lower)) && typeof value === "boolean") value = String(value);
1413
- if (lower === "class" && !isRawHtml(value)) value = normalizeTemplateClass(value);
1414
- const structuredStyle = lower === "style" && isPlainObject(value) && !isRawHtml(value);
1415
- if (structuredStyle) value = serializeStyle(value);
1416
- if (isUrlAttributeName(lower) && !isRawHtml(value) && !isSafeUrlAttrValue(node.tag, lower, String(value))) continue;
1417
- if (value === false || value == null) continue;
1418
- if (attribute?.bare && value === "" || value === true && !TEMPLATE_STRING_BOOLEANS.has(lower) && !lower.startsWith("aria-")) {
1419
- attrs += ` ${name}`;
1420
- continue;
1421
- }
1422
- const text = isRawHtml(value) ? value.value : String(value);
1423
- const escaped = isRawHtml(value) ? text : escapeHtml(text);
1424
- attrs += attribute && !attribute.quoted && !structuredStyle && !/[\s"'`=<>]/.test(text) ? ` ${name}=${escaped}` : ` ${name}="${escaped}"`;
1425
- }
1426
- const key = node.props.key;
1427
- if (key != null && node.props["data-key"] == null) attrs += ` data-key="${escapeHtml(key)}"`;
1428
- if (bindSpecs.length) attrs += ` data-ilha-bind="${bindSpecs.join(",")}"`;
1429
- if (eventSpecs.length) attrs += ` data-ilha-on="${eventSpecs.join(",")}"`;
1430
- if (TEMPLATE_VOID_ELEMENTS.has(node.tag.toLowerCase())) return `<${node.tag}${attrs}${node.selfClosing ? " /" : ""}>`;
1431
- return `<${node.tag}${attrs}>${node.children.map((child) => renderTemplateNode(child, node.tag)).join("")}</${node.tag}>`;
1432
- }
1433
- function renderTemplateNode(node, parentTag) {
1434
- switch (node.kind) {
1435
- case "fragment": return node.children.map((child) => renderTemplateNode(child, parentTag)).join("");
1436
- case "element": return renderTemplateElement(node);
1437
- case "dynamic": return interpolateValue(node.value);
1438
- case "comment": {
1439
- const parts = templateParts(node.value);
1440
- return `<!--${(parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : String(node.value)).replace(/--/g, "- -")}-->`;
1441
- }
1442
- case "text": {
1443
- const parent = parentTag?.toLowerCase();
1444
- return parent === "script" || parent === "style" ? node.value : escapeHtml(node.value);
1445
- }
1446
- }
1447
- }
1448
- function renderTemplate(node) {
1449
- return ilhaRaw(renderTemplateNode(node));
1450
- }
1451
- setTemplateRenderer(renderTemplate);
1452
- function ilhaHtml(strings, ...values) {
1453
- return renderTemplate(htmlTemplate(strings, values));
1454
- }
1455
- function isRawHtml(v) {
1456
- if (typeof v !== "object" || v === null) return false;
1457
- return RAW in v && typeof v.value === "string";
1458
- }
1459
- function unwrapHtml(v) {
1460
- return isRawHtml(v) ? v.value : v;
1461
- }
1462
- const contextRegistry = /* @__PURE__ */ new Map();
1463
- function ilhaContextFn(key, initial) {
1464
- if (contextRegistry.has(key)) return contextRegistry.get(key);
1465
- const s = signal(initial);
1466
- const accessor = markSignalAccessor(() => s(), (next) => {
1467
- s(next);
1468
- });
1469
- contextRegistry.set(key, accessor);
1470
- return accessor;
1471
- }
1472
- const ilhaContext = Object.assign(ilhaContextFn, {
1473
- /** Remove a context signal from the registry. Returns true if it existed. */
1474
- delete(key) {
1475
- return contextRegistry.delete(key);
1476
- },
1477
- /** Remove all context signals from the registry (e.g. between tests). */
1478
- clear() {
1479
- contextRegistry.clear();
1480
- }
1481
- });
1482
- /**
1483
- * Create a free-standing reactive signal that lives outside any island.
1484
- * Internal: used by state()'s slot-drift fallback. The public way to create
1485
- * free-standing shared state is `context(key, initial)`.
1486
- */
1487
- function ilhaSignal(initial) {
1488
- if (currentFrame()?.creating && __DEV__);
1489
- const s = signal(initial);
1490
- return markSignalAccessor(() => s(), (next) => {
1491
- s(next);
1492
- });
1493
- }
1494
- /**
1495
- * Run a free-standing reactive effect outside any island. `fn` runs once
1496
- * immediately and again whenever a signal it read changes. It may return a
1497
- * cleanup function, invoked before each re-run and on stop. Signal writes
1498
- * inside the effect are batched. Returns a stop function that disposes the
1499
- * effect and runs the final cleanup.
1500
- *
1501
- * ```ts
1502
- * const stop = effect(() => {
1503
- * document.title = `${cart.count()} items`;
1504
- * });
1505
- * ```
1506
- */
1507
- function ilhaEffect(fn) {
1508
- let cleanup;
1509
- const runCleanup = () => {
1510
- if (typeof cleanup === "function") {
1511
- try {
1512
- cleanup();
1513
- } catch (err) {
1514
- console.error(err);
1515
- }
1516
- cleanup = void 0;
1517
- }
1518
- };
1519
- const stop = effect$1(() => {
1520
- runCleanup();
1521
- startBatch();
1522
- try {
1523
- cleanup = fn();
1524
- } catch (err) {
1525
- console.error(err);
1526
- } finally {
1527
- endBatch();
1528
- }
1529
- });
1530
- return () => {
1531
- stop();
1532
- runCleanup();
1533
- };
1534
- }
1535
- /**
1536
- * Run `fn` with reactive tracking suspended. Reading signals inside `fn`
1537
- * returns their current value without subscribing the surrounding scope.
1538
- * Use this in effects/deriveds when you want to peek at state without
1539
- * causing a re-run on its changes.
1540
- */
1541
- function untrack(fn) {
1542
- const prev = setActiveSub(void 0);
1543
- try {
1544
- return fn();
1545
- } finally {
1546
- setActiveSub(prev);
1547
- }
1548
- }
1549
- /**
1550
- * Run `fn` as an atomic batch — multiple signal writes inside the callback
1551
- * produce a single propagation pass, so dependents (effects, deriveds,
1552
- * island re-renders) see the final state and run once instead of once per
1553
- * write. Returns whatever `fn` returns.
1554
- *
1555
- * Note: `.on()` handlers and `.effect()` runs are batched implicitly, so
1556
- * you only need this when triggering multiple writes from outside an
1557
- * island (e.g. from a top-level event listener or async callback).
1558
- */
1559
- function batch(fn) {
1560
- startBatch();
1561
- try {
1562
- return fn();
1563
- } finally {
1564
- endBatch();
1565
- }
1566
- }
1567
- /**
1568
- * Keep a standalone signal in sync with persistent storage:
1569
- *
1570
- * 1. On call, reads `key` and writes the stored value into the signal.
1571
- * 2. Subscribes to the signal and writes its value back on every change.
1572
- * 3. Optionally mirrors writes from other tabs (`storage` events).
1573
- *
1574
- * No-op on the server (returns an inert unsubscribe). Call the returned
1575
- * unsubscribe to stop syncing.
1576
- *
1577
- * ```ts
1578
- * const cart = signal([] as string[]);
1579
- * persist(cart, "cart");
1580
- * ```
1581
- */
1582
- function persist(accessor, key, options = {}) {
1583
- if (typeof window === "undefined") return () => {};
1584
- const storage = options.storage ?? window.localStorage;
1585
- const serialize = options.serialize ?? JSON.stringify;
1586
- const deserialize = options.deserialize ?? ((raw) => {
1587
- try {
1588
- return JSON.parse(raw);
1589
- } catch {
1590
- return null;
1591
- }
1592
- });
1593
- const label = `[ilha] persist("${key}")`;
1594
- const applyRaw = (raw) => {
1595
- if (raw == null) return;
1596
- try {
1597
- const value = deserialize(raw);
1598
- if (value !== null) {
1599
- const prevSub = setActiveSub(void 0);
1600
- try {
1601
- accessor.set(value);
1602
- } finally {
1603
- setActiveSub(prevSub);
1604
- }
1605
- }
1606
- } catch (err) {
1607
- console.error(`${label}: failed to restore`, err);
1608
- }
1609
- };
1610
- try {
1611
- applyRaw(storage.getItem(key));
1612
- } catch (err) {
1613
- console.error(`${label}: failed to read storage`, err);
1614
- }
1615
- const stopEffect = ilhaEffect(() => {
1616
- try {
1617
- storage.setItem(key, serialize(accessor()));
1618
- } catch (err) {
1619
- console.error(`${label}: failed to write storage`, err);
1620
- }
1621
- });
1622
- let onStorage = null;
1623
- if (options.crossTab !== false && storage === window.localStorage) {
1624
- onStorage = (e) => {
1625
- if (e.key !== key) return;
1626
- applyRaw(e.newValue);
1627
- };
1628
- window.addEventListener("storage", onStorage);
1629
- }
1630
- return () => {
1631
- stopEffect();
1632
- if (onStorage) window.removeEventListener("storage", onStorage);
1633
- };
1634
- }
1635
- function createDerivedAccessor(read, write) {
1636
- const accessor = markSignalAccessor(() => read().value, (value) => {
1637
- if (write) write(value);
1638
- });
1639
- return new Proxy(accessor, { get(target, prop, receiver) {
1640
- if (prop === "loading" || prop === "value" || prop === "error") return read()[prop];
1641
- return Reflect.get(target, prop, receiver);
1642
- } });
1643
- }
1644
- function defaultDerivedAccessor() {
1645
- return createDerivedAccessor(() => ({
1646
- loading: false,
1647
- value: void 0,
1648
- error: void 0
17
+ const base = stream;
18
+ const src = parent.runtime.ssr ? base.pipe(Stream.take(1)) : base;
19
+ yield* Stream.runForEach(src, (value) => Effect.sync(() => {
20
+ if (child) closeFiber(child);
21
+ child = makeFiber(parent.runtime, parent.root, paint, { onFail: parent.fail });
22
+ interpret((() => body(value)), child);
23
+ }));
1649
24
  }));
1650
25
  }
1651
- const BIND_SENTINEL_ATTR = "data-ilha-bind";
1652
- const EVENT_SENTINEL_ATTR = "data-ilha-on";
1653
- function parseBindSentinel(value) {
1654
- const out = [];
1655
- for (const part of value.split(",")) {
1656
- const [kind, idx] = part.split(":");
1657
- if (!kind || !idx) continue;
1658
- const i = Number(idx);
1659
- if (!Number.isInteger(i) || i < 0) continue;
1660
- out.push({
1661
- kind,
1662
- index: i
1663
- });
1664
- }
1665
- return out;
1666
- }
1667
- function resolveBindOps(el, kind) {
1668
- const input = el;
1669
- switch (kind) {
1670
- case "value": return {
1671
- event: el.tagName === "SELECT" ? "change" : "input",
1672
- read: (el) => el.value,
1673
- write: (el, v) => el.value = v == null ? "" : String(v)
1674
- };
1675
- case "checked": return {
1676
- event: "change",
1677
- read: (el) => el.checked,
1678
- write: (el, v) => el.checked = Boolean(v)
1679
- };
1680
- case "files": return {
1681
- event: "change",
1682
- read: (el) => el.files,
1683
- write: () => {}
1684
- };
1685
- case "open": return {
1686
- event: "toggle",
1687
- read: (el) => el.open,
1688
- write: (el, v) => el.open = Boolean(v)
1689
- };
1690
- case "this": return {
1691
- event: null,
1692
- read: () => void 0,
1693
- write: () => {}
1694
- };
1695
- case "group": {
1696
- const isCheckbox = input.type === "checkbox";
1697
- return {
1698
- event: "change",
1699
- read: (el) => {
1700
- const i = el;
1701
- if (isCheckbox) return {
1702
- __ilhaGroup: true,
1703
- value: i.value,
1704
- checked: i.checked
1705
- };
1706
- return i.checked ? i.value : void 0;
1707
- },
1708
- write: (el, v) => {
1709
- const i = el;
1710
- if (isCheckbox) i.checked = (Array.isArray(v) ? v.map(String) : []).includes(i.value);
1711
- else i.checked = v != null && String(v) === i.value;
1712
- }
1713
- };
1714
- }
1715
- }
1716
- }
1717
- function templateElementBelongsToHost(host, candidate) {
1718
- if (candidate === host) return true;
1719
- let current = candidate;
1720
- while (current && current !== host) {
1721
- if (current.hasAttribute(SLOT_ATTR) || current.hasAttribute("data-ilha")) return false;
1722
- current = current.parentElement;
1723
- }
1724
- return current === host;
1725
- }
1726
- function applyJsxEvents({ host, records, reportError, unmountSignal, onceFired }) {
1727
- if (records.length === 0) return () => {};
1728
- const cleanups = [];
1729
- const elements = [];
1730
- if (host.hasAttribute(EVENT_SENTINEL_ATTR)) elements.push(host);
1731
- elements.push(...Array.from(host.querySelectorAll(`[${EVENT_SENTINEL_ATTR}]`)).filter((element) => templateElementBelongsToHost(host, element)));
1732
- for (const el of elements) {
1733
- const sentinel = el.getAttribute(EVENT_SENTINEL_ATTR) ?? "";
1734
- for (const part of sentinel.split(",")) {
1735
- const separator = part.lastIndexOf(":");
1736
- if (separator < 1) continue;
1737
- const type = part.slice(0, separator);
1738
- const index = Number(part.slice(separator + 1));
1739
- if (!Number.isInteger(index) || index < 0) continue;
1740
- const record = records[index];
1741
- if (!record || record.type !== type) continue;
1742
- if (record.modifier === "once" && onceFired.get(el)?.has(type)) continue;
1743
- const listenerController = new AbortController();
1744
- let invocationController;
1745
- const options = {
1746
- once: record.modifier === "once",
1747
- capture: record.modifier === "capture",
1748
- passive: record.modifier === "passive"
1749
- };
1750
- const reportHandlerError = (error) => {
1751
- if (error?.name !== "AbortError") reportError(error);
1752
- };
1753
- const listener = (event) => {
1754
- if (record.modifier === "once") {
1755
- let fired = onceFired.get(el);
1756
- if (!fired) onceFired.set(el, fired = /* @__PURE__ */ new Set());
1757
- fired.add(type);
1758
- }
1759
- if (record.modifier === "abortable") {
1760
- invocationController?.abort();
1761
- invocationController = new AbortController();
1762
- }
1763
- const signal = invocationController ? AbortSignal.any([listenerController.signal, invocationController.signal]) : listenerController.signal;
1764
- let result;
1765
- startBatch();
1766
- try {
1767
- result = record.handler(event, { signal });
1768
- } catch (error) {
1769
- reportHandlerError(error);
1770
- return;
1771
- } finally {
1772
- endBatch();
1773
- }
1774
- if (result != null && typeof result.then === "function") Promise.resolve(result).catch(reportHandlerError);
1775
- };
1776
- const stop = () => {
1777
- listenerController.abort();
1778
- invocationController?.abort();
1779
- el.removeEventListener(type, listener, options);
1780
- };
1781
- el.addEventListener(type, listener, options);
1782
- unmountSignal.addEventListener("abort", stop, { once: true });
1783
- cleanups.push(() => {
1784
- unmountSignal.removeEventListener("abort", stop);
1785
- stop();
1786
- });
1787
- }
1788
- }
1789
- return () => cleanups.forEach((cleanup) => cleanup());
1790
- }
1791
- function applyTemplateBindings(host, binds, phase = "all") {
1792
- if (binds.length === 0) return () => {};
1793
- const cleanups = [];
1794
- const elements = [];
1795
- if (host.hasAttribute(BIND_SENTINEL_ATTR)) elements.push(host);
1796
- for (const el of host.querySelectorAll(`[${BIND_SENTINEL_ATTR}]`)) if (templateElementBelongsToHost(host, el)) elements.push(el);
1797
- for (const el of elements) {
1798
- const specs = parseBindSentinel(el.getAttribute(BIND_SENTINEL_ATTR));
1799
- for (const spec of specs) {
1800
- const record = binds[spec.index];
1801
- if (!record) continue;
1802
- if (record.kind !== spec.kind) continue;
1803
- const { event, read, write } = resolveBindOps(el, spec.kind);
1804
- const accessor = record.accessor;
1805
- if (spec.kind === "this") {
1806
- if (phase !== "listen") {
1807
- accessor.set(el);
1808
- cleanups.push(() => accessor.set(null));
1809
- }
1810
- continue;
1811
- }
1812
- if (phase !== "listen") try {
1813
- write(el, accessor());
1814
- } catch (err) {}
1815
- if (phase === "reflect" || event === null) continue;
1816
- const listener = () => {
1817
- const raw = read(el);
1818
- if (spec.kind === "group") {
1819
- const groupRead = raw;
1820
- if (groupRead === void 0) return;
1821
- if (typeof groupRead === "object" && groupRead.__ilhaGroup) {
1822
- const currentArr = accessor();
1823
- const arr = Array.isArray(currentArr) ? [...currentArr] : [];
1824
- const idx = arr.findIndex((x) => String(x) === groupRead.value);
1825
- if (groupRead.checked && idx === -1) {
1826
- let coercedVal = groupRead.value;
1827
- const templateVal = Array.isArray(currentArr) && currentArr.length > 0 ? currentArr[0] : void 0;
1828
- if (templateVal !== void 0) {
1829
- if (typeof templateVal === "number") {
1830
- const n = Number(coercedVal);
1831
- coercedVal = Number.isNaN(n) ? coercedVal : n;
1832
- } else if (typeof templateVal === "boolean") coercedVal = Boolean(coercedVal);
1833
- }
1834
- arr.push(coercedVal);
1835
- } else if (!groupRead.checked && idx !== -1) arr.splice(idx, 1);
1836
- accessor.set(arr);
1837
- return;
1838
- }
1839
- const currentVal = accessor();
1840
- let coerced = groupRead;
1841
- if (typeof currentVal === "number" && typeof groupRead === "string") {
1842
- const n = Number(groupRead);
1843
- coerced = Number.isNaN(n) ? 0 : n;
1844
- } else if (typeof currentVal === "boolean") coerced = Boolean(groupRead);
1845
- accessor.set(coerced);
1846
- return;
1847
- }
1848
- const currentVal = accessor();
1849
- let value = raw;
1850
- if (typeof currentVal === "number" && typeof raw === "string") {
1851
- const n = Number(raw);
1852
- value = Number.isNaN(n) ? 0 : n;
1853
- } else if (typeof currentVal === "boolean") value = Boolean(raw);
1854
- accessor.set(value);
1855
- };
1856
- el.addEventListener(event, listener);
1857
- cleanups.push(() => el.removeEventListener(event, listener));
1858
- }
1859
- }
1860
- return () => cleanups.forEach((c) => c());
1861
- }
1862
- const globalErrorHandlers = /* @__PURE__ */ new Set();
1863
- function reportToGlobal(error, source) {
1864
- if (globalErrorHandlers.size === 0) return false;
1865
- for (const handler of globalErrorHandlers) try {
1866
- handler(error, source);
1867
- } catch (handlerErr) {
1868
- console.error(handlerErr);
1869
- }
1870
- return true;
1871
- }
1872
- /**
1873
- * Register a global error handler invoked when any island reports an error
1874
- * and has no local onError() handler. Returns an unsubscribe function.
1875
- */
1876
- function onUncaughtError(fn) {
1877
- globalErrorHandlers.add(fn);
1878
- return () => {
1879
- globalErrorHandlers.delete(fn);
1880
- };
1881
- }
1882
- const FRAME_STACK = Symbol.for("ilha.primitiveFrameStack");
1883
- function frameStack() {
1884
- const g = globalThis;
1885
- return g[FRAME_STACK] ??= [];
1886
- }
1887
- function currentFrame() {
1888
- const stack = frameStack();
1889
- return stack[stack.length - 1];
1890
- }
1891
- function withFrame(frame, fn) {
1892
- frameStack().push(frame);
1893
- try {
1894
- return fn();
1895
- } finally {
1896
- frameStack().pop();
1897
- }
1898
- }
1899
- function freshFrame(options) {
1900
- return {
1901
- slots: [],
1902
- cursor: 0,
1903
- creating: true,
1904
- ssr: options?.ssr ?? false,
1905
- instance: options?.instance ?? null,
1906
- stateIndex: 0,
1907
- derivedIndex: 0,
1908
- actionIndex: 0,
1909
- pendingDerived: options?.ssr ? [] : null,
1910
- actionManifest: options?.ssr ? /* @__PURE__ */ new Map() : null
1911
- };
1912
- }
1913
- const PRIMITIVE_NAMES = {
1914
- state: "state",
1915
- derived: "derived",
1916
- action: "action",
1917
- effect: "effect",
1918
- once: "effect.once",
1919
- error: "onError"
1920
- };
1921
- /** Reserve the next slot for a primitive call. Throws outside an island
1922
- * render; warns in dev on hook count/kind drift during re-renders. */
1923
- function acquireSlot(kind) {
1924
- const frame = currentFrame();
1925
- if (!frame) throw new Error(`[ilha] ${PRIMITIVE_NAMES[kind]}() called outside an island render. Primitives are only valid while an island component (or a plain component owned by one) is rendering.`);
1926
- const index = frame.cursor++;
1927
- if (frame.creating) return void 0;
1928
- const existing = frame.slots[index];
1929
- return existing && existing.kind === kind ? existing : void 0;
1930
- }
1931
- /**
1932
- * Declare island-local reactive state at this call position. The initializer
1933
- * applies only when the instance is created — later renders reuse the same
1934
- * underlying signal, so prop-driven initializers never reset user state.
1935
- *
1936
- * A function argument is treated as a lazy initializer:
1937
- * const count = state(() => expensiveInitialValue());
1938
- * To store a function VALUE, pass it to `.set`:
1939
- * onSave.set(nextCallback);
1940
- */
1941
- function state(init) {
1942
- const frame = currentFrame();
1943
- const existing = acquireSlot("state");
1944
- if (existing) return existing.acc;
1945
- const f = frame;
1946
- if (!f.creating) return ilhaSignal(void 0);
1947
- const index = f.stateIndex++;
1948
- const snap = f.ssr ? void 0 : f.instance?.stateSnapshot?.s?.[index];
1949
- const initial = snap === void 0 ? typeof init === "function" ? init() : init : snap;
1950
- const s = signal(initial);
1951
- const acc = markSignalAccessor(() => s(), (next) => {
1952
- s(next);
1953
- });
1954
- f.slots[f.cursor - 1] = {
1955
- kind: "state",
1956
- acc
1957
- };
1958
- return acc;
1959
- }
1960
- /** True for async iterables (incl. async generators). */
1961
- function isAsyncIterable(value) {
1962
- return value !== null && typeof value[Symbol.asyncIterator] === "function";
1963
- }
1964
- /**
1965
- * Declare derived state at this call position: synchronous computations,
1966
- * Promises, or async generators. Reads track their dependencies; async work
1967
- * races latest-run-wins and aborts stale runs through `ctx.signal`.
1968
- */
1969
- function derived(fn) {
1970
- const frame = currentFrame();
1971
- const existing = acquireSlot("derived");
1972
- const f = frame;
1973
- if (existing) {
1974
- const slot = existing;
1975
- slot.fn = fn;
1976
- if (f.ssr && !f.creating && !slot.ssrAsync) {
1977
- const ac = new AbortController();
1978
- const prevSub = setActiveSub(void 0);
1979
- try {
1980
- const result = slot.fn({ signal: ac.signal });
1981
- if (!(result instanceof Promise) && !isAsyncIterable(result)) slot.env({
1982
- loading: false,
1983
- value: result,
1984
- error: void 0
1985
- });
1986
- } catch (err) {
1987
- slot.env({
1988
- loading: false,
1989
- value: void 0,
1990
- error: err instanceof Error ? err : new Error(String(err))
1991
- });
1992
- } finally {
1993
- setActiveSub(prevSub);
1994
- }
1995
- }
1996
- return slot.acc;
1997
- }
1998
- if (!f.creating) return defaultDerivedAccessor();
1999
- const index = f.derivedIndex++;
2000
- const env = signal({
2001
- loading: false,
2002
- value: void 0,
2003
- error: void 0
2004
- });
2005
- let fromSnapshot = false;
2006
- if (!f.ssr) {
2007
- const snap = f.instance?.stateSnapshot?.d?.[index];
2008
- if (snap) {
2009
- env({ ...snap });
2010
- fromSnapshot = true;
2011
- }
2012
- }
2013
- const accessor = createDerivedAccessor(() => env(), (value) => {
2014
- const prevSub = setActiveSub(void 0);
2015
- try {
2016
- env({
2017
- loading: false,
2018
- value,
2019
- error: void 0
2020
- });
2021
- } finally {
2022
- setActiveSub(prevSub);
2023
- }
2024
- });
2025
- const slot = {
2026
- kind: "derived",
2027
- env,
2028
- acc: accessor,
2029
- fn,
2030
- ssrAsync: false,
2031
- fromSnapshot
2032
- };
2033
- if (f.ssr) {
2034
- const ac = new AbortController();
2035
- const prevSub = setActiveSub(void 0);
2036
- let result;
2037
- let threw = false;
2038
- let syncError;
2039
- try {
2040
- result = slot.fn({ signal: ac.signal });
2041
- } catch (err) {
2042
- threw = true;
2043
- syncError = err;
2044
- } finally {
2045
- setActiveSub(prevSub);
2046
- }
2047
- let pull = null;
2048
- if (threw) env({
2049
- loading: false,
2050
- value: void 0,
2051
- error: syncError instanceof Error ? syncError : new Error(String(syncError))
2052
- });
2053
- else if (result instanceof Promise) {
2054
- env({
2055
- loading: true,
2056
- value: void 0,
2057
- error: void 0
2058
- });
2059
- pull = result.then((value) => env({
2060
- loading: false,
2061
- value,
2062
- error: void 0
2063
- }), (err) => env({
2064
- loading: false,
2065
- value: void 0,
2066
- error: err instanceof Error ? err : new Error(String(err))
2067
- }));
2068
- result.catch(() => {});
2069
- } else if (isAsyncIterable(result)) {
2070
- env({
2071
- loading: true,
2072
- value: void 0,
2073
- error: void 0
2074
- });
2075
- const iterable = result;
2076
- pull = (async () => {
2077
- const it = iterable[Symbol.asyncIterator]();
2078
- try {
2079
- const first = await it.next();
2080
- env({
2081
- loading: false,
2082
- value: first.done ? void 0 : first.value,
2083
- error: void 0
2084
- });
2085
- } finally {
2086
- await it.return?.(void 0);
2087
- }
2088
- })();
2089
- } else env({
2090
- loading: false,
2091
- value: result,
2092
- error: void 0
2093
- });
2094
- slot.ssrAsync = pull !== null;
2095
- if (pull) f.pendingDerived.push({
2096
- index,
2097
- promise: pull.catch(() => {})
2098
- });
2099
- }
2100
- f.slots[f.cursor - 1] = slot;
2101
- return accessor;
2102
- }
2103
- /**
2104
- * Declare a reactive operation at this call position. Use plain functions
2105
- * for ordinary operations; action() adds pending/data/error tracking,
2106
- * concurrent-invocation bookkeeping, and lifecycle cancellation.
2107
- */
2108
- function action(fn) {
2109
- const frame = currentFrame();
2110
- const existing = acquireSlot("action");
2111
- const f = frame;
2112
- if (existing) return existing.acc;
2113
- if (!f.creating) {
2114
- const noop = (() => {});
2115
- Object.defineProperties(noop, {
2116
- pending: { get: () => false },
2117
- data: { get: () => void 0 },
2118
- error: { get: () => void 0 }
2119
- });
2120
- attachWith(noop);
2121
- return noop;
2122
- }
2123
- const manifestId = `a${f.actionIndex++}`;
2124
- if (f.ssr) {
2125
- const invoke = (..._callArgs) => {};
2126
- invoke[SSR_ACTION_STUB] = true;
2127
- invoke[ACTION_MANIFEST_ID] = manifestId;
2128
- Object.defineProperties(invoke, {
2129
- pending: {
2130
- get: () => false,
2131
- enumerable: true
2132
- },
2133
- data: {
2134
- get: () => void 0,
2135
- enumerable: true
2136
- },
2137
- error: {
2138
- get: () => void 0,
2139
- enumerable: true
2140
- }
2141
- });
2142
- f.actionManifest.set(invoke, manifestId);
2143
- f.slots[f.cursor - 1] = {
2144
- kind: "action",
2145
- acc: invoke,
2146
- fn
2147
- };
2148
- const ssrAccessor = invoke;
2149
- attachWith(ssrAccessor);
2150
- return ssrAccessor;
2151
- }
2152
- const instance = f.instance;
2153
- const envelope = signal({
2154
- pending: 0,
2155
- data: void 0,
2156
- error: void 0
2157
- });
2158
- const readEnvelope = () => untrack(() => envelope());
2159
- let latestRun = 0;
2160
- const invoke = (payload) => {
2161
- if (currentRenderCtx()) return;
2162
- if (instance.disposed) return;
2163
- const run = ++latestRun;
2164
- const runSignal = instance.unmountController.signal;
2165
- let result;
2166
- let isAsync = false;
2167
- startBatch();
2168
- try {
2169
- result = slot.fn(payload, { signal: runSignal });
2170
- isAsync = result != null && typeof result.then === "function";
2171
- const current = readEnvelope();
2172
- envelope(isAsync ? {
2173
- pending: current.pending + 1,
2174
- data: current.data,
2175
- error: void 0
2176
- } : {
2177
- pending: current.pending,
2178
- data: result,
2179
- error: void 0
2180
- });
2181
- } catch (error) {
2182
- const normalized = error instanceof Error ? error : new Error(String(error));
2183
- envelope({
2184
- pending: readEnvelope().pending,
2185
- data: void 0,
2186
- error: normalized
2187
- });
2188
- instance.reportError(normalized, "action");
26
+
27
+ //#endregion
28
+ //#region src/watch.ts
29
+ function watch(source, fn) {
30
+ return instr(Effect.sync(() => {
31
+ const fiber = (isAtomHandle(source) ? handleOwner.get(source) : void 0) ?? getFiber();
32
+ if (Stream.isStream(source)) {
33
+ fiber.run(Stream.runForEach(source, (v) => Effect.sync(() => fn(v))));
2189
34
  return;
2190
- } finally {
2191
- endBatch();
2192
- }
2193
- if (!isAsync) return;
2194
- Promise.resolve(result).then((data) => {
2195
- if (instance.disposed || runSignal.aborted || run !== latestRun) return;
2196
- const value = readEnvelope();
2197
- envelope({
2198
- pending: value.pending,
2199
- data,
2200
- error: void 0
2201
- });
2202
- }).catch((error) => {
2203
- if (instance.disposed || runSignal.aborted || error?.name === "AbortError") return;
2204
- const normalized = error instanceof Error ? error : new Error(String(error));
2205
- if (run === latestRun) {
2206
- const value = readEnvelope();
2207
- envelope({
2208
- pending: value.pending,
2209
- data: void 0,
2210
- error: normalized
2211
- });
2212
- }
2213
- instance.reportError(normalized, "action");
2214
- }).finally(() => {
2215
- if (instance.disposed) return;
2216
- const value = readEnvelope();
2217
- envelope({
2218
- ...value,
2219
- pending: Math.max(0, value.pending - 1)
2220
- });
2221
- });
2222
- };
2223
- const slot = {
2224
- kind: "action",
2225
- acc: invoke,
2226
- fn
2227
- };
2228
- Object.defineProperties(invoke, {
2229
- pending: {
2230
- get: () => envelope().pending > 0,
2231
- enumerable: true
2232
- },
2233
- data: {
2234
- get: () => envelope().data,
2235
- enumerable: true
2236
- },
2237
- error: {
2238
- get: () => envelope().error,
2239
- enumerable: true
2240
35
  }
2241
- });
2242
- f.slots[f.cursor - 1] = slot;
2243
- instance.actionIds.set(slot.acc, manifestId);
2244
- const clientAccessor = invoke;
2245
- clientAccessor[ACTION_MANIFEST_ID] = manifestId;
2246
- attachWith(clientAccessor);
2247
- return clientAccessor;
2248
- }
2249
- /**
2250
- * Reactive side effect. Inside an island render, registers an ordered effect
2251
- * slot that tracks signals read in the body, reruns on change, cleans up
2252
- * before rerun and on unmount, and is client-only. Outside an island render,
2253
- * behaves as the standalone reactive effect and returns a stop function.
2254
- *
2255
- * @returns Inside an island render, `void` (an effect slot is registered and
2256
- * cleaned up automatically). At module scope, a stop function that disposes
2257
- * the effect and runs its final cleanup. The union return type is deliberate:
2258
- * the same call shape cannot be discriminated at the call site.
2259
- */
2260
- const effect = Object.assign((fn) => {
2261
- const frame = currentFrame();
2262
- if (!frame) {
2263
- let cleanup;
2264
- let controller = null;
2265
- const runCleanup = () => {
2266
- if (typeof cleanup === "function") {
2267
- try {
2268
- cleanup();
2269
- } catch (err) {
2270
- console.error(err);
2271
- }
2272
- cleanup = void 0;
2273
- }
2274
- };
2275
- const stop = ilhaEffect(() => {
2276
- runCleanup();
2277
- if (controller) controller.abort();
2278
- controller = new AbortController();
2279
- startBatch();
2280
- try {
2281
- cleanup = fn({ signal: controller.signal });
2282
- } catch (err) {
2283
- console.error(err);
2284
- } finally {
2285
- endBatch();
2286
- }
2287
- });
2288
- return () => {
2289
- stop();
2290
- runCleanup();
2291
- controller?.abort();
2292
- };
2293
- }
2294
- const existing = acquireSlot("effect");
2295
- if (existing) {
2296
- existing.fn = fn;
2297
- return;
2298
- }
2299
- if (!frame.creating) return;
2300
- frame.slots[frame.cursor - 1] = {
2301
- kind: "effect",
2302
- fn
2303
- };
2304
- }, { once(fn) {
2305
- if (acquireSlot("once")) return;
2306
- const f = currentFrame();
2307
- if (!f) throw new Error("[ilha] effect.once() called outside an island render. It registers per-instance setup and is only valid while an island component is rendering.");
2308
- if (!f.creating) return;
2309
- f.slots[f.cursor - 1] = {
2310
- kind: "once",
2311
- fn
2312
- };
2313
- } });
2314
- /**
2315
- * Register an error handler slot for this island. Handlers run in
2316
- * declaration order; state, derived values, actions, and props should be
2317
- * accessed through lexical closure rather than the context object.
2318
- */
2319
- function onError(fn) {
2320
- if (!currentFrame()) throw new Error("[ilha] onError() called outside an island render. Register it while an island component is rendering.");
2321
- const existing = acquireSlot("error");
2322
- if (existing) {
2323
- existing.fn = fn;
2324
- return;
2325
- }
2326
- const f = currentFrame();
2327
- if (!f || !f.creating) return;
2328
- f.slots[f.cursor - 1] = {
2329
- kind: "error",
2330
- fn
2331
- };
2332
- }
2333
- function parseStateSnapshot(raw) {
2334
- const parsed = safeParseSnapshot(raw, STATE_ATTR);
2335
- if (parsed === void 0) return void 0;
2336
- const rec = parsed;
2337
- if (rec["v"] !== 2) return;
2338
- const out = { skipOnce: rec["_skipOnMount"] === true };
2339
- if (Array.isArray(rec["s"])) {
2340
- stripUnsafeKeys(rec["s"]);
2341
- out.s = rec["s"];
2342
- }
2343
- if (Array.isArray(rec["d"])) {
2344
- stripUnsafeKeys(rec["d"]);
2345
- out.d = rec["d"].map((entry) => {
2346
- const e = entry ?? {};
2347
- const errorValue = e.error;
2348
- return {
2349
- loading: e.loading === true,
2350
- value: e.value,
2351
- error: errorValue == null ? void 0 : errorValue instanceof Error ? errorValue : new Error(String(errorValue))
2352
- };
2353
- });
2354
- }
2355
- return out;
36
+ const a = isAtomHandle(source) ? source.atom : source;
37
+ const unsub = fiber.registry.subscribe(a, fn, { immediate: true });
38
+ Effect.runSync(Scope.addFinalizer(fiber.scope, Effect.sync(unsub)));
39
+ }));
2356
40
  }
2357
- const ilha = ((...args) => {
2358
- let schema = null;
2359
- let component;
2360
- let options;
2361
- const first = args[0];
2362
- const second = args[1];
2363
- if (isStandardSchema(first)) {
2364
- schema = first;
2365
- component = second;
2366
- options = args[2];
2367
- } else {
2368
- component = first;
2369
- options = second;
2370
- }
2371
- if (typeof component !== "function") throw new Error("[ilha] ilha() requires a component function.");
2372
- const configuredSlotTag = options?.as === void 0 ? "div" : assertValidSlotTagName(options.as);
2373
- function resolveInput(props) {
2374
- const merged = { ...props };
2375
- if (!schema) return merged;
2376
- return validateSchema(schema, merged);
2377
- }
2378
- function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
2379
- const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
41
+
42
+ //#endregion
43
+ //#region src/wait.ts
44
+ function wait(body) {
45
+ return instr(Effect.gen(function* () {
46
+ const parent = getFiber();
47
+ const deferred = yield* Deferred.make();
48
+ let settled = false;
49
+ const done = (value) => {
50
+ if (settled) return;
51
+ settled = true;
52
+ Effect.runSync(Deferred.succeed(deferred, value));
53
+ };
54
+ const child = makeFiber(parent.runtime, parent.root, paint, { onFail: parent.fail });
55
+ interpret((() => body(done)), child);
2380
56
  try {
2381
- const html = unwrapHtml(render());
2382
- const slots = ctx.slots;
2383
- const binds = ctx.binds;
2384
- const events = ctx.events;
2385
- if (ctx.pending && ctx.pending.size > 0) {
2386
- const pending = ctx.pending;
2387
- return (async () => {
2388
- return {
2389
- html: await resolveAsyncChildren(html, pending),
2390
- slots,
2391
- binds,
2392
- events
2393
- };
2394
- })();
2395
- }
2396
- return {
2397
- html,
2398
- slots,
2399
- binds,
2400
- events
2401
- };
57
+ return yield* Deferred.await(deferred);
2402
58
  } finally {
2403
- popRenderCtx();
2404
- }
2405
- }
2406
- /** Invoke the component inside the given frame + render context. */
2407
- function runInFrame(frame, props) {
2408
- return () => withFrame(frame, () => component(props));
2409
- }
2410
- function recordEventsManifest(eventsOut, events, frame) {
2411
- if (!eventsOut || events.length === 0) return;
2412
- events.forEach((record, index) => {
2413
- const id = frame.actionManifest?.get(record.handler) ?? actionManifestId(record.handler);
2414
- if (!id) return;
2415
- const args = actionBoundArgs(record.handler);
2416
- eventsOut.set(`${record.type}:${index}`, Array.isArray(args) ? {
2417
- k: id,
2418
- a: args
2419
- } : id);
2420
- });
2421
- }
2422
- function renderToString(props, sync = false, eventsOut, forceAsyncChildren = false) {
2423
- const input = resolveInput(props);
2424
- const frame = freshFrame({ ssr: true });
2425
- const first = forceAsyncChildren ? renderWithCtx({
2426
- render: runInFrame(frame, input),
2427
- asyncChildren: true,
2428
- manifest: eventsOut !== void 0
2429
- }) : renderWithCtx({
2430
- render: runInFrame(frame, input),
2431
- manifest: eventsOut !== void 0
2432
- });
2433
- const finish = (firstHtml) => firstHtml.html;
2434
- const needsAsync = forceAsyncChildren || (frame.pendingDerived?.length ?? 0) > 0;
2435
- if (first instanceof Promise) return first.then((out) => {
2436
- recordEventsManifest(eventsOut, out.events, frame);
2437
- return needsAsync ? awaitPending(out) : out.html;
2438
- });
2439
- recordEventsManifest(eventsOut, first.events, frame);
2440
- if (!needsAsync || sync) {
2441
- if (sync && (frame.pendingDerived?.length ?? 0) > 0 && __DEV__);
2442
- return finish(first);
2443
- }
2444
- return awaitPending(first);
2445
- function awaitPending(out) {
2446
- const pendings = frame.pendingDerived ?? [];
2447
- if (pendings.length === 0) return Promise.resolve(out.html);
2448
- return Promise.all(pendings.map((p) => p.promise)).then(() => {
2449
- frame.creating = false;
2450
- frame.cursor = 0;
2451
- const second = forceAsyncChildren ? renderWithCtx({
2452
- render: runInFrame(frame, input),
2453
- asyncChildren: true,
2454
- manifest: eventsOut !== void 0
2455
- }) : renderWithCtx({
2456
- render: runInFrame(frame, input),
2457
- manifest: eventsOut !== void 0
2458
- });
2459
- const done = (o) => {
2460
- recordEventsManifest(eventsOut, o.events, frame);
2461
- return o.html;
2462
- };
2463
- return second instanceof Promise ? second.then(done) : done(second);
2464
- });
2465
- }
2466
- }
2467
- function mountIsland(host, props) {
2468
- return mountIslandInternal(host, props).unmount;
2469
- }
2470
- function mountIslandInternal(host, props) {
2471
- if (props === void 0) {
2472
- const rawProps = host.getAttribute(PROPS_ATTR);
2473
- if (rawProps) {
2474
- const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
2475
- if (parsed !== void 0) props = reviveSlotProps(parsed);
2476
- }
2477
- }
2478
- const inputSignal = signal(resolveInput(props));
2479
- const derivedPropsVersion = signal(0);
2480
- const input = new Proxy({}, {
2481
- get(_t, key) {
2482
- return inputSignal()[key];
2483
- },
2484
- has(_t, key) {
2485
- return key in inputSignal();
2486
- },
2487
- ownKeys() {
2488
- const prevSub = setActiveSub(void 0);
2489
- try {
2490
- return Reflect.ownKeys(inputSignal());
2491
- } finally {
2492
- setActiveSub(prevSub);
2493
- }
2494
- },
2495
- getOwnPropertyDescriptor(_t, key) {
2496
- const prevSub = setActiveSub(void 0);
2497
- try {
2498
- return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
2499
- } finally {
2500
- setActiveSub(prevSub);
2501
- }
2502
- }
2503
- });
2504
- const snapshotRaw = host.getAttribute(STATE_ATTR);
2505
- const snapshot = snapshotRaw ? parseStateSnapshot(snapshotRaw) : void 0;
2506
- const hydrated = snapshot != null;
2507
- const shouldSkipOnce = snapshot?.skipOnce === true;
2508
- const cleanups = [];
2509
- const unmountController = new AbortController();
2510
- cleanups.push(() => unmountController.abort());
2511
- const errorSlots = [];
2512
- const reportError = (err, source) => {
2513
- const error = err instanceof Error ? err : new Error(String(err));
2514
- if (errorSlots.length === 0) {
2515
- if (!reportToGlobal(error, source)) console.error(error);
2516
- return;
2517
- }
2518
- for (const slot of errorSlots) try {
2519
- slot.fn({
2520
- error,
2521
- source,
2522
- host
2523
- });
2524
- } catch (handlerErr) {
2525
- console.error(handlerErr);
2526
- }
2527
- };
2528
- const instance = {
2529
- host,
2530
- inputSignal,
2531
- input,
2532
- unmountController,
2533
- cleanups,
2534
- reportError,
2535
- actionIds: /* @__PURE__ */ new Map(),
2536
- stateSnapshot: snapshot ? {
2537
- s: snapshot.s,
2538
- d: snapshot.d
2539
- } : void 0,
2540
- hydrated,
2541
- disposed: false,
2542
- frame: null
2543
- };
2544
- cleanups.push(() => {
2545
- instance.disposed = true;
2546
- });
2547
- const frame = freshFrame({
2548
- ssr: false,
2549
- instance
2550
- });
2551
- instance.frame = frame;
2552
- const mountedSlots = /* @__PURE__ */ new Map();
2553
- function teardownMountedSlot(id, entry) {
2554
- mountedSlots.delete(id);
2555
- entry.el.remove();
2556
- entry.unmount();
2557
- }
2558
- function slotBelongsToHost(candidate) {
2559
- let el = candidate.parentElement;
2560
- while (el && el !== host) {
2561
- if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
2562
- el = el.parentElement;
2563
- }
2564
- return el === host;
59
+ closeFiber(child);
2565
60
  }
2566
- function buildSlotIndex() {
2567
- const index = /* @__PURE__ */ new Map();
2568
- for (const candidate of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
2569
- const id = candidate.getAttribute(SLOT_ATTR);
2570
- if (id === null || index.has(id)) continue;
2571
- if (slotBelongsToHost(candidate)) index.set(id, candidate);
2572
- }
2573
- return index;
2574
- }
2575
- function pushUpdatedProps(nextSlots) {
2576
- for (const [id, entry] of mountedSlots) {
2577
- const next = nextSlots.get(id);
2578
- if (next) entry.updateProps(next.props);
2579
- }
2580
- }
2581
- function mountSlots(slotMap) {
2582
- for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
2583
- for (const [id, entry] of mountedSlots) {
2584
- const next = slotMap.get(id);
2585
- if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2586
- }
2587
- let slotIndex = null;
2588
- for (const [id, { island: childIsland, props: childProps }] of slotMap) {
2589
- const existing = mountedSlots.get(id);
2590
- if (existing) {
2591
- existing.updateProps(childProps);
2592
- continue;
2593
- }
2594
- if (slotIndex === null) slotIndex = buildSlotIndex();
2595
- const slotEl = slotIndex.get(id) ?? null;
2596
- if (!slotEl) continue;
2597
- let slotProps = childProps;
2598
- if (slotProps === void 0) {
2599
- const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
2600
- if (rawProps) {
2601
- const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
2602
- if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
2603
- }
2604
- }
2605
- const prevSub = setActiveSub(void 0);
2606
- let handle;
2607
- try {
2608
- const internal = childIsland[ISLAND_MOUNT_INTERNAL];
2609
- handle = internal ? internal(slotEl, slotProps) : {
2610
- unmount: childIsland.mount(slotEl, slotProps),
2611
- updateProps: () => {}
2612
- };
2613
- } finally {
2614
- setActiveSub(prevSub);
2615
- }
2616
- mountedSlots.set(id, {
2617
- el: slotEl,
2618
- island: childIsland,
2619
- ...handle
2620
- });
2621
- }
2622
- }
2623
- const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2624
- let stopJsxEvents = () => {};
2625
- let stopBindings = () => {};
2626
- /** Register derived scopes before the render effect so source updates
2627
- * settle derived envelopes before a render can consume them. */
2628
- function startDerivedScopes() {
2629
- for (const slot of frame.slots) if (slot.kind === "derived") startDerivedScope(slot);
2630
- }
2631
- /** Register side-effecting scopes after the render effect so prop-change
2632
- * renders refresh their closures before those scopes execute. */
2633
- function startEffectScopes() {
2634
- for (const slot of frame.slots) if (slot.kind === "effect") startEffectScope(slot);
2635
- else if (slot.kind === "once") startOnceScope(slot);
2636
- else if (slot.kind === "error") errorSlots.push(slot);
2637
- }
2638
- function startDerivedScope(slot) {
2639
- let ac = new AbortController();
2640
- let skipFirst = slot.fromSnapshot;
2641
- const stopScope = effect$1(() => {
2642
- ac.abort();
2643
- ac = new AbortController();
2644
- const currentAc = ac;
2645
- derivedPropsVersion();
2646
- let result;
2647
- try {
2648
- result = slot.fn({ signal: currentAc.signal });
2649
- } catch (err) {
2650
- if (skipFirst) {
2651
- skipFirst = false;
2652
- return;
2653
- }
2654
- writeEnvelope(currentAc, {
2655
- loading: false,
2656
- value: void 0,
2657
- error: err instanceof Error ? err : new Error(String(err))
2658
- });
2659
- return;
2660
- }
2661
- if (skipFirst) {
2662
- skipFirst = false;
2663
- if (result instanceof Promise) result.catch(() => {});
2664
- return;
2665
- }
2666
- if (!(result instanceof Promise)) {
2667
- if (isAsyncIterable(result)) {
2668
- const iterable = result;
2669
- writeEnvelope(currentAc, {
2670
- loading: true,
2671
- value: readEnv().value,
2672
- error: void 0
2673
- });
2674
- (async () => {
2675
- try {
2676
- for await (const value of iterable) if (writeEnvelope(currentAc, {
2677
- loading: false,
2678
- value,
2679
- error: void 0
2680
- })) break;
2681
- } catch (err) {
2682
- writeEnvelope(currentAc, {
2683
- loading: false,
2684
- value: void 0,
2685
- error: err instanceof Error ? err : new Error(String(err))
2686
- });
2687
- }
2688
- })();
2689
- return;
2690
- }
2691
- writeEnvelope(currentAc, {
2692
- loading: false,
2693
- value: result,
2694
- error: void 0
2695
- });
2696
- return;
2697
- }
2698
- writeEnvelope(currentAc, {
2699
- loading: true,
2700
- value: readEnv().value,
2701
- error: void 0
2702
- });
2703
- result.then((value) => {
2704
- writeEnvelope(currentAc, {
2705
- loading: false,
2706
- value,
2707
- error: void 0
2708
- });
2709
- }).catch((err) => {
2710
- writeEnvelope(currentAc, {
2711
- loading: false,
2712
- value: void 0,
2713
- error: err instanceof Error ? err : new Error(String(err))
2714
- });
2715
- });
2716
- });
2717
- cleanups.push(() => {
2718
- stopScope();
2719
- ac.abort();
2720
- });
2721
- function readEnv() {
2722
- const prevSub = setActiveSub(void 0);
2723
- try {
2724
- return slot.env();
2725
- } finally {
2726
- setActiveSub(prevSub);
2727
- }
2728
- }
2729
- function writeEnvelope(ac, patch) {
2730
- if (ac.signal.aborted) return true;
2731
- const prevSub = setActiveSub(void 0);
2732
- try {
2733
- slot.env(patch);
2734
- } finally {
2735
- setActiveSub(prevSub);
2736
- }
2737
- return false;
2738
- }
2739
- }
2740
- function startEffectScope(slot) {
2741
- let userCleanup;
2742
- let runController = null;
2743
- const stopScope = effect$1(() => {
2744
- if (userCleanup) {
2745
- try {
2746
- userCleanup();
2747
- } catch (err) {
2748
- reportError(err, "effect");
2749
- }
2750
- userCleanup = void 0;
2751
- }
2752
- if (runController) runController.abort();
2753
- runController = new AbortController();
2754
- const runSignal = AbortSignal.any([unmountController.signal, runController.signal]);
2755
- inputSignal();
2756
- startBatch();
2757
- try {
2758
- userCleanup = slot.fn({ signal: runSignal });
2759
- } catch (err) {
2760
- reportError(err, "effect");
2761
- } finally {
2762
- endBatch();
2763
- }
2764
- });
2765
- cleanups.push(() => {
2766
- stopScope();
2767
- if (userCleanup) try {
2768
- userCleanup();
2769
- } catch (err) {
2770
- reportError(err, "effect");
2771
- }
2772
- if (runController) runController.abort();
2773
- });
2774
- }
2775
- function startOnceScope(slot) {
2776
- if (shouldSkipOnce) return;
2777
- const prevSub = setActiveSub(void 0);
2778
- let userCleanup;
2779
- try {
2780
- userCleanup = slot.fn({
2781
- host,
2782
- hydrated,
2783
- signal: unmountController.signal
2784
- });
2785
- } catch (err) {
2786
- reportError(err, "once");
2787
- } finally {
2788
- setActiveSub(prevSub);
2789
- }
2790
- if (userCleanup) {
2791
- const teardown = userCleanup;
2792
- cleanups.push(() => {
2793
- try {
2794
- teardown();
2795
- } catch (err) {
2796
- reportError(err, "once");
2797
- }
2798
- });
2799
- }
2800
- }
2801
- const preserveSSRDom = hydrated && host.childNodes.length > 0;
2802
- const initial = renderWithCtx({
2803
- render: runInFrame(frame, input),
2804
- liveHost: preserveSSRDom ? host : void 0
2805
- });
2806
- if (!preserveSSRDom) host.innerHTML = initial.html;
2807
- const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2808
- let stopInitialBindingListeners = null;
2809
- stopBindings = stopInitialBindingReflection;
2810
- cleanups.push(() => stopBindings());
2811
- stopJsxEvents = applyJsxEvents({
2812
- host,
2813
- records: initial.events,
2814
- reportError: (error) => reportError(error, "event"),
2815
- unmountSignal: unmountController.signal,
2816
- onceFired: nativeOnceFired
2817
- });
2818
- cleanups.push(() => stopJsxEvents());
2819
- mountSlots(initial.slots);
2820
- cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2821
- stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2822
- stopBindings = () => {
2823
- stopInitialBindingListeners();
2824
- stopInitialBindingReflection();
2825
- };
2826
- let initialized = false;
2827
- const initialRenderedHtml = initial.html;
2828
- let lastRendered = null;
2829
- let renderEpoch = 0;
2830
- const renderReactive = () => {
2831
- const epoch = ++renderEpoch;
2832
- frame.creating = false;
2833
- frame.cursor = 0;
2834
- const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2835
- render: runInFrame(frame, input),
2836
- liveHost: host
2837
- });
2838
- stopJsxEvents();
2839
- stopJsxEvents = applyJsxEvents({
2840
- host,
2841
- records: newEvents,
2842
- reportError: (error) => reportError(error, "event"),
2843
- unmountSignal: unmountController.signal,
2844
- onceFired: nativeOnceFired
2845
- });
2846
- if (!initialized) {
2847
- initialized = true;
2848
- if (preserveSSRDom) {
2849
- pushUpdatedProps(newSlotMap);
2850
- if (rendered === initialRenderedHtml) {
2851
- lastRendered = rendered;
2852
- return;
2853
- }
2854
- }
2855
- if (rendered === initialRenderedHtml) {
2856
- lastRendered = rendered;
2857
- return;
2858
- }
2859
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2860
- initialHtml: initialRenderedHtml,
2861
- renderedHtml: rendered,
2862
- slotIds: newSlotMap.keys()
2863
- })) {
2864
- pushUpdatedProps(newSlotMap);
2865
- return;
2866
- }
2867
- }
2868
- if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
2869
- pushUpdatedProps(newSlotMap);
2870
- return;
2871
- }
2872
- stopBindings();
2873
- stopJsxEvents();
2874
- const prevMountedCount = mountedSlots.size;
2875
- for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) teardownMountedSlot(id, entry);
2876
- for (const [id, entry] of mountedSlots) {
2877
- const next = newSlotMap.get(id);
2878
- if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2879
- }
2880
- const applyMorph = () => {
2881
- if (epoch !== renderEpoch) return;
2882
- if (newSlotMap.size < prevMountedCount) {
2883
- let divergeFrom = Infinity;
2884
- for (const [id, entry] of mountedSlots) {
2885
- if (!id.startsWith("p:")) continue;
2886
- const next = newSlotMap.get(id);
2887
- if (!next) continue;
2888
- const index = Number(id.slice(2));
2889
- if (Number.isNaN(index) || index >= divergeFrom) continue;
2890
- if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2891
- }
2892
- for (const [id, entry] of mountedSlots) {
2893
- if (!newSlotMap.has(id)) continue;
2894
- if (!entry.el.isConnected) continue;
2895
- if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
2896
- }
2897
- }
2898
- const tpl = document.createElement("template");
2899
- const morphRootTag = host.tagName.toLowerCase();
2900
- tpl.innerHTML = `<${morphRootTag}>${rendered}</${morphRootTag}>`;
2901
- morphInner(host, tpl.content.firstElementChild);
2902
- lastRendered = rendered;
2903
- let rehomeIndex = null;
2904
- for (const [id, entry] of mountedSlots) {
2905
- if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2906
- rehomeIndex ??= buildSlotIndex();
2907
- const stub = rehomeIndex.get(id);
2908
- if (stub && stub !== entry.el) stub.replaceWith(entry.el);
2909
- }
2910
- stopBindings = applyTemplateBindings(host, newBinds);
2911
- stopJsxEvents = applyJsxEvents({
2912
- host,
2913
- records: newEvents,
2914
- reportError: (error) => reportError(error, "event"),
2915
- unmountSignal: unmountController.signal,
2916
- onceFired: nativeOnceFired
2917
- });
2918
- mountSlots(newSlotMap);
2919
- };
2920
- applyMorph();
2921
- };
2922
- let stopRender = effect$1(renderReactive);
2923
- startDerivedScopes();
2924
- stopRender();
2925
- stopRender = effect$1(renderReactive);
2926
- startEffectScopes();
2927
- let tornDown = false;
2928
- const unmount = () => {
2929
- if (tornDown) return;
2930
- tornDown = true;
2931
- instance.disposed = true;
2932
- unmountController.abort();
2933
- ISLAND_MOUNT_HANDLES.delete(host);
2934
- stopRender();
2935
- for (const [, entry] of mountedSlots) entry.unmount();
2936
- for (const c of cleanups) c();
2937
- };
2938
- const updateProps = (nextProps) => {
2939
- if (tornDown) return;
2940
- const next = resolveInput(nextProps);
2941
- if (shallowEqualInput(inputSignal(), next)) return;
2942
- inputSignal(next);
2943
- derivedPropsVersion(derivedPropsVersion() + 1);
2944
- };
2945
- const handle = {
2946
- unmount,
2947
- updateProps
2948
- };
2949
- ISLAND_MOUNT_HANDLES.set(host, handle);
2950
- return handle;
2951
- }
2952
- const island = ((props) => ({
2953
- [ISLAND_CALL]: true,
2954
- island,
2955
- props,
2956
- key: void 0
2957
61
  }));
2958
- island.toString = (props) => renderToString(props, true);
2959
- island.toStringAsync = (props) => Promise.resolve(renderToString(props, false));
2960
- island.mount = (host, props) => mountIsland(host, props);
2961
- island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2962
- island[ISLAND_SLOT_TAG] = configuredSlotTag;
2963
- island[ISLAND_RENDER_STATE] = async (props) => {
2964
- const manifest = /* @__PURE__ */ new Map();
2965
- const html = await renderToString(props ?? void 0, false, manifest, true);
2966
- return serializeServerManifest(manifest) + html;
2967
- };
2968
- island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, eventsOut, true);
2969
- island.key = (key) => {
2970
- if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2971
- if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
2972
- const keyed = ((props) => ({
2973
- [ISLAND_CALL]: true,
2974
- island,
2975
- props,
2976
- key
2977
- }));
2978
- keyed[ISLAND_CALL] = true;
2979
- return keyed;
2980
- };
2981
- island[ISLAND] = true;
2982
- const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2983
- if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2984
- island.define = (tagName, options) => {
2985
- if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2986
- `${tagName}`;
2987
- return;
2988
- }
2989
- const CE_RESERVED = /* @__PURE__ */ new Set([
2990
- "annotation-xml",
2991
- "color-profile",
2992
- "font-face",
2993
- "font-face-src",
2994
- "font-face-uri",
2995
- "font-face-format",
2996
- "font-face-name",
2997
- "missing-glyph"
2998
- ]);
2999
- if (typeof tagName !== "string" || !/^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(tagName) || CE_RESERVED.has(tagName)) {
3000
- `${tagName}`;
3001
- return;
3002
- }
3003
- if (customElements.get(tagName)) {
3004
- `${tagName}`;
3005
- return;
3006
- }
3007
- const observe = options?.observe ?? [];
3008
- class IlhaIslandElement extends HTMLElement {
3009
- static observedAttributes = observe;
3010
- _handle = null;
3011
- _props;
3012
- _unmounting = false;
3013
- _reconnect = false;
3014
- get props() {
3015
- return this._props;
3016
- }
3017
- set props(p) {
3018
- this._props = p;
3019
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
3020
- }
3021
- _mergedProps() {
3022
- const attrProps = {};
3023
- let hasAttrs = false;
3024
- for (const name of observe) {
3025
- const v = this.getAttribute(name);
3026
- if (v !== null) {
3027
- attrProps[name] = v;
3028
- hasAttrs = true;
3029
- }
3030
- }
3031
- if (!hasAttrs && this._props === void 0) return void 0;
3032
- return {
3033
- ...attrProps,
3034
- ...this._props
3035
- };
3036
- }
3037
- connectedCallback() {
3038
- if (this._unmounting) {
3039
- this._reconnect = true;
3040
- return;
3041
- }
3042
- if (this._handle) return;
3043
- this._handle = mountIslandInternal(this, this._mergedProps());
3044
- }
3045
- disconnectedCallback() {
3046
- if (!this._handle || this._unmounting) return;
3047
- this._unmounting = true;
3048
- this._reconnect = false;
3049
- Promise.resolve(this._handle.unmount()).finally(() => {
3050
- this._handle = null;
3051
- this._unmounting = false;
3052
- if (this._reconnect) {
3053
- this._reconnect = false;
3054
- if (this.isConnected) this._handle = mountIslandInternal(this, this._mergedProps());
3055
- }
3056
- });
3057
- }
3058
- attributeChangedCallback() {
3059
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
3060
- }
3061
- }
3062
- customElements.define(tagName, IlhaIslandElement);
3063
- };
3064
- island.hydratable = async (props, opts) => {
3065
- const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
3066
- const tag = assertValidSlotTagName(rawTag);
3067
- const resolvedProps = props ?? {};
3068
- const innerPromise = renderToString(resolvedProps, false, void 0, true);
3069
- let stateAttr = "";
3070
- if (snapshot !== false) {
3071
- const doState = snapshot === true || snapshot.state !== false;
3072
- const doDerived = snapshot === true || snapshot.derived !== false;
3073
- const doSkipOnMount = explicitSkipOnMount ?? false;
3074
- const snapshotData = { v: 2 };
3075
- const input = resolveInput(resolvedProps);
3076
- const probeFrame = freshFrame({ ssr: true });
3077
- withFrame(probeFrame, () => {
3078
- component(input);
3079
- });
3080
- if (doState) {
3081
- const states = [];
3082
- for (const slot of probeFrame.slots) {
3083
- if (slot.kind !== "state") continue;
3084
- const prevSub = setActiveSub(void 0);
3085
- try {
3086
- states.push(slot.acc());
3087
- } finally {
3088
- setActiveSub(prevSub);
3089
- }
3090
- }
3091
- snapshotData["s"] = states;
3092
- }
3093
- if (doDerived) {
3094
- const probePendings = probeFrame.pendingDerived ?? [];
3095
- if (probePendings.length > 0) {
3096
- await Promise.all(probePendings.map((p) => p.promise));
3097
- probeFrame.creating = false;
3098
- probeFrame.cursor = 0;
3099
- withFrame(probeFrame, () => {
3100
- component(input);
3101
- });
3102
- }
3103
- const derivedEntries = [];
3104
- for (const slot of probeFrame.slots) {
3105
- if (slot.kind !== "derived") continue;
3106
- const prevSub = setActiveSub(void 0);
3107
- let envelope;
3108
- try {
3109
- envelope = { ...slot.env() };
3110
- } finally {
3111
- setActiveSub(prevSub);
3112
- }
3113
- const entry = {
3114
- loading: false,
3115
- value: envelope.value
3116
- };
3117
- if (envelope.error) entry["error"] = envelope.error.message;
3118
- derivedEntries.push(entry);
3119
- }
3120
- snapshotData["d"] = derivedEntries;
3121
- }
3122
- if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
3123
- stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
3124
- }
3125
- const inner = await innerPromise;
3126
- return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${escapeHtml(JSON.stringify(resolvedProps))}'${stateAttr}>${inner}</${tag}>`;
3127
- };
3128
- return island;
3129
- });
3130
- function mountAll(registry, options = {}) {
3131
- const root = options.root ?? document.body;
3132
- const lazy = options.lazy ?? false;
3133
- const unmounts = [];
3134
- function activateEl(host) {
3135
- const name = host.getAttribute("data-ilha");
3136
- if (!name) return;
3137
- const island = registry[name];
3138
- if (!island) {
3139
- `${name}${Object.keys(registry).join(", ")}`;
3140
- return;
3141
- }
3142
- let props = {};
3143
- const rawProps = host.getAttribute(PROPS_ATTR);
3144
- if (rawProps) {
3145
- const parsed = safeParseSnapshot(rawProps, `${PROPS_ATTR} on [data-ilha="${name}"]`);
3146
- if (parsed !== void 0) props = reviveSlotProps(parsed);
3147
- }
3148
- unmounts.push(island.mount(host, props));
3149
- }
3150
- const els = Array.from(root.querySelectorAll("[data-ilha]"));
3151
- if (lazy && typeof IntersectionObserver !== "undefined") {
3152
- let disposed = false;
3153
- const io = new IntersectionObserver((entries) => {
3154
- if (disposed) return;
3155
- for (const entry of entries) if (entry.isIntersecting) {
3156
- activateEl(entry.target);
3157
- io.unobserve(entry.target);
3158
- }
3159
- });
3160
- els.forEach((el) => io.observe(el));
3161
- unmounts.push(() => {
3162
- disposed = true;
3163
- io.disconnect();
3164
- });
3165
- } else els.forEach(activateEl);
3166
- return { unmount: () => {
3167
- const pending = [];
3168
- for (const u of unmounts) {
3169
- const result = u();
3170
- if (result instanceof Promise) pending.push(result);
3171
- }
3172
- if (pending.length > 0) return Promise.all(pending).then(() => {});
3173
- } };
3174
- }
3175
- setJsxRuntimeBridge({
3176
- registerEvent({ type, handler, modifier }) {
3177
- const ctx = currentRenderCtx();
3178
- if (!ctx) return void 0;
3179
- SSR_ACTION_STUB in handler;
3180
- const index = ctx.events.length;
3181
- ctx.events.push({
3182
- type,
3183
- handler,
3184
- modifier
3185
- });
3186
- return index;
3187
- },
3188
- slot({ island, props, key }) {
3189
- return ilhaRaw(emitIslandSlot({
3190
- island,
3191
- props,
3192
- key
3193
- }));
3194
- }
3195
- });
3196
- /**
3197
- * Morph an element's children toward `html`, patching in place: surviving
3198
- * elements keep their identity, listeners, and focus (see the morph engine).
3199
- * Falls back to an innerHTML write when the root tags don't match.
3200
- */
3201
- function morph(host, html) {
3202
- if (typeof document === "undefined") return;
3203
- const openTag = host.localName ?? "div";
3204
- const tpl = document.createElement("template");
3205
- tpl.innerHTML = `<${openTag}>${html}</${openTag}>`;
3206
- const next = tpl.content.firstElementChild;
3207
- if (!next || next.localName !== host.localName) {
3208
- host.innerHTML = html;
3209
- return;
3210
- }
3211
- try {
3212
- morphInner(host, next);
3213
- } catch {
3214
- host.innerHTML = html;
3215
- }
3216
62
  }
3217
- const html = ilhaHtml;
3218
- const raw = ilhaRaw;
3219
- const mount = mountAll;
3220
- const context = ilhaContext;
3221
63
 
3222
64
  //#endregion
3223
- export { action, batch, context, css, derived, each, effect, html, ilha, isSafeUrl, isSafeUrlAttrValue, isUrlAttributeName, json, morph, mount, onError, onUncaughtError, persist, raw, serializeStyle, state, untrack };
65
+ export { Fragment, atom, h, mount, renderToString, wait, watch, when };