ilha 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -242,6 +242,8 @@ type OnMountContext<TInput, TStateMap extends Record<string, unknown>, TDerivedM
242
242
  input: TInput;
243
243
  host: Element;
244
244
  hydrated: boolean;
245
+ /** AbortSignal that aborts when the island unmounts. */
246
+ signal: AbortSignal;
245
247
  };
246
248
  type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
247
249
  state: IslandState<TStateMap>;
@@ -283,6 +285,20 @@ interface StateEntry<TInput> {
283
285
  key: string;
284
286
  init: StateInit<TInput, unknown>;
285
287
  }
288
+ /** A state key fed by a generator (`.stream()`). The runtime iterates the
289
+ * generator and writes each yielded value into the state signal. During SSR
290
+ * only the first value is pulled so it can render inline; on the client the
291
+ * generator is consumed continuously. */
292
+ interface StreamEntry<TInput> {
293
+ key: string;
294
+ fn: StreamFn<TInput>;
295
+ }
296
+ type StreamFnContext<TInput> = {
297
+ input: TInput;
298
+ /** Aborts when the island unmounts. Pass through to fetch/SSE layers. */
299
+ signal: AbortSignal;
300
+ };
301
+ type StreamFn<TInput, V = unknown> = (ctx: StreamFnContext<TInput>) => AsyncGenerator<V> | Generator<V>;
286
302
  interface ActionEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
287
303
  key: string;
