@barefootjs/client 0.5.0 → 0.5.2

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.
@@ -304,6 +304,9 @@ var BF_SCOPE_COMMENT_PREFIX = "bf-scope:";
304
304
  var BF_LOOP_START = "bf-loop";
305
305
  var BF_LOOP_END = "bf-/loop";
306
306
  var BF_LOOP_ITEM = "bf-loop-i";
307
+ function loopItemMarker(key) {
308
+ return `${BF_LOOP_ITEM}:${key}`;
309
+ }
307
310
  function loopStartMarker(markerId) {
308
311
  return `${BF_LOOP_START}:${markerId}`;
309
312
  }
@@ -552,10 +555,18 @@ function getPortalScopeId(element) {
552
555
  return info?.scopeId ?? null;
553
556
  }
554
557
  function getCommentScopeBoundary(commentNode) {
558
+ const isLoopItem = commentNode.nodeValue?.startsWith(`${BF_LOOP_ITEM}:`) ?? false;
555
559
  let node = commentNode.nextSibling;
556
560
  while (node) {
557
- if (node.nodeType === Node.COMMENT_NODE && node.nodeValue?.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
558
- return node;
561
+ if (node.nodeType === Node.COMMENT_NODE) {
562
+ const value = node.nodeValue ?? "";
563
+ if (isLoopItem) {
564
+ if (value.startsWith(`${BF_LOOP_ITEM}:`) || value.startsWith(`${BF_LOOP_END}:`)) {
565
+ return node;
566
+ }
567
+ } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
568
+ return node;
569
+ }
559
570
  }
560
571
  node = node.nextSibling;
561
572
  }
@@ -1131,7 +1142,9 @@ function setParentScopeId(id) {
1131
1142
  }
1132
1143
  var propsUpdateMap = new WeakMap;
1133
1144
  var propsMap = new WeakMap;
1134
- function createComponent(nameOrDef, props, key, slot) {
1145
+ function createComponent(nameOrDef, props = {}, key, slot) {
1146
+ if (props == null)
1147
+ props = {};
1135
1148
  if (typeof nameOrDef !== "string") {
1136
1149
  return createComponentFromDef(nameOrDef, props, key);
1137
1150
  }
@@ -1915,6 +1928,150 @@ function mapArray(accessor, container, getKey, renderItem, markerId) {
1915
1928
  }
1916
1929
  });
1917
1930
  }
