@barefootjs/client 0.26.4 → 0.27.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.
@@ -1758,8 +1758,6 @@ var _parentScopeId = null;
1758
1758
  function setParentScopeId(id) {
1759
1759
  _parentScopeId = id;
1760
1760
  }
1761
- var propsUpdateMap = new WeakMap;
1762
- var propsMap = new WeakMap;
1763
1761
  function createComponent(nameOrDef, props = {}, key, slot) {
1764
1762
  if (props == null)
1765
1763
  props = {};
@@ -1851,24 +1849,8 @@ function createComponent(nameOrDef, props = {}, key, slot) {
1851
1849
  }
1852
1850
  setCurrentScope(prevScope);
1853
1851
  hydratedScopes.add(element);
1854
- propsMap.set(element, props);
1855
- registerPropsUpdate(element, name, props);
1856
1852
  return element;
1857
1853
  }
1858
- function getComponentProps(element) {
1859
- return propsMap.get(element);
1860
- }
1861
- function registerPropsUpdate(element, name, _initialProps) {
1862
- propsUpdateMap.set(element, (newProps) => {
1863
- const init = getComponentInit(name);
1864
- if (init) {
1865
- init(element, newProps);
1866
- }
1867
- });
1868
- }
1869
- function getPropsUpdateFn(element) {
1870
- return propsUpdateMap.get(element);
1871
- }
1872
1854
  function renderChild(name, props, key, slotSuffix) {
1873
1855
  const templateFn = getTemplate(name);
1874
1856
  const suffix = slotSuffix ? `_${slotSuffix}` : "";
@@ -1934,6 +1916,11 @@ function escapeText(value) {
1934
1916
  return "";
1935
1917
  return escapeAttr(value);
1936
1918
  }
1919
+ function escapeTextOrNode(value) {
1920
+ if (typeof Node !== "undefined" && value instanceof Node)
1921
+ return value;
1922
+ return escapeText(value);
1923
+ }
1937
1924
  var SVG_NS = "http://www.w3.org/2000/svg";
1938
1925
  function parseHTML(html, parent) {
1939
1926
  const tpl = document.createElement("template");
@@ -1998,7 +1985,6 @@ function createComponentFromDef(def, props, key) {
1998
1985
  }
1999
1986
  def.init(element, props);
2000
1987
  hydratedScopes.add(element);
2001
- propsMap.set(element, props);
2002
1988
  return element;
2003
1989
  }
2004
1990
 
@@ -2056,7 +2042,7 @@ function createPortal(children, container = document.body, options) {
2056
2042
  }
2057
2043
  };
2058
2044
  }
2059
- // src/runtime/reconcile-elements.ts
2045
+ // src/runtime/loop-markers.ts
2060
2046
  function findLoopMarkers(container, markerId) {
2061
2047
  let startMarker = null;
2062
2048
  let endMarker = null;
@@ -2101,14 +2087,6 @@ function getElementsBetweenMarkers(start, end) {
2101
2087
  }
2102
2088
  return elements;
2103
2089
  }
