@barefootjs/client 0.28.1 → 0.30.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.
@@ -6,12 +6,39 @@
6
6
  */
7
7
  import type { ComponentDef } from './types.ts';
8
8
  export declare function setParentScopeId(id: string | null): void;
9
- /** Where `mapArray` wants a fresh loop row connected before its `init` runs. */
9
+ /**
10
+ * Where `mapArray` wants a fresh loop row connected before its `init` runs.
11
+ *
12
+ * `mounted` is written back by `mountRowRoot` so the caller can undo the
13
+ * connection if the rest of the row's body then throws — see `createItemScope`.
14
+ * Without it a half-built row stays visible, which detached rows never did.
15
+ */
10
16
  export type RowMountPoint = {
11
17
  container: Node;
12
18
  anchor: Node | null;
19
+ mounted?: Element | null;
13
20
  };
14
21
  export declare function setRowMountPoint(p: RowMountPoint | null): RowMountPoint | null;
22
+ /**
23
+ * Connect a loop row whose root is a template clone, before the emitted
24
+ * renderItem body's tail runs.
25
+ *
26
+ * The tail is where the row's nested children initialise (`upsertChild` and
27
+ * friends), and `useContext` resolves by walking `parentElement` — so a child
28
+ * that inits inside a detached row finds no ancestors and falls through to the
29
+ * global last-writer-wins context store, reading whichever provider on the page
30
+ * wrote last. `createComponent` row roots already avoid this by consuming the
31
+ * same ambient (step 7b); a clone root has no runtime function in the middle,
32
+ * so the compiler emits this call for it instead.
33
+ *
34
+ * Take-and-clear, like the `createComponent` path: a nested `createComponent`
35
+ * driven by this row's own children must not re-use the row's mount point.
36
+ *
37
+ * No-op when there is no ambient point — the hydration branch never calls this
38
+ * (its row came from SSR markup and is already in the document), and a loop
39
+ * runtime that does not hand one down leaves the row exactly as it was.
40
+ */
41
+ export declare function mountRowRoot(el: Element): Element;
15
42
  /**
16
43
  * Create a component instance with DOM element and initialized state.
17
44
  *
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/runtime/component.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAS9C,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAED,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAAA;AAsBpE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,GAAG,aAAa,GAAG,IAAI,CAI9E;AAQD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;;;GAGG;AACH;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,IAAI,CAAC,EAAE,uBAAuB,EAC9B,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GACvB,WAAW,CAcb;AA2ND;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAiDR;AA+DD;;;;;GAKG;AACH;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG9D;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,gBAAgB,CAcjF"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/runtime/component.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAS9C,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAAA;AAsB9F,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,GAAG,aAAa,GAAG,IAAI,CAI9E;AAQD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAQjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;;;GAGG;AACH;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,IAAI,CAAC,EAAE,uBAAuB,EAC9B,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GACvB,WAAW,CAcb;AAyOD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAqER;AA+DD;;;;;GAKG;AACH;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG9D;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,gBAAgB,CAcjF"}
@@ -15,7 +15,7 @@ export { mapArrayLazy, type LazyRowEntry, type LazyRowPlan } from './map-array-l
15
15
  export { patchLeaf } from './patch-leaf.ts';
16
16
  export { claimSlots, lazySlots, lazyClaimSlots, textOrNode, type SlotSpec, type ClaimPlan, type ClaimedSlots, type ClaimedSlotsRW, type SlotWriter } from './claim-slots.ts';
17
17
  export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template.ts';
18
- export { createComponent, renderChild, parseHTML, escapeAttr, escapeText, escapeTextOrNode, } from './component.ts';
18
+ export { createComponent, mountRowRoot, renderChild, parseHTML, escapeAttr, escapeText, escapeTextOrNode, } from './component.ts';
19
19
  export { applyRestAttrs } from './apply-rest-attrs.ts';
20
20
  export { spreadAttrs } from './spread-attrs.ts';
21
21
  export { styleToCss } from './style.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,cAAc,EACd,UAAU,EACV,SAAS,EACT,OAAO,EACP,OAAO,EACP,KAAK,EACL,eAAe,EACf,SAAS,EACT,OAAO,EACP,gBAAgB,EAOhB,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EACL,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAI9C,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,eAAe,EACf,KAAK,OAAO,GACb,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,GACpB,MAAM,aAAa,CAAA;AAIpB,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAI3D,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACvF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAK3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG5K,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAG3F,OAAO,EACL,eAAe,EACf,WAAW,EACX,SAAS,EACT,UAAU,EACV,UAAU,EACV,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC9G,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpH,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3F,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAQ3C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAG5C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGrD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG1D,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
@@ -1361,6 +1361,14 @@ function takeRowMountPoint() {
1361
1361
  _rowMountPoint = null;
1362
1362
  return p;
1363
1363
  }
1364
+ function mountRowRoot(el) {
1365
+ const p = takeRowMountPoint();
1366
+ if (p) {
1367
+ p.container.insertBefore(el, p.anchor);
1368
+ p.mounted = el;
1369
+ }
1370
+ return el;
1371
+ }
1364
1372
  function createComponent(nameOrDef, props = {}, key, slot, mountAt) {
1365
1373
  const element = materializeComponent(nameOrDef, props, key, slot, mountAt);
1366
1374
  if (mountAt && mountAt.parentNode && element !== mountAt) {
@@ -1404,12 +1412,13 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1404
1412
  });
1405
1413
  const def = getRegisteredDef(name);
1406
1414
  const isCommentWrapper = def?.comment === true;
1407
- const scopeId = isCommentWrapper ? null : `${name}_${generateId()}`;
1415
+ const derivedScopeId = slot?.parent && slot.mount ? `${slot.parent}_${slot.mount}` : null;
1416
+ const scopeId = isCommentWrapper ? null : derivedScopeId ?? `${name}_${generateId()}`;
1408
1417
  const prevParentScopeId = _parentScopeId;
1409
- if (slot?.parent) {
1410
- _parentScopeId = slot.parent;
1411
- } else if (scopeId) {
1418
+ if (scopeId) {
1412
1419
  _parentScopeId = scopeId;
1420
+ } else if (slot?.parent) {
1421
+ _parentScopeId = slot.parent;
1413
1422
  }
1414
1423
  let html;
1415
1424
  try {
@@ -1477,7 +1486,8 @@ function renderChild(name, props, key, slotSuffix) {
1477
1486
  if (!templateFn) {
1478
1487
  return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`;
1479
1488
  }
1480
- let html = templateFn(props).trim().replace(PLACEHOLDER_ATTR_PATTERN, _parentScopeId ? ` bf-s="${_parentScopeId}"` : "");
1489
+ const raw = templateFn(props);
1490
+ let html = raw.trim().replace(PLACEHOLDER_ATTR_PATTERN, _parentScopeId ? ` bf-s="${_parentScopeId}"` : "");
1481
1491
  const firstElMatch = html.match(/<(\w+)/);
1482
1492
  if (!firstElMatch)
1483
1493
  return html;
@@ -1738,7 +1748,7 @@ function* itemRootElements(primaryEl) {
1738
1748
  if (n.nodeType === Node.COMMENT_NODE) {
1739
1749
  const v = n.nodeValue ?? "";
1740
1750
  if (v === BF_LOOP_ITEM || v === BF_LOOP_START || v.startsWith(startPrefix) || v === BF_LOOP_END || v.startsWith(endPrefix)) {
1741
- return;
1751
+ break;
1742
1752
  }
1743
1753
  } else if (n.nodeType === Node.ELEMENT_NODE) {
1744
1754
  yield n;
@@ -1911,9 +1921,16 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
1911
1921
  const prevRowMount = ownsRowMount ? setRowMountPoint(rowMount) : null;
1912
1922
  try {
1913
1923
  primaryEl = renderItem(itemAccessor, index, existingPrimary);
1924
+ } catch (err) {
1925
+ const parked = rowMount?.mounted;
1926
+ if (parked?.parentNode)
1927
+ parked.remove();
1928
+ throw err;
1914
1929
  } finally {
1915
1930
  if (ownsRowMount)
1916
1931
  setRowMountPoint(prevRowMount);
1932
+ if (rowMount)
1933
+ rowMount.mounted = null;
1917
1934
  }
1918
1935
  if (existingPrimary) {
1919
1936
  extras = existingExtras ?? [];
@@ -3248,6 +3265,7 @@ export {
3248
3265
  parseHTML,
3249
3266
  onMount,
3250
3267
  onCleanup,
3268
+ mountRowRoot,
3251
3269
  mapArrayLazy,
3252
3270
  mapArrayAnchored,
3253
3271
  mapArray,
@@ -1 +1 @@
1
- {"version":3,"file":"map-array.d.ts","sourceRoot":"","sources":["../../src/runtime/map-array.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqCH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EACtB,QAAQ,CAAC,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CA+BhD;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA2B3E;AAkFD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,KAAK,WAAW,EACjF,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CA6RN;AAqHD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,gBAAgB,GAAG,OAAO,EAC5F,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CA+FN"}
1
+ {"version":3,"file":"map-array.d.ts","sourceRoot":"","sources":["../../src/runtime/map-array.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqCH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EACtB,QAAQ,CAAC,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CA+BhD;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA2B3E;AA2FD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,KAAK,WAAW,EACjF,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CA6RN;AAqHD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,gBAAgB,GAAG,OAAO,EAC5F,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CA+FN"}
@@ -20,18 +20,22 @@
20
20
  * cannot escape into a neighbouring item or into nodes outside the
21
21
  * loop.
22
22
  * 3. The CSR-only `__bfExtras` stash. During renderItem-body setup
23
- * (between the template clone and the function's return), the
24
- * primary and extras are still detached nodes `__el.nextSibling`
25
- * is `null` and step 2 yields nothing. Reading `__bfExtras` lets
26
- * lookups reach the still-pending extras before `mapArray` inserts
27
- * them into the DOM.
23
+ * (between the template clone and the function's return), the extras
24
+ * are not siblings yet. Reading `__bfExtras` lets lookups reach them
25
+ * before `mapArray` inserts them into the DOM.
28
26
  *
29
- * Step 3's reliance on the primary being detached during setup is why
30
- * `createItemScope` un-parks a row that turns out to carry extras: the
31
- * connect-before-init mount point applies to `createComponent` row roots,
32
- * which are single-root by construction and so never reach this module. An
33
- * attached primary would make step 2's sibling walk run past the item's own
34
- * roots into a neighbouring item's elements before step 3 is ever consulted.
27
+ * Step 3 must stay reachable whether or not the primary is attached, which
28
+ * is why step 2 ends its walk with `break` rather than `return`. Only the
29
+ * sibling walk is bounded; ending the generator there would skip the stash.
30
+ *
31
+ * This used to be a `return`, and the difference was invisible: with a
32
+ * detached primary `nextSibling` is `null`, so the walk never runs and
33
+ * control reaches the stash regardless. Attaching the primary made the very
34
+ * first sibling a boundary comment, which ended the generator before the
35
+ * stash was consulted and left the item's child placeholders unreplaced.
36
+ * Measured, not deduced — and note this is NOT the failure the earlier
37
+ * comment here described: the walk never ran past the item into a
38
+ * neighbouring one, it stopped correctly and then gave up too early.
35
39
  */
