ilha 0.11.0 → 0.12.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,6 +1,8 @@
1
- import { computed, effect, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
1
+ import { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, serializeServerManifest, setJsxRuntimeBridge } from "./internal.js";
2
+ import { effect as effect$1, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
2
3
 
3
4
  //#region src/index.ts
5
+ const __DEV__ = false;
4
6
  const MAX_SNAPSHOT_CHARS = 262144;
5
7
  const MAX_SNAPSHOT_DEPTH = 32;
6
8
  function exceedsMaxDepth(value, depth) {
@@ -94,8 +96,8 @@ function extractSlotPropsAttr(root, slotId) {
94
96
  const SLOT_TAG_NAME_RE = /^[a-z][a-z0-9-]*$/i;
95
97
  function assertValidSlotTagName(tag) {
96
98
  const trimmed = tag.trim();
97
- if (trimmed.length === 0) throw new Error("island.as() requires a non-empty HTML tag name.");
98
- if (!SLOT_TAG_NAME_RE.test(trimmed)) throw new Error(`island.as() tag must be a valid HTML element name (got "${tag}"). Use names like "span", "div", or "li".`);
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".`);
99
101
  return trimmed.toLowerCase();
100
102
  }
101
103
  function getIslandSlotTag(island) {
@@ -134,11 +136,18 @@ function slotPropsForAttr(props, captureActions = false) {
134
136
  if (key === "children") continue;
135
137
  const value = props[key];
136
138
  if (typeof value === "function") {
137
- const action = captureActions ? captureHandlerActions(value) : void 0;
138
- if (action) (out ??= {})[key] = {
139
- __ilha: "action",
140
- ...action
141
- };
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
+ }
142
151
  continue;
143
152
  }
144
153
  if (typeof value === "symbol") continue;
@@ -188,32 +197,10 @@ function reviveSlotPropValue(value) {
188
197
  for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
189
198
  return out;
190
199
  }
191
- /**
192
- * Exact legacy shape produced when RawHtml lost its brand under JSON.stringify:
193
- * a plain object with a single own key `value` that is a string.
194
- */
195
- function isLegacyRawHtmlBlob(value) {
196
- if (!value || typeof value !== "object" || isRawHtmlValue(value)) return false;
197
- if (Object.getPrototypeOf(value) !== Object.prototype) return false;
198
- const keys = Object.keys(value);
199
- return keys.length === 1 && keys[0] === "value" && typeof value.value === "string";
200
- }
201
- /** Revive attr-parsed props (RawHtml tags). Children stay as plain data if present. */
200
+ /** Revive attr-parsed props, restoring tagged `__ilha:raw` RawHtml values. */
202
201
  function reviveSlotProps(props) {
203
202
  if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
204
- const out = reviveSlotPropValue(props);
205
- if ("children" in out) out.children = reviveLegacyChildren(out.children);
206
- return out;
207
- }
208
- function reviveLegacyChildren(children) {
209
- if (!Array.isArray(children)) {
210
- if (isLegacyRawHtmlBlob(children)) return makeRawHtml(children.value);
211
- return reviveSlotPropValue(children);
212
- }
213
- return children.map((child) => {
214
- if (isLegacyRawHtmlBlob(child)) return makeRawHtml(child.value);
215
- return reviveSlotPropValue(child);
216
- });
203
+ return reviveSlotPropValue(props);
217
204
  }
218
205
  /** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
219
206
  function serializeSlotProps(props) {
@@ -328,7 +315,7 @@ function morphChildren(fromParent, toParent) {
328
315
  fromParent.replaceChild(toEl.cloneNode(true), fromEl);
329
316
  continue;
330
317
  }
331
- if (fromEl.hasAttribute(LEAVING_ATTR)) {
318
+ if (fromEl.hasAttribute(SLOT_ATTR) && !fromEl.isConnected) {
332
319
  fromParent.replaceChild(toEl.cloneNode(true), fromEl);
333
320
  continue;
334
321
  }
@@ -460,6 +447,70 @@ const ESC = {
460
447
  function escapeHtml(value) {
461
448
  return String(value).replace(/[&<>"']/g, (c) => ESC[c]);
462
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
+ }
463
514
  function dedentString(str) {
464
515
  if (str.length === 0 || str[0] !== "\n") return str;
465
516
  const lines = str.split("\n");
@@ -474,17 +525,8 @@ const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
474
525
  const ISLAND = Symbol.for("ilha.island");
475
526
  const ISLAND_CALL = Symbol.for("ilha.islandCall");
476
527
  const ASTRO_RENDERER_GLOBAL = Symbol.for("ilha.astroRenderer");
477
- /** @internal Internal hook used by a parent's mountSlots to mount a child island and
478
- * retain a handle to push updated props into it on subsequent parent
479
- * re-renders. Not part of the public surface. */
480
- const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
481
528
  const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
482
529
  const ISLAND_CLIENT_REF = Symbol.for("ilha.clientRef");
483
- /** @internal Live mount handles keyed by host element. Lets @ilha/router adopt
484
- * islands hydrated by `ilha.mount()` (whose handles it never saw) and push new
485
- * loader props into them in place instead of remounting. Entries are removed
486
- * on unmount. Not part of the public surface. */
487
- const ISLAND_MOUNT_HANDLES = /* @__PURE__ */ new WeakMap();
488
530
  /** @internal Sync re-render of an island from a state snapshot. Attached by
489
531
  * render(); used by server-frame endpoints to produce HTML for streamed
490
532
  * updates without a live mount. Not part of the public surface. */
@@ -493,60 +535,140 @@ const ISLAND_RENDER_STATE = Symbol.for("ilha.renderState");
493
535
  * provided map. Used by emitIslandSlot so nested server islands ship their
494
536
  * interactive surface alongside their inlined HTML. */
495
537
  const ISLAND_SSR_MANIFEST = Symbol.for("ilha.ssrManifest");
496
- function manifestTemplatePrefix(manifest) {
497
- if (manifest.size === 0) return "";
498
- const json = escapeHtml(JSON.stringify(Object.fromEntries(manifest)));
499
- return `<template ${ACTIONS_ATTR}='${json}'></template>`;
500
- }
501
538
  const SLOT_ATTR = "data-ilha-slot";
502
- /** Marks a slot host whose child island is mid leave-transition: it has been
503
- * unmounted (and its slot id stripped) but stays connected so the leave
504
- * animation can paint. The morph replaces such elements instead of matching
505
- * or mutating them. */
506
- const LEAVING_ATTR = "data-ilha-leaving";
507
539
  const PROPS_ATTR = "data-ilha-props";
508
540
  const CLIENT_REF_ATTR = "data-ilha-client-ref";
509
541
  const STATE_ATTR = "data-ilha-state";
510
- const CSS_ATTR = "data-ilha-css";
511
- /** Hydration manifest mapping `${type}:${index}` event-sentinel pairs to the
512
- * action keys that own them. Emitted by hydratable(); lets server-owned
513
- * islands (whose render fn never ships to the client) reconnect their
514
- * interactive surface. */
515
- const ACTIONS_ATTR = "data-ilha-actions";
516
- function buildScopedStyle(css) {
517
- const safeCss = css.replace(/<\/style/gi, "<\\/style");
518
- return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${safeCss}}</style>`;
519
- }
520
- /** Marks SSR action stubs so event registration never capture-invokes them
521
- * (invoking the real action body during render would run its side effects). */
522
- const SSR_ACTION_STUB = Symbol.for("ilha.ssrActionStub");
523
- const captureStack = [];
542
+ function normalizeElseOutput(value) {
543
+ if (value == null || value === false) return value;
544
+ return Array.isArray(value) ? value : [value];
545
+ }
546
+ function createEachResult(items, mapFn) {
547
+ const mapped = items.length === 0 ? [] : items.map(mapFn);
548
+ const result = mapped;
549
+ Object.defineProperty(result, "else", {
550
+ value(fallback) {
551
+ if (items.length === 0) return normalizeElseOutput(typeof fallback === "function" ? fallback(items) : fallback);
552
+ return mapped;
553
+ },
554
+ enumerable: false
555
+ });
556
+ return result;
557
+ }
558
+ function createEachBuilder(items) {
559
+ return {
560
+ key(keyFn) {
561
+ return { as(mapFn) {
562
+ return createEachResult(items, (item, index) => mapFn(item, index, keyFn(item, index)));
563
+ } };
564
+ },
565
+ as(mapFn) {
566
+ return createEachResult(items, mapFn);
567
+ }
568
+ };
569
+ }
524
570
  /**
525
- * Invoke a forwarding closure once against sentinel arguments so any
526
- * `action.x(...)` call inside it is RECORDED, not executed. Returns the first
527
- * recorded call, or undefined when the closure throws or calls no action.
571
+ * Svelte-style `{#each}` helper for mapping collections to rendered output
572
+ * with an optional empty fallback.
573
+ *
574
+ * @example — plain list (empty → [])
575
+ * each(items).as((item) => html`<li>${item.name}</li>`)
528
576
  *
529
- * ponytail: the closure body really executes once per manifest render (SSR
530
- * and every frame) — only the action call is intercepted. Handlers must be
531
- * pure "call one action" thunks; side effects before/around the action call
532
- * run server-side on every frame. Upgrade path: static-scan the closure
533
- * instead of invoking it.
577
+ * @example — keyed list feeding island.key(), with empty fallback
578
+ * each(items)
579
+ * .key((item) => item.id)
580
+ * .as((item, _i, id) => Row.key(id)({ item }))
581
+ * .else(() => html`<EmptyState />`)
534
582
  */
535
- function captureHandlerActions(handler) {
536
- const frame = [];
537
- captureStack.push(frame);
538
- try {
539
- const result = handler({
540
- preventDefault: () => {},
541
- stopPropagation: () => {},
542
- stopImmediatePropagation: () => {},
543
- persist: () => {}
544
- }, { signal: new AbortController().signal });
545
- Promise.resolve(result).catch(() => {});
546
- } catch {} finally {
547
- captureStack.pop();
548
- }
549
- return frame[0];
583
+ function each(items) {
584
+ 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.");
585
+ return createEachBuilder(Array.isArray(items) ? items : []);
586
+ }
587
+ /** Marks SSR action stubs so event registration can recognize action
588
+ * references without ever invoking them (executing a handler body during
589
+ * render would run its side effects server-side). Symbol.for brands keep
590
+ * this recognizable across duplicate ilha copies and external action
591
+ * implementations (e.g. oxidejs) in the same realm. */
592
+ const SSR_ACTION_STUB = Symbol.for("ilha.ssrActionStub");
593
+ const ACTION_MANIFEST_ID = Symbol.for("ilha.actionManifestId");
594
+ const ACTION_BOUND_ARGS = Symbol.for("ilha.actionBoundArgs");
595
+ /** Serialized size ceiling for `.withArgs()` payloads (JSON chars). */
596
+ const MAX_BOUND_ARGS_CHARS = 8192;
597
+ /** Manifest id for a brand-recognized action reference, else undefined.
598
+ * Anything that isn't a branded action stub is rejected outright — event
599
+ * handlers are never executed during SSR to discover what they call. */
600
+ function actionManifestId(fn) {
601
+ if (typeof fn !== "function" || !(SSR_ACTION_STUB in fn)) return void 0;
602
+ return fn[ACTION_MANIFEST_ID];
603
+ }
604
+ /** Bound args recorded by `.withArgs()` on an action reference, else undefined. */
605
+ function actionBoundArgs(fn) {
606
+ if (typeof fn !== "function" || !(ACTION_BOUND_ARGS in fn)) return void 0;
607
+ const args = fn[ACTION_BOUND_ARGS];
608
+ return Array.isArray(args) ? args : void 0;
609
+ }
610
+ /** Attach `.withArgs(...args)` to an action accessor. Returns an explicit,
611
+ * serializable action REFERENCE — never a closure to inspect or probe.
612
+ * Arguments are validated JSON-safe at bind time; the hydration manifest
613
+ * stores `{ actionId, args }` directly and the client RPC path sends them
614
+ * only after a real user event. On the client, calling the bound reference
615
+ * invokes the accessor with the bound payload. */
616
+ /** Keys that must never appear in a bound payload — they become dangerous
617
+ * if an RPC handler or downstream library merges the object into another. */
618
+ const UNSAFE_KEYS = /* @__PURE__ */ new Set([
619
+ "__proto__",
620
+ "constructor",
621
+ "prototype"
622
+ ]);
623
+ /** Strict JSON-safety validator: accept, or throw — never transform.
624
+ * Rejects undefined, functions, symbols, bigint, non-finite numbers,
625
+ * Dates/custom prototypes/Map/Set, circular values, and unsafe keys so a
626
+ * bound payload is byte-identical to what the author wrote. */
627
+ function assertJsonSafe(value, seen = /* @__PURE__ */ new WeakSet()) {
628
+ if (value === null) return;
629
+ switch (typeof value) {
630
+ case "string":
631
+ case "boolean": return;
632
+ case "number":
633
+ if (Number.isFinite(value)) return;
634
+ throw new TypeError("withArgs() rejects non-finite numbers.");
635
+ case "object": break;
636
+ default: throw new TypeError(`withArgs() rejects ${typeof value} values.`);
637
+ }
638
+ if (seen.has(value)) throw new TypeError("withArgs() rejects circular values.");
639
+ seen.add(value);
640
+ if (Array.isArray(value)) {
641
+ for (const item of value) assertJsonSafe(item, seen);
642
+ return;
643
+ }
644
+ if (Object.getPrototypeOf(value) !== Object.prototype) throw new TypeError("withArgs() accepts plain objects only.");
645
+ for (const [key, child] of Object.entries(value)) {
646
+ if (UNSAFE_KEYS.has(key)) throw new TypeError(`withArgs() rejects unsafe key "${key}".`);
647
+ assertJsonSafe(child, seen);
648
+ }
649
+ }
650
+ /** Deep-clone an already-validated JSON-safe value. */
651
+ function cloneJsonSafe(value) {
652
+ return structuredClone(value);
653
+ }
654
+ function attachWithArgs(accessor) {
655
+ Object.defineProperty(accessor, "withArgs", {
656
+ enumerable: false,
657
+ value: (...args) => {
658
+ for (const arg of args) assertJsonSafe(arg);
659
+ const serialized = cloneJsonSafe(args);
660
+ if (JSON.stringify(serialized).length > MAX_BOUND_ARGS_CHARS) throw new RangeError(`withArgs() payload exceeds ${MAX_BOUND_ARGS_CHARS} JSON characters.`);
661
+ const manifestId = actionManifestId(accessor);
662
+ const bound = (...runtimeArgs) => {
663
+ return accessor(serialized.length > 0 ? serialized.length === 1 ? serialized[0] : serialized : runtimeArgs.length === 1 ? runtimeArgs[0] : runtimeArgs);
664
+ };
665
+ const brand = bound;
666
+ brand[SSR_ACTION_STUB] = true;
667
+ brand[ACTION_MANIFEST_ID] = manifestId;
668
+ brand[ACTION_BOUND_ARGS] = serialized;
669
+ return bound;
670
+ }
671
+ });
550
672
  }
551
673
  const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
552
674
  function renderCtxStack() {
@@ -605,8 +727,8 @@ function emitIslandSlot({ island, props, key }) {
605
727
  try {
606
728
  const manifest = /* @__PURE__ */ new Map();
607
729
  const ssrWithManifest = island[ISLAND_SSR_MANIFEST];
608
- const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island(props);
609
- const prefix = () => manifestTemplatePrefix(manifest);
730
+ const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island.toStringAsync(props);
731
+ const prefix = () => serializeServerManifest(manifest);
610
732
  if (result instanceof Promise) {
611
733
  ctx.pending.set(id, result.then(String).then((html) => prefix() + html));
612
734
  return wrapIslandSlotHtml({
@@ -656,7 +778,7 @@ function resolveSignalSetter(read, value) {
656
778
  function markSignalAccessor(fn) {
657
779
  fn[SIGNAL_ACCESSOR] = true;
658
780
  const accessor = fn;
659
- accessor.select = (selector) => createSelectAccessor(accessor, selector);
781
+ accessor.select = ((...args) => createSelectAccessor(accessor, args));
660
782
  return accessor;
661
783
  }
662
784
  function isSignalAccessor(v) {
@@ -731,22 +853,27 @@ function trackSelectPath(rootState, selector) {
731
853
  selector(track(rootState));
732
854
  return path;
733
855
  }
734
- function createSelectAccessor(root, selector) {
735
- const path = trackSelectPath(root(), selector);
736
- const selected = selector(root());
737
- const resolved = path.length === 0 ? root() : getAtPath(root(), path);
738
- 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.");
856
+ function createSelectAccessor(root, input) {
857
+ const selector = typeof input[0] === "function" ? input[0] : void 0;
858
+ let path;
859
+ if (selector) {
860
+ path = trackSelectPath(root(), selector);
861
+ const selected = selector(root());
862
+ const resolved = path.length === 0 ? root() : getAtPath(root(), path);
863
+ 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.");
864
+ } else path = input;
865
+ const read = (from) => selector ? path.length === 0 ? selector(from) : getAtPath(from, path) : getAtPath(from, path);
739
866
  return markSignalAccessor((...args) => {
740
- if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
867
+ if (args.length === 0) return read(root());
741
868
  const previousRoot = root();
742
- const previousSelected = path.length === 0 ? selector(previousRoot) : getAtPath(previousRoot, path);
869
+ const previousSelected = read(previousRoot);
743
870
  const value = resolveSignalSetter(() => previousSelected, args[0]);
744
- const next = path.length === 0 ? value : setAtPath({
871
+ const next = path.length === 0 && selector ? value : setAtPath({
745
872
  object: previousRoot,
746
873
  path,
747
874
  value
748
875
  });
749
- if (!Object.is(previousRoot, next)) root((() => next));
876
+ if (!Object.is(previousRoot, next)) root(() => next);
750
877
  });
751
878
  }
752
879
  function ilhaRaw(value) {
@@ -755,14 +882,37 @@ function ilhaRaw(value) {
755
882
  value
756
883
  };
757
884
  }
758
- function ilhaCss(strings, ...values) {
759
- if (typeof strings === "string") return strings;
760
- let result = "";
761
- for (let i = 0; i < strings.length; i++) {
762
- result += strings[i];
763
- if (i < values.length) result += String(values[i]);
764
- }
765
- return result;
885
+ /**
886
+ * Serialize a JSON-safe value into executable `<script>` content safely. `<`
887
+ * is escaped as `\u003C`, so a value can never close the script block or open
888
+ * the `<!--` escape hatch. Use it for script-element data:
889
+ *
890
+ * ```ts
891
+ * html`<script>const d = ${json(payload)};<\/script>`
892
+ * ```
893
+ *
894
+ * Like `raw()`, `json()` is for element *content* only — interpolating it into
895
+ * an attribute is author error. It does not protect against values that are
896
+ * not valid JSON (functions, symbols, undefined, cyclic objects are dropped
897
+ * or throw, matching `JSON.stringify`).
898
+ */
899
+ function json(value) {
900
+ return ilhaRaw(serializeScriptJson(value));
901
+ }
902
+ /**
903
+ * Escape text for a `<style>` element body. `<` becomes the CSS hex escape
904
+ * `\3C `, so the value can never close the style block or open the `<!--`
905
+ * escape hatch while surviving CSS parsing:
906
+ *
907
+ * ```ts
908
+ * html`<style>${css(cssSource)}</style>`
909
+ * ```
910
+ *
911
+ * Like `raw()`, `css()` is for element *content* only — interpolating it into
912
+ * an attribute is author error.
913
+ */
914
+ function css(value) {
915
+ return ilhaRaw(escapeStyleText(value));
766
916
  }
767
917
  function interpolateValue(v) {
768
918
  if (v == null || v === true || v === false) return "";
@@ -788,21 +938,31 @@ function interpolateValue(v) {
788
938
  const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
789
939
  "value",
790
940
  "checked",
791
- "valueAsNumber",
792
- "valueAsDate",
793
941
  "files",
794
942
  "open",
795
943
  "group",
796
944
  "this"
797
945
  ]);
798
946
  const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
799
- const EVENT_PREFIX_RE = /(?:^|\s)on([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\s*=\s*("|')?$/;
947
+ const EVENT_PREFIX_RE = /(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::([a-zA-Z][a-zA-Z0-9-]*))?\s*=\s*("|')?$/;
800
948
  const NATIVE_EVENT_MODIFIERS = /* @__PURE__ */ new Set([
801
949
  "abortable",
802
950
  "once",
803
951
  "capture",
804
952
  "passive"
805
953
  ]);
954
+ const FULL_VALUE_ATTR_RE = /(?:^|\s)([a-zA-Z_:][a-zA-Z0-9:._-]*)\s*=\s*("|')?$/;
955
+ const UNQUOTED_HAZARD_RE = /[\s=]|`/;
956
+ function resolveTemplateValue(v) {
957
+ if (isSignalAccessor(v)) return v();
958
+ if (typeof v === "function") return v();
959
+ return v;
960
+ }
961
+ function openTagName(source, index) {
962
+ const open = source.lastIndexOf("<", index);
963
+ if (open === -1) return "element";
964
+ return (source.slice(open).match(/^<\s*([a-zA-Z][a-zA-Z0-9:._-]*)/)?.[1] ?? "element").toLowerCase();
965
+ }
806
966
  function isEventAttributePosition(source, index) {
807
967
  let open = -1;
808
968
  let quote = null;
@@ -837,14 +997,6 @@ function findTagCloseIndex(chunk, initialQuote) {
837
997
  quote
838
998
  };
839
999
  }
840
- function formatDateForInput(d) {
841
- if (d instanceof Date) {
842
- if (isNaN(d.getTime())) return "";
843
- const pad = (n) => String(n).padStart(2, "0");
844
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
845
- }
846
- return "";
847
- }
848
1000
  function extractElementStaticValue(prefix) {
849
1001
  const lastOpen = prefix.lastIndexOf("<");
850
1002
  if (lastOpen === -1) return null;
@@ -864,8 +1016,6 @@ function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
864
1016
  }
865
1017
  switch (kind) {
866
1018
  case "value": return [` value="${escapeHtml(v ?? "")}"`, spec];
867
- case "valueAsNumber": return [` value="${escapeHtml(v == null ? "" : String(v))}"`, spec];
868
- case "valueAsDate": return [` value="${escapeHtml(formatDateForInput(v))}"`, spec];
869
1019
  case "checked": return [v ? ` checked` : ``, spec];
870
1020
  case "open": return [v ? ` open` : ``, spec];
871
1021
  case "files": return [``, spec];
@@ -910,24 +1060,29 @@ function ilhaHtml(strings, ...values) {
910
1060
  const value = values[i];
911
1061
  const eventMatch = chunk.match(EVENT_PREFIX_RE);
912
1062
  const eventMatchStart = eventMatch ? chunk.length - eventMatch[0].length : -1;
1063
+ if (!eventMatch && typeof value === "function") {
1064
+ const ambiguous = chunk.match(/(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::[a-zA-Z][a-zA-Z0-9-]*)?\s*=\s*(?:(?:"[^"]*)|(?:'[^']*)|(?:[^\s"'=<>`]+))$/);
1065
+ const start = ambiguous ? chunk.length - ambiguous[0].length : -1;
1066
+ if (ambiguous && isEventAttributePosition(result + chunk, result.length + start)) throw new Error(`[ilha] on${ambiguous[1]} handler must occupy the entire attribute value. Write on${ambiguous[1]}=\${handler}.`);
1067
+ }
913
1068
  const eventAttributeFragment = eventMatch !== null && chunk.slice(0, eventMatchStart).trim() === "";
914
1069
  if (eventMatch && (eventAttributeFragment || isEventAttributePosition(result + chunk, result.length + eventMatchStart))) {
915
- const eventName = eventMatch[1];
916
- const rawModifier = eventMatch[2];
917
- const modifier = NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
1070
+ const eventName = eventMatch[1].toLowerCase();
1071
+ const rawModifier = eventMatch[2]?.toLowerCase();
1072
+ const modifier = rawModifier !== void 0 && NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
918
1073
  const openQuote = eventMatch[3] ?? null;
1074
+ const nextChunk = strings[i + 1] ?? "";
1075
+ if (!(openQuote ? nextChunk.startsWith(openQuote) : /^(?:\s|\/?>|$)/.test(nextChunk))) throw new Error(`[ilha] on${eventName} handler must occupy the entire attribute value. Write on${eventName}=\${handler}.`);
919
1076
  const cleanPrefix = chunk.slice(0, eventMatchStart).replace(/\s+$/, "");
920
- const isActionStub = typeof value === "function" && SSR_ACTION_STUB in value;
921
- const capture = ctx?.manifest && typeof value === "function" && !isActionStub ? captureHandlerActions(value) : void 0;
922
1077
  result += cleanPrefix;
1078
+ typeof value === "function" && SSR_ACTION_STUB in value;
923
1079
  if (ctx && typeof value === "function") {
924
1080
  const index = ctx.events.length;
925
1081
  if (rawModifier && !modifier) {} else {
926
1082
  ctx.events.push({
927
1083
  type: eventName,
928
1084
  handler: value,
929
- modifier,
930
- capture
1085
+ modifier
931
1086
  });
932
1087
  pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
933
1088
  }
@@ -968,6 +1123,72 @@ function ilhaHtml(strings, ...values) {
968
1123
  if (openQuote) stripLeadingQuote = openQuote;
969
1124
  continue;
970
1125
  }
1126
+ const attrMatch = chunk.match(FULL_VALUE_ATTR_RE);
1127
+ if (attrMatch && isEventAttributePosition(result + chunk, result.length + attrMatch.index)) {
1128
+ const attrName = attrMatch[1];
1129
+ const openQuote = attrMatch[2] ?? null;
1130
+ if (openQuote) stripLeadingQuote = openQuote;
1131
+ const cleanPrefix = chunk.slice(0, attrMatch.index).replace(/\s+$/, "");
1132
+ if (attrName.length > 2 && /^on/i.test(attrName)) {
1133
+ result += cleanPrefix;
1134
+ continue;
1135
+ }
1136
+ if (attrName.toLowerCase() === "srcdoc") {
1137
+ result += cleanPrefix;
1138
+ continue;
1139
+ }
1140
+ if (isIsland(value) || isIslandCall(value)) {
1141
+ stripLeadingQuote = null;
1142
+ result += chunk + interpolateValue(value);
1143
+ continue;
1144
+ }
1145
+ if (isUrlAttributeName(attrName)) {
1146
+ if (isRawHtml(value)) {
1147
+ result += cleanPrefix + ` ${attrName}="${value.value}"`;
1148
+ continue;
1149
+ }
1150
+ const resolved = resolveTemplateValue(value);
1151
+ if (resolved == null || resolved === false) {
1152
+ result += cleanPrefix;
1153
+ continue;
1154
+ }
1155
+ const url = String(resolved);
1156
+ if (!isSafeUrlAttrValue(openTagName(result + chunk, result.length + attrMatch.index), attrName, url)) {
1157
+ result += cleanPrefix;
1158
+ continue;
1159
+ }
1160
+ const quotedUrl = openQuote !== null || UNQUOTED_HAZARD_RE.test(url);
1161
+ result += cleanPrefix + ` ${attrName}=` + (quotedUrl ? `"${escapeHtml(url)}"` : escapeHtml(url));
1162
+ continue;
1163
+ }
1164
+ if (attrName.toLowerCase() === "style") {
1165
+ if (!isRawHtml(value)) {
1166
+ const resolved = resolveTemplateValue(value);
1167
+ if (isPlainObject(resolved)) {
1168
+ result += cleanPrefix + ` style="${escapeHtml(serializeStyle(resolved))}"`;
1169
+ continue;
1170
+ }
1171
+ }
1172
+ }
1173
+ if (isRawHtml(value)) {
1174
+ stripLeadingQuote = null;
1175
+ result += chunk + interpolateValue(value);
1176
+ continue;
1177
+ }
1178
+ const resolved = resolveTemplateValue(value);
1179
+ if (resolved == null || resolved === false) {
1180
+ result += cleanPrefix;
1181
+ continue;
1182
+ }
1183
+ if (resolved === true) {
1184
+ result += cleanPrefix + ` ${attrName}`;
1185
+ continue;
1186
+ }
1187
+ const generic = String(resolved);
1188
+ if (openQuote !== null || UNQUOTED_HAZARD_RE.test(generic)) result += cleanPrefix + ` ${attrName}="${escapeHtml(generic)}"`;
1189
+ else result += cleanPrefix + ` ${attrName}=${escapeHtml(generic)}`;
1190
+ continue;
1191
+ }
971
1192
  result += chunk + interpolateValue(value);
972
1193
  }
973
1194
  if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
@@ -988,10 +1209,10 @@ const contextRegistry = /* @__PURE__ */ new Map();
988
1209
  function ilhaContextFn(key, initial) {
989
1210
  if (contextRegistry.has(key)) return contextRegistry.get(key);
990
1211
  const s = signal(initial);
991
- const accessor = (...args) => {
1212
+ const accessor = markSignalAccessor((...args) => {
992
1213
  if (args.length === 0) return s();
993
1214
  s(resolveSignalSetter(() => s(), args[0]));
994
- };
1215
+ });
995
1216
  contextRegistry.set(key, accessor);
996
1217
  return accessor;
997
1218
  }
@@ -1007,16 +1228,11 @@ const ilhaContext = Object.assign(ilhaContextFn, {
1007
1228
  });
1008
1229
  /**
1009
1230
  * Create a free-standing reactive signal that lives outside any island.
1010
- * Useful for sharing state across islands without prop drilling, or for
1011
- * binding form inputs to module-level state via the `bind:value=${signal}`
1012
- * template syntax.
1013
- *
1014
- * The returned accessor is a getter when called with no arguments and a
1015
- * setter when called with one. Reading it inside a `.derived()`, `.effect()`,
1016
- * or `.render()` automatically subscribes the surrounding reactive scope —
1017
- * so when the signal changes, dependents re-run as if it were local state.
1231
+ * Internal: used by state()'s slot-drift fallback. The public way to create
1232
+ * free-standing shared state is `context(key, initial)`.
1018
1233
  */
1019
1234
  function ilhaSignal(initial) {
1235
+ if (currentFrame()?.creating && __DEV__);
1020
1236
  const s = signal(initial);
1021
1237
  return markSignalAccessor((...args) => {
1022
1238
  if (args.length === 0) return s();
@@ -1024,25 +1240,6 @@ function ilhaSignal(initial) {
1024
1240
  });
1025
1241
  }
1026
1242
  /**
1027
- * Create a free-standing read-only reactive value derived from other signals.
1028
- * The computation is lazy and cached: `fn` re-runs only when a signal it read
1029
- * changed and the computed is read again. Reading it inside a `.derived()`,
1030
- * `.effect()`, `.render()`, or top-level `effect()` subscribes that scope —
1031
- * dependents re-run when the computed's value changes.
1032
- *
1033
- * ```ts
1034
- * const items = ilha.signal([1, 2, 3]);
1035
- * const total = ilha.computed(() => items().reduce((a, b) => a + b, 0));
1036
- * ```
1037
- */
1038
- function ilhaComputed(fn) {
1039
- const c = computed(fn);
1040
- return markSignalAccessor((...args) => {
1041
- if (args.length > 0) return;
1042
- return c();
1043
- });
1044
- }
1045
- /**
1046
1243
  * Run a free-standing reactive effect outside any island. `fn` runs once
1047
1244
  * immediately and again whenever a signal it read changes. It may return a
1048
1245
  * cleanup function, invoked before each re-run and on stop. Signal writes
@@ -1067,7 +1264,7 @@ function ilhaEffect(fn) {
1067
1264
  cleanup = void 0;
1068
1265
  }
1069
1266
  };
1070
- const stop = effect(() => {
1267
+ const stop = effect$1(() => {
1071
1268
  runCleanup();
1072
1269
  startBatch();
1073
1270
  try {
@@ -1115,6 +1312,74 @@ function batch(fn) {
1115
1312
  endBatch();
1116
1313
  }
1117
1314
  }
1315
+ /**
1316
+ * Keep a standalone signal in sync with persistent storage:
1317
+ *
1318
+ * 1. On call, reads `key` and writes the stored value into the signal.
1319
+ * 2. Subscribes to the signal and writes its value back on every change.
1320
+ * 3. Optionally mirrors writes from other tabs (`storage` events).
1321
+ *
1322
+ * No-op on the server (returns an inert unsubscribe). Call the returned
1323
+ * unsubscribe to stop syncing.
1324
+ *
1325
+ * ```ts
1326
+ * const cart = signal([] as string[]);
1327
+ * persist(cart, "cart");
1328
+ * ```
1329
+ */
1330
+ function persist(accessor, key, options = {}) {
1331
+ if (typeof window === "undefined") return () => {};
1332
+ const storage = options.storage ?? window.localStorage;
1333
+ const serialize = options.serialize ?? JSON.stringify;
1334
+ const deserialize = options.deserialize ?? ((raw) => {
1335
+ try {
1336
+ return JSON.parse(raw);
1337
+ } catch {
1338
+ return null;
1339
+ }
1340
+ });
1341
+ const label = `[ilha] persist("${key}")`;
1342
+ const applyRaw = (raw) => {
1343
+ if (raw == null) return;
1344
+ try {
1345
+ const value = deserialize(raw);
1346
+ if (value !== null) {
1347
+ const prevSub = setActiveSub(void 0);
1348
+ try {
1349
+ accessor(() => value);
1350
+ } finally {
1351
+ setActiveSub(prevSub);
1352
+ }
1353
+ }
1354
+ } catch (err) {
1355
+ console.error(`${label}: failed to restore`, err);
1356
+ }
1357
+ };
1358
+ try {
1359
+ applyRaw(storage.getItem(key));
1360
+ } catch (err) {
1361
+ console.error(`${label}: failed to read storage`, err);
1362
+ }
1363
+ const stopEffect = ilhaEffect(() => {
1364
+ try {
1365
+ storage.setItem(key, serialize(accessor()));
1366
+ } catch (err) {
1367
+ console.error(`${label}: failed to write storage`, err);
1368
+ }
1369
+ });
1370
+ let onStorage = null;
1371
+ if (options.crossTab !== false && storage === window.localStorage) {
1372
+ onStorage = (e) => {
1373
+ if (e.key !== key) return;
1374
+ applyRaw(e.newValue);
1375
+ };
1376
+ window.addEventListener("storage", onStorage);
1377
+ }
1378
+ return () => {
1379
+ stopEffect();
1380
+ if (onStorage) window.removeEventListener("storage", onStorage);
1381
+ };
1382
+ }
1118
1383
  function createDerivedAccessor(read, write) {
1119
1384
  const accessor = markSignalAccessor((...args) => {
1120
1385
  if (args.length > 0) {
@@ -1135,342 +1400,69 @@ function defaultDerivedAccessor() {
1135
1400
  error: void 0
1136
1401
  }));
1137
1402
  }
1138
- function buildDerivedAccessors(envelopes) {
1139
- const accessors = /* @__PURE__ */ new Map();
1140
- for (const [key, envelope] of Object.entries(envelopes)) accessors.set(key, createDerivedAccessor(() => envelope, (value) => {
1141
- envelope.loading = false;
1142
- envelope.value = value;
1143
- envelope.error = void 0;
1144
- }));
1145
- return new Proxy({}, { get(_, key) {
1146
- if (typeof key !== "string") return void 0;
1147
- return accessors.get(key) ?? defaultDerivedAccessor();
1148
- } });
1149
- }
1150
- const ILHA_QUERY = Symbol.for("ilha.store.query");
1151
- const ILHA_QUERY_RUN = Symbol.for("ilha.store.query.run");
1152
- const ILHA_QUERY_PENDING = Symbol.for("ilha.store.query.pending");
1153
- const ILHA_QUERY_STACK = Symbol.for("ilha.store.query.stack");
1154
- function queryCaptureStack() {
1155
- const g = globalThis;
1156
- let stack = g[ILHA_QUERY_STACK];
1157
- if (!stack) {
1158
- stack = [];
1159
- g[ILHA_QUERY_STACK] = stack;
1160
- }
1161
- return stack;
1162
- }
1163
- function isQueryCall(value) {
1164
- return value !== null && typeof value === "object" && ILHA_QUERY in value;
1165
- }
1166
- function resolveQueryCall(result, captureSlot) {
1167
- const pending = globalThis[ILHA_QUERY_PENDING];
1168
- globalThis[ILHA_QUERY_PENDING] = void 0;
1169
- return captureSlot.queryCall ?? pending ?? (isQueryCall(result) ? result : void 0);
1170
- }
1171
- function runBrandedQuery(queryCall, abortSignal, env) {
1172
- const writeEnv = (next) => {
1173
- if (abortSignal.aborted) return;
1174
- const prevSub = setActiveSub(void 0);
1175
- env(next);
1176
- setActiveSub(prevSub);
1177
- };
1178
- const run = queryCall[ILHA_QUERY_RUN];
1179
- if (typeof run !== "function") {
1180
- writeEnv({
1181
- loading: false,
1182
- value: void 0,
1183
- error: /* @__PURE__ */ new Error("[@ilha] branded query is missing a callable runner")
1184
- });
1185
- return;
1186
- }
1187
- try {
1188
- run.call(queryCall, abortSignal, {
1189
- onResult: (value) => {
1190
- writeEnv({
1191
- loading: false,
1192
- value,
1193
- error: void 0
1194
- });
1195
- },
1196
- onError: (err) => {
1197
- writeEnv({
1198
- loading: false,
1199
- value: void 0,
1200
- error: err
1201
- });
1202
- },
1203
- onFetchStart: () => {
1204
- const prevSub = setActiveSub(void 0);
1205
- env({
1206
- loading: true,
1207
- value: env().value,
1208
- error: void 0
1209
- });
1210
- setActiveSub(prevSub);
1211
- }
1212
- });
1213
- } catch (err) {
1214
- writeEnv({
1215
- loading: false,
1216
- value: void 0,
1217
- error: err instanceof Error ? err : new Error(String(err))
1403
+ const BIND_SENTINEL_ATTR = "data-ilha-bind";
1404
+ const EVENT_SENTINEL_ATTR = "data-ilha-on";
1405
+ function parseBindSentinel(value) {
1406
+ const out = [];
1407
+ for (const part of value.split(",")) {
1408
+ const [kind, idx] = part.split(":");
1409
+ if (!kind || !idx) continue;
1410
+ const i = Number(idx);
1411
+ if (!Number.isInteger(i) || i < 0) continue;
1412
+ out.push({
1413
+ kind,
1414
+ index: i
1218
1415
  });
1219
1416
  }
1417
+ return out;
1220
1418
  }
1221
- function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
1222
- const envelopes = /* @__PURE__ */ new Map();
1223
- for (const entry of entries) {
1224
- let initialEnvelope;
1225
- if (derivedSnapshot != null && entry.key in derivedSnapshot) initialEnvelope = { ...derivedSnapshot[entry.key] };
1226
- else if (entry.fn.constructor.name === "AsyncFunction" || entry.fn.constructor.name === "AsyncGeneratorFunction") initialEnvelope = {
1227
- loading: true,
1228
- value: void 0,
1229
- error: void 0
1419
+ function resolveBindOps(el, kind) {
1420
+ const input = el;
1421
+ switch (kind) {
1422
+ case "value": return {
1423
+ event: el.tagName === "SELECT" ? "change" : "input",
1424
+ read: (el) => el.value,
1425
+ write: (el, v) => el.value = v == null ? "" : String(v)
1230
1426
  };
1231
- else {
1232
- const ac = new AbortController();
1233
- const captureSlot = {};
1234
- const stack = queryCaptureStack();
1235
- globalThis[ILHA_QUERY_PENDING] = void 0;
1236
- stack.push(captureSlot);
1237
- try {
1238
- const result = entry.fn({
1239
- state,
1240
- input,
1241
- signal: ac.signal
1242
- });
1243
- if (resolveQueryCall(result, captureSlot)) initialEnvelope = {
1244
- loading: true,
1245
- value: void 0,
1246
- error: void 0
1247
- };
1248
- else if (result instanceof Promise) {
1249
- initialEnvelope = {
1250
- loading: true,
1251
- value: void 0,
1252
- error: void 0
1427
+ case "checked": return {
1428
+ event: "change",
1429
+ read: (el) => el.checked,
1430
+ write: (el, v) => el.checked = Boolean(v)
1431
+ };
1432
+ case "files": return {
1433
+ event: "change",
1434
+ read: (el) => el.files,
1435
+ write: () => {}
1436
+ };
1437
+ case "open": return {
1438
+ event: "toggle",
1439
+ read: (el) => el.open,
1440
+ write: (el, v) => el.open = Boolean(v)
1441
+ };
1442
+ case "this": return {
1443
+ event: null,
1444
+ read: () => void 0,
1445
+ write: () => {}
1446
+ };
1447
+ case "group": {
1448
+ const isCheckbox = input.type === "checkbox";
1449
+ return {
1450
+ event: "change",
1451
+ read: (el) => {
1452
+ const i = el;
1453
+ if (isCheckbox) return {
1454
+ __ilhaGroup: true,
1455
+ value: i.value,
1456
+ checked: i.checked
1253
1457
  };
1254
- result.catch(() => {});
1255
- } else initialEnvelope = {
1256
- loading: false,
1257
- value: result,
1258
- error: void 0
1259
- };
1260
- } catch (err) {
1261
- globalThis[ILHA_QUERY_PENDING] = void 0;
1262
- initialEnvelope = {
1263
- loading: false,
1264
- value: void 0,
1265
- error: err instanceof Error ? err : new Error(String(err))
1266
- };
1267
- } finally {
1268
- stack.pop();
1269
- ac.abort();
1270
- }
1271
- }
1272
- const env = signal(initialEnvelope);
1273
- envelopes.set(entry.key, env);
1274
- }
1275
- const accessors = /* @__PURE__ */ new Map();
1276
- for (const entry of entries) {
1277
- const env = envelopes.get(entry.key);
1278
- accessors.set(entry.key, createDerivedAccessor(() => env(), (value) => {
1279
- const prevSub = setActiveSub(void 0);
1280
- env({
1281
- loading: false,
1282
- value,
1283
- error: void 0
1284
- });
1285
- setActiveSub(prevSub);
1286
- }));
1287
- }
1288
- const proxy = new Proxy({}, { get(_, key) {
1289
- if (typeof key !== "string") return void 0;
1290
- return accessors.get(key) ?? defaultDerivedAccessor();
1291
- } });
1292
- const setup = () => {
1293
- const stops = [];
1294
- for (const entry of entries) {
1295
- const env = envelopes.get(entry.key);
1296
- let ac = new AbortController();
1297
- let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
1298
- const stopEffect = effect(() => {
1299
- ac.abort();
1300
- ac = new AbortController();
1301
- const currentAc = ac;
1302
- let result;
1303
- const captureSlot = {};
1304
- const stack = queryCaptureStack();
1305
- globalThis[ILHA_QUERY_PENDING] = void 0;
1306
- stack.push(captureSlot);
1307
- try {
1308
- result = entry.fn({
1309
- state,
1310
- input,
1311
- signal: currentAc.signal
1312
- });
1313
- } catch (err) {
1314
- globalThis[ILHA_QUERY_PENDING] = void 0;
1315
- if (skipFirst) {
1316
- skipFirst = false;
1317
- return;
1318
- }
1319
- const prevSub = setActiveSub(void 0);
1320
- env({
1321
- loading: false,
1322
- value: void 0,
1323
- error: err instanceof Error ? err : new Error(String(err))
1324
- });
1325
- setActiveSub(prevSub);
1326
- return;
1327
- } finally {
1328
- stack.pop();
1329
- }
1330
- const queryCall = resolveQueryCall(result, captureSlot);
1331
- if (skipFirst) {
1332
- skipFirst = false;
1333
- if (result instanceof Promise) result.catch(() => {});
1334
- return;
1335
- }
1336
- if (queryCall) {
1337
- runBrandedQuery(queryCall, currentAc.signal, env);
1338
- return;
1339
- }
1340
- if (!(result instanceof Promise)) {
1341
- const prevSub = setActiveSub(void 0);
1342
- env({
1343
- loading: false,
1344
- value: result,
1345
- error: void 0
1346
- });
1347
- setActiveSub(prevSub);
1348
- return;
1349
- }
1350
- const prevSub = setActiveSub(void 0);
1351
- const prevVal = env();
1352
- env({
1353
- loading: true,
1354
- value: prevVal.value,
1355
- error: void 0
1356
- });
1357
- setActiveSub(prevSub);
1358
- result.then((value) => {
1359
- if (currentAc.signal.aborted) return;
1360
- if (isQueryCall(value)) {
1361
- runBrandedQuery(value, currentAc.signal, env);
1362
- return;
1363
- }
1364
- const latePending = globalThis[ILHA_QUERY_PENDING];
1365
- globalThis[ILHA_QUERY_PENDING] = void 0;
1366
- if (latePending) {
1367
- runBrandedQuery(latePending, currentAc.signal, env);
1368
- return;
1369
- }
1370
- env({
1371
- loading: false,
1372
- value,
1373
- error: void 0
1374
- });
1375
- }).catch((err) => {
1376
- if (currentAc.signal.aborted) return;
1377
- env({
1378
- loading: false,
1379
- value: void 0,
1380
- error: err instanceof Error ? err : new Error(String(err))
1381
- });
1382
- });
1383
- });
1384
- stops.push(() => {
1385
- stopEffect();
1386
- ac.abort();
1387
- });
1388
- }
1389
- return () => stops.forEach((s) => s());
1390
- };
1391
- return {
1392
- proxy,
1393
- setup
1394
- };
1395
- }
1396
- const BIND_SENTINEL_ATTR = "data-ilha-bind";
1397
- const EVENT_SENTINEL_ATTR = "data-ilha-on";
1398
- function parseBindSentinel(value) {
1399
- const out = [];
1400
- for (const part of value.split(",")) {
1401
- const [kind, idx] = part.split(":");
1402
- if (!kind || !idx) continue;
1403
- const i = Number(idx);
1404
- if (!Number.isInteger(i) || i < 0) continue;
1405
- out.push({
1406
- kind,
1407
- index: i
1408
- });
1409
- }
1410
- return out;
1411
- }
1412
- function resolveBindOps(el, kind) {
1413
- const input = el;
1414
- switch (kind) {
1415
- case "value": return {
1416
- event: el.tagName === "SELECT" ? "change" : "input",
1417
- read: (el) => el.value,
1418
- write: (el, v) => el.value = v == null ? "" : String(v)
1419
- };
1420
- case "valueAsNumber": return {
1421
- event: "input",
1422
- read: (el) => {
1423
- const n = el.valueAsNumber;
1424
- return Number.isNaN(n) ? null : n;
1425
- },
1426
- write: (el, v) => el.value = v == null || Number.isNaN(v) ? "" : String(v)
1427
- };
1428
- case "valueAsDate": return {
1429
- event: "input",
1430
- read: (el) => el.valueAsDate,
1431
- write: (el, v) => {
1432
- el.value = formatDateForInput(v);
1433
- }
1434
- };
1435
- case "checked": return {
1436
- event: "change",
1437
- read: (el) => el.checked,
1438
- write: (el, v) => el.checked = Boolean(v)
1439
- };
1440
- case "files": return {
1441
- event: "change",
1442
- read: (el) => el.files,
1443
- write: () => {}
1444
- };
1445
- case "open": return {
1446
- event: "toggle",
1447
- read: (el) => el.open,
1448
- write: (el, v) => el.open = Boolean(v)
1449
- };
1450
- case "this": return {
1451
- event: null,
1452
- read: () => void 0,
1453
- write: () => {}
1454
- };
1455
- case "group": {
1456
- const isCheckbox = input.type === "checkbox";
1457
- return {
1458
- event: "change",
1459
- read: (el) => {
1460
- const i = el;
1461
- if (isCheckbox) return {
1462
- __ilhaGroup: true,
1463
- value: i.value,
1464
- checked: i.checked
1465
- };
1466
- return i.checked ? i.value : void 0;
1467
- },
1468
- write: (el, v) => {
1469
- const i = el;
1470
- if (isCheckbox) i.checked = (Array.isArray(v) ? v.map(String) : []).includes(i.value);
1471
- else i.checked = v != null && String(v) === i.value;
1472
- }
1473
- };
1458
+ return i.checked ? i.value : void 0;
1459
+ },
1460
+ write: (el, v) => {
1461
+ const i = el;
1462
+ if (isCheckbox) i.checked = (Array.isArray(v) ? v.map(String) : []).includes(i.value);
1463
+ else i.checked = v != null && String(v) === i.value;
1464
+ }
1465
+ };
1474
1466
  }
1475
1467
  }
1476
1468
  }
@@ -1520,7 +1512,7 @@ function applyJsxEvents({ host, records, reportError, unmountSignal, onceFired }
1520
1512
  invocationController?.abort();
1521
1513
  invocationController = new AbortController();
1522
1514
  }
1523
- const signal = invocationController ? anySignal([listenerController.signal, invocationController.signal]) : listenerController.signal;
1515
+ const signal = invocationController ? AbortSignal.any([listenerController.signal, invocationController.signal]) : listenerController.signal;
1524
1516
  let result;
1525
1517
  startBatch();
1526
1518
  try {
@@ -1619,47 +1611,6 @@ function applyTemplateBindings(host, binds, phase = "all") {
1619
1611
  }
1620
1612
  return () => cleanups.forEach((c) => c());
1621
1613
  }
1622
- function parseOnArgs(selectorOrCombined) {
1623
- const atIdx = selectorOrCombined.lastIndexOf("@");
1624
- const selector = atIdx === -1 ? "" : selectorOrCombined.slice(0, atIdx);
1625
- const parts = (atIdx === -1 ? selectorOrCombined : selectorOrCombined.slice(atIdx + 1)).split(":");
1626
- const eventType = parts[0];
1627
- const modifiers = new Set(parts.slice(1));
1628
- return {
1629
- selector,
1630
- eventType,
1631
- options: {
1632
- once: modifiers.has("once"),
1633
- capture: modifiers.has("capture"),
1634
- passive: modifiers.has("passive")
1635
- },
1636
- abortable: modifiers.has("abortable")
1637
- };
1638
- }
1639
- /**
1640
- * Combine multiple AbortSignals into one that aborts when any input aborts.
1641
- * Uses native AbortSignal.any when available, falls back to a manual chain
1642
- * otherwise (for older runtimes).
1643
- */
1644
- function anySignal(signals) {
1645
- if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
1646
- const controller = new AbortController();
1647
- const cleanups = [];
1648
- for (const s of signals) {
1649
- if (s.aborted) {
1650
- controller.abort(s.reason);
1651
- cleanups.forEach((c) => c());
1652
- return controller.signal;
1653
- }
1654
- const handler = () => controller.abort(s.reason);
1655
- s.addEventListener("abort", handler, { once: true });
1656
- cleanups.push(() => s.removeEventListener("abort", handler));
1657
- }
1658
- if (!controller.signal.aborted) controller.signal.addEventListener("abort", () => {
1659
- cleanups.forEach((c) => c());
1660
- });
1661
- return controller.signal;
1662
- }
1663
1614
  const globalErrorHandlers = /* @__PURE__ */ new Set();
1664
1615
  function reportToGlobal(error, source) {
1665
1616
  if (globalErrorHandlers.size === 0) return false;
@@ -1672,9 +1623,7 @@ function reportToGlobal(error, source) {
1672
1623
  }
1673
1624
  /**
1674
1625
  * Register a global error handler invoked when any island reports an error
1675
- * (from .on, .effect, .onMount, or transitions) and has no local .onError()
1676
- * handler. Returns an unsubscribe function. Islands with their own .onError()
1677
- * are handled locally and do not reach the global sink.
1626
+ * and has no local onError() handler. Returns an unsubscribe function.
1678
1627
  */
1679
1628
  function onUncaughtError(fn) {
1680
1629
  globalErrorHandlers.add(fn);
@@ -1682,1313 +1631,1256 @@ function onUncaughtError(fn) {
1682
1631
  globalErrorHandlers.delete(fn);
1683
1632
  };
1684
1633
  }
1685
- var IlhaBuilder = class IlhaBuilder {
1686
- _cfg;
1687
- constructor(cfg) {
1688
- this._cfg = cfg;
1689
- }
1690
- input(initialOrSchema) {
1691
- let schema = null;
1692
- let defaultInput = null;
1693
- if (initialOrSchema !== void 0) if (isStandardSchema(initialOrSchema)) schema = initialOrSchema;
1694
- else defaultInput = initialOrSchema;
1695
- return new IlhaBuilder({
1696
- schema,
1697
- defaultInput,
1698
- states: [],
1699
- streams: [],
1700
- deriveds: [],
1701
- actions: [],
1702
- ons: [],
1703
- effects: [],
1704
- onMounts: [],
1705
- onErrors: [],
1706
- transition: null,
1707
- css: null,
1708
- as: null
1709
- });
1710
- }
1711
- as(tag) {
1712
- return new IlhaBuilder({
1713
- ...this._cfg,
1714
- as: assertValidSlotTagName(tag)
1715
- });
1716
- }
1717
- state(key, init) {
1718
- const cfg = this._cfg;
1719
- return new IlhaBuilder({
1720
- ...cfg,
1721
- states: [...cfg.states, {
1722
- key,
1723
- init
1724
- }]
1725
- });
1726
- }
1727
- stream(key, fn) {
1728
- const cfg = this._cfg;
1729
- const states = cfg.states.some((entry) => entry.key === key) ? cfg.states : [...cfg.states, {
1730
- key,
1731
- init: void 0
1732
- }];
1733
- return new IlhaBuilder({
1734
- ...cfg,
1735
- states,
1736
- streams: [...cfg.streams.filter((entry) => entry.key !== key), {
1737
- key,
1738
- fn
1739
- }]
1740
- });
1741
- }
1742
- derived(key, fn) {
1743
- const cfg = this._cfg;
1744
- return new IlhaBuilder({
1745
- ...cfg,
1746
- deriveds: [...cfg.deriveds, {
1747
- key,
1748
- fn
1749
- }]
1750
- });
1751
- }
1752
- action(key, fn) {
1753
- return new IlhaBuilder({
1754
- ...this._cfg,
1755
- actions: [...this._cfg.actions, {
1756
- key,
1757
- fn
1758
- }]
1759
- });
1760
- }
1761
- on(selectorOrCombined, handler) {
1762
- const parsed = parseOnArgs(selectorOrCombined);
1763
- return new IlhaBuilder({
1764
- ...this._cfg,
1765
- ons: [...this._cfg.ons, {
1766
- selector: parsed.selector,
1767
- event: parsed.eventType,
1768
- options: parsed.options,
1769
- abortable: parsed.abortable,
1770
- handler
1771
- }]
1772
- });
1773
- }
1774
- effect(fn) {
1775
- return new IlhaBuilder({
1776
- ...this._cfg,
1777
- effects: [...this._cfg.effects, { fn }]
1778
- });
1779
- }
1780
- onMount(fn) {
1781
- return new IlhaBuilder({
1782
- ...this._cfg,
1783
- onMounts: [...this._cfg.onMounts, { fn }]
1784
- });
1785
- }
1786
- onError(fn) {
1787
- return new IlhaBuilder({
1788
- ...this._cfg,
1789
- onErrors: [...this._cfg.onErrors, { fn }]
1790
- });
1791
- }
1792
- transition(opts) {
1793
- return new IlhaBuilder({
1794
- ...this._cfg,
1795
- transition: opts
1796
- });
1797
- }
1798
- css(strings, ...values) {
1799
- let source;
1800
- if (typeof strings === "string") source = strings;
1801
- else {
1802
- let result = "";
1803
- for (let i = 0; i < strings.length; i++) {
1804
- result += strings[i];
1805
- if (i < values.length) result += String(values[i]);
1806
- }
1807
- source = result;
1808
- }
1809
- return new IlhaBuilder({
1810
- ...this._cfg,
1811
- css: source
1812
- });
1634
+ const FRAME_STACK = Symbol.for("ilha.primitiveFrameStack");
1635
+ function frameStack() {
1636
+ const g = globalThis;
1637
+ return g[FRAME_STACK] ??= [];
1638
+ }
1639
+ function currentFrame() {
1640
+ const stack = frameStack();
1641
+ return stack[stack.length - 1];
1642
+ }
1643
+ function withFrame(frame, fn) {
1644
+ frameStack().push(frame);
1645
+ try {
1646
+ return fn();
1647
+ } finally {
1648
+ frameStack().pop();
1813
1649
  }
1814
- render(fn) {
1815
- const { schema, defaultInput, states, streams, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1816
- const stylePrefix = cssSource == null ? "" : buildScopedStyle(cssSource);
1817
- const configuredSlotTag = slotAs ?? "div";
1818
- function resolveInput(props) {
1819
- const merged = {
1820
- ...defaultInput ?? {},
1821
- ...props ?? {}
1822
- };
1823
- if (!schema) return merged;
1824
- return validateSchema(schema, merged);
1825
- }
1826
- function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
1827
- const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
1650
+ }
1651
+ function freshFrame(options) {
1652
+ return {
1653
+ slots: [],
1654
+ cursor: 0,
1655
+ creating: true,
1656
+ ssr: options?.ssr ?? false,
1657
+ instance: options?.instance ?? null,
1658
+ stateIndex: 0,
1659
+ derivedIndex: 0,
1660
+ actionIndex: 0,
1661
+ pendingDerived: options?.ssr ? [] : null,
1662
+ actionManifest: options?.ssr ? /* @__PURE__ */ new Map() : null
1663
+ };
1664
+ }
1665
+ const PRIMITIVE_NAMES = {
1666
+ state: "state",
1667
+ derived: "derived",
1668
+ action: "action",
1669
+ effect: "effect",
1670
+ once: "effect.once",
1671
+ error: "onError"
1672
+ };
1673
+ /** Reserve the next slot for a primitive call. Throws outside an island
1674
+ * render; warns in dev on hook count/kind drift during re-renders. */
1675
+ function acquireSlot(kind) {
1676
+ const frame = currentFrame();
1677
+ 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.`);
1678
+ const index = frame.cursor++;
1679
+ if (frame.creating) return void 0;
1680
+ const existing = frame.slots[index];
1681
+ return existing && existing.kind === kind ? existing : void 0;
1682
+ }
1683
+ /**
1684
+ * Declare island-local reactive state at this call position. The initializer
1685
+ * applies only when the instance is created — later renders reuse the same
1686
+ * underlying signal, so prop-driven initializers never reset user state.
1687
+ *
1688
+ * A function argument is treated as a lazy initializer:
1689
+ * const count = state(() => expensiveInitialValue());
1690
+ * To store a function VALUE, return it from the updater wrapper on write:
1691
+ * setCallback(() => nextCallback);
1692
+ */
1693
+ function state(init) {
1694
+ const frame = currentFrame();
1695
+ const existing = acquireSlot("state");
1696
+ if (existing) return existing.acc;
1697
+ const f = frame;
1698
+ if (!f.creating) return ilhaSignal(void 0);
1699
+ const index = f.stateIndex++;
1700
+ const snap = f.ssr ? void 0 : f.instance?.stateSnapshot?.s?.[index];
1701
+ const initial = snap === void 0 ? typeof init === "function" ? init() : init : snap;
1702
+ const s = signal(initial);
1703
+ const acc = markSignalAccessor((...args) => {
1704
+ if (args.length === 0) return s();
1705
+ const next = resolveSignalSetter(() => s(), args[0]);
1706
+ s(next);
1707
+ });
1708
+ f.slots[f.cursor - 1] = {
1709
+ kind: "state",
1710
+ acc
1711
+ };
1712
+ return acc;
1713
+ }
1714
+ /** True for async iterables (incl. async generators). */
1715
+ function isAsyncIterable(value) {
1716
+ return value !== null && typeof value[Symbol.asyncIterator] === "function";
1717
+ }
1718
+ /**
1719
+ * Declare derived state at this call position: synchronous computations,
1720
+ * Promises, or async generators. Reads track their dependencies; async work
1721
+ * races latest-run-wins and aborts stale runs through `ctx.signal`.
1722
+ */
1723
+ function derived(fn) {
1724
+ const frame = currentFrame();
1725
+ const existing = acquireSlot("derived");
1726
+ const f = frame;
1727
+ if (existing) {
1728
+ const slot = existing;
1729
+ slot.fn = fn;
1730
+ if (f.ssr && !f.creating && !slot.ssrAsync) {
1731
+ const ac = new AbortController();
1732
+ const prevSub = setActiveSub(void 0);
1828
1733
  try {
1829
- const html = unwrapHtml(render());
1830
- const slots = ctx.slots;
1831
- const binds = ctx.binds;
1832
- const events = ctx.events;
1833
- if (ctx.pending && ctx.pending.size > 0) {
1834
- const pending = ctx.pending;
1835
- return (async () => {
1836
- return {
1837
- html: await resolveAsyncChildren(html, pending),
1838
- slots,
1839
- binds,
1840
- events
1841
- };
1842
- })();
1843
- }
1844
- return {
1845
- html,
1846
- slots,
1847
- binds,
1848
- events
1849
- };
1850
- } finally {
1851
- popRenderCtx();
1852
- }
1853
- }
1854
- function buildPlainState(input) {
1855
- const state = {};
1856
- for (const entry of states) {
1857
- const value = typeof entry.init === "function" ? entry.init(input) : entry.init;
1858
- const accessor = markSignalAccessor((...args) => {
1859
- if (args.length === 0) return value;
1734
+ const result = slot.fn({ signal: ac.signal });
1735
+ if (!(result instanceof Promise) && !isAsyncIterable(result)) slot.env({
1736
+ loading: false,
1737
+ value: result,
1738
+ error: void 0
1860
1739
  });
1861
- state[entry.key] = accessor;
1862
- }
1863
- return state;
1864
- }
1865
- function buildSignalState(input, snapshot) {
1866
- const state = {};
1867
- for (const entry of states) {
1868
- const initial = snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init;
1869
- const s = signal(initial);
1870
- const accessor = markSignalAccessor((...args) => {
1871
- if (args.length === 0) return s();
1872
- s(resolveSignalSetter(() => s(), args[0]));
1740
+ } catch (err) {
1741
+ slot.env({
1742
+ loading: false,
1743
+ value: void 0,
1744
+ error: err instanceof Error ? err : new Error(String(err))
1873
1745
  });
1874
- state[entry.key] = accessor;
1746
+ } finally {
1747
+ setActiveSub(prevSub);
1875
1748
  }
1876
- return state;
1877
1749
  }
1878
- function buildSsrActions() {
1879
- const out = {};
1880
- const lookup = /* @__PURE__ */ new Map();
1881
- for (const entry of actions) {
1882
- const invoke = (...callArgs) => {
1883
- const frame = captureStack[captureStack.length - 1];
1884
- if (frame) {
1885
- if (!frame.some((r) => r.k === entry.key)) frame.push({
1886
- k: entry.key,
1887
- a: callArgs
1888
- });
1889
- return;
1890
- }
1891
- };
1892
- invoke[SSR_ACTION_STUB] = true;
1893
- Object.defineProperties(invoke, {
1894
- pending: {
1895
- get: () => false,
1896
- enumerable: true
1897
- },
1898
- data: {
1899
- get: () => void 0,
1900
- enumerable: true
1901
- },
1902
- error: {
1903
- get: () => void 0,
1904
- enumerable: true
1905
- }
1906
- });
1907
- out[entry.key] = invoke;
1908
- lookup.set(invoke, entry.key);
1909
- }
1910
- return {
1911
- action: out,
1912
- lookup
1913
- };
1750
+ return slot.acc;
1751
+ }
1752
+ if (!f.creating) return defaultDerivedAccessor();
1753
+ const index = f.derivedIndex++;
1754
+ const env = signal({
1755
+ loading: false,
1756
+ value: void 0,
1757
+ error: void 0
1758
+ });
1759
+ let fromSnapshot = false;
1760
+ if (!f.ssr) {
1761
+ const snap = f.instance?.stateSnapshot?.d?.[index];
1762
+ if (snap) {
1763
+ env({ ...snap });
1764
+ fromSnapshot = true;
1914
1765
  }
1915
- /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1916
- function createSsrOnMountHost() {
1917
- if (typeof document !== "undefined") return document.createElement("div");
1918
- const stub = {
1919
- matches: () => false,
1920
- querySelector: () => null,
1921
- querySelectorAll: () => [],
1922
- setAttribute: () => {},
1923
- getAttribute: () => null,
1924
- removeAttribute: () => {},
1925
- appendChild: () => stub
1926
- };
1927
- return stub;
1766
+ }
1767
+ const accessor = createDerivedAccessor(() => env(), (value) => {
1768
+ const prevSub = setActiveSub(void 0);
1769
+ try {
1770
+ env({
1771
+ loading: false,
1772
+ value,
1773
+ error: void 0
1774
+ });
1775
+ } finally {
1776
+ setActiveSub(prevSub);
1928
1777
  }
1929
- /** Run .onMount() before top-level SSR only (seed module/external state from `input`). */
1930
- function runOnMountForRender({ input, state, derived, action }) {
1931
- if (onMounts.length === 0) return;
1932
- if (currentRenderCtx()) return;
1933
- const host = createSsrOnMountHost();
1934
- const controller = new AbortController();
1935
- const ssrCleanups = [];
1936
- for (const entry of onMounts) {
1937
- const prevSub = setActiveSub(void 0);
1938
- let userCleanup;
1939
- try {
1940
- userCleanup = entry.fn({
1941
- state,
1942
- derived,
1943
- action,
1944
- input,
1945
- host,
1946
- hydrated: false,
1947
- signal: controller.signal
1778
+ });
1779
+ const slot = {
1780
+ kind: "derived",
1781
+ env,
1782
+ acc: accessor,
1783
+ fn,
1784
+ ssrAsync: false,
1785
+ fromSnapshot
1786
+ };
1787
+ if (f.ssr) {
1788
+ const ac = new AbortController();
1789
+ const prevSub = setActiveSub(void 0);
1790
+ let result;
1791
+ let threw = false;
1792
+ let syncError;
1793
+ try {
1794
+ result = slot.fn({ signal: ac.signal });
1795
+ } catch (err) {
1796
+ threw = true;
1797
+ syncError = err;
1798
+ } finally {
1799
+ setActiveSub(prevSub);
1800
+ }
1801
+ let pull = null;
1802
+ if (threw) env({
1803
+ loading: false,
1804
+ value: void 0,
1805
+ error: syncError instanceof Error ? syncError : new Error(String(syncError))
1806
+ });
1807
+ else if (result instanceof Promise) {
1808
+ env({
1809
+ loading: true,
1810
+ value: void 0,
1811
+ error: void 0
1812
+ });
1813
+ pull = result.then((value) => env({
1814
+ loading: false,
1815
+ value,
1816
+ error: void 0
1817
+ }), (err) => env({
1818
+ loading: false,
1819
+ value: void 0,
1820
+ error: err instanceof Error ? err : new Error(String(err))
1821
+ }));
1822
+ result.catch(() => {});
1823
+ } else if (isAsyncIterable(result)) {
1824
+ env({
1825
+ loading: true,
1826
+ value: void 0,
1827
+ error: void 0
1828
+ });
1829
+ const iterable = result;
1830
+ pull = (async () => {
1831
+ const it = iterable[Symbol.asyncIterator]();
1832
+ try {
1833
+ const first = await it.next();
1834
+ env({
1835
+ loading: false,
1836
+ value: first.done ? void 0 : first.value,
1837
+ error: void 0
1948
1838
  });
1949
- } catch (err) {
1950
- const error = err instanceof Error ? err : new Error(String(err));
1951
- if (onErrors.length === 0) {
1952
- if (!reportToGlobal(error, "mount")) console.error(error);
1953
- } else for (const oe of onErrors) try {
1954
- oe.fn({
1955
- error,
1956
- source: "mount",
1957
- state,
1958
- derived,
1959
- action,
1960
- input,
1961
- host
1962
- });
1963
- } catch (handlerErr) {
1964
- console.error(handlerErr);
1965
- }
1966
1839
  } finally {
1967
- setActiveSub(prevSub);
1968
- }
1969
- if (typeof userCleanup === "function") ssrCleanups.push(userCleanup);
1970
- }
1971
- controller.abort();
1972
- for (const teardown of ssrCleanups.reverse()) try {
1973
- teardown();
1974
- } catch (err) {
1975
- const error = err instanceof Error ? err : new Error(String(err));
1976
- if (onErrors.length === 0) {
1977
- if (!reportToGlobal(error, "mount")) console.error(error);
1840
+ await it.return?.(void 0);
1978
1841
  }
1842
+ })();
1843
+ } else env({
1844
+ loading: false,
1845
+ value: result,
1846
+ error: void 0
1847
+ });
1848
+ slot.ssrAsync = pull !== null;
1849
+ if (pull) f.pendingDerived.push({
1850
+ index,
1851
+ promise: pull.catch(() => {})
1852
+ });
1853
+ }
1854
+ f.slots[f.cursor - 1] = slot;
1855
+ return accessor;
1856
+ }
1857
+ /**
1858
+ * Declare a reactive operation at this call position. Use plain functions
1859
+ * for ordinary operations; action() adds pending/data/error tracking,
1860
+ * concurrent-invocation bookkeeping, and lifecycle cancellation.
1861
+ */
1862
+ function action(fn) {
1863
+ const frame = currentFrame();
1864
+ const existing = acquireSlot("action");
1865
+ const f = frame;
1866
+ if (existing) return existing.acc;
1867
+ if (!f.creating) {
1868
+ const noop = (() => {});
1869
+ Object.defineProperties(noop, {
1870
+ pending: { get: () => false },
1871
+ data: { get: () => void 0 },
1872
+ error: { get: () => void 0 }
1873
+ });
1874
+ attachWithArgs(noop);
1875
+ return noop;
1876
+ }
1877
+ const manifestId = `a${f.actionIndex++}`;
1878
+ if (f.ssr) {
1879
+ const invoke = (..._callArgs) => {};
1880
+ invoke[SSR_ACTION_STUB] = true;
1881
+ invoke[ACTION_MANIFEST_ID] = manifestId;
1882
+ Object.defineProperties(invoke, {
1883
+ pending: {
1884
+ get: () => false,
1885
+ enumerable: true
1886
+ },
1887
+ data: {
1888
+ get: () => void 0,
1889
+ enumerable: true
1890
+ },
1891
+ error: {
1892
+ get: () => void 0,
1893
+ enumerable: true
1979
1894
  }
1895
+ });
1896
+ f.actionManifest.set(invoke, manifestId);
1897
+ f.slots[f.cursor - 1] = {
1898
+ kind: "action",
1899
+ acc: invoke,
1900
+ fn
1901
+ };
1902
+ const ssrAccessor = invoke;
1903
+ attachWithArgs(ssrAccessor);
1904
+ return ssrAccessor;
1905
+ }
1906
+ const instance = f.instance;
1907
+ const envelope = signal({
1908
+ pending: 0,
1909
+ data: void 0,
1910
+ error: void 0
1911
+ });
1912
+ const readEnvelope = () => untrack(() => envelope());
1913
+ let latestRun = 0;
1914
+ const invoke = (payload) => {
1915
+ if (currentRenderCtx()) return;
1916
+ if (instance.disposed) return;
1917
+ const run = ++latestRun;
1918
+ const runSignal = instance.unmountController.signal;
1919
+ let result;
1920
+ let isAsync = false;
1921
+ startBatch();
1922
+ try {
1923
+ result = slot.fn(payload, { signal: runSignal });
1924
+ isAsync = result != null && typeof result.then === "function";
1925
+ const current = readEnvelope();
1926
+ envelope(isAsync ? {
1927
+ pending: current.pending + 1,
1928
+ data: current.data,
1929
+ error: void 0
1930
+ } : {
1931
+ pending: current.pending,
1932
+ data: result,
1933
+ error: void 0
1934
+ });
1935
+ } catch (error) {
1936
+ const normalized = error instanceof Error ? error : new Error(String(error));
1937
+ envelope({
1938
+ pending: readEnvelope().pending,
1939
+ data: void 0,
1940
+ error: normalized
1941
+ });
1942
+ instance.reportError(normalized, "action");
1943
+ return;
1944
+ } finally {
1945
+ endBatch();
1980
1946
  }
1981
- function renderToString(props, sync = false, streamOut, eventsOut, stateOverride, forceAsyncChildren = false) {
1982
- const input = resolveInput(props);
1983
- const state = buildSignalState(input, stateOverride);
1984
- const { action, lookup: actionLookup } = buildSsrActions();
1985
- const recordEventsManifest = (events) => {
1986
- if (!eventsOut || events.length === 0) return;
1987
- events.forEach((record, index) => {
1988
- if (record.capture) {
1989
- eventsOut.set(`${record.type}:${index}`, record.capture);
1990
- return;
1991
- }
1992
- const key = actionLookup.get(record.handler);
1993
- if (key) eventsOut.set(`${record.type}:${index}`, key);
1947
+ if (!isAsync) return;
1948
+ Promise.resolve(result).then((data) => {
1949
+ if (instance.disposed || runSignal.aborted || run !== latestRun) return;
1950
+ const value = readEnvelope();
1951
+ envelope({
1952
+ pending: value.pending,
1953
+ data,
1954
+ error: void 0
1955
+ });
1956
+ }).catch((error) => {
1957
+ if (instance.disposed || runSignal.aborted || error?.name === "AbortError") return;
1958
+ const normalized = error instanceof Error ? error : new Error(String(error));
1959
+ if (run === latestRun) {
1960
+ const value = readEnvelope();
1961
+ envelope({
1962
+ pending: value.pending,
1963
+ data: void 0,
1964
+ error: normalized
1994
1965
  });
1995
- };
1996
- const pullStreamInitials = async (streamOut) => {
1997
- for (const entry of streams) {
1998
- const controller = new AbortController();
1999
- try {
2000
- const gen = await entry.fn({
2001
- input,
2002
- signal: controller.signal
2003
- });
2004
- const first = await gen.next();
2005
- controller.abort();
2006
- await gen.return?.(void 0);
2007
- if (first.done) continue;
2008
- if (streamOut) streamOut[entry.key] = first.value;
2009
- const prevSub = setActiveSub(void 0);
2010
- try {
2011
- const setter = state[entry.key];
2012
- setter?.(first.value);
2013
- } finally {
2014
- setActiveSub(prevSub);
2015
- }
2016
- } catch {}
2017
- }
2018
- };
2019
- const computeDerivedResults = () => deriveds.map((entry) => {
2020
- const prevSub = setActiveSub(void 0);
1966
+ }
1967
+ instance.reportError(normalized, "action");
1968
+ }).finally(() => {
1969
+ if (instance.disposed) return;
1970
+ const value = readEnvelope();
1971
+ envelope({
1972
+ ...value,
1973
+ pending: Math.max(0, value.pending - 1)
1974
+ });
1975
+ });
1976
+ };
1977
+ const slot = {
1978
+ kind: "action",
1979
+ acc: invoke,
1980
+ fn
1981
+ };
1982
+ Object.defineProperties(invoke, {
1983
+ pending: {
1984
+ get: () => envelope().pending > 0,
1985
+ enumerable: true
1986
+ },
1987
+ data: {
1988
+ get: () => envelope().data,
1989
+ enumerable: true
1990
+ },
1991
+ error: {
1992
+ get: () => envelope().error,
1993
+ enumerable: true
1994
+ }
1995
+ });
1996
+ f.slots[f.cursor - 1] = slot;
1997
+ instance.actionIds.set(slot.acc, manifestId);
1998
+ const clientAccessor = invoke;
1999
+ clientAccessor[ACTION_MANIFEST_ID] = manifestId;
2000
+ attachWithArgs(clientAccessor);
2001
+ return clientAccessor;
2002
+ }
2003
+ /**
2004
+ * Reactive side effect. Inside an island render, registers an ordered effect
2005
+ * slot that tracks signals read in the body, reruns on change, cleans up
2006
+ * before rerun and on unmount, and is client-only. Outside an island render,
2007
+ * behaves as the standalone reactive effect and returns a stop function.
2008
+ *
2009
+ * @returns Inside an island render, `void` (an effect slot is registered and
2010
+ * cleaned up automatically). At module scope, a stop function that disposes
2011
+ * the effect and runs its final cleanup. The union return type is deliberate:
2012
+ * the same call shape cannot be discriminated at the call site.
2013
+ */
2014
+ const effect = Object.assign((fn) => {
2015
+ const frame = currentFrame();
2016
+ if (!frame) {
2017
+ let cleanup;
2018
+ let controller = null;
2019
+ const runCleanup = () => {
2020
+ if (typeof cleanup === "function") {
2021
2021
  try {
2022
- return {
2023
- key: entry.key,
2024
- result: entry.fn({
2025
- state,
2026
- input,
2027
- signal: new AbortController().signal
2028
- })
2029
- };
2022
+ cleanup();
2030
2023
  } catch (err) {
2031
- return {
2032
- key: entry.key,
2033
- result: Promise.reject(err)
2034
- };
2035
- } finally {
2036
- setActiveSub(prevSub);
2024
+ console.error(err);
2037
2025
  }
2038
- });
2039
- if (!sync && (streams.length > 0 || forceAsyncChildren)) return pullStreamInitials(streamOut).then(() => renderToStringTail());
2040
- return renderToStringTail();
2041
- function renderToStringTail() {
2042
- const results = computeDerivedResults();
2043
- if (!(forceAsyncChildren || results.some((r) => r.result instanceof Promise)) || sync) {
2044
- const envelopes = {};
2045
- for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
2046
- loading: true,
2047
- value: void 0,
2048
- error: void 0
2049
- };
2050
- else envelopes[r.key] = {
2051
- loading: false,
2052
- value: r.result,
2053
- error: void 0
2026
+ cleanup = void 0;
2027
+ }
2028
+ };
2029
+ const stop = ilhaEffect(() => {
2030
+ runCleanup();
2031
+ if (controller) controller.abort();
2032
+ controller = new AbortController();
2033
+ startBatch();
2034
+ try {
2035
+ cleanup = fn({ signal: controller.signal });
2036
+ } catch (err) {
2037
+ console.error(err);
2038
+ } finally {
2039
+ endBatch();
2040
+ }
2041
+ });
2042
+ return () => {
2043
+ stop();
2044
+ runCleanup();
2045
+ controller?.abort();
2046
+ };
2047
+ }
2048
+ const existing = acquireSlot("effect");
2049
+ if (existing) {
2050
+ existing.fn = fn;
2051
+ return;
2052
+ }
2053
+ if (!frame.creating) return;
2054
+ frame.slots[frame.cursor - 1] = {
2055
+ kind: "effect",
2056
+ fn
2057
+ };
2058
+ }, { once(fn) {
2059
+ if (acquireSlot("once")) return;
2060
+ const f = currentFrame();
2061
+ 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.");
2062
+ if (!f.creating) return;
2063
+ f.slots[f.cursor - 1] = {
2064
+ kind: "once",
2065
+ fn
2066
+ };
2067
+ } });
2068
+ /**
2069
+ * Register an error handler slot for this island. Handlers run in
2070
+ * declaration order; state, derived values, actions, and props should be
2071
+ * accessed through lexical closure rather than the context object.
2072
+ */
2073
+ function onError(fn) {
2074
+ if (!currentFrame()) throw new Error("[ilha] onError() called outside an island render. Register it while an island component is rendering.");
2075
+ const existing = acquireSlot("error");
2076
+ if (existing) {
2077
+ existing.fn = fn;
2078
+ return;
2079
+ }
2080
+ const f = currentFrame();
2081
+ if (!f || !f.creating) return;
2082
+ f.slots[f.cursor - 1] = {
2083
+ kind: "error",
2084
+ fn
2085
+ };
2086
+ }
2087
+ function parseStateSnapshot(raw) {
2088
+ const parsed = safeParseSnapshot(raw, STATE_ATTR);
2089
+ if (parsed === void 0) return void 0;
2090
+ const rec = parsed;
2091
+ if (rec["v"] !== 2) return;
2092
+ const out = { skipOnce: rec["_skipOnMount"] === true };
2093
+ if (Array.isArray(rec["s"])) {
2094
+ stripUnsafeKeys(rec["s"]);
2095
+ out.s = rec["s"];
2096
+ }
2097
+ if (Array.isArray(rec["d"])) {
2098
+ stripUnsafeKeys(rec["d"]);
2099
+ out.d = rec["d"].map((entry) => {
2100
+ const e = entry ?? {};
2101
+ const errorValue = e.error;
2102
+ return {
2103
+ loading: e.loading === true,
2104
+ value: e.value,
2105
+ error: errorValue == null ? void 0 : errorValue instanceof Error ? errorValue : new Error(String(errorValue))
2106
+ };
2107
+ });
2108
+ }
2109
+ return out;
2110
+ }
2111
+ const ilha = ((...args) => {
2112
+ let schema = null;
2113
+ let component;
2114
+ let options;
2115
+ const first = args[0];
2116
+ const second = args[1];
2117
+ if (isStandardSchema(first)) {
2118
+ schema = first;
2119
+ component = second;
2120
+ options = args[2];
2121
+ } else {
2122
+ component = first;
2123
+ options = second;
2124
+ }
2125
+ if (typeof component !== "function") throw new Error("[ilha] ilha() requires a component function.");
2126
+ const configuredSlotTag = options?.as === void 0 ? "div" : assertValidSlotTagName(options.as);
2127
+ function resolveInput(props) {
2128
+ const merged = { ...props };
2129
+ if (!schema) return merged;
2130
+ return validateSchema(schema, merged);
2131
+ }
2132
+ function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
2133
+ const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
2134
+ try {
2135
+ const html = unwrapHtml(render());
2136
+ const slots = ctx.slots;
2137
+ const binds = ctx.binds;
2138
+ const events = ctx.events;
2139
+ if (ctx.pending && ctx.pending.size > 0) {
2140
+ const pending = ctx.pending;
2141
+ return (async () => {
2142
+ return {
2143
+ html: await resolveAsyncChildren(html, pending),
2144
+ slots,
2145
+ binds,
2146
+ events
2054
2147
  };
2055
- const derived = buildDerivedAccessors(envelopes);
2056
- if (!currentRenderCtx()) runOnMountForRender({
2057
- input,
2058
- state,
2059
- derived,
2060
- action
2061
- });
2062
- const prevSub = setActiveSub(void 0);
2063
- try {
2064
- const { html, events } = renderWithCtx({
2065
- render: () => fn({
2066
- state,
2067
- derived,
2068
- action,
2069
- input
2070
- }),
2071
- manifest: eventsOut !== void 0
2072
- });
2073
- recordEventsManifest(events);
2074
- return stylePrefix + html;
2075
- } finally {
2076
- setActiveSub(prevSub);
2077
- }
2078
- }
2079
- return Promise.all(results.map(async (r) => {
2080
- try {
2081
- return {
2082
- key: r.key,
2083
- envelope: {
2084
- loading: false,
2085
- value: await Promise.resolve(r.result),
2086
- error: void 0
2087
- }
2088
- };
2089
- } catch (err) {
2090
- return {
2091
- key: r.key,
2092
- envelope: {
2093
- loading: false,
2094
- value: void 0,
2095
- error: err instanceof Error ? err : new Error(String(err))
2096
- }
2097
- };
2098
- }
2099
- })).then(async (resolved) => {
2100
- const envelopes = {};
2101
- for (const r of resolved) envelopes[r.key] = r.envelope;
2102
- const derived = buildDerivedAccessors(envelopes);
2103
- if (!currentRenderCtx()) runOnMountForRender({
2104
- input,
2105
- state,
2106
- derived,
2107
- action
2108
- });
2109
- const prevSub = setActiveSub(void 0);
2110
- try {
2111
- const { html, events } = await renderWithCtx({
2112
- render: () => fn({
2113
- state,
2114
- derived,
2115
- action,
2116
- input
2117
- }),
2118
- asyncChildren: true,
2119
- manifest: eventsOut !== void 0
2120
- });
2121
- recordEventsManifest(events);
2122
- return stylePrefix + html;
2123
- } finally {
2124
- setActiveSub(prevSub);
2125
- }
2126
- });
2148
+ })();
2127
2149
  }
2150
+ return {
2151
+ html,
2152
+ slots,
2153
+ binds,
2154
+ events
2155
+ };
2156
+ } finally {
2157
+ popRenderCtx();
2128
2158
  }
2129
- function mountIsland(host, props) {
2130
- return mountIslandInternal(host, props).unmount;
2159
+ }
2160
+ /** Invoke the component inside the given frame + render context. */
2161
+ function runInFrame(frame, props) {
2162
+ return () => withFrame(frame, () => component(props));
2163
+ }
2164
+ function recordEventsManifest(eventsOut, events, frame) {
2165
+ if (!eventsOut || events.length === 0) return;
2166
+ events.forEach((record, index) => {
2167
+ const id = frame.actionManifest?.get(record.handler) ?? actionManifestId(record.handler);
2168
+ if (!id) return;
2169
+ const args = actionBoundArgs(record.handler);
2170
+ eventsOut.set(`${record.type}:${index}`, Array.isArray(args) ? {
2171
+ k: id,
2172
+ a: args
2173
+ } : id);
2174
+ });
2175
+ }
2176
+ function renderToString(props, sync = false, eventsOut, forceAsyncChildren = false) {
2177
+ const input = resolveInput(props);
2178
+ const frame = freshFrame({ ssr: true });
2179
+ const first = forceAsyncChildren ? renderWithCtx({
2180
+ render: runInFrame(frame, input),
2181
+ asyncChildren: true,
2182
+ manifest: eventsOut !== void 0
2183
+ }) : renderWithCtx({
2184
+ render: runInFrame(frame, input),
2185
+ manifest: eventsOut !== void 0
2186
+ });
2187
+ const finish = (firstHtml) => firstHtml.html;
2188
+ const needsAsync = forceAsyncChildren || (frame.pendingDerived?.length ?? 0) > 0;
2189
+ if (first instanceof Promise) return first.then((out) => {
2190
+ recordEventsManifest(eventsOut, out.events, frame);
2191
+ return needsAsync ? awaitPending(out) : out.html;
2192
+ });
2193
+ recordEventsManifest(eventsOut, first.events, frame);
2194
+ if (!needsAsync || sync) {
2195
+ if (sync && (frame.pendingDerived?.length ?? 0) > 0 && __DEV__);
2196
+ return finish(first);
2131
2197
  }
2132
- function mountIslandInternal(host, props) {
2133
- if (props === void 0) {
2134
- const rawProps = host.getAttribute(PROPS_ATTR);
2135
- if (rawProps) {
2136
- const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
2137
- if (parsed !== void 0) props = reviveSlotProps(parsed);
2138
- }
2139
- }
2140
- const inputSignal = signal(resolveInput(props));
2141
- const input = new Proxy({}, {
2142
- get(_t, key) {
2143
- return inputSignal()[key];
2144
- },
2145
- has(_t, key) {
2146
- return key in inputSignal();
2147
- },
2148
- ownKeys() {
2149
- const prevSub = setActiveSub(void 0);
2150
- try {
2151
- return Reflect.ownKeys(inputSignal());
2152
- } finally {
2153
- setActiveSub(prevSub);
2154
- }
2155
- },
2156
- getOwnPropertyDescriptor(_t, key) {
2157
- const prevSub = setActiveSub(void 0);
2158
- try {
2159
- return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
2160
- } finally {
2161
- setActiveSub(prevSub);
2162
- }
2163
- }
2164
- });
2165
- let snapshotRaw;
2166
- const rawState = host.getAttribute(STATE_ATTR);
2167
- if (rawState) {
2168
- const parsed = safeParseSnapshot(rawState, STATE_ATTR);
2169
- if (parsed !== void 0) snapshotRaw = parsed;
2170
- }
2171
- const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount" && k !== "_streams")) : void 0;
2172
- const derivedSnapshotRaw = snapshotRaw?._derived;
2173
- let derivedSnapshot;
2174
- if (derivedSnapshotRaw) {
2175
- derivedSnapshot = {};
2176
- for (const [k, v] of Object.entries(derivedSnapshotRaw)) if (v.error && !(v.error instanceof Error)) derivedSnapshot[k] = {
2177
- ...v,
2178
- error: new Error(String(v.error))
2198
+ return awaitPending(first);
2199
+ function awaitPending(out) {
2200
+ const pendings = frame.pendingDerived ?? [];
2201
+ if (pendings.length === 0) return Promise.resolve(out.html);
2202
+ return Promise.all(pendings.map((p) => p.promise)).then(() => {
2203
+ frame.creating = false;
2204
+ frame.cursor = 0;
2205
+ const second = forceAsyncChildren ? renderWithCtx({
2206
+ render: runInFrame(frame, input),
2207
+ asyncChildren: true,
2208
+ manifest: eventsOut !== void 0
2209
+ }) : renderWithCtx({
2210
+ render: runInFrame(frame, input),
2211
+ manifest: eventsOut !== void 0
2212
+ });
2213
+ const done = (o) => {
2214
+ recordEventsManifest(eventsOut, o.events, frame);
2215
+ return o.html;
2179
2216
  };
2180
- else derivedSnapshot[k] = v;
2181
- }
2182
- const hydrated = snapshotRaw != null;
2183
- const snapshotHasIslandState = stateSnapshot != null && Object.keys(stateSnapshot).length > 0;
2184
- const shouldSkipOnMount = hydrated && snapshotRaw?.["_skipOnMount"] === true && snapshotHasIslandState;
2185
- const state = buildSignalState(input, stateSnapshot);
2186
- const cleanups = [];
2187
- const unmountController = new AbortController();
2188
- cleanups.push(() => unmountController.abort());
2189
- const { proxy: derived, setup: setupDerived } = createDerivedProxy({
2190
- entries: deriveds,
2191
- state,
2192
- input,
2193
- derivedSnapshot
2217
+ return second instanceof Promise ? second.then(done) : done(second);
2194
2218
  });
2195
- let stopDerived = () => {};
2196
- let action = {};
2197
- function reportError(err, source) {
2198
- const error = err instanceof Error ? err : new Error(String(err));
2199
- if (onErrors.length === 0) {
2200
- if (!reportToGlobal(error, source)) console.error(error);
2201
- return;
2219
+ }
2220
+ }
2221
+ function mountIsland(host, props) {
2222
+ return mountIslandInternal(host, props).unmount;
2223
+ }
2224
+ function mountIslandInternal(host, props) {
2225
+ if (props === void 0) {
2226
+ const rawProps = host.getAttribute(PROPS_ATTR);
2227
+ if (rawProps) {
2228
+ const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
2229
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
2230
+ }
2231
+ }
2232
+ const inputSignal = signal(resolveInput(props));
2233
+ const derivedPropsVersion = signal(0);
2234
+ const input = new Proxy({}, {
2235
+ get(_t, key) {
2236
+ return inputSignal()[key];
2237
+ },
2238
+ has(_t, key) {
2239
+ return key in inputSignal();
2240
+ },
2241
+ ownKeys() {
2242
+ const prevSub = setActiveSub(void 0);
2243
+ try {
2244
+ return Reflect.ownKeys(inputSignal());
2245
+ } finally {
2246
+ setActiveSub(prevSub);
2202
2247
  }
2203
- for (const entry of onErrors) try {
2204
- entry.fn({
2205
- error,
2206
- source,
2207
- state,
2208
- derived,
2209
- action,
2210
- input,
2211
- host
2212
- });
2213
- } catch (handlerErr) {
2214
- console.error(handlerErr);
2248
+ },
2249
+ getOwnPropertyDescriptor(_t, key) {
2250
+ const prevSub = setActiveSub(void 0);
2251
+ try {
2252
+ return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
2253
+ } finally {
2254
+ setActiveSub(prevSub);
2215
2255
  }
2216
2256
  }
2217
- const actionEntries = actions;
2218
- const actionObject = {};
2219
- let disposed = false;
2220
- cleanups.push(() => {
2221
- disposed = true;
2222
- });
2223
- for (const entry of actionEntries) {
2224
- const envelope = signal({
2225
- pending: 0,
2226
- data: void 0,
2227
- error: void 0
2228
- });
2229
- const readEnvelope = () => untrack(() => envelope());
2230
- let latestRun = 0;
2231
- const invoke = (props) => {
2232
- if (currentRenderCtx()) return;
2233
- if (disposed) return;
2234
- const run = ++latestRun;
2235
- const runSignal = unmountController.signal;
2236
- const ctx = {
2237
- state,
2238
- derived,
2239
- input,
2240
- host,
2241
- signal: runSignal
2242
- };
2243
- let result;
2244
- let isAsync = false;
2245
- startBatch();
2246
- try {
2247
- result = entry.fn(props, ctx);
2248
- isAsync = result != null && typeof result.then === "function";
2249
- const current = readEnvelope();
2250
- envelope(isAsync ? {
2251
- pending: current.pending + 1,
2252
- data: current.data,
2253
- error: void 0
2254
- } : {
2255
- pending: current.pending,
2256
- data: result,
2257
- error: void 0
2258
- });
2259
- } catch (error) {
2260
- const normalized = error instanceof Error ? error : new Error(String(error));
2261
- envelope({
2262
- pending: readEnvelope().pending,
2263
- data: void 0,
2264
- error: normalized
2265
- });
2266
- reportError(normalized, "action");
2267
- return;
2268
- } finally {
2269
- endBatch();
2270
- }
2271
- if (!isAsync) return;
2272
- Promise.resolve(result).then((data) => {
2273
- if (disposed || runSignal.aborted || run !== latestRun) return;
2274
- const value = readEnvelope();
2275
- envelope({
2276
- pending: value.pending,
2277
- data,
2278
- error: void 0
2279
- });
2280
- }).catch((error) => {
2281
- if (disposed || runSignal.aborted || error?.name === "AbortError") return;
2282
- const normalized = error instanceof Error ? error : new Error(String(error));
2283
- if (run === latestRun) {
2284
- const value = readEnvelope();
2285
- envelope({
2286
- pending: value.pending,
2287
- data: void 0,
2288
- error: normalized
2289
- });
2290
- }
2291
- reportError(normalized, "action");
2292
- }).finally(() => {
2293
- if (disposed) return;
2294
- const value = readEnvelope();
2295
- envelope({
2296
- ...value,
2297
- pending: Math.max(0, value.pending - 1)
2298
- });
2299
- });
2300
- };
2301
- Object.defineProperties(invoke, {
2302
- pending: {
2303
- get: () => envelope().pending > 0,
2304
- enumerable: true
2305
- },
2306
- data: {
2307
- get: () => envelope().data,
2308
- enumerable: true
2309
- },
2310
- error: {
2311
- get: () => envelope().error,
2312
- enumerable: true
2313
- }
2314
- });
2315
- actionObject[entry.key] = invoke;
2257
+ });
2258
+ const snapshotRaw = host.getAttribute(STATE_ATTR);
2259
+ const snapshot = snapshotRaw ? parseStateSnapshot(snapshotRaw) : void 0;
2260
+ const hydrated = snapshot != null;
2261
+ const shouldSkipOnce = snapshot?.skipOnce === true;
2262
+ const cleanups = [];
2263
+ const unmountController = new AbortController();
2264
+ cleanups.push(() => unmountController.abort());
2265
+ const errorSlots = [];
2266
+ const reportError = (err, source) => {
2267
+ const error = err instanceof Error ? err : new Error(String(err));
2268
+ if (errorSlots.length === 0) {
2269
+ if (!reportToGlobal(error, source)) console.error(error);
2270
+ return;
2316
2271
  }
2317
- action = actionObject;
2318
- if (transition?.enter) try {
2319
- const result = transition.enter(host);
2320
- if (result instanceof Promise) result.catch((err) => reportError(err, "transition"));
2321
- } catch (err) {
2322
- reportError(err, "transition");
2272
+ for (const slot of errorSlots) try {
2273
+ slot.fn({
2274
+ error,
2275
+ source,
2276
+ host
2277
+ });
2278
+ } catch (handlerErr) {
2279
+ console.error(handlerErr);
2323
2280
  }
2324
- const mountedSlots = /* @__PURE__ */ new Map();
2325
- const leavingSlots = /* @__PURE__ */ new Map();
2326
- function teardownMountedSlot(id, entry) {
2327
- mountedSlots.delete(id);
2328
- const token = Symbol();
2329
- const result = entry.unmount();
2330
- const remove = (stub) => {
2331
- const leaving = leavingSlots.get(id);
2332
- if (!leaving || leaving.token !== token) return;
2333
- leaving.el.remove();
2334
- stub?.remove();
2335
- leavingSlots.delete(id);
2336
- };
2337
- if (result instanceof Promise) {
2338
- entry.el.removeAttribute(SLOT_ATTR);
2339
- entry.el.setAttribute(LEAVING_ATTR, "");
2340
- leavingSlots.set(id, {
2341
- el: entry.el,
2342
- token
2343
- });
2344
- return result.finally(() => remove());
2345
- }
2346
- entry.el.remove();
2281
+ };
2282
+ const instance = {
2283
+ host,
2284
+ inputSignal,
2285
+ input,
2286
+ unmountController,
2287
+ cleanups,
2288
+ reportError,
2289
+ actionIds: /* @__PURE__ */ new Map(),
2290
+ stateSnapshot: snapshot ? {
2291
+ s: snapshot.s,
2292
+ d: snapshot.d
2293
+ } : void 0,
2294
+ hydrated,
2295
+ disposed: false,
2296
+ frame: null
2297
+ };
2298
+ cleanups.push(() => {
2299
+ instance.disposed = true;
2300
+ });
2301
+ const frame = freshFrame({
2302
+ ssr: false,
2303
+ instance
2304
+ });
2305
+ instance.frame = frame;
2306
+ const mountedSlots = /* @__PURE__ */ new Map();
2307
+ function teardownMountedSlot(id, entry) {
2308
+ mountedSlots.delete(id);
2309
+ entry.el.remove();
2310
+ entry.unmount();
2311
+ }
2312
+ function slotBelongsToHost(candidate) {
2313
+ let el = candidate.parentElement;
2314
+ while (el && el !== host) {
2315
+ if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
2316
+ el = el.parentElement;
2347
2317
  }
2348
- function slotBelongsToHost(candidate) {
2349
- let el = candidate.parentElement;
2350
- while (el && el !== host) {
2351
- if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
2352
- el = el.parentElement;
2353
- }
2354
- return el === host;
2318
+ return el === host;
2319
+ }
2320
+ function buildSlotIndex() {
2321
+ const index = /* @__PURE__ */ new Map();
2322
+ for (const candidate of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
2323
+ const id = candidate.getAttribute(SLOT_ATTR);
2324
+ if (id === null || index.has(id)) continue;
2325
+ if (slotBelongsToHost(candidate)) index.set(id, candidate);
2355
2326
  }
2356
- function buildSlotIndex() {
2357
- const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
2358
- const index = /* @__PURE__ */ new Map();
2359
- for (const candidate of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
2360
- if (leavingEls.has(candidate)) continue;
2361
- const id = candidate.getAttribute(SLOT_ATTR);
2362
- if (id === null || index.has(id)) continue;
2363
- if (slotBelongsToHost(candidate)) index.set(id, candidate);
2364
- }
2365
- return index;
2327
+ return index;
2328
+ }
2329
+ function pushUpdatedProps(nextSlots) {
2330
+ for (const [id, entry] of mountedSlots) {
2331
+ const next = nextSlots.get(id);
2332
+ if (next) entry.updateProps(next.props);
2366
2333
  }
2367
- /** Push live props from a fresh slot map into already-mounted children. */
2368
- function pushUpdatedProps(nextSlots) {
2369
- for (const [id, entry] of mountedSlots) {
2370
- const next = nextSlots.get(id);
2371
- if (next) entry.updateProps(next.props);
2372
- }
2334
+ }
2335
+ function mountSlots(slotMap) {
2336
+ for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
2337
+ for (const [id, entry] of mountedSlots) {
2338
+ const next = slotMap.get(id);
2339
+ if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2373
2340
  }
2374
- function mountSlots(slotMap) {
2375
- for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
2376
- for (const [id, entry] of mountedSlots) {
2377
- const next = slotMap.get(id);
2378
- if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2341
+ let slotIndex = null;
2342
+ for (const [id, { island: childIsland, props: childProps }] of slotMap) {
2343
+ const existing = mountedSlots.get(id);
2344
+ if (existing) {
2345
+ existing.updateProps(childProps);
2346
+ continue;
2379
2347
  }
2380
- let slotIndex = null;
2381
- for (const [id, { island: childIsland, props }] of slotMap) {
2382
- const existing = mountedSlots.get(id);
2383
- if (existing) {
2384
- existing.updateProps(props);
2385
- continue;
2386
- }
2387
- if (slotIndex === null) slotIndex = buildSlotIndex();
2388
- const slotEl = slotIndex.get(id) ?? null;
2389
- if (!slotEl) continue;
2390
- let slotProps = props;
2391
- if (slotProps === void 0) {
2392
- const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
2393
- if (rawProps) {
2394
- const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
2395
- if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
2396
- }
2348
+ if (slotIndex === null) slotIndex = buildSlotIndex();
2349
+ const slotEl = slotIndex.get(id) ?? null;
2350
+ if (!slotEl) continue;
2351
+ let slotProps = childProps;
2352
+ if (slotProps === void 0) {
2353
+ const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
2354
+ if (rawProps) {
2355
+ const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
2356
+ if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
2397
2357
  }
2398
- const prevSub = setActiveSub(void 0);
2399
- let handle;
2400
- try {
2401
- const internal = childIsland[ISLAND_MOUNT_INTERNAL];
2402
- handle = internal ? internal(slotEl, slotProps) : {
2403
- unmount: childIsland.mount(slotEl, slotProps),
2404
- updateProps: () => {}
2405
- };
2406
- } finally {
2407
- setActiveSub(prevSub);
2408
- }
2409
- mountedSlots.set(id, {
2410
- el: slotEl,
2411
- island: childIsland,
2412
- ...handle
2413
- });
2414
2358
  }
2359
+ const prevSub = setActiveSub(void 0);
2360
+ let handle;
2361
+ try {
2362
+ const internal = childIsland[ISLAND_MOUNT_INTERNAL];
2363
+ handle = internal ? internal(slotEl, slotProps) : {
2364
+ unmount: childIsland.mount(slotEl, slotProps),
2365
+ updateProps: () => {}
2366
+ };
2367
+ } finally {
2368
+ setActiveSub(prevSub);
2369
+ }
2370
+ mountedSlots.set(id, {
2371
+ el: slotEl,
2372
+ island: childIsland,
2373
+ ...handle
2374
+ });
2415
2375
  }
2416
- const listeners = [];
2417
- const firedOnce = /* @__PURE__ */ new Set();
2418
- const invocationControllers = /* @__PURE__ */ new WeakMap();
2419
- function attachListeners() {
2420
- for (const entry of ons) {
2421
- if (entry.options.once && firedOnce.has(entry)) continue;
2422
- let targets;
2423
- try {
2424
- targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector)).filter((element) => templateElementBelongsToHost(host, element));
2425
- } catch {
2426
- continue;
2376
+ }
2377
+ const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2378
+ let stopJsxEvents = () => {};
2379
+ let stopBindings = () => {};
2380
+ /** Register derived scopes before the render effect so source updates
2381
+ * settle derived envelopes before a render can consume them. */
2382
+ function startDerivedScopes() {
2383
+ for (const slot of frame.slots) if (slot.kind === "derived") startDerivedScope(slot);
2384
+ }
2385
+ /** Register side-effecting scopes after the render effect so prop-change
2386
+ * renders refresh their closures before those scopes execute. */
2387
+ function startEffectScopes() {
2388
+ for (const slot of frame.slots) if (slot.kind === "effect") startEffectScope(slot);
2389
+ else if (slot.kind === "once") startOnceScope(slot);
2390
+ else if (slot.kind === "error") errorSlots.push(slot);
2391
+ }
2392
+ function startDerivedScope(slot) {
2393
+ let ac = new AbortController();
2394
+ let skipFirst = slot.fromSnapshot;
2395
+ const stopScope = effect$1(() => {
2396
+ ac.abort();
2397
+ ac = new AbortController();
2398
+ const currentAc = ac;
2399
+ derivedPropsVersion();
2400
+ let result;
2401
+ try {
2402
+ result = slot.fn({ signal: currentAc.signal });
2403
+ } catch (err) {
2404
+ if (skipFirst) {
2405
+ skipFirst = false;
2406
+ return;
2427
2407
  }
2428
- targets.forEach((listenerTarget) => {
2429
- const listener = (event) => {
2430
- if (entry.options.once) {
2431
- firedOnce.add(entry);
2432
- for (const l of listeners.filter((l) => l.entry === entry)) l.target.removeEventListener(l.type, l.fn, l.options);
2433
- listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
2434
- }
2435
- const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
2436
- const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
2437
- let handlerSignal;
2438
- if (entry.abortable) {
2439
- let entryMap = invocationControllers.get(entry);
2440
- if (!entryMap) {
2441
- entryMap = /* @__PURE__ */ new WeakMap();
2442
- invocationControllers.set(entry, entryMap);
2443
- }
2444
- const prev = entryMap.get(listenerTarget);
2445
- if (prev) prev.abort();
2446
- const invocationController = new AbortController();
2447
- entryMap.set(listenerTarget, invocationController);
2448
- handlerSignal = anySignal([unmountController.signal, invocationController.signal]);
2449
- } else handlerSignal = unmountController.signal;
2450
- let result;
2451
- startBatch();
2452
- try {
2453
- result = entry.handler({
2454
- state,
2455
- derived,
2456
- action,
2457
- input,
2458
- host,
2459
- target: handlerTarget,
2460
- event,
2461
- signal: handlerSignal
2462
- });
2463
- } catch (err) {
2464
- reportError(err, "on");
2465
- endBatch();
2466
- return;
2467
- }
2468
- endBatch();
2469
- if (result instanceof Promise) result.catch((err) => {
2470
- if (err && err.name === "AbortError") return;
2471
- reportError(err, "on");
2472
- });
2473
- };
2474
- const opts = {
2475
- ...entry.options,
2476
- once: false
2477
- };
2478
- listenerTarget.addEventListener(entry.event, listener, opts);
2479
- listeners.push({
2480
- target: listenerTarget,
2481
- type: entry.event,
2482
- fn: listener,
2483
- options: opts,
2484
- entry
2485
- });
2408
+ writeEnvelope(currentAc, {
2409
+ loading: false,
2410
+ value: void 0,
2411
+ error: err instanceof Error ? err : new Error(String(err))
2486
2412
  });
2413
+ return;
2487
2414
  }
2488
- }
2489
- function detachListeners() {
2490
- for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
2491
- listeners.length = 0;
2492
- }
2493
- function invokeOnMounts() {
2494
- for (const entry of onMounts) {
2495
- const prevSub = setActiveSub(void 0);
2496
- let userCleanup;
2497
- try {
2498
- userCleanup = entry.fn({
2499
- state,
2500
- derived,
2501
- action,
2502
- input,
2503
- host,
2504
- hydrated,
2505
- signal: unmountController.signal
2415
+ if (skipFirst) {
2416
+ skipFirst = false;
2417
+ if (result instanceof Promise) result.catch(() => {});
2418
+ return;
2419
+ }
2420
+ if (!(result instanceof Promise)) {
2421
+ if (isAsyncIterable(result)) {
2422
+ const iterable = result;
2423
+ writeEnvelope(currentAc, {
2424
+ loading: true,
2425
+ value: readEnv().value,
2426
+ error: void 0
2506
2427
  });
2507
- } catch (err) {
2508
- reportError(err, "mount");
2509
- } finally {
2510
- setActiveSub(prevSub);
2511
- }
2512
- if (userCleanup) {
2513
- const teardown = userCleanup;
2514
- cleanups.push(() => {
2428
+ (async () => {
2515
2429
  try {
2516
- teardown();
2430
+ for await (const value of iterable) if (writeEnvelope(currentAc, {
2431
+ loading: false,
2432
+ value,
2433
+ error: void 0
2434
+ })) break;
2517
2435
  } catch (err) {
2518
- reportError(err, "mount");
2436
+ writeEnvelope(currentAc, {
2437
+ loading: false,
2438
+ value: void 0,
2439
+ error: err instanceof Error ? err : new Error(String(err))
2440
+ });
2519
2441
  }
2520
- });
2442
+ })();
2443
+ return;
2521
2444
  }
2445
+ writeEnvelope(currentAc, {
2446
+ loading: false,
2447
+ value: result,
2448
+ error: void 0
2449
+ });
2450
+ return;
2522
2451
  }
2523
- }
2524
- const preserveSSRDom = hydrated && host.childNodes.length > 0;
2525
- if (hydrated && onMounts.length > 0 && !shouldSkipOnMount) invokeOnMounts();
2526
- const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2527
- const initial = renderWithCtx({
2528
- render: () => fn({
2529
- state,
2530
- derived,
2531
- action,
2532
- input
2533
- }),
2534
- liveHost: preserveSSRDom ? host : void 0
2452
+ writeEnvelope(currentAc, {
2453
+ loading: true,
2454
+ value: readEnv().value,
2455
+ error: void 0
2456
+ });
2457
+ result.then((value) => {
2458
+ writeEnvelope(currentAc, {
2459
+ loading: false,
2460
+ value,
2461
+ error: void 0
2462
+ });
2463
+ }).catch((err) => {
2464
+ writeEnvelope(currentAc, {
2465
+ loading: false,
2466
+ value: void 0,
2467
+ error: err instanceof Error ? err : new Error(String(err))
2468
+ });
2469
+ });
2535
2470
  });
2536
- if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
2537
- const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2538
- let stopBindings = stopInitialBindingReflection;
2539
- cleanups.push(() => stopBindings());
2540
- let stopJsxEvents = applyJsxEvents({
2541
- host,
2542
- records: initial.events,
2543
- reportError: (error) => reportError(error, "on"),
2544
- unmountSignal: unmountController.signal,
2545
- onceFired: nativeOnceFired
2471
+ cleanups.push(() => {
2472
+ stopScope();
2473
+ ac.abort();
2546
2474
  });
2547
- cleanups.push(() => stopJsxEvents());
2548
- mountSlots(initial.slots);
2549
- cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2550
- attachListeners();
2551
- if (!hydrated && onMounts.length > 0) invokeOnMounts();
2552
- const stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2553
- stopBindings = () => {
2554
- stopInitialBindingListeners();
2555
- stopInitialBindingReflection();
2556
- };
2557
- for (const entry of effects) {
2558
- let userCleanup;
2559
- let runController = null;
2560
- const stopEffect = effect(() => {
2561
- if (userCleanup) {
2562
- try {
2563
- userCleanup();
2564
- } catch (err) {
2565
- reportError(err, "effect");
2566
- }
2567
- userCleanup = void 0;
2568
- }
2569
- if (runController) runController.abort();
2570
- runController = new AbortController();
2571
- const runSignal = anySignal([unmountController.signal, runController.signal]);
2572
- startBatch();
2475
+ function readEnv() {
2476
+ const prevSub = setActiveSub(void 0);
2477
+ try {
2478
+ return slot.env();
2479
+ } finally {
2480
+ setActiveSub(prevSub);
2481
+ }
2482
+ }
2483
+ function writeEnvelope(ac, patch) {
2484
+ if (ac.signal.aborted) return true;
2485
+ const prevSub = setActiveSub(void 0);
2486
+ try {
2487
+ slot.env(patch);
2488
+ } finally {
2489
+ setActiveSub(prevSub);
2490
+ }
2491
+ return false;
2492
+ }
2493
+ }
2494
+ function startEffectScope(slot) {
2495
+ let userCleanup;
2496
+ let runController = null;
2497
+ const stopScope = effect$1(() => {
2498
+ if (userCleanup) {
2573
2499
  try {
2574
- userCleanup = entry.fn({
2575
- state,
2576
- derived,
2577
- action,
2578
- input,
2579
- host,
2580
- signal: runSignal
2581
- });
2582
- } catch (err) {
2583
- reportError(err, "effect");
2584
- } finally {
2585
- endBatch();
2586
- }
2587
- });
2588
- cleanups.push(() => {
2589
- stopEffect();
2590
- if (userCleanup) try {
2591
2500
  userCleanup();
2592
2501
  } catch (err) {
2593
2502
  reportError(err, "effect");
2594
2503
  }
2595
- if (runController) runController.abort();
2504
+ userCleanup = void 0;
2505
+ }
2506
+ if (runController) runController.abort();
2507
+ runController = new AbortController();
2508
+ const runSignal = AbortSignal.any([unmountController.signal, runController.signal]);
2509
+ inputSignal();
2510
+ startBatch();
2511
+ try {
2512
+ userCleanup = slot.fn({ signal: runSignal });
2513
+ } catch (err) {
2514
+ reportError(err, "effect");
2515
+ } finally {
2516
+ endBatch();
2517
+ }
2518
+ });
2519
+ cleanups.push(() => {
2520
+ stopScope();
2521
+ if (userCleanup) try {
2522
+ userCleanup();
2523
+ } catch (err) {
2524
+ reportError(err, "effect");
2525
+ }
2526
+ if (runController) runController.abort();
2527
+ });
2528
+ }
2529
+ function startOnceScope(slot) {
2530
+ if (shouldSkipOnce) return;
2531
+ const prevSub = setActiveSub(void 0);
2532
+ let userCleanup;
2533
+ try {
2534
+ userCleanup = slot.fn({
2535
+ host,
2536
+ hydrated,
2537
+ signal: unmountController.signal
2596
2538
  });
2539
+ } catch (err) {
2540
+ reportError(err, "once");
2541
+ } finally {
2542
+ setActiveSub(prevSub);
2597
2543
  }
2598
- stopDerived = setupDerived();
2599
- cleanups.push(stopDerived);
2600
- for (const entry of streams) {
2601
- let streamAborted = false;
2602
- const streamController = new AbortController();
2603
- const runStream = async () => {
2544
+ if (userCleanup) {
2545
+ const teardown = userCleanup;
2546
+ cleanups.push(() => {
2604
2547
  try {
2605
- const gen = await entry.fn({
2606
- input,
2607
- signal: streamController.signal
2608
- });
2609
- while (!streamAborted && !streamController.signal.aborted) {
2610
- const { done, value } = await gen.next();
2611
- if (streamAborted || streamController.signal.aborted || done) break;
2612
- const prevSub = setActiveSub(void 0);
2613
- try {
2614
- const setter = state[entry.key];
2615
- setter?.(value);
2616
- } finally {
2617
- setActiveSub(prevSub);
2618
- }
2619
- }
2548
+ teardown();
2620
2549
  } catch (err) {
2621
- if (!streamAborted && !streamController.signal.aborted && err?.name !== "AbortError") reportError(err, "stream");
2550
+ reportError(err, "once");
2622
2551
  }
2623
- };
2624
- runStream();
2625
- cleanups.push(() => {
2626
- streamAborted = true;
2627
- streamController.abort();
2628
2552
  });
2629
2553
  }
2630
- let initialized = false;
2631
- const initialRenderedHtml = initial.html;
2632
- let lastRendered = null;
2633
- let renderEpoch = 0;
2634
- const stopRender = effect(() => {
2635
- const epoch = ++renderEpoch;
2636
- const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2637
- render: () => fn({
2638
- state,
2639
- derived,
2640
- action,
2641
- input
2642
- }),
2643
- liveHost: host
2644
- });
2645
- stopJsxEvents();
2646
- stopJsxEvents = applyJsxEvents({
2647
- host,
2648
- records: newEvents,
2649
- reportError: (error) => reportError(error, "on"),
2650
- unmountSignal: unmountController.signal,
2651
- onceFired: nativeOnceFired
2652
- });
2653
- const html = stylePrefix + rendered;
2654
- if (!initialized) {
2655
- initialized = true;
2656
- if (preserveSSRDom) {
2657
- pushUpdatedProps(newSlotMap);
2658
- if (rendered === initialRenderedHtml) {
2659
- lastRendered = rendered;
2660
- return;
2661
- }
2662
- }
2554
+ }
2555
+ const preserveSSRDom = hydrated && host.childNodes.length > 0;
2556
+ const initial = renderWithCtx({
2557
+ render: runInFrame(frame, input),
2558
+ liveHost: preserveSSRDom ? host : void 0
2559
+ });
2560
+ if (!preserveSSRDom) host.innerHTML = initial.html;
2561
+ const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2562
+ let stopInitialBindingListeners = null;
2563
+ stopBindings = stopInitialBindingReflection;
2564
+ cleanups.push(() => stopBindings());
2565
+ stopJsxEvents = applyJsxEvents({
2566
+ host,
2567
+ records: initial.events,
2568
+ reportError: (error) => reportError(error, "event"),
2569
+ unmountSignal: unmountController.signal,
2570
+ onceFired: nativeOnceFired
2571
+ });
2572
+ cleanups.push(() => stopJsxEvents());
2573
+ mountSlots(initial.slots);
2574
+ cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2575
+ stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2576
+ stopBindings = () => {
2577
+ stopInitialBindingListeners();
2578
+ stopInitialBindingReflection();
2579
+ };
2580
+ let initialized = false;
2581
+ const initialRenderedHtml = initial.html;
2582
+ let lastRendered = null;
2583
+ let renderEpoch = 0;
2584
+ const renderReactive = () => {
2585
+ const epoch = ++renderEpoch;
2586
+ frame.creating = false;
2587
+ frame.cursor = 0;
2588
+ const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2589
+ render: runInFrame(frame, input),
2590
+ liveHost: host
2591
+ });
2592
+ stopJsxEvents();
2593
+ stopJsxEvents = applyJsxEvents({
2594
+ host,
2595
+ records: newEvents,
2596
+ reportError: (error) => reportError(error, "event"),
2597
+ unmountSignal: unmountController.signal,
2598
+ onceFired: nativeOnceFired
2599
+ });
2600
+ if (!initialized) {
2601
+ initialized = true;
2602
+ if (preserveSSRDom) {
2603
+ pushUpdatedProps(newSlotMap);
2663
2604
  if (rendered === initialRenderedHtml) {
2664
2605
  lastRendered = rendered;
2665
2606
  return;
2666
2607
  }
2667
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2668
- initialHtml: initialRenderedHtml,
2669
- renderedHtml: rendered,
2670
- slotIds: newSlotMap.keys()
2671
- })) {
2672
- pushUpdatedProps(newSlotMap);
2673
- return;
2674
- }
2675
2608
  }
2676
- if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
2677
- pushUpdatedProps(newSlotMap);
2609
+ if (rendered === initialRenderedHtml) {
2610
+ lastRendered = rendered;
2678
2611
  return;
2679
2612
  }
2680
- detachListeners();
2681
- stopBindings();
2682
- stopJsxEvents();
2683
- const prevMountedCount = mountedSlots.size;
2684
- const leavingPromises = [];
2685
- for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
2686
- const r = teardownMountedSlot(id, entry);
2687
- if (r instanceof Promise) leavingPromises.push(r);
2688
- }
2689
- for (const [id, entry] of mountedSlots) {
2690
- const next = newSlotMap.get(id);
2691
- if (next && next.island !== entry.island) {
2692
- const r = teardownMountedSlot(id, entry);
2693
- if (r instanceof Promise) leavingPromises.push(r);
2694
- }
2613
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2614
+ initialHtml: initialRenderedHtml,
2615
+ renderedHtml: rendered,
2616
+ slotIds: newSlotMap.keys()
2617
+ })) {
2618
+ pushUpdatedProps(newSlotMap);
2619
+ return;
2695
2620
  }
2696
- const applyMorph = () => {
2697
- if (epoch !== renderEpoch) return;
2698
- if (newSlotMap.size < prevMountedCount) {
2699
- let divergeFrom = Infinity;
2700
- for (const [id, entry] of mountedSlots) {
2701
- if (!id.startsWith("p:")) continue;
2702
- const next = newSlotMap.get(id);
2703
- if (!next) continue;
2704
- const index = Number(id.slice(2));
2705
- if (Number.isNaN(index) || index >= divergeFrom) continue;
2706
- if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2707
- }
2708
- for (const [id, entry] of mountedSlots) {
2709
- if (!newSlotMap.has(id)) continue;
2710
- if (!entry.el.isConnected) continue;
2711
- if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
2712
- }
2621
+ }
2622
+ if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
2623
+ pushUpdatedProps(newSlotMap);
2624
+ return;
2625
+ }
2626
+ stopBindings();
2627
+ stopJsxEvents();
2628
+ const prevMountedCount = mountedSlots.size;
2629
+ for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) teardownMountedSlot(id, entry);
2630
+ for (const [id, entry] of mountedSlots) {
2631
+ const next = newSlotMap.get(id);
2632
+ if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2633
+ }
2634
+ const applyMorph = () => {
2635
+ if (epoch !== renderEpoch) return;
2636
+ if (newSlotMap.size < prevMountedCount) {
2637
+ let divergeFrom = Infinity;
2638
+ for (const [id, entry] of mountedSlots) {
2639
+ if (!id.startsWith("p:")) continue;
2640
+ const next = newSlotMap.get(id);
2641
+ if (!next) continue;
2642
+ const index = Number(id.slice(2));
2643
+ if (Number.isNaN(index) || index >= divergeFrom) continue;
2644
+ if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2713
2645
  }
2714
- const tpl = document.createElement("template");
2715
- const morphRootTag = host.tagName.toLowerCase();
2716
- tpl.innerHTML = `<${morphRootTag}>${html}</${morphRootTag}>`;
2717
- morphInner(host, tpl.content.firstElementChild);
2718
- lastRendered = rendered;
2719
- let rehomeIndex = null;
2720
2646
  for (const [id, entry] of mountedSlots) {
2721
- if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2722
- rehomeIndex ??= buildSlotIndex();
2723
- const stub = rehomeIndex.get(id);
2724
- if (stub && stub !== entry.el) stub.replaceWith(entry.el);
2647
+ if (!newSlotMap.has(id)) continue;
2648
+ if (!entry.el.isConnected) continue;
2649
+ if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
2725
2650
  }
2726
- attachListeners();
2727
- stopBindings = applyTemplateBindings(host, newBinds);
2728
- stopJsxEvents = applyJsxEvents({
2729
- host,
2730
- records: newEvents,
2731
- reportError: (error) => reportError(error, "on"),
2732
- unmountSignal: unmountController.signal,
2733
- onceFired: nativeOnceFired
2734
- });
2735
- mountSlots(newSlotMap);
2736
- };
2737
- if (leavingPromises.length > 0) Promise.allSettled(leavingPromises).then((results) => {
2738
- for (const r of results) if (r.status === "rejected") console.error(r.reason);
2739
- applyMorph();
2740
- });
2741
- else applyMorph();
2742
- });
2743
- let tornDown = false;
2744
- const unmount = () => {
2745
- if (tornDown) return;
2746
- tornDown = true;
2747
- disposed = true;
2748
- unmountController.abort();
2749
- ISLAND_MOUNT_HANDLES.delete(host);
2750
- stopRender();
2751
- detachListeners();
2752
- const pending = [];
2753
- for (const [, entry] of mountedSlots) {
2754
- const result = entry.unmount();
2755
- if (result instanceof Promise) pending.push(result);
2756
2651
  }
2757
- if (transition?.leave) try {
2758
- const result = transition.leave(host);
2759
- if (result instanceof Promise) pending.push(result);
2760
- } catch (err) {
2761
- reportError(err, "transition");
2652
+ const tpl = document.createElement("template");
2653
+ const morphRootTag = host.tagName.toLowerCase();
2654
+ tpl.innerHTML = `<${morphRootTag}>${rendered}</${morphRootTag}>`;
2655
+ morphInner(host, tpl.content.firstElementChild);
2656
+ lastRendered = rendered;
2657
+ let rehomeIndex = null;
2658
+ for (const [id, entry] of mountedSlots) {
2659
+ if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2660
+ rehomeIndex ??= buildSlotIndex();
2661
+ const stub = rehomeIndex.get(id);
2662
+ if (stub && stub !== entry.el) stub.replaceWith(entry.el);
2762
2663
  }
2763
- const finish = () => {
2764
- for (const c of cleanups) c();
2765
- };
2766
- if (pending.length > 0) return Promise.all(pending).then(finish).catch((err) => {
2767
- reportError(err, "transition");
2768
- finish();
2664
+ stopBindings = applyTemplateBindings(host, newBinds);
2665
+ stopJsxEvents = applyJsxEvents({
2666
+ host,
2667
+ records: newEvents,
2668
+ reportError: (error) => reportError(error, "event"),
2669
+ unmountSignal: unmountController.signal,
2670
+ onceFired: nativeOnceFired
2769
2671
  });
2770
- finish();
2771
- };
2772
- const updateProps = (nextProps) => {
2773
- if (tornDown) return;
2774
- const next = resolveInput(nextProps);
2775
- if (shallowEqualInput(inputSignal(), next)) return;
2776
- inputSignal(next);
2777
- };
2778
- const handle = {
2779
- unmount,
2780
- updateProps
2781
- };
2782
- ISLAND_MOUNT_HANDLES.set(host, handle);
2783
- return handle;
2784
- }
2785
- /**
2786
- * Dual-mode island invocation:
2787
- * - When called inside an `html` interpolation (i.e. {@link currentRenderCtx}
2788
- * is active), returns an {@link IslandCall} carrying props to be emitted
2789
- * as a child slot marker; {@link interpolateValue} consumes this marker.
2790
- * - When called at the top level with no active render context, returns
2791
- * SSR HTML via {@link renderToString} (backward-compatible public API).
2792
- *
2793
- * The declared return type omits {@link IslandCall} because TypeScript
2794
- * cannot narrow based on runtime stack state.
2795
- */
2796
- const island = ((props) => {
2797
- if (currentRenderCtx()) return {
2798
- [ISLAND_CALL]: true,
2799
- island,
2800
- props,
2801
- key: void 0
2672
+ mountSlots(newSlotMap);
2802
2673
  };
2803
- return renderToString(props);
2804
- });
2805
- island.toString = (props) => renderToString(props, true);
2806
- island.mount = (host, props) => mountIsland(host, props);
2807
- island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2808
- island[ISLAND_SLOT_TAG] = configuredSlotTag;
2809
- island[ISLAND_RENDER_STATE] = async (props) => {
2810
- const manifest = /* @__PURE__ */ new Map();
2811
- const html = await renderToString(props ?? void 0, false, void 0, manifest, void 0, true);
2812
- return manifestTemplatePrefix(manifest) + html;
2674
+ applyMorph();
2813
2675
  };
2814
- island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, void 0, eventsOut, void 0, true);
2815
- island.key = (key) => {
2816
- if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2817
- if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
2818
- const keyed = ((props) => ({
2819
- [ISLAND_CALL]: true,
2820
- island,
2821
- props,
2822
- key
2823
- }));
2824
- keyed[ISLAND_CALL] = true;
2825
- return keyed;
2676
+ let stopRender = effect$1(renderReactive);
2677
+ startDerivedScopes();
2678
+ stopRender();
2679
+ stopRender = effect$1(renderReactive);
2680
+ startEffectScopes();
2681
+ let tornDown = false;
2682
+ const unmount = () => {
2683
+ if (tornDown) return;
2684
+ tornDown = true;
2685
+ instance.disposed = true;
2686
+ unmountController.abort();
2687
+ ISLAND_MOUNT_HANDLES.delete(host);
2688
+ stopRender();
2689
+ for (const [, entry] of mountedSlots) entry.unmount();
2690
+ for (const c of cleanups) c();
2826
2691
  };
2827
- island[ISLAND] = true;
2828
- const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2829
- if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2830
- island.define = (tagName, options) => {
2831
- if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2832
- `${tagName}`;
2833
- return;
2834
- }
2835
- const CE_RESERVED = /* @__PURE__ */ new Set([
2836
- "annotation-xml",
2837
- "color-profile",
2838
- "font-face",
2839
- "font-face-src",
2840
- "font-face-uri",
2841
- "font-face-format",
2842
- "font-face-name",
2843
- "missing-glyph"
2844
- ]);
2845
- if (typeof tagName !== "string" || !/^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(tagName) || CE_RESERVED.has(tagName)) {
2846
- `${tagName}`;
2847
- return;
2692
+ const updateProps = (nextProps) => {
2693
+ if (tornDown) return;
2694
+ const next = resolveInput(nextProps);
2695
+ if (shallowEqualInput(inputSignal(), next)) return;
2696
+ inputSignal(next);
2697
+ derivedPropsVersion(derivedPropsVersion() + 1);
2698
+ };
2699
+ const handle = {
2700
+ unmount,
2701
+ updateProps
2702
+ };
2703
+ ISLAND_MOUNT_HANDLES.set(host, handle);
2704
+ return handle;
2705
+ }
2706
+ const island = ((props) => ({
2707
+ [ISLAND_CALL]: true,
2708
+ island,
2709
+ props,
2710
+ key: void 0
2711
+ }));
2712
+ island.toString = (props) => renderToString(props, true);
2713
+ island.toStringAsync = (props) => Promise.resolve(renderToString(props, false));
2714
+ island.mount = (host, props) => mountIsland(host, props);
2715
+ island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2716
+ island[ISLAND_SLOT_TAG] = configuredSlotTag;
2717
+ island[ISLAND_RENDER_STATE] = async (props) => {
2718
+ const manifest = /* @__PURE__ */ new Map();
2719
+ const html = await renderToString(props ?? void 0, false, manifest, true);
2720
+ return serializeServerManifest(manifest) + html;
2721
+ };
2722
+ island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, eventsOut, true);
2723
+ island.key = (key) => {
2724
+ if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2725
+ if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
2726
+ const keyed = ((props) => ({
2727
+ [ISLAND_CALL]: true,
2728
+ island,
2729
+ props,
2730
+ key
2731
+ }));
2732
+ keyed[ISLAND_CALL] = true;
2733
+ return keyed;
2734
+ };
2735
+ island[ISLAND] = true;
2736
+ const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2737
+ if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2738
+ island.define = (tagName, options) => {
2739
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2740
+ `${tagName}`;
2741
+ return;
2742
+ }
2743
+ const CE_RESERVED = /* @__PURE__ */ new Set([
2744
+ "annotation-xml",
2745
+ "color-profile",
2746
+ "font-face",
2747
+ "font-face-src",
2748
+ "font-face-uri",
2749
+ "font-face-format",
2750
+ "font-face-name",
2751
+ "missing-glyph"
2752
+ ]);
2753
+ if (typeof tagName !== "string" || !/^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(tagName) || CE_RESERVED.has(tagName)) {
2754
+ `${tagName}`;
2755
+ return;
2756
+ }
2757
+ if (customElements.get(tagName)) {
2758
+ `${tagName}`;
2759
+ return;
2760
+ }
2761
+ const observe = options?.observe ?? [];
2762
+ class IlhaIslandElement extends HTMLElement {
2763
+ static observedAttributes = observe;
2764
+ _handle = null;
2765
+ _props;
2766
+ _unmounting = false;
2767
+ _reconnect = false;
2768
+ get props() {
2769
+ return this._props;
2848
2770
  }
2849
- if (customElements.get(tagName)) {
2850
- `${tagName}`;
2851
- return;
2771
+ set props(p) {
2772
+ this._props = p;
2773
+ if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2852
2774
  }
2853
- const observe = options?.observe ?? [];
2854
- class IlhaIslandElement extends HTMLElement {
2855
- static observedAttributes = observe;
2856
- _handle = null;
2857
- _props;
2858
- _unmounting = false;
2859
- _reconnect = false;
2860
- get props() {
2861
- return this._props;
2775
+ _mergedProps() {
2776
+ const attrProps = {};
2777
+ let hasAttrs = false;
2778
+ for (const name of observe) {
2779
+ const v = this.getAttribute(name);
2780
+ if (v !== null) {
2781
+ attrProps[name] = v;
2782
+ hasAttrs = true;
2783
+ }
2862
2784
  }
2863
- set props(p) {
2864
- this._props = p;
2865
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2785
+ if (!hasAttrs && this._props === void 0) return void 0;
2786
+ return {
2787
+ ...attrProps,
2788
+ ...this._props
2789
+ };
2790
+ }
2791
+ connectedCallback() {
2792
+ if (this._unmounting) {
2793
+ this._reconnect = true;
2794
+ return;
2866
2795
  }
2867
- _mergedProps() {
2868
- const attrProps = {};
2869
- let hasAttrs = false;
2870
- for (const name of observe) {
2871
- const v = this.getAttribute(name);
2872
- if (v !== null) {
2873
- attrProps[name] = v;
2874
- hasAttrs = true;
2875
- }
2796
+ if (this._handle) return;
2797
+ this._handle = mountIslandInternal(this, this._mergedProps());
2798
+ }
2799
+ disconnectedCallback() {
2800
+ if (!this._handle || this._unmounting) return;
2801
+ this._unmounting = true;
2802
+ this._reconnect = false;
2803
+ Promise.resolve(this._handle.unmount()).finally(() => {
2804
+ this._handle = null;
2805
+ this._unmounting = false;
2806
+ if (this._reconnect) {
2807
+ this._reconnect = false;
2808
+ if (this.isConnected) this._handle = mountIslandInternal(this, this._mergedProps());
2876
2809
  }
2877
- if (!hasAttrs && this._props === void 0) return void 0;
2878
- return {
2879
- ...attrProps,
2880
- ...this._props ?? {}
2881
- };
2882
- }
2883
- connectedCallback() {
2884
- if (this._unmounting) {
2885
- this._reconnect = true;
2886
- return;
2810
+ });
2811
+ }
2812
+ attributeChangedCallback() {
2813
+ if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2814
+ }
2815
+ }
2816
+ customElements.define(tagName, IlhaIslandElement);
2817
+ };
2818
+ island.hydratable = async (props, opts) => {
2819
+ const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2820
+ const tag = assertValidSlotTagName(rawTag);
2821
+ const resolvedProps = props ?? {};
2822
+ const innerPromise = renderToString(resolvedProps, false, void 0, true);
2823
+ let stateAttr = "";
2824
+ if (snapshot !== false) {
2825
+ const doState = snapshot === true || snapshot.state !== false;
2826
+ const doDerived = snapshot === true || snapshot.derived !== false;
2827
+ const doSkipOnMount = explicitSkipOnMount ?? (doState || doDerived);
2828
+ const snapshotData = { v: 2 };
2829
+ const input = resolveInput(resolvedProps);
2830
+ const probeFrame = freshFrame({ ssr: true });
2831
+ withFrame(probeFrame, () => {
2832
+ component(input);
2833
+ });
2834
+ if (doState) {
2835
+ const states = [];
2836
+ for (const slot of probeFrame.slots) {
2837
+ if (slot.kind !== "state") continue;
2838
+ const prevSub = setActiveSub(void 0);
2839
+ try {
2840
+ states.push(slot.acc());
2841
+ } finally {
2842
+ setActiveSub(prevSub);
2887
2843
  }
2888
- if (this._handle) return;
2889
- this._handle = mountIslandInternal(this, this._mergedProps());
2890
2844
  }
2891
- disconnectedCallback() {
2892
- if (!this._handle || this._unmounting) return;
2893
- this._unmounting = true;
2894
- this._reconnect = false;
2895
- Promise.resolve(this._handle.unmount()).finally(() => {
2896
- this._handle = null;
2897
- this._unmounting = false;
2898
- if (this._reconnect) {
2899
- this._reconnect = false;
2900
- if (this.isConnected) this._handle = mountIslandInternal(this, this._mergedProps());
2901
- }
2845
+ snapshotData["s"] = states;
2846
+ }
2847
+ if (doDerived) {
2848
+ const probePendings = probeFrame.pendingDerived ?? [];
2849
+ if (probePendings.length > 0) {
2850
+ await Promise.all(probePendings.map((p) => p.promise));
2851
+ probeFrame.creating = false;
2852
+ probeFrame.cursor = 0;
2853
+ withFrame(probeFrame, () => {
2854
+ component(input);
2902
2855
  });
2903
2856
  }
2904
- attributeChangedCallback() {
2905
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2906
- }
2907
- }
2908
- customElements.define(tagName, IlhaIslandElement);
2909
- };
2910
- island.hydratable = async (props, opts) => {
2911
- const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2912
- const tag = assertValidSlotTagName(rawTag);
2913
- const resolvedProps = props ?? {};
2914
- const streamValues = {};
2915
- const eventsManifest = /* @__PURE__ */ new Map();
2916
- const inner = await renderToString(resolvedProps, false, streamValues, eventsManifest, void 0, true);
2917
- const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
2918
- let stateAttr = "";
2919
- if (snapshot !== false) {
2920
- const doState = snapshot === true || snapshot.state !== false;
2921
- const doDerived = snapshot === true || snapshot.derived !== false;
2922
- const doSkipOnMount = explicitSkipOnMount ?? (doState || doDerived);
2923
- const snapshotData = {};
2924
- const input = resolveInput(resolvedProps);
2925
- const plainState = buildPlainState(input);
2926
- const stateForDerived = { ...plainState };
2927
- for (const [k, v] of Object.entries(streamValues)) stateForDerived[k] = () => v;
2928
- if (doState) for (const entry of states) snapshotData[entry.key] = entry.key in streamValues ? streamValues[entry.key] : plainState[entry.key]();
2929
- if (doDerived) {
2930
- const derivedResults = {};
2931
- for (const entry of deriveds) {
2932
- const prevSub = setActiveSub(void 0);
2933
- let resultPromise;
2934
- let syncError;
2935
- let threw = false;
2936
- try {
2937
- resultPromise = entry.fn({
2938
- state: stateForDerived,
2939
- input,
2940
- signal: new AbortController().signal
2941
- });
2942
- } catch (err) {
2943
- threw = true;
2944
- syncError = err;
2945
- } finally {
2946
- setActiveSub(prevSub);
2947
- }
2948
- if (threw) {
2949
- derivedResults[entry.key] = {
2950
- loading: false,
2951
- value: void 0,
2952
- error: syncError instanceof Error ? syncError.message : String(syncError)
2953
- };
2954
- continue;
2955
- }
2956
- try {
2957
- const result = await Promise.resolve(resultPromise);
2958
- derivedResults[entry.key] = {
2959
- loading: false,
2960
- value: result,
2961
- error: void 0
2962
- };
2963
- } catch (err) {
2964
- derivedResults[entry.key] = {
2965
- loading: false,
2966
- value: void 0,
2967
- error: err instanceof Error ? err.message : String(err)
2968
- };
2969
- }
2857
+ const derivedEntries = [];
2858
+ for (const slot of probeFrame.slots) {
2859
+ if (slot.kind !== "derived") continue;
2860
+ const prevSub = setActiveSub(void 0);
2861
+ let envelope;
2862
+ try {
2863
+ envelope = { ...slot.env() };
2864
+ } finally {
2865
+ setActiveSub(prevSub);
2970
2866
  }
2971
- snapshotData["_derived"] = derivedResults;
2867
+ const entry = {
2868
+ loading: false,
2869
+ value: envelope.value
2870
+ };
2871
+ if (envelope.error) entry["error"] = envelope.error.message;
2872
+ derivedEntries.push(entry);
2972
2873
  }
2973
- if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2974
- if (streams.length > 0) snapshotData["_streams"] = Object.fromEntries(streams.map((entry) => [entry.key, true]));
2975
- stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2874
+ snapshotData["d"] = derivedEntries;
2976
2875
  }
2977
- let actionsAttr = "";
2978
- if (eventsManifest.size > 0) actionsAttr = ` ${ACTIONS_ATTR}='${escapeHtml(JSON.stringify(Object.fromEntries(eventsManifest)))}'`;
2979
- return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}${actionsAttr}>${inner}</${tag}>`;
2980
- };
2981
- return island;
2982
- }
2983
- };
2984
- function ilhaFrom(selector, island, props) {
2985
- const host = typeof selector === "string" ? document.querySelector(selector) : selector;
2986
- if (!host) {
2987
- console.warn(`[ilha] from(): element not found: ${selector}`);
2988
- return null;
2989
- }
2990
- return island.mount(host, props);
2991
- }
2876
+ if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2877
+ stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2878
+ }
2879
+ const inner = await innerPromise;
2880
+ return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${escapeHtml(JSON.stringify(resolvedProps))}'${stateAttr}>${inner}</${tag}>`;
2881
+ };
2882
+ return island;
2883
+ });
2992
2884
  function mountAll(registry, options = {}) {
2993
2885
  const root = options.root ?? document.body;
2994
2886
  const lazy = options.lazy ?? false;
@@ -3034,66 +2926,27 @@ function mountAll(registry, options = {}) {
3034
2926
  if (pending.length > 0) return Promise.all(pending).then(() => {});
3035
2927
  } };
3036
2928
  }
3037
- const rootBuilder = new IlhaBuilder({
3038
- schema: null,
3039
- defaultInput: null,
3040
- states: [],
3041
- streams: [],
3042
- deriveds: [],
3043
- actions: [],
3044
- ons: [],
3045
- effects: [],
3046
- onMounts: [],
3047
- onErrors: [],
3048
- transition: null,
3049
- css: null,
3050
- as: null
3051
- });
3052
- const callableRoot = ((fn) => rootBuilder.input().render(fn));
3053
- Object.defineProperty(callableRoot, "_cfg", {
3054
- value: rootBuilder._cfg,
3055
- enumerable: false
3056
- });
3057
- for (const key of Object.getOwnPropertyNames(IlhaBuilder.prototype)) {
3058
- if (key === "constructor") continue;
3059
- const descriptor = Object.getOwnPropertyDescriptor(IlhaBuilder.prototype, key);
3060
- if (descriptor) Object.defineProperty(callableRoot, key, descriptor);
3061
- }
3062
- const ilha = Object.assign(callableRoot, {
3063
- html: ilhaHtml,
3064
- raw: ilhaRaw,
3065
- mount: mountAll,
3066
- from: ilhaFrom,
3067
- context: ilhaContext,
3068
- signal: ilhaSignal,
3069
- computed: ilhaComputed,
3070
- batch,
3071
- untrack,
3072
- onUncaughtError
2929
+ setJsxRuntimeBridge({
2930
+ registerEvent({ type, handler, modifier }) {
2931
+ const ctx = currentRenderCtx();
2932
+ if (!ctx) return void 0;
2933
+ SSR_ACTION_STUB in handler;
2934
+ const index = ctx.events.length;
2935
+ ctx.events.push({
2936
+ type,
2937
+ handler,
2938
+ modifier
2939
+ });
2940
+ return index;
2941
+ },
2942
+ slot({ island, props, key }) {
2943
+ return ilhaRaw(emitIslandSlot({
2944
+ island,
2945
+ props,
2946
+ key
2947
+ }));
2948
+ }
3073
2949
  });
3074
- /** @internal Used by the separate JSX runtime to register a native event handler. */
3075
- function __ilhaJsxEvent({ type, handler, modifier }) {
3076
- const ctx = currentRenderCtx();
3077
- if (!ctx) return void 0;
3078
- const isActionStub = SSR_ACTION_STUB in handler;
3079
- const capture = ctx.manifest && !isActionStub ? captureHandlerActions(handler) : void 0;
3080
- const index = ctx.events.length;
3081
- ctx.events.push({
3082
- type,
3083
- handler,
3084
- modifier,
3085
- capture
3086
- });
3087
- return index;
3088
- }
3089
- /** @internal Used by the separate JSX runtime entry to preserve island slot composition. */
3090
- function __ilhaJsxSlot({ island, props, key }) {
3091
- return ilhaRaw(emitIslandSlot({
3092
- island,
3093
- props,
3094
- key
3095
- }));
3096
- }
3097
2950
  /**
3098
2951
  * Morph an element's children toward `html`, patching in place: surviving
3099
2952
  * elements keep their identity, listeners, and focus (see the morph engine).
@@ -3117,10 +2970,8 @@ function morph(host, html) {
3117
2970
  }
3118
2971
  const html = ilhaHtml;
3119
2972
  const raw = ilhaRaw;
3120
- const css = ilhaCss;
3121
2973
  const mount = mountAll;
3122
- const from = ilhaFrom;
3123
2974
  const context = ilhaContext;
3124
2975
 
3125
2976
  //#endregion
3126
- export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
2977
+ export { action, batch, context, css, derived, each, effect, html, ilha, ilhaSignal, isSafeUrl, isSafeUrlAttrValue, isUrlAttributeName, json, morph, mount, onError, onUncaughtError, persist, raw, serializeStyle, state, untrack };