ilha 0.9.4 → 0.9.5

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +116 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -420,6 +420,8 @@ const Card = ilha.render(
420
420
  );
421
421
  ```
422
422
 
423
+ Props (including JSX `children` and function callbacks) stay on the **live slot map** during parent render/mount. `data-ilha-props` only carries JSON-safe scalars for hydration hints — never rely on it for children or handlers. Nested islands under layout shells (`defineLayout` / `wrapLayout`) use the same path as page-level composition.
424
+
423
425
  **Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
424
426
 
425
427
  ```ts
package/dist/index.js CHANGED
@@ -121,9 +121,96 @@ function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
121
121
  }
122
122
  return true;
123
123
  }
124
+ /**
125
+ * Props safe to embed in `data-ilha-props`.
126
+ * Functions cannot round-trip through JSON; children are owned by the live slot
127
+ * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
128
+ * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
129
+ */
130
+ function slotPropsForAttr(props) {
131
+ if (props === void 0) return void 0;
132
+ let out;
133
+ for (const key of Object.keys(props)) {
134
+ if (key === "children") continue;
135
+ const value = props[key];
136
+ if (typeof value === "function" || typeof value === "symbol") continue;
137
+ const encoded = encodeSlotPropValue(value);
138
+ if (encoded === void 0 && value !== void 0 && value !== null) continue;
139
+ (out ??= {})[key] = encoded;
140
+ }
141
+ return out;
142
+ }
143
+ const SLOT_RAW = Symbol.for("ilha.raw");
144
+ function isRawHtmlValue(v) {
145
+ return !!(v && typeof v === "object" && SLOT_RAW in v);
146
+ }
147
+ function encodeSlotPropValue(value, seen) {
148
+ if (value == null) return value;
149
+ if (typeof value === "function" || typeof value === "symbol") return void 0;
150
+ if (typeof value !== "object") return value;
151
+ if (isRawHtmlValue(value)) return {
152
+ __ilha: "raw",
153
+ value: value.value
154
+ };
155
+ const visited = seen ?? /* @__PURE__ */ new WeakSet();
156
+ if (visited.has(value)) throw new TypeError("encodeSlotPropValue: circular reference in slot props");
157
+ visited.add(value);
158
+ if (Array.isArray(value)) return value.map((item) => encodeSlotPropValue(item, visited)).filter((item) => item !== void 0);
159
+ if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
160
+ const obj = value;
161
+ const encoded = {};
162
+ for (const key of Object.keys(obj)) {
163
+ const next = encodeSlotPropValue(obj[key], visited);
164
+ if (next !== void 0 || obj[key] === null) encoded[key] = next;
165
+ }
166
+ return encoded;
167
+ }
168
+ function makeRawHtml(value) {
169
+ return {
170
+ [SLOT_RAW]: true,
171
+ value
172
+ };
173
+ }
174
+ function reviveSlotPropValue(value) {
175
+ if (value == null || typeof value !== "object") return value;
176
+ if (Array.isArray(value)) return value.map(reviveSlotPropValue);
177
+ const obj = value;
178
+ if (obj.__ilha === "raw" && typeof obj.value === "string") return makeRawHtml(obj.value);
179
+ const out = {};
180
+ for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
181
+ return out;
182
+ }
183
+ /**
184
+ * Exact legacy shape produced when RawHtml lost its brand under JSON.stringify:
185
+ * a plain object with a single own key `value` that is a string.
186
+ */
187
+ function isLegacyRawHtmlBlob(value) {
188
+ if (!value || typeof value !== "object" || isRawHtmlValue(value)) return false;
189
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
190
+ const keys = Object.keys(value);
191
+ return keys.length === 1 && keys[0] === "value" && typeof value.value === "string";
192
+ }
193
+ /** Revive attr-parsed props (RawHtml tags). Children stay as plain data if present. */
194
+ function reviveSlotProps(props) {
195
+ if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
196
+ const out = reviveSlotPropValue(props);
197
+ if ("children" in out) out.children = reviveLegacyChildren(out.children);
198
+ return out;
199
+ }
200
+ function reviveLegacyChildren(children) {
201
+ if (!Array.isArray(children)) {
202
+ if (isLegacyRawHtmlBlob(children)) return makeRawHtml(children.value);
203
+ return reviveSlotPropValue(children);
204
+ }
205
+ return children.map((child) => {
206
+ if (isLegacyRawHtmlBlob(child)) return makeRawHtml(child.value);
207
+ return reviveSlotPropValue(child);
208
+ });
209
+ }
124
210
  /** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
125
211
  function serializeSlotProps(props) {
126
- return props === void 0 ? "" : JSON.stringify(props);
212
+ const safe = slotPropsForAttr(props);
213
+ return safe === void 0 ? "" : JSON.stringify(safe);
127
214
  }
128
215
  const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
129
216
  const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
@@ -400,7 +487,11 @@ const CSS_ATTR = "data-ilha-css";
400
487
  function buildScopedStyle(css) {
401
488
  return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css.replace(/<\/style/gi, "<\\/style")}}</style>`;
402
489
  }
403
- const renderCtxStack = [];
490
+ const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
491
+ function renderCtxStack() {
492
+ const g = globalThis;
493
+ return g[RENDER_CTX_STACK] ??= [];
494
+ }
404
495
  function pushRenderCtx(liveHost, asyncChildren) {
405
496
  const ctx = {
406
497
  slots: /* @__PURE__ */ new Map(),