36
40
  /**
37
41
  * Find an element matching `selector` within an item's range. Searches
@@ -1 +1 @@
1
- {"version":3,"file":"qsa-item.d.ts","sourceRoot":"","sources":["../../src/runtime/qsa-item.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAsCH;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAQnF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B,WAAW,GAAG,IAAI,CAuBpB"}
1
+ {"version":3,"file":"qsa-item.d.ts","sourceRoot":"","sources":["../../src/runtime/qsa-item.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AA4CH;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAQnF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EACrB,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B,WAAW,GAAG,IAAI,CAuBpB"}
@@ -1767,6 +1767,14 @@ function takeRowMountPoint() {
1767
1767
  _rowMountPoint = null;
1768
1768
  return p;
1769
1769
  }
1770
+ function mountRowRoot(el) {
1771
+ const p = takeRowMountPoint();
1772
+ if (p) {
1773
+ p.container.insertBefore(el, p.anchor);
1774
+ p.mounted = el;
1775
+ }
1776
+ return el;
1777
+ }
1770
1778
  function createComponent(nameOrDef, props = {}, key, slot, mountAt) {
1771
1779
  const element = materializeComponent(nameOrDef, props, key, slot, mountAt);
1772
1780
  if (mountAt && mountAt.parentNode && element !== mountAt) {
@@ -1810,12 +1818,13 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1810
1818
  });
1811
1819
  const def = getRegisteredDef(name);
1812
1820
  const isCommentWrapper = def?.comment === true;
1813
- const scopeId = isCommentWrapper ? null : `${name}_${generateId()}`;
1821
+ const derivedScopeId = slot?.parent && slot.mount ? `${slot.parent}_${slot.mount}` : null;
1822
+ const scopeId = isCommentWrapper ? null : derivedScopeId ?? `${name}_${generateId()}`;
1814
1823
  const prevParentScopeId = _parentScopeId;
1815
- if (slot?.parent) {
1816
- _parentScopeId = slot.parent;
1817
- } else if (scopeId) {
1824
+ if (scopeId) {
1818
1825
  _parentScopeId = scopeId;
1826
+ } else if (slot?.parent) {
1827
+ _parentScopeId = slot.parent;
1819
1828
  }
1820
1829
  let html;
1821
1830
  try {
@@ -1883,7 +1892,8 @@ function renderChild(name, props, key, slotSuffix) {
1883
1892
  if (!templateFn) {
1884
1893
  return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`;
1885
1894
  }
1886
- let html = templateFn(props).trim().replace(PLACEHOLDER_ATTR_PATTERN, _parentScopeId ? ` bf-s="${_parentScopeId}"` : "");
1895
+ const raw = templateFn(props);
1896
+ let html = raw.trim().replace(PLACEHOLDER_ATTR_PATTERN, _parentScopeId ? ` bf-s="${_parentScopeId}"` : "");
1887
1897
  const firstElMatch = html.match(/<(\w+)/);
1888
1898
  if (!firstElMatch)
1889
1899
  return html;
@@ -2144,7 +2154,7 @@ function* itemRootElements(primaryEl) {
2144
2154
  if (n.nodeType === Node.COMMENT_NODE) {
2145
2155
  const v = n.nodeValue ?? "";
2146
2156
  if (v === BF_LOOP_ITEM || v === BF_LOOP_START || v.startsWith(startPrefix) || v === BF_LOOP_END || v.startsWith(endPrefix)) {
2147
- return;
2157
+ break;
2148
2158
  }
2149
2159
  } else if (n.nodeType === Node.ELEMENT_NODE) {
2150
2160
  yield n;
@@ -2316,9 +2326,16 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
2316
2326
  const prevRowMount = ownsRowMount ? setRowMountPoint(rowMount) : null;
2317
2327
  try {
2318
2328
  primaryEl = renderItem(itemAccessor, index, existingPrimary);
2329
+ } catch (err) {
2330
+ const parked = rowMount?.mounted;
2331
+ if (parked?.parentNode)
2332
+ parked.remove();
2333
+ throw err;
2319
2334
  } finally {
2320
2335
  if (ownsRowMount)
2321
2336
  setRowMountPoint(prevRowMount);
2337
+ if (rowMount)
2338
+ rowMount.mounted = null;
2322
2339
  }
2323
2340
  if (existingPrimary) {
2324
2341
  extras = existingExtras ?? [];
@@ -3647,6 +3664,7 @@ export {
3647
3664
  parseHTML,
3648
3665
  onMount,
3649
3666
  onCleanup,
3667
+ mountRowRoot,
3650
3668
  mapArrayLazy,
3651
3669
  mapArrayAnchored,
3652
3670
  mapArray,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.28.1",
3
+ "version": "0.30.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.28.1"
58
+ "@barefootjs/shared": "0.30.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
@@ -25,8 +25,14 @@ export function setParentScopeId(id: string | null): void {
25
25
  _parentScopeId = id
26
26
  }
27
27
 
28
- /** Where `mapArray` wants a fresh loop row connected before its `init` runs. */
29
- export type RowMountPoint = { container: Node; anchor: Node | null }
28
+ /**
29
+ * Where `mapArray` wants a fresh loop row connected before its `init` runs.
30
+ *
31
+ * `mounted` is written back by `mountRowRoot` so the caller can undo the
32
+ * connection if the rest of the row's body then throws — see `createItemScope`.
33
+ * Without it a half-built row stays visible, which detached rows never did.
34
+ */
35
+ export type RowMountPoint = { container: Node; anchor: Node | null; mounted?: Element | null }
30
36
 