2104
- function removeElementsBetweenMarkers(start, end) {
2105
- let node = start.nextSibling;
2106
- while (node && node !== end) {
2107
- const next = node.nextSibling;
2108
- node.parentNode?.removeChild(node);
2109
- node = next;
2110
- }
2111
- }
2112
2090
  function getLoopChildren(container, markerId) {
2113
2091
  const { startMarker, endMarker } = findLoopMarkers(container, markerId);
2114
2092
  if (startMarker && endMarker) {
@@ -2129,159 +2107,6 @@ function getLoopNodes(container, markerId) {
2129
2107
  }
2130
2108
  return Array.from(container.childNodes);
2131
2109
  }
2132
- function reconcileElements(container, items, getKey, renderItem, firstElement, markerId) {
2133
- if (!container || !items)
2134
- return;
2135
- const { startMarker, endMarker } = findLoopMarkers(container, markerId);
2136
- const existingByKey = new Map;
2137
- let hasKeyedChildren = false;
2138
- const loopChildren = startMarker ? getElementsBetweenMarkers(startMarker, endMarker) : Array.from(container.children);
2139
- for (const child of loopChildren) {
2140
- const el = child;
2141
- const key = el.dataset?.key;
2142
- if (key !== undefined) {
2143
- existingByKey.set(key, el);
2144
- hasKeyedChildren = true;
2145
- }
2146
- }
2147
- if (!hasKeyedChildren) {
2148
- if (items.length === 0) {
2149
- if (startMarker) {
2150
- removeElementsBetweenMarkers(startMarker, endMarker);
2151
- } else {
2152
- container.innerHTML = "";
2153
- }
2154
- return;
2155
- }
2156
- const fragment = document.createDocumentFragment();
2157
- for (let i = 0;i < items.length; i++) {
2158
- const el = i === 0 && firstElement ? firstElement : renderItem(items[i], i);
2159
- const key = getKey ? getKey(items[i], i) : String(i);
2160
- if (!el.dataset.key)
2161
- el.setAttribute(BF_KEY, key);
2162
- fragment.appendChild(el);
2163
- }
2164
- if (startMarker) {
2165
- removeElementsBetweenMarkers(startMarker, endMarker);
2166
- endMarker.parentNode.insertBefore(fragment, endMarker);
2167
- } else {
2168
- container.innerHTML = "";
2169
- container.appendChild(fragment);
2170
- }
2171
- return;
2172
- }
2173
- let insertAnchor = endMarker ?? null;
2174
- if (!startMarker) {
2175
- let foundKeyed = false;
2176
- for (const child of Array.from(container.childNodes)) {
2177
- if (child.nodeType === Node.ELEMENT_NODE && child.dataset.key !== undefined) {
2178
- foundKeyed = true;
2179
- } else if (foundKeyed) {
2180
- insertAnchor = child;
2181
- break;
2182
- }
2183
- }
2184
- }
2185
- let focusedKey = null;
2186
- const activeEl = document.activeElement;
2187
- if (activeEl && activeEl !== document.body) {
2188
- const tag = activeEl.tagName;
2189
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || activeEl.isContentEditable) {
2190
- for (const [key, el] of existingByKey) {
2191
- if (el.contains(activeEl)) {
2192
- focusedKey = key;
2193
- break;
2194
- }
2195
- }
2196
- }
2197
- }
2198
- const desiredElements = [];
2199
- const toRemove = [];
2200
- let focusTarget = null;
2201
- for (let i = 0;i < items.length; i++) {
2202
- const item = items[i];
2203
- const key = getKey ? getKey(item, i) : String(i);
2204
- const createEl = () => i === 0 && firstElement ? firstElement : renderItem(item, i);
2205
- const existing = existingByKey.get(key);
2206
- if (existing) {
2207
- existingByKey.delete(key);
2208
- if (existing.getAttribute(BF_SCOPE) && !hydratedScopes.has(existing)) {
2209
- const newEl = createEl();
2210
- if (!newEl.dataset.key)
2211
- newEl.setAttribute(BF_KEY, key);
2212
- desiredElements.push(newEl);
2213
- toRemove.push(existing);
2214
- } else if (focusedKey === key) {
2215
- const newEl = createEl();
2216
- if (!newEl.dataset.key)
2217
- newEl.setAttribute(BF_KEY, key);
2218
- focusTarget = prepareInputTransfer(existing, newEl);
2219
- desiredElements.push(newEl);
2220
- toRemove.push(existing);
2221
- } else {
2222
- const newEl = createEl();
2223
- if (!newEl.dataset.key)
2224
- newEl.setAttribute(BF_KEY, key);
2225
- desiredElements.push(newEl);
2226
- toRemove.push(existing);
2227
- }
2228
- } else {
2229
- const el = createEl();
2230
- if (!el.dataset.key)
2231
- el.setAttribute(BF_KEY, key);
2232
- desiredElements.push(el);
2233
- }
2234
- }
2235
- for (const el of existingByKey.values()) {
2236
- toRemove.push(el);
2237
- }
2238
- for (const el of toRemove) {
2239
- if (el.parentNode)
2240
- el.remove();
2241
- }
2242
- for (const el of desiredElements) {
2243
- container.insertBefore(el, insertAnchor);
2244
- }
2245
- if (focusTarget) {
2246
- focusTarget.target.focus();
2247
- if (typeof focusTarget.selectionStart === "number") {
2248
- focusTarget.target.selectionStart = focusTarget.selectionStart;
2249
- focusTarget.target.selectionEnd = focusTarget.selectionEnd;
2250
- }
2251
- }
2252
- }
2253
- function prepareInputTransfer(oldEl, newEl) {
2254
- const focused = oldEl.contains(document.activeElement) ? document.activeElement : null;
2255
- if (!focused)
2256
- return null;
2257
- const tag = focused.tagName;
2258
- const oldInputs = Array.from(oldEl.querySelectorAll(tag));
2259
- const idx = oldInputs.indexOf(focused);
2260
- if (idx < 0)
2261
- return null;
2262
- const newInputs = Array.from(newEl.querySelectorAll(tag));
2263
- const target = newInputs[idx];
2264
- if (!target)
2265
- return null;
2266
- target.value = focused.value;
2267
- return {
2268
- target,
2269
- selectionStart: focused.selectionStart,
2270
- selectionEnd: focused.selectionEnd
2271
- };
2272
- }
2273
-
2274
- // src/runtime/list.ts
2275
- function reconcileList(container, items, getKey, renderItem) {
2276
- if (!container || !items)
2277
- return;
2278
- if (items.length === 0) {
2279
- container.innerHTML = "";
2280
- return;
2281
- }
2282
- const firstElement = renderItem(items[0], 0);
2283
- reconcileElements(container, items, getKey, renderItem, firstElement);
2284
- }
2285
2110
  // src/runtime/qsa-item.ts