@@ -409,14 +500,15 @@ function pushRenderCtx(liveHost, asyncChildren) {
409
500
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
410
501
  binds: []
411
502
  };
412
- renderCtxStack.push(ctx);
503
+ renderCtxStack().push(ctx);
413
504
  return ctx;
414
505
  }
415
506
  function popRenderCtx() {
416
- renderCtxStack.pop();
507
+ renderCtxStack().pop();
417
508
  }
418
509
  function currentRenderCtx() {
419
- return renderCtxStack[renderCtxStack.length - 1];
510
+ const stack = renderCtxStack();
511
+ return stack[stack.length - 1];
420
512
  }
421
513
  function isIsland(v) {
422
514
  return typeof v === "function" && ISLAND in v;
@@ -436,7 +528,8 @@ function emitIslandSlot(island, props, key) {
436
528
  props
437
529
  });
438
530
  const slotTag = getIslandSlotTag(island);
439
- const propsAttr = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
531
+ const attrProps = slotPropsForAttr(props);
532
+ const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
440
533
  if (ctx?.liveHost) return wrapIslandSlotHtml(slotTag, id, propsAttr, "");
441
534
  if (ctx?.pending) {
442
535
  popRenderCtx();
@@ -448,7 +541,7 @@ function emitIslandSlot(island, props, key) {
448
541
  }
449
542
  return wrapIslandSlotHtml(slotTag, id, propsAttr, String(result));
450
543
  } finally {
451
- renderCtxStack.push(ctx);
544
+ renderCtxStack().push(ctx);
452
545
  }
453
546
  }
454
547
  const inner = island.toString(props);
@@ -1539,7 +1632,7 @@ var IlhaBuilder = class IlhaBuilder {
1539
1632
  const rawProps = host.getAttribute(PROPS_ATTR);
1540
1633
  if (rawProps) {
1541
1634
  const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
1542
- if (parsed !== void 0) props = parsed;
1635
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
1543
1636
  }
1544
1637
  }
1545
1638
  const inputSignal = signal(resolveInput(props));
@@ -1661,6 +1754,13 @@ var IlhaBuilder = class IlhaBuilder {
1661
1754
  }
1662
1755
  return index;
1663
1756
  }
1757
+ /** Push live props from a fresh slot map into already-mounted children. */
1758
+ function pushUpdatedProps(nextSlots) {
1759
+ for (const [id, entry] of mountedSlots) {
1760
+ const next = nextSlots.get(id);
1761
+ if (next) entry.updateProps(next.props);
1762
+ }
1763
+ }
1664
1764
  function mountSlots(slotMap) {
1665
1765
  for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
1666
1766
  for (const [id, entry] of mountedSlots) {
@@ -1682,7 +1782,7 @@ var IlhaBuilder = class IlhaBuilder {
1682
1782
  const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
1683
1783
  if (rawProps) {
1684
1784
  const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
1685
- if (parsed !== void 0) slotProps = parsed;
1785
+ if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
1686
1786
  }
1687
1787
  }
1688
1788
  const prevSub = setActiveSub(void 0);
@@ -1873,10 +1973,7 @@ var IlhaBuilder = class IlhaBuilder {
1873
1973
  if (!initialized) {
1874
1974
  initialized = true;
1875
1975
  if (preserveSSRDom) {
1876
- for (const [id, entry] of mountedSlots) {
1877
- const next = newSlotMap.get(id);
1878
- if (next) entry.updateProps(next.props);
1879
- }
1976
+ pushUpdatedProps(newSlotMap);
1880
1977
  if (rendered === initialRenderedHtml) {
1881
1978
  lastRendered = rendered;
1882
1979
  return;
@@ -1886,13 +1983,13 @@ var IlhaBuilder = class IlhaBuilder {
1886
1983
  lastRendered = rendered;
1887
1984
  return;
1888
1985
  }
1889
- if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
1986
+ if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) {
1987
+ pushUpdatedProps(newSlotMap);
1988
+ return;
1989
+ }
1890
1990
  }
1891
1991
  if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
1892
- for (const [id, entry] of mountedSlots) {
1893
- const next = newSlotMap.get(id);
1894
- if (next) entry.updateProps(next.props);
1895
- }
1992
+ pushUpdatedProps(newSlotMap);
1896
1993
  return;
1897
1994
  }
1898
1995
  detachListeners();
@@ -2198,7 +2295,7 @@ function mountAll(registry, options = {}) {
2198
2295
  const rawProps = host.getAttribute(PROPS_ATTR);
2199
2296
  if (rawProps) {
2200
2297
  const parsed = safeParseSnapshot(rawProps, `${PROPS_ATTR} on [data-ilha="${name}"]`);
2201
- if (parsed !== void 0) props = parsed;
2298
+ if (parsed !== void 0) props = reviveSlotProps(parsed);
2202
2299
  }
2203
2300
  unmounts.push(island.mount(host, props));
2204
2301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "keywords": [
6
6
  "framework-free",