288
304
  fn: (props: unknown, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => unknown;
@@ -300,7 +316,7 @@ interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerive
300
316
  /** Where the error originated. `"on"` covers sync throws and async rejections
301
317
  * from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
302
318
  * (async work spawned inside an effect is not awaited by the runtime). */
303
- type ErrorSource = "on" | "effect" | "mount" | "transition" | "action";
319
+ type ErrorSource = "on" | "effect" | "mount" | "transition" | "action" | "stream";
304
320
  /**
305
321
  * Register a global error handler invoked when any island reports an error
306
322
  * (from .on, .effect, .onMount, or transitions) and has no local .onError()
@@ -339,6 +355,7 @@ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDeri
339
355
  /** Shallow defaults merged before props (POJO `.input({ ... })` only). */
340
356
  defaultInput: Record<string, unknown> | null;
341
357
  states: StateEntry<TInput>[];
358
+ streams: StreamEntry<TInput>[];
342
359
  deriveds: DerivedEntry<TInput, TStateMap>[];
343
360
  actions: ActionEntry<TInput, TStateMap, TDerivedMap>[];
344
361
  ons: OnEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
@@ -358,6 +375,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
358
375
  input<T extends Record<string, unknown>>(defaults: T): IlhaBuilder<T, Record<never, never>, Record<never, never>, Record<never, never>>;
359
376
  as<Tag extends string>(tag: Tag): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
360
377
  state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TActionMap>;
378
+ stream<K extends string, V>(key: K, fn: StreamFn<TInput, V>): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TActionMap>;
361
379
  derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>, TActionMap>;
362
380
  action<K extends string, P = undefined, R = void>(key: K, fn: (props: P, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => R): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap & Record<K, typeof fn>>;
363
381
  on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : HandlerContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
@@ -406,6 +424,12 @@ declare function __ilhaJsxSlot({ island, props, key }: {
406
424
  props: Record<string, unknown> | undefined;
407
425
  key: string | undefined;
408
426
  }): RawHtml;
427
+ /**
428
+ * Morph an element's children toward `html`, patching in place: surviving
429
+ * elements keep their identity, listeners, and focus (see the morph engine).
430
+ * Falls back to an innerHTML write when the root tags don't match.
431
+ */
432
+ declare function morph(host: Element, html: string): void;
409
433
  declare const html: typeof ilhaHtml;
410
434
  declare const raw: typeof ilhaRaw;
411
435
  declare const css: typeof ilhaCss;
@@ -418,4 +442,4 @@ declare const context: typeof ilhaContextFn & {
418
442
  clear(): void;
419
443
  };
420
444
  //#endregion
421
- export { __ilhaJsxSlot as A, mount as B, NativeEventModifier as C, SignalSetter as D, SignalAccessor as E, html as F, raw as H, ilha as I, ilhaComputed as L, context as M, css as N, SignalWriter as O, from as P, ilhaEffect as R, NativeEventHandler as S, RawHtml as T, untrack as U, onUncaughtError as V, IslandState as _, EffectContext as a, MountResult as b, ExternalSignal as c, HydratableOptions as d, ISLAND_MOUNT_HANDLES as f, IslandDerived as g, IslandActions as h, DerivedValue as i, batch as j, __ilhaJsxEvent as k, HandlerContext as l, Island as m, ActionContext as n, ErrorContext as o, ISLAND_MOUNT_INTERNAL as p, DerivedAccessor as r, ErrorSource as s, ActionAccessor as t, HandlerContextFor as u, KeyedIsland as v, OnMountContext as w, NativeEventContext as x, MountOptions as y, ilhaSignal as z };
445
+ export { StreamFnContext as A, ilhaEffect as B, NativeEventModifier as C, SignalSetter as D, SignalAccessor as E, css as F, raw as G, morph as H, from as I, untrack as K, html as L, __ilhaJsxSlot as M, batch as N, SignalWriter as O, context as P, ilha as R, NativeEventHandler as S, RawHtml as T, mount as U, ilhaSignal as V, onUncaughtError as W, IslandState as _, EffectContext as a, MountResult as b, ExternalSignal as c, HydratableOptions as d, ISLAND_MOUNT_HANDLES as f, IslandDerived as g, IslandActions as h, DerivedValue as i, __ilhaJsxEvent as j, StreamFn as k, HandlerContext as l, Island as m, ActionContext as n, ErrorContext as o, ISLAND_MOUNT_INTERNAL as p, DerivedAccessor as r, ErrorSource as s, ActionAccessor as t, HandlerContextFor as u, KeyedIsland as v, OnMountContext as w, NativeEventContext as x, MountOptions as y, ilhaComputed as z };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as __ilhaJsxSlot, B as mount, C as NativeEventModifier, D as SignalSetter, E as SignalAccessor, F as html, H as raw, I as ilha, L as ilhaComputed, M as context, N as css, O as SignalWriter, P as from, R as ilhaEffect, S as NativeEventHandler, T as RawHtml, U as untrack, V as onUncaughtError, _ as IslandState, a as EffectContext, b as MountResult, c as ExternalSignal, d as HydratableOptions, f as ISLAND_MOUNT_HANDLES, g as IslandDerived, h as IslandActions, i as DerivedValue, j as batch, k as __ilhaJsxEvent, l as HandlerContext, m as Island, n as ActionContext, o as ErrorContext, p as ISLAND_MOUNT_INTERNAL, r as DerivedAccessor, s as ErrorSource, t as ActionAccessor, u as HandlerContextFor, v as KeyedIsland, w as OnMountContext, x as NativeEventContext, y as MountOptions, z as ilhaSignal } from "./index-Bi_I3qEL.js";
2
- export { ActionAccessor, ActionContext, DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandActions, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, NativeEventContext, NativeEventHandler, NativeEventModifier, OnMountContext, RawHtml, SignalAccessor, SignalSetter, SignalWriter, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
1
+ import { A as StreamFnContext, B as ilhaEffect, C as NativeEventModifier, D as SignalSetter, E as SignalAccessor, F as css, G as raw, H as morph, I as from, K as untrack, L as html, M as __ilhaJsxSlot, N as batch, O as SignalWriter, P as context, R as ilha, S as NativeEventHandler, T as RawHtml, U as mount, V as ilhaSignal, W as onUncaughtError, _ as IslandState, a as EffectContext, b as MountResult, c as ExternalSignal, d as HydratableOptions, f as ISLAND_MOUNT_HANDLES, g as IslandDerived, h as IslandActions, i as DerivedValue, j as __ilhaJsxEvent, k as StreamFn, l as HandlerContext, m as Island, n as ActionContext, o as ErrorContext, p as ISLAND_MOUNT_INTERNAL, r as DerivedAccessor, s as ErrorSource, t as ActionAccessor, u as HandlerContextFor, v as KeyedIsland, w as OnMountContext, x as NativeEventContext, y as MountOptions, z as ilhaComputed } from "./index-Cx35Xugv.js";
2
+ export { ActionAccessor, ActionContext, DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandActions, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, NativeEventContext, NativeEventHandler, NativeEventModifier, OnMountContext, RawHtml, SignalAccessor, SignalSetter, SignalWriter, StreamFn, StreamFnContext, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
package/dist/index.js CHANGED
@@ -127,13 +127,21 @@ function isStableInlineSlotMount({ initialHtml, renderedHtml, slotIds }) {
127
127
  * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
128
128
  * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
129
129
  */
130
- function slotPropsForAttr(props) {
130
+ function slotPropsForAttr(props, captureActions = false) {
131
131
  if (props === void 0) return void 0;
132
132
  let out;
133
133
  for (const key of Object.keys(props)) {
134
134
  if (key === "children") continue;
135
135
  const value = props[key];
136
- if (typeof value === "function" || typeof value === "symbol") continue;
136
+ if (typeof value === "function") {
137
+ const action = captureActions ? captureHandlerActions(value) : void 0;
138
+ if (action) (out ??= {})[key] = {
139
+ __ilha: "action",
140
+ ...action
141
+ };
142
+ continue;
143
+ }
144
+ if (typeof value === "symbol") continue;
137
145
  const encoded = encodeSlotPropValue(value);
138
146
  if (encoded === void 0 && value !== void 0 && value !== null) continue;
139
147
  (out ??= {})[key] = encoded;
@@ -465,16 +473,31 @@ const RAW = Symbol.for("ilha.raw");
465
473
  const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
466
474
  const ISLAND = Symbol.for("ilha.island");
467
475
  const ISLAND_CALL = Symbol.for("ilha.islandCall");
476
+ const ASTRO_RENDERER_GLOBAL = Symbol.for("ilha.astroRenderer");
468
477
  /** @internal Internal hook used by a parent's mountSlots to mount a child island and
469
478
  * retain a handle to push updated props into it on subsequent parent
470
479
  * re-renders. Not part of the public surface. */
471
480
  const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
472
481
  const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
482
+ const ISLAND_CLIENT_REF = Symbol.for("ilha.clientRef");
473
483
  /** @internal Live mount handles keyed by host element. Lets @ilha/router adopt
474
484
  * islands hydrated by `ilha.mount()` (whose handles it never saw) and push new
475
485
  * loader props into them in place instead of remounting. Entries are removed
476
486
  * on unmount. Not part of the public surface. */
477
487
  const ISLAND_MOUNT_HANDLES = /* @__PURE__ */ new WeakMap();
488
+ /** @internal Sync re-render of an island from a state snapshot. Attached by
489
+ * render(); used by server-frame endpoints to produce HTML for streamed
490
+ * updates without a live mount. Not part of the public surface. */
491
+ const ISLAND_RENDER_STATE = Symbol.for("ilha.renderState");
492
+ /** @internal Async SSR render that records the event→action manifest into the
493
+ * provided map. Used by emitIslandSlot so nested server islands ship their
494
+ * interactive surface alongside their inlined HTML. */
495
+ 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
+ }
478
501
  const SLOT_ATTR = "data-ilha-slot";
479
502
  /** Marks a slot host whose child island is mid leave-transition: it has been
480
503
  * unmounted (and its slot id stripped) but stays connected so the leave
@@ -482,25 +505,63 @@ const SLOT_ATTR = "data-ilha-slot";
482
505
  * or mutating them. */
483
506
  const LEAVING_ATTR = "data-ilha-leaving";
484
507
  const PROPS_ATTR = "data-ilha-props";
508
+ const CLIENT_REF_ATTR = "data-ilha-client-ref";
485
509
  const STATE_ATTR = "data-ilha-state";
486
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";
487
516
  function buildScopedStyle(css) {
488
517
  const safeCss = css.replace(/<\/style/gi, "<\\/style");
489
518
  return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${safeCss}}</style>`;
490
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 = [];
524
+ /**
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.
528
+ *
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.
534
+ */
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];
550
+ }
491
551
  const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
492
552
  function renderCtxStack() {
493
553
  const g = globalThis;
494
554
  return g[RENDER_CTX_STACK] ??= [];
495
555
  }
496
- function pushRenderCtx(liveHost, asyncChildren) {
556
+ function pushRenderCtx(liveHost, asyncChildren, manifest) {
497
557
  const ctx = {
498
558
  slots: /* @__PURE__ */ new Map(),
499
559
  positional: 0,
500
560
  liveHost,
501
561
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
502
562
  binds: [],
503
- events: []
563
+ events: [],
564
+ manifest
504
565
  };
505
566
  renderCtxStack().push(ctx);
506
567
  return ctx;
@@ -523,15 +584,16 @@ function isIslandCall(v) {
523
584
  function emitIslandSlot({ island, props, key }) {
524
585
  const ctx = currentRenderCtx();
525
586
  let id;
526
- if (key !== void 0) id = `k:${key}`;
527
- else id = ctx ? `p:${ctx.positional++}` : "p:0";
587
+ if (key === void 0) id = ctx ? `p:${ctx.positional++}` : "p:0";
588
+ else id = `k:${key}`;
528
589
  if (ctx) ctx.slots.set(id, {
529
590
  island,
530
591
  props
531
592
  });
532
593
  const slotTag = getIslandSlotTag(island);
533
- const attrProps = slotPropsForAttr(props);
534
- const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
594
+ const clientRef = island[ISLAND_CLIENT_REF];
595
+ const attrProps = slotPropsForAttr(props, typeof clientRef === "string" && ctx?.manifest === true);
596
+ const propsAttr = (attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "") + (typeof clientRef === "string" ? ` ${CLIENT_REF_ATTR}="${escapeHtml(clientRef)}"` : "");
535
597
  if (ctx?.liveHost) return wrapIslandSlotHtml({
536
598
  tag: slotTag,
537
599
  id,
@@ -541,9 +603,12 @@ function emitIslandSlot({ island, props, key }) {
541
603
  if (ctx?.pending) {
542
604
  popRenderCtx();
543
605
  try {
544
- const result = island(props);
606
+ const manifest = /* @__PURE__ */ new Map();
607
+ const ssrWithManifest = island[ISLAND_SSR_MANIFEST];
608
+ const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island(props);
609
+ const prefix = () => manifestTemplatePrefix(manifest);
545
610
  if (result instanceof Promise) {
546
- ctx.pending.set(id, result.then(String));
611
+ ctx.pending.set(id, result.then(String).then((html) => prefix() + html));
547
612
  return wrapIslandSlotHtml({
548
613
  tag: slotTag,
549
614
  id,
@@ -555,7 +620,7 @@ function emitIslandSlot({ island, props, key }) {
555
620
  tag: slotTag,
556
621
  id,
557
622
  propsAttr,
558
- inner: String(result)
623
+ inner: prefix() + String(result)
559
624
  });
560
625
  } finally {
561
626
  renderCtxStack().push(ctx);
@@ -827,7 +892,8 @@ function ilhaHtml(strings, ...values) {
827
892
  }
828
893
  if (pendingBindSpecs !== "" || pendingEventSpecs !== "") {
829
894
  const { index: gtIdx, quote } = findTagCloseIndex(chunk, pendingQuote);
830
- if (gtIdx !== -1) {
895
+ if (gtIdx === -1) pendingQuote = quote;
896
+ else {
831
897
  const head = chunk.slice(0, gtIdx).replace(/\s*\/\s*$/, "");
832
898
  const bindAttr = pendingBindSpecs ? ` data-ilha-bind="${pendingBindSpecs}"` : "";
833
899
  const eventAttr = pendingEventSpecs ? ` data-ilha-on="${pendingEventSpecs}"` : "";
@@ -835,7 +901,7 @@ function ilhaHtml(strings, ...values) {
835
901
  pendingBindSpecs = "";
836
902
  pendingEventSpecs = "";
837
903
  pendingQuote = null;
838
- } else pendingQuote = quote;
904
+ }
839
905
  }
840
906
  if (i >= values.length) {
841
907
  result += chunk;
@@ -843,13 +909,16 @@ function ilhaHtml(strings, ...values) {
843
909
  }
844
910
  const value = values[i];
845
911
  const eventMatch = chunk.match(EVENT_PREFIX_RE);
846
- if (eventMatch && isEventAttributePosition(result + chunk, result.length + chunk.length - eventMatch[0].length)) {
912
+ const eventMatchStart = eventMatch ? chunk.length - eventMatch[0].length : -1;
913
+ const eventAttributeFragment = eventMatch !== null && chunk.slice(0, eventMatchStart).trim() === "";
914
+ if (eventMatch && (eventAttributeFragment || isEventAttributePosition(result + chunk, result.length + eventMatchStart))) {
847
915
  const eventName = eventMatch[1];
848
916
  const rawModifier = eventMatch[2];
849
917
  const modifier = NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
850
918
  const openQuote = eventMatch[3] ?? null;
851
- const matchStart = chunk.length - eventMatch[0].length;
852
- const cleanPrefix = chunk.slice(0, matchStart).replace(/\s+$/, "");
919
+ 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;
853
922
  result += cleanPrefix;
854
923
  if (ctx && typeof value === "function") {
855
924
  const index = ctx.events.length;
@@ -857,7 +926,8 @@ function ilhaHtml(strings, ...values) {
857
926
  ctx.events.push({
858
927
  type: eventName,
859
928
  handler: value,
860
- modifier
929
+ modifier,
930
+ capture
861
931
  });
862
932
  pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
863
933
  }
@@ -1153,7 +1223,12 @@ function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
1153
1223
  for (const entry of entries) {
1154
1224
  let initialEnvelope;
1155
1225
  if (derivedSnapshot != null && entry.key in derivedSnapshot) initialEnvelope = { ...derivedSnapshot[entry.key] };
1156
- else if (!(entry.fn.constructor.name === "AsyncFunction" || entry.fn.constructor.name === "AsyncGeneratorFunction")) {
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
1230
+ };
1231
+ else {
1157
1232
  const ac = new AbortController();
1158
1233
  const captureSlot = {};
1159
1234
  const stack = queryCaptureStack();
@@ -1170,19 +1245,18 @@ function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
1170
1245
  value: void 0,
1171
1246
  error: void 0
1172
1247
  };
1173
- else if (!(result instanceof Promise)) initialEnvelope = {
1174
- loading: false,
1175
- value: result,
1176
- error: void 0
1177
- };
1178
- else {
1248
+ else if (result instanceof Promise) {
1179
1249
  initialEnvelope = {
1180
1250
  loading: true,
1181
1251
  value: void 0,
1182
1252
  error: void 0
1183
1253
  };
1184
1254
  result.catch(() => {});
1185
- }
1255
+ } else initialEnvelope = {
1256
+ loading: false,
1257
+ value: result,
1258
+ error: void 0
1259
+ };
1186
1260
  } catch (err) {
1187
1261
  globalThis[ILHA_QUERY_PENDING] = void 0;
1188
1262
  initialEnvelope = {
@@ -1194,11 +1268,7 @@ function createDerivedProxy({ entries, state, input, derivedSnapshot }) {
1194
1268
  stack.pop();
1195
1269
  ac.abort();
1196
1270
  }
1197
- } else initialEnvelope = {
1198
- loading: true,
1199
- value: void 0,
1200
- error: void 0
1201
- };
1271
+ }
1202
1272
  const env = signal(initialEnvelope);
1203
1273
  envelopes.set(entry.key, env);
1204
1274
  }
@@ -1478,7 +1548,7 @@ function applyJsxEvents({ host, records, reportError, unmountSignal, onceFired }
1478
1548
  }
1479
1549
  return () => cleanups.forEach((cleanup) => cleanup());
1480
1550
  }
1481
- function applyTemplateBindings(host, binds) {
1551
+ function applyTemplateBindings(host, binds, phase = "all") {
1482
1552
  if (binds.length === 0) return () => {};
1483
1553
  const cleanups = [];
1484
1554
  const elements = [];
@@ -1493,14 +1563,16 @@ function applyTemplateBindings(host, binds) {
1493
1563
  const { event, read, write } = resolveBindOps(el, spec.kind);
1494
1564
  const accessor = record.accessor;
1495
1565
  if (spec.kind === "this") {
1496
- accessor(el);
1497
- cleanups.push(() => accessor(null));
1566
+ if (phase !== "listen") {
1567
+ accessor(el);
1568
+ cleanups.push(() => accessor(null));
1569
+ }
1498
1570
  continue;
1499
1571
  }
1500
- try {
1572
+ if (phase !== "listen") try {
1501
1573
  write(el, accessor());
1502
1574
  } catch (err) {}
1503
- if (event === null) continue;
1575
+ if (phase === "reflect" || event === null) continue;
1504
1576
  const listener = () => {
1505
1577
  const raw = read(el);
1506
1578
  if (spec.kind === "group") {
@@ -1624,6 +1696,7 @@ var IlhaBuilder = class IlhaBuilder {
1624
1696
  schema,
1625
1697
  defaultInput,
1626
1698
  states: [],
1699
+ streams: [],
1627
1700
  deriveds: [],
1628
1701
  actions: [],
1629
1702
  ons: [],
@@ -1651,6 +1724,21 @@ var IlhaBuilder = class IlhaBuilder {
1651
1724
  }]
1652
1725
  });
1653
1726
  }
1727
+ stream(key, fn) {
1728
+ const cfg = this._cfg;
1729
+ const states = cfg.states.some((entry) => entry.key === key) ? cfg.states : [...cfg.states, {
1730
+ key,
1731
+ init: void 0
1732
+ }];
1733
+ return new IlhaBuilder({
1734
+ ...cfg,
1735
+ states,
1736
+ streams: [...cfg.streams.filter((entry) => entry.key !== key), {
1737
+ key,
1738
+ fn
1739
+ }]
1740
+ });
1741
+ }
1654
1742
  derived(key, fn) {
1655
1743
  const cfg = this._cfg;
1656
1744
  return new IlhaBuilder({
@@ -1724,8 +1812,8 @@ var IlhaBuilder = class IlhaBuilder {
1724
1812
  });
1725
1813
  }
1726
1814
  render(fn) {
1727
- const { schema, defaultInput, states, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1728
- const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
1815
+ const { schema, defaultInput, states, streams, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1816
+ const stylePrefix = cssSource == null ? "" : buildScopedStyle(cssSource);
1729
1817
  const configuredSlotTag = slotAs ?? "div";
1730
1818
  function resolveInput(props) {
1731
1819
  const merged = {
@@ -1735,8 +1823,8 @@ var IlhaBuilder = class IlhaBuilder {
1735
1823
  if (!schema) return merged;
1736
1824
  return validateSchema(schema, merged);
1737
1825
  }
1738
- function renderWithCtx({ render, liveHost, asyncChildren }) {
1739
- const ctx = pushRenderCtx(liveHost, asyncChildren);
1826
+ function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
1827
+ const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
1740
1828
  try {
1741
1829
  const html = unwrapHtml(render());
1742
1830
  const slots = ctx.slots;
@@ -1789,8 +1877,19 @@ var IlhaBuilder = class IlhaBuilder {
1789
1877
  }
1790
1878
  function buildSsrActions() {
1791
1879
  const out = {};
1880
+ const lookup = /* @__PURE__ */ new Map();
1792
1881
  for (const entry of actions) {
1793
- const invoke = () => {};
1882
+ const invoke = (...callArgs) => {
1883
+ const frame = captureStack[captureStack.length - 1];
1884
+ if (frame) {
1885
+ if (!frame.some((r) => r.k === entry.key)) frame.push({
1886
+ k: entry.key,
1887
+ a: callArgs
1888
+ });
1889
+ return;
1890
+ }
1891
+ };
1892
+ invoke[SSR_ACTION_STUB] = true;
1794
1893
  Object.defineProperties(invoke, {
1795
1894
  pending: {
1796
1895
  get: () => false,
@@ -1806,8 +1905,12 @@ var IlhaBuilder = class IlhaBuilder {
1806
1905
  }
1807
1906
  });
1808
1907
  out[entry.key] = invoke;
1908
+ lookup.set(invoke, entry.key);
1809
1909
  }
1810
- return out;
1910
+ return {
1911
+ action: out,
1912
+ lookup
1913
+ };
1811
1914
  }
1812
1915
  /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1813
1916
  function createSsrOnMountHost() {
@@ -1828,6 +1931,7 @@ var IlhaBuilder = class IlhaBuilder {
1828
1931
  if (onMounts.length === 0) return;
1829
1932
  if (currentRenderCtx()) return;
1830
1933
  const host = createSsrOnMountHost();
1934
+ const controller = new AbortController();
1831
1935
  const ssrCleanups = [];
1832
1936
  for (const entry of onMounts) {
1833
1937
  const prevSub = setActiveSub(void 0);
@@ -1839,7 +1943,8 @@ var IlhaBuilder = class IlhaBuilder {
1839
1943
  action,
1840
1944
  input,
1841
1945
  host,
1842
- hydrated: false
1946
+ hydrated: false,
1947
+ signal: controller.signal
1843
1948
  });
1844
1949
  } catch (err) {
1845
1950
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1863,6 +1968,7 @@ var IlhaBuilder = class IlhaBuilder {
1863
1968
  }
1864
1969
  if (typeof userCleanup === "function") ssrCleanups.push(userCleanup);
1865
1970
  }
1971
+ controller.abort();
1866
1972
  for (const teardown of ssrCleanups.reverse()) try {
1867
1973
  teardown();
1868
1974
  } catch (err) {
@@ -1872,11 +1978,45 @@ var IlhaBuilder = class IlhaBuilder {
1872
1978
  }
1873
1979
  }
1874
1980
  }
1875
- function renderToString(props, sync = false) {
1981
+ function renderToString(props, sync = false, streamOut, eventsOut, stateOverride, forceAsyncChildren = false) {
1876
1982
  const input = resolveInput(props);
1877
- const state = buildSignalState(input);
1878
- const action = buildSsrActions();
1879
- const results = deriveds.map((entry) => {
1983
+ const state = buildSignalState(input, stateOverride);
1984
+ const { action, lookup: actionLookup } = buildSsrActions();
1985
+ const recordEventsManifest = (events) => {
1986
+ if (!eventsOut || events.length === 0) return;
1987
+ events.forEach((record, index) => {
1988
+ if (record.capture) {
1989
+ eventsOut.set(`${record.type}:${index}`, record.capture);
1990
+ return;
1991
+ }
1992
+ const key = actionLookup.get(record.handler);
1993
+ if (key) eventsOut.set(`${record.type}:${index}`, key);
1994
+ });
1995
+ };
1996
+ const pullStreamInitials = async (streamOut) => {
1997
+ for (const entry of streams) {
1998
+ const controller = new AbortController();
1999
+ try {
2000
+ const gen = await entry.fn({
2001
+ input,
2002
+ signal: controller.signal
2003
+ });
2004
+ const first = await gen.next();
2005
+ controller.abort();
2006
+ await gen.return?.(void 0);
2007
+ if (first.done) continue;
2008
+ if (streamOut) streamOut[entry.key] = first.value;
2009
+ const prevSub = setActiveSub(void 0);
2010
+ try {
2011
+ const setter = state[entry.key];
2012
+ setter?.(first.value);
2013
+ } finally {
2014
+ setActiveSub(prevSub);
2015
+ }
2016
+ } catch {}
2017
+ }
2018
+ };
2019
+ const computeDerivedResults = () => deriveds.map((entry) => {
1880
2020
  const prevSub = setActiveSub(void 0);
1881
2021
  try {
1882
2022
  return {
@@ -1896,84 +2036,95 @@ var IlhaBuilder = class IlhaBuilder {
1896
2036
  setActiveSub(prevSub);
1897
2037
  }
1898
2038
  });
1899
- if (!results.some((r) => r.result instanceof Promise) || sync) {
1900
- const envelopes = {};
1901
- for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
1902
- loading: true,
1903
- value: void 0,
1904
- error: void 0
1905
- };
1906
- else envelopes[r.key] = {
1907
- loading: false,
1908
- value: r.result,
1909
- error: void 0
1910
- };
1911
- const derived = buildDerivedAccessors(envelopes);
1912
- if (!currentRenderCtx()) runOnMountForRender({
1913
- input,
1914
- state,
1915
- derived,
1916
- action
1917
- });
1918
- const prevSub = setActiveSub(void 0);
1919
- try {
1920
- const { html } = renderWithCtx({ render: () => fn({
1921
- state,
1922
- derived,
1923
- action,
1924
- input
1925
- }) });
1926
- return stylePrefix + html;
1927
- } finally {
1928
- setActiveSub(prevSub);
1929
- }
1930
- }
1931
- return Promise.all(results.map(async (r) => {
1932
- try {
1933
- return {
1934
- key: r.key,
1935
- envelope: {
1936
- loading: false,
1937
- value: await Promise.resolve(r.result),
1938
- error: void 0
1939
- }
2039
+ if (!sync && (streams.length > 0 || forceAsyncChildren)) return pullStreamInitials(streamOut).then(() => renderToStringTail());
2040
+ return renderToStringTail();
2041
+ function renderToStringTail() {
2042
+ const results = computeDerivedResults();
2043
+ if (!(forceAsyncChildren || results.some((r) => r.result instanceof Promise)) || sync) {
2044
+ const envelopes = {};
2045
+ for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
2046
+ loading: true,
2047
+ value: void 0,
2048
+ error: void 0
1940
2049
  };
1941
- } catch (err) {
1942
- return {
1943
- key: r.key,
1944
- envelope: {
1945
- loading: false,
1946
- value: void 0,
1947
- error: err instanceof Error ? err : new Error(String(err))
1948
- }
2050
+ else envelopes[r.key] = {
2051
+ loading: false,
2052
+ value: r.result,
2053
+ error: void 0
1949
2054
  };
1950
- }
1951
- })).then(async (resolved) => {
1952
- const envelopes = {};
1953
- for (const r of resolved) envelopes[r.key] = r.envelope;
1954
- const derived = buildDerivedAccessors(envelopes);
1955
- if (!currentRenderCtx()) runOnMountForRender({
1956
- input,
1957
- state,
1958
- derived,
1959
- action
1960
- });
1961
- const prevSub = setActiveSub(void 0);
1962
- try {
1963
- const { html } = await renderWithCtx({
1964
- render: () => fn({
1965
- state,
1966
- derived,
1967
- action,
1968
- input
1969
- }),
1970
- asyncChildren: true
2055
+ const derived = buildDerivedAccessors(envelopes);
2056
+ if (!currentRenderCtx()) runOnMountForRender({
2057
+ input,
2058
+ state,
2059
+ derived,
2060
+ action
1971
2061
  });
1972
- return stylePrefix + html;
1973
- } finally {
1974
- setActiveSub(prevSub);
2062
+ const prevSub = setActiveSub(void 0);
2063
+ try {
2064
+ const { html, events } = renderWithCtx({
2065
+ render: () => fn({
2066
+ state,
2067
+ derived,
2068
+ action,
2069
+ input
2070
+ }),
2071
+ manifest: eventsOut !== void 0
2072
+ });
2073
+ recordEventsManifest(events);
2074
+ return stylePrefix + html;
2075
+ } finally {
2076
+ setActiveSub(prevSub);
2077
+ }
1975
2078
  }
1976
- });
2079
+ return Promise.all(results.map(async (r) => {
2080
+ try {
2081
+ return {
2082
+ key: r.key,
2083
+ envelope: {
2084
+ loading: false,
2085
+ value: await Promise.resolve(r.result),
2086
+ error: void 0
2087
+ }
2088
+ };
2089
+ } catch (err) {
2090
+ return {
2091
+ key: r.key,
2092
+ envelope: {
2093
+ loading: false,
2094
+ value: void 0,
2095
+ error: err instanceof Error ? err : new Error(String(err))
2096
+ }
2097
+ };
2098
+ }
2099
+ })).then(async (resolved) => {
2100
+ const envelopes = {};
2101
+ for (const r of resolved) envelopes[r.key] = r.envelope;
2102
+ const derived = buildDerivedAccessors(envelopes);
2103
+ if (!currentRenderCtx()) runOnMountForRender({
2104
+ input,
2105
+ state,
2106
+ derived,
2107
+ action
2108
+ });
2109
+ const prevSub = setActiveSub(void 0);
2110
+ try {
2111
+ const { html, events } = await renderWithCtx({
2112
+ render: () => fn({
2113
+ state,
2114
+ derived,
2115
+ action,
2116
+ input
2117
+ }),
2118
+ asyncChildren: true,
2119
+ manifest: eventsOut !== void 0
2120
+ });
2121
+ recordEventsManifest(events);
2122
+ return stylePrefix + html;
2123
+ } finally {
2124
+ setActiveSub(prevSub);
2125
+ }
2126
+ });
2127
+ }
1977
2128
  }
1978
2129
  function mountIsland(host, props) {
1979
2130
  return mountIslandInternal(host, props).unmount;
@@ -2017,7 +2168,7 @@ var IlhaBuilder = class IlhaBuilder {
2017
2168
  const parsed = safeParseSnapshot(rawState, STATE_ATTR);
2018
2169
  if (parsed !== void 0) snapshotRaw = parsed;
2019
2170
  }
2020
- const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount")) : void 0;
2171
+ const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount" && k !== "_streams")) : void 0;
2021
2172
  const derivedSnapshotRaw = snapshotRaw?._derived;
2022
2173
  let derivedSnapshot;
2023
2174
  if (derivedSnapshotRaw) {
@@ -2350,7 +2501,8 @@ var IlhaBuilder = class IlhaBuilder {
2350
2501
  action,
2351
2502
  input,
2352
2503
  host,
2353
- hydrated
2504
+ hydrated,
2505
+ signal: unmountController.signal
2354
2506
  });
2355
2507
  } catch (err) {
2356
2508
  reportError(err, "mount");
@@ -2382,7 +2534,8 @@ var IlhaBuilder = class IlhaBuilder {
2382
2534
  liveHost: preserveSSRDom ? host : void 0
2383
2535
  });
2384
2536
  if (!preserveSSRDom) host.innerHTML = stylePrefix + initial.html;
2385
- let stopBindings = applyTemplateBindings(host, initial.binds);
2537
+ const stopInitialBindingReflection = applyTemplateBindings(host, initial.binds, "reflect");
2538
+ let stopBindings = stopInitialBindingReflection;
2386
2539
  cleanups.push(() => stopBindings());
2387
2540
  let stopJsxEvents = applyJsxEvents({
2388
2541
  host,
@@ -2396,6 +2549,11 @@ var IlhaBuilder = class IlhaBuilder {
2396
2549
  cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
2397
2550
  attachListeners();
2398
2551
  if (!hydrated && onMounts.length > 0) invokeOnMounts();
2552
+ const stopInitialBindingListeners = applyTemplateBindings(host, initial.binds, "listen");
2553
+ stopBindings = () => {
2554
+ stopInitialBindingListeners();
2555
+ stopInitialBindingReflection();
2556
+ };
2399
2557
  for (const entry of effects) {
2400
2558
  let userCleanup;
2401
2559
  let runController = null;
@@ -2439,6 +2597,36 @@ var IlhaBuilder = class IlhaBuilder {
2439
2597
  }
2440
2598
  stopDerived = setupDerived();
2441
2599
  cleanups.push(stopDerived);
2600
+ for (const entry of streams) {
2601
+ let streamAborted = false;
2602
+ const streamController = new AbortController();
2603
+ const runStream = async () => {
2604
+ try {
2605
+ const gen = await entry.fn({
2606
+ input,
2607
+ signal: streamController.signal
2608
+ });
2609
+ while (!streamAborted && !streamController.signal.aborted) {
2610
+ const { done, value } = await gen.next();
2611
+ if (streamAborted || streamController.signal.aborted || done) break;
2612
+ const prevSub = setActiveSub(void 0);
2613
+ try {
2614
+ const setter = state[entry.key];
2615
+ setter?.(value);
2616
+ } finally {
2617
+ setActiveSub(prevSub);
2618
+ }
2619
+ }
2620
+ } catch (err) {
2621
+ if (!streamAborted && !streamController.signal.aborted && err?.name !== "AbortError") reportError(err, "stream");
2622
+ }
2623
+ };
2624
+ runStream();
2625
+ cleanups.push(() => {
2626
+ streamAborted = true;
2627
+ streamController.abort();
2628
+ });
2629
+ }
2442
2630
  let initialized = false;
2443
2631
  const initialRenderedHtml = initial.html;
2444
2632
  let lastRendered = null;
@@ -2618,6 +2806,12 @@ var IlhaBuilder = class IlhaBuilder {
2618
2806
  island.mount = (host, props) => mountIsland(host, props);
2619
2807
  island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2620
2808
  island[ISLAND_SLOT_TAG] = configuredSlotTag;
2809
+ island[ISLAND_RENDER_STATE] = async (props) => {
2810
+ const manifest = /* @__PURE__ */ new Map();
2811
+ const html = await renderToString(props ?? void 0, false, void 0, manifest, void 0, true);
2812
+ return manifestTemplatePrefix(manifest) + html;
2813
+ };
2814
+ island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, void 0, eventsOut, void 0, true);
2621
2815
  island.key = (key) => {
2622
2816
  if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2623
2817
  if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
@@ -2631,6 +2825,8 @@ var IlhaBuilder = class IlhaBuilder {
2631
2825
  return keyed;
2632
2826
  };
2633
2827
  island[ISLAND] = true;
2828
+ const astroRendererName = globalThis[ASTRO_RENDERER_GLOBAL];
2829
+ if (typeof astroRendererName === "string") island[Symbol.for("astro:renderer")] = astroRendererName;
2634
2830
  island.define = (tagName, options) => {
2635
2831
  if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
2636
2832
  `${tagName}`;
@@ -2715,7 +2911,9 @@ var IlhaBuilder = class IlhaBuilder {
2715
2911
  const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2716
2912
  const tag = assertValidSlotTagName(rawTag);
2717
2913
  const resolvedProps = props ?? {};
2718
- const inner = await renderToString(resolvedProps);
2914
+ const streamValues = {};
2915
+ const eventsManifest = /* @__PURE__ */ new Map();
2916
+ const inner = await renderToString(resolvedProps, false, streamValues, eventsManifest, void 0, true);
2719
2917
  const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
2720
2918
  let stateAttr = "";
2721
2919
  if (snapshot !== false) {
@@ -2725,7 +2923,9 @@ var IlhaBuilder = class IlhaBuilder {
2725
2923
  const snapshotData = {};
2726
2924
  const input = resolveInput(resolvedProps);
2727
2925
  const plainState = buildPlainState(input);
2728
- if (doState) for (const entry of states) snapshotData[entry.key] = plainState[entry.key]();
2926
+ const stateForDerived = { ...plainState };
2927
+ for (const [k, v] of Object.entries(streamValues)) stateForDerived[k] = () => v;
2928
+ if (doState) for (const entry of states) snapshotData[entry.key] = entry.key in streamValues ? streamValues[entry.key] : plainState[entry.key]();
2729
2929
  if (doDerived) {
2730
2930
  const derivedResults = {};
2731
2931
  for (const entry of deriveds) {
@@ -2735,7 +2935,7 @@ var IlhaBuilder = class IlhaBuilder {
2735
2935
  let threw = false;
2736
2936
  try {
2737
2937
  resultPromise = entry.fn({
2738
- state: plainState,
2938
+ state: stateForDerived,
2739
2939
  input,
2740
2940
  signal: new AbortController().signal
2741
2941
  });
@@ -2771,9 +2971,12 @@ var IlhaBuilder = class IlhaBuilder {
2771
2971
  snapshotData["_derived"] = derivedResults;
2772
2972
  }
2773
2973
  if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2974
+ if (streams.length > 0) snapshotData["_streams"] = Object.fromEntries(streams.map((entry) => [entry.key, true]));
2774
2975
  stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2775
2976
  }
2776
- return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}>${inner}</${tag}>`;
2977
+ let actionsAttr = "";
2978
+ if (eventsManifest.size > 0) actionsAttr = ` ${ACTIONS_ATTR}='${escapeHtml(JSON.stringify(Object.fromEntries(eventsManifest)))}'`;
2979
+ return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}${actionsAttr}>${inner}</${tag}>`;
2777
2980
  };
2778
2981
  return island;
2779
2982
  }
@@ -2835,6 +3038,7 @@ const rootBuilder = new IlhaBuilder({
2835
3038
  schema: null,
2836
3039
  defaultInput: null,
2837
3040
  states: [],
3041
+ streams: [],
2838
3042
  deriveds: [],
2839
3043
  actions: [],
2840
3044
  ons: [],
@@ -2871,11 +3075,14 @@ const ilha = Object.assign(callableRoot, {
2871
3075
  function __ilhaJsxEvent({ type, handler, modifier }) {
2872
3076
  const ctx = currentRenderCtx();
2873
3077
  if (!ctx) return void 0;
3078
+ const isActionStub = SSR_ACTION_STUB in handler;
3079
+ const capture = ctx.manifest && !isActionStub ? captureHandlerActions(handler) : void 0;
2874
3080
  const index = ctx.events.length;
2875
3081
  ctx.events.push({
2876
3082
  type,
2877
3083
  handler,
2878
- modifier
3084
+ modifier,
3085
+ capture
2879
3086
  });
2880
3087
  return index;
2881
3088
  }
@@ -2887,6 +3094,27 @@ function __ilhaJsxSlot({ island, props, key }) {
2887
3094
  key
2888
3095
  }));
2889
3096
  }
3097
+ /**
3098
+ * Morph an element's children toward `html`, patching in place: surviving
3099
+ * elements keep their identity, listeners, and focus (see the morph engine).
3100
+ * Falls back to an innerHTML write when the root tags don't match.
3101
+ */
3102
+ function morph(host, html) {
3103
+ if (typeof document === "undefined") return;
3104
+ const openTag = host.localName ?? "div";
3105
+ const tpl = document.createElement("template");
3106
+ tpl.innerHTML = `<${openTag}>${html}</${openTag}>`;
3107
+ const next = tpl.content.firstElementChild;
3108
+ if (!next || next.localName !== host.localName) {
3109
+ host.innerHTML = html;
3110
+ return;
3111
+ }
3112
+ try {
3113
+ morphInner(host, next);
3114
+ } catch {
3115
+ host.innerHTML = html;
3116
+ }
3117
+ }
2890
3118
  const html = ilhaHtml;
2891
3119
  const raw = ilhaRaw;
2892
3120
  const css = ilhaCss;
@@ -2895,4 +3123,4 @@ const from = ilhaFrom;
2895
3123
  const context = ilhaContext;
2896
3124
 
2897
3125
  //#endregion
2898
- export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
3126
+ export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
@@ -1,2 +1,2 @@
1
- import { a as JSX, r as jsxDEV, t as Fragment } from "./jsx-runtime-DJ2B4fvA.js";
1
+ import { a as JSX, r as jsxDEV, t as Fragment } from "./jsx-runtime-B-viVOB1.js";
2
2
  export { Fragment, type JSX, jsxDEV };
@@ -1,4 +1,4 @@
1
- import { C as NativeEventModifier, E as SignalAccessor, O as SignalWriter, S as NativeEventHandler, T as RawHtml } from "./index-Bi_I3qEL.js";
1
+ import { C as NativeEventModifier, E as SignalAccessor, O as SignalWriter, S as NativeEventHandler, T as RawHtml } from "./index-Cx35Xugv.js";
2
2
  //#region src/jsx-types.d.ts
3
3
  declare namespace JSX {
4
4
  export type Child = unknown;
@@ -1,2 +1,2 @@
1
- import { a as JSX, i as jsxs, n as jsx, r as jsxDEV, t as Fragment } from "./jsx-runtime-DJ2B4fvA.js";
1
+ import { a as JSX, i as jsxs, n as jsx, r as jsxDEV, t as Fragment } from "./jsx-runtime-B-viVOB1.js";
2
2
  export { Fragment, type JSX, jsx, jsxDEV, jsxs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "keywords": [
6
6
  "framework-free",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsc && tsdown",
56
+ "cleanup": "rimraf dist node_modules",
56
57
  "test": "bun test"
57
58
  },
58
59
  "dependencies": {