2286
2111
  function* itemRootElements(primaryEl) {
2287
2112
  yield primaryEl;
@@ -2817,35 +2642,44 @@ function patchLeaf(el, html) {
2817
2642
  }
2818
2643
  el.replaceChildren(...Array.from(next.childNodes));
2819
2644
  }
2820
- // src/runtime/patch-slot-range.ts
2821
- function patchSlotRange(scope, id, html) {
2645
+ // src/runtime/claim-slots.ts
2646
+ function resolvePath(root, path) {
2647
+ let node = root;
2648
+ for (const index of path) {
2649
+ const child = node.childNodes[index];
2650
+ if (!child)
2651
+ return null;
2652
+ node = child;
2653
+ }
2654
+ return node;
2655
+ }
2656
+ function isSlotComment(node, id) {
2657
+ return node != null && node.nodeType === Node.COMMENT_NODE && node.nodeValue === `bf:${id}`;
2658
+ }
2659
+ function findOwnedMarker(root, id) {
2822
2660
  const marker = `bf:${id}`;
2823
- let start = null;
2824
- const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT);
2825
- while (walker.nextNode()) {
2826
- const comment = walker.currentNode;
2661
+ const parentOwned = id.startsWith(BF_PARENT_OWNED_PREFIX);
2662
+ const registryInfo = commentScopeRegistry.get(root);
2663
+ const boundary = registryInfo ? registryInfo.commentNode.parentElement : root;
2664
+ for (const comment of commentsInScope(root)) {
2827
2665
  if (comment.nodeValue !== marker)
2828
2666
  continue;
2667
+ if (parentOwned)
2668
+ return comment;
2829
2669
  let owned = true;
2830
- for (let el = comment.parentElement;el && el !== scope; el = el.parentElement) {
2670
+ for (let el = comment.parentElement;el && el !== boundary; el = el.parentElement) {
2831
2671
  if (el.hasAttribute(BF_SCOPE)) {
2832
2672
  owned = false;
2833
2673
  break;
2834
2674
  }
2835
2675
  }
2836
- if (owned) {
2837
- start = comment;
2838
- break;
2839
- }
2840
- }
2841
- const parent = start?.parentNode;
2842
- if (!start || !parent) {
2843
- console.warn(`[barefootjs] preamble region marker bf:${id} not found in row; skipping patch`);
2844
- return;
2676
+ if (owned)
2677
+ return comment;
2845
2678
  }
2679
+ return null;
2680
+ }
2681
+ function findMarkupEnd(start) {
2846
2682
  let depth = 0;
2847
- let end = null;
2848
- const toRemove = [];
2849
2683
  let node = start.nextSibling;
2850
2684
  while (node) {
2851
2685
  if (node.nodeType === Node.COMMENT_NODE) {
@@ -2853,25 +2687,131 @@ function patchSlotRange(scope, id, html) {
2853
2687
  if (value.startsWith("bf:")) {
2854
2688
  depth++;
2855
2689
  } else if (value === "/") {
2856
- if (depth === 0) {
2857
- end = node;
2858
- break;
2859
- }
2690
+ if (depth === 0)
2691
+ return node;
2860
2692
  depth--;
2861
2693
  }
2862
2694
  }
2863
- toRemove.push(node);
2864
2695
  node = node.nextSibling;
2865
2696
  }
2697
+ return null;
2698
+ }
2699
+ function resolveAnchor(root, spec) {
2700
+ if (spec.path.length > 0) {
2701
+ const resolved = resolvePath(root, spec.path);
2702
+ if (isSlotComment(resolved, spec.id))
2703
+ return resolved;
2704
+ console.warn(`[barefootjs] claim path for slot ${spec.id} did not resolve to its bf:${spec.id} marker; falling back to a scan`);
2705
+ }
2706
+ const found = findOwnedMarker(root, spec.id);
2707
+ if (!found) {
2708
+ console.warn(`[barefootjs] slot ${spec.id} marker not found; skipping`);
2709
+ }
2710
+ return found;
2711
+ }
2712
+ function claimMarkerlessText(root, spec) {
2713
+ if (spec.path.length === 0) {
2714
+ console.warn(`[barefootjs] markerless slot ${spec.id} has an empty path; skipping`);
2715
+ return null;
2716
+ }
2717
+ const parentPath = spec.path.slice(0, -1);
2718
+ const idx = spec.path[spec.path.length - 1];
2719
+ const parent = resolvePath(root, parentPath);
2720
+ if (!parent) {
2721
+ console.warn(`[barefootjs] markerless claim path for slot ${spec.id} did not resolve to a parent node; skipping`);
2722
+ return null;
2723
+ }
2724
+ const existing = parent.childNodes[idx];
2725
+ if (existing && existing.nodeType === Node.TEXT_NODE) {
2726
+ return { kind: "text", node: existing };
2727
+ }
2728
+ const node = document.createTextNode("");
2729
+ parent.insertBefore(node, existing ?? null);
2730
+ return { kind: "text", node };
2731
+ }
2732
+ function claimOne(root, spec) {
2733
+ if (spec.kind === "text" && spec.markerless) {
2734
+ return claimMarkerlessText(root, spec);
2735
+ }
2736
+ const anchor = resolveAnchor(root, spec);
2737
+ if (!anchor)
2738
+ return null;
2739
+ if (spec.kind === "text") {
2740
+ return { kind: "text", node: textNodeAfterComment(anchor) };
2741
+ }
2742
+ const end = findMarkupEnd(anchor);
2866
2743
  if (!end) {
2867
- console.warn(`[barefootjs] preamble region bf:${id} has no end marker; skipping patch`);
2744
+ console.warn(`[barefootjs] slot ${spec.id} has no end marker; skipping`);
2745
+ return null;
2746
+ }
2747
+ return { kind: "markup", start: anchor, end, last: undefined };
2748
+ }
2749
+ function writeText(ref, value) {
2750
+ ref.node.nodeValue = String(value ?? "");
2751
+ }
2752
+ function clearMarkupRange(start, end) {
2753
+ const parent = end.parentNode;
2754
+ if (!parent)
2755
+ return;
2756
+ let node = start.nextSibling;
2757
+ while (node && node !== end) {
2758
+ const next = node.nextSibling;
2759
+ parent.removeChild(node);
2760
+ node = next;
2761
+ }
2762
+ }
2763
+ function writeMarkup(ref, value) {
2764
+ const { start, end } = ref;
2765
+ const parent = end.parentNode;
2766
+ if (!parent)
2767
+ return;
2768
+ if (value != null && value.__isSlot)
2769
+ return;
2770
+ if (typeof Node !== "undefined" && value instanceof Node) {
2771
+ if (value === ref.last)
2772
+ return;
2773
+ clearMarkupRange(start, end);
2774
+ parent.insertBefore(value, end);
2775
+ ref.last = value;
2868
2776
  return;
2869
2777
  }
2870
- for (const n of toRemove)
2871
- parent.removeChild(n);
2778
+ const text = String(value ?? "");
2779
+ if (text === ref.last)
2780
+ return;
2781
+ clearMarkupRange(start, end);
2872
2782
  const tpl = document.createElement("template");
2873
- tpl.innerHTML = html;
2783
+ tpl.innerHTML = text;
2874
2784
  parent.insertBefore(tpl.content, end);
2785
+ ref.last = text;
2786
+ }
2787
+ function writeSlot(refs, id, value) {
2788
+ const ref = refs.get(id);
2789
+ if (!ref) {
2790
+ console.warn(`[barefootjs] no claimed slot for id ${id}; write ignored`);
2791
+ return;
2792
+ }
2793
+ if (ref.kind === "text") {
2794
+ writeText(ref, value);
2795
+ } else {
2796
+ writeMarkup(ref, value);
2797
+ }
2798
+ }
2799
+ function claimSlots(root, plan) {
2800
+ const refs = new Map;
2801
+ for (const spec of plan) {
2802
+ const ref = claimOne(root, spec);
2803
+ if (ref)
2804
+ refs.set(spec.id, ref);
2805
+ }
2806
+ return { write: (id, value) => writeSlot(refs, id, value) };
2807
+ }
2808
+ function lazySlots(root, plan) {
2809
+ let claimed = null;
2810
+ return (id, value) => {
2811
+ if (!claimed)
2812
+ claimed = claimSlots(root, plan);
2813
+ claimed.write(id, value);
2814
+ };
2875
2815
  }
2876
2816
  // src/runtime/style.ts
2877
2817
  function styleToCss(value) {
@@ -3314,26 +3254,6 @@ function __bfText(current, value) {
3314
3254
  }
3315
3255
  return textNode;
3316
3256
  }
3317
- // src/runtime/client-marker.ts
3318
- function updateClientMarker(scope, id, value) {
3319
- if (!scope)
3320
- return;
3321
- const marker = `bf-client:${id}`;
3322
- const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT);
3323
- while (walker.nextNode()) {
3324
- if (walker.currentNode.nodeValue === marker) {
3325
- const comment = walker.currentNode;
3326
- let textNode = comment.nextSibling;
3327
- if (textNode?.nodeType !== Node.TEXT_NODE || !textNode.nodeValue?.startsWith("​")) {
3328
- textNode = document.createTextNode("​" + String(value ?? ""));
3329
- comment.parentNode?.insertBefore(textNode, comment.nextSibling);
3330
- } else {
3331
- textNode.nodeValue = "​" + String(value ?? "");
3332
- }
3333
- return;
3334
- }
3335
- }
3336
- }
3337
3257
  // src/runtime/render.ts
3338
3258
  function render(container, nameOrDef, props = {}) {
3339
3259
  let name;
@@ -3414,7 +3334,6 @@ export {
3414
3334
  useContext,
3415
3335
  upsertChildItem,
3416
3336
  upsertChild,
3417
- updateClientMarker,
3418
3337
  unwrap,
3419
3338
  untrack,
3420
3339
  textNodeAfterComment as tAfter,
@@ -3430,21 +3349,19 @@ export {
3430
3349
  rehydrateAll,
3431
3350
  registerTemplate,
3432
3351
  registerComponent,
3433
- reconcileList,
3434
- reconcileElements,
3435
3352
  queryHref,
3436
3353
  qsaItem,
3437
3354
  qsaChildScopes,
3438
3355
  qsaChildScope,
3439
3356
  qsa,
3440
3357
  provideContext,
3441
- patchSlotRange,
3442
3358
  patchLeaf,
3443
3359
  parseHTML,
3444
3360
  onMount,
3445
3361
  onCleanup,
3446
3362
  mapArrayAnchored,
3447
3363
  mapArray,
3364
+ lazySlots,
3448
3365
  isSSRPortal,
3449
3366
  insert,
3450
3367
  initChild,
@@ -3453,10 +3370,8 @@ export {
3453
3370
  hasTemplate,
3454
3371
  getTemplate,
3455
3372
  getRegisteredDef,
3456
- getPropsUpdateFn,
3457
3373
  getLoopNodes,
3458
3374
  getLoopChildren,
3459
- getComponentProps,
3460
3375
  getComponentInit,
3461
3376
  forwardProps,
3462
3377
  formatDate,
@@ -3464,6 +3379,7 @@ export {
3464
3379
  findSiblingSlot,
3465
3380
  findScope,
3466
3381
  find,
3382
+ escapeTextOrNode,
3467
3383
  escapeText,
3468
3384
  escapeAttr,
3469
3385
  endTurn,
@@ -3482,6 +3398,7 @@ export {
3482
3398
  createContext,
3483
3399
  createComponent,
3484
3400
  cleanupPortalPlaceholder,
3401
+ claimSlots,
3485
3402
  beginTurn,
3486
3403
  batch,
3487
3404
  applyRestAttrs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.26.4",
3
+ "version": "0.27.0",
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.26.4"
58
+ "@barefootjs/shared": "0.27.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
package/src/reactive.ts CHANGED
@@ -319,7 +319,8 @@ export function createSignal<T>(initialValue: T, __bfId?: string): Signal<T> {
319
319
  export function createEffect(fn: EffectFn, __bfId?: string, __bfKind: SubscriberKind = 'effect'): void {
320
320
  // Note: Nested effects are now allowed. runEffect() properly saves/restores
321
321
  // prevEffect, so nested effects correctly track their own dependencies.
322
- // This enables synchronous component initialization in reconcileList.
322
+ // This enables synchronous component initialization inside loop reconcilers
323
+ // (mapArray/mapArrayAnchored).
323
324
 
324
325
  const effect: EffectContext = {
325
326
  fn,