1931
+ var ITEM_PREFIX = `${BF_LOOP_ITEM}:`;
1932
+ function isItemAnchor(node) {
1933
+ return node.nodeType === Node.COMMENT_NODE && (node.nodeValue ?? "").startsWith(ITEM_PREFIX);
1934
+ }
1935
+ function collectAnchorRange(anchor, end) {
1936
+ const nodes = [anchor];
1937
+ let node = anchor.nextSibling;
1938
+ while (node && node !== end) {
1939
+ if (isItemAnchor(node))
1940
+ break;
1941
+ nodes.push(node);
1942
+ node = node.nextSibling;
1943
+ }
1944
+ return nodes;
1945
+ }
1946
+ function findItemAnchors(start, end) {
1947
+ const anchors = [];
1948
+ let node = start.nextSibling;
1949
+ while (node && node !== end) {
1950
+ if (isItemAnchor(node))
1951
+ anchors.push(node);
1952
+ node = node.nextSibling;
1953
+ }
1954
+ return anchors;
1955
+ }
1956
+ function placeAnchorScope(scope, container, before, end) {
1957
+ if (scope.pending) {
1958
+ container.insertBefore(scope.pending, before);
1959
+ scope.pending = null;
1960
+ return;
1961
+ }
1962
+ for (const node of collectAnchorRange(scope.anchor, end)) {
1963
+ container.insertBefore(node, before);
1964
+ }
1965
+ }
1966
+ function removeAnchorScope(scope, end) {
1967
+ for (const node of collectAnchorRange(scope.anchor, end)) {
1968
+ node.parentNode?.removeChild(node);
1969
+ }
1970
+ }
1971
+ function createAnchorScope(item, index, key, renderItem, existingAnchor) {
1972
+ let dispose;
1973
+ let setItem;
1974
+ let returned;
1975
+ createRoot((d) => {
1976
+ dispose = d;
1977
+ const [itemAccessor, itemSetter] = createSignal(item);
1978
+ setItem = itemSetter;
1979
+ returned = renderItem(itemAccessor, index, existingAnchor);
1980
+ return;
1981
+ });
1982
+ if (existingAnchor) {
1983
+ return { anchor: existingAnchor, pending: null, dispose, setItem };
1984
+ }
1985
+ const frag = returned;
1986
+ const anchor = frag.firstChild;
1987
+ if (anchor && !anchor.nodeValue?.startsWith(ITEM_PREFIX)) {
1988
+ anchor.nodeValue = loopItemMarker(key);
1989
+ }
1990
+ return { anchor, pending: frag, dispose, setItem };
1991
+ }
1992
+ function mapArrayAnchored(accessor, container, getKey, renderItem, markerId) {
1993
+ if (!container)
1994
+ return;
1995
+ const scopes = new Map;
1996
+ let hydrated = false;
1997
+ createEffect(() => {
1998
+ const items = accessor();
1999
+ if (!items)
2000
+ return;
2001
+ const { start, end } = findLoopMarkers2(container, markerId);
2002
+ if (!start || !end)
2003
+ return;
2004
+ if (!hydrated) {
2005
+ hydrated = true;
2006
+ const existing = findItemAnchors(start, end);
2007
+ if (existing.length > 0 && scopes.size === 0) {
2008
+ for (let i = 0;i < existing.length && i < items.length; i++) {
2009
+ const item = items[i];
2010
+ const key = getKey ? getKey(item, i) : String(i);
2011
+ scopes.set(key, createAnchorScope(item, i, key, renderItem, existing[i]));
2012
+ }
2013
+ for (let i = existing.length;i < items.length; i++) {
2014
+ const item = items[i];
2015
+ const key = getKey ? getKey(item, i) : String(i);
2016
+ const scope = createAnchorScope(item, i, key, renderItem);
2017
+ scopes.set(key, scope);
2018
+ placeAnchorScope(scope, container, end, end);
2019
+ }
2020
+ for (let i = items.length;i < existing.length; i++) {
2021
+ for (const node of collectAnchorRange(existing[i], end)) {
2022
+ node.parentNode?.removeChild(node);
2023
+ }
2024
+ }
2025
+ return;
2026
+ }
2027
+ }
2028
+ const newKeys = new Set;
2029
+ const warnedKeys = new Set;
2030
+ const desiredOrder = [];
2031
+ for (let i = 0;i < items.length; i++) {
2032
+ const item = items[i];
2033
+ const key = getKey ? getKey(item, i) : String(i);
2034
+ if (newKeys.has(key) && !warnedKeys.has(key)) {
2035
+ warnedKeys.add(key);
2036
+ console.warn(`[BarefootJS] mapArrayAnchored: duplicate key "${key}" — items with this key collapse to a ` + `single DOM scope, so only the last one renders. Use a per-item identifier (e.g. \`key={item.id}\`).`);
2037
+ }
2038
+ newKeys.add(key);
2039
+ const existing = scopes.get(key);
2040
+ if (existing) {
2041
+ existing.setItem(item);
2042
+ desiredOrder.push(existing);
2043
+ } else {
2044
+ const scope = createAnchorScope(item, i, key, renderItem);
2045
+ scopes.set(key, scope);
2046
+ desiredOrder.push(scope);
2047
+ }
2048
+ }
2049
+ for (const [key, scope] of scopes) {
2050
+ if (!newKeys.has(key)) {
2051
+ scope.dispose();
2052
+ removeAnchorScope(scope, end);
2053
+ scopes.delete(key);
2054
+ }
2055
+ }
2056
+ let inOrder = true;
2057
+ const domAnchors = findItemAnchors(start, end);
2058
+ if (domAnchors.length !== desiredOrder.length) {
2059
+ inOrder = false;
2060
+ } else {
2061
+ for (let i = 0;i < desiredOrder.length; i++) {
2062
+ if (domAnchors[i] !== desiredOrder[i].anchor) {
2063
+ inOrder = false;
2064
+ break;
2065
+ }
2066
+ }
2067
+ }
2068
+ if (!inOrder) {
2069
+ for (const scope of desiredOrder) {
2070
+ placeAnchorScope(scope, container, end, end);
2071
+ }
2072
+ }
2073
+ });
2074
+ }
1918
2075
  // src/runtime/style.ts