31
37
  // Ambient mount point for a loop row.
32
38
  //
@@ -60,6 +66,35 @@ function takeRowMountPoint(): RowMountPoint | null {
60
66
  return p
61
67
  }
62
68
 
69
+ /**
70
+ * Connect a loop row whose root is a template clone, before the emitted
71
+ * renderItem body's tail runs.
72
+ *
73
+ * The tail is where the row's nested children initialise (`upsertChild` and
74
+ * friends), and `useContext` resolves by walking `parentElement` — so a child
75
+ * that inits inside a detached row finds no ancestors and falls through to the
76
+ * global last-writer-wins context store, reading whichever provider on the page
77
+ * wrote last. `createComponent` row roots already avoid this by consuming the
78
+ * same ambient (step 7b); a clone root has no runtime function in the middle,
79
+ * so the compiler emits this call for it instead.
80
+ *
81
+ * Take-and-clear, like the `createComponent` path: a nested `createComponent`
82
+ * driven by this row's own children must not re-use the row's mount point.
83
+ *
84
+ * No-op when there is no ambient point — the hydration branch never calls this
85
+ * (its row came from SSR markup and is already in the document), and a loop
86
+ * runtime that does not hand one down leaves the row exactly as it was.
87
+ */
88
+ export function mountRowRoot(el: Element): Element {
89
+ const p = takeRowMountPoint()
90
+ if (p) {
91
+ p.container.insertBefore(el, p.anchor)
92
+ // Record it so the row can be un-mounted if the body throws after this.
93
+ p.mounted = el
94
+ }
95
+ return el
96
+ }
97
+
63
98
  /**
64
99
  * Create a component instance with DOM element and initialized state.
65
100
  *
@@ -200,9 +235,18 @@ function materializeComponent(
200
235
  // already the inner component's root with its own bf-s. Don't overwrite
201
236
  // it (scopeId stays null), or `$c(__scope, 's0')` from the wrapper's
202
237
  // init resolves to null.
238
+ //
239
+ // `slot` is only supplied by `upsertChild` / `upsertChildItem` mounting a
240
+ // component nested below a loop row root — the SSR reference (Hono)
241
+ // derives that child's `bf-s` from `${hostScope}_${mountSlot}` rather
242
+ // than randomizing it (#2444), so CSR must match or the primary
243
+ // `(bf-h, bf-m)` SSR-scope lookup never finds the CSR-created element.
244
+ // A row root itself is never passed a `slot`, so it keeps its own
245
+ // random id, matching the reference behaviour.
203
246
  const def = getRegisteredDef(name)
204
247
  const isCommentWrapper = def?.comment === true
205
- const scopeId = isCommentWrapper ? null : `${name}_${generateId()}`
248
+ const derivedScopeId = slot?.parent && slot.mount ? `${slot.parent}_${slot.mount}` : null
249
+ const scopeId = isCommentWrapper ? null : (derivedScopeId ?? `${name}_${generateId()}`)
206
250
 
207
251
  // 5. Generate HTML from props.
208
252
  //
@@ -211,13 +255,18 @@ function materializeComponent(
211
255
  // bf-m on child components — matching the SSR convention so a later
212
256
  // `$c(scope, 'sN')` lookup resolves them. Without this, CSR-created
213
257
  // children carry a random prefix and their event handlers never wire
214
- // up (#1627). `slot.parent` takes precedence so hoisted-children
215
- // placeholders (#1320) still resolve to the calling site's scope.
258
+ // up (#1627). `scopeId` takes precedence over `slot.parent` once a
259
+ // slotted component derives its OWN scope id (above), that derived id
260
+ // is what ITS children must nest under, not the grandparent's `slot.parent`
261
+ // (that used to collapse a third composition level back onto the second,
262
+ // #2444's `grandchild-composition` case). A comment wrapper keeps
263
+ // `scopeId === null` and falls through to `slot?.parent`, preserving the
264
+ // hoisted-children placeholder resolution (#1320).
216
265
  const prevParentScopeId = _parentScopeId
217
- if (slot?.parent) {
218
- _parentScopeId = slot.parent
219
- } else if (scopeId) {
266
+ if (scopeId) {
220
267
  _parentScopeId = scopeId
268
+ } else if (slot?.parent) {
269
+ _parentScopeId = slot.parent
221
270
  }
222
271
  let html: string
223
272
  try {
@@ -381,11 +430,31 @@ export function renderChild(
381
430
  return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`
382
431
  }
383
432
 
433
+ // NOTE: does NOT push `_parentScopeId` to this child's own derived scope
434
+ // while `templateFn` evaluates. That was tried (deriving a GRANDCHILD's
435
+ // scope from this scope rather than the caller's, to fix a third
436
+ // composition level collapsing onto the second — `grandchild-composition`)
437
+ // but it collides with `comment: true` wrapper transparency (#1211
438
+ // synthesized inline-JSX-callback wrappers, e.g. a `renderNode` callback
439
+ // prop): such a wrapper's OWN scope element IS its single child's element,
440
+ // and its init resolves itself via `$c(scope, 's0')`'s short-suffix
441
+ // self-match fallback (`query.ts`'s `$cSingle`). Pushing this scope makes
442
+ // a nested descendant of that SAME child derive `..._s0_s0` whenever the
443
+ // descendant's own first slot id also happens to be `s0` — the PRECISE
444
+ // suffix match then finds that unrelated descendant instead of falling
445
+ // through to the self-match, silently misrouting `initChild` (confirmed
446
+ // via `site/ui`'s xyflow Highlight-Depth demo, where this broke the
447
+ // `--node-glow` style effect). `grandchild-composition` stays a known
448
+ // limitation (CSR conformance skip) until the self-match lookup can be
449
+ // made unambiguous against a nested descendant sharing the same slot
450
+ // suffix.
451
+ const raw = templateFn(props)
452
+
384
453
  // The placeholder substitution is anchored to the exact `bf-s="…"`
385
454
  // shape so user content that contains the sentinel as text survives
386
455
  // unchanged. When `_parentScopeId` is null (top-level render) the
387
456
  // attribute strips rather than emitting `bf-s=""`. (#1320)
388
- let html = templateFn(props).trim().replace(
457
+ let html = raw.trim().replace(
389
458
  PLACEHOLDER_ATTR_PATTERN,
390
459
  _parentScopeId ? ` bf-s="${_parentScopeId}"` : '',
391
460
  )
@@ -99,6 +99,7 @@ export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './t
99
99
  // Component creation
100
100
  export {
101
101
  createComponent,
102
+ mountRowRoot,
102
103
  renderChild,
103
104
  parseHTML,
104
105
  escapeAttr,
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { createSignal, createEffect, createRoot } from '@barefootjs/client/reactive'
22
22
  import { hydratedScopes } from './hydration-state.ts'
23
- import { setRowMountPoint } from './component.ts'
23
+ import { setRowMountPoint, type RowMountPoint } from './component.ts'
24
24
  import {
25
25
  BF_KEY,
26
26
  BF_LOOP_START,
@@ -234,7 +234,7 @@ function createItemScope<T>(
234
234
  existingPrimary?: HTMLElement,
235
235
  existingExtras?: HTMLElement[],
236
236
  existingStart?: Comment | null,
237
- rowMount?: { container: Node; anchor: Node | null } | null,
237
+ rowMount?: RowMountPoint | null,
238
238
  ): ItemScope<T> {
239
239
  let primaryEl!: HTMLElement
240
240
  let dispose!: () => void
@@ -246,10 +246,11 @@ function createItemScope<T>(
246
246
  dispose = d
247
247
  const [itemAccessor, itemSetter] = createSignal(item)
248
248
  setItem = itemSetter
249
- // Fresh row: hand the mount point to the row's own `createComponent` so
250
- // its `init` observes a connected element. A renderItem body that clones a
251
- // template instead of calling `createComponent` (composite / plain loops)
252
- // leaves it unconsumed, hence the restore below rather than a plain clear.
249
+ // Fresh row: hand the mount point down so the row's own root — whether a
250
+ // `createComponent` call or a `mountRowRoot(clone)` observes a connected
251
+ // element when the body's tail initialises its children. A body that does
252
+ // neither (a plain loop's clone) leaves the point unconsumed, hence the
253
+ // restore below rather than a plain clear.
253
254
  //
254
255
  // Only touch the ambient when we are the one setting it, and put back what
255
256
  // was there rather than `null`: this row's `init` can drive a nested
@@ -259,8 +260,16 @@ function createItemScope<T>(
259
260
  const prevRowMount = ownsRowMount ? setRowMountPoint(rowMount) : null
260
261
  try {
261
262
  primaryEl = renderItem(itemAccessor, index, existingPrimary)
263
+ } catch (err) {
264
+ // A row that connected itself and then failed to finish would stay
265
+ // visible as a half-built row. Detached rows never could, so undo the
266
+ // connection before rethrowing and keep the failure mode as it was.
267
+ const parked = rowMount?.mounted
268
+ if (parked?.parentNode) parked.remove()
269
+ throw err
262
270
  } finally {
263
271
  if (ownsRowMount) setRowMountPoint(prevRowMount)
272
+ if (rowMount) rowMount.mounted = null
264
273
  }
265
274
  if (existingPrimary) {
266
275
  extras = existingExtras ?? []
@@ -20,18 +20,22 @@
20
20
  * cannot escape into a neighbouring item or into nodes outside the
21
21
  * loop.
22
22
  * 3. The CSR-only `__bfExtras` stash. During renderItem-body setup
23
- * (between the template clone and the function's return), the
24
- * primary and extras are still detached nodes `__el.nextSibling`
25
- * is `null` and step 2 yields nothing. Reading `__bfExtras` lets
26
- * lookups reach the still-pending extras before `mapArray` inserts
27
- * them into the DOM.
23
+ * (between the template clone and the function's return), the extras
24
+ * are not siblings yet. Reading `__bfExtras` lets lookups reach them
25
+ * before `mapArray` inserts them into the DOM.
28
26
  *
29
- * Step 3's reliance on the primary being detached during setup is why
30
- * `createItemScope` un-parks a row that turns out to carry extras: the
31
- * connect-before-init mount point applies to `createComponent` row roots,
32
- * which are single-root by construction and so never reach this module. An
33
- * attached primary would make step 2's sibling walk run past the item's own
34
- * roots into a neighbouring item's elements before step 3 is ever consulted.
27
+ * Step 3 must stay reachable whether or not the primary is attached, which
28
+ * is why step 2 ends its walk with `break` rather than `return`. Only the
29
+ * sibling walk is bounded; ending the generator there would skip the stash.
30
+ *
31
+ * This used to be a `return`, and the difference was invisible: with a
32
+ * detached primary `nextSibling` is `null`, so the walk never runs and
33
+ * control reaches the stash regardless. Attaching the primary made the very
34
+ * first sibling a boundary comment, which ended the generator before the
35
+ * stash was consulted and left the item's child placeholders unreplaced.
36
+ * Measured, not deduced — and note this is NOT the failure the earlier
37
+ * comment here described: the walk never ran past the item into a
38
+ * neighbouring one, it stopped correctly and then gave up too early.
35
39
  */
36
40
 
37
41
  import { BF_LOOP_ITEM, BF_LOOP_START, BF_LOOP_END } from '@barefootjs/shared'
@@ -51,10 +55,16 @@ function* itemRootElements(primaryEl: Element): Iterable<Element> {
51
55
  // Hard stops: another item starts, or the loop range ends. The
52
56
  // BF_LOOP_START check defends against a sibling loop block whose
53
57
  // start marker happens to follow ours.
58
+ // `break`, not `return`: stop the SIBLING walk, but still fall through
59
+ // to the `__bfExtras` stash below. A `return` ends the generator, and
60
+ // the stash is the only way to reach extras that are not siblings yet.
61
+ // That difference is invisible while the primary is detached — then
62
+ // `nextSibling` is `null`, this loop never runs, and control reaches the
63
+ // stash anyway — and decides the outcome the moment it is attached.
54
64
  if (v === BF_LOOP_ITEM
55
65
  || v === BF_LOOP_START || v.startsWith(startPrefix)
56
66
  || v === BF_LOOP_END || v.startsWith(endPrefix)) {
57
- return
67
+ break
58
68
  }
59
69
  } else if (n.nodeType === Node.ELEMENT_NODE) {
60
70
  yield n as Element