ilha 0.11.1 → 0.12.1

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, setServerActionBinder } 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,61 +535,148 @@ 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 `.with()` 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 `.with()` 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 `.with(...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("with() rejects non-finite numbers.");
635
+ case "object": break;
636
+ default: throw new TypeError(`with() rejects ${typeof value} values.`);
637
+ }
638
+ if (seen.has(value)) throw new TypeError("with() 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("with() accepts plain objects only.");
645
+ for (const [key, child] of Object.entries(value)) {
646
+ if (UNSAFE_KEYS.has(key)) throw new TypeError(`with() 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 attachWith(accessor) {
655
+ Object.defineProperty(accessor, "with", {
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(`with() 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
  }
673
+ setServerActionBinder(function bindExternalAction(fn, manifestId) {
674
+ const action = fn;
675
+ action[SSR_ACTION_STUB] = true;
676
+ action[ACTION_MANIFEST_ID] = manifestId;
677
+ attachWith(action);
678
+ return action;
679
+ });
551
680
  const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
552
681
  function renderCtxStack() {
553
682
  const g = globalThis;
@@ -605,8 +734,8 @@ function emitIslandSlot({ island, props, key }) {
605
734
  try {
606
735
  const manifest = /* @__PURE__ */ new Map();
607
736
  const ssrWithManifest = island[ISLAND_SSR_MANIFEST];
608
- const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island(props);
609
- const prefix = () => manifestTemplatePrefix(manifest);
737
+ const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island.toStringAsync(props);
738
+ const prefix = () => serializeServerManifest(manifest);
610
739
  if (result instanceof Promise) {
611
740
  ctx.pending.set(id, result.then(String).then((html) => prefix() + html));
612
741
  return wrapIslandSlotHtml({
@@ -656,7 +785,7 @@ function resolveSignalSetter(read, value) {
656
785
  function markSignalAccessor(fn) {
657
786
  fn[SIGNAL_ACCESSOR] = true;
658
787
  const accessor = fn;
659
- accessor.select = (selector) => createSelectAccessor(accessor, selector);
788
+ accessor.select = ((...args) => createSelectAccessor(accessor, args));
660
789
  return accessor;
661
790
  }
662
791
  function isSignalAccessor(v) {
@@ -731,22 +860,27 @@ function trackSelectPath(rootState, selector) {
731
860
  selector(track(rootState));
732
861
  return path;
733
862
  }
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.");
863
+ function createSelectAccessor(root, input) {
864
+ const selector = typeof input[0] === "function" ? input[0] : void 0;
865
+ let path;
866
+ if (selector) {
867
+ path = trackSelectPath(root(), selector);
868
+ const selected = selector(root());
869
+ const resolved = path.length === 0 ? root() : getAtPath(root(), path);
870
+ 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.");
871
+ } else path = input;
872
+ const read = (from) => selector ? path.length === 0 ? selector(from) : getAtPath(from, path) : getAtPath(from, path);
739
873
  return markSignalAccessor((...args) => {
740
- if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
874
+ if (args.length === 0) return read(root());
741
875
  const previousRoot = root();
742
- const previousSelected = path.length === 0 ? selector(previousRoot) : getAtPath(previousRoot, path);
876
+ const previousSelected = read(previousRoot);
743
877
  const value = resolveSignalSetter(() => previousSelected, args[0]);
744
- const next = path.length === 0 ? value : setAtPath({
878
+ const next = path.length === 0 && selector ? value : setAtPath({
745
879
  object: previousRoot,
746
880
  path,
747
881
  value
748
882
  });
749
- if (!Object.is(previousRoot, next)) root((() => next));
883
+ if (!Object.is(previousRoot, next)) root(() => next);
750
884
  });
751
885
  }
752
886
  function ilhaRaw(value) {
@@ -755,14 +889,37 @@ function ilhaRaw(value) {
755
889
  value
756
890
  };
757
891
  }
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;
892
+ /**
893
+ * Serialize a JSON-safe value into executable `<script>` content safely. `<`
894
+ * is escaped as `\u003C`, so a value can never close the script block or open
895
+ * the `<!--` escape hatch. Use it for script-element data:
896
+ *
897
+ * ```ts
898
+ * html`<script>const d = ${json(payload)};<\/script>`
899
+ * ```
900
+ *
901
+ * Like `raw()`, `json()` is for element *content* only — interpolating it into
902
+ * an attribute is author error. It does not protect against values that are
903
+ * not valid JSON (functions, symbols, undefined, cyclic objects are dropped
904
+ * or throw, matching `JSON.stringify`).
905
+ */
906
+ function json(value) {
907
+ return ilhaRaw(serializeScriptJson(value));
908
+ }
909
+ /**
910
+ * Escape text for a `<style>` element body. `<` becomes the CSS hex escape
911
+ * `\3C `, so the value can never close the style block or open the `<!--`
912
+ * escape hatch while surviving CSS parsing:
913
+ *
914
+ * ```ts
915
+ * html`<style>${css(cssSource)}</style>`
916
+ * ```
917
+ *
918
+ * Like `raw()`, `css()` is for element *content* only — interpolating it into
919
+ * an attribute is author error.
920
+ */
921
+ function css(value) {
922
+ return ilhaRaw(escapeStyleText(value));
766
923
  }
767
924
  function interpolateValue(v) {
768
925
  if (v == null || v === true || v === false) return "";
@@ -788,21 +945,31 @@ function interpolateValue(v) {
788
945
  const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
789
946
  "value",
790
947
  "checked",
791
- "valueAsNumber",
792
- "valueAsDate",
793
948
  "files",
794
949
  "open",
795
950
  "group",
796
951
  "this"
797
952
  ]);
798
953
  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*("|')?$/;
954
+ const EVENT_PREFIX_RE = /(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::([a-zA-Z][a-zA-Z0-9-]*))?\s*=\s*("|')?$/;
800
955
  const NATIVE_EVENT_MODIFIERS = /* @__PURE__ */ new Set([
801
956
  "abortable",
802
957
  "once",
803
958
  "capture",
804
959
  "passive"
805
960
  ]);
961
+ const FULL_VALUE_ATTR_RE = /(?:^|\s)([a-zA-Z_:][a-zA-Z0-9:._-]*)\s*=\s*("|')?$/;
962
+ const UNQUOTED_HAZARD_RE = /[\s=]|`/;
963
+ function resolveTemplateValue(v) {
964
+ if (isSignalAccessor(v)) return v();
965
+ if (typeof v === "function") return v();
966
+ return v;
967
+ }
968
+ function openTagName(source, index) {
969
+ const open = source.lastIndexOf("<", index);
970
+ if (open === -1) return "element";
971
+ return (source.slice(open).match(/^<\s*([a-zA-Z][a-zA-Z0-9:._-]*)/)?.[1] ?? "element").toLowerCase();
972
+ }
806
973
  function isEventAttributePosition(source, index) {
807
974
  let open = -1;
808
975
  let quote = null;
@@ -837,14 +1004,6 @@ function findTagCloseIndex(chunk, initialQuote) {
837
1004
  quote
838
1005
  };
839
1006
  }
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
1007
  function extractElementStaticValue(prefix) {
849
1008
  const lastOpen = prefix.lastIndexOf("<");
850
1009
  if (lastOpen === -1) return null;
@@ -864,8 +1023,6 @@ function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
864
1023
  }
865
1024
  switch (kind) {
866
1025
  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
1026
  case "checked": return [v ? ` checked` : ``, spec];
870
1027
  case "open": return [v ? ` open` : ``, spec];
871
1028
  case "files": return [``, spec];
@@ -910,24 +1067,29 @@ function ilhaHtml(strings, ...values) {
910
1067
  const value = values[i];
911
1068
  const eventMatch = chunk.match(EVENT_PREFIX_RE);
912
1069
  const eventMatchStart = eventMatch ? chunk.length - eventMatch[0].length : -1;
1070
+ if (!eventMatch && typeof value === "function") {
1071
+ const ambiguous = chunk.match(/(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::[a-zA-Z][a-zA-Z0-9-]*)?\s*=\s*(?:(?:"[^"]*)|(?:'[^']*)|(?:[^\s"'=<>`]+))$/);
1072
+ const start = ambiguous ? chunk.length - ambiguous[0].length : -1;
1073
+ 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}.`);
1074
+ }
913
1075
  const eventAttributeFragment = eventMatch !== null && chunk.slice(0, eventMatchStart).trim() === "";
914
1076
  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;
1077
+ const eventName = eventMatch[1].toLowerCase();
1078
+ const rawModifier = eventMatch[2]?.toLowerCase();
1079
+ const modifier = rawModifier !== void 0 && NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
918
1080
  const openQuote = eventMatch[3] ?? null;
1081
+ const nextChunk = strings[i + 1] ?? "";
1082
+ 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
1083
  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
1084
  result += cleanPrefix;
1085
+ typeof value === "function" && SSR_ACTION_STUB in value;
923
1086
  if (ctx && typeof value === "function") {
924
1087
  const index = ctx.events.length;
925
1088
  if (rawModifier && !modifier) {} else {
926
1089
  ctx.events.push({
927
1090
  type: eventName,
928
1091
  handler: value,
929
- modifier,
930
- capture
1092
+ modifier
931
1093
  });
932
1094
  pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
933
1095
  }
@@ -968,6 +1130,72 @@ function ilhaHtml(strings, ...values) {
968
1130
  if (openQuote) stripLeadingQuote = openQuote;
969
1131
  continue;
970
1132
  }
1133
+ const attrMatch = chunk.match(FULL_VALUE_ATTR_RE);
1134
+ if (attrMatch && isEventAttributePosition(result + chunk, result.length + attrMatch.index)) {
1135
+ const attrName = attrMatch[1];
1136
+ const openQuote = attrMatch[2] ?? null;
1137
+ if (openQuote) stripLeadingQuote = openQuote;
1138
+ const cleanPrefix = chunk.slice(0, attrMatch.index).replace(/\s+$/, "");
1139
+ if (attrName.length > 2 && /^on/i.test(attrName)) {
1140
+ result += cleanPrefix;
1141
+ continue;
1142
+ }
1143
+ if (attrName.toLowerCase() === "srcdoc") {
1144
+ result += cleanPrefix;
1145
+ continue;
1146
+ }
1147
+ if (isIsland(value) || isIslandCall(value)) {
1148
+ stripLeadingQuote = null;
1149
+ result += chunk + interpolateValue(value);
1150
+ continue;
1151
+ }
1152
+ if (isUrlAttributeName(attrName)) {
1153
+ if (isRawHtml(value)) {
1154
+ result += cleanPrefix + ` ${attrName}="${value.value}"`;
1155
+ continue;
1156
+ }
1157
+ const resolved = resolveTemplateValue(value);
1158
+ if (resolved == null || resolved === false) {
1159
+ result += cleanPrefix;
1160
+ continue;
1161
+ }
1162
+ const url = String(resolved);
1163
+ if (!isSafeUrlAttrValue(openTagName(result + chunk, result.length + attrMatch.index), attrName, url)) {
1164
+ result += cleanPrefix;
1165
+ continue;
1166
+ }
1167
+ const quotedUrl = openQuote !== null || UNQUOTED_HAZARD_RE.test(url);
1168
+ result += cleanPrefix + ` ${attrName}=` + (quotedUrl ? `"${escapeHtml(url)}"` : escapeHtml(url));
1169
+ continue;
1170
+ }
1171
+ if (attrName.toLowerCase() === "style") {
1172
+ if (!isRawHtml(value)) {
1173
+ const resolved = resolveTemplateValue(value);
1174
+ if (isPlainObject(resolved)) {
1175
+ result += cleanPrefix + ` style="${escapeHtml(serializeStyle(resolved))}"`;
1176
+ continue;
1177
+ }
1178
+ }
1179
+ }
1180
+ if (isRawHtml(value)) {
1181
+ stripLeadingQuote = null;
1182
+ result += chunk + interpolateValue(value);
1183
+ continue;
1184
+ }
1185
+ const resolved = resolveTemplateValue(value);
1186
+ if (resolved == null || resolved === false) {
1187
+ result += cleanPrefix;
1188
+ continue;
1189
+ }
1190
+ if (resolved === true) {
1191
+ result += cleanPrefix + ` ${attrName}`;
1192
+ continue;
1193
+ }
1194
+ const generic = String(resolved);
1195
+ if (openQuote !== null || UNQUOTED_HAZARD_RE.test(generic)) result += cleanPrefix + ` ${attrName}="${escapeHtml(generic)}"`;
1196
+ else result += cleanPrefix + ` ${attrName}=${escapeHtml(generic)}`;
1197
+ continue;
1198
+ }
971
1199
  result += chunk + interpolateValue(value);
972
1200
  }
973
1201
  if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
@@ -988,10 +1216,10 @@ const contextRegistry = /* @__PURE__ */ new Map();
988
1216
  function ilhaContextFn(key, initial) {
989
1217
  if (contextRegistry.has(key)) return contextRegistry.get(key);
990
1218
  const s = signal(initial);
991
- const accessor = (...args) => {
1219
+ const accessor = markSignalAccessor((...args) => {
992
1220
  if (args.length === 0) return s();
993
1221
  s(resolveSignalSetter(() => s(), args[0]));
994
- };
1222
+ });
995
1223
  contextRegistry.set(key, accessor);
996
1224
  return accessor;
997
1225
  }
@@ -1007,16 +1235,11 @@ const ilhaContext = Object.assign(ilhaContextFn, {
1007
1235
  });
1008
1236
  /**
1009
1237
  * 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.
1238
+ * Internal: used by state()'s slot-drift fallback. The public way to create
1239
+ * free-standing shared state is `context(key, initial)`.
1018
1240
  */
1019
1241
  function ilhaSignal(initial) {
1242
+ if (currentFrame()?.creating && __DEV__);
1020
1243
  const s = signal(initial);
1021
1244
  return markSignalAccessor((...args) => {
1022
1245
  if (args.length === 0) return s();
@@ -1024,25 +1247,6 @@ function ilhaSignal(initial) {
1024
1247
  });
1025
1248
  }
1026
1249
  /**
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
1250
  * Run a free-standing reactive effect outside any island. `fn` runs once
1047
1251
  * immediately and again whenever a signal it read changes. It may return a
1048
1252
  * cleanup function, invoked before each re-run and on stop. Signal writes
@@ -1067,7 +1271,7 @@ function ilhaEffect(fn) {
1067
1271
  cleanup = void 0;
1068
1272
  }
1069
1273
  };
1070
- const stop = effect(() => {
1274
+ const stop = effect$1(() => {
1071
1275
  runCleanup();
1072
1276
  startBatch();
1073
1277
  try {
@@ -1115,6 +1319,74 @@ function batch(fn) {
1115
1319
  endBatch();
1116
1320
  }
1117
1321
  }
1322
+ /**
1323
+ * Keep a standalone signal in sync with persistent storage:
1324
+ *
1325
+ * 1. On call, reads `key` and writes the stored value into the signal.
1326
+ * 2. Subscribes to the signal and writes its value back on every change.
1327
+ * 3. Optionally mirrors writes from other tabs (`storage` events).
1328
+ *
1329
+ * No-op on the server (returns an inert unsubscribe). Call the returned
1330
+ * unsubscribe to stop syncing.
1331
+ *
1332
+ * ```ts
1333
+ * const cart = signal([] as string[]);
1334
+ * persist(cart, "cart");
1335
+ * ```
1336
+ */
1337
+ function persist(accessor, key, options = {}) {
1338
+ if (typeof window === "undefined") return () => {};
1339
+ const storage = options.storage ?? window.localStorage;
1340
+ const serialize = options.serialize ?? JSON.stringify;
1341
+ const deserialize = options.deserialize ?? ((raw) => {
1342
+ try {
1343
+ return JSON.parse(raw);
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ });
1348
+ const label = `[ilha] persist("${key}")`;
1349
+ const applyRaw = (raw) => {
1350
+ if (raw == null) return;
1351
+ try {
1352
+ const value = deserialize(raw);
1353
+ if (value !== null) {
1354
+ const prevSub = setActiveSub(void 0);
1355
+ try {
1356
+ accessor(() => value);
1357
+ } finally {
1358
+ setActiveSub(prevSub);
1359
+ }
1360
+ }
1361
+ } catch (err) {
1362
+ console.error(`${label}: failed to restore`, err);
1363
+ }
1364
+ };
1365
+ try {
1366
+ applyRaw(storage.getItem(key));
1367
+ } catch (err) {
1368
+ console.error(`${label}: failed to read storage`, err);
1369
+ }
1370
+ const stopEffect = ilhaEffect(() => {
1371
+ try {
1372
+ storage.setItem(key, serialize(accessor()));
1373
+ } catch (err) {
1374
+ console.error(`${label}: failed to write storage`, err);
1375
+ }
1376
+ });
1377
+ let onStorage = null;
1378
+ if (options.crossTab !== false && storage === window.localStorage) {
1379
+ onStorage = (e) => {
1380
+ if (e.key !== key) return;
1381
+ applyRaw(e.newValue);
1382
+ };
1383
+ window.addEventListener("storage", onStorage);
1384
+ }
1385
+ return () => {
1386
+ stopEffect();
1387
+ if (onStorage) window.removeEventListener("storage", onStorage);
1388
+ };
1389
+ }
1118
1390
  function createDerivedAccessor(read, write) {
1119
1391
  const accessor = markSignalAccessor((...args) => {
1120
1392
  if (args.length > 0) {
@@ -1135,333 +1407,60 @@ function defaultDerivedAccessor() {
1135
1407
  error: void 0
1136
1408
  }));
1137
1409
  }
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))
1410
+ const BIND_SENTINEL_ATTR = "data-ilha-bind";
1411
+ const EVENT_SENTINEL_ATTR = "data-ilha-on";
1412
+ function parseBindSentinel(value) {
1413
+ const out = [];
1414
+ for (const part of value.split(",")) {
1415
+ const [kind, idx] = part.split(":");
1416
+ if (!kind || !idx) continue;
1417
+ const i = Number(idx);
1418
+ if (!Number.isInteger(i) || i < 0) continue;
1419
+ out.push({
1420
+ kind,
1421
+ index: i
1218
1422
  });
1219
1423
  }
1424
+ return out;
1220
1425
  }
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
1426
+ function resolveBindOps(el, kind) {
1427
+ const input = el;
1428
+ switch (kind) {
1429
+ case "value": return {
1430
+ event: el.tagName === "SELECT" ? "change" : "input",
1431
+ read: (el) => el.value,
1432
+ write: (el, v) => el.value = v == null ? "" : String(v)
1230
1433
  };
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
1253
- };
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
1434
+ case "checked": return {
1435
+ event: "change",
1436
+ read: (el) => el.checked,
1437
+ write: (el, v) => el.checked = Boolean(v)
1438
+ };
1439
+ case "files": return {
1440
+ event: "change",
1441
+ read: (el) => el.files,
1442
+ write: () => {}
1443
+ };
1444
+ case "open": return {
1445
+ event: "toggle",
1446
+ read: (el) => el.open,
1447
+ write: (el, v) => el.open = Boolean(v)
1448
+ };
1449
+ case "this": return {
1450
+ event: null,
1451
+ read: () => void 0,
1452
+ write: () => {}
1453
+ };
1454
+ case "group": {
1455
+ const isCheckbox = input.type === "checkbox";
1456
+ return {
1457
+ event: "change",
1458
+ read: (el) => {
1459
+ const i = el;
1460
+ if (isCheckbox) return {
1461
+ __ilhaGroup: true,
1462
+ value: i.value,
1463
+ checked: i.checked
1465
1464
  };
1466
1465
  return i.checked ? i.value : void 0;
1467
1466
  },
@@ -1520,7 +1519,7 @@ function applyJsxEvents({ host, records, reportError, unmountSignal, onceFired }
1520
1519
  invocationController?.abort();
1521
1520
  invocationController = new AbortController();
1522
1521
  }
1523
- const signal = invocationController ? anySignal([listenerController.signal, invocationController.signal]) : listenerController.signal;
1522
+ const signal = invocationController ? AbortSignal.any([listenerController.signal, invocationController.signal]) : listenerController.signal;
1524
1523
  let result;
1525
1524
  startBatch();
1526
1525
  try {
@@ -1619,47 +1618,6 @@ function applyTemplateBindings(host, binds, phase = "all") {
1619
1618
  }
1620
1619
  return () => cleanups.forEach((c) => c());
1621
1620
  }
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
1621
  const globalErrorHandlers = /* @__PURE__ */ new Set();
1664
1622
  function reportToGlobal(error, source) {
1665
1623
  if (globalErrorHandlers.size === 0) return false;
@@ -1672,9 +1630,7 @@ function reportToGlobal(error, source) {
1672
1630
  }
1673
1631
  /**
1674
1632
  * 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.
1633
+ * and has no local onError() handler. Returns an unsubscribe function.
1678
1634
  */
1679
1635
  function onUncaughtError(fn) {
1680
1636
  globalErrorHandlers.add(fn);
@@ -1682,1238 +1638,1256 @@ function onUncaughtError(fn) {
1682
1638
  globalErrorHandlers.delete(fn);
1683
1639
  };
1684
1640
  }
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) {
1694
- if (isStandardSchema(initialOrSchema)) schema = initialOrSchema;
1695
- else defaultInput = initialOrSchema;
1696
- }
1697
- return new IlhaBuilder({
1698
- schema,
1699
- defaultInput,
1700
- states: [],
1701
- streams: [],
1702
- deriveds: [],
1703
- actions: [],
1704
- ons: [],
1705
- effects: [],
1706
- onMounts: [],
1707
- onErrors: [],
1708
- transition: null,
1709
- css: null,
1710
- as: null
1711
- });
1712
- }
1713
- as(tag) {
1714
- return new IlhaBuilder({
1715
- ...this._cfg,
1716
- as: assertValidSlotTagName(tag)
1717
- });
1718
- }
1719
- state(key, init) {
1720
- const cfg = this._cfg;
1721
- return new IlhaBuilder({
1722
- ...cfg,
1723
- states: [...cfg.states, {
1724
- key,
1725
- init
1726
- }]
1727
- });
1728
- }
1729
- stream(key, fn) {
1730
- const cfg = this._cfg;
1731
- const states = cfg.states.some((entry) => entry.key === key) ? cfg.states : [...cfg.states, {
1732
- key,
1733
- init: void 0
1734
- }];
1735
- return new IlhaBuilder({
1736
- ...cfg,
1737
- states,
1738
- streams: [...cfg.streams.filter((entry) => entry.key !== key), {
1739
- key,
1740
- fn
1741
- }]
1742
- });
1743
- }
1744
- derived(key, fn) {
1745
- const cfg = this._cfg;
1746
- return new IlhaBuilder({
1747
- ...cfg,
1748
- deriveds: [...cfg.deriveds, {
1749
- key,
1750
- fn
1751
- }]
1752
- });
1641
+ const FRAME_STACK = Symbol.for("ilha.primitiveFrameStack");
1642
+ function frameStack() {
1643
+ const g = globalThis;
1644
+ return g[FRAME_STACK] ??= [];
1645
+ }
1646
+ function currentFrame() {
1647
+ const stack = frameStack();
1648
+ return stack[stack.length - 1];
1649
+ }
1650
+ function withFrame(frame, fn) {
1651
+ frameStack().push(frame);
1652
+ try {
1653
+ return fn();
1654
+ } finally {
1655
+ frameStack().pop();
1753
1656
  }
1754
- action(key, fn) {
1755
- return new IlhaBuilder({
1756
- ...this._cfg,
1757
- actions: [...this._cfg.actions, {
1758
- key,
1759
- fn
1760
- }]
1761
- });
1657
+ }
1658
+ function freshFrame(options) {
1659
+ return {
1660
+ slots: [],
1661
+ cursor: 0,
1662
+ creating: true,
1663
+ ssr: options?.ssr ?? false,
1664
+ instance: options?.instance ?? null,
1665
+ stateIndex: 0,
1666
+ derivedIndex: 0,
1667
+ actionIndex: 0,
1668
+ pendingDerived: options?.ssr ? [] : null,
1669
+ actionManifest: options?.ssr ? /* @__PURE__ */ new Map() : null
1670
+ };
1671
+ }
1672
+ const PRIMITIVE_NAMES = {
1673
+ state: "state",
1674
+ derived: "derived",
1675
+ action: "action",
1676
+ effect: "effect",
1677
+ once: "effect.once",
1678
+ error: "onError"
1679
+ };
1680
+ /** Reserve the next slot for a primitive call. Throws outside an island
1681
+ * render; warns in dev on hook count/kind drift during re-renders. */
1682
+ function acquireSlot(kind) {
1683
+ const frame = currentFrame();
1684
+ 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.`);
1685
+ const index = frame.cursor++;
1686
+ if (frame.creating) return void 0;
1687
+ const existing = frame.slots[index];
1688
+ return existing && existing.kind === kind ? existing : void 0;
1689
+ }
1690
+ /**
1691
+ * Declare island-local reactive state at this call position. The initializer
1692
+ * applies only when the instance is created — later renders reuse the same
1693
+ * underlying signal, so prop-driven initializers never reset user state.
1694
+ *
1695
+ * A function argument is treated as a lazy initializer:
1696
+ * const count = state(() => expensiveInitialValue());
1697
+ * To store a function VALUE, return it from the updater wrapper on write:
1698
+ * setCallback(() => nextCallback);
1699
+ */
1700
+ function state(init) {
1701
+ const frame = currentFrame();
1702
+ const existing = acquireSlot("state");
1703
+ if (existing) return existing.acc;
1704
+ const f = frame;
1705
+ if (!f.creating) return ilhaSignal(void 0);
1706
+ const index = f.stateIndex++;
1707
+ const snap = f.ssr ? void 0 : f.instance?.stateSnapshot?.s?.[index];
1708
+ const initial = snap === void 0 ? typeof init === "function" ? init() : init : snap;
1709
+ const s = signal(initial);
1710
+ const acc = markSignalAccessor((...args) => {
1711
+ if (args.length === 0) return s();
1712
+ const next = resolveSignalSetter(() => s(), args[0]);
1713
+ s(next);
1714
+ });
1715
+ f.slots[f.cursor - 1] = {
1716
+ kind: "state",
1717
+ acc
1718
+ };
1719
+ return acc;
1720
+ }
1721
+ /** True for async iterables (incl. async generators). */
1722
+ function isAsyncIterable(value) {
1723
+ return value !== null && typeof value[Symbol.asyncIterator] === "function";
1724
+ }
1725
+ /**
1726
+ * Declare derived state at this call position: synchronous computations,
1727
+ * Promises, or async generators. Reads track their dependencies; async work
1728
+ * races latest-run-wins and aborts stale runs through `ctx.signal`.
1729
+ */
1730
+ function derived(fn) {
1731
+ const frame = currentFrame();
1732
+ const existing = acquireSlot("derived");
1733
+ const f = frame;
1734
+ if (existing) {
1735
+ const slot = existing;
1736
+ slot.fn = fn;
1737
+ if (f.ssr && !f.creating && !slot.ssrAsync) {
1738
+ const ac = new AbortController();
1739
+ const prevSub = setActiveSub(void 0);
1740
+ try {
1741
+ const result = slot.fn({ signal: ac.signal });
1742
+ if (!(result instanceof Promise) && !isAsyncIterable(result)) slot.env({
1743
+ loading: false,
1744
+ value: result,
1745
+ error: void 0
1746
+ });
1747
+ } catch (err) {
1748
+ slot.env({
1749
+ loading: false,
1750
+ value: void 0,
1751
+ error: err instanceof Error ? err : new Error(String(err))
1752
+ });
1753
+ } finally {
1754
+ setActiveSub(prevSub);
1755
+ }
1756
+ }
1757
+ return slot.acc;
1762
1758
  }
1763
- on(selectorOrCombined, handler) {
1764
- const parsed = parseOnArgs(selectorOrCombined);
1765
- return new IlhaBuilder({
1766
- ...this._cfg,
1767
- ons: [...this._cfg.ons, {
1768
- selector: parsed.selector,
1769
- event: parsed.eventType,
1770
- options: parsed.options,
1771
- abortable: parsed.abortable,
1772
- handler
1773
- }]
1774
- });
1759
+ if (!f.creating) return defaultDerivedAccessor();
1760
+ const index = f.derivedIndex++;
1761
+ const env = signal({
1762
+ loading: false,
1763
+ value: void 0,
1764
+ error: void 0
1765
+ });
1766
+ let fromSnapshot = false;
1767
+ if (!f.ssr) {
1768
+ const snap = f.instance?.stateSnapshot?.d?.[index];
1769
+ if (snap) {
1770
+ env({ ...snap });
1771
+ fromSnapshot = true;
1772
+ }
1775
1773
  }
1776
- effect(fn) {
1777
- return new IlhaBuilder({
1778
- ...this._cfg,
1779
- effects: [...this._cfg.effects, { fn }]
1774
+ const accessor = createDerivedAccessor(() => env(), (value) => {
1775
+ const prevSub = setActiveSub(void 0);
1776
+ try {
1777
+ env({
1778
+ loading: false,
1779
+ value,
1780
+ error: void 0
1781
+ });
1782
+ } finally {
1783
+ setActiveSub(prevSub);
1784
+ }
1785
+ });
1786
+ const slot = {
1787
+ kind: "derived",
1788
+ env,
1789
+ acc: accessor,
1790
+ fn,
1791
+ ssrAsync: false,
1792
+ fromSnapshot
1793
+ };
1794
+ if (f.ssr) {
1795
+ const ac = new AbortController();
1796
+ const prevSub = setActiveSub(void 0);
1797
+ let result;
1798
+ let threw = false;
1799
+ let syncError;
1800
+ try {
1801
+ result = slot.fn({ signal: ac.signal });
1802
+ } catch (err) {
1803
+ threw = true;
1804
+ syncError = err;
1805
+ } finally {
1806
+ setActiveSub(prevSub);
1807
+ }
1808
+ let pull = null;
1809
+ if (threw) env({
1810
+ loading: false,
1811
+ value: void 0,
1812
+ error: syncError instanceof Error ? syncError : new Error(String(syncError))
1780
1813
  });
1781
- }
1782
- onMount(fn) {
1783
- return new IlhaBuilder({
1784
- ...this._cfg,
1785
- onMounts: [...this._cfg.onMounts, { fn }]
1814
+ else if (result instanceof Promise) {
1815
+ env({
1816
+ loading: true,
1817
+ value: void 0,
1818
+ error: void 0
1819
+ });
1820
+ pull = result.then((value) => env({
1821
+ loading: false,
1822
+ value,
1823
+ error: void 0
1824
+ }), (err) => env({
1825
+ loading: false,
1826
+ value: void 0,
1827
+ error: err instanceof Error ? err : new Error(String(err))
1828
+ }));
1829
+ result.catch(() => {});
1830
+ } else if (isAsyncIterable(result)) {
1831
+ env({
1832
+ loading: true,
1833
+ value: void 0,
1834
+ error: void 0
1835
+ });
1836
+ const iterable = result;
1837
+ pull = (async () => {
1838
+ const it = iterable[Symbol.asyncIterator]();
1839
+ try {
1840
+ const first = await it.next();
1841
+ env({
1842
+ loading: false,
1843
+ value: first.done ? void 0 : first.value,
1844
+ error: void 0
1845
+ });
1846
+ } finally {
1847
+ await it.return?.(void 0);
1848
+ }
1849
+ })();
1850
+ } else env({
1851
+ loading: false,
1852
+ value: result,
1853
+ error: void 0
1786
1854
  });
1787
- }
1788
- onError(fn) {
1789
- return new IlhaBuilder({
1790
- ...this._cfg,
1791
- onErrors: [...this._cfg.onErrors, { fn }]
1855
+ slot.ssrAsync = pull !== null;
1856
+ if (pull) f.pendingDerived.push({
1857
+ index,
1858
+ promise: pull.catch(() => {})
1792
1859
  });
1793
1860
  }
1794
- transition(opts) {
1795
- return new IlhaBuilder({
1796
- ...this._cfg,
1797
- transition: opts
1861
+ f.slots[f.cursor - 1] = slot;
1862
+ return accessor;
1863
+ }
1864
+ /**
1865
+ * Declare a reactive operation at this call position. Use plain functions
1866
+ * for ordinary operations; action() adds pending/data/error tracking,
1867
+ * concurrent-invocation bookkeeping, and lifecycle cancellation.
1868
+ */
1869
+ function action(fn) {
1870
+ const frame = currentFrame();
1871
+ const existing = acquireSlot("action");
1872
+ const f = frame;
1873
+ if (existing) return existing.acc;
1874
+ if (!f.creating) {
1875
+ const noop = (() => {});
1876
+ Object.defineProperties(noop, {
1877
+ pending: { get: () => false },
1878
+ data: { get: () => void 0 },
1879
+ error: { get: () => void 0 }
1798
1880
  });
1799
- }
1800
- css(strings, ...values) {
1801
- let source;
1802
- if (typeof strings === "string") source = strings;
1803
- else {
1804
- let result = "";
1805
- for (let i = 0; i < strings.length; i++) {
1806
- result += strings[i];
1807
- if (i < values.length) result += String(values[i]);
1881
+ attachWith(noop);
1882
+ return noop;
1883
+ }
1884
+ const manifestId = `a${f.actionIndex++}`;
1885
+ if (f.ssr) {
1886
+ const invoke = (..._callArgs) => {};
1887
+ invoke[SSR_ACTION_STUB] = true;
1888
+ invoke[ACTION_MANIFEST_ID] = manifestId;
1889
+ Object.defineProperties(invoke, {
1890
+ pending: {
1891
+ get: () => false,
1892
+ enumerable: true
1893
+ },
1894
+ data: {
1895
+ get: () => void 0,
1896
+ enumerable: true
1897
+ },
1898
+ error: {
1899
+ get: () => void 0,
1900
+ enumerable: true
1808
1901
  }
1809
- source = result;
1810
- }
1811
- return new IlhaBuilder({
1812
- ...this._cfg,
1813
- css: source
1814
1902
  });
1815
- }
1816
- render(fn) {
1817
- const { schema, defaultInput, states, streams, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1818
- const stylePrefix = cssSource == null ? "" : buildScopedStyle(cssSource);
1819
- const configuredSlotTag = slotAs ?? "div";
1820
- function resolveInput(props) {
1821
- const merged = {
1822
- ...defaultInput ?? {},
1823
- ...props ?? {}
1824
- };
1825
- if (!schema) return merged;
1826
- return validateSchema(schema, merged);
1827
- }
1828
- function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
1829
- const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
1830
- try {
1831
- const html = unwrapHtml(render());
1832
- const slots = ctx.slots;
1833
- const binds = ctx.binds;
1834
- const events = ctx.events;
1835
- if (ctx.pending && ctx.pending.size > 0) {
1836
- const pending = ctx.pending;
1837
- return (async () => {
1838
- return {
1839
- html: await resolveAsyncChildren(html, pending),
1840
- slots,
1841
- binds,
1842
- events
1843
- };
1844
- })();
1845
- }
1846
- return {
1847
- html,
1848
- slots,
1849
- binds,
1850
- events
1851
- };
1852
- } finally {
1853
- popRenderCtx();
1854
- }
1903
+ f.actionManifest.set(invoke, manifestId);
1904
+ f.slots[f.cursor - 1] = {
1905
+ kind: "action",
1906
+ acc: invoke,
1907
+ fn
1908
+ };
1909
+ const ssrAccessor = invoke;
1910
+ attachWith(ssrAccessor);
1911
+ return ssrAccessor;
1912
+ }
1913
+ const instance = f.instance;
1914
+ const envelope = signal({
1915
+ pending: 0,
1916
+ data: void 0,
1917
+ error: void 0
1918
+ });
1919
+ const readEnvelope = () => untrack(() => envelope());
1920
+ let latestRun = 0;
1921
+ const invoke = (payload) => {
1922
+ if (currentRenderCtx()) return;
1923
+ if (instance.disposed) return;
1924
+ const run = ++latestRun;
1925
+ const runSignal = instance.unmountController.signal;
1926
+ let result;
1927
+ let isAsync = false;
1928
+ startBatch();
1929
+ try {
1930
+ result = slot.fn(payload, { signal: runSignal });
1931
+ isAsync = result != null && typeof result.then === "function";
1932
+ const current = readEnvelope();
1933
+ envelope(isAsync ? {
1934
+ pending: current.pending + 1,
1935
+ data: current.data,
1936
+ error: void 0
1937
+ } : {
1938
+ pending: current.pending,
1939
+ data: result,
1940
+ error: void 0
1941
+ });
1942
+ } catch (error) {
1943
+ const normalized = error instanceof Error ? error : new Error(String(error));
1944
+ envelope({
1945
+ pending: readEnvelope().pending,
1946
+ data: void 0,
1947
+ error: normalized
1948
+ });
1949
+ instance.reportError(normalized, "action");
1950
+ return;
1951
+ } finally {
1952
+ endBatch();
1855
1953
  }
1856
- function buildPlainState(input) {
1857
- const state = {};
1858
- for (const entry of states) {
1859
- const value = typeof entry.init === "function" ? entry.init(input) : entry.init;
1860
- const accessor = markSignalAccessor((...args) => {
1861
- if (args.length === 0) return value;
1954
+ if (!isAsync) return;
1955
+ Promise.resolve(result).then((data) => {
1956
+ if (instance.disposed || runSignal.aborted || run !== latestRun) return;
1957
+ const value = readEnvelope();
1958
+ envelope({
1959
+ pending: value.pending,
1960
+ data,
1961
+ error: void 0
1962
+ });
1963
+ }).catch((error) => {
1964
+ if (instance.disposed || runSignal.aborted || error?.name === "AbortError") return;
1965
+ const normalized = error instanceof Error ? error : new Error(String(error));
1966
+ if (run === latestRun) {
1967
+ const value = readEnvelope();
1968
+ envelope({
1969
+ pending: value.pending,
1970
+ data: void 0,
1971
+ error: normalized
1862
1972
  });
1863
- state[entry.key] = accessor;
1864
1973
  }
1865
- return state;
1974
+ instance.reportError(normalized, "action");
1975
+ }).finally(() => {
1976
+ if (instance.disposed) return;
1977
+ const value = readEnvelope();
1978
+ envelope({
1979
+ ...value,
1980
+ pending: Math.max(0, value.pending - 1)
1981
+ });
1982
+ });
1983
+ };
1984
+ const slot = {
1985
+ kind: "action",
1986
+ acc: invoke,
1987
+ fn
1988
+ };
1989
+ Object.defineProperties(invoke, {
1990
+ pending: {
1991
+ get: () => envelope().pending > 0,
1992
+ enumerable: true
1993
+ },
1994
+ data: {
1995
+ get: () => envelope().data,
1996
+ enumerable: true
1997
+ },
1998
+ error: {
1999
+ get: () => envelope().error,
2000
+ enumerable: true
1866
2001
  }
1867
- function buildSignalState(input, snapshot) {
1868
- const state = {};
1869
- for (const entry of states) {
1870
- const initial = snapshot && entry.key in snapshot ? snapshot[entry.key] : typeof entry.init === "function" ? entry.init(input) : entry.init;
1871
- const s = signal(initial);
1872
- const accessor = markSignalAccessor((...args) => {
1873
- if (args.length === 0) return s();
1874
- s(resolveSignalSetter(() => s(), args[0]));
1875
- });
1876
- state[entry.key] = accessor;
2002
+ });
2003
+ f.slots[f.cursor - 1] = slot;
2004
+ instance.actionIds.set(slot.acc, manifestId);
2005
+ const clientAccessor = invoke;
2006
+ clientAccessor[ACTION_MANIFEST_ID] = manifestId;
2007
+ attachWith(clientAccessor);
2008
+ return clientAccessor;
2009
+ }
2010
+ /**
2011
+ * Reactive side effect. Inside an island render, registers an ordered effect
2012
+ * slot that tracks signals read in the body, reruns on change, cleans up
2013
+ * before rerun and on unmount, and is client-only. Outside an island render,
2014
+ * behaves as the standalone reactive effect and returns a stop function.
2015
+ *
2016
+ * @returns Inside an island render, `void` (an effect slot is registered and
2017
+ * cleaned up automatically). At module scope, a stop function that disposes
2018
+ * the effect and runs its final cleanup. The union return type is deliberate:
2019
+ * the same call shape cannot be discriminated at the call site.
2020
+ */
2021
+ const effect = Object.assign((fn) => {
2022
+ const frame = currentFrame();
2023
+ if (!frame) {
2024
+ let cleanup;
2025
+ let controller = null;
2026
+ const runCleanup = () => {
2027
+ if (typeof cleanup === "function") {
2028
+ try {
2029
+ cleanup();
2030
+ } catch (err) {
2031
+ console.error(err);
2032
+ }
2033
+ cleanup = void 0;
1877
2034
  }
1878
- return state;
1879
- }
1880
- function buildSsrActions() {
1881
- const out = {};
1882
- const lookup = /* @__PURE__ */ new Map();
1883
- for (const entry of actions) {
1884
- const invoke = (...callArgs) => {
1885
- const frame = captureStack[captureStack.length - 1];
1886
- if (frame) {
1887
- if (!frame.some((r) => r.k === entry.key)) frame.push({
1888
- k: entry.key,
1889
- a: callArgs
1890
- });
1891
- return;
1892
- }
1893
- };
1894
- invoke[SSR_ACTION_STUB] = true;
1895
- Object.defineProperties(invoke, {
1896
- pending: {
1897
- get: () => false,
1898
- enumerable: true
1899
- },
1900
- data: {
1901
- get: () => void 0,
1902
- enumerable: true
1903
- },
1904
- error: {
1905
- get: () => void 0,
1906
- enumerable: true
1907
- }
1908
- });
1909
- out[entry.key] = invoke;
1910
- lookup.set(invoke, entry.key);
2035
+ };
2036
+ const stop = ilhaEffect(() => {
2037
+ runCleanup();
2038
+ if (controller) controller.abort();
2039
+ controller = new AbortController();
2040
+ startBatch();
2041
+ try {
2042
+ cleanup = fn({ signal: controller.signal });
2043
+ } catch (err) {
2044
+ console.error(err);
2045
+ } finally {
2046
+ endBatch();
2047
+ }
2048
+ });
2049
+ return () => {
2050
+ stop();
2051
+ runCleanup();
2052
+ controller?.abort();
2053
+ };
2054
+ }
2055
+ const existing = acquireSlot("effect");
2056
+ if (existing) {
2057
+ existing.fn = fn;
2058
+ return;
2059
+ }
2060
+ if (!frame.creating) return;
2061
+ frame.slots[frame.cursor - 1] = {
2062
+ kind: "effect",
2063
+ fn
2064
+ };
2065
+ }, { once(fn) {
2066
+ if (acquireSlot("once")) return;
2067
+ const f = currentFrame();
2068
+ 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.");
2069
+ if (!f.creating) return;
2070
+ f.slots[f.cursor - 1] = {
2071
+ kind: "once",
2072
+ fn
2073
+ };
2074
+ } });
2075
+ /**
2076
+ * Register an error handler slot for this island. Handlers run in
2077
+ * declaration order; state, derived values, actions, and props should be
2078
+ * accessed through lexical closure rather than the context object.
2079
+ */
2080
+ function onError(fn) {
2081
+ if (!currentFrame()) throw new Error("[ilha] onError() called outside an island render. Register it while an island component is rendering.");
2082
+ const existing = acquireSlot("error");
2083
+ if (existing) {
2084
+ existing.fn = fn;
2085
+ return;
2086
+ }
2087
+ const f = currentFrame();
2088
+ if (!f || !f.creating) return;
2089
+ f.slots[f.cursor - 1] = {
2090
+ kind: "error",
2091
+ fn
2092
+ };
2093
+ }
2094
+ function parseStateSnapshot(raw) {
2095
+ const parsed = safeParseSnapshot(raw, STATE_ATTR);
2096
+ if (parsed === void 0) return void 0;
2097
+ const rec = parsed;
2098
+ if (rec["v"] !== 2) return;
2099
+ const out = { skipOnce: rec["_skipOnMount"] === true };
2100
+ if (Array.isArray(rec["s"])) {
2101
+ stripUnsafeKeys(rec["s"]);
2102
+ out.s = rec["s"];
2103
+ }
2104
+ if (Array.isArray(rec["d"])) {
2105
+ stripUnsafeKeys(rec["d"]);
2106
+ out.d = rec["d"].map((entry) => {
2107
+ const e = entry ?? {};
2108
+ const errorValue = e.error;
2109
+ return {
2110
+ loading: e.loading === true,
2111
+ value: e.value,
2112
+ error: errorValue == null ? void 0 : errorValue instanceof Error ? errorValue : new Error(String(errorValue))
2113
+ };
2114
+ });
2115
+ }
2116
+ return out;
2117
+ }
2118
+ const ilha = ((...args) => {
2119
+ let schema = null;
2120
+ let component;
2121
+ let options;
2122
+ const first = args[0];
2123
+ const second = args[1];
2124
+ if (isStandardSchema(first)) {
2125
+ schema = first;
2126
+ component = second;
2127
+ options = args[2];
2128
+ } else {
2129
+ component = first;
2130
+ options = second;
2131
+ }
2132
+ if (typeof component !== "function") throw new Error("[ilha] ilha() requires a component function.");
2133
+ const configuredSlotTag = options?.as === void 0 ? "div" : assertValidSlotTagName(options.as);
2134
+ function resolveInput(props) {
2135
+ const merged = { ...props };
2136
+ if (!schema) return merged;
2137
+ return validateSchema(schema, merged);
2138
+ }
2139
+ function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
2140
+ const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
2141
+ try {
2142
+ const html = unwrapHtml(render());
2143
+ const slots = ctx.slots;
2144
+ const binds = ctx.binds;
2145
+ const events = ctx.events;
2146
+ if (ctx.pending && ctx.pending.size > 0) {
2147
+ const pending = ctx.pending;
2148
+ return (async () => {
2149
+ return {
2150
+ html: await resolveAsyncChildren(html, pending),
2151
+ slots,
2152
+ binds,
2153
+ events
2154
+ };
2155
+ })();
1911
2156
  }
1912
2157
  return {
1913
- action: out,
1914
- lookup
2158
+ html,
2159
+ slots,
2160
+ binds,
2161
+ events
1915
2162
  };
2163
+ } finally {
2164
+ popRenderCtx();
1916
2165
  }
1917
- function renderToString(props, sync = false, streamOut, eventsOut, stateOverride, forceAsyncChildren = false) {
1918
- const input = resolveInput(props);
1919
- const state = buildSignalState(input, stateOverride);
1920
- const { action, lookup: actionLookup } = buildSsrActions();
1921
- const recordEventsManifest = (events) => {
1922
- if (!eventsOut || events.length === 0) return;
1923
- events.forEach((record, index) => {
1924
- if (record.capture) {
1925
- eventsOut.set(`${record.type}:${index}`, record.capture);
1926
- return;
1927
- }
1928
- const key = actionLookup.get(record.handler);
1929
- if (key) eventsOut.set(`${record.type}:${index}`, key);
2166
+ }
2167
+ /** Invoke the component inside the given frame + render context. */
2168
+ function runInFrame(frame, props) {
2169
+ return () => withFrame(frame, () => component(props));
2170
+ }
2171
+ function recordEventsManifest(eventsOut, events, frame) {
2172
+ if (!eventsOut || events.length === 0) return;
2173
+ events.forEach((record, index) => {
2174
+ const id = frame.actionManifest?.get(record.handler) ?? actionManifestId(record.handler);
2175
+ if (!id) return;
2176
+ const args = actionBoundArgs(record.handler);
2177
+ eventsOut.set(`${record.type}:${index}`, Array.isArray(args) ? {
2178
+ k: id,
2179
+ a: args
2180
+ } : id);
2181
+ });
2182
+ }
2183
+ function renderToString(props, sync = false, eventsOut, forceAsyncChildren = false) {
2184
+ const input = resolveInput(props);
2185
+ const frame = freshFrame({ ssr: true });
2186
+ const first = forceAsyncChildren ? renderWithCtx({
2187
+ render: runInFrame(frame, input),
2188
+ asyncChildren: true,
2189
+ manifest: eventsOut !== void 0
2190
+ }) : renderWithCtx({
2191
+ render: runInFrame(frame, input),
2192
+ manifest: eventsOut !== void 0
2193
+ });
2194
+ const finish = (firstHtml) => firstHtml.html;
2195
+ const needsAsync = forceAsyncChildren || (frame.pendingDerived?.length ?? 0) > 0;
2196
+ if (first instanceof Promise) return first.then((out) => {
2197
+ recordEventsManifest(eventsOut, out.events, frame);
2198
+ return needsAsync ? awaitPending(out) : out.html;
2199
+ });
2200
+ recordEventsManifest(eventsOut, first.events, frame);
2201
+ if (!needsAsync || sync) {
2202
+ if (sync && (frame.pendingDerived?.length ?? 0) > 0 && __DEV__);
2203
+ return finish(first);
2204
+ }
2205
+ return awaitPending(first);
2206
+ function awaitPending(out) {
2207
+ const pendings = frame.pendingDerived ?? [];
2208
+ if (pendings.length === 0) return Promise.resolve(out.html);
2209
+ return Promise.all(pendings.map((p) => p.promise)).then(() => {
2210
+ frame.creating = false;
2211
+ frame.cursor = 0;
2212
+ const second = forceAsyncChildren ? renderWithCtx({
2213
+ render: runInFrame(frame, input),
2214
+ asyncChildren: true,
2215
+ manifest: eventsOut !== void 0
2216
+ }) : renderWithCtx({
2217
+ render: runInFrame(frame, input),
2218
+ manifest: eventsOut !== void 0
1930
2219
  });
1931
- };
1932
- const pullStreamInitials = async (streamOut) => {
1933
- for (const entry of streams) {
1934
- const controller = new AbortController();
1935
- try {
1936
- const gen = await entry.fn({
1937
- input,
1938
- signal: controller.signal
1939
- });
1940
- const first = await gen.next();
1941
- controller.abort();
1942
- await gen.return?.(void 0);
1943
- if (first.done) continue;
1944
- if (streamOut) streamOut[entry.key] = first.value;
1945
- const prevSub = setActiveSub(void 0);
1946
- try {
1947
- const setter = state[entry.key];
1948
- setter?.(first.value);
1949
- } finally {
1950
- setActiveSub(prevSub);
1951
- }
1952
- } catch {}
1953
- }
1954
- };
1955
- const computeDerivedResults = () => deriveds.map((entry) => {
2220
+ const done = (o) => {
2221
+ recordEventsManifest(eventsOut, o.events, frame);
2222
+ return o.html;
2223
+ };
2224
+ return second instanceof Promise ? second.then(done) : done(second);
2225
+ });
2226
+ }
2227
+ }
2228
+ function mountIsland(host, props) {
2229
+ return mountIslandInternal(host, props).unmount;
2230
+ }
2231
+ function mountIslandInternal(host, props) {
2232
+ if (props === void 0) {
2233
+ const rawProps = host.getAttribute(PROPS_ATTR);
2234
+ if (rawProps) {
2235
+ const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
2236
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
2237
+ }
2238
+ }
2239
+ const inputSignal = signal(resolveInput(props));
2240
+ const derivedPropsVersion = signal(0);
2241
+ const input = new Proxy({}, {
2242
+ get(_t, key) {
2243
+ return inputSignal()[key];
2244
+ },
2245
+ has(_t, key) {
2246
+ return key in inputSignal();
2247
+ },
2248
+ ownKeys() {
1956
2249
  const prevSub = setActiveSub(void 0);
1957
2250
  try {
1958
- return {
1959
- key: entry.key,
1960
- result: entry.fn({
1961
- state,
1962
- input,
1963
- signal: new AbortController().signal
1964
- })
1965
- };
1966
- } catch (err) {
1967
- return {
1968
- key: entry.key,
1969
- result: Promise.reject(err)
1970
- };
2251
+ return Reflect.ownKeys(inputSignal());
1971
2252
  } finally {
1972
2253
  setActiveSub(prevSub);
1973
2254
  }
1974
- });
1975
- if (!sync && (streams.length > 0 || forceAsyncChildren)) return pullStreamInitials(streamOut).then(() => renderToStringTail());
1976
- return renderToStringTail();
1977
- function renderToStringTail() {
1978
- const results = computeDerivedResults();
1979
- if (!(forceAsyncChildren || results.some((r) => r.result instanceof Promise)) || sync) {
1980
- const envelopes = {};
1981
- for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
1982
- loading: true,
1983
- value: void 0,
1984
- error: void 0
1985
- };
1986
- else envelopes[r.key] = {
1987
- loading: false,
1988
- value: r.result,
1989
- error: void 0
1990
- };
1991
- const derived = buildDerivedAccessors(envelopes);
1992
- const prevSub = setActiveSub(void 0);
1993
- try {
1994
- const { html, events } = renderWithCtx({
1995
- render: () => fn({
1996
- state,
1997
- derived,
1998
- action,
1999
- input
2000
- }),
2001
- manifest: eventsOut !== void 0
2002
- });
2003
- recordEventsManifest(events);
2004
- return stylePrefix + html;
2005
- } finally {
2006
- setActiveSub(prevSub);
2007
- }
2255
+ },
2256
+ getOwnPropertyDescriptor(_t, key) {
2257
+ const prevSub = setActiveSub(void 0);
2258
+ try {
2259
+ return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
2260
+ } finally {
2261
+ setActiveSub(prevSub);
2008
2262
  }
2009
- return Promise.all(results.map(async (r) => {
2010
- try {
2011
- return {
2012
- key: r.key,
2013
- envelope: {
2014
- loading: false,
2015
- value: await Promise.resolve(r.result),
2016
- error: void 0
2017
- }
2018
- };
2019
- } catch (err) {
2020
- return {
2021
- key: r.key,
2022
- envelope: {
2023
- loading: false,
2024
- value: void 0,
2025
- error: err instanceof Error ? err : new Error(String(err))
2026
- }
2027
- };
2028
- }
2029
- })).then(async (resolved) => {
2030
- const envelopes = {};
2031
- for (const r of resolved) envelopes[r.key] = r.envelope;
2032
- const derived = buildDerivedAccessors(envelopes);
2033
- const prevSub = setActiveSub(void 0);
2034
- try {
2035
- const { html, events } = await renderWithCtx({
2036
- render: () => fn({
2037
- state,
2038
- derived,
2039
- action,
2040
- input
2041
- }),
2042
- asyncChildren: true,
2043
- manifest: eventsOut !== void 0
2044
- });
2045
- recordEventsManifest(events);
2046
- return stylePrefix + html;
2047
- } finally {
2048
- setActiveSub(prevSub);
2049
- }
2263
+ }
2264
+ });
2265
+ const snapshotRaw = host.getAttribute(STATE_ATTR);
2266
+ const snapshot = snapshotRaw ? parseStateSnapshot(snapshotRaw) : void 0;
2267
+ const hydrated = snapshot != null;
2268
+ const shouldSkipOnce = snapshot?.skipOnce === true;
2269
+ const cleanups = [];
2270
+ const unmountController = new AbortController();
2271
+ cleanups.push(() => unmountController.abort());
2272
+ const errorSlots = [];
2273
+ const reportError = (err, source) => {
2274
+ const error = err instanceof Error ? err : new Error(String(err));
2275
+ if (errorSlots.length === 0) {
2276
+ if (!reportToGlobal(error, source)) console.error(error);
2277
+ return;
2278
+ }
2279
+ for (const slot of errorSlots) try {
2280
+ slot.fn({
2281
+ error,
2282
+ source,
2283
+ host
2050
2284
  });
2285
+ } catch (handlerErr) {
2286
+ console.error(handlerErr);
2051
2287
  }
2288
+ };
2289
+ const instance = {
2290
+ host,
2291
+ inputSignal,
2292
+ input,
2293
+ unmountController,
2294
+ cleanups,
2295
+ reportError,
2296
+ actionIds: /* @__PURE__ */ new Map(),
2297
+ stateSnapshot: snapshot ? {
2298
+ s: snapshot.s,
2299
+ d: snapshot.d
2300
+ } : void 0,
2301
+ hydrated,
2302
+ disposed: false,
2303
+ frame: null
2304
+ };
2305
+ cleanups.push(() => {
2306
+ instance.disposed = true;
2307
+ });
2308
+ const frame = freshFrame({
2309
+ ssr: false,
2310
+ instance
2311
+ });
2312
+ instance.frame = frame;
2313
+ const mountedSlots = /* @__PURE__ */ new Map();
2314
+ function teardownMountedSlot(id, entry) {
2315
+ mountedSlots.delete(id);
2316
+ entry.el.remove();
2317
+ entry.unmount();
2052
2318
  }
2053
- function mountIsland(host, props) {
2054
- return mountIslandInternal(host, props).unmount;
2319
+ function slotBelongsToHost(candidate) {
2320
+ let el = candidate.parentElement;
2321
+ while (el && el !== host) {
2322
+ if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
2323
+ el = el.parentElement;
2324
+ }
2325
+ return el === host;
2055
2326
  }
2056
- function mountIslandInternal(host, props) {
2057
- if (props === void 0) {
2058
- const rawProps = host.getAttribute(PROPS_ATTR);
2059
- if (rawProps) {
2060
- const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
2061
- if (parsed !== void 0) props = reviveSlotProps(parsed);
2062
- }
2327
+ function buildSlotIndex() {
2328
+ const index = /* @__PURE__ */ new Map();
2329
+ for (const candidate of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
2330
+ const id = candidate.getAttribute(SLOT_ATTR);
2331
+ if (id === null || index.has(id)) continue;
2332
+ if (slotBelongsToHost(candidate)) index.set(id, candidate);
2063
2333
  }
2064
- const inputSignal = signal(resolveInput(props));
2065
- const input = new Proxy({}, {
2066
- get(_t, key) {
2067
- return inputSignal()[key];
2068
- },
2069
- has(_t, key) {
2070
- return key in inputSignal();
2071
- },
2072
- ownKeys() {
2073
- const prevSub = setActiveSub(void 0);
2074
- try {
2075
- return Reflect.ownKeys(inputSignal());
2076
- } finally {
2077
- setActiveSub(prevSub);
2078
- }
2079
- },
2080
- getOwnPropertyDescriptor(_t, key) {
2081
- const prevSub = setActiveSub(void 0);
2082
- try {
2083
- return Reflect.getOwnPropertyDescriptor(inputSignal(), key);
2084
- } finally {
2085
- setActiveSub(prevSub);
2086
- }
2087
- }
2088
- });
2089
- let snapshotRaw;
2090
- const rawState = host.getAttribute(STATE_ATTR);
2091
- if (rawState) {
2092
- const parsed = safeParseSnapshot(rawState, STATE_ATTR);
2093
- if (parsed !== void 0) snapshotRaw = parsed;
2334
+ return index;
2335
+ }
2336
+ function pushUpdatedProps(nextSlots) {
2337
+ for (const [id, entry] of mountedSlots) {
2338
+ const next = nextSlots.get(id);
2339
+ if (next) entry.updateProps(next.props);
2094
2340
  }
2095
- const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount" && k !== "_streams")) : void 0;
2096
- const derivedSnapshotRaw = snapshotRaw?._derived;
2097
- let derivedSnapshot;
2098
- if (derivedSnapshotRaw) {
2099
- derivedSnapshot = {};
2100
- for (const [k, v] of Object.entries(derivedSnapshotRaw)) if (v.error && !(v.error instanceof Error)) derivedSnapshot[k] = {
2101
- ...v,
2102
- error: new Error(String(v.error))
2103
- };
2104
- else derivedSnapshot[k] = v;
2341
+ }
2342
+ function mountSlots(slotMap) {
2343
+ for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
2344
+ for (const [id, entry] of mountedSlots) {
2345
+ const next = slotMap.get(id);
2346
+ if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2105
2347
  }
2106
- const hydrated = snapshotRaw != null;
2107
- const snapshotHasIslandState = stateSnapshot != null && Object.keys(stateSnapshot).length > 0;
2108
- const shouldSkipOnMount = hydrated && snapshotRaw?.["_skipOnMount"] === true && snapshotHasIslandState;
2109
- const state = buildSignalState(input, stateSnapshot);
2110
- const cleanups = [];
2111
- const unmountController = new AbortController();
2112
- cleanups.push(() => unmountController.abort());
2113
- const { proxy: derived, setup: setupDerived } = createDerivedProxy({
2114
- entries: deriveds,
2115
- state,
2116
- input,
2117
- derivedSnapshot
2118
- });
2119
- let stopDerived = () => {};
2120
- let action = {};
2121
- function reportError(err, source) {
2122
- const error = err instanceof Error ? err : new Error(String(err));
2123
- if (onErrors.length === 0) {
2124
- if (!reportToGlobal(error, source)) console.error(error);
2125
- return;
2126
- }
2127
- for (const entry of onErrors) try {
2128
- entry.fn({
2129
- error,
2130
- source,
2131
- state,
2132
- derived,
2133
- action,
2134
- input,
2135
- host
2136
- });
2137
- } catch (handlerErr) {
2138
- console.error(handlerErr);
2348
+ let slotIndex = null;
2349
+ for (const [id, { island: childIsland, props: childProps }] of slotMap) {
2350
+ const existing = mountedSlots.get(id);
2351
+ if (existing) {
2352
+ existing.updateProps(childProps);
2353
+ continue;
2139
2354
  }
2140
- }
2141
- const actionEntries = actions;
2142
- const actionObject = {};
2143
- let disposed = false;
2144
- cleanups.push(() => {
2145
- disposed = true;
2146
- });
2147
- for (const entry of actionEntries) {
2148
- const envelope = signal({
2149
- pending: 0,
2150
- data: void 0,
2151
- error: void 0
2152
- });
2153
- const readEnvelope = () => untrack(() => envelope());
2154
- let latestRun = 0;
2155
- const invoke = (props) => {
2156
- if (currentRenderCtx()) return;
2157
- if (disposed) return;
2158
- const run = ++latestRun;
2159
- const runSignal = unmountController.signal;
2160
- const ctx = {
2161
- state,
2162
- derived,
2163
- input,
2164
- host,
2165
- signal: runSignal
2166
- };
2167
- let result;
2168
- let isAsync = false;
2169
- startBatch();
2170
- try {
2171
- result = entry.fn(props, ctx);
2172
- isAsync = result != null && typeof result.then === "function";
2173
- const current = readEnvelope();
2174
- envelope(isAsync ? {
2175
- pending: current.pending + 1,
2176
- data: current.data,
2177
- error: void 0
2178
- } : {
2179
- pending: current.pending,
2180
- data: result,
2181
- error: void 0
2182
- });
2183
- } catch (error) {
2184
- const normalized = error instanceof Error ? error : new Error(String(error));
2185
- envelope({
2186
- pending: readEnvelope().pending,
2187
- data: void 0,
2188
- error: normalized
2189
- });
2190
- reportError(normalized, "action");
2191
- return;
2192
- } finally {
2193
- endBatch();
2194
- }
2195
- if (!isAsync) return;
2196
- Promise.resolve(result).then((data) => {
2197
- if (disposed || runSignal.aborted || run !== latestRun) return;
2198
- const value = readEnvelope();
2199
- envelope({
2200
- pending: value.pending,
2201
- data,
2202
- error: void 0
2203
- });
2204
- }).catch((error) => {
2205
- if (disposed || runSignal.aborted || error?.name === "AbortError") return;
2206
- const normalized = error instanceof Error ? error : new Error(String(error));
2207
- if (run === latestRun) {
2208
- const value = readEnvelope();
2209
- envelope({
2210
- pending: value.pending,
2211
- data: void 0,
2212
- error: normalized
2213
- });
2214
- }
2215
- reportError(normalized, "action");
2216
- }).finally(() => {
2217
- if (disposed) return;
2218
- const value = readEnvelope();
2219
- envelope({
2220
- ...value,
2221
- pending: Math.max(0, value.pending - 1)
2222
- });
2223
- });
2224
- };
2225
- Object.defineProperties(invoke, {
2226
- pending: {
2227
- get: () => envelope().pending > 0,
2228
- enumerable: true
2229
- },
2230
- data: {
2231
- get: () => envelope().data,
2232
- enumerable: true
2233
- },
2234
- error: {
2235
- get: () => envelope().error,
2236
- enumerable: true
2355
+ if (slotIndex === null) slotIndex = buildSlotIndex();
2356
+ const slotEl = slotIndex.get(id) ?? null;
2357
+ if (!slotEl) continue;
2358
+ let slotProps = childProps;
2359
+ if (slotProps === void 0) {
2360
+ const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
2361
+ if (rawProps) {
2362
+ const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
2363
+ if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
2237
2364
  }
2238
- });
2239
- actionObject[entry.key] = invoke;
2240
- }
2241
- action = actionObject;
2242
- if (transition?.enter) try {
2243
- const result = transition.enter(host);
2244
- if (result instanceof Promise) result.catch((err) => reportError(err, "transition"));
2245
- } catch (err) {
2246
- reportError(err, "transition");
2247
- }
2248
- const mountedSlots = /* @__PURE__ */ new Map();
2249
- const leavingSlots = /* @__PURE__ */ new Map();
2250
- function teardownMountedSlot(id, entry) {
2251
- mountedSlots.delete(id);
2252
- const token = Symbol();
2253
- const result = entry.unmount();
2254
- const remove = (stub) => {
2255
- const leaving = leavingSlots.get(id);
2256
- if (!leaving || leaving.token !== token) return;
2257
- leaving.el.remove();
2258
- stub?.remove();
2259
- leavingSlots.delete(id);
2260
- };
2261
- if (result instanceof Promise) {
2262
- entry.el.removeAttribute(SLOT_ATTR);
2263
- entry.el.setAttribute(LEAVING_ATTR, "");
2264
- leavingSlots.set(id, {
2265
- el: entry.el,
2266
- token
2267
- });
2268
- return result.finally(() => remove());
2269
- }
2270
- entry.el.remove();
2271
- }
2272
- function slotBelongsToHost(candidate) {
2273
- let el = candidate.parentElement;
2274
- while (el && el !== host) {
2275
- if (el.hasAttribute(SLOT_ATTR) || el.hasAttribute("data-ilha")) return false;
2276
- el = el.parentElement;
2277
- }
2278
- return el === host;
2279
- }
2280
- function buildSlotIndex() {
2281
- const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
2282
- const index = /* @__PURE__ */ new Map();
2283
- for (const candidate of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
2284
- if (leavingEls.has(candidate)) continue;
2285
- const id = candidate.getAttribute(SLOT_ATTR);
2286
- if (id === null || index.has(id)) continue;
2287
- if (slotBelongsToHost(candidate)) index.set(id, candidate);
2288
2365
  }
2289
- return index;
2290
- }
2291
- /** Push live props from a fresh slot map into already-mounted children. */
2292
- function pushUpdatedProps(nextSlots) {
2293
- for (const [id, entry] of mountedSlots) {
2294
- const next = nextSlots.get(id);
2295
- if (next) entry.updateProps(next.props);
2366
+ const prevSub = setActiveSub(void 0);
2367
+ let handle;
2368
+ try {
2369
+ const internal = childIsland[ISLAND_MOUNT_INTERNAL];
2370
+ handle = internal ? internal(slotEl, slotProps) : {
2371
+ unmount: childIsland.mount(slotEl, slotProps),
2372
+ updateProps: () => {}
2373
+ };
2374
+ } finally {
2375
+ setActiveSub(prevSub);
2296
2376
  }
2377
+ mountedSlots.set(id, {
2378
+ el: slotEl,
2379
+ island: childIsland,
2380
+ ...handle
2381
+ });
2297
2382
  }
2298
- function mountSlots(slotMap) {
2299
- for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
2300
- for (const [id, entry] of mountedSlots) {
2301
- const next = slotMap.get(id);
2302
- if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2303
- }
2304
- let slotIndex = null;
2305
- for (const [id, { island: childIsland, props }] of slotMap) {
2306
- const existing = mountedSlots.get(id);
2307
- if (existing) {
2308
- existing.updateProps(props);
2309
- continue;
2310
- }
2311
- if (slotIndex === null) slotIndex = buildSlotIndex();
2312
- const slotEl = slotIndex.get(id) ?? null;
2313
- if (!slotEl) continue;
2314
- let slotProps = props;
2315
- if (slotProps === void 0) {
2316
- const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
2317
- if (rawProps) {
2318
- const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
2319
- if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
2320
- }
2321
- }
2322
- const prevSub = setActiveSub(void 0);
2323
- let handle;
2324
- try {
2325
- const internal = childIsland[ISLAND_MOUNT_INTERNAL];
2326
- handle = internal ? internal(slotEl, slotProps) : {
2327
- unmount: childIsland.mount(slotEl, slotProps),
2328
- updateProps: () => {}
2329
- };
2330
- } finally {
2331
- setActiveSub(prevSub);
2383
+ }
2384
+ const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2385
+ let stopJsxEvents = () => {};
2386
+ let stopBindings = () => {};
2387
+ /** Register derived scopes before the render effect so source updates
2388
+ * settle derived envelopes before a render can consume them. */
2389
+ function startDerivedScopes() {
2390
+ for (const slot of frame.slots) if (slot.kind === "derived") startDerivedScope(slot);
2391
+ }
2392
+ /** Register side-effecting scopes after the render effect so prop-change
2393
+ * renders refresh their closures before those scopes execute. */
2394
+ function startEffectScopes() {
2395
+ for (const slot of frame.slots) if (slot.kind === "effect") startEffectScope(slot);
2396
+ else if (slot.kind === "once") startOnceScope(slot);
2397
+ else if (slot.kind === "error") errorSlots.push(slot);
2398
+ }
2399
+ function startDerivedScope(slot) {
2400
+ let ac = new AbortController();
2401
+ let skipFirst = slot.fromSnapshot;
2402
+ const stopScope = effect$1(() => {
2403
+ ac.abort();
2404
+ ac = new AbortController();
2405
+ const currentAc = ac;
2406
+ derivedPropsVersion();
2407
+ let result;
2408
+ try {
2409
+ result = slot.fn({ signal: currentAc.signal });
2410
+ } catch (err) {
2411
+ if (skipFirst) {
2412
+ skipFirst = false;
2413
+ return;
2332
2414
  }
2333
- mountedSlots.set(id, {
2334
- el: slotEl,
2335
- island: childIsland,
2336
- ...handle
2415
+ writeEnvelope(currentAc, {
2416
+ loading: false,
2417
+ value: void 0,
2418
+ error: err instanceof Error ? err : new Error(String(err))
2337
2419
  });
2420
+ return;
2338
2421
  }
2339
- }
2340
- const listeners = [];
2341
- const firedOnce = /* @__PURE__ */ new Set();
2342
- const invocationControllers = /* @__PURE__ */ new WeakMap();
2343
- function attachListeners() {
2344
- for (const entry of ons) {
2345
- if (entry.options.once && firedOnce.has(entry)) continue;
2346
- let targets;
2347
- try {
2348
- targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector)).filter((element) => templateElementBelongsToHost(host, element));
2349
- } catch {
2350
- continue;
2351
- }
2352
- targets.forEach((listenerTarget) => {
2353
- const listener = (event) => {
2354
- if (entry.options.once) {
2355
- firedOnce.add(entry);
2356
- for (const l of listeners.filter((l) => l.entry === entry)) l.target.removeEventListener(l.type, l.fn, l.options);
2357
- listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
2358
- }
2359
- const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
2360
- const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
2361
- let handlerSignal;
2362
- if (entry.abortable) {
2363
- let entryMap = invocationControllers.get(entry);
2364
- if (!entryMap) {
2365
- entryMap = /* @__PURE__ */ new WeakMap();
2366
- invocationControllers.set(entry, entryMap);
2367
- }
2368
- const prev = entryMap.get(listenerTarget);
2369
- if (prev) prev.abort();
2370
- const invocationController = new AbortController();
2371
- entryMap.set(listenerTarget, invocationController);
2372
- handlerSignal = anySignal([unmountController.signal, invocationController.signal]);
2373
- } else handlerSignal = unmountController.signal;
2374
- let result;
2375
- startBatch();
2376
- try {
2377
- result = entry.handler({
2378
- state,
2379
- derived,
2380
- action,
2381
- input,
2382
- host,
2383
- target: handlerTarget,
2384
- event,
2385
- signal: handlerSignal
2386
- });
2387
- } catch (err) {
2388
- reportError(err, "on");
2389
- endBatch();
2390
- return;
2391
- }
2392
- endBatch();
2393
- if (result instanceof Promise) result.catch((err) => {
2394
- if (err && err.name === "AbortError") return;
2395
- reportError(err, "on");
2396
- });
2397
- };
2398
- const opts = {
2399
- ...entry.options,
2400
- once: false
2401
- };
2402
- listenerTarget.addEventListener(entry.event, listener, opts);
2403
- listeners.push({
2404
- target: listenerTarget,
2405
- type: entry.event,
2406
- fn: listener,
2407
- options: opts,
2408
- entry
2409
- });
2410
- });
2422
+ if (skipFirst) {
2423
+ skipFirst = false;
2424
+ if (result instanceof Promise) result.catch(() => {});
2425
+ return;
2411
2426
  }
2412
- }
2413
- function detachListeners() {
2414
- for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
2415
- listeners.length = 0;
2416
- }
2417
- function invokeOnMounts() {
2418
- for (const entry of onMounts) {
2419
- const prevSub = setActiveSub(void 0);
2420
- let userCleanup;
2421
- try {
2422
- userCleanup = entry.fn({
2423
- state,
2424
- derived,
2425
- action,
2426
- input,
2427
- host,
2428
- hydrated,
2429
- signal: unmountController.signal
2427
+ if (!(result instanceof Promise)) {
2428
+ if (isAsyncIterable(result)) {
2429
+ const iterable = result;
2430
+ writeEnvelope(currentAc, {
2431
+ loading: true,
2432
+ value: readEnv().value,
2433
+ error: void 0
2430
2434
  });
2431
- } catch (err) {
2432
- reportError(err, "mount");
2433
- } finally {
2434
- setActiveSub(prevSub);
2435
- }
2436
- if (userCleanup) {
2437
- const teardown = userCleanup;
2438
- cleanups.push(() => {
2435
+ (async () => {
2439
2436
  try {
2440
- teardown();
2437
+ for await (const value of iterable) if (writeEnvelope(currentAc, {
2438
+ loading: false,
2439
+ value,
2440
+ error: void 0
2441
+ })) break;
2441
2442
  } catch (err) {
2442
- reportError(err, "mount");
2443
+ writeEnvelope(currentAc, {
2444
+ loading: false,
2445
+ value: void 0,
2446
+ error: err instanceof Error ? err : new Error(String(err))
2447
+ });
2443
2448
  }
2444
- });
2449
+ })();
2450
+ return;
2445
2451
  }
2452
+ writeEnvelope(currentAc, {
2453
+ loading: false,
2454
+ value: result,
2455
+ error: void 0
2456
+ });
2457
+ return;
2446
2458
  }
2447
- }
2448
- const preserveSSRDom = hydrated && host.childNodes.length > 0;
2449
- if (hydrated && onMounts.length > 0 && !shouldSkipOnMount) invokeOnMounts();
2450
- const nativeOnceFired = /* @__PURE__ */ new WeakMap();
2451
- const initial = renderWithCtx({
2452
- render: () => fn({
2453
- state,
2454
- derived,
2455
- action,
2456
- input
2457
- }),
2458
- liveHost: preserveSSRDom ? host : void 0
2459
+ writeEnvelope(currentAc, {
2460
+ loading: true,
2461
+ value: readEnv().value,
2462
+ error: void 0
2463
+ });
2464
+ result.then((value) => {
2465
+ writeEnvelope(currentAc, {
2466
+ loading: false,
2467
+ value,
2468
+ error: void 0
2469
+ });
2470
+ }).catch((err) => {
2471
+ writeEnvelope(currentAc, {
2472
+ loading: false,
2473
+ value: void 0,
2474
+ error: err instanceof Error ? err : new Error(String(err))
2475
+ });
2476
+ });
2459
2477
  });
2460
- if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
2461
- const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2462
- let stopBindings = stopInitialBindingReflection;
2463
- cleanups.push(() => stopBindings());
2464
- let stopJsxEvents = applyJsxEvents({
2465
- host,
2466
- records: initial.events,
2467
- reportError: (error) => reportError(error, "on"),
2468
- unmountSignal: unmountController.signal,
2469
- onceFired: nativeOnceFired
2478
+ cleanups.push(() => {
2479
+ stopScope();
2480
+ ac.abort();
2470
2481
  });
2471
- cleanups.push(() => stopJsxEvents());
2472
- mountSlots(initial.slots);
2473
- cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2474
- attachListeners();
2475
- if (!hydrated && onMounts.length > 0) invokeOnMounts();
2476
- const stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2477
- stopBindings = () => {
2478
- stopInitialBindingListeners();
2479
- stopInitialBindingReflection();
2480
- };
2481
- for (const entry of effects) {
2482
- let userCleanup;
2483
- let runController = null;
2484
- const stopEffect = effect(() => {
2485
- if (userCleanup) {
2486
- try {
2487
- userCleanup();
2488
- } catch (err) {
2489
- reportError(err, "effect");
2490
- }
2491
- userCleanup = void 0;
2492
- }
2493
- if (runController) runController.abort();
2494
- runController = new AbortController();
2495
- const runSignal = anySignal([unmountController.signal, runController.signal]);
2496
- startBatch();
2482
+ function readEnv() {
2483
+ const prevSub = setActiveSub(void 0);
2484
+ try {
2485
+ return slot.env();
2486
+ } finally {
2487
+ setActiveSub(prevSub);
2488
+ }
2489
+ }
2490
+ function writeEnvelope(ac, patch) {
2491
+ if (ac.signal.aborted) return true;
2492
+ const prevSub = setActiveSub(void 0);
2493
+ try {
2494
+ slot.env(patch);
2495
+ } finally {
2496
+ setActiveSub(prevSub);
2497
+ }
2498
+ return false;
2499
+ }
2500
+ }
2501
+ function startEffectScope(slot) {
2502
+ let userCleanup;
2503
+ let runController = null;
2504
+ const stopScope = effect$1(() => {
2505
+ if (userCleanup) {
2497
2506
  try {
2498
- userCleanup = entry.fn({
2499
- state,
2500
- derived,
2501
- action,
2502
- input,
2503
- host,
2504
- signal: runSignal
2505
- });
2506
- } catch (err) {
2507
- reportError(err, "effect");
2508
- } finally {
2509
- endBatch();
2510
- }
2511
- });
2512
- cleanups.push(() => {
2513
- stopEffect();
2514
- if (userCleanup) try {
2515
2507
  userCleanup();
2516
2508
  } catch (err) {
2517
2509
  reportError(err, "effect");
2518
2510
  }
2519
- if (runController) runController.abort();
2511
+ userCleanup = void 0;
2512
+ }
2513
+ if (runController) runController.abort();
2514
+ runController = new AbortController();
2515
+ const runSignal = AbortSignal.any([unmountController.signal, runController.signal]);
2516
+ inputSignal();
2517
+ startBatch();
2518
+ try {
2519
+ userCleanup = slot.fn({ signal: runSignal });
2520
+ } catch (err) {
2521
+ reportError(err, "effect");
2522
+ } finally {
2523
+ endBatch();
2524
+ }
2525
+ });
2526
+ cleanups.push(() => {
2527
+ stopScope();
2528
+ if (userCleanup) try {
2529
+ userCleanup();
2530
+ } catch (err) {
2531
+ reportError(err, "effect");
2532
+ }
2533
+ if (runController) runController.abort();
2534
+ });
2535
+ }
2536
+ function startOnceScope(slot) {
2537
+ if (shouldSkipOnce) return;
2538
+ const prevSub = setActiveSub(void 0);
2539
+ let userCleanup;
2540
+ try {
2541
+ userCleanup = slot.fn({
2542
+ host,
2543
+ hydrated,
2544
+ signal: unmountController.signal
2520
2545
  });
2546
+ } catch (err) {
2547
+ reportError(err, "once");
2548
+ } finally {
2549
+ setActiveSub(prevSub);
2521
2550
  }
2522
- stopDerived = setupDerived();
2523
- cleanups.push(stopDerived);
2524
- for (const entry of streams) {
2525
- let streamAborted = false;
2526
- const streamController = new AbortController();
2527
- const runStream = async () => {
2551
+ if (userCleanup) {
2552
+ const teardown = userCleanup;
2553
+ cleanups.push(() => {
2528
2554
  try {
2529
- const gen = await entry.fn({
2530
- input,
2531
- signal: streamController.signal
2532
- });
2533
- while (!streamAborted && !streamController.signal.aborted) {
2534
- const { done, value } = await gen.next();
2535
- if (streamAborted || streamController.signal.aborted || done) break;
2536
- const prevSub = setActiveSub(void 0);
2537
- try {
2538
- const setter = state[entry.key];
2539
- setter?.(value);
2540
- } finally {
2541
- setActiveSub(prevSub);
2542
- }
2543
- }
2555
+ teardown();
2544
2556
  } catch (err) {
2545
- if (!streamAborted && !streamController.signal.aborted && err?.name !== "AbortError") reportError(err, "stream");
2557
+ reportError(err, "once");
2546
2558
  }
2547
- };
2548
- runStream();
2549
- cleanups.push(() => {
2550
- streamAborted = true;
2551
- streamController.abort();
2552
2559
  });
2553
2560
  }
2554
- let initialized = false;
2555
- const initialRenderedHtml = initial.html;
2556
- let lastRendered = null;
2557
- let renderEpoch = 0;
2558
- const stopRender = effect(() => {
2559
- const epoch = ++renderEpoch;
2560
- const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2561
- render: () => fn({
2562
- state,
2563
- derived,
2564
- action,
2565
- input
2566
- }),
2567
- liveHost: host
2568
- });
2569
- stopJsxEvents();
2570
- stopJsxEvents = applyJsxEvents({
2571
- host,
2572
- records: newEvents,
2573
- reportError: (error) => reportError(error, "on"),
2574
- unmountSignal: unmountController.signal,
2575
- onceFired: nativeOnceFired
2576
- });
2577
- const html = stylePrefix + rendered;
2578
- if (!initialized) {
2579
- initialized = true;
2580
- if (preserveSSRDom) {
2581
- pushUpdatedProps(newSlotMap);
2582
- if (rendered === initialRenderedHtml) {
2583
- lastRendered = rendered;
2584
- return;
2585
- }
2586
- }
2561
+ }
2562
+ const preserveSSRDom = hydrated && host.childNodes.length > 0;
2563
+ const initial = renderWithCtx({
2564
+ render: runInFrame(frame, input),
2565
+ liveHost: preserveSSRDom ? host : void 0
2566
+ });
2567
+ if (!preserveSSRDom) host.innerHTML = initial.html;
2568
+ const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2569
+ let stopInitialBindingListeners = null;
2570
+ stopBindings = stopInitialBindingReflection;
2571
+ cleanups.push(() => stopBindings());
2572
+ stopJsxEvents = applyJsxEvents({
2573
+ host,
2574
+ records: initial.events,
2575
+ reportError: (error) => reportError(error, "event"),
2576
+ unmountSignal: unmountController.signal,
2577
+ onceFired: nativeOnceFired
2578
+ });
2579
+ cleanups.push(() => stopJsxEvents());
2580
+ mountSlots(initial.slots);
2581
+ cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2582
+ stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2583
+ stopBindings = () => {
2584
+ stopInitialBindingListeners();
2585
+ stopInitialBindingReflection();
2586
+ };
2587
+ let initialized = false;
2588
+ const initialRenderedHtml = initial.html;
2589
+ let lastRendered = null;
2590
+ let renderEpoch = 0;
2591
+ const renderReactive = () => {
2592
+ const epoch = ++renderEpoch;
2593
+ frame.creating = false;
2594
+ frame.cursor = 0;
2595
+ const { html: rendered, slots: newSlotMap, binds: newBinds, events: newEvents } = renderWithCtx({
2596
+ render: runInFrame(frame, input),
2597
+ liveHost: host
2598
+ });
2599
+ stopJsxEvents();
2600
+ stopJsxEvents = applyJsxEvents({
2601
+ host,
2602
+ records: newEvents,
2603
+ reportError: (error) => reportError(error, "event"),
2604
+ unmountSignal: unmountController.signal,
2605
+ onceFired: nativeOnceFired
2606
+ });
2607
+ if (!initialized) {
2608
+ initialized = true;
2609
+ if (preserveSSRDom) {
2610
+ pushUpdatedProps(newSlotMap);
2587
2611
  if (rendered === initialRenderedHtml) {
2588
2612
  lastRendered = rendered;
2589
2613
  return;
2590
2614
  }
2591
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2592
- initialHtml: initialRenderedHtml,
2593
- renderedHtml: rendered,
2594
- slotIds: newSlotMap.keys()
2595
- })) {
2596
- pushUpdatedProps(newSlotMap);
2597
- return;
2598
- }
2599
2615
  }
2600
- if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
2601
- pushUpdatedProps(newSlotMap);
2616
+ if (rendered === initialRenderedHtml) {
2617
+ lastRendered = rendered;
2602
2618
  return;
2603
2619
  }
2604
- detachListeners();
2605
- stopBindings();
2606
- stopJsxEvents();
2607
- const prevMountedCount = mountedSlots.size;
2608
- const leavingPromises = [];
2609
- for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
2610
- const r = teardownMountedSlot(id, entry);
2611
- if (r instanceof Promise) leavingPromises.push(r);
2612
- }
2613
- for (const [id, entry] of mountedSlots) {
2614
- const next = newSlotMap.get(id);
2615
- if (next && next.island !== entry.island) {
2616
- const r = teardownMountedSlot(id, entry);
2617
- if (r instanceof Promise) leavingPromises.push(r);
2618
- }
2620
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount({
2621
+ initialHtml: initialRenderedHtml,
2622
+ renderedHtml: rendered,
2623
+ slotIds: newSlotMap.keys()
2624
+ })) {
2625
+ pushUpdatedProps(newSlotMap);
2626
+ return;
2619
2627
  }
2620
- const applyMorph = () => {
2621
- if (epoch !== renderEpoch) return;
2622
- if (newSlotMap.size < prevMountedCount) {
2623
- let divergeFrom = Infinity;
2624
- for (const [id, entry] of mountedSlots) {
2625
- if (!id.startsWith("p:")) continue;
2626
- const next = newSlotMap.get(id);
2627
- if (!next) continue;
2628
- const index = Number(id.slice(2));
2629
- if (Number.isNaN(index) || index >= divergeFrom) continue;
2630
- if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2631
- }
2632
- for (const [id, entry] of mountedSlots) {
2633
- if (!newSlotMap.has(id)) continue;
2634
- if (!entry.el.isConnected) continue;
2635
- if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
2636
- }
2628
+ }
2629
+ if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
2630
+ pushUpdatedProps(newSlotMap);
2631
+ return;
2632
+ }
2633
+ stopBindings();
2634
+ stopJsxEvents();
2635
+ const prevMountedCount = mountedSlots.size;
2636
+ for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) teardownMountedSlot(id, entry);
2637
+ for (const [id, entry] of mountedSlots) {
2638
+ const next = newSlotMap.get(id);
2639
+ if (next && next.island !== entry.island) teardownMountedSlot(id, entry);
2640
+ }
2641
+ const applyMorph = () => {
2642
+ if (epoch !== renderEpoch) return;
2643
+ if (newSlotMap.size < prevMountedCount) {
2644
+ let divergeFrom = Infinity;
2645
+ for (const [id, entry] of mountedSlots) {
2646
+ if (!id.startsWith("p:")) continue;
2647
+ const next = newSlotMap.get(id);
2648
+ if (!next) continue;
2649
+ const index = Number(id.slice(2));
2650
+ if (Number.isNaN(index) || index >= divergeFrom) continue;
2651
+ if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
2637
2652
  }
2638
- const tpl = document.createElement("template");
2639
- const morphRootTag = host.tagName.toLowerCase();
2640
- tpl.innerHTML = `<${morphRootTag}>${html}</${morphRootTag}>`;
2641
- morphInner(host, tpl.content.firstElementChild);
2642
- lastRendered = rendered;
2643
- let rehomeIndex = null;
2644
2653
  for (const [id, entry] of mountedSlots) {
2645
- if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2646
- rehomeIndex ??= buildSlotIndex();
2647
- const stub = rehomeIndex.get(id);
2648
- if (stub && stub !== entry.el) stub.replaceWith(entry.el);
2654
+ if (!newSlotMap.has(id)) continue;
2655
+ if (!entry.el.isConnected) continue;
2656
+ if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
2649
2657
  }
2650
- attachListeners();
2651
- stopBindings = applyTemplateBindings(host, newBinds);
2652
- stopJsxEvents = applyJsxEvents({
2653
- host,
2654
- records: newEvents,
2655
- reportError: (error) => reportError(error, "on"),
2656
- unmountSignal: unmountController.signal,
2657
- onceFired: nativeOnceFired
2658
- });
2659
- mountSlots(newSlotMap);
2660
- };
2661
- if (leavingPromises.length > 0) Promise.allSettled(leavingPromises).then((results) => {
2662
- for (const r of results) if (r.status === "rejected") console.error(r.reason);
2663
- applyMorph();
2664
- });
2665
- else applyMorph();
2666
- });
2667
- let tornDown = false;
2668
- const unmount = () => {
2669
- if (tornDown) return;
2670
- tornDown = true;
2671
- disposed = true;
2672
- unmountController.abort();
2673
- ISLAND_MOUNT_HANDLES.delete(host);
2674
- stopRender();
2675
- detachListeners();
2676
- const pending = [];
2677
- for (const [, entry] of mountedSlots) {
2678
- const result = entry.unmount();
2679
- if (result instanceof Promise) pending.push(result);
2680
2658
  }
2681
- if (transition?.leave) try {
2682
- const result = transition.leave(host);
2683
- if (result instanceof Promise) pending.push(result);
2684
- } catch (err) {
2685
- reportError(err, "transition");
2659
+ const tpl = document.createElement("template");
2660
+ const morphRootTag = host.tagName.toLowerCase();
2661
+ tpl.innerHTML = `<${morphRootTag}>${rendered}</${morphRootTag}>`;
2662
+ morphInner(host, tpl.content.firstElementChild);
2663
+ lastRendered = rendered;
2664
+ let rehomeIndex = null;
2665
+ for (const [id, entry] of mountedSlots) {
2666
+ if (!newSlotMap.has(id) || entry.el.isConnected) continue;
2667
+ rehomeIndex ??= buildSlotIndex();
2668
+ const stub = rehomeIndex.get(id);
2669
+ if (stub && stub !== entry.el) stub.replaceWith(entry.el);
2686
2670
  }
2687
- const finish = () => {
2688
- for (const c of cleanups) c();
2689
- };
2690
- if (pending.length > 0) return Promise.all(pending).then(finish).catch((err) => {
2691
- reportError(err, "transition");
2692
- finish();
2671
+ stopBindings = applyTemplateBindings(host, newBinds);
2672
+ stopJsxEvents = applyJsxEvents({
2673
+ host,
2674
+ records: newEvents,
2675
+ reportError: (error) => reportError(error, "event"),
2676
+ unmountSignal: unmountController.signal,
2677
+ onceFired: nativeOnceFired
2693
2678
  });
2694
- finish();
2695
- };
2696
- const updateProps = (nextProps) => {
2697
- if (tornDown) return;
2698
- const next = resolveInput(nextProps);
2699
- if (shallowEqualInput(inputSignal(), next)) return;
2700
- inputSignal(next);
2701
- };
2702
- const handle = {
2703
- unmount,
2704
- updateProps
2679
+ mountSlots(newSlotMap);
2705
2680
  };
2706
- ISLAND_MOUNT_HANDLES.set(host, handle);
2707
- return handle;
2708
- }
2709
- /**
2710
- * Dual-mode island invocation:
2711
- * - When called inside an `html` interpolation (i.e. {@link currentRenderCtx}
2712
- * is active), returns an {@link IslandCall} carrying props to be emitted
2713
- * as a child slot marker; {@link interpolateValue} consumes this marker.
2714
- * - When called at the top level with no active render context, returns
2715
- * SSR HTML via {@link renderToString} (backward-compatible public API).
2716
- *
2717
- * The declared return type omits {@link IslandCall} because TypeScript
2718
- * cannot narrow based on runtime stack state.
2719
- */
2720
- const island = ((props) => {
2721
- if (currentRenderCtx()) return {
2722
- [ISLAND_CALL]: true,
2723
- island,
2724
- props,
2725
- key: void 0
2726
- };
2727
- return renderToString(props);
2728
- });
2729
- island.toString = (props) => renderToString(props, true);
2730
- island.toStringAsync = (props) => Promise.resolve(renderToString(props, false));
2731
- island.mount = (host, props) => mountIsland(host, props);
2732
- island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2733
- island[ISLAND_SLOT_TAG] = configuredSlotTag;
2734
- island[ISLAND_RENDER_STATE] = async (props) => {
2735
- const manifest = /* @__PURE__ */ new Map();
2736
- const html = await renderToString(props ?? void 0, false, void 0, manifest, void 0, true);
2737
- return manifestTemplatePrefix(manifest) + html;
2681
+ applyMorph();
2738
2682
  };
2739
- island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, void 0, eventsOut, void 0, true);
2740
- island.key = (key) => {
2741
- if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2742
- if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
2743
- const keyed = ((props) => ({
2744
- [ISLAND_CALL]: true,
2745
- island,
2746
- props,
2747
- key
2748
- }));
2749
- keyed[ISLAND_CALL] = true;
2750
- return keyed;
2683
+ let stopRender = effect$1(renderReactive);
2684
+ startDerivedScopes();
2685
+ stopRender();
2686
+ stopRender = effect$1(renderReactive);
2687
+ startEffectScopes();
2688
+ let tornDown = false;
2689
+ const unmount = () => {
2690
+ if (tornDown) return;
2691
+ tornDown = true;
2692
+ instance.disposed = true;
2693
+ unmountController.abort();
2694
+ ISLAND_MOUNT_HANDLES.delete(host);
2695
+ stopRender();
2696
+ for (const [, entry] of mountedSlots) entry.unmount();
2697
+ for (const c of cleanups) c();
2751
2698
  };
2752
- island[ISLAND] = true;
2753
- const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2754
- if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2755
- island.define = (tagName, options) => {
2756
- if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2757
- `${tagName}`;
2758
- return;
2759
- }
2760
- const CE_RESERVED = /* @__PURE__ */ new Set([
2761
- "annotation-xml",
2762
- "color-profile",
2763
- "font-face",
2764
- "font-face-src",
2765
- "font-face-uri",
2766
- "font-face-format",
2767
- "font-face-name",
2768
- "missing-glyph"
2769
- ]);
2770
- if (typeof tagName !== "string" || !/^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(tagName) || CE_RESERVED.has(tagName)) {
2771
- `${tagName}`;
2772
- return;
2699
+ const updateProps = (nextProps) => {
2700
+ if (tornDown) return;
2701
+ const next = resolveInput(nextProps);
2702
+ if (shallowEqualInput(inputSignal(), next)) return;
2703
+ inputSignal(next);
2704
+ derivedPropsVersion(derivedPropsVersion() + 1);
2705
+ };
2706
+ const handle = {
2707
+ unmount,
2708
+ updateProps
2709
+ };
2710
+ ISLAND_MOUNT_HANDLES.set(host, handle);
2711
+ return handle;
2712
+ }
2713
+ const island = ((props) => ({
2714
+ [ISLAND_CALL]: true,
2715
+ island,
2716
+ props,
2717
+ key: void 0
2718
+ }));
2719
+ island.toString = (props) => renderToString(props, true);
2720
+ island.toStringAsync = (props) => Promise.resolve(renderToString(props, false));
2721
+ island.mount = (host, props) => mountIsland(host, props);
2722
+ island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2723
+ island[ISLAND_SLOT_TAG] = configuredSlotTag;
2724
+ island[ISLAND_RENDER_STATE] = async (props) => {
2725
+ const manifest = /* @__PURE__ */ new Map();
2726
+ const html = await renderToString(props ?? void 0, false, manifest, true);
2727
+ return serializeServerManifest(manifest) + html;
2728
+ };
2729
+ island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, eventsOut, true);
2730
+ island.key = (key) => {
2731
+ if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2732
+ if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
2733
+ const keyed = ((props) => ({
2734
+ [ISLAND_CALL]: true,
2735
+ island,
2736
+ props,
2737
+ key
2738
+ }));
2739
+ keyed[ISLAND_CALL] = true;
2740
+ return keyed;
2741
+ };
2742
+ island[ISLAND] = true;
2743
+ const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2744
+ if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2745
+ island.define = (tagName, options) => {
2746
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2747
+ `${tagName}`;
2748
+ return;
2749
+ }
2750
+ const CE_RESERVED = /* @__PURE__ */ new Set([
2751
+ "annotation-xml",
2752
+ "color-profile",
2753
+ "font-face",
2754
+ "font-face-src",
2755
+ "font-face-uri",
2756
+ "font-face-format",
2757
+ "font-face-name",
2758
+ "missing-glyph"
2759
+ ]);
2760
+ if (typeof tagName !== "string" || !/^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(tagName) || CE_RESERVED.has(tagName)) {
2761
+ `${tagName}`;
2762
+ return;
2763
+ }
2764
+ if (customElements.get(tagName)) {
2765
+ `${tagName}`;
2766
+ return;
2767
+ }
2768
+ const observe = options?.observe ?? [];
2769
+ class IlhaIslandElement extends HTMLElement {
2770
+ static observedAttributes = observe;
2771
+ _handle = null;
2772
+ _props;
2773
+ _unmounting = false;
2774
+ _reconnect = false;
2775
+ get props() {
2776
+ return this._props;
2773
2777
  }
2774
- if (customElements.get(tagName)) {
2775
- `${tagName}`;
2776
- return;
2778
+ set props(p) {
2779
+ this._props = p;
2780
+ if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2777
2781
  }
2778
- const observe = options?.observe ?? [];
2779
- class IlhaIslandElement extends HTMLElement {
2780
- static observedAttributes = observe;
2781
- _handle = null;
2782
- _props;
2783
- _unmounting = false;
2784
- _reconnect = false;
2785
- get props() {
2786
- return this._props;
2782
+ _mergedProps() {
2783
+ const attrProps = {};
2784
+ let hasAttrs = false;
2785
+ for (const name of observe) {
2786
+ const v = this.getAttribute(name);
2787
+ if (v !== null) {
2788
+ attrProps[name] = v;
2789
+ hasAttrs = true;
2790
+ }
2787
2791
  }
2788
- set props(p) {
2789
- this._props = p;
2790
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2792
+ if (!hasAttrs && this._props === void 0) return void 0;
2793
+ return {
2794
+ ...attrProps,
2795
+ ...this._props
2796
+ };
2797
+ }
2798
+ connectedCallback() {
2799
+ if (this._unmounting) {
2800
+ this._reconnect = true;
2801
+ return;
2791
2802
  }
2792
- _mergedProps() {
2793
- const attrProps = {};
2794
- let hasAttrs = false;
2795
- for (const name of observe) {
2796
- const v = this.getAttribute(name);
2797
- if (v !== null) {
2798
- attrProps[name] = v;
2799
- hasAttrs = true;
2800
- }
2803
+ if (this._handle) return;
2804
+ this._handle = mountIslandInternal(this, this._mergedProps());
2805
+ }
2806
+ disconnectedCallback() {
2807
+ if (!this._handle || this._unmounting) return;
2808
+ this._unmounting = true;
2809
+ this._reconnect = false;
2810
+ Promise.resolve(this._handle.unmount()).finally(() => {
2811
+ this._handle = null;
2812
+ this._unmounting = false;
2813
+ if (this._reconnect) {
2814
+ this._reconnect = false;
2815
+ if (this.isConnected) this._handle = mountIslandInternal(this, this._mergedProps());
2801
2816
  }
2802
- if (!hasAttrs && this._props === void 0) return void 0;
2803
- return {
2804
- ...attrProps,
2805
- ...this._props ?? {}
2806
- };
2807
- }
2808
- connectedCallback() {
2809
- if (this._unmounting) {
2810
- this._reconnect = true;
2811
- return;
2817
+ });
2818
+ }
2819
+ attributeChangedCallback() {
2820
+ if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2821
+ }
2822
+ }
2823
+ customElements.define(tagName, IlhaIslandElement);
2824
+ };
2825
+ island.hydratable = async (props, opts) => {
2826
+ const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2827
+ const tag = assertValidSlotTagName(rawTag);
2828
+ const resolvedProps = props ?? {};
2829
+ const innerPromise = renderToString(resolvedProps, false, void 0, true);
2830
+ let stateAttr = "";
2831
+ if (snapshot !== false) {
2832
+ const doState = snapshot === true || snapshot.state !== false;
2833
+ const doDerived = snapshot === true || snapshot.derived !== false;
2834
+ const doSkipOnMount = explicitSkipOnMount ?? false;
2835
+ const snapshotData = { v: 2 };
2836
+ const input = resolveInput(resolvedProps);
2837
+ const probeFrame = freshFrame({ ssr: true });
2838
+ withFrame(probeFrame, () => {
2839
+ component(input);
2840
+ });
2841
+ if (doState) {
2842
+ const states = [];
2843
+ for (const slot of probeFrame.slots) {
2844
+ if (slot.kind !== "state") continue;
2845
+ const prevSub = setActiveSub(void 0);
2846
+ try {
2847
+ states.push(slot.acc());
2848
+ } finally {
2849
+ setActiveSub(prevSub);
2812
2850
  }
2813
- if (this._handle) return;
2814
- this._handle = mountIslandInternal(this, this._mergedProps());
2815
2851
  }
2816
- disconnectedCallback() {
2817
- if (!this._handle || this._unmounting) return;
2818
- this._unmounting = true;
2819
- this._reconnect = false;
2820
- Promise.resolve(this._handle.unmount()).finally(() => {
2821
- this._handle = null;
2822
- this._unmounting = false;
2823
- if (this._reconnect) {
2824
- this._reconnect = false;
2825
- if (this.isConnected) this._handle = mountIslandInternal(this, this._mergedProps());
2826
- }
2852
+ snapshotData["s"] = states;
2853
+ }
2854
+ if (doDerived) {
2855
+ const probePendings = probeFrame.pendingDerived ?? [];
2856
+ if (probePendings.length > 0) {
2857
+ await Promise.all(probePendings.map((p) => p.promise));
2858
+ probeFrame.creating = false;
2859
+ probeFrame.cursor = 0;
2860
+ withFrame(probeFrame, () => {
2861
+ component(input);
2827
2862
  });
2828
2863
  }
2829
- attributeChangedCallback() {
2830
- if (this._handle && !this._unmounting) this._handle.updateProps(this._mergedProps());
2831
- }
2832
- }
2833
- customElements.define(tagName, IlhaIslandElement);
2834
- };
2835
- island.hydratable = async (props, opts) => {
2836
- const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2837
- const tag = assertValidSlotTagName(rawTag);
2838
- const resolvedProps = props ?? {};
2839
- const streamValues = {};
2840
- const eventsManifest = /* @__PURE__ */ new Map();
2841
- const inner = await renderToString(resolvedProps, false, streamValues, eventsManifest, void 0, true);
2842
- const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
2843
- let stateAttr = "";
2844
- if (snapshot !== false) {
2845
- const doState = snapshot === true || snapshot.state !== false;
2846
- const doDerived = snapshot === true || snapshot.derived !== false;
2847
- const doSkipOnMount = explicitSkipOnMount ?? (doState || doDerived);
2848
- const snapshotData = {};
2849
- const input = resolveInput(resolvedProps);
2850
- const plainState = buildPlainState(input);
2851
- const stateForDerived = { ...plainState };
2852
- for (const [k, v] of Object.entries(streamValues)) stateForDerived[k] = () => v;
2853
- if (doState) for (const entry of states) snapshotData[entry.key] = entry.key in streamValues ? streamValues[entry.key] : plainState[entry.key]();
2854
- if (doDerived) {
2855
- const derivedResults = {};
2856
- for (const entry of deriveds) {
2857
- const prevSub = setActiveSub(void 0);
2858
- let resultPromise;
2859
- let syncError;
2860
- let threw = false;
2861
- try {
2862
- resultPromise = entry.fn({
2863
- state: stateForDerived,
2864
- input,
2865
- signal: new AbortController().signal
2866
- });
2867
- } catch (err) {
2868
- threw = true;
2869
- syncError = err;
2870
- } finally {
2871
- setActiveSub(prevSub);
2872
- }
2873
- if (threw) {
2874
- derivedResults[entry.key] = {
2875
- loading: false,
2876
- value: void 0,
2877
- error: syncError instanceof Error ? syncError.message : String(syncError)
2878
- };
2879
- continue;
2880
- }
2881
- try {
2882
- const result = await Promise.resolve(resultPromise);
2883
- derivedResults[entry.key] = {
2884
- loading: false,
2885
- value: result,
2886
- error: void 0
2887
- };
2888
- } catch (err) {
2889
- derivedResults[entry.key] = {
2890
- loading: false,
2891
- value: void 0,
2892
- error: err instanceof Error ? err.message : String(err)
2893
- };
2894
- }
2864
+ const derivedEntries = [];
2865
+ for (const slot of probeFrame.slots) {
2866
+ if (slot.kind !== "derived") continue;
2867
+ const prevSub = setActiveSub(void 0);
2868
+ let envelope;
2869
+ try {
2870
+ envelope = { ...slot.env() };
2871
+ } finally {
2872
+ setActiveSub(prevSub);
2895
2873
  }
2896
- snapshotData["_derived"] = derivedResults;
2874
+ const entry = {
2875
+ loading: false,
2876
+ value: envelope.value
2877
+ };
2878
+ if (envelope.error) entry["error"] = envelope.error.message;
2879
+ derivedEntries.push(entry);
2897
2880
  }
2898
- if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2899
- if (streams.length > 0) snapshotData["_streams"] = Object.fromEntries(streams.map((entry) => [entry.key, true]));
2900
- stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2881
+ snapshotData["d"] = derivedEntries;
2901
2882
  }
2902
- let actionsAttr = "";
2903
- if (eventsManifest.size > 0) actionsAttr = ` ${ACTIONS_ATTR}='${escapeHtml(JSON.stringify(Object.fromEntries(eventsManifest)))}'`;
2904
- return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}${actionsAttr}>${inner}</${tag}>`;
2905
- };
2906
- return island;
2907
- }
2908
- };
2909
- function ilhaFrom(selector, island, props) {
2910
- const host = typeof selector === "string" ? document.querySelector(selector) : selector;
2911
- if (!host) {
2912
- console.warn(`[ilha] from(): element not found: ${selector}`);
2913
- return null;
2914
- }
2915
- return island.mount(host, props);
2916
- }
2883
+ if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2884
+ stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2885
+ }
2886
+ const inner = await innerPromise;
2887
+ return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${escapeHtml(JSON.stringify(resolvedProps))}'${stateAttr}>${inner}</${tag}>`;
2888
+ };
2889
+ return island;
2890
+ });
2917
2891
  function mountAll(registry, options = {}) {
2918
2892
  const root = options.root ?? document.body;
2919
2893
  const lazy = options.lazy ?? false;
@@ -2959,67 +2933,27 @@ function mountAll(registry, options = {}) {
2959
2933
  if (pending.length > 0) return Promise.all(pending).then(() => {});
2960
2934
  } };
2961
2935
  }
2962
- const rootBuilder = new IlhaBuilder({
2963
- schema: null,
2964
- defaultInput: null,
2965
- states: [],
2966
- streams: [],
2967
- deriveds: [],
2968
- actions: [],
2969
- ons: [],
2970
- effects: [],
2971
- onMounts: [],
2972
- onErrors: [],
2973
- transition: null,
2974
- css: null,
2975
- as: null
2976
- });
2977
- const callableRoot = ((fn) => rootBuilder.input().render(fn));
2978
- Object.defineProperty(callableRoot, "_cfg", {
2979
- value: rootBuilder._cfg,
2980
- enumerable: false
2981
- });
2982
- for (const key of Object.getOwnPropertyNames(IlhaBuilder.prototype)) {
2983
- if (key === "constructor") continue;
2984
- const descriptor = Object.getOwnPropertyDescriptor(IlhaBuilder.prototype, key);
2985
- if (descriptor) Object.defineProperty(callableRoot, key, descriptor);
2986
- }
2987
- const ilha = Object.assign(callableRoot, {
2988
- html: ilhaHtml,
2989
- raw: ilhaRaw,
2990
- mount: mountAll,
2991
- from: ilhaFrom,
2992
- context: ilhaContext,
2993
- signal: ilhaSignal,
2994
- computed: ilhaComputed,
2995
- morph,
2996
- batch,
2997
- untrack,
2998
- onUncaughtError
2936
+ setJsxRuntimeBridge({
2937
+ registerEvent({ type, handler, modifier }) {
2938
+ const ctx = currentRenderCtx();
2939
+ if (!ctx) return void 0;
2940
+ SSR_ACTION_STUB in handler;
2941
+ const index = ctx.events.length;
2942
+ ctx.events.push({
2943
+ type,
2944
+ handler,
2945
+ modifier
2946
+ });
2947
+ return index;
2948
+ },
2949
+ slot({ island, props, key }) {
2950
+ return ilhaRaw(emitIslandSlot({
2951
+ island,
2952
+ props,
2953
+ key
2954
+ }));
2955
+ }
2999
2956
  });
3000
- /** @internal Used by the separate JSX runtime to register a native event handler. */
3001
- function __ilhaJsxEvent({ type, handler, modifier }) {
3002
- const ctx = currentRenderCtx();
3003
- if (!ctx) return void 0;
3004
- const isActionStub = SSR_ACTION_STUB in handler;
3005
- const capture = ctx.manifest && !isActionStub ? captureHandlerActions(handler) : void 0;
3006
- const index = ctx.events.length;
3007
- ctx.events.push({
3008
- type,
3009
- handler,
3010
- modifier,
3011
- capture
3012
- });
3013
- return index;
3014
- }
3015
- /** @internal Used by the separate JSX runtime entry to preserve island slot composition. */
3016
- function __ilhaJsxSlot({ island, props, key }) {
3017
- return ilhaRaw(emitIslandSlot({
3018
- island,
3019
- props,
3020
- key
3021
- }));
3022
- }
3023
2957
  /**
3024
2958
  * Morph an element's children toward `html`, patching in place: surviving
3025
2959
  * elements keep their identity, listeners, and focus (see the morph engine).
@@ -3043,10 +2977,8 @@ function morph(host, html) {
3043
2977
  }
3044
2978
  const html = ilhaHtml;
3045
2979
  const raw = ilhaRaw;
3046
- const css = ilhaCss;
3047
2980
  const mount = mountAll;
3048
- const from = ilhaFrom;
3049
2981
  const context = ilhaContext;
3050
2982
 
3051
2983
  //#endregion
3052
- export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilhaEffect as effect, from, html, ilha, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
2984
+ export { action, batch, context, css, derived, each, effect, html, ilha, ilhaSignal, isSafeUrl, isSafeUrlAttrValue, isUrlAttributeName, json, morph, mount, onError, onUncaughtError, persist, raw, serializeStyle, state, untrack };