1919
2076
  function styleToCss(value) {
1920
2077
  if (value == null)
@@ -2019,6 +2176,55 @@ function spreadAttrs(obj) {
2019
2176
  return parts.join(" ");
2020
2177
  }
2021
2178
  // src/runtime/insert.ts
2179
+ function makeRegion(scope) {
2180
+ if (scope.nodeType === Node.COMMENT_NODE) {
2181
+ const anchor = scope;
2182
+ const parentEl = anchor.parentElement;
2183
+ const componentScope = parentEl?.closest(`[${BF_SCOPE}]`) ?? null;
2184
+ const parentScopeId = componentScope?.getAttribute(BF_SCOPE) ?? null;
2185
+ const proxyEl = document.createElement("bf-loop-item");
2186
+ commentScopeRegistry.set(proxyEl, { commentNode: anchor, scopeId: parentScopeId ?? "" });
2187
+ return { anchor, bindScope: proxyEl, parentScopeId };
2188
+ }
2189
+ const el = scope;
2190
+ return { anchor: null, bindScope: el, parentScopeId: el.getAttribute(BF_SCOPE) };
2191
+ }
2192
+ function findCondElInRange(anchor, id) {
2193
+ const sel = `[${BF_COND}="${id}"]`;
2194
+ const boundary = getCommentScopeBoundary(anchor);
2195
+ let node = anchor.nextSibling;
2196
+ while (node && node !== boundary) {
2197
+ if (node.nodeType === Node.ELEMENT_NODE) {
2198
+ const el = node;
2199
+ if (el.matches?.(sel))
2200
+ return el;
2201
+ const inner = el.querySelector(sel);
2202
+ if (inner)
2203
+ return inner;
2204
+ }
2205
+ node = node.nextSibling;
2206
+ }
2207
+ return null;
2208
+ }
2209
+ function findCondStartInRange(anchor, id) {
2210
+ const want = `bf-cond-start:${id}`;
2211
+ const boundary = getCommentScopeBoundary(anchor);
2212
+ let node = anchor.nextSibling;
2213
+ while (node && node !== boundary) {
2214
+ if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === want) {
2215
+ return node;
2216
+ }
2217
+ if (node.nodeType === Node.ELEMENT_NODE) {
2218
+ const w = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT);
2219
+ while (w.nextNode()) {
2220
+ if (w.currentNode.nodeValue === want)
2221
+ return w.currentNode;
2222
+ }
2223
+ }
2224
+ node = node.nextSibling;
2225
+ }
2226
+ return null;
2227
+ }
2022
2228
  var EMPTY_SLOTS = [];
2023
2229
  function normalizeTemplate(value) {
2024
2230
  return typeof value === "string" ? { html: value, slots: EMPTY_SLOTS } : value;
@@ -2029,7 +2235,8 @@ function evalBranchTemplate(branch) {
2029
2235
  function insert(scope, id, conditionFn, whenTrue, whenFalse) {
2030
2236
  if (!scope)
2031
2237
  return;
2032
- const parentScopeId = scope.getAttribute(BF_SCOPE);
2238
+ const region = makeRegion(scope);
2239
+ const parentScopeId = region.parentScopeId;
2033
2240
  let isFragmentCond = false;
2034
2241
  try {
2035
2242
  const sampleTrue = evalBranchTemplate(whenTrue);
@@ -2072,23 +2279,23 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse) {
2072
2279
  } finally {
2073
2280
  setParentScopeId(null);
2074
2281
  }
2075
- const existingEl = find(scope, `[${BF_COND}="${id}"]`);
2282
+ const existingEl = region.anchor ? findCondElInRange(region.anchor, id) : find(region.bindScope, `[${BF_COND}="${id}"]`);
2076
2283
  if (existingEl) {
2077
2284
  const expectedSig = getTemplateRootSignature(result2.html);
2078
2285
  const existingSig = existingEl.outerHTML.match(/^<[^>]+>/)?.[0] ?? null;
2079
2286
  if (isFragmentCond && (!expectedSig || existingSig !== expectedSig)) {
2080
- updateFragmentConditional(scope, id, result2);
2287
+ updateFragmentConditional(region, id, result2);
2081
2288
  } else if (!isFragmentCond && expectedSig && existingSig && expectedSig !== existingSig) {
2082
- updateElementConditional(scope, id, result2);
2289
+ updateElementConditional(region, id, result2);
2083
2290
  } else if (result2.slots.length > 0) {
2084
- updateElementConditional(scope, id, result2);
2291
+ updateElementConditional(region, id, result2);
2085
2292
  }
2086
2293
  } else if (isFragmentCond) {
2087
- updateFragmentConditional(scope, id, result2);
2294
+ updateFragmentConditional(region, id, result2);
2088
2295
  }
2089
- const cleanup2 = branch.bindEvents(scope, { isFirstRun: true });
2296
+ const cleanup2 = branch.bindEvents(region.bindScope, { isFirstRun: true });
2090
2297
  branchCleanup = typeof cleanup2 === "function" ? cleanup2 : null;
2091
- autoFocusConditionalElement(scope, id);
2298
+ autoFocusConditionalElement(region, id);
2092
2299
  return;
2093
2300
  }
2094
2301
  if (currCond === prevVal) {
@@ -2106,18 +2313,18 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse) {
2106
2313
  setParentScopeId(null);
2107
2314
  }
2108
2315
  if (isFragmentCond) {
2109
- updateFragmentConditional(scope, id, result);
2316
+ updateFragmentConditional(region, id, result);
2110
2317
  } else {
2111
- updateElementConditional(scope, id, result);
2318
+ updateElementConditional(region, id, result);
2112
2319
  }
2113
- const cleanup = branch.bindEvents(scope, { isFirstRun: false });
2320
+ const cleanup = branch.bindEvents(region.bindScope, { isFirstRun: false });
2114
2321
  branchCleanup = typeof cleanup === "function" ? cleanup : null;
2115
- autoFocusConditionalElement(scope, id);
2322
+ autoFocusConditionalElement(region, id);
2116
2323
  });
2117
2324
  }
2118
- function autoFocusConditionalElement(scope, id) {
2325
+ function autoFocusConditionalElement(region, id) {
2119
2326
  requestAnimationFrame(() => {
2120
- const condEl = scope.querySelector(`[${BF_COND}="${id}"]`);
2327
+ const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope.querySelector(`[${BF_COND}="${id}"]`);
2121
2328
  if (condEl) {
2122
2329
  const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
2123
2330
  if (autofocusEl && typeof autofocusEl.focus === "function") {
@@ -2150,18 +2357,23 @@ function spliceSlots(fragment, slots) {
2150
2357
  }
2151
2358
  return fragment;
2152
2359
  }
2153
- function updateFragmentConditional(scope, id, result) {
2360
+ function updateFragmentConditional(region, id, result) {
2154
2361
  const { html, slots } = result;
2362
+ const scope = region.bindScope;
2155
2363
  const startMarker = `bf-cond-start:${id}`;
2156
2364
  let startComment = null;
2157
- const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT);
2158
- while (walker.nextNode()) {
2159
- if (walker.currentNode.nodeValue === startMarker) {
2160
- startComment = walker.currentNode;
2161
- break;
2365
+ if (region.anchor) {
2366
+ startComment = findCondStartInRange(region.anchor, id);
2367
+ } else {
2368
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT);
2369
+ while (walker.nextNode()) {
2370
+ if (walker.currentNode.nodeValue === startMarker) {
2371
+ startComment = walker.currentNode;
2372
+ break;
2373
+ }
2162
2374
  }
2163
2375
  }
2164
- const condEl = scope.querySelector(`[${BF_COND}="${id}"]`);
2376
+ const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope.querySelector(`[${BF_COND}="${id}"]`);
2165
2377
  const endMarker = `bf-cond-end:${id}`;
2166
2378
  if (startComment) {
2167
2379
  const nodesToRemove = [];
@@ -2200,8 +2412,8 @@ function updateFragmentConditional(scope, id, result) {
2200
2412
  }
2201
2413
  }
2202
2414
  }
2203
- function updateElementConditional(scope, id, result) {
2204
- const condEl = scope.querySelector(`[${BF_COND}="${id}"]`);
2415
+ function updateElementConditional(region, id, result) {
2416
+ const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope.querySelector(`[${BF_COND}="${id}"]`);
2205
2417
  if (!condEl)
2206
2418
  return;
2207
2419
  const { html, slots } = result;
@@ -2226,6 +2438,58 @@ function __bfSlot(value, slots) {
2226
2438
  }
2227
2439
  return String(value);
2228
2440
  }
2441
+ // src/runtime/dynamic-text.ts
2442
+ var END_MARKER = "/";
2443
+ function clearSlotRegion(start, keep) {
2444
+ let n = start.nextSibling;
2445
+ while (n && !(n.nodeType === Node.COMMENT_NODE && n.nodeValue === END_MARKER)) {
2446
+ const next = n.nextSibling;
2447
+ if (n !== keep)
2448
+ n.parentNode?.removeChild(n);
2449
+ n = next;
2450
+ }
2451
+ }
2452
+ function slotStart(node) {
2453
+ let n = node.previousSibling;
2454
+ while (n && n.nodeType !== Node.COMMENT_NODE)
2455
+ n = n.previousSibling;
2456
+ return n;
2457
+ }
2458
+ function __bfText(current, value) {
2459
+ if (!current)
2460
+ return current;
2461
+ if (value != null && value.__isSlot)
2462
+ return current;
2463
+ if (typeof Node !== "undefined" && value instanceof Node) {
2464
+ if (value === current)
2465
+ return current;
2466
+ const start2 = current.previousSibling;
2467
+ if (start2 && start2.nodeType === Node.COMMENT_NODE) {
2468
+ clearSlotRegion(start2);
2469
+ start2.parentNode?.insertBefore(value, start2.nextSibling);
2470
+ return value;
2471
+ }
2472
+ current.parentNode?.replaceChild(value, current);
2473
+ return value;
2474
+ }
2475
+ const text = String(value ?? "");
2476
+ if (current.nodeType === Node.TEXT_NODE) {
2477
+ current.nodeValue = text;
2478
+ const start2 = slotStart(current);
2479
+ if (start2 && start2.nodeType === Node.COMMENT_NODE)
2480
+ clearSlotRegion(start2, current);
2481
+ return current;
2482
+ }
2483
+ const start = current.previousSibling;
2484
+ const textNode = (current.ownerDocument ?? document).createTextNode(text);
2485
+ if (start && start.nodeType === Node.COMMENT_NODE) {
2486
+ clearSlotRegion(start);
2487
+ start.parentNode?.insertBefore(textNode, start.nextSibling);
2488
+ } else {
2489
+ current.parentNode?.replaceChild(textNode, current);
2490
+ }
2491
+ return textNode;
2492
+ }
2229
2493
  // src/runtime/client-marker.ts
2230
2494
  function updateClientMarker(scope, id, value) {
2231
2495
  if (!scope)
@@ -2346,6 +2610,7 @@ export {
2346
2610
  parseHTML,
2347
2611
  onMount,
2348
2612
  onCleanup,
2613
+ mapArrayAnchored,
2349
2614
  mapArray,
2350
2615
  isSSRPortal,
2351
2616
  insert,
@@ -2379,6 +2644,7 @@ export {
2379
2644
  applyRestAttrs,
2380
2645
  __slot,
2381
2646
  __bf_swap,
2647
+ __bfText,
2382
2648
  __bfSlot,
2383
2649
  $t,
2384
2650
  $c,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "directory": "packages/client"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.5.0"
58
+ "@barefootjs/shared": "0.5.2"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
@@ -74,10 +74,15 @@ export interface CreateComponentSlotInfo {
74
74
 
75
75
  export function createComponent(
76
76
  nameOrDef: string | ComponentDef,
77
- props: Record<string, unknown>,
77
+ props: Record<string, unknown> = {},
78
78
  key?: string | number,
79
79
  slot?: CreateComponentSlotInfo,
80
80
  ): HTMLElement {
81
+ // A bare callable shim invoked from user code (e.g. an object-literal
82
+ // value `LOGOS[id]()` whose arrow the compiler hoisted into a component)
83
+ // reaches us with no props (#1663). Normalize to an empty object so the
84
+ // descriptor probes below don't throw on `undefined`.
85
+ if (props == null) props = {}
81
86
  // ComponentDef mode: use def directly instead of registry lookup
82
87
  if (typeof nameOrDef !== 'string') {
83
88
  return createComponentFromDef(nameOrDef, props, key)
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Dynamic text/JSX slot updater (#1663).
3
+ *
4
+ * The compiler wraps reactive child expressions (`<div>{expr}</div>`) in a
5
+ * `createEffect` that writes the value into the text node sitting between
6
+ * the slot's `<!--bf:sX-->` / `<!--/-->` comment markers. That was a pure
7
+ * `nodeValue = String(value)` assignment, which is correct for primitives
8
+ * but destroys a live `Node` — e.g. when `expr` is a JSX-returning call such
9
+ * as `{themeLogo(id)}` / `{LOGOS[id]()}` whose value is the `HTMLElement`
10
+ * returned by `createComponent`. Stringifying it produced
11
+ * `"[object HTMLElement]"` (and clobbered the server-rendered subtree).
12
+ *
13
+ * `__bfText` mirrors `__bfSlot` (the branch-template equivalent): when the
14
+ * value is a `Node`, it replaces the slot region with that node by identity;
15
+ * otherwise it behaves exactly like the previous text assignment. It returns
16
+ * the node that now occupies the slot so the caller can track it across
17
+ * reactive re-runs (the previous node is detached once replaced).
18
+ */
19
+
20
+ const END_MARKER = '/'
21
+
22
+ /** Remove every sibling between `start` (the `<!--bf:sX-->` comment) and the
23
+ * matching `<!--/-->` end comment, leaving both markers in place. When `keep`
24
+ * is supplied that node is left in place (used when writing a primitive
25
+ * through a text anchor that must survive while stale siblings are cleared). */
26
+ function clearSlotRegion(start: Node, keep?: Node): void {
27
+ let n = start.nextSibling
28
+ while (
29
+ n &&
30
+ !(n.nodeType === Node.COMMENT_NODE && (n as Comment).nodeValue === END_MARKER)
31
+ ) {
32
+ const next = n.nextSibling
33
+ if (n !== keep) n.parentNode?.removeChild(n)
34
+ n = next
35
+ }
36
+ }
37
+
38
+ /** Walk back from `node` to the nearest preceding comment marker (the slot's
39
+ * `<!--bf:sX-->` start), skipping any stale element siblings in between. */
40
+ function slotStart(node: Node): Node | null {
41
+ let n = node.previousSibling
42
+ while (n && n.nodeType !== Node.COMMENT_NODE) n = n.previousSibling
43
+ return n
44
+ }
45
+
46
+ export function __bfText(current: Node | null, value: unknown): Node | null {
47
+ if (!current) return current
48
+ // Slot markers (`__slot()`): leave the server-rendered DOM untouched.
49
+ if (value != null && (value as { __isSlot?: boolean }).__isSlot) return current
50
+
51
+ if (typeof Node !== 'undefined' && value instanceof Node) {
52
+ if (value === current) return current
53
+ const start = current.previousSibling
54
+ if (start && start.nodeType === Node.COMMENT_NODE) {
55
+ clearSlotRegion(start)
56
+ start.parentNode?.insertBefore(value, start.nextSibling)
57
+ return value
58
+ }
59
+ // No marker to anchor against — best-effort in-place replacement.
60
+ current.parentNode?.replaceChild(value, current)
61
+ return value
62
+ }
63
+
64
+ const text = String(value ?? '')
65
+ if (current.nodeType === Node.TEXT_NODE) {
66
+ current.nodeValue = text
67
+ // The conditional-slot path re-resolves the anchor via `$t()` on every
68
+ // run, which can hand back a freshly created text node sitting *before* a
69
+ // stale element left by a previous Node-valued run. Clear any remaining
70
+ // siblings up to the end marker so switching JSX → text doesn't render
71
+ // both the old element and the new text.
72
+ const start = slotStart(current)
73
+ if (start && start.nodeType === Node.COMMENT_NODE) clearSlotRegion(start, current)
74
+ return current
75
+ }
76
+
77
+ // Switching back from a Node value to text: drop the element and restore a
78
+ // text node in the slot region.
79
+ const start = current.previousSibling
80
+ const textNode = (current.ownerDocument ?? document).createTextNode(text)
81
+ if (start && start.nodeType === Node.COMMENT_NODE) {
82
+ clearSlotRegion(start)
83
+ start.parentNode?.insertBefore(textNode, start.nextSibling)
84
+ } else {
85
+ current.parentNode?.replaceChild(textNode, current)
86
+ }
87
+ return textNode
88
+ }
@@ -56,7 +56,7 @@ export {
56
56
  export { reconcileList, type RenderItemFn } from './list'
57
57
  export { reconcileElements, getLoopChildren, getLoopNodes } from './reconcile-elements'
58
58
  export { qsaItem, upsertChildItem } from './qsa-item'
59
- export { mapArray } from './map-array'
59
+ export { mapArray, mapArrayAnchored } from './map-array'
60
60
 
61
61
  // Template registry
62
62
  export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template'
@@ -81,6 +81,7 @@ export { hydrate, rehydrateAll, flushHydration, getRegisteredDef } from './hydra
81
81
  export { registerComponent, getComponentInit, initChild, upsertChild } from './registry'
82
82
  export { insert, type BranchConfig, type BranchTemplateResult } from './insert'
83
83
  export { __bfSlot } from './branch-slot'
84
+ export { __bfText } from './dynamic-text'
84
85
  export { updateClientMarker } from './client-marker'
85
86
 
86
87
  // Hydration state